nitro_kit 2.0.0.alpha.2 → 2.0.0.alpha.3
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 +50 -1
- data/README.md +23 -4
- data/STYLE_GUIDE.md +37 -5
- data/app/assets/stylesheets/nitro_kit.css +1714 -1358
- data/app/components/nitro_kit/accordion.rb +1 -7
- data/app/components/nitro_kit/app_navigation.rb +1 -2
- data/app/components/nitro_kit/app_shell.rb +1 -7
- data/app/components/nitro_kit/appearance_picker.rb +1 -11
- data/app/components/nitro_kit/avatar.rb +6 -1
- data/app/components/nitro_kit/avatar_stack.rb +3 -0
- 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 +3 -16
- data/app/components/nitro_kit/command_palette.rb +41 -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 +3 -14
- 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 +0 -1
- 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 +3 -3
- 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 +1 -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 +21 -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 +8 -1
- data/app/javascript/controllers/nk/tooltip_controller.js +9 -0
- data/config/locales/en.yml +4 -1
- data/docs/agent_guide.md +31 -16
- data/docs/agent_native_spec.md +27 -10
- data/docs/browser_support.md +193 -0
- data/docs/component_contracts.md +88 -82
- data/docs/customization.md +27 -15
- data/docs/hotwire.md +15 -3
- data/docs/initialization_prompt.md +31 -6
- data/docs/migration_1_to_2.md +61 -19
- data/docs/patterns/application_foundation.md +6 -4
- data/docs/patterns/crud_resource.md +6 -3
- data/docs/patterns/destructive_action.md +25 -0
- data/docs/patterns/resource_form.md +1 -1
- data/docs/rails_conventions.md +4 -1
- data/docs/rails_integration.md +39 -11
- data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +12 -0
- data/lib/nitro_kit/installation.rb +9 -4
- data/lib/nitro_kit/migration_inventory.rb +268 -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 +6 -1
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +18 -8
- data/src/stylesheets/nitro_kit/components/accordion.css +40 -40
- data/src/stylesheets/nitro_kit/components/alert.css +9 -9
- data/src/stylesheets/nitro_kit/components/app_navigation.css +19 -19
- data/src/stylesheets/nitro_kit/components/app_shell.css +196 -193
- data/src/stylesheets/nitro_kit/components/appearance_picker.css +41 -31
- data/src/stylesheets/nitro_kit/components/avatar.css +3 -3
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +36 -19
- data/src/stylesheets/nitro_kit/components/button.css +83 -58
- data/src/stylesheets/nitro_kit/components/button_group.css +19 -14
- data/src/stylesheets/nitro_kit/components/card.css +11 -5
- data/src/stylesheets/nitro_kit/components/checkbox.css +40 -36
- data/src/stylesheets/nitro_kit/components/checkbox_group.css +13 -13
- data/src/stylesheets/nitro_kit/components/combobox.css +33 -23
- data/src/stylesheets/nitro_kit/components/command_palette.css +48 -47
- data/src/stylesheets/nitro_kit/components/control_group.css +52 -58
- data/src/stylesheets/nitro_kit/components/danger_zone.css +3 -3
- data/src/stylesheets/nitro_kit/components/data_section.css +1 -1
- data/src/stylesheets/nitro_kit/components/details_table.css +13 -13
- data/src/stylesheets/nitro_kit/components/dialog.css +6 -3
- data/src/stylesheets/nitro_kit/components/dropdown.css +29 -23
- data/src/stylesheets/nitro_kit/components/dropzone.css +71 -28
- data/src/stylesheets/nitro_kit/components/empty_state.css +6 -6
- data/src/stylesheets/nitro_kit/components/field.css +30 -26
- data/src/stylesheets/nitro_kit/components/field_group.css +13 -0
- data/src/stylesheets/nitro_kit/components/fieldset.css +6 -0
- data/src/stylesheets/nitro_kit/components/input.css +9 -2
- data/src/stylesheets/nitro_kit/components/layout.css +165 -165
- data/src/stylesheets/nitro_kit/components/page_header.css +3 -3
- data/src/stylesheets/nitro_kit/components/pagination.css +55 -46
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +7 -7
- data/src/stylesheets/nitro_kit/components/palette.css +20 -20
- data/src/stylesheets/nitro_kit/components/progressive_image.css +44 -54
- data/src/stylesheets/nitro_kit/components/radio_button.css +36 -29
- data/src/stylesheets/nitro_kit/components/radio_button_group.css +43 -43
- data/src/stylesheets/nitro_kit/components/rich_text_area.css +15 -0
- data/src/stylesheets/nitro_kit/components/select.css +2 -9
- data/src/stylesheets/nitro_kit/components/settings_layout.css +21 -18
- data/src/stylesheets/nitro_kit/components/settings_section.css +84 -0
- data/src/stylesheets/nitro_kit/components/sheet.css +23 -3
- data/src/stylesheets/nitro_kit/components/stat_grid.css +10 -9
- data/src/stylesheets/nitro_kit/components/switch.css +37 -33
- data/src/stylesheets/nitro_kit/components/table.css +35 -31
- data/src/stylesheets/nitro_kit/components/tabs.css +43 -39
- data/src/stylesheets/nitro_kit/components/toast.css +20 -9
- data/src/stylesheets/nitro_kit/components/toolbar.css +14 -39
- data/src/stylesheets/nitro_kit/components/tooltip.css +34 -29
- data/src/stylesheets/nitro_kit/components/typeset.css +147 -1
- data/src/stylesheets/nitro_kit/reset.css +9 -9
- data/src/stylesheets/nitro_kit/tokens.css +8 -9
- metadata +4 -3
- 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
|
|
|
@@ -152,11 +152,5 @@ module NitroKit
|
|
|
152
152
|
|
|
153
153
|
raise ArgumentError, "Accordion #{name} must be true or false"
|
|
154
154
|
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
155
|
end
|
|
162
156
|
end
|
|
@@ -223,8 +223,7 @@ module NitroKit
|
|
|
223
223
|
:item_link,
|
|
224
224
|
attributes: {
|
|
225
225
|
href: entry.href,
|
|
226
|
-
aria: { current: entry.current ? "page" : nil }
|
|
227
|
-
data: { state: entry.current ? "current" : "default" }
|
|
226
|
+
aria: { current: entry.current ? "page" : nil }
|
|
228
227
|
},
|
|
229
228
|
html: entry.html,
|
|
230
229
|
aria: entry.aria,
|
|
@@ -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
|
|
|
@@ -20,7 +20,7 @@ module NitroKit
|
|
|
20
20
|
data: {},
|
|
21
21
|
desperately_need_a_class: nil
|
|
22
22
|
)
|
|
23
|
-
@identifier =
|
|
23
|
+
@identifier = validate_id!("AppearancePicker id", id)
|
|
24
24
|
@label = validate_label!(label)
|
|
25
25
|
@presentation = validate_choice!(:presentation, presentation, PRESENTATIONS)
|
|
26
26
|
@preference = validate_choice!(:preference, preference, PREFERENCES)
|
|
@@ -137,16 +137,6 @@ module NitroKit
|
|
|
137
137
|
"#{identifier}-#{preference}"
|
|
138
138
|
end
|
|
139
139
|
|
|
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
140
|
def validate_label!(value)
|
|
151
141
|
return value if value.is_a?(String) && value.present?
|
|
152
142
|
|
|
@@ -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)
|
|
@@ -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",
|
|
@@ -299,18 +298,6 @@ module NitroKit
|
|
|
299
298
|
end
|
|
300
299
|
end
|
|
301
300
|
|
|
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
301
|
def visible_label(value)
|
|
315
302
|
return value if value == false
|
|
316
303
|
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
|
|
@@ -278,6 +256,7 @@ module NitroKit
|
|
|
278
256
|
action: [
|
|
279
257
|
"click->nk--dialog#dismiss",
|
|
280
258
|
"cancel->nk--dialog#cancel",
|
|
259
|
+
"close->nk--dialog#restoreFocus",
|
|
281
260
|
"close->nk--command-palette#closed"
|
|
282
261
|
].join(" ")
|
|
283
262
|
}
|
|
@@ -342,6 +321,7 @@ module NitroKit
|
|
|
342
321
|
variant: :ghost,
|
|
343
322
|
size: :md,
|
|
344
323
|
html: { command: "close", commandfor: panel_id },
|
|
324
|
+
data: { nk__dialog_command: "close" },
|
|
345
325
|
aria: { label: @close_label }
|
|
346
326
|
),
|
|
347
327
|
:close
|
|
@@ -392,7 +372,9 @@ module NitroKit
|
|
|
392
372
|
def root_actions
|
|
393
373
|
actions = [
|
|
394
374
|
"turbo:before-visit@document->nk--command-palette#closeForVisit",
|
|
395
|
-
"turbo:before-cache@document->nk--command-palette#closeForVisit"
|
|
375
|
+
"turbo:before-cache@document->nk--command-palette#closeForVisit",
|
|
376
|
+
"click->nk--dialog#invoke",
|
|
377
|
+
"turbo:before-cache@document->nk--dialog#closeForCache"
|
|
396
378
|
]
|
|
397
379
|
actions.prepend("keydown@document->nk--command-palette#shortcut") if @shortcut
|
|
398
380
|
actions.join(" ")
|
|
@@ -7,6 +7,7 @@ module NitroKit
|
|
|
7
7
|
# Data keys a component sets for itself through `attributes:`.
|
|
8
8
|
COMPONENT_OWNED_DATA_ATTRIBUTES = %w[
|
|
9
9
|
state disabled required orientation presentation placement layout side field-type
|
|
10
|
+
dir gap align justify wrap cols mode key
|
|
10
11
|
].freeze
|
|
11
12
|
# Every data key Nitro owns. Applications may not pass any of them through `data:`.
|
|
12
13
|
RESERVED_DATA_ATTRIBUTES = (
|
|
@@ -93,6 +94,18 @@ module NitroKit
|
|
|
93
94
|
raise ArgumentError, "#{name} must be #{expected}; received #{value.inspect}"
|
|
94
95
|
end
|
|
95
96
|
|
|
97
|
+
def validate_id!(name, value)
|
|
98
|
+
return value if value.is_a?(String) && value.match?(/\A[A-Za-z0-9][A-Za-z0-9_-]*\z/)
|
|
99
|
+
|
|
100
|
+
raise ArgumentError, "#{name} must be a fragment-safe String using letters, numbers, underscores, or hyphens"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def validate_text!(name, text)
|
|
104
|
+
return text if text.is_a?(String) && !text.strip.empty?
|
|
105
|
+
|
|
106
|
+
raise ArgumentError, "#{name} must be a non-blank String"
|
|
107
|
+
end
|
|
108
|
+
|
|
96
109
|
def owned_attributes(attributes:, html:, aria:, data:, owned_data:, desperately_need_a_class:)
|
|
97
110
|
attributes = attribute_hash(attributes, name: "attributes")
|
|
98
111
|
html = attribute_hash(html, name: "html")
|
|
@@ -117,8 +130,7 @@ module NitroKit
|
|
|
117
130
|
merged[:data] = merged_data if merged_data.any?
|
|
118
131
|
|
|
119
132
|
unless desperately_need_a_class.nil?
|
|
120
|
-
|
|
121
|
-
merged[:class] = desperately_need_a_class
|
|
133
|
+
merged[:class] = normalize_class_escape(desperately_need_a_class)
|
|
122
134
|
merged[:data] = merge_owned_data(merged.fetch(:data, {}), { nk_escape: "class" })
|
|
123
135
|
end
|
|
124
136
|
|
|
@@ -293,6 +305,43 @@ module NitroKit
|
|
|
293
305
|
@component_name.tr("-", " ")
|
|
294
306
|
end
|
|
295
307
|
|
|
308
|
+
# Conventions shared by the choice group components. They read the `id`
|
|
309
|
+
# and `options` readers those components already keep.
|
|
310
|
+
|
|
311
|
+
def deterministic_id(name)
|
|
312
|
+
derived = name.to_s.gsub(/[^a-zA-Z0-9_-]+/, "_").gsub(/\A_+|_+\z/, "")
|
|
313
|
+
return derived unless derived.empty?
|
|
314
|
+
|
|
315
|
+
raise ArgumentError, "name #{name.inspect} cannot derive an id; pass id:"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def choice_id(index)
|
|
319
|
+
"#{id}-#{index}" if id
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def validate_unique_choices!
|
|
323
|
+
duplicate_value = options.group_by { |choice| choice.value.to_s }.find { |_value, matches| matches.many? }
|
|
324
|
+
raise ArgumentError, "choice values must be unique" if duplicate_value
|
|
325
|
+
|
|
326
|
+
ids = options.each_with_index.filter_map { |choice, index| choice.id || choice_id(index) }
|
|
327
|
+
raise ArgumentError, "choice ids must be unique" unless ids.uniq.length == ids.length
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Command invoker plumbing shared by the nk--dialog overlay components.
|
|
331
|
+
|
|
332
|
+
def command_attributes(html, command, commandfor:, disabled: false)
|
|
333
|
+
raise ArgumentError, "html must be a Hash" unless html.is_a?(Hash)
|
|
334
|
+
|
|
335
|
+
collision = html.keys.find { |key| %w[command commandfor].include?(key.to_s.downcase) }
|
|
336
|
+
raise ArgumentError, "#{collision}: is owned by #{component_words.capitalize}" if collision
|
|
337
|
+
|
|
338
|
+
disabled ? html : html.merge(command:, commandfor:)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def command_data(data, command, disabled: false)
|
|
342
|
+
disabled ? data : data.merge(nk__dialog_command: command)
|
|
343
|
+
end
|
|
344
|
+
|
|
296
345
|
def validate_optional_text!(name, text)
|
|
297
346
|
return if text.nil?
|
|
298
347
|
return text if text.is_a?(String) && !text.strip.empty?
|
|
@@ -306,10 +355,13 @@ module NitroKit
|
|
|
306
355
|
end
|
|
307
356
|
end
|
|
308
357
|
|
|
309
|
-
def
|
|
310
|
-
|
|
358
|
+
def normalize_class_escape(value)
|
|
359
|
+
tokens = ActionView::Helpers::TagHelper.build_tag_values(value)
|
|
360
|
+
.flat_map { CGI.unescape_html(_1.to_s).split }
|
|
361
|
+
.uniq
|
|
362
|
+
return tokens.join(" ") if tokens.any?
|
|
311
363
|
|
|
312
|
-
raise ArgumentError, "desperately_need_a_class: must
|
|
364
|
+
raise ArgumentError, "desperately_need_a_class: must contain at least one class name"
|
|
313
365
|
end
|
|
314
366
|
|
|
315
367
|
def text_or_block(text = nil, &block)
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module NitroKit
|
|
4
4
|
class DangerZone < Component
|
|
5
|
+
TITLE_LEVELS = (1..6).freeze
|
|
6
|
+
|
|
5
7
|
Child = Data.define(:component, :content)
|
|
6
8
|
|
|
7
9
|
def initialize(
|
|
8
10
|
title: nil,
|
|
9
11
|
description: nil,
|
|
12
|
+
level: 2,
|
|
10
13
|
id: nil,
|
|
11
14
|
html: {},
|
|
12
15
|
aria: {},
|
|
@@ -15,6 +18,7 @@ module NitroKit
|
|
|
15
18
|
)
|
|
16
19
|
@title_content = content_from_keyword(:title, title)
|
|
17
20
|
@description_content = content_from_keyword(:description, description)
|
|
21
|
+
@level = validate_choice!(:level, level, TITLE_LEVELS)
|
|
18
22
|
@confirmation = nil
|
|
19
23
|
@escape = nil
|
|
20
24
|
|
|
@@ -36,7 +40,7 @@ module NitroKit
|
|
|
36
40
|
|
|
37
41
|
section(**root_attributes) do
|
|
38
42
|
header(**slot_attributes(:header)) do
|
|
39
|
-
|
|
43
|
+
public_send(:"h#{@level}", **slot_attributes(:title)) { render_deferred_content(@title_content) }
|
|
40
44
|
p(**slot_attributes(:description)) { render_deferred_content(@description_content) }
|
|
41
45
|
end
|
|
42
46
|
div(**slot_attributes(:confirmation), &@confirmation)
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module NitroKit
|
|
4
4
|
class DataSection < Component
|
|
5
|
+
TITLE_LEVELS = (1..6).freeze
|
|
5
6
|
Child = Data.define(:component, :content)
|
|
6
7
|
|
|
7
8
|
def initialize(
|
|
8
9
|
title: nil,
|
|
9
10
|
description: nil,
|
|
11
|
+
level: 2,
|
|
10
12
|
id: nil,
|
|
11
13
|
html: {},
|
|
12
14
|
aria: {},
|
|
@@ -15,12 +17,14 @@ module NitroKit
|
|
|
15
17
|
)
|
|
16
18
|
@title_content = content_from_keyword(:title, title)
|
|
17
19
|
@description_content = content_from_keyword(:description, description)
|
|
20
|
+
@level = validate_choice!(:level, level, TITLE_LEVELS)
|
|
18
21
|
@actions = nil
|
|
19
22
|
@content = nil
|
|
23
|
+
@title_id = "#{id || "nk-data-section-#{SecureRandom.hex(4)}"}-title"
|
|
20
24
|
|
|
21
25
|
super(
|
|
22
26
|
component: :data_section,
|
|
23
|
-
attributes: { id: }.compact,
|
|
27
|
+
attributes: { id:, aria: { labelledby: @title_id } }.compact,
|
|
24
28
|
html:,
|
|
25
29
|
aria:,
|
|
26
30
|
data:,
|
|
@@ -28,15 +32,17 @@ module NitroKit
|
|
|
28
32
|
)
|
|
29
33
|
end
|
|
30
34
|
|
|
31
|
-
def view_template
|
|
32
|
-
|
|
35
|
+
def view_template(&block)
|
|
36
|
+
collect_declarations(&block)
|
|
33
37
|
require_content!("DataSection", :title, @title_content)
|
|
34
38
|
raise ArgumentError, "DataSection requires exactly one table or EmptyState" unless @content
|
|
35
39
|
|
|
36
40
|
section(**root_attributes) do
|
|
37
41
|
header(**slot_attributes(:header)) do
|
|
38
42
|
div(**slot_attributes(:heading)) do
|
|
39
|
-
|
|
43
|
+
public_send(:"h#{@level}", **slot_attributes(:title, attributes: { id: @title_id })) do
|
|
44
|
+
render_deferred_content(@title_content)
|
|
45
|
+
end
|
|
40
46
|
if @description_content
|
|
41
47
|
p(**slot_attributes(:description)) { render_deferred_content(@description_content) }
|
|
42
48
|
end
|
|
@@ -48,6 +54,7 @@ module NitroKit
|
|
|
48
54
|
end
|
|
49
55
|
|
|
50
56
|
def title(text = nil, &block)
|
|
57
|
+
ensure_collecting!
|
|
51
58
|
@title_content = declare_content(:title, @title_content, text, &block)
|
|
52
59
|
nil
|
|
53
60
|
end
|
|
@@ -60,6 +67,7 @@ module NitroKit
|
|
|
60
67
|
alias :html_table :table
|
|
61
68
|
|
|
62
69
|
def actions(component, &content)
|
|
70
|
+
ensure_collecting!
|
|
63
71
|
unless component.is_a?(NitroKit::ButtonGroup) || component.is_a?(NitroKit::Button)
|
|
64
72
|
raise ArgumentError, "DataSection actions must be a NitroKit::ButtonGroup or NitroKit::Button"
|
|
65
73
|
end
|
|
@@ -70,6 +78,7 @@ module NitroKit
|
|
|
70
78
|
end
|
|
71
79
|
|
|
72
80
|
def table(component, &content)
|
|
81
|
+
ensure_collecting!
|
|
73
82
|
unless table_component?(component)
|
|
74
83
|
raise ArgumentError, "DataSection table must be a NitroKit::Table or NitroKit::DetailsTable"
|
|
75
84
|
end
|
|
@@ -88,6 +97,21 @@ module NitroKit
|
|
|
88
97
|
|
|
89
98
|
private
|
|
90
99
|
|
|
100
|
+
def collect_declarations
|
|
101
|
+
return unless block_given?
|
|
102
|
+
|
|
103
|
+
@collecting = true
|
|
104
|
+
yield(self)
|
|
105
|
+
ensure
|
|
106
|
+
@collecting = false
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def ensure_collecting!
|
|
110
|
+
return if @collecting
|
|
111
|
+
|
|
112
|
+
raise ArgumentError, "DataSection content must be declared inside the render block"
|
|
113
|
+
end
|
|
114
|
+
|
|
91
115
|
def table_component?(component)
|
|
92
116
|
component.is_a?(NitroKit::Table) || component.is_a?(NitroKit::DetailsTable)
|
|
93
117
|
end
|