stimulus_plumbers 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9de763c041b8b091739e3130ac1b831a6819ca250c1068f91e9bad332892d37b
4
- data.tar.gz: fca860e3e80a04d86b381e3c5e6c38ae791f59195b3a776f6e0c07a1a1ee5db7
3
+ metadata.gz: 76a3529b969c106cec1fef750f3a76a5c05782f31b56e29c8c4640cd105d7dd7
4
+ data.tar.gz: 118813bf270d4eed2901da381d4effa1b663a5558e9c97b6041850ddeb63e572
5
5
  SHA512:
6
- metadata.gz: f279e071f01f01b98b097bb73f906f2b12c7e48bae08af840a25f1324be412b9e120799d9c384c025c7bcda9aba07c088a45efd829ed8de2cd5ac6d4f097fefd
7
- data.tar.gz: 12c31f2d9a804ddadee35f241806f1301f9211f3fa9ab2d2abe03d98b42d7ea1e92ef39f00a739d2fc7f58348efb52194a8ccba099e15ce38786d9cc16028327
6
+ metadata.gz: fccd6c0b45374752edd4583acb07d099668dcbdf3e745165775e86bd2b19603680f55b6641c37dbebe529b317772eceba6e1dc47bcc8b2637ae574009f6bc2b6
7
+ data.tar.gz: e7fe95f7e5f38a551a95cd27ac69466a73602603b6eac243e8469d59ea20e0f96cd9fcad8189742555ac6ba72f071ab92eadc2a9243e686258f6473d12416559
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
4
4
 
