Skip to content

Add DropdownComponent - #11

Open
PendragonDevelopment wants to merge 4 commits into
fix/lookbook-button-previewsfrom
feat/dropdown-component
Open

Add DropdownComponent#11
PendragonDevelopment wants to merge 4 commits into
fix/lookbook-button-previewsfrom
feat/dropdown-component

Conversation

@PendragonDevelopment

Copy link
Copy Markdown
Collaborator

Summary

New `Shipwright::DropdownComponent` — custom styled select with items that carry icon + primary + secondary text (not possible with native ``). Stack: `fix/lookbook-button-previews` (#1) → `feat/icon-component` (#4) → this PR (uses IconComponent for chevron + item icons). API ```erb <%= render(Shipwright::DropdownComponent.new( name: "category", label: "Category", caption: "Choose one", value: "starred", placeholder: "Select..." )) do |dropdown| %> <% dropdown.with_item(value: "folders", icon: :folder, primary: "Folders", secondary: "Organize your files") %> <% dropdown.with_item(value: "starred", icon: :star, primary: "Starred", secondary: "Your favorites") %> <% end %> ``` Param Values Default `name` Form field name `nil` `value` Initially selected value `nil` `label` Optional label above trigger `nil` `caption` Optional subtext below trigger `nil` `placeholder` Trigger text when no value `"Dropdown Menu"` `disabled` `true`/`false` `false` `variant` `:default`, `:destructive` `:default` `class` Any Tailwind classes `nil` Item slot (`with_item`): `value` — required; matches `value:` kwarg to show as selected `primary` — required label `icon` — optional IconComponent name (leading) `secondary` — optional smaller subtitle Architecture Native `/` for open/close — zero JS for toggle, keyboard navigation, focus management, a11y `<button role="option">` items inside the menu panel Hidden `` carries the value for form submission

  • Chevron rotates via Tailwind `group-open:rotate-180`
  • Inline `<script>` (installed once per page via `window.__shipwrightDropdownWired` flag) delegates clicks: updates trigger label, writes hidden input, marks `aria-selected`, closes `
    Details`. Also closes open dropdowns on outside click.
  • No new tokens needed.

    Test plan

    • 15 new tests pass (label/caption, hidden input, placeholder vs value, items with icon + secondary, disabled, destructive, consumer class, error cases)
    • Full suite: 67 tests, 136 assertions, 0 failures
    • `rake tailwindcss:build` compiles all utilities
    • `/components` shows the 4-tile Figma grid layout
    • Lookbook at `/lookbook/preview/shipwright/dropdown/*` — 6 scenarios

    🤖 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
    Custom styled select-like form control matching Shipwright Pro's
    Dropdown atom. Supports items with icon + primary + secondary text —
    things a native <select> can't display.
    
    API:
      <%= render Shipwright::DropdownComponent.new(
        name: "category", label: "Category", caption: "Choose one",
        value: "starred", placeholder: "Select..."
      ) do |dropdown| %>
        <% dropdown.with_item(value: "folders", icon: :folder,
                               primary: "Folders", secondary: "Organize your files") %>
        <% dropdown.with_item(value: "starred", icon: :star,
                               primary: "Starred", secondary: "Your favorites") %>
      <% end %>
    
    Props:
    - name: form field name (rendered as hidden input carrying the value)
    - value: currently selected value (trigger shows matching item's primary text)
    - label: optional text above the trigger
    - caption: optional text below the trigger
    - placeholder: text shown in trigger when no value (default "Dropdown Menu")
    - disabled: bool
    - variant: :default or :destructive (red border + text)
    - class: consumer override on wrapper
    - **html_attrs: pass-through on wrapper
    
    Item slot:
    - value: required, matches against `value:` to show as selected
    - primary: required label text
    - icon: optional IconComponent name
    - secondary: optional smaller subtitle text
    
    Architecture:
    - <details>/<summary> for native open/close (no JS for toggle, keyboard,
      or a11y)
    - <button role="option"> items inside the menu panel
    - Hidden <input> carries the value for form submission
    - Inline <script> (installed once per page via a window flag) delegates
      clicks: when an item is clicked, updates the trigger label, writes
      to hidden input, marks aria-selected, and closes the <details>. Also
      closes open dropdowns when clicking outside.
    - Chevron rotates via Tailwind's group-open:rotate-180 modifier
    
    15 new tests pass. Total suite: 67 tests, 136 assertions.
    Lookbook: default, with_value, no_icons, disabled, destructive, grid.
    Showcase page has a 4-tile grid matching the Figma "all states" layout.
    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