@hyvnt/hyvui 0.2.0 → 0.3.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 (86) hide show
  1. package/README.md +264 -253
  2. package/dist/components/ambient/CornerBrackets.svelte +83 -87
  3. package/dist/components/ambient/DataStream.svelte +111 -94
  4. package/dist/components/ambient/GlyphMark.svelte +69 -69
  5. package/dist/components/ambient/GridOverlay.svelte +26 -28
  6. package/dist/components/ambient/ParallaxLayer.svelte +37 -41
  7. package/dist/components/ambient/ScanBand.svelte +95 -91
  8. package/dist/components/ambient/SignalRing.svelte +100 -100
  9. package/dist/components/ambient/ThreadLine.svelte +71 -78
  10. package/dist/components/ambient/Vignette.svelte +24 -26
  11. package/dist/components/depth/DepthLayer.svelte +22 -27
  12. package/dist/components/depth/DepthStage.svelte +63 -62
  13. package/dist/components/depth/FloatCard.svelte +113 -104
  14. package/dist/components/depth/HorizonGrid.svelte +216 -160
  15. package/dist/components/depth/Plinth.svelte +52 -57
  16. package/dist/components/display/Avatar.svelte +64 -69
  17. package/dist/components/display/Badge.svelte +59 -63
  18. package/dist/components/display/Blockquote.svelte +31 -34
  19. package/dist/components/display/CodeBlock.svelte +71 -76
  20. package/dist/components/display/MetricCard.svelte +77 -83
  21. package/dist/components/display/Table.svelte +99 -104
  22. package/dist/components/feedback/Alert.svelte +71 -76
  23. package/dist/components/feedback/EmptyState.svelte +68 -68
  24. package/dist/components/feedback/ErrorState.svelte +73 -73
  25. package/dist/components/feedback/Skeleton.svelte +52 -52
  26. package/dist/components/feedback/StatusDot.svelte +49 -54
  27. package/dist/components/feedback/StatusLine.svelte +122 -122
  28. package/dist/components/feedback/Toast.svelte +130 -136
  29. package/dist/components/inputs/Button.svelte +240 -237
  30. package/dist/components/inputs/Checkbox.svelte +104 -105
  31. package/dist/components/inputs/FileUpload.svelte +165 -163
  32. package/dist/components/inputs/Input.svelte +145 -147
  33. package/dist/components/inputs/Select.svelte +156 -150
  34. package/dist/components/inputs/Textarea.svelte +153 -154
  35. package/dist/components/inputs/Toggle.svelte +120 -120
  36. package/dist/components/layout/Card.svelte +70 -76
  37. package/dist/components/layout/Drawer.svelte +133 -109
  38. package/dist/components/layout/Grid.svelte +118 -43
  39. package/dist/components/layout/Grid.svelte.d.ts +8 -2
  40. package/dist/components/layout/Modal.svelte +176 -159
  41. package/dist/components/layout/Panel.svelte +49 -54
  42. package/dist/components/layout/Popover.svelte +178 -67
  43. package/dist/components/layout/Popover.svelte.d.ts +10 -1
  44. package/dist/components/layout/Stack.svelte +53 -53
  45. package/dist/components/navigation/Breadcrumb.svelte +70 -73
  46. package/dist/components/navigation/DropdownMenu.svelte +167 -124
  47. package/dist/components/navigation/DropdownMenu.svelte.d.ts +12 -2
  48. package/dist/components/navigation/SidebarNav.svelte +86 -90
  49. package/dist/components/navigation/Tabs.svelte +81 -86
  50. package/dist/components/navigation/Topbar.svelte +85 -85
  51. package/dist/components/patterns/ActionBar.svelte +71 -76
  52. package/dist/components/patterns/ConfirmDialog.svelte +63 -64
  53. package/dist/components/patterns/PageHeader.svelte +109 -114
  54. package/dist/components/patterns/SearchBar.svelte +54 -59
  55. package/dist/components/patterns/TerminalBoot.svelte +104 -104
  56. package/dist/components/primitives/Divider.svelte +26 -29
  57. package/dist/components/primitives/Icon.svelte +44 -49
  58. package/dist/components/primitives/Label.svelte +39 -44
  59. package/dist/components/primitives/Surface.svelte +89 -87
  60. package/dist/components/primitives/Text.svelte +98 -98
  61. package/dist/components/scenes/ArchiveScene.svelte +92 -95
  62. package/dist/components/scenes/ArchiveScene.svelte.d.ts +7 -1
  63. package/dist/components/scenes/LogScene.svelte +72 -77
  64. package/dist/components/scenes/NarrativeScene.svelte +91 -92
  65. package/dist/components/scenes/ReadoutScene.svelte +120 -107
  66. package/dist/components/scenes/ReadoutScene.svelte.d.ts +3 -1
  67. package/dist/components/scenes/StageScene.svelte +97 -104
  68. package/dist/examples/FieldReport.svelte +226 -223
  69. package/dist/examples/ObservationDeck.svelte +333 -317
  70. package/dist/examples/SignalLost.svelte +191 -191
  71. package/dist/styles.css +113 -0
  72. package/dist/system/actions/echo.js +9 -9
  73. package/dist/system/actions/resolve.js +9 -9
  74. package/dist/system/actions/reveal.js +1 -1
  75. package/dist/system/actions/surface.js +13 -1
  76. package/dist/system/depth/depth.css +49 -49
  77. package/dist/system/depth/depth.js +1 -1
  78. package/dist/system/expressions.css +80 -80
  79. package/dist/system/override-template.css +72 -72
  80. package/dist/system/register.css +74 -74
  81. package/dist/system/scroll-lock.d.ts +6 -0
  82. package/dist/system/scroll-lock.js +23 -0
  83. package/dist/tokens/tokens.css +100 -86
  84. package/dist/tokens/tokens.js +4 -4
  85. package/dist/utils/motion.js +1 -1
  86. package/package.json +67 -60
