nitro_kit 2.0.0.alpha.6 → 2.0.0.beta.2
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 +68 -1
- data/README.md +16 -2
- data/STYLE_GUIDE.md +3 -1
- data/app/assets/stylesheets/nitro_kit.css +519 -35
- data/app/components/nitro_kit/app_shell.rb +41 -5
- data/app/components/nitro_kit/avatar.rb +21 -2
- data/app/components/nitro_kit/card.rb +28 -1
- data/app/javascript/controllers/nk/app_shell_controller.js +64 -2
- data/config/locales/en.yml +1 -0
- data/docs/agent_native_spec.md +5 -4
- data/docs/component_contracts.md +93 -5
- data/docs/customization.md +2 -2
- data/docs/eject.md +86 -0
- data/docs/migration_1_to_2.md +1 -1
- data/docs/patterns/application_foundation.md +22 -0
- data/docs/patterns/inset_workspace.md +9 -1
- data/docs/rails_integration.md +1 -1
- data/lib/generators/nitro_kit/eject_generator.rb +26 -0
- data/lib/nitro_kit/ejection.rb +136 -0
- data/lib/nitro_kit/version.rb +1 -1
- data/src/stylesheets/nitro_kit/components/app_navigation.css +4 -1
- data/src/stylesheets/nitro_kit/components/app_shell.css +209 -0
- data/src/stylesheets/nitro_kit/components/avatar.css +10 -0
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +9 -5
- data/src/stylesheets/nitro_kit/components/card.css +206 -15
- data/src/stylesheets/nitro_kit/components/danger_zone.css +1 -1
- data/src/stylesheets/nitro_kit/components/dropzone.css +15 -0
- data/src/stylesheets/nitro_kit/components/empty_state.css +3 -3
- data/src/stylesheets/nitro_kit/components/field_group.css +6 -2
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +3 -0
- data/src/stylesheets/nitro_kit/components/progressive_image.css +2 -0
- data/src/stylesheets/nitro_kit/components/settings_layout.css +2 -0
- data/src/stylesheets/nitro_kit/components/stat_grid.css +4 -4
- data/src/stylesheets/nitro_kit/components/toolbar.css +12 -0
- data/src/stylesheets/nitro_kit/components/tooltip.css +15 -1
- data/src/stylesheets/nitro_kit/components/typeset.css +4 -0
- data/src/stylesheets/nitro_kit/reset.css +11 -1
- data/src/stylesheets/nitro_kit/tokens.css +3 -2
- metadata +5 -2
|
@@ -5,6 +5,7 @@ module NitroKit
|
|
|
5
5
|
alias_method :html_main, :main
|
|
6
6
|
|
|
7
7
|
LAYOUTS = %i[sidebar topbar].freeze
|
|
8
|
+
SIDEBAR_STATES = %i[expanded collapsed].freeze
|
|
8
9
|
REGIONS = %i[brand navigation topbar main].freeze
|
|
9
10
|
REQUIRED_REGIONS = %i[navigation main].freeze
|
|
10
11
|
private_constant :REQUIRED_REGIONS
|
|
@@ -12,6 +13,9 @@ module NitroKit
|
|
|
12
13
|
def initialize(
|
|
13
14
|
id:,
|
|
14
15
|
layout: :sidebar,
|
|
16
|
+
collapsible: false,
|
|
17
|
+
sidebar: :expanded,
|
|
18
|
+
sidebar_toggle_label: I18n.t("nitro_kit.app_shell.pin_sidebar"),
|
|
15
19
|
skip_link_label: I18n.t("nitro_kit.app_shell.skip_link"),
|
|
16
20
|
open_navigation_label: I18n.t("nitro_kit.app_shell.open_navigation"),
|
|
17
21
|
close_navigation_label: I18n.t("nitro_kit.app_shell.close_navigation"),
|
|
@@ -23,6 +27,12 @@ module NitroKit
|
|
|
23
27
|
)
|
|
24
28
|
@identifier = validate_id!("AppShell id", id)
|
|
25
29
|
@layout = validate_choice!(:layout, layout, LAYOUTS)
|
|
30
|
+
@collapsible = validate_boolean!(:collapsible, collapsible)
|
|
31
|
+
@sidebar = validate_choice!(:sidebar, sidebar, SIDEBAR_STATES)
|
|
32
|
+
if @sidebar == :collapsed && !@collapsible
|
|
33
|
+
raise ArgumentError, "AppShell sidebar: :collapsed requires collapsible: true"
|
|
34
|
+
end
|
|
35
|
+
@sidebar_toggle_label = validate_label!(:sidebar_toggle_label, sidebar_toggle_label)
|
|
26
36
|
@skip_link_label = validate_label!(:skip_link_label, skip_link_label)
|
|
27
37
|
@open_navigation_label = validate_label!(:open_navigation_label, open_navigation_label)
|
|
28
38
|
@close_navigation_label = validate_label!(:close_navigation_label, close_navigation_label)
|
|
@@ -39,9 +49,12 @@ module NitroKit
|
|
|
39
49
|
layout: @layout,
|
|
40
50
|
state: "closed",
|
|
41
51
|
action: "turbo:before-visit@document->nk--app-shell#closeForNavigation " \
|
|
52
|
+
"turbo:before-morph-attribute->nk--app-shell#preserveSidebarState " \
|
|
42
53
|
"turbo:morph@document->nk--app-shell#syncViewport",
|
|
43
54
|
nk__app_shell_open_label_value: @open_navigation_label,
|
|
44
|
-
nk__app_shell_close_label_value: @close_navigation_label
|
|
55
|
+
nk__app_shell_close_label_value: @close_navigation_label,
|
|
56
|
+
nk__app_shell_collapsible_value: @collapsible.to_s,
|
|
57
|
+
nk__app_shell_pinned_value: (@sidebar == :expanded).to_s
|
|
45
58
|
}
|
|
46
59
|
},
|
|
47
60
|
html:,
|
|
@@ -65,8 +78,9 @@ module NitroKit
|
|
|
65
78
|
end
|
|
66
79
|
end
|
|
67
80
|
|
|
68
|
-
def brand(&content)
|
|
81
|
+
def brand(icon: nil, &content)
|
|
69
82
|
add_region(:brand, content:)
|
|
83
|
+
@brand_icon = Icon.new(icon, size: :sm) unless icon.nil?
|
|
70
84
|
end
|
|
71
85
|
|
|
72
86
|
def navigation(&content)
|
|
@@ -124,7 +138,12 @@ module NitroKit
|
|
|
124
138
|
|
|
125
139
|
def render_header
|
|
126
140
|
header(**slot_attributes(:header)) do
|
|
127
|
-
|
|
141
|
+
if region(:brand)
|
|
142
|
+
div(**slot_attributes(:brand, attributes: { data: { action: @collapsible ? "pointerleave->nk--app-shell#resumeHover" : nil } })) do
|
|
143
|
+
render_in_slot(@brand_icon, :brand_icon) if @brand_icon
|
|
144
|
+
div(**slot_attributes(:brand_content)) { render(region(:brand)) }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
128
147
|
render_mobile_trigger
|
|
129
148
|
div(**slot_attributes(:topbar)) { render(region(:topbar)) } if region(:topbar)
|
|
130
149
|
end
|
|
@@ -155,19 +174,32 @@ module NitroKit
|
|
|
155
174
|
**slot_attributes(
|
|
156
175
|
:sidebar,
|
|
157
176
|
attributes: {
|
|
158
|
-
data: {
|
|
177
|
+
data: {
|
|
178
|
+
nk__app_shell_target: "sidebar",
|
|
179
|
+
action: @collapsible ? "pointerleave->nk--app-shell#resumeHover" : nil
|
|
180
|
+
}
|
|
159
181
|
}
|
|
160
182
|
)
|
|
161
183
|
) do
|
|
162
184
|
div(
|
|
163
185
|
**slot_attributes(
|
|
164
186
|
:navigation,
|
|
165
|
-
attributes: { data: { nk__app_shell_target: "navigation" } }
|
|
187
|
+
attributes: { id: navigation_region_id, data: { nk__app_shell_target: "navigation" } }
|
|
166
188
|
)
|
|
167
189
|
) { render(region(:navigation)) }
|
|
190
|
+
render_sidebar_toggle if layout == :sidebar && @collapsible
|
|
168
191
|
end
|
|
169
192
|
end
|
|
170
193
|
|
|
194
|
+
def render_sidebar_toggle
|
|
195
|
+
render_in_slot(Button.new(
|
|
196
|
+
icon: :panel_left,
|
|
197
|
+
label: @sidebar_toggle_label,
|
|
198
|
+
aria: { controls: navigation_region_id, pressed: @sidebar == :expanded },
|
|
199
|
+
data: { nk__app_shell_target: "pin", action: "click->nk--app-shell#togglePin" }
|
|
200
|
+
), :sidebar_toggle)
|
|
201
|
+
end
|
|
202
|
+
|
|
171
203
|
def render_dialog
|
|
172
204
|
dialog(
|
|
173
205
|
**slot_attributes(
|
|
@@ -204,6 +236,10 @@ module NitroKit
|
|
|
204
236
|
html_main(**slot_attributes(:main, attributes: { id: main_id, tabindex: -1 })) { render(region(:main)) }
|
|
205
237
|
end
|
|
206
238
|
|
|
239
|
+
def navigation_region_id
|
|
240
|
+
"#{identifier}-navigation-region"
|
|
241
|
+
end
|
|
242
|
+
|
|
207
243
|
def drawer_id
|
|
208
244
|
"#{identifier}-navigation-drawer"
|
|
209
245
|
end
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
module NitroKit
|
|
4
4
|
class Avatar < Component
|
|
5
5
|
SIZES = %i[xs sm md lg].freeze
|
|
6
|
+
# Derived initials never exceed two characters; explicit fallbacks may run
|
|
7
|
+
# to four, which the stylesheet steps down so they stay inside the circle.
|
|
8
|
+
MAX_FALLBACK_LENGTH = 4
|
|
6
9
|
|
|
7
10
|
def initialize(
|
|
8
11
|
src: nil,
|
|
@@ -30,6 +33,7 @@ module NitroKit
|
|
|
30
33
|
raise ArgumentError, "Avatar images require alt: text unless decorative: true"
|
|
31
34
|
end
|
|
32
35
|
@fallback = fallback || initials_for(alt)
|
|
36
|
+
validate_fallback!(@fallback) unless fallback.nil?
|
|
33
37
|
@size = validate_choice!(:size, size, SIZES)
|
|
34
38
|
root_aria = fallback_aria(aria)
|
|
35
39
|
|
|
@@ -61,7 +65,7 @@ module NitroKit
|
|
|
61
65
|
span(
|
|
62
66
|
**slot_attributes(
|
|
63
67
|
:fallback,
|
|
64
|
-
attributes:
|
|
68
|
+
attributes: { data: { nk__avatar_target: src? ? "fallback" : nil, length: fallback_length }.compact },
|
|
65
69
|
aria: { hidden: (src? || !alt.empty?) ? true : nil }
|
|
66
70
|
)
|
|
67
71
|
) { fallback }
|
|
@@ -97,7 +101,22 @@ module NitroKit
|
|
|
97
101
|
label_key ? aria : { label: alt }.merge(aria)
|
|
98
102
|
end
|
|
99
103
|
|
|
100
|
-
|
|
104
|
+
def validate_fallback!(value)
|
|
105
|
+
if value.strip.empty?
|
|
106
|
+
raise ArgumentError, "fallback must not be blank"
|
|
107
|
+
end
|
|
108
|
+
if value.grapheme_clusters.size > MAX_FALLBACK_LENGTH
|
|
109
|
+
raise ArgumentError, "fallback must be at most #{MAX_FALLBACK_LENGTH} characters; an avatar shows initials, not a word"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Only fallbacks longer than derived initials need the stylesheet to react.
|
|
114
|
+
def fallback_length
|
|
115
|
+
length = fallback.grapheme_clusters.size
|
|
116
|
+
length if length > 2
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def initials_for(name)
|
|
101
120
|
words = name.strip.split
|
|
102
121
|
return "?" if words.empty?
|
|
103
122
|
|
|
@@ -3,14 +3,21 @@
|
|
|
3
3
|
module NitroKit
|
|
4
4
|
class Card < Component
|
|
5
5
|
TITLE_LEVELS = (1..6).freeze
|
|
6
|
+
SIZES = %i[sm md lg].freeze
|
|
7
|
+
VARIANTS = %i[default outline muted].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(size: :md, variant: :default, id: nil, html: {}, aria: {}, data: {}, desperately_need_a_class: nil)
|
|
10
|
+
size = validate_choice!(:size, size, SIZES)
|
|
11
|
+
variant = validate_choice!(:variant, variant, VARIANTS)
|
|
6
12
|
|
|
7
|
-
def initialize(id: nil, html: {}, aria: {}, data: {}, desperately_need_a_class: nil)
|
|
8
13
|
super(
|
|
9
14
|
component: :card,
|
|
10
15
|
attributes: { id: }.compact,
|
|
11
16
|
html:,
|
|
12
17
|
aria:,
|
|
13
18
|
data:,
|
|
19
|
+
size:,
|
|
20
|
+
variant:,
|
|
14
21
|
desperately_need_a_class:
|
|
15
22
|
)
|
|
16
23
|
end
|
|
@@ -23,8 +30,15 @@ module NitroKit
|
|
|
23
30
|
|
|
24
31
|
alias :html_title :title
|
|
25
32
|
alias :html_body :body
|
|
33
|
+
alias :html_header :header
|
|
26
34
|
alias :html_footer :footer
|
|
27
35
|
|
|
36
|
+
def header(html: {}, aria: {}, data: {}, desperately_need_a_class: nil)
|
|
37
|
+
raise ArgumentError, "Card header requires a block" unless block_given?
|
|
38
|
+
|
|
39
|
+
html_header(**slot_attributes(:header, html:, aria:, data:, desperately_need_a_class:)) { yield }
|
|
40
|
+
end
|
|
41
|
+
|
|
28
42
|
def title(text = nil, level: 2, html: {}, aria: {}, data: {}, desperately_need_a_class: nil, &block)
|
|
29
43
|
validate_choice!(:level, level, TITLE_LEVELS)
|
|
30
44
|
require_region!(:title, text, block)
|
|
@@ -34,6 +48,19 @@ module NitroKit
|
|
|
34
48
|
) { text_or_block(text, &block) }
|
|
35
49
|
end
|
|
36
50
|
|
|
51
|
+
def description(text = nil, html: {}, aria: {}, data: {}, desperately_need_a_class: nil, &block)
|
|
52
|
+
require_region!(:description, text, block)
|
|
53
|
+
p(**slot_attributes(:description, html:, aria:, data:, desperately_need_a_class:)) do
|
|
54
|
+
text_or_block(text, &block)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def actions(html: {}, aria: {}, data: {}, desperately_need_a_class: nil)
|
|
59
|
+
raise ArgumentError, "Card actions requires a block" unless block_given?
|
|
60
|
+
|
|
61
|
+
div(**slot_attributes(:actions, html:, aria:, data:, desperately_need_a_class:)) { yield }
|
|
62
|
+
end
|
|
63
|
+
|
|
37
64
|
def body(text = nil, html: {}, aria: {}, data: {}, desperately_need_a_class: nil, &block)
|
|
38
65
|
require_region!(:body, text, block)
|
|
39
66
|
div(**slot_attributes(:body, html:, aria:, data:, desperately_need_a_class:)) do
|
|
@@ -3,8 +3,14 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
3
3
|
const narrowViewport = "(width < 48rem)";
|
|
4
4
|
|
|
5
5
|
export default class extends Controller {
|
|
6
|
-
static targets = ["dialog", "navigation", "sidebar", "trigger"];
|
|
7
|
-
static values = {
|
|
6
|
+
static targets = ["dialog", "navigation", "sidebar", "trigger", "pin"];
|
|
7
|
+
static values = {
|
|
8
|
+
openLabel: String,
|
|
9
|
+
closeLabel: String,
|
|
10
|
+
collapsible: { type: Boolean, default: false },
|
|
11
|
+
pinned: { type: Boolean, default: true },
|
|
12
|
+
hoverSuppressed: { type: Boolean, default: false },
|
|
13
|
+
};
|
|
8
14
|
|
|
9
15
|
connect() {
|
|
10
16
|
this.onViewportChange = this.syncViewport.bind(this);
|
|
@@ -15,6 +21,7 @@ export default class extends Controller {
|
|
|
15
21
|
|
|
16
22
|
disconnect() {
|
|
17
23
|
this.viewport?.removeEventListener("change", this.onViewportChange);
|
|
24
|
+
this.hoverSuppressedValue = false;
|
|
18
25
|
this.restoreFocusAfterClose = false;
|
|
19
26
|
|
|
20
27
|
const dialog = this.element.querySelector(
|
|
@@ -49,6 +56,40 @@ export default class extends Controller {
|
|
|
49
56
|
this.dialogTarget.open ? this.closeDialog() : this.open();
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
togglePin(event) {
|
|
60
|
+
if (
|
|
61
|
+
!this.collapsibleValue ||
|
|
62
|
+
this.isNarrow ||
|
|
63
|
+
this.element.dataset.layout !== "sidebar"
|
|
64
|
+
)
|
|
65
|
+
return;
|
|
66
|
+
|
|
67
|
+
this.hoverSuppressedValue = this.pinnedValue && event.detail > 0;
|
|
68
|
+
this.pinnedValue = !this.pinnedValue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
resumeHover(event) {
|
|
72
|
+
const brand = this.element.querySelector(
|
|
73
|
+
':scope > [data-slot="app-shell-header"] > [data-slot="app-shell-brand"]',
|
|
74
|
+
);
|
|
75
|
+
if (
|
|
76
|
+
this.sidebarTarget.contains(event.relatedTarget) ||
|
|
77
|
+
brand?.contains(event.relatedTarget)
|
|
78
|
+
)
|
|
79
|
+
return;
|
|
80
|
+
|
|
81
|
+
this.hoverSuppressedValue = false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
pinnedValueChanged() {
|
|
85
|
+
if (this.hasPinTarget)
|
|
86
|
+
this.pinTarget.setAttribute("aria-pressed", String(this.pinnedValue));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pinTargetConnected(pin) {
|
|
90
|
+
pin.setAttribute("aria-pressed", String(this.pinnedValue));
|
|
91
|
+
}
|
|
92
|
+
|
|
52
93
|
open() {
|
|
53
94
|
if (!this.isNarrow || this.dialogTarget.open) return;
|
|
54
95
|
|
|
@@ -78,9 +119,29 @@ export default class extends Controller {
|
|
|
78
119
|
}
|
|
79
120
|
|
|
80
121
|
closeForNavigation() {
|
|
122
|
+
this.hoverSuppressedValue = false;
|
|
81
123
|
this.closeDialog({ restoreFocus: false });
|
|
82
124
|
}
|
|
83
125
|
|
|
126
|
+
preserveSidebarState(event) {
|
|
127
|
+
if (!this.collapsibleValue || this.element.dataset.layout !== "sidebar")
|
|
128
|
+
return;
|
|
129
|
+
|
|
130
|
+
const attribute = event.detail.attributeName;
|
|
131
|
+
const sidebarState =
|
|
132
|
+
event.target === this.element &&
|
|
133
|
+
[
|
|
134
|
+
"data-nk--app-shell-pinned-value",
|
|
135
|
+
"data-nk--app-shell-hover-suppressed-value",
|
|
136
|
+
].includes(attribute);
|
|
137
|
+
const pinState =
|
|
138
|
+
this.hasPinTarget &&
|
|
139
|
+
event.target === this.pinTarget &&
|
|
140
|
+
attribute === "aria-pressed";
|
|
141
|
+
|
|
142
|
+
if (sidebarState || pinState) event.preventDefault();
|
|
143
|
+
}
|
|
144
|
+
|
|
84
145
|
dialogClosed() {
|
|
85
146
|
this.finishClosing();
|
|
86
147
|
}
|
|
@@ -101,6 +162,7 @@ export default class extends Controller {
|
|
|
101
162
|
|
|
102
163
|
const enteringNarrow = this.wasNarrow === false && this.isNarrow;
|
|
103
164
|
this.wasNarrow = this.isNarrow;
|
|
165
|
+
if (this.isNarrow) this.hoverSuppressedValue = false;
|
|
104
166
|
|
|
105
167
|
if (!this.isNarrow) {
|
|
106
168
|
this.closeDialog({ restoreFocus: false });
|
data/config/locales/en.yml
CHANGED
data/docs/agent_native_spec.md
CHANGED
|
@@ -25,7 +25,8 @@ Applications own:
|
|
|
25
25
|
- application CSS and documented token overrides.
|
|
26
26
|
|
|
27
27
|
Components load from the gem. Generators may install application guidance and
|
|
28
|
-
integration files
|
|
28
|
+
integration files. The explicit, user-requested [eject generator](eject.md) is
|
|
29
|
+
the sole opt-out from gem ownership, one component at a time, not copy-install.
|
|
29
30
|
|
|
30
31
|
## Public API principles
|
|
31
32
|
|
|
@@ -52,8 +53,8 @@ end
|
|
|
52
53
|
component contracts define the current set.
|
|
53
54
|
- Components reject `class:` and `style:`. The audited
|
|
54
55
|
`desperately_need_a_class:` escape exists only for external integrations.
|
|
55
|
-
- There are no `nk_*` helpers, generated variant helpers,
|
|
56
|
-
or general ERB bridge.
|
|
56
|
+
- There are no `nk_*` helpers, generated variant helpers, copy-installed
|
|
57
|
+
components, or general ERB bridge.
|
|
57
58
|
|
|
58
59
|
Rails helpers remain first-class for forms, routes, DOM IDs, translations,
|
|
59
60
|
assets, Active Storage, Turbo Frames, and Turbo Streams.
|
|
@@ -105,7 +106,7 @@ hold delivery history; this document records only durable architecture.
|
|
|
105
106
|
|
|
106
107
|
## Outside the current architecture
|
|
107
108
|
|
|
108
|
-
Nitro Kit does not provide
|
|
109
|
+
Nitro Kit does not provide default copy-install, a generic utility DSL,
|
|
109
110
|
arbitrary breakpoints, a public component registry, an MCP server, or a
|
|
110
111
|
JavaScript custom-element runtime. New abstractions require demonstrated reuse
|
|
111
112
|
and an explicit public contract.
|
data/docs/component_contracts.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
API. This table is also parsed by the gallery; keep every row to four cells and
|
|
5
5
|
escape literal pipe characters.
|
|
6
6
|
|
|
7
|
-
This is the shipped public catalog for `2.0.0.
|
|
7
|
+
This is the shipped public catalog for `2.0.0.beta.2`. It records current Ruby
|
|
8
8
|
construction, rendered roots, closed vocabularies, compound cardinalities, and
|
|
9
9
|
integration boundaries. For task guidance, start with the
|
|
10
10
|
[agent guide](agent_guide.md) or [Rails integration](rails_integration.md).
|
|
@@ -37,7 +37,7 @@ Every default string a person can read or hear comes from the engine-loaded `nit
|
|
|
37
37
|
| `xs sm md lg` | `Avatar`, `AvatarStack` | Identity reads from a list row up to a profile header; nothing needs a poster-sized avatar. |
|
|
38
38
|
| `xs sm md` | `Badge` | A badge is compact by definition; a large badge would be a Button or an Alert. |
|
|
39
39
|
| `sm md lg xl` | `Container` | Content measures, resolved from the `--nk-content-*` tokens. |
|
|
40
|
-
| `sm md lg` | `Sheet`, `ProgressiveImage`
|
|
40
|
+
| `sm md lg` | `Card`, `Sheet`, `ProgressiveImage` | Panel and media footprints. |
|
|
41
41
|
| `md lg` | `Checkbox`, `RadioButton`, `Switch` | Selection controls have one comfortable size and one emphasized size, resolved from the choice and control ramps. |
|
|
42
42
|
|
|
43
43
|
## Atoms and components
|
|
@@ -48,7 +48,7 @@ Every default string a person can read or hear comes from the engine-loaded `nit
|
|
|
48
48
|
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
49
49
|
| `Alert` | `variant: :default`, `title: nil`, `description: nil`, `live: :off`, `id: nil` | `div[data-nk=alert]`; variants `default info success warning destructive`, the same families as `Toast::Item`, which spells its failure variant `error`; live modes `off polite assertive` | One semantic axis: `variant:` emits `data-variant` and drives the tint through the shared semantic palette, which resolves every family to the public `--nk-palette-*` tokens. An Alert and a `Toast::Item` of the same family render identically; Alert's `destructive` and Toast's `error` resolve to the same tint family. There is no `color:` option. Accepts at most one `NitroKit::Icon` through `icon`, plus `title` and `description` through constructor text or the matching compound method, never both. Nitro renders icon, title, then description regardless of declaration order; declaring outside the render block raises. `live: :polite` adds `role=status`; `live: :assertive` adds `role=alert`; static alerts have no live role by default. |
|
|
50
50
|
| `AppNavigation` | required `label:`; `id: nil` | native `nav[data-nk=app-navigation]` | Requires one body and at least one item. The body is a `ul`; every entry is an `li`. Optional unique `header` and `footer` surround ordered `section(label: nil, collapsible: false, expanded: true)`, `divider`, `item(text, href:, icon: nil, icon_end: nil, badge: nil, badge_color: :neutral, current: false, html:, aria:, data:, desperately_need_a_class:)`, and at most one `spacer`. A section renders a `span` label plus a nested `ul` named through `aria-label`; it emits no heading. A collapsible section requires a label and renders native `details` and `summary` with an owned chevron, so open state lives on the `open` attribute without JavaScript; `expanded:` sets the initial state. Items render `li > a[data-slot=app-navigation-item-link]` and carry the item attribute bags. Icon and badge vocabularies are validated as the item is declared. At most one item is current. |
|
|
51
|
-
| `Avatar` | `src: nil`, `alt: ""`, `fallback: nil`, `decorative: false`, `size: :md`, `loading: "lazy"`, `decoding: "async"`, `id: nil` | `span[data-nk=avatar]`; sizes `xs sm md lg`; state `error` | Renders fallback and optional image slots. The source is a keyword; there is no positional form. An image with empty `alt:` raises unless `decorative: true`. When a source is present the root carries `nk--avatar`, which sets `data-state="error"` if the image fails to load so the initials fallback shows through instead of a broken-image glyph, and unhides that fallback for assistive technology.
|
|
51
|
+
| `Avatar` | `src: nil`, `alt: ""`, `fallback: nil`, `decorative: false`, `size: :md`, `loading: "lazy"`, `decoding: "async"`, `id: nil` | `span[data-nk=avatar]`; sizes `xs sm md lg`; state `error` | Renders fallback and optional image slots. The source is a keyword; there is no positional form. `fallback:` is a non-blank String of at most four characters; derived initials never exceed two, and three- or four-character fallbacks carry `data-length` so the stylesheet steps the type down to stay inside the circle. An image with empty `alt:` raises unless `decorative: true`. When a source is present the root carries `nk--avatar`, which sets `data-state="error"` if the image fails to load so the initials fallback shows through instead of a broken-image glyph, and unhides that fallback for assistive technology. |
|
|
52
52
|
| `AvatarStack` | required `label:`; `size: :md`, `max: nil`, `id: nil` | `span[data-nk=avatar-stack][role=group]`; sizes `xs sm md lg` | `label:` names the group; `aria: { label: }` raises. `avatar(src:, alt:, fallback:, decorative:, loading:, decoding:, id:, html:, aria:, data:, desperately_need_a_class:)` takes explicit keywords and inherits the stack size. Declarations are collected, so Nitro renders the avatars then the single overflow regardless of declaration order. `max:` bounds the visible avatars and derives a `+N` indicator from the remainder; it cannot be combined with an explicit `overflow(count, label:)`, whose count must be positive and whose accessible label is owned by `label:`. The indicator carries `role="img"`; a derived one is named from `nitro_kit.avatar_stack.overflow` with the remaining count. |
|
|
53
53
|
| `Badge` | optional text or a content block; `variant: :default`, `size: :md`, `color: :neutral`, `id: nil` | `span[data-nk=badge]`; variants `default outline`; sizes `xs sm md`; semantic colors plus the decorative palette | Content has exactly one path: label text or a content block, never both. Blank text raises at construction, the earliest point it is knowable; missing content raises at render. The color axis carries two vocabularies with different jobs. The semantic families `neutral info success warning destructive` resolve to the `--nk-palette-{family}` tint roles and move with an application's theme; the seventeen decorative hues resolve to the `--nk-palette-{hue}` roles and stay the color they name. `red` and `destructive` are therefore independently themeable rather than two spellings of one value. `href:` and `dismissible:` are deliberately out of scope; wrap the Badge in a link or pair it with a Button instead. |
|
|
54
54
|
| `Button` | optional text or block; `href: nil`, `variant: :default`, `size: :md`, `icon: nil`, `icon_end: nil`, `label: nil`, `id: nil`, `type: :button`, `name: nil`, `value: nil`, `form: nil`, `target: nil`, `rel: nil`, `download: nil`, `disabled: false`, `loading: false`, `submission_indicator: nil` | native `button` or `a[data-nk=button]`; variants `default primary destructive ghost`; sizes `xs sm md lg xl`; types `button submit reset` | Requires text, a block, or an icon. `default` is the ordinary action treatment; `ghost` is reserved for deliberately low-emphasis interface chrome, not routine secondary actions. Icon-only buttons require `label:`, `aria: { label: }`, or `aria: { labelledby: }`; `label:` and `aria: { label: }` are the same attribute and collide. `icon_end:` matches the `button-icon-end` slot. Blank text, treatment vocabularies, and link/button option mixing raise on construction; text-plus-block and the icon-only accessible name raise at render because only render time knows whether a block supplies the label. `type:` applies to native buttons only and raises when combined with `href:`. `loading: true` disables the control, sets `aria-busy="true"`, and replaces the leading icon with the `button-spinner` slot. Disabled links lose `href`, receive `aria-disabled`, and leave the tab order. `submission_indicator: :spinner` applies only to native submit Buttons, cannot combine with `loading:`, and renders the `button-submission-spinner` slot that `nk--button` reveals during a slow Turbo submission. |
|
|
@@ -137,7 +137,7 @@ Field types are `button color date datetime datetime_local email file hidden mon
|
|
|
137
137
|
| Component | Constructor-specific options | Root and closed vocabulary | Compound contract |
|
|
138
138
|
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
139
139
|
| `Accordion` | required `id:`; `mode: :multiple` | `div[data-nk=accordion]`; modes `multiple single` | Requires one or more uniquely keyed `item(key, title:, expanded: false)` declarations. Each item renders native `details` and `summary`, so find-in-page and fragment navigation reveal matching closed content without a controller. Single mode gives every disclosure the same `name` and accepts at most one initially expanded item; multiple mode omits `name`. There is no disabled item or controller. |
|
|
140
|
-
| `Card`
|
|
140
|
+
| `Card` | `size: :md`, `variant: :default`, `id: nil` | `article[data-nk=card][data-size][data-variant]`; sizes `sm md lg`; variants `default muted outline`; title levels `1..6` | Requires a content block. `header`, `title`, `description`, `actions`, `body`, `footer`, `divider`, and `full` render immediately; their count and order are not constrained. `title`, `description`, `body`, and `footer` require non-blank text or a block; `header`, `actions`, and `full` require a block. `header` lays out a title and description beside trailing `actions`; `actions` inside a footer push to its end. The shadowed Phlex elements remain available as `html_title`, `html_body`, `html_header`, and `html_footer`. Sizes coordinate padding, gaps, corners, and title size. See Card sizing and surfaces below. |
|
|
141
141
|
| `Combobox` | required `id:`, `name:`, `label:`, `options:`; `value: nil`, `placeholder: nil`, `include_blank: true`, `placement: :bottom_start`, `required: false`, `disabled: false`, `autocomplete: "off"`, `control_aria: {}` | `div[data-nk=combobox]`; placements `bottom_start bottom_end top_start top_end` | Options are a non-empty set of unique typed choices; `Choice#description` renders as a described secondary line in the listbox. A non-nil value must match a declared option. `label:` renders a real `Label` bound to `#{id}-input`; `label: false` requires `control_aria: { label: }` or `{ labelledby: }` and names the input, listbox, and native select from it. `placeholder:` is the input hint and `include_blank:` is the native blank option. The named native Select is the truthful no-JavaScript control and submission source; Stimulus reveals and synchronizes the searchable combobox enhancement. |
|
|
142
142
|
| `Dialog` | required `id:`; `dismissible: true` | `div[data-nk=dialog]` owning exactly one native dialog | Requires a declaration block with exactly one `panel(title:, description: nil, nonmodal: false)` and at most one `trigger`, which forwards Button treatment options including `icon:`, `icon_end:`, and `label:`, matching Sheet and Dropdown. Nitro renders trigger then panel, and inside the panel it owns close button, title, description, then the captured application content, so a sticky close control survives long scrolling content. At most one `close_button(label:)` may be declared inside the panel block, whose label defaults to `I18n.t("nitro_kit.dialog.close")`; `dismissible: false` renders none and declaring one raises. `nonmodal: true` is the explicit server-open state and cannot be combined with a trigger, whose `command="show-modal"` would open the same panel modally. Trigger and close controls use native `command`/`commandfor`. Turbo confirmations are deliberately not a Dialog runtime: applications own inline confirmation dialogs, see [destructive action](patterns/destructive_action.md). |
|
|
143
143
|
| `DetailsTable` | `record`, `caption: nil`, `label: nil`, `empty_text: nil`, `boolean_labels: nil`, `route_base: nil`, `id: nil` | `div[data-nk=details-table]` containing a slotted `Table` | Requires one or more fields with unique keys, declared inside the render block. `field(attribute, label: nil, value:)` distinguishes omitted and explicit nil values; a block receives the resolved value. `fields(*attributes)` adds ordinary resolved fields. `caption:` names the table visibly and `label:` names it through `aria-label`; empty and boolean copy remain caller-owned keywords; when omitted they come from `nitro_kit.details_table.empty`, `.boolean_true`, and `.boolean_false`. |
|
|
@@ -150,6 +150,94 @@ Field types are `button color date datetime datetime_local email file hidden mon
|
|
|
150
150
|
| `Tooltip` | required `id:`, `content:`; `placement: :top` | `span[data-nk=tooltip]`; placements `top right bottom left` | Requires exactly one trigger. The default `as: NitroKit::Button` path supports native buttons and links; Button options on any other trigger raise. `as: :div` and `:span` create explicit focusable HTML descriptions. `as: :custom` yields `TriggerAttributes(html:, aria:, data:)`; applications must forward all three boundaries to the actual focusable control, including ButtonTo's nested Button or a Dialog trigger. Nitro appends the tooltip ID to existing `aria-describedby`. CSS owns hover/focus visibility; the controller's document-level Escape listener dismisses only while the tooltip is shown, so a wrapping dialog's cancel is not swallowed. |
|
|
151
151
|
| `Typeset` | `id: nil` | `div[data-nk=typeset]` | Requires direct content. Styles semantic rich content without constraining its width. Nested Nitro component roots and `data-typeset="off"` regions establish styling boundaries. The shipped stylesheet retains the `@scope` path and includes a low-specificity fallback for engines that do not parse `@scope`, including Firefox through 145. That fallback covers root typography plus explicitly anchored direct-child headings, flow elements, lists, code/pre, tables, and links within those supported elements; it is a documented semantic subset rather than full descendant parity. |
|
|
152
152
|
|
|
153
|
+
### Card sizing and surfaces
|
|
154
|
+
|
|
155
|
+
`Card.new(size: :md, variant: :default, id: nil)` also accepts the shared
|
|
156
|
+
attribute boundary. Sizes are `sm md lg`; variants are `default muted outline`.
|
|
157
|
+
Both closed vocabularies require symbols and emit owned `data-size` and
|
|
158
|
+
`data-variant` attributes. Unsupported values raise at construction.
|
|
159
|
+
|
|
160
|
+
Padding and inter-part gaps scale with `--nk-space`; corners use the radius
|
|
161
|
+
tokens. The card title is sized by the card, not the shared surface title
|
|
162
|
+
role, so a small card's heading stays proportionate. Description, body, and
|
|
163
|
+
footer text stay `--nk-text-sm`, and action gaps do not scale. The card's
|
|
164
|
+
`full` regions and dividers follow its padding, and bleeding corners follow
|
|
165
|
+
its radius.
|
|
166
|
+
|
|
167
|
+
| Size | Padding | Part gap | Radius | Title |
|
|
168
|
+
| ---- | ------- | -------- | ------ | ----- |
|
|
169
|
+
| `sm` | 4 space steps | 3 space steps | `--nk-radius-lg` | `--nk-text-sm` |
|
|
170
|
+
| `md` | 6 space steps | 4 space steps | `--nk-radius-xl` | `--nk-text-base` |
|
|
171
|
+
| `lg` | 8 space steps | 6 space steps | `--nk-radius-xl` + 1 space step | `--nk-text-lg` |
|
|
172
|
+
|
|
173
|
+
`default` is `--nk-color-surface` with the shared border and `--nk-shadow-xs`.
|
|
174
|
+
`muted` tints the parent's surface with 4% foreground and drops the shadow;
|
|
175
|
+
`outline` keeps only the border. Neither adds hover or link behavior.
|
|
176
|
+
|
|
177
|
+
A section set apart by a divider at the card's top or bottom edge is a band:
|
|
178
|
+
its distance to the card edge equals the part gap, so a divided header or
|
|
179
|
+
footer sits evenly between edge and divider. `full` regions keep the full
|
|
180
|
+
padding because they bleed through it. Tables inside `full` align their first
|
|
181
|
+
and last columns and their caption with the card's padding. When a parent such as
|
|
182
|
+
a Grid row stretches the card, a trailing footer and its divider stay on the
|
|
183
|
+
bottom edge so actions line up across the row.
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
render NitroKit::Card.new do |card|
|
|
187
|
+
card.header do
|
|
188
|
+
card.title("Profile", level: 3)
|
|
189
|
+
card.description("This is how others see you.")
|
|
190
|
+
card.actions { render NitroKit::Button.new("Edit", size: :sm) }
|
|
191
|
+
end
|
|
192
|
+
card.divider
|
|
193
|
+
card.body { "…" }
|
|
194
|
+
card.divider
|
|
195
|
+
card.footer do
|
|
196
|
+
plain "Last saved 2 minutes ago"
|
|
197
|
+
card.actions { render NitroKit::Button.new("Save", variant: :primary, size: :sm) }
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### Card configuration recipes
|
|
203
|
+
|
|
204
|
+
Choose density for the content's job, surface for its emphasis, and a parent
|
|
205
|
+
layout for its width. Neither card axis changes heading level, button state,
|
|
206
|
+
form semantics, or table scrolling.
|
|
207
|
+
|
|
208
|
+
| Configuration | Use |
|
|
209
|
+
| ------------- | --- |
|
|
210
|
+
| `size: :sm` | Dense widgets, link grids, and compact summaries |
|
|
211
|
+
| `size: :md` | Ordinary records; the default |
|
|
212
|
+
| `size: :lg` | Roomy forms and settings |
|
|
213
|
+
| `variant: :muted` | Secondary panels beside a primary card |
|
|
214
|
+
| `variant: :outline` | A quiet edge over the parent's surface |
|
|
215
|
+
|
|
216
|
+
The [card gallery](https://gallery.nitrokit.dev/gallery/components/card) covers
|
|
217
|
+
a simple confirmation, a header/body/footer profile form, every size and
|
|
218
|
+
surface, a divided settings list, media and table bleed, a responsive link
|
|
219
|
+
grid, footer Button states, partial structures, semantic title levels, and
|
|
220
|
+
hostile content.
|
|
221
|
+
|
|
222
|
+
Associate footer actions with the body's native form through `form:`. Use
|
|
223
|
+
`full` for media at either edge. An only-child `full` touches all four
|
|
224
|
+
corners; a leading or trailing region touches only that edge's corners. Do not
|
|
225
|
+
clip the root, since menus and tooltips may need to escape it.
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
render NitroKit::Card.new(size: :md) do |card|
|
|
229
|
+
card.full { img(src: "/images/hills.jpg", alt: "Green hills beneath a warm sun") }
|
|
230
|
+
card.header do
|
|
231
|
+
card.title("Morning hike", level: 3)
|
|
232
|
+
card.description("September 12 · 8.4 km")
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Compose collections with `Grid.new(cols: "1 sm:2 lg:3", gap: 4)`, not card width
|
|
238
|
+
options. Tables placed inside `full` retain their own horizontal scroll region.
|
|
239
|
+
Loading and disabled states belong to Buttons or application content, not Card.
|
|
240
|
+
|
|
153
241
|
## Layout primitives
|
|
154
242
|
|
|
155
243
|
Only closed, component-specific layout values are public. `VStack` and `HStack` have been removed; choose an explicit `Flex` direction instead.
|
|
@@ -210,7 +298,7 @@ Unknown values or prefixes, duplicate base/prefix tokens, missing base values, b
|
|
|
210
298
|
| Component | Constructor | Root | Compound contract |
|
|
211
299
|
| ----------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
212
300
|
| `AuthShell` | `id: nil` | `main[data-nk=auth-shell]` | Requires direct content. Owns `main` → medium Container → `Flex(dir: :col, gap: 6, align: :stretch)`; branding, Cards, and Turbo boundaries remain caller-owned. |
|
|
213
|
-
| `AppShell` | required `id:`; `layout: :sidebar`; configurable skip/open/close/dialog labels defaulting to `nitro_kit.app_shell.*` | `div[data-nk=app-shell][data-layout]`; layouts `sidebar topbar`; states `open closed`; `data-enhanced` is written by the Stimulus controller and reserved | Requires exactly one `navigation` and one `main`; optional `brand` and `topbar` are unique. Regions are declared inside the render block only. One navigation tree moves between its neutral desktop wrapper and a native modal dialog at narrow widths. IDs are fragment-safe; product policy remains caller-owned.
|
|
301
|
+
| `AppShell` | required `id:`; `layout: :sidebar`; `collapsible: false`; `sidebar: :expanded` (`expanded collapsed`); configurable skip/open/close/dialog/sidebar-toggle labels defaulting to `nitro_kit.app_shell.*` | `div[data-nk=app-shell][data-layout]`; layouts `sidebar topbar`; drawer states `open closed`; `data-enhanced` is written by the Stimulus controller and reserved | Requires exactly one `navigation` and one `main`; optional `brand(icon: nil)` and `topbar` are unique. Regions are declared inside the render block only. The default sidebar stays expanded with no toggle or peek. `collapsible: true` opts into a Button-backed pin toggle with `aria-pressed` and fine-pointer hover/focus overlay peek without changing the rail's layout width. Brand's optional validated Lucide icon stays visible on the rail; full brand content peeks instead of being cropped. Labels retain accessible names and vertical positions; supply item icons for the rail. `sidebar:` sets initial pin state; `sidebar: :collapsed` requires `collapsible: true`. These options do not affect topbar or the narrow native drawer. Pin state is not persisted across page loads. One navigation tree moves between its neutral desktop wrapper and a native modal dialog at narrow widths. IDs are fragment-safe; product policy remains caller-owned. |
|
|
214
302
|
| `SettingsLayout` | `id: nil` | `div[data-nk=settings-layout]` | Exactly one `navigation(label:)` and one `content` region, both block-only and declared inside the render block. The navigation requires at least one `item(text, href:, icon: nil, current: false, html:, aria:, data:, desperately_need_a_class:)` and renders `nav > ul > li > a`, where `icon:` names a Lucide icon and the item attribute bags land on the link; a current item uses `aria-current="page"` and at most one item is current. Routes remain caller-owned. |
|
|
215
303
|
| `Toolbar` | `id: nil` | `div[data-nk=toolbar]` | At most one `leading` and one `trailing`; at least one region total. Each region requires a content block. It deliberately has no toolbar role, sticky mode, or action registry. |
|
|
216
304
|
| `PaginationBar` | `id: nil` | `div[data-nk=pagination-bar]` | Exactly one typed `pagination(NitroKit::Pagination)` and at most one non-blank `summary`. The summary announces politely unless the caller supplies its own `aria-live`; the shadowed Phlex element remains available as `html_summary`. Counts, routes, and page math remain caller-owned. |
|
data/docs/customization.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
themes or composing application-owned UI. The first sections are task guidance;
|
|
5
5
|
the final token tables are exhaustive reference.
|
|
6
6
|
|
|
7
|
-
Nitro Kit owns component Ruby, markup, behavior, and default CSS. Applications customize
|
|
7
|
+
Nitro Kit owns component Ruby, markup, behavior, and default CSS. Applications normally customize public `--nk-*` properties and compose application UI. When source-level changes are necessary, explicitly [eject one component](eject.md); that isolated snapshot becomes application-owned and stops receiving component updates.
|
|
8
8
|
|
|
9
9
|
## Stylesheet order
|
|
10
10
|
|
|
@@ -517,7 +517,7 @@ The following variables are the complete public token set. Theme-independent tok
|
|
|
517
517
|
| `--nk-title-page-weight` | Page title weight. |
|
|
518
518
|
| `--nk-title-section-size` | Section title size: data, settings, and danger sections. |
|
|
519
519
|
| `--nk-title-section-weight` | Section title weight. |
|
|
520
|
-
| `--nk-title-surface-size` | Surface title size:
|
|
520
|
+
| `--nk-title-surface-size` | Surface title size: dialogs, sheets, empty states, fieldsets. |
|
|
521
521
|
| `--nk-title-surface-weight` | Surface title weight. |
|
|
522
522
|
| `--nk-title-compact-size` | Compact title size: legends, alert and toast titles. |
|
|
523
523
|
| `--nk-title-compact-weight` | Compact title weight. |
|