stimulus_plumbers 0.4.5 → 0.4.8

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/README.md +16 -0
  4. data/docs/component/button.md +1 -1
  5. data/docs/component/calendar.md +11 -4
  6. data/docs/component/card.md +14 -12
  7. data/docs/component/combobox.md +2 -2
  8. data/docs/component/form.md +32 -6
  9. data/docs/component/list.md +2 -2
  10. data/docs/component/plumber.md +22 -0
  11. data/docs/component/theme.md +15 -2
  12. data/docs/component/timeline.md +2 -2
  13. data/docs/guide.md +27 -0
  14. data/lib/generators/stimulus_plumbers/install/install_generator.rb +29 -0
  15. data/lib/stimulus_plumbers/components/button.rb +1 -1
  16. data/lib/stimulus_plumbers/components/card.rb +1 -1
  17. data/lib/stimulus_plumbers/components/combobox.rb +1 -1
  18. data/lib/stimulus_plumbers/components/link.rb +1 -1
  19. data/lib/stimulus_plumbers/components/list/item.rb +1 -1
  20. data/lib/stimulus_plumbers/components/timeline/event.rb +1 -1
  21. data/lib/stimulus_plumbers/engine.rb +4 -0
  22. data/lib/stimulus_plumbers/generators/css_entrypoint.rb +74 -0
  23. data/lib/stimulus_plumbers/generators/tokens_directive.rb +20 -0
  24. data/lib/stimulus_plumbers/plumber/slots.rb +10 -3
  25. data/lib/stimulus_plumbers/version.rb +1 -1
  26. data/lib/tasks/stimulus_plumbers.rake +18 -0
  27. data/vendor/ARIA.md +88 -0
  28. data/vendor/controller/docs/calendar.md +215 -0
  29. data/vendor/controller/docs/clipboard.md +61 -0
  30. data/vendor/controller/docs/combobox.md +242 -0
  31. data/vendor/controller/docs/dismisser.md +39 -0
  32. data/vendor/controller/docs/flipper.md +46 -0
  33. data/vendor/controller/docs/input-clearable.md +46 -0
  34. data/vendor/controller/docs/input-formatter.md +90 -0
  35. data/vendor/controller/docs/modal.md +60 -0
  36. data/vendor/controller/docs/panner.md +24 -0
  37. data/vendor/controller/docs/popover.md +113 -0
  38. data/vendor/controller/docs/timeline.md +84 -0
  39. data/vendor/controller/guide.md +16 -0
  40. metadata +20 -2
  41. /data/vendor/{controllers.manifest.json → controller/manifest.json} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76a3529b969c106cec1fef750f3a76a5c05782f31b56e29c8c4640cd105d7dd7
4
- data.tar.gz: 118813bf270d4eed2901da381d4effa1b663a5558e9c97b6041850ddeb63e572
3
+ metadata.gz: b138245a7a9711414d5c7bf052dbd55d42006f5e785626fd94bce96ffab6281d
4
+ data.tar.gz: e2e15e550f5678eecbfc7310628759590212fae21aa42c7eab60d7c049f2a47f
5
5
  SHA512:
