@kerfjs/ui 5.0.0-beta.100 → 5.0.0-beta.101

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 (148) hide show
  1. package/README.md +3 -1
  2. package/ai/component-catalog.json +214 -18
  3. package/ai/component-composition.json +216 -8
  4. package/ai/components/README.md +2 -0
  5. package/ai/components/backdrop-image.md +84 -0
  6. package/ai/components/nav-stack.md +6 -1
  7. package/ai/components/pane.md +11 -1
  8. package/ai/components/recipe-media-detail.md +57 -0
  9. package/ai/components/surface-scaffold.md +13 -6
  10. package/ai/components/tab-navigator.md +6 -1
  11. package/ai/components/toolbar-control-group.md +1 -0
  12. package/ai/public-api-signatures-v1.md +356 -32
  13. package/ai/skill.md +53 -5
  14. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  15. package/dist/backdrop-image.d.ts +132 -0
  16. package/dist/backdrop-image.js +5 -0
  17. package/dist/backdrop-image.js.map +1 -0
  18. package/dist/browser/backdrop-image.js +3 -0
  19. package/dist/catalog-resources.d.ts +2 -2
  20. package/dist/catalog.d.ts +2 -2
  21. package/dist/catalog.js +10 -9
  22. package/dist/catalog.js.map +1 -1
  23. package/dist/{chunk-QIG4IGUP.js → chunk-2PXZBFEC.js} +3 -1
  24. package/dist/chunk-2PXZBFEC.js.map +1 -0
  25. package/dist/{chunk-T4WJ5VGP.js → chunk-3SKAEPPZ.js} +2 -2
  26. package/dist/{chunk-T4WJ5VGP.js.map → chunk-3SKAEPPZ.js.map} +1 -1
  27. package/dist/chunk-5NQF35VO.js +67 -0
  28. package/dist/chunk-5NQF35VO.js.map +1 -0
  29. package/dist/{chunk-CMETHPKR.js → chunk-6E2H2E5A.js} +29 -8
  30. package/dist/chunk-6E2H2E5A.js.map +1 -0
  31. package/dist/{chunk-PIVY32AE.js → chunk-BYFTHA4I.js} +16 -3
  32. package/dist/chunk-BYFTHA4I.js.map +1 -0
  33. package/dist/{chunk-HGV7J4T6.js → chunk-EKHDLBRP.js} +1 -1
  34. package/dist/chunk-EKHDLBRP.js.map +1 -0
  35. package/dist/{chunk-QROYYGWU.js → chunk-FNZQOUZ5.js} +2 -2
  36. package/dist/{chunk-QROYYGWU.js.map → chunk-FNZQOUZ5.js.map} +1 -1
  37. package/dist/{chunk-EOO6PYGF.js → chunk-IXLUZHGT.js} +4 -4
  38. package/dist/{chunk-EOO6PYGF.js.map → chunk-IXLUZHGT.js.map} +1 -1
  39. package/dist/{chunk-UMKV7YVQ.js → chunk-LPHU44S3.js} +5 -1
  40. package/dist/chunk-LPHU44S3.js.map +1 -0
  41. package/dist/{chunk-INI4WDWC.js → chunk-M5KZ26VV.js} +27 -4
  42. package/dist/chunk-M5KZ26VV.js.map +1 -0
  43. package/dist/{chunk-TLMMFQWM.js → chunk-NS762OVM.js} +3 -3
  44. package/dist/{chunk-TLMMFQWM.js.map → chunk-NS762OVM.js.map} +1 -1
  45. package/dist/{chunk-CXZXHZTR.js → chunk-OZ6X76G5.js} +34 -1
  46. package/dist/chunk-OZ6X76G5.js.map +1 -0
  47. package/dist/{chunk-YOA5W5ZH.js → chunk-PKL6QJKK.js} +31 -3
  48. package/dist/chunk-PKL6QJKK.js.map +1 -0
  49. package/dist/chunk-QTSMHH5J.js +85 -0
  50. package/dist/chunk-QTSMHH5J.js.map +1 -0
  51. package/dist/chunk-SCDWURBO.js +136 -0
  52. package/dist/chunk-SCDWURBO.js.map +1 -0
  53. package/dist/{chunk-RHD7RKGI.js → chunk-SIW64V65.js} +3 -3
  54. package/dist/{chunk-RHD7RKGI.js.map → chunk-SIW64V65.js.map} +1 -1
  55. package/dist/chunk-SQ5DH66U.js +37 -0
  56. package/dist/chunk-SQ5DH66U.js.map +1 -0
  57. package/dist/{chunk-PYBBYSZ3.js → chunk-YBWQR2OK.js} +5 -5
  58. package/dist/{chunk-PYBBYSZ3.js.map → chunk-YBWQR2OK.js.map} +1 -1
  59. package/dist/{chunk-3GLYIIFR.js → chunk-ZYU5WRC3.js} +2 -2
  60. package/dist/{chunk-3GLYIIFR.js.map → chunk-ZYU5WRC3.js.map} +1 -1
  61. package/dist/collapsible-panel.js +3 -2
  62. package/dist/collapsible-panel.js.map +1 -1
  63. package/dist/index.d.ts +5 -3
  64. package/dist/index.js +24 -20
  65. package/dist/list-action-row.js +1 -1
  66. package/dist/list-header.js +1 -1
  67. package/dist/list-item.js +1 -1
  68. package/dist/{nav-stack-VXTumzKa.d.ts → nav-stack-CU_0dKtI.d.ts} +41 -5
  69. package/dist/nav-stack.d.ts +4 -4
  70. package/dist/nav-stack.js +2 -1
  71. package/dist/pane.d.ts +76 -4
  72. package/dist/pane.js +2 -1
  73. package/dist/select.js +1 -1
  74. package/dist/split-view.d.ts +4 -4
  75. package/dist/split-view.js +3 -2
  76. package/dist/split-view.js.map +1 -1
  77. package/dist/state-banner.js +1 -1
  78. package/dist/styles/backdrop-image.css +241 -0
  79. package/dist/styles/components/actions/toolbar/toolbar.css +9 -0
  80. package/dist/styles/components/actions/toolbar-control-group/toolbar-control-group.css +128 -0
  81. package/dist/styles/components/layout/pane/pane.css +121 -1
  82. package/dist/styles/components/navigation/nav-stack/nav-stack.css +207 -6
  83. package/dist/styles/components/navigation/tab-navigator/tab-navigator.css +121 -3
  84. package/dist/styles/components/surfaces/backdrop-image/backdrop-image.css +241 -0
  85. package/dist/styles/components/surfaces/surface-scaffold/surface-scaffold.css +166 -2
  86. package/dist/styles/foundation.css +165 -8
  87. package/dist/styles/nav-stack.css +207 -6
  88. package/dist/styles/overlay-sheet.css +211 -0
  89. package/dist/styles/pane.css +121 -1
  90. package/dist/styles/styles.css +1 -0
  91. package/dist/styles/surface-scaffold.css +166 -2
  92. package/dist/styles/tab-navigator.css +121 -3
  93. package/dist/styles/toolbar-control-group.css +128 -0
  94. package/dist/styles/toolbar.css +9 -0
  95. package/dist/styles/webawesome.css +10 -0
  96. package/dist/surface-scaffold-register.d.ts +3 -2
  97. package/dist/surface-scaffold-register.js +113 -10
  98. package/dist/surface-scaffold-register.js.map +1 -1
  99. package/dist/surface-scaffold.d.ts +38 -5
  100. package/dist/surface-scaffold.js +1 -1
  101. package/dist/tab-navigator.d.ts +43 -3
  102. package/dist/tab-navigator.js +35 -6
  103. package/dist/tab-navigator.js.map +1 -1
  104. package/dist/toolbar-control-group.d.ts +13 -2
  105. package/dist/toolbar-control-group.js +1 -1
  106. package/dist/toolbar-text.d.ts +6 -1
  107. package/dist/toolbar-text.js +1 -1
  108. package/dist/toolbar.d.ts +1 -0
  109. package/dist/wire-catalog.d.ts +2 -2
  110. package/dist/wire-catalog.js +1 -1
  111. package/dist/wire-nav-stack.js +29 -3
  112. package/dist/wire-nav-stack.js.map +1 -1
  113. package/dist/wire-overlay-sheets.d.ts +20 -0
  114. package/dist/wire-overlay-sheets.js +4 -0
  115. package/dist/wire-overlay-sheets.js.map +1 -0
  116. package/dist/wire-scroll-dividers.js +1 -1
  117. package/dist/wire-token-search-fields.js +1 -1
  118. package/dist/workbench.d.ts +2 -2
  119. package/dist/workbench.js +2 -1
  120. package/docs/accessibility.md +35 -0
  121. package/docs/app-layouts.md +13 -0
  122. package/docs/backdrop-image.md +128 -0
  123. package/docs/component-contract.md +2 -0
  124. package/docs/component-selection.md +11 -2
  125. package/docs/design/templates/toolbar-control-group/glass-dark.svg +1 -0
  126. package/docs/design/templates/toolbar-control-group/glass.svg +1 -0
  127. package/docs/design/templates/toolbar-control-group-dark.svg +1 -1
  128. package/docs/design/templates/toolbar-control-group.svg +1 -1
  129. package/docs/design/templates.md +8 -0
  130. package/docs/layout.md +135 -5
  131. package/docs/manual-test-plan.md +29 -0
  132. package/docs/nav-stack.md +37 -1
  133. package/docs/recipes.md +24 -0
  134. package/docs/surface-scaffold.md +149 -3
  135. package/docs/tab-navigator.md +36 -0
  136. package/docs/ux-demo.md +11 -1
  137. package/llms.txt +42 -4
  138. package/package.json +13 -1
  139. package/ux-demo/recipes/loaders.ts +2 -0
  140. package/ux-demo/recipes/media-detail.tsx +272 -0
  141. package/dist/chunk-CMETHPKR.js.map +0 -1
  142. package/dist/chunk-CXZXHZTR.js.map +0 -1
  143. package/dist/chunk-HGV7J4T6.js.map +0 -1
  144. package/dist/chunk-INI4WDWC.js.map +0 -1
  145. package/dist/chunk-PIVY32AE.js.map +0 -1
  146. package/dist/chunk-QIG4IGUP.js.map +0 -1
  147. package/dist/chunk-UMKV7YVQ.js.map +0 -1
  148. package/dist/chunk-YOA5W5ZH.js.map +0 -1
