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,6 +4,14 @@
4
4
  */
5
5
 
6
6
  /* Source: src/stylesheets/nitro_kit/layers.css */
7
+ /*
8
+ * Global cascade-layer order. Declared first so that, when an application also
9
+ * loads a compiled Tailwind CSS v4 stylesheet, Nitro Kit sits between
10
+ * Tailwind's base and components layers and Tailwind utilities still win.
11
+ * Without Tailwind the extra layers are simply unused.
12
+ */
13
+ @layer properties, theme, base, nitro-kit, components, utilities;
14
+
7
15
  @layer nitro-kit.tokens,
8
16
  nitro-kit.reset,
9
17
  nitro-kit.base,
@@ -40,8 +48,9 @@
40
48
 
41
49
  /* The title scale. Four roles cover every owned title and legend: page
42
50
  for the one page heading, section for full-width section headers,
43
- surface for cards, dialogs, sheets, empty states, and fieldsets, and
44
- compact for control-group legends and status titles. */
51
+ surface for dialogs, sheets, empty states, and fieldsets, and compact
52
+ for control-group legends and status titles. Card titles scale with
53
+ the card's own size instead. */
45
54
  --nk-title-page-size: var(--nk-text-2xl);
46
55
  --nk-title-page-weight: var(--nk-font-weight-bold);
47
56
  --nk-title-section-size: var(--nk-text-xl);
@@ -634,6 +643,45 @@
634
643
  }
635
644
  }
636
645
 
646
+ /* Source: src/stylesheets/nitro_kit/tailwind.css */
647
+ /*
648
+ * Tailwind CSS v4 theme aliases. Maps Nitro tokens onto the theme variables
649
+ * Tailwind utilities read, so bg-background, rounded-md, shadow-sm and friends
650
+ * share one palette with the components. Tailwind itself stays an application
651
+ * concern: Nitro Kit never compiles, configures, or depends on it.
652
+ */
653
+ @layer nitro-kit.tokens {
654
+ :where(:root, [data-theme]) {
655
+ --font-sans: var(--nk-font-sans);
656
+ --font-mono: var(--nk-font-mono);
657
+ --spacing: var(--nk-space);
658
+
659
+ --color-background: var(--nk-color-canvas);
660
+ --color-foreground: var(--nk-color-foreground);
661
+ --color-muted: var(--nk-color-muted);
662
+ --color-muted-content: var(--nk-color-muted-foreground);
663
+ --color-border: var(--nk-color-border);
664
+ --color-ring: var(--nk-color-focus);
665
+ --color-primary: var(--nk-color-primary);
666
+ --color-primary-foreground: var(--nk-color-primary-foreground);
667
+ --color-destructive: var(--nk-color-destructive);
668
+ --color-destructive-content: var(--nk-color-destructive-content);
669
+ --color-destructive-foreground: var(--nk-color-destructive-foreground);
670
+
671
+ --radius-sm: var(--nk-radius-sm);
672
+ --radius-md: var(--nk-radius-md);
673
+ --radius-lg: var(--nk-radius-lg);
674
+ --radius-xl: var(--nk-radius-xl);
675
+
676
+ --shadow-sm: var(--nk-shadow-sm);
677
+ --shadow-md: var(--nk-shadow-md);
678
+ --shadow-lg: var(--nk-shadow-lg);
679
+
680
+ --default-transition-duration: var(--nk-duration-normal);
681
+ --default-transition-timing-function: var(--nk-ease);
682
+ }
683
+ }
684
+
637
685
  /* Source: src/stylesheets/nitro_kit/reset.css */
638
686
  /* Nitro Kit ships its own full-page preflight. It lives in the
639
687
  `nitro-kit.reset` cascade layer, so unlayered application CSS always wins,
@@ -654,9 +702,19 @@
654
702
  }
655
703
 
656
704
  /* Nitro-owned boxes never establish a min-content floor, so long slot
657
- content shrinks instead of overflowing its flex or grid parent. */
705
+ content shrinks instead of overflowing its flex or grid parent, and a
706
+ word too long for its line breaks rather than painting past the box.
707
+ `break-word` leaves intrinsic sizing alone, so nothing shrinks that would
708
+ not have shrunk already; `nowrap` slots keep their single line. */
658
709
  :where([data-nk], [data-nk] [data-slot]) {
659
710
  min-inline-size: 0;
711
+ overflow-wrap: break-word;
712
+ }
713
+
714
+ /* A legend sizes to its content regardless of its fieldset, so a long
715
+ label would widen the page; cap it and let it wrap like any label. */
716
+ :where([data-nk] legend) {
717
+ max-inline-size: 100%;
660
718
  }
661
719
 
662
720
  :where(html, :host) {
@@ -743,7 +801,7 @@
743
801
  }
744
802
 
745
803
  :where(:disabled),
