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
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Initialize Nitro Kit 2 in this Rails application
|
|
2
|
+
|
|
3
|
+
Finish the application-specific Nitro Kit 2 setup. Do not use Nitro Kit 1.x
|
|
4
|
+
APIs or examples.
|
|
5
|
+
|
|
6
|
+
1. Run `bundle show nitro_kit` and confirm the resolved version begins with
|
|
7
|
+
`2.`.
|
|
8
|
+
2. Read `docs/agent_guide.md` from that installed gem, then read the locally
|
|
9
|
+
installed `nitro-kit-rails`, `nitro-kit-hotwire`, and `nitro-kit-ui` skills.
|
|
10
|
+
3. Inspect the application before editing. Preserve established application
|
|
11
|
+
conventions unless they conflict with the requested Nitro Kit 2 setup.
|
|
12
|
+
4. Ensure an application base Phlex component includes `NitroKit` once and
|
|
13
|
+
product components inherit from it.
|
|
14
|
+
5. Re-run `bin/rails generate nitro_kit:install`. Ensure its application layout
|
|
15
|
+
setup has one appearance bootstrap before every stylesheet, then optional
|
|
16
|
+
third-party base styles, the optional Tailwind adapter, `nitro_kit`, compiled
|
|
17
|
+
Tailwind, and application token overrides in that order. Add flash toast
|
|
18
|
+
rendering when the application uses it.
|
|
19
|
+
6. Ensure Turbo and Stimulus are wired and the normal Stimulus loader can
|
|
20
|
+
discover the gem-owned `nk--*` controllers. Never copy Nitro components,
|
|
21
|
+
helpers, or controllers into the application.
|
|
22
|
+
7. Remove confirmed Nitro Kit 1.x shadows such as application-owned
|
|
23
|
+
`NitroKit` components, `nk_*` helpers, or `controllers/nk` only when this
|
|
24
|
+
task is authorized to migrate the application.
|
|
25
|
+
8. Use ordinary Rails routes, models, forms, and server-rendered HTML. Follow
|
|
26
|
+
the installed Rails and Hotwire guidance for new work. During a migration,
|
|
27
|
+
replace an existing control only when Nitro Kit 2 has a genuine semantic
|
|
28
|
+
and behavioral equivalent. Otherwise preserve it as application-owned
|
|
29
|
+
Rails and semantic HTML; never downgrade specialized behavior or retain
|
|
30
|
+
copied Nitro Kit 1.x source as the fallback.
|
|
31
|
+
9. Run `bin/rails nitro_kit:doctor`, fix actionable failures, and run the
|
|
32
|
+
application's relevant tests.
|
|
33
|
+
|
|
34
|
+
Report what you changed, any existing convention you deliberately preserved,
|
|
35
|
+
any unsupported control recorded as a Nitro Kit coverage gap, and any warning
|
|
36
|
+
that still needs a product decision.
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# Migrating a Nitro Kit 1.x application
|
|
2
|
+
|
|
3
|
+
Treat a 1.x migration as a product-flow review, not a helper rename. Nitro Kit
|
|
4
|
+
2 deliberately removed copied components, `nk_*` helpers, application-owned
|
|
5
|
+
`controllers/nk`, and unrestricted utility-class customization.
|
|
6
|
+
|
|
7
|
+
## Install the 2.0 alpha before migrating
|
|
8
|
+
|
|
9
|
+
Add the 2.0 alpha prerelease to the application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "nitro_kit", "2.0.0.alpha.2"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Bundler records the exact released version in `Gemfile.lock`; commit `Gemfile`
|
|
16
|
+
and `Gemfile.lock` together. Before upgrading during the migration, review the
|
|
17
|
+
changelog, run `bundle update nitro_kit`, rerun the installer and doctor, and
|
|
18
|
+
exercise the converted flows before committing the updated lockfile.
|
|
19
|
+
Production applications should use the released gem and a committed lockfile
|
|
20
|
+
rather than a moving Git branch.
|
|
21
|
+
|
|
22
|
+
## Inventory behavior before editing
|
|
23
|
+
|
|
24
|
+
1. List every `nk_*` helper, `NitroKit::*` component, copied Nitro source file,
|
|
25
|
+
and `controllers/nk` controller.
|
|
26
|
+
2. Group usage by user flow: authentication, settings, collection browsing,
|
|
27
|
+
mobile navigation, editing, destructive actions, notifications, and
|
|
28
|
+
copy/share controls.
|
|
29
|
+
3. Record behavior that must survive: native element, submitted method and
|
|
30
|
+
parameters, Turbo target, accessible name and description, focus behavior,
|
|
31
|
+
narrow-screen presentation, empty/error state, and visual density.
|
|
32
|
+
4. Capture representative wide and narrow screenshots before conversion.
|
|
33
|
+
|
|
34
|
+
If the Nitro Kit MCP catalog is connected, search it by workflow after this
|
|
35
|
+
inventory — for example `mobile transcript navigation`, `settings form`, or
|
|
36
|
+
`empty query results`. Do not search only for an old component name. MCP can
|
|
37
|
+
deliver complete compositions; the installed gem remains the authority for
|
|
38
|
+
the free component contract.
|
|
39
|
+
|
|
40
|
+
## Select semantics before atoms
|
|
41
|
+
|
|
42
|
+
Map each flow to the highest-level matching 2.x component first:
|
|
43
|
+
|
|
44
|
+
| Existing need | Begin with |
|
|
45
|
+
| --------------------------------------- | --------------------------------------- |
|
|
46
|
+
| Sign-in or recovery card | `AuthShell` |
|
|
47
|
+
| Application navigation | `AppShell`, `AppNavigation` |
|
|
48
|
+
| Mobile contextual navigation or details | `Sheet` |
|
|
49
|
+
| Settings screen | `SettingsLayout`, `FormSection` |
|
|
50
|
+
| Empty collection card | `EmptyState` |
|
|
51
|
+
| Data collection | `DataSection`, `Table`, `PaginationBar` |
|
|
52
|
+
| Destructive settings | `DangerZone`, `Dialog`, `ButtonTo` |
|
|
53
|
+
| Joined copy or filter controls | `ControlGroup` |
|
|
54
|
+
|
|
55
|
+
Only then replace remaining atoms. Common direct mappings include:
|
|
56
|
+
|
|
57
|
+
- `nk_button_link_to` → `NitroKit::Button.new(..., href:)`
|
|
58
|
+
- `nk_button_to` → `NitroKit::ButtonTo.new(..., href:, method:)`
|
|
59
|
+
- `nk_form_with` → Rails `form_with(..., builder: NitroKit::FormBuilder)`
|
|
60
|
+
- block-wrapped tooltips → a linked Button trigger or Tooltip `as: :custom`
|
|
61
|
+
- copied mobile Sidebar → `Sheet`, not a plain disclosure
|
|
62
|
+
|
|
63
|
+
Do not preserve a Card merely because 1.x used one. Empty states,
|
|
64
|
+
authentication shells, settings regions, and data sections have stronger
|
|
65
|
+
semantics and more useful responsive behavior.
|
|
66
|
+
|
|
67
|
+
## Move ERB collections and yielded content into compound declarations
|
|
68
|
+
|
|
69
|
+
The ERB below is representative application-owned migration input, not a
|
|
70
|
+
Nitro Kit 2 API. Nitro Kit 2 has no ERB component bridge. Move the collection
|
|
71
|
+
to the Phlex component, then declare entries only inside their owning compound
|
|
72
|
+
region. Content formerly yielded by a partial belongs directly inside the
|
|
73
|
+
matching `panel` or `content` block.
|
|
74
|
+
|
|
75
|
+
### AppNavigation
|
|
76
|
+
|
|
77
|
+
ERB source — the call site owns the destination collection and the partial
|
|
78
|
+
iterates it:
|
|
79
|
+
|
|
80
|
+
```erb
|
|
81
|
+
<% destinations = [["Home", root_path], ["Projects", projects_path]] %>
|
|
82
|
+
<%= render "app_navigation", label: "Primary", destinations: destinations %>
|
|
83
|
+
|
|
84
|
+
<%# _app_navigation.html.erb %>
|
|
85
|
+
<nav aria-label="<%= label %>">
|
|
86
|
+
<ul>
|
|
87
|
+
<% destinations.each do |text, href| %>
|
|
88
|
+
<li><%= link_to text, href %></li>
|
|
89
|
+
<% end %>
|
|
90
|
+
</ul>
|
|
91
|
+
</nav>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Phlex destination — declare the collection before `AppNavigation`; consume it
|
|
95
|
+
inside the required `body` collection region:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
destinations = [["Home", root_path], ["Projects", projects_path]]
|
|
99
|
+
|
|
100
|
+
render NitroKit::AppNavigation.new(label: "Primary") do |navigation|
|
|
101
|
+
navigation.body do
|
|
102
|
+
destinations.each_with_index do |(text, href), index|
|
|
103
|
+
navigation.item(text, href:, current: index.zero?)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The executable minimal version is on the
|
|
110
|
+
[`AppNavigation` gallery page](/gallery/components/app-navigation#example-app-navigation-minimal).
|
|
111
|
+
|
|
112
|
+
### Dialog
|
|
113
|
+
|
|
114
|
+
ERB source — the call-site block is yielded inside the partial's panel:
|
|
115
|
+
|
|
116
|
+
```erb
|
|
117
|
+
<%= render "dialog", id: "transcript-details", title: "Transcript details" do %>
|
|
118
|
+
<p>The transcript was recorded at 09:42 UTC.</p>
|
|
119
|
+
<% end %>
|
|
120
|
+
|
|
121
|
+
<%# _dialog.html.erb %>
|
|
122
|
+
<button command="show-modal" commandfor="<%= id %>-panel">Details</button>
|
|
123
|
+
<dialog id="<%= id %>-panel">
|
|
124
|
+
<h2><%= title %></h2>
|
|
125
|
+
<%= yield %>
|
|
126
|
+
</dialog>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Phlex destination — trigger and panel declarations live inside `Dialog`; the
|
|
130
|
+
former yielded content lives inside `panel`:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
render NitroKit::Dialog.new(id: "transcript-details") do |dialog|
|
|
134
|
+
dialog.trigger("Details")
|
|
135
|
+
dialog.panel(title: "Transcript details") do
|
|
136
|
+
p { "The transcript was recorded at 09:42 UTC." }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Placement belongs to the parent. In the conversion that exposed this rule, a
|
|
142
|
+
`Flex` containing **Redact** and **Permalink** was followed by a `Dialog`
|
|
143
|
+
sibling, so the Dialog trigger started a second line. Put the Dialog root
|
|
144
|
+
inside the same no-wrap action cluster:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
Flex(dir: :row, gap: 1, align: :center, wrap: :nowrap) do
|
|
148
|
+
Button("Redact", size: :sm, variant: :destructive)
|
|
149
|
+
Button("Permalink", href: transcript_path(transcript), size: :sm)
|
|
150
|
+
|
|
151
|
+
Dialog(id: dom_id(transcript, :details)) do |dialog|
|
|
152
|
+
dialog.trigger("Details", size: :sm)
|
|
153
|
+
dialog.panel(title: "Transcript details") do
|
|
154
|
+
render UI::TranscriptDetails.new(transcript)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The gallery runs this structure at narrow widths in
|
|
161
|
+
[`Narrow transcript actions`](/gallery/components/dialog#example-dialog-narrow-action-cluster).
|
|
162
|
+
|
|
163
|
+
### Sheet
|
|
164
|
+
|
|
165
|
+
ERB source — the partial yields contextual content into its side panel:
|
|
166
|
+
|
|
167
|
+
```erb
|
|
168
|
+
<%= render "sheet", id: "transcript-prompts", title: "Prompts" do %>
|
|
169
|
+
<%= render "prompts", prompts: @prompts %>
|
|
170
|
+
<% end %>
|
|
171
|
+
|
|
172
|
+
<%# _sheet.html.erb %>
|
|
173
|
+
<button command="show-modal" commandfor="<%= id %>-panel">Prompts</button>
|
|
174
|
+
<dialog id="<%= id %>-panel">
|
|
175
|
+
<h2><%= title %></h2>
|
|
176
|
+
<%= yield %>
|
|
177
|
+
</dialog>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Phlex destination — declare the collection before `Sheet`; render it only
|
|
181
|
+
inside the `panel` content slot:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
prompts = transcript.prompts.map { |prompt| [prompt.title, prompt_path(prompt)] }
|
|
185
|
+
|
|
186
|
+
render NitroKit::Sheet.new(id: "transcript-prompts", side: :left) do |sheet|
|
|
187
|
+
sheet.trigger("Prompts", icon: :list)
|
|
188
|
+
sheet.panel(title: "Transcript prompts") do
|
|
189
|
+
render NitroKit::AppNavigation.new(label: "Transcript prompts") do |navigation|
|
|
190
|
+
navigation.body do
|
|
191
|
+
prompts.each do |text, href|
|
|
192
|
+
navigation.item(text, href:)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
See the executable
|
|
201
|
+
[`Sheet` collection example](/gallery/components/sheet#example-sheet-constructions).
|
|
202
|
+
|
|
203
|
+
### SettingsLayout
|
|
204
|
+
|
|
205
|
+
ERB source — the call site supplies both the navigation collection and yielded
|
|
206
|
+
settings content:
|
|
207
|
+
|
|
208
|
+
```erb
|
|
209
|
+
<% sections = [["Profile", profile_settings_path], ["Security", security_settings_path]] %>
|
|
210
|
+
<%= render "settings_layout", sections: sections do %>
|
|
211
|
+
<%= render "profile_form" %>
|
|
212
|
+
<% end %>
|
|
213
|
+
|
|
214
|
+
<%# _settings_layout.html.erb %>
|
|
215
|
+
<nav aria-label="Settings">
|
|
216
|
+
<% sections.each do |text, href| %>
|
|
217
|
+
<%= link_to text, href %>
|
|
218
|
+
<% end %>
|
|
219
|
+
</nav>
|
|
220
|
+
<main><%= yield %></main>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Phlex destination — navigation entries stay inside `navigation`; the former
|
|
224
|
+
yield lives inside the one `content` region:
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
sections = [["Profile", profile_settings_path], ["Security", security_settings_path]]
|
|
228
|
+
|
|
229
|
+
render NitroKit::SettingsLayout.new do |layout|
|
|
230
|
+
layout.navigation(label: "Settings") do
|
|
231
|
+
sections.each_with_index do |(text, href), index|
|
|
232
|
+
layout.item(text, href:, current: index.zero?)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
layout.content { render UI::ProfileForm.new(profile) }
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
See the executable
|
|
240
|
+
[`SettingsLayout` minimal example](/gallery/components/settings-layout#example-settings-layout-cardinality-states).
|
|
241
|
+
|
|
242
|
+
## Preserve unsupported behavior honestly
|
|
243
|
+
|
|
244
|
+
When no equivalent exists, keep semantic Rails or HTML under the application
|
|
245
|
+
namespace and report the missing capability. Do not retain copied 1.x source,
|
|
246
|
+
downgrade a specialized control, or hide the gap behind a generic component.
|
|
247
|
+
|
|
248
|
+
Tooltip custom triggers are the explicit composition path for an existing
|
|
249
|
+
focusable control. Forward every yielded boundary to that actual control:
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
render NitroKit::Tooltip.new(id: "revoke-help", content: "Revokes access immediately") do |tooltip|
|
|
253
|
+
tooltip.trigger(as: :custom) do |attributes|
|
|
254
|
+
render NitroKit::ButtonTo.new(
|
|
255
|
+
"Revoke",
|
|
256
|
+
href: token_path(token),
|
|
257
|
+
method: :delete,
|
|
258
|
+
variant: :destructive,
|
|
259
|
+
button_html: attributes.html,
|
|
260
|
+
button_aria: attributes.aria,
|
|
261
|
+
button_data: attributes.data
|
|
262
|
+
)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Verify fidelity
|
|
268
|
+
|
|
269
|
+
Install and run Nitro Kit's focused host-integration acceptance flow:
|
|
270
|
+
|
|
271
|
+
```sh
|
|
272
|
+
bin/rails generate nitro_kit:upgrade_smoke_tests
|
|
273
|
+
bin/rails test test/integration/nitro_kit_upgrade_smoke_test.rb
|
|
274
|
+
bin/rails test test/system/nitro_kit_upgrade_smoke_test.rb
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The generator does not overwrite existing tests. It generates only files
|
|
278
|
+
supported by the host's Rails Minitest and system-test setup and prints setup
|
|
279
|
+
guidance for skipped files. The tests use the currently bundled gem and cover
|
|
280
|
+
the shared upgrade boundary — browser-submitted Turbo validation and mutation,
|
|
281
|
+
Dialog and Sheet, layout-owned Rails flash, Turbo Frame identity, redirects,
|
|
282
|
+
and post-mutation Phlex rendering. Their collision-checked route exists only
|
|
283
|
+
during each test and is restored afterward; they add no production route or
|
|
284
|
+
component source. Keep application-specific migration tests for inventoried
|
|
285
|
+
product behavior alongside them.
|
|
286
|
+
|
|
287
|
+
The endpoint deliberately inherits `ApplicationController` callbacks. If the
|
|
288
|
+
application requires authentication or current-account state, fill in the
|
|
289
|
+
generated `prepare_nitro_kit_upgrade_smoke_test` methods with the same sign-in
|
|
290
|
+
and account-selection helpers used by ordinary integration and system tests.
|
|
291
|
+
Extend those application-owned classes rather than changing gem test support
|
|
292
|
+
or skipping host callbacks.
|
|
293
|
+
|
|
294
|
+
Run focused request and component tests, then compare the converted flows in a
|
|
295
|
+
browser at wide and narrow widths. Exercise keyboard focus, dialogs and sheets,
|
|
296
|
+
Turbo submissions, errors, empty states, light/dark appearance, and dense
|
|
297
|
+
metadata. A green request suite does not prove that a tooltip, off-canvas
|
|
298
|
+
panel, interactive Card treatment, or responsive composition survived.
|
|
299
|
+
|
|
300
|
+
Finish by deleting copied components, helpers, and controllers; run
|
|
301
|
+
`bin/rails nitro_kit:doctor`; and record every remaining application-owned
|
|
302
|
+
fallback as either intentional product UI or a Nitro Kit coverage gap.
|
|
303
|
+
|
|
304
|
+
Doctor inventories only concrete Nitro Kit 1.x conventions: `nk_*` helpers,
|
|
305
|
+
generated files under `app/components/nitro_kit`, controllers under
|
|
306
|
+
`app/javascript/controllers/nk`, the old Floating UI and combobox packages,
|
|
307
|
+
and `tailwind_merge`. Every finding includes a file and replacement. Its
|
|
308
|
+
disposition is:
|
|
309
|
+
|
|
310
|
+
- `migrated` — no remaining occurrence in that category.
|
|
311
|
+
- `unresolved` — a known 1.x integration still needs its documented 2.x
|
|
312
|
+
replacement or removal.
|
|
313
|
+
- `application-owned` — custom or unsupported behavior must be preserved under
|
|
314
|
+
an application namespace, not as a Nitro shadow.
|
|
315
|
+
|
|
316
|
+
The inventory deliberately does not guess from generic component, JavaScript,
|
|
317
|
+
or dependency names. Review application-owned product behavior separately and
|
|
318
|
+
keep its migration record with the application.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# New application strategy
|
|
2
|
+
|
|
3
|
+
Recommend a Rails application template for new applications, not a return to Nitro Kit's old component-copying generator.
|
|
4
|
+
|
|
5
|
+
Rails application templates are designed to configure a new app during `rails new`, can add gems, and can run generators after Bundler finishes. Nitro Kit's starter therefore remains a thin one-command entry point:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
rails new my_app -m https://nitrokit.dev/template.rb
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The template adds Nitro Kit and invokes `nitro_kit:install`. The generator owns
|
|
12
|
+
the project-local skills and `AGENTS.md`. The template does not copy Nitro
|
|
13
|
+
components, controllers, authentication, teams, billing, or product models.
|
|
14
|
+
|
|
15
|
+
Existing applications install the gem directly and run the setup generator.
|
|
16
|
+
Agent discovery, version-matched skill routing, diagnostics, and initialization
|
|
17
|
+
handoff are meaningful application-owned setup; component source remains
|
|
18
|
+
gem-owned.
|
|
19
|
+
|
|
20
|
+
An optional `nitro_kit:application` generator can later create an application-owned base: layout, `UI` namespace, shell choice, root screen, and authentication hooks. Keep that separate from installing the UI gem so teams can adopt Nitro Kit without adopting an application architecture.
|
|
21
|
+
|
|
22
|
+
Promote the template into a versioned starter application only when Nitro Kit deliberately owns opinions beyond UI and interaction conventions, such as authentication, accounts, teams, billing, jobs, mail, and deployment. Until then, the application template is easier to inspect, test, and evolve and does not impose the maintenance cost of a forked starter repository.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# A durable application foundation
|
|
2
|
+
|
|
3
|
+
Start a small authenticated application with the same records and page grammar
|
|
4
|
+
it will need after the second person joins. The first-user case should be the
|
|
5
|
+
smallest instance of the team model, not a separate personal mode that must be
|
|
6
|
+
replaced later.
|
|
7
|
+
|
|
8
|
+
## Summary
|
|
9
|
+
|
|
10
|
+
- Model `User`, `Team`, and `Membership` from the first signup; role belongs to
|
|
11
|
+
`Membership`, and every team-owned record loads through `Current.team`.
|
|
12
|
+
- One hybrid `AppShell` frames the authenticated product: `AppNavigation` owns
|
|
13
|
+
brand and destinations, the shell `Toolbar` owns route titles and persistent
|
|
14
|
+
basic actions.
|
|
15
|
+
- One wrapper immediately inside `shell.main` owns responsive page padding for
|
|
16
|
+
every route; the shell owns viewport height and scrolling.
|
|
17
|
+
- Infrequent account destinations go after `navigation.spacer`; settings
|
|
18
|
+
compose with `SettingsLayout` and plain `FormSection` regions.
|
|
19
|
+
- When destination count warrants search, compose one `CommandPalette` in the
|
|
20
|
+
shell and render only routes the current membership may visit.
|
|
21
|
+
- Centralize cross-cutting feedback in one flash-driven toast region rendered
|
|
22
|
+
by the layout.
|
|
23
|
+
|
|
24
|
+
## Use memberships from the first user
|
|
25
|
+
|
|
26
|
+
Use `User`, `Team`, and `Membership` even when signup creates exactly one of
|
|
27
|
+
each. Put the role on `Membership`, not `User`, because authority belongs to a
|
|
28
|
+
person's relationship with a team. Create the first team and owner membership
|
|
29
|
+
in the same transaction as signup.
|
|
30
|
+
|
|
31
|
+
Set `Current.user`, `Current.membership`, and `Current.team` from the session.
|
|
32
|
+
Load every team-owned record through `Current.team`; use `Current.user` for
|
|
33
|
+
authorship and audit fields. Start with the smallest role vocabulary the
|
|
34
|
+
product needs, usually owner, administrator, and member. Protect the last owner
|
|
35
|
+
in the domain model rather than only hiding a button.
|
|
36
|
+
|
|
37
|
+
Invitations should belong to a team, record the inviter and intended role,
|
|
38
|
+
expire, match the invited email, and be consumed when accepted. An existing
|
|
39
|
+
user accepts into a new membership; a new user completes signup and then uses
|
|
40
|
+
the same acceptance path.
|
|
41
|
+
|
|
42
|
+
## Compose one authenticated frame
|
|
43
|
+
|
|
44
|
+
Use one hybrid `AppShell` for the authenticated product. Keep the brand and
|
|
45
|
+
primary destinations in `AppNavigation`; place route titles and persistent
|
|
46
|
+
basic actions in the shell `Toolbar`. One wrapper immediately inside
|
|
47
|
+
`shell.main` owns responsive page padding for every route. The topbar and
|
|
48
|
+
sidebar header should use the shell's shared height and border tokens rather
|
|
49
|
+
than independent padding guesses.
|
|
50
|
+
|
|
51
|
+
Put infrequent account navigation after `navigation.spacer`, near the account
|
|
52
|
+
controls at the bottom of the sidebar:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
shell.navigation do
|
|
56
|
+
AppNavigation(label: "Workspace navigation") do |navigation|
|
|
57
|
+
navigation.body do
|
|
58
|
+
navigation.item("Inventory", href: assets_path, icon: :archive)
|
|
59
|
+
navigation.item("Team", href: team_path, icon: :users)
|
|
60
|
+
navigation.spacer
|
|
61
|
+
navigation.item("Settings", href: settings_profile_path, icon: :settings)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Application code owns destinations and current-route policy. Nitro owns shell
|
|
68
|
+
layout, mobile disclosure, focus management, and navigation semantics.
|
|
69
|
+
For larger products, place one `CommandPalette` in the shell. Its native links
|
|
70
|
+
remain the navigation authority while Command-K or Control-K adds fast
|
|
71
|
+
filtering. Render the same authorized destination set the user can reach in
|
|
72
|
+
ordinary navigation; do not use the palette to bypass route policy.
|
|
73
|
+
When the destination set is too large or dynamic to render eagerly, pass
|
|
74
|
+
`search_url:` and return `CommandPalette::Results` from that endpoint. Keep the
|
|
75
|
+
same authorization scope on the initial links and every remote query.
|
|
76
|
+
Let the shell own viewport height and scrolling; do not add `min-height: 100vh`
|
|
77
|
+
to its main region or page wrapper. Put brand and destination icons through the
|
|
78
|
+
navigation slots so they share the same left alignment.
|
|
79
|
+
|
|
80
|
+
Team is an administration surface, not merely a roster. Include pending
|
|
81
|
+
invitations and the complete invite, role-change, removal, and revoke paths,
|
|
82
|
+
with last-owner protection in the model and authorization on every mutation.
|
|
83
|
+
|
|
84
|
+
Authentication is a standalone form surface. Inside `AuthShell`, use Rails
|
|
85
|
+
`form_with` with `NitroKit::FormBuilder` and put the visible fields, submit
|
|
86
|
+
control, and related recovery link in one `form.group`. The group owns their
|
|
87
|
+
vertical rhythm; `AuthShell` owns only the page container and spacing between
|
|
88
|
+
its major regions.
|
|
89
|
+
|
|
90
|
+
## Keep settings plain
|
|
91
|
+
|
|
92
|
+
Use `SettingsLayout` inside the normal shell main region. Its navigation lists
|
|
93
|
+
stable subsections such as Profile, Notifications, Appearance, and Password;
|
|
94
|
+
its content renders the selected form. Use `FormSection` for genuinely distinct
|
|
95
|
+
form regions and ordinary whitespace or dividers between them.
|
|
96
|
+
|
|
97
|
+
Render subsection destinations as links and mark the active link with
|
|
98
|
+
`aria-current="page"`. They navigate between routes; Buttons and ButtonGroup
|
|
99
|
+
would incorrectly present them as in-page actions. Small preferences may
|
|
100
|
+
submit on change through a tiny application Stimulus controller that calls the
|
|
101
|
+
form's native `requestSubmit`. Keep a submit control in `noscript` so the form
|
|
102
|
+
still works without JavaScript.
|
|
103
|
+
|
|
104
|
+
The route still has one `h1` in the shell toolbar. Do not repeat “Settings” in
|
|
105
|
+
the page body, wrap each subsection in a Card, or give every form its own outer
|
|
106
|
+
padding. A toolbar Save button can submit the selected form with the native
|
|
107
|
+
`form:` attribute, so the action stays in the same place at narrow and wide
|
|
108
|
+
widths without JavaScript.
|
|
109
|
+
|
|
110
|
+
## Centralize cross-cutting feedback
|
|
111
|
+
|
|
112
|
+
Render `Toast::FlashMessages` once in the application layout. Keep using
|
|
113
|
+
ordinary Rails flash and `303 See Other` redirects from controllers.
|
|
114
|
+
|
|
115
|
+
Compact destructive actions should continue to declare
|
|
116
|
+
`data: { turbo_confirm: "…" }` and use Turbo's native browser confirmation.
|
|
117
|
+
When the user needs branded review UI or more context than one sentence,
|
|
118
|
+
compose a dedicated native Nitro `Dialog` at the action's call site. The
|
|
119
|
+
browser's top layer keeps that inline dialog clear of ancestor clipping and
|
|
120
|
+
stacking contexts. Put record deletion on the edit route rather than adding a
|
|
121
|
+
danger surface to every operational show page.
|
|
122
|
+
|
|
123
|
+
## Baseline acceptance path
|
|
124
|
+
|
|
125
|
+
Before polishing empty-state illustration or dashboard summaries, verify:
|
|
126
|
+
|
|
127
|
+
- signup or sign-in selects the current membership and team;
|
|
128
|
+
- another team cannot load the current team's records;
|
|
129
|
+
- owner, administrator, and member policy differs where intended;
|
|
130
|
+
- populated, empty, invalid, narrow, and destructive states work;
|
|
131
|
+
- settings forms preserve validation and use one content gutter;
|
|
132
|
+
- successful mutations redirect with `303` and invalid forms render with
|
|
133
|
+
`422`;
|
|
134
|
+
- confirmation has both cancel and confirm coverage;
|
|
135
|
+
- the shell, headings, tables, and forms remain usable without custom request
|
|
136
|
+
JavaScript.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# A complete product resource
|
|
2
|
+
|
|
3
|
+
Build CRUD as one coherent product surface, not independent generated screens.
|
|
4
|
+
Before writing the views, name the resource, tenant boundary, actor, lifecycle,
|
|
5
|
+
visibility rules, and states. Implement index, form, detail, destructive action,
|
|
6
|
+
and their tests together.
|
|
7
|
+
|
|
8
|
+
## Summary
|
|
9
|
+
|
|
10
|
+
- Build index, form, detail, destructive action, and their tests as one
|
|
11
|
+
surface; name the resource, tenant boundary, actor, lifecycle, and states
|
|
12
|
+
before writing views.
|
|
13
|
+
- `AppShell(layout: :hybrid)` frames the admin area, and a `Toolbar` in
|
|
14
|
+
`shell.topbar` carries the route's single `h1` plus its basic actions.
|
|
15
|
+
- Spend hierarchy once: one route, one `h1`, an `h2` only for a genuinely
|
|
16
|
+
separate region, and no resource name repeated across toolbar, `PageHeader`,
|
|
17
|
+
`DataSection`, Card, and caption.
|
|
18
|
+
- Child routes add one compact icon Back link before the title instead of a
|
|
19
|
+
trailing Cancel action.
|
|
20
|
+
- One responsive padding rule on the main wrapper owns the page gutter;
|
|
21
|
+
data-heavy hybrid CRUD content uses the full available width.
|
|
22
|
+
|
|
23
|
+
## Use the hybrid application frame
|
|
24
|
+
|
|
25
|
+
An authenticated admin area defaults to `AppShell(layout: :hybrid)`. Nitro Kit
|
|
26
|
+
owns the desktop sidebar, mobile menu button, navigation dialog, focus behavior,
|
|
27
|
+
and responsive transition. The application provides destinations and current
|
|
28
|
+
state.
|
|
29
|
+
|
|
30
|
+
Put a `Toolbar` in `shell.topbar`. Its leading region contains the route's one
|
|
31
|
+
`h1`; its trailing region contains basic actions such as New, Edit, Cancel,
|
|
32
|
+
Save, Publish, or View. A Button outside a form can submit it through the
|
|
33
|
+
native `form:` attribute. This keeps the same action hierarchy on narrow and
|
|
34
|
+
wide screens without custom JavaScript.
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
AppShell(id: "admin", layout: :hybrid) do |shell|
|
|
38
|
+
shell.navigation { render admin_navigation }
|
|
39
|
+
shell.topbar do
|
|
40
|
+
Toolbar do |toolbar|
|
|
41
|
+
toolbar.leading { h1 { page_title } }
|
|
42
|
+
toolbar.trailing do
|
|
43
|
+
Button(
|
|
44
|
+
"Save",
|
|
45
|
+
type: :submit,
|
|
46
|
+
form: dom_id(@post, :form),
|
|
47
|
+
variant: :primary
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
shell.main do
|
|
53
|
+
div(data: { ui: "admin-main" }) do
|
|
54
|
+
render page
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Child routes add one compact Back link before the title. Prefer an icon-only
|
|
61
|
+
Button with an explicit label such as `aria: { label: "Back to projects" }`.
|
|
62
|
+
Do not repeat that navigation as a trailing Cancel action.
|
|
63
|
+
|
|
64
|
+
The application stylesheet gives `admin-main` one responsive padding rule.
|
|
65
|
+
Child pages do not add another outer gutter. Keep data-heavy hybrid CRUD
|
|
66
|
+
content full width; constrain only a specific content-led region whose measure
|
|
67
|
+
benefits from it.
|
|
68
|
+
|
|
69
|
+
Do not add viewport height or another outer padding rule to `admin-main`; the
|
|
70
|
+
shell owns viewport geometry and the wrapper owns the one page gutter. Use the
|
|
71
|
+
same shell and gutter on team administration and settings routes.
|
|
72
|
+
Place a bottom-anchored Settings destination after `AppNavigation#spacer`, then
|
|
73
|
+
compose settings subsections with `SettingsLayout` and plain `FormSection`
|
|
74
|
+
regions. Settings destinations are links with `aria-current`, not action
|
|
75
|
+
Buttons. Read `application_foundation.md` for the complete application frame.
|
|
76
|
+
|
|
77
|
+
## Spend hierarchy once
|
|
78
|
+
|
|
79
|
+
- One route, one `h1`, normally in the shell toolbar.
|
|
80
|
+
- Add an `h2` only for a genuinely separate region.
|
|
81
|
+
- Do not repeat “Posts” in the toolbar, `PageHeader`, `DataSection`, Card, and
|
|
82
|
+
visible table caption.
|
|
83
|
+
- Use `PageHeader` for a content-led introduction, not as mandatory CRUD
|
|
84
|
+
ceremony under an existing toolbar.
|
|
85
|
+
- Prefer whitespace and dividers. Use Card only for a bounded object that
|
|
86
|
+
benefits from its own surface. Never default to Card inside Card.
|
|
87
|
+
|
|
88
|
+
An index begins with a short introduction only if it adds useful context, then
|
|
89
|
+
renders its table and pagination directly. Use `DataSection` when a page has
|
|
90
|
+
multiple independently named datasets. At zero records, replace the data region
|
|
91
|
+
with one intentional `EmptyState`; keep the primary New action in the toolbar.
|
|
92
|
+
|
|
93
|
+
A new or edit page begins with `FormSection`. Use one form component for new,
|
|
94
|
+
edit, and invalid renders. Put the primary submit in the toolbar by setting the
|
|
95
|
+
button's `form:` to the form's stable DOM ID. Invalid submissions render the
|
|
96
|
+
same model and form with `422 Unprocessable Entity`. The toolbar owns that
|
|
97
|
+
action: do not render a second Save or Create submit inside the form body.
|
|
98
|
+
|
|
99
|
+
A detail page begins with status or stable metadata, then the resource itself.
|
|
100
|
+
Keep status inside that normal details flow instead of detaching it into a
|
|
101
|
+
second side panel.
|
|
102
|
+
Use the authenticated `show` route as the operational detail or draft preview.
|
|
103
|
+
Put lifecycle forms in the page and associate their toolbar buttons with
|
|
104
|
+
`form:`. Put destructive confirmation in one separate `DangerZone` on edit,
|
|
105
|
+
with a safe escape back to the record. Do not make every show page end in a
|
|
106
|
+
large deletion surface.
|
|
107
|
+
|
|
108
|
+
## Model and route the lifecycle
|
|
109
|
+
|
|
110
|
+
Scope every lookup through `Current.team` or `Current.account`. Record
|
|
111
|
+
`Current.user` as author, creator, or publisher. If a state has provenance,
|
|
112
|
+
timing, or behavior, model it as a record and expose it as a noun resource:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
namespace :admin do
|
|
116
|
+
resources :posts do
|
|
117
|
+
resource :publication,
|
|
118
|
+
only: %i[create destroy],
|
|
119
|
+
module: :posts
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The main controller keeps the seven REST actions. Successful mutations redirect
|
|
125
|
+
with `303 See Other`. Publication create and destroy invoke domain methods and
|
|
126
|
+
redirect. Public controllers query published records only. Ordinary Rails forms
|
|
127
|
+
and Turbo Drive are the default; do not add fetch code for CRUD submissions.
|
|
128
|
+
|
|
129
|
+
## Ship the acceptance path
|
|
130
|
+
|
|
131
|
+
Request tests cover tenant isolation, public visibility, successful `303`
|
|
132
|
+
redirects, invalid `422` renders, pagination, and lifecycle resources. Rendering
|
|
133
|
+
assertions should also protect the high-level composition: hybrid AppShell,
|
|
134
|
+
one `h1`, navigation, toolbar action, actual form association, table or empty
|
|
135
|
+
state, and destructive confirmation. Add one browser test for the meaningful
|
|
136
|
+
end-to-end path, using Capybara waiting assertions instead of sleeps.
|
|
137
|
+
|
|
138
|
+
Assert that each primary action has one visible control. A toolbar-associated
|
|
139
|
+
form submit plus an identical body submit is duplication, even when both invoke
|
|
140
|
+
the same valid form.
|
|
141
|
+
|
|
142
|
+
Before finishing, inspect a populated index, empty index, invalid form, narrow
|
|
143
|
+
form, draft detail, published detail, and edit-owned destructive dialog. Remove any extra
|
|
144
|
+
heading, surface, wrapper, or page gutter that does not communicate information.
|