From f22f726313f7593c70845de3db69862bee313d51 Mon Sep 17 00:00:00 2001 From: Jordan Burke Date: Fri, 17 Apr 2026 13:49:23 -0400 Subject: [PATCH 01/24] feat: add BadgeComponent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches Shipwright Pro's Badge atom (Figma node 2013:5696). Renders as either a colored dot (no content) or a text badge (with content block). New tokens in engine.css (Utility namespace): - --color-utility-neutral-default: #292c39 - --color-utility-information-default: #206ac7 - --color-utility-positive-default: #48a356 - --color-utility-warning-default: #c09c17 - --color-border-inverse: #fdfdfd (for the white badge border) BaseComponent MERGER config updated to recognize the new color tokens so consumer class overrides resolve conflicts correctly. API: <%= render Shipwright::BadgeComponent.new(sentiment: :positive, size: :md) %> <%= render(Shipwright::BadgeComponent.new(sentiment: :informative)) { "20" } %> Params: - sentiment: :neutral (default), :informative, :positive, :negative, :warning - size: :sm (8px dot), :md (12px dot), :lg (20px dot) — default :md - class: consumer override When a content block is present, the badge grows to fit the text with min-width matching height (keeps single-char badges circular) and adds inverse text color. Text badges work at all sizes; Figma only shows :lg but the scaling down is consistent. 15 new tests pass. Total: 40 tests, 81 assertions. Lookbook previews: neutral_dot, all_sentiments, all_sizes, text_badges, count_notifications (showing single/double-digit/overflow cases). --- app/assets/tailwind/shipwright/engine.css | 7 +- app/components/shipwright/badge_component.rb | 53 +++++++++++ app/components/shipwright/base_component.rb | 5 + .../shipwright/badge_component_preview.rb | 28 ++++++ .../all_sentiments.html.erb | 7 ++ .../all_sizes.html.erb | 5 + .../count_notifications.html.erb | 21 +++++ .../text_badges.html.erb | 7 ++ .../shipwright/badge_component_test.rb | 93 +++++++++++++++++++ test/dummy/app/assets/builds/tailwind.css | 2 +- .../dummy/app/views/pages/components.html.erb | 24 +++++ 11 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 app/components/shipwright/badge_component.rb create mode 100644 test/components/previews/shipwright/badge_component_preview.rb create mode 100644 test/components/previews/shipwright/badge_component_preview/all_sentiments.html.erb create mode 100644 test/components/previews/shipwright/badge_component_preview/all_sizes.html.erb create mode 100644 test/components/previews/shipwright/badge_component_preview/count_notifications.html.erb create mode 100644 test/components/previews/shipwright/badge_component_preview/text_badges.html.erb create mode 100644 test/components/shipwright/badge_component_test.rb diff --git a/app/assets/tailwind/shipwright/engine.css b/app/assets/tailwind/shipwright/engine.css index db2420a..c905c64 100644 --- a/app/assets/tailwind/shipwright/engine.css +++ b/app/assets/tailwind/shipwright/engine.css @@ -25,9 +25,14 @@ /* Border colors */ --color-border-interactive: #14161c; + --color-border-inverse: #fdfdfd; - /* Utility colors (destructive, success, warning, info, etc.) */ + /* Utility colors (semantic sentiments) */ + --color-utility-neutral-default: #292c39; + --color-utility-information-default: #206ac7; + --color-utility-positive-default: #48a356; --color-utility-negative-default: #b62e2e; + --color-utility-warning-default: #c09c17; /* Text colors */ --color-text-primary: #050507; diff --git a/app/components/shipwright/badge_component.rb b/app/components/shipwright/badge_component.rb new file mode 100644 index 0000000..2e89d6a --- /dev/null +++ b/app/components/shipwright/badge_component.rb @@ -0,0 +1,53 @@ +module Shipwright + class BadgeComponent < BaseComponent + # Sentiment maps to a utility color token, matching the Figma "Sentiment" variant. + SENTIMENTS = { + neutral: "bg-utility-neutral-default", + informative: "bg-utility-information-default", + positive: "bg-utility-positive-default", + negative: "bg-utility-negative-default", + warning: "bg-utility-warning-default" + }.freeze + + # Dot dimensions (used when no content block is given). Shipwright Pro spec: + # sm = 8px, md = 12px, lg = 20px. + DOT_SIZES = { + sm: "w-2 h-2", + md: "w-3 h-3", + lg: "w-5 h-5" + }.freeze + + # Text badge dimensions (used when a content block is given). Minimum width + # matches the height so single-char badges stay circular; padding lets them + # grow for 2+ chars. Text is always inverse (white on the sentiment color). + TEXT_SIZES = { + sm: "h-4 min-w-4 px-1 text-[10px] leading-none", + md: "h-5 min-w-5 px-1.5 text-xs leading-none", + lg: "h-6 min-w-6 px-2 text-xs leading-none" + }.freeze + + # `class:` is a Ruby reserved word used as a keyword arg — requires + # binding.local_variable_get. Shipwright convention. + def initialize(sentiment: :neutral, size: :md, class: nil, **html_attrs) + @sentiment = sentiment + @size = size + @class = binding.local_variable_get(:class) + @html_attrs = html_attrs + end + + def call + sentiment_bg = SENTIMENTS.fetch(@sentiment) { |s| raise ArgumentError, "#{self.class}: unknown sentiment :#{s}. Valid: #{SENTIMENTS.keys.join(', ')}" } + size_classes = (content.present? ? TEXT_SIZES : DOT_SIZES).fetch(@size) { |v| raise ArgumentError, "#{self.class}: unknown size :#{v}. Valid: #{DOT_SIZES.keys.join(', ')}" } + + content_tag :span, content, + class: classes( + "inline-flex items-center justify-center rounded-full border-2 border-border-inverse font-sans font-normal", + sentiment_bg, + size_classes, + content.present? ? "text-text-inverse" : nil, + consumer: @class + ), + **@html_attrs + end + end +end diff --git a/app/components/shipwright/base_component.rb b/app/components/shipwright/base_component.rb index f75b669..05641a2 100644 --- a/app/components/shipwright/base_component.rb +++ b/app/components/shipwright/base_component.rb @@ -13,7 +13,12 @@ class BaseComponent < ViewComponent::Base interactive-secondary-default interactive-disable border-interactive + border-inverse + utility-neutral-default + utility-information-default + utility-positive-default utility-negative-default + utility-warning-default text-primary text-inverse text-disabled diff --git a/test/components/previews/shipwright/badge_component_preview.rb b/test/components/previews/shipwright/badge_component_preview.rb new file mode 100644 index 0000000..85fb9a2 --- /dev/null +++ b/test/components/previews/shipwright/badge_component_preview.rb @@ -0,0 +1,28 @@ +module Shipwright + class BadgeComponentPreview < Lookbook::Preview + # @label Neutral Dot (default) + def neutral_dot + render Shipwright::BadgeComponent.new + end + + # @label All Sentiments (dots) + def all_sentiments + render_with_template + end + + # @label All Sizes (dots) + def all_sizes + render_with_template + end + + # @label Text Badges + def text_badges + render_with_template + end + + # @label Count Notifications + def count_notifications + render_with_template + end + end +end diff --git a/test/components/previews/shipwright/badge_component_preview/all_sentiments.html.erb b/test/components/previews/shipwright/badge_component_preview/all_sentiments.html.erb new file mode 100644 index 0000000..ccabae9 --- /dev/null +++ b/test/components/previews/shipwright/badge_component_preview/all_sentiments.html.erb @@ -0,0 +1,7 @@ +
+ <%= render Shipwright::BadgeComponent.new(sentiment: :neutral) %> + <%= render Shipwright::BadgeComponent.new(sentiment: :informative) %> + <%= render Shipwright::BadgeComponent.new(sentiment: :positive) %> + <%= render Shipwright::BadgeComponent.new(sentiment: :negative) %> + <%= render Shipwright::BadgeComponent.new(sentiment: :warning) %> +
diff --git a/test/components/previews/shipwright/badge_component_preview/all_sizes.html.erb b/test/components/previews/shipwright/badge_component_preview/all_sizes.html.erb new file mode 100644 index 0000000..7e35b9c --- /dev/null +++ b/test/components/previews/shipwright/badge_component_preview/all_sizes.html.erb @@ -0,0 +1,5 @@ +
+ <%= render Shipwright::BadgeComponent.new(sentiment: :positive, size: :sm) %> + <%= render Shipwright::BadgeComponent.new(sentiment: :positive, size: :md) %> + <%= render Shipwright::BadgeComponent.new(sentiment: :positive, size: :lg) %> +
diff --git a/test/components/previews/shipwright/badge_component_preview/count_notifications.html.erb b/test/components/previews/shipwright/badge_component_preview/count_notifications.html.erb new file mode 100644 index 0000000..19e7527 --- /dev/null +++ b/test/components/previews/shipwright/badge_component_preview/count_notifications.html.erb @@ -0,0 +1,21 @@ +
+
+ Single char + <%= render(Shipwright::BadgeComponent.new(sentiment: :negative, size: :sm)) { "3" } %> + <%= render(Shipwright::BadgeComponent.new(sentiment: :negative, size: :md)) { "3" } %> + <%= render(Shipwright::BadgeComponent.new(sentiment: :negative, size: :lg)) { "3" } %> +
+ +
+ Double digit + <%= render(Shipwright::BadgeComponent.new(sentiment: :informative, size: :sm)) { "99" } %> + <%= render(Shipwright::BadgeComponent.new(sentiment: :informative, size: :md)) { "99" } %> + <%= render(Shipwright::BadgeComponent.new(sentiment: :informative, size: :lg)) { "99" } %> +
+ +
+ Overflow + <%= render(Shipwright::BadgeComponent.new(sentiment: :warning, size: :lg)) { "99+" } %> + <%= render(Shipwright::BadgeComponent.new(sentiment: :positive, size: :lg)) { "NEW" } %> +
+
diff --git a/test/components/previews/shipwright/badge_component_preview/text_badges.html.erb b/test/components/previews/shipwright/badge_component_preview/text_badges.html.erb new file mode 100644 index 0000000..a394476 --- /dev/null +++ b/test/components/previews/shipwright/badge_component_preview/text_badges.html.erb @@ -0,0 +1,7 @@ +
+ <%= render(Shipwright::BadgeComponent.new(sentiment: :neutral, size: :lg)) { "20" } %> + <%= render(Shipwright::BadgeComponent.new(sentiment: :informative, size: :lg)) { "20" } %> + <%= render(Shipwright::BadgeComponent.new(sentiment: :positive, size: :lg)) { "20" } %> + <%= render(Shipwright::BadgeComponent.new(sentiment: :negative, size: :lg)) { "20" } %> + <%= render(Shipwright::BadgeComponent.new(sentiment: :warning, size: :lg)) { "20" } %> +
diff --git a/test/components/shipwright/badge_component_test.rb b/test/components/shipwright/badge_component_test.rb new file mode 100644 index 0000000..b2c81ee --- /dev/null +++ b/test/components/shipwright/badge_component_test.rb @@ -0,0 +1,93 @@ +require "test_helper" + +class Shipwright::BadgeComponentTest < ViewComponent::TestCase + def test_renders_neutral_dot_by_default + render_inline(Shipwright::BadgeComponent.new) + + assert_selector "span.bg-utility-neutral-default" + assert_selector "span.rounded-full" + end + + def test_renders_informative_sentiment + render_inline(Shipwright::BadgeComponent.new(sentiment: :informative)) + assert_selector "span.bg-utility-information-default" + end + + def test_renders_positive_sentiment + render_inline(Shipwright::BadgeComponent.new(sentiment: :positive)) + assert_selector "span.bg-utility-positive-default" + end + + def test_renders_negative_sentiment + render_inline(Shipwright::BadgeComponent.new(sentiment: :negative)) + assert_selector "span.bg-utility-negative-default" + end + + def test_renders_warning_sentiment + render_inline(Shipwright::BadgeComponent.new(sentiment: :warning)) + assert_selector "span.bg-utility-warning-default" + end + + def test_dot_size_small + render_inline(Shipwright::BadgeComponent.new(size: :sm)) + assert_selector "span.w-2.h-2" + end + + def test_dot_size_medium + render_inline(Shipwright::BadgeComponent.new(size: :md)) + assert_selector "span.w-3.h-3" + end + + def test_dot_size_large + render_inline(Shipwright::BadgeComponent.new(size: :lg)) + assert_selector "span.w-5.h-5" + end + + def test_renders_text_badge_when_content_given + render_inline(Shipwright::BadgeComponent.new(sentiment: :informative)) { "20" } + + assert_selector "span", text: "20" + assert_selector "span.text-text-inverse" + # Text badge should not use dot dimensions (no fixed w-3 h-3) + assert_no_selector "span.w-3.h-3" + end + + def test_text_badge_applies_text_size_classes + render_inline(Shipwright::BadgeComponent.new(size: :lg)) { "99" } + + assert_selector "span.h-6" + assert_selector "span.px-2" + end + + def test_consumer_class_overrides_defaults + render_inline(Shipwright::BadgeComponent.new(class: "bg-utility-warning-default")) + + assert_selector "span.bg-utility-warning-default" + assert_no_selector "span.bg-utility-neutral-default" + end + + def test_passes_html_attributes + render_inline(Shipwright::BadgeComponent.new(id: "notif-badge", data: { count: "3" })) { "3" } + + assert_selector "span#notif-badge[data-count='3']" + end + + def test_raises_on_invalid_sentiment + assert_raises(ArgumentError) do + render_inline(Shipwright::BadgeComponent.new(sentiment: :purple)) + end + end + + def test_raises_on_invalid_size + assert_raises(ArgumentError) do + render_inline(Shipwright::BadgeComponent.new(size: :xl)) + end + end + + def test_includes_rounded_full_and_border + render_inline(Shipwright::BadgeComponent.new) + + assert_selector "span.rounded-full" + assert_selector "span.border-2" + end +end diff --git a/test/dummy/app/assets/builds/tailwind.css b/test/dummy/app/assets/builds/tailwind.css index ca8b6d6..f22f13e 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-xs:.75rem;--text-xs--line-height:calc(1 / .75);--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-border-inverse:#fdfdfd;--color-utility-neutral-default:#292c39;--color-utility-information-default:#206ac7;--color-utility-positive-default:#48a356;--color-utility-negative-default:#b62e2e;--color-utility-warning-default:#c09c17;--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-2{height:calc(var(--spacing) * 2)}.h-3{height:calc(var(--spacing) * 3)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.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-2{width:calc(var(--spacing) * 2)}.w-3{width:calc(var(--spacing) * 3)}.w-5{width:calc(var(--spacing) * 5)}.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-4{min-width:calc(var(--spacing) * 4)}.min-w-5{min-width:calc(var(--spacing) * 5)}.min-w-6{min-width:calc(var(--spacing) * 6)}.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-2{border-style:var(--tw-border-style);border-width:2px}.border-border-interactive{border-color:var(--color-border-interactive)}.border-border-inverse{border-color:var(--color-border-inverse)}.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-information-default{background-color:var(--color-utility-information-default)}.bg-utility-negative-default{background-color:var(--color-utility-negative-default)}.bg-utility-neutral-default{background-color:var(--color-utility-neutral-default)}.bg-utility-positive-default{background-color:var(--color-utility-positive-default)}.bg-utility-warning-default{background-color:var(--color-utility-warning-default)}.p-0{padding:calc(var(--spacing) * 0)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.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))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.leading-2xs{--tw-leading:var(--leading-2xs);line-height:var(--leading-2xs)}.leading-none{--tw-leading:1;line-height:1}.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..6c95ba2 100644 --- a/test/dummy/app/views/pages/components.html.erb +++ b/test/dummy/app/views/pages/components.html.erb @@ -49,4 +49,28 @@
<%= render Shipwright::ButtonComponent.new(class: "w-full") do %>Full Width Override<% end %>
+ +

