@nonoun/native-ui 0.2.4 → 0.2.6

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 (35) hide show
  1. package/dist/components/ui-kbd/index.d.ts +3 -0
  2. package/dist/components/ui-kbd/index.d.ts.map +1 -0
  3. package/dist/components/ui-kbd/ui-kbd-element.d.ts +5 -0
  4. package/dist/components/ui-kbd/ui-kbd-element.d.ts.map +1 -0
  5. package/dist/components/ui-kbd/ui-kbd.d.ts +3 -0
  6. package/dist/components/ui-kbd/ui-kbd.d.ts.map +1 -0
  7. package/dist/components/ui-nav/ui-nav-group-element.d.ts +8 -0
  8. package/dist/components/ui-nav/ui-nav-group-element.d.ts.map +1 -1
  9. package/dist/components/ui-nav/ui-nav-group-header-element.d.ts.map +1 -1
  10. package/dist/components-lean.css +319 -226
  11. package/dist/components.css +321 -233
  12. package/dist/containers/ui-layout-sidebar/index.d.ts +1 -1
  13. package/dist/containers/ui-layout-sidebar/index.d.ts.map +1 -1
  14. package/dist/containers/ui-layout-sidebar/ui-layout-sidebar-element.d.ts +4 -1
  15. package/dist/containers/ui-layout-sidebar/ui-layout-sidebar-element.d.ts.map +1 -1
  16. package/dist/containers/ui-layout-sidebar/ui-layout-sidebar-item-element.d.ts +10 -0
  17. package/dist/containers/ui-layout-sidebar/ui-layout-sidebar-item-element.d.ts.map +1 -0
  18. package/dist/containers/ui-layout-sidebar/ui-layout-sidebar-item.d.ts +3 -0
  19. package/dist/containers/ui-layout-sidebar/ui-layout-sidebar-item.d.ts.map +1 -0
  20. package/dist/containers/ui-layout-sidebar/ui-layout-sidebar.d.ts +1 -1
  21. package/dist/containers/ui-layout-sidebar/ui-layout-sidebar.d.ts.map +1 -1
  22. package/dist/custom-elements.json +2507 -2099
  23. package/dist/foundation.css +79 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/native-ui-lean.css +398 -227
  27. package/dist/native-ui.css +400 -234
  28. package/dist/native-ui.js +3 -6
  29. package/dist/register-all.js +1 -1
  30. package/dist/ui-icon-element.js +100 -7
  31. package/package.json +1 -1
  32. package/dist/containers/ui-layout-sidebar/ui-layout-sidebar-trigger-element.d.ts +0 -8
  33. package/dist/containers/ui-layout-sidebar/ui-layout-sidebar-trigger-element.d.ts.map +0 -1
  34. package/dist/containers/ui-layout-sidebar/ui-layout-sidebar-trigger.d.ts +0 -3
  35. package/dist/containers/ui-layout-sidebar/ui-layout-sidebar-trigger.d.ts.map +0 -1
@@ -365,7 +365,7 @@
365
365
  font-weight: var(--_font-weight);
366
366
 
367
367
  border-radius: var(--_radius);
368
- border: 1px solid var(--_border-color, var(--neutral-border-muted));
368
+ border: 1px solid var(--_border-color, var(--_border-muted));
369
369
 
370
370
  background: var(--_background, var(--_button));
371
371
  color: var(--_color, var(--_ink));
@@ -382,17 +382,17 @@
382
382
 
383
383
  :where(ui-button):hover,
384
384
  :where(ui-button)[force-hover] {
385
- background: var(--_background-hover, var(--neutral-button-hover));
385
+ background: var(--_background-hover, var(--_button-hover));
386
386
  color: var(--_color-hover, var(--_ink-hover));
387
- border-color: var(--_border-color-hover, var(--neutral-border-hover));
387
+ border-color: var(--_border-color-hover, var(--_border-hover));
388
388
  }
389
389
 
390
390
  :where(ui-button):active,
391
391
  :where(ui-button)[pressed],
392
392
  :where(ui-button)[force-active] {
393
- background: var(--_background-active, var(--neutral-button-active));
393
+ background: var(--_background-active, var(--_button-active));
394
394
  color: var(--_color-active, var(--_ink-active));
395
- border-color: var(--_border-color-active, var(--neutral-border-active));
395
+ border-color: var(--_border-color-active, var(--_border-active));
396
396
  }
397
397
 
398
398
  :where(ui-button):focus-visible,
@@ -402,9 +402,9 @@
402
402
  }
403
403
 
404
404
  :where(ui-button)[aria-disabled="true"] {
405
- background: var(--_background-disabled, var(--neutral-button-disabled));
405
+ background: var(--_background-disabled, var(--_button-disabled));
406
406
  color: var(--_color-disabled, var(--_ink-disabled));
407
- border-color: var(--_border-color-disabled, var(--neutral-border-muted));
407
+ border-color: var(--_border-color-disabled, var(--_border-muted));
408
408
  cursor: not-allowed;
409
409
  pointer-events: none;
410
410
  }
@@ -977,34 +977,25 @@
977
977
  }
978
978
 
979
979
  /* ── Checked State ── */
980
- /* WHY: Default checked fill uses accent so checkbox is visually distinct
981
- without needing intent="accent". Unchecked borders stay neutral.
980
+ /* WHY: Default checked fill uses --_surface (accent by default via
981
+ intent="accent" being the implicit default for toggle controls).
982
982
  When an explicit [intent] is set, the intent selector's --_surface wins. */
983
983
 
984
984
  :where(ui-checkbox)[aria-checked="true"]::before {
985
- background: var(--accent-surface);
986
- border-color: var(--accent-surface);
987
- }
988
-
989
- :where(ui-checkbox)[aria-checked="true"]::after {
990
- background: var(--accent-surface-ink);
991
- transform: translateY(-50%) scale(1);
992
- }
993
-
994
- :where(ui-checkbox)[intent][aria-checked="true"]::before {
995
985
  background: var(--_surface);
996
986
  border-color: var(--_surface);
997
987
  }
