senren-ui 0.1.6 → 0.2.0

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -0
  3. data/CONTRIBUTING.md +40 -3
  4. data/README.md +110 -33
  5. data/Rakefile +14 -1
  6. data/docs/components.md +10 -10
  7. data/docs/hot_reload.md +103 -0
  8. data/docs/performance_testing.md +7 -3
  9. data/lib/generators/senren/component/templates/controller.js.tt +7 -4
  10. data/lib/generators/senren/install/install_generator.rb +74 -0
  11. data/lib/generators/senren/install/templates/base_component.rb.tt +52 -4
  12. data/lib/generators/senren/install/templates/conventions.md.tt +16 -1
  13. data/lib/senren/rails/agent_rules_writer.rb +62 -19
  14. data/lib/senren/rails/asset_path_guard.rb +128 -0
  15. data/lib/senren/rails/base_component_patch.rb +64 -0
  16. data/lib/senren/rails/component_copier.rb +101 -50
  17. data/lib/senren/rails/component_installer.rb +26 -0
  18. data/lib/senren/rails/doctor.rb +7 -4
  19. data/lib/senren/rails/engine.rb +23 -0
  20. data/lib/senren/rails/host_paths.rb +11 -2
  21. data/lib/senren/rails/marker_block.rb +81 -0
  22. data/lib/senren/rails/registry.rb +10 -4
  23. data/lib/senren/rails/safe_write.rb +169 -0
  24. data/lib/senren/rails/skill_writer.rb +47 -11
  25. data/lib/senren/rails/version.rb +1 -1
  26. data/lib/senren/rails.rb +1 -1
  27. data/lib/senren-ui.rb +15 -0
  28. data/lib/tasks/senren.rake +43 -17
  29. data/registry/components.yml +45 -0
  30. data/registry/recipes.yml +12 -0
  31. data/templates/components/alert_dialog/alert_dialog_component.rb +1 -1
  32. data/templates/components/api_key_field/api_key_field_component.html.erb +1 -1
  33. data/templates/components/aspect_ratio/aspect_ratio_component.rb +7 -0
  34. data/templates/components/avatar/avatar_component.rb +8 -1
  35. data/templates/components/cart/cart_component.html.erb +61 -0
  36. data/templates/components/cart/cart_component.rb +71 -0
  37. data/templates/components/checkbox/checkbox_component.rb +1 -1
  38. data/templates/components/clipboard/clipboard_component.html.erb +1 -1
  39. data/templates/components/command/command_component.rb +1 -1
  40. data/templates/components/date_picker/date_picker_component.html.erb +1 -1
  41. data/templates/components/dialog/dialog_component.rb +1 -1
  42. data/templates/components/form/form_component.rb +9 -1
  43. data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +1 -1
  44. data/templates/components/product_card/product_card_component.html.erb +38 -0
  45. data/templates/components/product_card/product_card_component.rb +49 -0
  46. data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.html.erb +1 -1
  47. data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.rb +1 -1
  48. data/templates/components/separator/separator_component.rb +7 -0
  49. data/templates/components/sheet/sheet_component.rb +1 -1
  50. data/templates/components/tooltip/tooltip_component.rb +2 -2
  51. data/templates/components/typography/typography_component.rb +7 -0
  52. data/templates/controllers/accordion_controller.js +1 -1
  53. data/templates/controllers/alert_dialog_controller.js +31 -7
  54. data/templates/controllers/cart_controller.js +83 -0
  55. data/templates/controllers/clipboard_controller.js +12 -1
  56. data/templates/controllers/command_controller.js +3 -4
  57. data/templates/controllers/context_menu_controller.js +38 -11
  58. data/templates/controllers/data_table_controller.js +8 -3
  59. data/templates/controllers/dialog_controller.js +47 -21
  60. data/templates/controllers/dropdown_menu_controller.js +40 -27
  61. data/templates/controllers/hover_card_controller.js +8 -0
  62. data/templates/controllers/invite_member_dialog_controller.js +6 -0
  63. data/templates/controllers/masked_input_controller.js +8 -1
  64. data/templates/controllers/popover_controller.js +25 -10
  65. data/templates/controllers/rich_text_editor_lite_controller.js +165 -28
  66. data/templates/controllers/sheet_controller.js +41 -11
  67. metadata +13 -17
  68. data/lib/senren/rails/installer.rb +0 -85
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58a8b09eaa25d57baa0dded24c204b71ba71addac1acf81ae27e08e148d39c06
4
- data.tar.gz: 04b5733b08a0dfb54192e35c57bc289800988d9254a7c4ee1fcb6d66520299ee
3
+ metadata.gz: d99681351df1dad873d945bf82b3070b3f8393c9d14b3ca65917262e6258ef6c
4
+ data.tar.gz: b487d9ac589b1d9ba2c304a5f53e4d93064b321d76e97a43ccfa1b9909a44b58
5
5
  SHA512:
