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
|
@@ -11,6 +11,24 @@ module Senren
|
|
|
11
11
|
class_option :force, type: :boolean, default: false,
|
|
12
12
|
desc: 'Overwrite existing Senren-managed files.'
|
|
13
13
|
|
|
14
|
+
# Nothing by default: English renders with no file. Every locale, English
|
|
15
|
+
# included, is asked for by name. A shipped translation adds its locale to
|
|
16
|
+
# I18n.available_locales, which a language switcher built from that list
|
|
17
|
+
# will then offer; a copied English file overrides the app's own en.yml.
|
|
18
|
+
#
|
|
19
|
+
# `enum:` makes Thor refuse an unknown locale while it parses the options,
|
|
20
|
+
# before any step has run. Refusing in the middle would leave an app with the
|
|
21
|
+
# components copied and no instruction files.
|
|
22
|
+
class_option :locales, type: :array, default: [],
|
|
23
|
+
enum: Senren::Rails::LocaleInstaller.available,
|
|
24
|
+
desc: 'Translations to copy into config/locales.'
|
|
25
|
+
|
|
26
|
+
# --force refreshes the files Senren owns. A locale file stops being one
|
|
27
|
+
# the moment it is copied: it is the app's translation. So replacing it is
|
|
28
|
+
# its own flag, and a routine --force re-run cannot undo someone's wording.
|
|
29
|
+
class_option :force_locales, type: :boolean, default: false,
|
|
30
|
+
desc: 'Replace locale files that already exist with the shipped ones.'
|
|
31
|
+
|
|
14
32
|
def create_senren_dir
|
|
15
33
|
empty_directory '.senren'
|
|
16
34
|
end
|
|
@@ -30,6 +48,9 @@ module Senren
|
|
|
30
48
|
def copy_base_files
|
|
31
49
|
template 'base_component.rb.tt', 'app/components/senren/base_component.rb'
|
|
32
50
|
template 'senren.css.tt', 'app/assets/stylesheets/senren.css'
|
|
51
|
+
# Optional: link it only if you want the palette presets. senren.css
|
|
52
|
+
# alone gives you the default, so this file costs nothing until used.
|
|
53
|
+
template 'senren_themes.css.tt', 'app/assets/stylesheets/senren_themes.css'
|
|
33
54
|
template 'conventions.md.tt', '.senren/conventions.md'
|
|
34
55
|
template 'installed_components.yml.tt', '.senren/installed_components.yml'
|
|
35
56
|
end
|
|
@@ -38,6 +59,23 @@ module Senren
|
|
|
38
59
|
copy_file Senren::Rails.registry_path, '.senren/registry.yml'
|
|
39
60
|
end
|
|
40
61
|
|
|
62
|
+
# Components work without these: the English is in the code. Nothing is
|
|
63
|
+
# copied unless asked for, English included -- a copied senren.en.yml would
|
|
64
|
+
# override the app's own en.yml and pin today's wording (see
|
|
65
|
+
# LocaleInstaller). An existing file is a translation someone wrote, so it
|
|
66
|
+
# is kept unless --force-locales is given.
|
|
67
|
+
def copy_locale_files
|
|
68
|
+
locales = options[:locales]
|
|
69
|
+
if locales.empty?
|
|
70
|
+
say_status :senren, 'no locale files copied: English needs none (bin/rails senren:locales vi to add one)'
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
say_status :senren, "copying locale files (#{locales.join(', ')}) to config/locales"
|
|
75
|
+
installer = Senren::Rails::LocaleInstaller.new(paths: host_paths)
|
|
76
|
+
installer.install(locales: locales, force: options[:force_locales])
|
|
77
|
+
end
|
|
78
|
+
|
|
41
79
|
# Switches Stimulus to on-demand loading instead of documenting it.
|
|
42
80
|
#
|
|
43
81
|
# Rails' default is `eagerLoadControllersFrom("controllers", application)`,
|
|
@@ -72,6 +110,7 @@ module Senren
|
|
|
72
110
|
def print_next_steps
|
|
73
111
|
say "\nSenren installed."
|
|
74
112
|
say 'Next: bin/rails senren:add button card badge alert dialog'
|
|
113
|
+
say 'Translate the components: bin/rails senren:locales vi (see docs/i18n.md)'
|
|
75
114
|
say 'Then: bin/rails senren:agents:sync'
|
|
76
115
|
end
|
|
77
116
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'uri'
|
|
2
|
+
require 'active_support/i18n'
|
|
2
3
|
require 'view_component'
|
|
3
4
|
|
|
4
5
|
module Senren
|
|
@@ -34,25 +35,51 @@ module Senren
|
|
|
34
35
|
# Splatting html_attrs after a computed `data:` used to overwrite it
|
|
35
36
|
# wholesale, so any component given a `data:` hash silently lost its
|
|
36
37
|
# data-senren-component marker. MaskedInput hit this on every render.
|
|
37
|
-
data = (
|
|
38
|
-
.merge(extra.delete(:data) || {})
|
|
39
|
-
.merge(senren_component: senren_component_name)
|
|
38
|
+
data = merge_data(caller_data, extra.delete(:data), { senren_component: senren_component_name })
|
|
40
39
|
|
|
41
40
|
# `class` had exactly the same defect and it was never fixed alongside
|
|
42
41
|
# `data`. Every Rails tag helper accepts `class:`, so it is the first
|
|
43
42
|
# thing anyone types — and splatting html_attrs replaced the computed
|
|
44
43
|
# 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
|
-
#
|
|
44
|
+
# no warning. It is merged like any other caller-supplied class instead.
|
|
45
|
+
#
|
|
46
|
+
# Merged, not resolved, and the distinction matters. See merge_classes.
|
|
47
47
|
tag_class = merge_classes(
|
|
48
48
|
classes,
|
|
49
49
|
self.class::VARIANTS[@variant],
|
|
50
50
|
self.class::SIZES[@size],
|
|
51
51
|
@class_name,
|
|
52
52
|
extra.delete(:class),
|
|
53
|
-
|
|
53
|
+
caller_class
|
|
54
54
|
)
|
|
55
|
-
{ class: tag_class, data: data, **
|
|
55
|
+
{ class: tag_class, data: data, **html_attrs_without_class_and_data, **extra }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# For components whose root is a wrapper and whose styling lives on an inner
|
|
59
|
+
# element -- every overlay: the panel is what gets positioned, the root just
|
|
60
|
+
# hosts the controller.
|
|
61
|
+
#
|
|
62
|
+
# Same merging as root_attrs, minus VARIANTS and SIZES. Passing those to the
|
|
63
|
+
# root of a sheet applied `translate-x-full` there, and a transformed
|
|
64
|
+
# ancestor becomes the containing block for its `position: fixed`
|
|
65
|
+
# descendants -- so the panel was positioned against the shifted root and
|
|
66
|
+
# rendered off-screen. It opened; you just could not see it.
|
|
67
|
+
def wrapper_attrs(*classes, **extra)
|
|
68
|
+
data = merge_data(caller_data, extra.delete(:data), { senren_component: senren_component_name })
|
|
69
|
+
tag_class = merge_classes(classes, extra.delete(:class))
|
|
70
|
+
|
|
71
|
+
{ class: tag_class.presence, data: data, **html_attrs_without_class_and_data, **extra }.compact
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The class list for the element a wrapper component actually styles.
|
|
75
|
+
#
|
|
76
|
+
# For an overlay the caller means the panel: `class_name: "max-w-2xl"` on a
|
|
77
|
+
# dialog is a request to widen the dialog, not to widen an invisible
|
|
78
|
+
# wrapper. Putting it on the root left it in the DOM doing nothing, which is
|
|
79
|
+
# harder to debug than dropping it -- the class is right there and the panel
|
|
80
|
+
# is still max-w-lg.
|
|
81
|
+
def panel_class(*classes)
|
|
82
|
+
merge_classes(classes, @class_name, caller_class)
|
|
56
83
|
end
|
|
57
84
|
|
|
58
85
|
def senren_component_name
|
|
@@ -79,8 +106,83 @@ module Senren
|
|
|
79
106
|
"#{base}-#{seed.parameterize}"
|
|
80
107
|
end
|
|
81
108
|
|
|
109
|
+
# Translates a string the component shows or announces: a visible label, an
|
|
110
|
+
# aria-label, sr-only text, a default placeholder, an announcement a Stimulus
|
|
111
|
+
# controller will read out.
|
|
112
|
+
#
|
|
113
|
+
# senren_t('pagination.next', default: 'Next')
|
|
114
|
+
# senren_t('cart.remove_item', default: 'Remove %{name}', name: item[:name])
|
|
115
|
+
#
|
|
116
|
+
# `default:` is the English text and it is required. It is what renders when
|
|
117
|
+
# the host app has no `senren.<key>` translation, which is why installing
|
|
118
|
+
# Senren needs no locale file, why a key nobody translated never raises (not
|
|
119
|
+
# even under `config.i18n.raise_on_missing_translations`, which only fires
|
|
120
|
+
# for a key with no default), and why the output is the same as it was before
|
|
121
|
+
# any string was translatable.
|
|
122
|
+
#
|
|
123
|
+
# This is I18n.t on purpose, not ViewComponent's `t`. `t` needs a view
|
|
124
|
+
# context, so it cannot run in a unit test that renders a template without
|
|
125
|
+
# Rails; it resolves a leading-dot key against the component's own sidecar
|
|
126
|
+
# scope, which is the lookup this namespace must not have; and it marks keys
|
|
127
|
+
# ending in `_html` as safe, which no Senren string is.
|
|
128
|
+
#
|
|
129
|
+
# Keys are `senren.<component>.<key>`; `senren.shared.<key>` is for a word
|
|
130
|
+
# that is the same word in more than one component. Interpolate with `%{}`.
|
|
131
|
+
# With no values passed the raw template comes back untouched, which is how a
|
|
132
|
+
# Stimulus controller is handed a string it fills in itself.
|
|
133
|
+
#
|
|
134
|
+
# A translation that names a variable the call does not pass (`%{nmae}`
|
|
135
|
+
# for `%{name}`) would raise I18n::MissingInterpolationArgument and turn a
|
|
136
|
+
# typo in a locale file into a 500. It falls back to the English instead,
|
|
137
|
+
# interpolated with the values the call did pass.
|
|
138
|
+
#
|
|
139
|
+
# Public, because the unit tests that render a template without a view
|
|
140
|
+
# delegate every public component method to it.
|
|
141
|
+
def senren_t(key, default:, **options)
|
|
142
|
+
I18n.t("senren.#{key}", default: default, **options)
|
|
143
|
+
rescue I18n::MissingInterpolationArgument
|
|
144
|
+
I18n.interpolate(default, options)
|
|
145
|
+
end
|
|
146
|
+
|
|
82
147
|
private
|
|
83
148
|
|
|
149
|
+
# Callers write `data: { controller: "x" }` and `data: { "controller" => "x" }`
|
|
150
|
+
# interchangeably, and Rails renders both the same, so the merge has to see
|
|
151
|
+
# both the same. Reading with dig(:symbol) missed the String form entirely:
|
|
152
|
+
# the append never ran and the tag builder kept the caller's value alone,
|
|
153
|
+
# which for data-action removed the component's own close-on-click and
|
|
154
|
+
# arrow-key handling.
|
|
155
|
+
#
|
|
156
|
+
# `controller` and `action` are space-separated lists in Stimulus, so a
|
|
157
|
+
# caller's value is appended rather than substituted -- attaching your own
|
|
158
|
+
# controller to a Senren component must not unbind the component's.
|
|
159
|
+
APPENDABLE_DATA = %i[controller action].freeze
|
|
160
|
+
|
|
161
|
+
def merge_data(*sources)
|
|
162
|
+
sources.compact.map { |h| h.transform_keys(&:to_sym) }.reduce({}) do |acc, incoming|
|
|
163
|
+
acc.merge(incoming) do |key, old, new|
|
|
164
|
+
# Split before uniq. Deduplicating the two whole strings only catches an
|
|
165
|
+
# exact repeat: `data: { controller: "a senren--popover" }` on a popover
|
|
166
|
+
# produced "a senren--popover senren--popover", and Stimulus reads that
|
|
167
|
+
# list literally -- same controller connected twice on one element, every
|
|
168
|
+
# action firing twice, so a toggle opens and immediately closes.
|
|
169
|
+
APPENDABLE_DATA.include?(key) ? [old, new].map(&:to_s).join(' ').split.uniq.join(' ') : new
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def caller_class
|
|
175
|
+
html_attrs[:class] || html_attrs['class']
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def caller_data
|
|
179
|
+
html_attrs[:data] || html_attrs['data']
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def html_attrs_without_class_and_data
|
|
183
|
+
html_attrs.reject { |k, _| %w[class data].include?(k.to_s) }
|
|
184
|
+
end
|
|
185
|
+
|
|
84
186
|
def safe_url(value, fallback: '#', protocols: SAFE_URL_PROTOCOLS)
|
|
85
187
|
url = value.to_s.strip
|
|
86
188
|
return fallback if url.empty?
|
|
@@ -119,6 +221,33 @@ module Senren
|
|
|
119
221
|
raise ArgumentError, "Unknown #{label}: #{value.inspect}. Allowed: #{table.keys.join(', ')}"
|
|
120
222
|
end
|
|
121
223
|
|
|
224
|
+
# Concatenation, not conflict resolution — and the caller's class going last
|
|
225
|
+
# in the attribute does not make it win.
|
|
226
|
+
#
|
|
227
|
+
# `class="max-w-lg max-w-2xl"` leaves both declarations live. Which applies
|
|
228
|
+
# is decided by the order the two rules appear in the stylesheet, because
|
|
229
|
+
# their specificity is identical; the order of the attribute is not an input
|
|
230
|
+
# to that at all. Tailwind emits the named scale alphabetically, so measured
|
|
231
|
+
# against a real build:
|
|
232
|
+
#
|
|
233
|
+
# dialog base max-w-lg + class_name: "max-w-sm" -> sm wins (works)
|
|
234
|
+
# dialog base max-w-lg + class_name: "max-w-2xl" -> lg wins (silent no-op)
|
|
235
|
+
#
|
|
236
|
+
# Narrowing happens to work and widening happens not to, and nothing about
|
|
237
|
+
# either class says which you are getting. Any pair drawn from one utility
|
|
238
|
+
# family has this property.
|
|
239
|
+
#
|
|
240
|
+
# Resolving it properly means knowing which Tailwind utilities conflict,
|
|
241
|
+
# which is irregular enough (`text-sm` and `text-red-500` share a prefix and
|
|
242
|
+
# are different families) that a partial implementation would silently drop
|
|
243
|
+
# a component's own styling — a worse failure than the one it replaces. That
|
|
244
|
+
# is what tailwind_merge is for, and taking the dependency is a decision
|
|
245
|
+
# this library has not made.
|
|
246
|
+
#
|
|
247
|
+
# Until it does, the deterministic override is Tailwind's own important
|
|
248
|
+
# modifier, which beats source order rather than depending on it:
|
|
249
|
+
#
|
|
250
|
+
# render Senren::DialogComponent.new(class_name: "max-w-2xl!")
|
|
122
251
|
def merge_classes(*sources)
|
|
123
252
|
sources.flatten.map { |s| s.to_s.strip }.reject(&:empty?).join(' ')
|
|
124
253
|
end
|
|
@@ -42,6 +42,63 @@ 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
|
+
|
|
86
|
+
12. **Every word a component shows or announces goes through `senren_t`.**
|
|
87
|
+
Visible text, `aria-label`, `sr-only` text, a default `label:` or
|
|
88
|
+
`placeholder:`, and any sentence a Stimulus controller will say. The English
|
|
89
|
+
is the `default:`, the key is `<component>.<key>`, and both are literals:
|
|
90
|
+
```ruby
|
|
91
|
+
senren_t('pagination.next', default: 'Next')
|
|
92
|
+
senren_t('cart.remove_item', default: 'Remove %{name}', name: item[:name])
|
|
93
|
+
def initialize(label: senren_t('tabs.label', default: 'Tabs'), **html)
|
|
94
|
+
```
|
|
95
|
+
Never hard-code a sentence in a template, and never write one in a Stimulus
|
|
96
|
+
controller: hand the controller a translated string as a value. An app with
|
|
97
|
+
no locale file renders the English unchanged; to change a word, set its
|
|
98
|
+
`senren.*` key in a locale file, and to add a language, start from
|
|
99
|
+
`bin/rails senren:locales en` (`bin/rails senren:locales vi` installs a
|
|
100
|
+
shipped translation).
|
|
101
|
+
|
|
45
102
|
## File ownership
|
|
46
103
|
|
|
47
104
|
| Path | Owned by |
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Senren UI - optional palette presets.
|
|
3
|
+
*
|
|
4
|
+
* Link this after senren.css and set data-senren-theme on <html> to switch:
|
|
5
|
+
*
|
|
6
|
+
* <%%= stylesheet_link_tag "senren" %>
|
|
7
|
+
* <%%= stylesheet_link_tag "senren_themes" %>
|
|
8
|
+
*
|
|
9
|
+
* <html data-senren-theme="slate">
|
|
10
|
+
*
|
|
11
|
+
* Leave the attribute off for the default Spring Garden palette, which
|
|
12
|
+
* senren.css declares in :root.
|
|
13
|
+
*
|
|
14
|
+
* Every theme below is nothing but a re-declaration of the same token set that
|
|
15
|
+
* senren.css defines. No component is aware that themes exist: components read
|
|
16
|
+
* hsl(var(--senren-*)) and inherit whatever is in scope. Adding your own theme
|
|
17
|
+
* means copying one of these blocks and changing the values — there is no
|
|
18
|
+
* component to touch and no build step to run.
|
|
19
|
+
*
|
|
20
|
+
* Scoping: [data-senren-theme] on <html>, so it composes with the existing
|
|
21
|
+
* .dark class rather than replacing it. Each theme therefore ships a light and
|
|
22
|
+
* a dark block, and light/dark stays orthogonal to the palette choice.
|
|
23
|
+
*
|
|
24
|
+
* `senren` is the default and needs no block: it is what senren.css already
|
|
25
|
+
* declares in :root and .dark.
|
|
26
|
+
*
|
|
27
|
+
* test/themes_test.rb asserts every theme here declares the complete token set,
|
|
28
|
+
* so a palette cannot ship with a variable quietly missing.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/* ── slate ────────────────────────────────────────────────────────────────
|
|
32
|
+
Cool neutrals, near-black primary, tight radius. The restrained look most
|
|
33
|
+
developer tools converge on. */
|
|
34
|
+
[data-senren-theme="slate"] {
|
|
35
|
+
--senren-background: 0 0% 100%;
|
|
36
|
+
--senren-foreground: 222 47% 11%;
|
|
37
|
+
--senren-muted: 210 40% 96%;
|
|
38
|
+
--senren-muted-foreground: 215 16% 47%;
|
|
39
|
+
--senren-card: 0 0% 100%;
|
|
40
|
+
--senren-card-foreground: 222 47% 11%;
|
|
41
|
+
--senren-popover: 0 0% 100%;
|
|
42
|
+
--senren-popover-foreground: 222 47% 11%;
|
|
43
|
+
--senren-border: 214 32% 91%;
|
|
44
|
+
--senren-input: 214 32% 91%;
|
|
45
|
+
--senren-ring: 222 47% 11%;
|
|
46
|
+
--senren-primary: 222 47% 11%;
|
|
47
|
+
--senren-primary-foreground: 210 40% 98%;
|
|
48
|
+
--senren-secondary: 210 40% 96%;
|
|
49
|
+
--senren-secondary-foreground: 222 47% 11%;
|
|
50
|
+
--senren-accent: 210 40% 94%;
|
|
51
|
+
--senren-accent-foreground: 222 47% 11%;
|
|
52
|
+
--senren-destructive: 0 72% 51%;
|
|
53
|
+
--senren-destructive-foreground: 210 40% 98%;
|
|
54
|
+
--senren-success: 142 71% 36%;
|
|
55
|
+
--senren-success-foreground: 210 40% 98%;
|
|
56
|
+
--senren-warning: 38 92% 50%;
|
|
57
|
+
--senren-warning-foreground: 222 47% 11%;
|
|
58
|
+
--senren-radius: 0.375rem;
|
|
59
|
+
--senren-palette-sky: 217 91% 60%;
|
|
60
|
+
--senren-palette-sakura: 330 81% 60%;
|
|
61
|
+
--senren-palette-pond: 199 89% 48%;
|
|
62
|
+
--senren-palette-leaf: 142 71% 45%;
|
|
63
|
+
--senren-palette-iris: 258 90% 66%;
|
|
64
|
+
--senren-palette-paper: 210 40% 98%;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.dark[data-senren-theme="slate"] {
|
|
68
|
+
--senren-background: 222 47% 7%;
|
|
69
|
+
--senren-foreground: 210 40% 98%;
|
|
70
|
+
--senren-muted: 217 33% 17%;
|
|
71
|
+
--senren-muted-foreground: 215 20% 65%;
|
|
72
|
+
--senren-card: 222 47% 9%;
|
|
73
|
+
--senren-card-foreground: 210 40% 98%;
|
|
74
|
+
--senren-popover: 222 47% 9%;
|
|
75
|
+
--senren-popover-foreground: 210 40% 98%;
|
|
76
|
+
--senren-border: 217 33% 20%;
|
|
77
|
+
--senren-input: 217 33% 20%;
|
|
78
|
+
--senren-ring: 213 27% 84%;
|
|
79
|
+
--senren-primary: 210 40% 98%;
|
|
80
|
+
--senren-primary-foreground: 222 47% 11%;
|
|
81
|
+
--senren-secondary: 217 33% 17%;
|
|
82
|
+
--senren-secondary-foreground: 210 40% 98%;
|
|
83
|
+
--senren-accent: 217 33% 20%;
|
|
84
|
+
--senren-accent-foreground: 210 40% 98%;
|
|
85
|
+
--senren-destructive: 0 63% 46%;
|
|
86
|
+
--senren-destructive-foreground: 210 40% 98%;
|
|
87
|
+
--senren-success: 142 60% 40%;
|
|
88
|
+
--senren-success-foreground: 210 40% 98%;
|
|
89
|
+
--senren-warning: 38 85% 52%;
|
|
90
|
+
--senren-warning-foreground: 222 47% 11%;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* ── indigo ───────────────────────────────────────────────────────────────
|
|
94
|
+
The default of a great many B2B products: white surfaces, indigo action
|
|
95
|
+
colour, cool grey text. */
|
|
96
|
+
[data-senren-theme="indigo"] {
|
|
97
|
+
--senren-background: 0 0% 100%;
|
|
98
|
+
--senren-foreground: 224 71% 4%;
|
|
99
|
+
--senren-muted: 220 14% 96%;
|
|
100
|
+
--senren-muted-foreground: 220 9% 46%;
|
|
101
|
+
--senren-card: 0 0% 100%;
|
|
102
|
+
--senren-card-foreground: 224 71% 4%;
|
|
103
|
+
--senren-popover: 0 0% 100%;
|
|
104
|
+
--senren-popover-foreground: 224 71% 4%;
|
|
105
|
+
--senren-border: 220 13% 91%;
|
|
106
|
+
--senren-input: 220 13% 91%;
|
|
107
|
+
--senren-ring: 239 84% 67%;
|
|
108
|
+
--senren-primary: 243 75% 59%;
|
|
109
|
+
--senren-primary-foreground: 0 0% 100%;
|
|
110
|
+
--senren-secondary: 226 100% 97%;
|
|
111
|
+
--senren-secondary-foreground: 243 75% 40%;
|
|
112
|
+
--senren-accent: 226 100% 94%;
|
|
113
|
+
--senren-accent-foreground: 243 75% 40%;
|
|
114
|
+
--senren-destructive: 0 84% 60%;
|
|
115
|
+
--senren-destructive-foreground: 0 0% 100%;
|
|
116
|
+
--senren-success: 160 84% 32%;
|
|
117
|
+
--senren-success-foreground: 0 0% 100%;
|
|
118
|
+
--senren-warning: 32 95% 52%;
|
|
119
|
+
--senren-warning-foreground: 224 71% 4%;
|
|
120
|
+
--senren-radius: 0.5rem;
|
|
121
|
+
--senren-palette-sky: 217 91% 60%;
|
|
122
|
+
--senren-palette-sakura: 330 81% 70%;
|
|
123
|
+
--senren-palette-pond: 199 89% 60%;
|
|
124
|
+
--senren-palette-leaf: 160 84% 39%;
|
|
125
|
+
--senren-palette-iris: 243 75% 59%;
|
|
126
|
+
--senren-palette-paper: 220 14% 96%;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.dark[data-senren-theme="indigo"] {
|
|
130
|
+
--senren-background: 229 42% 8%;
|
|
131
|
+
--senren-foreground: 220 14% 96%;
|
|
132
|
+
--senren-muted: 229 30% 16%;
|
|
133
|
+
--senren-muted-foreground: 218 11% 65%;
|
|
134
|
+
--senren-card: 229 40% 11%;
|
|
135
|
+
--senren-card-foreground: 220 14% 96%;
|
|
136
|
+
--senren-popover: 229 40% 11%;
|
|
137
|
+
--senren-popover-foreground: 220 14% 96%;
|
|
138
|
+
--senren-border: 229 25% 22%;
|
|
139
|
+
--senren-input: 229 25% 22%;
|
|
140
|
+
--senren-ring: 239 84% 72%;
|
|
141
|
+
--senren-primary: 239 84% 70%;
|
|
142
|
+
--senren-primary-foreground: 229 42% 8%;
|
|
143
|
+
--senren-secondary: 229 30% 18%;
|
|
144
|
+
--senren-secondary-foreground: 226 100% 88%;
|
|
145
|
+
--senren-accent: 229 30% 22%;
|
|
146
|
+
--senren-accent-foreground: 226 100% 92%;
|
|
147
|
+
--senren-destructive: 0 72% 55%;
|
|
148
|
+
--senren-destructive-foreground: 0 0% 100%;
|
|
149
|
+
--senren-success: 160 70% 42%;
|
|
150
|
+
--senren-success-foreground: 229 42% 8%;
|
|
151
|
+
--senren-warning: 32 90% 58%;
|
|
152
|
+
--senren-warning-foreground: 229 42% 8%;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ── emerald ──────────────────────────────────────────────────────────────
|
|
156
|
+
Green action colour on soft neutrals, with a generous radius. Common in
|
|
157
|
+
finance, health, and sustainability products. */
|
|
158
|
+
[data-senren-theme="emerald"] {
|
|
159
|
+
--senren-background: 150 20% 99%;
|
|
160
|
+
--senren-foreground: 160 40% 10%;
|
|
161
|
+
--senren-muted: 150 24% 95%;
|
|
162
|
+
--senren-muted-foreground: 160 12% 42%;
|
|
163
|
+
--senren-card: 0 0% 100%;
|
|
164
|
+
--senren-card-foreground: 160 40% 10%;
|
|
165
|
+
--senren-popover: 0 0% 100%;
|
|
166
|
+
--senren-popover-foreground: 160 40% 10%;
|
|
167
|
+
--senren-border: 152 22% 88%;
|
|
168
|
+
--senren-input: 152 22% 88%;
|
|
169
|
+
--senren-ring: 160 84% 39%;
|
|
170
|
+
--senren-primary: 160 84% 30%;
|
|
171
|
+
--senren-primary-foreground: 150 30% 98%;
|
|
172
|
+
--senren-secondary: 152 45% 92%;
|
|
173
|
+
--senren-secondary-foreground: 160 60% 20%;
|
|
174
|
+
--senren-accent: 168 60% 90%;
|
|
175
|
+
--senren-accent-foreground: 168 70% 18%;
|
|
176
|
+
--senren-destructive: 0 74% 50%;
|
|
177
|
+
--senren-destructive-foreground: 150 30% 98%;
|
|
178
|
+
--senren-success: 160 84% 32%;
|
|
179
|
+
--senren-success-foreground: 150 30% 98%;
|
|
180
|
+
--senren-warning: 40 95% 50%;
|
|
181
|
+
--senren-warning-foreground: 160 40% 10%;
|
|
182
|
+
--senren-radius: 0.75rem;
|
|
183
|
+
--senren-palette-sky: 190 80% 45%;
|
|
184
|
+
--senren-palette-sakura: 340 70% 82%;
|
|
185
|
+
--senren-palette-pond: 172 66% 78%;
|
|
186
|
+
--senren-palette-leaf: 145 63% 49%;
|
|
187
|
+
--senren-palette-iris: 250 60% 78%;
|
|
188
|
+
--senren-palette-paper: 150 24% 95%;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.dark[data-senren-theme="emerald"] {
|
|
192
|
+
--senren-background: 165 40% 6%;
|
|
193
|
+
--senren-foreground: 150 24% 95%;
|
|
194
|
+
--senren-muted: 165 28% 14%;
|
|
195
|
+
--senren-muted-foreground: 152 14% 62%;
|
|
196
|
+
--senren-card: 165 38% 9%;
|
|
197
|
+
--senren-card-foreground: 150 24% 95%;
|
|
198
|
+
--senren-popover: 165 38% 9%;
|
|
199
|
+
--senren-popover-foreground: 150 24% 95%;
|
|
200
|
+
--senren-border: 165 22% 20%;
|
|
201
|
+
--senren-input: 165 22% 20%;
|
|
202
|
+
--senren-ring: 160 84% 45%;
|
|
203
|
+
--senren-primary: 158 74% 44%;
|
|
204
|
+
--senren-primary-foreground: 165 40% 6%;
|
|
205
|
+
--senren-secondary: 165 28% 16%;
|
|
206
|
+
--senren-secondary-foreground: 152 45% 88%;
|
|
207
|
+
--senren-accent: 168 40% 20%;
|
|
208
|
+
--senren-accent-foreground: 168 60% 88%;
|
|
209
|
+
--senren-destructive: 0 66% 52%;
|
|
210
|
+
--senren-destructive-foreground: 150 30% 98%;
|
|
211
|
+
--senren-success: 158 70% 42%;
|
|
212
|
+
--senren-success-foreground: 165 40% 6%;
|
|
213
|
+
--senren-warning: 40 88% 56%;
|
|
214
|
+
--senren-warning-foreground: 165 40% 6%;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* ── rose ─────────────────────────────────────────────────────────────────
|
|
218
|
+
Warm neutrals and a soft pink action colour, with the roundest corners of
|
|
219
|
+
the set. The consumer-facing end of SaaS. */
|
|
220
|
+
[data-senren-theme="rose"] {
|
|
221
|
+
--senren-background: 20 40% 99%;
|
|
222
|
+
--senren-foreground: 340 30% 12%;
|
|
223
|
+
--senren-muted: 20 30% 96%;
|
|
224
|
+
--senren-muted-foreground: 340 8% 46%;
|
|
225
|
+
--senren-card: 0 0% 100%;
|
|
226
|
+
--senren-card-foreground: 340 30% 12%;
|
|
227
|
+
--senren-popover: 0 0% 100%;
|
|
228
|
+
--senren-popover-foreground: 340 30% 12%;
|
|
229
|
+
--senren-border: 20 25% 90%;
|
|
230
|
+
--senren-input: 20 25% 90%;
|
|
231
|
+
--senren-ring: 347 77% 60%;
|
|
232
|
+
--senren-primary: 347 77% 50%;
|
|
233
|
+
--senren-primary-foreground: 20 40% 99%;
|
|
234
|
+
--senren-secondary: 350 80% 95%;
|
|
235
|
+
--senren-secondary-foreground: 347 70% 32%;
|
|
236
|
+
--senren-accent: 350 80% 92%;
|
|
237
|
+
--senren-accent-foreground: 347 70% 30%;
|
|
238
|
+
--senren-destructive: 0 78% 52%;
|
|
239
|
+
--senren-destructive-foreground: 20 40% 99%;
|
|
240
|
+
--senren-success: 150 62% 36%;
|
|
241
|
+
--senren-success-foreground: 20 40% 99%;
|
|
242
|
+
--senren-warning: 35 95% 55%;
|
|
243
|
+
--senren-warning-foreground: 340 30% 12%;
|
|
244
|
+
--senren-radius: 1rem;
|
|
245
|
+
--senren-palette-sky: 205 85% 58%;
|
|
246
|
+
--senren-palette-sakura: 350 85% 84%;
|
|
247
|
+
--senren-palette-pond: 190 70% 82%;
|
|
248
|
+
--senren-palette-leaf: 140 55% 55%;
|
|
249
|
+
--senren-palette-iris: 280 70% 78%;
|
|
250
|
+
--senren-palette-paper: 20 30% 96%;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.dark[data-senren-theme="rose"] {
|
|
254
|
+
--senren-background: 340 22% 8%;
|
|
255
|
+
--senren-foreground: 20 30% 96%;
|
|
256
|
+
--senren-muted: 340 16% 16%;
|
|
257
|
+
--senren-muted-foreground: 340 8% 64%;
|
|
258
|
+
--senren-card: 340 20% 11%;
|
|
259
|
+
--senren-card-foreground: 20 30% 96%;
|
|
260
|
+
--senren-popover: 340 20% 11%;
|
|
261
|
+
--senren-popover-foreground: 20 30% 96%;
|
|
262
|
+
--senren-border: 340 14% 22%;
|
|
263
|
+
--senren-input: 340 14% 22%;
|
|
264
|
+
--senren-ring: 347 77% 66%;
|
|
265
|
+
--senren-primary: 347 82% 62%;
|
|
266
|
+
--senren-primary-foreground: 340 22% 8%;
|
|
267
|
+
--senren-secondary: 340 16% 18%;
|
|
268
|
+
--senren-secondary-foreground: 350 80% 90%;
|
|
269
|
+
--senren-accent: 340 20% 24%;
|
|
270
|
+
--senren-accent-foreground: 350 80% 90%;
|
|
271
|
+
--senren-destructive: 0 70% 55%;
|
|
272
|
+
--senren-destructive-foreground: 20 40% 99%;
|
|
273
|
+
--senren-success: 150 55% 44%;
|
|
274
|
+
--senren-success-foreground: 340 22% 8%;
|
|
275
|
+
--senren-warning: 35 90% 60%;
|
|
276
|
+
--senren-warning-foreground: 340 22% 8%;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* ── amber ────────────────────────────────────────────────────────────────
|
|
280
|
+
Sand-toned neutrals, amber action colour, near-square corners. The
|
|
281
|
+
document-and-workspace look. */
|
|
282
|
+
[data-senren-theme="amber"] {
|
|
283
|
+
--senren-background: 40 30% 98%;
|
|
284
|
+
--senren-foreground: 30 25% 12%;
|
|
285
|
+
--senren-muted: 40 24% 94%;
|
|
286
|
+
--senren-muted-foreground: 32 10% 44%;
|
|
287
|
+
--senren-card: 40 40% 99%;
|
|
288
|
+
--senren-card-foreground: 30 25% 12%;
|
|
289
|
+
--senren-popover: 40 40% 99%;
|
|
290
|
+
--senren-popover-foreground: 30 25% 12%;
|
|
291
|
+
--senren-border: 36 20% 87%;
|
|
292
|
+
--senren-input: 36 20% 87%;
|
|
293
|
+
--senren-ring: 32 95% 44%;
|
|
294
|
+
--senren-primary: 30 30% 16%;
|
|
295
|
+
--senren-primary-foreground: 40 30% 98%;
|
|
296
|
+
--senren-secondary: 40 60% 90%;
|
|
297
|
+
--senren-secondary-foreground: 30 40% 20%;
|
|
298
|
+
--senren-accent: 38 80% 88%;
|
|
299
|
+
--senren-accent-foreground: 30 45% 20%;
|
|
300
|
+
--senren-destructive: 4 74% 48%;
|
|
301
|
+
--senren-destructive-foreground: 40 30% 98%;
|
|
302
|
+
--senren-success: 140 55% 34%;
|
|
303
|
+
--senren-success-foreground: 40 30% 98%;
|
|
304
|
+
--senren-warning: 32 95% 46%;
|
|
305
|
+
--senren-warning-foreground: 40 30% 98%;
|
|
306
|
+
--senren-radius: 0.25rem;
|
|
307
|
+
--senren-palette-sky: 200 75% 50%;
|
|
308
|
+
--senren-palette-sakura: 20 80% 82%;
|
|
309
|
+
--senren-palette-pond: 190 60% 80%;
|
|
310
|
+
--senren-palette-leaf: 100 45% 50%;
|
|
311
|
+
--senren-palette-iris: 265 55% 74%;
|
|
312
|
+
--senren-palette-paper: 40 24% 94%;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.dark[data-senren-theme="amber"] {
|
|
316
|
+
--senren-background: 30 18% 8%;
|
|
317
|
+
--senren-foreground: 40 24% 94%;
|
|
318
|
+
--senren-muted: 30 14% 15%;
|
|
319
|
+
--senren-muted-foreground: 36 10% 62%;
|
|
320
|
+
--senren-card: 30 16% 11%;
|
|
321
|
+
--senren-card-foreground: 40 24% 94%;
|
|
322
|
+
--senren-popover: 30 16% 11%;
|
|
323
|
+
--senren-popover-foreground: 40 24% 94%;
|
|
324
|
+
--senren-border: 30 12% 21%;
|
|
325
|
+
--senren-input: 30 12% 21%;
|
|
326
|
+
--senren-ring: 32 95% 55%;
|
|
327
|
+
--senren-primary: 36 90% 58%;
|
|
328
|
+
--senren-primary-foreground: 30 25% 10%;
|
|
329
|
+
--senren-secondary: 30 14% 17%;
|
|
330
|
+
--senren-secondary-foreground: 40 60% 86%;
|
|
331
|
+
--senren-accent: 32 30% 22%;
|
|
332
|
+
--senren-accent-foreground: 38 80% 86%;
|
|
333
|
+
--senren-destructive: 4 68% 52%;
|
|
334
|
+
--senren-destructive-foreground: 40 30% 98%;
|
|
335
|
+
--senren-success: 140 50% 42%;
|
|
336
|
+
--senren-success-foreground: 30 18% 8%;
|
|
337
|
+
--senren-warning: 32 90% 56%;
|
|
338
|
+
--senren-warning-foreground: 30 18% 8%;
|
|
339
|
+
}
|
|
@@ -106,6 +106,13 @@ module Senren
|
|
|
106
106
|
publishes every file under an asset path, so component `.rb` and
|
|
107
107
|
`.html.erb` source is precompiled into `public/assets` and served
|
|
108
108
|
over HTTP. Put sidecar assets in `app/components/assets` instead.
|
|
109
|
+
- A button inside a form needs no `type:`. ButtonComponent omits the
|
|
110
|
+
attribute so the browser submits, as plain HTML does. Pass
|
|
111
|
+
`type: :button` only for a trigger that must not submit.
|
|
112
|
+
- Text a component shows or announces goes through
|
|
113
|
+
`senren_t('<component>.<key>', default: 'English')`, with a literal key
|
|
114
|
+
and default. Never hard-code a sentence in a template or in a Stimulus
|
|
115
|
+
controller; hand the controller a translated string as a value.
|
|
109
116
|
|
|
110
117
|
## Important Files
|
|
111
118
|
|