nitro_kit 2.0.0.alpha.6 → 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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -1
  3. data/README.md +16 -2
  4. data/STYLE_GUIDE.md +3 -1
  5. data/app/assets/stylesheets/nitro_kit.css +519 -35
  6. data/app/components/nitro_kit/app_shell.rb +41 -5
  7. data/app/components/nitro_kit/avatar.rb +21 -2
  8. data/app/components/nitro_kit/card.rb +28 -1
  9. data/app/javascript/controllers/nk/app_shell_controller.js +64 -2
  10. data/config/locales/en.yml +1 -0
  11. data/docs/agent_native_spec.md +5 -4
  12. data/docs/component_contracts.md +93 -5
  13. data/docs/customization.md +2 -2
  14. data/docs/eject.md +86 -0
  15. data/docs/migration_1_to_2.md +1 -1
  16. data/docs/patterns/application_foundation.md +22 -0
  17. data/docs/patterns/inset_workspace.md +9 -1
  18. data/docs/rails_integration.md +1 -1
  19. data/lib/generators/nitro_kit/eject_generator.rb +26 -0
  20. data/lib/nitro_kit/ejection.rb +136 -0
  21. data/lib/nitro_kit/version.rb +1 -1
  22. data/src/stylesheets/nitro_kit/components/app_navigation.css +4 -1
  23. data/src/stylesheets/nitro_kit/components/app_shell.css +209 -0
  24. data/src/stylesheets/nitro_kit/components/avatar.css +10 -0
  25. data/src/stylesheets/nitro_kit/components/avatar_stack.css +9 -5
  26. data/src/stylesheets/nitro_kit/components/card.css +206 -15
  27. data/src/stylesheets/nitro_kit/components/danger_zone.css +1 -1
  28. data/src/stylesheets/nitro_kit/components/dropzone.css +15 -0
  29. data/src/stylesheets/nitro_kit/components/empty_state.css +3 -3
  30. data/src/stylesheets/nitro_kit/components/field_group.css +6 -2
  31. data/src/stylesheets/nitro_kit/components/pagination_bar.css +3 -0
  32. data/src/stylesheets/nitro_kit/components/progressive_image.css +2 -0
  33. data/src/stylesheets/nitro_kit/components/settings_layout.css +2 -0
  34. data/src/stylesheets/nitro_kit/components/stat_grid.css +4 -4
  35. data/src/stylesheets/nitro_kit/components/toolbar.css +12 -0
  36. data/src/stylesheets/nitro_kit/components/tooltip.css +15 -1
  37. data/src/stylesheets/nitro_kit/components/typeset.css +4 -0
  38. data/src/stylesheets/nitro_kit/reset.css +11 -1
  39. data/src/stylesheets/nitro_kit/tokens.css +3 -2
  40. metadata +5 -2
@@ -40,8 +40,9 @@
40
40
 
41
41
  /* The title scale. Four roles cover every owned title and legend: page
42
42
  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. */
43
+ surface for dialogs, sheets, empty states, and fieldsets, and compact
44
+ for control-group legends and status titles. Card titles scale with
45
+ the card's own size instead. */
45
46
  --nk-title-page-size: var(--nk-text-2xl);
46
47
  --nk-title-page-weight: var(--nk-font-weight-bold);
47
48
  --nk-title-section-size: var(--nk-text-xl);
@@ -654,9 +655,19 @@
654
655
  }
655
656
 
656
657
  /* Nitro-owned boxes never establish a min-content floor, so long slot
657
- content shrinks instead of overflowing its flex or grid parent. */
658
+ content shrinks instead of overflowing its flex or grid parent, and a
659
+ word too long for its line breaks rather than painting past the box.
660
+ `break-word` leaves intrinsic sizing alone, so nothing shrinks that would
661
+ not have shrunk already; `nowrap` slots keep their single line. */
658
662
  :where([data-nk], [data-nk] [data-slot]) {
659
663
  min-inline-size: 0;
664
+ overflow-wrap: break-word;
665
+ }
666
+
667
+ /* A legend sizes to its content regardless of its fieldset, so a long
668
+ label would widen the page; cap it and let it wrap like any label. */
669
+ :where([data-nk] legend) {
670
+ max-inline-size: 100%;
660
671
  }
661
672
 
662
673
  :where(html, :host) {
@@ -1105,6 +1116,8 @@
1105
1116
  min-block-size: var(--nk-control-height-md);
1106
1117
  margin: var(--nk-space) 0;
1107
1118
  padding: calc(var(--nk-space) * 2) calc(var(--nk-space) * 3);
1119
+ /* The label text is an anonymous flex item with a min-content floor. */
1120
+ overflow-wrap: anywhere;
1108
1121
  list-style: none;
1109
1122
  user-select: none;
1110
1123
  cursor: pointer;
@@ -1162,7 +1175,8 @@
1162
1175
  border-radius: var(--nk-radius-lg);
1163
1176
  transition:
1164
1177
  color var(--nk-duration-normal) var(--nk-ease),
1165
- background-color var(--nk-duration-normal) var(--nk-ease);
1178
+ background-color var(--nk-duration-normal) var(--nk-ease),
1179
+ inline-size var(--nk-duration-fast) var(--nk-ease);
1166
1180
  }
