senren-ui 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +111 -0
- data/CONTRIBUTING.md +1 -0
- data/README.md +28 -0
- data/docs/visual_style.md +80 -0
- data/lib/generators/senren/install/install_generator.rb +3 -0
- data/lib/generators/senren/install/templates/base_component.rb.tt +97 -7
- data/lib/generators/senren/install/templates/conventions.md.tt +41 -0
- data/lib/generators/senren/install/templates/senren_themes.css.tt +339 -0
- data/lib/senren/rails/agent_rules_writer.rb +3 -0
- data/lib/senren/rails/version.rb +1 -1
- data/templates/components/accordion/accordion_component.html.erb +3 -3
- data/templates/components/alert_dialog/alert_dialog_component.html.erb +6 -3
- data/templates/components/alert_dialog/alert_dialog_component.rb +4 -0
- data/templates/components/api_key_field/api_key_field_component.html.erb +1 -1
- data/templates/components/breadcrumb/breadcrumb_component.html.erb +8 -6
- data/templates/components/button/button_component.html.erb +1 -1
- data/templates/components/button/button_component.rb +27 -1
- data/templates/components/calendar/calendar_component.html.erb +2 -2
- data/templates/components/card/card_component.html.erb +4 -6
- data/templates/components/carousel/carousel_component.html.erb +1 -1
- data/templates/components/cart/cart_component.html.erb +7 -1
- data/templates/components/clipboard/clipboard_component.html.erb +3 -3
- data/templates/components/collapsible/collapsible_component.html.erb +3 -3
- data/templates/components/combobox/combobox_component.html.erb +3 -3
- data/templates/components/command/command_component.html.erb +1 -1
- data/templates/components/context_menu/context_menu_component.html.erb +6 -3
- data/templates/components/date_picker/date_picker_component.html.erb +3 -3
- data/templates/components/dialog/dialog_component.html.erb +7 -3
- data/templates/components/dialog/dialog_component.rb +11 -0
- data/templates/components/dropdown_menu/dropdown_menu_component.html.erb +6 -3
- data/templates/components/dropdown_menu/dropdown_menu_component.rb +57 -5
- data/templates/components/hover_card/hover_card_component.html.erb +6 -3
- data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +3 -2
- data/templates/components/link/link_component.html.erb +1 -1
- data/templates/components/native_select/native_select_component.html.erb +2 -2
- data/templates/components/pagination/pagination_component.html.erb +2 -2
- data/templates/components/popover/popover_component.html.erb +6 -3
- data/templates/components/product_card/product_card_component.html.erb +2 -2
- data/templates/components/progress/progress_component.html.erb +2 -2
- data/templates/components/search_input/search_input_component.html.erb +2 -2
- data/templates/components/sheet/sheet_component.html.erb +13 -4
- data/templates/components/sheet/sheet_component.rb +11 -0
- data/templates/components/shortcut_key/shortcut_key_component.html.erb +2 -2
- data/templates/components/sidebar/sidebar_component.html.erb +2 -2
- data/templates/components/switch/switch_component.html.erb +1 -1
- data/templates/components/table/table_component.html.erb +2 -2
- data/templates/components/tabs/tabs_component.html.erb +3 -3
- data/templates/components/tooltip/tooltip_component.html.erb +5 -3
- data/templates/components/top_nav/top_nav_component.html.erb +2 -2
- data/templates/controllers/cart_controller.js +3 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c822ae4c548e611bba93a0ffd7654a881b4626173e3a85898e8221318aca6f2e
|
|
4
|
+
data.tar.gz: 48b9d43cf61a38d652f5e993e5baff8d5bff3514b381ee4eeafce95dad7f1113
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cb02d352ef3be737cc00be01c8178874d6b9409c7e54da22917d2d6c4dbe1be0e3baa948bfabce77e5f18241284097bd254b520845ab71b5d1d4d01f573d4e64
|
|
7
|
+
data.tar.gz: edcb1633812058eba68a7014f7239cddf4df698dccc8371c3d968c73e6cbd0913453d7fb9fee00e791e12e4c1080a9b31f1c1db76b3ac42b069606975d07d684
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,117 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
v0.x is a pre-stable line: minor bumps may break things; patch bumps are
|
|
8
8
|
bug fixes only.
|
|
9
9
|
|
|
10
|
+
## [0.3.0] — 2026-09-01
|
|
11
|
+
|
|
12
|
+
### Breaking
|
|
13
|
+
|
|
14
|
+
- **`ButtonComponent` no longer emits `type="button"` by default.** The
|
|
15
|
+
attribute is omitted, so a button inside a form submits it, which is what
|
|
16
|
+
plain HTML does and what everyone expects. The old default silently swallowed
|
|
17
|
+
submits: a form's own submit button did nothing, with no error anywhere to
|
|
18
|
+
explain it. The new default fails the other way, and loudly — a trigger that
|
|
19
|
+
should not submit now has to say so.
|
|
20
|
+
|
|
21
|
+
**Migrate:** pass `type: :button` to any button that must not submit — overlay
|
|
22
|
+
triggers, menu triggers, a dialog's Cancel. Everything inside a form that is
|
|
23
|
+
meant to submit needs no change and starts working. Buttons outside a form
|
|
24
|
+
are unaffected either way.
|
|
25
|
+
|
|
26
|
+
```erb
|
|
27
|
+
<%= render(Senren::ButtonComponent.new(type: :button)) { "Open dialog" } %>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Every component now merges a caller's `class:` and `data:` instead of
|
|
33
|
+
dropping them. Splatting `**html_attrs` after the computed values replaced
|
|
34
|
+
them outright, so `class:` erased the component's own variant and size
|
|
35
|
+
styling and `data:` erased its `data-senren-component` marker.
|
|
36
|
+
- For the eight wrapper components — dialog, alert dialog, sheet, popover,
|
|
37
|
+
dropdown menu, context menu, hover card, tooltip — a caller's class now lands
|
|
38
|
+
on the panel, which is the element they style, not on the empty root, where it
|
|
39
|
+
sat in the DOM doing nothing.
|
|
40
|
+
|
|
41
|
+
This makes the extension point reachable; it does not make it resolve
|
|
42
|
+
conflicts. `class_name: "max-w-sm"` on a dialog emits
|
|
43
|
+
`class="max-w-lg max-w-sm"` and both declarations stay live, so which one
|
|
44
|
+
applies is decided by the order Tailwind emits them, not by the order of the
|
|
45
|
+
attribute. Measured against a real build: `max-w-sm` wins (narrowing works),
|
|
46
|
+
`max-w-2xl` loses (widening is a silent no-op). Use Tailwind's important
|
|
47
|
+
modifier — `class_name: "max-w-2xl!"` — for an override that does not depend
|
|
48
|
+
on emit order.
|
|
49
|
+
- `data-controller` and `data-action` are appended rather than substituted, so
|
|
50
|
+
attaching your own Stimulus controller to a Senren component no longer unbinds
|
|
51
|
+
the component's own.
|
|
52
|
+
- A caller's `data-controller` or `data-action` that repeated one of the
|
|
53
|
+
component's own tokens was appended rather than deduplicated:
|
|
54
|
+
`data: { controller: "a senren--popover" }` produced
|
|
55
|
+
`"a senren--popover senren--popover"`. Stimulus reads that list literally, so
|
|
56
|
+
the controller connected twice and every action fired twice — a toggle opened
|
|
57
|
+
and immediately closed. Tokens are deduplicated individually now.
|
|
58
|
+
- `data:` written with String keys (`data: { "action" => ... }`) is merged the
|
|
59
|
+
same as Symbol keys. The Symbol-only read meant a dropdown item passing a
|
|
60
|
+
String key lost close-on-click and arrow-key handling.
|
|
61
|
+
- A dropdown item's `class:` is merged rather than substituted. Losing the hover
|
|
62
|
+
style was cosmetic; losing `focus:bg-` removed the only indication a keyboard
|
|
63
|
+
user has of where they are in the menu.
|
|
64
|
+
- Sheet's scrolling body no longer clips focus rings.
|
|
65
|
+
- Dialog, sheet, and the invite-member dialog close on an overlay click.
|
|
66
|
+
- `method:` on a dropdown item reaches Turbo. It was passed to `link_to` as a
|
|
67
|
+
rails-ujs option, which Rails 7 dropped, so it had rendered an inert `method`
|
|
68
|
+
attribute since the library began targeting Rails 7.1.
|
|
69
|
+
- Card footer spacing, and pagination now wraps.
|
|
70
|
+
- `CartComponent` accepted `remove_url:` per item, normalised it, and wrote it
|
|
71
|
+
nowhere. Removal is client-side and dispatches `senren--cart:removed` for the
|
|
72
|
+
application to act on, so the URL was the one thing the listener needed and
|
|
73
|
+
the only thing it could not get. It is emitted as `data-remove-url` on the
|
|
74
|
+
line and carried in the event detail, omitted entirely when not supplied, and
|
|
75
|
+
passed through `safe_url` like every other URL the library renders.
|
|
76
|
+
- `senren_themes.css` shipped with its ERB examples unrendered, so the header
|
|
77
|
+
comment showed `<%%=` where it means `<%=`. The assertion guarding against that
|
|
78
|
+
named a single file, so the property held exactly where someone had thought to
|
|
79
|
+
look; it now runs over everything the generator writes.
|
|
80
|
+
|
|
81
|
+
### Documentation
|
|
82
|
+
|
|
83
|
+
- The palette presets shipped in 0.2.0 with no mention in the README, the docs,
|
|
84
|
+
or the generated conventions file. All three now cover them, including the
|
|
85
|
+
load-order constraint: `senren_themes.css` must be linked after `senren.css`
|
|
86
|
+
or the theme silently does nothing.
|
|
87
|
+
|
|
88
|
+
### Internal
|
|
89
|
+
|
|
90
|
+
- ERB linting names every linter it runs. Thirteen were running against nine
|
|
91
|
+
named in the config; eight formatting linters had been on by default and
|
|
92
|
+
unrecorded. A test now fails if an upgrade adds a fourteenth.
|
|
93
|
+
- `bin/ci` and the GitHub workflow call `erb_lint` rather than the deprecated
|
|
94
|
+
`erblint` shim.
|
|
95
|
+
- json bumped to 2.21.2 for CVE-2026-71847.
|
|
96
|
+
- Herb now lints the markup, and the 32 offenses it found in shipped templates
|
|
97
|
+
are fixed. It was adopted after a developer reported warnings in their editor
|
|
98
|
+
that none of this project's gates produced — RuboCop covered the Ruby, Biome
|
|
99
|
+
the Stimulus controllers, erb_lint the ERB structure, and the HTML itself had
|
|
100
|
+
no linter at all. Pinned to 0.10.3 in `.herb.yml`, and wired into `bin/ci`,
|
|
101
|
+
`bin/lint-fix` and the GitHub workflow.
|
|
102
|
+
|
|
103
|
+
Most of what it found is invisible in the rendered page but visible in the
|
|
104
|
+
editor of anyone who installs these components, since `senren:add` copies them
|
|
105
|
+
into their repository. Fifteen component roots moved from
|
|
106
|
+
`<div <%= tag.attributes(**root_attrs(...)) %>>` to `tag.div(...) do`, which
|
|
107
|
+
is what the overlays already used; seven conditional boolean attributes moved
|
|
108
|
+
from `<%= "hidden" unless expanded %>` to `<% unless expanded %>hidden<% end %>`;
|
|
109
|
+
three raw `<img>` tags became `image_tag`; breadcrumb stopped writing its whole
|
|
110
|
+
`<nav>` once per branch; and an empty `aria-activedescendant` is gone.
|
|
111
|
+
|
|
112
|
+
Nothing changes visually, but the markup string is not identical: rendering
|
|
113
|
+
all 64 components before and after produces 35 differing lines — 26 of them
|
|
114
|
+
whitespace and indentation, 6 the attribute order and self-closing slash that
|
|
115
|
+
`image_tag` emits, 2 the `href` moving to the front of an `<a>`, and 1 the
|
|
116
|
+
removed empty attribute. If you assert on exact markup anywhere, those
|
|
117
|
+
snapshots need regenerating. If you have already installed these components,
|
|
118
|
+
nothing in your application changes at all — the new markup arrives only if
|
|
119
|
+
you re-run `senren:add --force`.
|
|
120
|
+
|
|
10
121
|
## [0.2.0] — 2026-08-02
|
|
11
122
|
|
|
12
123
|
A hardening release. Most of it came out of an adversarial review of the whole
|
data/CONTRIBUTING.md
CHANGED
|
@@ -35,6 +35,7 @@ bin/matrix # unit tests on every supported Rails
|
|
|
35
35
|
bundle exec rubocop
|
|
36
36
|
bundle exec bundle-audit check --update
|
|
37
37
|
bun run controllers:check
|
|
38
|
+
bin/lint-fix # auto-fix RuboCop, ERB lint, controller JS
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
`bin/system` needs no setup: it uses a system Chrome and
|
data/README.md
CHANGED
|
@@ -53,6 +53,33 @@ bin/rails senren:add button card badge alert form input \
|
|
|
53
53
|
bracketed Rake task form, `bin/rails 'senren:add[button,card]'`, remains
|
|
54
54
|
supported for backward compatibility.
|
|
55
55
|
|
|
56
|
+
## Palettes
|
|
57
|
+
|
|
58
|
+
The install writes two stylesheets. `senren.css` is the token set every
|
|
59
|
+
component renders against; `senren_themes.css` is optional and adds five
|
|
60
|
+
alternate palettes.
|
|
61
|
+
|
|
62
|
+
```erb
|
|
63
|
+
<%= stylesheet_link_tag "senren" %>
|
|
64
|
+
<%= stylesheet_link_tag "senren_themes" %> <%# after senren.css %>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```erb
|
|
68
|
+
<html data-senren-theme="rose">
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`rose`, `slate`, `indigo`, `emerald`, `amber` — or omit the attribute for the
|
|
72
|
+
default, Spring Garden. Order matters: `:root` and `[data-senren-theme="rose"]`
|
|
73
|
+
have equal specificity, so the file that loads last wins. Reversed, nothing
|
|
74
|
+
errors and the page quietly renders in the default palette.
|
|
75
|
+
|
|
76
|
+
A theme is a re-declaration of the same tokens and nothing else. No component
|
|
77
|
+
reads the attribute, so a sixth palette is a copied CSS block — no component
|
|
78
|
+
change, no build step, no configuration. Switch at runtime with
|
|
79
|
+
`document.documentElement.dataset.senrenTheme = "slate"`.
|
|
80
|
+
|
|
81
|
+
Details and the token table: [docs/visual_style.md](docs/visual_style.md).
|
|
82
|
+
|
|
56
83
|
## Daily commands
|
|
57
84
|
|
|
58
85
|
```bash
|
|
@@ -235,6 +262,7 @@ bin/performance # local payload/performance budgets
|
|
|
235
262
|
bun run controllers:check # lint + syntax check for templates/controllers/*.js
|
|
236
263
|
bun run controllers:lint:fix # auto-fix lint issues for controllers
|
|
237
264
|
bundle exec rake test:system # Stimulus/system tests
|
|
265
|
+
bin/lint-fix # auto-fix RuboCop, ERB lint, and controller JS
|
|
238
266
|
```
|
|
239
267
|
|
|
240
268
|
## Contributing
|
data/docs/visual_style.md
CHANGED
|
@@ -37,6 +37,86 @@ tokens are available for documentation, examples, and branded surfaces.
|
|
|
37
37
|
| `--senren-palette-iris` | secondary visual accent |
|
|
38
38
|
| `--senren-palette-paper` | warm neutral surface |
|
|
39
39
|
|
|
40
|
+
## Palette Presets
|
|
41
|
+
|
|
42
|
+
`senren.css` declares Spring Garden in `:root`. That is the default and needs no
|
|
43
|
+
attribute. `senren_themes.css` adds five alternates, each one a re-declaration
|
|
44
|
+
of the same token table:
|
|
45
|
+
|
|
46
|
+
| `data-senren-theme` | Reads as |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| *(omitted)* | Spring Garden — warm paper, pine ink, garden green |
|
|
49
|
+
| `rose` | warm pink, low contrast |
|
|
50
|
+
| `slate` | neutral grey, high contrast |
|
|
51
|
+
| `indigo` | cool blue-violet |
|
|
52
|
+
| `emerald` | cool green |
|
|
53
|
+
| `amber` | warm gold |
|
|
54
|
+
|
|
55
|
+
```erb
|
|
56
|
+
<%= stylesheet_link_tag "senren" %>
|
|
57
|
+
<%= stylesheet_link_tag "senren_themes" %>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```erb
|
|
61
|
+
<html data-senren-theme="rose">
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Load `senren_themes.css` after `senren.css`.** `:root` and
|
|
65
|
+
`[data-senren-theme="rose"]` are equal specificity, so the later rule wins and
|
|
66
|
+
source order is the whole mechanism. Reversed, both files load, the attribute is
|
|
67
|
+
on `<html>`, nothing errors, and the page renders in the default palette — the
|
|
68
|
+
one failure mode of this feature, and it looks like the theme file was never
|
|
69
|
+
installed.
|
|
70
|
+
|
|
71
|
+
The attribute goes on `<html>`, not on `<body>` or a wrapper, so it composes
|
|
72
|
+
with the existing `.dark` class instead of replacing it. Light/dark stays
|
|
73
|
+
orthogonal to palette: each preset ships both blocks.
|
|
74
|
+
|
|
75
|
+
No component knows themes exist. They read `hsl(var(--senren-*))` and inherit
|
|
76
|
+
whatever is in scope, which is why a sixth palette is a copied CSS block and
|
|
77
|
+
nothing else — no component to touch, no build step, no configuration.
|
|
78
|
+
|
|
79
|
+
Switching at runtime is one attribute write:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
document.documentElement.dataset.senrenTheme = "slate"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Overriding a Component's Classes
|
|
86
|
+
|
|
87
|
+
`class_name:` and `class:` are both merged into the element the component
|
|
88
|
+
styles. Merged, not resolved — the component's own class stays in the list:
|
|
89
|
+
|
|
90
|
+
```erb
|
|
91
|
+
<%= render Senren::DialogComponent.new(class_name: "max-w-sm") %>
|
|
92
|
+
<!-- panel renders class="… max-w-lg max-w-sm …" -->
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Two `max-w-*` declarations with identical specificity, so the winner is whichever
|
|
96
|
+
Tailwind emits later in the stylesheet. The order of the HTML attribute is not an
|
|
97
|
+
input to that. Tailwind emits the named scale alphabetically, which measured
|
|
98
|
+
against a real build means:
|
|
99
|
+
|
|
100
|
+
| You pass | Against dialog's `max-w-lg` | Result |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `max-w-sm` | `sm` emitted after `lg` | applies |
|
|
103
|
+
| `max-w-2xl` | `2xl` emitted before `lg` | **silently ignored** |
|
|
104
|
+
|
|
105
|
+
Nothing about either class tells you which you are getting. For an override that
|
|
106
|
+
does not depend on emit order, use Tailwind's important modifier:
|
|
107
|
+
|
|
108
|
+
```erb
|
|
109
|
+
<%= render Senren::DialogComponent.new(class_name: "max-w-2xl!") %>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
This applies to any pair from one utility family — `w-`, `h-`, `p-`, `z-`,
|
|
113
|
+
`text-`, `bg-`. It is not specific to widths. Senren does not ship a
|
|
114
|
+
class-conflict resolver; adding one means a `tailwind_merge` dependency, which
|
|
115
|
+
the library has so far chosen not to take.
|
|
116
|
+
|
|
117
|
+
Classes from families the component does not use need no modifier — there is
|
|
118
|
+
nothing to conflict with.
|
|
119
|
+
|
|
40
120
|
## Component Rules
|
|
41
121
|
|
|
42
122
|
- Prefer semantic tokens over raw color utilities.
|
|
@@ -30,6 +30,9 @@ module Senren
|
|
|
30
30
|
def copy_base_files
|
|
31
31
|
template 'base_component.rb.tt', 'app/components/senren/base_component.rb'
|
|
32
32
|
template 'senren.css.tt', 'app/assets/stylesheets/senren.css'
|
|
33
|
+
# Optional: link it only if you want the palette presets. senren.css
|
|
34
|
+
# alone gives you the default, so this file costs nothing until used.
|
|
35
|
+
template 'senren_themes.css.tt', 'app/assets/stylesheets/senren_themes.css'
|
|
33
36
|
template 'conventions.md.tt', '.senren/conventions.md'
|
|
34
37
|
template 'installed_components.yml.tt', '.senren/installed_components.yml'
|
|
35
38
|
end
|
|
@@ -34,25 +34,51 @@ module Senren
|
|
|
34
34
|
# Splatting html_attrs after a computed `data:` used to overwrite it
|
|
35
35
|
# wholesale, so any component given a `data:` hash silently lost its
|
|
36
36
|
# data-senren-component marker. MaskedInput hit this on every render.
|
|
37
|
-
data = (
|
|
38
|
-
.merge(extra.delete(:data) || {})
|
|
39
|
-
.merge(senren_component: senren_component_name)
|
|
37
|
+
data = merge_data(caller_data, extra.delete(:data), { senren_component: senren_component_name })
|
|
40
38
|
|
|
41
39
|
# `class` had exactly the same defect and it was never fixed alongside
|
|
42
40
|
# `data`. Every Rails tag helper accepts `class:`, so it is the first
|
|
43
41
|
# thing anyone types — and splatting html_attrs replaced the computed
|
|
44
42
|
# value outright, erasing the component's variant and size styling with
|
|
45
|
-
# no warning. It is merged like any other caller-supplied class instead
|
|
46
|
-
#
|
|
43
|
+
# no warning. It is merged like any other caller-supplied class instead.
|
|
44
|
+
#
|
|
45
|
+
# Merged, not resolved, and the distinction matters. See merge_classes.
|
|
47
46
|
tag_class = merge_classes(
|
|
48
47
|
classes,
|
|
49
48
|
self.class::VARIANTS[@variant],
|
|
50
49
|
self.class::SIZES[@size],
|
|
51
50
|
@class_name,
|
|
52
51
|
extra.delete(:class),
|
|
53
|
-
|
|
52
|
+
caller_class
|
|
54
53
|
)
|
|
55
|
-
{ class: tag_class, data: data, **
|
|
54
|
+
{ class: tag_class, data: data, **html_attrs_without_class_and_data, **extra }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# For components whose root is a wrapper and whose styling lives on an inner
|
|
58
|
+
# element -- every overlay: the panel is what gets positioned, the root just
|
|
59
|
+
# hosts the controller.
|
|
60
|
+
#
|
|
61
|
+
# Same merging as root_attrs, minus VARIANTS and SIZES. Passing those to the
|
|
62
|
+
# root of a sheet applied `translate-x-full` there, and a transformed
|
|
63
|
+
# ancestor becomes the containing block for its `position: fixed`
|
|
64
|
+
# descendants -- so the panel was positioned against the shifted root and
|
|
65
|
+
# rendered off-screen. It opened; you just could not see it.
|
|
66
|
+
def wrapper_attrs(*classes, **extra)
|
|
67
|
+
data = merge_data(caller_data, extra.delete(:data), { senren_component: senren_component_name })
|
|
68
|
+
tag_class = merge_classes(classes, extra.delete(:class))
|
|
69
|
+
|
|
70
|
+
{ class: tag_class.presence, data: data, **html_attrs_without_class_and_data, **extra }.compact
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The class list for the element a wrapper component actually styles.
|
|
74
|
+
#
|
|
75
|
+
# For an overlay the caller means the panel: `class_name: "max-w-2xl"` on a
|
|
76
|
+
# dialog is a request to widen the dialog, not to widen an invisible
|
|
77
|
+
# wrapper. Putting it on the root left it in the DOM doing nothing, which is
|
|
78
|
+
# harder to debug than dropping it -- the class is right there and the panel
|
|
79
|
+
# is still max-w-lg.
|
|
80
|
+
def panel_class(*classes)
|
|
81
|
+
merge_classes(classes, @class_name, caller_class)
|
|
56
82
|
end
|
|
57
83
|
|
|
58
84
|
def senren_component_name
|
|
@@ -81,6 +107,43 @@ module Senren
|
|
|
81
107
|
|
|
82
108
|
private
|
|
83
109
|
|
|
110
|
+
# Callers write `data: { controller: "x" }` and `data: { "controller" => "x" }`
|
|
111
|
+
# interchangeably, and Rails renders both the same, so the merge has to see
|
|
112
|
+
# both the same. Reading with dig(:symbol) missed the String form entirely:
|
|
113
|
+
# the append never ran and the tag builder kept the caller's value alone,
|
|
114
|
+
# which for data-action removed the component's own close-on-click and
|
|
115
|
+
# arrow-key handling.
|
|
116
|
+
#
|
|
117
|
+
# `controller` and `action` are space-separated lists in Stimulus, so a
|
|
118
|
+
# caller's value is appended rather than substituted -- attaching your own
|
|
119
|
+
# controller to a Senren component must not unbind the component's.
|
|
120
|
+
APPENDABLE_DATA = %i[controller action].freeze
|
|
121
|
+
|
|
122
|
+
def merge_data(*sources)
|
|
123
|
+
sources.compact.map { |h| h.transform_keys(&:to_sym) }.reduce({}) do |acc, incoming|
|
|
124
|
+
acc.merge(incoming) do |key, old, new|
|
|
125
|
+
# Split before uniq. Deduplicating the two whole strings only catches an
|
|
126
|
+
# exact repeat: `data: { controller: "a senren--popover" }` on a popover
|
|
127
|
+
# produced "a senren--popover senren--popover", and Stimulus reads that
|
|
128
|
+
# list literally -- same controller connected twice on one element, every
|
|
129
|
+
# action firing twice, so a toggle opens and immediately closes.
|
|
130
|
+
APPENDABLE_DATA.include?(key) ? [old, new].map(&:to_s).join(' ').split.uniq.join(' ') : new
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def caller_class
|
|
136
|
+
html_attrs[:class] || html_attrs['class']
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def caller_data
|
|
140
|
+
html_attrs[:data] || html_attrs['data']
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def html_attrs_without_class_and_data
|
|
144
|
+
html_attrs.reject { |k, _| %w[class data].include?(k.to_s) }
|
|
145
|
+
end
|
|
146
|
+
|
|
84
147
|
def safe_url(value, fallback: '#', protocols: SAFE_URL_PROTOCOLS)
|
|
85
148
|
url = value.to_s.strip
|
|
86
149
|
return fallback if url.empty?
|
|
@@ -119,6 +182,33 @@ module Senren
|
|
|
119
182
|
raise ArgumentError, "Unknown #{label}: #{value.inspect}. Allowed: #{table.keys.join(', ')}"
|
|
120
183
|
end
|
|
121
184
|
|
|
185
|
+
# Concatenation, not conflict resolution — and the caller's class going last
|
|
186
|
+
# in the attribute does not make it win.
|
|
187
|
+
#
|
|
188
|
+
# `class="max-w-lg max-w-2xl"` leaves both declarations live. Which applies
|
|
189
|
+
# is decided by the order the two rules appear in the stylesheet, because
|
|
190
|
+
# their specificity is identical; the order of the attribute is not an input
|
|
191
|
+
# to that at all. Tailwind emits the named scale alphabetically, so measured
|
|
192
|
+
# against a real build:
|
|
193
|
+
#
|
|
194
|
+
# dialog base max-w-lg + class_name: "max-w-sm" -> sm wins (works)
|
|
195
|
+
# dialog base max-w-lg + class_name: "max-w-2xl" -> lg wins (silent no-op)
|
|
196
|
+
#
|
|
197
|
+
# Narrowing happens to work and widening happens not to, and nothing about
|
|
198
|
+
# either class says which you are getting. Any pair drawn from one utility
|
|
199
|
+
# family has this property.
|
|
200
|
+
#
|
|
201
|
+
# Resolving it properly means knowing which Tailwind utilities conflict,
|
|
202
|
+
# which is irregular enough (`text-sm` and `text-red-500` share a prefix and
|
|
203
|
+
# are different families) that a partial implementation would silently drop
|
|
204
|
+
# a component's own styling — a worse failure than the one it replaces. That
|
|
205
|
+
# is what tailwind_merge is for, and taking the dependency is a decision
|
|
206
|
+
# this library has not made.
|
|
207
|
+
#
|
|
208
|
+
# Until it does, the deterministic override is Tailwind's own important
|
|
209
|
+
# modifier, which beats source order rather than depending on it:
|
|
210
|
+
#
|
|
211
|
+
# render Senren::DialogComponent.new(class_name: "max-w-2xl!")
|
|
122
212
|
def merge_classes(*sources)
|
|
123
213
|
sources.flatten.map { |s| s.to_s.strip }.reject(&:empty?).join(' ')
|
|
124
214
|
end
|
|
@@ -42,6 +42,47 @@ and obey it strictly.
|
|
|
42
42
|
# NOT app/components
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
9. **Do not pass `type:` to a button inside a form.** `ButtonComponent`
|
|
46
|
+
omits the attribute, so the browser's own rule applies and the button
|
|
47
|
+
submits, exactly as plain HTML does. Pass `type: :button` only for a
|
|
48
|
+
control that must *not* submit — a dialog, sheet, or menu trigger.
|
|
49
|
+
```erb
|
|
50
|
+
<%%= form_with url: sessions_path do %>
|
|
51
|
+
<%%= render(Senren::ButtonComponent.new(variant: :primary)) { "Sign in" } %>
|
|
52
|
+
<%% end %>
|
|
53
|
+
|
|
54
|
+
<%%= render(Senren::ButtonComponent.new(type: :button)) { "Open dialog" } %>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
10. **Load `senren_themes.css` after `senren.css`.** The install writes both.
|
|
58
|
+
`senren.css` declares the token set in `:root`; `senren_themes.css`
|
|
59
|
+
redeclares it under `[data-senren-theme="..."]` for the `rose`, `slate`,
|
|
60
|
+
`indigo`, `emerald`, and `amber` presets. The two selectors have equal
|
|
61
|
+
specificity, so source order decides which wins. In the wrong order every
|
|
62
|
+
file still loads, the attribute is still on `<html>`, nothing errors — and
|
|
63
|
+
the page renders in the default palette.
|
|
64
|
+
```erb
|
|
65
|
+
<%%= stylesheet_link_tag "senren" %>
|
|
66
|
+
<%%= stylesheet_link_tag "senren_themes" %>
|
|
67
|
+
|
|
68
|
+
<html data-senren-theme="rose"> <%%# omit for the default palette %>
|
|
69
|
+
```
|
|
70
|
+
The attribute belongs on `<html>` so it composes with `.dark` rather than
|
|
71
|
+
replacing it. Add a palette by copying a block and changing values; no
|
|
72
|
+
component reads the theme, so none needs editing.
|
|
73
|
+
|
|
74
|
+
11. **`class:` is merged, not resolved.** Passing `class_name: "max-w-sm"` to a
|
|
75
|
+
dialog produces `class="… max-w-lg max-w-sm …"` — the component's own class
|
|
76
|
+
is still there. Both have the same specificity, so the winner is decided by
|
|
77
|
+
which one Tailwind emits later in the stylesheet, **not** by the order in
|
|
78
|
+
the attribute. Narrowing a dialog works; widening it silently does not.
|
|
79
|
+
When overriding a utility the component already uses, add Tailwind's
|
|
80
|
+
important modifier:
|
|
81
|
+
```erb
|
|
82
|
+
<%%= render Senren::DialogComponent.new(class_name: "max-w-2xl!") %>
|
|
83
|
+
```
|
|
84
|
+
Classes from families the component does not use need no modifier.
|
|
85
|
+
|
|
45
86
|
## File ownership
|
|
46
87
|
|
|
47
88
|
| Path | Owned by |
|