@juspay/svelte-ui-components 2.130.0 → 2.131.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.
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
+ import type { Action } from 'svelte/action';
2
3
  import Accordion from '../Accordion/Accordion.svelte';
3
4
  import Button from '../Button/Button.svelte';
4
5
  import Loader from '../Loader/Loader.svelte';
6
+ import Pill from '../Pill/Pill.svelte';
5
7
  import type { ThinkingIndicatorProperties } from './properties';
6
8
 
7
9
  let {
@@ -9,6 +11,7 @@
9
11
  detail,
10
12
  expanded = $bindable(false),
11
13
  variant = 'default',
14
+ showElapsed = false,
12
15
  onToggle,
13
16
  avatar,
14
17
  toggleIcon,
@@ -16,25 +19,197 @@
16
19
  toggleTestId,
17
20
  detailTestId,
18
21
  labelTestId,
19
- classes
22
+ classes,
23
+ rows,
24
+ kind = 'steps',
25
+ busy,
26
+ query,
27
+ moreLabel,
28
+ selectable = false,
29
+ selected = $bindable(null),
30
+ onrowselect,
31
+ onsettled,
32
+ collapseDelayMs = 2600
20
33
  }: ThinkingIndicatorProperties = $props();
21
34
 
22
- // A detail string is what makes the indicator expandable. Without one there is
23
- // nothing to reveal, so it renders as a plain live status line instead of a
35
+ // `rows` being present at all (even `[]`) puts the indicator into trace mode: the
36
+ // Accordion body renders the kind-aware trace instead of the `detail` paragraph,
37
+ // and that alone is enough to make the indicator expandable.
38
+ const hasTraceMode = $derived(Array.isArray(rows));
39
+ const traceRows = $derived(rows ?? []);
40
+
41
+ // A detail string or a trace is what makes the indicator expandable. Without either
42
+ // there is nothing to reveal, so it renders as a plain live status line instead of a
24
43
  // disclosure control. `bare` overrides that entirely.
25
44
  const isExpandable = $derived(
26
- variant !== 'bare' && typeof detail === 'string' && detail.length > 0
45
+ variant !== 'bare' && (hasTraceMode || (typeof detail === 'string' && detail.length > 0))
27
46
  );
28
47
 
48
+ // Backward-compatible shimmer rule: without an explicit `busy`, the expandable
49
+ // summary holds still (settled) and every other shape shimmers (live) — exactly
50
+ // today's released behaviour. Passing `busy` takes direct control of the shimmer
51
+ // in every shape, including `bare`.
52
+ const labelIsBusy = $derived(busy ?? !isExpandable);
53
+
54
+ // Set once a person clicks the disclosure open/closed — from then on the automatic
55
+ // busy-driven machine below leaves `expanded` alone for the rest of this mount.
56
+ let manuallyToggled = false;
57
+
29
58
  const handleToggle = (): void => {
59
+ manuallyToggled = true;
30
60
  expanded = !expanded;
31
61
  onToggle?.();
32
62
  };
