nitro_kit 2.0.0.beta.1 → 2.0.0.beta.3
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 +96 -1
- data/README.md +16 -2
- data/STYLE_GUIDE.md +4 -2
- data/app/assets/stylesheets/nitro_kit.css +567 -36
- data/app/assets/tailwind/nitro_kit/engine.css +15 -0
- 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/browser_support.md +7 -1
- data/docs/component_contracts.md +93 -5
- data/docs/customization.md +35 -15
- 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 +4 -2
- data/lib/generators/nitro_kit/eject_generator.rb +26 -0
- data/lib/nitro_kit/ejection.rb +136 -0
- data/lib/nitro_kit/engine.rb +13 -0
- data/lib/nitro_kit/installation.rb +29 -7
- data/lib/nitro_kit/version.rb +1 -1
- data/lib/tasks/nitro_kit_tasks.rake +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/layers.css +8 -0
- data/src/stylesheets/nitro_kit/reset.css +12 -2
- data/{app/assets/stylesheets/nitro_kit-tailwind-v4.css → src/stylesheets/nitro_kit/tailwind.css} +4 -5
- data/src/stylesheets/nitro_kit/tokens.css +3 -2
- metadata +7 -3
data/docs/customization.md
CHANGED
|
@@ -4,17 +4,16 @@
|
|
|
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
|
|
|
11
11
|
Load browser styles in this order:
|
|
12
12
|
|
|
13
13
|
1. Optional third-party base styles, such as Lexxy.
|
|
14
|
-
2. The
|
|
15
|
-
3. The
|
|
16
|
-
4.
|
|
17
|
-
5. Application styles, including Nitro token overrides.
|
|
14
|
+
2. The generated `nitro_kit` distribution stylesheet.
|
|
15
|
+
3. The application's compiled Tailwind CSS, when present.
|
|
16
|
+
4. Application styles, including Nitro token overrides.
|
|
18
17
|
|
|
19
18
|
`NitroKit::AppearanceBootstrap` precedes every entry in this list. The install
|
|
20
19
|
generator applies this order when it can identify conventional layout entries
|
|
@@ -34,14 +33,35 @@ A Rails application without Tailwind can use:
|
|
|
34
33
|
A Tailwind CSS v4 application can use:
|
|
35
34
|
|
|
36
35
|
```erb
|
|
37
|
-
<%= stylesheet_link_tag
|
|
38
|
-
"nitro_kit-tailwind-v4", \
|
|
39
|
-
"nitro_kit", \
|
|
40
|
-
"tailwind", \
|
|
41
|
-
"application", \
|
|
42
|
-
"data-turbo-track": "reload" %>
|
|
36
|
+
<%= stylesheet_link_tag "nitro_kit", "tailwind", "application", "data-turbo-track": "reload" %>
|
|
43
37
|
```
|
|
44
38
|
|
|
39
|
+
`nitro_kit.css` opens with the global cascade-layer order
|
|
40
|
+
`properties, theme, base, nitro-kit, components, utilities` and aliases Nitro
|
|
41
|
+
tokens onto Tailwind's theme variables, so it must load before the compiled
|
|
42
|
+
Tailwind stylesheet. The separate `nitro_kit-tailwind-v4` adapter from earlier
|
|
43
|
+
2.0 betas no longer exists; `nitro_kit:doctor` reports a leftover link.
|
|
44
|
+
|
|
45
|
+
A Tailwind CSS v4 application using `tailwindcss-rails` can instead ship one
|
|
46
|
+
stylesheet. Nitro Kit provides the engine entry `tailwindcss-rails` looks for,
|
|
47
|
+
`app/assets/tailwind/nitro_kit/engine.css`, and `tailwindcss:build` or
|
|
48
|
+
`tailwindcss:watch` generates `app/assets/builds/tailwind/nitro_kit.css` in the
|
|
49
|
+
application. Import it before Tailwind in `app/assets/tailwind/application.css`:
|
|
50
|
+
|
|
51
|
+
```css
|
|
52
|
+
@import "../builds/tailwind/nitro_kit";
|
|
53
|
+
@import "tailwindcss";
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then the layout loads only the compiled Tailwind stylesheet:
|
|
57
|
+
|
|
58
|
+
```erb
|
|
59
|
+
<%= stylesheet_link_tag "tailwind", "application", "data-turbo-track": "reload" %>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`nitro_kit:doctor` recognizes the import and reports a separate `nitro_kit`
|
|
63
|
+
link as a duplicate.
|
|
64
|
+
|
|
45
65
|
Keep overrides unlayered in application CSS and load them after Nitro Kit. Nitro's selectors use `:where()` inside named cascade layers, so an ordinary application rule can override a token without selector escalation or `!important`.
|
|
46
66
|
|
|
47
67
|
Do not edit `app/assets/stylesheets/nitro_kit.css` in the gem or a bundled copy of it. That file is generated from `src/stylesheets/nitro_kit/` and is replaced on upgrade. Variables named `--_nk-*` are private component mechanics and may change without notice. Only the `--nk-*` variables listed below are the customization contract.
|
|
@@ -472,9 +492,9 @@ This registers Nitro's `controllers/nk/*` modules together with application cont
|
|
|
472
492
|
For the resulting behavior when those modules are not registered, use the
|
|
473
493
|
canonical no-JavaScript matrix in [`browser_support.md`](browser_support.md).
|
|
474
494
|
|
|
475
|
-
##
|
|
495
|
+
## Tailwind CSS v4 theme aliases
|
|
476
496
|
|
|
477
|
-
Nitro Kit does not require Tailwind, Tailwind configuration, or Tailwind Preflight — it ships its own global preflight in the `nitro-kit.reset` cascade layer, which unlayered application CSS always overrides.
|
|
497
|
+
Nitro Kit does not require Tailwind, Tailwind configuration, or Tailwind Preflight — it ships its own global preflight in the `nitro-kit.reset` cascade layer, which unlayered application CSS always overrides. For applications that do compile Tailwind, `nitro_kit.css` establishes a compatible cascade-layer order and maps Nitro tokens to common Tailwind v4 theme variables, including background, foreground, primary, destructive, radii, shadows, fonts, spacing, and transition defaults. The aliases live in `src/stylesheets/nitro_kit/tailwind.css` inside the `nitro-kit.tokens` layer; without Tailwind they are inert custom properties.
|
|
478
498
|
|
|
479
499
|
Tailwind remains compiled and configured by the application. An application can add further aliases in its Tailwind CSS source with the v4 CSS-first API:
|
|
480
500
|
|
|
@@ -488,7 +508,7 @@ Tailwind remains compiled and configured by the application. An application can
|
|
|
488
508
|
}
|
|
489
509
|
```
|
|
490
510
|
|
|
491
|
-
Use `@theme inline` when a Tailwind theme variable references another custom property so generated utilities resolve the live Nitro value. The
|
|
511
|
+
Use `@theme inline` when a Tailwind theme variable references another custom property so generated utilities resolve the live Nitro value. The aliases do not make Tailwind a Nitro runtime dependency, configure source detection, generate utility classes, or permit Tailwind classes inside Nitro component APIs.
|
|
492
512
|
|
|
493
513
|
## Public token reference
|
|
494
514
|
|
|
@@ -517,7 +537,7 @@ The following variables are the complete public token set. Theme-independent tok
|
|
|
517
537
|
| `--nk-title-page-weight` | Page title weight. |
|
|
518
538
|
| `--nk-title-section-size` | Section title size: data, settings, and danger sections. |
|
|
519
539
|
| `--nk-title-section-weight` | Section title weight. |
|
|
520
|
-
| `--nk-title-surface-size` | Surface title size:
|
|
540
|
+
| `--nk-title-surface-size` | Surface title size: dialogs, sheets, empty states, fieldsets. |
|
|
521
541
|
| `--nk-title-surface-weight` | Surface title weight. |
|
|
522
542
|
| `--nk-title-compact-size` | Compact title size: legends, alert and toast titles. |
|
|
523
543
|
| `--nk-title-compact-weight` | Compact title weight. |
|
data/docs/eject.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Eject one component
|
|
2
|
+
|
|
3
|
+
Gem-owned components, token overrides, and composition remain the default.
|
|
4
|
+
Eject is an explicit source-level customization opt-out, never an installation
|
|
5
|
+
step. Free eject turns a gem component into your code; Pro exemplars are already
|
|
6
|
+
your code when retrieved.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
bin/rails generate nitro_kit:eject Button
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Use the ordinary Phlex constructor:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
render Ui::EjectedButton::Button.new("Save", icon: :check)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Files and integration
|
|
19
|
+
|
|
20
|
+
- `app/components/ui/ejected_button/`: Button, nested slot classes, transitive
|
|
21
|
+
Ruby component/support dependencies (such as Icon), and an application-owned
|
|
22
|
+
Component adapter with a snapshot of base component-level helpers.
|
|
23
|
+
- `app/assets/stylesheets/ejected_button.css`: those components' CSS plus shared
|
|
24
|
+
palette/layout rules where needed, with explicit cascade-layer order.
|
|
25
|
+
- `app/javascript/controllers/ui/ejected_button/`: referenced Stimulus
|
|
26
|
+
controllers and their local JavaScript imports.
|
|
27
|
+
- `config/nitro_kit/ejected/button.json`: source version, component, namespace,
|
|
28
|
+
and generated file inventory. Every source file also records the version.
|
|
29
|
+
|
|
30
|
+
Keep Nitro Kit installed. The attribute/render kernel (`NitroKit::Component`),
|
|
31
|
+
public `--nk-*` tokens, global reset, translations, appearance document runtime,
|
|
32
|
+
and third-party integrations (Phlex, Lucide, Turbo, Active Storage) stay shared.
|
|
33
|
+
This is not a standalone replacement for the gem. Pin the gem and test kernel
|
|
34
|
+
upgrades even after ejecting.
|
|
35
|
+
|
|
36
|
+
Load the generated CSS after Nitro Kit and before application overrides:
|
|
37
|
+
|
|
38
|
+
```erb
|
|
39
|
+
<%= stylesheet_link_tag "nitro_kit", "ejected_button", "application",
|
|
40
|
+
"data-turbo-track": "reload" %>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
In Phlex, use the Rails `stylesheet_link_tag` adapter with the same asset names;
|
|
44
|
+
CSS bundlers may import the generated file instead. The generator reports this
|
|
45
|
+
manual step rather than guessing which of your layouts or CSS entrypoints owns
|
|
46
|
+
the page. Keep the app's normal Stimulus loader enabled. Standard Rails importmap
|
|
47
|
+
apps already pin `app/javascript/controllers` recursively; bundler apps must
|
|
48
|
+
include the generated controllers in their controller loader/build as usual.
|
|
49
|
+
|
|
50
|
+
Dropzone's copied controller imports Active Storage's JavaScript even when
|
|
51
|
+
`direct_upload: false`. If the app omits Active Storage, its JavaScript asset
|
|
52
|
+
must still be available; eject does not install third-party integrations.
|
|
53
|
+
|
|
54
|
+
## Isolation and composition
|
|
55
|
+
|
|
56
|
+
Each ejection has its own dependency snapshot. Ruby uses `Ui::EjectedButton`,
|
|
57
|
+
not `NitroKit` or an existing `Ui::Button`. Root/slot identities use
|
|
58
|
+
`ui-ejected-button-*`; controller identifiers follow Rails filename conventions
|
|
59
|
+
(`ui--ejected-button--button`); CSS layers, private variables, and keyframes
|
|
60
|
+
are renamed. Public tokens and the global reset deliberately remain shared.
|
|
61
|
+
Gem Buttons and ejected Buttons can render on the same page without sharing
|
|
62
|
+
component CSS or controller identifiers.
|
|
63
|
+
|
|
64
|
+
Namespace spelling follows the host application's inflections (for example,
|
|
65
|
+
an app with the `UI` acronym gets `UI::EjectedButton`). Use the constructor
|
|
66
|
+
printed by the generator.
|
|
67
|
+
|
|
68
|
+
For typed slots, construct children from the same snapshot, for example
|
|
69
|
+
`Ui::EjectedEmptyState::Button`. Arbitrary content blocks may still compose gem
|
|
70
|
+
components, but snapshot-specific contextual styling targets snapshot children.
|
|
71
|
+
Separate ejections do not silently share mutable dependencies.
|
|
72
|
+
|
|
73
|
+
## Existing files and updates
|
|
74
|
+
|
|
75
|
+
Unknown names fail with a list of supported names before writing. CamelCase,
|
|
76
|
+
snake_case, and `NitroKit::Button` names are accepted. If any snapshot file
|
|
77
|
+
already exists, the entire operation skips without changing anything (including
|
|
78
|
+
with `--skip`). `--force` explicitly replaces the entire generated snapshot;
|
|
79
|
+
commit your modifications first. Stale files from older dependency graphs are
|
|
80
|
+
not deleted automatically.
|
|
81
|
+
|
|
82
|
+
Bundler updates do not update ejected component implementations, styles, or
|
|
83
|
+
controllers. Compare the recorded version to the changelog and port fixes
|
|
84
|
+
yourself, or generate a new snapshot in a disposable application and diff it.
|
|
85
|
+
The manifest enables future update notices; no updater or notice UI ships yet.
|
|
86
|
+
Commit all generated files and the stylesheet integration together.
|
data/docs/migration_1_to_2.md
CHANGED
|
@@ -12,7 +12,7 @@ Treat a 1.x migration as a product-flow review, not a helper rename. Nitro Kit
|
|
|
12
12
|
Add the 2.0 prerelease to the application's Gemfile:
|
|
13
13
|
|
|
14
14
|
```ruby
|
|
15
|
-
gem "nitro_kit", "2.0.0.beta.
|
|
15
|
+
gem "nitro_kit", "2.0.0.beta.3"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Bundler records the exact released version in `Gemfile.lock`; commit `Gemfile`
|
|
@@ -45,6 +45,28 @@ Nitro owns responsive disclosure and focus behavior. Put infrequent account
|
|
|
45
45
|
destinations after `navigation.spacer`. Add one `CommandPalette` only when the
|
|
46
46
|
destination count warrants search, and render only authorized routes.
|
|
47
47
|
|
|
48
|
+
The sidebar stays expanded by default, without a toggle or hover peek. Opt in
|
|
49
|
+
with `AppShell(id: "workspace", collapsible: true)` to include the bottom
|
|
50
|
+
**Pin sidebar** toggle that switches between the full sidebar and an icon rail.
|
|
51
|
+
Hovering with a fine pointer or focusing within the navigation or brand
|
|
52
|
+
reveals the full navigation as an overlay without moving page content.
|
|
53
|
+
The pin control does not trigger peek on hover or focus, so directly pinning
|
|
54
|
+
the rail starts the sidebar and content resize together.
|
|
55
|
+
Clicking the toggle pins the sidebar and reserves its full layout width.
|
|
56
|
+
Pointer-clicking to unpin collapses it immediately and suppresses hover peek
|
|
57
|
+
until the pointer leaves the sidebar once; keyboard focus still peeks. Use
|
|
58
|
+
`AppShell(id: "workspace", collapsible: true, sidebar: :collapsed)` to start with the rail, and
|
|
59
|
+
give navigation items icons so they remain recognizable. Labels keep their
|
|
60
|
+
accessible names and vertical positions. Touch does not hover-peek; narrow
|
|
61
|
+
screens still use the modal drawer. Pin state survives Turbo morph refreshes
|
|
62
|
+
of the same shell, but is not persisted across page loads.
|
|
63
|
+
`sidebar_toggle_label:` overrides its name.
|
|
64
|
+
|
|
65
|
+
For a brand mark that stays visible on the rail, declare
|
|
66
|
+
`shell.brand(icon: :zap) { ... }`. The icon stays aligned with navigation;
|
|
67
|
+
the full brand content appears on hover/focus or when pinned instead of being
|
|
68
|
+
cropped to fit the rail.
|
|
69
|
+
|
|
48
70
|
Use `AuthShell` with Rails `form_with` and `NitroKit::FormBuilder` for
|
|
49
71
|
authentication. Put visible fields, submit, and recovery link in one
|
|
50
72
|
`form.group`.
|
|
@@ -67,6 +67,13 @@ For `topbar`, put that Toolbar first inside `workspace-content` and omit
|
|
|
67
67
|
`shell.topbar`. Keep one route title and one set of actions. The header and
|
|
68
68
|
body in `sidebar` form one continuous canvas, not two stacked cards.
|
|
69
69
|
|
|
70
|
+
The inset composition also works with `collapsible: true`. Add
|
|
71
|
+
`sidebar: :expanded` to start pinned open, or `sidebar: :collapsed` to start
|
|
72
|
+
as an icon rail, and give `shell.brand` an `icon:` for its compact mark.
|
|
73
|
+
Pinning reserves navigation space; hover or keyboard-focus peeking overlays
|
|
74
|
+
the inset canvas without moving its toolbar or content. The App shell and
|
|
75
|
+
Sidebar operations galleries show static and collapsible inset examples.
|
|
76
|
+
|
|
70
77
|
Load this stylesheet after Nitro Kit:
|
|
71
78
|
|
|
72
79
|
```css
|
|
@@ -165,7 +172,8 @@ Load this stylesheet after Nitro Kit:
|
|
|
165
172
|
|
|
166
173
|
Navigation, mobile disclosure, and focus restoration remain Nitro-owned.
|
|
167
174
|
Application code owns the destinations and the composition. The public
|
|
168
|
-
|
|
175
|
+
App shell, Sidebar operations application, and Product resource galleries
|
|
176
|
+
run inset sidebar examples with this stylesheet at
|
|
169
177
|
`test/dummy/app/assets/stylesheets/inset_workspace.css`.
|
|
170
178
|
|
|
171
179
|
## Verify the result
|
data/docs/rails_integration.md
CHANGED
|
@@ -12,7 +12,7 @@ There are no `nk_form_with` helpers or general ERB component bridge.
|
|
|
12
12
|
Pin the current prerelease:
|
|
13
13
|
|
|
14
14
|
```ruby
|
|
15
|
-
gem "nitro_kit", "2.0.0.beta.
|
|
15
|
+
gem "nitro_kit", "2.0.0.beta.3"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Use the released gem and commit `Gemfile` with `Gemfile.lock`. Before upgrading,
|
|
@@ -36,7 +36,9 @@ Load Nitro Kit before application styles:
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
For third-party base CSS, Tailwind, appearance setup, and token overrides, use
|
|
39
|
-
the canonical [stylesheet order](customization.md#stylesheet-order).
|
|
39
|
+
the canonical [stylesheet order](customization.md#stylesheet-order). A
|
|
40
|
+
`tailwindcss-rails` application can bundle Nitro Kit into its compiled Tailwind
|
|
41
|
+
stylesheet through the engine entry described there.
|
|
40
42
|
|
|
41
43
|
## Stimulus
|
|
42
44
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "nitro_kit/ejection"
|
|
3
|
+
|
|
4
|
+
module NitroKit
|
|
5
|
+
class EjectGenerator < Rails::Generators::Base
|
|
6
|
+
argument :component, type: :string, desc: "Component to eject (for example Button)"
|
|
7
|
+
desc "Copy one component and its dependencies into isolated application-owned code."
|
|
8
|
+
|
|
9
|
+
def eject_component
|
|
10
|
+
ejection = Ejection.new(component)
|
|
11
|
+
files = ejection.files
|
|
12
|
+
existing = files.keys.select { |path| File.exist?(File.join(destination_root, path)) }
|
|
13
|
+
if existing.any? && !options[:force]
|
|
14
|
+
say_status :skip, "Already ejected or conflicting files: #{existing.join(', ')}. No files changed; use --force to replace the entire snapshot.", :yellow
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
files.each { |path, content| create_file(path, content) }
|
|
19
|
+
say "Render #{ejection.namespace}::#{ejection.component_name}.new(...)"
|
|
20
|
+
say "Load #{ejection.stylesheet_name}.css after nitro_kit.css (stylesheet_link_tag, or your CSS entrypoint)."
|
|
21
|
+
say "Keep Nitro Kit installed and keep the application's Stimulus controller loader enabled. See docs/eject.md."
|
|
22
|
+
rescue ArgumentError => error
|
|
23
|
+
raise Rails::Generators::Error, error.message
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require "ripper"
|
|
2
|
+
require "json"
|
|
3
|
+
require "nitro_kit/version"
|
|
4
|
+
|
|
5
|
+
module NitroKit
|
|
6
|
+
class Ejection
|
|
7
|
+
ROOT = File.expand_path("../..", __dir__)
|
|
8
|
+
SUPPORT_FILES = %w[component layout_options responsive_value].freeze
|
|
9
|
+
|
|
10
|
+
def initialize(name)
|
|
11
|
+
@name = name.delete_prefix("NitroKit::").underscore
|
|
12
|
+
@sources = Dir["#{ROOT}/app/components/nitro_kit/*.rb"].to_h do |path|
|
|
13
|
+
[ File.basename(path, ".rb").camelize, File.read(path) ]
|
|
14
|
+
end
|
|
15
|
+
unless @sources.key?(component_name) && !SUPPORT_FILES.include?(@name)
|
|
16
|
+
raise ArgumentError, "Unknown component #{name.inspect}. Choose one of: #{(@sources.keys - SUPPORT_FILES.map(&:camelize)).sort.join(', ')}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def component_name = @name.camelize
|
|
21
|
+
def stylesheet_name = "ejected_#{@name}"
|
|
22
|
+
def namespace = directory.camelize
|
|
23
|
+
def prefix = "ui-ejected-#{@name.dasherize}"
|
|
24
|
+
def directory = "ui/ejected_#{@name}"
|
|
25
|
+
|
|
26
|
+
def files
|
|
27
|
+
result = {}
|
|
28
|
+
ruby_names.each do |name|
|
|
29
|
+
result["app/components/#{directory}/#{name.underscore}.rb"] = header("#") + rewrite_ruby(@sources.fetch(name))
|
|
30
|
+
end
|
|
31
|
+
result["app/components/#{directory}/component.rb"] = header("#") + component_adapter
|
|
32
|
+
result["app/assets/stylesheets/#{stylesheet_name}.css"] = header("/*", " */") + stylesheet
|
|
33
|
+
javascript_paths.each do |path|
|
|
34
|
+
relative = path.delete_prefix("#{ROOT}/app/javascript/controllers/nk/")
|
|
35
|
+
source = rewrite_contracts(File.read(path))
|
|
36
|
+
.gsub("controllers/nk/", "controllers/#{directory}/")
|
|
37
|
+
result["app/javascript/controllers/#{directory}/#{relative}"] = header("//") + source
|
|
38
|
+
end
|
|
39
|
+
result["config/nitro_kit/ejected/#{@name}.json"] = JSON.pretty_generate(
|
|
40
|
+
component: component_name, version: VERSION, namespace:, files: result.keys
|
|
41
|
+
) + "\n"
|
|
42
|
+
result
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
def header(open, close = "")
|
|
47
|
+
"#{open} Ejected from Nitro Kit #{VERSION}: #{component_name}. Application-owned; not automatically upgraded.#{close}\n"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def ruby_names
|
|
51
|
+
@ruby_names ||= begin
|
|
52
|
+
names = [ component_name ]
|
|
53
|
+
names.each do |name|
|
|
54
|
+
Ripper.lex(@sources.fetch(name)).each do |_, type, token, _|
|
|
55
|
+
names << token if type == :on_const && @sources.key?(token) && token != "Component" && !names.include?(token)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
names
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def stylesheet
|
|
63
|
+
names = ruby_names.map(&:underscore)
|
|
64
|
+
names << "palette" if (ruby_names & %w[Badge Alert Toast]).any?
|
|
65
|
+
names << "layout" if (ruby_names & %w[Flex Grid]).any?
|
|
66
|
+
rules = names.filter_map do |name|
|
|
67
|
+
path = "#{ROOT}/src/stylesheets/nitro_kit/components/#{name}.css"
|
|
68
|
+
rewrite_contracts(File.read(path)) if File.file?(path)
|
|
69
|
+
end
|
|
70
|
+
layers = %w[base variant size state compound].map { |layer| "#{prefix}.#{layer}" }.join(", ")
|
|
71
|
+
"@layer #{layers};\n" + rules.join("\n")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def javascript_paths
|
|
75
|
+
ruby_source = ruby_names.map { |name| @sources.fetch(name) }.join("\n")
|
|
76
|
+
paths = Dir["#{ROOT}/app/javascript/controllers/nk/*_controller.js"].select do |path|
|
|
77
|
+
name = File.basename(path, "_controller.js")
|
|
78
|
+
ruby_source.include?("nk--#{name.dasherize}") || ruby_source.include?("nk__#{name}")
|
|
79
|
+
end
|
|
80
|
+
# Shared overlay command plumbing lives in the component adapter.
|
|
81
|
+
paths |= [ "#{ROOT}/app/javascript/controllers/nk/dialog_controller.js" ] if ruby_source.include?("command_data(")
|
|
82
|
+
paths.each do |path|
|
|
83
|
+
File.read(path).scan(/(?:from\s*|import\s*)["']([^"']+)["']/).flatten.each do |import|
|
|
84
|
+
dependency = if import.start_with?("controllers/nk/")
|
|
85
|
+
"#{ROOT}/app/javascript/#{import}.js"
|
|
86
|
+
elsif import.start_with?(".")
|
|
87
|
+
File.expand_path(import.end_with?(".js") ? import : "#{import}.js", File.dirname(path))
|
|
88
|
+
end
|
|
89
|
+
paths << dependency if dependency && File.file?(dependency) && !paths.include?(dependency)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
paths
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def rewrite_ruby(source)
|
|
96
|
+
rewrite_contracts(source.gsub("module NitroKit", "module #{namespace}").gsub("NitroKit::", "#{namespace}::"))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def rewrite_contracts(source)
|
|
100
|
+
source.gsub("nk--", "ui--ejected-#{@name.dasherize}--")
|
|
101
|
+
.gsub("nk__", "ui__ejected_#{@name}__")
|
|
102
|
+
.gsub(/(data-(?:nk|slot)(?:[~|^$*]?=)["'])([^"']+)/) { "#{$1}#{prefix}-#{$2}" }
|
|
103
|
+
.gsub(/(dataset\.slot\s*=\s*["'])([^"']+)/) { "#{$1}#{prefix}-#{$2}" }
|
|
104
|
+
.gsub("--_nk-", "--_#{prefix}-")
|
|
105
|
+
.gsub(/(?<![-\w])nk-(?!-)[a-z][a-z0-9-]*/) { |name| "#{prefix}-#{name.delete_prefix('nk-')}" }
|
|
106
|
+
.gsub("nitro-kit.", "#{prefix}.")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def component_adapter
|
|
110
|
+
# Copy component helpers, not the attribute kernel or the enclosing class.
|
|
111
|
+
helpers = @sources.fetch("Component").lines
|
|
112
|
+
.drop_while { |line| !line.start_with?(" def description_id") }[...-2].join
|
|
113
|
+
<<~RUBY
|
|
114
|
+
module #{namespace}
|
|
115
|
+
class Component < NitroKit::Component
|
|
116
|
+
def initialize(component:, **attributes)
|
|
117
|
+
super(component: "#{prefix}-\#{component.to_s.tr('_', '-')}", **attributes)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def qualified_slot(slot)
|
|
123
|
+
name = slot.to_s.tr("_", "-")
|
|
124
|
+
original = @component_name.delete_prefix("#{prefix}-")
|
|
125
|
+
name = name.delete_prefix("\#{original}-")
|
|
126
|
+
"\#{@component_name}-\#{name}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Snapshot of component-level helpers; attribute/render kernel stays gem-owned.
|
|
130
|
+
#{rewrite_contracts(helpers)}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
RUBY
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/nitro_kit/engine.rb
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
module NitroKit
|
|
2
2
|
class Engine < ::Rails::Engine
|
|
3
|
+
# Rails would otherwise derive "nitro_kit_engine". tailwindcss-rails looks up
|
|
4
|
+
# app/assets/tailwind/<engine_name>/engine.css, so the name is public.
|
|
5
|
+
engine_name "nitro_kit"
|
|
6
|
+
|
|
3
7
|
IMPORTMAP_PATH = root.join("config/importmap.rb")
|
|
4
8
|
JAVASCRIPT_PATHS = [ root.join("app/javascript") ].freeze
|
|
9
|
+
TAILWIND_PATH = root.join("app/assets/tailwind")
|
|
5
10
|
|
|
6
11
|
initializer "nitro_kit.assets" do |app|
|
|
7
12
|
next unless app.config.respond_to?(:assets)
|
|
@@ -11,6 +16,14 @@ module NitroKit
|
|
|
11
16
|
end
|
|
12
17
|
end
|
|
13
18
|
|
|
19
|
+
# The Tailwind engine entry is a build input for tailwindcss-rails, not a
|
|
20
|
+
# servable asset. Keep it out of Propshaft's load path.
|
|
21
|
+
initializer "nitro_kit.exclude_tailwind_path", before: "propshaft.append_assets_path" do |app|
|
|
22
|
+
next unless app.config.respond_to?(:assets) && app.config.assets.respond_to?(:excluded_paths)
|
|
23
|
+
|
|
24
|
+
app.config.assets.excluded_paths << TAILWIND_PATH unless app.config.assets.excluded_paths.include?(TAILWIND_PATH)
|
|
25
|
+
end
|
|
26
|
+
|
|
14
27
|
initializer "nitro_kit.importmap", before: "importmap" do |app|
|
|
15
28
|
next unless app.config.respond_to?(:importmap)
|
|
16
29
|
|
|
@@ -9,9 +9,13 @@ module NitroKit
|
|
|
9
9
|
ROOT = Pathname.new(File.expand_path("../..", __dir__))
|
|
10
10
|
SKILLS = %w[nitro-kit-hotwire nitro-kit-rails nitro-kit-ui].freeze
|
|
11
11
|
SKILL_ROOTS = [ ".agents/skills", ".claude/skills" ].freeze
|
|
12
|
+
# nitro_kit-tailwind-v4 is the beta-era adapter that nitro_kit.css absorbed;
|
|
13
|
+
# it stays managed so doctor flags a leftover link instead of ignoring it.
|
|
12
14
|
MANAGED_STYLESHEETS = %w[
|
|
13
15
|
lexxy nitro_kit-tailwind-v4 nitro_kit tailwind application
|
|
14
16
|
].freeze
|
|
17
|
+
TAILWIND_ENTRY = "app/assets/tailwind/application.css"
|
|
18
|
+
TAILWIND_BUNDLE_IMPORT = %r{@import\s+["'](?:\.\./)+builds/tailwind/nitro_kit(?:\.css)?["']}
|
|
15
19
|
AGENTS_START = "<!-- nitro-kit:start -->"
|
|
16
20
|
AGENTS_END = "<!-- nitro-kit:end -->"
|
|
17
21
|
AGENTS_BLOCK = <<~MARKDOWN.freeze
|
|
@@ -217,8 +221,12 @@ module NitroKit
|
|
|
217
221
|
errors << "installer left the layout unchanged: #{edit_error}"
|
|
218
222
|
end
|
|
219
223
|
|
|
220
|
-
if assets.include?("nitro_kit-tailwind-v4")
|
|
221
|
-
errors << "remove nitro_kit-tailwind-v4
|
|
224
|
+
if assets.include?("nitro_kit-tailwind-v4")
|
|
225
|
+
errors << "remove stylesheet \"nitro_kit-tailwind-v4\"; nitro_kit now includes the Tailwind v4 layer order and theme aliases"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
if tailwind_bundles_nitro_kit? && assets.include?("nitro_kit")
|
|
229
|
+
errors << "remove stylesheet \"nitro_kit\"; #{TAILWIND_ENTRY} already bundles it"
|
|
222
230
|
end
|
|
223
231
|
|
|
224
232
|
bootstrap_position = analysis.bootstraps.first&.range&.begin
|
|
@@ -569,17 +577,20 @@ module NitroKit
|
|
|
569
577
|
assets = managed_stylesheet_assets(analysis) - [ "nitro_kit-tailwind-v4" ]
|
|
570
578
|
assets << "lexxy" if dependency?("lexxy")
|
|
571
579
|
assets << "tailwind" if tailwind?(assets)
|
|
572
|
-
|
|
580
|
+
if tailwind_bundles_nitro_kit?
|
|
581
|
+
assets -= [ "nitro_kit" ]
|
|
582
|
+
assets << "tailwind"
|
|
583
|
+
else
|
|
584
|
+
assets << "nitro_kit"
|
|
585
|
+
end
|
|
573
586
|
assets << "application" if assets.include?("application") || application_stylesheet_asset?
|
|
574
|
-
assets << "nitro_kit-tailwind-v4" if assets.include?("tailwind")
|
|
575
587
|
ordered_stylesheets(assets.uniq)
|
|
576
588
|
end
|
|
577
589
|
|
|
578
590
|
def ordered_stylesheets(assets)
|
|
579
|
-
vendor = assets - %w[nitro_kit
|
|
591
|
+
vendor = assets - %w[nitro_kit tailwind application]
|
|
580
592
|
[
|
|
581
593
|
*vendor,
|
|
582
|
-
*(%w[nitro_kit-tailwind-v4] & assets),
|
|
583
594
|
*(%w[nitro_kit] & assets),
|
|
584
595
|
*(%w[tailwind] & assets),
|
|
585
596
|
*(%w[application] & assets)
|
|
@@ -597,6 +608,9 @@ module NitroKit
|
|
|
597
608
|
|
|
598
609
|
existing = managed_stylesheet_assets(analysis)
|
|
599
610
|
return "duplicate canonical stylesheet assets require manual review" if existing.uniq != existing
|
|
611
|
+
if existing.include?("nitro_kit-tailwind-v4")
|
|
612
|
+
return "drop \"nitro_kit-tailwind-v4\" from its stylesheet call without changing the call's options"
|
|
613
|
+
end
|
|
600
614
|
unless existing == expected.select { existing.include?(_1) }
|
|
601
615
|
return "existing canonical stylesheets are misordered; reorder their intact calls manually"
|
|
602
616
|
end
|
|
@@ -657,7 +671,15 @@ module NitroKit
|
|
|
657
671
|
end
|
|
658
672
|
|
|
659
673
|
def tailwind?(assets)
|
|
660
|
-
assets.include?("tailwind") || dependency?("tailwindcss-rails") || application_root.join(
|
|
674
|
+
assets.include?("tailwind") || dependency?("tailwindcss-rails") || application_root.join(TAILWIND_ENTRY).file?
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
# True when the application's Tailwind source imports the engine entry
|
|
678
|
+
# that tailwindcss-rails generates from app/assets/tailwind/nitro_kit/engine.css,
|
|
679
|
+
# so the compiled "tailwind" stylesheet already contains Nitro Kit.
|
|
680
|
+
def tailwind_bundles_nitro_kit?
|
|
681
|
+
entry = application_root.join(TAILWIND_ENTRY)
|
|
682
|
+
entry.file? && entry.read.match?(TAILWIND_BUNDLE_IMPORT)
|
|
661
683
|
end
|
|
662
684
|
|
|
663
685
|
def application_stylesheet_asset?
|
data/lib/nitro_kit/version.rb
CHANGED
|
@@ -9,7 +9,7 @@ module NitroKit
|
|
|
9
9
|
ROOT = Pathname.new(File.expand_path("../..", __dir__))
|
|
10
10
|
SOURCE_ROOT = ROOT.join("src/stylesheets/nitro_kit")
|
|
11
11
|
OUTPUT = ROOT.join("app/assets/stylesheets/nitro_kit.css")
|
|
12
|
-
FOUNDATION_SOURCES = %w[ layers.css tokens.css reset.css ].freeze
|
|
12
|
+
FOUNDATION_SOURCES = %w[ layers.css tokens.css tailwind.css reset.css ].freeze
|
|
13
13
|
BANNER = <<~CSS.freeze
|
|
14
14
|
/*
|
|
15
15
|
* Nitro Kit 2.0
|
|
@@ -78,6 +78,8 @@
|
|
|
78
78
|
min-block-size: var(--nk-control-height-md);
|
|
79
79
|
margin: var(--nk-space) 0;
|
|
80
80
|
padding: calc(var(--nk-space) * 2) calc(var(--nk-space) * 3);
|
|
81
|
+
/* The label text is an anonymous flex item with a min-content floor. */
|
|
82
|
+
overflow-wrap: anywhere;
|
|
81
83
|
list-style: none;
|
|
82
84
|
user-select: none;
|
|
83
85
|
cursor: pointer;
|
|
@@ -135,7 +137,8 @@
|
|
|
135
137
|
border-radius: var(--nk-radius-lg);
|
|
136
138
|
transition:
|
|
137
139
|
color var(--nk-duration-normal) var(--nk-ease),
|
|
138
|
-
background-color var(--nk-duration-normal) var(--nk-ease)
|
|
140
|
+
background-color var(--nk-duration-normal) var(--nk-ease),
|
|
141
|
+
inline-size var(--nk-duration-fast) var(--nk-ease);
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
:where([data-nk="app-navigation"] [data-slot="app-navigation-item-label"]) {
|