1167
1181
 
1168
1182
  :where([data-nk="app-navigation"] [data-slot="app-navigation-item-label"]) {
@@ -1614,6 +1628,209 @@
1614
1628
  }
1615
1629
 
1616
1630
  @layer nitro-kit.compound {
1631
+ :where(
1632
+ [data-nk="app-shell"]
1633
+ > [data-slot="app-shell-sidebar"]
1634
+ > [data-slot="app-shell-sidebar-toggle"]
1635
+ ) {
1636
+ display: none;
1637
+ }
1638
+
1639
+ @media (width >= 48rem) {
1640
+ :where(
1641
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1642
+ ) {
1643
+ --_nk-shell-rail-width: calc(var(--nk-space) * 16);
1644
+ --_nk-shell-panel-width: var(--nk-app-shell-sidebar-width);
1645
+ --_nk-shell-label-clip: none;
1646
+ transition: grid-template-columns var(--nk-duration-fast) var(--nk-ease);
1647
+ }
1648
+
1649
+ :where(
1650
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1651
+ > [data-slot="app-shell-sidebar"]
1652
+ ) {
1653
+ z-index: 30;
1654
+ inline-size: var(--_nk-shell-panel-width);
1655
+ background-color: var(--nk-app-shell-background);
1656
+ background-image: linear-gradient(
1657
+ var(--nk-app-shell-sidebar-background),
1658
+ var(--nk-app-shell-sidebar-background)
1659
+ );
1660
+ transition: inline-size var(--nk-duration-fast) var(--nk-ease);
1661
+ }
1662
+
1663
+ :where(
1664
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1665
+ > [data-slot="app-shell-header"]
1666
+ > [data-slot="app-shell-brand"]:has(
1667
+ > [data-slot="app-shell-brand-icon"]
1668
+ )
1669
+ ) {
1670
+ gap: calc(var(--nk-space) * 2);
1671
+ padding-inline: calc(var(--nk-space) * 6);
1672
+ }
1673
+
1674
+ :where(
1675
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1676
+ > [data-slot="app-shell-header"]
1677
+ > [data-slot="app-shell-brand"]
1678
+ > [data-slot="app-shell-brand-icon"]
1679
+ ) {
1680
+ flex: none;
1681
+ }
1682
+
1683
+ :where(
1684
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1685
+ > [data-slot="app-shell-header"]
1686
+ > [data-slot="app-shell-brand"]
1687
+ > [data-slot="app-shell-brand-content"]
1688
+ ) {
1689
+ min-inline-size: 0;
1690
+ overflow: hidden;
1691
+ clip-path: var(--_nk-shell-label-clip, none);
1692
+ }
1693
+
1694
+ :where(
1695
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1696
+ > [data-slot="app-shell-header"]
1697
+ > [data-slot="app-shell-brand"]
1698
+ ) {
1699
+ z-index: 30;
1700
+ inline-size: var(--_nk-shell-panel-width);
1701
+ overflow: hidden;
1702
+ white-space: nowrap;
1703
+ background-color: var(--nk-app-shell-background);
1704
+ background-image: linear-gradient(
1705
+ var(--nk-app-shell-sidebar-background),
1706
+ var(--nk-app-shell-sidebar-background)
1707
+ );
1708
+ transition: inline-size var(--nk-duration-fast) var(--nk-ease);
1709
+ }
1710
+
1711
+ :where(
1712
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1713
+ > [data-slot="app-shell-sidebar"]
1714
+ > [data-slot="app-shell-navigation"]
1715
+ ) {
1716
+ flex: 1 1 0;
1717
+ inline-size: calc(
1718
+ var(--nk-app-shell-sidebar-width) - var(--nk-border-width)
1719
+ );
1720
+ min-inline-size: calc(
1721
+ var(--nk-app-shell-sidebar-width) - var(--nk-border-width)
1722
+ );
1723
+ }
1724
+
1725
+ :where(
1726
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1727
+ > [data-slot="app-shell-sidebar"]
1728
+ > [data-slot="app-shell-sidebar-toggle"]
1729
+ ) {
1730
+ display: inline-flex;
1731
+ flex: none;
1732
+ align-self: flex-start;
1733
+ margin: calc(var(--nk-space) * 3);
1734
+ margin-inline-start: calc(
1735
+ (var(--_nk-shell-rail-width) - var(--nk-control-height-md)) / 2
1736
+ );
1737
+ }
1738
+
1739
+ :where(
1740
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1741
+ > [data-slot="app-shell-sidebar"]
1742
+ [data-slot="app-navigation-item-link"]
1743
+ ) {
1744
+ inline-size: min(
1745
+ 100%,
1746
+ calc(
1747
+ var(--_nk-shell-panel-width) - (var(--_nk-app-navigation-inset) * 2)
1748
+ )
1749
+ );
1750
+ overflow: hidden;
1751
+ }
1752
+
1753
+ :where(
1754
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"][data-nk--app-shell-pinned-value="false"]
1755
+ ) {
1756
+ --_nk-shell-panel-width: var(--_nk-shell-rail-width);
1757
+ grid-template-columns: var(--_nk-shell-rail-width) minmax(0, 1fr);
1758
+ }
1759
+
1760
+ /* Keep the full navigation geometry: only its labels disappear. Focus
1761
+ reveals arbitrary header/footer content before it can be activated. */
1762
+ :where(
1763
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"][data-nk--app-shell-pinned-value="false"]:not(
1764
+ :has(
1765
+ > [data-slot="app-shell-sidebar"]
1766
+ > [data-slot="app-shell-navigation"]:focus-within,
1767
+ > [data-slot="app-shell-header"]
1768
+ > [data-slot="app-shell-brand"]:focus-within
1769
+ )
1770
+ )
1771
+ ) {
1772
+ --_nk-shell-label-clip: inset(50%);
1773
+ }
1774
+
1775
+ :where(
1776
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1777
+ > [data-slot="app-shell-sidebar"]
1778
+ [data-slot="app-navigation-item-label"],
1779
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1780
+ > [data-slot="app-shell-sidebar"]
1781
+ [data-slot="app-navigation-section-label"],
1782
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1783
+ > [data-slot="app-shell-sidebar"]
1784
+ [data-slot="app-navigation-item-badge"],
1785
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1786
+ > [data-slot="app-shell-sidebar"]
1787
+ [data-slot="app-navigation-item-icon-end"],
1788
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1789
+ > [data-slot="app-shell-sidebar"]
1790
+ [data-slot="app-navigation-header"],
1791
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]
1792
+ > [data-slot="app-shell-sidebar"]
1793
+ [data-slot="app-navigation-footer"]
1794
+ ) {
1795
+ clip-path: var(--_nk-shell-label-clip, none);
1796
+ }
1797
+
1798
+ :where(
1799
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]:has(
1800
+ > [data-slot="app-shell-sidebar"]
1801
+ > [data-slot="app-shell-navigation"]:focus-within,
1802
+ > [data-slot="app-shell-header"]
1803
+ > [data-slot="app-shell-brand"]:focus-within
1804
+ )
1805
+ ) {
1806
+ --_nk-shell-panel-width: var(--nk-app-shell-sidebar-width);
1807
+ --_nk-shell-label-clip: none;
1808
+ }
1809
+
1810
+ @media (hover: hover) and (pointer: fine) {
1811
+ :where(
1812
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"]:not(
1813
+ [data-nk--app-shell-hover-suppressed-value="true"]
1814
+ ):has(
1815
+ > [data-slot="app-shell-sidebar"]
1816
+ > [data-slot="app-shell-navigation"]:hover,
1817
+ > [data-slot="app-shell-header"]
1818
+ > [data-slot="app-shell-brand"]:hover
1819
+ )
1820
+ ) {
1821
+ --_nk-shell-panel-width: var(--nk-app-shell-sidebar-width);
1822
+ --_nk-shell-label-clip: none;
1823
+ }
1824
+ }
1825
+
1826
+ :where(
1827
+ [data-nk="app-shell"][data-layout="sidebar"][data-enhanced][data-nk--app-shell-collapsible-value="true"][data-nk--app-shell-pinned-value="false"]
1828
+ > [data-slot="app-shell-sidebar"]
1829
+ ) {
1830
+ box-shadow: var(--nk-shadow-md);
1831
+ }
1832
+ }
1833
+
1617
1834
  @media (width < 48rem) {
1618
1835
  :where([data-nk="app-shell"]) {
1619
1836
  grid-template-rows: auto auto minmax(0, 1fr);
@@ -1831,6 +2048,12 @@
1831
2048
 
1832
2049
  @media (prefers-reduced-motion: reduce) {
1833
2050
  :where(
2051
+ [data-nk="app-shell"][data-nk--app-shell-collapsible-value="true"],
2052
+ [data-nk="app-shell"][data-nk--app-shell-collapsible-value="true"]
2053
+ > [data-slot="app-shell-sidebar"],
2054
+ [data-nk="app-shell"][data-nk--app-shell-collapsible-value="true"]
2055
+ > [data-slot="app-shell-header"]
2056
+ > [data-slot="app-shell-brand"],
1834
2057
  [data-nk="app-shell"] > [data-slot="app-shell-skip-link"],
1835
2058
  [data-nk="app-shell"] > [data-slot="app-shell-dialog"],
1836
2059
  [data-nk="app-shell"]
@@ -2052,6 +2275,16 @@
2052
2275
  z-index: 1;
2053
2276
  object-fit: cover;
2054
2277
  }
2278
+
2279
+ /* Derived initials are two characters. Explicit fallbacks may run to four,
2280
+ so longer ones step the type down instead of clipping mid-letter. */
2281
+ :where([data-nk="avatar"] > [data-slot="avatar-fallback"][data-length="3"]) {
2282
+ font-size: 80%;
2283
+ }
2284
+
2285
+ :where([data-nk="avatar"] > [data-slot="avatar-fallback"][data-length="4"]) {
2286
+ font-size: 62.5%;
2287
+ }
2055
2288
  }
2056
2289
 
2057
2290
  @layer nitro-kit.state {
@@ -2133,7 +2366,9 @@
2133
2366
  }
2134
2367
 
2135
2368
  /* 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. */
2369
+ same hairline ring, drawn as a real border since nothing overlaps it.
2370
+ Its size is a minimum, so a wide count grows it into a pill instead of
2371
+ spilling past the circle. */
2137
2372
  :where([data-nk="avatar-stack"] > [data-slot="avatar-stack-overflow"]) {
2138
2373
  /* Preceding avatars are positioned, so the overflow chip must be too or
2139
2374
  their paint order covers its leading "+". */
@@ -2143,16 +2378,18 @@
2143
2378
  font-weight: var(--nk-font-weight-semibold);
2144
2379
  line-height: 1;
2145
2380
  color: var(--nk-color-muted-foreground);
2381
+ white-space: nowrap;
2146
2382
  border: var(--nk-border-width) solid
2147
2383
  color-mix(in oklab, var(--nk-color-foreground) 10%, transparent);
2148
2384
  border-radius: var(--nk-radius-full);
2385
+ padding-inline: var(--nk-space);
2149
2386
  }
2150
2387
 
2151
2388
  :where(
2152
2389
  [data-nk="avatar-stack"][data-size="xs"]
2153
2390
  > [data-slot="avatar-stack-overflow"]
2154
2391
  ) {
2155
- inline-size: var(--nk-avatar-size-xs);
2392
+ min-inline-size: var(--nk-avatar-size-xs);
2156
2393
  block-size: var(--nk-avatar-size-xs);
2157
2394
  font-size: calc(var(--nk-text-xs) * 0.875);
2158
2395
  }
@@ -2161,7 +2398,7 @@
2161
2398
  [data-nk="avatar-stack"][data-size="sm"]
2162
2399
  > [data-slot="avatar-stack-overflow"]
2163
2400
  ) {
2164
- inline-size: var(--nk-avatar-size-sm);
2401
+ min-inline-size: var(--nk-avatar-size-sm);
2165
2402
  block-size: var(--nk-avatar-size-sm);
2166
2403
  font-size: var(--nk-text-xs);
2167
2404
  }
@@ -2170,7 +2407,7 @@
2170
2407
  [data-nk="avatar-stack"][data-size="md"]
2171
2408
  > [data-slot="avatar-stack-overflow"]
2172
2409
  ) {
2173
- inline-size: var(--nk-avatar-size-md);
2410
+ min-inline-size: var(--nk-avatar-size-md);
2174
2411
  block-size: var(--nk-avatar-size-md);
2175
2412
  font-size: var(--nk-text-sm);
2176
2413
  }