@@ -1,191 +1,191 @@
1
- <!--
2
- EXAMPLE: Signal Lost
3
- REGISTER: field-notebook
4
- CONCEPT: a 404 page that feels like arriving at coordinates where something used to be
5
- DEMONSTRATES: FloatCard, HorizonGrid, TerminalBoot, StatusDot, CornerBrackets, SignalRing, Text expressions, surface action, depth system
6
- INSPIRED BY: the last transmission log of a deep-space relay station
7
- -->
8
- <script lang="ts">
9
- import {
10
- Text,
11
- Label,
12
- Button,
13
- StatusDot,
14
- CornerBrackets,
15
- TerminalBoot,
16
- FloatCard,
17
- HorizonGrid,
18
- DepthStage,
19
- DepthLayer,
20
- SignalRing,
21
- GlyphMark,
22
- surface,
23
- applyRegister,
24
- } from '../index.js';
25
- import { onMount } from 'svelte';
26
-
27
- let bootComplete = $state(false);
28
-
29
- const bootLines: { status: 'ok' | 'pend' | 'warn' | 'fail'; message: string }[] = [
30
- { status: 'ok', message: 'local systems nominal' },
31
- { status: 'ok', message: 'navigation subsystem online' },
32
- { status: 'ok', message: 'scanning frequency range' },
33
- { status: 'pend', message: 'attempting signal acquisition' },
34
- { status: 'warn', message: 'no response on primary band' },
35
- { status: 'warn', message: 'sweeping secondary frequencies' },
36
- { status: 'fail', message: 'target signal not found at these coordinates' },
37
- ];
38
-
39
- onMount(() => {
40
- applyRegister('field-notebook');
41
- return () => {
42
- document.body.removeAttribute('data-register');
43
- };
44
- });
45
- </script>
46
-
47
- <div class="signal-lost">
48
- <div class="signal-lost-bg">
49
- <HorizonGrid animated rows={20} cols={14} vanishY={0.32} />
50
- </div>
51
-
52
- <div class="signal-lost-ring" aria-hidden="true">
53
- <SignalRing active size={400} />
54
- </div>
55
-
56
- <DepthStage perspective="mid" class="signal-lost-stage">
57
- <DepthLayer level="ground" class="signal-lost-terminal">
58
- <div class="signal-lost-terminal-inner" use:surface={{ delay: 800 }}>
59
- <TerminalBoot
60
- lines={bootLines}
61
- delay={1200}
62
- interval={900}
63
- oncomplete={() => (bootComplete = true)}
64
- />
65
- </div>
66
- </DepthLayer>
67
-
68
- <DepthLayer level="raised" class="signal-lost-center">
69
- <FloatCard tiltMax={6} class="signal-lost-card">
70
- <div class="signal-lost-content" style:position="relative">
71
- <CornerBrackets size={28} color="rgba(199, 156, 87, 0.2)" />
72
-
73
- <div class="signal-lost-status" use:surface={{ delay: 200 }}>
74
- <StatusDot status="fail" size={8} pulse />
75
- <Label color="muted">transmission ended</Label>
76
- </div>
77
-
78
- <div use:surface={{ delay: 400 }}>
79
- <Text variant="heading" expression="title-card" as="h1" color="primary">
80
- the coordinates are empty now
81
- </Text>
82
- </div>
83
-
84
- <div use:surface={{ delay: 600 }}>
85
- <Text expression="manifesto">
86
- something was broadcasting from here. the frequency is right,
87
- the heading is right. but the source has gone quiet.
88
- </Text>
89
- </div>
90
-
91
- <div class="signal-lost-meta" use:surface={{ delay: 700 }}>
92
- <GlyphMark variant="coord" size={16} color="var(--muted-strong)" />
93
- <Label color="muted">bearing 047.2 | range unknown | last contact 7h ago</Label>
94
- </div>
95
-
96
- <div class="signal-lost-action" use:surface={{ delay: 900 }}>
97
- <Button variant="ghost" onclick={() => window.history.back()}>
98
- return to last known position
99
- </Button>
100
- </div>
101
- </div>
102
- </FloatCard>
103
- </DepthLayer>
104
- </DepthStage>
105
- </div>
106
-
107
- <style>
108
- .signal-lost {
109
- position: relative;
110
- min-height: 100dvh;
111
- background-color: var(--bg);
112
- display: flex;
113
- align-items: center;
114
- justify-content: center;
115
- overflow: hidden;
116
- }
117
-
118
- .signal-lost-bg {
119
- position: absolute;
120
- inset: 0;
121
- opacity: 0.5;
122
- pointer-events: none;
123
- }
124
-
125
- .signal-lost-ring {
126
- position: absolute;
127
- top: 50%;
128
- left: 50%;
129
- transform: translate(-50%, -50%);
130
- opacity: 0.3;
131
- pointer-events: none;
132
- }
133
-
134
- :global(.signal-lost-stage) {
135
- position: relative;
136
- z-index: 1;
137
- display: flex;
138
- flex-direction: column;
139
- align-items: center;
140
- gap: 2rem;
141
- padding: var(--space-scene);
142
- width: 100%;
143
- max-width: 52rem;
144
- }
145
-
146
- :global(.signal-lost-center) {
147
- width: 100%;
148
- display: flex;
149
- justify-content: center;
150
- }
151
-
152
- :global(.signal-lost-card) {
153
- max-width: 36rem;
154
- width: 100%;
155
- }
156
-
157
- .signal-lost-content {
158
- display: flex;
159
- flex-direction: column;
160
- gap: 1.5rem;
161
- padding: 1rem;
162
- }
163
-
164
- .signal-lost-status {
165
- display: flex;
166
- align-items: center;
167
- gap: 0.625rem;
168
- }
169
-
170
- .signal-lost-meta {
171
- display: flex;
172
- align-items: center;
173
- gap: 0.5rem;
174
- }
175
-
176
- .signal-lost-action {
177
- margin-top: 0.5rem;
178
- }
179
-
180
- :global(.signal-lost-terminal) {
181
- width: 100%;
182
- max-width: 36rem;
183
- align-self: center;
184
- }
185
-
186
- .signal-lost-terminal-inner {
187
- padding: 1rem 0;
188
- border-left: 2px solid rgba(121, 166, 163, 0.14);
189
- padding-left: 1.25rem;
190
- }
191
- </style>
1
+ <!--
2
+ EXAMPLE: Signal Lost
3
+ REGISTER: field-notebook
4
+ CONCEPT: a 404 page that feels like arriving at coordinates where something used to be
5
+ DEMONSTRATES: FloatCard, HorizonGrid, TerminalBoot, StatusDot, CornerBrackets, SignalRing, Text expressions, surface action, depth system
6
+ INSPIRED BY: the last transmission log of a deep-space relay station
7
+ -->
8
+ <script lang="ts">
9
+ import {
10
+ Text,
11
+ Label,
12
+ Button,
13
+ StatusDot,
14
+ CornerBrackets,
15
+ TerminalBoot,
16
+ FloatCard,
17
+ HorizonGrid,
18
+ DepthStage,
19
+ DepthLayer,
20
+ SignalRing,
21
+ GlyphMark,
22
+ surface,
23
+ applyRegister
24
+ } from '../index.js';
25
+ import { onMount } from 'svelte';
26
+
27
+ let bootComplete = $state(false);
28
+
29
+ const bootLines: { status: 'ok' | 'pend' | 'warn' | 'fail'; message: string }[] = [
30
+ { status: 'ok', message: 'local systems nominal' },
31
+ { status: 'ok', message: 'navigation subsystem online' },
32
+ { status: 'ok', message: 'scanning frequency range' },
33
+ { status: 'pend', message: 'attempting signal acquisition' },
34
+ { status: 'warn', message: 'no response on primary band' },
35
+ { status: 'warn', message: 'sweeping secondary frequencies' },
36
+ { status: 'fail', message: 'target signal not found at these coordinates' }
37
+ ];
38
+
39
+ onMount(() => {
40
+ applyRegister('field-notebook');
41
+ return () => {
42
+ document.body.removeAttribute('data-register');
43
+ };
44
+ });
45
+ </script>
46
+
47
+ <div class="signal-lost">
48
+ <div class="signal-lost-bg">
49
+ <HorizonGrid animated rows={20} cols={14} vanishY={0.32} />
50
+ </div>
51
+
52
+ <div class="signal-lost-ring" aria-hidden="true">
53
+ <SignalRing active size={400} />
54
+ </div>
55
+
56
+ <DepthStage perspective="mid" class="signal-lost-stage">
57
+ <DepthLayer level="ground" class="signal-lost-terminal">
58
+ <div class="signal-lost-terminal-inner" use:surface={{ delay: 800 }}>
59
+ <TerminalBoot
60
+ lines={bootLines}
61
+ delay={1200}
62
+ interval={900}
63
+ oncomplete={() => (bootComplete = true)}
64
+ />
65
+ </div>
66
+ </DepthLayer>
67
+
68
+ <DepthLayer level="raised" class="signal-lost-center">
69
+ <FloatCard tiltMax={6} class="signal-lost-card">
70
+ <div class="signal-lost-content" style:position="relative">
71
+ <CornerBrackets size={28} color="rgba(199, 156, 87, 0.2)" />
72
+
73
+ <div class="signal-lost-status" use:surface={{ delay: 200 }}>
74
+ <StatusDot status="fail" size={8} pulse />
75
+ <Label color="muted">transmission ended</Label>
76
+ </div>
77
+
78
+ <div use:surface={{ delay: 400 }}>
79
+ <Text variant="heading" expression="title-card" as="h1" color="primary">
80
+ the coordinates are empty now
81
+ </Text>
82
+ </div>
83
+
84
+ <div use:surface={{ delay: 600 }}>
85
+ <Text expression="manifesto">
86
+ something was broadcasting from here. the frequency is right, the heading is right.
87
+ but the source has gone quiet.
88
+ </Text>
89
+ </div>
90
+
91
+ <div class="signal-lost-meta" use:surface={{ delay: 700 }}>
92
+ <GlyphMark variant="coord" size={16} color="var(--muted-strong)" />
93
+ <Label color="muted">bearing 047.2 | range unknown | last contact 7h ago</Label>
94
+ </div>
95
+
96
+ <div class="signal-lost-action" use:surface={{ delay: 900 }}>
97
+ <Button variant="ghost" onclick={() => window.history.back()}>
98
+ return to last known position
99
+ </Button>
100
+ </div>
101
+ </div>
102
+ </FloatCard>
103
+ </DepthLayer>
104
+ </DepthStage>
105
+ </div>
106
+
107
+ <style>
108
+ .signal-lost {
109
+ position: relative;
110
+ min-height: 100dvh;
111
+ background-color: var(--bg);
112
+ display: flex;
113
+ align-items: center;
114
+ justify-content: center;
115
+ overflow: hidden;
116
+ }
117
+
118
+ .signal-lost-bg {
119
+ position: absolute;
120
+ inset: 0;
121
+ opacity: 0.5;
122
+ pointer-events: none;
123
+ }
124
+
125
+ .signal-lost-ring {
126
+ position: absolute;
127
+ top: 50%;
128
+ left: 50%;
129
+ transform: translate(-50%, -50%);
130
+ opacity: 0.3;
131
+ pointer-events: none;
132
+ }
133
+
134
+ :global(.signal-lost-stage) {
135
+ position: relative;
136
+ z-index: 1;
137
+ display: flex;
138
+ flex-direction: column;
139
+ align-items: center;
140
+ gap: 2rem;
141
+ padding: var(--space-scene);
142
+ width: 100%;
143
+ max-width: 52rem;
144
+ }
145
+
146
+ :global(.signal-lost-center) {
147
+ width: 100%;
148
+ display: flex;
149
+ justify-content: center;
150
+ }
151
+
152
+ :global(.signal-lost-card) {
153
+ max-width: 36rem;
154
+ width: 100%;
155
+ }
156
+
157
+ .signal-lost-content {
158
+ display: flex;
159
+ flex-direction: column;
160
+ gap: 1.5rem;
161
+ padding: 1rem;
162
+ }
163
+
164
+ .signal-lost-status {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 0.625rem;
168
+ }
169
+
170
+ .signal-lost-meta {
171
+ display: flex;
172
+ align-items: center;
173
+ gap: 0.5rem;
174
+ }
175
+
176
+ .signal-lost-action {
177
+ margin-top: 0.5rem;
178
+ }
179
+
180
+ :global(.signal-lost-terminal) {
181
+ width: 100%;
182
+ max-width: 36rem;
183
+ align-self: center;
184
+ }
185
+
186
+ .signal-lost-terminal-inner {
187
+ padding: 1rem 0;
188
+ border-left: 2px solid rgba(121, 166, 163, 0.14);
189
+ padding-left: 1.25rem;
190
+ }
191
+ </style>
@@ -0,0 +1,113 @@
1
+ /* hyvui — base stylesheet entrypoint
2
+ *
3
+ * This file is meant to be imported by consuming applications:
4
+ * import '@hyvnt/hyvui/styles.css';
5
+ *
6
+ * It intentionally contains NO Tailwind directives, so it can be used in
7
+ * non-Tailwind projects. Tailwind theme mapping stays in app-level CSS.
8
+ */
9
+
10
+ @import './tokens/tokens.css';
11
+ @import './system/register.css';
12
+ @import './system/expressions.css';
13
+ @import './system/depth/depth.css';
14
+
15
+ /* ── base reset ─────────────────────────────────────────────────────── */
16
+ *,
17
+ *::before,
18
+ *::after {
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ html {
23
+ /* Critical fallback paint (also duplicated inline in app.html for FOUC) */
24
+ background-color: #08090b;
25
+ color: var(--text);
26
+ font-family: var(--font-body);
27
+ font-size: 16px;
28
+ -webkit-font-smoothing: antialiased;
29
+ -moz-osx-font-smoothing: grayscale;
30
+ scrollbar-gutter: stable;
31
+ color-scheme: dark;
32
+ }
33
+
34
+ body {
35
+ margin: 0;
36
+ min-height: 100dvh;
37
+ background: transparent;
38
+ color: var(--text);
39
+ overflow-x: hidden;
40
+
41
+ /* Ensure our fixed background layer can sit behind content without leaking
42
+ under <html> in weird stacking contexts. */
43
+ isolation: isolate;
44
+ }
45
+
46
+ /* Avoid scroll-jank from `background-attachment: fixed` by using a fixed layer. */
47
+ body::before {
48
+ content: '';
49
+ position: fixed;
50
+ inset: 0;
51
+ z-index: -1;
52
+ pointer-events: none;
53
+
54
+ /* atmospheric layered background — never a flat fill */
55
+ background:
56
+ radial-gradient(circle at top, rgba(198, 166, 112, 0.08), transparent 26%),
57
+ radial-gradient(circle at 20% 20%, rgba(121, 166, 163, 0.06), transparent 24%),
58
+ linear-gradient(180deg, #090b0d 0%, #08090b 35%, #050607 100%);
59
+ }
60
+
61
+ body > div {
62
+ min-height: 100dvh;
63
+ }
64
+
65
+ a {
66
+ color: inherit;
67
+ }
68
+
69
+ button,
70
+ input,
71
+ select,
72
+ textarea {
73
+ font: inherit;
74
+ }
75
+
76
+ img,
77
+ svg,
78
+ canvas {
79
+ display: block;
80
+ max-width: 100%;
81
+ }
82
+
83
+ :focus-visible {
84
+ outline: none;
85
+ box-shadow: var(--focus-ring);
86
+ }
87
+
88
+ ::selection {
89
+ background-color: rgba(199, 156, 87, 0.28);
90
+ color: var(--text);
91
+ }
92
+
93
+ /* ── shared animations ──────────────────────────────────────────────── */
94
+ @keyframes pulse-dot {
95
+ 0%,
96
+ 100% {
97
+ opacity: 0.4;
98
+ transform: scale(1);
99
+ }
100
+ 50% {
101
+ opacity: 1;
102
+ transform: scale(1.3);
103
+ }
104
+ }
105
+
106
+ @keyframes shimmer {
107
+ 0% {
108
+ background-position: -200% 0;
109
+ }
110
+ 100% {
111
+ background-position: 200% 0;
112
+ }
113
+ }
@@ -15,14 +15,14 @@ export function echo(node) {
15
15
  const x = ((e.clientX - rect.left) / rect.width) * 100;
16
16
  const y = ((e.clientY - rect.top) / rect.height) * 100;
17
17
  const overlay = document.createElement('span');
18
- overlay.style.cssText = `
19
- position: absolute;
20
- inset: 0;
21
- border-radius: inherit;
22
- pointer-events: none;
23
- background: radial-gradient(circle at ${x}% ${y}%, rgba(199, 156, 87, 0.22), transparent 70%);
24
- opacity: 0;
25
- transition: opacity 0.15s ease-out;
18
+ overlay.style.cssText = `
19
+ position: absolute;
20
+ inset: 0;
21
+ border-radius: inherit;
22
+ pointer-events: none;
23
+ background: radial-gradient(circle at ${x}% ${y}%, rgba(199, 156, 87, 0.22), transparent 70%);
24
+ opacity: 0;
25
+ transition: opacity 0.15s ease-out;
26
26
  `;
27
27
  const position = getComputedStyle(node).position;
28
28
  if (position === 'static')
@@ -41,6 +41,6 @@ export function echo(node) {
41
41
  return {
42
42
  destroy() {
43
43
  node.removeEventListener('click', handleClick);
44
- },
44
+ }
45
45
  };
46
46
  }
@@ -2,7 +2,7 @@ const statusColors = {
2
2
  ok: 'var(--status-ok)',
3
3
  warn: 'var(--status-warn)',
4
4
  fail: 'var(--status-fail)',
5
- pend: 'var(--status-pend)',
5
+ pend: 'var(--status-pend)'
6
6
  };
7
7
  /**
8
8
  * Animates a status change on an element. A brief overlay flash in the
@@ -32,14 +32,14 @@ export function resolve(node, register) {
32
32
  return;
33
33
  }
34
34
  const overlay = document.createElement('span');
35
- overlay.style.cssText = `
36
- position: absolute;
37
- inset: 0;
38
- pointer-events: none;
39
- background: ${statusColors[status]};
40
- opacity: 0;
41
- border-radius: inherit;
42
- transition: opacity 0.12s ease-out;
35
+ overlay.style.cssText = `
36
+ position: absolute;
37
+ inset: 0;
38
+ pointer-events: none;
39
+ background: ${statusColors[status]};
40
+ opacity: 0;
41
+ border-radius: inherit;
42
+ transition: opacity 0.12s ease-out;
43
43
  `;
44
44
  node.appendChild(overlay);
45
45
  requestAnimationFrame(() => {
@@ -49,6 +49,6 @@ export function reveal(node, options) {
49
49
  node.removeEventListener('mouseleave', hide);
50
50
  node.removeEventListener('focusin', show);
51
51
  node.removeEventListener('focusout', hide);
52
- },
52
+ }
53
53
  };
54
54
  }
@@ -10,16 +10,28 @@ export function surface(node, options = {}) {
10
10
  const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
11
11
  if (prefersReduced)
12
12
  return {};
13
+ node.style.willChange = 'opacity, transform';
13
14
  node.style.opacity = '0';
14
15
  node.style.transform = 'translateY(6px)';
15
16
  node.style.transition = `opacity 0.5s ease-out ${delay}ms, transform 0.5s ease-out ${delay}ms`;
17
+ function onEnd(e) {
18
+ if (e.propertyName !== 'transform')
19
+ return;
20
+ node.removeEventListener('transitionend', onEnd);
21
+ // Clear transforms to avoid creating containing blocks for fixed/floating UI.
22
+ node.style.transform = 'none';
23
+ node.style.transition = '';
24
+ node.style.willChange = '';
25
+ }
26
+ node.addEventListener('transitionend', onEnd);
16
27
  const frame = requestAnimationFrame(() => {
17
28
  node.style.opacity = '1';
18
29
  node.style.transform = 'translateY(0)';
19
30
  });
20
31
  return {
21
32
  destroy() {
33
+ node.removeEventListener('transitionend', onEnd);
22
34
  cancelAnimationFrame(frame);
23
- },
35
+ }
24
36
  };
25
37
  }