5
+ ---
6
+ ## [0.4.4](https://github.com/ryancyq/stimulus-plumbers/compare/stimulus-plumbers-rails/v0.4.3..stimulus-plumbers-rails/v0.4.4) - 2026-06-30
7
+
8
+ ### Dependencies
9
+
10
+ - update npm package dist to have both es + umd + source map - ([64ec309](https://github.com/ryancyq/stimulus-plumbers/commit/64ec309ca171584d1604e4e7d3d3533878ebed7a)) - Ryan Chang
11
+
5
12
  ---
6
13
  ## [0.4.3](https://github.com/ryancyq/stimulus-plumbers/compare/stimulus-plumbers-rails/v0.4.2..stimulus-plumbers-rails/v0.4.3) - 2026-06-30
7
14
 
@@ -0,0 +1,33 @@
1
+ # Rails Package Architecture
2
+
3
+ ## Schema Ranges Convention
4
+
5
+ Validation ranges for theme schema params live under `lib/stimulus_plumbers/themes/schema/`.
6
+
7
+ **Where ranges live — the branching rule:**
8
+
9
+ - If a component branches on the values internally (e.g. `when *FLOATING_TYPES`), the component owns the constant (`Form::Field::FLOATING_TYPES`). The schema references it directly — no alias.
10
+ - If the component only passes the value through to the theme, the schema owns the range (e.g. `Schema::Button::Ranges::TYPE`).
11
+
12
+ **Namespace rules:**
13
+
14
+ **Namespaces:**
15
+
16
+ - `Schema::Ranges` — cross-cutting constants (e.g. `BOOL`)
17
+ - `Schema::<Component>::Ranges` — per-component constants
18
+ - `Schema::Link::Ranges` — link-specific; uses `:default` as base instead of `:primary`
19
+ - `Schema::Form::<Input>::Ranges` — per-input-type constants
20
+ - `Schema::Form::Ranges` — form-level ranges (`LAYOUT`, `VARIANT`)
21
+
22
+ **Guidelines:**
23
+
24
+ - **No local aliases** — never re-export another module's constant inside a `Ranges` module.
25
+ - **Remove unused constants** — don't keep range constants with no call sites in `schema.rb`.
26
+
27
+ ## Icon-Only Detection Contract (Button + Link)
28
+
29
+ `Button#build_button` and `Link#build_content` always wrap non-nil text/block content in `<span>`. When content is nil and no block is given (icon-only), no `<span>` is rendered.
30
+
31
+ The active theme uses `:has(> span)` / `:not(:has(> span))` CSS to distinguish icon-only from text buttons without any Ruby flag.
32
+
33
+ **Do not change this contract** without updating `stimulus-plumbers-tailwind`'s `Button::LAYOUT` and `Link::BUTTON` constants accordingly. See [tailwind architecture doc](../../stimulus-plumbers-tailwind/docs/architecture.md#icon-only-square-pattern).
@@ -0,0 +1,84 @@
1
+ # Avatar
2
+
3
+ Rails helper for rendering a user avatar — image, initials, or fallback silhouette.
4
+
5
+ ## Helper
6
+
7
+ ### `sp_avatar`
8
+
9
+ ```erb
10
+ <%# Image from URL %>
11
+ <%= sp_avatar url: user.avatar_url, name: user.name %>
12
+
13
+ <%# Initials (SVG text) %>
14
+ <%= sp_avatar initials: "RC", name: "Ryan Chang" %>
15
+
16
+ <%# Named variant + size %>
17
+ <%= sp_avatar initials: "AB", name: "Alice B", variant: :purple, size: :lg %>
18
+
19
+ <%# Auto-selected variant (hashed from name) %>
20
+ <%= sp_avatar name: "Ryan Chang" %>
21
+
22
+ <%# Custom content block %>
23
+ <%= sp_avatar(name: "Ryan") { image_tag "custom.png" } %>
24
+ ```
25
+
26
+ | Option | Default | Description |
27
+ | ---------------- | ------- | ------------------------------------------------------------------------------------------ |
28
+ | `name` | `nil` | Used for `aria-label` and auto-variant selection |
29
+ | `initials` | `nil` | Renders an SVG with the initials text; used when no `url` or block is given |
30
+ | `url` | `nil` | Image source; renders `<img>` with `alt="[name]'s avatar"` and `onerror` fallback |
31
+ | `variant` | `nil` | Color variant for the avatar background — auto-selected from theme range if omitted |
32
+ | `size` | `:md` | Size — values defined by the active theme (e.g. `:xs` \| `:sm` \| `:md` \| `:lg` \| `:xl`) |
33
+ | `**html_options` | — | Forwarded to the `<span>` wrapper |
34
+
35
+ **Render modes (in priority order):**
36
+
37
+ | Condition | Output |
38
+ | -------------------- | ------------------------------ |
39
+ | Block given | Custom content inside `<span>` |
40
+ | `url:` provided | `<img>` inside `<span>` |
41
+ | `initials:` provided | SVG with initials text |
42
+ | None of the above | SVG fallback silhouette |
43
+
44
+ ---
45
+
46
+ ## Rendered HTML Structure
47
+
48
+ ```html
49
+ <span role="img" aria-label="Ryan Chang" class="[avatar theme classes]">
50
+ <!-- image mode -->
51
+ <img
52
+ src="[url]"
53
+ alt="Ryan Chang's avatar"
54
+ class="[avatar_image theme classes]"
55
+ />
56
+
57
+ <!-- initials mode -->
58
+ <svg viewBox="0 0 40 40">
59
+ <text
60
+ x="50%"
61
+ y="50%"
62
+ dy="0.35em"
63
+ text-anchor="middle"
64
+ fill="currentColor"
65
+ font-size="20"
66
+ >
67
+ RC
68
+ </text>
69
+ </svg>
70
+
71
+ <!-- fallback mode -->
72
+ <svg viewBox="0 0 40 40">
73
+ <path fill="currentColor" d="..." />
74
+ </svg>
75
+ </span>
76
+ ```
77
+
78
+ ---
79
+
80
+ ## ARIA
81
+
82
+ - Always renders `role="img"` with `aria-label` set to `name` (when provided).
83
+ - Image `alt` is set to `"[name]'s avatar"`; for anonymous avatars `alt=""` is used.
84
+ - `onerror` on `<img>` clears the `src` to prevent broken-image icons.
@@ -0,0 +1,135 @@
1
+ # Button
2
+
3
+ Rails helpers for rendering themed, accessible buttons and links.
4
+
5
+ ## Helpers
6
+
7
+ ### `sp_button`
8
+
9
+ ```erb
10
+ <%= sp_button "Save" %>
11
+ <%= sp_button(icon_leading: "check") { "Confirm" } %>
12
+ <%= sp_button "Delete", type: :default, variant: :destructive, size: :sm %>
13
+ ```
14
+
15
+ | Option | Default | Description |
16
+ | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------- |
17
+ | `content` | `nil` | Button label — positional arg or block |
18
+ | `type` | `:default` | Visual style — `:default` \| `:outline` \| `:ghost` \| `:fab` \| `:fab_outline` \| `:dashed` \| `:card` |
19
+ | `variant` | `:primary` | Color source — `:primary` \| `:secondary` \| `:tertiary` \| `:success` \| `:destructive` \| `:warning` \| `:info` |
20
+ | `size` | `:md` | Size — `:xs` \| `:sm` \| `:md` \| `:lg` \| `:xl` (ignored when `type: :card`) |
21
+ | `icon_leading` | `nil` | Icon rendered **before** the label — icon name (string or symbol) or callable |
22
+ | `icon_trailing` | `nil` | Icon rendered **after** the label — icon name (string or symbol) or callable |
23
+ | `**html_options` | — | Forwarded to the `<button>` element |
24
+
25
+ **`type:` — visual style**
26
+
27
+ | Value | Appearance |
28
+ | -------------- | ------------------------------------------------------------------------------ |
29
+ | `:default` | Filled — solid background from `variant` color |
30
+ | `:outline` | Surface background, colored border and text; subtle tint on hover |
31
+ | `:ghost` | Transparent, no border; subtle tint on hover |
32
+ | `:fab` | Floating action button — `rounded-full`, elevated shadow, filled |
33
+ | `:fab_outline` | Floating action button — `rounded-full`, elevated shadow, fills solid on hover |
34
+ | `:dashed` | Dashed border, surface background |
35
+ | `:card` | Full-padding card — `flex-1`, `justify-start`; `size:` ignored |
36
+
37
+ **`variant:` — color source**
38
+
39
+ | Value | Color tokens used |
40
+ | -------------- | ------------------------------ |
41
+ | `:primary` | `--sp-color-primary-*` |
42
+ | `:secondary` | `--sp-color-secondary-*` |
43
+ | `:tertiary` | `--sp-color-muted-*` (neutral) |
44
+ | `:success` | `--sp-color-success-*` |
45
+ | `:destructive` | `--sp-color-destructive-*` |
46
+ | `:warning` | `--sp-color-warning-*` |
47
+ | `:info` | `--sp-color-info-*` |
48
+
49
+ **Icon values:**
50
+
51
+ - String or symbol — resolved by name through the active theme's icon registry, rendered via `sp_icon` with `button_icon` theme classes applied
52
+ - Callable (e.g. `-> { tag.span "★" }`) — rendered as-is, no theme applied
53
+
54
+ ### `sp_button_group`
55
+
56
+ Wraps buttons in a themed container `<div>`.
57
+
58
+ ```erb
59
+ <%= sp_button_group do %>
60
+ <%= sp_button "Cancel", type: :outline %>
61
+ <%= sp_button "Save" %>
62
+ <% end %>
63
+
64
+ <%= sp_button_group(layout: :stacked) do %>
65
+ ...
66
+ <% end %>
67
+ ```
68
+
69
+ | Option | Default | Description |
70
+ | ---------------- | --------- | ------------------------------------------ |
71
+ | `layout` | `:inline` | Layout direction — `:inline` \| `:stacked` |
72
+ | `**html_options` | — | Forwarded to the wrapper `<div>` |
73
+
74
+ ---
75
+
76
+ ## Rendered HTML Structure
77
+
78
+ ### Button (action)
79
+
80
+ Text content is always wrapped in a `<span>`:
81
+
82
+ ```html
83
+ <button type="button" class="[theme classes]"><span>Save</span></button>
84
+ ```
85
+
86
+ ### With icons
87
+
88
+ Icons are rendered **inside** the `<button>`, before or after the label `<span>`:
89
+
90
+ ```html
91
+ <button type="button" class="[theme classes]">
92
+ <svg aria-hidden="true" class="[button_icon theme classes]">
93
+ <!-- icon svg -->
94
+ </svg>
95
+ <span>Confirm</span>
96
+ </button>
97
+ ```
98
+
99
+ ```html
100
+ <button type="button" class="[theme classes]">
101
+ <span>Next</span>
102
+ <svg aria-hidden="true" class="[button_icon theme classes]">
103
+ <!-- icon svg -->
104
+ </svg>
105
+ </button>
106
+ ```
107
+
108
+ ### Icon only
109
+
110
+ When no text is provided, no `<span>` is rendered. The theme uses `:has(> span)` to detect this and applies `aspect-square` + `px-0`, making the button square (or a circle for `type: :fab` / `:fab_outline`):
111
+
112
+ ```html
113
+ <button type="button" aria-label="Add" class="[theme classes]">
114
+ <svg aria-hidden="true" class="[button_icon theme classes]">
115
+ <!-- icon svg -->
116
+ </svg>
117
+ </button>
118
+ ```
119
+
120
+ ### Button group
121
+
122
+ ```html
123
+ <div role="group" class="[button_group theme classes]">
124
+ <button type="button">Cancel</button>
125
+ <button type="button">Save</button>
126
+ </div>
127
+ ```
128
+
129
+ ---
130
+
131
+ ## ARIA
132
+
133
+ - `<button>` always has `type="button"` to prevent accidental form submission.
134
+ - Icon-only buttons must supply an accessible label via `aria: { label: "..." }` in `html_options`.
135
+ - For navigation actions (links), use `sp_link` instead — it renders an `<a>` element.
@@ -0,0 +1,223 @@
1
+ # Calendar
2
+
3
+ Rails helpers for rendering calendar grids. Two rendering modes are supported:
4
+
5
+ - **CSR (Stimulus)** — JS controller renders the month grid client-side; drill-down (year/decade views) driven by `combobox-date`.
6
+ - **SSR (Turbo)** — Server renders each view as a Turbo Frame; controller handles navigation via Turbo Drive.
7
+
8
+ ---
9
+
10
+ ## CSR — Stimulus calendar
11
+
12
+ ### Setup
13
+
14
+ ```ruby
15
+ module ApplicationHelper
16
+ include StimulusPlumbers::Helpers::CalendarHelper
17
+ end
18
+ ```
19
+
20
+ ### Usage
21
+
22
+ ```erb
23
+ <%# Current month (defaults to today) %>
24
+ <%= sp_calendar_month %>
25
+
26
+ <%# Navigate to a specific month %>
27
+ <%= sp_calendar_month(date: Date.new(2024, 2, 1)) %>
28
+
29
+ <%# Inside a combobox-date wrapper for drill-down navigation %>
30
+ <div data-controller="combobox-date"
31
+ data-combobox-date-calendar-month-outlet="[data-controller~='calendar-month']">
32
+ <%= sp_calendar_month(date: @date) %>
33
+ </div>
34
+ ```
35
+
36
+ | Option | Description |
37
+ | ---------------- | ------------------------------------------------------------------------------------ |
38
+ | `date` | `Date` — sets `year-value`, `month-value` (0-indexed), `day-value` on the controller |
39
+ | `**html_options` | Forwarded to the `calendar-month` controller root element |
40
+
41
+ For the JS controller API (targets, values, keyboard behaviour), see the [JS package docs](../../../stimulus-plumbers/docs/component/calendar.md).
42
+
43
+ ### Rendered HTML structure
44
+
45
+ `sp_calendar_month` renders a `calendar-month` controller shell. The JS controller populates the day/week grids on connect and on each navigation. Year/decade drill-down views are separate controllers wired by `combobox-date` — see the [JS package docs](../../../stimulus-plumbers/docs/component/calendar.md).
46
+
47
+ ```html
48
+ <!-- sp_calendar_month(date: Date.new(2024, 2, 15)) -->
49
+ <div
50
+ data-controller="calendar-month"
51
+ data-calendar-month-year-value="2024"
52
+ data-calendar-month-month-value="1"
53
+ data-calendar-month-day-value="15"
54
+ role="grid"
55
+ >
56
+ <!-- days-of-week header row (JS-populated) -->
57
+ <div data-calendar-month-target="daysOfWeek"></div>
58
+
59
+ <!-- days-of-month body (JS-populated) -->
60
+ <div role="rowgroup" data-calendar-month-target="daysOfMonth"></div>
61
+ </div>
62
+ ```
63
+
64
+ ---
65
+
66
+ ## SSR — Turbo calendar
67
+
68
+ ### Setup
69
+
70
+ ```ruby
71
+ module ApplicationHelper
72
+ include StimulusPlumbers::Helpers::CalendarTurboHelper
73
+ end
74
+ ```
75
+
76
+ ### Usage
77
+
78
+ `sp_calendar_turbo` renders all three view frames at once (month visible, year/decade hidden). Each frame is a `<turbo-frame>` targeted by `combobox-date`.
79
+
80
+ ```erb
81
+ <%= sp_calendar_turbo(date: @date, today: @today, selectable: true) %>
82
+ ```
83
+
84
+ Individual view helpers render a single view (used for Turbo Frame responses). They accept the same common options plus extras:
85
+
86
+ ```erb
87
+ <%# Month view (days grid) %>
88
+ <%= sp_calendar_turbo_month(date: @date, today: @today, weekday_format: :narrow) %>
89
+
90
+ <%# Year view (months grid) %>
91
+ <%= sp_calendar_turbo_year(date: @date, today: @today, month_format: :long) %>
92
+
93
+ <%# Decade view (years grid) %>
94
+ <%= sp_calendar_turbo_decade(date: @date, today: @today) %>
95
+ ```
96
+
97
+ | Helper | Extra option | Description |
98
+ | ------------------------- | ---------------- | ------------------------------------------------------------------ |
99
+ | `sp_calendar_turbo_month` | `weekday_format` | `:short` (default) \| `:long` \| `:narrow` — weekday header format |
100
+ | `sp_calendar_turbo_year` | `month_format` | `:short` (default) \| `:long` \| `:narrow` — month button format |
101
+
102
+ **Common options** (all helpers and `sp_calendar_turbo`):
103
+
104
+ | Option | Description |
105
+ | ------------------- | -------------------------------------------------------- |
106
+ | `date` | `Date` — the month/year being displayed |
107
+ | `today` | `Date` — used to mark today and aria-current |
108
+ | `selectable` | `true` renders day cells as `<button>` (month only) |
109
+ | `selected_date` | `Date` — marks the selected cell with aria-selected |
110
+ | `show_other_months` | `true` shows padding days from adjacent months |
111
+ | `since` | `Date` — minimum selectable date (disables earlier days) |
112
+ | `till` | `Date` — maximum selectable date (disables later days) |
113
+ | `disabled_months` | Array — month numbers/names to disable in the year view |
114
+ | `disabled_years` | Array — years to disable in the decade view |
115
+
116
+ ### Rendered HTML structure
117
+
118
+ #### Month view (`sp_calendar_turbo_month`)
119
+
120
+ ```html
121
+ <!-- sp_calendar_turbo_month(date: @date, today: @today, selectable: true) -->
122
+ <div role="grid" data-controller="calendar-month-selector">
123
+ <!-- weekday column headers -->
124
+ <div role="row">
125
+ <span role="columnheader">Su</span>
126
+ <!-- … -->
127
+ </div>
128
+
129
+ <!-- day cells -->
130
+ <div role="rowgroup">
131
+ <div role="row">
132
+ <!-- selectable: true → <button>; selectable: false → <span> -->
133
+ <button role="gridcell" tabindex="0" aria-selected="false">
134
+ <time datetime="2024-02-01">1</time>
135
+ </button>
136
+ <!-- … -->
137
+ </div>
138
+ </div>
139
+ </div>
140
+ ```
141
+
142
+ #### Year view (`sp_calendar_turbo_year`)
143
+
144
+ Displays 12 month buttons in a 4-column grid. Used when drilling up from the month view.
145
+
146
+ ```html
147
+ <!-- sp_calendar_turbo_year(date: @date, today: @today) -->
148
+ <div
149
+ role="grid"
150
+ aria-label="Year view"
151
+ data-controller="calendar-year-selector"
152
+ >
153
+ <div role="rowgroup">
154
+ <div role="row">
155
+ <button
156
+ role="gridcell"
157
+ data-month="1"
158
+ aria-current="month"
159
+ aria-selected="false"
160
+ >
161
+ Jan
162
+ </button>
163
+ <!-- … 11 more months … -->
164
+ </div>
165
+ </div>
166
+ </div>
167
+ ```
168
+
169
+ #### Decade view (`sp_calendar_turbo_decade`)
170
+
171
+ Displays 10 year buttons in a 4-column grid (plus 2 buffer cells, disabled). Used when drilling up from the year view.
172
+
173
+ ```html
174
+ <!-- sp_calendar_turbo_decade(date: @date, today: @today) -->
175
+ <div
176
+ role="grid"
177
+ aria-label="Decade view"
178
+ data-controller="calendar-decade-selector"
179
+ >
180
+ <div role="rowgroup">
181
+ <div role="row">
182
+ <button
183
+ role="gridcell"
184
+ data-year="2020"
185
+ aria-current="year"
186
+ aria-selected="false"
187
+ >
188
+ 2020
189
+ </button>
190
+ <!-- … 9 more years … -->
191
+ </div>
192
+ </div>
193
+ </div>
194
+ ```
195
+
196
+ #### Full Turbo setup (`sp_calendar_turbo`)
197
+
198
+ Renders all three frames. Used inside a `combobox-date` controller for drill-down navigation.
199
+
200
+ ```html
201
+ <!-- month view frame — visible by default -->
202
+ <turbo-frame id="calendar-month-frame" data-combobox-date-target="monthView">
203
+ <!-- sp_calendar_turbo_month output -->
204
+ </turbo-frame>
205
+
206
+ <!-- year view frame — hidden until drill-up -->
207
+ <turbo-frame
208
+ id="calendar-year-frame"
209
+ hidden
210
+ data-combobox-date-target="yearView"
211
+ >
212
+ <!-- sp_calendar_turbo_year output -->
213
+ </turbo-frame>
214
+
215
+ <!-- decade view frame — hidden until drill-up twice -->
216
+ <turbo-frame
217
+ id="calendar-decade-frame"
218
+ hidden
219
+ data-combobox-date-target="decadeView"
220
+ >
221
+ <!-- sp_calendar_turbo_decade output -->
222
+ </turbo-frame>
223
+ ```
@@ -0,0 +1,92 @@
1
+ # Card
2
+
3
+ Rails helper for rendering a themed card with optional icon, title, body, and action slots.
4
+
5
+ ## Helper
6
+
7
+ ### `sp_card`
8
+
9
+ ```erb
10
+ <%# Minimal %>
11
+ <%= sp_card do |card| %>
12
+ <% card.with_body { "Simple content." } %>
13
+ <% end %>
14
+
15
+ <%# With title and icon %>
16
+ <%= sp_card(title_tag: :h3) do |card| %>
17
+ <% card.with_icon("user") %>
18
+ <% card.with_title("Account") %>
19
+ <% card.with_body { "Your account is active." } %>
20
+ <% end %>
21
+
22
+ <%# With action link %>
23
+ <%= sp_card do |card| %>
24
+ <% card.with_title("Settings") %>
25
+ <% card.with_action("Manage", url: settings_path) %>
26
+ <% end %>
27
+
28
+ <%# With action button (no url) %>
29
+ <%= sp_card do |card| %>
30
+ <% card.with_action("Open") %>
31
+ <% end %>
32
+ ```
33
+
34
+ | Option | Default | Description |
35
+ | ---------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ |
36
+ | `variant:` | `:tertiary` | Theme variant — `:primary` \| `:secondary` \| `:tertiary` \| `:success` \| `:destructive` \| `:warning` \| `:info` |
37
+ | `title_tag:` | `:h2` | HTML tag used by `with_title` |
38
+ | `**html_options` | — | Forwarded to the outer `<div>` |
39
+
40
+ ### Slot methods (yielded as `card`)
41
+
42
+ | Slot method | Description |
43
+ | ---------------------------------- | ---------------------------------------------------------------------------- |
44
+ | `card.with_icon(name_or_html)` | Icon before the title — string/symbol resolves via `Icon`; HTML passed as-is |
45
+ | `card.with_title(text)` | Title text rendered as `title_tag` |
46
+ | `card.with_body { content }` | Body content — block required |
47
+ | `card.with_action(text, url: nil)` | Action link (`<a>`) when `url:` present; `<button>` otherwise |
48
+
49
+ `with_action` raises `ArgumentError` when `url:` is given but no content (text or block).
50
+
51
+ ---
52
+
53
+ ## Rendered HTML Structure
54
+
55
+ ```html
56
+ <div class="[card theme classes]">
57
+ <!-- header: rendered when icon or title is present -->
58
+ <div class="[card_header theme classes]">
59
+ <svg aria-hidden="true" class="[card_icon theme classes]">...</svg>
60
+ <h2 class="[card_title theme classes]">Account</h2>
61
+ </div>
62
+
63
+ <!-- body -->
64
+ <div class="[card_body theme classes]">Your account is active.</div>
65
+
66
+ <!-- action -->
67
+ <div class="[card_action theme classes]">
68
+ <a href="/settings">Manage</a>
69
+ </div>
70
+ </div>
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Theme keys
76
+
77
+ | Key | Element | Variants |
78
+ | ------------- | ----------------------------------- | --------------------------------------------- |
79
+ | `card` | Outer `<div>` | `variant: :primary\|:secondary\|:tertiary\|…` |
80
+ | `card_header` | Header wrapper `<div>` (icon+title) | — |
81
+ | `card_icon` | Icon inside the header | — |
82
+ | `card_title` | Title element (`h2` etc.) | — |
83
+ | `card_body` | Body `<div>` | — |
84
+ | `card_action` | Action `<div>` | — |
85
+
86
+ ---
87
+
88
+ ## ARIA
89
+
90
+ - `sp_card` is a layout container — no ARIA roles are added automatically.
91
+ - Supply `role:` or `aria:` via `html_options` when the card plays a semantic role (e.g. `role: "region"`, `aria: { label: "..." }`).
92
+ - For interactive card-style inputs (radio/checkbox), see `f.choice` with `type: :card` in the [form doc](form.md).