senren-ui 0.2.0 → 0.4.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 +238 -0
- data/CONTRIBUTING.md +11 -0
- data/README.md +64 -0
- data/docs/i18n.md +194 -0
- data/docs/visual_style.md +80 -0
- data/lib/generators/senren/install/install_generator.rb +39 -0
- data/lib/generators/senren/install/templates/base_component.rb.tt +136 -7
- data/lib/generators/senren/install/templates/conventions.md.tt +57 -0
- data/lib/generators/senren/install/templates/senren_themes.css.tt +339 -0
- data/lib/senren/rails/agent_rules_writer.rb +7 -0
- data/lib/senren/rails/base_component_patch.rb +24 -0
- data/lib/senren/rails/component_copier.rb +21 -0
- data/lib/senren/rails/host_paths.rb +5 -0
- data/lib/senren/rails/locale_installer.rb +90 -0
- data/lib/senren/rails/version.rb +1 -1
- data/lib/senren/rails.rb +1 -0
- data/lib/tasks/senren.rake +9 -0
- data/templates/components/accordion/accordion_component.html.erb +24 -6
- data/templates/components/accordion/accordion_component.rb +2 -1
- data/templates/components/alert_dialog/alert_dialog_component.html.erb +7 -4
- data/templates/components/alert_dialog/alert_dialog_component.rb +4 -0
- data/templates/components/api_key_field/api_key_field_component.html.erb +2 -2
- data/templates/components/api_key_field/api_key_field_component.rb +8 -1
- data/templates/components/app_shell/app_shell_component.rb +2 -1
- data/templates/components/breadcrumb/breadcrumb_component.html.erb +8 -6
- data/templates/components/breadcrumb/breadcrumb_component.rb +2 -1
- data/templates/components/bulk_action_bar/bulk_action_bar_component.html.erb +1 -1
- data/templates/components/bulk_action_bar/bulk_action_bar_component.rb +4 -2
- 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/calendar/calendar_component.rb +37 -2
- data/templates/components/card/card_component.html.erb +4 -6
- data/templates/components/carousel/carousel_component.html.erb +6 -6
- data/templates/components/carousel/carousel_component.rb +12 -1
- data/templates/components/cart/cart_component.html.erb +13 -7
- data/templates/components/cart/cart_component.rb +11 -2
- data/templates/components/clipboard/clipboard_component.html.erb +3 -3
- data/templates/components/clipboard/clipboard_component.rb +6 -1
- data/templates/components/collapsible/collapsible_component.html.erb +3 -3
- data/templates/components/collapsible/collapsible_component.rb +1 -1
- data/templates/components/combobox/combobox_component.html.erb +3 -3
- data/templates/components/combobox/combobox_component.rb +2 -1
- data/templates/components/command/command_component.html.erb +1 -1
- data/templates/components/command/command_component.rb +3 -1
- data/templates/components/context_menu/context_menu_component.html.erb +6 -3
- data/templates/components/data_table/data_table_component.html.erb +3 -3
- data/templates/components/data_table/data_table_component.rb +16 -1
- data/templates/components/date_picker/date_picker_component.html.erb +5 -5
- data/templates/components/date_picker/date_picker_component.rb +2 -1
- data/templates/components/dialog/dialog_component.html.erb +9 -5
- 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/filter_bar/filter_bar_component.rb +1 -1
- data/templates/components/form/form_component.html.erb +1 -1
- data/templates/components/form/form_component.rb +21 -0
- data/templates/components/hover_card/hover_card_component.html.erb +6 -3
- data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +11 -9
- data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +11 -3
- 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/native_select/native_select_component.rb +12 -3
- data/templates/components/pagination/pagination_component.html.erb +4 -4
- data/templates/components/pagination/pagination_component.rb +2 -1
- data/templates/components/popover/popover_component.html.erb +6 -3
- data/templates/components/product_card/product_card_component.html.erb +3 -3
- data/templates/components/product_card/product_card_component.rb +2 -2
- data/templates/components/progress/progress_component.html.erb +2 -2
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.html.erb +2 -16
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.rb +32 -1
- data/templates/components/search_input/search_input_component.html.erb +2 -2
- data/templates/components/search_input/search_input_component.rb +2 -1
- data/templates/components/sheet/sheet_component.html.erb +15 -6
- 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 +4 -4
- data/templates/components/sidebar/sidebar_component.rb +4 -1
- 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/tabs/tabs_component.rb +4 -2
- data/templates/components/theme_toggle/theme_toggle_component.html.erb +17 -3
- data/templates/components/theme_toggle/theme_toggle_component.rb +6 -1
- data/templates/components/tooltip/tooltip_component.html.erb +5 -3
- data/templates/components/top_nav/top_nav_component.html.erb +3 -3
- data/templates/components/top_nav/top_nav_component.rb +3 -2
- data/templates/controllers/accordion_controller.js +68 -3
- data/templates/controllers/api_key_field_controller.js +3 -2
- data/templates/controllers/carousel_controller.js +6 -1
- data/templates/controllers/cart_controller.js +3 -1
- data/templates/controllers/clipboard_controller.js +3 -3
- data/templates/controllers/rich_text_editor_lite_controller.js +2 -2
- data/templates/controllers/theme_toggle_controller.js +13 -3
- data/templates/locales/senren.en.yml +151 -0
- data/templates/locales/senren.vi.yml +151 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 99b7430e710e44023cbc886c862b4d237d06bb489a4a00f5e30685b59768f6dc
|
|
4
|
+
data.tar.gz: 6ead04eac8e4e518cbf7129c65dfb63ad683db3dfb44b13a794366eee8be37dd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7cf42a8f5992b7de9bc4135c7d6bea7c44987ca3844de06b575876957877efd773711cdfcb9cd54d2ddc1ca878f38842d4df082b0fb7451b5b63613c9bb5f056
|
|
7
|
+
data.tar.gz: e3c329bc7eb8ef0c7b90af18971c92f24dfdea4822e17a9e43faff3b1e94a6815ed4b6c18cff30a2e65932cd8bf96d88b2e64153671d72e2a52c8ffe2d759c76
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,244 @@ 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.4.0] — 2026-10-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Every word a component shows or announces can be translated from the host
|
|
15
|
+
app, without editing a component.** Visible text, `aria-label` and `sr-only`
|
|
16
|
+
text, the defaults of `label:`, `placeholder:` and `empty_text:`, the
|
|
17
|
+
calendar's month and weekday names, and the sentences a Stimulus controller
|
|
18
|
+
says after load now go through one helper, `senren_t`, with the English as its
|
|
19
|
+
default: `senren_t('pagination.next', default: 'Next')` is
|
|
20
|
+
`I18n.t("senren.pagination.next", default: "Next")`. 111 keys across 27
|
|
21
|
+
components, named `senren.<component>.<key>`, with `senren.shared.*` for the
|
|
22
|
+
two words that are the same word in several components.
|
|
23
|
+
|
|
24
|
+
An app with no locale file renders exactly what it did before, and a missing
|
|
25
|
+
translation never raises, including under
|
|
26
|
+
`config.i18n.raise_on_missing_translations`. Text you pass in yourself is used
|
|
27
|
+
as given and never looked up; `label: nil` still means "none", and leaving the
|
|
28
|
+
argument out is what gets the translated default.
|
|
29
|
+
|
|
30
|
+
The gem ships `senren.en.yml` and `senren.vi.yml`, and copies either only
|
|
31
|
+
when asked for: `bin/rails generate senren:install --locales vi`, or later
|
|
32
|
+
`bin/rails senren:locales vi`. Not even English is copied by default: a copied
|
|
33
|
+
`senren.en.yml` loads after the app's `en.yml`, overriding any `senren.*` key
|
|
34
|
+
set there, and pins today's wording against later releases. An existing file
|
|
35
|
+
is never overwritten unless that is asked for by name
|
|
36
|
+
(`senren:locales vi --force`, or `senren:install --force-locales`); the
|
|
37
|
+
`--force` that refreshes Senren's own files leaves translations alone. To add
|
|
38
|
+
another language, `bin/rails senren:locales en`, copy the file, change `en:`,
|
|
39
|
+
and translate the values. A translation with a misspelt `%{variable}` falls
|
|
40
|
+
back to the English instead of raising. See [docs/i18n.md](docs/i18n.md).
|
|
41
|
+
|
|
42
|
+
`bin/i18n-sync` regenerates `templates/locales/senren.en.yml` from the code,
|
|
43
|
+
and a test fails if it, `senren.vi.yml`, and the components disagree, or if a
|
|
44
|
+
template shows a word that did not go through `senren_t`.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **Five Stimulus controllers no longer carry an English sentence.** `clipboard`
|
|
49
|
+
("Copied", "Copied to clipboard"), `api_key_field` ("Copy complete"),
|
|
50
|
+
`carousel` ("Slide 2 of 3"), `theme_toggle` ("Light theme", "Dark theme") and
|
|
51
|
+
`rich_text_editor_lite` (the link prompt) read the sentence from a Stimulus
|
|
52
|
+
value the component renders, so they speak the host's language. The markup
|
|
53
|
+
gains six `data-senren--<controller>-…-value` attributes and is otherwise
|
|
54
|
+
byte-for-byte what it was.
|
|
55
|
+
|
|
56
|
+
**Migrate:** none for an unmodified install. The controller and its component
|
|
57
|
+
go together: if you copied a component and edited its controller, or the other
|
|
58
|
+
way round, copy both again (`bin/rails senren:add <name> --force`) so the value
|
|
59
|
+
the controller reads is the one the component writes.
|
|
60
|
+
- `bin/rails senren:add` appends `senren_t` to an existing
|
|
61
|
+
`app/components/senren/base_component.rb` that lacks it, so a component added
|
|
62
|
+
after upgrading the gem does not raise `NoMethodError`. Components you copied
|
|
63
|
+
earlier keep the English written into them until you copy them again.
|
|
64
|
+
- The performance budgets for component source (140000 -> 150000 bytes) and for
|
|
65
|
+
the Stimulus payload (56000 -> 56500) are raised, with the reasons recorded in
|
|
66
|
+
`config/performance_budgets.yml`. The per-file caps and the gzip total are
|
|
67
|
+
unchanged.
|
|
68
|
+
- **`NativeSelectComponent` defaults to its own chevron instead of the browser's
|
|
69
|
+
arrow** (`native_arrow: false`). A select was the one control in the library
|
|
70
|
+
with no visible state: the OS arrow cannot be styled or animated, so nothing
|
|
71
|
+
distinguished an open select from a closed one. The custom chevron already
|
|
72
|
+
existed, rotated on focus, and had to be asked for by name — which meant
|
|
73
|
+
nobody got it. Pass `native_arrow: true` to opt back in; worth doing on
|
|
74
|
+
mobile, where the OS renders its own picker anyway.
|
|
75
|
+
|
|
76
|
+
The component marker moves with it. In custom-arrow mode
|
|
77
|
+
`data-senren-component="native_select"` sits on the wrapper that positions the
|
|
78
|
+
chevron, and the `<select>` underneath still carries the name, the controller
|
|
79
|
+
and the selection. Anything selecting on `select[data-senren-component]` needs
|
|
80
|
+
updating.
|
|
81
|
+
- `InviteMemberDialogComponent` renders its role field through
|
|
82
|
+
`NativeSelectComponent` rather than a hand-written `<select>` whose styling
|
|
83
|
+
had been copied out of it — which is why it silently missed everything that
|
|
84
|
+
component gained afterwards, the chevron included.
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
- `FormComponent` yields the Rails form builder, as `docs/components.md` always
|
|
89
|
+
said it did. The block received the component instead, and because a
|
|
90
|
+
component is an `ActionView::Base` the documented `f.text_field :title,
|
|
91
|
+
class: "x"` did not raise: it reached the template-level helper and rendered
|
|
92
|
+
`name="title[{class: "x"}]"`, so the field posted under a name no controller
|
|
93
|
+
reads. Blocks that never touch `f`, and `with_content`, render as before.
|
|
94
|
+
|
|
95
|
+
Code that treated `|f|` as the component breaks: `f.tag`, `f.link_to`,
|
|
96
|
+
`f.render`, `f.model` and `f.url` now raise `NoMethodError` on the builder.
|
|
97
|
+
Call those on the view instead. A `FormComponent` rendered with neither a
|
|
98
|
+
block nor content now renders an empty `<form>` rather than raising.
|
|
99
|
+
- `TopNavComponent`'s default variant had no background. Its class,
|
|
100
|
+
`bg-[hsl(var(--senren-background))/0.88]`, put the alpha outside `hsl()`,
|
|
101
|
+
inside the brackets, so Tailwind emitted `hsl(...)/0.88` verbatim and the
|
|
102
|
+
browser dropped it as an invalid colour: the sticky header was transparent
|
|
103
|
+
over scrolled content. It is now `bg-[hsl(var(--senren-background)/0.88)]`,
|
|
104
|
+
the form every other translucent class in the library uses, and a test bans
|
|
105
|
+
the broken shape across `templates/` and `lib/`.
|
|
106
|
+
- `TableComponent` and `DataTableComponent` are now `relative`, so their
|
|
107
|
+
`sr-only` caption is positioned inside the component. `sr-only` is
|
|
108
|
+
`position: absolute`, and with nothing positioned the caption escaped the
|
|
109
|
+
components' own overflow clipping and was placed against the page. In a
|
|
110
|
+
fixed-height shell whose `<main>` scrolls, a captioned table below the fold
|
|
111
|
+
made the document taller, and the window scrolled into blank space under a
|
|
112
|
+
shell that should never scroll.
|
|
113
|
+
- `DataTableComponent` no longer breaks a `<td>` whose cell holds markup. The
|
|
114
|
+
cell value was written straight into `data-sort-value="..."`, and a badge,
|
|
115
|
+
link or anything else built with `tag` or `render` is a SafeBuffer, which ERB
|
|
116
|
+
does not escape: the markup's first `"` closed the attribute, the rest became
|
|
117
|
+
junk attributes, and every row sorted on the same `<span class=` fragment.
|
|
118
|
+
The sort value is now the cell's decoded text, escaped once, so a cell
|
|
119
|
+
showing `R&D` sorts as `R&D`. Plain-string and numeric cells are unchanged.
|
|
120
|
+
- `ThemeToggleComponent` rendered the letters `O`, `D` and `L` where an icon
|
|
121
|
+
belongs. It now ships sun and moon SVGs and swaps which is hidden, so the
|
|
122
|
+
correct icon is in the HTML the server sends rather than written in by
|
|
123
|
+
JavaScript after first paint. The icon names the theme the click switches to,
|
|
124
|
+
matching the label.
|
|
125
|
+
- `AccordionComponent` showed a static `+` and snapped open. It now has a
|
|
126
|
+
chevron that turns over, driven by `aria-expanded` so the icon cannot drift
|
|
127
|
+
out of step with the state assistive technology is told, and the panel
|
|
128
|
+
animates via `grid-template-rows: 0fr → 1fr` — which reaches the content's own
|
|
129
|
+
height without measuring it in JavaScript.
|
|
130
|
+
|
|
131
|
+
The `hidden` attribute is kept through the animation. Content that is visually
|
|
132
|
+
collapsed but present is still focusable and still read aloud, so the
|
|
133
|
+
controller drops `hidden` before opening and restores it once the closing
|
|
134
|
+
transition ends, with a timer for the cases where `transitionend` never
|
|
135
|
+
arrives: reduced motion, an off-screen element, an already-collapsed panel.
|
|
136
|
+
|
|
137
|
+
## [0.3.0] — 2026-09-01
|
|
138
|
+
|
|
139
|
+
### Breaking
|
|
140
|
+
|
|
141
|
+
- **`ButtonComponent` no longer emits `type="button"` by default.** The
|
|
142
|
+
attribute is omitted, so a button inside a form submits it, which is what
|
|
143
|
+
plain HTML does and what everyone expects. The old default silently swallowed
|
|
144
|
+
submits: a form's own submit button did nothing, with no error anywhere to
|
|
145
|
+
explain it. The new default fails the other way, and loudly — a trigger that
|
|
146
|
+
should not submit now has to say so.
|
|
147
|
+
|
|
148
|
+
**Migrate:** pass `type: :button` to any button that must not submit — overlay
|
|
149
|
+
triggers, menu triggers, a dialog's Cancel. Everything inside a form that is
|
|
150
|
+
meant to submit needs no change and starts working. Buttons outside a form
|
|
151
|
+
are unaffected either way.
|
|
152
|
+
|
|
153
|
+
```erb
|
|
154
|
+
<%= render(Senren::ButtonComponent.new(type: :button)) { "Open dialog" } %>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Fixed
|
|
158
|
+
|
|
159
|
+
- Every component now merges a caller's `class:` and `data:` instead of
|
|
160
|
+
dropping them. Splatting `**html_attrs` after the computed values replaced
|
|
161
|
+
them outright, so `class:` erased the component's own variant and size
|
|
162
|
+
styling and `data:` erased its `data-senren-component` marker.
|
|
163
|
+
- For the eight wrapper components — dialog, alert dialog, sheet, popover,
|
|
164
|
+
dropdown menu, context menu, hover card, tooltip — a caller's class now lands
|
|
165
|
+
on the panel, which is the element they style, not on the empty root, where it
|
|
166
|
+
sat in the DOM doing nothing.
|
|
167
|
+
|
|
168
|
+
This makes the extension point reachable; it does not make it resolve
|
|
169
|
+
conflicts. `class_name: "max-w-sm"` on a dialog emits
|
|
170
|
+
`class="max-w-lg max-w-sm"` and both declarations stay live, so which one
|
|
171
|
+
applies is decided by the order Tailwind emits them, not by the order of the
|
|
172
|
+
attribute. Measured against a real build: `max-w-sm` wins (narrowing works),
|
|
173
|
+
`max-w-2xl` loses (widening is a silent no-op). Use Tailwind's important
|
|
174
|
+
modifier — `class_name: "max-w-2xl!"` — for an override that does not depend
|
|
175
|
+
on emit order.
|
|
176
|
+
- `data-controller` and `data-action` are appended rather than substituted, so
|
|
177
|
+
attaching your own Stimulus controller to a Senren component no longer unbinds
|
|
178
|
+
the component's own.
|
|
179
|
+
- A caller's `data-controller` or `data-action` that repeated one of the
|
|
180
|
+
component's own tokens was appended rather than deduplicated:
|
|
181
|
+
`data: { controller: "a senren--popover" }` produced
|
|
182
|
+
`"a senren--popover senren--popover"`. Stimulus reads that list literally, so
|
|
183
|
+
the controller connected twice and every action fired twice — a toggle opened
|
|
184
|
+
and immediately closed. Tokens are deduplicated individually now.
|
|
185
|
+
- `data:` written with String keys (`data: { "action" => ... }`) is merged the
|
|
186
|
+
same as Symbol keys. The Symbol-only read meant a dropdown item passing a
|
|
187
|
+
String key lost close-on-click and arrow-key handling.
|
|
188
|
+
- A dropdown item's `class:` is merged rather than substituted. Losing the hover
|
|
189
|
+
style was cosmetic; losing `focus:bg-` removed the only indication a keyboard
|
|
190
|
+
user has of where they are in the menu.
|
|
191
|
+
- Sheet's scrolling body no longer clips focus rings.
|
|
192
|
+
- Dialog, sheet, and the invite-member dialog close on an overlay click.
|
|
193
|
+
- `method:` on a dropdown item reaches Turbo. It was passed to `link_to` as a
|
|
194
|
+
rails-ujs option, which Rails 7 dropped, so it had rendered an inert `method`
|
|
195
|
+
attribute since the library began targeting Rails 7.1.
|
|
196
|
+
- Card footer spacing, and pagination now wraps.
|
|
197
|
+
- `CartComponent` accepted `remove_url:` per item, normalised it, and wrote it
|
|
198
|
+
nowhere. Removal is client-side and dispatches `senren--cart:removed` for the
|
|
199
|
+
application to act on, so the URL was the one thing the listener needed and
|
|
200
|
+
the only thing it could not get. It is emitted as `data-remove-url` on the
|
|
201
|
+
line and carried in the event detail, omitted entirely when not supplied, and
|
|
202
|
+
passed through `safe_url` like every other URL the library renders.
|
|
203
|
+
- `senren_themes.css` shipped with its ERB examples unrendered, so the header
|
|
204
|
+
comment showed `<%%=` where it means `<%=`. The assertion guarding against that
|
|
205
|
+
named a single file, so the property held exactly where someone had thought to
|
|
206
|
+
look; it now runs over everything the generator writes.
|
|
207
|
+
|
|
208
|
+
### Documentation
|
|
209
|
+
|
|
210
|
+
- The palette presets shipped in 0.2.0 with no mention in the README, the docs,
|
|
211
|
+
or the generated conventions file. All three now cover them, including the
|
|
212
|
+
load-order constraint: `senren_themes.css` must be linked after `senren.css`
|
|
213
|
+
or the theme silently does nothing.
|
|
214
|
+
|
|
215
|
+
### Internal
|
|
216
|
+
|
|
217
|
+
- ERB linting names every linter it runs. Thirteen were running against nine
|
|
218
|
+
named in the config; eight formatting linters had been on by default and
|
|
219
|
+
unrecorded. A test now fails if an upgrade adds a fourteenth.
|
|
220
|
+
- `bin/ci` and the GitHub workflow call `erb_lint` rather than the deprecated
|
|
221
|
+
`erblint` shim.
|
|
222
|
+
- json bumped to 2.21.2 for CVE-2026-71847.
|
|
223
|
+
- Herb now lints the markup, and the 32 offenses it found in shipped templates
|
|
224
|
+
are fixed. It was adopted after a developer reported warnings in their editor
|
|
225
|
+
that none of this project's gates produced — RuboCop covered the Ruby, Biome
|
|
226
|
+
the Stimulus controllers, erb_lint the ERB structure, and the HTML itself had
|
|
227
|
+
no linter at all. Pinned to 0.10.3 in `.herb.yml`, and wired into `bin/ci`,
|
|
228
|
+
`bin/lint-fix` and the GitHub workflow.
|
|
229
|
+
|
|
230
|
+
Most of what it found is invisible in the rendered page but visible in the
|
|
231
|
+
editor of anyone who installs these components, since `senren:add` copies them
|
|
232
|
+
into their repository. Fifteen component roots moved from
|
|
233
|
+
`<div <%= tag.attributes(**root_attrs(...)) %>>` to `tag.div(...) do`, which
|
|
234
|
+
is what the overlays already used; seven conditional boolean attributes moved
|
|
235
|
+
from `<%= "hidden" unless expanded %>` to `<% unless expanded %>hidden<% end %>`;
|
|
236
|
+
three raw `<img>` tags became `image_tag`; breadcrumb stopped writing its whole
|
|
237
|
+
`<nav>` once per branch; and an empty `aria-activedescendant` is gone.
|
|
238
|
+
|
|
239
|
+
Nothing changes visually, but the markup string is not identical: rendering
|
|
240
|
+
all 64 components before and after produces 35 differing lines — 26 of them
|
|
241
|
+
whitespace and indentation, 6 the attribute order and self-closing slash that
|
|
242
|
+
`image_tag` emits, 2 the `href` moving to the front of an `<a>`, and 1 the
|
|
243
|
+
removed empty attribute. If you assert on exact markup anywhere, those
|
|
244
|
+
snapshots need regenerating. If you have already installed these components,
|
|
245
|
+
nothing in your application changes at all — the new markup arrives only if
|
|
246
|
+
you re-run `senren:add --force`.
|
|
247
|
+
|
|
10
248
|
## [0.2.0] — 2026-08-02
|
|
11
249
|
|
|
12
250
|
A hardening release. Most of it came out of an adversarial review of the whole
|
data/CONTRIBUTING.md
CHANGED
|
@@ -35,6 +35,8 @@ 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
|
|
39
|
+
bin/i18n-sync --check # templates/locales/senren.en.yml matches the components
|
|
38
40
|
```
|
|
39
41
|
|
|
40
42
|
`bin/system` needs no setup: it uses a system Chrome and
|
|
@@ -86,6 +88,13 @@ real documentation/reference app is maintained separately in
|
|
|
86
88
|
- ViewComponent Ruby class under `Senren::` namespace.
|
|
87
89
|
- ERB template with `data-senren-component="<name>"` on root.
|
|
88
90
|
- Tailwind classes use semantic tokens only.
|
|
91
|
+
- Every word the component shows or announces (visible text, `aria-label`,
|
|
92
|
+
`sr-only`, a default `label:` or `placeholder:`, a sentence a controller will
|
|
93
|
+
say) goes through `senren_t('<component>.<key>', default: 'English')` with a
|
|
94
|
+
literal key and a literal default, and a controller receives it as a Stimulus
|
|
95
|
+
value, never as text in JavaScript. Run `bin/i18n-sync` to regenerate
|
|
96
|
+
`templates/locales/senren.en.yml` and add the same key to `senren.vi.yml`.
|
|
97
|
+
See `docs/i18n.md`.
|
|
89
98
|
- Variants/sizes declared as class-level constants.
|
|
90
99
|
- Stimulus controller iff client behavior is needed
|
|
91
100
|
(`app/javascript/controllers/senren/<name>_controller.js`).
|
|
@@ -114,6 +123,8 @@ real documentation/reference app is maintained separately in
|
|
|
114
123
|
templates, Stimulus controllers, or Importmap loading guidance.
|
|
115
124
|
- Run `bun run controllers:check` before pushing if you touched
|
|
116
125
|
`templates/controllers/*.js`.
|
|
126
|
+
- Run `bin/i18n-sync --check` before pushing if you touched text in a
|
|
127
|
+
component; `bin/i18n-sync` fixes what it reports.
|
|
117
128
|
|
|
118
129
|
## Pull request workflow
|
|
119
130
|
|
data/README.md
CHANGED
|
@@ -53,6 +53,67 @@ 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
|
+
|
|
83
|
+
## Translating components
|
|
84
|
+
|
|
85
|
+
Every word a component shows or announces (labels, `aria-label` and
|
|
86
|
+
screen-reader text, default placeholders, the calendar's month and weekday names,
|
|
87
|
+
and what a Stimulus controller says after load) goes through one helper with its
|
|
88
|
+
English as the default:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
senren_t('pagination.next', default: 'Next') # I18n.t("senren.pagination.next", default: "Next")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
An app with no locale file renders exactly the English it always did, and the
|
|
95
|
+
installer copies none unless asked. To change a word, set its `senren.*` key in
|
|
96
|
+
your own locale file. To add a language, copy a shipped file and translate it:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
bin/rails generate senren:install --locales vi # writes config/locales/senren.vi.yml
|
|
100
|
+
bin/rails senren:locales en # every key and its English, to translate from
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
# config/locales/senren.fr.yml - copy senren.en.yml, change `en:` to `fr:`, translate the values
|
|
105
|
+
fr:
|
|
106
|
+
senren:
|
|
107
|
+
pagination:
|
|
108
|
+
previous: "Précédent"
|
|
109
|
+
next: "Suivant"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The gem ships `en` and `vi`. A key you leave out stays English, and a missing
|
|
113
|
+
key never raises. [docs/i18n.md](docs/i18n.md) has the key naming, the values a
|
|
114
|
+
controller reads, what is not translated, and how to move components you copied
|
|
115
|
+
before this existed.
|
|
116
|
+
|
|
56
117
|
## Daily commands
|
|
57
118
|
|
|
58
119
|
```bash
|
|
@@ -61,6 +122,7 @@ bin/rails generate senren:component picker --client # custom component with Sti
|
|
|
61
122
|
bin/rails generate senren:component picker --no-client # without Stimulus
|
|
62
123
|
bin/rails senren:add dialog --client # install interactive official component
|
|
63
124
|
bin/rails senren:add button # install static official component
|
|
125
|
+
bin/rails senren:locales vi # copy a shipped translation to config/locales
|
|
64
126
|
bundle exec rails senren:add form input # equivalent alternate entry point
|
|
65
127
|
bin/rails senren:skill:sync # rebuild .senren/skill.md
|
|
66
128
|
bin/rails senren:agents:sync # rebuild .senren/agent-rules + adapters
|
|
@@ -235,6 +297,8 @@ bin/performance # local payload/performance budgets
|
|
|
235
297
|
bun run controllers:check # lint + syntax check for templates/controllers/*.js
|
|
236
298
|
bun run controllers:lint:fix # auto-fix lint issues for controllers
|
|
237
299
|
bundle exec rake test:system # Stimulus/system tests
|
|
300
|
+
bin/lint-fix # auto-fix RuboCop, ERB lint, and controller JS
|
|
301
|
+
bin/i18n-sync # rewrite templates/locales/senren.en.yml from the senren_t calls
|
|
238
302
|
```
|
|
239
303
|
|
|
240
304
|
## Contributing
|
data/docs/i18n.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Translating Senren components
|
|
2
|
+
|
|
3
|
+
Every word a component shows or announces can be translated from your app's
|
|
4
|
+
locale files, without editing a component. That covers the visible labels
|
|
5
|
+
("Previous", "Next", "Close", "Today"), the `aria-label` and screen-reader text
|
|
6
|
+
("Toggle sidebar", "Go to slide 2"), the defaults for arguments such as
|
|
7
|
+
`label:` and `placeholder:`, the calendar's month and weekday names, and the
|
|
8
|
+
sentences a Stimulus controller writes into the page after it loads ("Copied",
|
|
9
|
+
"Slide 2 of 3").
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
Each string goes through one helper, `senren_t`, with its English as the
|
|
14
|
+
default:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
senren_t('pagination.next', default: 'Next')
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That is `I18n.t("senren.pagination.next", default: "Next")`. If your app defines
|
|
21
|
+
`senren.pagination.next` for the current locale it wins; if not, the English in
|
|
22
|
+
the component is used. Two things follow:
|
|
23
|
+
|
|
24
|
+
- **Nothing is required for English.** Install Senren, render a component, and it
|
|
25
|
+
says what it always said. There is no locale file to forget.
|
|
26
|
+
- **A missing translation never raises**, not even with
|
|
27
|
+
`config.i18n.raise_on_missing_translations = true`, because I18n only reports
|
|
28
|
+
a key that has no default.
|
|
29
|
+
|
|
30
|
+
Keys are `senren.<component>.<key>`. `senren.shared.*` holds the few words that
|
|
31
|
+
are the same word in more than one component (`open`, `close`).
|
|
32
|
+
|
|
33
|
+
## Install the locale files
|
|
34
|
+
|
|
35
|
+
`senren:install` copies no locale file: English needs none. Every file is
|
|
36
|
+
copied only when you ask for it by name:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bin/rails generate senren:install --locales vi # at install time
|
|
40
|
+
bin/rails senren:locales vi # any time later
|
|
41
|
+
bin/rails senren:locales en # every key and its English, to translate from
|
|
42
|
+
bin/rails senren:locales vi --force # replace your copy with the shipped one
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Today the gem ships `en` and `vi`. The files are yours once they are copied:
|
|
46
|
+
the installer never overwrites one that exists unless you ask for that file to
|
|
47
|
+
be replaced (`senren:locales <name> --force`, or `--force-locales` on
|
|
48
|
+
`senren:install`). The `--force` that refreshes Senren's own files leaves them
|
|
49
|
+
alone, so a re-run after a gem upgrade does not undo your wording.
|
|
50
|
+
|
|
51
|
+
A copied `senren.en.yml` has two side effects, which is why it is not copied by
|
|
52
|
+
default. Rails loads `config/locales` in name order, so it loads after your
|
|
53
|
+
`en.yml` and overrides any `senren.*` key you set there. And it pins today's
|
|
54
|
+
English: when a later Senren release improves a default, an app holding the copy
|
|
55
|
+
keeps the old wording. Copy it to translate from, and delete it once your own
|
|
56
|
+
language file exists.
|
|
57
|
+
|
|
58
|
+
A translation is only loaded when its file is in `config/locales`, and a locale
|
|
59
|
+
becomes known to I18n when a file for it is loaded. If your app builds a
|
|
60
|
+
language switcher from `I18n.available_locales`, copying `senren.vi.yml` is what
|
|
61
|
+
makes Vietnamese appear in it, which is why it is opt-in.
|
|
62
|
+
|
|
63
|
+
## Change one word
|
|
64
|
+
|
|
65
|
+
Define just that key in any locale file you already have. You do not need to
|
|
66
|
+
copy the whole file:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
# config/locales/en.yml
|
|
70
|
+
en:
|
|
71
|
+
senren:
|
|
72
|
+
pagination:
|
|
73
|
+
next: "Newer"
|
|
74
|
+
previous: "Older"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Everything you do not mention keeps its English. If you copied
|
|
78
|
+
`config/locales/senren.en.yml`, edit the value there instead: that file loads
|
|
79
|
+
after `en.yml` and wins.
|
|
80
|
+
|
|
81
|
+
## Add a language
|
|
82
|
+
|
|
83
|
+
1. Run `bin/rails senren:locales en`, then copy `config/locales/senren.en.yml` to
|
|
84
|
+
`config/locales/senren.fr.yml`.
|
|
85
|
+
2. Change the top-level key from `en:` to `fr:`.
|
|
86
|
+
3. Translate the values.
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
fr:
|
|
90
|
+
senren:
|
|
91
|
+
pagination:
|
|
92
|
+
label: "Pagination"
|
|
93
|
+
previous: "Précédent"
|
|
94
|
+
next: "Suivant"
|
|
95
|
+
cart:
|
|
96
|
+
remove_item: "Retirer %{name}"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Keep every `%{name}` spelled exactly as it is. They are filled in when the
|
|
100
|
+
component renders. A translation that names a variable the component does not
|
|
101
|
+
pass (a typo such as `%{nmae}`) cannot be filled in, so that string falls back to
|
|
102
|
+
its English rather than raising. You can
|
|
103
|
+
move a variable anywhere in the sentence, and leave one out if the sentence does
|
|
104
|
+
not need it. A key you delete falls back to English, so a partial translation is
|
|
105
|
+
a working translation.
|
|
106
|
+
|
|
107
|
+
Then select the locale the way your app already does, for example
|
|
108
|
+
`I18n.locale = :fr` in an `around_action`. A word you did not translate shows in
|
|
109
|
+
English, because the English is the default.
|
|
110
|
+
|
|
111
|
+
If you would like the language included in the gem, send the file in a pull
|
|
112
|
+
request: `templates/locales/senren.<locale>.yml`, with the same keys as
|
|
113
|
+
`senren.en.yml`. `test/i18n_catalog_test.rb` checks that the keys and the
|
|
114
|
+
`%{}` variables match.
|
|
115
|
+
|
|
116
|
+
## Text you pass in yourself
|
|
117
|
+
|
|
118
|
+
An argument you pass is used exactly as given and is never looked up:
|
|
119
|
+
|
|
120
|
+
```erb
|
|
121
|
+
<%= render Senren::PaginationComponent.new(label: "Pages", current_page: 2, total_pages: 5) %>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Only the argument you leave out gets the translated default. Two details:
|
|
125
|
+
|
|
126
|
+
- `label: nil` still means "no label". For an attribute such as `aria-label` that
|
|
127
|
+
omits it, as it always has. Pass `""` for an empty string and leave the
|
|
128
|
+
argument out for the default.
|
|
129
|
+
- Words that arrive as *data* are yours to supply: a column header built from the
|
|
130
|
+
key `:created_at` ("Created at"), the items of a menu, the text in a slot. Give
|
|
131
|
+
`TableComponent` and `DataTableComponent` a `label:` per column to control it.
|
|
132
|
+
|
|
133
|
+
Values you interpolate into a translation are text. A name containing `<b>`
|
|
134
|
+
shows as the characters `<b>`, not as an element.
|
|
135
|
+
|
|
136
|
+
## What a controller says
|
|
137
|
+
|
|
138
|
+
Five controllers write a sentence into the page after it loads. They no longer
|
|
139
|
+
carry the English in JavaScript: the component renders each one into a Stimulus
|
|
140
|
+
value, and the controller reads it.
|
|
141
|
+
|
|
142
|
+
| Controller | Value | Key |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| `clipboard` | `copied-status` | `senren.clipboard.copied_status` |
|
|
145
|
+
| `api_key_field` | `copied-status` | `senren.api_key_field.copied_status` |
|
|
146
|
+
| `carousel` | `status-template` | `senren.carousel.status` |
|
|
147
|
+
| `theme_toggle` | `light-label`, `dark-label` | `senren.theme_toggle.light`, `.dark` |
|
|
148
|
+
| `rich_text_editor_lite` | `link-prompt` | `senren.rich_text_editor_lite.link_prompt` |
|
|
149
|
+
|
|
150
|
+
The carousel's value is a template. Its `%{current}` and `%{total}` are left in
|
|
151
|
+
for the controller to fill in on every slide change, so a translation has to keep
|
|
152
|
+
them.
|
|
153
|
+
|
|
154
|
+
## Components you copied before this existed
|
|
155
|
+
|
|
156
|
+
`bin/rails senren:add <anything>` adds the `senren_t` helper to your
|
|
157
|
+
`app/components/senren/base_component.rb` if it is missing, so components you add
|
|
158
|
+
from now on work.
|
|
159
|
+
|
|
160
|
+
Components you copied earlier still have their English written into them. To
|
|
161
|
+
translate one, copy it again with `bin/rails senren:add pagination --force`
|
|
162
|
+
(which replaces any edits you made to that component), or change its strings by
|
|
163
|
+
hand. It is one call per string: swap `"Next"` for
|
|
164
|
+
`senren_t('pagination.next', default: 'Next')`. `senren.en.yml` lists the key for
|
|
165
|
+
every string.
|
|
166
|
+
|
|
167
|
+
## Writing a component or an edit that has text
|
|
168
|
+
|
|
169
|
+
Anything a person reads or a screen reader announces goes through `senren_t`
|
|
170
|
+
with a literal key and a literal English default:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
def initialize(label: senren_t('tabs.label', default: 'Tabs'), **html)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```erb
|
|
177
|
+
<button><%= senren_t('date_picker.today', default: 'Today') %></button>
|
|
178
|
+
<button aria-label="<%= senren_t('cart.remove_item', default: 'Remove %{name}', name: item[:name]) %>">
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- Use the component's own namespace. Use `shared` only for a word that more than
|
|
182
|
+
one component says.
|
|
183
|
+
- Put the English where the call is. Do not write it in a locale file first.
|
|
184
|
+
- Give an argument its default in the signature, as above. Ruby evaluates it
|
|
185
|
+
when the component is built, in the locale that is current then.
|
|
186
|
+
- A string a controller needs goes to it as a Stimulus value, never as text in the
|
|
187
|
+
JavaScript.
|
|
188
|
+
- Symbols that are not words (`×`, `+`, `*`) stay as they are.
|
|
189
|
+
|
|
190
|
+
If you are working on the gem itself, `bin/i18n-sync` regenerates
|
|
191
|
+
`templates/locales/senren.en.yml` from the calls, and `bin/i18n-sync --check`
|
|
192
|
+
reports whether it is current. `senren.vi.yml` is written by hand. The tests fail
|
|
193
|
+
if the two files disagree with the code or with each other, if a call is not
|
|
194
|
+
literal, and if a template shows a word that did not go through `senren_t`.
|
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.
|