6
- metadata.gz: 424219c05372db86c1df17e5274b5432b8fab5fbe82318fc760c7e23e3bd18beec2757e94abe5f2e21e788bd3326b5f2011c7554cb738192c5aca62588d5cf53
7
- data.tar.gz: a517a125cb116c79c86094561f59e50b3c54d8ba29f0cdfde8a188b205b504ff1742ac2e14541996fb78fbcfe9a5e74152ba91de47293b3cea45c198373dd803
6
+ metadata.gz: f383e88dea3c90da1f1f93831bdf26c4bc5e00e1c63ffeb8ae53031d7c7e38c8afbaa30c445839cdbf8ad346acf247dcd04ae597c01b23704837daeb4eb4c2bf
7
+ data.tar.gz: 54e1f4ad10351c05906cdf1f864160cd3a0555e1698c0fa4944a728752cd97560300a9bae40a337a214a39d19eae87a3620fd4690e412d689b9430d022df0662
data/CHANGELOG.md CHANGED
@@ -7,6 +7,110 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
  v0.x is a pre-stable line: minor bumps may break things; patch bumps are
8
8
  bug fixes only.
9
9
 
10
+ ## [0.2.0] — 2026-08-02
11
+
12
+ A hardening release. Most of it came out of an adversarial review of the whole
13
+ library; each item below was reproduced by running it before being fixed, and
14
+ pinned by a test that was watched failing first.
15
+
16
+ ### Upgrading
17
+
18
+ Four changes alter existing behaviour. None requires a code change in your app,
19
+ but read these before upgrading:
20
+
21
+ - **`class:` now merges instead of replacing.** Previously
22
+ `ButtonComponent.new(variant: :primary, class: "mt-2")` rendered
23
+ `class="mt-2"` and dropped the variant and size styling entirely. It now
24
+ renders both. If you worked around the old behaviour by re-specifying every
25
+ utility, you can stop.
26
+ - **DOM ids are derived from component arguments, not random.** Ids are now
27
+ stable across renders, which is what makes Turbo morphing, fragment caching,
28
+ and ETags work. If you hard-coded a generated id in a test or a stylesheet,
29
+ it will have changed.
30
+ - **Booting with `app/components` on the asset load path now fails in
31
+ production.** See *Security* below. If you added that line for sidecar
32
+ assets, point it at `app/components/assets` instead.
33
+ - **`Senren::Rails::Installer` was removed.** It had no callers, and copying
34
+ `.tt` templates without rendering them would have written raw ERB into a host
35
+ app. `senren:install` is the supported path.
36
+
37
+ ### Added
38
+
39
+ - `CartComponent` and `ProductCardComponent`, plus a `storefront` recipe.
40
+ The cart keeps a live subtotal and quantity steppers client-side and
41
+ announces changes with `senren--cart:changed`; the product tile submits a
42
+ form and ships no JavaScript, so listing pages stay light.
43
+ - `lib/senren-ui.rb`, so `gem "senren-ui"` loads the engine without a
44
+ `require:` option. Previously that form silently loaded nothing — no engine,
45
+ no rake tasks, and no asset guard — while the generator kept working.
46
+ - On-demand Stimulus loading, installed rather than documented.
47
+ `senren:install` switches `controllers/index.js` to `lazyLoadControllersFrom`
48
+ and adds `preload: false` to the controllers pin. It leaves a customised
49
+ `index.js` or a non-importmap app alone and reports what it did.
50
+ - `bin/watch`, which syncs template edits into the local preview app and
51
+ reloads the browser. Development-only; nothing ships to host apps.
52
+ - A Ruby 3.2–3.4 × Rails 7.1–8.1 test matrix, so the versions the gemspec
53
+ claims are the versions that are proved.
54
+
55
+ ### Changed
56
+
57
+ - Overlay components (dialog, sheet, popover, dropdown, context menu, alert
58
+ dialog) drive their state through Stimulus values instead of writing to the
59
+ DOM directly, so state survives Turbo morphs.
60
+ - `--client` / `--no-client` applies only to the components you name.
61
+ It used to apply to the whole dependency closure, so
62
+ `senren:add context_menu --no-client` also stripped `dropdown_menu`'s
63
+ controller and the installed menu silently never opened.
64
+ - `.senren/skill.md` describes what was actually installed. It previously
65
+ reported the registry default, naming controller files that were not on disk.
66
+ - `.senren/registry.yml` refreshes on every install instead of drifting from
67
+ the gem after the first one.
68
+ - Rake helpers live in `SenrenRakeArgs` rather than as top-level methods on
69
+ `Object`, and argument scanning stops at the next rake task —
70
+ `rake 'senren:add[button]' db:seed` no longer tries to install `db:seed`.
71
+ - Documentation states the library's scope without characterising other
72
+ ecosystems.
73
+
74
+ ### Security
75
+
76
+ - **Component source could be published in production.** With
77
+ `config.assets.paths << Rails.root.join("app/components")` — a line that
78
+ circulates as ViewComponent sidecar-asset guidance — Propshaft resolved every
79
+ component `.rb` and `.html.erb` as an asset, `assets:precompile` copied them
80
+ into `public/assets`, `.manifest.json` listed each by name, and the digested
81
+ URL returned Ruby source with `HTTP 200`. A boot check now raises in any
82
+ deployed environment and warns in development. Sidecar assets in their own
83
+ directory are unaffected.
84
+ - **The installer could write outside the application root.** A checkout
85
+ shipping `app/components/senren` as a symlink redirected copied files, and
86
+ the agent-adapter writers read their destination before rewriting it, so
87
+ content outside the checkout was modified too. All writes now go through one
88
+ containment layer that resolves symlinks, covers dangling links, and refuses
89
+ only paths that leave the root — an in-repo symlink such as
90
+ `ln -s AGENTS.md CLAUDE.md` keeps working.
91
+ - **`FormComponent#url` and `AvatarComponent#src` were unsanitised.** The first
92
+ reaches `form_with`'s action, where a protocol-relative URL sends every field
93
+ and the CSRF token off-origin. Both now use the same URL policy as the rest
94
+ of the library, and a property test covers every component rather than a list
95
+ of known ones.
96
+ - Rich-text paste is sanitised, and the two URL policies (markup versus typed
97
+ input) are separated so neither can promote a relative path to another origin.
98
+ - Five dependency advisories resolved, and `bundler-audit` is now a CI gate.
99
+
100
+ ### Fixed
101
+
102
+ - `senren:doctor` reported success unconditionally.
103
+ - Marker-managed files could be corrupted by generated content containing
104
+ regexp backreferences, and could inject their own markers.
105
+ - `TypographyComponent`, `SeparatorComponent`, and `AspectRatioComponent`
106
+ raised on `.new` without an explicit variant.
107
+ - `data:` passed to any component dropped its `data-senren-component` marker.
108
+ - Eight components produced a new DOM id on every render.
109
+ - `date_picker` lost its height to a fused CSS class.
110
+ - Stimulus controllers no longer leak timers or document-level listeners across
111
+ Turbo navigations.
112
+ - Checkbox, radio, and switch controls are reachable by their accessible name.
113
+
10
114
  ## [0.1.6] — 2026-06-09
