Skip to content

Add BreadcrumbComponent - #9

Open
PendragonDevelopment wants to merge 5 commits into
fix/lookbook-button-previewsfrom
feat/breadcrumb-component
Open

Add BreadcrumbComponent#9
PendragonDevelopment wants to merge 5 commits into
fix/lookbook-button-previewsfrom
feat/breadcrumb-component

Conversation

@PendragonDevelopment

Copy link
Copy Markdown
Collaborator

Summary

New `Shipwright::BreadcrumbComponent` — horizontal navigation trail matching Shipwright Pro's Breadcrumb atom. Renders semantic `<nav aria-label="Breadcrumb">

    ` with the last item automatically marked `aria-current="page"`.

    Stack: `fix/lookbook-button-previews` (#1) → `feat/icon-component` (#4) → this PR (uses IconComponent for optional leading icon + chevron separator).

    API

    ```erb
    <%= render Shipwright::BreadcrumbComponent.new do |crumb| %>
    <% crumb.with_item(href: "/", icon: :home) { "Home" } %>
    <% crumb.with_item(href: "/products") { "Products" } %>
    <% crumb.with_item { "Widget Pro" } %> <%# no href = current page %>
    <% end %>
    ```

    Param Values Default
    `separator` `:chevron` (chevron-right icon), `:slash` (literal "/") `:chevron`
    `class` Any Tailwind classes `nil`

    Per-item:

    • `href` — optional. When present (and not the last item), renders as ``. Otherwise renders as ``.
    • `icon` — optional IconComponent name for a leading icon
    • Block content = label

    The last item is always rendered as a non-link span with `aria-current="page"` regardless of whether `href` was provided — matches the browser/a11y convention where the current page shouldn't link to itself.

    Implementation note

    Uses a lambda slot that returns a minimal nested `Item` ViewComponent. The parent template accesses `item.href`, `item.icon`, `item.label`, `item.link?` via `ViewComponent::Slot`'s `method_missing` delegation, which only works for component-instance slots (not struct-valued content). Block content is captured immediately via `view_context.capture`.

    Test plan

    • 14 new tests pass (nav/ol structure, link vs span rendering, aria-current on last item, separator between items only, chevron vs slash, optional leading icon, single item edge case, consumer class override, error cases)
    • Full suite: 66 tests, 130 assertions, 0 failures
    • `rake tailwindcss:build` compiles new utility classes
    • `/components` renders both chevron and slash variants
    • Lookbook at `/lookbook/preview/shipwright/breadcrumb/*` with 5 scenarios (including a 6-level long_trail)

    No new tokens needed — uses existing Interactive/Text/Background palette.

    🤖 Generated with Claude Code

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.
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).
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
Horizontal navigation trail matching Shipwright Pro's Breadcrumb atom.
Renders a semantic <nav aria-label="Breadcrumb"> > <ol> with each item
as an <li>. The last item is automatically marked aria-current="page"
and rendered as a non-link.

API:
  <%= 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 %>

Props:
- separator: :chevron (default — uses chevron-right icon) or :slash
  (literal "/" character)
- class: consumer override on <nav>
- **html_attrs: pass-through

Per-item slot (with_item):
- href: optional — becomes an <a> link when present (except last item)
- icon: optional — name of an IconComponent to render as leading icon
- Block content: item label

Implementation notes:
- Uses a lambda slot returning a minimal Item ViewComponent subclass so
  the Slot wrapper's method_missing delegates href/icon/label/link? to
  it. The struct-based approach didn't work because Slot only delegates
  to component instances, not to arbitrary content values.
- Block content captured immediately via view_context.capture inside
  the slot lambda — matches how VC's block forwarding works in 4.6+.
- Separator rendered between items (not after last) with aria-hidden=true
  since it's decorative; the nav's aria-label tells assistive tech
  this is a breadcrumb.

No new tokens needed — uses existing Interactive/Text/Background palette.

14 new tests pass. Total suite: 66 tests, 130 assertions.
Lookbook: default, slash_separator, single_item, no_icons, long_trail.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant