senren-ui 0.1.5 → 0.1.6

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -2
  3. data/CONTRIBUTING.md +41 -8
  4. data/README.md +65 -7
  5. data/docs/components.md +222 -0
  6. data/docs/performance_testing.md +34 -0
  7. data/lib/commands/senren/add/add_command.rb +35 -0
  8. data/lib/generators/senren/install/templates/base_component.rb.tt +39 -6
  9. data/lib/generators/senren/install/templates/conventions.md.tt +10 -6
  10. data/lib/senren/rails/component_copier.rb +75 -6
  11. data/lib/senren/rails/component_installer.rb +47 -0
  12. data/lib/senren/rails/installer.rb +1 -0
  13. data/lib/senren/rails/registry.rb +63 -31
  14. data/lib/senren/rails/skill_writer.rb +1 -1
  15. data/lib/senren/rails/version.rb +1 -1
  16. data/lib/senren/rails.rb +1 -0
  17. data/lib/tasks/senren.rake +11 -18
  18. data/templates/components/billing_plan_card/billing_plan_card_component.html.erb +1 -1
  19. data/templates/components/breadcrumb/breadcrumb_component.rb +2 -2
  20. data/templates/components/button/button_component.html.erb +1 -1
  21. data/templates/components/carousel/carousel_component.rb +1 -1
  22. data/templates/components/command/command_component.rb +1 -1
  23. data/templates/components/dropdown_menu/dropdown_menu_component.rb +10 -7
  24. data/templates/components/form/form_component.html.erb +8 -1
  25. data/templates/components/form/form_component.rb +3 -1
  26. data/templates/components/input/input_component.html.erb +1 -1
  27. data/templates/components/input/input_component.rb +19 -0
  28. data/templates/components/label/label_component.html.erb +1 -2
  29. data/templates/components/label/label_component.rb +12 -2
  30. data/templates/components/link/link_component.html.erb +1 -1
  31. data/templates/components/native_select/native_select_component.html.erb +19 -5
  32. data/templates/components/native_select/native_select_component.rb +17 -5
  33. data/templates/components/pagination/pagination_component.rb +2 -1
  34. data/templates/components/sidebar/sidebar_component.rb +2 -2
  35. data/templates/components/switch/switch_component.html.erb +2 -2
  36. data/templates/components/top_nav/top_nav_component.rb +2 -2
  37. data/templates/controllers/rich_text_editor_lite_controller.js +12 -2
  38. metadata +22 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e233d7736e61036693bb895d99e0fd11a37eb9e183979fa9ea4f1d17d1686354
4
- data.tar.gz: dbe764908e51d5007e876c412e2cdffeac6d2b36ac10b8b16187f376ef1e24f0
3
+ metadata.gz: 58a8b09eaa25d57baa0dded24c204b71ba71addac1acf81ae27e08e148d39c06
4
+ data.tar.gz: 04b5733b08a0dfb54192e35c57bc289800988d9254a7c4ee1fcb6d66520299ee
5
5
  SHA512:
6
- metadata.gz: 4a9bd5abdf7f39b9afd87d94e263b256c4e8fd226d5505bee5e1c7cd031e44fe917442b17479718fe39d23909ceee0f94e064073a7717b6309508471db18ce8b
7
- data.tar.gz: a40ab2909ac0f30900fe8e2aace6eba439820ac83cd2ccd37c6037bfcf372225c02d1268a721c3b38a4c243872c35b833ead558621a65a8399b9e8ea632d96b8
6
+ metadata.gz: 424219c05372db86c1df17e5274b5432b8fab5fbe82318fc760c7e23e3bd18beec2757e94abe5f2e21e788bd3326b5f2011c7554cb738192c5aca62588d5cf53
7
+ data.tar.gz: a517a125cb116c79c86094561f59e50b3c54d8ba29f0cdfde8a188b205b504ff1742ac2e14541996fb78fbcfe9a5e74152ba91de47293b3cea45c198373dd803
data/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ 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.1.6] — 2026-06-09
11
+
12
+ ### Added
13
+
14
+ - Local preview app now seeds the full registered Senren component set and renders an exhaustive component kitchen sink.
15
+
16
+ ### Changed
17
+
18
+ - `bin/seed_preview` is now the canonical local preview seed command and targets `.local/preview`.
19
+ - `bin/seed_preview` writes a local-path gem entry that works for custom preview roots.
20
+ - `.rubocop.yml` target Ruby version now matches the gem runtime floor required by ViewComponent 4.x.
21
+
22
+ ### Fixed
23
+
24
+ - `safe_url` now accepts same-origin relative URLs such as `?page=:page`, `./settings`, and `settings` while still rejecting unsafe schemes and hosts.
25
+ - `ComponentCopier` applies the same URL rules when patching existing host apps.
26
+ - Local preview layout keeps the Tailwind browser compiler enabled so the preview renders correctly out of the box.
27
+
10
28
  ## [0.1.5] — 2026-05-03
11
29
 
12
30
  ### Added
@@ -79,7 +97,7 @@ bug fixes only.
79
97
  - Tailwind design-token stylesheet (`senren.css`) with light/dark.
80
98
  - Centralized `.senren/skill.md` system with preserved user-region.
81
99
  - `public/llms.txt` and `public/llms-full.txt` generation.
82
- - `apps/todolist` Rails app dogfooding the gem via local path.
100
+ - A Rails dogfooding app for local-path gem integration.
83
101
  - Bun-based JS tooling for Stimulus templates:
84
102
  - `bun run controllers:syntax`
85
103
  - `bun run controllers:lint`
@@ -108,4 +126,4 @@ bug fixes only.
108
126
  ## [0.1.0] — 2026-04-27
109
127
 
110
128
  First tagged release once the Unreleased entries are validated end-to-end
111
- in `apps/todolist` per `plans/011_release_checklist.md`.
129
+ against the project dogfooding app per `plans/011_release_checklist.md`.
data/CONTRIBUTING.md CHANGED
@@ -17,24 +17,31 @@ this file before opening a PR.
17
17
 
18
18
  ```bash
19
19
  git clone <repo>
20
- cd senren-rails
20
+ cd senren-ui
21
21
  bundle install
22
22
  bun install
23
23
  bundle exec rake test
24
+ bin/system
25
+ bin/performance
26
+ bundle exec rubocop
24
27
  bun run controllers:check
25
28
  ```
26
29
 
27
- To exercise the gem against a real Rails app, use the bundled workspace:
30
+ To exercise the gem against a local host app inside this repo:
28
31
 
29
32
  ```bash
30
- cd ../apps/todolist
31
- bundle install
32
- bin/rails db:setup
33
- bin/rails generate senren:install
34
- bin/rails senren:add button card badge alert
33
+ cd /path/to/senren-ui
34
+ bin/seed_preview
35
+ cd .local/preview
35
36
  bin/rails server
37
+ # optional custom path:
38
+ # SENREN_PREVIEW_ROOT=/abs/path/to/your/preview-app bin/seed_preview
36
39
  ```
37
40
 
41
+ The local preview uses Tailwind's browser runtime for convenience. The
42
+ real documentation/reference app is maintained separately in
43
+ `senren-ui-page`.
44
+
38
45
  ## What to forbid in PRs
39
46
 
40
47
  - React, Vue, Alpine, lit, or any other JS framework dependency.
@@ -56,12 +63,38 @@ bin/rails server
56
63
  - System test in `test/system/` if interactive.
57
64
  - Registry entry in `registry/components.yml` (full schema).
58
65
  - Skill block produced by `SkillWriter`.
59
- - Demo usage in `apps/todolist` if relevant to the Todo UI.
66
+ - Demo usage in `.local/preview` if relevant to the local preview UI.
60
67
 