@@ -2179,7 +2416,7 @@
2179
2416
  [data-nk="avatar-stack"][data-size="lg"]
2180
2417
  > [data-slot="avatar-stack-overflow"]
2181
2418
  ) {
2182
- inline-size: var(--nk-avatar-size-lg);
2419
+ min-inline-size: var(--nk-avatar-size-lg);
2183
2420
  block-size: var(--nk-avatar-size-lg);
2184
2421
  font-size: var(--nk-text-base);
2185
2422
  }
@@ -2666,26 +2903,88 @@
2666
2903
  @layer nitro-kit.base {
2667
2904
  :where([data-nk="card"]) {
2668
2905
  --_nk-card-padding: calc(var(--nk-space) * 6);
2906
+ --_nk-card-gap: calc(var(--nk-space) * 4);
2907
+ --_nk-card-radius: var(--nk-radius-xl);
2908
+ --_nk-card-title-size: var(--nk-text-base);
2909
+ --_nk-card-title-leading: calc(
2910
+ var(--_nk-card-title-size) * var(--nk-leading-tight)
2911
+ );
2669
2912
 
2670
2913
  display: flex;
2671
2914
  flex-direction: column;
2672
2915
  align-items: stretch;
2673
- gap: calc(var(--nk-space) * 4);
2916
+ gap: var(--_nk-card-gap);
2674
2917
  padding: var(--_nk-card-padding);
2675
2918
  color: var(--nk-color-foreground);
2676
2919
  background: var(--nk-color-surface);
2677
2920
  border: var(--nk-border-width) solid var(--nk-color-border);
2678
- border-radius: var(--nk-radius-lg);
2679
- box-shadow: var(--nk-shadow-sm);
2921
+ border-radius: var(--_nk-card-radius);
2922
+ box-shadow: var(--nk-shadow-xs);
2680
2923
  }
2681
2924
 
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);
2925
+ :where(
2926
+ [data-nk="card"] > [data-slot="card-title"],
2927
+ [data-nk="card"] > [data-slot="card-header"] > [data-slot="card-title"]
2928
+ ) {
2929
+ font-size: var(--_nk-card-title-size);
2930
+ font-weight: var(--nk-font-weight-semibold);
2931
+ line-height: var(--_nk-card-title-leading);
2686
2932
  text-wrap: balance;
2687
2933
  }