746
- ::file-selector-button:disabled {
804
+ :disabled::file-selector-button {
747
805
  cursor: default;
748
806
  }
749
807
 
@@ -1105,6 +1163,8 @@
1105
1163
  min-block-size: var(--nk-control-height-md);
1106
1164
  margin: var(--nk-space) 0;
1107
1165
  padding: calc(var(--nk-space) * 2) calc(var(--nk-space) * 3);
1166
+ /* The label text is an anonymous flex item with a min-content floor. */
1167
+ overflow-wrap: anywhere;
1108
1168
  list-style: none;
1109
1169
  user-select: none;
1110
1170
  cursor: pointer;
@@ -1162,7 +1222,8 @@
1162
1222
  border-radius: var(--nk-radius-lg);
1163
1223
  transition:
1164
1224
  color var(--nk-duration-normal) var(--nk-ease),
1165
- background-color var(--nk-duration-normal) var(--nk-ease);
1225
+ background-color var(--nk-duration-normal) var(--nk-ease),
1226
+ inline-size var(--nk-duration-fast) var(--nk-ease);
1166
1227
  }
1167
1228
 
1168
1229
  :where([data-nk="app-navigation"] [data-slot="app-navigation-item-label"]) {
@@ -1614,6 +1675,209 @@
1614
1675
  }
1615
1676
 
