senren-ui 0.1.6 → 0.3.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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +215 -0
  3. data/CONTRIBUTING.md +41 -3
  4. data/README.md +138 -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/docs/visual_style.md +80 -0
  10. data/lib/generators/senren/component/templates/controller.js.tt +7 -4
  11. data/lib/generators/senren/install/install_generator.rb +77 -0
  12. data/lib/generators/senren/install/templates/base_component.rb.tt +142 -4
  13. data/lib/generators/senren/install/templates/conventions.md.tt +57 -1
  14. data/lib/generators/senren/install/templates/senren_themes.css.tt +339 -0
  15. data/lib/senren/rails/agent_rules_writer.rb +65 -19
  16. data/lib/senren/rails/asset_path_guard.rb +128 -0
  17. data/lib/senren/rails/base_component_patch.rb +64 -0
  18. data/lib/senren/rails/component_copier.rb +101 -50
  19. data/lib/senren/rails/component_installer.rb +26 -0
  20. data/lib/senren/rails/doctor.rb +7 -4
  21. data/lib/senren/rails/engine.rb +23 -0
  22. data/lib/senren/rails/host_paths.rb +11 -2
  23. data/lib/senren/rails/marker_block.rb +81 -0
  24. data/lib/senren/rails/registry.rb +10 -4
  25. data/lib/senren/rails/safe_write.rb +169 -0
  26. data/lib/senren/rails/skill_writer.rb +47 -11
  27. data/lib/senren/rails/version.rb +1 -1
  28. data/lib/senren/rails.rb +1 -1
  29. data/lib/senren-ui.rb +15 -0
  30. data/lib/tasks/senren.rake +43 -17
  31. data/registry/components.yml +45 -0
  32. data/registry/recipes.yml +12 -0
  33. data/templates/components/accordion/accordion_component.html.erb +3 -3
  34. data/templates/components/alert_dialog/alert_dialog_component.html.erb +6 -3
  35. data/templates/components/alert_dialog/alert_dialog_component.rb +5 -1
  36. data/templates/components/api_key_field/api_key_field_component.html.erb +1 -1
  37. data/templates/components/aspect_ratio/aspect_ratio_component.rb +7 -0
  38. data/templates/components/avatar/avatar_component.rb +8 -1
  39. data/templates/components/breadcrumb/breadcrumb_component.html.erb +8 -6
  40. data/templates/components/button/button_component.html.erb +1 -1
  41. data/templates/components/button/button_component.rb +27 -1
  42. data/templates/components/calendar/calendar_component.html.erb +2 -2
  43. data/templates/components/card/card_component.html.erb +4 -6
  44. data/templates/components/carousel/carousel_component.html.erb +1 -1
  45. data/templates/components/cart/cart_component.html.erb +67 -0
  46. data/templates/components/cart/cart_component.rb +71 -0
  47. data/templates/components/checkbox/checkbox_component.rb +1 -1
  48. data/templates/components/clipboard/clipboard_component.html.erb +3 -3
  49. data/templates/components/collapsible/collapsible_component.html.erb +3 -3
  50. data/templates/components/combobox/combobox_component.html.erb +3 -3
  51. data/templates/components/command/command_component.html.erb +1 -1
  52. data/templates/components/command/command_component.rb +1 -1
  53. data/templates/components/context_menu/context_menu_component.html.erb +6 -3
  54. data/templates/components/date_picker/date_picker_component.html.erb +3 -3
  55. data/templates/components/dialog/dialog_component.html.erb +7 -3
  56. data/templates/components/dialog/dialog_component.rb +12 -1
  57. data/templates/components/dropdown_menu/dropdown_menu_component.html.erb +6 -3
  58. data/templates/components/dropdown_menu/dropdown_menu_component.rb +57 -5
  59. data/templates/components/form/form_component.rb +9 -1
  60. data/templates/components/hover_card/hover_card_component.html.erb +6 -3
  61. data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +3 -2
  62. data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +1 -1
  63. data/templates/components/link/link_component.html.erb +1 -1
  64. data/templates/components/native_select/native_select_component.html.erb +2 -2
  65. data/templates/components/pagination/pagination_component.html.erb +2 -2
  66. data/templates/components/popover/popover_component.html.erb +6 -3
  67. data/templates/components/product_card/product_card_component.html.erb +38 -0
  68. data/templates/components/product_card/product_card_component.rb +49 -0
  69. data/templates/components/progress/progress_component.html.erb +2 -2
  70. data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.html.erb +1 -1
  71. data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.rb +1 -1
  72. data/templates/components/search_input/search_input_component.html.erb +2 -2
  73. data/templates/components/separator/separator_component.rb +7 -0
  74. data/templates/components/sheet/sheet_component.html.erb +13 -4
  75. data/templates/components/sheet/sheet_component.rb +12 -1
  76. data/templates/components/shortcut_key/shortcut_key_component.html.erb +2 -2
  77. data/templates/components/sidebar/sidebar_component.html.erb +2 -2
  78. data/templates/components/switch/switch_component.html.erb +1 -1
  79. data/templates/components/table/table_component.html.erb +2 -2
  80. data/templates/components/tabs/tabs_component.html.erb +3 -3
  81. data/templates/components/tooltip/tooltip_component.html.erb +5 -3
  82. data/templates/components/tooltip/tooltip_component.rb +2 -2
  83. data/templates/components/top_nav/top_nav_component.html.erb +2 -2
  84. data/templates/components/typography/typography_component.rb +7 -0
  85. data/templates/controllers/accordion_controller.js +1 -1
  86. data/templates/controllers/alert_dialog_controller.js +31 -7
  87. data/templates/controllers/cart_controller.js +85 -0
  88. data/templates/controllers/clipboard_controller.js +12 -1
  89. data/templates/controllers/command_controller.js +3 -4
  90. data/templates/controllers/context_menu_controller.js +38 -11
  91. data/templates/controllers/data_table_controller.js +8 -3
  92. data/templates/controllers/dialog_controller.js +47 -21
  93. data/templates/controllers/dropdown_menu_controller.js +40 -27
  94. data/templates/controllers/hover_card_controller.js +8 -0
  95. data/templates/controllers/invite_member_dialog_controller.js +6 -0
  96. data/templates/controllers/masked_input_controller.js +8 -1
  97. data/templates/controllers/popover_controller.js +25 -10
  98. data/templates/controllers/rich_text_editor_lite_controller.js +165 -28
  99. data/templates/controllers/sheet_controller.js +41 -11
  100. metadata +14 -17
  101. 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: c822ae4c548e611bba93a0ffd7654a881b4626173e3a85898e8221318aca6f2e