63
+
64
+ // Elapsed counter: ticks while the label is live (see `labelIsBusy`); freezes at its
65
+ // last value once it settles. Resets to 0 each time a fresh busy phase starts.
66
+ let elapsedSeconds = $state(0);
67
+ let hasTicked = $state(false);
68
+ let tickInterval: ReturnType<typeof setInterval> | null = null;
69
+
70
+ const startTicking = (): void => {
71
+ elapsedSeconds = 0;
72
+ hasTicked = true;
73
+ if (tickInterval) {
74
+ clearInterval(tickInterval);
75
+ }
76
+ tickInterval = setInterval(() => {
77
+ elapsedSeconds += 1;
78
+ }, 1000);
79
+ };
80
+
81
+ const stopTicking = (): void => {
82
+ if (tickInterval) {
83
+ clearInterval(tickInterval);
84
+ tickInterval = null;
85
+ }
86
+ };
87
+
88
+ const elapsedWatcher: Action<HTMLElement, boolean> = (_node, initialShouldTick) => {
89
+ // Reset-and-start happens inside `startTicking` itself, and only when this
90
+ // mount is actually asked to tick. That matters because this same action is
91
+ // mounted on BOTH the status-host and expandable roots, which are separate
92
+ // elements swapped by the `{#if}` above: when a `detail`/`rows`-less status
93
+ // line becomes expandable mid-life, Svelte destroys the status-host node and
94
+ // mounts a fresh expandable one, re-running this action with an initial value
95
+ // that is now `false` (busy has already settled) — skipping the reset here
96
+ // lets `hasTicked`/`elapsedSeconds` carry the frozen final value across that
97
+ // swap instead of wiping it back to 0/false.
98
+ let previous = initialShouldTick;
99
+ if (initialShouldTick) {
100
+ startTicking();
101
+ }
102
+ return {
103
+ update(nowShouldTick: boolean): void {
104
+ if (nowShouldTick !== previous) {
105
+ previous = nowShouldTick;
106
+ if (nowShouldTick) {
107
+ startTicking();
108
+ } else {
109
+ // Freeze, don't hide: stop the interval but leave `hasTicked` set so
110
+ // the expandable branch keeps showing the final elapsed value. The
111
+ // `showElapsed` prop itself (not `hasTicked`) is what lets a host
112
+ // hide the counter outright.
113
+ stopTicking();
114
+ }
115
+ }
116
+ },
117
+ destroy(): void {
118
+ stopTicking();
119
+ }
120
+ };
121
+ };
122
+
123
+ // Disclosure machine (the ThinkingTrace settleWatcher pattern, adapted to a plain
124
+ // bindable boolean instead of a tri-state): auto-open while busy, auto-collapse a
125
+ // beat after settling. A manual toggle (handleToggle, above) permanently takes over
126
+ // for the rest of this mount. Only engages once a host passes `busy` at all —
127
+ // omitting it keeps today's released behaviour (manual toggling only, no timers).
128
+ let settledOnce = false;
129
+ let collapseTimer: ReturnType<typeof setTimeout> | null = null;
130
+
131
+ const applyBusyChange = (nowBusy: boolean): void => {
132
+ if (nowBusy) {
133
+ settledOnce = false;
134
+ if (collapseTimer) {
135
+ clearTimeout(collapseTimer);
136
+ collapseTimer = null;
137
+ }
138
+ if (!manuallyToggled) {
139
+ expanded = true;
140
+ }
141
+ return;
142
+ }
143
+ if (!settledOnce) {
144
+ settledOnce = true;
145
+ onsettled?.();
146
+ if (collapseDelayMs !== null && !manuallyToggled) {
147
+ collapseTimer = setTimeout(() => {
148
+ if (!manuallyToggled) {
149
+ expanded = false;
150
+ }
151
+ }, collapseDelayMs);
152
+ }
153
+ }
154
+ };
155
+
156
+ const busyWatcher: Action<HTMLElement, boolean | null> = (_node, initialBusy) => {
157
+ let previous = initialBusy;
158
+ if (initialBusy === true) {
159
+ expanded = true;
160
+ } else if (initialBusy === false) {
161
+ settledOnce = true;
162
+ }
163
+ return {
164
+ update(nowBusy: boolean | null): void {
165
+ if (nowBusy !== previous) {
166
+ previous = nowBusy;
167
+ if (typeof nowBusy === 'boolean') {
168
+ applyBusyChange(nowBusy);
169
+ }
170
+ }
171
+ },
172
+ destroy(): void {
173
+ if (collapseTimer) {
174
+ clearTimeout(collapseTimer);
175
+ }
176
+ }
177
+ };
178
+ };
179
+
180
+ // Trace rows appended in one update stagger relative to the batch, not the list start.
181
+ let staggerBase = $state(0);
182
+
183
+ const growthWatcher: Action<HTMLElement, number> = (_node, initialCount) => {
184
+ let previousCount = initialCount;
185
+ return {
186
+ update(count: number): void {
187
+ if (count > previousCount) {
188
+ staggerBase = previousCount;
189
+ }
190
+ previousCount = count;
191
+ }
192
+ };
193
+ };
194
+
195
+ const rowDelay = (index: number): string => {
196
+ return `${Math.max(0, index - staggerBase) * 120}ms`;
197
+ };
198
+
199
+ const handleRowSelect = (index: number): void => {
200
+ selected = selected === index ? null : index;
201
+ onrowselect?.(selected);
202
+ };
203
+
204
+ let rowsHeight = $state(0);
33
205
  </script>
