@twentyfirstmedia/ui-core 0.1.0 → 0.2.0

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 (84) hide show
  1. package/dist/Icon.d.ts +1 -1
  2. package/dist/components/admin.d.ts.map +1 -1
  3. package/dist/components/admin.js +2 -2
  4. package/dist/components/admin.js.map +1 -1
  5. package/dist/components/charts.js +4 -4
  6. package/dist/components/charts.js.map +1 -1
  7. package/dist/components/data-display.d.ts +2 -0
  8. package/dist/components/data-display.d.ts.map +1 -1
  9. package/dist/components/data-display.js +32 -8
  10. package/dist/components/data-display.js.map +1 -1
  11. package/dist/components/developer.d.ts.map +1 -1
  12. package/dist/components/developer.js +4 -2
  13. package/dist/components/developer.js.map +1 -1
  14. package/dist/components/feedback.d.ts +1 -1
  15. package/dist/components/feedback.d.ts.map +1 -1
  16. package/dist/components/feedback.js +9 -5
  17. package/dist/components/feedback.js.map +1 -1
  18. package/dist/components/forms.d.ts.map +1 -1
  19. package/dist/components/forms.js +3 -1
  20. package/dist/components/forms.js.map +1 -1
  21. package/dist/components/index.d.ts +1 -1
  22. package/dist/components/index.d.ts.map +1 -1
  23. package/dist/components/index.js +1 -1
  24. package/dist/components/index.js.map +1 -1
  25. package/dist/components/overlays.d.ts.map +1 -1
  26. package/dist/components/overlays.js +5 -4
  27. package/dist/components/overlays.js.map +1 -1
  28. package/dist/components/primitives.d.ts.map +1 -1
  29. package/dist/components/primitives.js +3 -1
  30. package/dist/components/primitives.js.map +1 -1
  31. package/dist/components/shell.d.ts +11 -1
  32. package/dist/components/shell.d.ts.map +1 -1
  33. package/dist/components/shell.js +41 -4
  34. package/dist/components/shell.js.map +1 -1
  35. package/dist/components/types.d.ts +54 -2
  36. package/dist/components/types.d.ts.map +1 -1
  37. package/dist/components/workspace.d.ts +2 -2
  38. package/dist/components/workspace.d.ts.map +1 -1
  39. package/dist/components/workspace.js +9 -3
  40. package/dist/components/workspace.js.map +1 -1
  41. package/dist/cx.d.ts +7 -2
  42. package/dist/cx.d.ts.map +1 -1
  43. package/dist/cx.js +25 -3
  44. package/dist/cx.js.map +1 -1
  45. package/dist/index.d.ts +3 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +2 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/motion.d.ts +13 -0
  50. package/dist/motion.d.ts.map +1 -0
  51. package/dist/motion.js +44 -0
  52. package/dist/motion.js.map +1 -0
  53. package/dist/presence.d.ts +18 -0
  54. package/dist/presence.d.ts.map +1 -0
  55. package/dist/presence.js +49 -0
  56. package/dist/presence.js.map +1 -0
  57. package/dist/provider.d.ts +22 -0
  58. package/dist/provider.d.ts.map +1 -1
  59. package/dist/provider.js +18 -4
  60. package/dist/provider.js.map +1 -1
  61. package/dist/themes.css +1821 -102
  62. package/dist/toast-store.d.ts +2 -0
  63. package/dist/toast-store.d.ts.map +1 -1
  64. package/dist/toast-store.js +28 -1
  65. package/dist/toast-store.js.map +1 -1
  66. package/package.json +1 -1
  67. package/src/components/admin.tsx +7 -3
  68. package/src/components/charts.tsx +4 -4
  69. package/src/components/data-display.tsx +48 -8
  70. package/src/components/developer.tsx +4 -2
  71. package/src/components/feedback.tsx +9 -4
  72. package/src/components/forms.tsx +4 -1
  73. package/src/components/index.ts +1 -1
  74. package/src/components/overlays.tsx +15 -5
  75. package/src/components/primitives.tsx +4 -1
  76. package/src/components/shell.tsx +103 -4
  77. package/src/components/types.ts +54 -2
  78. package/src/components/workspace.tsx +8 -1
  79. package/src/cx.ts +22 -5
  80. package/src/index.ts +3 -0
  81. package/src/motion.ts +48 -0
  82. package/src/presence.ts +58 -0
  83. package/src/provider.tsx +44 -5
  84. package/src/toast-store.ts +28 -1
