nitro_kit 2.0.0.beta.1 → 2.0.0.beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -1
- data/README.md +16 -2
- data/STYLE_GUIDE.md +3 -1
- data/app/assets/stylesheets/nitro_kit.css +519 -35
- data/app/components/nitro_kit/app_shell.rb +41 -5
- data/app/components/nitro_kit/avatar.rb +21 -2
- data/app/components/nitro_kit/card.rb +28 -1
- data/app/javascript/controllers/nk/app_shell_controller.js +64 -2
- data/config/locales/en.yml +1 -0
- data/docs/agent_native_spec.md +5 -4
- data/docs/component_contracts.md +93 -5
- data/docs/customization.md +2 -2
- data/docs/eject.md +86 -0
- data/docs/migration_1_to_2.md +1 -1
- data/docs/patterns/application_foundation.md +22 -0
- data/docs/patterns/inset_workspace.md +9 -1
- data/docs/rails_integration.md +1 -1
- data/lib/generators/nitro_kit/eject_generator.rb +26 -0
- data/lib/nitro_kit/ejection.rb +136 -0
- data/lib/nitro_kit/version.rb +1 -1
- data/src/stylesheets/nitro_kit/components/app_navigation.css +4 -1
- data/src/stylesheets/nitro_kit/components/app_shell.css +209 -0
- data/src/stylesheets/nitro_kit/components/avatar.css +10 -0
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +9 -5
- data/src/stylesheets/nitro_kit/components/card.css +206 -15
- data/src/stylesheets/nitro_kit/components/danger_zone.css +1 -1
- data/src/stylesheets/nitro_kit/components/dropzone.css +15 -0
- data/src/stylesheets/nitro_kit/components/empty_state.css +3 -3
- data/src/stylesheets/nitro_kit/components/field_group.css +6 -2
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +3 -0
- data/src/stylesheets/nitro_kit/components/progressive_image.css +2 -0
- data/src/stylesheets/nitro_kit/components/settings_layout.css +2 -0
- data/src/stylesheets/nitro_kit/components/stat_grid.css +4 -4
- data/src/stylesheets/nitro_kit/components/toolbar.css +12 -0
- data/src/stylesheets/nitro_kit/components/tooltip.css +15 -1
- data/src/stylesheets/nitro_kit/components/typeset.css +4 -0
- data/src/stylesheets/nitro_kit/reset.css +11 -1
- data/src/stylesheets/nitro_kit/tokens.css +3 -2
- metadata +5 -2
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.2"
|
|
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.2"
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Use the released gem and commit `Gemfile` with `Gemfile.lock`. Before upgrading,
|
|
@@ -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/version.rb
CHANGED
|
@@ -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"]) {
|
|
@@ -357,6 +357,209 @@
|
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
@layer nitro-kit.compound {
|
|
360
|
+
:where(
|
|
361
|
+
[data-nk="app-shell"]
|
|
362
|
+
> [data-slot="app-shell-sidebar"]
|
|
363
|
+
> [data-slot="app-shell-sidebar-toggle"]
|
|
364
|
+
) {
|
|
365
|
+
display: none;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
@media (width >= 48rem) {
|
|
369
|
+
:where(
|
|
370
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
371
|
+
) {
|
|
372
|
+
--_nk-shell-rail-width: calc(var(--nk-space) * 16);
|
|
373
|
+
--_nk-shell-panel-width: var(--nk-app-shell-sidebar-width);
|
|
374
|
+
--_nk-shell-label-clip: none;
|
|
375
|
+
transition: grid-template-columns var(--nk-duration-fast) var(--nk-ease);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
:where(
|
|
379
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
380
|
+
> [data-slot="app-shell-sidebar"]
|
|
381
|
+
) {
|
|
382
|
+
z-index: 30;
|
|
383
|
+
inline-size: var(--_nk-shell-panel-width);
|
|
384
|
+
background-color: var(--nk-app-shell-background);
|
|
385
|
+
background-image: linear-gradient(
|
|
386
|
+
var(--nk-app-shell-sidebar-background),
|
|
387
|
+
var(--nk-app-shell-sidebar-background)
|
|
388
|
+
);
|
|
389
|
+
transition: inline-size var(--nk-duration-fast) var(--nk-ease);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
:where(
|
|
393
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
394
|
+
> [data-slot="app-shell-header"]
|
|
395
|
+
> [data-slot="app-shell-brand"]:has(
|
|
396
|
+
> [data-slot="app-shell-brand-icon"]
|
|
397
|
+
)
|
|
398
|
+
) {
|
|
399
|
+
gap: calc(var(--nk-space) * 2);
|
|
400
|
+
padding-inline: calc(var(--nk-space) * 6);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
:where(
|
|
404
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
405
|
+
> [data-slot="app-shell-header"]
|
|
406
|
+
> [data-slot="app-shell-brand"]
|
|
407
|
+
> [data-slot="app-shell-brand-icon"]
|
|
408
|
+
) {
|
|
409
|
+
flex: none;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
:where(
|
|
413
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
414
|
+
> [data-slot="app-shell-header"]
|
|
415
|
+
> [data-slot="app-shell-brand"]
|
|
416
|
+
> [data-slot="app-shell-brand-content"]
|
|
417
|
+
) {
|
|
418
|
+
min-inline-size: 0;
|
|
419
|
+
overflow: hidden;
|
|
420
|
+
clip-path: var(--_nk-shell-label-clip, none);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
:where(
|
|
424
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
425
|
+
> [data-slot="app-shell-header"]
|
|
426
|
+
> [data-slot="app-shell-brand"]
|
|
427
|
+
) {
|
|
428
|
+
z-index: 30;
|
|
429
|
+
inline-size: var(--_nk-shell-panel-width);
|
|
430
|
+
overflow: hidden;
|
|
431
|
+
white-space: nowrap;
|
|
432
|
+
background-color: var(--nk-app-shell-background);
|
|
433
|
+
background-image: linear-gradient(
|
|
434
|
+
var(--nk-app-shell-sidebar-background),
|
|
435
|
+
var(--nk-app-shell-sidebar-background)
|
|
436
|
+
);
|
|
437
|
+
transition: inline-size var(--nk-duration-fast) var(--nk-ease);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
:where(
|
|
441
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
442
|
+
> [data-slot="app-shell-sidebar"]
|
|
443
|
+
> [data-slot="app-shell-navigation"]
|
|
444
|
+
) {
|
|
445
|
+
flex: 1 1 0;
|
|
446
|
+
inline-size: calc(
|
|
447
|
+
var(--nk-app-shell-sidebar-width) - var(--nk-border-width)
|
|
448
|
+
);
|
|
449
|
+
min-inline-size: calc(
|
|
450
|
+
var(--nk-app-shell-sidebar-width) - var(--nk-border-width)
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
:where(
|
|
455
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
456
|
+
> [data-slot="app-shell-sidebar"]
|
|
457
|
+
> [data-slot="app-shell-sidebar-toggle"]
|
|
458
|
+
) {
|
|
459
|
+
display: inline-flex;
|
|
460
|
+
flex: none;
|
|
461
|
+
align-self: flex-start;
|
|
462
|
+
margin: calc(var(--nk-space) * 3);
|
|
463
|
+
margin-inline-start: calc(
|
|
464
|
+
(var(--_nk-shell-rail-width) - var(--nk-control-height-md)) / 2
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
:where(
|
|
469
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
470
|
+
> [data-slot="app-shell-sidebar"]
|
|
471
|
+
[data-slot="app-navigation-item-link"]
|
|
472
|
+
) {
|
|
473
|
+
inline-size: min(
|
|
474
|
+
100%,
|
|
475
|
+
calc(
|
|
476
|
+
var(--_nk-shell-panel-width) - (var(--_nk-app-navigation-inset) * 2)
|
|
477
|
+
)
|
|
478
|
+
);
|
|
479
|
+
overflow: hidden;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
:where(
|
|
483
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"][data-nk--app-shell-pinned-value="false"]
|
|
484
|
+
) {
|
|
485
|
+
--_nk-shell-panel-width: var(--_nk-shell-rail-width);
|
|
486
|
+
grid-template-columns: var(--_nk-shell-rail-width) minmax(0, 1fr);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* Keep the full navigation geometry: only its labels disappear. Focus
|
|
490
|
+
reveals arbitrary header/footer content before it can be activated. */
|
|
491
|
+
:where(
|
|
492
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"][data-nk--app-shell-pinned-value="false"]:not(
|
|
493
|
+
:has(
|
|
494
|
+
> [data-slot="app-shell-sidebar"]
|
|
495
|
+
> [data-slot="app-shell-navigation"]:focus-within,
|
|
496
|
+
> [data-slot="app-shell-header"]
|
|
497
|
+
> [data-slot="app-shell-brand"]:focus-within
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
) {
|
|
501
|
+
--_nk-shell-label-clip: inset(50%);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
:where(
|
|
505
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
506
|
+
> [data-slot="app-shell-sidebar"]
|
|
507
|
+
[data-slot="app-navigation-item-label"],
|
|
508
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
509
|
+
> [data-slot="app-shell-sidebar"]
|
|
510
|
+
[data-slot="app-navigation-section-label"],
|
|
511
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
512
|
+
> [data-slot="app-shell-sidebar"]
|
|
513
|
+
[data-slot="app-navigation-item-badge"],
|
|
514
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
515
|
+
> [data-slot="app-shell-sidebar"]
|
|
516
|
+
[data-slot="app-navigation-item-icon-end"],
|
|
517
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
518
|
+
> [data-slot="app-shell-sidebar"]
|
|
519
|
+
[data-slot="app-navigation-header"],
|
|
520
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
|
|
521
|
+
> [data-slot="app-shell-sidebar"]
|
|
522
|
+
[data-slot="app-navigation-footer"]
|
|
523
|
+
) {
|
|
524
|
+
clip-path: var(--_nk-shell-label-clip, none);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
:where(
|
|
528
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]:has(
|
|
529
|
+
> [data-slot="app-shell-sidebar"]
|
|
530
|
+
> [data-slot="app-shell-navigation"]:focus-within,
|
|
531
|
+
> [data-slot="app-shell-header"]
|
|
532
|
+
> [data-slot="app-shell-brand"]:focus-within
|
|
533
|
+
)
|
|
534
|
+
) {
|
|
535
|
+
--_nk-shell-panel-width: var(--nk-app-shell-sidebar-width);
|
|
536
|
+
--_nk-shell-label-clip: none;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
@media (hover: hover) and (pointer: fine) {
|
|
540
|
+
:where(
|
|
541
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]:not(
|
|
542
|
+
[data-nk--app-shell-hover-suppressed-value="true"]
|
|
543
|
+
):has(
|
|
544
|
+
> [data-slot="app-shell-sidebar"]
|
|
545
|
+
> [data-slot="app-shell-navigation"]:hover,
|
|
546
|
+
> [data-slot="app-shell-header"]
|
|
547
|
+
> [data-slot="app-shell-brand"]:hover
|
|
548
|
+
)
|
|
549
|
+
) {
|
|
550
|
+
--_nk-shell-panel-width: var(--nk-app-shell-sidebar-width);
|
|
551
|
+
--_nk-shell-label-clip: none;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
:where(
|
|
556
|
+
[data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"][data-nk--app-shell-pinned-value="false"]
|
|
557
|
+
> [data-slot="app-shell-sidebar"]
|
|
558
|
+
) {
|
|
559
|
+
box-shadow: var(--nk-shadow-md);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
360
563
|
@media (width < 48rem) {
|
|
361
564
|
:where([data-nk="app-shell"]) {
|
|
362
565
|
grid-template-rows: auto auto minmax(0, 1fr);
|
|
@@ -574,6 +777,12 @@
|
|
|
574
777
|
|
|
575
778
|
@media (prefers-reduced-motion: reduce) {
|
|
576
779
|
:where(
|
|
780
|
+
[data-nk="app-shell"][data-nk--app-shell-collapsible-value="true"],
|
|
781
|
+
[data-nk="app-shell"][data-nk--app-shell-collapsible-value="true"]
|
|
782
|
+
> [data-slot="app-shell-sidebar"],
|
|
783
|
+
[data-nk="app-shell"][data-nk--app-shell-collapsible-value="true"]
|
|
784
|
+
> [data-slot="app-shell-header"]
|
|
785
|
+
> [data-slot="app-shell-brand"],
|
|
577
786
|
[data-nk="app-shell"] > [data-slot="app-shell-skip-link"],
|
|
578
787
|
[data-nk="app-shell"] > [data-slot="app-shell-dialog"],
|
|
579
788
|
[data-nk="app-shell"]
|
|
@@ -52,6 +52,16 @@
|
|
|
52
52
|
z-index: 1;
|
|
53
53
|
object-fit: cover;
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
/* Derived initials are two characters. Explicit fallbacks may run to four,
|
|
57
|
+
so longer ones step the type down instead of clipping mid-letter. */
|
|
58
|
+
:where([data-nk="avatar"] > [data-slot="avatar-fallback"][data-length="3"]) {
|
|
59
|
+
font-size: 80%;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
:where([data-nk="avatar"] > [data-slot="avatar-fallback"][data-length="4"]) {
|
|
63
|
+
font-size: 62.5%;
|
|
64
|
+
}
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
@layer nitro-kit.state {
|
|
@@ -46,7 +46,9 @@
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/* The chip is one of the heads: same fill as the avatar fallbacks and the
|
|
49
|
-
same hairline ring, drawn as a real border since nothing overlaps it.
|
|
49
|
+
same hairline ring, drawn as a real border since nothing overlaps it.
|
|
50
|
+
Its size is a minimum, so a wide count grows it into a pill instead of
|
|
51
|
+
spilling past the circle. */
|
|
50
52
|
:where([data-nk="avatar-stack"] > [data-slot="avatar-stack-overflow"]) {
|
|
51
53
|
/* Preceding avatars are positioned, so the overflow chip must be too or
|
|
52
54
|
their paint order covers its leading "+". */
|
|
@@ -56,16 +58,18 @@
|
|
|
56
58
|
font-weight: var(--nk-font-weight-semibold);
|
|
57
59
|
line-height: 1;
|
|
58
60
|
color: var(--nk-color-muted-foreground);
|
|
61
|
+
white-space: nowrap;
|
|
59
62
|
border: var(--nk-border-width) solid
|
|
60
63
|
color-mix(in oklab, var(--nk-color-foreground) 10%, transparent);
|
|
61
64
|
border-radius: var(--nk-radius-full);
|
|
65
|
+
padding-inline: var(--nk-space);
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
:where(
|
|
65
69
|
[data-nk="avatar-stack"][data-size="xs"]
|
|
66
70
|
> [data-slot="avatar-stack-overflow"]
|
|
67
71
|
) {
|
|
68
|
-
inline-size: var(--nk-avatar-size-xs);
|
|
72
|
+
min-inline-size: var(--nk-avatar-size-xs);
|
|
69
73
|
block-size: var(--nk-avatar-size-xs);
|
|
70
74
|
font-size: calc(var(--nk-text-xs) * 0.875);
|
|
71
75
|
}
|
|
@@ -74,7 +78,7 @@
|
|
|
74
78
|
[data-nk="avatar-stack"][data-size="sm"]
|
|
75
79
|
> [data-slot="avatar-stack-overflow"]
|
|
76
80
|
) {
|
|
77
|
-
inline-size: var(--nk-avatar-size-sm);
|
|
81
|
+
min-inline-size: var(--nk-avatar-size-sm);
|
|
78
82
|
block-size: var(--nk-avatar-size-sm);
|
|
79
83
|
font-size: var(--nk-text-xs);
|
|
80
84
|
}
|
|
@@ -83,7 +87,7 @@
|
|
|
83
87
|
[data-nk="avatar-stack"][data-size="md"]
|
|
84
88
|
> [data-slot="avatar-stack-overflow"]
|
|
85
89
|
) {
|
|
86
|
-
inline-size: var(--nk-avatar-size-md);
|
|
90
|
+
min-inline-size: var(--nk-avatar-size-md);
|
|
87
91
|
block-size: var(--nk-avatar-size-md);
|
|
88
92
|
font-size: var(--nk-text-sm);
|
|
89
93
|
}
|
|
@@ -92,7 +96,7 @@
|
|
|
92
96
|
[data-nk="avatar-stack"][data-size="lg"]
|
|
93
97
|
> [data-slot="avatar-stack-overflow"]
|
|
94
98
|
) {
|
|
95
|
-
inline-size: var(--nk-avatar-size-lg);
|
|
99
|
+
min-inline-size: var(--nk-avatar-size-lg);
|
|
96
100
|
block-size: var(--nk-avatar-size-lg);
|
|
97
101
|
font-size: var(--nk-text-base);
|
|
98
102
|
}
|