2688
2934
 
2935
+ :where(
2936
+ [data-nk="card"] > [data-slot="card-description"],
2937
+ [data-nk="card"]
2938
+ > [data-slot="card-header"]
2939
+ > [data-slot="card-description"]
2940
+ ) {
2941
+ color: var(--nk-color-muted-foreground);
2942
+ font-size: var(--nk-text-sm);
2943
+ line-height: var(--nk-leading-normal);
2944
+ text-wrap: pretty;
2945
+ }
2946
+
2947
+ /* The header pairs a title and description with trailing actions centred
2948
+ beside them. Actions take negative block margins so a small button never
2949
+ makes a title-only header taller than its title line. */
2950
+ :where([data-nk="card"] > [data-slot="card-header"]) {
2951
+ display: grid;
2952
+ grid-template-columns: minmax(0, 1fr) auto;
2953
+ column-gap: calc(var(--nk-space) * 4);
2954
+ align-items: start;
2955
+ }
2956
+
2957
+ :where(
2958
+ [data-nk="card"]
2959
+ > [data-slot="card-header"]
2960
+ > [data-slot="card-title"]
2961
+ + [data-slot="card-description"]
2962
+ ) {
2963
+ margin-block-start: calc(var(--nk-space) * 1);
2964
+ }
2965
+
2966
+ :where([data-nk="card"] > [data-slot="card-header"] > *) {
2967
+ grid-column: 1;
2968
+ }
2969
+
2970
+ :where(
2971
+ [data-nk="card"] > [data-slot="card-header"] > [data-slot="card-actions"]
2972
+ ) {
2973
+ grid-column: 2;
2974
+ grid-row: 1 / span 2;
2975
+ align-self: center;
2976
+ margin-block: calc(
2977
+ (var(--_nk-card-title-leading) - var(--nk-control-height-sm)) / 2
2978
+ );
2979
+ }
2980
+
2981
+ :where([data-nk="card"] [data-slot="card-actions"]) {
2982
+ display: flex;
2983
+ align-items: center;
2984
+ flex-wrap: wrap;
2985
+ gap: calc(var(--nk-space) * 2);
2986
+ }
2987
+
2689
2988
  :where([data-nk="card"] > [data-slot="card-body"]) {
2690
2989
  color: var(--nk-color-muted-foreground);
2691
2990
  font-size: var(--nk-text-sm);
@@ -2698,10 +2997,96 @@
2698
2997
  align-items: center;
2699
2998
  flex-wrap: wrap;
2700
2999
  gap: calc(var(--nk-space) * 2);
3000
+ color: var(--nk-color-muted-foreground);
3001
+ font-size: var(--nk-text-sm);
3002
+ line-height: var(--nk-leading-normal);
3003
+ /* Raw text becomes an anonymous flex item. Include emergency breaks in
3004
+ its intrinsic width so long URLs can shrink with the footer. */
3005
+ overflow-wrap: anywhere;
3006
+ }
3007
+
3008
+ :where(
3009
+ [data-nk="card"] > [data-slot="card-footer"] > [data-slot="card-actions"]
3010
+ ) {
3011
+ margin-inline-start: auto;
3012
+ }
3013
+ }
3014
+
3015
+ @layer nitro-kit.variant {
3016
+ :where([data-nk="card"][data-variant="outline"]) {
3017
+ background: transparent;
3018
+ box-shadow: none;
3019
+ }
3020
+
3021
+ :where([data-nk="card"][data-variant="muted"]) {
3022
+ background: color-mix(in oklab, var(--nk-color-foreground) 4%, transparent);
3023
+ box-shadow: none;
3024
+ }
3025
+ }
3026
+
3027
+ @layer nitro-kit.size {
3028
+ :where([data-nk="card"][data-size="sm"]) {
3029
+ --_nk-card-padding: calc(var(--nk-space) * 4);
3030
+ --_nk-card-gap: calc(var(--nk-space) * 3);
3031
+ --_nk-card-radius: var(--nk-radius-lg);
3032
+ --_nk-card-title-size: var(--nk-text-sm);
3033
+ }
3034
+
3035
+ :where([data-nk="card"][data-size="lg"]) {
3036
+ --_nk-card-padding: calc(var(--nk-space) * 8);
3037
+ --_nk-card-gap: calc(var(--nk-space) * 6);
3038
+ --_nk-card-radius: calc(var(--nk-radius-xl) + var(--nk-space));
3039
+ --_nk-card-title-size: var(--nk-text-lg);
2701
3040
  }
2702
3041
  }