Badge — Sentiments

+
+ <%= render Shipwright::BadgeComponent.new(sentiment: :neutral) %> + <%= render Shipwright::BadgeComponent.new(sentiment: :informative) %> + <%= render Shipwright::BadgeComponent.new(sentiment: :positive) %> + <%= render Shipwright::BadgeComponent.new(sentiment: :negative) %> + <%= render Shipwright::BadgeComponent.new(sentiment: :warning) %> +
+ +

Badge — Sizes

+
+ <%= render Shipwright::BadgeComponent.new(sentiment: :positive, size: :sm) %> + <%= render Shipwright::BadgeComponent.new(sentiment: :positive, size: :md) %> + <%= render Shipwright::BadgeComponent.new(sentiment: :positive, size: :lg) %> +
+ +

Badge — With Text

+
+ <%= render(Shipwright::BadgeComponent.new(sentiment: :negative, size: :lg)) { "3" } %> + <%= render(Shipwright::BadgeComponent.new(sentiment: :informative, size: :lg)) { "20" } %> + <%= render(Shipwright::BadgeComponent.new(sentiment: :warning, size: :lg)) { "99+" } %> + <%= render(Shipwright::BadgeComponent.new(sentiment: :positive, size: :lg)) { "NEW" } %> +
From 92d779b9c8c34db423129133ec3fbb7229052159 Mon Sep 17 00:00:00 2001 From: Jordan Burke Date: Fri, 17 Apr 2026 14:12:06 -0400 Subject: [PATCH 02/24] 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 03/24] 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 04/24] 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 %> +
From d9374b648c85b1a6bff2be0cf54c579bf21c337c Mon Sep 17 00:00:00 2001 From: Jordan Burke Date: Sat, 18 Apr 2026 16:24:49 -0400 Subject: [PATCH 05/24] docs: document the develop integration branch and its maintenance --- DEVELOP.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 DEVELOP.md diff --git a/DEVELOP.md b/DEVELOP.md new file mode 100644 index 0000000..b4c31e8 --- /dev/null +++ b/DEVELOP.md @@ -0,0 +1,52 @@ +# `develop` integration branch + +While individual component PRs are in review, the `develop` branch +aggregates them so that consuming apps can target a single branch to +preview the full in-flight work without juggling multiple PR branches. + +## What's included + +Currently integrates: + +| Branch / PR | Adds | +|-------------|------| +| `fix/lookbook-button-previews` (#1) | Button & ButtonGroup fixes, Shipwright Pro tokens, Manrope, Lookbook block binding fix | +| `feat/badge-component` (#3) | BadgeComponent, utility color palette | +| `feat/icon-component` (#4) | IconComponent + Feather icon manifest | +| `feat/link-component` (#5) | LinkComponent | +| `feat/banner-component` (#6) | BannerComponent (molecule composing Icon + Link) | + +## How to consume + +Point your app's Gemfile at this branch: + +```ruby +gem "shipwright", github: "headwayio/shipwright-rails", branch: "develop" +``` + +## Maintenance + +### When a feature branch merges to `main` + +1. `git checkout develop && git fetch origin && git merge origin/main` +2. Resolve any conflicts (usually `tailwind.css`, `components.html.erb`, + `CHANGELOG.md`) and re-run `cd test/dummy && bundle exec rake tailwindcss:build` +3. `bundle exec rake test` — confirm still green +4. `git push origin develop` + +### When a new feature branch opens + +1. `git checkout develop` +2. `git merge feat/` +3. Resolve conflicts, rebuild Tailwind, run tests +4. Push + +### When develop drifts too far from main + +Once all in-flight PRs are merged, consider deleting and recreating +develop from main so it doesn't accumulate stale merge commits. + +## Not intended for merging to `main` + +`develop` is a preview branch. Don't merge it into `main`. Individual +feature PRs are the source of truth. From 63161a87229e62c687e251a2eef77b73b1ec8030 Mon Sep 17 00:00:00 2001 From: Jordan Burke Date: Sat, 18 Apr 2026 16:48:19 -0400 Subject: [PATCH 06/24] feat(icons): expand to full Feather set (289 icons) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated from feather-icons@4.29.2 icons.json plus two Shipwright additions to match Figma's icon page (node 2:163): - code-horizontal: alias for Feather's 'code' (matches Shipwright Pro's naming convention) - star-filled: filled variant using the star polygon with fill=currentColor Previously only 21 starter icons were shipped. This adds the remaining ~270 to cover every icon on the Shipwright Pro 'Icons / General' page. Brand icons (payment methods at node 2:824 and social icons at 2:940) are multi-color composite SVGs served from Figma's temporary CDN and need a different rendering approach — they'll ship in a follow-up BrandIconComponent PR. Gallery preview updated to render all icons in an 8-column grid. Showcase page shows a curated sample plus pointer to Lookbook for the full set. All 38 existing tests pass (Icon manifest expansion preserves the starter icons we already shipped). --- app/components/shipwright/icons.rb | 319 ++++++++++++++++-- .../icon_component_preview/gallery.html.erb | 20 +- test/dummy/app/assets/builds/tailwind.css | 2 +- .../dummy/app/views/pages/components.html.erb | 7 +- 4 files changed, 314 insertions(+), 34 deletions(-) diff --git a/app/components/shipwright/icons.rb b/app/components/shipwright/icons.rb index 1667596..047a160 100644 --- a/app/components/shipwright/icons.rb +++ b/app/components/shipwright/icons.rb @@ -3,30 +3,301 @@ module Shipwright # 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. + # Full set (287 icons) plus Shipwright additions: + # - code-horizontal: alias for Feather's `code` (matches Figma naming) + # - star-filled: filled variant using the star polygon with fill=currentColor + # + # To add a brand icon (payment/social), use BrandIconComponent instead — + # those are multi-color SVGs that need different rendering. 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 + "activity" => %().html_safe, + "airplay" => %().html_safe, + "alert-circle" => %().html_safe, + "alert-octagon" => %().html_safe, + "alert-triangle" => %().html_safe, + "align-center" => %().html_safe, + "align-justify" => %().html_safe, + "align-left" => %().html_safe, + "align-right" => %().html_safe, + "anchor" => %().html_safe, + "aperture" => %().html_safe, + "archive" => %().html_safe, + "arrow-down" => %().html_safe, + "arrow-down-circle" => %().html_safe, + "arrow-down-left" => %().html_safe, + "arrow-down-right" => %().html_safe, + "arrow-left" => %().html_safe, + "arrow-left-circle" => %().html_safe, + "arrow-right" => %().html_safe, + "arrow-right-circle" => %().html_safe, + "arrow-up" => %().html_safe, + "arrow-up-circle" => %().html_safe, + "arrow-up-left" => %().html_safe, + "arrow-up-right" => %().html_safe, + "at-sign" => %().html_safe, + "award" => %().html_safe, + "bar-chart" => %().html_safe, + "bar-chart-2" => %().html_safe, + "battery" => %().html_safe, + "battery-charging" => %().html_safe, + "bell" => %().html_safe, + "bell-off" => %().html_safe, + "bluetooth" => %().html_safe, + "bold" => %().html_safe, + "book" => %().html_safe, + "book-open" => %().html_safe, + "bookmark" => %().html_safe, + "box" => %().html_safe, + "briefcase" => %().html_safe, + "calendar" => %().html_safe, + "camera" => %().html_safe, + "camera-off" => %().html_safe, + "cast" => %().html_safe, + "check" => %().html_safe, + "check-circle" => %().html_safe, + "check-square" => %().html_safe, + "chevron-down" => %().html_safe, + "chevron-left" => %().html_safe, + "chevron-right" => %().html_safe, + "chevron-up" => %().html_safe, + "chevrons-down" => %().html_safe, + "chevrons-left" => %().html_safe, + "chevrons-right" => %().html_safe, + "chevrons-up" => %().html_safe, + "chrome" => %().html_safe, + "circle" => %().html_safe, + "clipboard" => %().html_safe, + "clock" => %().html_safe, + "cloud" => %().html_safe, + "cloud-drizzle" => %().html_safe, + "cloud-lightning" => %().html_safe, + "cloud-off" => %().html_safe, + "cloud-rain" => %().html_safe, + "cloud-snow" => %().html_safe, + "code" => %().html_safe, + "code-horizontal" => %().html_safe, + "codepen" => %().html_safe, + "codesandbox" => %().html_safe, + "coffee" => %().html_safe, + "columns" => %().html_safe, + "command" => %().html_safe, + "compass" => %().html_safe, + "copy" => %().html_safe, + "corner-down-left" => %().html_safe, + "corner-down-right" => %().html_safe, + "corner-left-down" => %().html_safe, + "corner-left-up" => %().html_safe, + "corner-right-down" => %().html_safe, + "corner-right-up" => %().html_safe, + "corner-up-left" => %().html_safe, + "corner-up-right" => %().html_safe, + "cpu" => %().html_safe, + "credit-card" => %().html_safe, + "crop" => %().html_safe, + "crosshair" => %().html_safe, + "database" => %().html_safe, + "delete" => %().html_safe, + "disc" => %().html_safe, + "divide" => %().html_safe, + "divide-circle" => %().html_safe, + "divide-square" => %().html_safe, + "dollar-sign" => %().html_safe, + "download" => %().html_safe, + "download-cloud" => %().html_safe, + "dribbble" => %().html_safe, + "droplet" => %().html_safe, + "edit" => %().html_safe, + "edit-2" => %().html_safe, + "edit-3" => %().html_safe, + "external-link" => %().html_safe, + "eye" => %().html_safe, + "eye-off" => %().html_safe, + "facebook" => %().html_safe, + "fast-forward" => %().html_safe, + "feather" => %().html_safe, + "figma" => %().html_safe, + "file" => %().html_safe, + "file-minus" => %().html_safe, + "file-plus" => %().html_safe, + "file-text" => %().html_safe, + "film" => %().html_safe, + "filter" => %().html_safe, + "flag" => %().html_safe, + "folder" => %().html_safe, + "folder-minus" => %().html_safe, + "folder-plus" => %().html_safe, + "framer" => %().html_safe, + "frown" => %().html_safe, + "gift" => %().html_safe, + "git-branch" => %().html_safe, + "git-commit" => %().html_safe, + "git-merge" => %().html_safe, + "git-pull-request" => %().html_safe, + "github" => %().html_safe, + "gitlab" => %().html_safe, + "globe" => %().html_safe, + "grid" => %().html_safe, + "hard-drive" => %().html_safe, + "hash" => %().html_safe, + "headphones" => %().html_safe, + "heart" => %().html_safe, + "help-circle" => %().html_safe, + "hexagon" => %().html_safe, + "home" => %().html_safe, + "image" => %().html_safe, + "inbox" => %().html_safe, + "info" => %().html_safe, + "instagram" => %().html_safe, + "italic" => %().html_safe, + "key" => %().html_safe, + "layers" => %().html_safe, + "layout" => %().html_safe, + "life-buoy" => %().html_safe, + "link" => %().html_safe, + "link-2" => %().html_safe, + "linkedin" => %().html_safe, + "list" => %().html_safe, + "loader" => %().html_safe, + "lock" => %().html_safe, + "log-in" => %().html_safe, + "log-out" => %().html_safe, + "mail" => %().html_safe, + "map" => %().html_safe, + "map-pin" => %().html_safe, + "maximize" => %().html_safe, + "maximize-2" => %().html_safe, + "meh" => %().html_safe, + "menu" => %().html_safe, + "message-circle" => %().html_safe, + "message-square" => %().html_safe, + "mic" => %().html_safe, + "mic-off" => %().html_safe, + "minimize" => %().html_safe, + "minimize-2" => %().html_safe, + "minus" => %().html_safe, + "minus-circle" => %().html_safe, + "minus-square" => %().html_safe, + "monitor" => %().html_safe, + "moon" => %().html_safe, + "more-horizontal" => %().html_safe, + "more-vertical" => %().html_safe, + "mouse-pointer" => %().html_safe, + "move" => %().html_safe, + "music" => %().html_safe, + "navigation" => %().html_safe, + "navigation-2" => %().html_safe, + "octagon" => %().html_safe, + "package" => %().html_safe, + "paperclip" => %().html_safe, + "pause" => %().html_safe, + "pause-circle" => %().html_safe, + "pen-tool" => %().html_safe, + "percent" => %().html_safe, + "phone" => %().html_safe, + "phone-call" => %().html_safe, + "phone-forwarded" => %().html_safe, + "phone-incoming" => %().html_safe, + "phone-missed" => %().html_safe, + "phone-off" => %().html_safe, + "phone-outgoing" => %().html_safe, + "pie-chart" => %().html_safe, + "play" => %().html_safe, + "play-circle" => %().html_safe, + "plus" => %().html_safe, + "plus-circle" => %().html_safe, + "plus-square" => %().html_safe, + "pocket" => %().html_safe, + "power" => %().html_safe, + "printer" => %().html_safe, + "radio" => %().html_safe, + "refresh-ccw" => %().html_safe, + "refresh-cw" => %().html_safe, + "repeat" => %().html_safe, + "rewind" => %().html_safe, + "rotate-ccw" => %().html_safe, + "rotate-cw" => %().html_safe, + "rss" => %().html_safe, + "save" => %().html_safe, + "scissors" => %().html_safe, + "search" => %().html_safe, + "send" => %().html_safe, + "server" => %().html_safe, + "settings" => %().html_safe, + "share" => %().html_safe, + "share-2" => %().html_safe, + "shield" => %().html_safe, + "shield-off" => %().html_safe, + "shopping-bag" => %().html_safe, + "shopping-cart" => %().html_safe, + "shuffle" => %().html_safe, + "sidebar" => %().html_safe, + "skip-back" => %().html_safe, + "skip-forward" => %().html_safe, + "slack" => %().html_safe, + "slash" => %().html_safe, + "sliders" => %().html_safe, + "smartphone" => %().html_safe, + "smile" => %().html_safe, + "speaker" => %().html_safe, + "square" => %().html_safe, + "star" => %().html_safe, + "star-filled" => %().html_safe, + "stop-circle" => %().html_safe, + "sun" => %().html_safe, + "sunrise" => %().html_safe, + "sunset" => %().html_safe, + "table" => %().html_safe, + "tablet" => %().html_safe, + "tag" => %().html_safe, + "target" => %().html_safe, + "terminal" => %().html_safe, + "thermometer" => %().html_safe, + "thumbs-down" => %().html_safe, + "thumbs-up" => %().html_safe, + "toggle-left" => %().html_safe, + "toggle-right" => %().html_safe, + "tool" => %().html_safe, + "trash" => %().html_safe, + "trash-2" => %().html_safe, + "trello" => %().html_safe, + "trending-down" => %().html_safe, + "trending-up" => %().html_safe, + "triangle" => %().html_safe, + "truck" => %().html_safe, + "tv" => %().html_safe, + "twitch" => %().html_safe, + "twitter" => %().html_safe, + "type" => %().html_safe, + "umbrella" => %().html_safe, + "underline" => %().html_safe, + "unlock" => %().html_safe, + "upload" => %().html_safe, + "upload-cloud" => %().html_safe, + "user" => %().html_safe, + "user-check" => %().html_safe, + "user-minus" => %().html_safe, + "user-plus" => %().html_safe, + "user-x" => %().html_safe, + "users" => %().html_safe, + "video" => %().html_safe, + "video-off" => %().html_safe, + "voicemail" => %().html_safe, + "volume" => %().html_safe, + "volume-1" => %().html_safe, + "volume-2" => %().html_safe, + "volume-x" => %().html_safe, + "watch" => %().html_safe, + "wifi" => %().html_safe, + "wifi-off" => %().html_safe, + "wind" => %().html_safe, + "x" => %().html_safe, + "x-circle" => %().html_safe, + "x-octagon" => %().html_safe, + "x-square" => %().html_safe, + "youtube" => %().html_safe, + "zap" => %().html_safe, + "zap-off" => %().html_safe, + "zoom-in" => %().html_safe, + "zoom-out" => %().html_safe, }.freeze end diff --git a/test/components/previews/shipwright/icon_component_preview/gallery.html.erb b/test/components/previews/shipwright/icon_component_preview/gallery.html.erb index d754621..daefa2b 100644 --- a/test/components/previews/shipwright/icon_component_preview/gallery.html.erb +++ b/test/components/previews/shipwright/icon_component_preview/gallery.html.erb @@ -1,8 +1,14 @@ -
- <% Shipwright::IconComponent.icon_names.each do |name| %> -
- <%= render Shipwright::IconComponent.new(name: name) %> - <%= name %> -
- <% end %> +
+

