From 92d779b9c8c34db423129133ec3fbb7229052159 Mon Sep 17 00:00:00 2001 From: Jordan Burke Date: Fri, 17 Apr 2026 14:12:06 -0400 Subject: [PATCH 1/3] feat: add IconComponent with Feather icon set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Shipwright::IconComponent renders inline SVG icons from a manifest of Feather Icons (https://feathericons.com, MIT licensed). Matches Shipwright Pro's Figma icon set (node 2:163). Starter manifest includes 21 commonly-needed icons: info, alert-circle, alert-triangle, check, check-circle, x, x-circle, chevron-{up,down,left,right}, plus, minus, search, menu, external-link, settings, arrow-{left,right}, help-circle, trash. Icons live in app/components/shipwright/icons.rb as a frozen constant hash of name => inner-SVG fragment. Adding a new icon = paste Feather's inner markup into the hash. API: <%= render Shipwright::IconComponent.new(name: :info) %> <%= render Shipwright::IconComponent.new(name: :check, size: :lg, class: "text-utility-negative-default") %> <%= render Shipwright::IconComponent.new(name: :info, "aria-label": "More info") %> Props: - name: required symbol/string matching a key in ICONS - size: :sm (16px), :md (24px, default), :lg (32px) - class: consumer override - **html_attrs: pass-through (id, data-*, aria-*, etc.) Icons use stroke="currentColor" so they inherit the parent's text color — consumers control color via text-* utilities. Decorative by default (aria-hidden=true); providing aria-label makes the icon meaningful and skips aria-hidden. 13 new component tests pass. Total suite: 38 tests, 81 assertions. Lookbook previews: default, gallery (all 21), sizes, colored, with_aria_label. --- app/components/shipwright/icon_component.rb | 55 ++++++++++++ app/components/shipwright/icons.rb | 32 +++++++ .../shipwright/icon_component_preview.rb | 28 +++++++ .../icon_component_preview/colored.html.erb | 6 ++ .../icon_component_preview/gallery.html.erb | 8 ++ .../icon_component_preview/sizes.html.erb | 5 ++ .../shipwright/icon_component_test.rb | 84 +++++++++++++++++++ test/dummy/app/assets/builds/tailwind.css | 2 +- .../dummy/app/views/pages/components.html.erb | 17 ++++ 9 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 app/components/shipwright/icon_component.rb create mode 100644 app/components/shipwright/icons.rb create mode 100644 test/components/previews/shipwright/icon_component_preview.rb create mode 100644 test/components/previews/shipwright/icon_component_preview/colored.html.erb create mode 100644 test/components/previews/shipwright/icon_component_preview/gallery.html.erb create mode 100644 test/components/previews/shipwright/icon_component_preview/sizes.html.erb create mode 100644 test/components/shipwright/icon_component_test.rb diff --git a/app/components/shipwright/icon_component.rb b/app/components/shipwright/icon_component.rb new file mode 100644 index 0000000..5c91570 --- /dev/null +++ b/app/components/shipwright/icon_component.rb @@ -0,0 +1,55 @@ +require_relative "icons" + +module Shipwright + class IconComponent < BaseComponent + SIZES = { + sm: "w-4 h-4", # 16px + md: "w-6 h-6", # 24px — matches Feather native viewBox + lg: "w-8 h-8" # 32px + }.freeze + + class << self + # Exposed for tests / discoverability. Returns symbol keys. + def icon_names + @icon_names ||= ICONS.keys.map(&:to_sym).freeze + end + end + + # `class:` is a Ruby reserved word used as a keyword arg — requires + # binding.local_variable_get. Shipwright convention. + def initialize(name:, size: :md, class: nil, **html_attrs) + @name = name.to_s + @size = size + @class = binding.local_variable_get(:class) + @html_attrs = html_attrs + end + + def call + paths = ICONS.fetch(@name) do + raise ArgumentError, "#{self.class}: unknown icon :#{@name}. Available: #{ICONS.keys.sort.join(', ')}" + end + size_classes = SIZES.fetch(@size) do |v| + raise ArgumentError, "#{self.class}: unknown size :#{v}. Valid: #{SIZES.keys.join(', ')}" + end + + # Icons without an explicit aria-label are decorative and hidden from + # screen readers. When a consumer provides aria-label, treat the icon + # as meaningful and skip aria-hidden. + aria_label = @html_attrs[:"aria-label"] || @html_attrs[:aria_label] + accessibility_attrs = aria_label ? {} : { "aria-hidden": "true" } + + content_tag :svg, + paths, + class: classes(size_classes, consumer: @class), + xmlns: "http://www.w3.org/2000/svg", + viewBox: "0 0 24 24", + fill: "none", + stroke: "currentColor", + "stroke-width": "2", + "stroke-linecap": "round", + "stroke-linejoin": "round", + **accessibility_attrs, + **@html_attrs + end + end +end diff --git a/app/components/shipwright/icons.rb b/app/components/shipwright/icons.rb new file mode 100644 index 0000000..1667596 --- /dev/null +++ b/app/components/shipwright/icons.rb @@ -0,0 +1,32 @@ +module Shipwright + # Feather Icons (https://feathericons.com) — MIT licensed, 24x24 viewBox, + # stroke-based design. Values are the inner SVG fragment; the wrapping + # with viewBox/stroke attributes is provided by IconComponent. + # + # To add an icon: copy its inner content from https://feathericons.com/ + # and paste it below. Keep icon names in kebab-case matching Figma's + # icon name in the design file. + ICONS = { + "info" => %().html_safe, + "alert-circle" => %().html_safe, + "alert-triangle" => %().html_safe, + "check" => %().html_safe, + "check-circle" => %().html_safe, + "x" => %().html_safe, + "x-circle" => %().html_safe, + "chevron-down" => %().html_safe, + "chevron-up" => %().html_safe, + "chevron-left" => %().html_safe, + "chevron-right" => %().html_safe, + "plus" => %().html_safe, + "minus" => %().html_safe, + "search" => %().html_safe, + "menu" => %().html_safe, + "external-link" => %().html_safe, + "settings" => %().html_safe, + "arrow-right" => %().html_safe, + "arrow-left" => %().html_safe, + "help-circle" => %().html_safe, + "trash" => %().html_safe + }.freeze +end diff --git a/test/components/previews/shipwright/icon_component_preview.rb b/test/components/previews/shipwright/icon_component_preview.rb new file mode 100644 index 0000000..0bc4272 --- /dev/null +++ b/test/components/previews/shipwright/icon_component_preview.rb @@ -0,0 +1,28 @@ +module Shipwright + class IconComponentPreview < Lookbook::Preview + # @label Default (info, md) + def default + render Shipwright::IconComponent.new(name: :info) + end + + # @label All Icons + def gallery + render_with_template + end + + # @label Sizes + def sizes + render_with_template + end + + # @label Colored (via text-*) + def colored + render_with_template + end + + # @label With aria-label (non-decorative) + def with_aria_label + render Shipwright::IconComponent.new(name: :info, "aria-label": "More information") + end + end +end diff --git a/test/components/previews/shipwright/icon_component_preview/colored.html.erb b/test/components/previews/shipwright/icon_component_preview/colored.html.erb new file mode 100644 index 0000000..b76910f --- /dev/null +++ b/test/components/previews/shipwright/icon_component_preview/colored.html.erb @@ -0,0 +1,6 @@ +
+ <%= render Shipwright::IconComponent.new(name: :"alert-circle", class: "text-interactive-primary-default") %> + <%= render Shipwright::IconComponent.new(name: :"alert-triangle", class: "text-utility-negative-default") %> + <%= render Shipwright::IconComponent.new(name: :"check-circle", class: "text-interactive-primary-default") %> + <%= render Shipwright::IconComponent.new(name: :info, class: "text-interactive-primary-hover") %> +
diff --git a/test/components/previews/shipwright/icon_component_preview/gallery.html.erb b/test/components/previews/shipwright/icon_component_preview/gallery.html.erb new file mode 100644 index 0000000..d754621 --- /dev/null +++ b/test/components/previews/shipwright/icon_component_preview/gallery.html.erb @@ -0,0 +1,8 @@ +
+ <% Shipwright::IconComponent.icon_names.each do |name| %> +
+ <%= render Shipwright::IconComponent.new(name: name) %> + <%= name %> +
+ <% end %> +
diff --git a/test/components/previews/shipwright/icon_component_preview/sizes.html.erb b/test/components/previews/shipwright/icon_component_preview/sizes.html.erb new file mode 100644 index 0000000..2f351a9 --- /dev/null +++ b/test/components/previews/shipwright/icon_component_preview/sizes.html.erb @@ -0,0 +1,5 @@ +
+ <%= render Shipwright::IconComponent.new(name: :info, size: :sm) %> + <%= render Shipwright::IconComponent.new(name: :info, size: :md) %> + <%= render Shipwright::IconComponent.new(name: :info, size: :lg) %> +
diff --git a/test/components/shipwright/icon_component_test.rb b/test/components/shipwright/icon_component_test.rb new file mode 100644 index 0000000..996d1c9 --- /dev/null +++ b/test/components/shipwright/icon_component_test.rb @@ -0,0 +1,84 @@ +require "test_helper" + +class Shipwright::IconComponentTest < ViewComponent::TestCase + def test_renders_known_icon_by_name + render_inline(Shipwright::IconComponent.new(name: :info)) + + assert_selector "svg" + assert_selector "svg circle[cx='12'][cy='12'][r='10']" + end + + def test_uses_current_color_for_stroke + render_inline(Shipwright::IconComponent.new(name: :check)) + + assert_selector "svg[stroke='currentColor']" + end + + def test_default_size_is_medium_24 + render_inline(Shipwright::IconComponent.new(name: :info)) + + assert_selector "svg.w-6.h-6" + end + + def test_small_size_16 + render_inline(Shipwright::IconComponent.new(name: :info, size: :sm)) + + assert_selector "svg.w-4.h-4" + end + + def test_large_size_32 + render_inline(Shipwright::IconComponent.new(name: :info, size: :lg)) + + assert_selector "svg.w-8.h-8" + end + + def test_accepts_string_or_symbol_name + render_inline(Shipwright::IconComponent.new(name: "info")) + assert_selector "svg" + end + + def test_consumer_class_merges + render_inline(Shipwright::IconComponent.new(name: :info, class: "text-utility-negative-default")) + + assert_selector "svg.text-utility-negative-default" + end + + def test_passes_html_attributes + render_inline(Shipwright::IconComponent.new(name: :info, id: "info-icon", data: { action: "tooltip" })) + + assert_selector "svg#info-icon[data-action='tooltip']" + end + + def test_aria_hidden_by_default + render_inline(Shipwright::IconComponent.new(name: :info)) + + assert_selector "svg[aria-hidden='true']" + end + + def test_aria_label_makes_icon_visible_to_assistive_tech + render_inline(Shipwright::IconComponent.new(name: :info, "aria-label": "More information")) + + assert_selector "svg[aria-label='More information']" + # When aria-label is explicitly set, the icon should not be aria-hidden + assert_no_selector "svg[aria-hidden='true']" + end + + def test_raises_on_unknown_icon + assert_raises(ArgumentError) do + render_inline(Shipwright::IconComponent.new(name: :nonexistent)) + end + end + + def test_raises_on_invalid_size + assert_raises(ArgumentError) do + render_inline(Shipwright::IconComponent.new(name: :info, size: :xl)) + end + end + + def test_exposes_available_icon_names + assert_kind_of Array, Shipwright::IconComponent.icon_names + assert_includes Shipwright::IconComponent.icon_names, :info + assert_includes Shipwright::IconComponent.icon_names, :check + assert_includes Shipwright::IconComponent.icon_names, :"chevron-down" + end +end diff --git a/test/dummy/app/assets/builds/tailwind.css b/test/dummy/app/assets/builds/tailwind.css index ca8b6d6..120a09b 100644 --- a/test/dummy/app/assets/builds/tailwind.css +++ b/test/dummy/app/assets/builds/tailwind.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:"Manrope", ui-sans-serif, system-ui, sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--font-weight-normal:400;--font-weight-medium:500;--radius-md:6px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-interactive-primary-default:#14161c;--color-interactive-primary-hover:#595d6a;--color-interactive-primary-pressed:#333747;--color-interactive-secondary-default:#fdfdfd;--color-interactive-disable:#eaeaea;--color-border-interactive:#14161c;--color-utility-negative-default:#b62e2e;--color-text-primary:#050507;--color-text-inverse:#fdfdfd;--color-text-disabled:#595d6a;--color-background-primary:#fdfdfd;--leading-2xs:1.25rem;--spacing-2xs:8px;--spacing-sm:16px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.inline-flex{display:inline-flex}.h-8{height:calc(var(--spacing) * 8)}.h-11{height:calc(var(--spacing) * 11)}.h-13{height:calc(var(--spacing) * 13)}.w-8{width:calc(var(--spacing) * 8)}.w-11{width:calc(var(--spacing) * 11)}.w-13{width:calc(var(--spacing) * 13)}.w-full{width:100%}.cursor-not-allowed{cursor:not-allowed}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2xs{gap:var(--spacing-2xs)}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-border-interactive{border-color:var(--color-border-interactive)}.bg-interactive-disable{background-color:var(--color-interactive-disable)}.bg-interactive-primary-default{background-color:var(--color-interactive-primary-default)}.bg-interactive-secondary-default{background-color:var(--color-interactive-secondary-default)}.bg-transparent{background-color:#0000}.bg-utility-negative-default{background-color:var(--color-utility-negative-default)}.p-0{padding:calc(var(--spacing) * 0)}.px-sm{padding-inline:var(--spacing-sm)}.font-sans{font-family:var(--font-sans)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.leading-2xs{--tw-leading:var(--leading-2xs);line-height:var(--leading-2xs)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.text-text-disabled{color:var(--color-text-disabled)}.text-text-inverse{color:var(--color-text-inverse)}.text-text-primary{color:var(--color-text-primary)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-interactive-disable:hover{background-color:var(--color-interactive-disable)}.hover\:bg-interactive-primary-hover:hover{background-color:var(--color-interactive-primary-hover)}.hover\:bg-utility-negative-default\/90:hover{background-color:#b62e2ee6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-utility-negative-default\/90:hover{background-color:color-mix(in oklab, var(--color-utility-negative-default) 90%, transparent)}}.hover\:text-text-disabled:hover{color:var(--color-text-disabled)}.hover\:text-text-inverse:hover{color:var(--color-text-inverse)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-interactive-primary-default:focus-visible{--tw-ring-color:var(--color-interactive-primary-default)}.focus-visible\:ring-utility-negative-default:focus-visible{--tw-ring-color:var(--color-utility-negative-default)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:ring-offset-background-primary:focus-visible{--tw-ring-offset-color:var(--color-background-primary)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:bg-interactive-disable:active{background-color:var(--color-interactive-disable)}.active\:bg-interactive-primary-pressed:active{background-color:var(--color-interactive-primary-pressed)}.active\:bg-utility-negative-default\/80:active{background-color:#b62e2ecc}@supports (color:color-mix(in lab, red, red)){.active\:bg-utility-negative-default\/80:active{background-color:color-mix(in oklab, var(--color-utility-negative-default) 80%, transparent)}}.active\:text-text-inverse:active{color:var(--color-text-inverse)}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:"Manrope", ui-sans-serif, system-ui, sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--font-weight-normal:400;--radius-md:6px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-interactive-primary-default:#14161c;--color-interactive-primary-hover:#595d6a;--color-interactive-primary-pressed:#333747;--color-interactive-secondary-default:#fdfdfd;--color-interactive-disable:#eaeaea;--color-border-interactive:#14161c;--color-utility-negative-default:#b62e2e;--color-text-primary:#050507;--color-text-inverse:#fdfdfd;--color-text-disabled:#595d6a;--color-background-primary:#fdfdfd;--leading-2xs:1.25rem;--spacing-2xs:8px;--spacing-sm:16px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.inline-flex{display:inline-flex}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-11{height:calc(var(--spacing) * 11)}.h-13{height:calc(var(--spacing) * 13)}.w-4{width:calc(var(--spacing) * 4)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-11{width:calc(var(--spacing) * 11)}.w-13{width:calc(var(--spacing) * 13)}.w-full{width:100%}.cursor-not-allowed{cursor:not-allowed}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2xs{gap:var(--spacing-2xs)}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-border-interactive{border-color:var(--color-border-interactive)}.bg-interactive-disable{background-color:var(--color-interactive-disable)}.bg-interactive-primary-default{background-color:var(--color-interactive-primary-default)}.bg-interactive-secondary-default{background-color:var(--color-interactive-secondary-default)}.bg-transparent{background-color:#0000}.bg-utility-negative-default{background-color:var(--color-utility-negative-default)}.p-0{padding:calc(var(--spacing) * 0)}.px-sm{padding-inline:var(--spacing-sm)}.font-sans{font-family:var(--font-sans)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.leading-2xs{--tw-leading:var(--leading-2xs);line-height:var(--leading-2xs)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.text-text-disabled{color:var(--color-text-disabled)}.text-text-inverse{color:var(--color-text-inverse)}.text-text-primary{color:var(--color-text-primary)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-interactive-disable:hover{background-color:var(--color-interactive-disable)}.hover\:bg-interactive-primary-hover:hover{background-color:var(--color-interactive-primary-hover)}.hover\:bg-utility-negative-default\/90:hover{background-color:#b62e2ee6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-utility-negative-default\/90:hover{background-color:color-mix(in oklab, var(--color-utility-negative-default) 90%, transparent)}}.hover\:text-text-disabled:hover{color:var(--color-text-disabled)}.hover\:text-text-inverse:hover{color:var(--color-text-inverse)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-interactive-primary-default:focus-visible{--tw-ring-color:var(--color-interactive-primary-default)}.focus-visible\:ring-utility-negative-default:focus-visible{--tw-ring-color:var(--color-utility-negative-default)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:ring-offset-background-primary:focus-visible{--tw-ring-offset-color:var(--color-background-primary)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:bg-interactive-disable:active{background-color:var(--color-interactive-disable)}.active\:bg-interactive-primary-pressed:active{background-color:var(--color-interactive-primary-pressed)}.active\:bg-utility-negative-default\/80:active{background-color:#b62e2ecc}@supports (color:color-mix(in lab, red, red)){.active\:bg-utility-negative-default\/80:active{background-color:color-mix(in oklab, var(--color-utility-negative-default) 80%, transparent)}}.active\:text-text-inverse:active{color:var(--color-text-inverse)}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} \ No newline at end of file diff --git a/test/dummy/app/views/pages/components.html.erb b/test/dummy/app/views/pages/components.html.erb index de3e4fc..2ae52ca 100644 --- a/test/dummy/app/views/pages/components.html.erb +++ b/test/dummy/app/views/pages/components.html.erb @@ -49,4 +49,21 @@
<%= render Shipwright::ButtonComponent.new(class: "w-full") do %>Full Width Override<% end %>
+ +

Icons

+
+ <% Shipwright::IconComponent.icon_names.first(10).each do |name| %> +
+ <%= render Shipwright::IconComponent.new(name: name) %> + <%= name %> +
+ <% end %> +
+ +

Icon — Sizes

+
+ <%= render Shipwright::IconComponent.new(name: :info, size: :sm) %> + <%= render Shipwright::IconComponent.new(name: :info, size: :md) %> + <%= render Shipwright::IconComponent.new(name: :info, size: :lg) %> +
From 5eb7e289bb41ddc846168ec3c2fe130966036072 Mon Sep 17 00:00:00 2001 From: Jordan Burke Date: Fri, 17 Apr 2026 14:14:26 -0400 Subject: [PATCH 2/3] feat: add LinkComponent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Shipwright::LinkComponent — inline text link primitive. Matches Shipwright Pro's Link atom (Figma node 2015:15). Colors pulled from Figma variables (no new tokens needed): - Default: text-interactive-primary-default (#14161c) - Hover: text-interactive-primary-hover (#595d6a) - Pressed: text-interactive-primary-pressed (#333747) - Disabled: text-interactive-disable (#eaeaea) API: <%= render(Shipwright::LinkComponent.new(href: "/about")) { "About" } %> <%= render(Shipwright::LinkComponent.new(href: "#", decoration: :none)) { "Plain link" } %> <%= render(Shipwright::LinkComponent.new(href: "#", disabled: true)) { "Unavailable" } %> Props: - href: optional; omitted when disabled so the link isn't navigable - decoration: :underline (default) or :none — maps to Figma's Decoration variant - disabled: bool — adds aria-disabled and tabindex=-1, removes href - class: consumer override - **html_attrs: pass-through (target, rel, id, data-*, etc.) Typography matches Figma's Label/Small spec: text-sm (14px) + leading-4 (16px) + Manrope sans (from --font-sans token). Hover/pressed/focus states use Tailwind pseudo-class modifiers so browser events drive the visuals rather than JS state. 11 new tests pass. Total suite: 36 tests, 83 assertions. Lookbook previews: default, no_decoration, disabled, all_states, external. --- app/components/shipwright/link_component.rb | 59 ++++++++++++++ .../shipwright/link_component_preview.rb | 28 +++++++ .../all_states.html.erb | 22 +++++ .../shipwright/link_component_test.rb | 81 +++++++++++++++++++ test/dummy/app/assets/builds/tailwind.css | 2 +- .../dummy/app/views/pages/components.html.erb | 7 ++ 6 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 app/components/shipwright/link_component.rb create mode 100644 test/components/previews/shipwright/link_component_preview.rb create mode 100644 test/components/previews/shipwright/link_component_preview/all_states.html.erb create mode 100644 test/components/shipwright/link_component_test.rb diff --git a/app/components/shipwright/link_component.rb b/app/components/shipwright/link_component.rb new file mode 100644 index 0000000..addb20c --- /dev/null +++ b/app/components/shipwright/link_component.rb @@ -0,0 +1,59 @@ +module Shipwright + class LinkComponent < BaseComponent + # Decoration controls the text-decoration (underline). Maps to Figma's + # "Decoration" variant (Yes/No). + DECORATIONS = { + underline: "underline underline-offset-2", + none: "no-underline" + }.freeze + + # Default state classes. Hover/active/focus states use Tailwind pseudo-class + # modifiers so browser events drive the visual changes, matching Figma's + # State variants (Default/Hover/Pressed/Focused). + ENABLED_CLASSES = [ + "text-interactive-primary-default", + "hover:text-interactive-primary-hover", + "active:text-interactive-primary-pressed", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-interactive-primary-default focus-visible:ring-offset-2 focus-visible:ring-offset-background-primary", + "transition-colors" + ].join(" ").freeze + + # Disabled state — uses interactive-disable (matches Figma's Disabled + # variant which resolved to Interactive/Disable=#eaeaea). + DISABLED_CLASSES = "text-interactive-disable cursor-not-allowed".freeze + + # `class:` is a Ruby reserved word used as a keyword arg — requires + # binding.local_variable_get. Shipwright convention. + def initialize(href: nil, decoration: :underline, disabled: false, class: nil, **html_attrs) + @href = href + @decoration = decoration + @disabled = disabled + @class = binding.local_variable_get(:class) + @html_attrs = html_attrs + end + + def call + decoration_class = DECORATIONS.fetch(@decoration) do |v| + raise ArgumentError, "#{self.class}: unknown decoration :#{v}. Valid: #{DECORATIONS.keys.join(', ')}" + end + + attrs = @html_attrs.dup + if @disabled + # Drop href so the link isn't navigable; signal state to AT. + attrs[:"aria-disabled"] = "true" + attrs[:tabindex] = "-1" + else + attrs[:href] = @href if @href + end + + content_tag :a, content, + class: classes( + "inline text-sm font-sans font-normal leading-4", + decoration_class, + @disabled ? DISABLED_CLASSES : ENABLED_CLASSES, + consumer: @class + ), + **attrs + end + end +end diff --git a/test/components/previews/shipwright/link_component_preview.rb b/test/components/previews/shipwright/link_component_preview.rb new file mode 100644 index 0000000..f46cc6d --- /dev/null +++ b/test/components/previews/shipwright/link_component_preview.rb @@ -0,0 +1,28 @@ +module Shipwright + class LinkComponentPreview < Lookbook::Preview + # @label Default (underlined) + def default + render(Shipwright::LinkComponent.new(href: "#")) { "Link" } + end + + # @label No decoration + def no_decoration + render(Shipwright::LinkComponent.new(href: "#", decoration: :none)) { "Link" } + end + + # @label Disabled + def disabled + render(Shipwright::LinkComponent.new(href: "#", disabled: true)) { "Link" } + end + + # @label All States + def all_states + render_with_template + end + + # @label External Link + def external + render(Shipwright::LinkComponent.new(href: "https://example.com", target: "_blank", rel: "noopener")) { "External" } + end + end +end diff --git a/test/components/previews/shipwright/link_component_preview/all_states.html.erb b/test/components/previews/shipwright/link_component_preview/all_states.html.erb new file mode 100644 index 0000000..41e8387 --- /dev/null +++ b/test/components/previews/shipwright/link_component_preview/all_states.html.erb @@ -0,0 +1,22 @@ +
+
+ Decoration: underline +
+ <%= render(Shipwright::LinkComponent.new(href: "#")) { "Default" } %> + <%= render(Shipwright::LinkComponent.new(href: "#", disabled: true)) { "Disabled" } %> +
+
+
+ Decoration: none +
+ <%= render(Shipwright::LinkComponent.new(href: "#", decoration: :none)) { "Default" } %> + <%= render(Shipwright::LinkComponent.new(href: "#", decoration: :none, disabled: true)) { "Disabled" } %> +
+
+
+ Within paragraph text +

+ This is a sentence with <%= render(Shipwright::LinkComponent.new(href: "#")) { "an inline link" } %> that should wrap naturally and maintain the baseline of the surrounding text. +

+
+
diff --git a/test/components/shipwright/link_component_test.rb b/test/components/shipwright/link_component_test.rb new file mode 100644 index 0000000..d41e44b --- /dev/null +++ b/test/components/shipwright/link_component_test.rb @@ -0,0 +1,81 @@ +require "test_helper" + +class Shipwright::LinkComponentTest < ViewComponent::TestCase + def test_renders_anchor_with_content_and_href + render_inline(Shipwright::LinkComponent.new(href: "/about")) { "About" } + + assert_selector "a[href='/about']", text: "About" + end + + def test_default_has_primary_color_and_underline + render_inline(Shipwright::LinkComponent.new(href: "#")) { "Link" } + + assert_selector "a.text-interactive-primary-default" + assert_selector "a.underline" + end + + def test_no_decoration_removes_underline + render_inline(Shipwright::LinkComponent.new(href: "#", decoration: :none)) { "Link" } + + assert_selector "a.no-underline" + assert_no_selector "a.underline" + end + + def test_applies_hover_and_active_color_classes + render_inline(Shipwright::LinkComponent.new(href: "#")) { "Link" } + + assert_selector "a.hover\\:text-interactive-primary-hover" + assert_selector "a.active\\:text-interactive-primary-pressed" + end + + def test_disabled_link_uses_disabled_color_and_aria + render_inline(Shipwright::LinkComponent.new(href: "#", disabled: true)) { "Off" } + + assert_selector "a[aria-disabled='true']" + assert_selector "a.text-interactive-disable" + assert_selector "a.cursor-not-allowed" + # Disabled links shouldn't carry an href to prevent navigation + assert_no_selector "a[href='#']" + end + + def test_consumer_class_overrides_defaults + render_inline(Shipwright::LinkComponent.new(href: "#", class: "text-utility-negative-default")) { "Danger" } + + assert_selector "a.text-utility-negative-default" + assert_no_selector "a.text-interactive-primary-default" + end + + def test_passes_html_attributes + render_inline(Shipwright::LinkComponent.new(href: "#", target: "_blank", id: "ext", data: { turbo: "false" })) { "External" } + + assert_selector "a#ext[target='_blank'][data-turbo='false']" + end + + def test_renders_without_href_when_not_provided + render_inline(Shipwright::LinkComponent.new) { "Trigger" } + + assert_selector "a", text: "Trigger" + assert_no_selector "a[href]" + end + + def test_includes_focus_ring_classes + render_inline(Shipwright::LinkComponent.new(href: "#")) { "Link" } + + html = rendered_content + assert_includes html, "focus-visible:" + assert_includes html, "ring" + end + + def test_raises_on_invalid_decoration + assert_raises(ArgumentError) do + render_inline(Shipwright::LinkComponent.new(href: "#", decoration: :strikethrough)) { "Bad" } + end + end + + def test_typography_classes_applied + render_inline(Shipwright::LinkComponent.new(href: "#")) { "Link" } + + assert_selector "a.text-sm" + assert_selector "a.font-sans" + end +end diff --git a/test/dummy/app/assets/builds/tailwind.css b/test/dummy/app/assets/builds/tailwind.css index ca8b6d6..8ef3f4a 100644 --- a/test/dummy/app/assets/builds/tailwind.css +++ b/test/dummy/app/assets/builds/tailwind.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:"Manrope", ui-sans-serif, system-ui, sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--font-weight-normal:400;--font-weight-medium:500;--radius-md:6px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-interactive-primary-default:#14161c;--color-interactive-primary-hover:#595d6a;--color-interactive-primary-pressed:#333747;--color-interactive-secondary-default:#fdfdfd;--color-interactive-disable:#eaeaea;--color-border-interactive:#14161c;--color-utility-negative-default:#b62e2e;--color-text-primary:#050507;--color-text-inverse:#fdfdfd;--color-text-disabled:#595d6a;--color-background-primary:#fdfdfd;--leading-2xs:1.25rem;--spacing-2xs:8px;--spacing-sm:16px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.inline-flex{display:inline-flex}.h-8{height:calc(var(--spacing) * 8)}.h-11{height:calc(var(--spacing) * 11)}.h-13{height:calc(var(--spacing) * 13)}.w-8{width:calc(var(--spacing) * 8)}.w-11{width:calc(var(--spacing) * 11)}.w-13{width:calc(var(--spacing) * 13)}.w-full{width:100%}.cursor-not-allowed{cursor:not-allowed}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2xs{gap:var(--spacing-2xs)}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-border-interactive{border-color:var(--color-border-interactive)}.bg-interactive-disable{background-color:var(--color-interactive-disable)}.bg-interactive-primary-default{background-color:var(--color-interactive-primary-default)}.bg-interactive-secondary-default{background-color:var(--color-interactive-secondary-default)}.bg-transparent{background-color:#0000}.bg-utility-negative-default{background-color:var(--color-utility-negative-default)}.p-0{padding:calc(var(--spacing) * 0)}.px-sm{padding-inline:var(--spacing-sm)}.font-sans{font-family:var(--font-sans)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.leading-2xs{--tw-leading:var(--leading-2xs);line-height:var(--leading-2xs)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.text-text-disabled{color:var(--color-text-disabled)}.text-text-inverse{color:var(--color-text-inverse)}.text-text-primary{color:var(--color-text-primary)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-interactive-disable:hover{background-color:var(--color-interactive-disable)}.hover\:bg-interactive-primary-hover:hover{background-color:var(--color-interactive-primary-hover)}.hover\:bg-utility-negative-default\/90:hover{background-color:#b62e2ee6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-utility-negative-default\/90:hover{background-color:color-mix(in oklab, var(--color-utility-negative-default) 90%, transparent)}}.hover\:text-text-disabled:hover{color:var(--color-text-disabled)}.hover\:text-text-inverse:hover{color:var(--color-text-inverse)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-interactive-primary-default:focus-visible{--tw-ring-color:var(--color-interactive-primary-default)}.focus-visible\:ring-utility-negative-default:focus-visible{--tw-ring-color:var(--color-utility-negative-default)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:ring-offset-background-primary:focus-visible{--tw-ring-offset-color:var(--color-background-primary)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:bg-interactive-disable:active{background-color:var(--color-interactive-disable)}.active\:bg-interactive-primary-pressed:active{background-color:var(--color-interactive-primary-pressed)}.active\:bg-utility-negative-default\/80:active{background-color:#b62e2ecc}@supports (color:color-mix(in lab, red, red)){.active\:bg-utility-negative-default\/80:active{background-color:color-mix(in oklab, var(--color-utility-negative-default) 80%, transparent)}}.active\:text-text-inverse:active{color:var(--color-text-inverse)}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:"Manrope", ui-sans-serif, system-ui, sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--font-weight-normal:400;--radius-md:6px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-interactive-primary-default:#14161c;--color-interactive-primary-hover:#595d6a;--color-interactive-primary-pressed:#333747;--color-interactive-secondary-default:#fdfdfd;--color-interactive-disable:#eaeaea;--color-border-interactive:#14161c;--color-utility-negative-default:#b62e2e;--color-text-primary:#050507;--color-text-inverse:#fdfdfd;--color-text-disabled:#595d6a;--color-background-primary:#fdfdfd;--leading-2xs:1.25rem;--spacing-2xs:8px;--spacing-sm:16px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.inline{display:inline}.inline-flex{display:inline-flex}.h-8{height:calc(var(--spacing) * 8)}.h-11{height:calc(var(--spacing) * 11)}.h-13{height:calc(var(--spacing) * 13)}.w-8{width:calc(var(--spacing) * 8)}.w-11{width:calc(var(--spacing) * 11)}.w-13{width:calc(var(--spacing) * 13)}.w-full{width:100%}.cursor-not-allowed{cursor:not-allowed}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2xs{gap:var(--spacing-2xs)}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-border-interactive{border-color:var(--color-border-interactive)}.bg-interactive-disable{background-color:var(--color-interactive-disable)}.bg-interactive-primary-default{background-color:var(--color-interactive-primary-default)}.bg-interactive-secondary-default{background-color:var(--color-interactive-secondary-default)}.bg-transparent{background-color:#0000}.bg-utility-negative-default{background-color:var(--color-utility-negative-default)}.p-0{padding:calc(var(--spacing) * 0)}.px-sm{padding-inline:var(--spacing-sm)}.font-sans{font-family:var(--font-sans)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.leading-2xs{--tw-leading:var(--leading-2xs);line-height:var(--leading-2xs)}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.text-interactive-disable{color:var(--color-interactive-disable)}.text-interactive-primary-default{color:var(--color-interactive-primary-default)}.text-text-disabled{color:var(--color-text-disabled)}.text-text-inverse{color:var(--color-text-inverse)}.text-text-primary{color:var(--color-text-primary)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-interactive-disable:hover{background-color:var(--color-interactive-disable)}.hover\:bg-interactive-primary-hover:hover{background-color:var(--color-interactive-primary-hover)}.hover\:bg-utility-negative-default\/90:hover{background-color:#b62e2ee6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-utility-negative-default\/90:hover{background-color:color-mix(in oklab, var(--color-utility-negative-default) 90%, transparent)}}.hover\:text-interactive-primary-hover:hover{color:var(--color-interactive-primary-hover)}.hover\:text-text-disabled:hover{color:var(--color-text-disabled)}.hover\:text-text-inverse:hover{color:var(--color-text-inverse)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-interactive-primary-default:focus-visible{--tw-ring-color:var(--color-interactive-primary-default)}.focus-visible\:ring-utility-negative-default:focus-visible{--tw-ring-color:var(--color-utility-negative-default)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:ring-offset-background-primary:focus-visible{--tw-ring-offset-color:var(--color-background-primary)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:bg-interactive-disable:active{background-color:var(--color-interactive-disable)}.active\:bg-interactive-primary-pressed:active{background-color:var(--color-interactive-primary-pressed)}.active\:bg-utility-negative-default\/80:active{background-color:#b62e2ecc}@supports (color:color-mix(in lab, red, red)){.active\:bg-utility-negative-default\/80:active{background-color:color-mix(in oklab, var(--color-utility-negative-default) 80%, transparent)}}.active\:text-interactive-primary-pressed:active{color:var(--color-interactive-primary-pressed)}.active\:text-text-inverse:active{color:var(--color-text-inverse)}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} \ No newline at end of file diff --git a/test/dummy/app/views/pages/components.html.erb b/test/dummy/app/views/pages/components.html.erb index de3e4fc..5b2ee8e 100644 --- a/test/dummy/app/views/pages/components.html.erb +++ b/test/dummy/app/views/pages/components.html.erb @@ -49,4 +49,11 @@
<%= render Shipwright::ButtonComponent.new(class: "w-full") do %>Full Width Override<% end %>
+ +

Link

+
+ <%= render(Shipwright::LinkComponent.new(href: "#")) { "Default" } %> + <%= render(Shipwright::LinkComponent.new(href: "#", decoration: :none)) { "No decoration" } %> + <%= render(Shipwright::LinkComponent.new(href: "#", disabled: true)) { "Disabled" } %> +
From 88130dbec7b23d70ba3310b7a0057a945892af26 Mon Sep 17 00:00:00 2001 From: Jordan Burke Date: Fri, 17 Apr 2026 14:19:21 -0400 Subject: [PATCH 3/3] feat: add BannerComponent (molecule) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the Banner molecule from Shipwright Pro Figma (node 2013:5697). Composes IconComponent and LinkComponent as optional slotted atoms, per the monorepo-style component pattern. New token in engine.css: - --color-background-secondary: #f3f3f3 (Banner surface) BaseComponent MERGER config updated to recognize background-secondary. API: <%= render Shipwright::BannerComponent.new(header: "Alert") do |banner| %> <% banner.with_icon(name: :info) %> <% banner.with_link(href: "#") { "Link" } %> Place holder text for notifications. <% end %> Props: - header: optional string rendered in the leading cluster (e.g. "Alert") - class: consumer override - **html_attrs: pass-through (id, role, data-*, etc.) Slots: - with_icon(...) → renders a Shipwright::IconComponent (accepts any IconComponent props: name, size, class, etc.) - with_link(...) { ... } → renders a Shipwright::LinkComponent (href, decoration, disabled, etc. + block content) Content block → body text (main message). Layout matches Figma: flex row with p-4, optional leading cluster (icon + header) with pr-6 spacing, flex-1 body paragraph, optional trailing link cluster with pl-4 spacing. Typography: text-sm with leading-4 (16px) for header and leading-5 (20px) for body. 11 new tests pass. Total suite: 60 tests, 121 assertions. Lookbook previews: default, body_only, icon_and_body, body_and_link, header_and_body. --- app/assets/tailwind/shipwright/engine.css | 1 + .../shipwright/banner_component.html.erb | 20 +++++ app/components/shipwright/banner_component.rb | 32 +++++++ app/components/shipwright/base_component.rb | 1 + .../shipwright/banner_component_preview.rb | 32 +++++++ .../body_and_link.html.erb | 6 ++ .../banner_component_preview/default.html.erb | 7 ++ .../icon_and_body.html.erb | 16 ++++ .../shipwright/banner_component_test.rb | 88 +++++++++++++++++++ test/dummy/app/assets/builds/tailwind.css | 2 +- .../dummy/app/views/pages/components.html.erb | 19 ++++ 11 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 app/components/shipwright/banner_component.html.erb create mode 100644 app/components/shipwright/banner_component.rb create mode 100644 test/components/previews/shipwright/banner_component_preview.rb create mode 100644 test/components/previews/shipwright/banner_component_preview/body_and_link.html.erb create mode 100644 test/components/previews/shipwright/banner_component_preview/default.html.erb create mode 100644 test/components/previews/shipwright/banner_component_preview/icon_and_body.html.erb create mode 100644 test/components/shipwright/banner_component_test.rb diff --git a/app/assets/tailwind/shipwright/engine.css b/app/assets/tailwind/shipwright/engine.css index db2420a..a88704c 100644 --- a/app/assets/tailwind/shipwright/engine.css +++ b/app/assets/tailwind/shipwright/engine.css @@ -36,6 +36,7 @@ /* Background colors */ --color-background-primary: #fdfdfd; + --color-background-secondary: #f3f3f3; /* Typography — font loading is the consuming app's responsibility */ --font-sans: "Manrope", ui-sans-serif, system-ui, sans-serif; diff --git a/app/components/shipwright/banner_component.html.erb b/app/components/shipwright/banner_component.html.erb new file mode 100644 index 0000000..c16fb3e --- /dev/null +++ b/app/components/shipwright/banner_component.html.erb @@ -0,0 +1,20 @@ +<%= content_tag :div, class: container_classes, **html_attrs do %> + <% if header_present? %> +
+ <%= icon if icon? %> + <% if header.present? %> +

<%= header %>

+ <% end %> +
+ <% end %> + +

+ <%= content %> +

+ + <% if link? %> +
+ <%= link %> +
+ <% end %> +<% end %> diff --git a/app/components/shipwright/banner_component.rb b/app/components/shipwright/banner_component.rb new file mode 100644 index 0000000..b181719 --- /dev/null +++ b/app/components/shipwright/banner_component.rb @@ -0,0 +1,32 @@ +module Shipwright + class BannerComponent < BaseComponent + # Optional leading icon. Consumer-facing API: + # banner.with_icon(name: :info) + renders_one :icon, IconComponent + + # Optional trailing link action. + # banner.with_link(href: "#") { "Link" } + renders_one :link, LinkComponent + + # `class:` is a Ruby reserved word used as a keyword arg — requires + # binding.local_variable_get. Shipwright convention. + def initialize(header: nil, class: nil, **html_attrs) + @header = header + @class = binding.local_variable_get(:class) + @html_attrs = html_attrs + end + + def header_present? + @header.present? || icon? + end + + attr_reader :header, :html_attrs + + def container_classes + classes( + "flex items-start gap-0 p-4 bg-background-secondary font-sans", + consumer: @class + ) + end + end +end diff --git a/app/components/shipwright/base_component.rb b/app/components/shipwright/base_component.rb index f75b669..7efe294 100644 --- a/app/components/shipwright/base_component.rb +++ b/app/components/shipwright/base_component.rb @@ -18,6 +18,7 @@ class BaseComponent < ViewComponent::Base text-inverse text-disabled background-primary + background-secondary ], "radius" => %w[md] } diff --git a/test/components/previews/shipwright/banner_component_preview.rb b/test/components/previews/shipwright/banner_component_preview.rb new file mode 100644 index 0000000..ae77117 --- /dev/null +++ b/test/components/previews/shipwright/banner_component_preview.rb @@ -0,0 +1,32 @@ +module Shipwright + class BannerComponentPreview < Lookbook::Preview + # @label Default (icon + header + body + link) + def default + render_with_template + end + + # @label Body only + def body_only + render(Shipwright::BannerComponent.new) do + "Place holder text for notifications. Enter text into this container." + end + end + + # @label Icon + body + def icon_and_body + render_with_template + end + + # @label Body + link (no icon) + def body_and_link + render_with_template + end + + # @label Header + body (no icon) + def header_and_body + render(Shipwright::BannerComponent.new(header: "Alert")) do + "Place holder text for notifications." + end + end + end +end diff --git a/test/components/previews/shipwright/banner_component_preview/body_and_link.html.erb b/test/components/previews/shipwright/banner_component_preview/body_and_link.html.erb new file mode 100644 index 0000000..64dc28f --- /dev/null +++ b/test/components/previews/shipwright/banner_component_preview/body_and_link.html.erb @@ -0,0 +1,6 @@ +
+ <%= render Shipwright::BannerComponent.new do |banner| %> + <% banner.with_link(href: "#") { "Learn more" } %> + Place holder text for notifications. Enter text into this container. + <% end %> +
diff --git a/test/components/previews/shipwright/banner_component_preview/default.html.erb b/test/components/previews/shipwright/banner_component_preview/default.html.erb new file mode 100644 index 0000000..323af1d --- /dev/null +++ b/test/components/previews/shipwright/banner_component_preview/default.html.erb @@ -0,0 +1,7 @@ +
+ <%= render Shipwright::BannerComponent.new(header: "Alert") do |banner| %> + <% banner.with_icon(name: :info) %> + <% banner.with_link(href: "#") { "Link" } %> + Place holder text for notifications. Enter Text into this container. + <% end %> +
diff --git a/test/components/previews/shipwright/banner_component_preview/icon_and_body.html.erb b/test/components/previews/shipwright/banner_component_preview/icon_and_body.html.erb new file mode 100644 index 0000000..4194020 --- /dev/null +++ b/test/components/previews/shipwright/banner_component_preview/icon_and_body.html.erb @@ -0,0 +1,16 @@ +
+ <%= render Shipwright::BannerComponent.new(header: "Info") do |banner| %> + <% banner.with_icon(name: :info) %> + Your session will expire in 5 minutes. + <% end %> + + <%= render Shipwright::BannerComponent.new(header: "Warning") do |banner| %> + <% banner.with_icon(name: :"alert-triangle") %> + Some fields were missing before submission. + <% end %> + + <%= render Shipwright::BannerComponent.new(header: "Success") do |banner| %> + <% banner.with_icon(name: :"check-circle") %> + Your changes were saved. + <% end %> +
diff --git a/test/components/shipwright/banner_component_test.rb b/test/components/shipwright/banner_component_test.rb new file mode 100644 index 0000000..2bcfc97 --- /dev/null +++ b/test/components/shipwright/banner_component_test.rb @@ -0,0 +1,88 @@ +require "test_helper" + +class Shipwright::BannerComponentTest < ViewComponent::TestCase + def test_renders_body_content + render_inline(Shipwright::BannerComponent.new) { "Body message here." } + + assert_text "Body message here." + end + + def test_has_secondary_background + render_inline(Shipwright::BannerComponent.new) { "Body." } + + assert_selector "div.bg-background-secondary" + end + + def test_renders_header_when_provided + render_inline(Shipwright::BannerComponent.new(header: "Alert")) { "Body." } + + assert_text "Alert" + end + + def test_omits_header_block_when_no_header_or_icon + render_inline(Shipwright::BannerComponent.new) { "Body." } + + assert_no_selector "[data-banner-header]" + end + + def test_renders_icon_slot + render_inline(Shipwright::BannerComponent.new(header: "Alert")) do |banner| + banner.with_icon(name: :info) + "Body text." + end + + assert_selector "[data-banner-header] svg" + end + + def test_renders_link_slot + render_inline(Shipwright::BannerComponent.new) do |banner| + banner.with_link(href: "/more") { "Details" } + "Body text." + end + + assert_selector "a[href='/more']", text: "Details" + end + + def test_renders_icon_and_link_together + render_inline(Shipwright::BannerComponent.new(header: "Alert")) do |banner| + banner.with_icon(name: :info) + banner.with_link(href: "#") { "Link" } + "Body text." + end + + assert_selector "[data-banner-header] svg" + assert_text "Alert" + assert_text "Body text." + assert_selector "a", text: "Link" + end + + def test_consumer_class_overrides_default_bg + render_inline(Shipwright::BannerComponent.new(class: "bg-utility-warning-default")) { "Warn." } + + assert_selector "div.bg-utility-warning-default" + assert_no_selector "div.bg-background-secondary" + end + + def test_passes_html_attributes + render_inline(Shipwright::BannerComponent.new(id: "my-banner", role: "status")) { "Info." } + + assert_selector "div#my-banner[role='status']" + end + + def test_body_text_uses_typography_classes + render_inline(Shipwright::BannerComponent.new) { "Body." } + + assert_selector "div.font-sans" + assert_selector "p.text-sm", text: "Body." + end + + def test_shows_only_icon_without_header + render_inline(Shipwright::BannerComponent.new) do |banner| + banner.with_icon(name: :"alert-circle") + "Body only." + end + + assert_selector "svg" + assert_text "Body only." + end +end diff --git a/test/dummy/app/assets/builds/tailwind.css b/test/dummy/app/assets/builds/tailwind.css index 0fc4df7..4a43d11 100644 --- a/test/dummy/app/assets/builds/tailwind.css +++ b/test/dummy/app/assets/builds/tailwind.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:"Manrope", ui-sans-serif, system-ui, sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--font-weight-normal:400;--radius-md:6px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-interactive-primary-default:#14161c;--color-interactive-primary-hover:#595d6a;--color-interactive-primary-pressed:#333747;--color-interactive-secondary-default:#fdfdfd;--color-interactive-disable:#eaeaea;--color-border-interactive:#14161c;--color-utility-negative-default:#b62e2e;--color-text-primary:#050507;--color-text-inverse:#fdfdfd;--color-text-disabled:#595d6a;--color-background-primary:#fdfdfd;--leading-2xs:1.25rem;--spacing-2xs:8px;--spacing-sm:16px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.inline{display:inline}.inline-flex{display:inline-flex}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-11{height:calc(var(--spacing) * 11)}.h-13{height:calc(var(--spacing) * 13)}.w-4{width:calc(var(--spacing) * 4)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-11{width:calc(var(--spacing) * 11)}.w-13{width:calc(var(--spacing) * 13)}.w-full{width:100%}.cursor-not-allowed{cursor:not-allowed}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2xs{gap:var(--spacing-2xs)}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-border-interactive{border-color:var(--color-border-interactive)}.bg-interactive-disable{background-color:var(--color-interactive-disable)}.bg-interactive-primary-default{background-color:var(--color-interactive-primary-default)}.bg-interactive-secondary-default{background-color:var(--color-interactive-secondary-default)}.bg-transparent{background-color:#0000}.bg-utility-negative-default{background-color:var(--color-utility-negative-default)}.p-0{padding:calc(var(--spacing) * 0)}.px-sm{padding-inline:var(--spacing-sm)}.font-sans{font-family:var(--font-sans)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.leading-2xs{--tw-leading:var(--leading-2xs);line-height:var(--leading-2xs)}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.text-interactive-disable{color:var(--color-interactive-disable)}.text-interactive-primary-default{color:var(--color-interactive-primary-default)}.text-text-disabled{color:var(--color-text-disabled)}.text-text-inverse{color:var(--color-text-inverse)}.text-text-primary{color:var(--color-text-primary)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-interactive-disable:hover{background-color:var(--color-interactive-disable)}.hover\:bg-interactive-primary-hover:hover{background-color:var(--color-interactive-primary-hover)}.hover\:bg-utility-negative-default\/90:hover{background-color:#b62e2ee6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-utility-negative-default\/90:hover{background-color:color-mix(in oklab, var(--color-utility-negative-default) 90%, transparent)}}.hover\:text-interactive-primary-hover:hover{color:var(--color-interactive-primary-hover)}.hover\:text-text-disabled:hover{color:var(--color-text-disabled)}.hover\:text-text-inverse:hover{color:var(--color-text-inverse)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-interactive-primary-default:focus-visible{--tw-ring-color:var(--color-interactive-primary-default)}.focus-visible\:ring-utility-negative-default:focus-visible{--tw-ring-color:var(--color-utility-negative-default)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:ring-offset-background-primary:focus-visible{--tw-ring-offset-color:var(--color-background-primary)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:bg-interactive-disable:active{background-color:var(--color-interactive-disable)}.active\:bg-interactive-primary-pressed:active{background-color:var(--color-interactive-primary-pressed)}.active\:bg-utility-negative-default\/80:active{background-color:#b62e2ecc}@supports (color:color-mix(in lab, red, red)){.active\:bg-utility-negative-default\/80:active{background-color:color-mix(in oklab, var(--color-utility-negative-default) 80%, transparent)}}.active\:text-interactive-primary-pressed:active{color:var(--color-interactive-primary-pressed)}.active\:text-text-inverse:active{color:var(--color-text-inverse)}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:"Manrope", ui-sans-serif, system-ui, sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--font-weight-normal:400;--radius-md:6px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-interactive-primary-default:#14161c;--color-interactive-primary-hover:#595d6a;--color-interactive-primary-pressed:#333747;--color-interactive-secondary-default:#fdfdfd;--color-interactive-disable:#eaeaea;--color-border-interactive:#14161c;--color-utility-negative-default:#b62e2e;--color-text-primary:#050507;--color-text-inverse:#fdfdfd;--color-text-disabled:#595d6a;--color-background-primary:#fdfdfd;--color-background-secondary:#f3f3f3;--leading-2xs:1.25rem;--spacing-2xs:8px;--spacing-sm:16px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.flex{display:flex}.inline{display:inline}.inline-flex{display:inline-flex}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-11{height:calc(var(--spacing) * 11)}.h-13{height:calc(var(--spacing) * 13)}.w-4{width:calc(var(--spacing) * 4)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-11{width:calc(var(--spacing) * 11)}.w-13{width:calc(var(--spacing) * 13)}.w-full{width:100%}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.cursor-not-allowed{cursor:not-allowed}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-center{justify-content:center}.gap-0{gap:calc(var(--spacing) * 0)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2xs{gap:var(--spacing-2xs)}.self-stretch{align-self:stretch}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-border-interactive{border-color:var(--color-border-interactive)}.bg-background-secondary{background-color:var(--color-background-secondary)}.bg-interactive-disable{background-color:var(--color-interactive-disable)}.bg-interactive-primary-default{background-color:var(--color-interactive-primary-default)}.bg-interactive-secondary-default{background-color:var(--color-interactive-secondary-default)}.bg-transparent{background-color:#0000}.bg-utility-negative-default{background-color:var(--color-utility-negative-default)}.p-0{padding:calc(var(--spacing) * 0)}.p-4{padding:calc(var(--spacing) * 4)}.px-sm{padding-inline:var(--spacing-sm)}.pr-6{padding-right:calc(var(--spacing) * 6)}.pl-4{padding-left:calc(var(--spacing) * 4)}.font-sans{font-family:var(--font-sans)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.leading-2xs{--tw-leading:var(--leading-2xs);line-height:var(--leading-2xs)}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.text-interactive-disable{color:var(--color-interactive-disable)}.text-interactive-primary-default{color:var(--color-interactive-primary-default)}.text-text-disabled{color:var(--color-text-disabled)}.text-text-inverse{color:var(--color-text-inverse)}.text-text-primary{color:var(--color-text-primary)}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:bg-interactive-disable:hover{background-color:var(--color-interactive-disable)}.hover\:bg-interactive-primary-hover:hover{background-color:var(--color-interactive-primary-hover)}.hover\:bg-utility-negative-default\/90:hover{background-color:#b62e2ee6}@supports (color:color-mix(in lab, red, red)){.hover\:bg-utility-negative-default\/90:hover{background-color:color-mix(in oklab, var(--color-utility-negative-default) 90%, transparent)}}.hover\:text-interactive-primary-hover:hover{color:var(--color-interactive-primary-hover)}.hover\:text-text-disabled:hover{color:var(--color-text-disabled)}.hover\:text-text-inverse:hover{color:var(--color-text-inverse)}}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-interactive-primary-default:focus-visible{--tw-ring-color:var(--color-interactive-primary-default)}.focus-visible\:ring-utility-negative-default:focus-visible{--tw-ring-color:var(--color-utility-negative-default)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:ring-offset-background-primary:focus-visible{--tw-ring-offset-color:var(--color-background-primary)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.active\:bg-interactive-disable:active{background-color:var(--color-interactive-disable)}.active\:bg-interactive-primary-pressed:active{background-color:var(--color-interactive-primary-pressed)}.active\:bg-utility-negative-default\/80:active{background-color:#b62e2ecc}@supports (color:color-mix(in lab, red, red)){.active\:bg-utility-negative-default\/80:active{background-color:color-mix(in oklab, var(--color-utility-negative-default) 80%, transparent)}}.active\:text-interactive-primary-pressed:active{color:var(--color-interactive-primary-pressed)}.active\:text-text-inverse:active{color:var(--color-text-inverse)}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000} \ No newline at end of file diff --git a/test/dummy/app/views/pages/components.html.erb b/test/dummy/app/views/pages/components.html.erb index 660632e..e2e76d0 100644 --- a/test/dummy/app/views/pages/components.html.erb +++ b/test/dummy/app/views/pages/components.html.erb @@ -73,4 +73,23 @@ <%= render(Shipwright::LinkComponent.new(href: "#", decoration: :none)) { "No decoration" } %> <%= render(Shipwright::LinkComponent.new(href: "#", disabled: true)) { "Disabled" } %> + +

Banner

+
+ <%= render Shipwright::BannerComponent.new(header: "Alert") do |banner| %> + <% banner.with_icon(name: :info) %> + <% banner.with_link(href: "#") { "Link" } %> + Place holder text for notifications. Enter text into this container. + <% end %> + + <%= render Shipwright::BannerComponent.new(header: "Success") do |banner| %> + <% banner.with_icon(name: :"check-circle") %> + Your changes were saved. + <% end %> + + <%= render Shipwright::BannerComponent.new do |banner| %> + <% banner.with_link(href: "#") { "Learn more" } %> + A simple banner without an icon or header. + <% end %> +