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. 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 @@ + + + + + diff --git a/app/assets/tailwind/shipwright/engine.css b/app/assets/tailwind/shipwright/engine.css index db2420a..3e7a121 100644 --- a/app/assets/tailwind/shipwright/engine.css +++ b/app/assets/tailwind/shipwright/engine.css @@ -25,9 +25,15 @@ /* Border colors */ --color-border-interactive: #14161c; + --color-border-inverse: #fdfdfd; + --color-border-primary: #eaeaea; - /* 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; @@ -36,6 +42,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..2048980 --- /dev/null +++ b/app/components/shipwright/accordion_component.rb @@ -0,0 +1,67 @@ +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", + # 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 + + # 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 + # 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 + "transition-transform duration-200 group-open:rotate-180" + end + end +end 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/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/banner_component.html.erb b/app/components/shipwright/banner_component.html.erb new file mode 100644 index 0000000..83f9593 --- /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? %> +
+ <%= leading_visual if leading_visual? %> + <% 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..e780a08 --- /dev/null +++ b/app/components/shipwright/banner_component.rb @@ -0,0 +1,44 @@ +module Shipwright + class BannerComponent < BaseComponent + # Optional leading visual — exactly one of icon, avatar, or logo. + # banner.with_icon(name: :info) + # banner.with_avatar(src: "/u.jpg", alt: "User") + # banner.with_logo(src: "/l.png", alt: "Acme") + renders_one :icon, IconComponent + renders_one :avatar, AvatarComponent + renders_one :logo, LogoComponent + + # Optional trailing link action. + # banner.with_link(href: "#") { "Link" } + renders_one :link, LinkComponent + + def leading_visual + icon || avatar || logo + end + + def leading_visual? + icon? || avatar? || logo? + end + + # `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? || leading_visual? + 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..67f4857 100644 --- a/app/components/shipwright/base_component.rb +++ b/app/components/shipwright/base_component.rb @@ -13,11 +13,18 @@ class BaseComponent < ViewComponent::Base interactive-secondary-default interactive-disable border-interactive + border-inverse + border-primary + utility-neutral-default + utility-information-default + utility-positive-default utility-negative-default + utility-warning-default text-primary text-inverse text-disabled background-primary + background-secondary ], "radius" => %w[md] } 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(" +
    + <% items.each_with_index do |item, index| %> + <% is_last = (index == items.size - 1) %> +
  1. + <% if item.link? && !is_last %> + <%= link_to item.href, class: link_classes do %> + <% if item.icon %> + <%= render Shipwright::IconComponent.new(name: item.icon, size: :sm) %> + <% elsif item.avatar %> + <%= render Shipwright::AvatarComponent.new(**item.avatar.reverse_merge(size: :xxsm)) %> + <% elsif item.logo %> + <%= render Shipwright::LogoComponent.new(**item.logo.reverse_merge(size: :xs)) %> + <% end %> + <%= item.label %> + <% end %> + <% else %> + > + <% if item.icon %> + <%= render Shipwright::IconComponent.new(name: item.icon, size: :sm) %> + <% elsif item.avatar %> + <%= render Shipwright::AvatarComponent.new(**item.avatar.reverse_merge(size: :xxsm)) %> + <% elsif item.logo %> + <%= render Shipwright::LogoComponent.new(**item.logo.reverse_merge(size: :xs)) %> + <% end %> + <%= item.label %> + + <% end %> + + <% unless is_last %> + + <% end %> +
  2. + <% end %> +
