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
|
@@ -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
|
|
@@ -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
|
|
@@ -44,7 +44,7 @@ module NitroKit
|
|
|
44
44
|
name:,
|
|
45
45
|
label: I18n.t("nitro_kit.dropzone.label"),
|
|
46
46
|
description: nil,
|
|
47
|
-
presentation: :
|
|
47
|
+
presentation: :minimal,
|
|
48
48
|
direct_upload: true,
|
|
49
49
|
multiple: false,
|
|
50
50
|
accept: nil,
|
|
@@ -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)
|
|
@@ -116,7 +115,7 @@ module NitroKit
|
|
|
116
115
|
id: nil,
|
|
117
116
|
label: I18n.t("nitro_kit.dropzone.label"),
|
|
118
117
|
description: nil,
|
|
119
|
-
presentation: :
|
|
118
|
+
presentation: :minimal,
|
|
120
119
|
direct_upload: true,
|
|
121
120
|
multiple: false,
|
|
122
121
|
accept: nil,
|
|
@@ -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:,
|