998
988
 
999
- :where(ui-checkbox)[intent][aria-checked="true"]::after {
989
+ :where(ui-checkbox)[aria-checked="true"]::after {
1000
990
  background: var(--_surface-ink);
991
+ transform: translateY(-50%) scale(1);
1001
992
  }
1002
993
 
1003
994
  /* ── Indeterminate State ── */
1004
995
 
1005
996
  :where(ui-checkbox)[aria-checked="mixed"]::before {
1006
- background: var(--accent-surface);
1007
- border-color: var(--accent-surface);
997
+ background: var(--_surface);
998
+ border-color: var(--_surface);
1008
999
  }
1009
1000
 
1010
1001
  :where(ui-checkbox)[aria-checked="mixed"]::after {
@@ -1012,17 +1003,8 @@
1012
1003
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' fill='currentColor' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M228,128a12,12,0,0,1-12,12H40a12,12,0,0,1,0-24H216A12,12,0,0,1,228,128Z'/%3E%3C/svg%3E");
1013
1004
  mask-size: 75%;
1014
1005
  -webkit-mask-size: 75%;
1015
- background: var(--accent-surface-ink);
1016
- transform: translateY(-50%) scale(1);
1017
- }
1018
-
1019
- :where(ui-checkbox)[intent][aria-checked="mixed"]::before {
1020
- background: var(--_surface);
1021
- border-color: var(--_surface);
1022
- }
1023
-
1024
- :where(ui-checkbox)[intent][aria-checked="mixed"]::after {
1025
1006
  background: var(--_surface-ink);
1007
+ transform: translateY(-50%) scale(1);
1026
1008
  }
1027
1009
 
1028
1010
  /* ── Hover ── */
@@ -1042,14 +1024,6 @@
1042
1024
  :where(ui-checkbox)[aria-checked="mixed"]:hover::before,
1043
1025
  :where(ui-checkbox)[aria-checked="true"][force-hover]::before,
1044
1026
  :where(ui-checkbox)[aria-checked="mixed"][force-hover]::before {
1045
- background: var(--accent-surface-hover);
1046
- border-color: var(--accent-surface-hover);
1047
- }
1048
-
1049
- :where(ui-checkbox)[intent][aria-checked="true"]:hover::before,
1050
- :where(ui-checkbox)[intent][aria-checked="mixed"]:hover::before,
1051
- :where(ui-checkbox)[intent][aria-checked="true"][force-hover]::before,
1052
- :where(ui-checkbox)[intent][aria-checked="mixed"][force-hover]::before {
1053
1027
  background: var(--_surface-hover);
1054
1028
  border-color: var(--_surface-hover);
1055
1029
  }
@@ -1063,12 +1037,6 @@
1063
1037
 
1064
1038
  :where(ui-checkbox)[aria-checked="true"][pressed]::before,
1065
1039
  :where(ui-checkbox)[aria-checked="mixed"][pressed]::before {
1066
- background: var(--accent-surface-active);
1067
- border-color: var(--accent-surface-active);
1068
- }
1069
-
1070
- :where(ui-checkbox)[intent][aria-checked="true"][pressed]::before,
1071
- :where(ui-checkbox)[intent][aria-checked="mixed"][pressed]::before {
1072
1040
  background: var(--_surface-active);
1073
1041
  border-color: var(--_surface-active);
1074
1042
  }
@@ -1722,6 +1690,45 @@
1722
1690
 
1723
1691
  }
1724
1692
 
1693
+ @layer ui {
1694
+
1695
+ /* ╭──────────────────────────────────────────────────────────╮
1696
+ │ ui-kbd │
1697
+ │ Keyboard shortcut indicator. Own font/size scale │
1698
+ │ (like ui-badge) — zero-attribute = md. │
1699
+ ╰──────────────────────────────────────────────────────────╯ */
1700
+
1701
+ :where(ui-kbd) {
1702
+ --_icon-size: 1.125em;
1703
+
1704
+ display: inline-flex;
1705
+ align-items: center;
1706
+ justify-content: center;
1707
+ font-family: ui-monospace, monospace;
1708
+ font-size: var(--ui-kbd-font-md);
1709
+ line-height: 1;
1710
+ min-height: var(--ui-kbd-size-md);
1711
+ padding-inline: 0.4em;
1712
+ border-radius: 0.5em;
1713
+ background: var(--_ground, var(--_body));
1714
+ border: 1px solid var(--_border-muted);
1715
+ color: var(--_ink-muted);
1716
+ white-space: nowrap;
1717
+ user-select: none;
1718
+ flex-shrink: 0;
1719
+ vertical-align: baseline;
1720
+ }
1721
+
1722
+ /* ── Sizes ── */
1723
+
1724
+ :where(ui-kbd[size="xs"]) { font-size: var(--ui-kbd-font-xs); min-height: var(--ui-kbd-size-xs); }
1725
+ :where(ui-kbd[size="sm"]) { font-size: var(--ui-kbd-font-sm); min-height: var(--ui-kbd-size-sm); }
1726
+ :where(ui-kbd[size="md"]) { font-size: var(--ui-kbd-font-md); min-height: var(--ui-kbd-size-md); }
1727
+ :where(ui-kbd[size="lg"]) { font-size: var(--ui-kbd-font-lg); min-height: var(--ui-kbd-size-lg); }
1728
+ :where(ui-kbd[size="xl"]) { font-size: var(--ui-kbd-font-xl); min-height: var(--ui-kbd-size-xl); }
1729
+
1730
+ }
1731
+
1725
1732
  @layer ui {
1726
1733
 
1727
1734
  /* ── Listbox Base ── */
@@ -1862,11 +1869,14 @@
1862
1869
  │ Uses ui-nav-item / ui-nav-group for children. │
1863
1870
  ╰──────────────────────────────────────────────────────────╯ */
1864
1871
 
1872
+ /* WHY: ui-nav is a transparent flex wrapper — no inline padding.
1873
+ Inline padding is owned by leaf items (nav-item, summary).
1874
+ Block padding is owned by nav-group margins. */
1865
1875
  :where(ui-nav) {
1866
1876
  display: flex;
1867
1877
  flex-direction: column;
1868
1878
  gap: 0;
1869
- padding: var(--_space);
1879
+ padding: 0;
1870
1880
  outline: none;
1871
1881
  }
1872
1882
 
@@ -1881,6 +1891,7 @@
1881
1891
  display: flex;
1882
1892
  align-items: center;
1883
1893
  gap: calc(var(--_space) * 2);
1894
+ padding-inline: calc(var(--_space-k) * var(--_space));
1884
1895
 
1885
1896
  min-height: var(--_min-height);
1886
1897
 
@@ -1980,6 +1991,7 @@
1980
1991
  display: flex;
1981
1992
  align-items: center;
1982
1993
  gap: calc(var(--_space) * 2);
1994
+ padding-inline: calc(var(--_space-k) * var(--_space));
1983
1995
  cursor: pointer;
1984
1996
  user-select: none;
1985
1997
  list-style: none;
@@ -2014,6 +2026,18 @@
2014
2026
  user-select: none;
2015
2027
  }