6
- metadata.gz: fccd6c0b45374752edd4583acb07d099668dcbdf3e745165775e86bd2b19603680f55b6641c37dbebe529b317772eceba6e1dc47bcc8b2637ae574009f6bc2b6
7
- data.tar.gz: e7fe95f7e5f38a551a95cd27ac69466a73602603b6eac243e8469d59ea20e0f96cd9fcad8189742555ac6ba72f071ab92eadc2a9243e686258f6473d12416559
6
+ metadata.gz: 624a411ff21cb32deebdde9a3c68c0d0c43fed7ade306cc7299fdaadca7604a3053a2f9251df1608af393fc0129bffd75b6e244b9fd9e0477a4df72f859e0ddb
7
+ data.tar.gz: 8176752b32e5e730e7c8a47ba5aa44d96b591a38c44d0a71ad6d2d147404227b02aa581b00db36711da357cda872b96260187b62749e74f652acb2bd1a26a970
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.5](https://github.com/ryancyq/stimulus-plumbers/compare/stimulus-plumbers-rails/v0.4.4..stimulus-plumbers-rails/v0.4.5) - 2026-07-01
7
+
8
+ ### Bug Fixes
9
+
10
+ - mcp executable ([#151](https://github.com/ryancyq/stimulus-plumbers/issues/151)) - ([5e772ce](https://github.com/ryancyq/stimulus-plumbers/commit/5e772ce2d8ceb7c517c2e7fdd44d3438b1e38139)) - Ryan Chang
11
+
5
12
  ---
6
13
  ## [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
14
 
data/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  Rails helpers for accessible, WCAG 2.1 AA compliant UI components built on [Stimulus](https://stimulus.hotwired.dev/). Pairs with the [`@stimulus-plumbers/controllers`](../stimulus-plumbers) npm package.
8
8
 
9
+ See [docs/guide.md](docs/guide.md) for a quick guide to building forms and views with this gem.
10
+
9
11
  ## Requirements
10
12
 
11
13
  - Ruby >= 3.0
@@ -40,6 +42,20 @@ Or use the form builder globally:
40
42
  config.action_view.default_form_builder = StimulusPlumbers::Form::Builder
41
43
  ```
42
44
 
45
+ Run the install generator once to inject the `tokens.css` import into your CSS entry file:
46
+
47
+ ```bash
48
+ bin/rails generate stimulus_plumbers:install
49
+ ```
50
+
51
+ The generator checks these files in order:
52
+
53
+ - `app/assets/stylesheets/application.tailwind.css`
54
+ - `app/assets/stylesheets/application.css`
55
+ - `app/javascript/entrypoints/application.css`
56
+
57
+ Override the detected file with `STIMULUS_PLUMBERS_CSS_FILE=/path/to/entry.css`. After the initial install, the import stays current automatically — the engine hooks `stimulus_plumbers:install` as a prerequisite of `assets:precompile`.
58
+
43
59
  ## Components
44
60
 
45
61
  | Component | Helper(s) | Docs |
@@ -107,7 +107,7 @@ Icons are rendered **inside** the `<button>`, before or after the label `<span>`
107
107
 
108
108
  ### Icon only
109
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`):
110
+ When no text is provided, no `<span>` is rendered, and the button becomes square (or a circle for `type: :fab` / `:fab_outline`):
111
111
 
112
112
  ```html
113
113
  <button type="button" aria-label="Add" class="[theme classes]">
@@ -94,10 +94,17 @@ Individual view helpers render a single view (used for Turbo Frame responses). T
94
94
  <%= sp_calendar_turbo_decade(date: @date, today: @today) %>
95
95
  ```
96
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 |
97
+ **`sp_calendar_turbo_month`** extra option:
98
+
99
+ | Option | Description |
100
+ | ---------------- | ------------------------------------------------------------------ |
101
+ | `weekday_format` | `:short` (default) \| `:long` \| `:narrow` — weekday header format |
102
+
103
+ **`sp_calendar_turbo_year`** — extra option:
104
+
105
+ | Option | Description |
106
+ | -------------- | ---------------------------------------------------------------- |
107
+ | `month_format` | `:short` (default) \| `:long` \| `:narrow` — month button format |
101
108
 
102
109
  **Common options** (all helpers and `sp_calendar_turbo`):
103
110
 
@@ -63,10 +63,12 @@ Rails helper for rendering a themed card with optional icon, title, body, and ac
63
63
  <!-- body -->
64
64
  <div class="[card_body theme classes]">Your account is active.</div>
65
65
 
66
- <!-- action -->
67
- <div class="[card_action theme classes]">
68
- <a href="/settings">Manage</a>
69
- </div>
66
+ <!-- action: a ghost/tertiary sp_button, with card_action classes merged directly onto it (no wrapping div) -->
67
+ <a
68
+ href="/settings"
69
+ class="[button ghost/tertiary + card_action theme classes]"
70
+ ><span>Manage</span></a
71
+ >
70
72
  </div>
71
73
  ```
72
74
 
@@ -74,14 +76,14 @@ Rails helper for rendering a themed card with optional icon, title, body, and ac
74
76
 
75
77
  ## Theme keys
76
78
 
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>` | — |
79
+ | Key | Element | Variants |
80
+ | ------------- | ------------------------------------------------------------- | --------------------------------------------- |
81
+ | `card` | Outer `<div>` | `variant: :primary\|:secondary\|:tertiary\|…` |
82
+ | `card_header` | Header wrapper `<div>` (icon+title) | — |
83
+ | `card_icon` | Icon inside the header | — |
84
+ | `card_title` | Title element (`h2` etc.) | — |
85
+ | `card_body` | Body `<div>` | — |
86
+ | `card_action` | Merged onto the action `<a>`/`<button>` (no wrapping element) | — |
85
87
 
86
88
  ---
87
89
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Rails helpers that render fully-wired combobox components. Each variant shares the same wrapper structure (`input-combobox` + `input-formatter`) and differs only in its popover body.
4
4
 
5
- See [docs/component/combobox.md](../../stimulus-plumbers/docs/component/combobox.md) in the JS package for the underlying controller API.
5
+ See [docs/component/combobox.md](../../../stimulus-plumbers/docs/component/combobox.md) in the JS package for the underlying controller API.
6
6
 
7
7
  ## Helpers
8
8
 
@@ -127,7 +127,7 @@ iOS-style drum/scroll-wheel time picker.
127
127
 
128
128
  ## Form Builder
129
129
 
130
- `StimulusPlumbers::Form::Builder` exposes combobox-backed fields through purpose-specific methods rather than a single generic helper. See [form_builder.md](form_builder.md) for the full API.
130
+ `StimulusPlumbers::Form::Builder` exposes combobox-backed fields through purpose-specific methods rather than a single generic helper. See [form.md](form.md) for the full API.
131
131
 
132
132
  ```erb
133
133
  <%= form_with model: @user, builder: StimulusPlumbers::Form::Builder do |f| %>
@@ -118,9 +118,34 @@ Three methods render a complete, accessible field:
118
118
 
119
119
  Use `f.time_field` for a plain `<input type="time">`.
120
120
 
121
- **Select** (`as: :select`) — read-only listbox backed by `combobox-dropdown`. Accepts `choices:`, `include_blank:`, `prompt:`, `selected:`. Use `f.select` for a native `<select>`.
121
+ **Select** (`as: :select`) — read-only listbox backed by `combobox-dropdown`.
122
122
 
123
- **Search** (`as: :search`) editable typeahead backed by `combobox-dropdown`. Accepts `choices:`, `url:`, `clearable:`. Use `f.search_field` for a native `<input type="search">`.
123
+ | Option | Values | Default | Description |
124
+ | --------------- | -------------- | ------- | ------------------------------------------------------------- |
125
+ | `choices` | Array | `[]` | `[label, value]` pairs, or a flat array of strings |
126
+ | `include_blank` | Boolean/String | `nil` | Prepends a blank option; String overrides its label |
127
+ | `prompt` | Boolean/String | `nil` | Prepends a disabled placeholder option |
128
+ | `selected` | Value | `nil` | Pre-selected value; defaults to the attribute's current value |
129
+
130
+ Use `f.select` for a native `<select>`.
131
+
132
+ **Search** (`as: :search`) — editable typeahead backed by `combobox-dropdown`.
133
+
134
+ | Option | Values | Default | Description |
135
+ | ----------- | ------- | ------- | ----------------------------------------------------------------------------------- |
136
+ | `choices` | Array | `[]` | Client-side options to filter; omit when using `url:` |
137
+ | `url` | String | `nil` | Server-side endpoint, receives `?q=<query>`, returns `<li role="option">` fragments |
138
+ | `clearable` | Boolean | `false` | Adds a clear button that resets the input |
139
+
140
+ Use `f.search_field` for a native `<input type="search">`.
141
+
142
+ **Password** (`as: :password`) — reveal-toggle wrapper backed by `input-formatter`.
143
+
144
+ | Option | Values | Default | Description |
145
+ | ------------ | ------- | ------- | ------------------------------------------- |
146
+ | `revealable` | Boolean | `false` | Adds a show/hide toggle button on the input |
147
+
148
+ Use `f.password_field` for a plain `<input type="password">` (also accepts `revealable:`).
124
149
 
125
150
  ---
126
151
 
@@ -169,9 +194,9 @@ Renders a `<fieldset>` / `<legend>` for accessible grouping, or a single checkbo
169
194
 
170
195
  **Card / button behaviour:**
171
196
 
172
- - **Checkbox card** — input visible on right; card border changes on check via `has-[:checked]:`
173
- - **Radio card** — input hidden (`hidden peer`); entire card is the clickable area via `peer-checked:`
174
- - **Radio button** — input hidden (`hidden peer`); inline pill style via `peer-checked:`
197
+ - **Checkbox card** — input visible on the right; the card border highlights when checked
198
+ - **Radio card** — input visually hidden; the whole card is the clickable target and highlights when selected
199
+ - **Radio button** — input visually hidden; renders as an inline pill that highlights when selected
175
200
 
176
201
  ---
177
202
 
@@ -185,7 +210,8 @@ Renders a `<fieldset>` / `<legend>` for accessible grouping, or a single checkbo
185
210
  <input
186
211
  id="user_email"
187
212
  type="email"
188
- aria-describedby="user_email_hint"
213
+ aria-describedby="user_email_hint user_email_error"
214
+ aria-invalid="true"
189
215
  required
190
216
  aria-required="true"
191
217
  />
@@ -39,7 +39,7 @@ Rails helper for rendering an accessible list of links, buttons, or grouped sect
39
39
  | `role:` | `"list"` | ARIA role on the `<ul>` — use `"menu"` for interactive menus |
40
40
  | `**html_options` | — | Forwarded to the `<ul>` |
41
41
 
42
- ### `list.section(title:, description:, **html_options)`
42
+ ### `list.section(title:, description:, **html_options, &block)`
43
43
 
44
44
  Renders a `<li>` with an optional heading/description and a nested `<ul>`.
45
45
 
@@ -60,7 +60,7 @@ Renders a `<li>` containing an `<a>` (when `url:` present) or `<button>`.
60
60
  | `target:` | `nil` | Forwarded to the `<a>` (e.g. `"_blank"`) |
61
61
  | `**html_options` | — | Forwarded to the inner `<a>` or `<button>` |
62
62
 
63
- When `target: "_blank"` is set, `icon_trailing: "external-link"` is added automatically.
63
+ When `url:` is present and `target: "_blank"` is set, `icon_trailing: "external-link"` is added automatically.
64
64
 
65
65
  ### Item slot methods (yielded as `item`)
66
66
 
@@ -103,6 +103,28 @@ end
103
103
 
104
104
  ---
105
105
 
106
+ ## Plumber::Slots
107
+
108
+ Base class for a component's slot DSL (e.g. `renderer.with_title(value)`, `renderer.with_content { ... }`). Used by `Card`, `Button`, `Link`, `List::Item`, `Timeline::Event`, and `Combobox::Builder`.
109
+
110
+ ```ruby
111
+ class MySlots < StimulusPlumbers::Plumber::Slots
112
+ slot :icon, :title # flat slots: with_icon(value) / with_title(value)
113
+ slot :header, by: HeaderSlots # nested slots: with_header { |h| h.with_x(...) }
114
+ end
115
+ ```
116
+
117
+ Always construct with the view `template` — required for a slot's multi-line block form (`with_content do ... end`) to render correctly:
118
+
119
+ ```ruby
120
+ slots = MySlots.new(template)
121
+ yield slots if block_given?
122
+ ```
123
+
124
+ Slots needing custom validation (e.g. a required keyword) define `with_*` manually instead of using the `slot` DSL — see `Card::Slots#with_action`.
125
+
126
+ ---
127
+
106
128
  ## Plumber::Options::Html
107
129
 
108
130
  Mixin for safely deep-merging HTML attribute hashes. Handles three concerns that a plain `Hash#merge` gets wrong:
@@ -4,14 +4,27 @@ Stimulus Plumbers uses a theme system to apply presentational CSS classes to for
4
4
 
5
5
  ## Configuration
6
6
 
7
+ Adding the `stimulus_plumbers_tailwind` gem registers and activates the Tailwind theme automatically — no config needed. To switch to a custom theme instead:
8
+
7
9
  ```ruby
8
10
  # config/initializers/stimulus_plumbers.rb
9
11
  StimulusPlumbers.configure do |config|
10
- config.theme.use(:tailwind)
11
- # config.theme.use(MyCustomTheme.new)
12
+ config.theme.use(MyCustomTheme.new)
12
13
  end
13
14
  ```
14
15
 
16
+ ## Design tokens
17
+
18
+ - Component classes reference CSS custom properties (e.g. `--sp-color-primary`) defined in `stimulus_plumbers/tokens.css`
19
+ - Run `bin/rails generate stimulus_plumbers:install` once to import it — see the [README](../../README.md#installation)
20
+ - Override tokens with your own `:root` block after the generated import — the cascade applies your values without forking the file
21
+
22
+ ```css
23
+ :root {
24
+ --sp-color-primary: oklch(60% 0.18 260);
25
+ }
26
+ ```
27
+
15
28
  ## Theme keys
16
29
 
17
30
  Each row lists the theme key, the component that uses it, and the variants it accepts.
@@ -132,7 +132,7 @@ Rails helper for rendering a themed timeline as an ordered list with optional in
132
132
 
133
133
  ## ARIA
134
134
 
135
- - See [ARIA.md](../../ARIA.md) for WCAG 2.1 AA criteria and disclosure widget patterns.
135
+ - See [ARIA.md](../../../ARIA.md) for WCAG 2.1 AA criteria and disclosure widget patterns.
136
136
  - The trigger button uses `aria-expanded` (managed by the JS controller) and `aria-controls` pointing to the detail `id`.
137
137
  - Indicator elements carry `aria-hidden="true"` to hide decorative markup from screen readers.
138
- - For JS controller targets, values, and actions, see the [stimulus-plumbers JS controller doc](../../stimulus-plumbers/docs/component/timeline.md).
138
+ - For JS controller targets, values, and actions, see the [stimulus-plumbers JS controller doc](../../../stimulus-plumbers/docs/component/timeline.md).
data/docs/guide.md ADDED
@@ -0,0 +1,27 @@
1
+ # Guide
2
+
3
+ ## Building forms
4
+
5
+ Use `StimulusPlumbers::Form::Builder` (set `config.action_view.default_form_builder`, or pass
6
+ `builder:` to `form_with`). Two levels:
7
+
8
+ - **Level 2 — recommended.** Full accessible field (label + input + hint + error):
9
+ `f.field(attr, as:)`, `f.collection_field(attr, as:, collection:, ...)`, `f.choice(attr, as:)`.
10
+ See [docs/component/form.md](component/form.md) for valid `as:` values per builder method and
11
+ which ones are backed by a Stimulus controller (date/time/select/search pickers).
12
+ - **Level 1.** Native helper overrides (`f.text_field`, `f.select`, `f.check_box`, ...) render
13
+ only the themed input element — use when you control the surrounding markup.
14
+
15
+ Submit with `f.submit` (themed button).
16
+
17
+ ## Building views
18
+
19
+ Render components with `sp_*` helpers (`sp_button`, `sp_button_group`, `sp_card`, `sp_list`,
20
+ `sp_link`, `sp_avatar`, `sp_divider`, `sp_icon`, `sp_popover`, ...) — see the
21
+ [Components table](../README.md#components) for the full helper list, and each component's
22
+ `docs/component/<name>.md` for its keyword options, slots, and themed params.
23
+
24
+ ## Stimulus integration
25
+
26
+ Most display components are pure markup; interactive ones (combobox, popover, calendar) emit their
27
+ `data-controller` attributes automatically — no manual wiring needed in Rails views.
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "stimulus_plumbers/generators/css_entrypoint"
5
+ require "stimulus_plumbers/generators/tokens_directive"
6
+
7
+ module StimulusPlumbers
8
+ module Generators
9
+ class InstallGenerator < Rails::Generators::Base
10
+ include StimulusPlumbers::Generators::CssEntrypoint
11
+
12
+ STIMULUS_PLUMBERS_CSS_FILE = CssEntrypoint::STIMULUS_PLUMBERS_CSS_FILE
13
+ CSS_CANDIDATES = %w[
14
+ app/assets/stylesheets/application.tailwind.css
15
+ app/assets/stylesheets/application.css
16
+ app/javascript/entrypoints/application.css
17
+ ].freeze
18
+
19
+ def install
20
+ css_file = entry_css_file(candidates: CSS_CANDIDATES, env_var: STIMULUS_PLUMBERS_CSS_FILE)
21
+ unless css_file
22
+ return warn_entry_css_not_found(candidates: CSS_CANDIDATES, env_var: STIMULUS_PLUMBERS_CSS_FILE, label: "CSS")
23
+ end
24
+
25
+ apply_edit(css_file, TokensDirective.directive, stale_pattern: TokensDirective.stale_pattern)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -4,7 +4,7 @@ module StimulusPlumbers
4
4
  module Components
5
5
  class Button < Plumber::Base
6
6
  def render(content = nil, icon_leading: nil, icon_trailing: nil, **kwargs, &block)
7
- slots = Button::Slots.new
7
+ slots = Button::Slots.new(template)
8
8
  slots.with_icon_leading(icon_leading) if icon_leading
9
9
  slots.with_icon_trailing(icon_trailing) if icon_trailing
10
10
 
@@ -4,7 +4,7 @@ module StimulusPlumbers
4
4
  module Components
5
5
  class Card < Plumber::Base
6
6
  def render(variant: :tertiary, title_tag: :h2, **kwargs, &block)
7
- slots = Card::Slots.new
7
+ slots = Card::Slots.new(template)
8
8
  yield slots if block_given?
9
9
 
10
10
  html_options = merge_html_options(theme.resolve(:card, variant: variant), kwargs)
@@ -21,7 +21,7 @@ module StimulusPlumbers
21
21
  private
22
22
 
23
23
  def resolve_builder
24
- builder = Combobox::Builder.new
24
+ builder = Combobox::Builder.new(template)
25
25
  yield builder if block_given?
26
26
  builder
27
27
  end
@@ -6,7 +6,7 @@ module StimulusPlumbers
6
6
  def render(content = nil, url:, icon_leading: nil, icon_trailing: nil, **kwargs, &block)
7
7
  icon_trailing ||= "external-link" if kwargs[:target] == "_blank"
8
8
 
9
- slots = Link::Slots.new
9
+ slots = Link::Slots.new(template)
10
10
  slots.with_icon_leading(icon_leading) if icon_leading
11
11
  slots.with_icon_trailing(icon_trailing) if icon_trailing
12
12
 
@@ -5,7 +5,7 @@ module StimulusPlumbers
5
5
  class List
6
6
  class Item < Plumber::Base
7
7
  def render(content = nil, **kwargs, &block)
8
- slots = List::Item::Slots.new
8
+ slots = List::Item::Slots.new(template)
9
9
  slots.with_title(content) if content
10
10
  slots.with_icon_trailing("external-link") if kwargs[:url].present? && kwargs[:target] == "_blank"
11
11
  yield slots if block_given?
@@ -13,7 +13,7 @@ module StimulusPlumbers
13
13
  def render(datetime: nil, id: nil, interactive: false, orientation: :vertical, **kwargs, &block)
14
14
  @detail_id = self.class.detail_id_for(id || template.sp_dom_id)
15
15
 
16
- slots = Timeline::Event::Slots.new
16
+ slots = Timeline::Event::Slots.new(template)
17
17
  yield slots if block_given?
18
18
 
19
19
  validate_slots!(slots, datetime: datetime)
@@ -18,5 +18,9 @@ module StimulusPlumbers
18
18
  include StimulusPlumbers::Helpers
19
19
  end
20
20
  end
21
+
22
+ rake_tasks do
23
+ load File.join(__dir__, "../tasks/stimulus_plumbers.rake")
24
+ end
21
25
  end
22
26
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Generators
5
+ module CssEntrypoint
6
+ STIMULUS_PLUMBERS_CSS_FILE = "STIMULUS_PLUMBERS_CSS_FILE"
7
+
8
+ def entry_css_file(candidates:, env_var:, fallback_env_var: nil)
9
+ [env_var, fallback_env_var].compact.each do |var|
10
+ next unless ENV[var]
11
+
12
+ path = File.expand_path(ENV.fetch(var, nil))
13
+ return path if File.exist?(path)
14
+ end
15
+
16
+ candidates
17
+ .map { |c| File.join(destination_root, c) }
18
+ .find { |f| File.exist?(f) }
19
+ end
20
+
21
+ def warn_entry_css_not_found(candidates:, env_var:, label:, fallback_env_var: nil)
22
+ tried = candidates.map { |c| File.join(destination_root, c) }
23
+ [env_var, fallback_env_var].compact.each do |var|
24
+ tried.unshift(File.expand_path(ENV[var])) if ENV[var]
25
+ end
26
+ say "Could not find a #{label} entry file. Tried: #{tried.join(", ")}. " \
27
+ "Set #{env_var}=/path/to/entry.css and re-run.",
28
+ :red
29
+ end
30
+
31
+ def apply_edit(css_file, directive, stale_pattern:, anchor_pattern: nil)
32
+ content = File.read(css_file)
33
+ new_content, status = content_edit(content, directive, stale_pattern: stale_pattern, anchor_pattern: anchor_pattern)
34
+
35
+ if new_content
36
+ File.write(css_file, new_content)
37
+ say_status status, relative_to_destination(css_file), :green
38
+ else
39
+ say_status :identical, relative_to_destination(css_file)
40
+ end
41
+ rescue Errno::EROFS, Errno::EACCES => e
42
+ say "Could not update #{relative_to_destination(css_file)}: #{e.message}. Skipping.", :yellow
43
+ end
44
+
45
+ # `stale_pattern:` is required — every call site needs stale-import detection,
46
+ # so making it optional would leave an untested "anchor without stale" state.
47
+ def content_edit(content, directive, stale_pattern:, anchor_pattern: nil)
48
+ return nil if content.include?(directive)
49
+
50
+ if (existing = content.match(stale_pattern))
51
+ return [content.sub(existing[0], directive), :update]
52
+ end
53
+
54
+ anchor_pattern ? insert_near_anchor(content, directive, anchor_pattern) : prepend(content, directive)
55
+ end
56
+
57
+ def prepend(content, directive)
58
+ ["#{directive}\n#{content}", :prepend]
59
+ end
60
+
61
+ def insert_near_anchor(content, directive, anchor_pattern)
62
+ if (anchor = content.match(anchor_pattern))
63
+ [content.sub(anchor[0], "#{anchor[0]}\n#{directive}"), :insert]
64
+ else
65
+ ["#{content.rstrip}\n#{directive}\n", :append]
66
+ end
67
+ end
68
+
69
+ def relative_to_destination(path)
70
+ path.delete_prefix("#{destination_root}/")
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module Generators
5
+ module TokensDirective
6
+ GEM_ROOT = File.expand_path("../../..", __dir__)
7
+ TOKENS_CSS_REL_PATH = "app/assets/stylesheets/stimulus_plumbers/tokens.css"
8
+
9
+ module_function
10
+
11
+ def directive
12
+ %(@import "#{GEM_ROOT}/#{TOKENS_CSS_REL_PATH}";)
13
+ end
14
+
15
+ def stale_pattern
16
+ %r{@import "[^"]*#{Regexp.escape(TOKENS_CSS_REL_PATH)}";}
17
+ end
18
+ end
19
+ end
20
+ end
@@ -11,8 +11,9 @@ module StimulusPlumbers
11
11
  end
12
12
  end
13
13
 
14
- def initialize
14
+ def initialize(template = nil)
15
15
  @slots = {}
16
+ @template = template
16
17
  end
17
18
 
18
19
  def resolve(name)
@@ -20,7 +21,7 @@ module StimulusPlumbers
20
21
  return unless entry
21
22
 
22
23
  value = entry[:value]
23
- value = value.call if value.is_a?(Proc)
24
+ value = capture_block(value) if value.is_a?(Proc)
24
25
  block_given? ? yield(value) : value
25
26
  end
26
27
 
@@ -38,6 +39,12 @@ module StimulusPlumbers
38
39
 
39
40
  private
40
41
 
42
+ # Runs the slot block through the view so its ERB output is returned,
43
+ # not written to whatever buffer happens to be active.
44
+ def capture_block(block)
45
+ @template ? @template.capture(&block) : block.call
46
+ end
47
+
41
48
  def set_slot(name, value, options = {})
42
49
  @slots[name] = { value: value, options: options }
43
50
  end
@@ -54,7 +61,7 @@ module StimulusPlumbers
54
61
 
55
62
  def define_by_slot(name, by)
56
63
  define_method(:"with_#{name}") do |&block|
57
- sub = by.new
64
+ sub = by.new(@template)
58
65
  block&.call(sub)
59
66
  set_slot(name, sub)
60
67
  nil
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StimulusPlumbers
4
- VERSION = "0.4.5"
4
+ VERSION = "0.4.8"
5
5
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :stimulus_plumbers do
4
+ desc "Add the tokens.css @import into the CSS entry file"
5
+ task(:install) do
6
+ unless ENV["STIMULUS_PLUMBERS_SKIP_INSTALL"]
7
+ require "generators/stimulus_plumbers/install/install_generator"
8
+ StimulusPlumbers::Generators::InstallGenerator.new(
9
+ [], {}, { destination_root: Rails.root.to_s }
10
+ ).invoke_all
11
+ end
12
+ end
13
+ end
14
+
15
+ Rake::Task["stimulus_plumbers:install"].enhance(["environment"]) unless ENV["STIMULUS_PLUMBERS_SKIP_INSTALL"]
16
+
17
+ # Hooks into "assets:precompile" as a prerequisite; Rake merges rather than overwrites, so order-independent.
18
+ task "assets:precompile" => ["stimulus_plumbers:install"]