+<% end %> diff --git a/app/components/shipwright/breadcrumb_component.rb b/app/components/shipwright/breadcrumb_component.rb new file mode 100644 index 0000000..0442c27 --- /dev/null +++ b/app/components/shipwright/breadcrumb_component.rb @@ -0,0 +1,81 @@ +module Shipwright + # Breadcrumb navigation — a horizontal list of page ancestors, with the + # final item marked as the current page. Matches Shipwright Pro's + # Breadcrumb atom. + # + # Usage: + # <%= render Shipwright::BreadcrumbComponent.new do |crumb| %> + # <% crumb.with_item(href: "/", icon: :home) { "Home" } %> + # <% crumb.with_item(href: "/products") { "Products" } %> + # <% crumb.with_item { "Widget" } %> # no href = current page + # <% end %> + class BreadcrumbComponent < BaseComponent + SEPARATORS = %i[chevron slash].freeze + + # Sub-component used only as a data holder for each slotted item. + # Inherits from ViewComponent::Base so the parent's Slot wrapper + # delegates method calls (href, icon, label, link?) to it via + # Slot#method_missing. The call method returns an empty string because + # rendering is driven by the parent template, not the slot itself. + class Item < ViewComponent::Base + attr_reader :href, :icon, :avatar, :logo, :label + + def initialize(href: nil, icon: nil, avatar: nil, logo: nil, label: nil) + if [icon, avatar, logo].compact.size > 1 + raise ArgumentError, "BreadcrumbComponent item: pass only one of :icon, :avatar, :logo" + end + + @href = href + @icon = icon + @avatar = avatar + @logo = logo + @label = label + end + + def link? + !href.nil? + end + + def call + "" + end + end + + renders_many :items, ->(href: nil, icon: nil, avatar: nil, logo: nil, &block) { + label = block ? view_context.capture(&block) : "" + Item.new(href: href, icon: icon, avatar: avatar, logo: logo, label: label) + } + + # `class:` is a Ruby reserved word used as a keyword arg — requires + # binding.local_variable_get. Shipwright convention. + def initialize(separator: :chevron, class: nil, **html_attrs) + unless SEPARATORS.include?(separator) + raise ArgumentError, "#{self.class}: unknown separator :#{separator}. Valid: #{SEPARATORS.join(', ')}" + end + @separator = separator + @class = binding.local_variable_get(:class) + @html_attrs = html_attrs + end + + attr_reader :separator, :html_attrs + + def nav_classes + classes("font-sans text-sm leading-5", consumer: @class) + end + + def link_classes + "inline-flex items-center gap-1 text-interactive-primary-default " \ + "hover:text-interactive-primary-hover active:text-interactive-primary-pressed " \ + "hover:underline transition-colors " \ + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-interactive-primary-default focus-visible:ring-offset-2 rounded-sm" + end + + def current_classes + "inline-flex items-center gap-1 text-text-primary font-medium" + end + + def separator_slash? + @separator == :slash + end + end +end diff --git a/app/components/shipwright/checkbox_component.html.erb b/app/components/shipwright/checkbox_component.html.erb new file mode 100644 index 0000000..0805e54 --- /dev/null +++ b/app/components/shipwright/checkbox_component.html.erb @@ -0,0 +1,40 @@ +<%= content_tag :label, + class: label_classes, + for: id, + data: { indeterminate: indeterminate? ? "true" : nil } do %> + + + <%= check_box_tag name, value, checked?, + id: id, + disabled: disabled?, + class: "peer sr-only", + **html_attrs %> + + + <%# Check icon — shown when the label (group) contains a checked input, + and the group is NOT in the indeterminate state. %> + + <%# Dash icon — shown when the label carries data-indeterminate="true". %> + + + + + <% if label_text %> + + <%= label_text %> + <% if caption %> + <%= caption %> + <% end %> + + <% end %> +<% end %> diff --git a/app/components/shipwright/checkbox_component.rb b/app/components/shipwright/checkbox_component.rb new file mode 100644 index 0000000..5181b86 --- /dev/null +++ b/app/components/shipwright/checkbox_component.rb @@ -0,0 +1,116 @@ +module Shipwright + # Checkbox form control with optional label + caption. Tri-state support + # (unchecked / checked / indeterminate) and destructive variant matching + # Shipwright Pro's Checkbox atom. + # + # Usage: + # <%= render Shipwright::CheckboxComponent.new(name: "terms", label: "Accept") %> + # <%= render Shipwright::CheckboxComponent.new(name: "x", label: "Yes", caption: "Opt-in") %> + # <%= render Shipwright::CheckboxComponent.new(name: "danger", label: "Delete", variant: :destructive) %> + # + # Styling strategy: the