@poodle64/librarian 2026.10.1 → 2026.10.3

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.
@@ -2,6 +2,12 @@
2
2
  The whole reading surface: every turn, the scroll that follows the stream,
3
3
  and the source pane beside it.
4
4
 
5
+ Before the first question it reads as ChatGPT's and Claude's empty chat: the
6
+ opening and the composer together in the middle of the panel, with nothing
7
+ else competing. The first question docks the composer at the foot and the
8
+ transcript scrolls above it. The composer is one element in both places, so
9
+ a draft or an attachment survives the move.
10
+
5
11
  This owns the SCROLL CONTAINER, which is the reason it exists rather than the
6
12
  host composing `AgentTranscript` in a div of its own. Follow-scroll, the
7
13
  jump-to-latest pill and a document pane that narrows the transcript instead
@@ -60,6 +66,10 @@
60
66
  /** When the last answer began, epoch ms, where this page did not see it
61
67
  * start (`Chat.answering`): it reads as still being written. */
62
68
  answering?: number | null;
69
+ /** A conversation is being read back and has no turns yet: the surface
70
+ * waits docked and says nothing, rather than centring a new
71
+ * conversation's opening for a moment and snapping away from it. */
72
+ reopening?: boolean;
63
73
  /** Overrides for the package's own words. */
64
74
  copy?: Partial<ChatCopy>;
65
75
  /** Enables the source pane. Without it, chips render but do not open. */
@@ -91,10 +101,16 @@
91
101
  * prose, its citation marks, its links) renders its own, and one that
92
102
  * only adds to it renders `AgentTranscript` inside it. */
93
103
  turn?: Snippet<[AgentTranscriptProps]>;
94
- /** Before the first turn, inside the scroll and under the opening and
95
- * the scope: what the host shows ahead of a question, which scrolls
96
- * with the conversation rather than taking its height. */
104
+ /** What leads the conversation: inside the scroll, under the opening and
105
+ * the scope, so a tall one scrolls rather than taking the panel's
106
+ * height; before the first question, under the centred composer, its
107
+ * examples and the scope. */
97
108
  lead?: Snippet;
109
+ /** The host's controls for the conversation as a whole (its past
110
+ * questions, a new one), at the measure's trailing edge above the
111
+ * transcript. It stays put while the transcript scrolls and narrows
112
+ * with it when a pane opens. */
113
+ toolbar?: Snippet;
98
114
  }
99
115
 
100
116
  let {
@@ -110,6 +126,7 @@
110
126
  onmark,
111
127
  onshare,
112
128
  answering = null,
129
+ reopening = false,
113
130
  copy,
114
131
  loadDocument,
115
132
  oncite,
@@ -120,6 +137,7 @@
120
137
  composer,
121
138
  turn: presentTurn,
122
139
  lead,
140
+ toolbar,
123
141
  formatCost
124
142
  }: Props = $props();
125
143
 
@@ -129,6 +147,10 @@
129
147
  const words = $derived(resolveCopy(copy, name));
130
148
  const who = $derived(personaName(name));
131
149
  const opening = $derived(welcome ?? words.welcome);
150
+ // A turn on its way (a reopened conversation, a job's first) is not a new
151
+ // conversation, so neither waits centred.
152
+ const empty = $derived(turns.length === 0 && !reopening);
153
+ const centred = $derived(empty && !running && Boolean(composer));
132
154
 