+ <%= Shipwright::IconComponent.icon_names.size %> icons available. Feather Icons (MIT) + plus Shipwright additions (code-horizontal, star-filled). +

+
+ <% Shipwright::IconComponent.icon_names.each do |name| %> +
+ <%= render Shipwright::IconComponent.new(name: name) %> + <%= name %> +
+ <% end %> +
diff --git a/test/dummy/app/assets/builds/tailwind.css b/test/dummy/app/assets/builds/tailwind.css index 120a09b..51f3d63 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-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 +@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;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@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)}.grid{display:grid}.inline-flex{display:inline-flex}.table{display:table}.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)}.italic{font-style:italic}.underline{text-decoration-line:underline}.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)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-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}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false} \ 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 2ae52ca..1a6be39 100644 --- a/test/dummy/app/views/pages/components.html.erb +++ b/test/dummy/app/views/pages/components.html.erb @@ -50,9 +50,12 @@ <%= render Shipwright::ButtonComponent.new(class: "w-full") do %>Full Width Override<% end %>
-

Icons

+

Icons — sample (full gallery in Lookbook)

+

+ <%= Shipwright::IconComponent.icon_names.size %> icons from Feather Icons (MIT). +

- <% Shipwright::IconComponent.icon_names.first(10).each do |name| %> + <% [:info, :"alert-circle", :"alert-triangle", :check, :x, :"chevron-down", :plus, :search, :menu, :settings, :heart, :star, :"star-filled"].each do |name| %>
<%= render Shipwright::IconComponent.new(name: name) %> <%= name %> From 6838929b14933e7178f56ec8a260fa67db206f6e Mon Sep 17 00:00:00 2001 From: Jordan Burke Date: Sat, 18 Apr 2026 16:57:05 -0400 Subject: [PATCH 07/24] feat: add BrandIconComponent with Figma SVG exporter Brand icons (payment methods + social platforms) from Shipwright Pro's Figma are multi-color composite SVGs that need different rendering than Feather's monochrome stroke icons. Added as a parallel component: <%= render Shipwright::BrandIconComponent.new(name: :visa-color) %> Architecture: - SVGs live in app/assets/images/shipwright/brand/ (one file per icon) - Component loads and memoizes the directory on first access - Renders SVG inline (preserves brand colors, allows CSS styling) - Strips intrinsic width/height, applies h-* class + w-auto so non-square icons (e.g., Visa) keep their aspect ratio - Raises a helpful ArgumentError directing users to the exporter if they reference a missing icon New files: - script/export_brand_icons.rb: downloads all 59 brand SVGs from Shipwright Pro Figma via the Figma REST API. Maps every node ID from the Figma Icons/Payment Method and Icons/Social pages to a normalized kebab-case filename. Requires FIGMA_ACCESS_TOKEN env var. - app/components/shipwright/brand_icon_component.rb: the component. - test fixtures at test/fixtures/brand_icons/ so tests don't need Figma access. BrandIconComponent.asset_path is swappable per-test. - 4 Lookbook previews including an empty_state scenario explaining how to run the exporter. 14 new component tests pass. Total suite: 52 tests, 111 assertions. Usage: FIGMA_ACCESS_TOKEN=xxx ruby script/export_brand_icons.rb # Review, commit the SVGs. 59 icons covering: # Payment: amex, visa, mastercard, applepay, paypal, discover, # cash, cash-dollar, card-default (color/fill/outline) # Social: facebook, instagram, youtube, google, linkedin, apple, # snapchat, pinterest, medium, angelist, slack, dribbble, # figma, discord, clubhouse, tumblr, telegram, tiktok, # vk, signal, reddit, github, fb-messenger, skype, # spectrum, zoom, facetime, google-meet, behance, # invision, microsoft --- .../shipwright/brand_icon_component.rb | 113 ++++++++++++ script/export_brand_icons.rb | 161 ++++++++++++++++++ .../brand_icon_component_preview.rb | 23 +++ .../empty_state.html.erb | 13 ++ .../gallery.html.erb | 20 +++ .../rounded.html.erb | 9 + .../sizes.html.erb | 10 ++ .../shipwright/brand_icon_component_test.rb | 108 ++++++++++++ test/dummy/app/assets/builds/tailwind.css | 2 +- test/fixtures/brand_icons/placeholder.svg | 1 + test/fixtures/brand_icons/wide-brand.svg | 1 + 11 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 app/components/shipwright/brand_icon_component.rb create mode 100755 script/export_brand_icons.rb create mode 100644 test/components/previews/shipwright/brand_icon_component_preview.rb create mode 100644 test/components/previews/shipwright/brand_icon_component_preview/empty_state.html.erb create mode 100644 test/components/previews/shipwright/brand_icon_component_preview/gallery.html.erb create mode 100644 test/components/previews/shipwright/brand_icon_component_preview/rounded.html.erb create mode 100644 test/components/previews/shipwright/brand_icon_component_preview/sizes.html.erb create mode 100644 test/components/shipwright/brand_icon_component_test.rb create mode 100644 test/fixtures/brand_icons/placeholder.svg create mode 100644 test/fixtures/brand_icons/wide-brand.svg diff --git a/app/components/shipwright/brand_icon_component.rb b/app/components/shipwright/brand_icon_component.rb new file mode 100644 index 0000000..02cb287 --- /dev/null +++ b/app/components/shipwright/brand_icon_component.rb @@ -0,0 +1,113 @@ +module Shipwright + # Renders a multi-color brand SVG (payment method, social platform, etc.) + # exported from Shipwright Pro's Figma source. + # + # Brand icons differ from Feather icons (rendered by IconComponent) in + # that they preserve the brand's own colors rather than inheriting via + # currentColor. The component loads SVGs from a directory, strips their + # intrinsic width/height, and applies a Tailwind height class — width + # auto-scales so non-square icons (e.g., Visa) keep their aspect ratio. + # + # Populate the SVG directory by running: + # + # FIGMA_ACCESS_TOKEN=xxx ruby script/export_brand_icons.rb + # + # Available names are discoverable via `BrandIconComponent.icon_names`. + class BrandIconComponent < BaseComponent + # Default SVG directory — overridable for tests via the asset_path class setting. + DEFAULT_ASSET_PATH = Pathname.new(__dir__).join("../../assets/images/shipwright/brand").expand_path.freeze + + SIZES = { + sm: "h-4", # 16px + md: "h-6", # 24px + lg: "h-8" # 32px + }.freeze + + class << self + attr_writer :asset_path + + def asset_path + @asset_path ||= DEFAULT_ASSET_PATH + end + + # Scan the asset directory and return a hash of name => inline SVG string. + # Memoized per-asset-path so tests can swap paths cleanly. + def icons + @icons ||= {} + @icons[asset_path.to_s] ||= begin + pattern = asset_path.join("*.svg") + Dir.glob(pattern).each_with_object({}) do |file, hash| + name = File.basename(file, ".svg") + hash[name] = File.read(file) + end.freeze + end + end + + def icon_names + icons.keys.map(&:to_sym).sort.freeze + end + + # Allow tests to reset the memoization. + def reset_cache! + @icons = nil + 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 + raw_svg = self.class.icons.fetch(@name) do + available = self.class.icon_names + hint = if available.empty? + " No brand icons loaded — run `ruby script/export_brand_icons.rb` with a FIGMA_ACCESS_TOKEN to populate app/assets/images/shipwright/brand/." + else + " Available: #{available.join(', ')}" + end + raise ArgumentError, "#{self.class}: unknown brand icon :#{@name}.#{hint}" + end + + size_class = SIZES.fetch(@size) do |v| + raise ArgumentError, "#{self.class}: unknown size :#{v}. Valid: #{SIZES.keys.join(', ')}" + end + + # Strip intrinsic dimensions so CSS controls size; w-auto preserves aspect ratio. + normalized = raw_svg + .sub(/\swidth="[^"]*"/, "") + .sub(/\sheight="[^"]*"/, "") + + merged_class = classes("#{size_class} w-auto", consumer: @class) + + # Inject / merge our class attribute into the root . + if normalized =~ /]*)\sclass="([^"]*)"/ + normalized = normalized.sub(/]*)\sclass="([^"]*)"/) do + pre = Regexp.last_match(1) + existing = Regexp.last_match(2) + %(. + attr_str = @html_attrs.map { |k, v| %(#{k.to_s.tr('_', '-')}="#{ERB::Util.html_escape(v)}") }.join(" ") + if !attr_str.empty? + normalized = normalized.sub(" "amex-color", + "2:832" => "cash-dollar-color", + "2:836" => "paypal-color", + "2:845" => "applepay-color", + "2:849" => "visa-color", + "2:852" => "discover-color", + "2:864" => "cash-color", + "2:868" => "card-default-color", + "2:872" => "mastercard-color", + + # Payment / fill variants + "2:879" => "amex-fill", + "2:883" => "cash-dollar-fill", + "2:886" => "paypal-fill", + "2:889" => "cash-fill", + "2:892" => "applepay-fill", + "2:895" => "visa-fill", + "2:898" => "discover-fill", + "2:901" => "card-default-fill", + "2:905" => "mastercard-fill", + + # Payment / outline variants + "2:910" => "amex-outline", + "2:914" => "cash-dollar-outline", + "2:917" => "paypal-outline", + "2:920" => "applepay-outline", + "2:923" => "visa-outline", + "2:926" => "cash-outline", + "2:929" => "card-default-outline", + "2:933" => "discover-outline", + "2:936" => "mastercard-outline", + + # Social / main row + "2:944" => "facebook-color", + "2:947" => "youtube-color", + "2:950" => "instagram-color", + "2:955" => "google-color", + "2:960" => "linkedin-color", + "2:964" => "apple-color", + "2:966" => "snapchat-color", + "2:969" => "pinterest-color", + "2:972" => "medium-color", + "2:976" => "angelist-color", + "2:1019" => "slack-color", + + # Social / second row + "2:979" => "dribbble-color", + "2:984" => "figma-color", + "2:990" => "discord-color", + "2:994" => "clubhouse-color", + "2:997" => "tumblr-color", + "2:1000" => "telegram-color", + "2:1004" => "tiktok-color", + "2:1008" => "vk-color", + "2:1011" => "signal-color", + "2:1013" => "reddit-color", + "2:1016" => "github-color", + + # Social / tools row + "2:1025" => "fb-messenger-color", + "2:1028" => "skype-color", + "2:1034" => "spectrum-color", + "2:1037" => "zoom-color", + "2:1043" => "facetime-color", + "2:1046" => "google-meet-color", + "2:1057" => "behance-color", + "2:1060" => "invision-color", + "2:1063" => "apple-brand", + "2:1066" => "microsoft-color" +}.freeze + +def die(msg) + warn msg + exit 1 +end + +token = ENV["FIGMA_ACCESS_TOKEN"] || die("Set FIGMA_ACCESS_TOKEN (https://www.figma.com/settings)") + +FileUtils.mkdir_p(OUTPUT_DIR) + +# Figma's image endpoint supports up to ~1000 node IDs per call, but rate +# limits encourage batching. We'll batch in groups of 20. +def fetch_image_urls(token, file_key, ids) + uri = URI("https://api.figma.com/v1/images/#{file_key}?ids=#{ids.join(',')}&format=svg") + req = Net::HTTP::Get.new(uri) + req["X-Figma-Token"] = token + + res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } + raise "Figma API error: #{res.code} #{res.body}" unless res.is_a?(Net::HTTPSuccess) + + body = JSON.parse(res.body) + if body["err"] + raise "Figma returned err: #{body['err']}" + end + body["images"] +end + +def download(url) + uri = URI(url) + res = Net::HTTP.get_response(uri) + raise "Download failed: #{res.code} #{url}" unless res.is_a?(Net::HTTPSuccess) + + res.body +end + +ids = NODES.keys +puts "Fetching #{ids.size} brand icon export URLs from Figma..." + +urls = {} +ids.each_slice(20) do |batch| + urls.merge!(fetch_image_urls(token, FILE_KEY, batch)) + sleep 0.25 # polite throttle +end + +missing = ids.reject { |id| urls[id] } +unless missing.empty? + warn "Missing export URLs for: #{missing.map { |id| "#{id} (#{NODES[id]})" }.join(', ')}" +end + +downloaded = 0 +NODES.each do |node_id, filename| + url = urls[node_id] + next unless url + + svg = download(url) + path = File.join(OUTPUT_DIR, "#{filename}.svg") + File.write(path, svg) + downloaded += 1 + puts " wrote #{filename}.svg" +end + +puts "Done. Wrote #{downloaded}/#{NODES.size} SVGs to #{OUTPUT_DIR}" +puts "Review, then `git add app/assets/images/shipwright/brand && git commit`." diff --git a/test/components/previews/shipwright/brand_icon_component_preview.rb b/test/components/previews/shipwright/brand_icon_component_preview.rb new file mode 100644 index 0000000..4faf874 --- /dev/null +++ b/test/components/previews/shipwright/brand_icon_component_preview.rb @@ -0,0 +1,23 @@ +module Shipwright + class BrandIconComponentPreview < Lookbook::Preview + # @label Gallery (all installed brand icons) + def gallery + render_with_template + end + + # @label Sizes + def sizes + render_with_template + end + + # @label Rounded + def rounded + render_with_template + end + + # @label Empty state (no Figma export yet) + def empty_state + render_with_template + end + end +end diff --git a/test/components/previews/shipwright/brand_icon_component_preview/empty_state.html.erb b/test/components/previews/shipwright/brand_icon_component_preview/empty_state.html.erb new file mode 100644 index 0000000..e68e489 --- /dev/null +++ b/test/components/previews/shipwright/brand_icon_component_preview/empty_state.html.erb @@ -0,0 +1,13 @@ +
+

