@adecore/ui 0.54.11 → 0.54.13

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.
@@ -0,0 +1,7 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface EmptySceneProps {
3
+ compact?: boolean;
4
+ children: ReactNode;
5
+ className?: string;
6
+ }
7
+ export declare function EmptyScene({ compact, children, className }: EmptySceneProps): import("react").JSX.Element;
@@ -0,0 +1,96 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import clsx from 'clsx';
3
+ import { useId } from 'react';
4
+ /* The drawing an `EmptyState` shows through `scene`. It only decorates, so it stays out of the accessibility tree. */
5
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
6
+ export function EmptyScene(t0) {
7
+ const $ = _c(14);
8
+ const {
9
+ compact: t1,
10
+ children,
11
+ className
12
+ } = t0;
13
+ const compact = t1 === undefined ? false : t1;
14
+ const gradient = useId();
15
+ const t2 = `url("#${gradient}")`;
16
+ let t3;
17
+ if ($[0] !== t2) {
18
+ t3 = {
19
+ "--scene-fill": t2
20
+ };
21
+ $[0] = t2;
22
+ $[1] = t3;
23
+ } else {
24
+ t3 = $[1];
25
+ }
26
+ const style = t3;
27
+ const t4 = compact && "scene-compact";
28
+ let t5;
29
+ if ($[2] !== className || $[3] !== t4) {
30
+ t5 = clsx("scene", t4, className);
31
+ $[2] = className;
32
+ $[3] = t4;
33
+ $[4] = t5;
34
+ } else {
35
+ t5 = $[4];
36
+ }
37
+ let t6;
38
+ if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
39
+ t6 = /*#__PURE__*/_jsx("stop", {
40
+ offset: "0",
41
+ style: {
42
+ stopColor: "var(--scene-top)"
43
+ }
44
+ });
45
+ $[5] = t6;
46
+ } else {
47
+ t6 = $[5];
48
+ }
49
+ let t7;
50
+ if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
51
+ t7 = /*#__PURE__*/_jsx("stop", {
52
+ offset: "1",
53
+ style: {
54
+ stopColor: "var(--scene-bottom)"
55
+ }
56
+ });
57
+ $[6] = t7;
58
+ } else {
59
+ t7 = $[6];
60
+ }
61
+ let t8;
62
+ if ($[7] !== gradient) {
63
+ t8 = /*#__PURE__*/_jsx("defs", {
64
+ children: /*#__PURE__*/_jsxs("linearGradient", {
65
+ id: gradient,
66
+ x1: "0",
67
+ y1: "0",
68
+ x2: "0",
69
+ y2: "1",
70
+ children: [t6, t7]
71
+ })
72
+ });
73
+ $[7] = gradient;
74
+ $[8] = t8;
75
+ } else {
76
+ t8 = $[8];
77
+ }
78
+ let t9;
79
+ if ($[9] !== children || $[10] !== style || $[11] !== t5 || $[12] !== t8) {
80
+ t9 = /*#__PURE__*/_jsxs("svg", {
81
+ className: t5,
82
+ viewBox: "0 0 160 104",
83
+ style: style,
84
+ "aria-hidden": "true",
85
+ children: [t8, children]
86
+ });
87
+ $[9] = children;
88
+ $[10] = style;
89
+ $[11] = t5;
90
+ $[12] = t8;
91
+ $[13] = t9;
92
+ } else {
93
+ t9 = $[13];
94
+ }
95
+ return t9;
96
+ }
@@ -4,6 +4,7 @@ export interface EmptyStateProps {
4
4
  icon?: LucideIcon | ReactElement<{
5
5
  size?: number;
6
6
  }>;
7
+ scene?: ReactNode;
7
8
  busy?: boolean;
8
9
  title?: ReactNode;
9
10
  children: ReactNode;
@@ -11,4 +12,4 @@ export interface EmptyStateProps {
11
12
  className?: string;
12
13
  ref?: Ref<HTMLDivElement>;
13
14
  }
14
- export declare function EmptyState({ icon, busy, title, children, action, className, ref }: EmptyStateProps): import("react").JSX.Element;
15
+ export declare function EmptyState({ icon, scene, busy, title, children, action, className, ref }: EmptyStateProps): import("react").JSX.Element;
@@ -7,9 +7,10 @@ import { Spinner } from "./Spinner.js";
7
7
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  const ICON_SIZE = 20;
9
9
  export function EmptyState(t0) {
10
- const $ = _c(18);
10
+ const $ = _c(24);
11
11
  const {
12
12
  icon,
13
+ scene,
13
14
  busy: t1,
14
15
  title,
15
16
  children,
@@ -27,8 +28,20 @@ export function EmptyState(t0) {
27
28
  t2 = $[1];
28
29
  }
29
30
  let t3;
30
- if ($[2] !== busy || $[3] !== icon) {
31
- t3 = (busy || icon) && /*#__PURE__*/_jsx("span", {
31
+ if ($[2] !== busy || $[3] !== scene) {
32
+ t3 = scene && !busy && /*#__PURE__*/_jsx("div", {
33
+ className: "mb-2",
34
+ children: scene
35
+ });
36
+ $[2] = busy;
37
+ $[3] = scene;
38
+ $[4] = t3;
39
+ } else {
40
+ t3 = $[4];
41
+ }
42
+ let t4;
43
+ if ($[5] !== busy || $[6] !== icon || $[7] !== scene) {
44
+ t4 = (busy || icon && !scene) && /*#__PURE__*/_jsx("span", {
32
45
  className: "text-text-faint",
33
46
  children: busy ? /*#__PURE__*/_jsx(Spinner, {
34
47
  size: ICON_SIZE
@@ -39,61 +52,64 @@ export function EmptyState(t0) {
39
52
  size: ICON_SIZE
40
53
  })
41
54
  });
42
- $[2] = busy;
43
- $[3] = icon;
44
- $[4] = t3;
55
+ $[5] = busy;
56
+ $[6] = icon;
57
+ $[7] = scene;
58
+ $[8] = t4;
45
59
  } else {
46
- t3 = $[4];
60
+ t4 = $[8];
47
61
  }
48
- let t4;
49
- if ($[5] !== title) {
50
- t4 = title && /*#__PURE__*/_jsx("p", {
51
- className: "text-sm font-medium text-text",
62
+ let t5;
63
+ if ($[9] !== scene || $[10] !== title) {
64
+ t5 = title && /*#__PURE__*/_jsx("p", {
65
+ className: clsx("text-text", scene ? "text-base font-semibold" : "text-sm font-medium"),
52
66
  children: title
53
67
  });
54
- $[5] = title;
55
- $[6] = t4;
68
+ $[9] = scene;
69
+ $[10] = title;
70
+ $[11] = t5;
56
71
  } else {
57
- t4 = $[6];
72
+ t5 = $[11];
58
73
  }
59
- let t5;
60
- if ($[7] !== children) {
61
- t5 = /*#__PURE__*/_jsx("p", {
74
+ let t6;
75
+ if ($[12] !== children) {
76
+ t6 = /*#__PURE__*/_jsx("p", {
62
77
  className: "max-w-[280px] text-xs leading-snug text-text-muted",
63
78
  children: children
64
79
  });
65
- $[7] = children;
66
- $[8] = t5;
80
+ $[12] = children;
81
+ $[13] = t6;
67
82
  } else {
68
- t5 = $[8];
83
+ t6 = $[13];
69
84
  }
70
- let t6;
71
- if ($[9] !== action) {
72
- t6 = action && /*#__PURE__*/_jsx("div", {
85
+ let t7;
86
+ if ($[14] !== action) {
87
+ t7 = action && /*#__PURE__*/_jsx("div", {
73
88
  className: "mt-2",
74
89
  children: action
75
90
  });
76
- $[9] = action;
77
- $[10] = t6;
91
+ $[14] = action;
92
+ $[15] = t7;
78
93
  } else {
79
- t6 = $[10];
94
+ t7 = $[15];
80
95
  }
81
- let t7;
82
- if ($[11] !== ref || $[12] !== t2 || $[13] !== t3 || $[14] !== t4 || $[15] !== t5 || $[16] !== t6) {
83
- t7 = /*#__PURE__*/_jsxs("div", {
96
+ let t8;
97
+ if ($[16] !== ref || $[17] !== t2 || $[18] !== t3 || $[19] !== t4 || $[20] !== t5 || $[21] !== t6 || $[22] !== t7) {
98
+ t8 = /*#__PURE__*/_jsxs("div", {
84
99
  ref: ref,
85
100
  className: t2,
86
- children: [t3, t4, t5, t6]
101
+ children: [t3, t4, t5, t6, t7]
87
102
  });
88
- $[11] = ref;
89
- $[12] = t2;
90
- $[13] = t3;
91
- $[14] = t4;
92
- $[15] = t5;
93
- $[16] = t6;
94
- $[17] = t7;
103
+ $[16] = ref;
104
+ $[17] = t2;
105
+ $[18] = t3;
106
+ $[19] = t4;
107
+ $[20] = t5;
108
+ $[21] = t6;
109
+ $[22] = t7;
110
+ $[23] = t8;
95
111
  } else {
96
- t7 = $[17];
112
+ t8 = $[23];
97
113
  }
98
- return t7;
114
+ return t8;
99
115
  }
@@ -3,6 +3,8 @@ import type { ReactNode, Ref } from 'react';
3
3
  export interface PanelEmptyProps {
4
4
  header?: ReactNode;
5
5
  icon?: LucideIcon;
6
+ scene?: ReactNode;
7
+ title?: ReactNode;
6
8
  busy?: boolean;
7
9
  action?: ReactNode;
8
10
  sunken?: boolean;
@@ -11,4 +13,4 @@ export interface PanelEmptyProps {
11
13
  className?: string;
12
14
  ref?: Ref<HTMLDivElement>;
13
15
  }
14
- export declare function PanelEmpty({ header, icon, busy, action, sunken, fill, children, className, ref }: PanelEmptyProps): import("react").JSX.Element;
16
+ export declare function PanelEmpty({ header, icon, scene, title, busy, action, sunken, fill, children, className, ref }: PanelEmptyProps): import("react").JSX.Element;
@@ -3,10 +3,12 @@ import clsx from 'clsx';
3
3
  import { EmptyState } from "./EmptyState.js";
4
4
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  export function PanelEmpty(t0) {
6
- const $ = _c(14);
6
+ const $ = _c(16);
7
7
  const {
8
8
  header,
9
9
  icon,
10
+ scene,
11
+ title,
10
12
  busy: t1,
11
13
  action,
12
14
  sunken: t2,
@@ -31,9 +33,11 @@ export function PanelEmpty(t0) {
31
33
  t6 = $[3];
32
34
  }
33
35
  let t7;
34
- if ($[4] !== action || $[5] !== busy || $[6] !== children || $[7] !== icon) {
36
+ if ($[4] !== action || $[5] !== busy || $[6] !== children || $[7] !== icon || $[8] !== scene || $[9] !== title) {
35
37
  t7 = /*#__PURE__*/_jsx(EmptyState, {
36
38
  icon: icon,
39
+ scene: scene,
40
+ title: title,
37
41
  busy: busy,
38
42
  action: action,
39
43
  children: children
@@ -42,24 +46,26 @@ export function PanelEmpty(t0) {
42
46
  $[5] = busy;
43
47
  $[6] = children;
44
48
  $[7] = icon;
45
- $[8] = t7;
49
+ $[8] = scene;
50
+ $[9] = title;
51
+ $[10] = t7;
46
52
  } else {
47
- t7 = $[8];
53
+ t7 = $[10];
48
54
  }
49
55
  let t8;
50
- if ($[9] !== header || $[10] !== ref || $[11] !== t6 || $[12] !== t7) {
56
+ if ($[11] !== header || $[12] !== ref || $[13] !== t6 || $[14] !== t7) {
51
57
  t8 = /*#__PURE__*/_jsxs("div", {
52
58
  ref: ref,
53
59
  className: t6,
54
60
  children: [header, t7]
55
61
  });
56
- $[9] = header;
57
- $[10] = ref;
58
- $[11] = t6;
59
- $[12] = t7;
60
- $[13] = t8;
62
+ $[11] = header;
63
+ $[12] = ref;
64
+ $[13] = t6;
65
+ $[14] = t7;
66
+ $[15] = t8;
61
67
  } else {
62
- t8 = $[13];
68
+ t8 = $[15];
63
69
  }
64
70
  return t8;
65
71
  }
package/dist/index.d.ts CHANGED
@@ -30,6 +30,7 @@ export { ColorWheelPicker, type ColorWheelPickerProps } from './ColorWheelPicker
30
30
  export { ColumnResizeHandle, type ColumnResizeHandleProps } from './ColumnResizeHandle.tsx';
31
31
  export { DisabledReason, type DisabledReasonProps } from './DisabledReason.tsx';
32
32
  export { DockShell, type DockShellProps } from './DockShell.tsx';
33
+ export { EmptyScene, type EmptySceneProps } from './EmptyScene.tsx';
33
34
  export { EmptyState, type EmptyStateProps } from './EmptyState.tsx';
34
35
  export { ErrorBoundary, type ErrorBoundaryProps } from './ErrorBoundary.tsx';
35
36
  export { Field, FieldHint, FormError, type FieldHintProps, type FieldProps, type FormErrorProps } from './Field.tsx';
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ export { ColorWheelPicker } from "./ColorWheelPicker.js";
27
27
  export { ColumnResizeHandle } from "./ColumnResizeHandle.js";
28
28
  export { DisabledReason } from "./DisabledReason.js";
29
29
  export { DockShell } from "./DockShell.js";
30
+ export { EmptyScene } from "./EmptyScene.js";
30
31
  export { EmptyState } from "./EmptyState.js";
31
32
  export { ErrorBoundary } from "./ErrorBoundary.js";
32
33
  export { Field, FieldHint, FormError } from "./Field.js";
@@ -19,7 +19,7 @@ type ItemLook = {
19
19
  unstyled?: boolean;
20
20
  };
21
21
  type MenuItemProps = ComponentProps<typeof BaseMenu.Item> & ItemLook;
22
- export declare function MenuItem({ unstyled, className, ...props }: MenuItemProps): import("react").JSX.Element;
22
+ export declare function MenuItem({ unstyled, className, children, ...props }: MenuItemProps): import("react").JSX.Element;
23
23
  export interface MenuCheckProps {
24
24
  kind: 'radio' | 'checkbox';
25
25
  checked?: boolean;
@@ -159,8 +159,33 @@ export function MenuPopup(t0) {
159
159
  }
160
160
  return t8;
161
161
  }
162
+ /*
163
+ * Typing in an open menu moves to the first row whose text starts with the letters, read from the
164
+ * row's `label` or else its rendered text. The space JSX keeps between an icon and the name
165
+ * (`<Icon /> {name}`) draws nothing in a row laid out with a gap, but it would start that text and
166
+ * keep the row from ever matching. A row whose text starts with something other than its name,
167
+ * such as initials before it, sets `label`.
168
+ */
169
+ function withoutLeadingSpace(children) {
170
+ if (typeof children === 'string') {
171
+ return children.trimStart();
172
+ }
173
+ if (!Array.isArray(children)) {
174
+ return children;
175
+ }
176
+ let reached = false;
177
+ return children.map(child => {
178
+ if (reached || typeof child !== 'string') {
179
+ return child;
180
+ }
181
+ const trimmed = child.trimStart();
182
+ reached = trimmed !== '';
183
+ return trimmed;
184
+ });
185
+ }
162
186
  export function MenuItem(t0) {
163
- const $ = _c(10);
187
+ const $ = _c(15);
188
+ let children;
164
189
  let className;
165
190
  let props;
166
191
  let t1;
@@ -168,40 +193,54 @@ export function MenuItem(t0) {
168
193
  ({
169
194
  unstyled: t1,
170
195
  className,
196
+ children,
171
197
  ...props
172
198
  } = t0);
173
199
  $[0] = t0;
174
- $[1] = className;
175
- $[2] = props;
176
- $[3] = t1;
200
+ $[1] = children;
201
+ $[2] = className;
202
+ $[3] = props;
203
+ $[4] = t1;
177
204
  } else {
178
- className = $[1];
179
- props = $[2];
180
- t1 = $[3];
205
+ children = $[1];
206
+ className = $[2];
207
+ props = $[3];
208
+ t1 = $[4];
181
209
  }
182
210
  const unstyled = t1 === undefined ? false : t1;
183
211
  let t2;
184
- if ($[4] !== className || $[5] !== unstyled) {
212
+ if ($[5] !== className || $[6] !== unstyled) {
185
213
  t2 = unstyled ? className : withClass("menu-item", className);
186
- $[4] = className;
187
- $[5] = unstyled;
188
- $[6] = t2;
214
+ $[5] = className;
215
+ $[6] = unstyled;
216
+ $[7] = t2;
189
217
  } else {
190
- t2 = $[6];
218
+ t2 = $[7];
191
219
  }
192
220
  let t3;
193
- if ($[7] !== props || $[8] !== t2) {
194
- t3 = /*#__PURE__*/_jsx(BaseMenu.Item, {
221
+ if ($[8] !== children || $[9] !== unstyled) {
222
+ t3 = unstyled ? children : withoutLeadingSpace(children);
223
+ $[8] = children;
224
+ $[9] = unstyled;
225
+ $[10] = t3;
226
+ } else {
227
+ t3 = $[10];
228
+ }
229
+ let t4;
230
+ if ($[11] !== props || $[12] !== t2 || $[13] !== t3) {
231
+ t4 = /*#__PURE__*/_jsx(BaseMenu.Item, {
195
232
  className: t2,
196
- ...props
233
+ ...props,
234
+ children: t3
197
235
  });
198
- $[7] = props;
199
- $[8] = t2;
200
- $[9] = t3;
236
+ $[11] = props;
237
+ $[12] = t2;
238
+ $[13] = t3;
239
+ $[14] = t4;
201
240
  } else {
202
- t3 = $[9];
241
+ t4 = $[14];
203
242
  }
204
- return t3;
243
+ return t4;
205
244
  }
206
245
  /* One line of the row tall, so a row with a second line keeps the tick beside its first. */
207
246
  export function MenuCheck(t0) {
@@ -281,7 +320,7 @@ export function MenuCheck(t0) {
281
320
  const checkRow = (kind, unstyled, indicator, children) => unstyled ? children : /*#__PURE__*/_jsxs(_Fragment, {
282
321
  children: [indicator === 'start' && /*#__PURE__*/_jsx(MenuCheck, {
283
322
  kind: kind
284
- }), children, indicator === 'end' && /*#__PURE__*/_jsx(MenuCheck, {
323
+ }), withoutLeadingSpace(children), indicator === 'end' && /*#__PURE__*/_jsx(MenuCheck, {
285
324
  kind: kind,
286
325
  className: "ml-auto"
287
326
  })]
@@ -418,7 +457,7 @@ export function MenuRadioItem(t0) {
418
457
  return t5;
419
458
  }
420
459
  export function MenuSubmenuTrigger(t0) {
421
- const $ = _c(14);
460
+ const $ = _c(16);
422
461
  let children;
423
462
  let className;
424
463
  let props;
@@ -451,33 +490,41 @@ export function MenuSubmenuTrigger(t0) {
451
490
  t2 = $[6];
452
491
  }
453
492
  let t3;
454
- if ($[7] !== chevron) {
455
- t3 = chevron && /*#__PURE__*/_jsx(Icon, {
493
+ if ($[7] !== children) {
494
+ t3 = withoutLeadingSpace(children);
495
+ $[7] = children;
496
+ $[8] = t3;
497
+ } else {
498
+ t3 = $[8];
499
+ }
500
+ let t4;
501
+ if ($[9] !== chevron) {
502
+ t4 = chevron && /*#__PURE__*/_jsx(Icon, {
456
503
  icon: ChevronRight,
457
504
  size: 14,
458
505
  className: "ml-auto text-text-faint"
459
506
  });
460
- $[7] = chevron;
461
- $[8] = t3;
507
+ $[9] = chevron;
508
+ $[10] = t4;
462
509
  } else {
463
- t3 = $[8];
510
+ t4 = $[10];
464
511
  }
465
- let t4;
466
- if ($[9] !== children || $[10] !== props || $[11] !== t2 || $[12] !== t3) {
467
- t4 = /*#__PURE__*/_jsxs(BaseMenu.SubmenuTrigger, {
512
+ let t5;
513
+ if ($[11] !== props || $[12] !== t2 || $[13] !== t3 || $[14] !== t4) {
514
+ t5 = /*#__PURE__*/_jsxs(BaseMenu.SubmenuTrigger, {
468
515
  className: t2,
469
516
  ...props,
470
- children: [children, t3]
517
+ children: [t3, t4]
471
518
  });
472
- $[9] = children;
473
- $[10] = props;
474
- $[11] = t2;
475
- $[12] = t3;
476
- $[13] = t4;
519
+ $[11] = props;
520
+ $[12] = t2;
521
+ $[13] = t3;
522
+ $[14] = t4;
523
+ $[15] = t5;
477
524
  } else {
478
- t4 = $[13];
525
+ t5 = $[15];
479
526
  }
480
- return t4;
527
+ return t5;
481
528
  }
482
529
  /* Its first item is the row, every `RowAction` and `RowSubmenuTrigger` after it an item of its own, so the arrow keys reach each. */
483
530
  export function MenuRow(t0) {
package/dist/theme.css CHANGED
@@ -115,6 +115,16 @@
115
115
  --file-icon-purple: #a631be;
116
116
  --file-icon-pink: #d32a61;
117
117
  --file-icon-mauve: #594c5b;
118
+ /* `EmptyScene`: what a drawing of an empty place is made of. */
119
+ --scene-top: #ffffff;
120
+ --scene-bottom: #ececf1;
121
+ --scene-rim: rgb(0 0 0 / 0.09);
122
+ --scene-line: rgb(0 0 0 / 0.12);
123
+ --scene-inset: rgb(0 0 0 / 0.06);
124
+ --scene-text: rgb(0 0 0 / 0.14);
125
+ --scene-lift: drop-shadow(0 1px 1px rgb(0 0 0 / 0.08)) drop-shadow(0 5px 10px rgb(0 0 0 / 0.08));
126
+ /* What lands in a scene overshoots a little and settles. */
127
+ --scene-arrive: cubic-bezier(0.34, 1.5, 0.55, 1);
118
128
  }
119
129
 
120
130
  /* The theme attribute sits on `<html>`, the element the light rule above lands on too, so a token
@@ -191,6 +201,13 @@
191
201
  --file-icon-purple: #d568ea;
192
202
  --file-icon-pink: #ff678d;
193
203
  --file-icon-mauve: #79697b;
204
+ --scene-top: #2c2c32;
205
+ --scene-bottom: #1a1a1f;
206
+ --scene-rim: rgb(255 255 255 / 0.08);
207
+ --scene-line: rgb(255 255 255 / 0.1);
208
+ --scene-inset: rgb(0 0 0 / 0.3);
209
+ --scene-text: rgb(255 255 255 / 0.16);
210
+ --scene-lift: drop-shadow(0 1px 0 rgb(0 0 0 / 0.4)) drop-shadow(0 6px 10px rgb(0 0 0 / 0.35));
194
211
  }
195
212
 
196
213
  /* Code a person reads as code (an editor, a grid of values, a statement) at one size the app may set on
@@ -1178,3 +1195,93 @@
1178
1195
  .ade-scrollbar, .ade-scrollbar::before, .ade-scrollbar-thumb::before { transition: none; }
1179
1196
  }
1180
1197
  }
1198
+
1199
+ /* The drawing of an empty place (`EmptyScene`): shapes in a soft gradient with a thin rim and one detail
1200
+ in the accent color, on the same 8 second loop. Each rule below only animates: at rest, and under
1201
+ reduced motion, a drawing shows the end of what it plays. */
1202
+ .scene { flex: none; width: 160px; height: 104px; overflow: visible; }
1203
+ .scene-compact { width: 136px; height: 88px; }
1204
+ .scene-shape { fill: var(--scene-fill); stroke: var(--scene-rim); stroke-width: 1; stroke-linejoin: round; }
1205
+ .scene-edge { fill: none; stroke: var(--scene-line); stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; }
1206
+ .scene-inset { fill: var(--scene-inset); }
1207
+ .scene-line { fill: none; stroke: var(--text-faint); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
1208
+ .scene-text { fill: none; stroke: var(--scene-text); stroke-width: 4px; stroke-linecap: round; }
1209
+ .scene-hand { fill: none; stroke: var(--text-muted); stroke-width: 2.5; stroke-linecap: round; }
1210
+ .scene-dot { fill: var(--text-faint); }
1211
+ .scene-accent { fill: var(--accent); }
1212
+ .scene-accent-line { fill: none; stroke: var(--accent); stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
1213
+ .scene-lift { filter: var(--scene-lift); }
1214
+ .scene-cursor { fill: var(--text); stroke: var(--surface); stroke-width: 1; stroke-linejoin: round; }
1215
+ .scene [class*="scene-reveal-"] { transform-box: fill-box; transform-origin: center; }
1216
+ .scene [class*="scene-stroke-"], .scene [class*="scene-type-"] { stroke-dasharray: 1; }
1217
+
1218
+ @keyframes scene-loop { 0% { opacity: 0; } 3%, 88% { opacity: 1; } 95%, 100% { opacity: 0; } }
1219
+ @keyframes scene-reveal-0 { 0%, 3% { opacity: 0; transform: translateY(5px) scale(0.94); } 9%, 100% { opacity: 1; transform: none; } }
1220
+ @keyframes scene-stroke-0 { 0%, 3% { stroke-dashoffset: 1; } 10%, 100% { stroke-dashoffset: 0; } }
1221
+ @keyframes scene-reveal-1 { 0%, 9% { opacity: 0; transform: translateY(5px) scale(0.94); } 15%, 100% { opacity: 1; transform: none; } }
1222
+ @keyframes scene-stroke-1 { 0%, 9% { stroke-dashoffset: 1; } 16%, 100% { stroke-dashoffset: 0; } }
1223
+ @keyframes scene-reveal-2 { 0%, 15% { opacity: 0; transform: translateY(5px) scale(0.94); } 21%, 100% { opacity: 1; transform: none; } }
1224
+ @keyframes scene-stroke-2 { 0%, 15% { stroke-dashoffset: 1; } 22%, 100% { stroke-dashoffset: 0; } }
1225
+ @keyframes scene-reveal-3 { 0%, 21% { opacity: 0; transform: translateY(5px) scale(0.94); } 27%, 100% { opacity: 1; transform: none; } }
1226
+ @keyframes scene-stroke-3 { 0%, 21% { stroke-dashoffset: 1; } 28%, 100% { stroke-dashoffset: 0; } }
1227
+ @keyframes scene-reveal-4 { 0%, 27% { opacity: 0; transform: translateY(5px) scale(0.94); } 33%, 100% { opacity: 1; transform: none; } }
1228
+ @keyframes scene-stroke-4 { 0%, 27% { stroke-dashoffset: 1; } 34%, 100% { stroke-dashoffset: 0; } }
1229
+ @keyframes scene-reveal-5 { 0%, 33% { opacity: 0; transform: translateY(5px) scale(0.94); } 39%, 100% { opacity: 1; transform: none; } }
1230
+ @keyframes scene-stroke-5 { 0%, 33% { stroke-dashoffset: 1; } 40%, 100% { stroke-dashoffset: 0; } }
1231
+ @keyframes scene-reveal-6 { 0%, 39% { opacity: 0; transform: translateY(5px) scale(0.94); } 45%, 100% { opacity: 1; transform: none; } }
1232
+ @keyframes scene-stroke-6 { 0%, 39% { stroke-dashoffset: 1; } 46%, 100% { stroke-dashoffset: 0; } }
1233
+ @keyframes scene-reveal-7 { 0%, 45% { opacity: 0; transform: translateY(5px) scale(0.94); } 51%, 100% { opacity: 1; transform: none; } }
1234
+ @keyframes scene-stroke-7 { 0%, 45% { stroke-dashoffset: 1; } 52%, 100% { stroke-dashoffset: 0; } }
1235
+ @keyframes scene-reveal-8 { 0%, 51% { opacity: 0; transform: translateY(5px) scale(0.94); } 57%, 100% { opacity: 1; transform: none; } }
1236
+ @keyframes scene-stroke-8 { 0%, 51% { stroke-dashoffset: 1; } 58%, 100% { stroke-dashoffset: 0; } }
1237
+ @keyframes scene-reveal-9 { 0%, 57% { opacity: 0; transform: translateY(5px) scale(0.94); } 63%, 100% { opacity: 1; transform: none; } }
1238
+ @keyframes scene-stroke-9 { 0%, 57% { stroke-dashoffset: 1; } 64%, 100% { stroke-dashoffset: 0; } }
1239
+ @keyframes scene-reveal-10 { 0%, 63% { opacity: 0; transform: translateY(5px) scale(0.94); } 69%, 100% { opacity: 1; transform: none; } }
1240
+ @keyframes scene-stroke-10 { 0%, 63% { stroke-dashoffset: 1; } 70%, 100% { stroke-dashoffset: 0; } }
1241
+ @keyframes scene-reveal-11 { 0%, 69% { opacity: 0; transform: translateY(5px) scale(0.94); } 75%, 100% { opacity: 1; transform: none; } }
1242
+ @keyframes scene-stroke-11 { 0%, 69% { stroke-dashoffset: 1; } 76%, 100% { stroke-dashoffset: 0; } }
1243
+ @keyframes scene-reveal-12 { 0%, 75% { opacity: 0; transform: translateY(5px) scale(0.94); } 81%, 100% { opacity: 1; transform: none; } }
1244
+ @keyframes scene-stroke-12 { 0%, 75% { stroke-dashoffset: 1; } 82%, 100% { stroke-dashoffset: 0; } }
1245
+
1246
+ @media (prefers-reduced-motion: no-preference) {
1247
+ .scene-loop { animation: scene-loop 8s linear infinite; }
1248
+ .scene-reveal-0 { animation: scene-reveal-0 8s var(--scene-arrive) infinite; }
1249
+ .scene-stroke-0 { animation: scene-stroke-0 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1250
+ .scene-type-0 { animation: scene-stroke-0 8s steps(5, jump-none) infinite; }
1251
+ .scene-reveal-1 { animation: scene-reveal-1 8s var(--scene-arrive) infinite; }
1252
+ .scene-stroke-1 { animation: scene-stroke-1 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1253
+ .scene-type-1 { animation: scene-stroke-1 8s steps(5, jump-none) infinite; }
1254
+ .scene-reveal-2 { animation: scene-reveal-2 8s var(--scene-arrive) infinite; }
1255
+ .scene-stroke-2 { animation: scene-stroke-2 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1256
+ .scene-type-2 { animation: scene-stroke-2 8s steps(5, jump-none) infinite; }
1257
+ .scene-reveal-3 { animation: scene-reveal-3 8s var(--scene-arrive) infinite; }
1258
+ .scene-stroke-3 { animation: scene-stroke-3 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1259
+ .scene-type-3 { animation: scene-stroke-3 8s steps(5, jump-none) infinite; }
1260
+ .scene-reveal-4 { animation: scene-reveal-4 8s var(--scene-arrive) infinite; }
1261
+ .scene-stroke-4 { animation: scene-stroke-4 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1262
+ .scene-type-4 { animation: scene-stroke-4 8s steps(5, jump-none) infinite; }
1263
+ .scene-reveal-5 { animation: scene-reveal-5 8s var(--scene-arrive) infinite; }
1264
+ .scene-stroke-5 { animation: scene-stroke-5 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1265
+ .scene-type-5 { animation: scene-stroke-5 8s steps(5, jump-none) infinite; }
1266
+ .scene-reveal-6 { animation: scene-reveal-6 8s var(--scene-arrive) infinite; }
1267
+ .scene-stroke-6 { animation: scene-stroke-6 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1268
+ .scene-type-6 { animation: scene-stroke-6 8s steps(5, jump-none) infinite; }
1269
+ .scene-reveal-7 { animation: scene-reveal-7 8s var(--scene-arrive) infinite; }
1270
+ .scene-stroke-7 { animation: scene-stroke-7 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1271
+ .scene-type-7 { animation: scene-stroke-7 8s steps(5, jump-none) infinite; }
1272
+ .scene-reveal-8 { animation: scene-reveal-8 8s var(--scene-arrive) infinite; }
1273
+ .scene-stroke-8 { animation: scene-stroke-8 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1274
+ .scene-type-8 { animation: scene-stroke-8 8s steps(5, jump-none) infinite; }
1275
+ .scene-reveal-9 { animation: scene-reveal-9 8s var(--scene-arrive) infinite; }
1276
+ .scene-stroke-9 { animation: scene-stroke-9 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1277
+ .scene-type-9 { animation: scene-stroke-9 8s steps(5, jump-none) infinite; }
1278
+ .scene-reveal-10 { animation: scene-reveal-10 8s var(--scene-arrive) infinite; }
1279
+ .scene-stroke-10 { animation: scene-stroke-10 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1280
+ .scene-type-10 { animation: scene-stroke-10 8s steps(5, jump-none) infinite; }
1281
+ .scene-reveal-11 { animation: scene-reveal-11 8s var(--scene-arrive) infinite; }
1282
+ .scene-stroke-11 { animation: scene-stroke-11 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1283
+ .scene-type-11 { animation: scene-stroke-11 8s steps(5, jump-none) infinite; }
1284
+ .scene-reveal-12 { animation: scene-reveal-12 8s var(--scene-arrive) infinite; }
1285
+ .scene-stroke-12 { animation: scene-stroke-12 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1286
+ .scene-type-12 { animation: scene-stroke-12 8s steps(5, jump-none) infinite; }
1287
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/ui",
3
- "version": "0.54.11",
3
+ "version": "0.54.13",
4
4
  "description": "React components, a theme, formatters and a settings dialog for desktop-grade apps on React 19, Base UI and Tailwind 4.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "author": "Bas Milius",
@@ -0,0 +1,30 @@
1
+ import clsx from 'clsx';
2
+ import { useId, type CSSProperties, type ReactNode } from 'react';
3
+
4
+ export interface EmptySceneProps {
5
+ /* For a panel: 136 by 88 pixels instead of 160 by 104. */
6
+ compact?: boolean;
7
+
8
+ /* SVG in a 160 by 104 view box, drawn with the `scene-*` classes of the theme. */
9
+ children: ReactNode;
10
+
11
+ className?: string;
12
+ }
13
+
14
+ /* The drawing an `EmptyState` shows through `scene`. It only decorates, so it stays out of the accessibility tree. */
15
+ export function EmptyScene({ compact = false, children, className }: EmptySceneProps) {
16
+ const gradient = useId();
17
+ const style = { '--scene-fill': `url("#${gradient}")` } as CSSProperties;
18
+
19
+ return (
20
+ <svg className={clsx('scene', compact && 'scene-compact', className)} viewBox="0 0 160 104" style={style} aria-hidden="true">
21
+ <defs>
22
+ <linearGradient id={gradient} x1="0" y1="0" x2="0" y2="1">
23
+ <stop offset="0" style={{ stopColor: 'var(--scene-top)' }} />
24
+ <stop offset="1" style={{ stopColor: 'var(--scene-bottom)' }} />
25
+ </linearGradient>
26
+ </defs>
27
+ {children}
28
+ </svg>
29
+ );
30
+ }
@@ -11,6 +11,9 @@ export interface EmptyStateProps {
11
11
  /* A Lucide icon, or a mark of another kind that takes a `size`. */
12
12
  icon?: LucideIcon | ReactElement<{ size?: number }>;
13
13
 
14
+ /* A drawing in place of the icon, usually an `EmptyScene`, for a place a person may not know yet. */
15
+ scene?: ReactNode;
16
+
14
17
  /* A `Spinner` in place of the icon. */
15
18
  busy?: boolean;
16
19
 
@@ -27,10 +30,11 @@ export interface EmptyStateProps {
27
30
  ref?: Ref<HTMLDivElement>;
28
31
  }
29
32
 
30
- export function EmptyState({ icon, busy = false, title, children, action, className, ref }: EmptyStateProps) {
33
+ export function EmptyState({ icon, scene, busy = false, title, children, action, className, ref }: EmptyStateProps) {
31
34
  return (
32
35
  <div ref={ref} className={clsx('flex flex-col items-center justify-center gap-2 px-6 py-8 text-center', className)}>
33
- {(busy || icon) && (
36
+ {scene && !busy && <div className="mb-2">{scene}</div>}
37
+ {(busy || (icon && !scene)) && (
34
38
  <span className="text-text-faint">
35
39
  {busy ? (
36
40
  <Spinner size={ICON_SIZE} />
@@ -41,7 +45,7 @@ export function EmptyState({ icon, busy = false, title, children, action, classN
41
45
  )}
42
46
  </span>
43
47
  )}
44
- {title && <p className="text-sm font-medium text-text">{title}</p>}
48
+ {title && <p className={clsx('text-text', scene ? 'text-base font-semibold' : 'text-sm font-medium')}>{title}</p>}
45
49
  <p className="max-w-[280px] text-xs leading-snug text-text-muted">{children}</p>
46
50
  {/* The button is an answer to the sentence, not a third line of it. */}
47
51
  {action && <div className="mt-2">{action}</div>}
@@ -9,6 +9,11 @@ export interface PanelEmptyProps {
9
9
 
10
10
  icon?: LucideIcon;
11
11
 
12
+ /* A drawing in place of the icon, usually a compact `EmptyScene`. */
13
+ scene?: ReactNode;
14
+
15
+ title?: ReactNode;
16
+
12
17
  /* A `Spinner` in place of the icon. */
13
18
  busy?: boolean;
14
19
 
@@ -25,11 +30,11 @@ export interface PanelEmptyProps {
25
30
  ref?: Ref<HTMLDivElement>;
26
31
  }
27
32
 
28
- export function PanelEmpty({ header, icon, busy = false, action, sunken = false, fill = 'grow', children, className, ref }: PanelEmptyProps) {
33
+ export function PanelEmpty({ header, icon, scene, title, busy = false, action, sunken = false, fill = 'grow', children, className, ref }: PanelEmptyProps) {
29
34
  return (
30
35
  <div ref={ref} className={clsx('grid min-h-0 place-items-center', fill === 'full' ? 'h-full' : 'grow', sunken && 'bg-surface-sunken', className)}>
31
36
  {header}
32
- <EmptyState icon={icon} busy={busy} action={action}>
37
+ <EmptyState icon={icon} scene={scene} title={title} busy={busy} action={action}>
33
38
  {children}
34
39
  </EmptyState>
35
40
  </div>
package/src/index.ts CHANGED
@@ -48,6 +48,7 @@ export { ColorWheelPicker, type ColorWheelPickerProps } from './ColorWheelPicker
48
48
  export { ColumnResizeHandle, type ColumnResizeHandleProps } from './ColumnResizeHandle.tsx';
49
49
  export { DisabledReason, type DisabledReasonProps } from './DisabledReason.tsx';
50
50
  export { DockShell, type DockShellProps } from './DockShell.tsx';
51
+ export { EmptyScene, type EmptySceneProps } from './EmptyScene.tsx';
51
52
  export { EmptyState, type EmptyStateProps } from './EmptyState.tsx';
52
53
  export { ErrorBoundary, type ErrorBoundaryProps } from './ErrorBoundary.tsx';
53
54
  export { Field, FieldHint, FormError, type FieldHintProps, type FieldProps, type FormErrorProps } from './Field.tsx';
@@ -97,10 +97,44 @@ type ItemLook = {
97
97
  unstyled?: boolean;
98
98
  };
99
99
 
100
+ /*
101
+ * Typing in an open menu moves to the first row whose text starts with the letters, read from the
102
+ * row's `label` or else its rendered text. The space JSX keeps between an icon and the name
103
+ * (`<Icon /> {name}`) draws nothing in a row laid out with a gap, but it would start that text and
104
+ * keep the row from ever matching. A row whose text starts with something other than its name,
105
+ * such as initials before it, sets `label`.
106
+ */
107
+ function withoutLeadingSpace(children: ReactNode): ReactNode {
108
+ if (typeof children === 'string') {
109
+ return children.trimStart();
110
+ }
111
+
112
+ if (!Array.isArray(children)) {
113
+ return children;
114
+ }
115
+
116
+ let reached = false;
117
+
118
+ return children.map((child: ReactNode) => {
119
+ if (reached || typeof child !== 'string') {
120
+ return child;
121
+ }
122
+
123
+ const trimmed = child.trimStart();
124
+ reached = trimmed !== '';
125
+
126
+ return trimmed;
127
+ });
128
+ }
129
+
100
130
  type MenuItemProps = ComponentProps<typeof BaseMenu.Item> & ItemLook;
101
131
 
102
- export function MenuItem({ unstyled = false, className, ...props }: MenuItemProps) {
103
- return <BaseMenu.Item className={unstyled ? className : withClass('menu-item', className)} {...props} />;
132
+ export function MenuItem({ unstyled = false, className, children, ...props }: MenuItemProps) {
133
+ return (
134
+ <BaseMenu.Item className={unstyled ? className : withClass('menu-item', className)} {...props}>
135
+ {unstyled ? children : withoutLeadingSpace(children)}
136
+ </BaseMenu.Item>
137
+ );
104
138
  }
105
139
 
106
140
  export interface MenuCheckProps {
@@ -147,7 +181,7 @@ const checkRow = (kind: MenuCheckProps['kind'], unstyled: boolean, indicator: In
147
181
  ) : (
148
182
  <>
149
183
  {indicator === 'start' && <MenuCheck kind={kind} />}
150
- {children}
184
+ {withoutLeadingSpace(children)}
151
185
  {indicator === 'end' && <MenuCheck kind={kind} className="ml-auto" />}
152
186
  </>
153
187
  );
@@ -182,7 +216,7 @@ type SubmenuTriggerProps = ComponentProps<typeof BaseMenu.SubmenuTrigger> & {
182
216
  export function MenuSubmenuTrigger({ className, children, chevron = true, ...props }: SubmenuTriggerProps) {
183
217
  return (
184
218
  <BaseMenu.SubmenuTrigger className={withClass('menu-item', className)} {...props}>
185
- {children}
219
+ {withoutLeadingSpace(children)}
186
220
  {chevron && <Icon icon={ChevronRight} size={14} className="ml-auto text-text-faint" />}
187
221
  </BaseMenu.SubmenuTrigger>
188
222
  );
package/src/theme.css CHANGED
@@ -115,6 +115,16 @@
115
115
  --file-icon-purple: #a631be;
116
116
  --file-icon-pink: #d32a61;
117
117
  --file-icon-mauve: #594c5b;
118
+ /* `EmptyScene`: what a drawing of an empty place is made of. */
119
+ --scene-top: #ffffff;
120
+ --scene-bottom: #ececf1;
121
+ --scene-rim: rgb(0 0 0 / 0.09);
122
+ --scene-line: rgb(0 0 0 / 0.12);
123
+ --scene-inset: rgb(0 0 0 / 0.06);
124
+ --scene-text: rgb(0 0 0 / 0.14);
125
+ --scene-lift: drop-shadow(0 1px 1px rgb(0 0 0 / 0.08)) drop-shadow(0 5px 10px rgb(0 0 0 / 0.08));
126
+ /* What lands in a scene overshoots a little and settles. */
127
+ --scene-arrive: cubic-bezier(0.34, 1.5, 0.55, 1);
118
128
  }
119
129
 
120
130
  /* The theme attribute sits on `<html>`, the element the light rule above lands on too, so a token
@@ -191,6 +201,13 @@
191
201
  --file-icon-purple: #d568ea;
192
202
  --file-icon-pink: #ff678d;
193
203
  --file-icon-mauve: #79697b;
204
+ --scene-top: #2c2c32;
205
+ --scene-bottom: #1a1a1f;
206
+ --scene-rim: rgb(255 255 255 / 0.08);
207
+ --scene-line: rgb(255 255 255 / 0.1);
208
+ --scene-inset: rgb(0 0 0 / 0.3);
209
+ --scene-text: rgb(255 255 255 / 0.16);
210
+ --scene-lift: drop-shadow(0 1px 0 rgb(0 0 0 / 0.4)) drop-shadow(0 6px 10px rgb(0 0 0 / 0.35));
194
211
  }
195
212
 
196
213
  /* Code a person reads as code (an editor, a grid of values, a statement) at one size the app may set on
@@ -1178,3 +1195,93 @@
1178
1195
  .ade-scrollbar, .ade-scrollbar::before, .ade-scrollbar-thumb::before { transition: none; }
1179
1196
  }
1180
1197
  }
1198
+
1199
+ /* The drawing of an empty place (`EmptyScene`): shapes in a soft gradient with a thin rim and one detail
1200
+ in the accent color, on the same 8 second loop. Each rule below only animates: at rest, and under
1201
+ reduced motion, a drawing shows the end of what it plays. */
1202
+ .scene { flex: none; width: 160px; height: 104px; overflow: visible; }
1203
+ .scene-compact { width: 136px; height: 88px; }
1204
+ .scene-shape { fill: var(--scene-fill); stroke: var(--scene-rim); stroke-width: 1; stroke-linejoin: round; }
1205
+ .scene-edge { fill: none; stroke: var(--scene-line); stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; }
1206
+ .scene-inset { fill: var(--scene-inset); }
1207
+ .scene-line { fill: none; stroke: var(--text-faint); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
1208
+ .scene-text { fill: none; stroke: var(--scene-text); stroke-width: 4px; stroke-linecap: round; }
1209
+ .scene-hand { fill: none; stroke: var(--text-muted); stroke-width: 2.5; stroke-linecap: round; }
1210
+ .scene-dot { fill: var(--text-faint); }
1211
+ .scene-accent { fill: var(--accent); }
1212
+ .scene-accent-line { fill: none; stroke: var(--accent); stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
1213
+ .scene-lift { filter: var(--scene-lift); }
1214
+ .scene-cursor { fill: var(--text); stroke: var(--surface); stroke-width: 1; stroke-linejoin: round; }
1215
+ .scene [class*="scene-reveal-"] { transform-box: fill-box; transform-origin: center; }
1216
+ .scene [class*="scene-stroke-"], .scene [class*="scene-type-"] { stroke-dasharray: 1; }
1217
+
1218
+ @keyframes scene-loop { 0% { opacity: 0; } 3%, 88% { opacity: 1; } 95%, 100% { opacity: 0; } }
1219
+ @keyframes scene-reveal-0 { 0%, 3% { opacity: 0; transform: translateY(5px) scale(0.94); } 9%, 100% { opacity: 1; transform: none; } }
1220
+ @keyframes scene-stroke-0 { 0%, 3% { stroke-dashoffset: 1; } 10%, 100% { stroke-dashoffset: 0; } }
1221
+ @keyframes scene-reveal-1 { 0%, 9% { opacity: 0; transform: translateY(5px) scale(0.94); } 15%, 100% { opacity: 1; transform: none; } }
1222
+ @keyframes scene-stroke-1 { 0%, 9% { stroke-dashoffset: 1; } 16%, 100% { stroke-dashoffset: 0; } }
1223
+ @keyframes scene-reveal-2 { 0%, 15% { opacity: 0; transform: translateY(5px) scale(0.94); } 21%, 100% { opacity: 1; transform: none; } }
1224
+ @keyframes scene-stroke-2 { 0%, 15% { stroke-dashoffset: 1; } 22%, 100% { stroke-dashoffset: 0; } }
1225
+ @keyframes scene-reveal-3 { 0%, 21% { opacity: 0; transform: translateY(5px) scale(0.94); } 27%, 100% { opacity: 1; transform: none; } }
1226
+ @keyframes scene-stroke-3 { 0%, 21% { stroke-dashoffset: 1; } 28%, 100% { stroke-dashoffset: 0; } }
1227
+ @keyframes scene-reveal-4 { 0%, 27% { opacity: 0; transform: translateY(5px) scale(0.94); } 33%, 100% { opacity: 1; transform: none; } }
1228
+ @keyframes scene-stroke-4 { 0%, 27% { stroke-dashoffset: 1; } 34%, 100% { stroke-dashoffset: 0; } }
1229
+ @keyframes scene-reveal-5 { 0%, 33% { opacity: 0; transform: translateY(5px) scale(0.94); } 39%, 100% { opacity: 1; transform: none; } }
1230
+ @keyframes scene-stroke-5 { 0%, 33% { stroke-dashoffset: 1; } 40%, 100% { stroke-dashoffset: 0; } }
1231
+ @keyframes scene-reveal-6 { 0%, 39% { opacity: 0; transform: translateY(5px) scale(0.94); } 45%, 100% { opacity: 1; transform: none; } }
1232
+ @keyframes scene-stroke-6 { 0%, 39% { stroke-dashoffset: 1; } 46%, 100% { stroke-dashoffset: 0; } }
1233
+ @keyframes scene-reveal-7 { 0%, 45% { opacity: 0; transform: translateY(5px) scale(0.94); } 51%, 100% { opacity: 1; transform: none; } }
1234
+ @keyframes scene-stroke-7 { 0%, 45% { stroke-dashoffset: 1; } 52%, 100% { stroke-dashoffset: 0; } }
1235
+ @keyframes scene-reveal-8 { 0%, 51% { opacity: 0; transform: translateY(5px) scale(0.94); } 57%, 100% { opacity: 1; transform: none; } }
1236
+ @keyframes scene-stroke-8 { 0%, 51% { stroke-dashoffset: 1; } 58%, 100% { stroke-dashoffset: 0; } }
1237
+ @keyframes scene-reveal-9 { 0%, 57% { opacity: 0; transform: translateY(5px) scale(0.94); } 63%, 100% { opacity: 1; transform: none; } }
1238
+ @keyframes scene-stroke-9 { 0%, 57% { stroke-dashoffset: 1; } 64%, 100% { stroke-dashoffset: 0; } }
1239
+ @keyframes scene-reveal-10 { 0%, 63% { opacity: 0; transform: translateY(5px) scale(0.94); } 69%, 100% { opacity: 1; transform: none; } }
1240
+ @keyframes scene-stroke-10 { 0%, 63% { stroke-dashoffset: 1; } 70%, 100% { stroke-dashoffset: 0; } }
1241
+ @keyframes scene-reveal-11 { 0%, 69% { opacity: 0; transform: translateY(5px) scale(0.94); } 75%, 100% { opacity: 1; transform: none; } }
1242
+ @keyframes scene-stroke-11 { 0%, 69% { stroke-dashoffset: 1; } 76%, 100% { stroke-dashoffset: 0; } }
1243
+ @keyframes scene-reveal-12 { 0%, 75% { opacity: 0; transform: translateY(5px) scale(0.94); } 81%, 100% { opacity: 1; transform: none; } }
1244
+ @keyframes scene-stroke-12 { 0%, 75% { stroke-dashoffset: 1; } 82%, 100% { stroke-dashoffset: 0; } }
1245
+
1246
+ @media (prefers-reduced-motion: no-preference) {
1247
+ .scene-loop { animation: scene-loop 8s linear infinite; }
1248
+ .scene-reveal-0 { animation: scene-reveal-0 8s var(--scene-arrive) infinite; }
1249
+ .scene-stroke-0 { animation: scene-stroke-0 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1250
+ .scene-type-0 { animation: scene-stroke-0 8s steps(5, jump-none) infinite; }
1251
+ .scene-reveal-1 { animation: scene-reveal-1 8s var(--scene-arrive) infinite; }
1252
+ .scene-stroke-1 { animation: scene-stroke-1 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1253
+ .scene-type-1 { animation: scene-stroke-1 8s steps(5, jump-none) infinite; }
1254
+ .scene-reveal-2 { animation: scene-reveal-2 8s var(--scene-arrive) infinite; }
1255
+ .scene-stroke-2 { animation: scene-stroke-2 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1256
+ .scene-type-2 { animation: scene-stroke-2 8s steps(5, jump-none) infinite; }
1257
+ .scene-reveal-3 { animation: scene-reveal-3 8s var(--scene-arrive) infinite; }
1258
+ .scene-stroke-3 { animation: scene-stroke-3 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1259
+ .scene-type-3 { animation: scene-stroke-3 8s steps(5, jump-none) infinite; }
1260
+ .scene-reveal-4 { animation: scene-reveal-4 8s var(--scene-arrive) infinite; }
1261
+ .scene-stroke-4 { animation: scene-stroke-4 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1262
+ .scene-type-4 { animation: scene-stroke-4 8s steps(5, jump-none) infinite; }
1263
+ .scene-reveal-5 { animation: scene-reveal-5 8s var(--scene-arrive) infinite; }
1264
+ .scene-stroke-5 { animation: scene-stroke-5 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1265
+ .scene-type-5 { animation: scene-stroke-5 8s steps(5, jump-none) infinite; }
1266
+ .scene-reveal-6 { animation: scene-reveal-6 8s var(--scene-arrive) infinite; }
1267
+ .scene-stroke-6 { animation: scene-stroke-6 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1268
+ .scene-type-6 { animation: scene-stroke-6 8s steps(5, jump-none) infinite; }
1269
+ .scene-reveal-7 { animation: scene-reveal-7 8s var(--scene-arrive) infinite; }
1270
+ .scene-stroke-7 { animation: scene-stroke-7 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1271
+ .scene-type-7 { animation: scene-stroke-7 8s steps(5, jump-none) infinite; }
1272
+ .scene-reveal-8 { animation: scene-reveal-8 8s var(--scene-arrive) infinite; }
1273
+ .scene-stroke-8 { animation: scene-stroke-8 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1274
+ .scene-type-8 { animation: scene-stroke-8 8s steps(5, jump-none) infinite; }
1275
+ .scene-reveal-9 { animation: scene-reveal-9 8s var(--scene-arrive) infinite; }
1276
+ .scene-stroke-9 { animation: scene-stroke-9 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1277
+ .scene-type-9 { animation: scene-stroke-9 8s steps(5, jump-none) infinite; }
1278
+ .scene-reveal-10 { animation: scene-reveal-10 8s var(--scene-arrive) infinite; }
1279
+ .scene-stroke-10 { animation: scene-stroke-10 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1280
+ .scene-type-10 { animation: scene-stroke-10 8s steps(5, jump-none) infinite; }
1281
+ .scene-reveal-11 { animation: scene-reveal-11 8s var(--scene-arrive) infinite; }
1282
+ .scene-stroke-11 { animation: scene-stroke-11 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1283
+ .scene-type-11 { animation: scene-stroke-11 8s steps(5, jump-none) infinite; }
1284
+ .scene-reveal-12 { animation: scene-reveal-12 8s var(--scene-arrive) infinite; }
1285
+ .scene-stroke-12 { animation: scene-stroke-12 8s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
1286
+ .scene-type-12 { animation: scene-stroke-12 8s steps(5, jump-none) infinite; }
1287
+ }