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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -1
  3. data/README.md +16 -2
  4. data/STYLE_GUIDE.md +4 -2
  5. data/app/assets/stylesheets/nitro_kit.css +567 -36
  6. data/app/assets/tailwind/nitro_kit/engine.css +15 -0
  7. data/app/components/nitro_kit/app_shell.rb +41 -5
  8. data/app/components/nitro_kit/avatar.rb +21 -2
  9. data/app/components/nitro_kit/card.rb +28 -1
  10. data/app/javascript/controllers/nk/app_shell_controller.js +64 -2
  11. data/config/locales/en.yml +1 -0
  12. data/docs/agent_native_spec.md +5 -4
  13. data/docs/browser_support.md +7 -1
  14. data/docs/component_contracts.md +93 -5
  15. data/docs/customization.md +35 -15
  16. data/docs/eject.md +86 -0
  17. data/docs/migration_1_to_2.md +1 -1
  18. data/docs/patterns/application_foundation.md +22 -0
  19. data/docs/patterns/inset_workspace.md +9 -1
  20. data/docs/rails_integration.md +4 -2
  21. data/lib/generators/nitro_kit/eject_generator.rb +26 -0
  22. data/lib/nitro_kit/ejection.rb +136 -0
  23. data/lib/nitro_kit/engine.rb +13 -0
  24. data/lib/nitro_kit/installation.rb +29 -7
  25. data/lib/nitro_kit/version.rb +1 -1
  26. data/lib/tasks/nitro_kit_tasks.rake +1 -1
  27. data/src/stylesheets/nitro_kit/components/app_navigation.css +4 -1
  28. data/src/stylesheets/nitro_kit/components/app_shell.css +209 -0
  29. data/src/stylesheets/nitro_kit/components/avatar.css +10 -0
  30. data/src/stylesheets/nitro_kit/components/avatar_stack.css +9 -5
  31. data/src/stylesheets/nitro_kit/components/card.css +206 -15
  32. data/src/stylesheets/nitro_kit/components/danger_zone.css +1 -1
  33. data/src/stylesheets/nitro_kit/components/dropzone.css +15 -0
  34. data/src/stylesheets/nitro_kit/components/empty_state.css +3 -3
  35. data/src/stylesheets/nitro_kit/components/field_group.css +6 -2
  36. data/src/stylesheets/nitro_kit/components/pagination_bar.css +3 -0
  37. data/src/stylesheets/nitro_kit/components/progressive_image.css +2 -0
  38. data/src/stylesheets/nitro_kit/components/settings_layout.css +2 -0
  39. data/src/stylesheets/nitro_kit/components/stat_grid.css +4 -4
  40. data/src/stylesheets/nitro_kit/components/toolbar.css +12 -0
  41. data/src/stylesheets/nitro_kit/components/tooltip.css +15 -1
  42. data/src/stylesheets/nitro_kit/components/typeset.css +4 -0
  43. data/src/stylesheets/nitro_kit/layers.css +8 -0
  44. data/src/stylesheets/nitro_kit/reset.css +12 -2
  45. data/{app/assets/stylesheets/nitro_kit-tailwind-v4.css → src/stylesheets/nitro_kit/tailwind.css} +4 -5
  46. data/src/stylesheets/nitro_kit/tokens.css +3 -2
  47. metadata +7 -3
@@ -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 the system by overriding the public `--nk-*` custom properties, composing components into application UI, and occasionally creating a narrow subclass. Applications do not copy or edit Nitro components.
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 optional `nitro_kit-tailwind-v4` adapter.
15
- 3. The generated `nitro_kit` distribution stylesheet.
16
- 4. The application's compiled Tailwind CSS, when present.
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
- ## Optional Tailwind CSS v4 adapter
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. The optional `nitro_kit-tailwind-v4.css` asset only establishes 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.
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 adapter does not make Tailwind a Nitro runtime dependency, configure source detection, generate utility classes, or permit Tailwind classes inside Nitro component APIs.
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: cards, dialogs, sheets, empty states, fieldsets. |
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.
@@ -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.1"
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
- Product resource gallery runs the sidebar example with this stylesheet at
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
@@ -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.1"
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
@@ -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") && !expected.include?("tailwind")
221
- errors << "remove nitro_kit-tailwind-v4 or load compiled Tailwind after nitro_kit"
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
- assets << "nitro_kit"
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-tailwind-v4 nitro_kit tailwind application]
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("app/assets/tailwind/application.css").file?
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?
@@ -1,3 +1,3 @@
1
1
  module NitroKit
2
- VERSION = "2.0.0.beta.1"
2
+ VERSION = "2.0.0.beta.3"
3
3
  end
@@ -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"]) {