Brand icons are populated by a one-time export from Figma.

+

+ Run the exporter with a Figma personal access token + (get one here): +

+
FIGMA_ACCESS_TOKEN=xxx ruby script/export_brand_icons.rb
+

+ SVGs are written to app/assets/images/shipwright/brand/ and loaded automatically + on next app boot. Rendering without a matching SVG raises ArgumentError with the + full list of available icons. +

+
diff --git a/test/components/previews/shipwright/brand_icon_component_preview/gallery.html.erb b/test/components/previews/shipwright/brand_icon_component_preview/gallery.html.erb new file mode 100644 index 0000000..ab8d68f --- /dev/null +++ b/test/components/previews/shipwright/brand_icon_component_preview/gallery.html.erb @@ -0,0 +1,20 @@ +<% names = Shipwright::BrandIconComponent.icon_names %> +
+

+ <% if names.any? %> + <%= names.size %> brand icons installed. Payment + social icons exported from Shipwright Pro Figma. + <% else %> + No brand icons installed yet. Run + ruby script/export_brand_icons.rb + with a FIGMA_ACCESS_TOKEN to populate. + <% end %> +

+
+ <% names.each do |name| %> +
+ <%= render Shipwright::BrandIconComponent.new(name: name) %> + <%= name %> +
+ <% end %> +
+
diff --git a/test/components/previews/shipwright/brand_icon_component_preview/rounded.html.erb b/test/components/previews/shipwright/brand_icon_component_preview/rounded.html.erb new file mode 100644 index 0000000..b82c07b --- /dev/null +++ b/test/components/previews/shipwright/brand_icon_component_preview/rounded.html.erb @@ -0,0 +1,9 @@ +<% name = Shipwright::BrandIconComponent.icon_names.first %> +<% if name %> +
+ <%= render Shipwright::BrandIconComponent.new(name: name, size: :lg, class: "rounded-md overflow-hidden") %> + <%= render Shipwright::BrandIconComponent.new(name: name, size: :lg, class: "rounded-full overflow-hidden") %> +
+<% else %> +

No brand icons installed — run the exporter.

+<% end %> diff --git a/test/components/previews/shipwright/brand_icon_component_preview/sizes.html.erb b/test/components/previews/shipwright/brand_icon_component_preview/sizes.html.erb new file mode 100644 index 0000000..3026980 --- /dev/null +++ b/test/components/previews/shipwright/brand_icon_component_preview/sizes.html.erb @@ -0,0 +1,10 @@ +<% name = Shipwright::BrandIconComponent.icon_names.first %> +<% if name %> +
+ <%= render Shipwright::BrandIconComponent.new(name: name, size: :sm) %> + <%= render Shipwright::BrandIconComponent.new(name: name, size: :md) %> + <%= render Shipwright::BrandIconComponent.new(name: name, size: :lg) %> +
+<% else %> +

No brand icons installed — run the exporter.