4
+ data.tar.gz: 48b9d43cf61a38d652f5e993e5baff8d5bff3514b381ee4eeafce95dad7f1113
5
5
  SHA512:
6
- metadata.gz: 424219c05372db86c1df17e5274b5432b8fab5fbe82318fc760c7e23e3bd18beec2757e94abe5f2e21e788bd3326b5f2011c7554cb738192c5aca62588d5cf53
7
- data.tar.gz: a517a125cb116c79c86094561f59e50b3c54d8ba29f0cdfde8a188b205b504ff1742ac2e14541996fb78fbcfe9a5e74152ba91de47293b3cea45c198373dd803
6
+ metadata.gz: cb02d352ef3be737cc00be01c8178874d6b9409c7e54da22917d2d6c4dbe1be0e3baa948bfabce77e5f18241284097bd254b520845ab71b5d1d4d01f573d4e64
7
+ data.tar.gz: edcb1633812058eba68a7014f7239cddf4df698dccc8371c3d968c73e6cbd0913453d7fb9fee00e791e12e4c1080a9b31f1c1db76b3ac42b069606975d07d684
data/CHANGELOG.md CHANGED
@@ -7,6 +7,221 @@ 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.3.0] — 2026-09-01
11
+
12
+ ### Breaking
13
+
14
+ - **`ButtonComponent` no longer emits `type="button"` by default.** The
15
+ attribute is omitted, so a button inside a form submits it, which is what
16
+ plain HTML does and what everyone expects. The old default silently swallowed
17
+ submits: a form's own submit button did nothing, with no error anywhere to
18
+ explain it. The new default fails the other way, and loudly — a trigger that
19
+ should not submit now has to say so.
20
+
21
+ **Migrate:** pass `type: :button` to any button that must not submit — overlay
22
+ triggers, menu triggers, a dialog's Cancel. Everything inside a form that is
23
+ meant to submit needs no change and starts working. Buttons outside a form
24
+ are unaffected either way.
25
+
26
+ ```erb
27
+ <%= render(Senren::ButtonComponent.new(type: :button)) { "Open dialog" } %>
28
+ ```
29
+
30
+ ### Fixed
31
+
32
+ - Every component now merges a caller's `class:` and `data:` instead of
33
+ dropping them. Splatting `**html_attrs` after the computed values replaced
34
+ them outright, so `class:` erased the component's own variant and size
35
+ styling and `data:` erased its `data-senren-component` marker.
36
+ - For the eight wrapper components — dialog, alert dialog, sheet, popover,
37
+ dropdown menu, context menu, hover card, tooltip — a caller's class now lands
38
+ on the panel, which is the element they style, not on the empty root, where it
39
+ sat in the DOM doing nothing.
40
+
41
+ This makes the extension point reachable; it does not make it resolve
42
+ conflicts. `class_name: "max-w-sm"` on a dialog emits
43
+ `class="max-w-lg max-w-sm"` and both declarations stay live, so which one
44
+ applies is decided by the order Tailwind emits them, not by the order of the
45
+ attribute. Measured against a real build: `max-w-sm` wins (narrowing works),
46
+ `max-w-2xl` loses (widening is a silent no-op). Use Tailwind's important
47
+ modifier — `class_name: "max-w-2xl!"` — for an override that does not depend
48
+ on emit order.
49
+ - `data-controller` and `data-action` are appended rather than substituted, so
50
+ attaching your own Stimulus controller to a Senren component no longer unbinds
51
+ the component's own.
52
+ - A caller's `data-controller` or `data-action` that repeated one of the
53
+ component's own tokens was appended rather than deduplicated:
54
+ `data: { controller: "a senren--popover" }` produced
55
+ `"a senren--popover senren--popover"`. Stimulus reads that list literally, so
56
+ the controller connected twice and every action fired twice — a toggle opened
57
+ and immediately closed. Tokens are deduplicated individually now.
58
+ - `data:` written with String keys (`data: { "action" => ... }`) is merged the
59
+ same as Symbol keys. The Symbol-only read meant a dropdown item passing a
60
+ String key lost close-on-click and arrow-key handling.
61
+ - A dropdown item's `class:` is merged rather than substituted. Losing the hover
62
+ style was cosmetic; losing `focus:bg-` removed the only indication a keyboard
63
+ user has of where they are in the menu.
64
+ - Sheet's scrolling body no longer clips focus rings.
65
+ - Dialog, sheet, and the invite-member dialog close on an overlay click.
66
+ - `method:` on a dropdown item reaches Turbo. It was passed to `link_to` as a
67
+ rails-ujs option, which Rails 7 dropped, so it had rendered an inert `method`
68
+ attribute since the library began targeting Rails 7.1.
69
+ - Card footer spacing, and pagination now wraps.
70
+ - `CartComponent` accepted `remove_url:` per item, normalised it, and wrote it
71
+ nowhere. Removal is client-side and dispatches `senren--cart:removed` for the
72
+ application to act on, so the URL was the one thing the listener needed and
73
+ the only thing it could not get. It is emitted as `data-remove-url` on the
74
+ line and carried in the event detail, omitted entirely when not supplied, and
75
+ passed through `safe_url` like every other URL the library renders.
76
+ - `senren_themes.css` shipped with its ERB examples unrendered, so the header
77
+ comment showed `<%%=` where it means `<%=`. The assertion guarding against that
78
+ named a single file, so the property held exactly where someone had thought to
79
+ look; it now runs over everything the generator writes.
80
+
81
+ ### Documentation
82
+
83
+ - The palette presets shipped in 0.2.0 with no mention in the README, the docs,
84
+ or the generated conventions file. All three now cover them, including the
85
+ load-order constraint: `senren_themes.css` must be linked after `senren.css`
86
+ or the theme silently does nothing.
87
+
88
+ ### Internal
89
+
90
+ - ERB linting names every linter it runs. Thirteen were running against nine
91
+ named in the config; eight formatting linters had been on by default and
92
+ unrecorded. A test now fails if an upgrade adds a fourteenth.
93
+ - `bin/ci` and the GitHub workflow call `erb_lint` rather than the deprecated
94
+ `erblint` shim.
95
+ - json bumped to 2.21.2 for CVE-2026-71847.
96
+ - Herb now lints the markup, and the 32 offenses it found in shipped templates
97
+ are fixed. It was adopted after a developer reported warnings in their editor
98
+ that none of this project's gates produced — RuboCop covered the Ruby, Biome
99
+ the Stimulus controllers, erb_lint the ERB structure, and the HTML itself had
100
+ no linter at all. Pinned to 0.10.3 in `.herb.yml`, and wired into `bin/ci`,
101
+ `bin/lint-fix` and the GitHub workflow.
102
+
103
+ Most of what it found is invisible in the rendered page but visible in the
104
+ editor of anyone who installs these components, since `senren:add` copies them
105
+ into their repository. Fifteen component roots moved from
106
+ `<div <%= tag.attributes(**root_attrs(...)) %>>` to `tag.div(...) do`, which
107
+ is what the overlays already used; seven conditional boolean attributes moved
108
+ from `<%= "hidden" unless expanded %>` to `<% unless expanded %>hidden<% end %>`;
109
+ three raw `<img>` tags became `image_tag`; breadcrumb stopped writing its whole
110
+ `<nav>` once per branch; and an empty `aria-activedescendant` is gone.
111
+
112
+ Nothing changes visually, but the markup string is not identical: rendering
113
+ all 64 components before and after produces 35 differing lines — 26 of them
114
+ whitespace and indentation, 6 the attribute order and self-closing slash that
115
+ `image_tag` emits, 2 the `href` moving to the front of an `<a>`, and 1 the
116
+ removed empty attribute. If you assert on exact markup anywhere, those
117
+ snapshots need regenerating. If you have already installed these components,
118
+ nothing in your application changes at all — the new markup arrives only if
119
+ you re-run `senren:add --force`.
120
+
121
+ ## [0.2.0] — 2026-08-02
122
+
123
+ A hardening release. Most of it came out of an adversarial review of the whole
124
+ library; each item below was reproduced by running it before being fixed, and
125
+ pinned by a test that was watched failing first.
126
+
127
+ ### Upgrading
128
+
129
+ Four changes alter existing behaviour. None requires a code change in your app,
130
+ but read these before upgrading:
131
+
132
+ - **`class:` now merges instead of replacing.** Previously
133
+ `ButtonComponent.new(variant: :primary, class: "mt-2")` rendered
134
+ `class="mt-2"` and dropped the variant and size styling entirely. It now
135
+ renders both. If you worked around the old behaviour by re-specifying every
136
+ utility, you can stop.
137
+ - **DOM ids are derived from component arguments, not random.** Ids are now
138
+ stable across renders, which is what makes Turbo morphing, fragment caching,
139
+ and ETags work. If you hard-coded a generated id in a test or a stylesheet,
140
+ it will have changed.
141
+ - **Booting with `app/components` on the asset load path now fails in
142
+ production.** See *Security* below. If you added that line for sidecar
143
+ assets, point it at `app/components/assets` instead.
144
+ - **`Senren::Rails::Installer` was removed.** It had no callers, and copying
145
+ `.tt` templates without rendering them would have written raw ERB into a host
146
+ app. `senren:install` is the supported path.
147
+
148
+ ### Added
149
+
150
+ - `CartComponent` and `ProductCardComponent`, plus a `storefront` recipe.
151
+ The cart keeps a live subtotal and quantity steppers client-side and
152
+ announces changes with `senren--cart:changed`; the product tile submits a
153
+ form and ships no JavaScript, so listing pages stay light.
154
+ - `lib/senren-ui.rb`, so `gem "senren-ui"` loads the engine without a
155
+ `require:` option. Previously that form silently loaded nothing — no engine,
156
+ no rake tasks, and no asset guard — while the generator kept working.
157
+ - On-demand Stimulus loading, installed rather than documented.
158
+ `senren:install` switches `controllers/index.js` to `lazyLoadControllersFrom`
159
+ and adds `preload: false` to the controllers pin. It leaves a customised
160
+ `index.js` or a non-importmap app alone and reports what it did.
161
+ - `bin/watch`, which syncs template edits into the local preview app and
162
+ reloads the browser. Development-only; nothing ships to host apps.
163
+ - A Ruby 3.2–3.4 × Rails 7.1–8.1 test matrix, so the versions the gemspec
164
+ claims are the versions that are proved.
165
+
166
+ ### Changed
167
+
168
+ - Overlay components (dialog, sheet, popover, dropdown, context menu, alert
169
+ dialog) drive their state through Stimulus values instead of writing to the
170
+ DOM directly, so state survives Turbo morphs.
171
+ - `--client` / `--no-client` applies only to the components you name.
172
+ It used to apply to the whole dependency closure, so
173
+ `senren:add context_menu --no-client` also stripped `dropdown_menu`'s
174
+ controller and the installed menu silently never opened.
175
+ - `.senren/skill.md` describes what was actually installed. It previously
176
+ reported the registry default, naming controller files that were not on disk.
177
+ - `.senren/registry.yml` refreshes on every install instead of drifting from
178
+ the gem after the first one.
179
+ - Rake helpers live in `SenrenRakeArgs` rather than as top-level methods on
180
+ `Object`, and argument scanning stops at the next rake task —
181
+ `rake 'senren:add[button]' db:seed` no longer tries to install `db:seed`.
182
+ - Documentation states the library's scope without characterising other
183
+ ecosystems.
184
+
185
+ ### Security
186
+
187
+ - **Component source could be published in production.** With
188
+ `config.assets.paths << Rails.root.join("app/components")` — a line that
189
+ circulates as ViewComponent sidecar-asset guidance — Propshaft resolved every
190
+ component `.rb` and `.html.erb` as an asset, `assets:precompile` copied them
191
+ into `public/assets`, `.manifest.json` listed each by name, and the digested
192
+ URL returned Ruby source with `HTTP 200`. A boot check now raises in any
193
+ deployed environment and warns in development. Sidecar assets in their own
194
+ directory are unaffected.
195
+ - **The installer could write outside the application root.** A checkout
196
+ shipping `app/components/senren` as a symlink redirected copied files, and
197
+ the agent-adapter writers read their destination before rewriting it, so
198
+ content outside the checkout was modified too. All writes now go through one
199
+ containment layer that resolves symlinks, covers dangling links, and refuses
200
+ only paths that leave the root — an in-repo symlink such as
201
+ `ln -s AGENTS.md CLAUDE.md` keeps working.
202
+ - **`FormComponent#url` and `AvatarComponent#src` were unsanitised.** The first
203
+ reaches `form_with`'s action, where a protocol-relative URL sends every field
204
+ and the CSRF token off-origin. Both now use the same URL policy as the rest
205
+ of the library, and a property test covers every component rather than a list
206
+ of known ones.
207
+ - Rich-text paste is sanitised, and the two URL policies (markup versus typed
208
+ input) are separated so neither can promote a relative path to another origin.
209
+ - Five dependency advisories resolved, and `bundler-audit` is now a CI gate.
210
+
211
+ ### Fixed
212
+
213
+ - `senren:doctor` reported success unconditionally.
214
+ - Marker-managed files could be corrupted by generated content containing
215
+ regexp backreferences, and could inject their own markers.
216
+ - `TypographyComponent`, `SeparatorComponent`, and `AspectRatioComponent`
217
+ raised on `.new` without an explicit variant.
218
+ - `data:` passed to any component dropped its `data-senren-component` marker.
219
+ - Eight components produced a new DOM id on every render.
220
+ - `date_picker` lost its height to a fused CSS class.
221
+ - Stimulus controllers no longer leak timers or document-level listeners across
222
+ Turbo navigations.
223
+ - Checkbox, radio, and switch controls are reachable by their accessible name.
224
+
10
225
  ## [0.1.6] — 2026-06-09