133
155
  /**
134
156
  * When each turn arrived, for the turns this surface actually watched
@@ -247,105 +269,115 @@
247
269
  </script>
248
270
 
249
271
  <div class="ds-lib-surface">
250
- <div class="ds-lib-main">
251
- <!-- The pill is positioned against the SCROLL region, not the column: the
252
- composer is in the column too, and a pill over the input is a pill in
253
- the way. -->
254
- <div class="ds-lib-scroll-region">
255
- <div
256
- bind:this={viewport}
257
- onscroll={scrolled}
258
- role="log"
259
- aria-live="polite"
260
- aria-busy={running}
261
- aria-label={words.conversationLabel}
262
- class="ds-lib-scroll"
263
- >
264
- <!-- The measure, in this package's own stylesheet.
265
- `--ds-lib-measure` is the one knob a host may turn, and it is a
266
- token rather than a class precisely because a class here is a
267
- string a consuming app's Tailwind has to find in `dist` — the
268
- arrangement that silently produced `max-width: none` and a
269
- 2302px line in Pebblestone's production build. -->
270
- <div class="ds-lib-column">
271
- {#if turns.length === 0}
272
- <div class="ds-lib-empty">
273
- <p class="ds-lib-welcome">{opening}</p>
274
- {#if examples.length > 0}
275
- <ul class="ds-lib-chips">
276
- {#each examples.slice(0, 3) as example (example)}
277
- <li>
278
- <button type="button" class="ds-lib-chip" onclick={() => onexample?.(example)}>
279
- {example}
280
- </button>
281
- </li>
282
- {/each}
283
- </ul>
272
+ <div class="ds-lib-main" class:is-centred={centred}>
273
+ {#if toolbar}
274
+ <div class="ds-lib-toolbar">
275
+ <div class="ds-lib-column ds-lib-column-flush ds-lib-toolbar-row">{@render toolbar()}</div>
276
+ </div>
277
+ {/if}
278
+
279
+ <div class="ds-lib-stage">
280
+ <!-- The pill is positioned against the SCROLL region, not the column: the
281
+ composer is in the column too, and a pill over the input is a pill in
282
+ the way. -->
283
+ <div class="ds-lib-scroll-region">
284
+ <div
285
+ bind:this={viewport}
286
+ onscroll={scrolled}
287
+ role="log"
288
+ aria-live="polite"
289
+ aria-busy={running}
290
+ aria-label={words.conversationLabel}
291
+ class="ds-lib-scroll"
292
+ >
293
+ <!-- The measure, in this package's own stylesheet.
294
+ `--ds-lib-measure` is the one knob a host may turn, and it is a
295
+ token rather than a class precisely because a class here is a
296
+ string a consuming app's Tailwind has to find in `dist` — the
297
+ arrangement that silently produced `max-width: none` and a
298
+ 2302px line in Pebblestone's production build. -->
299
+ <div class="ds-lib-column">
300
+ {#if empty}
301
+ <div class="ds-lib-empty">
302
+ <p class="ds-lib-welcome">{opening}</p>
303
+ {#if !centred}{@render suggestions()}{/if}
304
+ </div>
305
+ {/if}
306
+
307
+ <!-- Above the first turn, and in ONE place in the markup: the empty
308
+ state carries the welcome, this carries the boundary, and both
309
+ sit at the top of the same column so a reader meets them in
310
+ the same order whether or not they have asked anything yet.
311
+ Centred, both sit under the composer instead. -->
312
+ {#if !centred}
313
+ {#if scope}
314
+ <ScopeStatement statement={scope} expanded={false} copy={words} />
284
315
  {/if}
285
- </div>
286
- {/if}
287
-
288
- <!-- Above the first turn, and in ONE place in the markup: the empty
289
- state carries the welcome, this carries the boundary, and both
290
- sit at the top of the same column so a reader meets them in
291
- the same order whether or not they have asked anything yet. -->
292
- {#if scope}
293
- <ScopeStatement statement={scope} expanded={turns.length === 0} copy={words} />
294
- {/if}
295
-
296
- {@render lead?.()}
297
-
298
- {#each turns as turn, index (turn.id)}
299
- {@const last = index === turns.length - 1}
300
- {@const props = {
301
- question: turn.question,
302
- blocks: turn.blocks,
303
- outcome: turn.outcome,
304
- running: running && last,
305
- answering: last ? (answering ?? undefined) : undefined,
306
- citations: turn.citations ?? [],
307
- collectionNames,
308
- copy: words,
309
- name: who,
310
- at: turn.at ?? stamps[turn.id],
311
- artefact: turn.artefact,
312
- artefactOpen: isShowing(turn),
313
- onopenartefact: opener(turn),
314
- describeTool,
315
- formatCost,
316
- oncite: oncite || loadDocument ? cite : undefined,
317
- onregenerate: last && !running ? onregenerate : undefined,
318
- onmark:
319
- last && !running && onmark ? (verdict: Verdict) => onmark(turn, verdict) : undefined,
320
- shared: turn.shared,
321
- onshare:
322
- onshare && !(last && running) ? (share: boolean) => onshare(turn, share) : undefined
323
- } satisfies AgentTranscriptProps}
324
- {#if presentTurn}
325
- {@render presentTurn(props)}
326
- {:else}
327
- <AgentTranscript {...props} />
316
+ {@render lead?.()}
328
317
  {/if}
329
- {/each}
318
+
319
+ {#each turns as turn, index (turn.id)}
320
+ {@const last = index === turns.length - 1}
321
+ {@const props = {
322
+ question: turn.question,
323
+ blocks: turn.blocks,
324
+ outcome: turn.outcome,
325
+ running: running && last,
326
+ answering: last ? (answering ?? undefined) : undefined,
327
+ citations: turn.citations ?? [],
328
+ collectionNames,
329
+ copy: words,
330
+ name: who,
331
+ at: turn.at ?? stamps[turn.id],
332
+ artefact: turn.artefact,
333
+ artefactOpen: isShowing(turn),
334
+ onopenartefact: opener(turn),
335
+ describeTool,
336
+ formatCost,
337
+ oncite: oncite || loadDocument ? cite : undefined,
338
+ onregenerate: last && !running ? onregenerate : undefined,
339
+ onmark:
340
+ last && !running && onmark ? (verdict: Verdict) => onmark(turn, verdict) : undefined,
341
+ shared: turn.shared,
342
+ onshare:
343
+ onshare && !(last && running) ? (share: boolean) => onshare(turn, share) : undefined
344
+ } satisfies AgentTranscriptProps}
345
+ {#if presentTurn}
346
+ {@render presentTurn(props)}
347
+ {:else}
348
+ <AgentTranscript {...props} />
349
+ {/if}
350
+ {/each}
351
+ </div>
330
352
  </div>
353
+
354
+ {#if !follow.following && turns.length > 0}
355
+ <button type="button" class="ds-lib-jump" onclick={jump}>
356
+ <ArrowDownIcon size={14} />
357
+ {words.jumpToLatest}
358
+ </button>
359
+ {/if}
331
360
  </div>
332
361
 
333
- {#if !follow.following && turns.length > 0}
334
- <button type="button" class="ds-lib-jump" onclick={jump}>
335
- <ArrowDownIcon size={14} />
336
- {words.jumpToLatest}
337
- </button>
362
+ {#if composer}
363
+ <div class="ds-lib-composer">
364
+ <div class="ds-lib-column ds-lib-column-flush">
365
+ {@render composer()}
366
+ {#if centred}
367
+ <div class="ds-lib-after">
368
+ {@render suggestions()}
369
+ {#if scope}
370
+ <div class="ds-lib-after-scope">
371
+ <ScopeStatement statement={scope} expanded={false} copy={words} />
372
+ </div>
373
+ {/if}
374
+ {@render lead?.()}
375
+ </div>
376
+ {/if}
377
+ </div>
378
+ </div>
338
379
  {/if}
339
380
  </div>
340
-
341
- {#if composer}
342
- <!-- Lifted off the scroll area, and on the app's own background rather
343
- than the transcript's: prose scrolling up to the exact edge of the
344
- input is the one place a reader loses track of which is which. -->
345
- <div class="ds-lib-composer">
346
- <div class="ds-lib-column ds-lib-column-flush">{@render composer()}</div>
347
- </div>
348
- {/if}
349
381
  </div>
350
382
 
351
383
  {#if column?.kind === 'document' && loadDocument}
@@ -365,6 +397,20 @@
365
397
  {/if}
366
398
  </div>
367
399
 
400
+ {#snippet suggestions()}
401
+ {#if examples.length > 0}
402
+ <ul class="ds-lib-chips">
403
+ {#each examples.slice(0, 3) as example (example)}
404
+ <li>
405
+ <button type="button" class="ds-lib-chip" onclick={() => onexample?.(example)}>
406
+ {example}
407
+ </button>
408
+ </li>
409
+ {/each}
410
+ </ul>
411
+ {/if}
412
+ {/snippet}
413
+
368
414
  <style>
369
415
  /* `min-width: 0` for a host that sets this in a row beside columns of its
370
416
  own: a flex item's automatic minimum is its widest content, and a table
@@ -384,6 +430,7 @@
384
430
  flex-direction: column;
385
431
  }
386
432
 
433
+ .ds-lib-stage,
387
434
  .ds-lib-scroll-region {
388
435
  position: relative;
389
436
  display: flex;
@@ -392,19 +439,31 @@
392
439
  flex-direction: column;
393
440
  }
394
441
 
442
+ /* The transcript fades at its edges, under the toolbar and into the
443
+ composer, rather than stopping at a rule across the panel: the box is
444
+ edge enough, and a fade lies over any ground the app paints, a texture
445
+ included. Each fade is the column's own padding at either end. */
395
446
  .ds-lib-scroll {
396
447
  min-height: 0;
397
448
  flex: 1;
398
449
  overflow-y: auto;
399
450
  overscroll-behavior: contain;
451
+ mask-image: linear-gradient(
452
+ to bottom,
453
+ transparent,
454
+ #000 1.25rem,
455
+ #000 calc(100% - 1.5rem),
456
+ transparent
457
+ );
400
458
  }