@@ -18,6 +18,8 @@ export interface ToastData extends ToastOptions {
18
18
  id: string;
19
19
  tone: ToastTone;
20
20
  duration: number;
21
+ /** Animating out; removed after `--dur-exit`. */
22
+ leaving?: boolean;
21
23
  }
22
24
  /** Shows a toast in the `<Toaster/>`; at most three are visible. Returns its id. */
23
25
  export declare function toast(o: ToastOptions | string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"toast-store.d.ts","sourceRoot":"","sources":["../src/toast-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;AAClE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IAChD,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AACD,MAAM,WAAW,SAAU,SAAQ,YAAY;IAC7C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAMD,oFAAoF;AACpF,wBAAgB,KAAK,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,GAAG,MAAM,CAWtD;yBAXe,KAAK;sBAYA,MAAM,KAAG,IAAI;qBAIhB,IAAI;;AAKtB,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAG1D;AACD,wBAAgB,SAAS,IAAI,SAAS,EAAE,CAEvC;AAED,wBAAgB,eAAe,IAAI,SAAS,EAAE,CAE7C"}
1
+ {"version":3,"file":"toast-store.d.ts","sourceRoot":"","sources":["../src/toast-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;AAClE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IAChD,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AACD,MAAM,WAAW,SAAU,SAAQ,YAAY;IAC7C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAMD,oFAAoF;AACpF,wBAAgB,KAAK,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,GAAG,MAAM,CAWtD;yBAXe,KAAK;sBAYA,MAAM,KAAG,IAAI;qBA6BhB,IAAI;;AAKtB,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAG1D;AACD,wBAAgB,SAAS,IAAI,SAAS,EAAE,CAEvC;AAED,wBAAgB,eAAe,IAAI,SAAS,EAAE,CAE7C"}
@@ -15,9 +15,36 @@ export function toast(o) {
15
15
  return t.id;
16
16
  }
17
17
  toast.dismiss = (id) => {
18
- list = list.filter((t) => t.id !== id);
18
+ const ms = exitMs();
19
+ if (ms <= 0) {
20
+ list = list.filter((t) => t.id !== id);
21
+ emit();
22
+ return;
23
+ }
24
+ list = list.map((t) => (t.id === id ? { ...t, leaving: true } : t));
19
25
  emit();
26
+ setTimeout(() => {
27
+ list = list.filter((t) => t.id !== id);
28
+ emit();
29
+ }, ms);
20
30
  };
31
+ /** How long a toast takes to leave (0 without motion). */
32
+ function exitMs() {
33
+ try {
34
+ if (typeof window === "undefined")
35
+ return 0;
36
+ if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches)
37
+ return 0;
38
+ if (document.documentElement.closest("[data-motion]")?.getAttribute("data-motion") === "off")
39
+ return 0;
40
+ const raw = getComputedStyle(document.documentElement).getPropertyValue("--dur-exit").trim();
41
+ const n = parseFloat(raw);
42
+ return isNaN(n) ? 0 : raw.endsWith("ms") ? n : n * 1000;
43
+ }
44
+ catch {
45
+ return 0;
46
+ }
47
+ }
21
48
  toast.clear = () => {
22
49
  list = [];
23
50
  emit();
@@ -1 +1 @@
1
- {"version":3,"file":"toast-store.js","sourceRoot":"","sources":["../src/toast-store.ts"],"names":[],"mappings":"AAoBA,IAAI,IAAI,GAAgB,EAAE,CAAC;AAC3B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAc,CAAC;AACnC,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AAE5C,oFAAoF;AACpF,MAAM,UAAU,KAAK,CAAC,CAAwB;IAC5C,MAAM,IAAI,GAAiB,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,MAAM,CAAC,GAAc;QACnB,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QACvC,GAAG,IAAI;QACP,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,SAAS;QAC5B,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;KACrD,CAAC;IACF,IAAI,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9B,IAAI,EAAE,CAAC;IACP,OAAO,CAAC,CAAC,EAAE,CAAC;AACd,CAAC;AACD,KAAK,CAAC,OAAO,GAAG,CAAC,EAAU,EAAQ,EAAE;IACnC,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IACvC,IAAI,EAAE,CAAC;AACT,CAAC,CAAC;AACF,KAAK,CAAC,KAAK,GAAG,GAAS,EAAE;IACvB,IAAI,GAAG,EAAE,CAAC;IACV,IAAI,EAAE,CAAC;AACT,CAAC,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,EAAc;IAC5C,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IACb,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAC/B,CAAC;AACD,MAAM,UAAU,SAAS;IACvB,OAAO,IAAI,CAAC;AACd,CAAC;AACD,MAAM,KAAK,GAAgB,EAAE,CAAC;AAC9B,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"toast-store.js","sourceRoot":"","sources":["../src/toast-store.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twentyfirstmedia/ui-core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Components, hooks, icons and formatters shared by the Halo and Pro kits.",
5
5
  "license": "MIT",
6
6
  "author": "Twentyfirst Media (https://twentyfirst.media)",
@@ -539,7 +539,7 @@ export function MapView(p: MapViewProps) {
539
539
  })}
540
540
  {cur && (
541
541
  <div
542
- className="tf-map-pop"
542
+ className={cx("tf-map-pop", p.surface && "tf-" + p.surface)}
543
543
  style={{
544
544
  left: Math.min(72, Math.max(28, xy(cur).x)) + "%",
545
545
  top: xy(cur).y + "%",
@@ -555,8 +555,12 @@ export function MapView(p: MapViewProps) {
555
555
  )}
556
556
  </div>
557
557
  )}
558
- {p.legend && <div className="tf-map-legend">{p.legend}</div>}
559
- {p.controls && <div className="tf-map-ctl">{p.controls}</div>}
558
+ {p.legend && (
559
+ <div className={cx("tf-map-legend", p.surface && "tf-" + p.surface)}>{p.legend}</div>
560
+ )}
561
+ {p.controls && (
562
+ <div className={cx("tf-map-ctl", p.surface && "tf-" + p.surface)}>{p.controls}</div>
563
+ )}
560
564
  </div>
561
565
  );
562
566
  }