2016
2028
 
2029
+ /* WHY: Fixed-size icon well — matches ui-layout-sidebar-item [slot="icon"].
2030
+ All sidebar icons align on the same 1.5rem column regardless of icon size.
2031
+ JS wraps the first <ui-icon> child in a .icon-well span during setup(). */
2032
+ :where(ui-nav-group-header) > :where(.icon-well) {
2033
+ display: inline-flex;
2034
+ align-items: center;
2035
+ justify-content: center;
2036
+ width: 1.5rem;
2037
+ height: 1.5rem;
2038
+ flex-shrink: 0;
2039
+ }
2040
+
2017
2041
  /* ── Chevron icon (right side of summary) ── */
2018
2042
 
2019
2043
  :where(ui-nav-group) > :where(details) > :where(summary)::after {
@@ -2067,12 +2091,16 @@
2067
2091
  /* WHY: Connector draws from below the header to the bottom of the group.
2068
2092
  Only shown when header contains an icon. Scoped via :has(). */
2069
2093
 
2070
- :where(ui-nav-group):has(:where(ui-nav-group-header) :where(ui-icon)) {
2071
- --_group-line-inset: calc(var(--_icon-size) / 2);
2072
- --_group-child-inset: calc(var(--_icon-size) + var(--_space) * 2);
2094
+ /* WHY: Insets account for the summary's padding-inline so the connector
2095
+ line passes through the icon center and child text aligns with header text. */
2096
+ :where(ui-nav-group):has(:where(ui-nav-group-header) :where(.icon-well)) {
2097
+ --_group-pad: calc(var(--_space-k) * var(--_space));
2098
+ --_group-icon-well: 1.5rem;
2099
+ --_group-line-inset: calc(var(--_group-pad) + var(--_group-icon-well) / 2);
2100
+ --_group-child-inset: calc(var(--_group-icon-well) + var(--_space) * 2);
2073
2101
  }
2074
2102
 
2075
- :where(ui-nav-group):has(:where(ui-nav-group-header) :where(ui-icon)):has(:where(details[open]))::after {
2103
+ :where(ui-nav-group):has(:where(ui-nav-group-header) :where(.icon-well)):has(:where(details[open]))::after {
2076
2104
  content: '';
2077
2105
  position: absolute;
2078
2106
  inset-inline-start: var(--_group-line-inset);
@@ -2087,7 +2115,7 @@
2087
2115
  /* WHY: A ::before pseudo on the group slides along the connector line.
2088
2116
  JS sets --_indicator-index; :state(has-selection) gates visibility. */
2089
2117
 
2090
- :where(ui-nav-group):has(:where(ui-nav-group-header) :where(ui-icon)):has(:where(details[open])):state(has-selection)::before {
2118
+ :where(ui-nav-group):has(:where(ui-nav-group-header) :where(.icon-well)):has(:where(details[open])):state(has-selection)::before {
2091
2119
  content: '';
2092
2120
  position: absolute;
2093
2121
  z-index: 1;
@@ -2114,6 +2142,73 @@
2114
2142
  margin-inline-start: var(--_group-child-inset, 0);
2115
2143
  }
2116
2144
 
2145
+ /* ── Nav Group Flyout (collapsed sidebar) ── */
2146
+ /* WHY: In collapsed mode, summary click opens a ui-listbox popover to the right
2147
+ instead of expanding the <details>. Same pattern as sidebar-trigger menus. */
2148
+
2149
+ :where(ui-nav-group) > :where(ui-listbox.nav-group-flyout[popover]) {
2150
+ position: fixed;
2151
+ position-area: inline-end span-block-end;
2152
+ position-try-fallbacks: --nav-flyout-flip-up;
2153
+ margin: 0 0 0 var(--ui-popover-gap);
2154
+ min-width: 200px;
2155
+ max-height: var(--ui-popover-max-height);
2156
+ overflow-y: auto;
2157
+ }
2158
+
2159
+ @position-try --nav-flyout-flip-up {
2160
+ position-area: inline-end span-block-start;
2161
+ }
2162
+
2163
+ /* ── Container Query: Collapsed Sidebar ── */
2164
+ /* WHY: Nav components own their own collapsed behavior via @container.
2165
+ The sidebar aside declares container-name: sidebar. When it shrinks
2166
+ to 48px (icon rail), nav responds to the width — not to [collapsed].
2167
+ Threshold 80px: collapsed = 48px, min expanded = 160px. */
2168
+
2169
+ @container sidebar (max-width: 80px) {
2170
+
2171
+ /* Nav items hide entirely — only group headers (with icons) remain. */
2172
+ :where(ui-nav-item) {
2173
+ display: none;
2174
+ }
2175
+
2176
+ /* Summary shrinks to icon-only. Reduced inline padding wraps tightly
2177
+ around the icon; parent align-items: center handles horizontal centering. */
2178
+ :where(ui-nav-group) > :where(details) > :where(summary) {
2179
+ padding-inline: var(--_space);
2180
+ border-radius: var(--_radius);
2181
+ }
2182
+
2183
+ /* Hide chevron — no expand/collapse in icon rail (flyout instead). */
2184
+ :where(ui-nav-group) > :where(details) > :where(summary)::after {
2185
+ display: none;
2186
+ }
2187
+
2188
+ /* Hide vertical connector line and sliding indicator. */
2189
+ :where(ui-nav-group)::after,
2190
+ :where(ui-nav-group)::before {
2191
+ display: none;
2192
+ }
2193
+
2194
+ /* Header collapses to icon-only. font-size: 0 hides text nodes
2195
+ (can't be targeted by CSS). Icon overrides back to normal below. */
2196
+ :where(ui-nav-group-header) {
2197
+ flex: 0 0 auto;
2198
+ font-size: 0;
2199
+ gap: 0;
2200
+ }
2201
+
2202
+ :where(ui-nav-group-header) :where(.icon-well) :where(ui-icon) {
2203
+ font-size: var(--_font-size, 1rem);
2204
+ }
2205
+
2206
+ /* Collapse inter-group spacing in icon rail. */
2207
+ :where(ui-nav-group) + :where(ui-nav-group) {
2208
+ margin-block-start: 0;
2209
+ }
2210
+ }
2211
+
2117
2212
  }
2118
2213
 
2119
2214
  @layer ui {
@@ -2254,27 +2349,15 @@
2254
2349
  }
2255
2350
 
2256
2351
  /* ── Selected ── */
2257
- /* WHY: Default selected fill uses accent so radio is visually distinct
2258
- without needing intent="accent". Unchecked borders stay neutral.
2259
- When an explicit [intent] is set, the intent selector's --_surface wins. */
2260
2352
 
2261
2353
  :where(ui-radio)[aria-checked="true"]::before {
2262
- background: var(--accent-surface);
2263
- border-color: var(--accent-surface);
2264
- }
2265
-
2266
- :where(ui-radio)[aria-checked="true"]::after {
2267
- background: var(--accent-surface-ink);
2268
- transform: translateY(-50%) scale(1);
2269
- }
2270
-
2271
- :where(ui-radio-group)[intent] :where(ui-radio)[aria-checked="true"]::before {
2272
2354
  background: var(--_surface);
2273
2355
  border-color: var(--_surface);
2274
2356
  }
2275
2357
 
2276
- :where(ui-radio-group)[intent] :where(ui-radio)[aria-checked="true"]::after {
2358
+ :where(ui-radio)[aria-checked="true"]::after {
2277
2359
  background: var(--_surface-ink);
2360
+ transform: translateY(-50%) scale(1);
2278
2361
  }
2279
2362
 
2280
2363
  /* ── Hover ── */
@@ -2292,12 +2375,6 @@
2292
2375
 
2293
2376
  :where(ui-radio)[aria-checked="true"]:hover::before,
2294
2377
  :where(ui-radio)[aria-checked="true"][force-hover]::before {
2295
- background: var(--accent-surface-hover);
2296
- border-color: var(--accent-surface-hover);
2297
- }
2298
-
2299
- :where(ui-radio-group)[intent] :where(ui-radio)[aria-checked="true"]:hover::before,
2300
- :where(ui-radio-group)[intent] :where(ui-radio)[aria-checked="true"][force-hover]::before {
2301
2378
  background: var(--_surface-hover);
2302
2379
  border-color: var(--_surface-hover);
2303
2380
  }
@@ -2310,11 +2387,6 @@
2310
2387
  }
2311
2388
 
2312
2389
  :where(ui-radio)[aria-checked="true"][pressed]::before {
2313
- background: var(--accent-surface-active);
2314
- border-color: var(--accent-surface-active);
2315
- }
2316
-
2317
- :where(ui-radio-group)[intent] :where(ui-radio)[aria-checked="true"][pressed]::before {
2318
2390
  background: var(--_surface-active);
2319
2391
  border-color: var(--_surface-active);
2320
2392
  }
@@ -2413,16 +2485,12 @@
2413
2485
  /* 0% = track-height (circle), 100% = full width */
2414
2486
  width: calc(var(--_track-height) + (100% - var(--_track-height)) * var(--_progress));
2415
2487
  border-radius: calc(var(--_track-height) / 2);
2416
- background: var(--accent-surface);
2488
+ background: var(--_surface);
2417
2489
 
2418
2490
  transition:
2419
2491
  background var(--_duration) var(--_easing);
2420
2492
  }
2421
2493
 
2422
- :where(ui-range)[intent]::after {
2423
- background: var(--_surface);
2424
- }
2425
-
2426
2494
  /* ── Thumb ── */
2427
2495
 
2428
2496
  :where(ui-range) > :where(.ui-range-thumb) {
@@ -2434,8 +2502,8 @@
2434
2502
  width: var(--_thumb-size);
2435
2503
  height: var(--_thumb-size);
2436
2504
  border-radius: 50%;
2437
- background: var(--accent-surface-ink);
2438
- border: 2px solid var(--accent-surface);
2505
+ background: var(--_surface-ink);
2506
+ border: 2px solid var(--_surface);
2439
2507
  box-shadow: var(--ui-shadow-sm);
2440
2508
  pointer-events: none;
2441
2509
 
@@ -2445,11 +2513,6 @@
2445
2513
  border-color var(--_duration) var(--_easing);
2446
2514
  }
2447
2515
 
2448
- :where(ui-range)[intent] > :where(.ui-range-thumb) {
2449
- background: var(--_surface-ink);
2450
- border-color: var(--_surface);
2451
- }
2452
-
2453
2516
  /* ── Focus ── */
2454
2517
 
2455
2518
  :where(ui-range):focus-visible > :where(.ui-range-thumb),
@@ -2462,22 +2525,11 @@
2462
2525
 
2463
2526
  :where(ui-range):hover::after,
2464
2527
  :where(ui-range)[force-hover]::after {
2465
- background: var(--accent-surface-hover);
2466
- }
2467
-
2468
- :where(ui-range)[intent]:hover::after,
2469
- :where(ui-range)[intent][force-hover]::after {
2470
2528
  background: var(--_surface-hover);
2471
2529
  }
2472
2530
 
2473
2531
  :where(ui-range):hover > :where(.ui-range-thumb),
2474
2532
  :where(ui-range)[force-hover] > :where(.ui-range-thumb) {
2475
- border-color: var(--accent-surface-hover);
2476
- box-shadow: var(--ui-shadow-sm), 0 0 0 4px var(--accent-surface);
2477
- }
2478
-
2479
- :where(ui-range)[intent]:hover > :where(.ui-range-thumb),
2480
- :where(ui-range)[intent][force-hover] > :where(.ui-range-thumb) {
2481
2533
  border-color: var(--_surface-hover);
2482
2534
  box-shadow: var(--ui-shadow-sm), 0 0 0 4px var(--_surface);
2483
2535
  }
@@ -2485,19 +2537,10 @@
2485
2537
  /* ── Active (dragging) ── */
2486
2538
 
2487
2539
  :where(ui-range)[pressed]::after {
2488
- background: var(--accent-surface-active);
2489
- }
2490
-
2491
- :where(ui-range)[intent][pressed]::after {
2492
2540
  background: var(--_surface-active);
2493
2541
  }
2494
2542
 
2495
2543
  :where(ui-range)[pressed] > :where(.ui-range-thumb) {
2496
- border-color: var(--accent-surface-active);
2497
- box-shadow: var(--ui-shadow-sm), 0 0 0 6px var(--accent-surface);
2498
- }
2499
-
2500
- :where(ui-range)[intent][pressed] > :where(.ui-range-thumb) {
2501
2544
  border-color: var(--_surface-active);
2502
2545
  box-shadow: var(--ui-shadow-sm), 0 0 0 6px var(--_surface);
2503
2546
  }
@@ -3052,22 +3095,13 @@
3052
3095
  When an explicit [intent] is set, the intent selector's --_surface wins. */
3053
3096
 
3054
3097
  :where(ui-switch)[aria-checked="true"]::before {
3055
- background: var(--accent-surface);
3056
- border-color: var(--accent-surface);
3098
+ background: var(--_surface);
3099
+ border-color: var(--_surface);
3057
3100
  }
3058
3101
 
3059
3102
  :where(ui-switch)[aria-checked="true"]::after {
3060
3103
  /* Slide thumb to the right via translateX — GPU composited */
3061
3104
  transform: translateY(-50%) translateX(calc(var(--_track-width) - var(--_thumb-size) - var(--_thumb-offset) * 2));
3062
- background: var(--accent-surface-ink);
3063
- }
3064
-
3065
- :where(ui-switch)[intent][aria-checked="true"]::before {
3066
- background: var(--_surface);
3067
- border-color: var(--_surface);
3068
- }
3069
-
3070
- :where(ui-switch)[intent][aria-checked="true"]::after {
3071
3105
  background: var(--_surface-ink);
3072
3106
  }
3073
3107
 
@@ -3086,12 +3120,6 @@
3086
3120
 
3087
3121
  :where(ui-switch)[aria-checked="true"]:hover::before,
3088
3122
  :where(ui-switch)[aria-checked="true"][force-hover]::before {
3089
- background: var(--accent-surface-hover);
3090
- border-color: var(--accent-surface-hover);
3091
- }
3092
-
3093
- :where(ui-switch)[intent][aria-checked="true"]:hover::before,
3094
- :where(ui-switch)[intent][aria-checked="true"][force-hover]::before {
3095
3123
  background: var(--_surface-hover);
3096
3124
  border-color: var(--_surface-hover);
3097
3125
  }
@@ -3104,11 +3132,6 @@
3104
3132
  }
3105
3133
 
3106
3134
  :where(ui-switch)[aria-checked="true"][pressed]::before {
3107
- background: var(--accent-surface-active);
3108
- border-color: var(--accent-surface-active);
3109
- }
3110
-
3111
- :where(ui-switch)[intent][aria-checked="true"][pressed]::before {
3112
3135
  background: var(--_surface-active);
3113
3136
  border-color: var(--_surface-active);
3114
3137
  }
@@ -4568,7 +4591,7 @@
4568
4591
 
4569
4592
  :where(ui-layout-chat) :where(.layout-resize-handle):hover,
4570
4593
  :where(ui-layout-chat[resizing]) :where(.layout-resize-handle) {
4571
- background: var(--neutral-border-muted);
4594
+ background: var(--_border-muted);
4572
4595
  }
4573
4596
 
4574
4597
  }
@@ -4630,7 +4653,7 @@
4630
4653
 
4631
4654
  :where(ui-layout-inspector) :where(.layout-resize-handle):hover,
4632
4655
  :where(ui-layout-inspector[resizing]) :where(.layout-resize-handle) {
4633
- background: var(--neutral-border-muted);
4656
+ background: var(--_border-muted);
4634
4657
  }
4635
4658
 
4636
4659
  }