+<% end %> diff --git a/test/components/shipwright/brand_icon_component_test.rb b/test/components/shipwright/brand_icon_component_test.rb new file mode 100644 index 0000000..03f965d --- /dev/null +++ b/test/components/shipwright/brand_icon_component_test.rb @@ -0,0 +1,108 @@ +require "test_helper" + +class Shipwright::BrandIconComponentTest < ViewComponent::TestCase + setup do + Shipwright::BrandIconComponent.asset_path = Pathname.new(__dir__).join("../../fixtures/brand_icons").expand_path + Shipwright::BrandIconComponent.reset_cache! + end + + teardown do + Shipwright::BrandIconComponent.asset_path = Shipwright::BrandIconComponent::DEFAULT_ASSET_PATH + Shipwright::BrandIconComponent.reset_cache! + end + + def test_renders_svg_from_fixture + render_inline(Shipwright::BrandIconComponent.new(name: :placeholder)) + + assert_selector "svg" + assert_selector "svg rect[data-testid='placeholder-rect']" + end + + def test_default_size_is_h_6 + render_inline(Shipwright::BrandIconComponent.new(name: :placeholder)) + + assert_selector "svg.h-6" + end + + def test_small_size_is_h_4 + render_inline(Shipwright::BrandIconComponent.new(name: :placeholder, size: :sm)) + + assert_selector "svg.h-4" + end + + def test_large_size_is_h_8 + render_inline(Shipwright::BrandIconComponent.new(name: :placeholder, size: :lg)) + + assert_selector "svg.h-8" + end + + def test_width_auto_preserves_aspect_ratio + render_inline(Shipwright::BrandIconComponent.new(name: :"wide-brand")) + + assert_selector "svg.w-auto" + end + + def test_intrinsic_width_and_height_are_stripped + render_inline(Shipwright::BrandIconComponent.new(name: :placeholder)) + + html = rendered_content + refute_match(/]+\swidth="38"/, html) + refute_match(/]+\sheight="38"/, html) + end + + def test_brand_colors_preserved + render_inline(Shipwright::BrandIconComponent.new(name: :placeholder)) + + assert_includes rendered_content, "#206ac7" + end + + def test_aria_hidden_by_default + render_inline(Shipwright::BrandIconComponent.new(name: :placeholder)) + + assert_selector "svg[aria-hidden='true']" + end + + def test_aria_label_removes_aria_hidden + render_inline(Shipwright::BrandIconComponent.new(name: :placeholder, "aria-label": "Brand logo")) + + assert_selector "svg[aria-label='Brand logo']" + assert_no_selector "svg[aria-hidden='true']" + end + + def test_consumer_class_merges + render_inline(Shipwright::BrandIconComponent.new(name: :placeholder, class: "rounded-full")) + + assert_selector "svg.rounded-full" + assert_selector "svg.h-6" + end + + def test_raises_on_unknown_icon + err = assert_raises(ArgumentError) do + render_inline(Shipwright::BrandIconComponent.new(name: :"does-not-exist")) + end + assert_includes err.message, "unknown brand icon" + end + + def test_raises_on_invalid_size + assert_raises(ArgumentError) do + render_inline(Shipwright::BrandIconComponent.new(name: :placeholder, size: :xl)) + end + end + + def test_icon_names_lists_fixtures + names = Shipwright::BrandIconComponent.icon_names + assert_includes names, :placeholder + assert_includes names, :"wide-brand" + end + + def test_empty_directory_raises_with_helpful_hint + Shipwright::BrandIconComponent.asset_path = Pathname.new(Dir.mktmpdir) + Shipwright::BrandIconComponent.reset_cache! + + err = assert_raises(ArgumentError) do + render_inline(Shipwright::BrandIconComponent.new(name: :anything)) + end + assert_includes err.message, "No brand icons loaded" + assert_includes err.message, "script/export_brand_icons.rb" + end +end diff --git a/test/dummy/app/assets/builds/tailwind.css b/test/dummy/app/assets/builds/tailwind.css index 51f3d63..28fe205 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;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@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)}.grid{display:grid}.inline-flex{display:inline-flex}.table{display:table}.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)}.italic{font-style:italic}.underline{text-decoration-line:underline}.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)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-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}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false} \ 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;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@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)}.grid{display:grid}.inline-flex{display:inline-flex}.table{display:table}.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-auto{width:auto}.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)}.italic{font-style:italic}.underline{text-decoration-line:underline}.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)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-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}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false} \ No newline at end of file diff --git a/test/fixtures/brand_icons/placeholder.svg b/test/fixtures/brand_icons/placeholder.svg new file mode 100644 index 0000000..734cc58 --- /dev/null +++ b/test/fixtures/brand_icons/placeholder.svg @@ -0,0 +1 @@ +TEST diff --git a/test/fixtures/brand_icons/wide-brand.svg b/test/fixtures/brand_icons/wide-brand.svg new file mode 100644 index 0000000..9c58641 --- /dev/null +++ b/test/fixtures/brand_icons/wide-brand.svg @@ -0,0 +1 @@ + From 12dce194b2817223d147c875302a220d11e32cd1 Mon Sep 17 00:00:00 2001 From: Jordan Burke Date: Sat, 18 Apr 2026 17:01:06 -0400 Subject: [PATCH 08/24] chore: export Figma brand icons --- .../images/shipwright/brand/amex-color.svg | 5 +++++ .../images/shipwright/brand/amex-fill.svg | 5 +++++ .../images/shipwright/brand/amex-outline.svg | 5 +++++ .../shipwright/brand/angelist-color.svg | 3 +++ .../images/shipwright/brand/apple-brand.svg | 4 ++++ .../images/shipwright/brand/apple-color.svg | 10 ++++++++++ .../shipwright/brand/applepay-color.svg | 5 +++++ .../images/shipwright/brand/applepay-fill.svg | 4 ++++ .../shipwright/brand/applepay-outline.svg | 4 ++++ .../images/shipwright/brand/behance-color.svg | 6 ++++++ .../shipwright/brand/card-default-color.svg | 5 +++++ .../shipwright/brand/card-default-fill.svg | 5 +++++ .../shipwright/brand/card-default-outline.svg | 5 +++++ .../images/shipwright/brand/cash-color.svg | 5 +++++ .../shipwright/brand/cash-dollar-color.svg | 5 +++++ .../shipwright/brand/cash-dollar-fill.svg | 4 ++++ .../shipwright/brand/cash-dollar-outline.svg | 4 ++++ .../images/shipwright/brand/cash-fill.svg | 4 ++++ .../images/shipwright/brand/cash-outline.svg | 4 ++++ .../shipwright/brand/clubhouse-color.svg | 4 ++++ .../images/shipwright/brand/discord-color.svg | 4 ++++ .../shipwright/brand/discover-color.svg | 20 +++++++++++++++++++ .../images/shipwright/brand/discover-fill.svg | 4 ++++ .../shipwright/brand/discover-outline.svg | 4 ++++ .../shipwright/brand/dribbble-color.svg | 4 ++++ .../shipwright/brand/facebook-color.svg | 11 ++++++++++ .../shipwright/brand/facetime-color.svg | 11 ++++++++++ .../shipwright/brand/fb-messenger-color.svg | 10 ++++++++++ .../images/shipwright/brand/figma-color.svg | 14 +++++++++++++ .../images/shipwright/brand/github-color.svg | 10 ++++++++++ .../images/shipwright/brand/google-color.svg | 13 ++++++++++++ .../shipwright/brand/google-meet-color.svg | 11 ++++++++++ .../shipwright/brand/instagram-color.svg | 12 +++++++++++ .../shipwright/brand/invision-color.svg | 5 +++++ .../shipwright/brand/linkedin-color.svg | 10 ++++++++++ .../shipwright/brand/mastercard-color.svg | 7 +++++++ .../shipwright/brand/mastercard-fill.svg | 5 +++++ .../shipwright/brand/mastercard-outline.svg | 5 +++++ .../images/shipwright/brand/medium-color.svg | 12 +++++++++++ .../shipwright/brand/microsoft-color.svg | 6 ++++++ .../images/shipwright/brand/paypal-color.svg | 10 ++++++++++ .../images/shipwright/brand/paypal-fill.svg | 4 ++++ .../shipwright/brand/paypal-outline.svg | 4 ++++ .../shipwright/brand/pinterest-color.svg | 11 ++++++++++ .../images/shipwright/brand/reddit-color.svg | 11 ++++++++++ .../images/shipwright/brand/signal-color.svg | 3 +++ .../images/shipwright/brand/skype-color.svg | 12 +++++++++++ .../images/shipwright/brand/slack-color.svg | 6 ++++++ .../shipwright/brand/snapchat-color.svg | 4 ++++ .../shipwright/brand/spectrum-color.svg | 11 ++++++++++ .../shipwright/brand/telegram-color.svg | 15 ++++++++++++++ .../images/shipwright/brand/tiktok-color.svg | 5 +++++ .../images/shipwright/brand/tumblr-color.svg | 10 ++++++++++ .../images/shipwright/brand/visa-color.svg | 4 ++++ .../images/shipwright/brand/visa-fill.svg | 4 ++++ .../images/shipwright/brand/visa-outline.svg | 4 ++++ .../images/shipwright/brand/vk-color.svg | 11 ++++++++++ .../images/shipwright/brand/youtube-color.svg | 11 ++++++++++ .../images/shipwright/brand/zoom-color.svg | 5 +++++ 59 files changed, 419 insertions(+) create mode 100644 app/assets/images/shipwright/brand/amex-color.svg create mode 100644 app/assets/images/shipwright/brand/amex-fill.svg create mode 100644 app/assets/images/shipwright/brand/amex-outline.svg create mode 100644 app/assets/images/shipwright/brand/angelist-color.svg create mode 100644 app/assets/images/shipwright/brand/apple-brand.svg create mode 100644 app/assets/images/shipwright/brand/apple-color.svg create mode 100644 app/assets/images/shipwright/brand/applepay-color.svg create mode 100644 app/assets/images/shipwright/brand/applepay-fill.svg create mode 100644 app/assets/images/shipwright/brand/applepay-outline.svg create mode 100644 app/assets/images/shipwright/brand/behance-color.svg create mode 100644 app/assets/images/shipwright/brand/card-default-color.svg create mode 100644 app/assets/images/shipwright/brand/card-default-fill.svg create mode 100644 app/assets/images/shipwright/brand/card-default-outline.svg create mode 100644 app/assets/images/shipwright/brand/cash-color.svg create mode 100644 app/assets/images/shipwright/brand/cash-dollar-color.svg create mode 100644 app/assets/images/shipwright/brand/cash-dollar-fill.svg create mode 100644 app/assets/images/shipwright/brand/cash-dollar-outline.svg create mode 100644 app/assets/images/shipwright/brand/cash-fill.svg create mode 100644 app/assets/images/shipwright/brand/cash-outline.svg create mode 100644 app/assets/images/shipwright/brand/clubhouse-color.svg create mode 100644 app/assets/images/shipwright/brand/discord-color.svg create mode 100644 app/assets/images/shipwright/brand/discover-color.svg create mode 100644 app/assets/images/shipwright/brand/discover-fill.svg create mode 100644 app/assets/images/shipwright/brand/discover-outline.svg create mode 100644 app/assets/images/shipwright/brand/dribbble-color.svg create mode 100644 app/assets/images/shipwright/brand/facebook-color.svg create mode 100644 app/assets/images/shipwright/brand/facetime-color.svg create mode 100644 app/assets/images/shipwright/brand/fb-messenger-color.svg create mode 100644 app/assets/images/shipwright/brand/figma-color.svg create mode 100644 app/assets/images/shipwright/brand/github-color.svg create mode 100644 app/assets/images/shipwright/brand/google-color.svg create mode 100644 app/assets/images/shipwright/brand/google-meet-color.svg create mode 100644 app/assets/images/shipwright/brand/instagram-color.svg create mode 100644 app/assets/images/shipwright/brand/invision-color.svg create mode 100644 app/assets/images/shipwright/brand/linkedin-color.svg create mode 100644 app/assets/images/shipwright/brand/mastercard-color.svg create mode 100644 app/assets/images/shipwright/brand/mastercard-fill.svg create mode 100644 app/assets/images/shipwright/brand/mastercard-outline.svg create mode 100644 app/assets/images/shipwright/brand/medium-color.svg create mode 100644 app/assets/images/shipwright/brand/microsoft-color.svg create mode 100644 app/assets/images/shipwright/brand/paypal-color.svg create mode 100644 app/assets/images/shipwright/brand/paypal-fill.svg create mode 100644 app/assets/images/shipwright/brand/paypal-outline.svg create mode 100644 app/assets/images/shipwright/brand/pinterest-color.svg create mode 100644 app/assets/images/shipwright/brand/reddit-color.svg create mode 100644 app/assets/images/shipwright/brand/signal-color.svg create mode 100644 app/assets/images/shipwright/brand/skype-color.svg create mode 100644 app/assets/images/shipwright/brand/slack-color.svg create mode 100644 app/assets/images/shipwright/brand/snapchat-color.svg create mode 100644 app/assets/images/shipwright/brand/spectrum-color.svg create mode 100644 app/assets/images/shipwright/brand/telegram-color.svg create mode 100644 app/assets/images/shipwright/brand/tiktok-color.svg create mode 100644 app/assets/images/shipwright/brand/tumblr-color.svg create mode 100644 app/assets/images/shipwright/brand/visa-color.svg create mode 100644 app/assets/images/shipwright/brand/visa-fill.svg create mode 100644 app/assets/images/shipwright/brand/visa-outline.svg create mode 100644 app/assets/images/shipwright/brand/vk-color.svg create mode 100644 app/assets/images/shipwright/brand/youtube-color.svg create mode 100644 app/assets/images/shipwright/brand/zoom-color.svg diff --git a/app/assets/images/shipwright/brand/amex-color.svg b/app/assets/images/shipwright/brand/amex-color.svg new file mode 100644 index 0000000..47aa690 --- /dev/null +++ b/app/assets/images/shipwright/brand/amex-color.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/shipwright/brand/amex-fill.svg b/app/assets/images/shipwright/brand/amex-fill.svg new file mode 100644 index 0000000..3ed0495 --- /dev/null +++ b/app/assets/images/shipwright/brand/amex-fill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/shipwright/brand/amex-outline.svg b/app/assets/images/shipwright/brand/amex-outline.svg new file mode 100644 index 0000000..ebfe612 --- /dev/null +++ b/app/assets/images/shipwright/brand/amex-outline.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/shipwright/brand/angelist-color.svg b/app/assets/images/shipwright/brand/angelist-color.svg new file mode 100644 index 0000000..39e2e43 --- /dev/null +++ b/app/assets/images/shipwright/brand/angelist-color.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/shipwright/brand/apple-brand.svg b/app/assets/images/shipwright/brand/apple-brand.svg new file mode 100644 index 0000000..93612e7 --- /dev/null +++ b/app/assets/images/shipwright/brand/apple-brand.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/apple-color.svg b/app/assets/images/shipwright/brand/apple-color.svg new file mode 100644 index 0000000..bceb30a --- /dev/null +++ b/app/assets/images/shipwright/brand/apple-color.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/applepay-color.svg b/app/assets/images/shipwright/brand/applepay-color.svg new file mode 100644 index 0000000..0b59326 --- /dev/null +++ b/app/assets/images/shipwright/brand/applepay-color.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/shipwright/brand/applepay-fill.svg b/app/assets/images/shipwright/brand/applepay-fill.svg new file mode 100644 index 0000000..118b498 --- /dev/null +++ b/app/assets/images/shipwright/brand/applepay-fill.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/applepay-outline.svg b/app/assets/images/shipwright/brand/applepay-outline.svg new file mode 100644 index 0000000..a16c6fb --- /dev/null +++ b/app/assets/images/shipwright/brand/applepay-outline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/behance-color.svg b/app/assets/images/shipwright/brand/behance-color.svg new file mode 100644 index 0000000..1036872 --- /dev/null +++ b/app/assets/images/shipwright/brand/behance-color.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/assets/images/shipwright/brand/card-default-color.svg b/app/assets/images/shipwright/brand/card-default-color.svg new file mode 100644 index 0000000..c9ec0b1 --- /dev/null +++ b/app/assets/images/shipwright/brand/card-default-color.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/shipwright/brand/card-default-fill.svg b/app/assets/images/shipwright/brand/card-default-fill.svg new file mode 100644 index 0000000..1eec40b --- /dev/null +++ b/app/assets/images/shipwright/brand/card-default-fill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/shipwright/brand/card-default-outline.svg b/app/assets/images/shipwright/brand/card-default-outline.svg new file mode 100644 index 0000000..84da788 --- /dev/null +++ b/app/assets/images/shipwright/brand/card-default-outline.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/shipwright/brand/cash-color.svg b/app/assets/images/shipwright/brand/cash-color.svg new file mode 100644 index 0000000..2405fed --- /dev/null +++ b/app/assets/images/shipwright/brand/cash-color.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/shipwright/brand/cash-dollar-color.svg b/app/assets/images/shipwright/brand/cash-dollar-color.svg new file mode 100644 index 0000000..aadeff0 --- /dev/null +++ b/app/assets/images/shipwright/brand/cash-dollar-color.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/shipwright/brand/cash-dollar-fill.svg b/app/assets/images/shipwright/brand/cash-dollar-fill.svg new file mode 100644 index 0000000..f053084 --- /dev/null +++ b/app/assets/images/shipwright/brand/cash-dollar-fill.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/cash-dollar-outline.svg b/app/assets/images/shipwright/brand/cash-dollar-outline.svg new file mode 100644 index 0000000..c7cf2b6 --- /dev/null +++ b/app/assets/images/shipwright/brand/cash-dollar-outline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/cash-fill.svg b/app/assets/images/shipwright/brand/cash-fill.svg new file mode 100644 index 0000000..d900850 --- /dev/null +++ b/app/assets/images/shipwright/brand/cash-fill.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/cash-outline.svg b/app/assets/images/shipwright/brand/cash-outline.svg new file mode 100644 index 0000000..317ad71 --- /dev/null +++ b/app/assets/images/shipwright/brand/cash-outline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/clubhouse-color.svg b/app/assets/images/shipwright/brand/clubhouse-color.svg new file mode 100644 index 0000000..f53daa3 --- /dev/null +++ b/app/assets/images/shipwright/brand/clubhouse-color.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/discord-color.svg b/app/assets/images/shipwright/brand/discord-color.svg new file mode 100644 index 0000000..797e589 --- /dev/null +++ b/app/assets/images/shipwright/brand/discord-color.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/discover-color.svg b/app/assets/images/shipwright/brand/discover-color.svg new file mode 100644 index 0000000..80b45a7 --- /dev/null +++ b/app/assets/images/shipwright/brand/discover-color.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/discover-fill.svg b/app/assets/images/shipwright/brand/discover-fill.svg new file mode 100644 index 0000000..771748c --- /dev/null +++ b/app/assets/images/shipwright/brand/discover-fill.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/discover-outline.svg b/app/assets/images/shipwright/brand/discover-outline.svg new file mode 100644 index 0000000..b4aa2d5 --- /dev/null +++ b/app/assets/images/shipwright/brand/discover-outline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/dribbble-color.svg b/app/assets/images/shipwright/brand/dribbble-color.svg new file mode 100644 index 0000000..fa0fc2c --- /dev/null +++ b/app/assets/images/shipwright/brand/dribbble-color.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/facebook-color.svg b/app/assets/images/shipwright/brand/facebook-color.svg new file mode 100644 index 0000000..3da9c6c --- /dev/null +++ b/app/assets/images/shipwright/brand/facebook-color.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/facetime-color.svg b/app/assets/images/shipwright/brand/facetime-color.svg new file mode 100644 index 0000000..f16775e --- /dev/null +++ b/app/assets/images/shipwright/brand/facetime-color.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/fb-messenger-color.svg b/app/assets/images/shipwright/brand/fb-messenger-color.svg new file mode 100644 index 0000000..b2a636a --- /dev/null +++ b/app/assets/images/shipwright/brand/fb-messenger-color.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/figma-color.svg b/app/assets/images/shipwright/brand/figma-color.svg new file mode 100644 index 0000000..38c186d --- /dev/null +++ b/app/assets/images/shipwright/brand/figma-color.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/github-color.svg b/app/assets/images/shipwright/brand/github-color.svg new file mode 100644 index 0000000..cd9f38e --- /dev/null +++ b/app/assets/images/shipwright/brand/github-color.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/google-color.svg b/app/assets/images/shipwright/brand/google-color.svg new file mode 100644 index 0000000..5f1bea3 --- /dev/null +++ b/app/assets/images/shipwright/brand/google-color.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/google-meet-color.svg b/app/assets/images/shipwright/brand/google-meet-color.svg new file mode 100644 index 0000000..bd2287c --- /dev/null +++ b/app/assets/images/shipwright/brand/google-meet-color.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/instagram-color.svg b/app/assets/images/shipwright/brand/instagram-color.svg new file mode 100644 index 0000000..3db71f2 --- /dev/null +++ b/app/assets/images/shipwright/brand/instagram-color.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/invision-color.svg b/app/assets/images/shipwright/brand/invision-color.svg new file mode 100644 index 0000000..68d9325 --- /dev/null +++ b/app/assets/images/shipwright/brand/invision-color.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/shipwright/brand/linkedin-color.svg b/app/assets/images/shipwright/brand/linkedin-color.svg new file mode 100644 index 0000000..1078f0a --- /dev/null +++ b/app/assets/images/shipwright/brand/linkedin-color.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/mastercard-color.svg b/app/assets/images/shipwright/brand/mastercard-color.svg new file mode 100644 index 0000000..0cae20a --- /dev/null +++ b/app/assets/images/shipwright/brand/mastercard-color.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/assets/images/shipwright/brand/mastercard-fill.svg b/app/assets/images/shipwright/brand/mastercard-fill.svg new file mode 100644 index 0000000..92c17a5 --- /dev/null +++ b/app/assets/images/shipwright/brand/mastercard-fill.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/shipwright/brand/mastercard-outline.svg b/app/assets/images/shipwright/brand/mastercard-outline.svg new file mode 100644 index 0000000..337e869 --- /dev/null +++ b/app/assets/images/shipwright/brand/mastercard-outline.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/shipwright/brand/medium-color.svg b/app/assets/images/shipwright/brand/medium-color.svg new file mode 100644 index 0000000..05f2209 --- /dev/null +++ b/app/assets/images/shipwright/brand/medium-color.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/microsoft-color.svg b/app/assets/images/shipwright/brand/microsoft-color.svg new file mode 100644 index 0000000..707d27d --- /dev/null +++ b/app/assets/images/shipwright/brand/microsoft-color.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/assets/images/shipwright/brand/paypal-color.svg b/app/assets/images/shipwright/brand/paypal-color.svg new file mode 100644 index 0000000..402774b --- /dev/null +++ b/app/assets/images/shipwright/brand/paypal-color.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/paypal-fill.svg b/app/assets/images/shipwright/brand/paypal-fill.svg new file mode 100644 index 0000000..b465a35 --- /dev/null +++ b/app/assets/images/shipwright/brand/paypal-fill.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/paypal-outline.svg b/app/assets/images/shipwright/brand/paypal-outline.svg new file mode 100644 index 0000000..792e8d5 --- /dev/null +++ b/app/assets/images/shipwright/brand/paypal-outline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/pinterest-color.svg b/app/assets/images/shipwright/brand/pinterest-color.svg new file mode 100644 index 0000000..ff3bd6f --- /dev/null +++ b/app/assets/images/shipwright/brand/pinterest-color.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/reddit-color.svg b/app/assets/images/shipwright/brand/reddit-color.svg new file mode 100644 index 0000000..66a71ea --- /dev/null +++ b/app/assets/images/shipwright/brand/reddit-color.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/signal-color.svg b/app/assets/images/shipwright/brand/signal-color.svg new file mode 100644 index 0000000..aff7d0e --- /dev/null +++ b/app/assets/images/shipwright/brand/signal-color.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/images/shipwright/brand/skype-color.svg b/app/assets/images/shipwright/brand/skype-color.svg new file mode 100644 index 0000000..95daa93 --- /dev/null +++ b/app/assets/images/shipwright/brand/skype-color.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/slack-color.svg b/app/assets/images/shipwright/brand/slack-color.svg new file mode 100644 index 0000000..3c918b4 --- /dev/null +++ b/app/assets/images/shipwright/brand/slack-color.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/assets/images/shipwright/brand/snapchat-color.svg b/app/assets/images/shipwright/brand/snapchat-color.svg new file mode 100644 index 0000000..92c23c7 --- /dev/null +++ b/app/assets/images/shipwright/brand/snapchat-color.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/spectrum-color.svg b/app/assets/images/shipwright/brand/spectrum-color.svg new file mode 100644 index 0000000..6c5507e --- /dev/null +++ b/app/assets/images/shipwright/brand/spectrum-color.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/telegram-color.svg b/app/assets/images/shipwright/brand/telegram-color.svg new file mode 100644 index 0000000..308f764 --- /dev/null +++ b/app/assets/images/shipwright/brand/telegram-color.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/tiktok-color.svg b/app/assets/images/shipwright/brand/tiktok-color.svg new file mode 100644 index 0000000..d55530e --- /dev/null +++ b/app/assets/images/shipwright/brand/tiktok-color.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/images/shipwright/brand/tumblr-color.svg b/app/assets/images/shipwright/brand/tumblr-color.svg new file mode 100644 index 0000000..2f650fb --- /dev/null +++ b/app/assets/images/shipwright/brand/tumblr-color.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/visa-color.svg b/app/assets/images/shipwright/brand/visa-color.svg new file mode 100644 index 0000000..853f844 --- /dev/null +++ b/app/assets/images/shipwright/brand/visa-color.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/visa-fill.svg b/app/assets/images/shipwright/brand/visa-fill.svg new file mode 100644 index 0000000..cb44bca --- /dev/null +++ b/app/assets/images/shipwright/brand/visa-fill.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/visa-outline.svg b/app/assets/images/shipwright/brand/visa-outline.svg new file mode 100644 index 0000000..99c5324 --- /dev/null +++ b/app/assets/images/shipwright/brand/visa-outline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/images/shipwright/brand/vk-color.svg b/app/assets/images/shipwright/brand/vk-color.svg new file mode 100644 index 0000000..126cd63 --- /dev/null +++ b/app/assets/images/shipwright/brand/vk-color.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/youtube-color.svg b/app/assets/images/shipwright/brand/youtube-color.svg new file mode 100644 index 0000000..2e365fe --- /dev/null +++ b/app/assets/images/shipwright/brand/youtube-color.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/assets/images/shipwright/brand/zoom-color.svg b/app/assets/images/shipwright/brand/zoom-color.svg new file mode 100644 index 0000000..6e1a2b5 --- /dev/null +++ b/app/assets/images/shipwright/brand/zoom-color.svg @@ -0,0 +1,5 @@ + + + + + From 2fe9721979c7c7e77d96a709435c772d9cc40a6e Mon Sep 17 00:00:00 2001 From: Jordan Burke Date: Sat, 18 Apr 2026 17:26:53 -0400 Subject: [PATCH 09/24] feat: add AccordionComponent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single collapsible section using native HTML
/ — no JavaScript required. Stack multiple AccordionComponents for an FAQ-style group; native semantics handle toggle, keyboard navigation, and accessibility out of the box. API: <%= render(Shipwright::AccordionComponent.new(title: "FAQ question")) do %> Answer content here. <% end %> <%# Start expanded %> <%= render(Shipwright::AccordionComponent.new(title: "T", open: true)) { "..." } %> Props: - title: required string rendered in the summary - open: bool (default false) — sets the
attribute - class: consumer override on the
element - **html_attrs: pass-through (id, data-*, etc.) Content block → body text rendered below the summary. Styling matches Shipwright Pro Figma (node 2013:5693): - Default: text-interactive-primary-default, border-border-primary - Hover: bg-background-secondary, text-interactive-primary-hover - Focus-visible: 2px ring on interactive-primary-default - Chevron icon (IconComponent name: :chevron-down) rotates 180deg when open via Tailwind's group-open: modifier — works natively with
, no JS state management - Default summary marker hidden cross-browser via list-none + ::-webkit-details-marker New tokens: - --color-border-primary: #eaeaea (subtle gray borders, matches Shipwright Pro Border/Primary) - --color-background-secondary: #f3f3f3 (hover surface, matches Shipwright Pro Interactive/Secondary-Hover) BaseComponent MERGER config updated with both new color tokens. Uses IconComponent for the chevron (branch includes the icon-component merge for that dependency). 12 new tests pass. Total suite: 64 tests, 141 assertions. Lookbook previews: default, initially_open, stacked_group, long_body. --- app/assets/tailwind/shipwright/engine.css | 2 + .../shipwright/accordion_component.html.erb | 9 ++ .../shipwright/accordion_component.rb | 57 ++++++++++++ app/components/shipwright/base_component.rb | 2 + .../shipwright/accordion_component_preview.rb | 28 ++++++ .../long_body.html.erb | 18 ++++ .../stacked_group.html.erb | 11 +++ .../shipwright/accordion_component_test.rb | 91 +++++++++++++++++++ test/dummy/app/assets/builds/tailwind.css | 2 +- .../dummy/app/views/pages/components.html.erb | 13 +++ 10 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 app/components/shipwright/accordion_component.html.erb create mode 100644 app/components/shipwright/accordion_component.rb create mode 100644 test/components/previews/shipwright/accordion_component_preview.rb create mode 100644 test/components/previews/shipwright/accordion_component_preview/long_body.html.erb create mode 100644 test/components/previews/shipwright/accordion_component_preview/stacked_group.html.erb create mode 100644 test/components/shipwright/accordion_component_test.rb diff --git a/app/assets/tailwind/shipwright/engine.css b/app/assets/tailwind/shipwright/engine.css index db2420a..9648386 100644 --- a/app/assets/tailwind/shipwright/engine.css +++ b/app/assets/tailwind/shipwright/engine.css @@ -25,6 +25,7 @@ /* Border colors */ --color-border-interactive: #14161c; + --color-border-primary: #eaeaea; /* Utility colors (destructive, success, warning, info, etc.) */ --color-utility-negative-default: #b62e2e; @@ -36,6 +37,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/accordion_component.html.erb b/app/components/shipwright/accordion_component.html.erb new file mode 100644 index 0000000..c673980 --- /dev/null +++ b/app/components/shipwright/accordion_component.html.erb @@ -0,0 +1,9 @@ +<%= content_tag :details, class: container_classes, open: open_attr, **html_attrs do %> + + <%= render Shipwright::IconComponent.new(name: :"chevron-down", size: :sm, class: chevron_classes) %> + <%= title %> + +
+ <%= content %> +
+<% end %> diff --git a/app/components/shipwright/accordion_component.rb b/app/components/shipwright/accordion_component.rb new file mode 100644 index 0000000..558339f --- /dev/null +++ b/app/components/shipwright/accordion_component.rb @@ -0,0 +1,57 @@ +module Shipwright + # A single collapsible section. Uses native
/ for + # toggle behavior, keyboard support, and accessibility — no JavaScript + # required. Stack multiple AccordionComponents to build a group. + # + # Usage: + # <%= render(Shipwright::AccordionComponent.new(title: "FAQ question")) do %> + # Answer content here. + # <% end %> + # + # <%# Initially open %> + # <%= render(Shipwright::AccordionComponent.new(title: "T", open: true)) { "..." } %> + class AccordionComponent < BaseComponent + # `class:` is a Ruby reserved word used as a keyword arg — requires + # binding.local_variable_get. Shipwright convention. + def initialize(title:, open: false, class: nil, **html_attrs) + @title = title + @open = open + @class = binding.local_variable_get(:class) + @html_attrs = html_attrs + end + + attr_reader :title, :html_attrs + + def open_attr + @open + end + + def container_classes + classes( + "group block border border-border-primary rounded-md overflow-hidden font-sans", + consumer: @class + ) + end + + # Classes applied to . Hides the default marker cross-browser, + # adds padding, typography, and interactive state visuals. + def summary_classes + [ + "flex items-center gap-3 px-4 py-3 cursor-pointer select-none", + "text-base leading-5 text-interactive-primary-default", + "transition-colors", + "hover:bg-background-secondary hover:text-interactive-primary-hover", + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-interactive-primary-default focus-visible:ring-inset", + "list-none [&::-webkit-details-marker]:hidden" + ].join(" ") + end + + def body_classes + "px-4 pb-3 pt-0 text-sm leading-5 text-text-primary" + end + + def chevron_classes + "transition-transform duration-200 group-open:rotate-180" + end + end +end diff --git a/app/components/shipwright/base_component.rb b/app/components/shipwright/base_component.rb index f75b669..5fd9263 100644 --- a/app/components/shipwright/base_component.rb +++ b/app/components/shipwright/base_component.rb @@ -13,11 +13,13 @@ class BaseComponent < ViewComponent::Base interactive-secondary-default interactive-disable border-interactive + border-primary utility-negative-default text-primary text-inverse text-disabled background-primary + background-secondary ], "radius" => %w[md] } diff --git a/test/components/previews/shipwright/accordion_component_preview.rb b/test/components/previews/shipwright/accordion_component_preview.rb new file mode 100644 index 0000000..8ff9ac7 --- /dev/null +++ b/test/components/previews/shipwright/accordion_component_preview.rb @@ -0,0 +1,28 @@ +module Shipwright + class AccordionComponentPreview < Lookbook::Preview + # @label Default (closed) + def default + render(Shipwright::AccordionComponent.new(title: "Accordion title")) do + "Lorem ipsum dolor sit amet consectetur. Eget aenean eu in mattis ultrices tellus arcu. " \ + "Sed et turpis volutpat tristique risus. Elementum id interdum tortor faucibus ut." + end + end + + # @label Initially open + def initially_open + render(Shipwright::AccordionComponent.new(title: "Accordion title", open: true)) do + "Starts expanded. Great for the first item in an FAQ." + end + end + + # @label Stacked group + def stacked_group + render_with_template + end + + # @label Long body + def long_body + render_with_template + end + end +end diff --git a/test/components/previews/shipwright/accordion_component_preview/long_body.html.erb b/test/components/previews/shipwright/accordion_component_preview/long_body.html.erb new file mode 100644 index 0000000..eac456f --- /dev/null +++ b/test/components/previews/shipwright/accordion_component_preview/long_body.html.erb @@ -0,0 +1,18 @@ +
+ <%= render(Shipwright::AccordionComponent.new(title: "Terms and conditions", open: true)) do %> +

