nitro_kit 0.9.0 → 2.0.0.alpha.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/.agents/plugins/marketplace.json +20 -0
- data/CHANGELOG.md +172 -0
- data/LICENSE +28 -0
- data/README.md +22 -15
- data/STYLE_GUIDE.md +425 -0
- data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +38 -0
- data/app/assets/stylesheets/nitro_kit.css +7239 -0
- data/app/components/nitro_kit/accordion.rb +125 -65
- data/app/components/nitro_kit/alert.rb +80 -43
- data/app/components/nitro_kit/app_navigation.rb +307 -0
- data/app/components/nitro_kit/app_shell.rb +227 -0
- data/app/components/nitro_kit/appearance_bootstrap.rb +146 -0
- data/app/components/nitro_kit/appearance_picker.rb +156 -0
- data/app/components/nitro_kit/auth_shell.rb +34 -0
- data/app/components/nitro_kit/avatar.rb +85 -29
- data/app/components/nitro_kit/avatar_stack.rb +155 -9
- data/app/components/nitro_kit/badge.rb +43 -81
- data/app/components/nitro_kit/button.rb +180 -103
- data/app/components/nitro_kit/button_group.rb +63 -11
- data/app/components/nitro_kit/button_to.rb +69 -0
- data/app/components/nitro_kit/card.rb +42 -45
- data/app/components/nitro_kit/checkbox.rb +115 -62
- data/app/components/nitro_kit/checkbox_group.rb +131 -22
- data/app/components/nitro_kit/choice.rb +55 -0
- data/app/components/nitro_kit/combobox.rb +292 -105
- data/app/components/nitro_kit/command_palette.rb +412 -0
- data/app/components/nitro_kit/component.rb +322 -46
- data/app/components/nitro_kit/container.rb +32 -0
- data/app/components/nitro_kit/control_group.rb +45 -0
- data/app/components/nitro_kit/danger_zone.rb +78 -0
- data/app/components/nitro_kit/data_section.rb +106 -0
- data/app/components/nitro_kit/details_table.rb +229 -0
- data/app/components/nitro_kit/dialog.rb +257 -83
- data/app/components/nitro_kit/dropdown.rb +335 -122
- data/app/components/nitro_kit/dropzone.rb +326 -0
- data/app/components/nitro_kit/empty_state.rb +99 -0
- data/app/components/nitro_kit/field.rb +461 -236
- data/app/components/nitro_kit/field_group.rb +15 -8
- data/app/components/nitro_kit/fieldset.rb +37 -39
- data/app/components/nitro_kit/flex.rb +49 -0
- data/app/components/nitro_kit/form_builder.rb +313 -72
- data/app/components/nitro_kit/form_section.rb +79 -0
- data/app/components/nitro_kit/grid.rb +34 -0
- data/app/components/nitro_kit/icon.rb +54 -25
- data/app/components/nitro_kit/input.rb +97 -10
- data/app/components/nitro_kit/label.rb +22 -6
- data/app/components/nitro_kit/layout_options.rb +7 -0
- data/app/components/nitro_kit/page_header.rb +76 -0
- data/app/components/nitro_kit/pagination.rb +355 -69
- data/app/components/nitro_kit/pagination_bar.rb +90 -0
- data/app/components/nitro_kit/progressive_image.rb +181 -0
- data/app/components/nitro_kit/radio_button.rb +77 -55
- data/app/components/nitro_kit/radio_button_group.rb +107 -36
- data/app/components/nitro_kit/responsive_value.rb +94 -0
- data/app/components/nitro_kit/rich_text_area.rb +31 -0
- data/app/components/nitro_kit/select.rb +129 -64
- data/app/components/nitro_kit/settings_layout.rb +152 -0
- data/app/components/nitro_kit/sheet.rb +202 -0
- data/app/components/nitro_kit/stat_grid.rb +75 -0
- data/app/components/nitro_kit/switch.rb +108 -51
- data/app/components/nitro_kit/table.rb +237 -45
- data/app/components/nitro_kit/tabs.rb +168 -63
- data/app/components/nitro_kit/textarea.rb +81 -10
- data/app/components/nitro_kit/toast.rb +211 -80
- data/app/components/nitro_kit/toolbar.rb +75 -0
- data/app/components/nitro_kit/tooltip.rb +235 -34
- data/app/components/nitro_kit/typeset.rb +28 -0
- data/app/javascript/controllers/nk/app_shell_controller.js +224 -0
- data/app/javascript/controllers/nk/appearance_controller.js +73 -0
- data/app/javascript/controllers/nk/avatar_controller.js +15 -0
- data/app/javascript/controllers/nk/button_controller.js +56 -0
- data/app/javascript/controllers/nk/checkable_controller.js +41 -0
- data/app/javascript/controllers/nk/combobox_controller.js +314 -82
- data/app/javascript/controllers/nk/command_palette_controller.js +249 -0
- data/app/javascript/controllers/nk/dialog_controller.js +14 -10
- data/app/javascript/controllers/nk/dropdown_controller.js +106 -58
- data/app/javascript/controllers/nk/dropzone/direct_upload.js +61 -0
- data/app/javascript/controllers/nk/dropzone/file_rules.js +77 -0
- data/app/javascript/controllers/nk/dropzone/form_submit_lock.js +30 -0
- data/app/javascript/controllers/nk/dropzone_controller.js +482 -0
- data/app/javascript/controllers/nk/overlay_position.js +37 -0
- data/app/javascript/controllers/nk/progressive_image_controller.js +116 -0
- data/app/javascript/controllers/nk/tabs_controller.js +135 -23
- data/app/javascript/controllers/nk/toast_controller.js +112 -45
- data/app/javascript/controllers/nk/tooltip_controller.js +12 -47
- data/config/importmap.rb +2 -0
- data/config/locales/en.yml +98 -0
- data/docs/agent_guide.md +139 -0
- data/docs/agent_native_spec.md +422 -0
- data/docs/component_contracts.md +263 -0
- data/docs/customization.md +595 -0
- data/docs/hotwire.md +94 -0
- data/docs/initialization_prompt.md +36 -0
- data/docs/migration_1_to_2.md +318 -0
- data/docs/new_app_strategy.md +22 -0
- data/docs/patterns/application_foundation.md +136 -0
- data/docs/patterns/crud_resource.md +144 -0
- data/docs/patterns/destructive_action.md +105 -0
- data/docs/patterns/flash_and_toast.md +57 -0
- data/docs/patterns/inline_edit.md +59 -0
- data/docs/patterns/queryable_collection.md +187 -0
- data/docs/patterns/resource_form.md +126 -0
- data/docs/rails_conventions.md +95 -0
- data/docs/rails_integration.md +514 -0
- data/lib/generators/nitro_kit/install_generator.rb +48 -0
- data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +67 -0
- data/lib/nitro_kit/engine.rb +20 -0
- data/lib/nitro_kit/installation.rb +671 -0
- data/lib/nitro_kit/migration_inventory.rb +345 -0
- data/lib/nitro_kit/upgrade_smoke_test.rb +403 -0
- data/lib/nitro_kit/version.rb +1 -1
- data/lib/nitro_kit.rb +5 -42
- data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +42 -0
- data/lib/tasks/nitro_kit_tasks.rake +79 -4
- data/plugins/nitro-kit/.codex-plugin/plugin.json +28 -0
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +67 -0
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/agents/openai.yaml +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +60 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/agents/openai.yaml +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +62 -0
- data/plugins/nitro-kit/skills/nitro-kit-ui/agents/openai.yaml +4 -0
- data/src/stylesheets/nitro_kit/components/accordion.css +119 -0
- data/src/stylesheets/nitro_kit/components/alert.css +82 -0
- data/src/stylesheets/nitro_kit/components/app_navigation.css +156 -0
- data/src/stylesheets/nitro_kit/components/app_shell.css +605 -0
- data/src/stylesheets/nitro_kit/components/appearance_picker.css +117 -0
- data/src/stylesheets/nitro_kit/components/auth_shell.css +8 -0
- data/src/stylesheets/nitro_kit/components/avatar.css +81 -0
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +80 -0
- data/src/stylesheets/nitro_kit/components/badge.css +76 -0
- data/src/stylesheets/nitro_kit/components/button.css +283 -0
- data/src/stylesheets/nitro_kit/components/button_group.css +53 -0
- data/src/stylesheets/nitro_kit/components/button_to.css +6 -0
- data/src/stylesheets/nitro_kit/components/card.css +70 -0
- data/src/stylesheets/nitro_kit/components/checkbox.css +160 -0
- data/src/stylesheets/nitro_kit/components/checkbox_group.css +61 -0
- data/src/stylesheets/nitro_kit/components/combobox.css +148 -0
- data/src/stylesheets/nitro_kit/components/command_palette.css +255 -0
- data/src/stylesheets/nitro_kit/components/container.css +25 -0
- data/src/stylesheets/nitro_kit/components/control_group.css +149 -0
- data/src/stylesheets/nitro_kit/components/danger_zone.css +54 -0
- data/src/stylesheets/nitro_kit/components/data_section.css +40 -0
- data/src/stylesheets/nitro_kit/components/details_table.css +53 -0
- data/src/stylesheets/nitro_kit/components/dialog.css +80 -0
- data/src/stylesheets/nitro_kit/components/dropdown.css +152 -0
- data/src/stylesheets/nitro_kit/components/dropzone.css +198 -0
- data/src/stylesheets/nitro_kit/components/empty_state.css +53 -0
- data/src/stylesheets/nitro_kit/components/field.css +79 -0
- data/src/stylesheets/nitro_kit/components/field_group.css +6 -0
- data/src/stylesheets/nitro_kit/components/fieldset.css +28 -0
- data/src/stylesheets/nitro_kit/components/flex.css +455 -0
- data/src/stylesheets/nitro_kit/components/form_section.css +71 -0
- data/src/stylesheets/nitro_kit/components/grid.css +308 -0
- data/src/stylesheets/nitro_kit/components/icon.css +26 -0
- data/src/stylesheets/nitro_kit/components/input.css +80 -0
- data/src/stylesheets/nitro_kit/components/label.css +9 -0
- data/src/stylesheets/nitro_kit/components/layout.css +440 -0
- data/src/stylesheets/nitro_kit/components/page_header.css +61 -0
- data/src/stylesheets/nitro_kit/components/pagination.css +119 -0
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +41 -0
- data/src/stylesheets/nitro_kit/components/palette.css +113 -0
- data/src/stylesheets/nitro_kit/components/progressive_image.css +139 -0
- data/src/stylesheets/nitro_kit/components/radio_button.css +127 -0
- data/src/stylesheets/nitro_kit/components/radio_button_group.css +143 -0
- data/src/stylesheets/nitro_kit/components/rich_text_area.css +21 -0
- data/src/stylesheets/nitro_kit/components/select.css +70 -0
- data/src/stylesheets/nitro_kit/components/settings_layout.css +100 -0
- data/src/stylesheets/nitro_kit/components/sheet.css +100 -0
- data/src/stylesheets/nitro_kit/components/stat_grid.css +51 -0
- data/src/stylesheets/nitro_kit/components/switch.css +152 -0
- data/src/stylesheets/nitro_kit/components/table.css +116 -0
- data/src/stylesheets/nitro_kit/components/tabs.css +138 -0
- data/src/stylesheets/nitro_kit/components/textarea.css +39 -0
- data/src/stylesheets/nitro_kit/components/toast.css +131 -0
- data/src/stylesheets/nitro_kit/components/toolbar.css +78 -0
- data/src/stylesheets/nitro_kit/components/tooltip.css +124 -0
- data/src/stylesheets/nitro_kit/components/typeset.css +191 -0
- data/src/stylesheets/nitro_kit/layers.css +7 -0
- data/src/stylesheets/nitro_kit/reset.css +192 -0
- data/src/stylesheets/nitro_kit/tokens.css +198 -0
- metadata +182 -51
- data/Rakefile +0 -8
- data/app/assets/tailwind/application.css +0 -49
- data/app/components/nitro_kit/datepicker.rb +0 -9
- data/app/helpers/nitro_kit/accordion_helper.rb +0 -9
- data/app/helpers/nitro_kit/alert_helper.rb +0 -11
- data/app/helpers/nitro_kit/avatar_helper.rb +0 -13
- data/app/helpers/nitro_kit/badge_helper.rb +0 -13
- data/app/helpers/nitro_kit/button_group_helper.rb +0 -9
- data/app/helpers/nitro_kit/button_helper.rb +0 -49
- data/app/helpers/nitro_kit/card_helper.rb +0 -9
- data/app/helpers/nitro_kit/checkbox_helper.rb +0 -50
- data/app/helpers/nitro_kit/combobox_helper.rb +0 -9
- data/app/helpers/nitro_kit/datepicker_helper.rb +0 -9
- data/app/helpers/nitro_kit/dialog_helper.rb +0 -9
- data/app/helpers/nitro_kit/dropdown_helper.rb +0 -9
- data/app/helpers/nitro_kit/field_group_helper.rb +0 -9
- data/app/helpers/nitro_kit/field_helper.rb +0 -9
- data/app/helpers/nitro_kit/fieldset_helper.rb +0 -9
- data/app/helpers/nitro_kit/form_helper.rb +0 -13
- data/app/helpers/nitro_kit/icon_helper.rb +0 -9
- data/app/helpers/nitro_kit/input_helper.rb +0 -37
- data/app/helpers/nitro_kit/label_helper.rb +0 -19
- data/app/helpers/nitro_kit/pagination_helper.rb +0 -46
- data/app/helpers/nitro_kit/radio_button_helper.rb +0 -23
- data/app/helpers/nitro_kit/select_helper.rb +0 -24
- data/app/helpers/nitro_kit/switch_helper.rb +0 -9
- data/app/helpers/nitro_kit/table_helper.rb +0 -9
- data/app/helpers/nitro_kit/tabs_helper.rb +0 -9
- data/app/helpers/nitro_kit/textarea_helper.rb +0 -9
- data/app/helpers/nitro_kit/toast_helper.rb +0 -25
- data/app/helpers/nitro_kit/tooltip_helper.rb +0 -9
- data/app/javascript/controllers/nk/accordion_controller.js +0 -18
- data/app/javascript/controllers/nk/datepicker_controller.js +0 -5
- data/app/javascript/controllers/nk/switch_controller.js +0 -32
- data/lib/generators/nitro_kit/component_generator.rb +0 -84
- data/lib/nitro_kit/schema_builder.rb +0 -129
- data/lib/nitro_kit/variants.rb +0 -21
data/STYLE_GUIDE.md
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# Nitro Kit 2.0 component style guide
|
|
2
|
+
|
|
3
|
+
Nitro Kit is a gem-owned, Phlex-only UI system for Rails. Its public surface is typed Ruby composition, self-describing markup, and static CSS driven by custom properties.
|
|
4
|
+
|
|
5
|
+
`NitroKit` extends `Phlex::Kit`. Applications should include it once in their
|
|
6
|
+
base Phlex component and use capitalized Kit methods as the primary composition
|
|
7
|
+
syntax:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class ApplicationComponent < Phlex::HTML
|
|
11
|
+
include NitroKit
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class SaveButton < ApplicationComponent
|
|
15
|
+
def view_template
|
|
16
|
+
Button("Save", variant: :primary)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The explicit `render NitroKit::Button.new(...)` form remains supported and is
|
|
22
|
+
required when constructing a component object for another API. Kit methods
|
|
23
|
+
render immediately and only work from a Phlex context; they do not work in ERB.
|
|
24
|
+
|
|
25
|
+
## Principles
|
|
26
|
+
|
|
27
|
+
- Prefer the smallest obvious Ruby API.
|
|
28
|
+
- Compose components directly with Phlex.
|
|
29
|
+
- Make invalid component vocabulary impossible to render silently.
|
|
30
|
+
- Keep state and component identity visible in markup.
|
|
31
|
+
- Preserve native HTML and Rails semantics.
|
|
32
|
+
- Let applications customize themes and compose product-specific UI without editing Nitro internals.
|
|
33
|
+
- Keep behavior minimal, progressive, and Turbo-safe.
|
|
34
|
+
|
|
35
|
+
## File layout
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
app/components/nitro_kit/ # gem-owned atoms, layouts, blocks
|
|
39
|
+
app/javascript/controllers/nk/ # gem-owned Stimulus behavior
|
|
40
|
+
src/stylesheets/nitro_kit/ # plain CSS authoring sources
|
|
41
|
+
app/assets/stylesheets/ # generated browser-ready distribution CSS
|
|
42
|
+
test/components/ # focused render/contract tests
|
|
43
|
+
test/dummy/app/components/gallery/ # Phlex gallery pages
|
|
44
|
+
test/integration/ # catalog-driven route coverage
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Do not add component helper modules or copied-component generators.
|
|
48
|
+
|
|
49
|
+
## Component anatomy
|
|
50
|
+
|
|
51
|
+
Use explicit public keywords and pass an internal attribute bundle to the base component:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
module NitroKit
|
|
55
|
+
class Container < Component
|
|
56
|
+
SIZES = %i[sm md lg xl].freeze
|
|
57
|
+
|
|
58
|
+
def initialize(
|
|
59
|
+
size:,
|
|
60
|
+
id: nil,
|
|
61
|
+
html: {},
|
|
62
|
+
aria: {},
|
|
63
|
+
data: {},
|
|
64
|
+
desperately_need_a_class: nil
|
|
65
|
+
)
|
|
66
|
+
size = validate_choice!(:size, size, SIZES)
|
|
67
|
+
|
|
68
|
+
super(
|
|
69
|
+
component: :container,
|
|
70
|
+
attributes: { id: }.compact,
|
|
71
|
+
html:,
|
|
72
|
+
aria:,
|
|
73
|
+
data:,
|
|
74
|
+
size:,
|
|
75
|
+
desperately_need_a_class:
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def view_template
|
|
80
|
+
div(**root_attributes) { yield if block_given? }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
These base boundaries are settled for 2.0: explicit component options, deliberate native-attribute bags, reserved Nitro identity, validated closed vocabularies, and one centralized class escape hatch.
|
|
87
|
+
|
|
88
|
+
## Options and native attributes
|
|
89
|
+
|
|
90
|
+
Component semantics are explicit keywords. Do not use a broad `**attrs` or `**options` public argument.
|
|
91
|
+
|
|
92
|
+
Common element semantics can be first-class keywords when central to the component: `id:`, `href:`, `type:`, `name:`, `value:`, `disabled:`, `required:`, and similar.
|
|
93
|
+
|
|
94
|
+
Less common native attributes use:
|
|
95
|
+
|
|
96
|
+
- `html:` for ordinary attributes.
|
|
97
|
+
- `aria:` for ARIA attributes.
|
|
98
|
+
- `data:` for non-reserved application data and additive Stimulus controllers/actions.
|
|
99
|
+
|
|
100
|
+
Reject `class` and `style`, including nested in `html:`. Reject every spelling of Nitro-reserved data keys, including symbol/string and dashed/underscored forms.
|
|
101
|
+
|
|
102
|
+
Nitro-owned data cannot be replaced through the public boundary. Collisions raise, except user Stimulus `controller` and `action` values, which compose deterministically with Nitro-owned values.
|
|
103
|
+
|
|
104
|
+
`Component::COMPONENT_OWNED_DATA_ATTRIBUTES` lists the keys a component writes for itself through `attributes:` — `state`, `disabled`, `required`, `orientation`, `presentation`, `placement`, `layout`, and `field-type`. `Component::RESERVED_DATA_ATTRIBUTES` adds `nk`, `slot`, `variant`, `size`, `nk-escape`, and `enhanced`. Everything in the combined list is rejected from `data:`; the component-owned subset is the part a component may still set internally.
|
|
105
|
+
|
|
106
|
+
### Variant is an identity axis, not a style hook
|
|
107
|
+
|
|
108
|
+
A component root's `variant:` and `size:` are its identity axes, and only the base component emits them. Pass them through `super(variant:, size:)`; never write `data-variant` or `data-size` by hand on a root.
|
|
109
|
+
|
|
110
|
+
A slot may carry its own owned `data-variant` when the slot has variant identity of its own rather than merely inheriting the root's. Two precedents settle the shape:
|
|
111
|
+
|
|
112
|
+
- `Toast::Item` is a nested component. Its variant reaches `data-variant` through the ordinary root channel, and the parent attaches it to the `toast-item` slot.
|
|
113
|
+
- Dropdown `item` is a plain element, so it uses the base component's `slot_attributes(:item, variant:)` channel, which normalizes and owns the value exactly as a root does.
|
|
114
|
+
|
|
115
|
+
Use `slot_attributes(..., variant:)`; do not hand-merge a `variant` key into a slot's data bag. Both remain Nitro-owned: caller `data: { variant: }` is reserved and raises before it can reach a root or a slot. Do not introduce a slot `data-variant` that only restates the root's variant, and do not accept a public `variant:` on a slot that has no closed vocabulary of its own.
|
|
116
|
+
|
|
117
|
+
## Validation
|
|
118
|
+
|
|
119
|
+
Validate every closed vocabulary at construction time:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
@variant = validate_choice!(:variant, variant, VARIANTS)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Errors identify the invalid option or slot, the received value where useful, and the accepted vocabulary. Do not silently fall back.
|
|
126
|
+
|
|
127
|
+
Required slots and invalid slot combinations should raise as soon as the component can know them. Do not attempt to validate whole-page information architecture in the component kernel.
|
|
128
|
+
|
|
129
|
+
## Identity and slots
|
|
130
|
+
|
|
131
|
+
Every component root emits `data-nk`:
|
|
132
|
+
|
|
133
|
+
```html
|
|
134
|
+
<article data-nk="card"></article>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Every owned part has a component-qualified slot:
|
|
138
|
+
|
|
139
|
+
```html
|
|
140
|
+
<article data-nk="card">
|
|
141
|
+
<h2 data-slot="card-title">...</h2>
|
|
142
|
+
<div data-slot="card-body">...</div>
|
|
143
|
+
</article>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
A nested component can have both identities:
|
|
147
|
+
|
|
148
|
+
```html
|
|
149
|
+
<input data-nk="input" data-slot="field-control" />
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The parent supplies contextual slot identity. An atom such as `Label` must not globally identify itself as every parent's `label` slot.
|
|
153
|
+
|
|
154
|
+
Prefer direct-child contracts for component-owned structure. Do not style arbitrary application descendants merely because they appear inside a content slot.
|
|
155
|
+
|
|
156
|
+
## Compound components and content
|
|
157
|
+
|
|
158
|
+
Direct Phlex Kit composition replaces the old template-aware `builder do`
|
|
159
|
+
wrapper. Compound APIs should be ordinary Ruby methods that render into the
|
|
160
|
+
current Phlex context.
|
|
161
|
+
|
|
162
|
+
Named leaf slots may accept arbitrary application content. That is normal composition, not an escape.
|
|
163
|
+
|
|
164
|
+
When a fixed-order block exposes textual constructor keywords, expose matching deferred compound methods too:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
EmptyState(level: 3) do |empty|
|
|
168
|
+
empty.title { plain "No records for "; strong { "Production" } }
|
|
169
|
+
empty.description("Remove one or more filters and try again.")
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Constructor text and the matching compound method are two forms of the same region. Accept either one, reject both or repeated declarations, and let required regions be satisfied by either form. Constructor values and compound-method text remain non-blank strings; a compound-method block may render arbitrary Phlex content. Collect these declarations before rendering so the component's owned DOM order does not depend on caller order.
|
|
174
|
+
|
|
175
|
+
Do not add an untyped structural bypass. If a legitimate application-content boundary is missing, add the smallest named compound method supported by real composition evidence.
|
|
176
|
+
|
|
177
|
+
## Internationalization
|
|
178
|
+
|
|
179
|
+
Nitro owns no hardcoded user-facing English. Every string a person can read or
|
|
180
|
+
hear — visible copy, ARIA names, live-region announcements, validation
|
|
181
|
+
messages — comes from `config/locales/en.yml`, which the engine loads
|
|
182
|
+
automatically and which is the single authoritative source of the shipped
|
|
183
|
+
copy.
|
|
184
|
+
|
|
185
|
+
Components call `I18n.t` with a fully qualified key and no `:default`:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
I18n.t("nitro_kit.dropzone.prompt")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Do not duplicate copy into a `default:` argument or a Ruby constant; the
|
|
192
|
+
locale file would drift from it. Keys live under one `nitro_kit:` namespace and
|
|
193
|
+
are grouped by component (`nitro_kit.<component>.<key>`), with nested `status:`
|
|
194
|
+
and `errors:` groups where a component has many. Counted strings use ordinary
|
|
195
|
+
`one`/`other` pluralization and `%{...}` interpolation.
|
|
196
|
+
|
|
197
|
+
Translated text that is a public option stays a public option. Resolve the
|
|
198
|
+
default in the keyword itself so an application override still wins:
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
def initialize(label: I18n.t("nitro_kit.pagination.label"))
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Stimulus controllers never contain user-facing English as behavior. The Ruby
|
|
205
|
+
component translates each string the controller needs and emits it through the
|
|
206
|
+
Stimulus values API:
|
|
207
|
+
|
|
208
|
+
```html
|
|
209
|
+
<div data-nk--dropzone-queued-value="Queued">
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The controller reads the value and keeps the shipped English literal only as an
|
|
213
|
+
inert fallback for markup assembled without the component. Runtime
|
|
214
|
+
interpolation uses the same `%{name}` placeholders the locale file declares, so
|
|
215
|
+
a translator sees one grammar in both languages.
|
|
216
|
+
|
|
217
|
+
## Class escape
|
|
218
|
+
|
|
219
|
+
Nitro components never emit or depend on classes. The only exception is:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
desperately_need_a_class: "external-widget-hook"
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
It must produce both the class and `data-nk-escape="class"`. Blank or non-string values raise. Implement this once in the base component.
|
|
226
|
+
|
|
227
|
+
## CSS architecture
|
|
228
|
+
|
|
229
|
+
Author plain CSS in split source files and generate one committed `nitro_kit.css` distribution asset.
|
|
230
|
+
|
|
231
|
+
Declare deterministic layers:
|
|
232
|
+
|
|
233
|
+
```css
|
|
234
|
+
@layer nitro-kit.tokens,
|
|
235
|
+
nitro-kit.reset,
|
|
236
|
+
nitro-kit.base,
|
|
237
|
+
nitro-kit.variant,
|
|
238
|
+
nitro-kit.size,
|
|
239
|
+
nitro-kit.state,
|
|
240
|
+
nitro-kit.compound;
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Every Nitro selector uses `:where()`:
|
|
244
|
+
|
|
245
|
+
```css
|
|
246
|
+
@layer nitro-kit.base {
|
|
247
|
+
:where([data-nk="button"]) {
|
|
248
|
+
background: var(--_nk-button-background);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Variants assign private values and generic state consumes them:
|
|
254
|
+
|
|
255
|
+
```css
|
|
256
|
+
@layer nitro-kit.variant {
|
|
257
|
+
:where([data-nk="button"][data-variant="primary"]) {
|
|
258
|
+
--_nk-button-background: var(--nk-color-primary);
|
|
259
|
+
--_nk-button-hover-background: var(--nk-color-primary-hover);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@layer nitro-kit.state {
|
|
264
|
+
@media (hover: hover) {
|
|
265
|
+
:where([data-nk="button"]:hover) {
|
|
266
|
+
background: var(--_nk-button-hover-background);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Never target an unqualified `[data-slot]`. Never use `transition: all`.
|
|
273
|
+
|
|
274
|
+
## Tokens and themes
|
|
275
|
+
|
|
276
|
+
Public `--nk-*` variables cover themeable decisions: semantic colors, paired foregrounds, typography, spacing, radii, control dimensions, shadows, borders, motion, and content widths.
|
|
277
|
+
|
|
278
|
+
Buttons and data-entry controls share control heights, radii, border geometry, focus treatment, and disabled treatment, but they do not share one surface role. Inputs, selects, textareas, and unchecked controls use the general surface tokens. The raised default Button uses the public `--nk-button-default-background`, `--nk-button-default-hover-background`, `--nk-button-default-foreground`, and `--nk-button-default-border` tokens. Button-like controls such as the native file selector may consume the same treatment; do not make ordinary data-entry surfaces depend on it.
|
|
279
|
+
|
|
280
|
+
Private `--_nk-*` variables coordinate component mechanics and are not a theme API.
|
|
281
|
+
|
|
282
|
+
The browser asset order is optional third-party base CSS (such as Lexxy) → optional Tailwind adapter → generated Nitro Kit CSS → compiled Tailwind CSS when present → unlayered application theme overrides. Without Tailwind, load Nitro Kit before application overrides. `AppearanceBootstrap` precedes every stylesheet. Never edit the generated distribution asset; keep the complete supported-token inventory and copyable recipes in [`docs/customization.md`](docs/customization.md).
|
|
283
|
+
|
|
284
|
+
Do not turn structural keywords, percentages, zero values, grid mechanics, or layout breakpoints into theme tokens. Responsive layout breakpoints are fixed component API, not customizable `--nk-*` values.
|
|
285
|
+
|
|
286
|
+
Before JavaScript connects, `:root` follows `prefers-color-scheme`. Explicit `[data-theme="light"]` and `[data-theme="dark"]` contracts override it, use the same public tokens, and set the matching `color-scheme`. `data-theme` always describes the resolved light or dark appearance; store a light, dark, or system preference separately. Render Nitro's appearance bootstrap in `head` before stylesheet links so persisted explicit choices do not flash. Its fixed, hashable script body owns one idempotent document-level runtime; defaults live in script data rather than interpolated JavaScript. The runtime validates and persists preference, resolves system appearance, listens for media and cross-tab storage changes even when no picker is present, and broadcasts changes. Any number of appearance pickers only request changes and subscribe. The bootstrap must support nonce- and hash-based host CSPs and tolerate unavailable storage.
|
|
287
|
+
|
|
288
|
+
Customization tools may read, preview, and export documented public tokens. Model customizer state as an immutable, explicitly versioned value object with closed named choices and readable URL parameters; never serialize an opaque arbitrary-token blob. Exports use stable selector and declaration ordering, cover light, dark, and system fallback, and contain only documented public variables. Structural preview choices may emit copyable component-composition examples, but never component implementations. Customization tools must not expose private `--_nk-*` values, edit the generated distribution asset, generate component copies, or turn arbitrary CSS into a Nitro contract.
|
|
289
|
+
|
|
290
|
+
The optional `nitro_kit-tailwind-v4.css` adapter is a separate asset. It may map Nitro values into Tailwind v4 theme variables, but Tailwind compilation, source detection, and utilities remain application concerns. Do not add Tailwind as a Nitro runtime dependency.
|
|
291
|
+
|
|
292
|
+
## Baseline
|
|
293
|
+
|
|
294
|
+
Nitro CSS never relies on Tailwind Preflight. Nitro ships its own global preflight in `src/stylesheets/nitro_kit/reset.css`, entirely inside the `nitro-kit.reset` cascade layer. It normalizes the whole page — box sizing, margins and padding, borders, root typography, headings, form-control typography and appearance, placeholders, tables, replaced elements, and native-control quirks — so arbitrary content inside a Nitro component behaves the same as Nitro's own markup.
|
|
295
|
+
|
|
296
|
+
Because it is layered, unlayered application CSS always wins without needing extra specificity.
|
|
297
|
+
|
|
298
|
+
Two deliberate exceptions to Preflight parity:
|
|
299
|
+
|
|
300
|
+
- `min-width: 0` applies only to `[data-nk]` roots and owned `[data-slot]` parts.
|
|
301
|
+
- `list-style: none` applies only to Nitro-owned lists. Prose lists keep real markers; `Typeset` states its own list styles in `nitro-kit.base`.
|
|
302
|
+
|
|
303
|
+
## Layout sizing
|
|
304
|
+
|
|
305
|
+
Parents own external placement and available width. Components own intrinsic geometry.
|
|
306
|
+
|
|
307
|
+
- Naturally stretchable: inputs, textareas, selects, tables, broad surfaces.
|
|
308
|
+
- Naturally intrinsic: buttons, badges, avatars, icons, switches.
|
|
309
|
+
- `Flex` decides direction, alignment, distribution, wrapping, and gap through closed responsive values.
|
|
310
|
+
- `Grid` decides a 1–12-column equal-track collection and gap through the same responsive grammar.
|
|
311
|
+
- `Container` decides available width through a closed size enumeration.
|
|
312
|
+
|
|
313
|
+
`VStack` and `HStack` are not public components. Use `Flex` with an explicit `dir:`:
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
Flex(dir: :col, gap: 4, align: :stretch) do
|
|
317
|
+
render ProfileForm.new
|
|
318
|
+
render AccountActions.new
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
Flex(
|
|
322
|
+
dir: "col md:row",
|
|
323
|
+
gap: "3 md:6",
|
|
324
|
+
align: "stretch md:center",
|
|
325
|
+
justify: "start md:between"
|
|
326
|
+
) do
|
|
327
|
+
render WorkspaceSummary.new
|
|
328
|
+
render WorkspaceActions.new
|
|
329
|
+
end
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Every responsive property accepts either one scalar or its own whitespace-separated string in the grammar `BASE sm:VALUE md:VALUE lg:VALUE xl:VALUE 2xl:VALUE`. The unprefixed base is required and applies mobile-first. Prefixes are fixed minimum widths: `sm` 40rem, `md` 48rem, `lg` 64rem, `xl` 80rem, and `2xl` 96rem. Reject duplicate prefixes, unknown prefixes, missing bases, and values outside the property's closed vocabulary. Normalize output to base then breakpoint order and mirror that exact string in the owned data attribute.
|
|
333
|
+
|
|
334
|
+
`Flex.new(dir:, gap: 4, align: :start, justify: :start, wrap: :nowrap)` accepts directions `row col row-reverse col-reverse`, alignments `start center end stretch baseline`, justifications `start center end between around evenly`, wraps `nowrap wrap wrap-reverse`, and gaps `0 1 2 3 4 5 6 8 10 12 16`. Responsive strings and rendered attributes use the hyphenated tokens; idiomatic scalar symbols use `:row_reverse`, `:col_reverse`, and `:wrap_reverse` and normalize to them. `Grid.new(cols:, gap: 4)` accepts columns `1..12` and the same gaps. Do not add a generic utility parser, Tailwind runtime, arbitrary/custom breakpoints, max/range/container prefixes, or property values outside these lists.
|
|
335
|
+
|
|
336
|
+
## Application shells
|
|
337
|
+
|
|
338
|
+
Application shells own responsive chrome, not application policy. Every shell has exactly one navigation tree and one main region, plus optional unique brand and topbar regions. Account actions compose inside the topbar. Navigation uses named header, body, section, footer, spacer, divider, and item parts. Keep product routes and current-destination policy in the caller, and use native navigation and landmark elements.
|
|
339
|
+
|
|
340
|
+
Nitro owns the shell breakpoint, fixed/sticky placement, overscroll background, independent navigation scrolling, and narrow-screen disclosure behavior. Sidebar places brand/navigation in a sticky desktop sidebar; topbar places brand/navigation/actions in a sticky desktop header; hybrid combines sidebar navigation with the header. All three reflow the one navigation tree into the narrow drawer. Do not add arbitrary breakpoint, sticky-region, or route-registry options. Reflect open state through ARIA and `data-state`; expose a skip link and labelled landmarks; trap, move, and restore focus for the narrow drawer; close on Escape, backdrop, outside activation, and Turbo navigation; and release inert state, scroll locks, and listeners on disconnect. Visible desktop navigation must not retain drawer semantics, `inert`, or `aria-hidden`. Keep shell-specific public tokens semantic and limited to canvas/sidebar colors, border, sidebar width, and topbar height.
|
|
341
|
+
|
|
342
|
+
## Rails forms
|
|
343
|
+
|
|
344
|
+
Use Rails `form_with` from Phlex with `NitroKit::FormBuilder`:
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
form_with(model:, builder: NitroKit::FormBuilder) do |form|
|
|
348
|
+
form.field(:email)
|
|
349
|
+
form.submit
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Keep Rails naming, IDs, values, CSRF, validations, multipart behavior, and error semantics. Refactor the builder for direct Phlex; do not restore `nk_form_with` or `nk_form_for`.
|
|
354
|
+
|
|
355
|
+
Default form composition uses `Fieldset` and `FieldGroup` where semantics call for them.
|
|
356
|
+
|
|
357
|
+
## Optional Rails integrations
|
|
358
|
+
|
|
359
|
+
Lexxy is Nitro Kit's preferred Action Text editor. `FormBuilder#field(as:
|
|
360
|
+
:rich_text)` must preserve Lexxy's native Action Text naming, hidden input,
|
|
361
|
+
attachments, prompts, validation, and editor behavior while providing the
|
|
362
|
+
ordinary Nitro Field label, description, error, theme, and layout contract.
|
|
363
|
+
Nitro does not fork Lexxy or own its JavaScript.
|
|
364
|
+
|
|
365
|
+
Active Storage components preserve native inputs and ordinary form submission without JavaScript. Keep upload limits and image sizes explicit and validated. File drops are an enhancement to a labelled, keyboard-operable native input; expose progress with native progress semantics and announce status and errors. Progressive images expose exactly one accessible image while placeholders remain decorative. Reflect asynchronous progress and errors through owned state and accessible native elements.
|
|
366
|
+
|
|
367
|
+
Do not add mandatory Dropzone.js, Ransack, or image-processing dependencies to Nitro Kit. A generic component may document an app-level or gallery adapter for an optional gem, but Nitro must not absorb that gem's query, route, or authorization policy.
|
|
368
|
+
|
|
369
|
+
## Stimulus and Hotwire
|
|
370
|
+
|
|
371
|
+
Native HTML and CSS own behavior when they already provide the required semantics. Stimulus adds the smallest missing enhancement.
|
|
372
|
+
|
|
373
|
+
- Use `details`/`summary` for disclosure, declarative `command`/`commandfor` for dialogs, native Popover for dropdown visibility, and CSS hover/focus for tooltips.
|
|
374
|
+
- Do not mirror browser-owned open state into `data-state`, `aria-expanded`, or hidden attributes. Use targets and values only for state Nitro genuinely owns.
|
|
375
|
+
- The exception is state HTML cannot express as an attribute at all. A checkbox's `indeterminate` is a DOM property with no markup form, so `Checkbox` mounts `nk--checkable` only when `indeterminate: true`, and that controller's whole scope is applying the property and owning the matching `data-state="indeterminate"`. Ordinary checked state stays native, with no controller and no mirrored `data-state`. Do not widen a controller past the one state the browser cannot express.
|
|
376
|
+
- Keep native state selectors such as `[open]` and `:popover-open` authoritative in CSS.
|
|
377
|
+
- Clean up every external listener, timer, observer, and other resource in `disconnect`.
|
|
378
|
+
- Avoid duplicate initialization through Turbo morphs.
|
|
379
|
+
- Test keyboard behavior and Turbo Drive/Frame/Stream/morph lifecycles.
|
|
380
|
+
|
|
381
|
+
## Interface quality
|
|
382
|
+
|
|
383
|
+
- Omit `variant:` for ordinary actions so `Button` uses its `:default` treatment. Reserve `:ghost` for deliberately low-emphasis interface chrome such as compact dismiss, pagination, toolbar, and shell-navigation controls; it is not a generic secondary-action style.
|
|
384
|
+
- Headings use balanced wrapping; short descriptions use pretty wrapping.
|
|
385
|
+
- Dynamic numeric columns use tabular numerals.
|
|
386
|
+
- Interactive hit areas are at least 40×40px without overlapping.
|
|
387
|
+
- Use concentric radii for closely nested surfaces.
|
|
388
|
+
- Prefer subtle layered shadows for elevated surfaces and borders for true separators/form outlines.
|
|
389
|
+
- Interactive transitions are interruptible and declare exact transitioned properties.
|
|
390
|
+
- Respect `prefers-reduced-motion`.
|
|
391
|
+
- Use `will-change` only after observing a compositing problem.
|
|
392
|
+
|
|
393
|
+
## Testing checklist
|
|
394
|
+
|
|
395
|
+
Every component includes:
|
|
396
|
+
|
|
397
|
+
- Direct-Phlex rendering coverage.
|
|
398
|
+
- Every option and invalid value.
|
|
399
|
+
- Reserved attribute rejection.
|
|
400
|
+
- Class escape output.
|
|
401
|
+
- Structural and component-specific accessibility assertions.
|
|
402
|
+
- A gallery combination page with meaningful permutations.
|
|
403
|
+
- Long, missing optional, disabled, validation/error, dark, and narrow-width examples where relevant.
|
|
404
|
+
- Behavior tests for interactive components.
|
|
405
|
+
|
|
406
|
+
The gallery uses explicit Phlex page classes and `Gallery::Catalog`. Do not add ERB component examples or infer routes from filenames. Every `Gallery::Example` pairs Preview and Code tabs. Keep the preview in the block passed to the gallery helper so `Gallery::SourceCode` can extract, highlight, and copy its executable Ruby body; use a concrete method source for inherited flow wrappers instead of duplicating snippets.
|
|
407
|
+
|
|
408
|
+
Complete application examples combine shells, navigation, forms, tables, uploads, images, and overlays in sidebar, topbar, and hybrid layouts. Appearance coverage includes zero, one, and multiple pickers; light, dark, cross-tab updates, and live system preference changes. Theme coverage proves the contract rather than a tool: the documented token set, the set declared in `tokens.css`, and the set the bundled stylesheet serves are the same set, component CSS consumes only declared public tokens, and scoped `--nk-*` overrides on an application-owned wrapper reach Nitro descendants through inheritance.
|
|
409
|
+
|
|
410
|
+
Ship a public customization guide and align the README and Rails integration docs whenever appearance, theme tokens, or shell composition changes. Document token load order, scoped light/dark overrides, CSP setup, customizer-export installation, and copyable Phlex examples; architecture notes alone are not user documentation.
|
|
411
|
+
|
|
412
|
+
Browser verification may live outside Minitest, but it must enumerate the explicit gallery catalog, prove code-source parity and escaping, and exercise interactive behavior through Turbo lifecycles.
|
|
413
|
+
|
|
414
|
+
## Component completion checklist
|
|
415
|
+
|
|
416
|
+
- Explicit public options.
|
|
417
|
+
- Enumerated options validated.
|
|
418
|
+
- Stable `data-nk` root.
|
|
419
|
+
- Component-qualified slots.
|
|
420
|
+
- Static CSS source and generated bundle updated.
|
|
421
|
+
- No internal class/style output.
|
|
422
|
+
- No Tailwind runtime assumption.
|
|
423
|
+
- Direct-Phlex tests and gallery examples.
|
|
424
|
+
- Rails/Hotwire behavior preserved where applicable.
|
|
425
|
+
- Relevant `tk` ticket updated with verification notes.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Nitro Kit Tailwind CSS v4 adapter.
|
|
3
|
+
* Load this before both Nitro Kit and compiled Tailwind stylesheets.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
@layer properties, theme, base, nitro-kit, components, utilities;
|
|
7
|
+
|
|
8
|
+
@layer nitro-kit.tokens {
|
|
9
|
+
:where(:root, [data-theme]) {
|
|
10
|
+
--font-sans: var(--nk-font-sans);
|
|
11
|
+
--font-mono: var(--nk-font-mono);
|
|
12
|
+
--spacing: var(--nk-space);
|
|
13
|
+
|
|
14
|
+
--color-background: var(--nk-color-canvas);
|
|
15
|
+
--color-foreground: var(--nk-color-foreground);
|
|
16
|
+
--color-muted: var(--nk-color-muted);
|
|
17
|
+
--color-muted-content: var(--nk-color-muted-foreground);
|
|
18
|
+
--color-border: var(--nk-color-border);
|
|
19
|
+
--color-ring: var(--nk-color-focus);
|
|
20
|
+
--color-primary: var(--nk-color-primary);
|
|
21
|
+
--color-primary-foreground: var(--nk-color-primary-foreground);
|
|
22
|
+
--color-destructive: var(--nk-color-danger);
|
|
23
|
+
--color-destructive-content: var(--nk-color-danger-content);
|
|
24
|
+
--color-destructive-foreground: var(--nk-color-danger-foreground);
|
|
25
|
+
|
|
26
|
+
--radius-sm: var(--nk-radius-sm);
|
|
27
|
+
--radius-md: var(--nk-radius-md);
|
|
28
|
+
--radius-lg: var(--nk-radius-lg);
|
|
29
|
+
--radius-xl: var(--nk-radius-xl);
|
|
30
|
+
|
|
31
|
+
--shadow-sm: var(--nk-shadow-sm);
|
|
32
|
+
--shadow-md: var(--nk-shadow-md);
|
|
33
|
+
--shadow-lg: var(--nk-shadow-lg);
|
|
34
|
+
|
|
35
|
+
--default-transition-duration: var(--nk-duration-normal);
|
|
36
|
+
--default-transition-timing-function: var(--nk-ease);
|
|
37
|
+
}
|
|
38
|
+
}
|