2703
3042
 
2704
3043
  @layer nitro-kit.compound {
3044
+ /* A description directly under a root-level title belongs to it. */
3045
+ :where(
3046
+ [data-nk="card"] > [data-slot="card-title"] + [data-slot="card-description"]
3047
+ ) {
3048
+ margin-block-start: calc(var(--nk-space) * 1 - var(--_nk-card-gap));
3049
+ }
3050
+
3051
+ /* A section set apart by a divider at the card's edge is a band: the space
3052
+ between the band and the edge matches the space between the band and its
3053
+ divider, so a footer's actions or a header's title sit evenly. Full
3054
+ regions keep the padding because they bleed through it. */
3055
+ :where(
3056
+ [data-nk="card"]:has(
3057
+ > :first-child:not([data-slot="card-full"]):not(
3058
+ [data-slot="card-divider"]
3059
+ )
3060
+ + [data-slot="card-divider"]
3061
+ )
3062
+ ) {
3063
+ padding-block-start: var(--_nk-card-gap);
3064
+ }
3065
+
3066
+ :where(
3067
+ [data-nk="card"]:has(
3068
+ > [data-slot="card-divider"]
3069
+ + :last-child:not([data-slot="card-full"]):not(
3070
+ [data-slot="card-divider"]
3071
+ )
3072
+ )
3073
+ ) {
3074
+ padding-block-end: var(--_nk-card-gap);
3075
+ }
3076
+
3077
+ /* When a parent stretches the card, such as a grid row, the spare height
3078
+ goes above a trailing footer and its divider, keeping actions on the
3079
+ card's bottom edge across a row. */
3080
+ :where(
3081
+ [data-nk="card"]
3082
+ > :not([data-slot="card-divider"])
3083
+ + [data-slot="card-footer"]:last-child,
3084
+ [data-nk="card"]
3085
+ > [data-slot="card-divider"]:has(+ [data-slot="card-footer"]:last-child)
3086
+ ) {
3087
+ margin-block-start: auto;
3088
+ }
3089
+
2705
3090
  /* Only the region that bleeds to the edge clips, never the card itself, so
2706
3091
  an edge-to-edge image cannot break the card's corners while tooltips,
2707
3092
  dropdowns, and other overlays inside a card stay free to escape its box. */
@@ -2713,27 +3098,70 @@
2713
3098
  :where([data-nk="card"] > [data-slot="card-full"]:first-child) {
2714
3099
  margin-block-start: calc(var(--_nk-card-padding) * -1);
2715
3100
  border-start-start-radius: calc(
2716
- var(--nk-radius-lg) - var(--nk-border-width)
3101
+ var(--_nk-card-radius) - var(--nk-border-width)
3102
+ );
3103
+ border-start-end-radius: calc(
3104
+ var(--_nk-card-radius) - var(--nk-border-width)
2717
3105
  );
2718
- border-start-end-radius: calc(var(--nk-radius-lg) - var(--nk-border-width));
2719
3106
  }
2720
3107
 
2721
3108
  :where([data-nk="card"] > [data-slot="card-full"]:last-child) {
2722
3109
  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));
