@twentyfirstmedia/ui-core 0.1.1 → 0.2.1
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.
- package/dist/Icon.d.ts +1 -1
- package/dist/components/admin.d.ts.map +1 -1
- package/dist/components/admin.js +2 -2
- package/dist/components/admin.js.map +1 -1
- package/dist/components/charts.js +4 -4
- package/dist/components/charts.js.map +1 -1
- package/dist/components/data-display.d.ts +2 -0
- package/dist/components/data-display.d.ts.map +1 -1
- package/dist/components/data-display.js +32 -8
- package/dist/components/data-display.js.map +1 -1
- package/dist/components/developer.d.ts.map +1 -1
- package/dist/components/developer.js +4 -2
- package/dist/components/developer.js.map +1 -1
- package/dist/components/feedback.d.ts +1 -1
- package/dist/components/feedback.d.ts.map +1 -1
- package/dist/components/feedback.js +9 -5
- package/dist/components/feedback.js.map +1 -1
- package/dist/components/forms.d.ts.map +1 -1
- package/dist/components/forms.js +3 -1
- package/dist/components/forms.js.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/overlays.d.ts.map +1 -1
- package/dist/components/overlays.js +5 -4
- package/dist/components/overlays.js.map +1 -1
- package/dist/components/primitives.d.ts.map +1 -1
- package/dist/components/primitives.js +3 -1
- package/dist/components/primitives.js.map +1 -1
- package/dist/components/shell.d.ts +11 -1
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +41 -4
- package/dist/components/shell.js.map +1 -1
- package/dist/components/types.d.ts +54 -2
- package/dist/components/types.d.ts.map +1 -1
- package/dist/components/workspace.d.ts +2 -2
- package/dist/components/workspace.d.ts.map +1 -1
- package/dist/components/workspace.js +9 -3
- package/dist/components/workspace.js.map +1 -1
- package/dist/cx.d.ts +7 -2
- package/dist/cx.d.ts.map +1 -1
- package/dist/cx.js +25 -3
- package/dist/cx.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/motion.d.ts +13 -0
- package/dist/motion.d.ts.map +1 -0
- package/dist/motion.js +44 -0
- package/dist/motion.js.map +1 -0
- package/dist/presence.d.ts +18 -0
- package/dist/presence.d.ts.map +1 -0
- package/dist/presence.js +49 -0
- package/dist/presence.js.map +1 -0
- package/dist/provider.d.ts +22 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +18 -4
- package/dist/provider.js.map +1 -1
- package/dist/themes.css +1821 -102
- package/dist/toast-store.d.ts +2 -0
- package/dist/toast-store.d.ts.map +1 -1
- package/dist/toast-store.js +28 -1
- package/dist/toast-store.js.map +1 -1
- package/package.json +1 -1
- package/src/components/admin.tsx +7 -3
- package/src/components/charts.tsx +4 -4
- package/src/components/data-display.tsx +48 -8
- package/src/components/developer.tsx +4 -2
- package/src/components/feedback.tsx +9 -4
- package/src/components/forms.tsx +4 -1
- package/src/components/index.ts +1 -1
- package/src/components/overlays.tsx +15 -5
- package/src/components/primitives.tsx +4 -1
- package/src/components/shell.tsx +103 -4
- package/src/components/types.ts +54 -2
- package/src/components/workspace.tsx +8 -1
- package/src/cx.ts +22 -5
- package/src/index.ts +3 -0
- package/src/motion.ts +48 -0
- package/src/presence.ts +58 -0
- package/src/provider.tsx +44 -5
- package/src/toast-store.ts +28 -1
package/dist/toast-store.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/toast-store.js
CHANGED
|
@@ -15,9 +15,36 @@ export function toast(o) {
|
|
|
15
15
|
return t.id;
|
|
16
16
|
}
|
|
17
17
|
toast.dismiss = (id) => {
|
|
18
|
-
|
|
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();
|
package/dist/toast-store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toast-store.js","sourceRoot":"","sources":["../src/toast-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"toast-store.js","sourceRoot":"","sources":["../src/toast-store.ts"],"names":[],"mappings":"AAsBA,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,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC;IACpB,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;QACZ,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACvC,IAAI,EAAE,CAAC;QACP,OAAO;IACT,CAAC;IACD,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpE,IAAI,EAAE,CAAC;IACP,UAAU,CAAC,GAAG,EAAE;QACd,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACvC,IAAI,EAAE,CAAC;IACT,CAAC,EAAE,EAAE,CAAC,CAAC;AACT,CAAC,CAAC;AAEF,0DAA0D;AAC1D,SAAS,MAAM;IACb,IAAI,CAAC;QACH,IAAI,OAAO,MAAM,KAAK,WAAW;YAAE,OAAO,CAAC,CAAC;QAC5C,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC,kCAAkC,CAAC,CAAC,OAAO;YAAE,OAAO,CAAC,CAAC;QAC9E,IAAI,QAAQ,CAAC,eAAe,CAAC,OAAO,CAAC,eAAe,CAAC,EAAE,YAAY,CAAC,aAAa,CAAC,KAAK,KAAK;YAC1F,OAAO,CAAC,CAAC;QACX,MAAM,GAAG,GAAG,gBAAgB,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,gBAAgB,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7F,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;QAC1B,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC;AACD,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"}
|
package/package.json
CHANGED
package/src/components/admin.tsx
CHANGED
|
@@ -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 &&
|
|
559
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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={() => {
|
package/src/components/forms.tsx
CHANGED
|
@@ -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
|
-
{
|
|
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"
|
package/src/components/index.ts
CHANGED
|
@@ -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
|
-
{
|
|
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
|
-
{
|
|
122
|
-
<span
|
|
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
|
-
{
|
|
256
|
+
{pr.mounted && (
|
|
255
257
|
<div
|
|
258
|
+
{...leaving(pr.closing)}
|
|
256
259
|
ref={card}
|
|
257
260
|
className={cx(
|
|
258
261
|
"tf-hovercard",
|
package/src/components/shell.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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}
|