11
115
 
12
116
  ### Added
data/CONTRIBUTING.md CHANGED
@@ -20,13 +20,32 @@ git clone <repo>
20
20
  cd senren-ui
21
21
  bundle install
22
22
  bun install
23
- bundle exec rake test
24
- bin/system
25
- bin/performance
23
+ bin/ci # every gate, reports all failures rather than the first
24
+ bin/ci --matrix # additionally run the Rails 7.1-8.1 matrix (slow)
25
+ ```
26
+
27
+ Individual gates, if you want to run one in isolation:
28
+
29
+ ```bash
30
+ bin/test # unit tests (no Rails app booted)
31
+ bundle exec rake test:integration # renders all 62 components via test/dummy
32
+ bin/system # headless browser tests
33
+ bin/performance # payload and runtime budgets
34
+ bin/matrix # unit tests on every supported Rails
26
35
  bundle exec rubocop
36
+ bundle exec bundle-audit check --update
27
37
  bun run controllers:check
28
38
  ```
29
39
 
40
+ `bin/system` needs no setup: it uses a system Chrome and
41
+ `/usr/bin/chromedriver` when present, and otherwise lets Selenium Manager
42
+ fetch a matching driver.
43
+
44
+ Supported versions are proved, not asserted. `gemfiles/rails_*.gemfile` drive
45
+ the matrix via `BUNDLE_GEMFILE`, CI runs Ruby 3.2-3.4 × Rails 7.1-8.1 with
46
+ `fail-fast: false`, and `test/gem_packaging_test.rb` fails if the gemspec's
47
+ floors drift out of that matrix.
48
+
30
49
  To exercise the gem against a local host app inside this repo:
31
50
 
32
51
  ```bash