61
68
  ## Commit hygiene
62
69
 
63
70
  - One logical change per commit.
64
71
  - Mention the affected plan and history files in the commit body.
65
72
  - Run `bundle exec rake test` before pushing.
73
+ - Run `bin/system` before pushing if you touched component templates,
74
+ Stimulus controllers, or the dummy preview app.
75
+ - Run `bundle exec rubocop` before pushing.
76
+ - Run `bin/performance` before pushing if you touched component
77
+ templates, Stimulus controllers, or Importmap loading guidance.
66
78
  - Run `bun run controllers:check` before pushing if you touched
67
79
  `templates/controllers/*.js`.
80
+
81
+ ## Pull request workflow
82
+
83
+ 1. Fork the repo and branch from `main`.
84
+ 2. Keep each PR scoped to one logical change.
85
+ 3. If the change is architectural, add or update a matching `plans/`
86
+ entry first.
87
+ 4. Before opening the PR, add or update the matching `history/` file.
88
+ 5. Fill in the PR template with exact validation commands and results.
89
+
90
+ ## Security workflow
91
+
92
+ - Do not report vulnerabilities in public issues.
93
+ - Use GitHub Security Advisories or the contact in `SECURITY.md`.
94
+ - Do not commit API keys, credentials, or private tokens, even in tests
95
+ or screenshots.
96
+ - Do not pass untrusted URLs directly to component `href`/`src`
97
+ attributes. Use the shared `safe_url` / `safe_media_url` helpers.
98
+ - Do not use `raw`, `html_safe`, `innerHTML =`,
99
+ `insertAdjacentHTML`, `eval`, direct SQL APIs, or string-built SQL.
100
+ The security tests intentionally fail on these escape hatches.
data/README.md CHANGED
@@ -42,6 +42,10 @@ bin/rails senren:add button card badge alert form input \
42
42
  textarea native_select table dropdown_menu dialog alert_dialog
43
43
  ```
44
44
 
45
+ `senren:add` also works via `bundle exec rails senren:add ...`. The older
46
+ bracketed Rake task form, `bin/rails 'senren:add[button,card]'`, remains
47
+ supported for backward compatibility.
48
+
45
49
  ## Daily commands
46
50
 
47
51
  ```bash
@@ -50,11 +54,39 @@ bin/rails generate senren:component picker --client # custom component with Sti
50
54
  bin/rails generate senren:component picker --no-client # without Stimulus
51
55
  bin/rails senren:add dialog --client # install interactive official component
52
56
  bin/rails senren:add button # install static official component
57
+ bundle exec rails senren:add form input # equivalent alternate entry point
53
58
  bin/rails senren:skill:sync # rebuild .senren/skill.md
54
59
  bin/rails senren:agents:sync # rebuild .senren/agent-rules + adapters
55
60
  bin/rails senren:doctor # check installation health
56
61
  ```
57
62
 
63
+ ## Keeping Stimulus JavaScript small
64
+
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:
69
+
70
+ ```javascript
71
+ // app/javascript/controllers/index.js
72
+ import { application } from "controllers/application"
73
+ import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
74
+
75
+ lazyLoadControllersFrom("controllers", application)
76
+ ```
77
+
78
+ Disable import-map preloading for those on-demand controller modules:
79
+
80
+ ```ruby
81
+ # config/importmap.rb
82
+ pin_all_from "app/javascript/controllers", under: "controllers", preload: false
83
+ ```
84
+
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.
89
+
58
90
  ## Using a component
59
91
 
60
92
  ```erb
@@ -73,17 +105,27 @@ bin/rails senren:doctor # check installation health
73
105
 
74
106
  ## Workspace layout
75
107
 
76
- This repository ships as a workspace with a real Rails dogfooding app:
108
+ This repository ships as a gem source checkout with a git-ignored local
109
+ preview host:
77
110
 
