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
@@ -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
  }
@@ -1,26 +1,88 @@
1
1
  @layer nitro-kit.base {
2
2
  :where([data-nk="card"]) {
3
3
  --_nk-card-padding: calc(var(--nk-space) * 6);
4
+ --_nk-card-gap: calc(var(--nk-space) * 4);
5
+ --_nk-card-radius: var(--nk-radius-xl);
6
+ --_nk-card-title-size: var(--nk-text-base);
7
+ --_nk-card-title-leading: calc(
8
+ var(--_nk-card-title-size) * var(--nk-leading-tight)
9
+ );
4
10
 
5
11
  display: flex;
6
12
  flex-direction: column;
7
13
  align-items: stretch;
8
- gap: calc(var(--nk-space) * 4);
14
+ gap: var(--_nk-card-gap);
9
15
  padding: var(--_nk-card-padding);
10
16
  color: var(--nk-color-foreground);
11
17
  background: var(--nk-color-surface);
12
18
  border: var(--nk-border-width) solid var(--nk-color-border);
13
- border-radius: var(--nk-radius-lg);
14
- box-shadow: var(--nk-shadow-sm);
19
+ border-radius: var(--_nk-card-radius);
20
+ box-shadow: var(--nk-shadow-xs);
15
21
  }
16
22
 
17
- :where([data-nk="card"] > [data-slot="card-title"]) {
18
- font-size: var(--nk-title-surface-size);
19
- font-weight: var(--nk-title-surface-weight);
20
- line-height: var(--nk-leading-tight);
23
+ :where(
24
+ [data-nk="card"] > [data-slot="card-title"],
25
+ [data-nk="card"] > [data-slot="card-header"] > [data-slot="card-title"]
26
+ ) {
27
+ font-size: var(--_nk-card-title-size);
28
+ font-weight: var(--nk-font-weight-semibold);
29
+ line-height: var(--_nk-card-title-leading);
21
30
  text-wrap: balance;
22
31
  }
23
32
 
33
+ :where(
34
+ [data-nk="card"] > [data-slot="card-description"],
35
+ [data-nk="card"]
36
+ > [data-slot="card-header"]
37
+ > [data-slot="card-description"]
38
+ ) {
39
+ color: var(--nk-color-muted-foreground);
40
+ font-size: var(--nk-text-sm);
41
+ line-height: var(--nk-leading-normal);
42
+ text-wrap: pretty;
43
+ }
44
+
45
+ /* The header pairs a title and description with trailing actions centred
46
+ beside them. Actions take negative block margins so a small button never
47
+ makes a title-only header taller than its title line. */
48
+ :where([data-nk="card"] > [data-slot="card-header"]) {
49
+ display: grid;
50
+ grid-template-columns: minmax(0, 1fr) auto;
51
+ column-gap: calc(var(--nk-space) * 4);
52
+ align-items: start;
53
+ }
54
+
55
+ :where(
56
+ [data-nk="card"]
57
+ > [data-slot="card-header"]
58
+ > [data-slot="card-title"]
59
+ + [data-slot="card-description"]
60
+ ) {
61
+ margin-block-start: calc(var(--nk-space) * 1);
62
+ }
63
+
64
+ :where([data-nk="card"] > [data-slot="card-header"] > *) {
65
+ grid-column: 1;
66
+ }
67
+
68
+ :where(
69
+ [data-nk="card"] > [data-slot="card-header"] > [data-slot="card-actions"]
70
+ ) {
71
+ grid-column: 2;
72
+ grid-row: 1 / span 2;
73
+ align-self: center;
74
+ margin-block: calc(
75
+ (var(--_nk-card-title-leading) - var(--nk-control-height-sm)) / 2
76
+ );
77
+ }
78
+
79
+ :where([data-nk="card"] [data-slot="card-actions"]) {
80
+ display: flex;
81
+ align-items: center;
82
+ flex-wrap: wrap;
83
+ gap: calc(var(--nk-space) * 2);
84
+ }
85
+
24
86
  :where([data-nk="card"] > [data-slot="card-body"]) {
25
87
  color: var(--nk-color-muted-foreground);
26
88
  font-size: var(--nk-text-sm);
@@ -33,10 +95,96 @@
33
95
  align-items: center;
34
96
  flex-wrap: wrap;
35
97
  gap: calc(var(--nk-space) * 2);
98
+ color: var(--nk-color-muted-foreground);
99
+ font-size: var(--nk-text-sm);
100
+ line-height: var(--nk-leading-normal);
101
+ /* Raw text becomes an anonymous flex item. Include emergency breaks in
102
+ its intrinsic width so long URLs can shrink with the footer. */
103
+ overflow-wrap: anywhere;
104
+ }
105
+
106
+ :where(
107
+ [data-nk="card"] > [data-slot="card-footer"] > [data-slot="card-actions"]
108
+ ) {
109
+ margin-inline-start: auto;
110
+ }
111
+ }
112
+
113
+ @layer nitro-kit.variant {
114
+ :where([data-nk="card"][data-variant="outline"]) {
115
+ background: transparent;
116
+ box-shadow: none;
117
+ }
118
+
119
+ :where([data-nk="card"][data-variant="muted"]) {
120
+ background: color-mix(in oklab, var(--nk-color-foreground) 4%, transparent);
121
+ box-shadow: none;
122
+ }
123
+ }
124
+
125
+ @layer nitro-kit.size {
126
+ :where([data-nk="card"][data-size="sm"]) {
127
+ --_nk-card-padding: calc(var(--nk-space) * 4);
128
+ --_nk-card-gap: calc(var(--nk-space) * 3);
129
+ --_nk-card-radius: var(--nk-radius-lg);
130
+ --_nk-card-title-size: var(--nk-text-sm);
131
+ }
132
+
133
+ :where([data-nk="card"][data-size="lg"]) {
134
+ --_nk-card-padding: calc(var(--nk-space) * 8);
135
+ --_nk-card-gap: calc(var(--nk-space) * 6);
136
+ --_nk-card-radius: calc(var(--nk-radius-xl) + var(--nk-space));
137
+ --_nk-card-title-size: var(--nk-text-lg);
36
138
  }
37
139
  }
38
140
 
39
141
  @layer nitro-kit.compound {
142
+ /* A description directly under a root-level title belongs to it. */
143
+ :where(
144
+ [data-nk="card"] > [data-slot="card-title"] + [data-slot="card-description"]
145
+ ) {
146
+ margin-block-start: calc(var(--nk-space) * 1 - var(--_nk-card-gap));
147
+ }
148
+
149
+ /* A section set apart by a divider at the card's edge is a band: the space
150
+ between the band and the edge matches the space between the band and its
151
+ divider, so a footer's actions or a header's title sit evenly. Full
152
+ regions keep the padding because they bleed through it. */
153
+ :where(
154
+ [data-nk="card"]:has(
155
+ > :first-child:not([data-slot="card-full"]):not(
156
+ [data-slot="card-divider"]
157
+ )
158
+ + [data-slot="card-divider"]
159
+ )
160
+ ) {
161
+ padding-block-start: var(--_nk-card-gap);
162
+ }
163
+
164
+ :where(
165
+ [data-nk="card"]:has(
166
+ > [data-slot="card-divider"]
167
+ + :last-child:not([data-slot="card-full"]):not(
168
+ [data-slot="card-divider"]
169
+ )
170
+ )
171
+ ) {
172
+ padding-block-end: var(--_nk-card-gap);
173
+ }
174
+
175
+ /* When a parent stretches the card, such as a grid row, the spare height
176
+ goes above a trailing footer and its divider, keeping actions on the
177
+ card's bottom edge across a row. */
178
+ :where(
179
+ [data-nk="card"]
180
+ > :not([data-slot="card-divider"])
181
+ + [data-slot="card-footer"]:last-child,
182
+ [data-nk="card"]
183
+ > [data-slot="card-divider"]:has(+ [data-slot="card-footer"]:last-child)
184
+ ) {
185
+ margin-block-start: auto;
186
+ }
187
+
40
188
  /* Only the region that bleeds to the edge clips, never the card itself, so
41
189
  an edge-to-edge image cannot break the card's corners while tooltips,
42
190
  dropdowns, and other overlays inside a card stay free to escape its box. */
@@ -48,27 +196,70 @@
48
196
  :where([data-nk="card"] > [data-slot="card-full"]:first-child) {
49
197
  margin-block-start: calc(var(--_nk-card-padding) * -1);
50
198
  border-start-start-radius: calc(
51
- var(--nk-radius-lg) - var(--nk-border-width)
199
+ var(--_nk-card-radius) - var(--nk-border-width)
200
+ );
201
+ border-start-end-radius: calc(
202
+ var(--_nk-card-radius) - var(--nk-border-width)
52
203
  );
53
- border-start-end-radius: calc(var(--nk-radius-lg) - var(--nk-border-width));
54
204
  }
55
205
 
56
206
  :where([data-nk="card"] > [data-slot="card-full"]:last-child) {
57
207
  margin-block-end: calc(var(--_nk-card-padding) * -1);
58
- border-end-start-radius: calc(var(--nk-radius-lg) - var(--nk-border-width));
59
- border-end-end-radius: calc(var(--nk-radius-lg) - var(--nk-border-width));
208
+ border-end-start-radius: calc(
209
+ var(--_nk-card-radius) - var(--nk-border-width)
210
+ );
211
+ border-end-end-radius: calc(
212
+ var(--_nk-card-radius) - var(--nk-border-width)
213
+ );
60
214
  }
61
215
 
62
216
  /* Full-width media fills the card edge to edge, so the card owns the corner
63
217
  radius and the media drops its own. */
64
218
  :where(
65
- [data-nk="card"]
66
- > [data-slot="card-full"]
67
- :is(img, [data-nk="progressive-image"])
68
- ) {
219
+ [data-nk="card"]
220
+ > [data-slot="card-full"]
221
+ :is(img, [data-nk="progressive-image"])
222
+ ) {
69
223
  border-radius: 0;
70
224
  }
71
225
 
226
+ /* A table bleeding to the card's edges keeps its first and last columns
227
+ and its caption on the card's content gutter. */
228
+ :where(
229
+ [data-nk="card"]
230
+ > [data-slot="card-full"]
231
+ > [data-nk="table"]
232
+ [data-slot="table-header"]:first-child,
233
+ [data-nk="card"]
234
+ > [data-slot="card-full"]
235
+ > [data-nk="table"]
236
+ [data-slot="table-cell"]:first-child
237
+ ) {
238
+ padding-inline-start: var(--_nk-card-padding);
239
+ }
240
+
241
+ :where(
242
+ [data-nk="card"]
243
+ > [data-slot="card-full"]
244
+ > [data-nk="table"]
245
+ [data-slot="table-header"]:last-child,
246
+ [data-nk="card"]
247
+ > [data-slot="card-full"]
248
+ > [data-nk="table"]
249
+ [data-slot="table-cell"]:last-child
250
+ ) {
251
+ padding-inline-end: var(--_nk-card-padding);
252
+ }
253
+
254
+ :where(
255
+ [data-nk="card"]
256
+ > [data-slot="card-full"]
257
+ > [data-nk="table"]
258
+ [data-slot="table-caption"]
259
+ ) {
260
+ padding-inline: var(--_nk-card-padding);
261
+ }
262
+
72
263
  :where([data-nk="card"] > [data-slot="card-divider"]) {
73
264
  margin-inline: calc(var(--_nk-card-padding) * -1);
74
265
  border-block-start: var(--nk-border-width) solid var(--nk-color-border);
@@ -13,7 +13,7 @@
13
13
  );
14
14
  border: var(--nk-border-width) solid
15
15
  color-mix(in oklab, var(--nk-color-destructive) 50%, transparent);
16
- border-radius: var(--nk-radius-lg);
16
+ border-radius: var(--nk-radius-xl);
17
17
  }
18
18
 
19
19
  :where([data-nk="danger-zone"] > [data-slot="danger-zone-header"]) {
@@ -443,3 +443,18 @@
443
443
  }
444
444
  }