@@ -38,6 +57,17 @@ bin/rails server
38
57
  # SENREN_PREVIEW_ROOT=/abs/path/to/your/preview-app bin/seed_preview
39
58
  ```
40
59
 
60
+ While the server runs, start the watcher from the gem root in another
61
+ terminal so template edits appear without re-seeding:
62
+
63
+ ```bash
64
+ bin/watch
65
+ ```
66
+
67
+ It copies changed files from `templates/` and `registry/` into the preview
68
+ app and reloads the browser. See `docs/hot_reload.md` for what does and does
69
+ not reload.
70
+
41
71
  The local preview uses Tailwind's browser runtime for convenience. The
42
72
  real documentation/reference app is maintained separately in
43
73
  `senren-ui-page`.
@@ -61,6 +91,10 @@ real documentation/reference app is maintained separately in
61
91
  (`app/javascript/controllers/senren/<name>_controller.js`).
62
92
  - Component test in `test/components/`.
63
93
  - System test in `test/system/` if interactive.
94
+ - Nothing extra is needed for render coverage: `test/integration/` renders
95
+ every registered component in every declared variant and size, driven
96
+ from the registry, so a new component is covered as soon as it has a
97
+ preview in `ComponentPreviewHelper`.
64
98
  - Registry entry in `registry/components.yml` (full schema).
65
99
  - Skill block produced by `SkillWriter`.
66
100
  - Demo usage in `.local/preview` if relevant to the local preview UI.
@@ -69,6 +103,9 @@ real documentation/reference app is maintained separately in
69
103
 
70
104
  - One logical change per commit.
71
105
  - Mention the affected plan and history files in the commit body.
106
+ - Run `bin/ci` before pushing.
107
+ - Run `bin/ci --matrix` if you touched the gemspec, the Gemfile, or anything
108
+ version sensitive.
72
109
  - Run `bundle exec rake test` before pushing.
73
110
  - Run `bin/system` before pushing if you touched component templates,
74
111
  Stimulus controllers, or the dummy preview app.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Senren UI (`senren-ui`)
2
2
 
3
- > 洗練 — refined, polished, sophisticated.
3
+ > 洗練 *(senren)* Japanese for "refinement".
4
4
 
5
5
  **[Live Docs & Component Reference → senren-ui.dev](https://www.senren-ui.dev)**
6
6
 
@@ -16,12 +16,19 @@ skill system and a source-copy install model inspired by shadcn/ui.
16
16
  every installed component, its dependencies, and its anti-patterns.
17
17
  - A multi-agent instruction sync for Codex, Claude, Copilot, and Cursor.
18
18
 
19
- ## What Senren is not
19
+ ## Scope
20
20
 
21
- - Not React, Vue, Alpine, or any external state framework.
22
- - Not a CSS-only kit components ship Ruby + ERB + (optional) Stimulus.
23
- - Not an opaque dependency installed components live in your app
24
- under `app/components/senren/` and you own them.
21
+ Senren renders on the server. Components are ViewComponent classes with ERB
22
+ templates, interactivity is Stimulus, and server state is Turbo's job. It is
23
+ not a client-side rendering layer: if your app is built around React, Vue, or
24
+ another client framework, a component library from that ecosystem will fit it
25
+ better than this one.
26
+
27
+ Two other things worth knowing up front:
28
+
29
+ - Components ship as Ruby + ERB + optional Stimulus, not as a CSS-only kit.
30
+ - They are copied into your app under `app/components/senren/`, so they are
31
+ yours to read and edit rather than an opaque dependency.
25
32
 
26
33
  ## Installation
27
34
 
@@ -60,32 +67,87 @@ bin/rails senren:agents:sync # rebuild .senren/agent-rules + adap
60
67
  bin/rails senren:doctor # check installation health
61
68
  ```
62
69
 
70
+ ### What reloads while you work
71
+
72
+ Components copied into your app are ordinary Rails code, so editing a
73
+ `*_component.rb` or `*.html.erb` takes effect on the next request with no
74
+ restart. The exception is Stimulus: a controller under
75
+ `app/javascript/controllers/senren/` is a module the browser has already
76
+ fetched, so editing it needs a page reload — the server does not need
77
+ restarting. `docs/hot_reload.md` has the details, including Turbo morphing.
78
+
63
79
  ## Keeping Stimulus JavaScript small
64
80
 