@@ -4657,16 +4680,16 @@
4657
4680
  content: '';
4658
4681
  width: var(--ui-layout-sidebar-width);
4659
4682
  flex-shrink: 0;
4660
- background: var(--neutral-body);
4661
- border-right: 1px solid var(--neutral-border-muted);
4683
+ background: var(--_body);
4684
+ border-right: 1px solid var(--_border-muted);
4662
4685
  }
4663
4686
 
4664
4687
  :where(ui-layout-sidebar):not([data-ready])[collapsed]::before {
4665
4688
  content: '';
4666
4689
  width: 48px;
4667
4690
  flex-shrink: 0;
4668
- background: var(--neutral-body);
4669
- border-right: 1px solid var(--neutral-border-muted);
4691
+ background: var(--_body);
4692
+ border-right: 1px solid var(--_border-muted);
4670
4693
  }
4671
4694
 
4672
4695
  /* ── Content Column ── */
@@ -4682,16 +4705,23 @@
4682
4705
  }
4683
4706
 
4684
4707
  /* ── Sidebar Aside ── */
4708
+ /* WHY: position: relative creates the containing block for absolute
4709
+ header/footer overlays. Content fills the full height and scrolls
4710
+ underneath the pinned header/footer. */
4685
4711
 
4686
4712
  :where(ui-layout-sidebar) > :where([slot="sidebar"]) {
4713
+ --_sidebar-header-height: 0px;
4714
+ --_sidebar-footer-height: 0px;
4715
+
4716
+ container-name: sidebar;
4717
+ container-type: inline-size;
4718
+ position: sticky;
4719
+ top: 0;
4687
4720
  width: var(--ui-layout-sidebar-width);
4688
4721
  min-width: 160px;
4689
4722
  max-width: 400px;
4690
4723
  height: 100dvh;
4691
- position: sticky;
4692
- top: 0;
4693
- display: flex;
4694
- flex-direction: column;
4724
+ display: block;
4695
4725
  transition: width var(--_duration) var(--_easing), min-width var(--_duration) var(--_easing);
4696
4726
  overflow: hidden;
4697
4727
  z-index: 10;
@@ -4703,7 +4733,9 @@
4703
4733
  }