78
111
  ```text
79
- senren-workspace/
80
- senren-rails/ # the local gem source directory
81
- apps/
82
- todolist/ # real Rails app that uses senren-ui via local path
112
+ senren-ui/
113
+ .local/
114
+ preview/ # local Rails preview host, ignored by git
115
+ ```
116
+
117
+ Use `bin/seed_preview` to create or refresh `.local/preview`. It
118
+ installs a small Senren component preview route, imports `senren.css`,
119
+ and loads Tailwind's browser runtime for local visual checks.
120
+
121
+ ```bash
122
+ bin/seed_preview
123
+ cd .local/preview
124
+ bin/rails server
83
125
  ```
84
126
 
85
- `apps/todolist` is the production-like acceptance test for Senren a
86
- small SaaS-style Todo manager built entirely from Senren components.
127
+ The full documentation/reference site lives outside this gem checkout in
128
+ `senren-ui-page`.
87
129
 
88
130
  ## AI Agent skill system
89
131
 
@@ -126,6 +168,8 @@ See `registry/components.yml` for the canonical list. v0.1 ships:
126
168
  bundle install
127
169
  bun install
128
170
  bundle exec rake test # gem tests
171
+ bin/system # headless browser system tests
172
+ bin/performance # local payload/performance budgets
129
173
  bun run controllers:check # lint + syntax check for templates/controllers/*.js
130
174
  bun run controllers:lint:fix # auto-fix lint issues for controllers
131
175
  bundle exec rake test:system # Stimulus/system tests
@@ -139,6 +183,20 @@ See `CONTRIBUTING.md`. Two rules to know up front:
139
183
  2. Architectural decisions are captured in `plans/` before code is
140
184
  written.
141
185
 