445
445
  }
446
+
447
+ @layer nitro-kit.base {
448
+ /* Message children are grid items placed at start, so they size to their
449
+ content; cap them at the cell so one long word wraps inside the target. */
450
+ :where(
451
+ [data-nk="dropzone"]
452
+ [data-slot="dropzone-message"]
453
+ > [data-slot="dropzone-title"],
454
+ [data-nk="dropzone"]
455
+ [data-slot="dropzone-message"]
456
+ > [data-slot="dropzone-description"]
457
+ ) {
458
+ max-inline-size: 100%;
459
+ }
460
+ }
@@ -9,7 +9,7 @@
9
9
  text-align: center;
10
10
  background: var(--nk-color-surface);
11
11
  border: var(--nk-border-width) dashed var(--nk-color-border);
12
- border-radius: var(--nk-radius-lg);
12
+ border-radius: var(--nk-radius-xl);
13
13
  }
14
14
 
15
15
  :where([data-nk="empty-state"] > [data-slot="empty-state-icon"]) {
@@ -19,7 +19,7 @@
19
19
  }
20
20
 
21
21
  :where([data-nk="empty-state"] > [data-slot="empty-state-title"]) {
22
- max-inline-size: var(--nk-content-md);
22
+ max-inline-size: min(100%, var(--nk-content-md));
23
23
  font-size: var(--nk-title-surface-size);
24
24
  font-weight: var(--nk-title-surface-weight);
25
25
  line-height: var(--nk-leading-tight);
@@ -27,7 +27,7 @@
27
27
  }