4704
4734
 
4705
4735
  /* ── Collapsed Icon Rail ── */
4706
- /* WHY: Collapsed = icon rail, not fully hidden. 48px fits icons (1rem) + padding. */
4736
+ /* WHY: Collapsed = icon rail, not fully hidden. 48px fits icons (1rem) + padding.
4737
+ [collapsed] drives the aside to 48px which triggers @container sidebar queries
4738
+ in this file and in child component CSS (ui-nav.css, etc.). */
4707
4739
 
4708
4740
  :where(ui-layout-sidebar)[collapsed] > :where([slot="sidebar"]) {
4709
4741
  width: 48px;
@@ -4724,11 +4756,7 @@
4724
4756
 
4725
4757
  :where(ui-layout-sidebar) :where(.layout-resize-handle):hover,
4726
4758
  :where(ui-layout-sidebar) > :where([slot="sidebar"][resizing]) :where(.layout-resize-handle) {
4727
- background: var(--neutral-border-muted);
4728
- }
4729
-
4730
- :where(ui-layout-sidebar)[collapsed] > :where([slot="sidebar"]) :where(.layout-resize-handle) {
4731
- display: none;
4759
+ background: var(--_border-muted);
4732
4760
  }
4733
4761
 
4734
4762
  /* WHY: When collapsed to icon rail, the sidebar edge meets content — drop left padding. */
@@ -4741,101 +4769,89 @@
4741
4769
  padding-left: 0;
4742
4770
  }