1616
1677
  @layer nitro-kit.compound {
1678
+ :where(
1679
+ [data-nk="app-shell"]
1680
+ > [data-slot="app-shell-sidebar"]
1681
+ > [data-slot="app-shell-sidebar-toggle"]
1682
+ ) {
1683
+ display: none;
1684
+ }
1685
+
1686
+ @media (width >= 48rem) {
1687
+ :where(
1688
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1689
+ ) {
1690
+ --_nk-shell-rail-width: calc(var(--nk-space) * 16);
1691
+ --_nk-shell-panel-width: var(--nk-app-shell-sidebar-width);
1692
+ --_nk-shell-label-clip: none;
1693
+ transition: grid-template-columns var(--nk-duration-fast) var(--nk-ease);
1694
+ }
1695
+
1696
+ :where(
1697
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1698
+ > [data-slot="app-shell-sidebar"]
1699
+ ) {
1700
+ z-index: 30;
1701
+ inline-size: var(--_nk-shell-panel-width);
1702
+ background-color: var(--nk-app-shell-background);
1703
+ background-image: linear-gradient(
1704
+ var(--nk-app-shell-sidebar-background),
1705
+ var(--nk-app-shell-sidebar-background)
1706
+ );
1707
+ transition: inline-size var(--nk-duration-fast) var(--nk-ease);
1708
+ }
1709
+
1710
+ :where(
1711
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1712
+ > [data-slot="app-shell-header"]
1713
+ > [data-slot="app-shell-brand"]:has(
1714
+ > [data-slot="app-shell-brand-icon"]
1715
+ )
1716
+ ) {
1717
+ gap: calc(var(--nk-space) * 2);
1718
+ padding-inline: calc(var(--nk-space) * 6);
1719
+ }
1720
+
1721
+ :where(
1722
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1723
+ > [data-slot="app-shell-header"]
1724
+ > [data-slot="app-shell-brand"]
1725
+ > [data-slot="app-shell-brand-icon"]
1726
+ ) {
1727
+ flex: none;
1728
+ }
1729
+
1730
+ :where(
1731
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1732
+ > [data-slot="app-shell-header"]
1733
+ > [data-slot="app-shell-brand"]
1734
+ > [data-slot="app-shell-brand-content"]
1735
+ ) {
1736
+ min-inline-size: 0;
1737
+ overflow: hidden;
1738
+ clip-path: var(--_nk-shell-label-clip, none);
1739
+ }
1740
+
1741
+ :where(
1742
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1743
+ > [data-slot="app-shell-header"]
1744
+ > [data-slot="app-shell-brand"]
1745
+ ) {
1746
+ z-index: 30;
1747
+ inline-size: var(--_nk-shell-panel-width);
1748
+ overflow: hidden;
1749
+ white-space: nowrap;
1750
+ background-color: var(--nk-app-shell-background);
1751
+ background-image: linear-gradient(
1752
+ var(--nk-app-shell-sidebar-background),
1753
+ var(--nk-app-shell-sidebar-background)
1754
+ );
1755
+ transition: inline-size var(--nk-duration-fast) var(--nk-ease);
1756
+ }
1757
+
1758
+ :where(
1759
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1760
+ > [data-slot="app-shell-sidebar"]
1761
+ > [data-slot="app-shell-navigation"]
1762
+ ) {
1763
+ flex: 1 1 0;
1764
+ inline-size: calc(
1765
+ var(--nk-app-shell-sidebar-width) - var(--nk-border-width)
1766
+ );
1767
+ min-inline-size: calc(
1768
+ var(--nk-app-shell-sidebar-width) - var(--nk-border-width)
1769
+ );
1770
+ }
1771
+
1772
+ :where(
1773
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1774
+ > [data-slot="app-shell-sidebar"]
1775
+ > [data-slot="app-shell-sidebar-toggle"]
1776
+ ) {
1777
+ display: inline-flex;
1778
+ flex: none;
1779
+ align-self: flex-start;
1780
+ margin: calc(var(--nk-space) * 3);
1781
+ margin-inline-start: calc(
1782
+ (var(--_nk-shell-rail-width) - var(--nk-control-height-md)) / 2
1783
+ );
1784
+ }
1785
+
1786
+ :where(
1787
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1788
+ > [data-slot="app-shell-sidebar"]
1789
+ [data-slot="app-navigation-item-link"]
1790
+ ) {
1791
+ inline-size: min(
1792
+ 100%,
1793
+ calc(
1794
+ var(--_nk-shell-panel-width) - (var(--_nk-app-navigation-inset) * 2)
1795
+ )
1796
+ );
1797
+ overflow: hidden;
1798
+ }
1799
+
1800
+ :where(
1801
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"][data-nk--app-shell-pinned-value="false"]
1802
+ ) {
1803
+ --_nk-shell-panel-width: var(--_nk-shell-rail-width);
1804
+ grid-template-columns: var(--_nk-shell-rail-width) minmax(0, 1fr);
1805
+ }
1806
+
1807
+ /* Keep the full navigation geometry: only its labels disappear. Focus
1808
+ reveals arbitrary header/footer content before it can be activated. */
1809
+ :where(
1810
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"][data-nk--app-shell-pinned-value="false"]:not(
1811
+ :has(
1812
+ > [data-slot="app-shell-sidebar"]
1813
+ > [data-slot="app-shell-navigation"]:focus-within,
1814
+ > [data-slot="app-shell-header"]
1815
+ > [data-slot="app-shell-brand"]:focus-within
1816
+ )
1817
+ )
1818
+ ) {
1819
+ --_nk-shell-label-clip: inset(50%);
1820
+ }
1821
+
1822
+ :where(
1823
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1824
+ > [data-slot="app-shell-sidebar"]
1825
+ [data-slot="app-navigation-item-label"],
1826
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1827
+ > [data-slot="app-shell-sidebar"]
1828
+ [data-slot="app-navigation-section-label"],
1829
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1830
+ > [data-slot="app-shell-sidebar"]
1831
+ [data-slot="app-navigation-item-badge"],
1832
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1833
+ > [data-slot="app-shell-sidebar"]
1834
+ [data-slot="app-navigation-item-icon-end"],
1835
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1836
+ > [data-slot="app-shell-sidebar"]
1837
+ [data-slot="app-navigation-header"],
1838
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1839
+ > [data-slot="app-shell-sidebar"]
1840
+ [data-slot="app-navigation-footer"]
1841
+ ) {
1842
+ clip-path: var(--_nk-shell-label-clip, none);
1843
+ }
1844
+
1845
+ :where(
1846
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]:has(
1847
+ > [data-slot="app-shell-sidebar"]
1848
+ > [data-slot="app-shell-navigation"]:focus-within,
1849
+ > [data-slot="app-shell-header"]
1850
+ > [data-slot="app-shell-brand"]:focus-within
1851
+ )
1852
+ ) {
1853
+ --_nk-shell-panel-width: var(--nk-app-shell-sidebar-width);
1854
+ --_nk-shell-label-clip: none;
1855
+ }
1856
+
1857
+ @media (hover: hover) and (pointer: fine) {
1858
+ :where(
1859
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]:not(
1860
+ [data-nk--app-shell-hover-suppressed-value="true"]
1861
+ ):has(
1862
+ > [data-slot="app-shell-sidebar"]
1863
+ > [data-slot="app-shell-navigation"]:hover,
1864
+ > [data-slot="app-shell-header"]
1865
+ > [data-slot="app-shell-brand"]:hover
1866
+ )
1867
+ ) {
1868
+ --_nk-shell-panel-width: var(--nk-app-shell-sidebar-width);
1869
+ --_nk-shell-label-clip: none;
1870
+ }
1871
+ }
1872
+
1873
+ :where(
1874
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"][data-nk--app-shell-pinned-value="false"]
1875
+ > [data-slot="app-shell-sidebar"]
1876
+ ) {
1877
+ box-shadow: var(--nk-shadow-md);
1878
+ }
1879
+ }
1880
+
1617
1881
  @media (width < 48rem) {
1618
1882
  :where([data-nk="app-shell"]) {
1619
1883
  grid-template-rows: auto auto minmax(0, 1fr);
@@ -1831,6 +2095,12 @@
1831
2095
 
1832
2096
  @media (prefers-reduced-motion: reduce) {
1833
2097
  :where(
2098
+ [data-nk="app-shell"][data-nk--app-shell-collapsible-value="true"],
2099
+ [data-nk="app-shell"][data-nk--app-shell-collapsible-value="true"]
2100
+ > [data-slot="app-shell-sidebar"],
2101
+ [data-nk="app-shell"][data-nk--app-shell-collapsible-value="true"]
2102
+ > [data-slot="app-shell-header"]
2103
+ > [data-slot="app-shell-brand"],
1834
2104
  [data-nk="app-shell"] > [data-slot="app-shell-skip-link"],
1835
2105
  [data-nk="app-shell"] > [data-slot="app-shell-dialog"],
1836
2106
  [data-nk="app-shell"]
@@ -2052,6 +2322,16 @@
2052
2322
  z-index: 1;
2053
2323
  object-fit: cover;
2054
2324
  }
2325
+
2326
+ /* Derived initials are two characters. Explicit fallbacks may run to four,
2327
+ so longer ones step the type down instead of clipping mid-letter. */
2328
+ :where([data-nk="avatar"] > [data-slot="avatar-fallback"][data-length="3"]) {
2329
+ font-size: 80%;
2330
+ }
2331
+
2332
+ :where([data-nk="avatar"] > [data-slot="avatar-fallback"][data-length="4"]) {
2333
+ font-size: 62.5%;
2334
+ }
2055
2335
  }
2056
2336
 
2057
2337
  @layer nitro-kit.state {
@@ -2133,7 +2413,9 @@
2133
2413
  }
2134
2414
 
2135
2415
  /* The chip is one of the heads: same fill as the avatar fallbacks and the
2136
- same hairline ring, drawn as a real border since nothing overlaps it. */
2416
+ same hairline ring, drawn as a real border since nothing overlaps it.
2417
+ Its size is a minimum, so a wide count grows it into a pill instead of
2418
+ spilling past the circle. */
2137
2419
  :where([data-nk="avatar-stack"] > [data-slot="avatar-stack-overflow"]) {
2138
2420
  /* Preceding avatars are positioned, so the overflow chip must be too or
2139
2421
  their paint order covers its leading "+". */
@@ -2143,16 +2425,18 @@
2143
2425
  font-weight: var(--nk-font-weight-semibold);
2144
2426
  line-height: 1;
2145
2427
  color: var(--nk-color-muted-foreground);
2428
+ white-space: nowrap;
2146
2429
  border: var(--nk-border-width) solid
2147
2430
  color-mix(in oklab, var(--nk-color-foreground) 10%, transparent);
2148
2431
  border-radius: var(--nk-radius-full);
2432
+ padding-inline: var(--nk-space);
2149
2433
  }
2150
2434
 
2151
2435
  :where(
2152
2436
  [data-nk="avatar-stack"][data-size="xs"]
2153
2437
  > [data-slot="avatar-stack-overflow"]
2154
2438
  ) {
2155
- inline-size: var(--nk-avatar-size-xs);
2439
+ min-inline-size: var(--nk-avatar-size-xs);
2156
2440
  block-size: var(--nk-avatar-size-xs);
2157
2441
  font-size: calc(var(--nk-text-xs) * 0.875);
2158
2442
  }
@@ -2161,7 +2445,7 @@
2161
2445
  [data-nk="avatar-stack"][data-size="sm"]
2162
2446
  > [data-slot="avatar-stack-overflow"]
2163
2447
  ) {
2164
- inline-size: var(--nk-avatar-size-sm);
2448
+ min-inline-size: var(--nk-avatar-size-sm);
2165
2449
  block-size: var(--nk-avatar-size-sm);
2166
2450
  font-size: var(--nk-text-xs);
2167
2451
  }
@@ -2170,7 +2454,7 @@
2170
2454
  [data-nk="avatar-stack"][data-size="md"]
2171
2455
  > [data-slot="avatar-stack-overflow"]
2172
2456
  ) {
2173
- inline-size: var(--nk-avatar-size-md);
2457
+ min-inline-size: var(--nk-avatar-size-md);
2174
2458
  block-size: var(--nk-avatar-size-md);
2175
2459
  font-size: var(--nk-text-sm);
2176
2460
  }