186
+ ## Open source maintenance baseline
187
+
188
+ This repo ships with a GitHub baseline for safer public maintenance:
189
+
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`.
195
+
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.
199
+
142
200
  ## License
143
201
 
144
202
  MIT — see `LICENSE`.
@@ -0,0 +1,222 @@
1
+ # Senren Components Guide
2
+
3
+ This guide covers the form-related components in detail, with runnable examples
4
+ and explicit guidance on common pitfalls.
5
+
6
+ ---
7
+
8
+ ## FormComponent
9
+
10
+ Wraps `form_with` with Senren semantic tokens and consistent spacing.
11
+
12
+ ### Key behavior
13
+
14
+ - **Yields a Rails form builder** (`|f|`) — use `f.text_field`, `f.select`, etc.
15
+ inside the block just like a normal Rails form.
16
+ - **`method:` defaults to `nil`** — Rails will infer `:post` for new records and
17
+ `:patch` for persisted records. Only pass `method:` explicitly when you need to
18
+ override this (e.g., `method: :delete`).
19
+ - **`model: nil` is safe** — model-less forms (login, search, password reset)
20
+ work without passing a model.
21
+
22
+ ### Examples
23
+
24
+ #### Basic model form (create)
25
+
26
+ ```erb
27
+ <%= render(Senren::FormComponent.new(model: @post, url: posts_path)) do |f| %>
28
+ <%= render(Senren::LabelComponent.new(for_field: "title", variant: :required)) { "Title" } %>
29
+ <%= f.text_field :title, class: "senren-input" %>
30
+ <%= render(Senren::ButtonComponent.new(variant: :primary, type: :submit)) { "Create" } %>
31
+ <% end %>
32
+ ```
33
+
34
+ #### Edit form (Rails infers PATCH automatically)
35
+
36
+ ```erb
37
+ <%= render(Senren::FormComponent.new(model: @post, url: post_path(@post))) do |f| %>
38
+ <%= render(Senren::LabelComponent.new(for_field: "title")) { "Title" } %>
39
+ <%= f.text_field :title, class: "senren-input" %>
40
+ <%= render(Senren::ButtonComponent.new(variant: :primary, type: :submit)) { "Update" } %>
41
+ <% end %>
42
+ ```
43
+
44
+ #### Model-less form (login)
45
+
46
+ ```erb
47
+ <%= render(Senren::FormComponent.new(url: session_path)) do |f| %>
48
+ <%= render(Senren::InputComponent.new(name: "email", type: "email", placeholder: "you@example.com")) %>
49
+ <%= render(Senren::InputComponent.new(name: "password", type: "password")) %>
50
+ <%= render(Senren::ButtonComponent.new(variant: :primary, type: :submit)) { "Sign in" } %>
51
+ <% end %>
52
+ ```
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"`.
57
+
58
+ ---
59
+
60
+ ## InputComponent
61
+
62
+ ### ⚠️ InputComponent vs `form.text_field`
63
+
64
+ **`InputComponent` renders its own `<input>` tag.** It is a **replacement** for
65
+ `form.text_field`, not an add-on. Do not combine them.
66
+
67
+ #### ✅ Do
68
+
69
+ ```erb
70
+ <%# Option A: Use InputComponent standalone %>
71
+ <%= render(Senren::InputComponent.new(name: "email", type: "email")) %>
72
+
73
+ <%# Option B: Use Rails form builder with plain classes %>
74
+ <%= f.text_field :email, class: "your-input-classes" %>
75
+ ```
76
+
77
+ #### ❌ Do NOT
78
+
79
+ ```erb
80
+ <%# WRONG: This renders two inputs %>
81
+ <%= render(Senren::InputComponent.new(name: "email")) do %>
82
+ <%= f.text_field :email %>
83
+ <% end %>
84
+ ```
85
+
86
+ ### Required params
87
+
88
+ | Param | Type | Required | Default | Description |
89
+ |-------|------|----------|---------|-------------|
90
+ | `name` | String/Symbol | **Yes** | — | Input `name` attribute |
91
+ | `type` | String | No | `"text"` | HTML input type |
92
+ | `value` | String | No | `nil` | Pre-filled value |
93
+ | `placeholder` | String | No | `nil` | Placeholder text |
94
+ | `id` | String | No | auto | Defaults to parameterized `name` |
95
+ | `variant` | Symbol | No | `:default` | `:default` or `:error` |
96
+ | `size` | Symbol | No | `:md` | `:sm`, `:md`, or `:lg` |
97
+
98
+ ### File inputs
99
+
100
+ File inputs automatically get styled button treatment:
101
+
102
+ ```erb
103
+ <%= render(Senren::InputComponent.new(name: "avatar", type: "file")) %>
104
+ ```
105
+
106
+ The file selector button uses a segmented style with a divider, semantic surface
107
+ color, and pointer cursor — no additional classes needed.
108
+
109
+ ### Date/time inputs
110
+
111
+ Native date and datetime-local inputs work correctly. The component intentionally
112
+ omits `display: flex` from base styles because it breaks browser-native
113
+ date/time picker UI on some engines.
114
+
115
+ ```erb
116
+ <%= render(Senren::InputComponent.new(name: "starts_at", type: "datetime-local")) %>
117
+ <%= render(Senren::InputComponent.new(name: "due_date", type: "date")) %>
118
+ ```
119
+
120
+ ---
121
+
122
+ ## LabelComponent
123
+
124
+ ### Required params
125
+
126
+ | Param | Type | Required | Default | Description |
127
+ |-------|------|----------|---------|-------------|
128
+ | `for_field` | String | **Yes** | — | ID of the associated form control |
129
+ | `text` | String | No | `nil` | Fallback label text (if block is empty) |
130
+ | `variant` | Symbol | No | `:default` | `:default` or `:required` (adds `*`) |
131
+
132
+ ### Examples
133
+
134
+ Both patterns below are fully supported and produce identical output:
135
+
136
+ ```erb
137
+ <%# Block syntax %>
138
+ <%= render(Senren::LabelComponent.new(for_field: "name", variant: :required)) { "Student name" } %>
139
+
140
+ <%# Text param syntax (useful when block content might be empty) %>
141
+ <%= render(Senren::LabelComponent.new(for_field: "name", text: "Student name", variant: :required)) %>
142
+ ```
143
+
144
+ > **Note**: The `text:` param acts as a fallback. If both a block and `text:` are
145
+ > provided, the block content wins.
146
+
147
+ ---
148
+
149
+ ## NativeSelectComponent
150
+
151
+ Renders a native `<select>` element with Senren styling.
152
+
153
+ ### Key behavior
154
+
155
+ - **Defaults to native browser arrow** (`native_arrow: true`). This preserves
156
+ the platform's familiar select appearance (iOS wheel, Android dropdown, etc.).
157
+ - Pass `native_arrow: false` to use a custom SVG chevron overlay instead.
158
+
159
+ ### Required params
160
+
161
+ | Param | Type | Required | Default | Description |
162
+ |-------|------|----------|---------|-------------|
163
+ | `name` | String/Symbol | **Yes** | — | Select `name` attribute |
164
+ | `options` | Array | **Yes** | — | `["a","b"]` or `[["val","Label"],...]` |
165
+ | `selected` | String | No | `nil` | Pre-selected value |
166
+ | `prompt` | String | No | `nil` | Blank option text (e.g., "Choose…") |
167
+ | `native_arrow` | Boolean | No | `true` | Use native browser arrow |
168
+ | `variant` | Symbol | No | `:default` | `:default` or `:error` |
169
+
170
+ ### Examples
171
+
172
+ ```erb
173
+ <%# Native arrow (default — recommended) %>
174
+ <%= render(Senren::NativeSelectComponent.new(
175
+ name: "role",
176
+ options: [["admin", "Admin"], ["member", "Member"]],
177
+ prompt: "Choose role…"
178
+ )) %>
179
+
180
+ <%# Custom SVG arrow %>
181
+ <%= render(Senren::NativeSelectComponent.new(
182
+ name: "role",
183
+ options: [["admin", "Admin"], ["member", "Member"]],
184
+ native_arrow: false
185
+ )) %>
186
+ ```
187
+
188
+ ---
189
+
190
+ ## FormFieldComponent (pattern — not yet a shipped component)
191
+
192
+ A common pattern when building forms is wrapping label + control + error. Until a
193
+ dedicated `FormFieldComponent` ships, use this pattern:
194
+
195
+ ```erb
196
+ <div class="space-y-1.5">
197
+ <%= render(Senren::LabelComponent.new(for_field: "email", variant: :required)) { "Email" } %>
198
+ <%= render(Senren::InputComponent.new(name: "email", type: "email", variant: @errors[:email] ? :error : :default)) %>
199
+ <% if @errors[:email] %>
200
+ <p class="text-sm text-[hsl(var(--senren-destructive))]"><%= @errors[:email] %></p>
201
+ <% end %>
202
+ </div>
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Render syntax reminder
208
+
209
+ Always use **parentheses** around `render` when passing an inline content block:
210
+
211
+ ```erb
212
+ <%# ✅ Correct: parens around render %>
213
+ <%= render(Senren::ButtonComponent.new(variant: :primary)) { "Save" } %>
214
+
215
+ <%# ✅ Correct: do/end block %>
216
+ <%= render Senren::ButtonComponent.new(variant: :primary) do %>
217
+ Save
218
+ <% end %>
219
+
220
+ <%# ❌ Wrong: block attaches to .new, not render %>
221
+ <%= render Senren::ButtonComponent.new(variant: :primary) { "Save" } %>
222
+ ```
@@ -0,0 +1,34 @@
1
+ # Performance Testing
2
+
3
+ Senren uses CI-safe performance gates by default. The goal is to catch
4
+ regressions that this source-copy UI gem can control: template payload
5
+ growth, Stimulus controller payload growth, eager controller loading, and
6
+ runtime-heavy client behavior.
7
+
8
+ ## Commands
9
+
10
+ ```bash
11
+ bin/performance
12
+ bin/system
13
+ bin/ci
14
+ ```
15
+
16
+ `bin/performance` reads `config/performance_budgets.yml`.
17
+ `bin/system` runs headless browser tests against `test/dummy`.
18
+
19
+ `bin/system` uses Selenium with local Chromium/ChromeDriver by default.
20
+ Override paths with `SENREN_CHROME_BIN` and `SENREN_CHROMEDRIVER` if your
21
+ machine installs them somewhere else.
22
+
23
+ ## Benchmark Model
24
+
25
+ - Static budgets are deterministic and run on every PR.
26
+ - System tests verify browser behavior and coarse budgets.
27
+ - Lighthouse CI is deferred until a stable preview/docs URL exists.
28
+ - Competitive framework benchmarking is out of scope for normal CI.
29
+
30
+ ## Browser Budgets
31
+
32
+ System tests assert gross DOM size, controller loading, external resource,
33
+ and interaction-duration budgets. They intentionally avoid tight timing
34
+ thresholds because shared CI hardware is noisy.
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/command'
4
+ require 'senren/rails'
5
+
6
+ module Senren
7
+ module Command
8
+ class AddCommand < ::Rails::Command::Base
9
+ class_option :client, type: :boolean, default: nil,
10
+ desc: 'Override registry client behavior for installed components.'
11
+ class_option :force, type: :boolean, default: false,
12
+ desc: 'Overwrite existing component files.'
13
+
14
+ desc 'add COMPONENT [COMPONENT...]',
15
+ 'Install one or more Senren components into the current Rails app.'
16
+ def perform(*names)
17
+ require_application!
18
+
19
+ component_installer.install(
20
+ names: names,
21
+ client_override: options[:client],
22
+ force: options[:force]
23
+ )
24
+ rescue ArgumentError => e
25
+ raise ::Rails::Command::Base::Error, e.message
26
+ end
27
+
28
+ private
29
+
30
+ def component_installer
31
+ Senren::Rails::ComponentInstaller.new
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,3 +1,6 @@
1
+ require 'uri'
2
+ require 'view_component'
3
+
1
4
  module Senren
2
5
  # Base class for every Senren ViewComponent.
3
6
  #
@@ -6,13 +9,16 @@ module Senren
6
9
  # - variant and size resolution against class-level constants
7
10
  # - small class merger (no external `tailwind_merge` dependency in v0.1)
8
11
  # - a default root-attribute helper that emits `data-senren-component="<name>"`
9
- class BaseComponent < ViewComponent::Base
10
- VARIANTS = { default: "" }.freeze
11
- SIZES = { md: "" }.freeze
12
+ class BaseComponent < ::ViewComponent::Base
13
+ VARIANTS = { default: '' }.freeze
14
+ SIZES = { md: '' }.freeze
15
+ SAFE_URL_PROTOCOLS = %w[http https mailto tel].freeze
16
+ SAFE_MEDIA_URL_PROTOCOLS = %w[http https].freeze
12
17
 
13
18
  attr_reader :class_name, :variant, :size, :html_attrs
14
19
 
15
20
  def initialize(variant: :default, size: :md, class_name: nil, **html_attrs)
21
+ super()
16
22
  @variant = resolve!(variant, self.class::VARIANTS, :variant)
17
23
  @size = resolve!(size, self.class::SIZES, :size)
18
24
  @class_name = class_name
@@ -22,24 +28,51 @@ module Senren
22
28
  # Compose final root attributes; subclasses pass their base classes.
23
29
  def root_attrs(*classes, **extra)
24
30
  data = (extra.delete(:data) || {}).merge(senren_component: senren_component_name)
25
- tag_class = merge_classes(classes, self.class::VARIANTS[@variant], self.class::SIZES[@size], @class_name, extra.delete(:class))
31
+ tag_class = merge_classes(
32
+ classes,
33
+ self.class::VARIANTS[@variant],
34
+ self.class::SIZES[@size],
35
+ @class_name,
36
+ extra.delete(:class)
37
+ )
26
38
  { class: tag_class, data: data, **html_attrs, **extra }
27
39
  end
28
40
 
29
41
  def senren_component_name
30
- self.class.name.to_s.sub(/^Senren::/, "").sub(/Component$/, "").gsub(/([a-z])([A-Z])/, '\1_\2').downcase
42
+ self.class.name.to_s.sub(/^Senren::/, '').sub(/Component$/, '').gsub(/([a-z])([A-Z])/, '\1_\2').downcase
31
43
  end
32
44
 
33
45
  private
34
46
 
47
+ def safe_url(value, fallback: '#', protocols: SAFE_URL_PROTOCOLS)
48
+ url = value.to_s.strip
49
+ return fallback if url.empty?
50
+ return url if url.start_with?('#')
51
+ return url if url.start_with?('/') && !url.start_with?('//')
52
+
53
+ uri = URI.parse(url)
54
+ return url if uri.scheme && Array(protocols).map(&:to_s).include?(uri.scheme.downcase)
55
+ return fallback if uri.host
56
+ return url unless uri.scheme
57
+
58
+ fallback
59
+ rescue URI::InvalidURIError
60
+ fallback
61
+ end
62
+
63
+ def safe_media_url(value, fallback: nil)
64
+ safe_url(value, fallback: fallback, protocols: SAFE_MEDIA_URL_PROTOCOLS)
65
+ end
66
+
35
67
  def resolve!(value, table, label)
36
68
  key = value.to_sym
37
69
  return key if table.key?(key)
70
+
38
71
  raise ArgumentError, "Unknown #{label}: #{value.inspect}. Allowed: #{table.keys.join(', ')}"
39
72
  end
40
73
 
41
74
  def merge_classes(*sources)
42
- sources.flatten.map { |s| s.to_s.strip }.reject(&:empty?).join(" ")
75
+ sources.flatten.map { |s| s.to_s.strip }.reject(&:empty?).join(' ')
43
76
  end
44
77
  end
45
78
  end
@@ -20,11 +20,11 @@ and obey it strictly.
20
20
  block to `.new`, **not** to `render`, producing an empty component.
21
21
  Use either of these forms instead:
22
22
  ```erb
23
- <%= render(Senren::ButtonComponent.new(variant: :primary)) { "Save" } %>
23
+ <%%= render(Senren::ButtonComponent.new(variant: :primary)) { "Save" } %>
24
24
 
25
- <%= render Senren::ButtonComponent.new(variant: :primary) do %>
25
+ <%%= render Senren::ButtonComponent.new(variant: :primary) do %>
26
26
  Save
27
- <% end %>
27
+ <%% end %>
28
28
  ```
29
29
 
30
30
  ## File ownership
@@ -47,9 +47,13 @@ and obey it strictly.
47
47
  ## Adding a component
48
48
 
49
49
  ```bash
50
- bin/rails senren:add <name> [<name>...]
51
- bin/rails senren:add dialog --no-client # override registry default
52
- bin/rails senren:add button --client # override registry default
50
+ bin/rails senren:add dialog
51
+ bin/rails senren:add button card badge
52
+ bin/rails senren:add dialog --no-client # override registry default
53
+ bundle exec rails senren:add button --client # equivalent alternate entry point
54
+
55
+ # Backward-compatible legacy task syntax:
56
+ bin/rails 'senren:add[button,card,badge]'
53
57
  ```
54
58
 
55
59
  After install you can edit any file under `app/components/senren/` directly.