11
226
 
12
227
  ### Added
data/CONTRIBUTING.md CHANGED
@@ -20,13 +20,33 @@ 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
38
+ bin/lint-fix # auto-fix RuboCop, ERB lint, controller JS
28
39
  ```
29
40
 
41
+ `bin/system` needs no setup: it uses a system Chrome and
42
+ `/usr/bin/chromedriver` when present, and otherwise lets Selenium Manager
43
+ fetch a matching driver.
44
+
45
+ Supported versions are proved, not asserted. `gemfiles/rails_*.gemfile` drive
46
+ the matrix via `BUNDLE_GEMFILE`, CI runs Ruby 3.2-3.4 × Rails 7.1-8.1 with
47
+ `fail-fast: false`, and `test/gem_packaging_test.rb` fails if the gemspec's
48
+ floors drift out of that matrix.
49
+
30
50
  To exercise the gem against a local host app inside this repo:
31
51
 
32
52
  ```bash
@@ -38,6 +58,17 @@ bin/rails server
38
58
  # SENREN_PREVIEW_ROOT=/abs/path/to/your/preview-app bin/seed_preview
39
59
  ```
40
60
 
61
+ While the server runs, start the watcher from the gem root in another
62
+ terminal so template edits appear without re-seeding:
63
+
64
+ ```bash
65
+ bin/watch
66
+ ```
67
+
68
+ It copies changed files from `templates/` and `registry/` into the preview
69
+ app and reloads the browser. See `docs/hot_reload.md` for what does and does
70
+ not reload.
71
+
41
72
  The local preview uses Tailwind's browser runtime for convenience. The
42
73
  real documentation/reference app is maintained separately in
43
74
  `senren-ui-page`.
@@ -61,6 +92,10 @@ real documentation/reference app is maintained separately in
61
92
  (`app/javascript/controllers/senren/<name>_controller.js`).
62
93
  - Component test in `test/components/`.
63
94
  - System test in `test/system/` if interactive.
95
+ - Nothing extra is needed for render coverage: `test/integration/` renders
96
+ every registered component in every declared variant and size, driven
97
+ from the registry, so a new component is covered as soon as it has a
98
+ preview in `ComponentPreviewHelper`.
64
99
  - Registry entry in `registry/components.yml` (full schema).
65
100
  - Skill block produced by `SkillWriter`.
66
101
  - Demo usage in `.local/preview` if relevant to the local preview UI.
@@ -69,6 +104,9 @@ real documentation/reference app is maintained separately in
69
104
 
70
105
  - One logical change per commit.
71
106
  - Mention the affected plan and history files in the commit body.
107
+ - Run `bin/ci` before pushing.
108
+ - Run `bin/ci --matrix` if you touched the gemspec, the Gemfile, or anything
109
+ version sensitive.
72
110
  - Run `bundle exec rake test` before pushing.
73
111
  - Run `bin/system` before pushing if you touched component templates,
74
112
  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
 
@@ -46,6 +53,33 @@ bin/rails senren:add button card badge alert form input \
46
53
  bracketed Rake task form, `bin/rails 'senren:add[button,card]'`, remains
47
54
  supported for backward compatibility.
48
55
 
56
+ ## Palettes
57
+
58
+ The install writes two stylesheets. `senren.css` is the token set every
59
+ component renders against; `senren_themes.css` is optional and adds five
60
+ alternate palettes.
61
+
62
+ ```erb
63
+ <%= stylesheet_link_tag "senren" %>
64
+ <%= stylesheet_link_tag "senren_themes" %> <%# after senren.css %>
65
+ ```
66
+
67
+ ```erb
68
+ <html data-senren-theme="rose">
69
+ ```
70
+
71
+ `rose`, `slate`, `indigo`, `emerald`, `amber` — or omit the attribute for the
72
+ default, Spring Garden. Order matters: `:root` and `[data-senren-theme="rose"]`
73
+ have equal specificity, so the file that loads last wins. Reversed, nothing
74
+ errors and the page quietly renders in the default palette.
75
+
76
+ A theme is a re-declaration of the same tokens and nothing else. No component
77
+ reads the attribute, so a sixth palette is a copied CSS block — no component
78
+ change, no build step, no configuration. Switch at runtime with
79
+ `document.documentElement.dataset.senrenTheme = "slate"`.
80
+
81
+ Details and the token table: [docs/visual_style.md](docs/visual_style.md).
82
+
49
83
  ## Daily commands
50
84
 
51
85
  ```bash