@@ -2179,7 +2463,7 @@
2179
2463
  [data-nk="avatar-stack"][data-size="lg"]
2180
2464
  > [data-slot="avatar-stack-overflow"]
2181
2465
  ) {
2182
- inline-size: var(--nk-avatar-size-lg);
2466
+ min-inline-size: var(--nk-avatar-size-lg);
2183
2467
  block-size: var(--nk-avatar-size-lg);
2184
2468
  font-size: var(--nk-text-base);
2185
2469
  }
@@ -2666,26 +2950,88 @@
2666
2950
  @layer nitro-kit.base {
2667
2951
  :where([data-nk="card"]) {
2668
2952
  --_nk-card-padding: calc(var(--nk-space) * 6);
2953
+ --_nk-card-gap: calc(var(--nk-space) * 4);
2954
+ --_nk-card-radius: var(--nk-radius-xl);
2955
+ --_nk-card-title-size: var(--nk-text-base);
2956
+ --_nk-card-title-leading: calc(
2957
+ var(--_nk-card-title-size) * var(--nk-leading-tight)
2958
+ );
2669
2959
 
2670
2960
  display: flex;
2671
2961
  flex-direction: column;
2672
2962
  align-items: stretch;
2673
- gap: calc(var(--nk-space) * 4);
2963
+ gap: var(--_nk-card-gap);
2674
2964
  padding: var(--_nk-card-padding);
2675
2965
  color: var(--nk-color-foreground);
2676
2966
  background: var(--nk-color-surface);
2677
2967
  border: var(--nk-border-width) solid var(--nk-color-border);
2678
- border-radius: var(--nk-radius-lg);
2679
- box-shadow: var(--nk-shadow-sm);
2968
+ border-radius: var(--_nk-card-radius);
2969
+ box-shadow: var(--nk-shadow-xs);
2680
2970
  }
2681
2971
 
2682
- :where([data-nk="card"] > [data-slot="card-title"]) {
2683
- font-size: var(--nk-title-surface-size);
2684
- font-weight: var(--nk-title-surface-weight);
2685
- line-height: var(--nk-leading-tight);
2972
+ :where(
2973
+ [data-nk="card"] > [data-slot="card-title"],
2974
+ [data-nk="card"] > [data-slot="card-header"] > [data-slot="card-title"]
2975
+ ) {
2976
+ font-size: var(--_nk-card-title-size);
2977
+ font-weight: var(--nk-font-weight-semibold);
2978
+ line-height: var(--_nk-card-title-leading);
2686
2979
  text-wrap: balance;
2687
2980
  }
2688
2981
 
2982
+ :where(
2983
+ [data-nk="card"] > [data-slot="card-description"],
2984
+ [data-nk="card"]
2985
+ > [data-slot="card-header"]
2986
+ > [data-slot="card-description"]
2987
+ ) {
2988
+ color: var(--nk-color-muted-foreground);
2989
+ font-size: var(--nk-text-sm);
2990
+ line-height: var(--nk-leading-normal);
2991
+ text-wrap: pretty;
2992
+ }
2993
+
2994
+ /* The header pairs a title and description with trailing actions centred
2995
+ beside them. Actions take negative block margins so a small button never
2996
+ makes a title-only header taller than its title line. */
2997
+ :where([data-nk="card"] > [data-slot="card-header"]) {
2998
+ display: grid;
2999
+ grid-template-columns: minmax(0, 1fr) auto;
3000
+ column-gap: calc(var(--nk-space) * 4);
3001
+ align-items: start;
3002
+ }
3003
+
3004
+ :where(
3005
+ [data-nk="card"]
3006
+ > [data-slot="card-header"]
3007
+ > [data-slot="card-title"]
3008
+ + [data-slot="card-description"]
3009
+ ) {
3010
+ margin-block-start: calc(var(--nk-space) * 1);
3011
+ }
3012
+
3013
+ :where([data-nk="card"] > [data-slot="card-header"] > *) {
3014
+ grid-column: 1;
3015
+ }
3016
+
3017
+ :where(
3018
+ [data-nk="card"] > [data-slot="card-header"] > [data-slot="card-actions"]
3019
+ ) {
3020
+ grid-column: 2;
3021
+ grid-row: 1 / span 2;
3022
+ align-self: center;
3023
+ margin-block: calc(
3024
+ (var(--_nk-card-title-leading) - var(--nk-control-height-sm)) / 2
3025
+ );
3026
+ }
3027
+
3028
+ :where([data-nk="card"] [data-slot="card-actions"]) {
3029
+ display: flex;
3030
+ align-items: center;
3031
+ flex-wrap: wrap;
3032
+ gap: calc(var(--nk-space) * 2);
3033
+ }
3034
+
2689
3035
  :where([data-nk="card"] > [data-slot="card-body"]) {
2690
3036
  color: var(--nk-color-muted-foreground);
2691
3037
  font-size: var(--nk-text-sm);
@@ -2698,10 +3044,96 @@
2698
3044
  align-items: center;
2699
3045
  flex-wrap: wrap;
2700
3046
  gap: calc(var(--nk-space) * 2);
3047
+ color: var(--nk-color-muted-foreground);
3048
+ font-size: var(--nk-text-sm);
3049
+ line-height: var(--nk-leading-normal);
3050
+ /* Raw text becomes an anonymous flex item. Include emergency breaks in
3051
+ its intrinsic width so long URLs can shrink with the footer. */
3052
+ overflow-wrap: anywhere;
3053
+ }
3054
+
3055
+ :where(
3056
+ [data-nk="card"] > [data-slot="card-footer"] > [data-slot="card-actions"]
3057
+ ) {
3058
+ margin-inline-start: auto;
3059
+ }
3060
+ }
3061
+
3062
+ @layer nitro-kit.variant {
3063
+ :where([data-nk="card"][data-variant="outline"]) {
3064
+ background: transparent;
3065
+ box-shadow: none;
3066
+ }
3067
+
3068
+ :where([data-nk="card"][data-variant="muted"]) {
3069
+ background: color-mix(in oklab, var(--nk-color-foreground) 4%, transparent);
3070
+ box-shadow: none;
3071
+ }
3072
+ }
3073
+
3074
+ @layer nitro-kit.size {
3075
+ :where([data-nk="card"][data-size="sm"]) {
3076
+ --_nk-card-padding: calc(var(--nk-space) * 4);
3077
+ --_nk-card-gap: calc(var(--nk-space) * 3);
3078
+ --_nk-card-radius: var(--nk-radius-lg);
3079
+ --_nk-card-title-size: var(--nk-text-sm);
3080
+ }
3081
+
3082
+ :where([data-nk="card"][data-size="lg"]) {
3083
+ --_nk-card-padding: calc(var(--nk-space) * 8);
3084
+ --_nk-card-gap: calc(var(--nk-space) * 6);
3085
+ --_nk-card-radius: calc(var(--nk-radius-xl) + var(--nk-space));
3086
+ --_nk-card-title-size: var(--nk-text-lg);
2701
3087
  }
2702
3088
  }
2703
3089
 
2704
3090
  @layer nitro-kit.compound {
3091
+ /* A description directly under a root-level title belongs to it. */
3092
+ :where(
3093
+ [data-nk="card"] > [data-slot="card-title"] + [data-slot="card-description"]
3094
+ ) {
3095
+ margin-block-start: calc(var(--nk-space) * 1 - var(--_nk-card-gap));
3096
+ }
3097
+
3098
+ /* A section set apart by a divider at the card's edge is a band: the space
3099
+ between the band and the edge matches the space between the band and its
3100
+ divider, so a footer's actions or a header's title sit evenly. Full
3101
+ regions keep the padding because they bleed through it. */
3102
+ :where(
3103
+ [data-nk="card"]:has(
3104
+ > :first-child:not([data-slot="card-full"]):not(
3105
+ [data-slot="card-divider"]
3106
+ )
3107
+ + [data-slot="card-divider"]
3108
+ )
3109
+ ) {
3110
+ padding-block-start: var(--_nk-card-gap);
3111
+ }
3112
+
3113
+ :where(
3114
+ [data-nk="card"]:has(
3115
+ > [data-slot="card-divider"]
3116
+ + :last-child:not([data-slot="card-full"]):not(
3117
+ [data-slot="card-divider"]
3118
+ )
3119
+ )
3120
+ ) {
3121
+ padding-block-end: var(--_nk-card-gap);
3122
+ }
3123
+
3124
+ /* When a parent stretches the card, such as a grid row, the spare height
3125
+ goes above a trailing footer and its divider, keeping actions on the
3126
+ card's bottom edge across a row. */
3127
+ :where(
3128
+ [data-nk="card"]
3129
+ > :not([data-slot="card-divider"])
3130
+ + [data-slot="card-footer"]:last-child,
3131
+ [data-nk="card"]
3132
+ > [data-slot="card-divider"]:has(+ [data-slot="card-footer"]:last-child)
3133
+ ) {
3134
+ margin-block-start: auto;
3135
+ }
3136
+
2705
3137
  /* Only the region that bleeds to the edge clips, never the card itself, so
2706
3138
  an edge-to-edge image cannot break the card's corners while tooltips,
2707
3139
  dropdowns, and other overlays inside a card stay free to escape its box. */
@@ -2713,27 +3145,70 @@
2713
3145
  :where([data-nk="card"] > [data-slot="card-full"]:first-child) {
2714
3146
  margin-block-start: calc(var(--_nk-card-padding) * -1);
2715
3147
  border-start-start-radius: calc(
2716
- var(--nk-radius-lg) - var(--nk-border-width)
3148
+ var(--_nk-card-radius) - var(--nk-border-width)
3149
+ );
3150
+ border-start-end-radius: calc(
3151
+ var(--_nk-card-radius) - var(--nk-border-width)
2717
3152
  );
2718
- border-start-end-radius: calc(var(--nk-radius-lg) - var(--nk-border-width));
2719
3153
  }
2720
3154
 
2721
3155
  :where([data-nk="card"] > [data-slot="card-full"]:last-child) {
2722
3156
  margin-block-end: calc(var(--_nk-card-padding) * -1);
2723
- border-end-start-radius: calc(var(--nk-radius-lg) - var(--nk-border-width));
2724
- border-end-end-radius: calc(var(--nk-radius-lg) - var(--nk-border-width));
3157
+ border-end-start-radius: calc(
3158
+ var(--_nk-card-radius) - var(--nk-border-width)
3159
+ );
3160
+ border-end-end-radius: calc(
3161
+ var(--_nk-card-radius) - var(--nk-border-width)
3162
+ );
2725
3163
  }
2726
3164
 
2727
3165
  /* Full-width media fills the card edge to edge, so the card owns the corner
2728
3166
  radius and the media drops its own. */
2729
3167
  :where(
2730
- [data-nk="card"]
2731
- > [data-slot="card-full"]
2732
- :is(img, [data-nk="progressive-image"])
2733
- ) {
3168
+ [data-nk="card"]
3169
+ > [data-slot="card-full"]
3170
+ :is(img, [data-nk="progressive-image"])
3171
+ ) {
2734
3172
  border-radius: 0;
2735
3173
  }
2736
3174
 
3175
+ /* A table bleeding to the card's edges keeps its first and last columns
3176
+ and its caption on the card's content gutter. */
3177
+ :where(
3178
+ [data-nk="card"]
3179
+ > [data-slot="card-full"]
3180
+ > [data-nk="table"]
3181
+ [data-slot="table-header"]:first-child,
3182
+ [data-nk="card"]
3183
+ > [data-slot="card-full"]
3184
+ > [data-nk="table"]
3185
+ [data-slot="table-cell"]:first-child
3186
+ ) {
3187
+ padding-inline-start: var(--_nk-card-padding);
3188
+ }
3189
+
3190
+ :where(
3191
+ [data-nk="card"]
3192
+ > [data-slot="card-full"]
3193
+ > [data-nk="table"]
3194
+ [data-slot="table-header"]:last-child,
3195
+ [data-nk="card"]
3196
+ > [data-slot="card-full"]
3197
+ > [data-nk="table"]
3198
+ [data-slot="table-cell"]:last-child
3199
+ ) {
3200
+ padding-inline-end: var(--_nk-card-padding);
3201
+ }
3202
+
3203
+ :where(
3204
+ [data-nk="card"]
3205
+ > [data-slot="card-full"]
3206
+ > [data-nk="table"]
3207
+ [data-slot="table-caption"]
3208
+ ) {
3209
+ padding-inline: var(--_nk-card-padding);
3210
+ }
3211
+
2737
3212
  :where([data-nk="card"] > [data-slot="card-divider"]) {
2738
3213
  margin-inline: calc(var(--_nk-card-padding) * -1);
2739
3214
  border-block-start: var(--nk-border-width) solid var(--nk-color-border);
@@ -3616,7 +4091,7 @@
3616
4091
  );
3617
4092
  border: var(--nk-border-width) solid
3618
4093
  color-mix(in oklab, var(--nk-color-destructive) 50%, transparent);
3619
- border-radius: var(--nk-radius-lg);
4094
+ border-radius: var(--nk-radius-xl);
3620
4095
  }
3621
4096
 
3622
4097
  :where([data-nk="danger-zone"] > [data-slot="danger-zone-header"]) {
@@ -4464,6 +4939,21 @@
4464
4939
  }
4465
4940
  }
