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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -2
- data/CONTRIBUTING.md +41 -8
- data/README.md +65 -7
- data/docs/components.md +222 -0
- data/docs/performance_testing.md +34 -0
- data/lib/commands/senren/add/add_command.rb +35 -0
- data/lib/generators/senren/install/templates/base_component.rb.tt +39 -6
- data/lib/generators/senren/install/templates/conventions.md.tt +10 -6
- data/lib/senren/rails/component_copier.rb +75 -6
- data/lib/senren/rails/component_installer.rb +47 -0
- data/lib/senren/rails/installer.rb +1 -0
- data/lib/senren/rails/registry.rb +63 -31
- data/lib/senren/rails/skill_writer.rb +1 -1
- data/lib/senren/rails/version.rb +1 -1
- data/lib/senren/rails.rb +1 -0
- data/lib/tasks/senren.rake +11 -18
- data/templates/components/billing_plan_card/billing_plan_card_component.html.erb +1 -1
- data/templates/components/breadcrumb/breadcrumb_component.rb +2 -2
- data/templates/components/button/button_component.html.erb +1 -1
- data/templates/components/carousel/carousel_component.rb +1 -1
- data/templates/components/command/command_component.rb +1 -1
- data/templates/components/dropdown_menu/dropdown_menu_component.rb +10 -7
- data/templates/components/form/form_component.html.erb +8 -1
- data/templates/components/form/form_component.rb +3 -1
- data/templates/components/input/input_component.html.erb +1 -1
- data/templates/components/input/input_component.rb +19 -0
- data/templates/components/label/label_component.html.erb +1 -2
- data/templates/components/label/label_component.rb +12 -2
- data/templates/components/link/link_component.html.erb +1 -1
- data/templates/components/native_select/native_select_component.html.erb +19 -5
- data/templates/components/native_select/native_select_component.rb +17 -5
- data/templates/components/pagination/pagination_component.rb +2 -1
- data/templates/components/sidebar/sidebar_component.rb +2 -2
- data/templates/components/switch/switch_component.html.erb +2 -2
- data/templates/components/top_nav/top_nav_component.rb +2 -2
- data/templates/controllers/rich_text_editor_lite_controller.js +12 -2
- metadata +22 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 58a8b09eaa25d57baa0dded24c204b71ba71addac1acf81ae27e08e148d39c06
|
|
4
|
+
data.tar.gz: 04b5733b08a0dfb54192e35c57bc289800988d9254a7c4ee1fcb6d66520299ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
-
|
|
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
|
-
|
|
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-
|
|
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
|
|
30
|
+
To exercise the gem against a local host app inside this repo:
|
|
28
31
|
|
|
29
32
|
```bash
|
|
30
|
-
cd
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
108
|
+
This repository ships as a gem source checkout with a git-ignored local
|
|
109
|
+
preview host:
|
|
77
110
|
|
|
78
111
|
```text
|
|
79
|
-
senren-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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`.
|
data/docs/components.md
ADDED
|
@@ -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:
|
|
11
|
-
SIZES = { md:
|
|
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(
|
|
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::/,
|
|
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
|
-
|
|
23
|
+
<%%= render(Senren::ButtonComponent.new(variant: :primary)) { "Save" } %>
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
<%%= render Senren::ButtonComponent.new(variant: :primary) do %>
|
|
26
26
|
Save
|
|
27
|
-
|
|
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
|
|
51
|
-
bin/rails senren:add
|
|
52
|
-
bin/rails senren:add
|
|
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.
|