65
- Senren copies only installed client controllers into your Rails app, but an
66
- Importmap app can still download every controller on initial page load if it
67
- keeps the default eager Stimulus loader and preload configuration. Once your
68
- app has several interactive components, switch Stimulus to lazy loading:
81
+ `bin/rails generate senren:install` wires on-demand controller loading for
82
+ you. It switches `app/javascript/controllers/index.js` from Rails' default
83
+ `eagerLoadControllersFrom` to `lazyLoadControllersFrom`, and adds
84
+ `preload: false` to the controllers pin in `config/importmap.rb`.
85
+
86
+ Without it, an Importmap app downloads **every** controller on **every** page.
87
+ `pin_all_from "app/javascript/controllers"` is recursive, so it covers
88
+ `app/javascript/controllers/senren` too — a static marketing page paid for the
89
+ rich text editor. With it, a module is fetched the first time its
90
+ `data-controller` identifier appears, including markup Turbo inserts later.
91
+
92
+ Two things worth knowing:
93
+
94
+ - It changes loading for **your** controllers as well, because it uses the
95
+ official `stimulus-loading` helper rather than a Senren-specific loader.
96
+ Lazy loading is the better default for almost every app, but it is your
97
+ call — the generator only edits the file while it still carries the
98
+ untouched Rails default, and reports what it did.
99
+ - If you have already customised `index.js`, or you bundle with esbuild or
100
+ Vite instead of Importmap, the generator leaves it alone and says so.
101
+
102
+ This used to be a paragraph asking you to do it by hand, and a CI check that
103
+ passed as long as the paragraph existed. It is now installed and asserted in
104
+ `test/generators/install_generator_test.rb` against the files `rails new`
105
+ actually produces.
106
+
107
+ ## Component source in production
69
108
 
70
- ```javascript
71
- // app/javascript/controllers/index.js
72
- import { application } from "controllers/application"
73
- import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
109
+ Senren copies editable ViewComponent, ERB and Stimulus source into your app.
110
+ That is the point of the design, and it means minification and source maps are
111
+ your app's decision, not the gem's — Senren ships readable source and gets out
112
+ of the way.
74
113
 
75
- lazyLoadControllersFrom("controllers", application)
114
+ Readable in your repo is not the same as readable over HTTP, and there is one
115
+ configuration that turns the first into the second:
116
+
117
+ ```ruby
118
+ # config/initializers/assets.rb — do NOT do this
119
+ Rails.application.config.assets.paths << Rails.root.join("app/components")
76
120
  ```
77
121
 
78
- Disable import-map preloading for those on-demand controller modules:
122
+ That line appears in guidance about ViewComponent sidecar assets, and under
123
+ Propshaft it publishes the whole directory. Reproduced against Propshaft 1.3.2:
124
+ every `.rb` and `.html.erb` under `app/components` was resolved as an asset,
125
+ `RAILS_ENV=production rails assets:precompile` copied them into `public/assets`,
126
+ `public/assets/.manifest.json` listed each one next to its digested filename —
127
+ so the digest is not a secret — and requesting the digested URL returned the
128
+ Ruby source with `HTTP 200`, served by the web server without Rails involved.
129
+
130
+ Senren refuses to let this ship. A boot check runs after your initializers:
131
+
132
+ - **production** — raises, so the app does not boot and `assets:precompile`
133
+ fails. A failed deploy is recoverable; published source is not.
134
+ - **anywhere else** — prints a warning and carries on, because development is a
135
+ different risk calculation.
136
+
137
+ It only objects when component source actually sits under an asset path, so
138
+ sidecar assets remain available — put them in their own directory:
79
139
 
80
140
  ```ruby
