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
|
@@ -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
|
|
@@ -77,20 +77,7 @@ module NitroKit
|
|
|
77
77
|
def render_toggle_icon
|
|
78
78
|
return if @multiple
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
**slot_attributes(
|
|
82
|
-
:icon,
|
|
83
|
-
attributes: {
|
|
84
|
-
viewbox: "0 0 20 20",
|
|
85
|
-
fill: "none",
|
|
86
|
-
stroke: "currentColor",
|
|
87
|
-
stroke_width: 1.5,
|
|
88
|
-
aria: { hidden: true }
|
|
89
|
-
}
|
|
90
|
-
)
|
|
91
|
-
) do |icon|
|
|
92
|
-
icon.path(d: "m6.5 8 3.5 3.5L13.5 8", stroke_linecap: "round", stroke_linejoin: "round")
|
|
93
|
-
end
|
|
80
|
+
render_in_slot(Icon.new(:chevron_down, size: :sm), :icon)
|
|
94
81
|
end
|
|
95
82
|
|
|
96
83
|
def control_attributes
|
|
@@ -151,11 +138,11 @@ module NitroKit
|
|
|
151
138
|
raise ArgumentError, "include_blank must be true, false, nil, or a String"
|
|
152
139
|
end
|
|
153
140
|
|
|
154
|
-
def
|
|
141
|
+
def validate_prompt!(value)
|
|
155
142
|
return if value.nil? || value == false
|
|
156
143
|
return value if value.is_a?(String) && !value.strip.empty?
|
|
157
144
|
|
|
158
|
-
raise ArgumentError, "
|
|
145
|
+
raise ArgumentError, "prompt must be a non-blank String, false, or nil"
|
|
159
146
|
end
|
|
160
147
|
end
|
|
161
148
|
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
|
|
@@ -5,10 +5,10 @@ module NitroKit
|
|
|
5
5
|
SIDES = %i[left right].freeze
|
|
6
6
|
SIZES = %i[sm md lg].freeze
|
|
7
7
|
|
|
8
|
-
Trigger = Data.define(:text, :variant, :size, :icon, :icon_end, :label, :disabled, :html, :aria, :data, :content)
|
|
8
|
+
Trigger = Data.define(:text, :variant, :size, :icon, :icon_end, :label, :disabled, :html, :aria, :data, :css_class, :content)
|
|
9
9
|
private_constant :Trigger
|
|
10
10
|
|
|
11
|
-
Panel = Data.define(:title, :description, :content)
|
|
11
|
+
Panel = Data.define(:title, :description, :html, :aria, :data, :css_class, :content)
|
|
12
12
|
private_constant :Panel
|
|
13
13
|
|
|
14
14
|
def initialize(
|
|
@@ -39,6 +39,10 @@ module NitroKit
|
|
|
39
39
|
id:,
|
|
40
40
|
data: {
|
|
41
41
|
controller: "nk--dialog",
|
|
42
|
+
action: [
|
|
43
|
+
"click->nk--dialog#invoke",
|
|
44
|
+
"turbo:before-cache@document->nk--dialog#closeForCache"
|
|
45
|
+
].join(" "),
|
|
42
46
|
side: @side,
|
|
43
47
|
nk__dialog_dismissible_value: true
|
|
44
48
|
}
|
|
@@ -73,6 +77,7 @@ module NitroKit
|
|
|
73
77
|
html: {},
|
|
74
78
|
aria: {},
|
|
75
79
|
data: {},
|
|
80
|
+
desperately_need_a_class: nil,
|
|
76
81
|
&content
|
|
77
82
|
)
|
|
78
83
|
ensure_collecting!
|
|
@@ -89,12 +94,21 @@ module NitroKit
|
|
|
89
94
|
html:,
|
|
90
95
|
aria:,
|
|
91
96
|
data:,
|
|
97
|
+
css_class: desperately_need_a_class,
|
|
92
98
|
content:
|
|
93
99
|
)
|
|
94
100
|
nil
|
|
95
101
|
end
|
|
96
102
|
|
|
97
|
-
def panel(
|
|
103
|
+
def panel(
|
|
104
|
+
title:,
|
|
105
|
+
description: nil,
|
|
106
|
+
html: {},
|
|
107
|
+
aria: {},
|
|
108
|
+
data: {},
|
|
109
|
+
desperately_need_a_class: nil,
|
|
110
|
+
&content
|
|
111
|
+
)
|
|
98
112
|
ensure_collecting!
|
|
99
113
|
raise ArgumentError, "Sheet accepts exactly one panel" if @panel
|
|
100
114
|
unless title.is_a?(String) && title.present?
|
|
@@ -104,7 +118,7 @@ module NitroKit
|
|
|
104
118
|
raise ArgumentError, "Sheet description: must be nil or a non-blank String"
|
|
105
119
|
end
|
|
106
120
|
|
|
107
|
-
@panel = Panel.new(title:, description:, content:)
|
|
121
|
+
@panel = Panel.new(title:, description:, html:, aria:, data:, css_class: desperately_need_a_class, content:)
|
|
108
122
|
nil
|
|
109
123
|
end
|
|
110
124
|
|
|
@@ -130,9 +144,10 @@ module NitroKit
|
|
|
130
144
|
icon_end: @trigger.icon_end,
|
|
131
145
|
label: @trigger.label,
|
|
132
146
|
disabled: @trigger.disabled,
|
|
133
|
-
html: command_attributes(@trigger.html, "show-modal", disabled: @trigger.disabled),
|
|
147
|
+
html: command_attributes(@trigger.html, "show-modal", commandfor: panel_id, disabled: @trigger.disabled),
|
|
134
148
|
aria: @trigger.aria.merge(haspopup: "dialog"),
|
|
135
|
-
data: @trigger.data
|
|
149
|
+
data: command_data(@trigger.data, "show-modal", disabled: @trigger.disabled),
|
|
150
|
+
desperately_need_a_class: @trigger.css_class
|
|
136
151
|
)
|
|
137
152
|
|
|
138
153
|
render_in_slot(button, :trigger, &@trigger.content)
|
|
@@ -151,9 +166,13 @@ module NitroKit
|
|
|
151
166
|
}.compact,
|
|
152
167
|
data: {
|
|
153
168
|
nk__dialog_target: "panel",
|
|
154
|
-
action: "click->nk--dialog#dismiss cancel->nk--dialog#cancel"
|
|
169
|
+
action: "click->nk--dialog#dismiss cancel->nk--dialog#cancel close->nk--dialog#restoreFocus"
|
|
155
170
|
}
|
|
156
|
-
}
|
|
171
|
+
},
|
|
172
|
+
html: @panel.html,
|
|
173
|
+
aria: @panel.aria,
|
|
174
|
+
data: @panel.data,
|
|
175
|
+
desperately_need_a_class: @panel.css_class
|
|
157
176
|
)
|
|
158
177
|
) do
|
|
159
178
|
render_close
|
|
@@ -172,23 +191,13 @@ module NitroKit
|
|
|
172
191
|
label: @close_label,
|
|
173
192
|
variant: :ghost,
|
|
174
193
|
size: :sm,
|
|
175
|
-
html: command_attributes({}, "close")
|
|
194
|
+
html: command_attributes({}, "close", commandfor: panel_id),
|
|
195
|
+
data: command_data({}, "close")
|
|
176
196
|
),
|
|
177
197
|
:close
|
|
178
198
|
)
|
|
179
199
|
end
|
|
180
200
|
|
|
181
|
-
def command_attributes(html, command, disabled: false)
|
|
182
|
-
unless html.is_a?(Hash)
|
|
183
|
-
raise ArgumentError, "html must be a Hash"
|
|
184
|
-
end
|
|
185
|
-
if html.keys.any? { |key| %w[command commandfor].include?(key.to_s.downcase) }
|
|
186
|
-
raise ArgumentError, "command and commandfor are owned by Sheet"
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
disabled ? html : html.merge(command:, commandfor: panel_id)
|
|
190
|
-
end
|
|
191
|
-
|
|
192
201
|
def panel_id = "#{id}-panel"
|
|
193
202
|
def title_id = "#{id}-title"
|
|
194
203
|
def description_id = "#{id}-description"
|
|
@@ -27,8 +27,8 @@ module NitroKit
|
|
|
27
27
|
)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
def view_template
|
|
31
|
-
|
|
30
|
+
def view_template(&block)
|
|
31
|
+
collect_declarations(&block)
|
|
32
32
|
raise ArgumentError, "StatGrid requires at least one stat" if @stats.empty?
|
|
33
33
|
|
|
34
34
|
div(**root_attributes) do
|
|
@@ -41,6 +41,7 @@ module NitroKit
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def stat(key:, label:, value:, detail: nil)
|
|
44
|
+
ensure_collecting!
|
|
44
45
|
key = normalize_identity(key, name: "stat key")
|
|
45
46
|
raise ArgumentError, "StatGrid stat keys must be unique: #{key.inspect}" if @stats.any? { |stat| stat.key == key }
|
|
46
47
|
|
|
@@ -57,6 +58,21 @@ module NitroKit
|
|
|
57
58
|
|
|
58
59
|
private
|
|
59
60
|
|
|
61
|
+
def collect_declarations
|
|
62
|
+
return unless block_given?
|
|
63
|
+
|
|
64
|
+
@collecting = true
|
|
65
|
+
yield(self)
|
|
66
|
+
ensure
|
|
67
|
+
@collecting = false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ensure_collecting!
|
|
71
|
+
return if @collecting
|
|
72
|
+
|
|
73
|
+
raise ArgumentError, "StatGrid stats must be declared inside the render block"
|
|
74
|
+
end
|
|
75
|
+
|
|
60
76
|
def render_stat(stat)
|
|
61
77
|
div(**slot_attributes(:stat, attributes: { data: { key: stat.key } })) do
|
|
62
78
|
dt(**slot_attributes(:label)) { plain(stat.label) }
|
|
@@ -28,6 +28,9 @@ module NitroKit
|
|
|
28
28
|
desperately_need_a_class: nil
|
|
29
29
|
)
|
|
30
30
|
@sort = sort.nil? ? nil : normalize_sort_key(sort, name: "sort")
|
|
31
|
+
unless direction.nil? || direction.is_a?(Symbol) || direction.is_a?(String)
|
|
32
|
+
raise ArgumentError, "Table direction must be a Symbol or String"
|
|
33
|
+
end
|
|
31
34
|
@direction = direction.nil? ? nil : validate_choice!(:direction, direction.to_sym, DIRECTIONS)
|
|
32
35
|
unless @sort.nil? == @direction.nil?
|
|
33
36
|
raise ArgumentError, "Table sort: and direction: must both be set or both be nil"
|
|
@@ -49,6 +52,7 @@ module NitroKit
|
|
|
49
52
|
data: table_data
|
|
50
53
|
)
|
|
51
54
|
@caption = nil
|
|
55
|
+
@caption_id = "#{id || "nk-table-#{SecureRandom.hex(4)}"}-caption"
|
|
52
56
|
@head = nil
|
|
53
57
|
@bodies = []
|
|
54
58
|
@sort_keys = []
|
|
@@ -61,7 +65,7 @@ module NitroKit
|
|
|
61
65
|
yield self if block_given?
|
|
62
66
|
@collecting_sections = false
|
|
63
67
|
|
|
64
|
-
div(**
|
|
68
|
+
div(**scroll_region_attributes) do
|
|
65
69
|
table(**@table_attributes) do
|
|
66
70
|
render_caption if @caption
|
|
67
71
|
render_section(:head, @head, :html_thead) if @head
|
|
@@ -141,6 +145,7 @@ module NitroKit
|
|
|
141
145
|
ensure_rendering_row!(:th)
|
|
142
146
|
raise ArgumentError, "Table th accepts text or a block, not both" if !text.nil? && block
|
|
143
147
|
raise ArgumentError, "Table th href: requires sort:" if sort.nil? && !href.nil?
|
|
148
|
+
raise ArgumentError, "Table th sort: must be declared inside thead" if !sort.nil? && @rendering_section != :head
|
|
144
149
|
alignment = validate_choice!(:align, align, ALIGNMENTS)
|
|
145
150
|
scope = validate_choice!(:scope, scope, SCOPES)
|
|
146
151
|
key = sort.nil? ? nil : declare_sort_header!(sort, href:)
|
|
@@ -235,10 +240,24 @@ module NitroKit
|
|
|
235
240
|
raise ArgumentError, "Table #{name} must be a Symbol or non-blank String"
|
|
236
241
|
end
|
|
237
242
|
|
|
243
|
+
# The wrapper scrolls horizontally, so keyboard users need it focusable and
|
|
244
|
+
# named. The caption is the only Nitro-owned name source, so the region is
|
|
245
|
+
# only exposed when one exists.
|
|
246
|
+
def scroll_region_attributes
|
|
247
|
+
return root_attributes unless @caption
|
|
248
|
+
|
|
249
|
+
root_attributes.merge(
|
|
250
|
+
tabindex: "0",
|
|
251
|
+
role: "region",
|
|
252
|
+
aria: root_attributes.fetch(:aria, {}).merge(labelledby: @caption_id)
|
|
253
|
+
)
|
|
254
|
+
end
|
|
255
|
+
|
|
238
256
|
def render_caption
|
|
239
257
|
html_caption(
|
|
240
258
|
**slot_attributes(
|
|
241
259
|
:caption,
|
|
260
|
+
attributes: { id: @caption_id },
|
|
242
261
|
html: @caption.html,
|
|
243
262
|
aria: @caption.aria,
|
|
244
263
|
data: @caption.data,
|
|
@@ -258,10 +277,10 @@ module NitroKit
|
|
|
258
277
|
desperately_need_a_class: section.css_class
|
|
259
278
|
)
|
|
260
279
|
) do
|
|
261
|
-
@rendering_section =
|
|
280
|
+
@rendering_section = slot
|
|
262
281
|
render(section.content)
|
|
263
282
|
ensure
|
|
264
|
-
@rendering_section =
|
|
283
|
+
@rendering_section = nil
|
|
265
284
|
end
|
|
266
285
|
end
|
|
267
286
|
|
|
@@ -17,7 +17,7 @@ module NitroKit
|
|
|
17
17
|
data: {},
|
|
18
18
|
desperately_need_a_class: nil
|
|
19
19
|
)
|
|
20
|
-
@identifier =
|
|
20
|
+
@identifier = validate_id!("Tabs id", id)
|
|
21
21
|
@default = default.nil? ? nil : normalize_identity(default, name: "default tab key")
|
|
22
22
|
@label = validate_label!(label)
|
|
23
23
|
@orientation = validate_choice!(:orientation, orientation, ORIENTATIONS)
|
|
@@ -140,6 +140,7 @@ module NitroKit
|
|
|
140
140
|
data: {
|
|
141
141
|
key: tab.key,
|
|
142
142
|
state: active ? "active" : "inactive",
|
|
143
|
+
action: "beforematch->nk--tabs#reveal",
|
|
143
144
|
nk__tabs_target: "panel"
|
|
144
145
|
}
|
|
145
146
|
}
|
|
@@ -193,17 +194,5 @@ module NitroKit
|
|
|
193
194
|
|
|
194
195
|
raise ArgumentError, "Tabs labels must be non-blank Strings"
|
|
195
196
|
end
|
|
196
|
-
|
|
197
|
-
def validate_boolean!(name, value)
|
|
198
|
-
return value if value == true || value == false
|
|
199
|
-
|
|
200
|
-
raise ArgumentError, "Tabs #{name} must be true or false"
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def component_id(value)
|
|
204
|
-
return value if value.is_a?(String) && value.present? && !value.match?(/\s/)
|
|
205
|
-
|
|
206
|
-
raise ArgumentError, "Tabs id must be a non-blank String without whitespace"
|
|
207
|
-
end
|
|
208
197
|
end
|
|
209
198
|
end
|
|
@@ -32,6 +32,7 @@ module NitroKit
|
|
|
32
32
|
attributes: {
|
|
33
33
|
id:,
|
|
34
34
|
role: ASSERTIVE_VARIANTS.include?(@variant) ? "alert" : "status",
|
|
35
|
+
aria: { atomic: true },
|
|
35
36
|
data: {
|
|
36
37
|
state: "open",
|
|
37
38
|
turbo_temporary: true,
|
|
@@ -47,7 +48,7 @@ module NitroKit
|
|
|
47
48
|
}
|
|
48
49
|
},
|
|
49
50
|
html:,
|
|
50
|
-
aria
|
|
51
|
+
aria:,
|
|
51
52
|
data:,
|
|
52
53
|
variant:,
|
|
53
54
|
desperately_need_a_class:
|
|
@@ -175,6 +176,7 @@ module NitroKit
|
|
|
175
176
|
attributes: {
|
|
176
177
|
id:,
|
|
177
178
|
role: "region",
|
|
179
|
+
aria: { label: },
|
|
178
180
|
data: {
|
|
179
181
|
controller: "nk--toast",
|
|
180
182
|
action: "turbo:before-cache@document->nk--toast#teardown",
|
|
@@ -182,7 +184,7 @@ module NitroKit
|
|
|
182
184
|
}
|
|
183
185
|
},
|
|
184
186
|
html:,
|
|
185
|
-
aria
|
|
187
|
+
aria:,
|
|
186
188
|
data:,
|
|
187
189
|
desperately_need_a_class:
|
|
188
190
|
)
|
|
@@ -30,6 +30,7 @@ module NitroKit
|
|
|
30
30
|
|
|
31
31
|
def leading(html: {}, aria: {}, data: {}, desperately_need_a_class: nil, &block)
|
|
32
32
|
raise ArgumentError, "Toolbar accepts at most one leading region" if @leading_region
|
|
33
|
+
raise ArgumentError, "Toolbar leading region requires a block" unless block
|
|
33
34
|
@leading_region = Region.new(
|
|
34
35
|
content: block,
|
|
35
36
|
html:,
|
|
@@ -42,6 +43,7 @@ module NitroKit
|
|
|
42
43
|
|
|
43
44
|
def trailing(html: {}, aria: {}, data: {}, desperately_need_a_class: nil, &block)
|
|
44
45
|
raise ArgumentError, "Toolbar accepts at most one trailing region" if @trailing_region
|
|
46
|
+
raise ArgumentError, "Toolbar trailing region requires a block" unless block
|
|
45
47
|
@trailing_region = Region.new(
|
|
46
48
|
content: block,
|
|
47
49
|
html:,
|
|
@@ -16,7 +16,6 @@ module NitroKit
|
|
|
16
16
|
:icon,
|
|
17
17
|
:icon_end,
|
|
18
18
|
:label,
|
|
19
|
-
:disabled,
|
|
20
19
|
:target,
|
|
21
20
|
:rel,
|
|
22
21
|
:download,
|
|
@@ -48,7 +47,7 @@ module NitroKit
|
|
|
48
47
|
controller: "nk--tooltip",
|
|
49
48
|
placement: @placement,
|
|
50
49
|
dismissed: nil,
|
|
51
|
-
action: "pointerleave->nk--tooltip#resetIfUninterested focusout->nk--tooltip#resetIfUninterested"
|
|
50
|
+
action: "keydown.esc@document->nk--tooltip#dismiss pointerleave->nk--tooltip#resetIfUninterested focusout->nk--tooltip#resetIfUninterested"
|
|
52
51
|
}
|
|
53
52
|
},
|
|
54
53
|
html:,
|
|
@@ -81,8 +80,8 @@ module NitroKit
|
|
|
81
80
|
text = nil,
|
|
82
81
|
as: Button,
|
|
83
82
|
href: nil,
|
|
84
|
-
variant:
|
|
85
|
-
size:
|
|
83
|
+
variant: nil,
|
|
84
|
+
size: nil,
|
|
86
85
|
icon: nil,
|
|
87
86
|
icon_end: nil,
|
|
88
87
|
label: nil,
|
|
@@ -106,7 +105,7 @@ module NitroKit
|
|
|
106
105
|
if as == :custom && content&.arity != 1
|
|
107
106
|
raise ArgumentError, "Tooltip custom trigger block must accept trigger attributes"
|
|
108
107
|
end
|
|
109
|
-
if as != Button && [ href, icon, icon_end, label, target, rel, download ].any?
|
|
108
|
+
if as != Button && [ href, variant, size, icon, icon_end, label, target, rel, download ].any?
|
|
110
109
|
raise ArgumentError, "Tooltip #{as.inspect} trigger does not accept Button options"
|
|
111
110
|
end
|
|
112
111
|
|
|
@@ -114,12 +113,11 @@ module NitroKit
|
|
|
114
113
|
as:,
|
|
115
114
|
text:,
|
|
116
115
|
href:,
|
|
117
|
-
variant
|
|
118
|
-
size
|
|
116
|
+
variant: variant || :default,
|
|
117
|
+
size: size || :md,
|
|
119
118
|
icon:,
|
|
120
119
|
icon_end:,
|
|
121
120
|
label:,
|
|
122
|
-
disabled: false,
|
|
123
121
|
target:,
|
|
124
122
|
rel:,
|
|
125
123
|
download:,
|
|
@@ -156,7 +154,6 @@ module NitroKit
|
|
|
156
154
|
icon: @trigger.icon,
|
|
157
155
|
icon_end: @trigger.icon_end,
|
|
158
156
|
label: @trigger.label,
|
|
159
|
-
disabled: @trigger.disabled,
|
|
160
157
|
target: @trigger.target,
|
|
161
158
|
rel: @trigger.rel,
|
|
162
159
|
download: @trigger.download,
|
|
@@ -192,7 +189,7 @@ module NitroKit
|
|
|
192
189
|
end
|
|
193
190
|
|
|
194
191
|
def render_custom_trigger
|
|
195
|
-
|
|
192
|
+
div(**slot_attributes(:trigger)) do
|
|
196
193
|
raw(safe(capture(trigger_attributes, &@trigger.content)))
|
|
197
194
|
end
|
|
198
195
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
|
2
2
|
|
|
3
|
-
const narrowViewport = "(
|
|
3
|
+
const narrowViewport = "(width < 48rem)";
|
|
4
4
|
|
|
5
5
|
export default class extends Controller {
|
|
6
6
|
static targets = ["dialog", "navigation", "sidebar", "trigger"];
|
|
@@ -168,8 +168,8 @@ export default class extends Controller {
|
|
|
168
168
|
const focusTarget = this.previouslyFocused?.isConnected
|
|
169
169
|
? this.previouslyFocused
|
|
170
170
|
: this.hasTriggerTarget
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
? this.triggerTarget
|
|
172
|
+
: null;
|
|
173
173
|
|
|
174
174
|
this.restoreNavigation();
|
|
175
175
|
this.setState(false);
|
|
@@ -48,6 +48,12 @@ export default class extends Controller {
|
|
|
48
48
|
input.checked = input.value === preference;
|
|
49
49
|
} else if (input.localName === "select") {
|
|
50
50
|
input.value = preference;
|
|
51
|
+
} else if (input.dataset.appearancePreference) {
|
|
52
|
+
input.setAttribute("role", "menuitemradio");
|
|
53
|
+
input.setAttribute(
|
|
54
|
+
"aria-checked",
|
|
55
|
+
input.dataset.appearancePreference === preference,
|
|
56
|
+
);
|
|
51
57
|
}
|
|
52
58
|
});
|
|
53
59
|
this.updateTriggerGlyph(preference);
|