4743
4771
 
4744
- /* ── Collapsed Icon-Rail Content ── */
4745
- /* WHY: When collapsed, sidebar slot becomes a narrow icon rail.
4746
- Text and trailing slots are hidden; only icons remain visible.
4747
- Nav items hide entirely — only group headers (with icons) stay. */
4748
-
4749
- :where(ui-layout-sidebar)[collapsed] :where([slot="sidebar"]) :where(ui-layout-sidebar-content) {
4750
- align-items: center;
4751
- padding: 0.5rem;
4752
- }
4753
-
4754
- :where(ui-layout-sidebar)[collapsed] :where([slot="sidebar"]) :where(ui-layout-sidebar-header),
4755
- :where(ui-layout-sidebar)[collapsed] :where([slot="sidebar"]) :where(ui-layout-sidebar-footer) {
4756
- justify-content: center;
4757
- padding-inline: 0.5rem;
4758
- }
4759
-
4760
- :where(ui-layout-sidebar)[collapsed] :where([slot="sidebar"]) :where(ui-layout-sidebar-trigger) :where([slot="label"]),
4761
- :where(ui-layout-sidebar)[collapsed] :where([slot="sidebar"]) :where(ui-layout-sidebar-trigger) :where([slot="trailing"]) {
4762
- display: none;
4763
- }
4764
-
4765
- :where(ui-layout-sidebar)[collapsed] :where([slot="sidebar"]) :where(ui-nav-item) {
4766
- display: none;
4767
- }
4768
-
4769
- :where(ui-layout-sidebar)[collapsed] :where([slot="sidebar"]) :where(ui-nav-group) > :where(details) > :where(summary) {
4770
- justify-content: center;
4771
- }
4772
-
4773
- :where(ui-layout-sidebar)[collapsed] :where([slot="sidebar"]) :where(ui-nav-group) > :where(details) > :where(summary)::after {
4774
- display: none;
4775
- }
4776
-
4777
- :where(ui-layout-sidebar)[collapsed] :where([slot="sidebar"]) :where(ui-nav-group)::after,
4778
- :where(ui-layout-sidebar)[collapsed] :where([slot="sidebar"]) :where(ui-nav-group)::before {
4779
- display: none;
4780
- }
4781
-
4782
- :where(ui-layout-sidebar)[collapsed] :where([slot="sidebar"]) :where(ui-nav-group-header) {
4783
- justify-content: center;
4784
- font-size: 0;
4785
- gap: 0;
4786
- }
4787
-
4788
- /* WHY: Icon keeps its size even though parent font-size is 0. */
4789
- :where(ui-layout-sidebar)[collapsed] :where([slot="sidebar"]) :where(ui-nav-group-header) :where(ui-icon) {
4790
- font-size: var(--_font-size, 1rem);
4791
- }
4792
-
4793
- :where(ui-layout-sidebar)[collapsed] :where([slot="sidebar"]) :where(ui-nav-group) + :where(ui-nav-group) {
4794
- margin-block-start: 0;
4795
- }
4796
-
4797
4772
  /* ── Sidebar Header ── */
