diff --git a/Makefile b/Makefile index 6d148df04..3adbf8059 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ install: @echo "Installing the Ignite command-line tool...\\n" @mkdir -p $(PREFIX_DIR) 2> /dev/null || ( echo "ā Unable to create install directory \`$(PREFIX_DIR)\`. You might need to run \`sudo make\`\\n"; exit 126 ) @(install .build/release/IgniteCLI $(PREFIX_DIR)/ignite && \ - install Sources/IgniteCLI/server.py $(PREFIX_DIR)/ignite-server.py && \ + install ./server.py $(PREFIX_DIR)/ignite-server.py && \ chmod +x $(PREFIX_DIR)/ignite && \ (echo \\nā Success! Run \`ignite\` to get started.)) || \ (echo \\nā Installation failed. You might need to run \`sudo make\` instead.\\n) diff --git a/Sources/Ignite/Actions/Event Modifiers/EventModifier.swift b/Sources/Ignite/Actions/Event Modifiers/EventModifier.swift index c267d824e..0e26009fe 100644 --- a/Sources/Ignite/Actions/Event Modifiers/EventModifier.swift +++ b/Sources/Ignite/Actions/Event Modifiers/EventModifier.swift @@ -5,14 +5,28 @@ // See LICENSE for license information. // -private extension HTML { - func eventModifier(type: EventType, actions: [Action]) -> any HTML { - guard !actions.isEmpty else { return self } - // Custom elements need to be wrapped in a primitive container to store attributes - var copy: any HTML = self.isPrimitive ? self : Section(self) - copy.attributes.events.append(Event(name: type.rawValue, actions: actions)) - return copy - } +@MainActor +private func eventModifier( + _ type: EventType, + actions: [Action], + content: any HTML +) -> any HTML { + guard !actions.isEmpty else { return content } + var copy: any HTML = content.isPrimitive ? content : Section(content) + copy.attributes.events.append(Event(name: type.rawValue, actions: actions)) + return copy +} + +@MainActor +private func eventModifier( + _ type: EventType, + actions: [Action], + content: any InlineElement +) -> any InlineElement { + guard !actions.isEmpty else { return content } + var copy: any InlineElement = content.isPrimitive ? content : Span(content) + copy.attributes.events.append(Event(name: type.rawValue, actions: actions)) + return copy } public extension HTML { @@ -22,7 +36,7 @@ public extension HTML { /// - actions: Array of actions to execute when the event occurs /// - Returns: A modified HTML element with the specified attribute. func onEvent(_ type: EventType, _ actions: [Action]) -> some HTML { - AnyHTML(eventModifier(type: type, actions: actions)) + AnyHTML(eventModifier(type, actions: actions, content: self)) } } @@ -33,6 +47,6 @@ public extension InlineElement { /// - actions: Array of actions to execute when the event occurs /// - Returns: A modified HTML element with the specified attribute. func onEvent(_ type: EventType, _ actions: [Action]) -> some InlineElement { - AnyHTML(eventModifier(type: type, actions: actions)) + AnyInlineElement(eventModifier(type, actions: actions, content: self)) } } diff --git a/Sources/Ignite/Actions/Event Modifiers/HoverModifier.swift b/Sources/Ignite/Actions/Event Modifiers/HoverModifier.swift index 5e143d497..6e5bbbb02 100644 --- a/Sources/Ignite/Actions/Event Modifiers/HoverModifier.swift +++ b/Sources/Ignite/Actions/Event Modifiers/HoverModifier.swift @@ -5,12 +5,18 @@ // See LICENSE for license information. // -private extension HTML { - func hoverModifier(hover: [Action], unhover: [Action]) -> any HTML { - self - .onEvent(.mouseOver, hover) - .onEvent(.mouseOut, unhover) - } +@MainActor +private func hoverModifier(hover: [Action], unhover: [Action], content: any HTML) -> any HTML { + content + .onEvent(.mouseOver, hover) + .onEvent(.mouseOut, unhover) +} + +@MainActor +private func hoverModifier(hover: [Action], unhover: [Action], content: any InlineElement) -> any InlineElement { + content + .onEvent(.mouseOver, hover) + .onEvent(.mouseOut, unhover) } public extension HTML { @@ -18,7 +24,7 @@ public extension HTML { /// - Parameter actions: A closure that takes a Boolean indicating hover state and returns actions to execute. /// - Returns: A modified HTML element with the hover event handlers attached. func onHover(@ActionBuilder actions: (_ isHovering: Bool) -> [Action]) -> some HTML { - AnyHTML(hoverModifier(hover: actions(true), unhover: actions(false))) + AnyHTML(hoverModifier(hover: actions(true), unhover: actions(false), content: self)) } } @@ -27,6 +33,6 @@ public extension InlineElement { /// - Parameter actions: A closure that takes a Boolean indicating hover state and returns actions to execute. /// - Returns: A modified inline HTML element with the hover event handlers attached. func onHover(@ActionBuilder actions: (_ isHovering: Bool) -> [Action]) -> some InlineElement { - AnyHTML(hoverModifier(hover: actions(true), unhover: actions(false))) + AnyInlineElement(hoverModifier(hover: actions(true), unhover: actions(false), content: self)) } } diff --git a/Sources/Ignite/Actions/SubscribeAction.swift b/Sources/Ignite/Actions/SubscribeAction.swift deleted file mode 100644 index cb04a6581..000000000 --- a/Sources/Ignite/Actions/SubscribeAction.swift +++ /dev/null @@ -1,116 +0,0 @@ -// -// SubscribeAction.swift -// Ignite -// https://www.github.com/twostraws/Ignite -// See LICENSE for license information. -// - -/// Represents a form submission action for newsletter subscriptions -public struct SubscribeAction: Action { - /// Represents different newsletter service providers - public enum EmailPlatform: Sendable { - /// Mailchimp newsletter integration - case mailchimp(username: String, uValue: String, listID: String) - /// SendFox newsletter integration - case sendFox(listID: String, formID: String) - /// Kit (ConvertKit) newsletter integration - case kit(String) - /// Buttondown newsletter integration - case buttondown(String) - - var endpoint: String { - switch self { - case .mailchimp(let username, let uValue, let listID): - "https://\(username).us1.list-manage.com/subscribe/post?u=\(uValue)&id=\(listID)" - case .kit(let token): - "https://app.convertkit.com/forms/\(token)/subscriptions" - case .sendFox(let listID, let formID): - "https://sendfox.com/form/\(listID)/\(formID)" - case .buttondown(let username): - "https://buttondown.com/api/emails/embed-subscribe/\(username)" - } - } - - var customAttributes: [Attribute] { - switch self { - case .mailchimp: - [.init(name: "name", value: "mc-embedded-subscribe-form")] - default: - [] - } - } - - var dataAttributes: [Attribute] { - switch self { - case .sendFox: - [.init(name: "async", value: "true"), - .init(name: "recaptcha", value: "true")] - default: [] - } - } - - var formClass: String? { - switch self { - case .mailchimp: - "validate" - case .sendFox: - "sendfox-form" - case .buttondown: - "embeddable-buttondown-form" - default: - nil - } - } - - var emailFieldID: String { - switch self { - case .mailchimp: - "mce-EMAIL" - case .sendFox: - "sendfox_form_email" - case .buttondown: - "bd-email" - default: - "" - } - } - - var emailFieldName: String? { - switch self { - case .sendFox, .buttondown: - "email" - case .mailchimp: - "EMAIL" - case .kit: - "email_address" - } - } - - var honeypotFieldName: String? { - switch self { - case .mailchimp(_, let uValue, let listID): - "b_\(uValue)_\(listID)" - case .sendFox: - "a_password" - default: - nil - } - } - } - - /// The email platform to use - let service: EmailPlatform - - /// Creates a new subscribe action for a specific form. - /// - Parameters: - /// - service: The email service to use for subscription. - public init(_ service: EmailPlatform) { - self.service = service - } - - /// Renders this action using publishing context passed in. - /// - Returns: The JavaScript for this action. - public func compile() -> String { - "" - } -} diff --git a/Sources/Ignite/Components/FeedLink.swift b/Sources/Ignite/Components/FeedLink.swift index 9f8dba366..025e3eb17 100644 --- a/Sources/Ignite/Components/FeedLink.swift +++ b/Sources/Ignite/Components/FeedLink.swift @@ -21,7 +21,7 @@ public struct FeedLink: HTML { } Link("RSS Feed", target: feedConfig.path) - EmptyHTML() + EmptyInlineElement() } .horizontalAlignment(.center) } diff --git a/Sources/Ignite/Elements/Abbreviation.swift b/Sources/Ignite/Elements/Abbreviation.swift index b9c1dd7c9..c5e060d96 100644 --- a/Sources/Ignite/Elements/Abbreviation.swift +++ b/Sources/Ignite/Elements/Abbreviation.swift @@ -8,7 +8,7 @@ /// Renders an abbreviation. public struct Abbreviation: InlineElement { /// The content and behavior of this HTML. - public var body: some HTML { self } + public var body: some InlineElement { self } /// The standard set of control attributes for HTML elements. public var attributes = CoreAttributes() @@ -41,7 +41,8 @@ public struct Abbreviation: InlineElement { /// Renders this element using publishing context passed in. /// - Returns: The HTML for this element. - public func render() -> String { - "\(contents.render())" + public func markup() -> Markup { + let contentHTML = contents.markupString() + return Markup("\(contentHTML)") } } diff --git a/Sources/Ignite/Elements/Accordion.swift b/Sources/Ignite/Elements/Accordion.swift index 12ebe814b..645949adb 100644 --- a/Sources/Ignite/Elements/Accordion.swift +++ b/Sources/Ignite/Elements/Accordion.swift @@ -17,6 +17,18 @@ public struct Accordion: HTML { case all } + /// The visual style of the accordion. + public enum Style: Sendable { + /// A style with outer borders and rounded corners. + case bordered + + /// Removes outer borders and rounded corners. + case plain + + /// The default styling based on context. + public static var automatic: Self { .bordered } + } + /// The content and behavior of this HTML. public var body: some HTML { self } @@ -27,7 +39,7 @@ public struct Accordion: HTML { public var isPrimitive: Bool { true } /// A collection of sections you want to show inside this accordion. - var items: [Item] + private var items: [Item] /// Adjusts what happens when a section is opened. /// Defaults to `.individual`, meaning that only one @@ -60,9 +72,69 @@ public struct Accordion: HTML { return copy } + /// Sets the visual style of the accordion. + /// - Parameter style: The style to apply to the accordion. + /// - Returns: A modified copy of this accordion with the new style applied. + public func accordionStyle(_ style: Style) -> Self { + var copy = self + copy.attributes.append(classes: "accordion-flush") + return copy + } + + /// Sets different background colors for normal and active states of accordion headers. + /// - Parameters: + /// - closed: The color to use for normal (inactive) headers. + /// - open: The color to use for active (selected) headers. + /// - Returns: A modified copy of this accordion with the new header backgrounds. + public func headerBackground(_ closed: Color, open: Color) -> Self { + var copy = self + copy.attributes.append(styles: .init("--bs-accordion-btn-bg", value: closed.description)) + copy.attributes.append(styles: .init("--bs-btn-hover-bg", value: closed.description)) + copy.attributes.append(styles: .init("--bs-btn-active-bg", value: closed.description)) + copy.attributes.append(styles: .init("--bs-accordion-active-bg", value: open.description)) + return copy + } + + /// Sets the same background color for both normal and active states of accordion headers. + /// - Parameter color: The color to use for all header states. + /// - Returns: A modified copy of this accordion with the new header background. + public func headerBackground(_ color: Color) -> Self { + self.headerBackground(color, open: color) + } + + /// Sets different text colors for normal and active states of accordion headers. + /// - Parameters: + /// - closed: The text color to use for normal (inactive) headers. + /// - open: The text color to use for active (selected) headers. + /// - Returns: A modified copy of this accordion with the new header text colors. + public func headerForegroundStyle(_ closed: Color, open: Color) -> Self { + var copy = self + copy.attributes.append(styles: .init("--bs-accordion-btn-color", value: closed.description)) + copy.attributes.append(styles: .init("--bs-btn-hover-color", value: closed.description)) + copy.attributes.append(styles: .init("--bs-btn-active-color", value: closed.description)) + copy.attributes.append(styles: .init("--bs-accordion-active-color", value: open.description)) + return copy + } + + /// Sets the same text color for both normal and active states of accordion headers. + /// - Parameter color: The text color to use for all header states. + /// - Returns: A modified copy of this accordion with the new header text color. + public func headerForegroundStyle(_ color: Color) -> Self { + self.headerForegroundStyle(color, open: color) + } + + /// Sets the border color for the accordion. + /// - Parameter color: The color to use for accordion borders. + /// - Returns: A modified copy of this accordion with the new border color. + public func borderColor(_ color: Color) -> Self { + var copy = self + copy.attributes.append(styles: .init("--bs-accordion-border-color", value: color.description)) + return copy + } + /// Renders this element using publishing context passed in. /// - Returns: The HTML for this element. - public func render() -> String { + public func markup() -> Markup { // Accordions with an individual open mode must have // each element linked back to a unique accordion ID. // This is generated below, then passed into individual @@ -77,6 +149,6 @@ public struct Accordion: HTML { .class("accordion") .id(accordionID) - return content.render() + return content.markup() } } diff --git a/Sources/Ignite/Elements/Alert.swift b/Sources/Ignite/Elements/Alert.swift index 52c745daf..f523b4da6 100644 --- a/Sources/Ignite/Elements/Alert.swift +++ b/Sources/Ignite/Elements/Alert.swift @@ -48,10 +48,10 @@ public struct Alert: HTML { /// Renders this element using publishing context passed in. /// - Returns: The HTML for this element. - public func render() -> String { + public func markup() -> Markup { Section(content) .class(alertClasses) .attributes(attributes) - .render() + .markup() } } diff --git a/Sources/Ignite/Elements/ArticlePreview.swift b/Sources/Ignite/Elements/ArticlePreview.swift index eb2a65f6e..cec56ddea 100644 --- a/Sources/Ignite/Elements/ArticlePreview.swift +++ b/Sources/Ignite/Elements/ArticlePreview.swift @@ -43,17 +43,17 @@ public struct ArticlePreview: HTML { /// Renders the article preview with either a custom layout or the default card. /// - Returns: A rendered string of HTML. - public func render() -> String { + public func markup() -> Markup { // If custom style is provided, use it; otherwise, // fallback to default layout. if let style { style.body(content: article) .attributes(attributes) - .render() + .markup() } else { defaultCardLayout() .attributes(attributes) - .render() + .markup() } } diff --git a/Sources/Ignite/Elements/Audio.swift b/Sources/Ignite/Elements/Audio.swift index 0636f52db..65a0e6b19 100644 --- a/Sources/Ignite/Elements/Audio.swift +++ b/Sources/Ignite/Elements/Audio.swift @@ -8,7 +8,7 @@ /// Plays Audio on your page. public struct Audio: InlineElement, LazyLoadable { /// The content and behavior of this HTML. - public var body: some HTML { self } + public var body: some InlineElement { self } /// The standard set of control attributes for HTML elements. public var attributes = CoreAttributes() @@ -18,7 +18,7 @@ public struct Audio: InlineElement, LazyLoadable { /// The name of the audio to display. This should be specified relative /// to the root of your site, e.g. /audio/bark.mp3. - var files: [String]? + private var files: [String]? /// Creates an `Audio` instance from the name of a file contained /// in your site's assets. This should be specified relative to the root @@ -32,7 +32,7 @@ public struct Audio: InlineElement, LazyLoadable { /// - Parameters: /// - files: The user audios to render. /// - Returns: The HTML for this element. - public func render(files: [String]) -> String { + private func render(files: [String]) -> Markup { var output = "" for filename in files { @@ -42,18 +42,18 @@ public struct Audio: InlineElement, LazyLoadable { } output += "Your browser does not support the audio element." - return "" + return Markup("") } /// Renders this element using publishing context passed in. /// - Returns: The HTML for this element. - public func render() -> String { + public func markup() -> Markup { guard let files = files else { publishingContext.addWarning(""" Creating audio with no name should not be possible. \ Please file a bug report on the Ignite project. """) - return "" + return Markup() } return render(files: files) diff --git a/Sources/Ignite/Elements/Badge.swift b/Sources/Ignite/Elements/Badge.swift index a712171e8..c8b5aae27 100644 --- a/Sources/Ignite/Elements/Badge.swift +++ b/Sources/Ignite/Elements/Badge.swift @@ -8,7 +8,7 @@ /// A small, capsule-shaped piece of information, such as a tag. public struct Badge: InlineElement { /// The content and behavior of this HTML. - public var body: some HTML { self } + public var body: some InlineElement { self } /// The standard set of control attributes for HTML elements. public var attributes = CoreAttributes() @@ -92,10 +92,10 @@ public struct Badge: InlineElement { /// Renders this element using publishing context passed in. /// - Returns: The HTML for this element. - public func render() -> String { + public func markup() -> Markup { let badgeAttributes = attributes.appending(classes: badgeClasses) return Span(text) .attributes(badgeAttributes) - .render() + .markup() } } diff --git a/Sources/Ignite/Elements/Body.swift b/Sources/Ignite/Elements/Body.swift index 3bb31a26e..a322a5c47 100644 --- a/Sources/Ignite/Elements/Body.swift +++ b/Sources/Ignite/Elements/Body.swift @@ -5,10 +5,7 @@ // See LICENSE for license information. // -public struct Body: DocumentElement { - /// The content and behavior of this HTML. - public var body: some HTML { self } - +public struct Body: MarkupElement { /// The standard set of control attributes for HTML elements. public var attributes = CoreAttributes() @@ -18,7 +15,7 @@ public struct Body: DocumentElement { /// Whether this HTML uses Bootstrap's `container` class to determine page width. var isBoundByContainer: Bool = true - var content: any HTML + var content: any BodyElement public init(@HTMLBuilder _ content: () -> some HTML) { self.content = content() @@ -41,17 +38,17 @@ public struct Body: DocumentElement { return copy } - public func render() -> String { + public func markup() -> Markup { var attributes = attributes - var output = content.render() + var output = content.markup() // Add required scripts if publishingContext.site.useDefaultBootstrapURLs == .localBootstrap { - output += Script(file: "/js/bootstrap.bundle.min.js").render() + output += Script(file: "/js/bootstrap.bundle.min.js").markup() } if publishingContext.hasSyntaxHighlighters == true { - output += Script(file: "/js/syntax-highlighting.js").render() + output += Script(file: "/js/syntax-highlighting.js").markup() } if case .visible(let firstLine, let shouldWrap) = @@ -66,18 +63,96 @@ public struct Body: DocumentElement { } } - if output.contains(#"data-bs-toggle="tooltip""#) { + if output.string.contains(#"data-bs-toggle="tooltip""#) { output += Script(code: """ const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)) - """).render() + """).markup() } - output += Script(file: "/js/ignite-core.js").render() + output += Script(file: "/js/ignite-core.js").markup() if isBoundByContainer { attributes.append(classes: ["container"]) } - return "
\(output)" + return Markup("\(output.string)") + } +} + +public extension Body { + /// Adds a data attribute to the element. + /// - Parameters: + /// - name: The name of the data attribute + /// - value: The value of the data attribute + /// - Returns: The modified `Body` element + func data(_ name: String, _ value: String) -> Self { + var copy = self + copy.attributes.data.append(.init(name: name, value: value)) + return copy + } + + /// Adds a custom attribute to the element. + /// - Parameters: + /// - name: The name of the custom attribute + /// - value: The value of the custom attribute + /// - Returns: The modified `HTML` element + func customAttribute(name: String, value: String) -> Self { + var copy = self + copy.attributes.append(customAttributes: .init(name: name, value: value)) + return copy + } +} + +public extension Body { + /// Applies margins on selected sides of this element. Defaults to 20 pixels. + /// - Parameters: + /// - edges: The edges where this margin should be applied. + /// - length: The amount of margin to apply, specified in + /// units of your choosing. + /// - Returns: A copy of the current element with the new margins applied. + func margin(_ edges: Edge, _ length: LengthUnit) -> Self { + let styles = content.edgeAdjustedStyles(prefix: "margin", edges, length.stringValue) + var copy = self + copy.attributes.append(styles: styles) + return copy + } + + /// Applies margins on selected sides of this element, using adaptive sizing. + /// - Parameters: + /// - edges: The edges where this margin should be applied. + /// - amount: The amount of margin to apply, specified as a + /// `SpacingAmount` case. + /// - Returns: A copy of the current element with the new margins applied. + func margin(_ edges: Edge, _ amount: SpacingAmount) -> Self { + let classes = content.edgeAdjustedClasses(prefix: "m", edges, amount.rawValue) + var copy = self + copy.attributes.append(classes: classes) + return copy + } + + /// Applies padding on selected sides of this element. Defaults to 20 pixels. + /// - Parameters: + /// - edges: The edges where this padding should be applied. + /// - length: The amount of padding to apply, specified in + /// units of your choosing. + /// - Returns: A copy of the current element with the new padding applied. + func padding(_ edges: Edge, _ length: LengthUnit) -> Self { + let styles = content.edgeAdjustedStyles(prefix: "padding", edges, length.stringValue) + var copy = self + copy.attributes.append(styles: styles) + return copy + } + + /// Applies padding on selected sides of this element using adaptive sizing. + /// - Parameters: + /// - edges: The edges where this padding should be applied. + /// - amount: The amount of padding to apply, specified as a + /// `SpacingAmount` case. + /// - Returns: A copy of the current element with the new padding applied. + func padding(_ edges: Edge, _ amount: SpacingAmount) -> Self { + let classes = content.edgeAdjustedClasses(prefix: "p", edges, amount.rawValue) + var copy = self + copy.attributes.append(classes: classes) + return copy } } diff --git a/Sources/Ignite/Elements/Button.swift b/Sources/Ignite/Elements/Button.swift index 17eab7042..31c1e999a 100644 --- a/Sources/Ignite/Elements/Button.swift +++ b/Sources/Ignite/Elements/Button.swift @@ -6,7 +6,7 @@ // /// A clickable button with a label and styling. -public struct Button: InlineElement { +public struct Button: InlineElement, FormItem { /// Controls the display size of buttons. Medium is the default. public enum Size: String, CaseIterable { case small, medium, large @@ -30,7 +30,7 @@ public struct Button: InlineElement { } /// The content and behavior of this HTML. - public var body: some HTML { self } + public var body: some InlineElement { self } /// The standard set of control attributes for HTML elements. public var attributes = CoreAttributes() @@ -48,7 +48,10 @@ public struct Button: InlineElement { var role = Role.default /// Elements to render inside this button. - var label: any HTML + var label: any InlineElement + + /// The icon element to display before the title. + var systemImage: String? /// Whether the button is disabled and cannot be interacted with. private var isDisabled = false @@ -56,7 +59,7 @@ public struct Button: InlineElement { /// Creates a button with no label. Used in some situations where /// exact styling is performed by Bootstrap, e.g. in Carousel. public init() { - self.label = EmptyHTML() + self.label = EmptyInlineElement() } /// Creates a button with a label. @@ -74,21 +77,27 @@ public struct Button: InlineElement { /// Creates a button with a label. /// - Parameters: - /// - label: The label text to display on this button. + /// - title: The label text to display on this button. + /// - systemImage: An image name chosen from https://icons.getbootstrap.com. /// - actions: An element builder that returns an array of actions to run when this button is pressed. /// - actions: An element builder that returns an array of actions to run when this button is pressed. - public init(_ label: String, @ActionBuilder actions: () -> [Action]) { - self.label = label + public init( + _ title: String, + systemImage: String? = nil, + @ActionBuilder actions: () -> [Action] = { [] } + ) { + self.label = title + self.systemImage = systemImage addEvent(name: "onclick", actions: actions()) } /// Creates a button with a label and actions to run when it's pressed. /// - Parameters: - /// - label: The label text to display on this button. /// - actions: An element builder that returns an array of actions to run when this button is pressed. + /// - label: The label text to display on this button. public init( - @InlineElementBuilder _ label: @escaping () -> some InlineElement, - @ActionBuilder actions: () -> [Action] + @ActionBuilder actions: () -> [Action], + @InlineElementBuilder label: @escaping () -> some InlineElement ) { self.label = label() addEvent(name: "onclick", actions: actions()) @@ -171,7 +180,7 @@ public struct Button: InlineElement { /// Renders this element using publishing context passed in. /// - Returns: The HTML for this element. - public func render() -> String { + public func markup() -> Markup { var buttonAttributes = attributes .appending(classes: Button.classes(forRole: role, size: size)) .appending(aria: Button.aria(forRole: role)) @@ -180,16 +189,20 @@ public struct Button: InlineElement { buttonAttributes.append(customAttributes: .disabled) } - let output = label.render() - return "" + var labelHTML = "" + if let systemImage, !systemImage.isEmpty { + labelHTML = " " + } + labelHTML += label.markupString() + return Markup("") } } -extension Button { +public extension Button { /// Adjusts the number of columns assigned to this element. /// - Parameter width: The new number of columns to use. /// - Returns: A copy of the current element with the adjusted column width. - public func width(_ width: Int) -> some InlineElement { + func width(_ width: Int) -> some InlineElement { self.class("w-100", ColumnWidth.count(width).className) } } diff --git a/Sources/Ignite/Elements/ButtonGroup.swift b/Sources/Ignite/Elements/ButtonGroup.swift index a9cdb46c1..c2569d03c 100644 --- a/Sources/Ignite/Elements/ButtonGroup.swift +++ b/Sources/Ignite/Elements/ButtonGroup.swift @@ -18,10 +18,10 @@ public struct ButtonGroup: HTML { public var isPrimitive: Bool { true } /// A required screen reader description for this element. - var accessibilityLabel: String + private var accessibilityLabel: String /// The buttons that should be displayed in this gorup. - var content: [Button] + private var content: HTMLCollection /// Creates a new `ButtonGroup` from the accessibility label and an /// element builder that must return the buttons to use. @@ -34,18 +34,16 @@ public struct ButtonGroup: HTML { @ElementBuilder