@@ -76,7 +76,7 @@ export function BarChart(p: BarChartProps) {
76
76
  height={bh}
77
77
  rx={Math.min(10, bw / 2)}
78
78
  style={{
79
- animationDelay: 120 + i * 55 + "ms",
79
+ animationDelay: `calc(var(--enter-delay, 0ms) + ${120 + i * 55}ms * var(--motion-time, 1))`,
80
80
  }}
81
81
  >
82
82
  <title>{d.label + ": " + d.value}</title>
@@ -164,7 +164,7 @@ export function LineChart(p: LineChartProps) {
164
164
  d={d}
165
165
  pathLength={1}
166
166
  style={{
167
- animationDelay: 150 + si * 200 + "ms",
167
+ animationDelay: `calc(var(--enter-delay, 0ms) + ${150 + si * 200}ms * var(--motion-time, 1))`,
168
168
  }}
169
169
  />
170
170
  <circle
@@ -430,7 +430,7 @@ export function StackedBarChart(p: StackedBarChartProps) {
430
430
  key={si}
431
431
  className={cx("tf-seg-bar", "tf-sf-" + (s.tint || "ink"))}
432
432
  style={{
433
- animationDelay: 120 + i * 45 + "ms",
433
+ animationDelay: `calc(var(--enter-delay, 0ms) + ${120 + i * 45}ms * var(--motion-time, 1))`,
434
434
  }}
435
435
  d={
436
436
  top
@@ -715,7 +715,7 @@ export function Heatmap(p: HeatmapProps) {
715
715
  hov[0] && hov[0][0] === ri && hov[0][1] === ci && "hov",
716
716
  )}
717
717
  style={{
718
- animationDelay: ci * 12 + ri * 30 + "ms",
718
+ animationDelay: `calc(var(--enter-delay, 0ms) + ${ci * 12 + ri * 30}ms * var(--motion-time, 1))`,
719
719
  }}
720
720
  onPointerEnter={() => {
721
721
  hov[1]([ri, ci]);
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
- import { cx, durationToken, reduced } from "../cx.js";
4
+ import { cx, durationToken, enterDelay, reduced } from "../cx.js";
5
5
  import { Icon } from "../Icon.js";
6
6
  import { useMounted, useScrollFocus } from "../hooks.js";
7
7
  import { Avatar, IconButton, TintIcon } from "./foundation.js";
@@ -59,18 +59,20 @@ export function CountUp(p: CountUpProps) {
59
59
  const o = parseNum(p.value),
60
60
  st = React.useState(o ? fmt(0, o) : String(p.value)),
61
61
  last = React.useRef(0),
62
- first = React.useRef(true);
62
+ first = React.useRef(true),
63
+ el = React.useRef<HTMLSpanElement>(null);
63
64
  React.useEffect(() => {
64
- if (!o || reduced()) {
65
+ if (!o || reduced(el.current)) {
65
66
  st[1](String(p.value));
66
67
  return;
67
68
  }
68
- const slow = durationToken("--dur-slow", 900);
69
+ const slow = durationToken("--dur-slow", 900, el.current);
69
70
  const from = last.current;
70
71
  let start: number | null = null,
71
72
  raf = 0;
72
73
  const dur = first.current ? p.duration || slow : slow / 2,
73
- delay = first.current ? p.delay || 0 : 0;
74
+ // the first count waits for the card it sits in to come in
75
+ delay = first.current ? (p.delay || 0) + enterDelay(el.current) : 0;
74
76
  first.current = false;
75
77
  const target = o;
76
78
  function tick(t: number) {
@@ -88,7 +90,11 @@ export function CountUp(p: CountUpProps) {
88
90
  cancelAnimationFrame(raf);
89
91
  };
90
92
  }, [p.value]);
91
- return <span className={p.className}>{st[0]}</span>;
93
+ return (
94
+ <span ref={el} className={p.className}>
95
+ {st[0]}
96
+ </span>
97
+ );
92
98
  }
93
99
  export function StatTile(p: StatTileProps) {
94
100
  const tint = p.tint || "sky";
@@ -114,7 +120,13 @@ export function Card(p: CardProps) {
114
120
  const tint = p.tint && p.tint !== "surface" ? "tf-bg-" + p.tint : null;
115
121
  return (
116
122
  <article
117
- className={cx("tf-card", tint, p.onClick && "tf-card-interactive", p.className)}
123
+ className={cx(
124
+ "tf-card",
125
+ tint,
126
+ p.onClick && "tf-card-interactive",
127
+ p.surface && "tf-" + p.surface,
128
+ p.className,
129
+ )}
118
130
  style={p.style}
119
131
  tabIndex={p.onClick ? 0 : undefined}
120
132
  onClick={p.onClick}
@@ -177,7 +189,10 @@ export function EventCard(p: EventCardProps) {
177
189
  }
178
190
  export function Panel(p: PanelProps) {
179
191
  return (
180
- <section className={cx("tf-panel", p.side && "tf-side", p.className)} style={p.style}>
192
+ <section
193
+ className={cx("tf-panel", p.side && "tf-side", p.surface && "tf-" + p.surface, p.className)}
194
+ style={p.style}
195
+ >
181
196
  {(p.title || p.action) && (
182
197
  <div className="tf-header">
183
198
  <div>
@@ -211,14 +226,39 @@ export function PageHeader(p: PageHeaderProps) {
211
226
  </header>
212
227
  );
213
228
  }
229
+ const useLayout = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
230
+
214
231
  export function Stagger(p: StaggerProps) {
215
232
  const st: Record<string, unknown> = { ...p.style };
216
233
  if (p.delay) st["--base"] = p.delay + "ms";
234
+ const ref = React.useRef<HTMLElement>(null);
235
+ // below the fold, the entrance waits until the group scrolls into view
236
+ const [waiting, setWaiting] = React.useState(false);
237
+ useLayout(() => {
238
+ const el = ref.current;
239
+ if (!el || p.reveal === false || typeof IntersectionObserver === "undefined" || reduced(el))
240
+ return;
241
+ if (el.getBoundingClientRect().top < window.innerHeight) return;
242
+ setWaiting(true);
243
+ const io = new IntersectionObserver(
244
+ (entries) => {
245
+ if (entries.some((e) => e.isIntersecting)) {
246
+ setWaiting(false);
247
+ io.disconnect();
248
+ }
249
+ },
250
+ { rootMargin: "0px 0px -8% 0px" },
251
+ );
252
+ io.observe(el);
253
+ return () => io.disconnect();
254
+ }, []);
217
255
  return React.createElement(
218
256
  p.as || "div",
219
257
  {
258
+ ref,
220
259
  className: cx("tf-stagger", p.className),
221
260
  style: st,
261
+ "data-reveal": waiting ? "waiting" : undefined,
222
262
  },
223
263
  p.children,
224
264
  );
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
+ import { leaving, usePresence } from "../presence.js";
3
4
  import { cx } from "../cx.js";
4
5
  import { Icon } from "../Icon.js";
5
6
  import { formatMoney, formatNumber, formatPercent } from "../format.js";
@@ -258,6 +259,7 @@ export function SettingsLayout(p: SettingsLayoutProps) {
258
259
  }
259
260
 
260
261
  export function ShortcutsOverlay(p: ShortcutsOverlayProps) {
262
+ const pr = usePresence(!!p.open);
261
263
  React.useEffect(() => {
262
264
  if (!p.open) return;
263
265
  function k(e: KeyboardEvent) {
@@ -268,10 +270,10 @@ export function ShortcutsOverlay(p: ShortcutsOverlayProps) {
268
270
  document.removeEventListener("keydown", k);
269
271
  };
270
272
  }, [p.open]);
271
- if (!p.open) return null;
273
+ if (!pr.mounted) return null;
272
274
  return (
273
275
  <Portal>
274
- <div className="tf-overlay">
276
+ <div {...leaving(pr.closing)} className="tf-overlay">
275
277
  <div className="tf-backdrop" onClick={p.onClose} />
276
278
  <div
277
279
  className="tf-modal tf-shortcuts"
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
+ import { leaving, usePresence } from "../presence.js";
3
4
  import { moveFocus, useId } from "../hooks.js";
4
5
  import { cx } from "../cx.js";
5
6
  import { Icon } from "../Icon.js";
@@ -178,8 +179,10 @@ export function Menu(p: MenuProps) {
178
179
  }
179
180
 
180
181
  export function Modal(p: ModalProps) {
182
+ const pr = usePresence(p.open ?? true);
183
+ if (!pr.mounted) return null;
181
184
  return (
182
- <div className="tf-modal-wrap" style={p.style}>
185
+ <div {...leaving(pr.closing)} className="tf-modal-wrap" style={p.style}>
183
186
  <div className="tf-backdrop" aria-hidden />
184
187
  <div className="tf-modal" role="dialog" aria-modal="true" aria-label={p.title}>
185
188
  <span className="tf-modal-close">
@@ -277,7 +280,8 @@ export function ConfirmDialog(p: ConfirmDialogProps) {
277
280
  const titleId = useId("confirm");
278
281
  const typed = React.useState(""),
279
282
  ok = !p.confirmText || typed[0] === p.confirmText,
280
- cancel = React.useRef<HTMLButtonElement>(null);
283
+ cancel = React.useRef<HTMLButtonElement>(null),
284
+ pr = usePresence(!!p.open);
281
285
  React.useEffect(() => {
282
286
  if (!p.open) return;
283
287
  typed[1]("");
@@ -292,10 +296,10 @@ export function ConfirmDialog(p: ConfirmDialogProps) {
292
296
  document.removeEventListener("keydown", k);
293
297
  };
294
298
  }, [p.open]);
295
- if (!p.open) return null;
299
+ if (!pr.mounted) return null;
296
300
  return (
297
301
  <Portal>
298
- <div className="tf-overlay">
302
+ <div {...leaving(pr.closing)} className="tf-overlay">
299
303
  <div className="tf-backdrop" onClick={p.onCancel} />
300
304
  <div
301
305
  className="tf-modal tf-confirm"
@@ -371,6 +375,7 @@ export function ToastItem(p: { t: ToastData }) {
371
375
  const tint = ALERT_TINT[t.tone] || "mint";
372
376
  return (
373
377
  <div
378
+ {...leaving(!!t.leaving)}
374
379
  className="tf-toast tf-toast-x"
375
380
  role={t.tone === "danger" ? "alert" : "status"}
376
381
  onPointerEnter={() => {
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
+ import { leaving, usePresence } from "../presence.js";
3
4
  import { cx } from "../cx.js";
4
5
  import { useFieldError } from "./form.js";
5
6
  import { formatDate, formatNumber, WEEK_START } from "../format.js";
@@ -235,6 +236,7 @@ export function Combobox(p: ComboboxProps) {
235
236
  act = React.useState(0),
236
237
  ref = React.useRef<HTMLDivElement>(null),
237
238
  inp = React.useRef<HTMLInputElement>(null);
239
+ const openPr = usePresence(!!open[0]);
238
240
  useOutside(ref, open[0], () => {
239
241
  open[1](false);
240
242
  q[1]("");
@@ -358,8 +360,9 @@ export function Combobox(p: ComboboxProps) {
358
360
  <Icon name="chevronDown" size={15} />
359
361
  </span>
360
362
  </div>
361
- {open[0] && (
363
+ {openPr.mounted && (
362
364
  <div
365
+ {...leaving(openPr.closing)}
363
366
  className="tf-popover tf-combo-pop"
364
367
  id={id + "-list"}
365
368
  role="listbox"
@@ -192,7 +192,7 @@ export {
192
192
  EnergyClass,
193
193
  } from "./real-estate.js";
194
194
  export { RichTextEditor } from "./rich-text-editor.js";
195
- export { TopBar, SideRail, Dock, NAV, AppShell } from "./shell.js";
195
+ export { TopBar, SideRail, Dock, FloatingLayout, Ambient, NAV, AppShell } from "./shell.js";
196
196
  export { ChatLauncher, CookieConsent, AnnouncementBar } from "./site.js";
197
197
  export { StatusShape, StatusIndicator } from "./status.js";
198
198
  export { StatusScreen, AuthCard } from "./system.js";
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
+ import { leaving, usePresence } from "../presence.js";
3
4
  import { useControlled, useId, useKeepInView, useOutside } from "../hooks.js";
4
5
  import { cx } from "../cx.js";
5
6
  import type { PopoverProps, TooltipProps } from "./types.js";
@@ -8,7 +9,8 @@ import type { PopoverProps, TooltipProps } from "./types.js";
8
9
  export function Popover(p: PopoverProps) {
9
10
  const ref = React.useRef<HTMLSpanElement>(null),
10
11
  pop = React.useRef<HTMLDivElement>(null),
11
- st = useControlled(p.open, false, p.onOpenChange);
12
+ st = useControlled(p.open, false, p.onOpenChange),
13
+ pr = usePresence(st[0]);
12
14
  useOutside(ref, st[0], () => {
13
15
  st[1](false);
14
16
  });
@@ -52,8 +54,9 @@ export function Popover(p: PopoverProps) {
52
54
  return (
53
55
  <span ref={ref} className={cx("tf-pop-anchor", p.block && "block", p.className)}>
54
56
  {trigger}
55
- {st[0] && (
57
+ {pr.mounted && (
56
58
  <div
59
+ {...leaving(pr.closing)}
57
60
  ref={pop}
58
61
  className={cx(
59
62
  "tf-popover",
@@ -88,7 +91,8 @@ export function Tooltip(p: TooltipProps) {
88
91
  st = React.useState(!!p.defaultOpen),
89
92
  t = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined),
90
93
  tip = React.useRef<HTMLSpanElement>(null),
91
- side = p.side || "top";
94
+ side = p.side || "top",
95
+ pr = usePresence(st[0]);
92
96
  // keep a top or bottom tooltip inside the screen (or the scrolling box around it)
93
97
  useKeepInView(tip, st[0] && side !== "left" && side !== "right");
94
98
  function show() {
@@ -118,8 +122,14 @@ export function Tooltip(p: TooltipProps) {
118
122
  {React.cloneElement(child, {
119
123
  "aria-describedby": st[0] ? id : undefined,
120
124
  })}
121
- {st[0] && (
122
- <span id={id} ref={tip} role="tooltip" className={cx("tf-tooltip", "tf-tip-" + side)}>
125
+ {pr.mounted && (
126
+ <span
127
+ {...leaving(pr.closing)}
128
+ id={id}
129
+ ref={tip}
130
+ role="tooltip"
131
+ className={cx("tf-tooltip", "tf-tip-" + side)}
132
+ >
123
133
  {p.label}
124
134
  {p.shortcut && <kbd>{p.shortcut}</kbd>}
125
135
  </span>
@@ -5,6 +5,7 @@
5
5
  * the WAI-ARIA authoring practices.
6
6
  */
7
7
  import * as React from "react";
8
+ import { leaving, usePresence } from "../presence.js";
8
9
  import { Icon } from "../Icon.js";
9
10
  import { cx } from "../cx.js";
10
11
  import { moveFocus, useControlled, useId, useKeepInView } from "../hooks.js";
@@ -230,6 +231,7 @@ export function HoverCard(p: HoverCardProps) {
230
231
  const [open, setOpen] = React.useState(false);
231
232
  const t = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
232
233
  const card = React.useRef<HTMLDivElement>(null);
234
+ const pr = usePresence(open);
233
235
  useKeepInView(card, open);
234
236
  const later = (v: boolean, ms: number) => {
235
237
  clearTimeout(t.current);
@@ -251,8 +253,9 @@ export function HoverCard(p: HoverCardProps) {
251
253
  }}
252
254
  >
253
255
  {p.trigger}
254
- {open && (
256
+ {pr.mounted && (
255
257
  <div
258
+ {...leaving(pr.closing)}
256
259
  ref={card}
257
260
  className={cx(
258
261
  "tf-hovercard",
@@ -1,12 +1,14 @@
1
1
  "use client";
2
2
  import * as React from "react";
3
3
  import { Icon } from "../Icon.js";
4
- import { cx } from "../cx.js";
4
+ import { cx, reduced } from "../cx.js";
5
5
  import { Avatar } from "./foundation.js";
6
6
  import type {
7
7
  AppShellProps,
8
8
  DockItem,
9
+ AmbientProps,
9
10
  DockProps,
11
+ FloatingLayoutProps,
10
12
  NavItem,
11
13
  RailItem,
12
14
  SideRailProps,
@@ -72,7 +74,7 @@ function railBtn(it: RailItem, i: number) {
72
74
  type="button"
73
75
  className="tf-rail-btn"
74
76
  style={{
75
- animationDelay: 200 + i * 50 + "ms",
77
+ animationDelay: `calc(${200 + i * 50}ms * var(--motion-time, 1))`,
76
78
  }}
77
79
  aria-label={it.count ? `${it.label}, ${it.count}` : it.label}
78
80
  title={it.label}
@@ -96,7 +98,12 @@ export function SideRail(p: SideRailProps) {
96
98
 
97
99
  export function Dock(p: DockProps) {
98
100
  return (
99
- <div className="tf-dock" role="toolbar" aria-label={p.label || "Quick actions"}>
101
+ <div
102
+ className={cx("tf-dock", p.surface && "tf-" + p.surface, p.className)}
103
+ style={p.style}
104
+ role="toolbar"
105
+ aria-label={p.label || "Quick actions"}
106
+ >
100
107
  {(p.items || []).map((it) => (
101
108
  <button
102
109
  key={it.label}
@@ -113,6 +120,98 @@ export function Dock(p: DockProps) {
113
120
  );
114
121
  }
115
122
 
123
+ /**
124
+ * UI floating over a full-bleed background (a map, a 3D scene, a video): four slots along the
125
+ * edges, glass by default. The background stays interactive wherever no slot sits on it.
126
+ */
127
+ export function FloatingLayout(p: FloatingLayoutProps) {
128
+ const surface = p.surface ?? "glass";
129
+ return (
130
+ <div
131
+ className={cx("tf-floating", p.className)}
132
+ style={{ minHeight: p.height ?? 640, ...p.style }}
133
+ role="region"
134
+ aria-label={p.label}
135
+ data-surface={surface}
136
+ >
137
+ {p.background != null && <div className="tf-floating-bg">{p.background}</div>}
138
+ <div className="tf-floating-ui">
139
+ {p.top != null && <div className="tf-floating-top">{p.top}</div>}
140
+ {p.start != null && <div className="tf-floating-start">{p.start}</div>}
141
+ {p.end != null && <div className="tf-floating-end">{p.end}</div>}
142
+ {p.bottom != null && <div className="tf-floating-bottom">{p.bottom}</div>}
143
+ </div>
144
+ {p.children}
145
+ </div>
146
+ );
147
+ }
148
+
149
+ /**
150
+ * A calm, living background for glass UI: soft colour fields from the kit that drift slowly, or a
151
+ * photo or video under a scrim, or your own layer. Sits behind its container's content.
152
+ */
153
+ export function Ambient(p: AmbientProps) {
154
+ const ref = React.useRef<HTMLDivElement>(null);
155
+ const [paused, setPaused] = React.useState(false);
156
+ const animate = p.animate !== false && !p.src && !p.children;
157
+ React.useEffect(() => {
158
+ const el = ref.current;
159
+ if (!el || !animate || typeof IntersectionObserver === "undefined") return;
160
+ const io = new IntersectionObserver((e) => setPaused(!e.some((x) => x.isIntersecting)));
161
+ io.observe(el);
162
+ return () => io.disconnect();
163
+ }, [animate]);
164
+ const vid = React.useRef<HTMLVideoElement>(null);
165
+ React.useEffect(() => {
166
+ const v = vid.current;
167
+ if (v && reduced(v)) v.pause();
168
+ }, [p.src]);
169
+ const colors = p.colors?.length
170
+ ? p.colors
171
+ : ["var(--ambient-1)", "var(--ambient-2)", "var(--ambient-3)", "var(--ambient-4)"];
172
+ const media = !!p.src && !p.children;
173
+ return (
174
+ <div
175
+ ref={ref}
176
+ aria-hidden
177
+ className={cx(
178
+ "tf-ambient",
179
+ p.fixed && "fixed",
180
+ media && "media",
181
+ !animate && "still",
182
+ p.className,
183
+ )}
184
+ style={p.style}
185
+ data-paused={paused ? "" : undefined}
186
+ >
187
+ {p.children ??
188
+ (media ? (
189
+ p.video ? (
190
+ <video
191
+ ref={vid}
192
+ className="tf-ambient-media"
193
+ src={p.src}
194
+ muted
195
+ loop
196
+ autoPlay
197
+ playsInline
198
+ />
199
+ ) : (
200
+ <img className="tf-ambient-media" src={p.src} alt="" />
201
+ )
202
+ ) : (
203
+ colors.map((c, i) => (
204
+ <span
205
+ key={i}
206
+ className="tf-ambient-field"
207
+ style={{ "--field": c } as React.CSSProperties}
208
+ />
209
+ ))
210
+ ))}
211
+ </div>
212
+ );
213
+ }
214
+
116
215
  /** Example navigation shown when `nav` is not given. */
117
216
  export const NAV = (): NavItem[] => [
118
217
  {
@@ -215,7 +314,7 @@ const DOCK = (): DockItem[] => [
215
314
 
216
315
  export function AppShell(p: AppShellProps) {
217
316
  return (
218
- <div className="tf-shell" style={p.style}>
317
+ <div className={cx("tf-shell", p.className)} style={p.style}>
219
318
  <TopBar
220
319
  brand={p.brand}
221
320
  tag={p.tag}