+ Lorem ipsum dolor sit amet consectetur. Eget aenean eu in mattis ultrices tellus arcu. + Sed et turpis volutpat tristique risus. Elementum id interdum tortor faucibus ut. + Maecenas eu pellentesque etiam vel. +

+

+ Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip + ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit + esse cillum dolore eu fugiat nulla pariatur. +

+

+ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt + mollit anim id est laborum. +

+ <% end %> +
diff --git a/test/components/previews/shipwright/accordion_component_preview/stacked_group.html.erb b/test/components/previews/shipwright/accordion_component_preview/stacked_group.html.erb new file mode 100644 index 0000000..4acf638 --- /dev/null +++ b/test/components/previews/shipwright/accordion_component_preview/stacked_group.html.erb @@ -0,0 +1,11 @@ +
+ <%= render(Shipwright::AccordionComponent.new(title: "What is Shipwright?")) do %> + A Rails ViewComponent library implementing the Shipwright Pro design system. + <% end %> + <%= render(Shipwright::AccordionComponent.new(title: "How do I install it?")) do %> + Add it to your Gemfile from the private repo and follow the setup instructions in the README. + <% end %> + <%= render(Shipwright::AccordionComponent.new(title: "Does it work with React Native?")) do %> + No — this library is Rails-only. See the sister project shipwright-ui for React + React Native. + <% end %> +
diff --git a/test/components/shipwright/accordion_component_test.rb b/test/components/shipwright/accordion_component_test.rb new file mode 100644 index 0000000..d58c6f3 --- /dev/null +++ b/test/components/shipwright/accordion_component_test.rb @@ -0,0 +1,91 @@ +require "test_helper" + +class Shipwright::AccordionComponentTest < ViewComponent::TestCase + def test_renders_title_and_body + render_inline(Shipwright::AccordionComponent.new(title: "What is it?")) { "A body." } + + assert_selector "details" + assert_selector "summary", text: "What is it?" + assert_text "A body." + end + + def test_closed_by_default + render_inline(Shipwright::AccordionComponent.new(title: "T")) { "B" } + + assert_no_selector "details[open]" + end + + def test_open_param_renders_as_open + render_inline(Shipwright::AccordionComponent.new(title: "T", open: true)) { "B" } + + assert_selector "details[open]" + end + + def test_chevron_icon_rendered + render_inline(Shipwright::AccordionComponent.new(title: "T")) { "B" } + + # Uses IconComponent, so an will appear inside + assert_selector "summary svg" + end + + def test_chevron_rotates_when_open + render_inline(Shipwright::AccordionComponent.new(title: "T", open: true)) { "B" } + + html = rendered_content + assert_includes html, "group-open:rotate-180" + end + + def test_summary_has_border_and_padding_classes + render_inline(Shipwright::AccordionComponent.new(title: "T")) { "B" } + + html = rendered_content + assert_includes html, "border-border-primary" + assert_includes html, "px-4" + assert_includes html, "py-3" + end + + def test_hover_and_focus_classes_present + render_inline(Shipwright::AccordionComponent.new(title: "T")) { "B" } + + html = rendered_content + assert_includes html, "hover:bg-background-secondary" + assert_includes html, "focus-visible:" + end + + def test_summary_hides_default_marker + render_inline(Shipwright::AccordionComponent.new(title: "T")) { "B" } + + html = rendered_content + assert_includes html, "list-none" + assert_includes html, "::-webkit-details-marker" + end + + def test_consumer_class_merges + render_inline(Shipwright::AccordionComponent.new(title: "T", class: "shadow-lg")) { "B" } + + assert_selector "details.shadow-lg" + end + + def test_passes_html_attributes_to_details + render_inline(Shipwright::AccordionComponent.new(title: "T", id: "faq-1", data: { section: "faq" })) { "B" } + + assert_selector "details#faq-1[data-section='faq']" + end + + def test_typography_applied_to_title + render_inline(Shipwright::AccordionComponent.new(title: "Title")) { "B" } + + html = rendered_content + # Title should have Manrope + base font size + assert_includes html, "text-base" + assert_includes html, "font-sans" + end + + def test_body_rendered_below_summary + render_inline(Shipwright::AccordionComponent.new(title: "T", open: true)) { "Body contents here." } + + # Body should be a sibling of summary inside details + assert_selector "details > summary" + assert_selector "details > div", text: "Body contents here." + end +end diff --git a/test/dummy/app/assets/builds/tailwind.css b/test/dummy/app/assets/builds/tailwind.css index 28fe205..673f1eb 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;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@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)}.grid{display:grid}.inline-flex{display:inline-flex}.table{display:table}.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-auto{width:auto}.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)}.italic{font-style:italic}.underline{text-decoration-line:underline}.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)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-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}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false} \ 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;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@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-border-primary:#eaeaea;--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)}.block{display:block}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.table{display:table}.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-auto{width:auto}.w-full{width:100%}.flex-1{flex:1}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2xs{gap:var(--spacing-2xs)}.gap-3{gap:calc(var(--spacing) * 3)}.overflow-hidden{overflow:hidden}.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)}.border-border-primary{border-color:var(--color-border-primary)}.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-4{padding-inline:calc(var(--spacing) * 4)}.px-sm{padding-inline:var(--spacing-sm)}.py-3{padding-block:calc(var(--spacing) * 3)}.pt-0{padding-top:calc(var(--spacing) * 0)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.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-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-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)}.italic{font-style:italic}.underline{text-decoration-line:underline}.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)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-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))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.select-none{-webkit-user-select:none;user-select:none}.group-open\:rotate-180:is(:where(.group):is([open],:popover-open,:open) *){rotate:180deg}@media (hover:hover){.hover\:bg-background-secondary:hover{background-color:var(--color-background-secondary)}.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}.focus-visible\:ring-inset:focus-visible{--tw-ring-inset:inset}.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)}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}}@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}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false} \ 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 1a6be39..56f11d0 100644 --- a/test/dummy/app/views/pages/components.html.erb +++ b/test/dummy/app/views/pages/components.html.erb @@ -69,4 +69,17 @@ <%= render Shipwright::IconComponent.new(name: :info, size: :md) %> <%= render Shipwright::IconComponent.new(name: :info, size: :lg) %>
+ +