28
28
 
29
29
  :where([data-nk="empty-state"] > [data-slot="empty-state-description"]) {
30
- max-inline-size: var(--nk-content-md);
30
+ max-inline-size: min(100%, var(--nk-content-md));
31
31
  color: var(--nk-color-muted-foreground);
32
32
  font-size: var(--nk-text-sm);
33
33
  line-height: var(--nk-leading-relaxed);
@@ -12,8 +12,12 @@
12
12
 
13
13
  /* A group keeps the same rhythm at either boundary as the fields inside it,
14
14
  so introductory copy and trailing actions never read as attached to the
15
- nearest control. */
16
- :where(* + [data-nk="field-group"], [data-nk="field-group"] + *) {
15
+ nearest control. Hidden inputs, such as the ones Rails emits at the top of
16
+ every form, are not visible neighbours and do not count. */
17
+ :where(
18
+ :not(input[type="hidden"]) + [data-nk="field-group"],
19
+ [data-nk="field-group"] + :not(input[type="hidden"])
20
+ ) {
17
21
  margin-block-start: calc(var(--nk-space) * 6);
18
22
  }
19
23
  }
@@ -28,6 +28,9 @@
28
28
  @media (width < 48rem) {
29
29
  :where([data-nk="pagination-bar"]) {
30
30
  flex-direction: column;
31
+ /* A wrapping column sizes its line to the widest item, so stretch
32
+ could not narrow anything; one column never needs to wrap. */
33
+ flex-wrap: nowrap;
31
34
  align-items: stretch;
32
35
  }
33
36
 
@@ -70,6 +70,8 @@
70
70
  font-size: var(--nk-text-sm);
71
71
  text-align: center;
72
72
  text-wrap: pretty;
73
+ /* The alt text is an anonymous grid item with a min-content floor. */
74
+ overflow-wrap: anywhere;
73
75
  }
74
76
  }
75
77
 
@@ -29,6 +29,8 @@
29
29
  display: flex;
30
30
  gap: calc(var(--nk-space) * 2);
31
31
  align-items: center;
32
+ /* The label text is an anonymous flex item with a min-content floor. */
33
+ overflow-wrap: anywhere;
32
34
  min-block-size: var(--nk-control-height-sm);
33
35
  padding: var(--nk-space) calc(var(--nk-space) * 2);
34
36
  color: var(--nk-color-muted-foreground);