@@ -60,32 +94,87 @@ bin/rails senren:agents:sync # rebuild .senren/agent-rules + adap
60
94
  bin/rails senren:doctor # check installation health
61
95
  ```
62
96
 
97
+ ### What reloads while you work
98
+
99
+ Components copied into your app are ordinary Rails code, so editing a
100
+ `*_component.rb` or `*.html.erb` takes effect on the next request with no
101
+ restart. The exception is Stimulus: a controller under
102
+ `app/javascript/controllers/senren/` is a module the browser has already
103
+ fetched, so editing it needs a page reload — the server does not need
104
+ restarting. `docs/hot_reload.md` has the details, including Turbo morphing.
105
+
63
106
  ## Keeping Stimulus JavaScript small
64
107
 
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:
108
+ `bin/rails generate senren:install` wires on-demand controller loading for
109
+ you. It switches `app/javascript/controllers/index.js` from Rails' default
110
+ `eagerLoadControllersFrom` to `lazyLoadControllersFrom`, and adds
111
+ `preload: false` to the controllers pin in `config/importmap.rb`.
69
112
 
70
- ```javascript
71
- // app/javascript/controllers/index.js
72
- import { application } from "controllers/application"
73
- import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
113
+ Without it, an Importmap app downloads **every** controller on **every** page.
114
+ `pin_all_from "app/javascript/controllers"` is recursive, so it covers
115
+ `app/javascript/controllers/senren` too a static marketing page paid for the
116
+ rich text editor. With it, a module is fetched the first time its
117
+ `data-controller` identifier appears, including markup Turbo inserts later.
74
118
 