34
206
 
35
207
  {#if isExpandable}
36
208
  <div
37
209
  class="thinking-indicator expandable {classes ?? ''}"
210
+ class:busy={busy === true}
211
+ use:busyWatcher={busy ?? null}
212
+ use:elapsedWatcher={showElapsed && labelIsBusy}
38
213
  data-pw={typeof testId === 'string' ? testId : null}
39
214
  testID={typeof testId === 'string' ? testId : null}
40
215
  >
@@ -45,37 +220,186 @@
45
220
  testId={toggleTestId ?? (testId && `${testId}-toggle`)}
46
221
  >
47
222
  <span class="status-row">
48
- <span class="avatar">
49
- {#if avatar}{@render avatar()}{:else}<Loader />{/if}
50
- </span>
51
- <span class="status-label static-label" data-pw={labelTestId ?? null}>{label}</span>
52
- </span>
53
- <span class="arrow" class:expanded aria-hidden="true">
54
- {#if toggleIcon}
55
- {@render toggleIcon()}
56
- {:else}
57
- <svg viewBox="0 0 16 16" fill="none">
58
- <path
59
- d="M4 6l4 4 4-4"
60
- stroke="currentColor"
61
- stroke-width="1.5"
62
- stroke-linecap="round"
63
- stroke-linejoin="round"
64
- />
65
- </svg>
223
+ {#if avatar || labelIsBusy}
224
+ <span class="avatar">
225
+ {#if avatar}{@render avatar()}{:else}<Loader />{/if}
226
+ </span>
66
227
  {/if}
228
+ <span class="label-cluster">
229
+ <span
230
+ class="status-label"
231
+ class:static-label={!labelIsBusy}
232
+ data-pw={labelTestId ?? null}>{label}</span
233
+ >
234
+ <span class="arrow" class:expanded aria-hidden="true">
235
+ {#if toggleIcon}
236
+ {@render toggleIcon()}
237
+ {:else}
238
+ <svg viewBox="0 0 16 16" fill="none">
239
+ <path
240
+ d="M4 6l4 4 4-4"
241
+ stroke="currentColor"
242
+ stroke-width="1.5"
243
+ stroke-linecap="round"
244
+ stroke-linejoin="round"
245
+ />
246
+ </svg>
247
+ {/if}
248
+ </span>
249
+ </span>
67
250
  </span>
251
+ {#if showElapsed && hasTicked}
252
+ <span
253
+ class="elapsed"
254
+ data-pw={typeof testId === 'string' ? `${testId}-elapsed` : null}
255
+ testID={typeof testId === 'string' ? `${testId}-elapsed` : null}>{elapsedSeconds}s</span
256
+ >
257
+ {/if}
68
258
  </Button>
69
259
  </div>
70
260
  <Accordion expand={expanded}>
71
- <p class="detail" data-pw={detailTestId ?? (testId && `${testId}-detail`) ?? null}>
72
- {detail}
73
- </p>
261
+ <div class="thinking-indicator-body" inert={!expanded}>
262
+ {#if hasTraceMode}
263
+ {#if kind === 'search' && typeof query === 'string' && query.length > 0}
264
+ <span class="trace-query-wrap">
265
+ <Pill text={query}>
266
+ {#snippet leadingIcon()}
267
+ <svg
268
+ viewBox="0 0 24 24"
269
+ fill="none"
270
+ stroke="currentColor"
271
+ stroke-width="2"
272
+ aria-hidden="true"
273
+ >
274
+ <circle cx="11" cy="11" r="7" />
275
+ <path d="m21 21-4.3-4.3" stroke-linecap="round" />
276
+ </svg>
277
+ {/snippet}
278
+ </Pill>
279
+ </span>
280
+ {/if}
281
+ <div class="trace-body">
282
+ <span class="trace-connector" style:height="{rowsHeight}px" aria-hidden="true"></span>
283
+ <div
284
+ class="trace-rows"
285
+ use:growthWatcher={traceRows.length}
286
+ bind:clientHeight={rowsHeight}
287
+ >
288
+ {#each traceRows as row, index (index)}
289
+ {#if kind === 'coding' && selectable}
290
+ <button
291
+ class="trace-row selectable"
292
+ class:selected={selected === index}
293
+ type="button"
294
+ aria-pressed={selected === index}
295
+ style:animation-delay={rowDelay(index)}
296
+ onclick={() => handleRowSelect(index)}
297
+ >
298
+ <b class="row-primary">{row.primary}</b>
299
+ {#if row.secondary}<span class="row-secondary" class:mono={row.mono}
300
+ >{row.secondary}</span
301
+ >{/if}
302
+ {#if typeof row.added === 'number' || typeof row.removed === 'number'}
303
+ <span class="row-diffstat">
304
+ {#if typeof row.added === 'number'}<span class="added">+{row.added}</span
305
+ >{/if}
306
+ {#if typeof row.removed === 'number'}<span class="removed"
307
+ >−{row.removed}</span
308
+ >{/if}
309
+ </span>
310
+ {/if}
311
+ </button>
312
+ {:else if kind === 'search' && typeof row.href === 'string'}
313
+ <a
314
+ class="trace-row linked"
315
+ href={row.href}
316
+ target="_blank"
317
+ rel="noopener noreferrer"
318
+ style:animation-delay={rowDelay(index)}
319
+ >
320
+ <span class="row-badge" data-tone={(index % 3) + 1} aria-hidden="true">
321
+ <svg
322
+ viewBox="0 0 24 24"
323
+ fill="none"
324
+ stroke="currentColor"
325
+ stroke-width="3"
326
+ stroke-linecap="round"
327
+ stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg
328
+ >
329
+ </span>
330
+ <b class="row-primary">{row.primary}</b>
331
+ {#if row.secondary}<span class="row-secondary">{row.secondary}</span>{/if}
332
+ </a>
333
+ {:else}
334
+ <div
335
+ class="trace-row"
336
+ class:prose={kind === 'reasoning'}
337
+ style:animation-delay={rowDelay(index)}
338
+ >
339
+ {#if kind === 'steps'}
340
+ <span class="row-icon" aria-hidden="true">
341
+ {#if busy && index === traceRows.length - 1}
342
+ <Loader />
343
+ {:else}
344
+ <svg
345
+ viewBox="0 0 24 24"
346
+ fill="none"
347
+ stroke="currentColor"
348
+ stroke-width="2.4"
349
+ stroke-linecap="round"
350
+ stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg
351
+ >
352
+ {/if}
353
+ </span>
354
+ {:else if kind === 'search'}
355
+ <span class="row-badge" data-tone={(index % 3) + 1} aria-hidden="true">
356
+ <svg
357
+ viewBox="0 0 24 24"
358
+ fill="none"
359
+ stroke="currentColor"
360
+ stroke-width="3"
361
+ stroke-linecap="round"
362
+ stroke-linejoin="round"><path d="M20 6 9 17l-5-5" /></svg
363
+ >
364
+ </span>
365
+ {/if}
366
+ {#if kind === 'reasoning'}
367
+ {row.primary}
368
+ {:else}
369
+ <b class="row-primary">{row.primary}</b>
370
+ {/if}
371
+ {#if row.secondary}<span class="row-secondary" class:mono={row.mono}
372
+ >{row.secondary}</span
373
+ >{/if}
374
+ {#if typeof row.added === 'number' || typeof row.removed === 'number'}
375
+ <span class="row-diffstat">
376
+ {#if typeof row.added === 'number'}<span class="added">+{row.added}</span
377
+ >{/if}
378
+ {#if typeof row.removed === 'number'}<span class="removed"
379
+ >−{row.removed}</span
380
+ >{/if}
381
+ </span>
382
+ {/if}
383
+ </div>
384
+ {/if}
385
+ {/each}
386
+ </div>
387
+ </div>
388
+ {#if !busy && typeof moreLabel === 'string' && moreLabel.length > 0}
389
+ <div class="trace-more">{moreLabel}</div>
390
+ {/if}
391
+ {:else}
392
+ <p class="detail" data-pw={detailTestId ?? (testId && `${testId}-detail`) ?? null}>
393
+ {detail}
394
+ </p>
395
+ {/if}
396
+ </div>
74
397
  </Accordion>
75
398
  </div>
76
399
  {:else if variant === 'bare'}
77
400
  <span
78
401
  class="status-label {classes ?? ''}"
402
+ class:static-label={!labelIsBusy}
79
403
  data-pw={typeof testId === 'string' ? testId : null}
80
404
  testID={typeof testId === 'string' ? testId : null}
81
405
  >{#if typeof labelTestId === 'string'}<span data-pw={labelTestId} testID={labelTestId}
@@ -85,6 +409,7 @@
85
409
  {:else}
86
410
  <div
87
411
  class="thinking-indicator status-host {classes ?? ''}"
412
+ use:elapsedWatcher={showElapsed && labelIsBusy}
88
413
  data-pw={typeof testId === 'string' ? testId : null}
89
414
  testID={typeof testId === 'string' ? testId : null}
90
415
  >
@@ -92,12 +417,51 @@
92
417
  <span class="avatar">
93
418
  {#if avatar}{@render avatar()}{:else}<Loader />{/if}
94
419
  </span>
95
- <span class="status-label" data-pw={labelTestId ?? null}>{label}</span>
420
+ <span class="status-label" class:static-label={!labelIsBusy} data-pw={labelTestId ?? null}
421
+ >{label}</span
422
+ >
96
423
  </span>
424
+ {#if showElapsed}
425
+ <span
426
+ class="elapsed"
427
+ data-pw={typeof testId === 'string' ? `${testId}-elapsed` : null}
428
+ testID={typeof testId === 'string' ? `${testId}-elapsed` : null}>{elapsedSeconds}s</span
429
+ >
430
+ {/if}
97
431
  </div>
98
432
  {/if}
99
433
 
100
434
  <style>
435
+ @keyframes thinking-indicator-shimmer {
436
+ 0% {
437
+ background-position: 200% 0;
438
+ }
439
+
440
+ 100% {
441
+ background-position: -200% 0;
442
+ }
443
+ }
444
+
445
+ @keyframes thinking-indicator-fade-up {
446
+ from {
447
+ opacity: 0;
448
+ transform: translateY(9px);
449
+ }
450
+ to {
451
+ opacity: 1;
452
+ transform: none;
453
+ }
454
+ }
455
+
456
+ @keyframes thinking-indicator-fade-in {
457
+ from {
458
+ opacity: 0;
459
+ }
460
+ to {
461
+ opacity: 1;
462
+ }
463
+ }
464
+
101
465
  .thinking-indicator {
102
466
  box-sizing: border-box;
103
467
  width: 100%;
@@ -107,6 +471,13 @@
107
471
  border-bottom: var(--thinking-indicator-border-bottom, 1px solid #e4e4e7);
108
472
  padding-block: var(--thinking-indicator-padding-block, 0.5rem);
109
473
  margin-bottom: var(--thinking-indicator-margin-bottom, 1rem);
474
+ min-height: 0;
475
+ transition: min-height 400ms
476
+ var(--thinking-indicator-trace-ease, cubic-bezier(0.23, 1, 0.32, 1));
477
+ }
478
+
479
+ .expandable.busy {
480
+ min-height: var(--thinking-indicator-trace-busy-min-height, 0px);
110
481
  }
111
482
 
112
483
  .toggle {
@@ -120,13 +491,17 @@
120
491
  --button-padding: 0;
121
492
  --button-justify-content: flex-start;
122
493
  --button-text-color: inherit;
494
+ --button-content-gap: var(--thinking-indicator-header-gap, 0.375rem);
123
495
  }
124
496
 
125
497
  /* A flex ROW host makes the inner status-row hug its content, so the shimmer
126
- gradient maps to the text's own width rather than the full container. */
498
+ gradient maps to the text's own width rather than the full container. Neither
499
+ form stretches to fill the width — a trailing gap after a short label is real
500
+ empty space, not a reserved box, so nothing pushes the elapsed counter away. */
127
501
  .status-host {
128
502
  display: flex;
129
503
  align-items: center;
504
+ gap: var(--thinking-indicator-header-gap, 0.375rem);
130
505
  }
131
506
 
132
507
  .status-row {
@@ -147,8 +522,17 @@
147
522
  --loader-height: var(--thinking-indicator-avatar-loader-size, 1rem);
148
523
  }
149
524
 
525
+ /* The chevron lives in the same flex cluster as the label so it hugs the text
526
+ immediately, instead of drifting to the far edge of the toggle button. */
527
+ .label-cluster {
528
+ display: flex;
529
+ align-items: center;
530
+ gap: var(--thinking-indicator-arrow-gap, 0.125rem);
531
+ min-width: 0;
532
+ }
533
+
150
534
  .status-label {
151
- flex: 1;
535
+ flex: 0 1 auto;
152
536
  min-width: 0;
153
537
  overflow: hidden;
154
538
  white-space: nowrap;
@@ -168,7 +552,7 @@
168
552
  infinite;
169
553
  }
170
554
 
171
- /* The expandable summary holds still; only live status lines shimmer. */
555
+ /* A settled label (no live busy phase) holds still instead of shimmering. */
172
556
  .static-label {
173
557
  animation: none;
174
558
  -webkit-text-fill-color: var(--thinking-indicator-label-color, #858585);
@@ -181,7 +565,6 @@
181
565
  flex-shrink: 0;
182
566
  width: var(--thinking-indicator-arrow-size, 1rem);
183
567
  height: var(--thinking-indicator-arrow-size, 1rem);
184
- margin-left: var(--thinking-indicator-arrow-margin-left, 0.25rem);
185
568
  color: var(--thinking-indicator-arrow-color, #7a7a7a);
186
569
  transform: rotate(-90deg);
187
570
  transition: var(--thinking-indicator-arrow-transition, transform 0.2s ease-in-out);
@@ -205,13 +588,216 @@
205
588
  text-align: left;
206
589
  }
207
590
 
208
- @keyframes thinking-indicator-shimmer {
209
- 0% {
210
- background-position: 200% 0;
591
+ .elapsed {
592
+ flex-shrink: 0;
593
+ font-variant-numeric: tabular-nums;
594
+ color: var(--thinking-indicator-elapsed-color, #9a9a9a);
595
+ font-size: var(--thinking-indicator-elapsed-font-size, 0.75rem);
596
+ }
597
+
598
+ /* ---- Trace body (rows?: ThinkingIndicatorTraceRow[]) ---- */
599
+
600
+ /* Wrapper exists only to carry `inert` while the Accordion has it collapsed —
601
+ no box of its own, so it can't disturb the detail/trace layout it wraps. */
602
+ .thinking-indicator-body {
603
+ display: contents;
604
+ }
605
+
606
+ .trace-query-wrap {
607
+ display: inline-flex;
608
+ margin: var(--thinking-indicator-trace-query-margin, 10px 0 0);
609
+ --pill-width: auto;
610
+ --pill-justify-content: flex-start;
611
+ --pill-cursor: default;
612
+ --pill-gap: 6px;
613
+ --pill-background: var(--thinking-indicator-trace-query-background, #f1f1f1);
614
+ --pill-color: var(--thinking-indicator-trace-query-color, #6b6b6b);
615
+ --pill-border-radius: var(--thinking-indicator-trace-query-radius, 999px);
616
+ --pill-padding: var(--thinking-indicator-trace-query-padding, 3px 11px);
617
+ --pill-font-size: var(--thinking-indicator-trace-query-font-size, 0.8125rem);
618
+ --pill-font-weight: 400;
619
+ }
620
+
621
+ .trace-query-wrap :global(svg) {
622
+ width: 11px;
623
+ height: 11px;
624
+ }
625
+
626
+ .trace-body {
627
+ display: flex;
628
+ gap: var(--thinking-indicator-trace-body-gap, 12px);
629
+ padding-top: var(--thinking-indicator-trace-body-padding-top, 10px);
630
+ }
631
+
632
+ .trace-connector {
633
+ width: 2px;
634
+ border-radius: 999px;
635
+ background: var(--thinking-indicator-trace-connector-color, #dcdcdc);
636
+ margin-left: 6px;
637
+ flex-shrink: 0;
638
+ transition: height 500ms var(--thinking-indicator-trace-ease, cubic-bezier(0.23, 1, 0.32, 1));
639
+ }
640
+
641
+ .trace-rows {
642
+ flex: 1;
643
+ min-width: 0;
644
+ display: flex;
645
+ flex-direction: column;
646
+ gap: var(--thinking-indicator-trace-row-gap, 7px);
647
+ align-self: flex-start;
648
+ }
649
+
650
+ .trace-row {
651
+ display: flex;
652
+ align-items: baseline;
653
+ gap: 8px;
654
+ font-size: var(--thinking-indicator-trace-row-font-size, 0.8125rem);
655
+ color: var(--thinking-indicator-trace-row-color, #2b2b2b);
656
+ animation: thinking-indicator-fade-up 320ms
657
+ var(--thinking-indicator-trace-ease, cubic-bezier(0.23, 1, 0.32, 1)) both;
658
+ text-decoration: none;
659
+ }
660
+
661
+ .trace-row.prose {
662
+ color: var(--thinking-indicator-trace-prose-color, #6b6b6b);
663
+ }
664
+
665
+ .trace-row.selectable {
666
+ border: none;
667
+ background: none;
668
+ font: inherit;
669
+ text-align: left;
670
+ padding: 3px 6px;
671
+ margin: -3px -6px;
672
+ border-radius: var(--thinking-indicator-trace-row-radius, 6px);
673
+ cursor: pointer;
674
+ transition: background 150ms ease;
675
+ width: calc(100% + 12px);
676
+ }
677
+
678
+ .trace-row.selectable:hover {
679
+ background: var(--thinking-indicator-trace-row-hover-background, #f4f4f4);
680
+ }
681
+
682
+ .trace-row.selectable.selected {
683
+ background: var(--thinking-indicator-trace-row-selected-background, #ececec);
684
+ }
685
+
686
+ .trace-row.linked:hover .row-primary {
687
+ text-decoration: underline;
688
+ }
689
+
690
+ .row-primary {
691
+ font-weight: var(--thinking-indicator-trace-row-weight, 500);
692
+ white-space: nowrap;
693
+ overflow: hidden;
694
+ text-overflow: ellipsis;
695
+ }
696
+
697
+ .row-secondary {
698
+ color: var(--thinking-indicator-trace-secondary-color, #9a9a9a);
699
+ font-size: var(--thinking-indicator-trace-secondary-font-size, 0.75rem);
700
+ white-space: nowrap;
701
+ }
702
+
703
+ .row-secondary.mono {
704
+ font-family: var(--thinking-indicator-trace-mono-font, ui-monospace, Menlo, monospace);
705
+ }
706
+
707
+ /* Steps kind: a static check icon, or — on the frontier row while busy — the
708
+ library Loader, recoloured/resized to match the row's small icon footprint. */
709
+ .row-icon {
710
+ display: inline-flex;
711
+ align-self: center;
712
+ width: 14px;
713
+ flex-shrink: 0;
714
+ color: var(--thinking-indicator-trace-icon-color, #9a9a9a);
715
+ --loader-width: var(--thinking-indicator-trace-spinner-size, 11px);
716
+ --loader-height: var(--thinking-indicator-trace-spinner-size, 11px);
717
+ --loader-before-width: 5px;
718
+ --loader-before-height: 5px;
719
+ --loader-after-width: 8px;
720
+ --loader-after-height: 8px;
721
+ --loader-foreground: var(--thinking-indicator-trace-spinner-color, #6b6b6b);
722
+ --loader-foreground-end: var(--thinking-indicator-trace-spinner-color-end, transparent);
723
+ --loader-background: var(--thinking-indicator-trace-spinner-track-color, #dcdcdc);
724
+ }
725
+
726
+ .row-icon svg {
727
+ width: 12px;
728
+ height: 12px;
729
+ }
730
+
731
+ .row-badge {
732
+ width: 15px;
733
+ height: 15px;
734
+ border-radius: 999px;
735
+ display: inline-flex;
736
+ align-items: center;
737
+ justify-content: center;
738
+ color: var(--thinking-indicator-trace-badge-check-color, #fff);
739
+ flex-shrink: 0;
740
+ align-self: center;
741
+ }
742
+
743
+ .row-badge[data-tone='1'] {
744
+ background: var(--thinking-indicator-trace-tone-1, #2f6fec);
745
+ }
746
+
747
+ .row-badge[data-tone='2'] {
748
+ background: var(--thinking-indicator-trace-tone-2, #e56d24);
749
+ }
750
+
751
+ .row-badge[data-tone='3'] {
752
+ background: var(--thinking-indicator-trace-tone-3, #1f7a5f);
753
+ }
754
+
755
+ .row-badge svg {
756
+ width: 9px;
757
+ height: 9px;
758
+ }
759
+
760
+ .row-diffstat {
761
+ margin-left: auto;
762
+ font-family: var(--thinking-indicator-trace-mono-font, ui-monospace, Menlo, monospace);
763
+ font-size: 0.75rem;
764
+ font-variant-numeric: tabular-nums;
765
+ white-space: nowrap;
766
+ display: inline-flex;
767
+ gap: 5px;
768
+ }
769
+
770
+ .row-diffstat .added {
771
+ color: var(--thinking-indicator-trace-added-color, #1f7a5f);
772
+ }
773
+
774
+ .row-diffstat .removed {
775
+ color: var(--thinking-indicator-trace-removed-color, #c93f38);
776
+ }
777
+
778
+ .trace-more {
779
+ font-size: var(--thinking-indicator-trace-more-font-size, 0.75rem);
780
+ color: var(--thinking-indicator-trace-more-color, #9a9a9a);
781
+ padding: 6px 0 0 22px;
782
+ animation: thinking-indicator-fade-in 300ms ease both;
783
+ }
784
+
785
+ @media (prefers-reduced-motion: reduce) {
786
+ .status-label,
787
+ .trace-row,
788
+ .trace-more {
789
+ animation-duration: 0.001s;
211
790
  }
212
791
 
213
- 100% {
214
- background-position: -200% 0;
792
+ .expandable,
793
+ .trace-connector,
794
+ .arrow {
795
+ transition-duration: 0.001s;
796
+ }
797
+
798
+ .status-label {
799
+ animation: none;
800
+ -webkit-text-fill-color: var(--thinking-indicator-label-color, #858585);
215
801
  }
216
802
  }
217
803
  </style>