4466
4941
 
4942
+ @layer nitro-kit.base {
4943
+ /* Message children are grid items placed at start, so they size to their
4944
+ content; cap them at the cell so one long word wraps inside the target. */
4945
+ :where(
4946
+ [data-nk="dropzone"]
4947
+ [data-slot="dropzone-message"]
4948
+ > [data-slot="dropzone-title"],
4949
+ [data-nk="dropzone"]
4950
+ [data-slot="dropzone-message"]
4951
+ > [data-slot="dropzone-description"]
4952
+ ) {
4953
+ max-inline-size: 100%;
4954
+ }
4955
+ }
4956
+
4467
4957
  /* Source: src/stylesheets/nitro_kit/components/empty_state.css */
4468
4958
  @layer nitro-kit.base {
4469
4959
  :where([data-nk="empty-state"]) {
@@ -4476,7 +4966,7 @@
4476
4966
  text-align: center;
4477
4967
  background: var(--nk-color-surface);
4478
4968
  border: var(--nk-border-width) dashed var(--nk-color-border);
4479
- border-radius: var(--nk-radius-lg);
4969
+ border-radius: var(--nk-radius-xl);
4480
4970
  }
4481
4971
 
4482
4972
  :where([data-nk="empty-state"] > [data-slot="empty-state-icon"]) {
@@ -4486,7 +4976,7 @@
4486
4976
  }
4487
4977
 
4488
4978
  :where([data-nk="empty-state"] > [data-slot="empty-state-title"]) {
4489
- max-inline-size: var(--nk-content-md);
4979
+ max-inline-size: min(100%, var(--nk-content-md));
4490
4980
  font-size: var(--nk-title-surface-size);
4491
4981
  font-weight: var(--nk-title-surface-weight);
4492
4982
  line-height: var(--nk-leading-tight);
@@ -4494,7 +4984,7 @@
4494
4984
  }
4495
4985
 
4496
4986
  :where([data-nk="empty-state"] > [data-slot="empty-state-description"]) {
4497
- max-inline-size: var(--nk-content-md);
4987
+ max-inline-size: min(100%, var(--nk-content-md));
4498
4988
  color: var(--nk-color-muted-foreground);
4499
4989
  font-size: var(--nk-text-sm);
4500
4990
  line-height: var(--nk-leading-relaxed);
@@ -4619,8 +5109,12 @@
4619
5109
 
4620
5110
  /* A group keeps the same rhythm at either boundary as the fields inside it,
4621
5111
  so introductory copy and trailing actions never read as attached to the
4622
- nearest control. */
4623
- :where(* + [data-nk="field-group"], [data-nk="field-group"] + *) {
5112
+ nearest control. Hidden inputs, such as the ones Rails emits at the top of
5113
+ every form, are not visible neighbours and do not count. */
5114
+ :where(
5115
+ :not(input[type="hidden"]) + [data-nk="field-group"],
5116
+ [data-nk="field-group"] + :not(input[type="hidden"])
5117
+ ) {
4624
5118
  margin-block-start: calc(var(--nk-space) * 6);
4625
5119
  }
4626
5120
  }
@@ -6238,6 +6732,9 @@
6238
6732
  @media (width < 48rem) {
6239
6733
  :where([data-nk="pagination-bar"]) {
6240
6734
  flex-direction: column;
6735
+ /* A wrapping column sizes its line to the widest item, so stretch
6736
+ could not narrow anything; one column never needs to wrap. */
6737
+ flex-wrap: nowrap;
6241
6738
  align-items: stretch;
6242
6739
  }
6243
6740
 
@@ -6506,6 +7003,8 @@
6506
7003
  font-size: var(--nk-text-sm);
6507
7004
  text-align: center;
6508
7005
  text-wrap: pretty;
7006
+ /* The alt text is an anonymous grid item with a min-content floor. */
7007
+ overflow-wrap: anywhere;
6509
7008
  }
6510
7009
  }
6511
7010
 
@@ -6991,6 +7490,8 @@
6991
7490
  display: flex;
6992
7491
  gap: calc(var(--nk-space) * 2);
6993
7492
  align-items: center;
7493
+ /* The label text is an anonymous flex item with a min-content floor. */
7494
+ overflow-wrap: anywhere;
6994
7495
  min-block-size: var(--nk-control-height-sm);
6995
7496
  padding: var(--nk-space) calc(var(--nk-space) * 2);
6996
7497
  color: var(--nk-color-muted-foreground);
@@ -7297,11 +7798,11 @@
7297
7798
  align-content: start;
7298
7799
  gap: calc(var(--nk-space) * 2);
7299
7800
  min-block-size: 100%;
7300
- padding: calc(var(--nk-space) * 5);
7801
+ padding: calc(var(--nk-space) * 6);
7301
7802
  background: var(--nk-color-surface);
7302
7803
  border: var(--nk-border-width) solid var(--nk-color-border);
7303
- border-radius: var(--nk-radius-lg);
7304
- box-shadow: var(--nk-shadow-sm);
7804
+ border-radius: var(--nk-radius-xl);
7805
+ box-shadow: var(--nk-shadow-xs);
7305
7806
  }