3110
+ border-end-start-radius: calc(
3111
+ var(--_nk-card-radius) - var(--nk-border-width)
3112
+ );
3113
+ border-end-end-radius: calc(
3114
+ var(--_nk-card-radius) - var(--nk-border-width)
3115
+ );
2725
3116
  }
2726
3117
 
2727
3118
  /* Full-width media fills the card edge to edge, so the card owns the corner
2728
3119
  radius and the media drops its own. */
2729
3120
  :where(
2730
- [data-nk="card"]
2731
- > [data-slot="card-full"]
2732
- :is(img, [data-nk="progressive-image"])
2733
- ) {
3121
+ [data-nk="card"]
3122
+ > [data-slot="card-full"]
3123
+ :is(img, [data-nk="progressive-image"])
3124
+ ) {
2734
3125
  border-radius: 0;
2735
3126
  }
2736
3127
 
3128
+ /* A table bleeding to the card's edges keeps its first and last columns
3129
+ and its caption on the card's content gutter. */
3130
+ :where(
3131
+ [data-nk="card"]
3132
+ > [data-slot="card-full"]
3133
+ > [data-nk="table"]
3134
+ [data-slot="table-header"]:first-child,
3135
+ [data-nk="card"]
3136
+ > [data-slot="card-full"]
3137
+ > [data-nk="table"]
3138
+ [data-slot="table-cell"]:first-child
3139
+ ) {
3140
+ padding-inline-start: var(--_nk-card-padding);
3141
+ }
3142
+
3143
+ :where(
3144
+ [data-nk="card"]
3145
+ > [data-slot="card-full"]
3146
+ > [data-nk="table"]
3147
+ [data-slot="table-header"]:last-child,
3148
+ [data-nk="card"]
3149
+ > [data-slot="card-full"]
3150
+ > [data-nk="table"]
3151
+ [data-slot="table-cell"]:last-child
3152
+ ) {
3153
+ padding-inline-end: var(--_nk-card-padding);
3154
+ }
3155
+
3156
+ :where(
3157
+ [data-nk="card"]
3158
+ > [data-slot="card-full"]
3159
+ > [data-nk="table"]
3160
+ [data-slot="table-caption"]
3161
+ ) {
3162
+ padding-inline: var(--_nk-card-padding);
3163
+ }
3164
+
2737
3165
  :where([data-nk="card"] > [data-slot="card-divider"]) {
2738
3166
  margin-inline: calc(var(--_nk-card-padding) * -1);
2739
3167
  border-block-start: var(--nk-border-width) solid var(--nk-color-border);
@@ -3616,7 +4044,7 @@
3616
4044
  );
3617
4045
  border: var(--nk-border-width) solid