Accordion

+
+ <%= render(Shipwright::AccordionComponent.new(title: "What is Shipwright?")) do %> + A Rails ViewComponent library implementing the Shipwright Pro design system. + <% end %> + <%= render(Shipwright::AccordionComponent.new(title: "How do I install it?", open: true)) do %> + Add it to your Gemfile and follow the setup instructions in the README. + <% end %> + <%= render(Shipwright::AccordionComponent.new(title: "Does it work with React Native?")) do %> + No — this is a Rails-only library. See shipwright-ui for React + React Native. + <% end %> +
From 98b3ec5ef771e0e5040437a27a54ffac436f9e1f Mon Sep 17 00:00:00 2001 From: Jordan Burke Date: Sat, 18 Apr 2026 17:46:42 -0400 Subject: [PATCH 10/24] feat(accordion): add CSS transition + body top padding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Animates open/close via the native
element's ::details-content pseudo-element, combined with interpolate-size: allow-keywords to enable transitioning to/from height: auto. No JavaScript required. - [&::details-content]:h-0 when collapsed, h-auto when [open] - 200ms transition on height + content-visibility (the latter needs transition-behavior allow-discrete which Tailwind 4 handles via the content-visibility transition entry) - Chevron rotation timing already matches at 200ms Body gets pt-2 (8px) for breathing room below the summary — matches Shipwright Pro's spacing scale. Browser support: Chromium 129+, Safari 18.2+ (late 2024). Firefox gracefully falls back to instant toggle until interpolate-size ships. No a11y impact —
still works natively on all browsers. --- app/components/shipwright/accordion_component.rb | 12 +++++++++++- test/dummy/app/assets/builds/tailwind.css | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/components/shipwright/accordion_component.rb b/app/components/shipwright/accordion_component.rb index 558339f..2048980 100644 --- a/app/components/shipwright/accordion_component.rb +++ b/app/components/shipwright/accordion_component.rb @@ -29,6 +29,14 @@ def open_attr def container_classes classes( "group block border border-border-primary rounded-md overflow-hidden font-sans", + # Height transition for open/close. + # Relies on interpolate-size + the ::details-content pseudo-element + # to animate from height 0 ↔ auto without JS measurement. + # Browsers without support (older Firefox) fall back to instant toggle. + "[interpolate-size:allow-keywords]", + "[&::details-content]:h-0 [&::details-content]:overflow-hidden", + "[&::details-content]:transition-[height,content-visibility] [&::details-content]:duration-200 [&::details-content]:ease-out", + "[&[open]::details-content]:h-auto", consumer: @class ) end @@ -47,7 +55,9 @@ def summary_classes end def body_classes - "px-4 pb-3 pt-0 text-sm leading-5 text-text-primary" + # pt-2 (8px / spacing-2xs) gives the body a small breathing gap below + # the summary's bottom padding. Matches Shipwright Pro's spacing scale. + "px-4 pb-3 pt-2 text-sm leading-5 text-text-primary" end def chevron_classes diff --git a/test/dummy/app/assets/builds/tailwind.css b/test/dummy/app/assets/builds/tailwind.css index 673f1eb..a96fad7 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;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@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-border-primary:#eaeaea;--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)}.block{display:block}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.table{display:table}.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-auto{width:auto}.w-full{width:100%}.flex-1{flex:1}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2xs{gap:var(--spacing-2xs)}.gap-3{gap:calc(var(--spacing) * 3)}.overflow-hidden{overflow:hidden}.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)}.border-border-primary{border-color:var(--color-border-primary)}.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-4{padding-inline:calc(var(--spacing) * 4)}.px-sm{padding-inline:var(--spacing-sm)}.py-3{padding-block:calc(var(--spacing) * 3)}.pt-0{padding-top:calc(var(--spacing) * 0)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.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-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-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)}.italic{font-style:italic}.underline{text-decoration-line:underline}.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)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-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))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.select-none{-webkit-user-select:none;user-select:none}.group-open\:rotate-180:is(:where(.group):is([open],:popover-open,:open) *){rotate:180deg}@media (hover:hover){.hover\:bg-background-secondary:hover{background-color:var(--color-background-secondary)}.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}.focus-visible\:ring-inset:focus-visible{--tw-ring-inset:inset}.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)}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}}@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}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false} \ 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;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial}}}@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;--ease-out:cubic-bezier(0, 0, .2, 1);--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-border-primary:#eaeaea;--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)}.block{display:block}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.table{display:table}.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-auto{width:auto}.w-full{width:100%}.flex-1{flex:1}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.list-none{list-style-type:none}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2xs{gap:var(--spacing-2xs)}.gap-3{gap:calc(var(--spacing) * 3)}.overflow-hidden{overflow:hidden}.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)}.border-border-primary{border-color:var(--color-border-primary)}.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-4{padding-inline:calc(var(--spacing) * 4)}.px-sm{padding-inline:var(--spacing-sm)}.py-3{padding-block:calc(var(--spacing) * 3)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.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-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-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)}.italic{font-style:italic}.underline{text-decoration-line:underline}.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)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-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))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.select-none{-webkit-user-select:none;user-select:none}.\[interpolate-size\:allow-keywords\]{interpolate-size:allow-keywords}.group-open\:rotate-180:is(:where(.group):is([open],:popover-open,:open) *){rotate:180deg}@media (hover:hover){.hover\:bg-background-secondary:hover{background-color:var(--color-background-secondary)}.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}.focus-visible\:ring-inset:focus-visible{--tw-ring-inset:inset}.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)}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}.\[\&\:\:details-content\]\:h-0::details-content{height:calc(var(--spacing) * 0)}.\[\&\:\:details-content\]\:overflow-hidden::details-content{overflow:hidden}.\[\&\:\:details-content\]\:transition-\[height\,content-visibility\]::details-content{transition-property:height,content-visibility;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.\[\&\:\:details-content\]\:duration-200::details-content{--tw-duration:.2s;transition-duration:.2s}.\[\&\:\:details-content\]\:ease-out::details-content{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.\[\&\[open\]\:\:details-content\]\:h-auto[open]::details-content{height:auto}}@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}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false} \ No newline at end of file From 4372f8291b43f0943682b868bec5e2805553a265 Mon Sep 17 00:00:00 2001 From: Jordan Burke Date: Sat, 18 Apr 2026 18:00:51 -0400 Subject: [PATCH 11/24] feat: add AvatarComponent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches Shipwright Pro's Avatar atom (Figma node 2013:5694). Renders as image, initials, or user-icon fallback with an optional status indicator dot in the bottom-right corner. API: <%= render Shipwright::AvatarComponent.new(src: "/john.jpg", alt: "John Legend") %> <%= render Shipwright::AvatarComponent.new(initials: "WS") %> <%= render Shipwright::AvatarComponent.new(alt: "Anonymous") %> # user icon fallback <%= render Shipwright::AvatarComponent.new(src: ..., alt: ..., status: :online) %> Props: - src: optional image URL (alt required when given) - alt: accessible name (required with src; used as aria-label on fallbacks) - initials: optional string — auto-derives 1-2 char uppercase initials ("Wendy Sullivan" → "WS"). Falls back to using the string as-is if already <= 2 chars. - size: :xxsm (16), :xsm (24), :sm (32), :md (40), :lg (56). Default :md. - status: :online, :away, :busy, :offline — optional indicator dot mapped to utility color tokens (positive/warning/negative/neutral) - class: consumer override - **html_attrs: pass-through Content priority: src > initials > user icon. New tokens (Shipwright Pro utility palette): - --color-utility-neutral-default: #292c39 - --color-utility-positive-default: #48a356 - --color-utility-warning-default: #c09c17 - --color-border-inverse: #fdfdfd (for the status dot's ring against the avatar) BaseComponent MERGER config updated for all four. 21 new tests pass. Total suite: 73 tests, 137 assertions. Lookbook: image, initials, icon_fallback, sizes, content_types, status, with_meta. --- app/assets/tailwind/shipwright/engine.css | 6 +- app/components/shipwright/avatar_component.rb | 131 ++++++++++++++++++ app/components/shipwright/base_component.rb | 4 + .../shipwright/avatar_component_preview.rb | 41 ++++++ .../content_types.html.erb | 10 ++ .../avatar_component_preview/sizes.html.erb | 7 + .../avatar_component_preview/status.html.erb | 8 ++ .../with_meta.html.erb | 11 ++ .../shipwright/avatar_component_test.rb | 129 +++++++++++++++++ test/dummy/app/assets/builds/tailwind.css | 2 +- .../dummy/app/views/pages/components.html.erb | 24 ++++ 11 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 app/components/shipwright/avatar_component.rb create mode 100644 test/components/previews/shipwright/avatar_component_preview.rb create mode 100644 test/components/previews/shipwright/avatar_component_preview/content_types.html.erb create mode 100644 test/components/previews/shipwright/avatar_component_preview/sizes.html.erb create mode 100644 test/components/previews/shipwright/avatar_component_preview/status.html.erb create mode 100644 test/components/previews/shipwright/avatar_component_preview/with_meta.html.erb create mode 100644 test/components/shipwright/avatar_component_test.rb diff --git a/app/assets/tailwind/shipwright/engine.css b/app/assets/tailwind/shipwright/engine.css index db2420a..c88a3ec 100644 --- a/app/assets/tailwind/shipwright/engine.css +++ b/app/assets/tailwind/shipwright/engine.css @@ -25,9 +25,13 @@ /* Border colors */ --color-border-interactive: #14161c; + --color-border-inverse: #fdfdfd; - /* Utility colors (destructive, success, warning, info, etc.) */ + /* Utility colors (semantic sentiments) */ + --color-utility-neutral-default: #292c39; + --color-utility-positive-default: #48a356; --color-utility-negative-default: #b62e2e; + --color-utility-warning-default: #c09c17; /* Text colors */ --color-text-primary: #050507; diff --git a/app/components/shipwright/avatar_component.rb b/app/components/shipwright/avatar_component.rb new file mode 100644 index 0000000..e4d3e52 --- /dev/null +++ b/app/components/shipwright/avatar_component.rb @@ -0,0 +1,131 @@ +module Shipwright + # A user avatar — image, initials, or icon fallback — with optional + # status indicator dot. Matches Shipwright Pro's Avatar atom + # (Figma node 2013:5694). + # + # Usage: + # <%= render Shipwright::AvatarComponent.new(src: "/john.jpg", alt: "John Legend") %> + # <%= render Shipwright::AvatarComponent.new(initials: "WS") %> + # <%= render Shipwright::AvatarComponent.new %> # user icon fallback + # <%= render Shipwright::AvatarComponent.new(src: ..., alt: ..., status: :online) %> + class AvatarComponent < BaseComponent + # Size tokens matching Shipwright Pro's five-step scale. + SIZES = { + xxsm: { + container: "w-4 h-4", + text: "text-[8px]", + icon: :sm, + status: "w-1.5 h-1.5 border" + }, + xsm: { + container: "w-6 h-6", + text: "text-[10px]", + icon: :sm, + status: "w-2 h-2 border" + }, + sm: { + container: "w-8 h-8", + text: "text-xs", + icon: :sm, + status: "w-2.5 h-2.5 border-2" + }, + md: { + container: "w-10 h-10", + text: "text-sm", + icon: :md, + status: "w-3 h-3 border-2" + }, + lg: { + container: "w-14 h-14", + text: "text-lg", + icon: :lg, + status: "w-4 h-4 border-2" + } + }.freeze + + # Status → background color mapping. Maps semantic status to utility palette. + STATUSES = { + online: "bg-utility-positive-default", + away: "bg-utility-warning-default", + busy: "bg-utility-negative-default", + offline: "bg-utility-neutral-default" + }.freeze + + # `class:` is a Ruby reserved word used as a keyword arg — requires + # binding.local_variable_get. Shipwright convention. + def initialize(src: nil, alt: nil, initials: nil, size: :md, status: nil, class: nil, **html_attrs) + raise ArgumentError, "AvatarComponent: alt is required when src is provided" if src && alt.nil? + + @src = src + @alt = alt + @initials = initials ? derive_initials(initials) : nil + @size = size + @status = status + @class = binding.local_variable_get(:class) + @html_attrs = html_attrs + end + + def call + size = SIZES.fetch(@size) do |v| + raise ArgumentError, "#{self.class}: unknown size :#{v}. Valid: #{SIZES.keys.join(', ')}" + end + if @status + STATUSES.fetch(@status) do |s| + raise ArgumentError, "#{self.class}: unknown status :#{s}. Valid: #{STATUSES.keys.join(', ')}" + end + end + + content_tag :span, + class: classes( + "relative inline-flex items-center justify-center shrink-0 overflow-visible", + size[:container], + consumer: @class + ), + **@html_attrs do + safe_join([inner_content(size), status_indicator(size)].compact) + end + end + + private + + # Derive 1-2 letter initials from an input string. "Wendy Sullivan" → "WS". + # If the input is already short (<= 2 chars), uppercase it as-is. + def derive_initials(input) + str = input.to_s.strip + return str.upcase if str.length <= 2 + + str.split(/\s+/).first(2).map { |word| word[0] }.join.upcase + end + + def inner_content(size) + if @src + content_tag :img, nil, + src: @src, + alt: @alt, + loading: "lazy", + class: "w-full h-full object-cover rounded-full" + elsif @initials + content_tag :span, @initials, + class: "w-full h-full flex items-center justify-center rounded-full bg-interactive-primary-default text-text-inverse font-sans font-medium #{size[:text]}" + else + content_tag :span, nil, + class: "w-full h-full flex items-center justify-center rounded-full bg-interactive-primary-default text-text-inverse", + "aria-label": @alt do + Shipwright::IconComponent.new(name: :user, size: size[:icon], class: "w-3/5 h-3/5").render_in(self) + end + end + end + + def status_indicator(size) + return nil unless @status + + content_tag :span, nil, + class: classes( + "absolute bottom-0 right-0 rounded-full border-border-inverse", + size[:status], + STATUSES.fetch(@status) + ), + "data-avatar-status": @status + end + end +end diff --git a/app/components/shipwright/base_component.rb b/app/components/shipwright/base_component.rb index f75b669..0a89727 100644 --- a/app/components/shipwright/base_component.rb +++ b/app/components/shipwright/base_component.rb @@ -13,7 +13,11 @@ class BaseComponent < ViewComponent::Base interactive-secondary-default interactive-disable border-interactive + border-inverse + utility-neutral-default + utility-positive-default utility-negative-default + utility-warning-default text-primary text-inverse text-disabled diff --git a/test/components/previews/shipwright/avatar_component_preview.rb b/test/components/previews/shipwright/avatar_component_preview.rb new file mode 100644 index 0000000..30c41c6 --- /dev/null +++ b/test/components/previews/shipwright/avatar_component_preview.rb @@ -0,0 +1,41 @@ +module Shipwright + class AvatarComponentPreview < Lookbook::Preview + # @label Image + def image + render Shipwright::AvatarComponent.new( + src: "https://i.pravatar.cc/150?img=12", + alt: "Portrait" + ) + end + + # @label Initials + def initials + render Shipwright::AvatarComponent.new(initials: "WS") + end + + # @label Icon fallback (no src / no initials) + def icon_fallback + render Shipwright::AvatarComponent.new(alt: "Anonymous user") + end + + # @label All Sizes + def sizes + render_with_template + end + + # @label Content Types + def content_types + render_with_template + end + + # @label Status Indicators + def status + render_with_template + end + + # @label With Name + Subtitle + def with_meta + render_with_template + end + end +end diff --git a/test/components/previews/shipwright/avatar_component_preview/content_types.html.erb b/test/components/previews/shipwright/avatar_component_preview/content_types.html.erb new file mode 100644 index 0000000..780c5f6 --- /dev/null +++ b/test/components/previews/shipwright/avatar_component_preview/content_types.html.erb @@ -0,0 +1,10 @@ +
+ <% [:sm, :md, :lg].each do |size| %> +
+ <%= render Shipwright::AvatarComponent.new(src: "https://i.pravatar.cc/150?img=12", alt: "Photo", size: size) %> + <%= render Shipwright::AvatarComponent.new(initials: "WS", size: size) %> + <%= render Shipwright::AvatarComponent.new(alt: "User", size: size) %> + <%= size %> +
+ <% end %> +
diff --git a/test/components/previews/shipwright/avatar_component_preview/sizes.html.erb b/test/components/previews/shipwright/avatar_component_preview/sizes.html.erb new file mode 100644 index 0000000..1faab30 --- /dev/null +++ b/test/components/previews/shipwright/avatar_component_preview/sizes.html.erb @@ -0,0 +1,7 @@ +
+ <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :xxsm) %> + <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :xsm) %> + <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :sm) %> + <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :md) %> + <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :lg) %> +
diff --git a/test/components/previews/shipwright/avatar_component_preview/status.html.erb b/test/components/previews/shipwright/avatar_component_preview/status.html.erb new file mode 100644 index 0000000..82b041d --- /dev/null +++ b/test/components/previews/shipwright/avatar_component_preview/status.html.erb @@ -0,0 +1,8 @@ +
+ <% [:online, :away, :busy, :offline].each do |status| %> +
+ <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :lg, status: status) %> + <%= status %> +
+ <% end %> +
diff --git a/test/components/previews/shipwright/avatar_component_preview/with_meta.html.erb b/test/components/previews/shipwright/avatar_component_preview/with_meta.html.erb new file mode 100644 index 0000000..5432c8d --- /dev/null +++ b/test/components/previews/shipwright/avatar_component_preview/with_meta.html.erb @@ -0,0 +1,11 @@ +
+ <% [:sm, :md, :lg].each do |size| %> +
+ <%= render Shipwright::AvatarComponent.new(src: "https://i.pravatar.cc/150?img=12", alt: "John Legend", size: size, status: :online) %> +
+ John Legend + Senior Designer +
+
+ <% end %> +
diff --git a/test/components/shipwright/avatar_component_test.rb b/test/components/shipwright/avatar_component_test.rb new file mode 100644 index 0000000..af34267 --- /dev/null +++ b/test/components/shipwright/avatar_component_test.rb @@ -0,0 +1,129 @@ +require "test_helper" + +class Shipwright::AvatarComponentTest < ViewComponent::TestCase + def test_renders_image_when_src_given + render_inline(Shipwright::AvatarComponent.new(src: "/john.jpg", alt: "John Legend")) + + assert_selector "img[src='/john.jpg'][alt='John Legend']" + end + + def test_image_uses_object_cover_and_rounded_full + render_inline(Shipwright::AvatarComponent.new(src: "/a.jpg", alt: "A")) + + assert_selector "img.object-cover" + assert_selector ".rounded-full" + end + + def test_renders_initials_when_no_src + render_inline(Shipwright::AvatarComponent.new(initials: "WS")) + + assert_no_selector "img" + assert_text "WS" + end + + def test_initials_styled_as_dark_with_inverse_text + render_inline(Shipwright::AvatarComponent.new(initials: "WS")) + + assert_selector ".bg-interactive-primary-default" + assert_selector ".text-text-inverse" + end + + def test_falls_back_to_user_icon_when_neither_src_nor_initials + render_inline(Shipwright::AvatarComponent.new) + + assert_no_selector "img" + # User icon from IconComponent + assert_selector "svg" + end + + def test_default_size_is_medium_40 + render_inline(Shipwright::AvatarComponent.new(initials: "WS")) + + assert_selector ".w-10.h-10" + end + + def test_size_xxsm_is_16 + render_inline(Shipwright::AvatarComponent.new(initials: "WS", size: :xxsm)) + assert_selector ".w-4.h-4" + end + + def test_size_xsm_is_24 + render_inline(Shipwright::AvatarComponent.new(initials: "WS", size: :xsm)) + assert_selector ".w-6.h-6" + end + + def test_size_sm_is_32 + render_inline(Shipwright::AvatarComponent.new(initials: "WS", size: :sm)) + assert_selector ".w-8.h-8" + end + + def test_size_lg_is_56 + render_inline(Shipwright::AvatarComponent.new(initials: "WS", size: :lg)) + assert_selector ".w-14.h-14" + end + + def test_status_indicator_renders_when_status_given + render_inline(Shipwright::AvatarComponent.new(initials: "WS", status: :online)) + + assert_selector "[data-avatar-status]" + assert_selector "[data-avatar-status].bg-utility-positive-default" + end + + def test_no_status_indicator_by_default + render_inline(Shipwright::AvatarComponent.new(initials: "WS")) + + assert_no_selector "[data-avatar-status]" + end + + def test_status_away_uses_warning_color + render_inline(Shipwright::AvatarComponent.new(initials: "WS", status: :away)) + assert_selector "[data-avatar-status].bg-utility-warning-default" + end + + def test_status_busy_uses_negative_color + render_inline(Shipwright::AvatarComponent.new(initials: "WS", status: :busy)) + assert_selector "[data-avatar-status].bg-utility-negative-default" + end + + def test_status_offline_uses_neutral_color + render_inline(Shipwright::AvatarComponent.new(initials: "WS", status: :offline)) + assert_selector "[data-avatar-status].bg-utility-neutral-default" + end + + def test_consumer_class_merges + render_inline(Shipwright::AvatarComponent.new(initials: "WS", class: "ring-2 ring-interactive-primary-default")) + + assert_selector ".ring-2" + end + + def test_raises_on_invalid_size + assert_raises(ArgumentError) do + render_inline(Shipwright::AvatarComponent.new(initials: "WS", size: :giant)) + end + end + + def test_raises_on_invalid_status + assert_raises(ArgumentError) do + render_inline(Shipwright::AvatarComponent.new(initials: "WS", status: :unknown)) + end + end + + def test_image_requires_alt + assert_raises(ArgumentError) do + render_inline(Shipwright::AvatarComponent.new(src: "/john.jpg")) + end + end + + def test_initials_normalized_to_uppercase_and_truncated + render_inline(Shipwright::AvatarComponent.new(initials: "Wendy Sullivan")) + + # Component should derive "WS" from the input — first letter of each word, max 2 chars + assert_text "WS" + end + + def test_passes_html_attributes + render_inline(Shipwright::AvatarComponent.new(initials: "WS", id: "me", data: { user_id: "42" })) + + assert_selector "[id='me'][data-user-id='42']" + end +end diff --git a/test/dummy/app/assets/builds/tailwind.css b/test/dummy/app/assets/builds/tailwind.css index 28fe205..2e29473 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;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@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)}.grid{display:grid}.inline-flex{display:inline-flex}.table{display:table}.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-auto{width:auto}.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)}.italic{font-style:italic}.underline{text-decoration-line:underline}.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)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-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}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false} \ 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;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@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-xs:.75rem;--text-xs--line-height:calc(1 / .75);--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-border-inverse:#fdfdfd;--color-utility-neutral-default:#292c39;--color-utility-positive-default:#48a356;--color-utility-negative-default:#b62e2e;--color-utility-warning-default:#c09c17;--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{.absolute{position:absolute}.relative{position:relative}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.right-0{right:calc(var(--spacing) * 0)}.bottom-0{bottom:calc(var(--spacing) * 0)}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.flex{display:flex}.grid{display:grid}.inline-flex{display:inline-flex}.table{display:table}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\/5{height:60%}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-10{height:calc(var(--spacing) * 10)}.h-11{height:calc(var(--spacing) * 11)}.h-13{height:calc(var(--spacing) * 13)}.h-14{height:calc(var(--spacing) * 14)}.h-full{height:100%}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-3{width:calc(var(--spacing) * 3)}.w-3\/5{width:60%}.w-4{width:calc(var(--spacing) * 4)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-10{width:calc(var(--spacing) * 10)}.w-11{width:calc(var(--spacing) * 11)}.w-13{width:calc(var(--spacing) * 13)}.w-14{width:calc(var(--spacing) * 14)}.w-auto{width:auto}.w-full{width:100%}.shrink-0{flex-shrink:0}.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)}.overflow-visible{overflow:visible}.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-2{border-style:var(--tw-border-style);border-width:2px}.border-border-interactive{border-color:var(--color-border-interactive)}.border-border-inverse{border-color:var(--color-border-inverse)}.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)}.bg-utility-neutral-default{background-color:var(--color-utility-neutral-default)}.bg-utility-positive-default{background-color:var(--color-utility-positive-default)}.bg-utility-warning-default{background-color:var(--color-utility-warning-default)}.object-cover{object-fit:cover}.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-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[8px\]{font-size:8px}.text-\[10px\]{font-size:10px}.leading-2xs{--tw-leading:var(--leading-2xs);line-height:var(--leading-2xs)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.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)}.italic{font-style:italic}.underline{text-decoration-line:underline}.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)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-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}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false} \ 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 1a6be39..cc6060b 100644 --- a/test/dummy/app/views/pages/components.html.erb +++ b/test/dummy/app/views/pages/components.html.erb @@ -69,4 +69,28 @@ <%= render Shipwright::IconComponent.new(name: :info, size: :md) %> <%= render Shipwright::IconComponent.new(name: :info, size: :lg) %> + +

