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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +215 -0
- data/CONTRIBUTING.md +41 -3
- data/README.md +138 -33
- data/Rakefile +14 -1
- data/docs/components.md +10 -10
- data/docs/hot_reload.md +103 -0
- data/docs/performance_testing.md +7 -3
- data/docs/visual_style.md +80 -0
- data/lib/generators/senren/component/templates/controller.js.tt +7 -4
- data/lib/generators/senren/install/install_generator.rb +77 -0
- data/lib/generators/senren/install/templates/base_component.rb.tt +142 -4
- data/lib/generators/senren/install/templates/conventions.md.tt +57 -1
- data/lib/generators/senren/install/templates/senren_themes.css.tt +339 -0
- data/lib/senren/rails/agent_rules_writer.rb +65 -19
- data/lib/senren/rails/asset_path_guard.rb +128 -0
- data/lib/senren/rails/base_component_patch.rb +64 -0
- data/lib/senren/rails/component_copier.rb +101 -50
- data/lib/senren/rails/component_installer.rb +26 -0
- data/lib/senren/rails/doctor.rb +7 -4
- data/lib/senren/rails/engine.rb +23 -0
- data/lib/senren/rails/host_paths.rb +11 -2
- data/lib/senren/rails/marker_block.rb +81 -0
- data/lib/senren/rails/registry.rb +10 -4
- data/lib/senren/rails/safe_write.rb +169 -0
- data/lib/senren/rails/skill_writer.rb +47 -11
- data/lib/senren/rails/version.rb +1 -1
- data/lib/senren/rails.rb +1 -1
- data/lib/senren-ui.rb +15 -0
- data/lib/tasks/senren.rake +43 -17
- data/registry/components.yml +45 -0
- data/registry/recipes.yml +12 -0
- data/templates/components/accordion/accordion_component.html.erb +3 -3
- data/templates/components/alert_dialog/alert_dialog_component.html.erb +6 -3
- data/templates/components/alert_dialog/alert_dialog_component.rb +5 -1
- data/templates/components/api_key_field/api_key_field_component.html.erb +1 -1
- data/templates/components/aspect_ratio/aspect_ratio_component.rb +7 -0
- data/templates/components/avatar/avatar_component.rb +8 -1
- data/templates/components/breadcrumb/breadcrumb_component.html.erb +8 -6
- data/templates/components/button/button_component.html.erb +1 -1
- data/templates/components/button/button_component.rb +27 -1
- data/templates/components/calendar/calendar_component.html.erb +2 -2
- data/templates/components/card/card_component.html.erb +4 -6
- data/templates/components/carousel/carousel_component.html.erb +1 -1
- data/templates/components/cart/cart_component.html.erb +67 -0
- data/templates/components/cart/cart_component.rb +71 -0
- data/templates/components/checkbox/checkbox_component.rb +1 -1
- data/templates/components/clipboard/clipboard_component.html.erb +3 -3
- data/templates/components/collapsible/collapsible_component.html.erb +3 -3
- data/templates/components/combobox/combobox_component.html.erb +3 -3
- data/templates/components/command/command_component.html.erb +1 -1
- data/templates/components/command/command_component.rb +1 -1
- data/templates/components/context_menu/context_menu_component.html.erb +6 -3
- data/templates/components/date_picker/date_picker_component.html.erb +3 -3
- data/templates/components/dialog/dialog_component.html.erb +7 -3
- data/templates/components/dialog/dialog_component.rb +12 -1
- data/templates/components/dropdown_menu/dropdown_menu_component.html.erb +6 -3
- data/templates/components/dropdown_menu/dropdown_menu_component.rb +57 -5
- data/templates/components/form/form_component.rb +9 -1
- data/templates/components/hover_card/hover_card_component.html.erb +6 -3
- data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +3 -2
- data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +1 -1
- data/templates/components/link/link_component.html.erb +1 -1
- data/templates/components/native_select/native_select_component.html.erb +2 -2
- data/templates/components/pagination/pagination_component.html.erb +2 -2
- data/templates/components/popover/popover_component.html.erb +6 -3
- data/templates/components/product_card/product_card_component.html.erb +38 -0
- data/templates/components/product_card/product_card_component.rb +49 -0
- data/templates/components/progress/progress_component.html.erb +2 -2
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.html.erb +1 -1
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.rb +1 -1
- data/templates/components/search_input/search_input_component.html.erb +2 -2
- data/templates/components/separator/separator_component.rb +7 -0
- data/templates/components/sheet/sheet_component.html.erb +13 -4
- data/templates/components/sheet/sheet_component.rb +12 -1
- data/templates/components/shortcut_key/shortcut_key_component.html.erb +2 -2
- data/templates/components/sidebar/sidebar_component.html.erb +2 -2
- data/templates/components/switch/switch_component.html.erb +1 -1
- data/templates/components/table/table_component.html.erb +2 -2
- data/templates/components/tabs/tabs_component.html.erb +3 -3
- data/templates/components/tooltip/tooltip_component.html.erb +5 -3
- data/templates/components/tooltip/tooltip_component.rb +2 -2
- data/templates/components/top_nav/top_nav_component.html.erb +2 -2
- data/templates/components/typography/typography_component.rb +7 -0
- data/templates/controllers/accordion_controller.js +1 -1
- data/templates/controllers/alert_dialog_controller.js +31 -7
- data/templates/controllers/cart_controller.js +85 -0
- data/templates/controllers/clipboard_controller.js +12 -1
- data/templates/controllers/command_controller.js +3 -4
- data/templates/controllers/context_menu_controller.js +38 -11
- data/templates/controllers/data_table_controller.js +8 -3
- data/templates/controllers/dialog_controller.js +47 -21
- data/templates/controllers/dropdown_menu_controller.js +40 -27
- data/templates/controllers/hover_card_controller.js +8 -0
- data/templates/controllers/invite_member_dialog_controller.js +6 -0
- data/templates/controllers/masked_input_controller.js +8 -1
- data/templates/controllers/popover_controller.js +25 -10
- data/templates/controllers/rich_text_editor_lite_controller.js +165 -28
- data/templates/controllers/sheet_controller.js +41 -11
- metadata +14 -17
- data/lib/senren/rails/installer.rb +0 -85
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c822ae4c548e611bba93a0ffd7654a881b4626173e3a85898e8221318aca6f2e
|
|
4
|
+
data.tar.gz: 48b9d43cf61a38d652f5e993e5baff8d5bff3514b381ee4eeafce95dad7f1113
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
24
|
-
bin/
|
|
25
|
-
|
|
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
|
-
> 洗練 —
|
|
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
|
-
##
|
|
19
|
+
## Scope
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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']
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
###
|
|
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
|
-
####
|
|
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
|
-
####
|
|
77
|
+
#### Not both together
|
|
78
78
|
|
|
79
79
|
```erb
|
|
80
|
-
<%#
|
|
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
|
-
<%#
|
|
212
|
+
<%# Correct: parens around render %>
|
|
213
213
|
<%= render(Senren::ButtonComponent.new(variant: :primary)) { "Save" } %>
|
|
214
214
|
|
|
215
|
-
<%#
|
|
215
|
+
<%# Correct: do/end block %>
|
|
216
216
|
<%= render Senren::ButtonComponent.new(variant: :primary) do %>
|
|
217
217
|
Save
|
|
218
218
|
<% end %>
|
|
219
219
|
|
|
220
|
-
<%#
|
|
220
|
+
<%# Wrong: the block attaches to .new, not to render %>
|
|
221
221
|
<%= render Senren::ButtonComponent.new(variant: :primary) { "Save" } %>
|
|
222
222
|
```
|