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
|
@@ -43,8 +43,8 @@ module NitroKit
|
|
|
43
43
|
|
|
44
44
|
attr_reader :record, :route_base
|
|
45
45
|
|
|
46
|
-
def view_template
|
|
47
|
-
|
|
46
|
+
def view_template(&block)
|
|
47
|
+
collect_declarations(&block)
|
|
48
48
|
raise ArgumentError, "DetailsTable requires at least one field" if @fields.empty?
|
|
49
49
|
|
|
50
50
|
div(**root_attributes) do
|
|
@@ -65,6 +65,7 @@ module NitroKit
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
def field(attribute, label: nil, value: UNSET, &content)
|
|
68
|
+
ensure_collecting!
|
|
68
69
|
attribute = validate_attribute!(attribute)
|
|
69
70
|
if @fields.any? { |field| field.attribute.to_s == attribute.to_s }
|
|
70
71
|
raise ArgumentError, "DetailsTable field keys must be unique: #{attribute.inspect}"
|
|
@@ -78,6 +79,21 @@ module NitroKit
|
|
|
78
79
|
|
|
79
80
|
private
|
|
80
81
|
|
|
82
|
+
def collect_declarations
|
|
83
|
+
return unless block_given?
|
|
84
|
+
|
|
85
|
+
@collecting = true
|
|
86
|
+
yield(self)
|
|
87
|
+
ensure
|
|
88
|
+
@collecting = false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def ensure_collecting!
|
|
92
|
+
return if @collecting
|
|
93
|
+
|
|
94
|
+
raise ArgumentError, "DetailsTable fields must be declared inside the render block"
|
|
95
|
+
end
|
|
96
|
+
|
|
81
97
|
def render_field(field)
|
|
82
98
|
@table.tr do
|
|
83
99
|
@table.th(field.label, scope: :row)
|
|
@@ -114,12 +130,7 @@ module NitroKit
|
|
|
114
130
|
|
|
115
131
|
def render_string(value)
|
|
116
132
|
if value.start_with?("https://", "http://")
|
|
117
|
-
a(
|
|
118
|
-
**slot_attributes(
|
|
119
|
-
:link,
|
|
120
|
-
attributes: { href: value, target: "_blank", rel: "noopener noreferrer" }
|
|
121
|
-
)
|
|
122
|
-
) { plain(value) }
|
|
133
|
+
a(**slot_attributes(:link, attributes: { href: value })) { plain(value) }
|
|
123
134
|
else
|
|
124
135
|
plain(value)
|
|
125
136
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module NitroKit
|
|
4
4
|
class Dialog < Component
|
|
5
|
-
Trigger = ::Data.define(:text, :variant, :size, :disabled, :html, :aria, :data, :css_class, :content)
|
|
5
|
+
Trigger = ::Data.define(:text, :variant, :size, :icon, :icon_end, :label, :disabled, :html, :aria, :data, :css_class, :content)
|
|
6
6
|
private_constant :Trigger
|
|
7
7
|
|
|
8
8
|
Panel = ::Data.define(:title, :description, :nonmodal, :html, :aria, :data, :css_class, :content)
|
|
@@ -32,6 +32,10 @@ module NitroKit
|
|
|
32
32
|
id:,
|
|
33
33
|
data: {
|
|
34
34
|
controller: "nk--dialog",
|
|
35
|
+
action: [
|
|
36
|
+
"click->nk--dialog#invoke",
|
|
37
|
+
"turbo:before-cache@document->nk--dialog#closeForCache"
|
|
38
|
+
].join(" "),
|
|
35
39
|
nk__dialog_dismissible_value: @dismissible
|
|
36
40
|
}
|
|
37
41
|
},
|
|
@@ -57,6 +61,9 @@ module NitroKit
|
|
|
57
61
|
text = nil,
|
|
58
62
|
variant: :default,
|
|
59
63
|
size: :md,
|
|
64
|
+
icon: nil,
|
|
65
|
+
icon_end: nil,
|
|
66
|
+
label: nil,
|
|
60
67
|
disabled: false,
|
|
61
68
|
html: {},
|
|
62
69
|
aria: {},
|
|
@@ -71,6 +78,9 @@ module NitroKit
|
|
|
71
78
|
text:,
|
|
72
79
|
variant:,
|
|
73
80
|
size:,
|
|
81
|
+
icon:,
|
|
82
|
+
icon_end:,
|
|
83
|
+
label:,
|
|
74
84
|
disabled: validate_boolean!(:disabled, disabled),
|
|
75
85
|
html:,
|
|
76
86
|
aria:,
|
|
@@ -115,7 +125,7 @@ module NitroKit
|
|
|
115
125
|
end
|
|
116
126
|
|
|
117
127
|
def close_button(
|
|
118
|
-
label:
|
|
128
|
+
label: default_close_label,
|
|
119
129
|
html: {},
|
|
120
130
|
aria: {},
|
|
121
131
|
data: {},
|
|
@@ -159,10 +169,13 @@ module NitroKit
|
|
|
159
169
|
@trigger.text,
|
|
160
170
|
variant: @trigger.variant,
|
|
161
171
|
size: @trigger.size,
|
|
172
|
+
icon: @trigger.icon,
|
|
173
|
+
icon_end: @trigger.icon_end,
|
|
174
|
+
label: @trigger.label,
|
|
162
175
|
disabled: @trigger.disabled,
|
|
163
|
-
html:
|
|
176
|
+
html: command_attributes(@trigger.html, "show-modal", commandfor: element_id(:panel), disabled: @trigger.disabled),
|
|
164
177
|
aria: @trigger.aria.merge(haspopup: "dialog"),
|
|
165
|
-
data: @trigger.data,
|
|
178
|
+
data: command_data(@trigger.data, "show-modal", disabled: @trigger.disabled),
|
|
166
179
|
desperately_need_a_class: @trigger.css_class
|
|
167
180
|
)
|
|
168
181
|
|
|
@@ -191,7 +204,7 @@ module NitroKit
|
|
|
191
204
|
aria: owned_aria,
|
|
192
205
|
data: {
|
|
193
206
|
nk__dialog_target: "panel",
|
|
194
|
-
action: "click->nk--dialog#dismiss cancel->nk--dialog#cancel"
|
|
207
|
+
action: "click->nk--dialog#dismiss cancel->nk--dialog#cancel close->nk--dialog#restoreFocus"
|
|
195
208
|
}
|
|
196
209
|
},
|
|
197
210
|
html: @panel.html,
|
|
@@ -221,7 +234,7 @@ module NitroKit
|
|
|
221
234
|
|
|
222
235
|
def render_close_button
|
|
223
236
|
declaration = @close_button || CloseButton.new(
|
|
224
|
-
label:
|
|
237
|
+
label: default_close_label,
|
|
225
238
|
html: {},
|
|
226
239
|
aria: {},
|
|
227
240
|
data: {},
|
|
@@ -233,9 +246,9 @@ module NitroKit
|
|
|
233
246
|
icon: :x,
|
|
234
247
|
variant: :ghost,
|
|
235
248
|
size: :sm,
|
|
236
|
-
html:
|
|
249
|
+
html: command_attributes(declaration.html, "close", commandfor: element_id(:panel)),
|
|
237
250
|
aria: declaration.aria.merge(label: declaration.label),
|
|
238
|
-
data: declaration.data,
|
|
251
|
+
data: command_data(declaration.data, "close"),
|
|
239
252
|
desperately_need_a_class: declaration.css_class
|
|
240
253
|
),
|
|
241
254
|
:close
|
|
@@ -276,17 +289,8 @@ module NitroKit
|
|
|
276
289
|
raise ArgumentError, "Dialog close button must be declared inside the panel block"
|
|
277
290
|
end
|
|
278
291
|
|
|
279
|
-
def
|
|
280
|
-
|
|
281
|
-
raise ArgumentError, "html must be a Hash"
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
collision = html.keys.find { |key| %w[command commandfor].include?(key.to_s.downcase) }
|
|
285
|
-
if collision
|
|
286
|
-
raise ArgumentError, "#{collision}: is owned by Dialog"
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
disabled ? html : html.merge(command:, commandfor: element_id(:panel))
|
|
292
|
+
def default_close_label
|
|
293
|
+
I18n.t("nitro_kit.dialog.close")
|
|
290
294
|
end
|
|
291
295
|
end
|
|
292
296
|
end
|
|
@@ -373,15 +373,10 @@ module NitroKit
|
|
|
373
373
|
"Unknown Dropdown trigger #{name} #{value.inspect}; expected one of: #{choices.map(&:inspect).join(", ")}"
|
|
374
374
|
end
|
|
375
375
|
|
|
376
|
-
# Private copy of the AppShell identifier contract. Extract a shared
|
|
377
|
-
# validator into Component once its owner lands.
|
|
378
376
|
def component_id(value)
|
|
379
377
|
return generated_id if value.nil?
|
|
380
|
-
return value if value.is_a?(String) && value.match?(/\A[A-Za-z0-9][A-Za-z0-9_-]*\z/)
|
|
381
378
|
|
|
382
|
-
|
|
383
|
-
"Dropdown id must be nil or a String starting with a letter or digit and containing " \
|
|
384
|
-
"only letters, digits, hyphens, and underscores"
|
|
379
|
+
validate_id!("Dropdown id", value)
|
|
385
380
|
end
|
|
386
381
|
|
|
387
382
|
def generated_id
|
|
@@ -57,8 +57,8 @@ module NitroKit
|
|
|
57
57
|
data: {},
|
|
58
58
|
desperately_need_a_class: nil
|
|
59
59
|
)
|
|
60
|
-
@identifier =
|
|
61
|
-
@name =
|
|
60
|
+
@identifier = validate_id!("Dropzone id", id)
|
|
61
|
+
@name = validate_text!("Dropzone name", name)
|
|
62
62
|
@label = required_text(:label, label)
|
|
63
63
|
@description = optional_text(:description, description)
|
|
64
64
|
@presentation = validate_choice!(:presentation, presentation, PRESENTATIONS)
|
|
@@ -127,6 +127,7 @@ module NitroKit
|
|
|
127
127
|
) do
|
|
128
128
|
strong(**slot_attributes(:title, attributes: { id: title_id })) { plain(@label) }
|
|
129
129
|
span(**slot_attributes(:instruction)) { plain(I18n.t("nitro_kit.dropzone.prompt")) }
|
|
130
|
+
span(**slot_attributes(:compact_instruction)) { plain(I18n.t("nitro_kit.dropzone.compact_prompt")) }
|
|
130
131
|
|
|
131
132
|
if description
|
|
132
133
|
span(**slot_attributes(:description, attributes: { id: description_id })) { plain(description) }
|
|
@@ -291,18 +292,6 @@ module NitroKit
|
|
|
291
292
|
Rails.application.routes.url_helpers
|
|
292
293
|
end
|
|
293
294
|
|
|
294
|
-
def component_id(value)
|
|
295
|
-
return value if value.is_a?(String) && value.present? && !value.match?(/\s/)
|
|
296
|
-
|
|
297
|
-
raise ArgumentError, "Dropzone id must be a non-blank String without whitespace"
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def form_name(value)
|
|
301
|
-
return value if value.is_a?(String) && value.present?
|
|
302
|
-
|
|
303
|
-
raise ArgumentError, "Dropzone name must be a non-blank String"
|
|
304
|
-
end
|
|
305
|
-
|
|
306
295
|
def required_text(name, value)
|
|
307
296
|
return value if value.is_a?(String) && value.present?
|
|
308
297
|
|
|
@@ -39,8 +39,8 @@ module NitroKit
|
|
|
39
39
|
|
|
40
40
|
attr_reader :level, :variant
|
|
41
41
|
|
|
42
|
-
def view_template
|
|
43
|
-
|
|
42
|
+
def view_template(&block)
|
|
43
|
+
collect_declarations(&block)
|
|
44
44
|
require_content!("EmptyState", :title, @title_content)
|
|
45
45
|
|
|
46
46
|
section(**root_attributes) do
|
|
@@ -54,16 +54,19 @@ module NitroKit
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def title(text = nil, &block)
|
|
57
|
+
ensure_collecting!
|
|
57
58
|
@title_content = declare_content(:title, @title_content, text, &block)
|
|
58
59
|
nil
|
|
59
60
|
end
|
|
60
61
|
|
|
61
62
|
def description(text = nil, &block)
|
|
63
|
+
ensure_collecting!
|
|
62
64
|
@description_content = declare_content(:description, @description_content, text, &block)
|
|
63
65
|
nil
|
|
64
66
|
end
|
|
65
67
|
|
|
66
68
|
def icon(component, &content)
|
|
69
|
+
ensure_collecting!
|
|
67
70
|
unless component.is_a?(NitroKit::Icon)
|
|
68
71
|
raise ArgumentError, "EmptyState icon must be a NitroKit::Icon"
|
|
69
72
|
end
|
|
@@ -74,6 +77,7 @@ module NitroKit
|
|
|
74
77
|
end
|
|
75
78
|
|
|
76
79
|
def action(component, &content)
|
|
80
|
+
ensure_collecting!
|
|
77
81
|
unless component.is_a?(NitroKit::Button)
|
|
78
82
|
raise ArgumentError, "EmptyState actions must be NitroKit::Button instances"
|
|
79
83
|
end
|
|
@@ -88,6 +92,21 @@ module NitroKit
|
|
|
88
92
|
|
|
89
93
|
private
|
|
90
94
|
|
|
95
|
+
def collect_declarations
|
|
96
|
+
return unless block_given?
|
|
97
|
+
|
|
98
|
+
@collecting = true
|
|
99
|
+
yield(self)
|
|
100
|
+
ensure
|
|
101
|
+
@collecting = false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def ensure_collecting!
|
|
105
|
+
return if @collecting
|
|
106
|
+
|
|
107
|
+
raise ArgumentError, "EmptyState declarations must be inside the render block"
|
|
108
|
+
end
|
|
109
|
+
|
|
91
110
|
def render_actions
|
|
92
111
|
div(**slot_attributes(:actions)) do
|
|
93
112
|
@actions.each do |action|
|
|
@@ -64,7 +64,7 @@ module NitroKit
|
|
|
64
64
|
@id = id || form&.field_id(field_name) || default_field_id
|
|
65
65
|
@name = name || form&.field_name(field_name, multiple: @multiple) || default_field_name
|
|
66
66
|
@explicit_value = value
|
|
67
|
-
@field_label = label.nil? ?
|
|
67
|
+
@field_label = label.nil? ? UNSET : label
|
|
68
68
|
@field_description = description
|
|
69
69
|
@field_error_messages = Array(errors).compact
|
|
70
70
|
@options = options
|
|
@@ -124,7 +124,11 @@ module NitroKit
|
|
|
124
124
|
)
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
-
attr_reader :as, :form, :name, :id, :
|
|
127
|
+
attr_reader :as, :form, :name, :id, :field_description, :field_error_messages
|
|
128
|
+
|
|
129
|
+
def field_label
|
|
130
|
+
@field_label.equal?(UNSET) ? derived_label : @field_label
|
|
131
|
+
end
|
|
128
132
|
|
|
129
133
|
def view_template(&block)
|
|
130
134
|
div(**root_attributes) do
|
|
@@ -335,17 +339,32 @@ module NitroKit
|
|
|
335
339
|
end
|
|
336
340
|
|
|
337
341
|
def rich_text_control(html:, aria:, data:)
|
|
342
|
+
editor_aria = control_aria(aria)
|
|
343
|
+
content = annotated_rich_text_content(editor_aria)
|
|
344
|
+
|
|
338
345
|
render_in_slot(
|
|
339
346
|
RichTextArea.new(
|
|
340
|
-
|
|
347
|
+
content,
|
|
341
348
|
html: @control_html.merge(html),
|
|
342
|
-
aria:
|
|
349
|
+
aria: content.equal?(@rich_text_content) ? editor_aria : {},
|
|
343
350
|
data: @control_data.merge(data)
|
|
344
351
|
),
|
|
345
352
|
:control
|
|
346
353
|
)
|
|
347
354
|
end
|
|
348
355
|
|
|
356
|
+
# The application owns the editor markup, so the control ARIA is injected
|
|
357
|
+
# into the lexxy-editor tag where assistive technology reads it instead of
|
|
358
|
+
# landing on the role-less wrapper.
|
|
359
|
+
def annotated_rich_text_content(aria)
|
|
360
|
+
return @rich_text_content unless @rich_text_content&.include?("<lexxy-editor")
|
|
361
|
+
|
|
362
|
+
attributes = aria.map { |key, value| %(aria-#{key}="#{ERB::Util.html_escape(value)}") }.join(" ")
|
|
363
|
+
return @rich_text_content if attributes.empty?
|
|
364
|
+
|
|
365
|
+
@rich_text_content.sub("<lexxy-editor", "<lexxy-editor #{attributes}").html_safe
|
|
366
|
+
end
|
|
367
|
+
|
|
349
368
|
def checkbox_control(html:, aria:, data:)
|
|
350
369
|
render_in_slot(
|
|
351
370
|
Checkbox.new(
|
|
@@ -360,7 +379,7 @@ module NitroKit
|
|
|
360
379
|
disabled: @disabled,
|
|
361
380
|
required: @required,
|
|
362
381
|
control_html: @control_html.merge(html),
|
|
363
|
-
control_aria:
|
|
382
|
+
control_aria: labeled_control_aria(aria),
|
|
364
383
|
control_data: @control_data.merge(data)
|
|
365
384
|
),
|
|
366
385
|
:control
|
|
@@ -381,7 +400,7 @@ module NitroKit
|
|
|
381
400
|
disabled: @disabled,
|
|
382
401
|
required: @required,
|
|
383
402
|
control_html: @control_html.merge(html),
|
|
384
|
-
control_aria:
|
|
403
|
+
control_aria: labeled_control_aria(aria),
|
|
385
404
|
control_data: @control_data.merge(data)
|
|
386
405
|
),
|
|
387
406
|
:control
|
|
@@ -401,7 +420,7 @@ module NitroKit
|
|
|
401
420
|
disabled: @disabled || choice.disabled,
|
|
402
421
|
required: @required,
|
|
403
422
|
control_html: @control_html.merge(html),
|
|
404
|
-
control_aria:
|
|
423
|
+
control_aria: labeled_control_aria(aria),
|
|
405
424
|
control_data: @control_data.merge(data)
|
|
406
425
|
),
|
|
407
426
|
:control
|
|
@@ -420,7 +439,9 @@ module NitroKit
|
|
|
420
439
|
disabled: @disabled,
|
|
421
440
|
required: @required,
|
|
422
441
|
html: @control_html.merge(html),
|
|
423
|
-
|
|
442
|
+
# The fieldset's implicit group role does not support aria-invalid,
|
|
443
|
+
# so the error state stays on the error list it references.
|
|
444
|
+
aria: control_aria(aria).except(:invalid),
|
|
424
445
|
data: @control_data.merge(data)
|
|
425
446
|
),
|
|
426
447
|
:control
|
|
@@ -443,13 +464,15 @@ module NitroKit
|
|
|
443
464
|
).compact
|
|
444
465
|
end
|
|
445
466
|
|
|
446
|
-
def
|
|
467
|
+
def labeled_control_aria(extra)
|
|
447
468
|
attributes = control_aria(extra)
|
|
448
469
|
return attributes unless field_label == false
|
|
449
470
|
return attributes if attributes.keys.any? { |key| %w[label labelledby].include?(key.to_s.delete("_-")) }
|
|
450
471
|
|
|
451
472
|
accessible_name = derived_label || name.to_s.humanize.presence
|
|
452
|
-
|
|
473
|
+
unless accessible_name
|
|
474
|
+
raise ArgumentError, "an unlabeled #{as.to_s.tr("_", " ")} requires a field name or control ARIA label"
|
|
475
|
+
end
|
|
453
476
|
|
|
454
477
|
attributes.merge(label: accessible_name)
|
|
455
478
|
end
|
|
@@ -460,10 +483,11 @@ module NitroKit
|
|
|
460
483
|
end
|
|
461
484
|
|
|
462
485
|
def derived_label
|
|
486
|
+
return @derived_label if defined?(@derived_label)
|
|
463
487
|
return unless @field_name
|
|
464
488
|
|
|
465
489
|
object = form&.object
|
|
466
|
-
if object && object.class.respond_to?(:human_attribute_name)
|
|
490
|
+
@derived_label = if object && object.class.respond_to?(:human_attribute_name)
|
|
467
491
|
object.class.human_attribute_name(@field_name)
|
|
468
492
|
else
|
|
469
493
|
@field_name.humanize
|
|
@@ -66,7 +66,6 @@ module NitroKit
|
|
|
66
66
|
&block
|
|
67
67
|
)
|
|
68
68
|
as = validate_as!(as)
|
|
69
|
-
label = default_label_for(field_name) if label.nil?
|
|
70
69
|
errors ||= errors_for(field_name)
|
|
71
70
|
control_html = merge_control_boundary(:html, control_html, html)
|
|
72
71
|
control_aria = merge_control_boundary(:aria, control_aria, aria)
|
|
@@ -31,10 +31,10 @@ module NitroKit
|
|
|
31
31
|
component: :progressive_image,
|
|
32
32
|
attributes: {
|
|
33
33
|
id:,
|
|
34
|
-
aria: { busy: @attached ? true : nil },
|
|
35
34
|
data: {
|
|
36
35
|
state: @attached ? "loading" : "empty",
|
|
37
|
-
controller: @attached ? "nk--progressive-image" : nil
|
|
36
|
+
controller: @attached ? "nk--progressive-image" : nil,
|
|
37
|
+
action: @attached ? "turbo:before-cache@document->nk--progressive-image#prepareForCache" : nil
|
|
38
38
|
}.compact
|
|
39
39
|
},
|
|
40
40
|
html:,
|
|
@@ -67,12 +67,15 @@ module NitroKit
|
|
|
67
67
|
render_in_slot(Label.new(for: id), :label) do
|
|
68
68
|
render_control
|
|
69
69
|
span(**slot_attributes(:indicator), aria: { hidden: "true" })
|
|
70
|
-
span(**slot_attributes(:
|
|
71
|
-
span(**slot_attributes(:label_text)) { text_or_block(label, &block) }
|
|
72
|
-
span(**slot_attributes(:description, attributes: { id: description_id })) { plain(description) } if description
|
|
73
|
-
end
|
|
70
|
+
span(**slot_attributes(:label_text)) { text_or_block(label, &block) }
|
|
74
71
|
end
|
|
75
72
|
end
|
|
73
|
+
|
|
74
|
+
# The description stays outside the label so aria-describedby carries it
|
|
75
|
+
# instead of it joining the control's accessible name.
|
|
76
|
+
if description
|
|
77
|
+
span(**slot_attributes(:description, attributes: { id: description_id })) { plain(description) }
|
|
78
|
+
end
|
|
76
79
|
end
|
|
77
80
|
end
|
|
78
81
|
|
|
@@ -99,30 +99,5 @@ module NitroKit
|
|
|
99
99
|
def selected?(choice)
|
|
100
100
|
!value.nil? && value.to_s == choice.value.to_s
|
|
101
101
|
end
|
|
102
|
-
|
|
103
|
-
def deterministic_id(name)
|
|
104
|
-
derived = name.to_s.gsub(/[^a-zA-Z0-9_-]+/, "_").gsub(/\A_+|_+\z/, "")
|
|
105
|
-
return derived unless derived.empty?
|
|
106
|
-
|
|
107
|
-
raise ArgumentError, "name #{name.inspect} cannot derive an id; pass id:"
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def choice_id(index)
|
|
111
|
-
"#{id}-#{index}" if id
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def validate_text!(name, text)
|
|
115
|
-
return text if text.is_a?(String) && !text.strip.empty?
|
|
116
|
-
|
|
117
|
-
raise ArgumentError, "#{name} must be a non-blank String"
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def validate_unique_choices!
|
|
121
|
-
duplicate_value = options.group_by { |choice| choice.value.to_s }.find { |_value, matches| matches.many? }
|
|
122
|
-
raise ArgumentError, "choice values must be unique" if duplicate_value
|
|
123
|
-
|
|
124
|
-
ids = options.each_with_index.filter_map { |choice, index| choice.id || choice_id(index) }
|
|
125
|
-
raise ArgumentError, "choice ids must be unique" unless ids.uniq.length == ids.length
|
|
126
|
-
end
|
|
127
102
|
end
|
|
128
103
|
end
|
|
@@ -26,7 +26,7 @@ module NitroKit
|
|
|
26
26
|
@option_tags = validate_option_tags!(option_tags)
|
|
27
27
|
@value = value
|
|
28
28
|
@include_blank = validate_blank_option!(include_blank)
|
|
29
|
-
@prompt =
|
|
29
|
+
@prompt = validate_prompt!(prompt)
|
|
30
30
|
if @include_blank && @prompt
|
|
31
31
|
raise ArgumentError, "include_blank: and prompt: are mutually exclusive"
|
|
32
32
|
end
|
|
@@ -151,11 +151,11 @@ module NitroKit
|
|
|
151
151
|
raise ArgumentError, "include_blank must be true, false, nil, or a String"
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
-
def
|
|
154
|
+
def validate_prompt!(value)
|
|
155
155
|
return if value.nil? || value == false
|
|
156
156
|
return value if value.is_a?(String) && !value.strip.empty?
|
|
157
157
|
|
|
158
|
-
raise ArgumentError, "
|
|
158
|
+
raise ArgumentError, "prompt must be a non-blank String, false, or nil"
|
|
159
159
|
end
|
|
160
160
|
end
|
|
161
161
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module NitroKit
|
|
4
4
|
class SettingsLayout < Component
|
|
5
|
-
Item = ::Data.define(:text, :href, :icon, :current)
|
|
5
|
+
Item = ::Data.define(:text, :href, :icon, :current, :html, :aria, :data, :css_class)
|
|
6
6
|
|
|
7
7
|
def initialize(id: nil, html: {}, aria: {}, data: {}, desperately_need_a_class: nil)
|
|
8
8
|
@label = nil
|
|
@@ -42,7 +42,16 @@ module NitroKit
|
|
|
42
42
|
nil
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
def item(
|
|
45
|
+
def item(
|
|
46
|
+
text,
|
|
47
|
+
href:,
|
|
48
|
+
icon: nil,
|
|
49
|
+
current: false,
|
|
50
|
+
html: {},
|
|
51
|
+
aria: {},
|
|
52
|
+
data: {},
|
|
53
|
+
desperately_need_a_class: nil
|
|
54
|
+
)
|
|
46
55
|
ensure_phase!(:navigation, :item)
|
|
47
56
|
text = validate_text!(:text, text)
|
|
48
57
|
href = validate_text!(:href, href)
|
|
@@ -54,7 +63,7 @@ module NitroKit
|
|
|
54
63
|
end
|
|
55
64
|
|
|
56
65
|
@current_item = true if current
|
|
57
|
-
@items << Item.new(text:, href:, icon:, current:)
|
|
66
|
+
@items << Item.new(text:, href:, icon:, current:, html:, aria:, data:, css_class: desperately_need_a_class)
|
|
58
67
|
nil
|
|
59
68
|
end
|
|
60
69
|
|
|
@@ -107,7 +116,11 @@ module NitroKit
|
|
|
107
116
|
href: item.href,
|
|
108
117
|
aria: { current: item.current ? "page" : nil },
|
|
109
118
|
data: { state: item.current ? "current" : "default" }
|
|
110
|
-
}
|
|
119
|
+
},
|
|
120
|
+
html: item.html,
|
|
121
|
+
aria: item.aria,
|
|
122
|
+
data: item.data,
|
|
123
|
+
desperately_need_a_class: item.css_class
|
|
111
124
|
)
|
|
112
125
|
) do
|
|
113
126
|
render_in_slot(item.icon, :item_icon) if item.icon
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module NitroKit
|
|
4
|
-
class
|
|
4
|
+
class SettingsSection < Component
|
|
5
5
|
Child = Data.define(:component, :content)
|
|
6
|
+
TITLE_LEVELS = (1..6).freeze
|
|
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: {},
|
|
13
15
|
data: {},
|
|
14
16
|
desperately_need_a_class: nil
|
|
15
17
|
)
|
|
18
|
+
@title_level = validate_choice!(:level, level, TITLE_LEVELS)
|
|
16
19
|
@title_content = content_from_keyword(:title, title)
|
|
17
20
|
@description_content = content_from_keyword(:description, description)
|
|
18
21
|
@status = nil
|
|
19
22
|
@form = nil
|
|
20
|
-
@title_id = "#{id || "nk-
|
|
23
|
+
@title_id = "#{id || "nk-settings-section-#{SecureRandom.hex(4)}"}-title"
|
|
21
24
|
|
|
22
25
|
super(
|
|
23
|
-
component: :
|
|
26
|
+
component: :settings_section,
|
|
24
27
|
attributes: { id:, aria: { labelledby: @title_id } }.compact,
|
|
25
28
|
html:,
|
|
26
29
|
aria:,
|
|
@@ -31,12 +34,15 @@ module NitroKit
|
|
|
31
34
|
|
|
32
35
|
def view_template
|
|
33
36
|
yield self if block_given?
|
|
34
|
-
require_content!("
|
|
35
|
-
raise ArgumentError, "
|
|
37
|
+
require_content!("SettingsSection", :title, @title_content)
|
|
38
|
+
raise ArgumentError, "SettingsSection requires exactly one form" unless @form
|
|
36
39
|
|
|
37
40
|
section(**root_attributes) do
|
|
38
41
|
header(**slot_attributes(:header)) do
|
|
39
|
-
|
|
42
|
+
public_send(
|
|
43
|
+
:"h#{@title_level}",
|
|
44
|
+
**slot_attributes(:title, attributes: { id: @title_id })
|
|
45
|
+
) do
|
|
40
46
|
render_deferred_content(@title_content)
|
|
41
47
|
end
|
|
42
48
|
if @description_content
|
|
@@ -48,7 +54,8 @@ module NitroKit
|
|
|
48
54
|
end
|
|
49
55
|
end
|
|
50
56
|
|
|
51
|
-
def title(text = nil, &block)
|
|
57
|
+
def title(text = nil, level: nil, &block)
|
|
58
|
+
@title_level = validate_choice!(:level, level, TITLE_LEVELS) if level
|
|
52
59
|
@title_content = declare_content(:title, @title_content, text, &block)
|
|
53
60
|
nil
|
|
54
61
|
end
|
|
@@ -60,17 +67,17 @@ module NitroKit
|
|
|
60
67
|
|
|
61
68
|
def status(component, &content)
|
|
62
69
|
unless component.is_a?(NitroKit::Alert)
|
|
63
|
-
raise ArgumentError, "
|
|
70
|
+
raise ArgumentError, "SettingsSection status must be a NitroKit::Alert"
|
|
64
71
|
end
|
|
65
|
-
raise ArgumentError, "
|
|
72
|
+
raise ArgumentError, "SettingsSection accepts at most one status" if @status
|
|
66
73
|
|
|
67
74
|
@status = Child.new(component:, content:)
|
|
68
75
|
nil
|
|
69
76
|
end
|
|
70
77
|
|
|
71
78
|
def form(&content)
|
|
72
|
-
raise ArgumentError, "
|
|
73
|
-
raise ArgumentError, "
|
|
79
|
+
raise ArgumentError, "SettingsSection form requires a block" unless content
|
|
80
|
+
raise ArgumentError, "SettingsSection accepts exactly one form" if @form
|
|
74
81
|
|
|
75
82
|
@form = content
|
|
76
83
|
nil
|