Avatar — Sizes

+
+ <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :xxsm) %> + <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :xsm) %> + <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :sm) %> + <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :md) %> + <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :lg) %> +
+ +

Avatar — Content Types

+
+ <%= render Shipwright::AvatarComponent.new(src: "https://i.pravatar.cc/150?img=12", alt: "Photo") %> + <%= render Shipwright::AvatarComponent.new(initials: "WS") %> + <%= render Shipwright::AvatarComponent.new(alt: "Anonymous") %> +
+ +

Avatar — Status

+
+ <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :lg, status: :online) %> + <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :lg, status: :away) %> + <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :lg, status: :busy) %> + <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :lg, status: :offline) %> +
From c96f8a9d998173ec2a3031d5008004fcf0abe319 Mon Sep 17 00:00:00 2001 From: Jordan Burke Date: Sat, 18 Apr 2026 18:05:46 -0400 Subject: [PATCH 12/24] fix: remove stale conflict marker in develop showcase page --- test/dummy/app/views/pages/components.html.erb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/dummy/app/views/pages/components.html.erb b/test/dummy/app/views/pages/components.html.erb index 3f95ea1..90bf8ed 100644 --- a/test/dummy/app/views/pages/components.html.erb +++ b/test/dummy/app/views/pages/components.html.erb @@ -155,6 +155,5 @@ <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :lg, status: :away) %> <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :lg, status: :busy) %> <%= render Shipwright::AvatarComponent.new(initials: "WS", size: :lg, status: :offline) %> ->>>>>>> feat/avatar-component From 0dc0d00cc0ebacc10d0ff926565072b225896967 Mon Sep 17 00:00:00 2001 From: Jordan Burke Date: Sat, 18 Apr 2026 18:16:01 -0400 Subject: [PATCH 13/24] feat: add BreadcrumbComponent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Horizontal navigation trail matching Shipwright Pro's Breadcrumb atom. Renders a semantic