fix: register Lookbook preview path before set_autoload_paths - #21
Open
PendragonDevelopment wants to merge 52 commits into
Open
fix: register Lookbook preview path before set_autoload_paths#21PendragonDevelopment wants to merge 52 commits into
PendragonDevelopment wants to merge 52 commits into
Conversation
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).
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.
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.
# Conflicts: # test/dummy/app/assets/builds/tailwind.css # test/dummy/app/views/pages/components.html.erb
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.
# Conflicts: # test/dummy/app/assets/builds/tailwind.css # test/dummy/app/views/pages/components.html.erb
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).
# Conflicts: # test/dummy/app/assets/builds/tailwind.css # test/dummy/app/views/pages/components.html.erb
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
# Conflicts: # test/dummy/app/assets/builds/tailwind.css
Single collapsible section using native HTML <details>/<summary> — 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 <details open> attribute
- class: consumer override on the <details> 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
<details open>, 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.
# Conflicts: # app/assets/tailwind/shipwright/engine.css # app/components/shipwright/base_component.rb # test/dummy/app/assets/builds/tailwind.css # test/dummy/app/views/pages/components.html.erb
Animates open/close via the native <details> 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 — <details> still works natively on all browsers.
# Conflicts: # test/dummy/app/assets/builds/tailwind.css
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.
# Conflicts: # app/assets/tailwind/shipwright/engine.css # app/components/shipwright/base_component.rb # test/dummy/app/assets/builds/tailwind.css # test/dummy/app/views/pages/components.html.erb
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.
# Conflicts: # test/dummy/app/assets/builds/tailwind.css # test/dummy/app/views/pages/components.html.erb
Tri-state form checkbox (unchecked / checked / indeterminate) with
optional label + caption. Matches Shipwright Pro's Checkbox atom.
API:
<%= render Shipwright::CheckboxComponent.new(name: "terms", label: "Accept") %>
<%= render Shipwright::CheckboxComponent.new(
name: "notify", label: "Email me", caption: "Weekly updates only",
checked: true) %>
<%= render Shipwright::CheckboxComponent.new(
name: "delete", label: "Permanent", variant: :destructive) %>
Props:
- name: form field name
- value: form field value (default "1")
- label: optional visible label text
- caption: optional secondary text beneath the label
- checked: default false
- indeterminate: default false (see note below)
- disabled: default false
- variant: :default (black) or :destructive (red)
- label_position: :right (default) or :left
- id: optional; auto-derives from name
- class: consumer override on the <label> wrapper
- **html_attrs: pass-through onto the <input>
Structure: a <label> wraps a hidden native <input type="checkbox"> (for
a11y + form submission) plus a styled <span> that is the visible box.
CSS peer-* selectors flip the visual's appearance based on the input's
:checked / :disabled / :focus-visible pseudo-classes.
Indeterminate state: sets data-indeterminate="true" on the wrapper for
CSS targeting. The native `input.indeterminate` property is JS-only —
consuming apps that need `:indeterminate` to apply on the input itself
can bootstrap it with a small script:
document.querySelectorAll('[data-indeterminate="true"] input')
.forEach(i => i.indeterminate = true)
The visual dash icon renders via the data-indeterminate attribute
selector, so the component looks right even without that bootstrap.
No new tokens — uses existing Interactive/Utility/Text palette.
23 new tests pass. Total suite: 48 tests, 89 assertions.
Lookbook: default, checked, with_caption, states, variants,
label_positions, group.
# Conflicts: # test/dummy/app/assets/builds/tailwind.css # test/dummy/app/views/pages/components.html.erb
Two bugs in the initial implementation:
1. Tailwind wasn't compiling `peer-checked:bg-*` utilities because
the classes were built with Ruby string interpolation
(`peer-checked:#{v[:bg]}`). Tailwind's scanner only picks up
literal class strings in source files — interpolated values are
invisible. Fixed by inlining complete class bundles per variant,
with full literal strings for every peer-* / group-* combination.
2. The check-mark and dash SVGs were styled with `peer-checked:`
but they are descendants of the visual span, not siblings of the
peer input. The `peer-*` modifier only targets siblings. Fixed
by putting `group` on the <label> and using `group-has-checked:`
and `group-data-[indeterminate=true]:` on the icons + visual.
These work regardless of DOM depth because they use CSS :has()
and attribute selectors matching any descendant.
Visible result: checked, indeterminate, and destructive states now
render correctly — filled box + appropriate check/dash icon, white
on default, white on red for destructive.
# Conflicts: # test/dummy/app/assets/builds/tailwind.css
Two fixes:
1. Auto-generated id previously matched the `name` kwarg alone, so
three checkboxes sharing `name: 'g1'` all got `id='g1'` and the
`<label for='g1'>` targeted only the first input in the DOM —
clicking any label toggled the same checkbox. Fixed by deriving
id as `#{name}_#{value}` (Rails form-helper convention); explicit
`id:` still wins when provided.
2. The 'Select all' parent/child demo in Lookbook had no JS wiring,
so clicking the parent didn't toggle children and vice versa.
Added a self-contained inline IIFE in the preview template that
handles parent ↔ children sync (indeterminate when mixed, all-on
/ all-off when parent clicked). The docs make clear this is a
consumer-app concern — use Stimulus or your framework's equivalent
in production; this script just makes the demo work.
Test added: test_shared_name_with_distinct_values_gets_unique_ids.
Existing test_auto_generated_id_from_name renamed and updated to
expect 'accept_terms_1' for the default value '1'.
The CheckboxComponent forwards html_attrs (data-*, id, etc.) onto the
hidden <input>, not the outer <label>. My demo JS was doing
`querySelector('[data-select-all-demo-target="parent"]')` which matched
the input and then setting `.dataset.indeterminate` on it — but the
visual's CSS rule keys off `.group[data-indeterminate=true]` on the
LABEL. So the attribute ended up on the wrong element and the
indeterminate visual never appeared when children were mixed.
Fixed by selecting the input directly (`input[data-...target]`) then
using `.closest('label')` to get the label for dataset updates.
Also documented the component's html_attrs forwarding behavior in a
comment inside the demo script.
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.
# Conflicts: # test/dummy/app/assets/builds/tailwind.css # test/dummy/app/views/pages/components.html.erb
… Breadcrumb
LogoComponent: brand mark (image src, inline SVG block, or image-icon fallback)
+ optional wordmark, 5 sizes (xs/sm/md/lg/xl), optional href link wrapper.
Extends leading-visual slot on Dropdown items, Banner, and Breadcrumb items
to accept avatar: {...} or logo: {...} as alternatives to icon:. Mutual
exclusion enforced; prior icon: API unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inline variant: Choose-file button + filename display, configurable button position (left/right) and variant (primary/secondary/on_primary). Dropzone variant: dashed-border drop target with upload icon and Browse files button; supports click-to-browse and drag-and-drop. Both variants support single or multiple files, staged files list with remove buttons, and client-side validation of accept types and max_size. File removal uses DataTransfer to mutate the real <input type=file> FileList so the form submits exactly the staged set. Vanilla JS wiring (matching dropdown/checkbox convention) installed once per page via window.__shipwrightFileUploadWired guard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Text input with optional leading/trailing icons, label, caption, and
three state variants (default/success/destructive). Supports :text,
:email, :password, :url, :search, :tel, :number, :date, :time, and
:datetime-local. Passing error: forces the destructive variant and
wires aria-invalid + aria-describedby for accessibility.
Unconsumed keyword args are forwarded to the underlying <input>, so
native HTML5 validation attrs (pattern, minlength, maxlength, min,
max, step, autocomplete, inputmode, title, etc.) work directly:
<%= render Shipwright::InputComponent.new(
name: "email", type: :email, required: true,
autocomplete: "email", inputmode: "email"
) %>
Documented validation usage in the component class comment and added
a with_html5_validation preview demonstrating a submit-validating form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ToastComponent — icon + title + body + optional ✕, variants success/error/ warning/info/neutral. Configurable auto-dismiss (default 5s, pass duration: 0 for sticky). Pause-on-hover. Enter/leave transitions via data-state. ToastContainerComponent — fixed-position stacking region. Six positions (top/bottom × left/right/center). Click-through (pointer-events-none); individual toasts re-enable pointer events. FlashHelper — Shipwright::FlashHelper#shipwright_flash_toasts renders one toast per entry in Rails flash, mapping notice/success→success, alert/error →error, warning→warning, info→info. Custom mappings supported. Auto-mixed into ActionController::Base via engine initializer. Turbo Stream pattern documented in ToastContainerComponent class comment: turbo_stream.append(DOM_ID, ToastComponent.new(...)). Vanilla JS wiring matches existing dropdown/checkbox convention. Re-scans on turbo:load, turbo:frame-load, and turbo:before-stream-render so Turbo-appended toasts activate correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Toast JS wiring was previously inlined only in ToastComponent's template, so a page that renders the container alone (and appends toasts via Turbo Stream or JS) had no __shipwrightToastActivate function to call. Extract the script to app/views/shipwright/_toast_init_script and render it from both ToastComponent and ToastContainerComponent. The __shipwrightToastWired guard keeps duplicate renders a no-op. Fixes the Lookbook container preview where live add-toast buttons did nothing because the activator was never defined. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rendering the shared init script via render "shipwright/toast_init_script" failed because the engine's app/views/ is not on the host application's view lookup paths (only view_component and lookbook manage view resolution here). Move the script to a frozen constant on ToastComponent and expose ToastComponent.init_script_html which returns it html_safe. Both the toast and container templates interpolate the constant directly. The __shipwrightToastWired guard still keeps duplicate renders a no-op. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each variant now ships a meaningful default icon: - success → check-circle - error → alert-circle - warning → alert-triangle - info → info - neutral → bell The existing `icon:` prop continues to override any variant default with any Feather icon name. Documented in the class comment with an example. Added a custom_icon preview to the ToastComponentPreview suite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Data table with slot-based API for columns and rows. Each cell receives
a block so consumers render anything (Avatar, Badge, progress bar, icon
group, plain text).
API:
table.with_column(label:, sortable:, sort_direction:, align:, width:)
table.with_row(selected:) do |row|
row.with_cell(align:) { ... }
end
Options: selectable (prepends checkbox column), striped (zebra rows),
density (:comfortable / :compact).
Sort indicator: sortable columns render a chevron button with data-
attributes (data-table-sort, data-table-sort-key, data-table-sort-
direction). No client behavior included — consumer wires via Turbo
Stream, Stimulus, or server round-trip.
Implementation note: cells are captured into SafeBuffer strings inside
the table's renders_many lambda (where view_context is available) via
a RowBuilder helper, rather than via nested slots. Nested-slot lambdas
run before their component is rendered so they have no view_context.
Also adds `chevrons-up-down` bidirectional-sort icon (Lucide-style,
not in base Feather).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Works with Pagy, Kaminari, or any custom paginator — takes plain props
(current_page, total_pages) and a page_url Proc for URL generation, and
optionally a pre-truncated series array (Pagy's pagy.series format works
directly). Auto-truncates with [1, window(current), :gap, last] when no
series is passed.
Variants:
:numeric — ← 1 2 3 … 9 10 → joined in one bordered pill
:numeric_loose — same entries but each button is separately bordered
:simple — "Page X of Y" label + prev/next arrows
(label_position: :left | :right | :center | :hidden)
Also supports a rows-per-page <select> (pass rows_per_page,
rows_per_page_options, rows_per_page_url) alongside the arrows.
Previous/next buttons are disabled (rendered as spans with
aria-disabled="true") at the boundaries. Current page renders as a span
with aria-current="page" instead of a link.
Accepts Pagy's string-encoded series entries (["1", "2", :gap, ...])
directly — normalized to integers + :gap sentinel internally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RadioComponent: single radio button with optional label + caption. Two sizes (:sm, :md), left/right label_position, default/destructive variants. Auto-derives id as "name_value" (Rails convention) so multiple radios sharing a name stay uniquely targetable. RadioGroupComponent: labeled fieldset/legend wrapper around a set of radios sharing one name. Horizontal or vertical layout, optional "(required)" marker. Each option is a ViewComponent slot that forwards to a RadioComponent. Styling strategy matches CheckboxComponent — .group on the label with peer sr-only input and CSS-driven state via group-has-checked: (no JS for the visual toggle). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The gemspec declares view_component as a runtime dependency, but lib/shipwright.rb never required it. Host apps that didn't already `require "view_component"` themselves would hit NameError when Shipwright's components were referenced, despite bundle install having fetched the gem. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lookbook's PreviewCollection#reload_all calls code_object.path.constantize on each discovered preview class, which requires the class to be on Rails' autoload paths. Lookbook's own lookbook.set_autoload_paths initializer ran BEFORE shipwright.lookbook, so Shipwright's preview directory never made it into Zeitwerk's autoload paths. YARD's static parser still found the files, but constantize raised NameError — which Lookbook silently swallows (rescue => e; logger.error; nil). Result: preview entries appeared in the tree but wouldn't render. Fix by scheduling shipwright.lookbook with `before: "lookbook.set_autoload_paths"` and additionally pushing the preview path onto app.config.autoload_paths directly as a belt-and-suspenders. Host apps no longer need to patch their config/application.rb. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Lookbook's `PreviewCollection#reload_all` calls `code_object.path.constantize` on each discovered preview class, which requires the class to be on Rails' autoload paths. Lookbook's own `lookbook.set_autoload_paths` initializer ran BEFORE Shipwright's `shipwright.lookbook` — so the Shipwright preview directory never made it into Zeitwerk's autoload paths.
YARD's static parser still found the files, so the preview entries appeared in the Lookbook tree. But `constantize` raised `NameError`, which Lookbook swallows with `rescue => e; logger.error; nil`. Previews looked registered but wouldn't render.
Fix
Host apps no longer need to patch `config/application.rb` to add Shipwright's preview path manually.
Test plan