75
- lazyLoadControllersFrom("controllers", application)
119
+ Two things worth knowing:
120
+
121
+ - It changes loading for **your** controllers as well, because it uses the
122
+ official `stimulus-loading` helper rather than a Senren-specific loader.
123
+ Lazy loading is the better default for almost every app, but it is your
124
+ call — the generator only edits the file while it still carries the
125
+ untouched Rails default, and reports what it did.
126
+ - If you have already customised `index.js`, or you bundle with esbuild or
127
+ Vite instead of Importmap, the generator leaves it alone and says so.
128
+
129
+ This used to be a paragraph asking you to do it by hand, and a CI check that
130
+ passed as long as the paragraph existed. It is now installed and asserted in
131
+ `test/generators/install_generator_test.rb` against the files `rails new`
132
+ actually produces.
133
+
134
+ ## Component source in production
135
+
136
+ Senren copies editable ViewComponent, ERB and Stimulus source into your app.
137
+ That is the point of the design, and it means minification and source maps are
138
+ your app's decision, not the gem's — Senren ships readable source and gets out
139
+ of the way.
140
+
141
+ Readable in your repo is not the same as readable over HTTP, and there is one
142
+ configuration that turns the first into the second:
143
+
144
+ ```ruby
145
+ # config/initializers/assets.rb — do NOT do this
146
+ Rails.application.config.assets.paths << Rails.root.join("app/components")
76
147
  ```