4773
+ /* WHY: Absolute-positioned overlay pinned to top of aside. Content scrolls
4774
+ underneath it. z-index: 2 sits above content (z-index: 0). */
4798
4775
 
4799
4776
  :where(ui-layout-sidebar-header) {
4777
+ position: absolute;
4778
+ top: 0;
4779
+ left: 0;
4780
+ right: 0;
4781
+ z-index: 2;
4800
4782
  display: flex;
4801
- align-items: center;
4802
- min-height: var(--ui-layout-bar-height);
4803
- gap: calc(var(--_space) * 2);
4804
- padding-block: var(--_space);
4805
- padding-inline: calc(var(--_space-k) * var(--_space));
4806
- flex-shrink: 0;
4783
+ flex-direction: column;
4807
4784
  }
4808
4785
 
4809
4786
  /* ── Sidebar Content ── */
4787
+ /* WHY: Full height of the aside, scrollable. Padding-block offsets keep
4788
+ content from starting behind header / ending behind footer.
4789
+ Fade-out alpha mask dissolves content edges under the overlays. */
4810
4790
 
4811
4791
  :where(ui-layout-sidebar-content) {
4812
4792
  display: flex;
4813
4793
  flex-direction: column;
4814
- gap: calc(var(--_space) * 2);
4815
- padding-block: var(--_space);
4816
- padding-inline: calc(var(--_space-k) * var(--_space));
4817
- flex: 1;
4818
- min-height: 0;
4794
+ padding-block-start: var(--_sidebar-header-height);
4795
+ padding-block-end: var(--_sidebar-footer-height);
4819
4796
  width: 100%;
4797
+ height: 100%;
4798
+ overflow-y: auto;
4799
+ scrollbar-width: none;
4800
+ }
4801
+
4802
+ /* WHY: When header is present, fade top edge. Content dissolves as it
4803
+ scrolls under the header — transparent at top, fully visible by 1.25×
4804
+ the header height. No opaque header background needed. */
4805
+ :where(ui-layout-sidebar-content)[data-has-header] {
4806
+ mask-image: linear-gradient(
4807
+ to bottom,
4808
+ transparent 0,
4809
+ black calc(var(--_sidebar-header-height) * 1.25),
4810
+ black 100%
4811
+ );
4812
+ }
4813
+
4814
+ /* WHY: When footer is present, fade bottom edge. */
4815
+ :where(ui-layout-sidebar-content)[data-has-footer] {
4816
+ mask-image: linear-gradient(
4817
+ to bottom,
4818
+ black 0,
4819
+ black calc(100% - var(--_sidebar-footer-height) * 1.25),
4820
+ transparent 100%
4821
+ );
4822
+ }
4823
+
4824
+ /* WHY: When both header AND footer are present, fade both edges. */
4825
+ :where(ui-layout-sidebar-content)[data-has-header][data-has-footer] {
4826
+ mask-image: linear-gradient(
4827
+ to bottom,
4828
+ transparent 0,
4829
+ black calc(var(--_sidebar-header-height) * 1.25),
4830
+ black calc(100% - var(--_sidebar-footer-height) * 1.25),
4831
+ transparent 100%
4832
+ );
4820
4833
  }
4821
4834
 
4822
4835
  /* ── Sidebar Footer ── */
4836
+ /* WHY: Absolute-positioned overlay pinned to bottom of aside. */
4823
4837
 
4824
4838
  :where(ui-layout-sidebar-footer) {
4839
+ position: absolute;
4840
+ bottom: 0;
4841
+ left: 0;
4842
+ right: 0;
4843
+ z-index: 2;
4825
4844
  display: flex;
4826
- align-items: center;
4827
- min-height: var(--ui-layout-bar-height);
4828
- gap: calc(var(--_space) * 2);
4829
- padding-block: var(--_space);
4830
- padding-inline: calc(var(--_space-k) * var(--_space));
4831
- flex-shrink: 0;
4845
+ flex-direction: column;
4832
4846
  }
4833
4847
 
4834
- /* ── Sidebar Trigger ── */
4835
- /* WHY: Menu trigger for sidebar header/footer. Styled like ui-nav-group-header:
4836
- bold text, icon support, hover color. Opens a popover on click. */
4848
+ /* ── Sidebar Item ── */
4849
+ /* WHY: Universal sidebar row element. Provides inline padding for any content
4850
+ (buttons, links, icons). When a child ui-listbox[popover] is present, JS
4851
+ wires PopoverController for click-to-toggle menu behavior.
4852
+ Absorbs the old ui-layout-sidebar-trigger role — one element for all rows. */
4837
4853
 