81
- # config/importmap.rb
82
- pin_all_from "app/javascript/controllers", under: "controllers", preload: false
141
+ Rails.application.config.assets.paths << Rails.root.join("app/components/assets")
83
142
  ```
84
143
 
85
- This keeps controllers mapped and usable while loading each module only when
86
- its `data-controller` identifier appears in the page. Minification or source
87
- maps are an application build/deployment decision; Senren deliberately ships
88
- editable source controllers into the host app.
144
+ The default configuration is unaffected: with no such line, every component
145
+ path returns `404`. The gem itself adds no production request-path code, and
146
+ this check is the sole exception.
147
+
148
+ See `test/asset_path_guard_test.rb` and
149
+ `test/integration/asset_path_guard_boot_test.rb`, which boot a real Rails app
150
+ with a real Propshaft and assert that precompile cannot publish source.
89
151
 
90
152
  ## Using a component
91
153
 
@@ -150,7 +212,7 @@ generated block and preserves your existing instructions outside that block.
150
212
 
151
213
  ## Component list
152
214
 
153
- See `registry/components.yml` for the canonical list. v0.1 ships:
215
+ See `registry/components.yml` for the canonical list. Currently shipping:
154
216
 
155
217
  - **Phase 1 — Foundation** (full): Button, Link, Badge, Typography,
156
218
  Separator, Skeleton, Avatar, Alert, Card, AspectRatio.
@@ -183,19 +245,34 @@ See `CONTRIBUTING.md`. Two rules to know up front:
183
245
  2. Architectural decisions are captured in `plans/` before code is
184
246
  written.
185
247
 
186
- ## Open source maintenance baseline
248
+ ## Project health
249
+
250
+ Every push runs the test suite, RuboCop, and the JavaScript checks against
251
+ Ruby 3.2–3.4 × Rails 7.1–8.1. CodeQL scans Ruby and JavaScript, and Dependabot
252
+ tracks Bundler, JS tooling, and Actions.
253
+
254
+ Security reports go to `SECURITY.md`.
255
+
256
+ ## Acknowledgements
187
257
 
188
- This repo ships with a GitHub baseline for safer public maintenance:
258
+ Senren builds on ideas from prior art. No source code from either project is
259
+ included here — the influence is on architecture and developer experience, and
260
+ both are credited because ideas have authors even when licences do not require
261
+ the notice.
189
262
 
190
- - CI on pull requests and `main` pushes for tests, RuboCop, and JS checks.
191
- - CodeQL analysis for Ruby and JavaScript.
192
- - Dependabot updates for Bundler, JS tooling, and GitHub Actions.
193
- - PR and issue templates, `CODEOWNERS`, `CODE_OF_CONDUCT.md`, and
194
- `SECURITY.md`.
263
+ - [shadcn/ui](https://github.com/shadcn-ui/ui) (MIT) the source-copy install
264
+ model: components are copied into your app so you own and can edit them,
265
+ rather than being rendered from inside a dependency.
266
+ - [jetrockets/jet_ui](https://github.com/jetrockets/jet_ui) (MIT, © 2026
267
+ JetRockets) — a Rails/ViewComponent UI library whose maintainers write openly
268
+ about the trade-offs they worked through. That writing informed
269
+ `plans/022_jet_ui_lessons.md`: how Tailwind's content scanning interacts with
270
+ components shipped from inside a gem, and the case for testing every Ruby and
271
+ Rails version a gem claims to support. Both points made this library better.
195
272
 
196
- Repository controls such as branch protection, required checks, release
197
- permissions, and auto-delete branch still need to be enabled in GitHub
198
- repository settings.
273
+ Both are MIT licensed, which permits use, modification, and study. The MIT
274
+ notice requirement attaches to copies of the software; since Senren copies no
275
+ code from either, this section is attribution rather than a licence obligation.
199
276
 
200
277
  ## License
201
278
 
data/Rakefile CHANGED
@@ -6,11 +6,24 @@ require 'rake/testtask'
6
6
  Rake::TestTask.new(:test) do |t|
7
7
  t.libs << 'test'
8
8
  t.libs << 'lib'
9
- t.test_files = FileList['test/**/*_test.rb'].exclude('test/system/**/*_test.rb')
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ .exclude('test/system/**/*_test.rb')
11
+ .exclude('test/integration/**/*_test.rb')
10
12
  t.warning = false
11
13
  end
12
14
 
13
15
  namespace :test do
16
+ # Separate process, like system tests, and for the same reason: the unit
17
+ # suite `load`s component classes directly, and booting the dummy app would
18
+ # load them a second time. ViewComponent 4 raises RedefinedSlotError on the
19
+ # duplicate declaration.
20
+ Rake::TestTask.new(:integration) do |t|
21
+ t.libs << 'test'
22
+ t.libs << 'lib'
23
+ t.test_files = FileList['test/integration/**/*_test.rb']
24
+ t.warning = false
25
+ end
26
+
14
27
  Rake::TestTask.new(:system) do |t|
15
28
  t.libs << 'test'
16
29
  t.libs << 'lib'
data/docs/components.md CHANGED
@@ -51,20 +51,20 @@ Wraps `form_with` with Senren semantic tokens and consistent spacing.
51
51
  <% end %>
52
52
  ```
53
53
 
54
- > **Warning**: Do NOT pass `method: :post` on edit forms for persisted models.
55
- > Rails needs `method:` to be nil to infer PATCH, otherwise you'll get
56
- > `No route matches [POST] "/resource/:id"`.
54
+ Leave `method:` unset on edit forms for persisted models. Rails needs it to be
55
+ `nil` to infer `PATCH`; passing `method: :post` produces
56
+ `No route matches [POST] "/resource/:id"`.
57
57
 