77
148
 
78
- Disable import-map preloading for those on-demand controller modules:
149
+ That line appears in guidance about ViewComponent sidecar assets, and under
150
+ Propshaft it publishes the whole directory. Reproduced against Propshaft 1.3.2:
151
+ every `.rb` and `.html.erb` under `app/components` was resolved as an asset,
152
+ `RAILS_ENV=production rails assets:precompile` copied them into `public/assets`,
153
+ `public/assets/.manifest.json` listed each one next to its digested filename —
154
+ so the digest is not a secret — and requesting the digested URL returned the
155
+ Ruby source with `HTTP 200`, served by the web server without Rails involved.
156
+
157
+ Senren refuses to let this ship. A boot check runs after your initializers:
158
+
159
+ - **production** — raises, so the app does not boot and `assets:precompile`
160
+ fails. A failed deploy is recoverable; published source is not.
161
+ - **anywhere else** — prints a warning and carries on, because development is a
162
+ different risk calculation.
163
+
164
+ It only objects when component source actually sits under an asset path, so
165
+ sidecar assets remain available — put them in their own directory:
79
166
 
80
167
  ```ruby
81
- # config/importmap.rb
82
- pin_all_from "app/javascript/controllers", under: "controllers", preload: false
168
+ Rails.application.config.assets.paths << Rails.root.join("app/components/assets")
83
169
  ```