4838
- :where(ui-layout-sidebar-trigger) {
4854
+ :where(ui-layout-sidebar-item) {
4839
4855
  display: flex;
4840
4856
  align-items: center;
4841
4857
  flex: 1;
@@ -4851,31 +4867,61 @@
4851
4867
  user-select: none;
4852
4868
  border: none;
4853
4869
  background: none;
4854
- padding: var(--_space);
4870
+ padding-block: var(--_space);
4871
+ padding-inline: calc(var(--_space-k) * var(--_space));
4855
4872
  border-radius: var(--_radius);
4856
4873
  transition: color var(--_duration) var(--_easing);
4857
4874
  }
4858
4875
 
4859
- :where(ui-layout-sidebar-trigger):hover,
4860
- :where(ui-layout-sidebar-trigger)[force-hover] {
4876
+ /* WHY: Items in header/footer match the breadcrumb bar height so the
4877
+ first sidebar row aligns horizontally with the breadcrumb. */
4878
+ :where(ui-layout-sidebar-header) > :where(ui-layout-sidebar-item),
4879
+ :where(ui-layout-sidebar-footer) > :where(ui-layout-sidebar-item) {
4880
+ min-height: var(--ui-layout-bar-height);
4881
+ }
4882
+
4883
+ :where(ui-layout-sidebar-item):hover,
4884
+ :where(ui-layout-sidebar-item)[force-hover] {
4861
4885
  color: var(--_ink-strong);
4862
4886
  }
4863
4887
 
4864
- :where(ui-layout-sidebar-trigger):focus-visible,
4865
- :where(ui-layout-sidebar-trigger)[force-focus-visible] {
4888
+ :where(ui-layout-sidebar-item):focus-visible,
4889
+ :where(ui-layout-sidebar-item)[force-focus-visible] {
4866
4890
  outline: 2px solid var(--ui-focus-ring);
4867
4891
  outline-offset: -2px;
4868
4892
  }
4869
4893
 
4894
+ /* WHY: Fixed-size icon well so all sidebar icons (header logo, nav group icons,
4895
+ standalone icons) center-align on the same column width regardless of intrinsic
4896
+ icon size. 1.5rem matches the sidebar's visual rhythm at default density.
4897
+ Consumer wraps the icon: <span slot="icon"><ui-icon name="..."></ui-icon></span>
4898
+ The wrapper is the well; the icon keeps its own --ui-icon-size dimensions. */
4899
+ :where(ui-layout-sidebar-item) > :where([slot="icon"]) {
4900
+ display: inline-flex;
4901
+ align-items: center;
4902
+ justify-content: center;
4903
+ width: 1.5rem;
4904
+ height: 1.5rem;
4905
+ flex-shrink: 0;
4906
+ }
4907
+
4870
4908
  /* WHY: Trailing caret pushes to end — same pattern as button justify="spread". */
4871
- :where(ui-layout-sidebar-trigger) > :where([slot="trailing"]) {
4909
+ :where(ui-layout-sidebar-item) > :where([slot="trailing"]) {
4872
4910
  margin-inline-start: auto;
4873
4911
  flex-shrink: 0;
4874
4912
  color: var(--_ink-muted);
4875
4913
  }
4876
4914
 
4915
+ /* WHY: When the item wraps a sub-component (e.g. ui-button) that provides
4916
+ its own icon, hide [slot="icon"] in expanded mode — it only serves as the
4917
+ bare collapsed-rail representation. Items without sub-components (just
4918
+ icon + label + trailing) keep [slot="icon"] visible in both modes. */
4919
+ :where(ui-layout-sidebar-item:has(> ui-button)) > :where([slot="icon"]) {
4920
+ display: none;
4921
+ }
4922
+
4877
4923
  /* WHY: Title text truncates when sidebar narrows during resize. */
4878
- :where(ui-layout-sidebar-trigger) > :where([slot="label"]) {
4924
+ :where(ui-layout-sidebar-item) > :where([slot="label"]) {
4879
4925
  flex: 1;
4880
4926
  min-width: 0;
4881
4927
  white-space: nowrap;
@@ -4883,25 +4929,67 @@
4883
4929
  text-overflow: ellipsis;
4884
4930
  }
4885
4931
 
4886
- /* ── Trigger Popover ── */
4887
- /* WHY: Popover opens to the right of the sidebar trigger.
4932
+ /* ── Item Popover ── */
4933
+ /* WHY: Popover opens to the right of the sidebar item.
4888
4934
  Default: top-aligned, grows downward (span-block-end).
4889
4935
  Flip: bottom-aligned, grows upward (span-block-start). */
4890
4936
 
4891
- :where(ui-layout-sidebar-trigger) > :where(ui-listbox[popover]) {
4937
+ :where(ui-layout-sidebar-item) > :where(ui-listbox[popover]) {
4892
4938
  position: fixed;
4893
4939
  position-area: inline-end span-block-end;
4894
- position-try-fallbacks: --flip-up;
4940
+ position-try-fallbacks: --sidebar-item-flip-up;
4895
4941
  margin: 0 0 0 var(--ui-popover-gap);
4896
4942
  min-width: 200px;
4897
4943
  max-height: var(--ui-popover-max-height);
4898
4944
  overflow-y: auto;
4899
4945
  }
4900
4946
 
4901
- @position-try --flip-up {
4947
+ @position-try --sidebar-item-flip-up {
4902
4948
  position-area: inline-end span-block-start;
4903
4949
  }
4904
4950
 
4951
+ /* ── Container Query: Collapsed Sidebar ── */
4952
+ /* WHY: Each component owns its own collapsed behavior via @container.
4953
+ The aside is the container (container-name: sidebar). When it shrinks
4954
+ to 48px (icon rail), components respond to the width, not to [collapsed].
4955
+ Threshold 80px: collapsed = 48px, min expanded = 160px. */
4956
+
4957
+ @container sidebar (max-width: 80px) {
4958
+
4959
+ /* Resize handle — no dragging in icon rail */
4960
+ :where(.layout-resize-handle) {
4961
+ display: none;
4962
+ }
4963
+
4964
+ /* Header/footer — center content horizontally */
4965
+ :where(ui-layout-sidebar-header),
4966
+ :where(ui-layout-sidebar-footer) {
4967
+ align-items: center;
4968
+ }
4969
+
4970
+ /* Content — center items in the 48px rail */
4971
+ :where(ui-layout-sidebar-content) {
4972
+ align-items: center;
4973
+ }
4974
+
4975
+ /* Item — shrink to icon-only. Show [slot="icon"], hide everything else.
4976
+ WHY: Sidebar items can contain complex children (buttons with slots,
4977
+ links, etc.). Rather than overriding sub-component styles, the item
4978
+ provides its own bare icon via [slot="icon"] for the collapsed rail. */
4979
+ :where(ui-layout-sidebar-item) {
4980
+ flex: 0 0 auto;
4981
+ padding-inline: var(--_space);
4982
+ }
4983
+
4984
+ :where(ui-layout-sidebar-item) > :where([slot="icon"]) {
4985
+ display: inline-flex;
4986
+ }
4987
+
4988
+ :where(ui-layout-sidebar-item) > :where(:not([slot="icon"]):not(ui-listbox[popover]):not(.nav-group-flyout)) {
4989
+ display: none;
4990
+ }
4991
+ }
4992
+
4905
4993
  }
4906
4994
 
4907
4995
  @layer ui {