58
58
  ---
59
59
 
60
60
  ## InputComponent
61
61
 
62
- ### ⚠️ InputComponent vs `form.text_field`
62
+ ### InputComponent vs `form.text_field`
63
63
 
64
64
  **`InputComponent` renders its own `<input>` tag.** It is a **replacement** for
65
65
  `form.text_field`, not an add-on. Do not combine them.
66
66
 
67
- #### Do
67
+ #### Use one or the other
68
68
 
69
69
  ```erb
70
70
  <%# Option A: Use InputComponent standalone %>
@@ -74,10 +74,10 @@ Wraps `form_with` with Senren semantic tokens and consistent spacing.
74
74
  <%= f.text_field :email, class: "your-input-classes" %>
75
75
  ```
76
76
 
77
- #### Do NOT
77
+ #### Not both together
78
78
 
79
79
  ```erb
80
- <%# WRONG: This renders two inputs %>
80
+ <%# This renders two inputs %>
81
81
  <%= render(Senren::InputComponent.new(name: "email")) do %>
82
82
  <%= f.text_field :email %>
83
83
  <% end %>
@@ -209,14 +209,14 @@ dedicated `FormFieldComponent` ships, use this pattern:
209
209
  Always use **parentheses** around `render` when passing an inline content block:
210
210
 
211
211
  ```erb
212
- <%# Correct: parens around render %>
212
+ <%# Correct: parens around render %>
213
213
  <%= render(Senren::ButtonComponent.new(variant: :primary)) { "Save" } %>
214
214
 
215
- <%# Correct: do/end block %>
215
+ <%# Correct: do/end block %>
216
216
  <%= render Senren::ButtonComponent.new(variant: :primary) do %>
217
217
  Save
218
218
  <% end %>
219
219
 
220
- <%# Wrong: block attaches to .new, not render %>
220
+ <%# Wrong: the block attaches to .new, not to render %>
221
221
  <%= render Senren::ButtonComponent.new(variant: :primary) { "Save" } %>
