nitro_kit 2.0.0.alpha.2 → 2.0.0.alpha.4
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 +229 -1
- data/README.md +25 -10
- data/STYLE_GUIDE.md +47 -5
- data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +3 -3
- data/app/assets/stylesheets/nitro_kit.css +2820 -1816
- data/app/components/nitro_kit/accordion.rb +2 -26
- data/app/components/nitro_kit/alert.rb +1 -11
- data/app/components/nitro_kit/app_navigation.rb +45 -11
- data/app/components/nitro_kit/app_shell.rb +1 -7
- data/app/components/nitro_kit/appearance_picker.rb +11 -12
- data/app/components/nitro_kit/avatar.rb +6 -1
- data/app/components/nitro_kit/avatar_stack.rb +3 -0
- data/app/components/nitro_kit/badge.rb +13 -4
- data/app/components/nitro_kit/button_group.rb +2 -2
- data/app/components/nitro_kit/checkbox.rb +7 -4
- data/app/components/nitro_kit/checkbox_group.rb +0 -25
- data/app/components/nitro_kit/combobox.rb +4 -33
- data/app/components/nitro_kit/command_palette.rb +42 -59
- data/app/components/nitro_kit/component.rb +57 -5
- data/app/components/nitro_kit/danger_zone.rb +5 -1
- data/app/components/nitro_kit/data_section.rb +28 -4
- data/app/components/nitro_kit/details_table.rb +19 -8
- data/app/components/nitro_kit/dialog.rb +23 -19
- data/app/components/nitro_kit/dropdown.rb +1 -6
- data/app/components/nitro_kit/dropzone.rb +4 -15
- data/app/components/nitro_kit/empty_state.rb +21 -2
- data/app/components/nitro_kit/field.rb +35 -11
- data/app/components/nitro_kit/form_builder.rb +1 -2
- data/app/components/nitro_kit/progressive_image.rb +2 -2
- data/app/components/nitro_kit/radio_button.rb +7 -4
- data/app/components/nitro_kit/radio_button_group.rb +0 -25
- data/app/components/nitro_kit/select.rb +4 -17
- data/app/components/nitro_kit/settings_layout.rb +17 -4
- data/app/components/nitro_kit/{form_section.rb → settings_section.rb} +18 -11
- data/app/components/nitro_kit/sheet.rb +29 -20
- data/app/components/nitro_kit/stat_grid.rb +18 -2
- data/app/components/nitro_kit/table.rb +22 -3
- data/app/components/nitro_kit/tabs.rb +2 -13
- data/app/components/nitro_kit/toast.rb +4 -2
- data/app/components/nitro_kit/toolbar.rb +2 -0
- data/app/components/nitro_kit/tooltip.rb +7 -10
- data/app/javascript/controllers/nk/app_shell_controller.js +3 -3
- data/app/javascript/controllers/nk/appearance_controller.js +6 -0
- data/app/javascript/controllers/nk/combobox_controller.js +51 -1
- data/app/javascript/controllers/nk/command_palette_controller.js +25 -11
- data/app/javascript/controllers/nk/dialog_controller.js +54 -0
- data/app/javascript/controllers/nk/dropdown_controller.js +45 -0
- data/app/javascript/controllers/nk/dropzone_controller.js +2 -2
- data/app/javascript/controllers/nk/progressive_image_controller.js +11 -7
- data/app/javascript/controllers/nk/tabs_controller.js +21 -2
- data/app/javascript/controllers/nk/tooltip_controller.js +9 -0
- data/config/locales/en.yml +4 -1
- data/docs/agent_guide.md +63 -123
- data/docs/agent_native_spec.md +72 -383
- data/docs/browser_support.md +82 -0
- data/docs/component_contracts.md +108 -82
- data/docs/customization.md +689 -91
- data/docs/hotwire.md +50 -76
- data/docs/initialization_prompt.md +26 -31
- data/docs/migration_1_to_2.md +88 -30
- data/docs/patterns/application_foundation.md +45 -119
- data/docs/patterns/crud_resource.md +44 -126
- data/docs/patterns/destructive_action.md +45 -77
- data/docs/patterns/flash_and_toast.md +18 -36
- data/docs/patterns/inline_edit.md +21 -44
- data/docs/patterns/queryable_collection.md +65 -161
- data/docs/patterns/resource_form.md +33 -81
- data/docs/rails_conventions.md +38 -75
- data/docs/rails_integration.md +96 -446
- data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +12 -0
- data/lib/nitro_kit/installation.rb +20 -7
- data/lib/nitro_kit/migration_inventory.rb +275 -8
- data/lib/nitro_kit/upgrade_smoke_test.rb +10 -12
- data/lib/nitro_kit/version.rb +1 -1
- data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +10 -3
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +13 -1
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +32 -15
- data/src/stylesheets/nitro_kit/components/accordion.css +44 -44
- data/src/stylesheets/nitro_kit/components/alert.css +27 -36
- data/src/stylesheets/nitro_kit/components/app_navigation.css +92 -20
- data/src/stylesheets/nitro_kit/components/app_shell.css +199 -196
- data/src/stylesheets/nitro_kit/components/appearance_picker.css +61 -39
- data/src/stylesheets/nitro_kit/components/auth_shell.css +2 -2
- data/src/stylesheets/nitro_kit/components/avatar.css +15 -11
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +48 -29
- data/src/stylesheets/nitro_kit/components/badge.css +13 -9
- data/src/stylesheets/nitro_kit/components/button.css +121 -79
- data/src/stylesheets/nitro_kit/components/button_group.css +19 -14
- data/src/stylesheets/nitro_kit/components/card.css +12 -6
- data/src/stylesheets/nitro_kit/components/checkbox.css +54 -67
- data/src/stylesheets/nitro_kit/components/checkbox_group.css +16 -16
- data/src/stylesheets/nitro_kit/components/combobox.css +39 -29
- data/src/stylesheets/nitro_kit/components/command_palette.css +110 -51
- data/src/stylesheets/nitro_kit/components/container.css +6 -6
- data/src/stylesheets/nitro_kit/components/control_group.css +61 -67
- data/src/stylesheets/nitro_kit/components/danger_zone.css +8 -8
- data/src/stylesheets/nitro_kit/components/data_section.css +4 -4
- data/src/stylesheets/nitro_kit/components/details_table.css +17 -15
- data/src/stylesheets/nitro_kit/components/dialog.css +8 -5
- data/src/stylesheets/nitro_kit/components/dropdown.css +36 -30
- data/src/stylesheets/nitro_kit/components/dropzone.css +78 -35
- data/src/stylesheets/nitro_kit/components/empty_state.css +7 -7
- data/src/stylesheets/nitro_kit/components/field.css +32 -28
- data/src/stylesheets/nitro_kit/components/field_group.css +13 -0
- data/src/stylesheets/nitro_kit/components/fieldset.css +8 -2
- data/src/stylesheets/nitro_kit/components/flex.css +1 -1
- data/src/stylesheets/nitro_kit/components/grid.css +1 -1
- data/src/stylesheets/nitro_kit/components/icon.css +5 -5
- data/src/stylesheets/nitro_kit/components/input.css +42 -8
- data/src/stylesheets/nitro_kit/components/layout.css +165 -165
- data/src/stylesheets/nitro_kit/components/page_header.css +7 -7
- data/src/stylesheets/nitro_kit/components/pagination.css +58 -49
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +9 -9
- data/src/stylesheets/nitro_kit/components/palette.css +145 -77
- data/src/stylesheets/nitro_kit/components/progressive_image.css +45 -55
- data/src/stylesheets/nitro_kit/components/radio_button.css +48 -46
- data/src/stylesheets/nitro_kit/components/radio_button_group.css +49 -49
- data/src/stylesheets/nitro_kit/components/rich_text_area.css +16 -1
- data/src/stylesheets/nitro_kit/components/select.css +24 -15
- data/src/stylesheets/nitro_kit/components/settings_layout.css +26 -23
- data/src/stylesheets/nitro_kit/components/settings_section.css +84 -0
- data/src/stylesheets/nitro_kit/components/sheet.css +30 -10
- data/src/stylesheets/nitro_kit/components/stat_grid.css +11 -10
- data/src/stylesheets/nitro_kit/components/switch.css +60 -62
- data/src/stylesheets/nitro_kit/components/table.css +43 -35
- data/src/stylesheets/nitro_kit/components/tabs.css +48 -40
- data/src/stylesheets/nitro_kit/components/textarea.css +20 -5
- data/src/stylesheets/nitro_kit/components/toast.css +11 -47
- data/src/stylesheets/nitro_kit/components/toolbar.css +16 -41
- data/src/stylesheets/nitro_kit/components/tooltip.css +38 -29
- data/src/stylesheets/nitro_kit/components/typeset.css +150 -4
- data/src/stylesheets/nitro_kit/reset.css +15 -15
- data/src/stylesheets/nitro_kit/tokens.css +501 -79
- metadata +4 -4
- data/docs/new_app_strategy.md +0 -22
- data/src/stylesheets/nitro_kit/components/form_section.css +0 -71
|
@@ -13,7 +13,7 @@ module NitroKit
|
|
|
13
13
|
data: {},
|
|
14
14
|
desperately_need_a_class: nil
|
|
15
15
|
)
|
|
16
|
-
@identifier =
|
|
16
|
+
@identifier = validate_id!("Accordion id", id)
|
|
17
17
|
@mode = validate_choice!(:mode, mode, MODES)
|
|
18
18
|
@items = []
|
|
19
19
|
|
|
@@ -106,25 +106,7 @@ module NitroKit
|
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
def chevron
|
|
109
|
-
|
|
110
|
-
**slot_attributes(
|
|
111
|
-
:icon,
|
|
112
|
-
attributes: {
|
|
113
|
-
viewbox: "0 0 16 16",
|
|
114
|
-
width: 16,
|
|
115
|
-
height: 16,
|
|
116
|
-
fill: "none",
|
|
117
|
-
stroke: "currentColor",
|
|
118
|
-
stroke_width: 1.5,
|
|
119
|
-
stroke_linecap: "round",
|
|
120
|
-
stroke_linejoin: "round",
|
|
121
|
-
focusable: "false",
|
|
122
|
-
aria: { hidden: true }
|
|
123
|
-
}
|
|
124
|
-
)
|
|
125
|
-
) do |svg|
|
|
126
|
-
svg.path(d: "m4 6 4 4 4-4")
|
|
127
|
-
end
|
|
109
|
+
render_in_slot(Icon.new(:chevron_down, size: :sm), :icon)
|
|
128
110
|
end
|
|
129
111
|
|
|
130
112
|
def trigger_id(item)
|
|
@@ -152,11 +134,5 @@ module NitroKit
|
|
|
152
134
|
|
|
153
135
|
raise ArgumentError, "Accordion #{name} must be true or false"
|
|
154
136
|
end
|
|
155
|
-
|
|
156
|
-
def component_id(value)
|
|
157
|
-
return value if value.is_a?(String) && value.present? && !value.match?(/\s/)
|
|
158
|
-
|
|
159
|
-
raise ArgumentError, "Accordion id must be a non-blank String without whitespace"
|
|
160
|
-
end
|
|
161
137
|
end
|
|
162
138
|
end
|
|
@@ -2,17 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module NitroKit
|
|
4
4
|
class Alert < Component
|
|
5
|
-
VARIANTS = %i[default info success warning
|
|
6
|
-
|
|
7
|
-
# The palette family each semantic variant borrows in `palette.css`.
|
|
8
|
-
# Alert owns this mapping; it is not the Badge color axis.
|
|
9
|
-
VARIANT_PALETTE = {
|
|
10
|
-
default: :zinc,
|
|
11
|
-
info: :blue,
|
|
12
|
-
success: :green,
|
|
13
|
-
warning: :amber,
|
|
14
|
-
error: :red
|
|
15
|
-
}.freeze
|
|
5
|
+
VARIANTS = %i[default info success warning destructive].freeze
|
|
16
6
|
|
|
17
7
|
LIVE_MODES = %i[off polite assertive].freeze
|
|
18
8
|
|
|
@@ -5,8 +5,8 @@ module NitroKit
|
|
|
5
5
|
alias_method :html_header, :header
|
|
6
6
|
alias_method :html_footer, :footer
|
|
7
7
|
|
|
8
|
-
Item = ::Data.define(:text, :href, :icon, :badge, :current, :html, :aria, :data, :css_class)
|
|
9
|
-
Section = ::Data.define(:label, :entries)
|
|
8
|
+
Item = ::Data.define(:text, :href, :icon, :icon_end, :badge, :current, :html, :aria, :data, :css_class)
|
|
9
|
+
Section = ::Data.define(:label, :entries, :collapsible, :expanded)
|
|
10
10
|
Entry = ::Data.define(:kind)
|
|
11
11
|
|
|
12
12
|
def initialize(
|
|
@@ -78,10 +78,16 @@ module NitroKit
|
|
|
78
78
|
nil
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
def section(label: nil, &content)
|
|
81
|
+
def section(label: nil, collapsible: false, expanded: true, &content)
|
|
82
82
|
ensure_phase!(:body, :section)
|
|
83
83
|
raise ArgumentError, "AppNavigation section requires a block" unless content
|
|
84
84
|
|
|
85
|
+
collapsible = validate_boolean!(:collapsible, collapsible)
|
|
86
|
+
expanded = validate_boolean!(:expanded, expanded)
|
|
87
|
+
if collapsible && label.nil?
|
|
88
|
+
raise ArgumentError, "AppNavigation collapsible section requires a label"
|
|
89
|
+
end
|
|
90
|
+
|
|
85
91
|
entries = []
|
|
86
92
|
previous_entries = @entry_target
|
|
87
93
|
previous_phase = @phase
|
|
@@ -94,7 +100,12 @@ module NitroKit
|
|
|
94
100
|
unless entries.any?(Item)
|
|
95
101
|
raise ArgumentError, "AppNavigation section requires at least one item"
|
|
96
102
|
end
|
|
97
|
-
previous_entries << Section.new(
|
|
103
|
+
previous_entries << Section.new(
|
|
104
|
+
label: validate_optional_text!(:label, label),
|
|
105
|
+
entries:,
|
|
106
|
+
collapsible:,
|
|
107
|
+
expanded:
|
|
108
|
+
)
|
|
98
109
|
nil
|
|
99
110
|
ensure
|
|
100
111
|
@entry_target = previous_entries
|
|
@@ -106,6 +117,7 @@ module NitroKit
|
|
|
106
117
|
text,
|
|
107
118
|
href:,
|
|
108
119
|
icon: nil,
|
|
120
|
+
icon_end: nil,
|
|
109
121
|
badge: nil,
|
|
110
122
|
badge_color: :neutral,
|
|
111
123
|
current: false,
|
|
@@ -118,6 +130,7 @@ module NitroKit
|
|
|
118
130
|
text = validate_text!(:text, text)
|
|
119
131
|
href = validate_text!(:href, href)
|
|
120
132
|
icon = item_icon(icon)
|
|
133
|
+
icon_end = item_icon(icon_end, name: :icon_end)
|
|
121
134
|
badge = item_badge(badge, badge_color)
|
|
122
135
|
current = validate_boolean!(:current, current)
|
|
123
136
|
|
|
@@ -130,6 +143,7 @@ module NitroKit
|
|
|
130
143
|
text:,
|
|
131
144
|
href:,
|
|
132
145
|
icon:,
|
|
146
|
+
icon_end:,
|
|
133
147
|
badge:,
|
|
134
148
|
current:,
|
|
135
149
|
html:,
|
|
@@ -205,13 +219,33 @@ module NitroKit
|
|
|
205
219
|
|
|
206
220
|
def render_section(entry)
|
|
207
221
|
li(**slot_attributes(:section)) do
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
222
|
+
if entry.collapsible
|
|
223
|
+
details(
|
|
224
|
+
**slot_attributes(:section_disclosure, attributes: { open: entry.expanded })
|
|
225
|
+
) do
|
|
226
|
+
summary(**slot_attributes(:section_label)) do
|
|
227
|
+
plain(entry.label)
|
|
228
|
+
section_chevron
|
|
229
|
+
end
|
|
230
|
+
render_section_list(entry)
|
|
231
|
+
end
|
|
232
|
+
else
|
|
233
|
+
span(**slot_attributes(:section_label)) { plain(entry.label) } if entry.label
|
|
234
|
+
render_section_list(entry)
|
|
211
235
|
end
|
|
212
236
|
end
|
|
213
237
|
end
|
|
214
238
|
|
|
239
|
+
def render_section_list(entry)
|
|
240
|
+
ul(**slot_attributes(:section_list, attributes: section_list_attributes(entry))) do
|
|
241
|
+
entry.entries.each { |child| render_entry(child) }
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def section_chevron
|
|
246
|
+
render_in_slot(Icon.new(:chevron_down, size: :xs), :section_icon)
|
|
247
|
+
end
|
|
248
|
+
|
|
215
249
|
def section_list_attributes(entry)
|
|
216
250
|
entry.label ? { aria: { label: entry.label } } : {}
|
|
217
251
|
end
|
|
@@ -223,8 +257,7 @@ module NitroKit
|
|
|
223
257
|
:item_link,
|
|
224
258
|
attributes: {
|
|
225
259
|
href: entry.href,
|
|
226
|
-
aria: { current: entry.current ? "page" : nil }
|
|
227
|
-
data: { state: entry.current ? "current" : "default" }
|
|
260
|
+
aria: { current: entry.current ? "page" : nil }
|
|
228
261
|
},
|
|
229
262
|
html: entry.html,
|
|
230
263
|
aria: entry.aria,
|
|
@@ -235,6 +268,7 @@ module NitroKit
|
|
|
235
268
|
render_in_slot(entry.icon, :item_icon) if entry.icon
|
|
236
269
|
span(**slot_attributes(:item_label)) { plain(entry.text) }
|
|
237
270
|
render_in_slot(entry.badge, :item_badge) if entry.badge
|
|
271
|
+
render_in_slot(entry.icon_end, :item_icon_end) if entry.icon_end
|
|
238
272
|
end
|
|
239
273
|
end
|
|
240
274
|
end
|
|
@@ -278,10 +312,10 @@ module NitroKit
|
|
|
278
312
|
validate_text!(name, value)
|
|
279
313
|
end
|
|
280
314
|
|
|
281
|
-
def item_icon(value)
|
|
315
|
+
def item_icon(value, name: :icon)
|
|
282
316
|
return if value.nil?
|
|
283
317
|
unless (value.is_a?(String) || value.is_a?(Symbol)) && !value.to_s.strip.empty?
|
|
284
|
-
raise ArgumentError, "
|
|
318
|
+
raise ArgumentError, "#{name} must be a non-blank String or Symbol"
|
|
285
319
|
end
|
|
286
320
|
|
|
287
321
|
Icon.new(value, size: :sm)
|
|
@@ -21,7 +21,7 @@ module NitroKit
|
|
|
21
21
|
data: {},
|
|
22
22
|
desperately_need_a_class: nil
|
|
23
23
|
)
|
|
24
|
-
@identifier =
|
|
24
|
+
@identifier = validate_id!("AppShell id", id)
|
|
25
25
|
@layout = validate_choice!(:layout, layout, LAYOUTS)
|
|
26
26
|
@skip_link_label = validate_label!(:skip_link_label, skip_link_label)
|
|
27
27
|
@open_navigation_label = validate_label!(:open_navigation_label, open_navigation_label)
|
|
@@ -212,12 +212,6 @@ module NitroKit
|
|
|
212
212
|
"#{identifier}-main"
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
-
def component_id(value)
|
|
216
|
-
return value if value.is_a?(String) && value.match?(/\A[A-Za-z0-9][A-Za-z0-9_-]*\z/)
|
|
217
|
-
|
|
218
|
-
raise ArgumentError, "AppShell id must be a fragment-safe String using letters, numbers, underscores, or hyphens"
|
|
219
|
-
end
|
|
220
|
-
|
|
221
215
|
def validate_label!(name, value)
|
|
222
216
|
return value if value.is_a?(String) && !value.strip.empty?
|
|
223
217
|
|
|
@@ -13,6 +13,7 @@ module NitroKit
|
|
|
13
13
|
def initialize(
|
|
14
14
|
id:,
|
|
15
15
|
label: I18n.t("nitro_kit.appearance_picker.label"),
|
|
16
|
+
label_visible: true,
|
|
16
17
|
presentation: :segmented,
|
|
17
18
|
preference: :system,
|
|
18
19
|
html: {},
|
|
@@ -20,15 +21,23 @@ module NitroKit
|
|
|
20
21
|
data: {},
|
|
21
22
|
desperately_need_a_class: nil
|
|
22
23
|
)
|
|
23
|
-
@identifier =
|
|
24
|
+
@identifier = validate_id!("AppearancePicker id", id)
|
|
24
25
|
@label = validate_label!(label)
|
|
26
|
+
@label_visible = validate_boolean!(:label_visible, label_visible)
|
|
25
27
|
@presentation = validate_choice!(:presentation, presentation, PRESENTATIONS)
|
|
26
28
|
@preference = validate_choice!(:preference, preference, PREFERENCES)
|
|
29
|
+
if !@label_visible && @presentation != :segmented
|
|
30
|
+
raise ArgumentError,
|
|
31
|
+
"AppearancePicker label_visible: false requires the segmented presentation"
|
|
32
|
+
end
|
|
27
33
|
|
|
28
34
|
super(
|
|
29
35
|
component: :appearance_picker,
|
|
30
36
|
attributes: {
|
|
31
37
|
id: @identifier,
|
|
38
|
+
# Browsers give legends special layout, so a hidden label omits the
|
|
39
|
+
# element entirely and the fieldset keeps its name through ARIA.
|
|
40
|
+
aria: @label_visible ? {} : { label: @label },
|
|
32
41
|
data: {
|
|
33
42
|
controller: "nk--appearance",
|
|
34
43
|
presentation: @presentation,
|
|
@@ -50,7 +59,7 @@ module NitroKit
|
|
|
50
59
|
return render_dropdown if @presentation == :dropdown
|
|
51
60
|
|
|
52
61
|
fieldset(**root_attributes) do
|
|
53
|
-
legend(**slot_attributes(:legend)) { plain(@label) }
|
|
62
|
+
legend(**slot_attributes(:legend)) { plain(@label) } if @label_visible
|
|
54
63
|
div(**slot_attributes(:options)) do
|
|
55
64
|
PREFERENCES.each { |preference| render_option(preference) }
|
|
56
65
|
end
|
|
@@ -137,16 +146,6 @@ module NitroKit
|
|
|
137
146
|
"#{identifier}-#{preference}"
|
|
138
147
|
end
|
|
139
148
|
|
|
140
|
-
# Private copy of the AppShell identifier contract. Extract a shared
|
|
141
|
-
# validator into Component once its owner lands.
|
|
142
|
-
def component_id(value)
|
|
143
|
-
return value if value.is_a?(String) && value.match?(/\A[A-Za-z0-9][A-Za-z0-9_-]*\z/)
|
|
144
|
-
|
|
145
|
-
raise ArgumentError,
|
|
146
|
-
"AppearancePicker id must be a String starting with a letter or digit and containing " \
|
|
147
|
-
"only letters, digits, hyphens, and underscores"
|
|
148
|
-
end
|
|
149
|
-
|
|
150
149
|
def validate_label!(value)
|
|
151
150
|
return value if value.is_a?(String) && value.present?
|
|
152
151
|
|
|
@@ -9,6 +9,7 @@ module NitroKit
|
|
|
9
9
|
alt: "",
|
|
10
10
|
fallback: nil,
|
|
11
11
|
size: :md,
|
|
12
|
+
decorative: false,
|
|
12
13
|
loading: "lazy",
|
|
13
14
|
decoding: "async",
|
|
14
15
|
id: nil,
|
|
@@ -18,12 +19,16 @@ module NitroKit
|
|
|
18
19
|
desperately_need_a_class: nil
|
|
19
20
|
)
|
|
20
21
|
raise ArgumentError, "alt must be a String" unless alt.is_a?(String)
|
|
22
|
+
@decorative = validate_boolean!(:decorative, decorative)
|
|
21
23
|
unless fallback.nil? || fallback.is_a?(String)
|
|
22
24
|
raise ArgumentError, "fallback must be a String or nil"
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
@src = src
|
|
26
28
|
@alt = alt
|
|
29
|
+
if src? && alt.empty? && !@decorative
|
|
30
|
+
raise ArgumentError, "Avatar images require alt: text unless decorative: true"
|
|
31
|
+
end
|
|
27
32
|
@fallback = fallback || initials_for(alt)
|
|
28
33
|
@size = validate_choice!(:size, size, SIZES)
|
|
29
34
|
root_aria = fallback_aria(aria)
|
|
@@ -49,7 +54,7 @@ module NitroKit
|
|
|
49
54
|
}
|
|
50
55
|
end
|
|
51
56
|
|
|
52
|
-
attr_reader :src, :alt, :fallback, :size
|
|
57
|
+
attr_reader :src, :alt, :fallback, :size, :decorative
|
|
53
58
|
|
|
54
59
|
def view_template
|
|
55
60
|
span(**root_attributes) do
|
|
@@ -61,6 +61,7 @@ module NitroKit
|
|
|
61
61
|
src: nil,
|
|
62
62
|
alt: "",
|
|
63
63
|
fallback: nil,
|
|
64
|
+
decorative: false,
|
|
64
65
|
loading: "lazy",
|
|
65
66
|
decoding: "async",
|
|
66
67
|
id: nil,
|
|
@@ -75,6 +76,7 @@ module NitroKit
|
|
|
75
76
|
src:,
|
|
76
77
|
alt:,
|
|
77
78
|
fallback:,
|
|
79
|
+
decorative:,
|
|
78
80
|
size:,
|
|
79
81
|
loading:,
|
|
80
82
|
decoding:,
|
|
@@ -156,6 +158,7 @@ module NitroKit
|
|
|
156
158
|
span(
|
|
157
159
|
**slot_attributes(
|
|
158
160
|
:overflow,
|
|
161
|
+
attributes: { role: "img" },
|
|
159
162
|
html: declaration.html,
|
|
160
163
|
aria: declaration.aria.merge(
|
|
161
164
|
label: declaration.label || I18n.t("nitro_kit.avatar_stack.overflow", count: declaration.count)
|
|
@@ -4,16 +4,25 @@ module NitroKit
|
|
|
4
4
|
class Badge < Component
|
|
5
5
|
VARIANTS = %i[default outline].freeze
|
|
6
6
|
SIZES = %i[xs sm md].freeze
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
# Two axes on one option, both public and both themeable.
|
|
8
|
+
#
|
|
9
|
+
# The semantic families follow the `--nk-palette-{family}` tint roles, so
|
|
10
|
+
# they move with an application's brand: retheme `--nk-palette-destructive` and
|
|
11
|
+
# every `destructive` badge follows. The decorative hues follow the
|
|
12
|
+
# `--nk-palette-{hue}` roles and stay the color they name, for categorical
|
|
13
|
+
# labelling where meaning is not the point.
|
|
14
|
+
SEMANTIC_COLORS = %i[neutral info success warning destructive].freeze
|
|
15
|
+
PALETTE_COLORS = %i[
|
|
16
|
+
red orange amber yellow lime green emerald teal cyan sky blue indigo
|
|
17
|
+
violet purple fuchsia pink rose
|
|
10
18
|
].freeze
|
|
19
|
+
COLORS = (PALETTE_COLORS + SEMANTIC_COLORS).freeze
|
|
11
20
|
|
|
12
21
|
def initialize(
|
|
13
22
|
text = nil,
|
|
14
23
|
variant: :default,
|
|
15
24
|
size: :md,
|
|
16
|
-
color: :
|
|
25
|
+
color: :neutral,
|
|
17
26
|
id: nil,
|
|
18
27
|
html: {},
|
|
19
28
|
aria: {},
|
|
@@ -28,9 +28,9 @@ module NitroKit
|
|
|
28
28
|
|
|
29
29
|
super(
|
|
30
30
|
component: :button_group,
|
|
31
|
-
attributes: { id:, role: "group" },
|
|
31
|
+
attributes: { id:, role: "group", aria: { label: }.compact },
|
|
32
32
|
html:,
|
|
33
|
-
aria
|
|
33
|
+
aria:,
|
|
34
34
|
data:,
|
|
35
35
|
desperately_need_a_class:
|
|
36
36
|
)
|
|
@@ -78,12 +78,15 @@ module NitroKit
|
|
|
78
78
|
render_in_slot(Label.new(for: id), :label) do
|
|
79
79
|
render_control
|
|
80
80
|
span(**slot_attributes(:indicator), aria: { hidden: "true" })
|
|
81
|
-
span(**slot_attributes(:
|
|
82
|
-
span(**slot_attributes(:label_text)) { text_or_block(label, &block) }
|
|
83
|
-
span(**slot_attributes(:description, attributes: { id: description_id })) { plain(description) } if description
|
|
84
|
-
end
|
|
81
|
+
span(**slot_attributes(:label_text)) { text_or_block(label, &block) }
|
|
85
82
|
end
|
|
86
83
|
end
|
|
84
|
+
|
|
85
|
+
# The description stays outside the label so aria-describedby carries it
|
|
86
|
+
# instead of it joining the control's accessible name.
|
|
87
|
+
if description
|
|
88
|
+
span(**slot_attributes(:description, attributes: { id: description_id })) { plain(description) }
|
|
89
|
+
end
|
|
87
90
|
end
|
|
88
91
|
end
|
|
89
92
|
|
|
@@ -122,30 +122,5 @@ module NitroKit
|
|
|
122
122
|
def multiple_name(name)
|
|
123
123
|
name.end_with?("[]") ? name : "#{name}[]"
|
|
124
124
|
end
|
|
125
|
-
|
|
126
|
-
def deterministic_id(name)
|
|
127
|
-
derived = name.to_s.gsub(/[^a-zA-Z0-9_-]+/, "_").gsub(/\A_+|_+\z/, "")
|
|
128
|
-
return derived unless derived.empty?
|
|
129
|
-
|
|
130
|
-
raise ArgumentError, "name #{name.inspect} cannot derive an id; pass id:"
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def choice_id(index)
|
|
134
|
-
"#{id}-#{index}" if id
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def validate_text!(name, text)
|
|
138
|
-
return text if text.is_a?(String) && !text.strip.empty?
|
|
139
|
-
|
|
140
|
-
raise ArgumentError, "#{name} must be a non-blank String"
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def validate_unique_choices!
|
|
144
|
-
duplicate_value = options.group_by { |choice| choice.value.to_s }.find { |_value, matches| matches.many? }
|
|
145
|
-
raise ArgumentError, "choice values must be unique" if duplicate_value
|
|
146
|
-
|
|
147
|
-
ids = options.each_with_index.filter_map { |choice, index| choice.id || choice_id(index) }
|
|
148
|
-
raise ArgumentError, "choice ids must be unique" unless ids.uniq.length == ids.length
|
|
149
|
-
end
|
|
150
125
|
end
|
|
151
126
|
end
|
|
@@ -22,8 +22,8 @@ module NitroKit
|
|
|
22
22
|
control_aria: {},
|
|
23
23
|
desperately_need_a_class: nil
|
|
24
24
|
)
|
|
25
|
-
@identifier =
|
|
26
|
-
@name =
|
|
25
|
+
@identifier = validate_id!("Combobox id", id)
|
|
26
|
+
@name = validate_text!("Combobox name", name)
|
|
27
27
|
@label = visible_label(label)
|
|
28
28
|
@options = typed_options(options)
|
|
29
29
|
@value = value
|
|
@@ -52,7 +52,6 @@ module NitroKit
|
|
|
52
52
|
controller: "nk--combobox",
|
|
53
53
|
placement: placement_value,
|
|
54
54
|
state: "closed",
|
|
55
|
-
action: "click@window->nk--combobox#closeFromOutside",
|
|
56
55
|
nk__combobox_open_value: "false",
|
|
57
56
|
nk__combobox_required_value: @required,
|
|
58
57
|
nk__combobox_invalid_selection_value: I18n.t("nitro_kit.combobox.invalid_selection"),
|
|
@@ -103,7 +102,7 @@ module NitroKit
|
|
|
103
102
|
autocomplete: "list",
|
|
104
103
|
controls: listbox_id,
|
|
105
104
|
expanded: false,
|
|
106
|
-
required: @required
|
|
105
|
+
required: @required || nil
|
|
107
106
|
),
|
|
108
107
|
data: {
|
|
109
108
|
nk__combobox_target: "input",
|
|
@@ -118,23 +117,7 @@ module NitroKit
|
|
|
118
117
|
:input
|
|
119
118
|
)
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
**slot_attributes(
|
|
123
|
-
:icon,
|
|
124
|
-
attributes: {
|
|
125
|
-
viewbox: "0 0 16 16",
|
|
126
|
-
width: 16,
|
|
127
|
-
height: 16,
|
|
128
|
-
fill: "none",
|
|
129
|
-
stroke: "currentColor",
|
|
130
|
-
stroke_width: 1.5,
|
|
131
|
-
stroke_linecap: "round",
|
|
132
|
-
stroke_linejoin: "round",
|
|
133
|
-
focusable: "false",
|
|
134
|
-
aria: { hidden: true }
|
|
135
|
-
}
|
|
136
|
-
)
|
|
137
|
-
) { |svg| svg.path(d: "m4 6 4 4 4-4") }
|
|
120
|
+
render_in_slot(Icon.new(:chevron_down, size: :sm), :icon)
|
|
138
121
|
end
|
|
139
122
|
end
|
|
140
123
|
|
|
@@ -299,18 +282,6 @@ module NitroKit
|
|
|
299
282
|
end
|
|
300
283
|
end
|
|
301
284
|
|
|
302
|
-
def component_id(value)
|
|
303
|
-
return value if value.is_a?(String) && value.present? && !value.match?(/\s/)
|
|
304
|
-
|
|
305
|
-
raise ArgumentError, "Combobox id must be a non-blank String without whitespace"
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
def form_name(value)
|
|
309
|
-
return value if value.is_a?(String) && value.present?
|
|
310
|
-
|
|
311
|
-
raise ArgumentError, "Combobox name must be a non-blank String"
|
|
312
|
-
end
|
|
313
|
-
|
|
314
285
|
def visible_label(value)
|
|
315
286
|
return value if value == false
|
|
316
287
|
return value if value.is_a?(String) && value.present?
|
|
@@ -16,12 +16,6 @@ module NitroKit
|
|
|
16
16
|
module Validation
|
|
17
17
|
private
|
|
18
18
|
|
|
19
|
-
def component_id(value)
|
|
20
|
-
return value if value.is_a?(String) && value.match?(/\A[A-Za-z0-9][A-Za-z0-9_-]*\z/)
|
|
21
|
-
|
|
22
|
-
raise ArgumentError, "CommandPalette id: must contain only letters, numbers, underscores, and hyphens"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
19
|
def required_text(name, value)
|
|
26
20
|
return value if value.is_a?(String) && !value.strip.empty?
|
|
27
21
|
|
|
@@ -30,10 +24,38 @@ module NitroKit
|
|
|
30
24
|
end
|
|
31
25
|
private_constant :Validation
|
|
32
26
|
|
|
27
|
+
module Destinations
|
|
28
|
+
def destination(
|
|
29
|
+
label,
|
|
30
|
+
href:,
|
|
31
|
+
description: nil,
|
|
32
|
+
html: {},
|
|
33
|
+
aria: {},
|
|
34
|
+
data: {},
|
|
35
|
+
desperately_need_a_class: nil
|
|
36
|
+
)
|
|
37
|
+
ensure_collecting!
|
|
38
|
+
|
|
39
|
+
@destinations << Destination.new(
|
|
40
|
+
label: required_text(:label, label),
|
|
41
|
+
href: required_text(:href, href),
|
|
42
|
+
description: validate_optional_text!(:description, description),
|
|
43
|
+
html:,
|
|
44
|
+
aria:,
|
|
45
|
+
data:,
|
|
46
|
+
css_class: desperately_need_a_class
|
|
47
|
+
)
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
private_constant :Destinations
|
|
52
|
+
|
|
33
53
|
include Validation
|
|
54
|
+
include Destinations
|
|
34
55
|
|
|
35
56
|
class Results < Component
|
|
36
57
|
include Validation
|
|
58
|
+
include Destinations
|
|
37
59
|
|
|
38
60
|
def initialize(
|
|
39
61
|
id:,
|
|
@@ -42,7 +64,7 @@ module NitroKit
|
|
|
42
64
|
data: {},
|
|
43
65
|
desperately_need_a_class: nil
|
|
44
66
|
)
|
|
45
|
-
@identifier =
|
|
67
|
+
@identifier = validate_id!("CommandPalette id:", id)
|
|
46
68
|
@destinations = []
|
|
47
69
|
|
|
48
70
|
super(
|
|
@@ -65,38 +87,16 @@ module NitroKit
|
|
|
65
87
|
)
|
|
66
88
|
end
|
|
67
89
|
|
|
68
|
-
def destination(
|
|
69
|
-
label,
|
|
70
|
-
href:,
|
|
71
|
-
description: nil,
|
|
72
|
-
html: {},
|
|
73
|
-
aria: {},
|
|
74
|
-
data: {},
|
|
75
|
-
desperately_need_a_class: nil
|
|
76
|
-
)
|
|
77
|
-
ensure_collecting!
|
|
78
|
-
|
|
79
|
-
@destinations << Destination.new(
|
|
80
|
-
label: required_text(:label, label),
|
|
81
|
-
href: required_text(:href, href),
|
|
82
|
-
description: validate_optional_text!(:description, description),
|
|
83
|
-
html:,
|
|
84
|
-
aria:,
|
|
85
|
-
data:,
|
|
86
|
-
css_class: desperately_need_a_class
|
|
87
|
-
)
|
|
88
|
-
nil
|
|
89
|
-
end
|
|
90
|
-
|
|
91
90
|
def view_template(&block)
|
|
92
91
|
@collecting = true
|
|
93
92
|
yield(self) if block
|
|
94
93
|
|
|
95
94
|
tag(:"turbo-frame", **root_attributes) do
|
|
96
95
|
nav(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
**slot_attributes(
|
|
97
|
+
:results,
|
|
98
|
+
attributes: { id: results_id, aria: { labelledby: title_id } }
|
|
99
|
+
)
|
|
100
100
|
) do
|
|
101
101
|
@destinations.each_with_index { |destination, index| render_destination(destination, index) }
|
|
102
102
|
end
|
|
@@ -163,7 +163,7 @@ module NitroKit
|
|
|
163
163
|
data: {},
|
|
164
164
|
desperately_need_a_class: nil
|
|
165
165
|
)
|
|
166
|
-
@identifier =
|
|
166
|
+
@identifier = validate_id!("CommandPalette id:", id)
|
|
167
167
|
@label = required_text(:label, label)
|
|
168
168
|
@placeholder = required_text(:placeholder, placeholder)
|
|
169
169
|
@empty_text = required_text(:empty_text, empty_text)
|
|
@@ -204,29 +204,6 @@ module NitroKit
|
|
|
204
204
|
end
|
|
205
205
|
end
|
|
206
206
|
|
|
207
|
-
def destination(
|
|
208
|
-
label,
|
|
209
|
-
href:,
|
|
210
|
-
description: nil,
|
|
211
|
-
html: {},
|
|
212
|
-
aria: {},
|
|
213
|
-
data: {},
|
|
214
|
-
desperately_need_a_class: nil
|
|
215
|
-
)
|
|
216
|
-
ensure_collecting!
|
|
217
|
-
|
|
218
|
-
@destinations << Destination.new(
|
|
219
|
-
label: required_text(:label, label),
|
|
220
|
-
href: required_text(:href, href),
|
|
221
|
-
description: validate_optional_text!(:description, description),
|
|
222
|
-
html:,
|
|
223
|
-
aria:,
|
|
224
|
-
data:,
|
|
225
|
-
css_class: desperately_need_a_class
|
|
226
|
-
)
|
|
227
|
-
nil
|
|
228
|
-
end
|
|
229
|
-
|
|
230
207
|
private
|
|
231
208
|
|
|
232
209
|
def collect_destinations
|
|
@@ -250,7 +227,8 @@ module NitroKit
|
|
|
250
227
|
}.compact,
|
|
251
228
|
data: {
|
|
252
229
|
nk__command_palette_target: "trigger",
|
|
253
|
-
|
|
230
|
+
nk__dialog_command: "show-modal",
|
|
231
|
+
action: "click->nk--command-palette#openedByTrigger"
|
|
254
232
|
}
|
|
255
233
|
),
|
|
256
234
|
:trigger
|
|
@@ -276,8 +254,10 @@ module NitroKit
|
|
|
276
254
|
nk__dialog_target: "panel",
|
|
277
255
|
nk__command_palette_target: "panel",
|
|
278
256
|
action: [
|
|
257
|
+
"command->nk--command-palette#guardOpen",
|
|
279
258
|
"click->nk--dialog#dismiss",
|
|
280
259
|
"cancel->nk--dialog#cancel",
|
|
260
|
+
"close->nk--dialog#restoreFocus",
|
|
281
261
|
"close->nk--command-palette#closed"
|
|
282
262
|
].join(" ")
|
|
283
263
|
}
|
|
@@ -342,6 +322,7 @@ module NitroKit
|
|
|
342
322
|
variant: :ghost,
|
|
343
323
|
size: :md,
|
|
344
324
|
html: { command: "close", commandfor: panel_id },
|
|
325
|
+
data: { nk__dialog_command: "close" },
|
|
345
326
|
aria: { label: @close_label }
|
|
346
327
|
),
|
|
347
328
|
:close
|
|
@@ -392,7 +373,9 @@ module NitroKit
|
|
|
392
373
|
def root_actions
|
|
393
374
|
actions = [
|
|
394
375
|
"turbo:before-visit@document->nk--command-palette#closeForVisit",
|
|
395
|
-
"turbo:before-cache@document->nk--command-palette#closeForVisit"
|
|
376
|
+
"turbo:before-cache@document->nk--command-palette#closeForVisit",
|
|
377
|
+
"click->nk--dialog#invoke",
|
|
378
|
+
"turbo:before-cache@document->nk--dialog#closeForCache"
|
|
396
379
|
]
|
|
397
380
|
actions.prepend("keydown@document->nk--command-palette#shortcut") if @shortcut
|
|
398
381
|
actions.join(" ")
|