@joewinke/jatui 0.38.6 → 0.40.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.
- package/CLAUDE.md +54 -5
- package/THIRD_PARTY_NOTICES.md +67 -0
- package/dist/actions/scrollEdge.d.ts +55 -0
- package/dist/actions/scrollEdge.js +144 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +1 -1
- package/dist/artifact-blocks/blocks/diagram/schema.d.ts +1 -1
- package/dist/components/BottomSheet.svelte +7 -6
- package/dist/components/BottomSheet.svelte.d.ts +2 -0
- package/dist/components/BulkActionBar.svelte +32 -2
- package/dist/components/BulkActionBar.svelte.d.ts +5 -1
- package/dist/components/CloseButton.svelte +75 -0
- package/dist/components/CloseButton.svelte.d.ts +29 -0
- package/dist/components/Drawer.svelte +10 -11
- package/dist/components/Drawer.svelte.d.ts +2 -8
- package/dist/components/FloatingActionBar.svelte +102 -34
- package/dist/components/FloatingActionBar.svelte.d.ts +9 -0
- package/dist/components/IconButton.svelte +200 -0
- package/dist/components/IconButton.svelte.d.ts +46 -0
- package/dist/components/LinkButton.svelte +117 -0
- package/dist/components/LinkButton.svelte.d.ts +32 -0
- package/dist/components/MediaExplorer.svelte +249 -0
- package/dist/components/MediaExplorer.svelte.d.ts +37 -0
- package/dist/components/MediaLightbox.svelte +2 -7
- package/dist/components/Modal.svelte +2 -10
- package/dist/components/NotificationCenter.svelte +481 -0
- package/dist/components/NotificationCenter.svelte.d.ts +29 -0
- package/dist/components/PinInput.svelte +94 -31
- package/dist/components/PinInput.svelte.d.ts +4 -0
- package/dist/components/SearchDropdown.svelte +54 -8
- package/dist/components/SearchDropdown.svelte.d.ts +12 -0
- package/dist/components/TextArea.svelte +67 -5
- package/dist/components/TextArea.svelte.d.ts +4 -6
- package/dist/components/ThemeSelector.svelte +29 -9
- package/dist/components/ThemeSelector.svelte.d.ts +6 -0
- package/dist/components/ThemeToggle.svelte +28 -3
- package/dist/components/ThemeToggle.svelte.d.ts +9 -0
- package/dist/components/agent/AgentLog.svelte +217 -0
- package/dist/components/agent/AgentLog.svelte.d.ts +72 -0
- package/dist/components/agent/AgentLogRow.svelte +281 -0
- package/dist/components/agent/AgentLogRow.svelte.d.ts +21 -0
- package/dist/components/agent/AgentTaskList.svelte +607 -0
- package/dist/components/agent/AgentTaskList.svelte.d.ts +40 -0
- package/dist/components/agent/AgentThinking.svelte +280 -0
- package/dist/components/agent/AgentThinking.svelte.d.ts +22 -0
- package/dist/components/agent/ComposerGlow.svelte +265 -0
- package/dist/components/agent/ComposerGlow.svelte.d.ts +18 -0
- package/dist/components/agent/ContextMeter.svelte +277 -0
- package/dist/components/agent/ContextMeter.svelte.d.ts +21 -0
- package/dist/components/agent/Questionnaire.svelte +767 -0
- package/dist/components/agent/Questionnaire.svelte.d.ts +29 -0
- package/dist/components/agent/ShimmerText.svelte +100 -0
- package/dist/components/agent/ShimmerText.svelte.d.ts +19 -0
- package/dist/components/agent/StreamedText.svelte +135 -0
- package/dist/components/agent/StreamedText.svelte.d.ts +13 -0
- package/dist/components/agent/agentLog.d.ts +22 -0
- package/dist/components/agent/agentLog.js +26 -0
- package/dist/components/agent/agentTaskList.d.ts +90 -0
- package/dist/components/agent/agentTaskList.js +93 -0
- package/dist/components/agent/agentThinking.d.ts +46 -0
- package/dist/components/agent/agentThinking.js +78 -0
- package/dist/components/agent/contextMeter.d.ts +97 -0
- package/dist/components/agent/contextMeter.js +156 -0
- package/dist/components/agent/questionnaire.d.ts +104 -0
- package/dist/components/agent/questionnaire.js +225 -0
- package/dist/components/agent/revealCore.d.ts +74 -0
- package/dist/components/agent/revealCore.js +160 -0
- package/dist/components/agent/revealTicker.d.ts +34 -0
- package/dist/components/agent/revealTicker.js +73 -0
- package/dist/components/agent/streamedText.d.ts +28 -0
- package/dist/components/agent/streamedText.js +59 -0
- package/dist/components/basic/Breadcrumb.svelte +226 -11
- package/dist/components/basic/Breadcrumb.svelte.d.ts +27 -1
- package/dist/components/basic/breadcrumbCollapse.d.ts +19 -0
- package/dist/components/basic/breadcrumbCollapse.js +12 -0
- package/dist/components/graph/GraphCanvas.svelte.d.ts +2 -2
- package/dist/components/messaging/MessageInput.svelte +34 -4
- package/dist/components/messaging/MessageInput.svelte.d.ts +7 -0
- package/dist/components/messaging/MessageItem.svelte +2 -2
- package/dist/components/notificationCenter.d.ts +65 -0
- package/dist/components/notificationCenter.js +110 -0
- package/dist/components/progress/Steps.svelte +269 -5
- package/dist/components/progress/Steps.svelte.d.ts +16 -1
- package/dist/components/progress/stepsSummary.d.ts +14 -0
- package/dist/components/progress/stepsSummary.js +32 -0
- package/dist/index.d.ts +48 -1
- package/dist/index.js +39 -1
- package/dist/selection/clickSelection.d.ts +89 -0
- package/dist/selection/clickSelection.js +152 -0
- package/dist/selection/createClickSelection.d.ts +79 -0
- package/dist/selection/createClickSelection.js +134 -0
- package/dist/styles/tokens.css +80 -1
- package/dist/utils/pinInput.d.ts +33 -0
- package/dist/utils/pinInput.js +63 -0
- package/dist/utils/textareaRows.d.ts +23 -0
- package/dist/utils/textareaRows.js +13 -0
- package/dist/utils/themeWipe.d.ts +36 -17
- package/dist/utils/themeWipe.js +145 -35
- package/package.json +3 -2
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
AgentThinking — the "agent is working" status line that sits above a
|
|
3
|
+
composer or at the tail of a transcript: a small indicator, a shimmering
|
|
4
|
+
label and an elapsed timer.
|
|
5
|
+
|
|
6
|
+
Variants (indicator geometry adapted from BoardUI's agent-thinking —
|
|
7
|
+
MIT License — Copyright (c) 2026 Mertcan Dundar Esmergul (BoardUI)):
|
|
8
|
+
wave 3×3 dot grid, a diagonal wavefront
|
|
9
|
+
spin 3×3 dot grid, a bright head orbiting clockwise
|
|
10
|
+
stars staggered sparkles
|
|
11
|
+
infinity a comet sweeping a figure-eight
|
|
12
|
+
|
|
13
|
+
Differences from the original: the dot comet is pure CSS (no 80 ms
|
|
14
|
+
setInterval re-render), the timer is aria-hidden so screen readers hear the
|
|
15
|
+
label once instead of a re-announcement every 100 ms, tones are tokens,
|
|
16
|
+
and `startedAt` lets a re-mounted row (virtualised list) keep the real
|
|
17
|
+
elapsed time.
|
|
18
|
+
|
|
19
|
+
Motion: the indicator carries meaning (work is in progress), so it keeps
|
|
20
|
+
running on data-motion="calm"; the label shimmer is decorative and stops
|
|
21
|
+
there. Under prefers-reduced-motion everything rests on a static frame.
|
|
22
|
+
-->
|
|
23
|
+
<script lang="ts">
|
|
24
|
+
import ShimmerText from './ShimmerText.svelte';
|
|
25
|
+
import {
|
|
26
|
+
DOT_SEED,
|
|
27
|
+
DOT_PERIOD_MS,
|
|
28
|
+
TONE_COLOR,
|
|
29
|
+
VARIANT_TONE,
|
|
30
|
+
dotDelayMs,
|
|
31
|
+
formatThinkingTime,
|
|
32
|
+
type AgentThinkingTone,
|
|
33
|
+
type AgentThinkingVariant
|
|
34
|
+
} from './agentThinking';
|
|
35
|
+
|
|
36
|
+
interface Props {
|
|
37
|
+
variant?: AgentThinkingVariant;
|
|
38
|
+
/** Status label, e.g. "Thinking" or "Searching the docs". */
|
|
39
|
+
label?: string;
|
|
40
|
+
/** Indicator + label tone. Defaults per variant (`stars` is subtle). */
|
|
41
|
+
tone?: AgentThinkingTone;
|
|
42
|
+
/** Travelling highlight across the label. */
|
|
43
|
+
shimmer?: boolean;
|
|
44
|
+
/** Elapsed time after the label. */
|
|
45
|
+
showTimer?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* When the work started (epoch ms, Date.now() basis). Defaults to mount
|
|
48
|
+
* time. Pass it when the row can re-mount so the timer keeps counting.
|
|
49
|
+
*/
|
|
50
|
+
startedAt?: number;
|
|
51
|
+
size?: 'sm' | 'md';
|
|
52
|
+
class?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let {
|
|
56
|
+
variant = 'wave',
|
|
57
|
+
label = 'Thinking',
|
|
58
|
+
tone,
|
|
59
|
+
shimmer = true,
|
|
60
|
+
showTimer = true,
|
|
61
|
+
startedAt,
|
|
62
|
+
size = 'md',
|
|
63
|
+
class: className = ''
|
|
64
|
+
}: Props = $props();
|
|
65
|
+
|
|
66
|
+
const toneKey = $derived(tone ?? VARIANT_TONE[variant]);
|
|
67
|
+
const color = $derived(TONE_COLOR[toneKey]);
|
|
68
|
+
|
|
69
|
+
const mountedAt = Date.now();
|
|
70
|
+
let now = $state(Date.now());
|
|
71
|
+
const elapsed = $derived(now - (startedAt ?? mountedAt));
|
|
72
|
+
|
|
73
|
+
$effect(() => {
|
|
74
|
+
if (!showTimer) return;
|
|
75
|
+
now = Date.now();
|
|
76
|
+
const id = setInterval(() => (now = Date.now()), 100);
|
|
77
|
+
return () => clearInterval(id);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const STAR_PERIOD_MS = 1400;
|
|
81
|
+
const STARS = [
|
|
82
|
+
{ x: 50, y: 46, scale: 1 },
|
|
83
|
+
{ x: 18, y: 22, scale: 0.55 },
|
|
84
|
+
{ x: 82, y: 26, scale: 0.45 },
|
|
85
|
+
{ x: 78, y: 76, scale: 0.55 },
|
|
86
|
+
{ x: 22, y: 78, scale: 0.4 }
|
|
87
|
+
];
|
|
88
|
+
const STAR_PATH = 'M12 0C13 7 17 11 24 12C17 13 13 17 12 24C11 17 7 13 0 12C7 11 11 7 12 0Z';
|
|
89
|
+
const INFINITY_PATH = 'M28 14C33 5 47 5 47 14C47 23 33 23 28 14C23 5 9 5 9 14C9 23 23 23 28 14Z';
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<div
|
|
93
|
+
class="agent-thinking agent-thinking--{size} {className}"
|
|
94
|
+
role="status"
|
|
95
|
+
style:color
|
|
96
|
+
style:--at-period="{DOT_PERIOD_MS}ms"
|
|
97
|
+
>
|
|
98
|
+
{#if variant === 'wave' || variant === 'spin'}
|
|
99
|
+
<span class="at-dots" aria-hidden="true">
|
|
100
|
+
{#each DOT_SEED as seed, i (i)}
|
|
101
|
+
<span
|
|
102
|
+
class="at-dot"
|
|
103
|
+
style:--at-seed={seed}
|
|
104
|
+
style:animation-delay="{dotDelayMs(variant, i)}ms"
|
|
105
|
+
></span>
|
|
106
|
+
{/each}
|
|
107
|
+
</span>
|
|
108
|
+
{:else if variant === 'stars'}
|
|
109
|
+
<span class="at-stars" aria-hidden="true">
|
|
110
|
+
{#each STARS as star, i (i)}
|
|
111
|
+
<svg
|
|
112
|
+
viewBox="0 0 24 24"
|
|
113
|
+
class="at-star"
|
|
114
|
+
style:width="{star.scale}em"
|
|
115
|
+
style:height="{star.scale}em"
|
|
116
|
+
style:left="{star.x}%"
|
|
117
|
+
style:top="{star.y}%"
|
|
118
|
+
style:animation-duration="{STAR_PERIOD_MS}ms"
|
|
119
|
+
style:animation-delay="{Math.round((i * STAR_PERIOD_MS * 0.7) / STARS.length)}ms"
|
|
120
|
+
>
|
|
121
|
+
<path d={STAR_PATH} fill="currentColor" />
|
|
122
|
+
</svg>
|
|
123
|
+
{/each}
|
|
124
|
+
</span>
|
|
125
|
+
{:else}
|
|
126
|
+
<svg class="at-infinity" viewBox="0 0 56 28" aria-hidden="true">
|
|
127
|
+
<path d={INFINITY_PATH} fill="none" stroke="currentColor" stroke-width="2.75" opacity="0.15" />
|
|
128
|
+
<path
|
|
129
|
+
class="at-comet"
|
|
130
|
+
d={INFINITY_PATH}
|
|
131
|
+
pathLength="100"
|
|
132
|
+
fill="none"
|
|
133
|
+
stroke="currentColor"
|
|
134
|
+
stroke-width="2.75"
|
|
135
|
+
stroke-linecap="round"
|
|
136
|
+
stroke-dasharray="11 89"
|
|
137
|
+
/>
|
|
138
|
+
</svg>
|
|
139
|
+
{/if}
|
|
140
|
+
|
|
141
|
+
<ShimmerText class="at-label" text={label} tone={toneKey} active={shimmer} />
|
|
142
|
+
|
|
143
|
+
{#if showTimer}
|
|
144
|
+
<span class="at-timer" aria-hidden="true">{formatThinkingTime(elapsed)}</span>
|
|
145
|
+
{/if}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<style>
|
|
149
|
+
.agent-thinking {
|
|
150
|
+
display: inline-flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
gap: 0.625rem;
|
|
153
|
+
min-width: 0;
|
|
154
|
+
font-size: 0.875rem;
|
|
155
|
+
line-height: 1.25rem;
|
|
156
|
+
}
|
|
157
|
+
.agent-thinking--sm {
|
|
158
|
+
gap: 0.5rem;
|
|
159
|
+
font-size: 0.75rem;
|
|
160
|
+
line-height: 1rem;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.agent-thinking :global(.at-label) {
|
|
164
|
+
font-weight: 500;
|
|
165
|
+
min-width: 0;
|
|
166
|
+
overflow: hidden;
|
|
167
|
+
text-overflow: ellipsis;
|
|
168
|
+
white-space: nowrap;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.at-timer {
|
|
172
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
173
|
+
font-size: 0.75rem;
|
|
174
|
+
font-variant-numeric: tabular-nums;
|
|
175
|
+
color: color-mix(in oklab, var(--color-base-content) 50%, transparent);
|
|
176
|
+
flex-shrink: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* ── dot grid ─────────────────────────────────────────────────────── */
|
|
180
|
+
.at-dots {
|
|
181
|
+
display: grid;
|
|
182
|
+
grid-template-columns: repeat(3, 4px);
|
|
183
|
+
gap: 2px;
|
|
184
|
+
flex-shrink: 0;
|
|
185
|
+
}
|
|
186
|
+
.agent-thinking--sm .at-dots {
|
|
187
|
+
grid-template-columns: repeat(3, 3px);
|
|
188
|
+
}
|
|
189
|
+
.at-dot {
|
|
190
|
+
aspect-ratio: 1;
|
|
191
|
+
border-radius: 1px;
|
|
192
|
+
background: currentColor;
|
|
193
|
+
opacity: var(--at-seed);
|
|
194
|
+
animation: at-comet-cell var(--at-period) linear infinite;
|
|
195
|
+
}
|
|
196
|
+
/* One lap: the head peaks at 0%/100% and the tail falls off over 30%,
|
|
197
|
+
floored at 0.12 (agentThinking.ts cometOpacity). */
|
|
198
|
+
@keyframes at-comet-cell {
|
|
199
|
+
0% {
|
|
200
|
+
opacity: 1;
|
|
201
|
+
}
|
|
202
|
+
10% {
|
|
203
|
+
opacity: 0.599;
|
|
204
|
+
}
|
|
205
|
+
20% {
|
|
206
|
+
opacity: 0.289;
|
|
207
|
+
}
|
|
208
|
+
30% {
|
|
209
|
+
opacity: 0.12;
|
|
210
|
+
}
|
|
211
|
+
95% {
|
|
212
|
+
opacity: 0.12;
|
|
213
|
+
}
|
|
214
|
+
100% {
|
|
215
|
+
opacity: 1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* ── stars ────────────────────────────────────────────────────────── */
|
|
220
|
+
.at-stars {
|
|
221
|
+
position: relative;
|
|
222
|
+
display: block;
|
|
223
|
+
flex-shrink: 0;
|
|
224
|
+
width: 1.3em;
|
|
225
|
+
height: 1.3em;
|
|
226
|
+
color: color-mix(in oklab, currentColor 80%, transparent);
|
|
227
|
+
}
|
|
228
|
+
.at-star {
|
|
229
|
+
position: absolute;
|
|
230
|
+
translate: -50% -50%;
|
|
231
|
+
transform: scale(0);
|
|
232
|
+
animation-name: at-star;
|
|
233
|
+
animation-timing-function: ease-in-out;
|
|
234
|
+
animation-iteration-count: infinite;
|
|
235
|
+
}
|
|
236
|
+
@keyframes at-star {
|
|
237
|
+
0%,
|
|
238
|
+
100% {
|
|
239
|
+
transform: scale(0);
|
|
240
|
+
opacity: 0;
|
|
241
|
+
}
|
|
242
|
+
40% {
|
|
243
|
+
transform: scale(1);
|
|
244
|
+
opacity: 1;
|
|
245
|
+
}
|
|
246
|
+
60% {
|
|
247
|
+
transform: scale(0.8);
|
|
248
|
+
opacity: 0.9;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* ── infinity ─────────────────────────────────────────────────────── */
|
|
253
|
+
.at-infinity {
|
|
254
|
+
flex-shrink: 0;
|
|
255
|
+
width: 2em;
|
|
256
|
+
height: 1em;
|
|
257
|
+
/* the figure-eight spans x 9–47 of 56: pull the dead space back in */
|
|
258
|
+
margin: 0 -0.25em;
|
|
259
|
+
}
|
|
260
|
+
.at-comet {
|
|
261
|
+
animation: at-dash 1200ms linear infinite;
|
|
262
|
+
}
|
|
263
|
+
@keyframes at-dash {
|
|
264
|
+
to {
|
|
265
|
+
stroke-dashoffset: -100;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@media (prefers-reduced-motion: reduce) {
|
|
270
|
+
.at-dot,
|
|
271
|
+
.at-comet {
|
|
272
|
+
animation: none;
|
|
273
|
+
}
|
|
274
|
+
.at-star {
|
|
275
|
+
animation: none;
|
|
276
|
+
transform: none;
|
|
277
|
+
opacity: 0.7;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type AgentThinkingTone, type AgentThinkingVariant } from './agentThinking';
|
|
2
|
+
interface Props {
|
|
3
|
+
variant?: AgentThinkingVariant;
|
|
4
|
+
/** Status label, e.g. "Thinking" or "Searching the docs". */
|
|
5
|
+
label?: string;
|
|
6
|
+
/** Indicator + label tone. Defaults per variant (`stars` is subtle). */
|
|
7
|
+
tone?: AgentThinkingTone;
|
|
8
|
+
/** Travelling highlight across the label. */
|
|
9
|
+
shimmer?: boolean;
|
|
10
|
+
/** Elapsed time after the label. */
|
|
11
|
+
showTimer?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* When the work started (epoch ms, Date.now() basis). Defaults to mount
|
|
14
|
+
* time. Pass it when the row can re-mount so the timer keeps counting.
|
|
15
|
+
*/
|
|
16
|
+
startedAt?: number;
|
|
17
|
+
size?: 'sm' | 'md';
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
declare const AgentThinking: import("svelte").Component<Props, {}, "">;
|
|
21
|
+
type AgentThinking = ReturnType<typeof AgentThinking>;
|
|
22
|
+
export default AgentThinking;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
ComposerGlow — wraps a composer and, while `active`, orbits a band of light
|
|
3
|
+
around its rim: a crisp line leading, a small glow and a soft bloom bleeding
|
|
4
|
+
inward (clipped by the box). Flip `active` while awaiting a reply; the light
|
|
5
|
+
fades in/out instead of snapping.
|
|
6
|
+
|
|
7
|
+
MIT License — Copyright (c) 2026 Mertcan Dundar Esmergul (BoardUI)
|
|
8
|
+
Adapted from BoardUI's composer-loader.tsx: the rounded-rect perimeter math,
|
|
9
|
+
the pathLength=100 dash + CSS dashoffset lap, the per-layer "head trails the
|
|
10
|
+
tip by N px" phase, the three blur scales, SVG-native feGaussianBlur (WebKit
|
|
11
|
+
ignores CSS `filter` on SVG children) and the ResizeObserver that keeps the
|
|
12
|
+
viewBox equal to the px box so stroke widths stay true.
|
|
13
|
+
Changed for jatui: colours are theme tokens (info → primary → mid →
|
|
14
|
+
secondary → accent, midpoint via color-mix in oklch — no hex, no JS colour
|
|
15
|
+
parsing); the surface is --color-base-100 on the box radius; knobs trimmed
|
|
16
|
+
to active / speed / intensity / radius; the 14-copy taper is dropped; the
|
|
17
|
+
lap only runs while visible; and on data-motion="calm" or under
|
|
18
|
+
prefers-reduced-motion there is no frozen arc — the light is replaced by a
|
|
19
|
+
plain static focus-ring tint.
|
|
20
|
+
|
|
21
|
+
The wrapped composer must not paint its own background (the glow paints the
|
|
22
|
+
surface under it):
|
|
23
|
+
|
|
24
|
+
<ComposerGlow active={busy}>
|
|
25
|
+
<MessageInput {busy} onStop={stop} onsend={send} />
|
|
26
|
+
</ComposerGlow>
|
|
27
|
+
-->
|
|
28
|
+
<script lang="ts">
|
|
29
|
+
import { untrack, type Snippet } from 'svelte';
|
|
30
|
+
|
|
31
|
+
interface Props {
|
|
32
|
+
/** Show the light. Fades in/out. */
|
|
33
|
+
active?: boolean;
|
|
34
|
+
/** Seconds per lap. Default 4.5. */
|
|
35
|
+
speed?: number;
|
|
36
|
+
intensity?: 'subtle' | 'normal' | 'vivid';
|
|
37
|
+
/**
|
|
38
|
+
* Corner radius: px number or any CSS length. Default: the box radius
|
|
39
|
+
* (--jatui-radius-box → --radius-box).
|
|
40
|
+
*/
|
|
41
|
+
radius?: number | string;
|
|
42
|
+
class?: string;
|
|
43
|
+
children: Snippet;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let {
|
|
47
|
+
active = false,
|
|
48
|
+
speed = 4.5,
|
|
49
|
+
intensity = 'normal',
|
|
50
|
+
radius,
|
|
51
|
+
class: className = '',
|
|
52
|
+
children
|
|
53
|
+
}: Props = $props();
|
|
54
|
+
|
|
55
|
+
const uid = $props.id();
|
|
56
|
+
const gid = `jatui-cg-${uid.replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
57
|
+
|
|
58
|
+
const INTENSITY = {
|
|
59
|
+
subtle: { light: 0.45, bloom: 0.22 },
|
|
60
|
+
normal: { light: 0.7, bloom: 0.3 },
|
|
61
|
+
vivid: { light: 0.95, bloom: 0.42 }
|
|
62
|
+
} as const;
|
|
63
|
+
const level = $derived(INTENSITY[intensity] ?? INTENSITY.normal);
|
|
64
|
+
|
|
65
|
+
const cssRadius = $derived(
|
|
66
|
+
radius == null
|
|
67
|
+
? 'var(--jatui-radius-box, var(--radius-box, 0.5rem))'
|
|
68
|
+
: typeof radius === 'number'
|
|
69
|
+
? `${radius}px`
|
|
70
|
+
: radius
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// ── Geometry (the px box, so stroke widths are true) ────────────────
|
|
74
|
+
let clipEl = $state<HTMLSpanElement | null>(null);
|
|
75
|
+
let box = $state({ w: 640, h: 52, r: 8 });
|
|
76
|
+
|
|
77
|
+
$effect(() => {
|
|
78
|
+
const el = clipEl;
|
|
79
|
+
if (!el || typeof ResizeObserver === 'undefined') return;
|
|
80
|
+
const measure = () => {
|
|
81
|
+
const w = Math.max(1, el.clientWidth);
|
|
82
|
+
const h = Math.max(1, el.clientHeight);
|
|
83
|
+
const raw = getComputedStyle(el).borderTopLeftRadius;
|
|
84
|
+
const n = parseFloat(raw);
|
|
85
|
+
const r = Number.isFinite(n) ? (raw.trim().endsWith('%') ? (n / 100) * h : n) : 8;
|
|
86
|
+
untrack(() => {
|
|
87
|
+
if (w !== box.w || h !== box.h || r !== box.r) box = { w, h, r };
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
measure();
|
|
91
|
+
const ro = new ResizeObserver(measure);
|
|
92
|
+
ro.observe(el);
|
|
93
|
+
return () => ro.disconnect();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Band = 120° of the lap; soft layers are slightly shorter and their heads
|
|
97
|
+
// sit N px behind the crisp tip.
|
|
98
|
+
const BAND = (120 / 360) * 100;
|
|
99
|
+
const LINE = 2.5;
|
|
100
|
+
const BLOOM = 16;
|
|
101
|
+
const rx = $derived(Math.min(box.r, box.w / 2, box.h / 2));
|
|
102
|
+
// Rounded-rect perimeter: straight runs + four quarter arcs.
|
|
103
|
+
const perimeter = $derived(Math.max(1, 2 * (box.w + box.h) - 8 * rx + 2 * Math.PI * rx));
|
|
104
|
+
|
|
105
|
+
/** Animation delay putting a layer `backPx` behind the tip (negative = head start, no stall). */
|
|
106
|
+
function delayFor(len: number, backPx: number): string {
|
|
107
|
+
const phase = len - BAND + (backPx * 100) / perimeter; // path units behind
|
|
108
|
+
const frac = (((phase / 100) % 1) + 1) % 1;
|
|
109
|
+
return `${((frac - 1) * speed).toFixed(3)}s`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const layers = $derived([
|
|
113
|
+
// wide bloom bleeding inward (outer half clipped by the box)
|
|
114
|
+
{ width: BLOOM * 2, blur: 14, opacity: level.bloom, len: BAND * 0.9, back: BLOOM + 16 },
|
|
115
|
+
// tight glow
|
|
116
|
+
{ width: LINE * 3.2, blur: 6, opacity: 0.8, len: BAND * 0.95, back: 10 },
|
|
117
|
+
// crisp line, tip leading
|
|
118
|
+
{ width: LINE, blur: 0.5, opacity: 1, len: BAND, back: 0 }
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
// Run the lap only while it can be seen: start on activate, stop after
|
|
122
|
+
// the fade-out has finished.
|
|
123
|
+
let running = $state(false);
|
|
124
|
+
$effect(() => {
|
|
125
|
+
if (active) {
|
|
126
|
+
running = true;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const t = setTimeout(() => (running = false), 700);
|
|
130
|
+
return () => clearTimeout(t);
|
|
131
|
+
});
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<div
|
|
135
|
+
class="composer-glow {className}"
|
|
136
|
+
class:composer-glow--active={active}
|
|
137
|
+
style:--cg-radius={cssRadius}
|
|
138
|
+
style:--cg-speed="{speed}s"
|
|
139
|
+
>
|
|
140
|
+
<span class="composer-glow-surface" aria-hidden="true"></span>
|
|
141
|
+
<span class="composer-glow-tint" aria-hidden="true"></span>
|
|
142
|
+
<span class="composer-glow-light" bind:this={clipEl} aria-hidden="true">
|
|
143
|
+
{#if running}
|
|
144
|
+
<svg
|
|
145
|
+
width="100%"
|
|
146
|
+
height="100%"
|
|
147
|
+
viewBox="0 0 {box.w} {box.h}"
|
|
148
|
+
preserveAspectRatio="none"
|
|
149
|
+
style:opacity={level.light}
|
|
150
|
+
>
|
|
151
|
+
<defs>
|
|
152
|
+
<linearGradient id={gid} x1="0" y1="0" x2="1" y2="0">
|
|
153
|
+
<stop offset="0%" style="stop-color: var(--color-info)" />
|
|
154
|
+
<stop offset="30%" style="stop-color: var(--color-primary)" />
|
|
155
|
+
<stop offset="50%" style="stop-color: color-mix(in oklch, var(--color-primary), var(--color-secondary))" />
|
|
156
|
+
<stop offset="70%" style="stop-color: var(--color-secondary)" />
|
|
157
|
+
<stop offset="100%" style="stop-color: var(--color-accent)" />
|
|
158
|
+
</linearGradient>
|
|
159
|
+
{#each layers as layer (layer.blur)}
|
|
160
|
+
<filter id="{gid}-b{String(layer.blur).replace('.', '_')}" x="-50%" y="-50%" width="200%" height="200%">
|
|
161
|
+
<feGaussianBlur stdDeviation={layer.blur} />
|
|
162
|
+
</filter>
|
|
163
|
+
{/each}
|
|
164
|
+
</defs>
|
|
165
|
+
{#each layers as layer (layer.blur)}
|
|
166
|
+
<rect
|
|
167
|
+
class="composer-glow-rect jatui-motion-ambient"
|
|
168
|
+
x="0"
|
|
169
|
+
y="0"
|
|
170
|
+
width={box.w}
|
|
171
|
+
height={box.h}
|
|
172
|
+
{rx}
|
|
173
|
+
pathLength="100"
|
|
174
|
+
fill="none"
|
|
175
|
+
stroke="url(#{gid})"
|
|
176
|
+
stroke-width={layer.width}
|
|
177
|
+
stroke-linecap="round"
|
|
178
|
+
stroke-dasharray="{layer.len} {100 - layer.len}"
|
|
179
|
+
filter="url(#{gid}-b{String(layer.blur).replace('.', '_')})"
|
|
180
|
+
style:opacity={layer.opacity}
|
|
181
|
+
style:animation-delay={delayFor(layer.len, layer.back)}
|
|
182
|
+
/>
|
|
183
|
+
{/each}
|
|
184
|
+
</svg>
|
|
185
|
+
{/if}
|
|
186
|
+
</span>
|
|
187
|
+
<div class="composer-glow-content">{@render children()}</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<style>
|
|
191
|
+
.composer-glow {
|
|
192
|
+
position: relative;
|
|
193
|
+
border-radius: var(--cg-radius);
|
|
194
|
+
/* 1 = decorative motion allowed (motion.css); 0 on calm / reduced motion. */
|
|
195
|
+
--cg-ambient: var(--motion-ambient, 1);
|
|
196
|
+
--cg-fade: calc(var(--motion-move, 300ms) * 1.5);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.composer-glow-surface,
|
|
200
|
+
.composer-glow-tint,
|
|
201
|
+
.composer-glow-light {
|
|
202
|
+
position: absolute;
|
|
203
|
+
inset: 0;
|
|
204
|
+
border-radius: var(--cg-radius);
|
|
205
|
+
pointer-events: none;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.composer-glow-surface {
|
|
209
|
+
background: var(--color-base-100);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Calm / reduced-motion stand-in: a plain static focus-ring tint. */
|
|
213
|
+
.composer-glow-tint {
|
|
214
|
+
box-shadow:
|
|
215
|
+
0 0 0 2px color-mix(in oklch, var(--color-primary) 30%, transparent),
|
|
216
|
+
inset 0 0 0 1px color-mix(in oklch, var(--color-primary) 45%, transparent);
|
|
217
|
+
opacity: 0;
|
|
218
|
+
transition: opacity var(--cg-fade) var(--motion-glide, ease);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.composer-glow-light {
|
|
222
|
+
overflow: hidden;
|
|
223
|
+
opacity: 0;
|
|
224
|
+
transition: opacity var(--cg-fade) var(--motion-glide, ease);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.composer-glow--active .composer-glow-light {
|
|
228
|
+
opacity: var(--cg-ambient);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.composer-glow--active .composer-glow-tint {
|
|
232
|
+
opacity: calc(1 - var(--cg-ambient));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.composer-glow-light svg {
|
|
236
|
+
display: block;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.composer-glow-rect {
|
|
240
|
+
animation: composer-glow-lap var(--cg-speed) linear infinite;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@keyframes composer-glow-lap {
|
|
244
|
+
from {
|
|
245
|
+
stroke-dashoffset: 0;
|
|
246
|
+
}
|
|
247
|
+
to {
|
|
248
|
+
stroke-dashoffset: -100;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.composer-glow-content {
|
|
253
|
+
position: relative;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* Hosts without motion.css still get the reduced-motion swap. */
|
|
257
|
+
@media (prefers-reduced-motion: reduce) {
|
|
258
|
+
.composer-glow {
|
|
259
|
+
--cg-ambient: 0;
|
|
260
|
+
}
|
|
261
|
+
.composer-glow-rect {
|
|
262
|
+
animation: none;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Show the light. Fades in/out. */
|
|
4
|
+
active?: boolean;
|
|
5
|
+
/** Seconds per lap. Default 4.5. */
|
|
6
|
+
speed?: number;
|
|
7
|
+
intensity?: 'subtle' | 'normal' | 'vivid';
|
|
8
|
+
/**
|
|
9
|
+
* Corner radius: px number or any CSS length. Default: the box radius
|
|
10
|
+
* (--jatui-radius-box → --radius-box).
|
|
11
|
+
*/
|
|
12
|
+
radius?: number | string;
|
|
13
|
+
class?: string;
|
|
14
|
+
children: Snippet;
|
|
15
|
+
}
|
|
16
|
+
declare const ComposerGlow: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type ComposerGlow = ReturnType<typeof ComposerGlow>;
|
|
18
|
+
export default ComposerGlow;
|