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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +104 -0
- data/CONTRIBUTING.md +40 -3
- data/README.md +110 -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/lib/generators/senren/component/templates/controller.js.tt +7 -4
- data/lib/generators/senren/install/install_generator.rb +74 -0
- data/lib/generators/senren/install/templates/base_component.rb.tt +52 -4
- data/lib/generators/senren/install/templates/conventions.md.tt +16 -1
- data/lib/senren/rails/agent_rules_writer.rb +62 -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/alert_dialog/alert_dialog_component.rb +1 -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/cart/cart_component.html.erb +61 -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 +1 -1
- data/templates/components/command/command_component.rb +1 -1
- data/templates/components/date_picker/date_picker_component.html.erb +1 -1
- data/templates/components/dialog/dialog_component.rb +1 -1
- data/templates/components/form/form_component.rb +9 -1
- data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +1 -1
- 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/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/separator/separator_component.rb +7 -0
- data/templates/components/sheet/sheet_component.rb +1 -1
- data/templates/components/tooltip/tooltip_component.rb +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 +83 -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 +13 -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: d99681351df1dad873d945bf82b3070b3f8393c9d14b3ca65917262e6258ef6c
|
|
4
|
+
data.tar.gz: b487d9ac589b1d9ba2c304a5f53e4d93064b321d76e97a43ccfa1b9909a44b58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
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
|
-
> 洗練 —
|
|
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
|
|
|
@@ -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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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']
|
|
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
|
```
|
data/docs/hot_reload.md
ADDED
|
@@ -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.
|
data/docs/performance_testing.md
CHANGED
|
@@ -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`
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
//
|
|
6
|
+
// Scope — local UI only:
|
|
7
7
|
// - Manage open/close, focus, or keyboard behavior for this component.
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// -
|
|
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 = {}
|