401
459
 
460
+ /* Turns are told apart by space and type, never boxes. */
402
461
  .ds-lib-column {
403
462
  display: flex;
404
463
  width: 100%;
405
464
  max-width: var(--ds-lib-measure, 46rem);
406
465
  flex-direction: column;
407
- gap: 1.5rem;
466
+ gap: 2.5rem;
408
467
  margin-inline: auto;
409
468
  padding: 1.5rem 1rem;
410
469
  }
@@ -414,6 +473,77 @@
414
473
  padding: 0;
415
474
  }
416
475
 
476
+ .ds-lib-toolbar {
477
+ flex: none;
478
+ padding: 0.5rem 1rem 0;
479
+ }
480
+
481
+ .ds-lib-toolbar-row {
482
+ flex-direction: row;
483
+ flex-wrap: wrap;
484
+ justify-content: flex-end;
485
+ gap: 0.25rem;
486
+ }
487
+
488
+ /* Centred: the stage scrolls as one, and the opening and the composer meet
489
+ in its middle. Auto margins centre them while they fit and let them
490
+ scroll from the top once a tall lead does not, where
491
+ `justify-content: center` would cut the top off. */
492
+ .is-centred .ds-lib-stage {
493
+ overflow-y: auto;
494
+ overscroll-behavior: contain;
495
+ }
496
+
497
+ .is-centred .ds-lib-scroll-region {
498
+ flex: none;
499
+ margin-top: auto;
500
+ }
501
+
502
+ .is-centred .ds-lib-scroll {
503
+ flex: none;
504
+ overflow: visible;
505
+ mask-image: none;
506
+ }
507
+
508
+ .is-centred .ds-lib-scroll .ds-lib-column {
509
+ padding-bottom: 1.25rem;
510
+ }
511
+
512
+ .is-centred .ds-lib-composer {
513
+ margin-bottom: auto;
514
+ padding-top: 0;
515
+ }
516
+
517
+ .is-centred .ds-lib-empty {
518
+ align-items: center;
519
+ padding-block: 0;
520
+ text-align: center;
521
+ }
522
+
523
+ .is-centred .ds-lib-welcome {
524
+ font-family: var(--ds-font-display, inherit);
525
+ font-size: 1.5rem;
526
+ line-height: 1.3;
527
+ text-wrap: balance;
528
+ }
529
+
530
+ .ds-lib-after {
531
+ display: flex;
532
+ flex-direction: column;
533
+ gap: 1.5rem;
534
+ padding-top: 1rem;
535
+ }
536
+
537
+ .ds-lib-after-scope {
538
+ display: flex;
539
+ justify-content: center;
540
+ }
541
+
542
+ .ds-lib-after .ds-lib-chips {
543
+ flex-flow: row wrap;
544
+ justify-content: center;
545
+ }
546
+
417
547
  .ds-lib-empty {
418
548
  display: flex;
419
549
  flex-direction: column;
@@ -501,8 +631,6 @@
501
631
 
502
632
  .ds-lib-composer {
503
633
  flex: none;
504
- border-top: 1px solid var(--ds-color-border);
505
- background: var(--ds-color-background);
506
- padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
634
+ padding: 0.25rem 1rem calc(0.75rem + env(safe-area-inset-bottom, 0px));
507
635
  }
508
636
  </style>
@@ -36,6 +36,10 @@ interface Props {
36
36
  /** When the last answer began, epoch ms, where this page did not see it
37
37
  * start (`Chat.answering`): it reads as still being written. */
38
38
  answering?: number | null;
39
+ /** A conversation is being read back and has no turns yet: the surface
40
+ * waits docked and says nothing, rather than centring a new
41
+ * conversation's opening for a moment and snapping away from it. */
42
+ reopening?: boolean;
39
43
  /** Overrides for the package's own words. */
40
44
  copy?: Partial<ChatCopy>;
41
45
  /** Enables the source pane. Without it, chips render but do not open. */
@@ -67,10 +71,16 @@ interface Props {
67
71
  * prose, its citation marks, its links) renders its own, and one that
68
72
  * only adds to it renders `AgentTranscript` inside it. */
69
73
  turn?: Snippet<[AgentTranscriptProps]>;
70
- /** Before the first turn, inside the scroll and under the opening and
71
- * the scope: what the host shows ahead of a question, which scrolls
72
- * with the conversation rather than taking its height. */
74
+ /** What leads the conversation: inside the scroll, under the opening and
75
+ * the scope, so a tall one scrolls rather than taking the panel's
76
+ * height; before the first question, under the centred composer, its
77
+ * examples and the scope. */
73
78
  lead?: Snippet;
79
+ /** The host's controls for the conversation as a whole (its past
80
+ * questions, a new one), at the measure's trailing edge above the
81
+ * transcript. It stays put while the transcript scrolls and narrows
82
+ * with it when a pane opens. */
83
+ toolbar?: Snippet;
74
84
  }
75
85
  declare const Conversation: import("svelte").Component<Props, {}, "">;
76
86
  type Conversation = ReturnType<typeof Conversation>;
@@ -4,7 +4,8 @@
4
4
 
5
5
  From cadmus's Past questions (cadmus#114). Presentation only: the host hands
6
6
  it the list and the acts (`Chat`'s own), nothing here fetches, and where it
7
- sits — a column, a sheet — is the host's.
7
+ sits — a column, a sheet — is the host's. A host that lays it over the page
8
+ passes `onclose`, and the header carries the way out beside New question.
8
9
 
9
10
  Each row's acts open under it rather than in a floating menu, and a delete
10
11
  asks under the row too: nothing to position, nothing to trap focus in, and
@@ -17,6 +18,7 @@
17
18
  import PencilIcon from '@lucide/svelte/icons/pencil';
18
19
  import PlusIcon from '@lucide/svelte/icons/plus';
19
20
  import TrashIcon from '@lucide/svelte/icons/trash-2';
21
+ import XIcon from '@lucide/svelte/icons/x';
20
22
  import type { ConversationSummary } from '../../chat.svelte';
21
23
  import { DEFAULT_PERSONA, resolveCopy, type ChatCopy } from '../../copy';
22
24
 
@@ -33,6 +35,8 @@
33
35
  /** A row chosen; with `href`, as well as following it. */
34
36
  onopen?: (id: string) => void;
35
37
  onnew: () => void;
38
+ /** Closes the list, for a host that lays it over the page. */
39
+ onclose?: () => void;
36
40
  /** Each act offered only when given; each resolves true once done. */
37
41
  onrename?: (id: string, title: string) => Promise<boolean>;
38
42
  ondownload?: (id: string) => Promise<boolean>;
@@ -49,6 +53,7 @@
49
53
  href,
50
54
  onopen,
51
55
  onnew,
56
+ onclose,
52
57
  onrename,
53
58
  ondownload,
54
59
  ondelete,
@@ -122,6 +127,16 @@
122
127
  <PlusIcon size={14} />
123
128
  <span>{words.newQuestion}</span>
124
129
  </button>
130
+ {#if onclose}
131
+ <button
132
+ type="button"
133
+ class="ds-lib-list-close"
134
+ onclick={onclose}
135
+ aria-label={words.closePastQuestions}
136
+ >
137
+ <XIcon size={16} />
138
+ </button>
139
+ {/if}
125
140
  </header>
126
141
 
127
142
  {#if failed}
@@ -405,7 +420,8 @@
405
420
  animation: ds-lib-list-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
406
421
  }
407
422
 
408
- .ds-lib-list-more {
423
+ .ds-lib-list-more,
424
+ .ds-lib-list-close {
409
425
  display: grid;
410
426
  width: 2.75rem;
411
427
  height: 2.75rem;
@@ -419,7 +435,8 @@
419
435
  }
420
436
 
421
437
  .ds-lib-list-more:hover,
422
- .ds-lib-list-more[aria-expanded='true'] {
438
+ .ds-lib-list-more[aria-expanded='true'],
439
+ .ds-lib-list-close:hover {
423
440
  color: var(--ds-color-foreground);
424
441
  }
425
442
 
@@ -468,7 +485,8 @@
468
485
  .ds-lib-list-new:focus-visible,
469
486
  .ds-lib-list-act:focus-visible,
470
487
  .ds-lib-list-open:focus-visible,
471
- .ds-lib-list-more:focus-visible {
488
+ .ds-lib-list-more:focus-visible,
489
+ .ds-lib-list-close:focus-visible {
472
490
  outline: 2px solid var(--ds-color-ring);
473
491
  outline-offset: -2px;
474
492
  }
@@ -13,6 +13,8 @@ interface Props {
13
13
  /** A row chosen; with `href`, as well as following it. */
14
14
  onopen?: (id: string) => void;
15
15
  onnew: () => void;
16
+ /** Closes the list, for a host that lays it over the page. */
17
+ onclose?: () => void;
16
18
  /** Each act offered only when given; each resolves true once done. */
17
19
  onrename?: (id: string, title: string) => Promise<boolean>;
18
20
  ondownload?: (id: string) => Promise<boolean>;
@@ -79,7 +79,7 @@
79
79
  }
80
80
 
81
81
  .ds-lib-fair-use a {
82
- color: var(--ds-color-primary);
82
+ color: var(--ds-color-primary-ink, var(--ds-color-primary));
83
83
  text-decoration: underline;
84
84
  text-underline-offset: 2px;
85
85
  }
@@ -214,8 +214,9 @@
214
214
  color: var(--ds-color-foreground);
215
215
  }
216
216
 
217
+ /* The accent as text: an app whose accent fails as text sets the ink. */
217
218
  .agent-prose :global(a) {
218
- color: var(--ds-color-primary);
219
+ color: var(--ds-color-primary-ink, var(--ds-color-primary));
219
220
  text-underline-offset: 0.2em;
220
221
  text-decoration-line: underline;
221
222
  }
@@ -5,11 +5,13 @@
5
5
  whether the answer they are about to read covers their case at all. Saying it
6
6
  once, in the persona's own words, is cheaper than every answer hedging.
7
7
 
8
- It leads while there is nothing else on the surface, and folds to one line as
9
- soon as there is: a boundary a reader has already read is a banner in the way
10
- of the conversation they came for. Folded is not gone — the same line reopens
11
- it, and a reader who opened it stays opened until the surface itself changes
12
- state.
8
+ One quiet line, never a card: the label and a chevron in the muted ink, the
9
+ statement under it in the same ink when opened (caspo#177). `Conversation`
10
+ renders it folded, under the empty chat's composer and over the transcript
11
+ alike: a boundary is a banner in the way of the conversation a reader came
12
+ for, and one line they can open costs it nothing. Folded is not gone: the
13
+ same line reopens it, and a reader who opened it stays opened until the
14
+ surface itself changes state.
13
15
  -->
14
16
  <script lang="ts">
15
17
  import { untrack } from 'svelte';
@@ -60,33 +62,32 @@
60
62
  </button>
61
63
 
62
64
  {#if open}
63
- <!-- No measure of its own: the card IS the measure, and a paragraph
64
- capped narrower than the border around it leaves a hand's width of
65
- empty card down the right at 1440. -->
66
65
  <p class="ds-lib-scope-text">{statement}</p>
67
66
  {/if}
68
67
  </section>
69
68
 
70
69
  <style>
71
70
  .ds-lib-scope {
72
- border: 1px solid var(--ds-color-border);
73
- border-radius: var(--ds-radius-xl);
74
- background: color-mix(in oklab, var(--ds-color-surface-2) 60%, transparent);
75
- padding: 0.5rem 0.75rem;
71
+ display: flex;
72
+ min-width: 0;
73
+ flex-direction: column;
74
+ align-items: flex-start;
76
75
  }
77
76
 
77
+ /* 44px tall: a thumb's target, with nothing drawn around it. */
78
78
  .ds-lib-scope-toggle {
79
79
  display: flex;
80
- width: 100%;
80
+ max-width: 100%;
81
+ min-height: 2.75rem;
81
82
  align-items: center;
82
- gap: 0.5rem;
83
+ gap: 0.375rem;
83
84
  border: 0;
84
85
  border-radius: var(--ds-radius-md);
85
86
  background: none;
86
87
  padding: 0;
87
88
  color: var(--ds-color-muted-foreground);
88
89
  font: inherit;
89
- font-size: var(--ds-text-2xs);
90
+ font-size: 0.8125rem;
90
91
  text-align: start;
91
92
  cursor: pointer;
92
93
  transition: color 150ms ease;
@@ -103,13 +104,9 @@
103
104
 
104
105
  .ds-lib-scope-label {
105
106
  min-width: 0;
106
- flex: 1;
107
107
  overflow: hidden;
108
108
  text-overflow: ellipsis;
109
109
  white-space: nowrap;
110
- font-weight: 500;
111
- letter-spacing: 0.05em;
112
- text-transform: uppercase;
113
110
  }
114
111
 
115
112
  .ds-lib-scope-chevron {
@@ -123,7 +120,7 @@
123
120
  }
124
121
 
125
122
  .ds-lib-scope-text {
126
- margin: 0.375rem 0 0;
123
+ margin: 0;
127
124
  color: var(--ds-color-muted-foreground);
128
125
  font-size: 0.875rem;
129
126
  line-height: 1.5rem;