84
170
 
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.
171
+ The default configuration is unaffected: with no such line, every component
172
+ path returns `404`. The gem itself adds no production request-path code, and
173
+ this check is the sole exception.
174
+
175
+ See `test/asset_path_guard_test.rb` and
176
+ `test/integration/asset_path_guard_boot_test.rb`, which boot a real Rails app
177
+ with a real Propshaft and assert that precompile cannot publish source.
89
178
 
90
179
  ## Using a component
91
180
 
@@ -150,7 +239,7 @@ generated block and preserves your existing instructions outside that block.
150
239
 
151
240
  ## Component list
152
241
 
153
- See `registry/components.yml` for the canonical list. v0.1 ships:
242
+ See `registry/components.yml` for the canonical list. Currently shipping:
154
243
 
155
244
  - **Phase 1 — Foundation** (full): Button, Link, Badge, Typography,
156
245
  Separator, Skeleton, Avatar, Alert, Card, AspectRatio.
@@ -173,6 +262,7 @@ bin/performance # local payload/performance budgets
173
262
  bun run controllers:check # lint + syntax check for templates/controllers/*.js
174
263
  bun run controllers:lint:fix # auto-fix lint issues for controllers
175
264
  bundle exec rake test:system # Stimulus/system tests
265
+ bin/lint-fix # auto-fix RuboCop, ERB lint, and controller JS
176
266
  ```
177
267
 
178
268
  ## Contributing
@@ -183,19 +273,34 @@ See `CONTRIBUTING.md`. Two rules to know up front:
183
273
  2. Architectural decisions are captured in `plans/` before code is
184
274
  written.
185
275
 
186
- ## Open source maintenance baseline
276
+ ## Project health
277
+
278
+ Every push runs the test suite, RuboCop, and the JavaScript checks against
279
+ Ruby 3.2–3.4 × Rails 7.1–8.1. CodeQL scans Ruby and JavaScript, and Dependabot
280
+ tracks Bundler, JS tooling, and Actions.
281
+
282
+ Security reports go to `SECURITY.md`.
283
+
284
+ ## Acknowledgements
187
285
 
188
- This repo ships with a GitHub baseline for safer public maintenance:
286
+ Senren builds on ideas from prior art. No source code from either project is
287
+ included here — the influence is on architecture and developer experience, and
288
+ both are credited because ideas have authors even when licences do not require
289
+ the notice.
189
290
 
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`.
291
+ - [shadcn/ui](https://github.com/shadcn-ui/ui) (MIT) the source-copy install
292
+ model: components are copied into your app so you own and can edit them,
293
+ rather than being rendered from inside a dependency.
294
+ - [jetrockets/jet_ui](https://github.com/jetrockets/jet_ui) (MIT, © 2026
295
+ JetRockets) — a Rails/ViewComponent UI library whose maintainers write openly
296
+ about the trade-offs they worked through. That writing informed
297
+ `plans/022_jet_ui_lessons.md`: how Tailwind's content scanning interacts with
298
+ components shipped from inside a gem, and the case for testing every Ruby and
299
+ Rails version a gem claims to support. Both points made this library better.
195
300
 
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.
301
+ Both are MIT licensed, which permits use, modification, and study. The MIT
302
+ notice requirement attaches to copies of the software; since Senren copies no
303
+ code from either, this section is attribution rather than a licence obligation.
199
304
 
200
305
  ## License
201
306
 
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
  ```