@@ -98,12 +98,30 @@
98
98
  --_kui-popup-row-icon-gap: 0.5rem;
99
99
  --_kui-popup-row-label-padding-block: 0.35rem;
100
100
  --_kui-popup-row-label-line-height: 1.25;
101
+ --_kui-popup-group-title-padding: 0.2rem 0.75rem 0.3rem;
102
+ --_kui-popup-group-title-font-weight: 750;
103
+ --_kui-popup-caret-open-rotation: -180deg;
104
+ /* The theme's brand text color as the root resolves it. A scoped tint
105
+ clamps its lightness for links and brand text (below); it lives outside
106
+ the palette block, so a tint inherits the root's brand rather than
107
+ re-deriving from itself. */
108
+ --_kui-tint-brand-source: var(--kui-color-brand-on-quiet);
109
+ --_kui-popup-caret-transition: rotate var(--wa-transition-slow, 0.3s)
110
+ var(--wa-transition-easing, ease);
111
+ }
112
+
113
+ /* The palette. A scoped tint ([data-kui-tint], below) re-declares this whole
114
+ block on its element: an unregistered custom property substitutes its var()
115
+ references where it is declared, so a derived token (the chrome material, a
116
+ quiet fill mixed into the surface, a popup's current row) resolved at the
117
+ root would keep the root's colors inside the tint. Only color tokens live
118
+ here, so a tint never resets an app's spacing or geometry override. */
119
+ :root,
120
+ [data-kui-tint] {
101
121
  --_kui-popup-row-current-background: var(--kui-color-brand-fill-normal);
102
122
  /* Current row text sits on the brand fill; the darker on-fill accent clears
103
123
  WCAG AA there (brand-on-quiet is 4.26:1 over that fill). */
104
124
  --_kui-popup-row-current-color: var(--kui-color-brand-on-fill);
105
- --_kui-popup-group-title-padding: 0.2rem 0.75rem 0.3rem;
106
- --_kui-popup-group-title-font-weight: 750;
107
125
  /* A capped, scrolling popup shows that it scrolls: a soft shadow along an
108
126
  edge while more choices lie beyond it. Covers painted in the surface
109
127
  color scroll with the content (`local`) and hide each shadow (`scroll`,
@@ -130,9 +148,6 @@
130
148
  )
131
149
  center bottom / 100% 0.75rem no-repeat scroll,
132
150
  var(--kui-color-surface-raised);
133
- --_kui-popup-caret-open-rotation: -180deg;
134
- --_kui-popup-caret-transition: rotate var(--wa-transition-slow, 0.3s)
135
- var(--wa-transition-easing, ease);
136
151
  /* macOS Tahoe light semantics used by Hot Sheet 2, with a compatible dark extension. */
137
152
  /* Every --kui-color-* token (and the focus-ring color) that reads a --wa-*
138
153
  token falls back to the value src/webawesome.css assigns to that token,
@@ -527,10 +542,127 @@
527
542
  --kui-safe-area-inline-end: env(safe-area-inset-left, 0px);
528
543
  }
529
544
 
545
+ /* Scoped tint. A Pane, NavStack, or TabNavigator `tint` ({ color, scheme })
546
+ marks its root `data-kui-tint="<scheme>"` with the color in
547
+ `--_kui-tint-color`. The app declares the scheme (there is no automatic
548
+ color detection), and the scope re-derives the base surface and text
549
+ tokens from the color; the palette block above then re-derives everything
550
+ built on them (raised and lowered surfaces, quiet fills, the chrome
551
+ material). Relative color syntax clamps each token's lightness to a band
552
+ that holds WCAG AA whatever the color: a light tint's surface stays at or
553
+ above 90% OKLCH lightness under 24% text, 44% quiet text, and brand text
554
+ at most 45%, and a dark tint's surface at or below 30% under 97% text, 80%
555
+ quiet text, and brand text at least 80%. Chroma is capped so
556
+ text stays near-neutral and a surface stays a tint, not a fill.
557
+ Without relative color syntax, and under `prefers-contrast: more`, the
558
+ scope keeps the plain palette in its declared scheme. */
559
+ [data-kui-tint="light"] {
560
+ color-scheme: light;
561
+ }
562
+
563
+ [data-kui-tint="dark"] {
564
+ color-scheme: dark;
565
+ }
566
+
567
+ /* Inside a tint the chrome material keeps its blur but drops the saturation
568
+ boost: over the scope's own colored surface, saturating it would paint a
569
+ visibly brighter band (and a lighter edge) under an overlay header at
570
+ rest, where the header should read as the same surface. */
571
+ [data-kui-tint] {
572
+ color: var(--kui-color-text);
573
+ --kui-chrome-material-filter: blur(20px);
574
+ }
575
+
576
+ @supports (color: oklch(from red l c h)) {
577
+ @media not (prefers-contrast: more) {
578
+ [data-kui-tint="light"] {
579
+ --wa-color-surface-default: oklch(
580
+ from var(--_kui-tint-color) clamp(0.9, l, 0.985) min(c, 0.08) h
581
+ );
582
+ --wa-color-surface-raised: oklch(
583
+ from var(--_kui-tint-color) min(1, calc(clamp(0.9, l, 0.985) + 0.03))
584
+ min(c, 0.05) h
585
+ );
586
+ --wa-color-text-normal: oklch(
587
+ from var(--_kui-tint-color) 0.24 min(c, 0.03) h
588
+ );
589
+ --wa-color-text-quiet: oklch(
590
+ from var(--_kui-tint-color) 0.44 min(c, 0.04) h
591
+ );
592
+ --wa-color-neutral-on-quiet: oklch(
593
+ from var(--_kui-tint-color) 0.42 min(c, 0.04) h
594
+ );
595
+ --wa-color-brand-on-quiet: oklch(
596
+ from var(--_kui-tint-brand-source) min(l, 0.45) c h
597
+ );
598
+ }
599
+
600
+ [data-kui-tint="dark"] {
601
+ --wa-color-surface-default: oklch(
602
+ from var(--_kui-tint-color) clamp(0.16, l, 0.3) min(c, 0.1) h
603
+ );
604
+ --wa-color-surface-raised: oklch(
605
+ from var(--_kui-tint-color) calc(clamp(0.16, l, 0.3) + 0.06)
606
+ min(c, 0.08) h
607
+ );
608
+ --wa-color-text-normal: oklch(
609
+ from var(--_kui-tint-color) 0.97 min(c, 0.02) h
610
+ );
611
+ --wa-color-text-quiet: oklch(
612
+ from var(--_kui-tint-color) 0.8 min(c, 0.04) h
613
+ );
614
+ --wa-color-neutral-on-quiet: oklch(
615
+ from var(--_kui-tint-color) 0.82 min(c, 0.04) h
616
+ );
617
+ --wa-color-brand-on-quiet: oklch(
618
+ from var(--_kui-tint-brand-source) max(l, 0.8) c h
619
+ );
620
+ }
621
+
622
+ /* An optional accent (`tint.accent`) re-derives the brand ramp in the
623
+ scope: the loud fill (buttons, the current row, checked controls), its
624
+ on-loud text, brand text and links, text on brand fills, and the focus
625
+ ring. Lightness is clamped so each pair keeps WCAG AA for any accent:
626
+ a light scope's fill stays at or below 50% under white text, a dark
627
+ scope's between 72% and 85% under near-black text, and brand text at
628
+ most 45% (light) or at least 80% (dark). */
629
+ [data-kui-tint="light"][data-kui-tint-accent] {
630
+ --wa-color-brand-fill-loud: oklch(
631
+ from var(--_kui-tint-accent) min(l, 0.5) c h
632
+ );
633
+ --wa-color-brand-on-loud: #fff;
634
+ --wa-color-brand-on-quiet: oklch(
635
+ from var(--_kui-tint-accent) min(l, 0.45) c h
636
+ );
637
+ --kui-color-brand-on-fill: oklch(
638
+ from var(--_kui-tint-accent) min(l, 0.38) c h
639
+ );
640
+ --wa-color-focus: oklch(from var(--_kui-tint-accent) min(l, 0.5) c h);
641
+ }
642
+
643
+ [data-kui-tint="dark"][data-kui-tint-accent] {
644
+ --wa-color-brand-fill-loud: oklch(
645
+ from var(--_kui-tint-accent) clamp(0.72, l, 0.85) c h
646
+ );
647
+ --wa-color-brand-on-loud: #111113;
648
+ --wa-color-brand-on-quiet: oklch(
649
+ from var(--_kui-tint-accent) max(l, 0.8) c h
650
+ );
651
+ --kui-color-brand-on-fill: oklch(
652
+ from var(--_kui-tint-accent) max(l, 0.82) c h
653
+ );
654
+ --wa-color-focus: oklch(
655
+ from var(--_kui-tint-accent) clamp(0.72, l, 0.85) c h
656
+ );
657
+ }
658
+ }
659
+ }
660
+
530
661
  @supports not (
531
662
  (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))
532
663
  ) {
533
- :root {
664
+ :root,
665
+ [data-kui-tint] {
534
666
  --kui-chrome-material: var(--kui-color-surface);
535
667
  --kui-chrome-material-raised: var(--kui-color-surface-raised);
536
668
  --kui-chrome-material-filter: none;
@@ -538,7 +670,31 @@
538
670
  }
539
671
 
540
672
  @media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
541
- :root {
673
+ :root,
674
+ [data-kui-tint] {
675
+ --kui-chrome-material: var(--kui-color-surface);
676
+ --kui-chrome-material-raised: var(--kui-color-surface-raised);
677
+ --kui-chrome-material-filter: none;
678
+ }
679
+ }
680
+
681
+ /* The static-effects switch. There is no reliable CSS signal for a low-power
682
+ or slow device, so an app that detects one itself sets
683
+ `data-kui-effects="static"` on the root element; a display that cannot
684
+ animate smoothly (`update: slow`, e-ink and similar) gets the same result
685
+ on its own. Either way every backdrop effect turns opaque here, and every
686
+ scroll-driven animation a component owns stops in that component's CSS
687
+ (tests/unit/static-effects.test.ts holds each one to it). */
688
+ :root[data-kui-effects="static"],
689
+ :root[data-kui-effects="static"] [data-kui-tint] {
690
+ --kui-chrome-material: var(--kui-color-surface);
691
+ --kui-chrome-material-raised: var(--kui-color-surface-raised);
692
+ --kui-chrome-material-filter: none;
693
+ }
694
+
695
+ @media (update: slow) {
696
+ :root,
697
+ [data-kui-tint] {
542
698
  --kui-chrome-material: var(--kui-color-surface);
543
699
  --kui-chrome-material-raised: var(--kui-color-surface-raised);
544
700
  --kui-chrome-material-filter: none;
@@ -546,7 +702,8 @@
546
702
  }
547
703
 
548
704
  @media (prefers-contrast: more) {
549
- :root {
705
+ :root,
706
+ [data-kui-tint] {
550
707
  --kui-color-pop-fill-quiet: Canvas;
551
708
  --kui-color-pop-fill-normal: Canvas;
552
709
  --kui-color-pop-fill-loud: Highlight;
@@ -6,7 +6,7 @@
6
6
  height: 100%;
7
7
  overflow: hidden;
8
8
  flex-direction: column;
9
- background: var(--kui-color-surface);
9
+ background: var(--_kui-nav-stack-background, var(--kui-color-surface));
10
10
  /* Safe areas: the stack's surfaces paint through unsafe areas; its chrome,
11
11
  bottom toolbar, and each view compensate for the edges the stack still
12
12
  touches (see pane.css for the `--kui-edge-inset-*` edge context). */
@@ -60,6 +60,25 @@
60
60
  opacity: 0;
61
61
  }
62
62
 
63
+ /* `background`: the stack's color paints the stack, its docked chrome and
64
+ bottom toolbar, and every view (a view's own `pane.background` paints over
65
+ it). Overlay chrome keeps the chrome material. A nested stack never
66
+ inherits the color. Forced colors paint the system surface. */
67
+ .kui-nav-stack:not([data-background]) {
68
+ --_kui-nav-stack-background: initial;
69
+ }
70
+
71
+ @media (forced-colors: active) {
72
+ .kui-nav-stack[data-background],
73
+ .kui-nav-stack[data-background]
74
+ > :is(.kui-nav-stack__chrome, .kui-nav-stack__bottom),
75
+ .kui-nav-stack[data-background]
76
+ > .kui-nav-stack__viewport
77
+ > .kui-nav-stack__view {
78
+ background: Canvas;
79
+ }
80
+ }
81
+
63
82
  /* The chrome hosts the stack's real top Toolbar. It hands that toolbar the
64
83
  edges the stack still touches through the edge context (never the bottom,
65
84
  which the viewport owns); the toolbar pads for the sides it claims with
@@ -68,7 +87,7 @@
68
87
  --kui-edge-inset-block-start: var(--_kui-nav-stack-safe-block-start);
69
88
  --kui-edge-inset-inline-start: var(--_kui-nav-stack-safe-inline-start);
70
89
  --kui-edge-inset-inline-end: var(--_kui-nav-stack-safe-inline-end);
71
- background: var(--kui-color-surface);
90
+ background: var(--_kui-nav-stack-background, var(--kui-color-surface));
72
91
  }
73
92
 
74
93
  .kui-nav-stack__viewport {
@@ -97,7 +116,7 @@
97
116
  .kui-nav-stack__view {
98
117
  position: absolute;
99
118
  overflow: auto;
100
- background: var(--kui-color-surface);
119
+ background: var(--_kui-nav-stack-background, var(--kui-color-surface));
101
120
  transition: transform var(--kui-nav-stack-transition-duration, 200ms) ease;
102
121
  inset: 0;
103
122
  }
@@ -179,7 +198,7 @@
179
198
  --kui-edge-inset-inline-end: var(--_kui-nav-stack-safe-inline-end);
180
199
  border-block-start: 1px solid
181
200
  var(--_kui-nav-stack-bottom-divider, transparent);
182
- background: var(--kui-color-surface);
201
+ background: var(--_kui-nav-stack-background, var(--kui-color-surface));
183
202
  }
184
203
 
185
204
  /* The live chrome is the containing block of its scroll divider; a
@@ -272,6 +291,182 @@
272
291
  }
273
292
  }
274
293
 
294
+ /* `clear` chrome and bottom toolbar: overlay bars (the same measuring,
295
+ placement, and edge insets as above) that are fully transparent, for
296
+ chrome over imagery. With `scrollEdge="material"` a pseudo-element
297
+ carrying the chrome material fades in (opacity only) while the active
298
+ view's content lies beneath the bar, as `wireScrollDividers` reports, and
299
+ out at rest. Clear bars draw no scroll divider. */
300
+ .kui-nav-stack[data-chrome-material="clear"] > .kui-nav-stack__chrome {
301
+ -webkit-backdrop-filter: none;
302
+ backdrop-filter: none;
303
+ background: transparent;
304
+ }
305
+
306
+ :where(.kui-nav-stack[data-chrome-material="clear"][data-scroll-edge])
307
+ > .kui-nav-stack__chrome:not(.kui-nav-stack__chrome-copy) {
308
+ position: relative;
309
+ isolation: isolate;
310
+ }
311
+
312
+ /* The scroll edge fades in as content scrolls beneath the bar: the wiring
313
+ publishes `--kui-scroll-edge-progress` (0 at rest, 1 once content lies
314
+ `scrollEdgeDistance` beyond the bar) and only the layer's opacity follows
315
+ it; the filter never animates. Unwired, it stays clear. */
316
+ .kui-nav-stack[data-chrome-material="clear"][data-scroll-edge]
317
+ > .kui-nav-stack__chrome::before {
318
+ position: absolute;
319
+ z-index: -1;
320
+ content: "";
321
+ inset: 0;
322
+ opacity: var(--kui-scroll-edge-progress, 0);
323
+ pointer-events: none;
324
+ }
325
+
326
+ .kui-nav-stack[data-chrome-material="clear"][data-scroll-edge="material"]
327
+ > .kui-nav-stack__chrome::before {
328
+ -webkit-backdrop-filter: var(--kui-chrome-material-filter);
329
+ backdrop-filter: var(--kui-chrome-material-filter);
330
+ background: var(--kui-chrome-material);
331
+ }
332
+
333
+ .kui-nav-stack[data-chrome-material="clear"][data-scroll-edge="surface"]
334
+ > .kui-nav-stack__chrome::before {
335
+ background: var(--kui-color-surface);
336
+ }
337
+
338
+ /* Static effects and slow displays snap the edge on while content lies
339
+ beneath the bar instead of tracking the scroll. */
340
+ :root[data-kui-effects="static"]
341
+ .kui-nav-stack[data-chrome-material="clear"][data-scroll-edge]
342
+ > .kui-nav-stack__chrome::before {
343
+ opacity: 0;
344
+ }
345
+
346
+ :root[data-kui-effects="static"]
347
+ .kui-nav-stack[data-chrome-material="clear"][data-scroll-edge]
348
+ > .kui-nav-stack__chrome[data-scroll-divider*="b"]::before {
349
+ opacity: 1;
350
+ }
351
+
352
+ @media (update: slow) {
353
+ .kui-nav-stack[data-chrome-material="clear"][data-scroll-edge]
354
+ > .kui-nav-stack__chrome::before {
355
+ opacity: 0;
356
+ }
357
+
358
+ .kui-nav-stack[data-chrome-material="clear"][data-scroll-edge]
359
+ > .kui-nav-stack__chrome[data-scroll-divider*="b"]::before {
360
+ opacity: 1;
361
+ }
362
+ }
363
+
364
+ /* Clear chrome turns opaque where transparency would hurt, like the
365
+ material itself. */
366
+ @media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
367
+ .kui-nav-stack[data-chrome-material="clear"] > .kui-nav-stack__chrome {
368
+ background: var(--kui-chrome-material);
369
+ }
370
+ }
371
+
372
+ @media (forced-colors: active) {
373
+ .kui-nav-stack[data-chrome-material="clear"] > .kui-nav-stack__chrome {
374
+ background: Canvas;
375
+ border-block-end: 1px solid CanvasText;
376
+ }
377
+
378
+ .kui-nav-stack[data-chrome-material="clear"][data-scroll-edge]
379
+ > .kui-nav-stack__chrome::before {
380
+ content: none;
381
+ }
382
+ }
383
+
384
+ .kui-nav-stack[data-bottom-toolbar-material="clear"] > .kui-nav-stack__bottom {
385
+ -webkit-backdrop-filter: none;
386
+ backdrop-filter: none;
387
+ background: transparent;
388
+ }
389
+
390
+ :where(.kui-nav-stack[data-bottom-toolbar-material="clear"][data-scroll-edge])
391
+ > .kui-nav-stack__bottom:not(.kui-nav-stack__chrome-copy) {
392
+ position: relative;
393
+ isolation: isolate;
394
+ }
395
+
396
+ /* The scroll edge fades in as content scrolls beneath the bar: the wiring
397
+ publishes `--kui-scroll-edge-progress` (0 at rest, 1 once content lies
398
+ `scrollEdgeDistance` beyond the bar) and only the layer's opacity follows
399
+ it; the filter never animates. Unwired, it stays clear. */
400
+ .kui-nav-stack[data-bottom-toolbar-material="clear"][data-scroll-edge]
401
+ > .kui-nav-stack__bottom::before {
402
+ position: absolute;
403
+ z-index: -1;
404
+ content: "";
405
+ inset: 0;
406
+ opacity: var(--kui-scroll-edge-progress, 0);
407
+ pointer-events: none;
408
+ }
409
+
410
+ .kui-nav-stack[data-bottom-toolbar-material="clear"][data-scroll-edge="material"]
411
+ > .kui-nav-stack__bottom::before {
412
+ -webkit-backdrop-filter: var(--kui-chrome-material-filter);
413
+ backdrop-filter: var(--kui-chrome-material-filter);
414
+ background: var(--kui-chrome-material);
415
+ }
416
+
417
+ .kui-nav-stack[data-bottom-toolbar-material="clear"][data-scroll-edge="surface"]
418
+ > .kui-nav-stack__bottom::before {
419
+ background: var(--kui-color-surface);
420
+ }
421
+
422
+ /* Static effects and slow displays snap the edge on while content lies
423
+ beneath the bar instead of tracking the scroll. */
424
+ :root[data-kui-effects="static"]
425
+ .kui-nav-stack[data-bottom-toolbar-material="clear"][data-scroll-edge]
426
+ > .kui-nav-stack__bottom::before {
427
+ opacity: 0;
428
+ }
429
+
430
+ :root[data-kui-effects="static"]
431
+ .kui-nav-stack[data-bottom-toolbar-material="clear"][data-scroll-edge]
432
+ > .kui-nav-stack__bottom[data-scroll-divider*="t"]::before {
433
+ opacity: 1;
434
+ }
435
+
436
+ @media (update: slow) {
437
+ .kui-nav-stack[data-bottom-toolbar-material="clear"][data-scroll-edge]
438
+ > .kui-nav-stack__bottom::before {
439
+ opacity: 0;
440
+ }
441
+
442
+ .kui-nav-stack[data-bottom-toolbar-material="clear"][data-scroll-edge]
443
+ > .kui-nav-stack__bottom[data-scroll-divider*="t"]::before {
444
+ opacity: 1;
445
+ }
446
+ }
447
+
448
+ /* Clear chrome turns opaque where transparency would hurt, like the
449
+ material itself. */
450
+ @media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
451
+ .kui-nav-stack[data-bottom-toolbar-material="clear"]
452
+ > .kui-nav-stack__bottom {
453
+ background: var(--kui-chrome-material);
454
+ }
455
+ }
456
+
457
+ @media (forced-colors: active) {
458
+ .kui-nav-stack[data-bottom-toolbar-material="clear"]
459
+ > .kui-nav-stack__bottom {
460
+ background: Canvas;
461
+ border-block-start-color: CanvasText;
462
+ }
463
+
464
+ .kui-nav-stack[data-bottom-toolbar-material="clear"][data-scroll-edge]
465
+ > .kui-nav-stack__bottom::before {
466
+ content: none;
467
+ }
468
+ }
469
+
275
470
  /* Chrome dividers: with `chromeDividers="scroll"` (the default, no
276
471
  attribute) the top chrome draws a line along its bottom edge while the
277
472
  active view's content is scrolled beneath it, and the bottom toolbar
@@ -290,7 +485,10 @@
290
485
  > [data-component="pane"][data-appearance="sunken"]
291
486
  )
292
487
  > .kui-nav-stack__chrome::after,
293
- .kui-nav-stack[data-separator="auto"]:not([data-chrome-dividers])
488
+ .kui-nav-stack[data-separator="auto"]:not(
489
+ [data-chrome-dividers],
490
+ [data-chrome-material="clear"]
491
+ )
294
492
  > .kui-nav-stack__chrome[data-scroll-divider*="b"]::after,
295
493
  .kui-nav-stack[data-separator="auto"][data-chrome-dividers="always"]
296
494
  > .kui-nav-stack__chrome::after {
@@ -309,7 +507,10 @@
309
507
  > [data-component="pane"][data-appearance="sunken"]
310
508
  )
311
509
  > .kui-nav-stack__bottom,
312
- .kui-nav-stack[data-separator="auto"]:not([data-chrome-dividers])
510
+ .kui-nav-stack[data-separator="auto"]:not(
511
+ [data-chrome-dividers],
512
+ [data-bottom-toolbar-material="clear"]
513
+ )
313
514
  > .kui-nav-stack__bottom[data-scroll-divider*="t"],
314
515
  .kui-nav-stack[data-separator="auto"][data-chrome-dividers="always"]
315
516
  > .kui-nav-stack__bottom {
@@ -0,0 +1,211 @@
1
+ /* Bottom-sheet presentation for the core `kerfjs/overlay` surfaces: `overlay()`
2
+ and the `confirm` / `prompt` / `form` / `choice` promise dialogs opened with
3
+ `presentation: 'bottom-sheet'`. The core ships no CSS; it marks the overlay
4
+ `data-presentation="bottom-sheet"` and publishes the virtual keyboard's
5
+ overlap as `--kerf-keyboard-inset`. This opt-in stylesheet gives those
6
+ sheets the geometry of a `DialogSurface` bottom sheet: resting on the
7
+ bottom edge with square bottom corners and the dialog radius on top,
8
+ capped below the top safe area, padded at its block end by the larger of
9
+ the safe area and the keyboard so its content stays above both, and
10
+ sliding up as it opens. It styles the plain path (the wrapper is the
11
+ backdrop and its content the sheet) and the `native: true` path (the
12
+ `<dialog>` is the sheet). */
13
+ div.kerf-overlay[data-presentation="bottom-sheet"] {
14
+ position: fixed;
15
+ z-index: var(--kui-overlay-sheet-z-index, 1000);
16
+ display: flex;
17
+ align-items: flex-end;
18
+ justify-content: center;
19
+ background: var(
20
+ --wa-color-overlay-modal,
21
+ light-dark(rgb(29 29 31 / 0.34), rgb(0 0 0 / 0.6))
22
+ );
23
+ inset: 0;
24
+ }
25
+
26
+ dialog.kerf-overlay[data-presentation="bottom-sheet"]::backdrop {
27
+ background: var(
28
+ --wa-color-overlay-modal,
29
+ light-dark(rgb(29 29 31 / 0.34), rgb(0 0 0 / 0.6))
30
+ );
31
+ }
32
+
33
+ div.kerf-overlay[data-presentation="bottom-sheet"] > *,
34
+ dialog.kerf-overlay[data-presentation="bottom-sheet"] {
35
+ --_kui-overlay-sheet-clearance: max(
36
+ var(--kui-safe-area-block-end, env(safe-area-inset-bottom, 0px)),
37
+ var(--kerf-keyboard-inset, 0px)
38
+ );
39
+ --_kui-overlay-sheet-cap: calc(
40
+ 100dvh - var(--kui-safe-area-block-start, env(safe-area-inset-top, 0px)) -
41
+ 1rem
42
+ );
43
+ --_kui-overlay-sheet-padding-end: calc(
44
+ var(--kui-space-xs) + var(--_kui-overlay-sheet-clearance)
45
+ );
46
+ box-sizing: border-box;
47
+ width: min(100%, 35rem);
48
+ max-width: 100%;
49
+ max-height: var(--_kui-overlay-sheet-cap);
50
+ margin: 0;
51
+ /* The 8px surface inset of a DialogSurface body: content items and lists
52
+ inside own their own inset from there. */
53
+ padding: var(--kui-space-xs) var(--kui-space-xs)
54
+ var(--_kui-overlay-sheet-padding-end);
55
+ overflow: auto;
56
+ border: 0;
57
+ /* Concentric with a pill control group at the 8px surface inset, like a
58
+ DialogSurface. */
59
+ border-radius: calc(
60
+ var(--kui-layout-pill-radius, calc(1px + 1.3125rem)) +
61
+ var(--kui-wa-surface-inset, 0.5rem)
62
+ )
63
+ calc(
64
+ var(--kui-layout-pill-radius, calc(1px + 1.3125rem)) +
65
+ var(--kui-wa-surface-inset, 0.5rem)
66
+ )
67
+ 0 0;
68
+ color: var(--kui-color-text);
69
+ background: var(--kui-color-surface-raised);
70
+ box-shadow: var(--kui-shadow-l);
71
+ overscroll-behavior: contain;
72
+ }
73
+
74
+ /* The native <dialog> keeps its top-layer placement; only the UA centering
75
+ gives way to the bottom edge. */
76
+ dialog.kerf-overlay[data-presentation="bottom-sheet"] {
77
+ margin: auto auto 0;
78
+ }
79
+
80
+ /* A resizable sheet (the `kui-overlay-sheet-resizable` class, wired by
81
+ `wireOverlaySheets` from `@kerfjs/ui/wire-overlay-sheets`) carries the
82
+ grabber of a resizable DialogSurface sheet at its top edge. It opens at its
83
+ content height; a drag or the keyboard publishes the visible height on the
84
+ wrapper, between the minimum and the cap. */
85
+ div.kerf-overlay.kui-overlay-sheet-resizable[data-presentation="bottom-sheet"]
86
+ > *,
87
+ dialog.kerf-overlay.kui-overlay-sheet-resizable[data-presentation="bottom-sheet"] {
88
+ height: calc(
89
+ var(--_kui-overlay-sheet-height) + var(--_kui-overlay-sheet-padding-end)
90
+ );
91
+ min-height: min(
92
+ calc(
93
+ var(--kui-overlay-sheet-min-height, 10rem) +
94
+ var(--_kui-overlay-sheet-padding-end)
95
+ ),
96
+ var(--_kui-overlay-sheet-cap)
97
+ );
98
+ padding-block-start: 0;
99
+ }
100
+
101
+ /* Sticky, so it stays at the top edge while the sheet's content scrolls, and
102
+ overlapping the content's first 8px, so the content starts 12px down as in
103
+ a resizable DialogSurface sheet. */
104
+ .kui-overlay-sheet-handle {
105
+ position: sticky;
106
+ z-index: 2;
107
+ inset-block-start: 0;
108
+ display: block;
109
+ width: 6rem;
110
+ height: 1.25rem;
111
+ margin: 0 auto calc(0.75rem - 1.25rem);
112
+ border-radius: var(--kui-radius-s);
113
+ cursor: ns-resize;
114
+ touch-action: none;
115
+ }
116
+
117
+ .kui-overlay-sheet-handle::before {
118
+ position: absolute;
119
+ inset-block-start: 0.375rem;
120
+ inset-inline: 0;
121
+ width: 2.25rem;
122
+ height: 0.3125rem;
123
+ margin-inline: auto;
124
+ border-radius: 0.15625rem;
125
+ background: var(--kui-color-neutral-border-loud);
126
+ content: "";
127
+ }
128
+
129
+ .kui-overlay-sheet-handle:focus-visible {
130
+ outline: var(--kui-focus-ring);
131
+ outline-offset: -3px;
132
+ }
133
+
134
+ @keyframes kui-overlay-sheet-slide {
135
+ from {
136
+ translate: 0 100%;
137
+ }
138
+ to {
139
+ translate: 0 0;
140
+ }
141
+ }
142
+
143
+ @media (prefers-reduced-motion: no-preference) {
144
+ :root:not([data-kui-effects="static"])
145
+ div.kerf-overlay[data-presentation="bottom-sheet"]
146
+ > *,
147
+ :root:not([data-kui-effects="static"])
148
+ dialog.kerf-overlay[data-presentation="bottom-sheet"] {
149
+ animation: kui-overlay-sheet-slide var(--wa-transition-slow, 300ms)
150
+ cubic-bezier(0.32, 0.72, 0, 1);
151
+ }
152
+ }
153
+
154
+ /* Exit motion: pass `exitClass: 'kui-overlay-sheet-exit'` (with an
155
+ `exitDuration` cap such as 400) to the overlay or promise dialog, and the
156
+ sheet slides back down while the backdrop fades. The core removes the node
157
+ once the animations finish, and skips the exit under reduced motion; the
158
+ static-effects switch leaves nothing running, so it is removed at once. A
159
+ closed native <dialog> is `display: none`, so the class keeps it painted
160
+ at the bottom edge for the exit. */
161
+ @keyframes kui-overlay-sheet-slide-out {
162
+ from {
163
+ translate: 0 0;
164
+ }
165
+ to {
166
+ translate: 0 100%;
167
+ }
168
+ }
169
+
170
+ @keyframes kui-overlay-sheet-backdrop-out {
171
+ to {
172
+ background-color: transparent;
173
+ }
174
+ }
175
+
176
+ dialog.kerf-overlay[data-presentation="bottom-sheet"].kui-overlay-sheet-exit {
177
+ display: block;
178
+ position: fixed;
179
+ z-index: var(--kui-overlay-sheet-z-index, 1000);
180
+ inset: auto 0 0;
181
+ margin-inline: auto;
182
+ }
183
+
184
+ @media (prefers-reduced-motion: no-preference) {
185
+ :root:not([data-kui-effects="static"])
186
+ div.kerf-overlay[data-presentation="bottom-sheet"].kui-overlay-sheet-exit {
187
+ animation: kui-overlay-sheet-backdrop-out var(--wa-transition-slow, 300ms)
188
+ ease both;
189
+ }
190
+
191
+ :root:not([data-kui-effects="static"])
192
+ div.kerf-overlay[data-presentation="bottom-sheet"].kui-overlay-sheet-exit
193
+ > *,
194
+ :root:not([data-kui-effects="static"])
195
+ dialog.kerf-overlay[data-presentation="bottom-sheet"].kui-overlay-sheet-exit {
196
+ animation: kui-overlay-sheet-slide-out var(--wa-transition-slow, 300ms)
197
+ cubic-bezier(0.32, 0.72, 0, 1) both;
198
+ }
199
+ }
200
+
201
+ @media (forced-colors: active) {
202
+ div.kerf-overlay[data-presentation="bottom-sheet"] > *,
203
+ dialog.kerf-overlay[data-presentation="bottom-sheet"] {
204
+ border-block-start: 1px solid CanvasText;
205
+ background: Canvas;
206
+ }
207
+
208
+ .kui-overlay-sheet-handle::before {
209
+ background: CanvasText;
210
+ }
211
+ }