222
222
  ```
@@ -0,0 +1,103 @@
1
+ # Hot reload
2
+
3
+ What reloads automatically, what does not, and why — for both people working
4
+ on the gem and people using it in an app.
5
+
6
+ ## Working on the gem
7
+
8
+ `templates/` is the source of truth, and the preview app under `.local/preview`
9
+ holds *copies* made by `bin/seed_preview`. Editing a template therefore has no
10
+ effect on the running preview until the copy is refreshed. `bin/watch` closes
11
+ that gap.
12
+
13
+ ```bash
14
+ bin/seed_preview # once
15
+ cd .local/preview && bin/rails server # terminal 1
16
+ bin/watch # terminal 2, from the gem root
17
+ ```
18
+
19
+ With both running, editing `templates/components/button/button_component.html.erb`
20
+ copies that one file into the preview app and the browser reloads within about
21
+ a second.
22
+
23
+ What each kind of edit does:
24
+
25
+ | You edit | `bin/watch` does | Result |
26
+ |---|---|---|
27
+ | `templates/components/<name>/*` | copies the one file | page reloads |
28
+ | `templates/controllers/*.js` | copies the one controller | page reloads |
29
+ | `registry/*.yml` | full reinstall of every component | page reloads |
30
+ | `lib/generators/senren/install/templates/*.tt` | copies to its host path | page reloads |
31
+ | `lib/**/*.rb` | nothing | restart `bin/watch` and the server |
32
+
33
+ `lib/` is deliberately not watched: changing gem internals requires restarting
34
+ the Ruby process, which a file copy cannot do.
35
+
36
+ `installed_components.yml.tt` is never synced. That file is a *template* for a
37
+ ledger, and the ledger in the preview app is state — copying over it would
38
+ erase the install history.
39
+
40
+ Options:
41
+
42
+ - `SENREN_PREVIEW_ROOT` — watch a preview app somewhere else. Matches
43
+ `bin/seed_preview`.
44
+ - `SENREN_WATCH_INTERVAL` — poll interval in seconds, default `0.5`.
45
+
46
+ ### How the browser reload works
47
+
48
+ `bin/watch` writes a timestamp to `tmp/senren-reload.txt` after every sync. The
49
+ preview layout polls `/senren/reload_token` twice a second and calls
50
+ `location.reload()` when the value changes.
51
+
52
+ This exists because Rails only reloads *Ruby and ERB*. A Stimulus controller is
53
+ JavaScript already delivered to the browser: syncing the file changes nothing
54
+ until the page is reloaded and the module is fetched again. The token is what
55
+ makes controller edits feel hot.
56
+
57
+ There is no websocket, no ActionCable, and no extra gem — deliberately, since
58
+ the whole plumbing exists only in the gitignored `.local/preview` app and must
59
+ never reach a published gem or a host app.
60
+
61
+ ### Manual check
62
+
63
+ Automated tests cover the path-mapping logic (`test/template_sync_test.rb`).
64
+ The polling loop itself is verified by hand:
65
+
66
+ 1. Start the server and `bin/watch`.
67
+ 2. Change a colour class in a component template — the page reloads with the
68
+ new style.
69
+ 3. Add a `console.log` to a controller — the page reloads and the log appears.
70
+ 4. Add a component to `registry/components.yml` — the watcher reports a full
71
+ reinstall and the new component renders.
72
+
73
+ ## Using the gem in your app
74
+
75
+ After `bin/rails senren:add`, the components are yours. Rails handles most of
76
+ the reloading, so there is little for Senren to add:
77
+
78
+ | You edit | Reloads? |
79
+ |---|---|
80
+ | `app/components/senren/*.rb` | Yes — Rails reloads on the next request |
81
+ | `app/components/senren/*.html.erb` | Yes — same |
82
+ | `app/assets/stylesheets/senren.css` | Yes, once Tailwind rebuilds |
83
+ | `app/javascript/controllers/senren/*.js` | **No** — needs a page reload |
84
+
85
+ The Stimulus row is the one to know about. With importmap the controller is a
86
+ module the browser has already fetched, so editing it does nothing until you
87
+ reload the page. In development Propshaft serves the new file immediately, so
88
+ an ordinary refresh is enough — you do not need to restart the server or clear
89
+ a cache.
90
+
91
+ If you want that refresh automated, the usual choice is the `hotwire-livereload`
92
+ gem in your own `Gemfile`. Senren does not ship one, because doing so would put
93
+ a development dependency and a websocket into every app that installs a
94
+ component library.
95
+
96
+ ### Turbo morphing
97
+
98
+ If your app uses Turbo 8 page morphing, a morph can move or replace the element
99
+ a controller is attached to, which triggers `disconnect()` and `connect()`.
100
+ Senren controllers clean up their document-level listeners in `disconnect()`,
101
+ so they survive repeated morphs. If you write your own controller, do the same:
102
+ anything you add to `document` or `window` in `connect()` or on open must be
103
+ removed in `disconnect()`, or it will accumulate on every navigation.
@@ -16,9 +16,13 @@ bin/ci
16
16
  `bin/performance` reads `config/performance_budgets.yml`.
17
17
  `bin/system` runs headless browser tests against `test/dummy`.
18
18
 
19
- `bin/system` uses Selenium with local Chromium/ChromeDriver by default.
20
- Override paths with `SENREN_CHROME_BIN` and `SENREN_CHROMEDRIVER` if your
21
- machine installs them somewhere else.
19
+ `bin/system` runs Selenium against headless Chrome. It uses a system
20
+ Chrome/Chromium and `/usr/bin/chromedriver` when they are present (the Linux CI
21
+ image), and otherwise lets Selenium Manager resolve and download a matching
22
+ driver — so macOS and other local machines need no setup.
23
+
24
+ Set `SENREN_CHROME_BIN` or `SENREN_CHROMEDRIVER` only to force a specific
25
+ binary; leaving them unset is the supported path.
22
26
 
23
27
  ## Benchmark Model
24
28
 
@@ -3,11 +3,14 @@ import { Controller } from "@hotwired/stimulus"
3
3
  // Stimulus controller for Senren <%= class_name %>.
4
4
  // Identifier: <%= stimulus_identifier %>
5
5
  //
6
- // Responsibilities (local UI only):
6
+ // Scope local UI only:
7
7
  // - Manage open/close, focus, or keyboard behavior for this component.
8
- // Forbidden:
9
- // - fetch / XHR (Turbo handles server state)
10
- // - importing React, Vue, Alpine, lit, or any other framework
8
+ //
9
+ // Out of scope:
10
+ // - fetch / XHR. Turbo owns server state; a controller that fetches ends up
11
+ // holding a second copy of it.
12
+ // - Importing a client-side rendering framework. This component's markup comes
13
+ // from the server, so a second renderer would be fighting it for the DOM.
11
14
  export default class extends Controller {
12
15
  static targets = []
13
16
  static values = {}