7306
7807
 
7307
7808
  :where([data-nk="stat-grid"] [data-slot="stat-grid-label"]) {
@@ -7313,7 +7814,7 @@
7313
7814
  :where([data-nk="stat-grid"] [data-slot="stat-grid-value"]) {
7314
7815
  font-size: var(--nk-text-2xl);
7315
7816
  font-variant-numeric: tabular-nums;
7316
- font-weight: var(--nk-font-weight-bold);
7817
+ font-weight: var(--nk-font-weight-semibold);
7317
7818
  line-height: var(--nk-leading-tight);
7318
7819
  overflow-wrap: anywhere;
7319
7820
  }
@@ -7935,12 +8436,24 @@
7935
8436
  justify-content: flex-end;
7936
8437
  margin-inline-start: auto;
7937
8438
  }
8439
+
8440
+ /* Application content in a region is a flex item; without this a heading
8441
+ holding one long word keeps its min-content width and widens the page. */
8442
+ :where(
8443
+ [data-nk="toolbar"] > [data-slot="toolbar-leading"] > *,
8444
+ [data-nk="toolbar"] > [data-slot="toolbar-trailing"] > *
8445
+ ) {
8446
+ min-inline-size: 0;
8447
+ }
7938
8448
  }
7939
8449
 
7940
8450
  @layer nitro-kit.state {
7941
8451
  @media (width < 48rem) {
7942
8452
  :where([data-nk="toolbar"]) {
7943
8453
  flex-direction: column;
8454
+ /* A wrapping column sizes its line to the widest item, so stretch
8455
+ could not narrow anything; one column never needs to wrap. */
8456
+ flex-wrap: nowrap;
7944
8457
  align-items: stretch;
7945
8458
  }
7946
8459
 
@@ -7973,6 +8486,9 @@
7973
8486
  line-height: var(--nk-leading-normal);
7974
8487
  text-wrap: pretty;
7975
8488
  pointer-events: auto;
8489
+ /* Hidden tooltips leave layout entirely; a merely invisible one still
8490
+ widens the page when it hangs past the viewport edge. */
8491
+ display: none;
7976
8492
  visibility: hidden;
7977
8493
  background: var(--nk-color-elevated);
7978
8494
  border: var(--nk-border-width) solid var(--nk-color-border);
@@ -7981,7 +8497,8 @@
7981
8497
  opacity: 0;
7982
8498
  transition:
7983
8499
  opacity var(--nk-duration-fast) var(--nk-ease),
7984
- visibility var(--nk-duration-fast) var(--nk-ease) allow-discrete;
8500
+ visibility var(--nk-duration-fast) var(--nk-ease) allow-discrete,
8501
+ display var(--nk-duration-fast) var(--nk-ease) allow-discrete;
7985
8502
  }
7986
8503
 
7987
8504
  :where([data-nk="tooltip"] > [data-slot="tooltip-content"]::before) {
@@ -8065,8 +8582,13 @@
8065
8582
  [data-nk="tooltip"]:not([data-dismissed]):focus-within
8066
8583
  > [data-slot="tooltip-content"]
8067
8584
  ) {
8585
+ display: block;
8068
8586
  visibility: visible;
8069
8587
  opacity: 1;
8588
+
8589
+ @starting-style {
8590
+ opacity: 0;
8591
+ }
8070
8592
  }
8071
8593
 
8072
8594
  @media (hover: hover) {
@@ -8074,8 +8596,13 @@
8074
8596
  [data-nk="tooltip"]:not([data-dismissed]):hover
8075
8597
  > [data-slot="tooltip-content"]
8076
8598
  ) {
8599
+ display: block;
8077
8600
  visibility: visible;
8078
8601
  opacity: 1;
8602
+
8603
+ @starting-style {
8604
+ opacity: 0;
8605
+ }
8079
8606
  }
8080
8607
  }
8081
8608
 
@@ -8207,6 +8734,9 @@
8207
8734
  padding: calc(var(--nk-space) * 2) calc(var(--nk-space) * 3);
8208
8735
  text-align: start;
8209
8736
  vertical-align: top;
8737
+ /* Prose tables have no scroll wrapper, so an unbroken email or
8738
+ identifier must be allowed to break inside its cell. */
8739
+ overflow-wrap: anywhere;
8210
8740
  border-block-end: var(--nk-border-width) solid var(--nk-color-border);
8211
8741
  }
8212
8742
 
@@ -8348,6 +8878,7 @@
8348
8878
  padding: calc(var(--nk-space) * 2) calc(var(--nk-space) * 3);
8349
8879
  text-align: start;
8350
8880
  vertical-align: top;
8881
+ overflow-wrap: anywhere;
8351
8882
  border-block-end: var(--nk-border-width) solid var(--nk-color-border);
8352
8883
  }
8353
8884