3618
4046
  color-mix(in oklab, var(--nk-color-destructive) 50%, transparent);
3619
- border-radius: var(--nk-radius-lg);
4047
+ border-radius: var(--nk-radius-xl);
3620
4048
  }
3621
4049
 
3622
4050
  :where([data-nk="danger-zone"] > [data-slot="danger-zone-header"]) {
@@ -4464,6 +4892,21 @@
4464
4892
  }
4465
4893
  }
4466
4894
 
4895
+ @layer nitro-kit.base {
4896
+ /* Message children are grid items placed at start, so they size to their
4897
+ content; cap them at the cell so one long word wraps inside the target. */
4898
+ :where(
4899
+ [data-nk="dropzone"]
4900
+ [data-slot="dropzone-message"]
4901
+ > [data-slot="dropzone-title"],
4902
+ [data-nk="dropzone"]
4903
+ [data-slot="dropzone-message"]
4904
+ > [data-slot="dropzone-description"]
4905
+ ) {
4906
+ max-inline-size: 100%;
4907
+ }
4908
+ }
4909
+
4467
4910
  /* Source: src/stylesheets/nitro_kit/components/empty_state.css */
4468
4911
  @layer nitro-kit.base {
4469
4912
  :where([data-nk="empty-state"]) {
@@ -4476,7 +4919,7 @@
4476
4919
  text-align: center;
4477
4920
  background: var(--nk-color-surface);
4478
4921
  border: var(--nk-border-width) dashed var(--nk-color-border);
4479
- border-radius: var(--nk-radius-lg);
4922
+ border-radius: var(--nk-radius-xl);
4480
4923
  }
4481
4924
 
4482
4925
  :where([data-nk="empty-state"] > [data-slot="empty-state-icon"]) {
@@ -4486,7 +4929,7 @@
4486
4929
  }
4487
4930
 
4488
4931
  :where([data-nk="empty-state"] > [data-slot="empty-state-title"]) {
4489
- max-inline-size: var(--nk-content-md);
4932
+ max-inline-size: min(100%, var(--nk-content-md));
4490
4933
  font-size: var(--nk-title-surface-size);
4491
4934
  font-weight: var(--nk-title-surface-weight);
4492
4935
  line-height: var(--nk-leading-tight);
@@ -4494,7 +4937,7 @@
4494
4937
  }
4495
4938
 
4496
4939
  :where([data-nk="empty-state"] > [data-slot="empty-state-description"]) {
4497
- max-inline-size: var(--nk-content-md);
4940
+ max-inline-size: min(100%, var(--nk-content-md));
4498
4941
  color: var(--nk-color-muted-foreground);
4499
4942
  font-size: var(--nk-text-sm);
4500
4943
  line-height: var(--nk-leading-relaxed);
@@ -4619,8 +5062,12 @@
4619
5062
 
4620
5063
  /* A group keeps the same rhythm at either boundary as the fields inside it,
4621
5064
  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"] + *) {
5065
+ nearest control. Hidden inputs, such as the ones Rails emits at the top of
5066
+ every form, are not visible neighbours and do not count. */
5067
+ :where(
5068
+ :not(input[type="hidden"]) + [data-nk="field-group"],
5069
+ [data-nk="field-group"] + :not(input[type="hidden"])
5070
+ ) {
4624
5071
  margin-block-start: calc(var(--nk-space) * 6);
4625
5072
  }
4626
5073
  }
@@ -6238,6 +6685,9 @@
6238
6685
  @media (width < 48rem) {
6239
6686
  :where([data-nk="pagination-bar"]) {
6240
6687
  flex-direction: column;
6688
+ /* A wrapping column sizes its line to the widest item, so stretch
6689
+ could not narrow anything; one column never needs to wrap. */
6690
+ flex-wrap: nowrap;
6241
6691
  align-items: stretch;
6242
6692
  }
6243
6693
 
@@ -6506,6 +6956,8 @@
6506
6956
  font-size: var(--nk-text-sm);
6507
6957
  text-align: center;
6508
6958
  text-wrap: pretty;
6959
+ /* The alt text is an anonymous grid item with a min-content floor. */
6960
+ overflow-wrap: anywhere;
6509
6961
  }
6510
6962
  }
6511
6963
 
@@ -6991,6 +7443,8 @@
6991
7443
  display: flex;
6992
7444
  gap: calc(var(--nk-space) * 2);
6993
7445
  align-items: center;
7446
+ /* The label text is an anonymous flex item with a min-content floor. */
7447
+ overflow-wrap: anywhere;
6994
7448
  min-block-size: var(--nk-control-height-sm);
6995
7449
  padding: var(--nk-space) calc(var(--nk-space) * 2);
6996
7450
  color: var(--nk-color-muted-foreground);
@@ -7297,11 +7751,11 @@
7297
7751
  align-content: start;
7298
7752
  gap: calc(var(--nk-space) * 2);
7299
7753
  min-block-size: 100%;
7300
- padding: calc(var(--nk-space) * 5);
7754
+ padding: calc(var(--nk-space) * 6);
7301
7755
  background: var(--nk-color-surface);
7302
7756
  border: var(--nk-border-width) solid var(--nk-color-border);
7303
- border-radius: var(--nk-radius-lg);
7304
- box-shadow: var(--nk-shadow-sm);
7757
+ border-radius: var(--nk-radius-xl);
7758
+ box-shadow: var(--nk-shadow-xs);
7305
7759
  }
7306
7760
 
7307
7761
  :where([data-nk="stat-grid"] [data-slot="stat-grid-label"]) {
@@ -7313,7 +7767,7 @@
7313
7767
  :where([data-nk="stat-grid"] [data-slot="stat-grid-value"]) {
7314
7768
  font-size: var(--nk-text-2xl);
7315
7769
  font-variant-numeric: tabular-nums;
7316
- font-weight: var(--nk-font-weight-bold);
7770
+ font-weight: var(--nk-font-weight-semibold);
7317
7771
  line-height: var(--nk-leading-tight);
7318
7772
  overflow-wrap: anywhere;
7319
7773
  }
@@ -7935,12 +8389,24 @@
7935
8389
  justify-content: flex-end;
7936
8390
  margin-inline-start: auto;
7937
8391
  }
8392
+
8393
+ /* Application content in a region is a flex item; without this a heading
8394
+ holding one long word keeps its min-content width and widens the page. */
8395
+ :where(
8396
+ [data-nk="toolbar"] > [data-slot="toolbar-leading"] > *,
8397
+ [data-nk="toolbar"] > [data-slot="toolbar-trailing"] > *
8398
+ ) {
8399
+ min-inline-size: 0;
8400
+ }
7938
8401
  }
7939
8402
 
7940
8403
  @layer nitro-kit.state {
7941
8404
  @media (width < 48rem) {
7942
8405
  :where([data-nk="toolbar"]) {
7943
8406
  flex-direction: column;
8407
+ /* A wrapping column sizes its line to the widest item, so stretch
8408
+ could not narrow anything; one column never needs to wrap. */
8409
+ flex-wrap: nowrap;
7944
8410
  align-items: stretch;
7945
8411
  }
7946
8412
 
@@ -7973,6 +8439,9 @@
7973
8439
  line-height: var(--nk-leading-normal);
7974
8440
  text-wrap: pretty;
7975
8441
  pointer-events: auto;
8442
+ /* Hidden tooltips leave layout entirely; a merely invisible one still
8443
+ widens the page when it hangs past the viewport edge. */
8444
+ display: none;
7976
8445
  visibility: hidden;
7977
8446
  background: var(--nk-color-elevated);
7978
8447
  border: var(--nk-border-width) solid var(--nk-color-border);
@@ -7981,7 +8450,8 @@
7981
8450
  opacity: 0;
7982
8451
  transition:
7983
8452
  opacity var(--nk-duration-fast) var(--nk-ease),
7984
- visibility var(--nk-duration-fast) var(--nk-ease) allow-discrete;
8453
+ visibility var(--nk-duration-fast) var(--nk-ease) allow-discrete,
8454
+ display var(--nk-duration-fast) var(--nk-ease) allow-discrete;
7985
8455
  }
7986
8456
 
7987
8457
  :where([data-nk="tooltip"] > [data-slot="tooltip-content"]::before) {
@@ -8065,8 +8535,13 @@
8065
8535
  [data-nk="tooltip"]:not([data-dismissed]):focus-within
8066
8536
  > [data-slot="tooltip-content"]
8067
8537
  ) {
8538
+ display: block;
8068
8539
  visibility: visible;
8069
8540
  opacity: 1;
8541
+
8542
+ @starting-style {
8543
+ opacity: 0;
8544
+ }
8070
8545
  }
8071
8546
 
8072
8547
  @media (hover: hover) {
@@ -8074,8 +8549,13 @@
8074
8549
  [data-nk="tooltip"]:not([data-dismissed]):hover
8075
8550
  > [data-slot="tooltip-content"]
8076
8551
  ) {
8552
+ display: block;
8077
8553
  visibility: visible;
8078
8554
  opacity: 1;
8555
+
8556
+ @starting-style {
8557
+ opacity: 0;
8558
+ }
8079
8559
  }
8080
8560
  }
8081
8561
 
@@ -8207,6 +8687,9 @@
8207
8687
  padding: calc(var(--nk-space) * 2) calc(var(--nk-space) * 3);
8208
8688
  text-align: start;
8209
8689
  vertical-align: top;
8690
+ /* Prose tables have no scroll wrapper, so an unbroken email or
8691
+ identifier must be allowed to break inside its cell. */
8692
+ overflow-wrap: anywhere;
8210
8693
  border-block-end: var(--nk-border-width) solid var(--nk-color-border);
8211
8694
  }
8212
8695
 
@@ -8348,6 +8831,7 @@
8348
8831
  padding: calc(var(--nk-space) * 2) calc(var(--nk-space) * 3);
8349
8832
  text-align: start;
8350
8833
  vertical-align: top;
8834
+ overflow-wrap: anywhere;
8351
8835
  border-block-end: var(--nk-border-width) solid var(--nk-color-border);
8352
8836
  }
8353
8837