@poodle64/librarian 2026.9.14 → 2026.9.16

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 (28) hide show
  1. package/README.md +77 -19
  2. package/dist/client.d.ts +16 -1
  3. package/dist/client.js +91 -21
  4. package/dist/components/activity-group/activity-group.svelte +76 -8
  5. package/dist/components/agent-transcript/agent-transcript.svelte +345 -155
  6. package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +7 -0
  7. package/dist/components/artefact-card/artefact-card.svelte +30 -2
  8. package/dist/components/artefact-pane/artefact-pane.svelte +141 -50
  9. package/dist/components/composer/composer.svelte +253 -39
  10. package/dist/components/composer/composer.svelte.d.ts +5 -1
  11. package/dist/components/conversation/conversation.svelte +247 -89
  12. package/dist/components/conversation/conversation.svelte.d.ts +6 -2
  13. package/dist/components/document-pane/document-pane.svelte +216 -33
  14. package/dist/components/markdown/markdown.svelte +73 -24
  15. package/dist/components/scope-statement/scope-statement.svelte +79 -12
  16. package/dist/components/source-list/index.d.ts +2 -0
  17. package/dist/components/source-list/index.js +2 -0
  18. package/dist/components/source-list/source-list.svelte +137 -0
  19. package/dist/components/source-list/source-list.svelte.d.ts +13 -0
  20. package/dist/components/thinking-row/thinking-row.svelte +69 -18
  21. package/dist/components/tool-row/tool-row.svelte +132 -26
  22. package/dist/components/working/working.svelte +51 -7
  23. package/dist/components/working/working.svelte.d.ts +3 -0
  24. package/dist/copy.d.ts +44 -12
  25. package/dist/copy.js +77 -33
  26. package/dist/transcript.svelte.d.ts +10 -0
  27. package/dist/transcript.svelte.js +4 -1
  28. package/package.json +4 -2
@@ -1,17 +1,18 @@
1
1
  <!--
2
2
  A study artefact, open in the reading column.
3
3
 
4
- The same shell shape `DocumentPane` uses (a column at `lg`, a bottom sheet
5
- below it) because decision 2 of the ask-milton surface is that a colleague
6
- never has to learn which of the two things is in the column before they
7
- open it — only ever one, in the same place, closing whichever was there.
4
+ The same shell shape `DocumentPane` uses (a column at 64rem, a bottom sheet
5
+ below it) because decision 2 of the agent surface is that a colleague never
6
+ has to learn which of the two things is in the column before they open it —
7
+ only ever one, in the same place, closing whichever was there.
8
8
  -->
9
9
  <script lang="ts">
10
10
  import XIcon from '@lucide/svelte/icons/x';
11
11
  import { segment, type TextBlock, type Turn } from '../../transcript.svelte';
12
- import { resolveCitations, splitSources, trustMark, type Citation } from '../../citations';
12
+ import { resolveCitations, splitSources, type Citation } from '../../citations';
13
13
  import { resolveCopy, type LibrarianCopy } from '../../copy';
14
14
  import Markdown from '../markdown/markdown.svelte';
15
+ import SourceList from '../source-list/source-list.svelte';
15
16
 
16
17
  interface Props {
17
18
  turn: Turn;
@@ -52,69 +53,159 @@
52
53
 
53
54
  <svelte:window onkeydown={keydown} />
54
55
 
55
- <aside
56
- aria-label="Study artefact"
57
- class="bg-surface-1 border-border fixed inset-x-0 bottom-0 z-40 flex h-[80svh] flex-col rounded-t-2xl border-t shadow-2xl lg:relative lg:h-auto lg:w-[560px] lg:shrink-0 lg:rounded-none lg:border-t-0 lg:border-l lg:shadow-none"
58
- >
59
- <!-- The sheet's grabber. Below `lg` this is an overlay a reader has to be
56
+ <aside aria-label="Study artefact" class="ds-lib-pane">
57
+ <!-- The sheet's grabber. On a phone this is an overlay a reader has to be
60
58
  able to see the top edge of; on a desktop it is a column, so there is
61
59
  nothing here to grab. -->
62
- <div class="flex justify-center pt-2 pb-1 lg:hidden" aria-hidden="true">
63
- <span class="bg-border h-1 w-9 rounded-full"></span>
64
- </div>
60
+ <div class="ds-lib-pane-grabber" aria-hidden="true"><span></span></div>
65
61
 
66
- <header class="border-border flex items-start gap-2 border-b px-4 py-3 lg:pt-3">
67
- <div class="min-w-0 flex-1">
68
- <h2 class="text-foreground truncate text-sm font-semibold">{turn.title ?? 'Briefing'}</h2>
69
- <p class="text-muted-foreground truncate text-xs">
62
+ <header class="ds-lib-pane-header">
63
+ <div class="ds-lib-pane-heading">
64
+ <h2 class="ds-lib-pane-title">{turn.title ?? 'Briefing'}</h2>
65
+ <p class="ds-lib-pane-subtitle">
70
66
  Briefing doc · {sources.length} source{sources.length === 1 ? '' : 's'}
71
67
  </p>
72
68
  </div>
73
69
  <button
74
70
  bind:this={closeButton}
75
71
  type="button"
72
+ class="ds-lib-pane-close"
76
73
  onclick={onclose}
77
74
  aria-label={words.closeSource}
78
- class="text-muted-foreground hover:text-foreground hover:bg-surface-2 focus-visible:ring-ring -mt-1 flex size-8 shrink-0 items-center justify-center rounded-lg transition-colors focus-visible:ring-2 focus-visible:outline-none"
79
75
  >
80
- <XIcon class="size-4" />
76
+ <XIcon size={16} />
81
77
  </button>
82
78
  </header>
83
79
 
84
- <div class="min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-3">
80
+ <div class="ds-lib-pane-body">
85
81
  <Markdown content={split.citations.length > 0 ? split.body : rawAnswer} />
86
82
 
87
83
  {#if sources.length > 0}
88
- <section class="mt-6">
89
- <h3 class="text-muted-foreground mb-1.5 text-xs font-medium tracking-wide uppercase">
90
- {words.sources}
91
- </h3>
92
- <ol class="flex flex-col gap-1">
93
- {#each sources as source (source.n)}
94
- {@const mark = trustMark(source, words)}
95
- <li>
96
- <button
97
- type="button"
98
- onclick={() => oncite?.(source)}
99
- disabled={!source.document_id || !oncite}
100
- class="border-border hover:border-border-strong hover:bg-surface-2 focus-visible:ring-ring flex w-full items-baseline gap-2 rounded-lg border px-2.5 py-1.5 text-left text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:cursor-default disabled:hover:bg-transparent"
101
- >
102
- <span
103
- class="bg-primary/15 text-foreground shrink-0 rounded px-1.5 font-mono text-xs tabular-nums"
104
- >{source.n}</span
105
- >
106
- <span class="min-w-0">
107
- <span class="text-foreground">{source.title}</span>
108
- {#if source.section}<span class="text-muted-foreground">
109
- · {source.section}</span
110
- >{/if}
111
- {#if mark}<span class="text-muted-foreground/80"> · {mark}</span>{/if}
112
- </span>
113
- </button>
114
- </li>
115
- {/each}
116
- </ol>
117
- </section>
84
+ <div class="ds-lib-pane-sources">
85
+ <SourceList {sources} {words} {oncite} level={3} />
86
+ </div>
118
87
  {/if}
119
88
  </div>
120
89
  </aside>
90
+
91
+ <style>
92
+ .ds-lib-pane {
93
+ position: fixed;
94
+ inset-inline: 0;
95
+ bottom: 0;
96
+ z-index: 40;
97
+ display: flex;
98
+ height: 80svh;
99
+ flex-direction: column;
100
+ border-top: 1px solid var(--ds-color-border);
101
+ border-start-start-radius: var(--ds-radius-xl);
102
+ border-start-end-radius: var(--ds-radius-xl);
103
+ background: var(--ds-color-surface-1);
104
+ box-shadow: 0 -12px 40px rgb(0 0 0 / 0.25);
105
+ }
106
+
107
+ .ds-lib-pane-grabber {
108
+ display: flex;
109
+ justify-content: center;
110
+ padding-top: 0.5rem;
111
+ padding-bottom: 0.25rem;
112
+ }
113
+
114
+ .ds-lib-pane-grabber span {
115
+ width: 2.25rem;
116
+ height: 0.25rem;
117
+ border-radius: var(--ds-radius-full);
118
+ background: var(--ds-color-border);
119
+ }
120
+
121
+ .ds-lib-pane-header {
122
+ display: flex;
123
+ align-items: flex-start;
124
+ gap: 0.5rem;
125
+ border-bottom: 1px solid var(--ds-color-border);
126
+ padding: 0.75rem 1rem;
127
+ }
128
+
129
+ .ds-lib-pane-heading {
130
+ min-width: 0;
131
+ flex: 1;
132
+ }
133
+
134
+ .ds-lib-pane-title,
135
+ .ds-lib-pane-subtitle {
136
+ margin: 0;
137
+ overflow: hidden;
138
+ text-overflow: ellipsis;
139
+ white-space: nowrap;
140
+ }
141
+
142
+ .ds-lib-pane-title {
143
+ color: var(--ds-color-foreground);
144
+ font-family: inherit;
145
+ font-size: 0.875rem;
146
+ font-weight: 600;
147
+ }
148
+
149
+ .ds-lib-pane-subtitle {
150
+ color: var(--ds-color-muted-foreground);
151
+ font-size: var(--ds-text-2xs);
152
+ }
153
+
154
+ .ds-lib-pane-close {
155
+ display: flex;
156
+ width: 2rem;
157
+ height: 2rem;
158
+ flex: none;
159
+ align-items: center;
160
+ justify-content: center;
161
+ margin-top: -0.25rem;
162
+ border: 0;
163
+ border-radius: var(--ds-radius-lg);
164
+ background: none;
165
+ color: var(--ds-color-muted-foreground);
166
+ cursor: pointer;
167
+ transition:
168
+ color 150ms ease,
169
+ background-color 150ms ease;
170
+ }
171
+
172
+ .ds-lib-pane-close:hover {
173
+ background: var(--ds-color-surface-2);
174
+ color: var(--ds-color-foreground);
175
+ }
176
+
177
+ .ds-lib-pane-close:focus-visible {
178
+ outline: 2px solid var(--ds-color-ring);
179
+ outline-offset: 2px;
180
+ }
181
+
182
+ .ds-lib-pane-body {
183
+ min-height: 0;
184
+ flex: 1;
185
+ overflow-y: auto;
186
+ overscroll-behavior: contain;
187
+ padding: 0.75rem 1rem;
188
+ }
189
+
190
+ .ds-lib-pane-sources {
191
+ margin-top: 1.5rem;
192
+ }
193
+
194
+ @media (min-width: 64rem) {
195
+ .ds-lib-pane {
196
+ position: relative;
197
+ inset: auto;
198
+ height: auto;
199
+ width: 35rem;
200
+ flex: none;
201
+ border-top: 0;
202
+ border-inline-start: 1px solid var(--ds-color-border);
203
+ border-radius: 0;
204
+ box-shadow: none;
205
+ }
206
+
207
+ .ds-lib-pane-grabber {
208
+ display: none;
209
+ }
210
+ }
211
+ </style>
@@ -23,6 +23,7 @@
23
23
  MAX_FILES,
24
24
  rejectionMessage
25
25
  } from '../../attachments';
26
+ import { DEFAULT_PERSONA, resolveCopy, type LibrarianCopy } from '../../copy';
26
27
 
27
28
  export type Scope = 'document' | 'collection' | 'library';
28
29
 
@@ -44,8 +45,11 @@
44
45
  * control renders — studying is a mode of asking, never a tab, so a
45
46
  * host with nothing to build offers none rather than a disabled one. */
46
47
  onbriefing?: () => void;
47
- /** The persona's display name; the placeholder in both states takes it. */
48
+ /** Who is being asked. A slug is fine — `penny` renders as "Penny" — and
49
+ * both placeholders are composed from it. */
48
50
  name?: string;
51
+ /** Overrides for the package's own words. */
52
+ copy?: Partial<LibrarianCopy>;
49
53
  }
50
54
 
51
55
  let {
@@ -60,9 +64,12 @@
60
64
  onsubmit,
61
65
  onstop,
62
66
  onbriefing,
63
- name = 'Milton'
67
+ name = DEFAULT_PERSONA,
68
+ copy
64
69
  }: Props = $props();
65
70
 
71
+ const words = $derived(resolveCopy(copy, name));
72
+
66
73
  // What Milton is being asked about — this document, this collection, or
67
74
  // the whole library. Callers that have no narrower scope pass neither
68
75
  // name, leaving "The whole library" the sole entry — and a sole entry is
@@ -180,30 +187,25 @@
180
187
  </script>
181
188
 
182
189
  <div
190
+ class="ds-lib-composer-lift"
183
191
  style="transform: translateY(-{lift}px); padding-bottom: {lift
184
192
  ? '0px'
185
193
  : 'env(safe-area-inset-bottom, 0px)'}"
186
- class="transition-transform duration-150"
187
194
  >
188
- <div
189
- class="border-border bg-surface-1 focus-within:border-primary/60 rounded-xl border transition-colors"
190
- >
195
+ <div class="ds-lib-box">
191
196
  {#if files.length > 0}
192
- <ul class="flex flex-wrap gap-1.5 px-3 pt-3">
197
+ <ul class="ds-lib-files">
193
198
  {#each files as file, index (file.name + file.size)}
194
- <li
195
- class="border-border bg-surface-2 flex max-w-full items-center gap-1.5 rounded-lg border py-1 pr-1 pl-2 text-xs"
196
- >
197
- <span class="text-foreground truncate">{file.name}</span>
198
- <span class="text-muted-foreground shrink-0 tabular-nums">{formatSize(file.size)}</span
199
- >
199
+ <li class="ds-lib-file">
200
+ <span class="ds-lib-file-name">{file.name}</span>
201
+ <span class="ds-lib-file-size">{formatSize(file.size)}</span>
200
202
  <button
201
203
  type="button"
204
+ class="ds-lib-file-remove"
202
205
  onclick={() => remove(index)}
203
206
  aria-label="Remove {file.name}"
204
- class="text-muted-foreground hover:text-foreground hover:bg-surface-3 focus-visible:ring-ring flex size-5 shrink-0 items-center justify-center rounded transition-colors focus-visible:ring-2 focus-visible:outline-none"
205
207
  >
206
- <XIcon class="size-3" />
208
+ <XIcon size={12} />
207
209
  </button>
208
210
  </li>
209
211
  {/each}
@@ -217,11 +219,11 @@
217
219
  onkeydown={keydown}
218
220
  rows="1"
219
221
  disabled={running}
220
- placeholder={running ? `${name} is answering…` : `Ask ${name}…`}
221
- class="text-foreground placeholder:text-muted-foreground w-full resize-none bg-transparent px-4 pt-3 pb-2 text-base outline-none disabled:opacity-60"
222
+ placeholder={running ? words.answeringPlaceholder : words.askPlaceholder}
223
+ class="ds-lib-input"
222
224
  ></textarea>
223
225
 
224
- <div class="flex items-center gap-2 px-3 pb-2.5">
226
+ <div class="ds-lib-controls">
225
227
  {#if attachments}
226
228
  <input
227
229
  bind:this={picker}
@@ -229,75 +231,287 @@
229
231
  multiple
230
232
  accept={ACCEPT_ATTRIBUTE}
231
233
  onchange={pick}
232
- class="hidden"
234
+ class="ds-lib-picker"
233
235
  tabindex="-1"
234
236
  aria-hidden="true"
235
237
  />
236
238
  <button
237
239
  type="button"
240
+ class="ds-lib-icon-button"
238
241
  onclick={() => picker?.click()}
239
242
  disabled={running || files.length >= MAX_FILES}
240
243
  aria-label="Attach a file"
241
- class="text-muted-foreground hover:text-foreground hover:bg-surface-2 focus-visible:ring-ring flex size-8 shrink-0 items-center justify-center rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:opacity-30"
242
244
  >
243
- <PaperclipIcon class="size-4" />
245
+ <PaperclipIcon size={16} />
244
246
  </button>
245
247
  {/if}
246
248
 
247
249
  {#if onbriefing}
248
250
  <button
249
251
  type="button"
252
+ class="ds-lib-icon-button"
250
253
  onclick={onbriefing}
251
254
  disabled={running}
252
255
  aria-label="Ask for a briefing"
253
256
  title="Ask for a briefing"
254
- class="text-muted-foreground hover:text-foreground hover:bg-surface-2 focus-visible:ring-ring flex size-8 shrink-0 items-center justify-center rounded-full transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:opacity-30"
255
257
  >
256
- <FileTextIcon class="size-4" />
258
+ <FileTextIcon size={16} />
257
259
  </button>
258
260
  {/if}
259
261
 
260
262
  {#if hasChoice}
261
- <div class="flex min-w-0 flex-wrap items-center gap-1">
263
+ <div class="ds-lib-scopes">
262
264
  {#each choices as choice (choice.id)}
263
265
  <button
264
266
  type="button"
267
+ class="ds-lib-scope-chip"
268
+ class:is-chosen={scope === choice.id}
265
269
  onclick={() => onscope(choice.id)}
266
- class="max-w-full truncate rounded-full px-2 py-0.5 text-xs transition-colors {scope ===
267
- choice.id
268
- ? 'bg-primary/15 text-foreground border-primary/40 border'
269
- : 'text-muted-foreground hover:text-foreground border border-transparent'}"
270
270
  >
271
271
  {choice.label}
272
272
  </button>
273
273
  {/each}
274
274
  </div>
275
275
  {/if}
276
- <span class="flex-1"></span>
276
+ <span class="ds-lib-spacer"></span>
277
277
  {#if running}
278
- <button
279
- type="button"
280
- onclick={onstop}
281
- aria-label="Stop"
282
- class="bg-foreground text-background hover:bg-foreground/90 flex size-8 items-center justify-center rounded-full transition-colors"
283
- >
284
- <SquareIcon class="size-3.5 fill-current" />
278
+ <button type="button" class="ds-lib-stop" onclick={onstop} aria-label="Stop">
279
+ <SquareIcon size={14} fill="currentColor" />
285
280
  </button>
286
281
  {:else}
287
282
  <button
288
283
  type="button"
284
+ class="ds-lib-send"
289
285
  onclick={submit}
290
286
  disabled={!value.trim()}
291
287
  aria-label="Send"
292
- class="bg-primary text-primary-foreground hover:bg-primary/90 disabled:hover:bg-primary flex size-8 items-center justify-center rounded-full transition-all disabled:opacity-30"
293
288
  >
294
- <ArrowUpIcon class="size-4" />
289
+ <ArrowUpIcon size={16} />
295
290
  </button>
296
291
  {/if}
297
292
  </div>
298
293
  </div>
299
294
 
300
295
  {#if rejected}
301
- <p class="text-status-error px-1 pt-1.5 text-xs" role="status" aria-live="polite">{rejected}</p>
296
+ <p class="ds-lib-rejected" role="status" aria-live="polite">{rejected}</p>
302
297
  {/if}
303
298
  </div>
299
+
300
+ <style>
301
+ .ds-lib-composer-lift {
302
+ transition: transform 150ms ease;
303
+ }
304
+
305
+ .ds-lib-box {
306
+ border: 1px solid var(--ds-color-border);
307
+ border-radius: var(--ds-radius-xl);
308
+ background: var(--ds-color-surface-1);
309
+ transition: border-color 150ms ease;
310
+ }
311
+
312
+ .ds-lib-box:focus-within {
313
+ border-color: color-mix(in oklab, var(--ds-color-primary) 60%, transparent);
314
+ }
315
+
316
+ .ds-lib-files {
317
+ display: flex;
318
+ flex-wrap: wrap;
319
+ gap: 0.375rem;
320
+ margin: 0;
321
+ padding: 0.75rem 0.75rem 0;
322
+ list-style: none;
323
+ }
324
+
325
+ .ds-lib-file {
326
+ display: flex;
327
+ max-width: 100%;
328
+ align-items: center;
329
+ gap: 0.375rem;
330
+ border: 1px solid var(--ds-color-border);
331
+ border-radius: var(--ds-radius-lg);
332
+ background: var(--ds-color-surface-2);
333
+ padding: 0.25rem 0.25rem 0.25rem 0.5rem;
334
+ font-size: var(--ds-text-2xs);
335
+ }
336
+
337
+ .ds-lib-file-name {
338
+ overflow: hidden;
339
+ text-overflow: ellipsis;
340
+ white-space: nowrap;
341
+ color: var(--ds-color-foreground);
342
+ }
343
+
344
+ .ds-lib-file-size {
345
+ flex: none;
346
+ color: var(--ds-color-muted-foreground);
347
+ font-variant-numeric: tabular-nums;
348
+ }
349
+
350
+ .ds-lib-file-remove {
351
+ display: flex;
352
+ width: 1.25rem;
353
+ height: 1.25rem;
354
+ flex: none;
355
+ align-items: center;
356
+ justify-content: center;
357
+ border: 0;
358
+ border-radius: var(--ds-radius-sm);
359
+ background: none;
360
+ color: var(--ds-color-muted-foreground);
361
+ cursor: pointer;
362
+ transition:
363
+ color 150ms ease,
364
+ background-color 150ms ease;
365
+ }
366
+
367
+ .ds-lib-file-remove:hover {
368
+ background: var(--ds-color-surface-3);
369
+ color: var(--ds-color-foreground);
370
+ }
371
+
372
+ .ds-lib-input {
373
+ display: block;
374
+ width: 100%;
375
+ resize: none;
376
+ border: 0;
377
+ background: transparent;
378
+ padding: 0.75rem 1rem 0.5rem;
379
+ color: var(--ds-color-foreground);
380
+ font: inherit;
381
+ font-size: 1rem;
382
+ outline: none;
383
+ }
384
+
385
+ .ds-lib-input::placeholder {
386
+ color: var(--ds-color-muted-foreground);
387
+ }
388
+
389
+ .ds-lib-input:disabled {
390
+ opacity: 0.6;
391
+ }
392
+
393
+ .ds-lib-controls {
394
+ display: flex;
395
+ align-items: center;
396
+ gap: 0.5rem;
397
+ padding: 0 0.75rem 0.625rem;
398
+ }
399
+
400
+ .ds-lib-picker {
401
+ display: none;
402
+ }
403
+
404
+ .ds-lib-icon-button,
405
+ .ds-lib-stop,
406
+ .ds-lib-send {
407
+ display: flex;
408
+ width: 2rem;
409
+ height: 2rem;
410
+ flex: none;
411
+ align-items: center;
412
+ justify-content: center;
413
+ border: 0;
414
+ border-radius: var(--ds-radius-full);
415
+ cursor: pointer;
416
+ transition:
417
+ color 150ms ease,
418
+ background-color 150ms ease,
419
+ opacity 150ms ease;
420
+ }
421
+
422
+ .ds-lib-icon-button {
423
+ background: none;
424
+ color: var(--ds-color-muted-foreground);
425
+ }
426
+
427
+ .ds-lib-icon-button:hover:not(:disabled) {
428
+ background: var(--ds-color-surface-2);
429
+ color: var(--ds-color-foreground);
430
+ }
431
+
432
+ .ds-lib-icon-button:disabled,
433
+ .ds-lib-send:disabled {
434
+ cursor: default;
435
+ opacity: 0.3;
436
+ }
437
+
438
+ .ds-lib-icon-button:focus-visible,
439
+ .ds-lib-stop:focus-visible,
440
+ .ds-lib-send:focus-visible,
441
+ .ds-lib-scope-chip:focus-visible,
442
+ .ds-lib-file-remove:focus-visible {
443
+ outline: 2px solid var(--ds-color-ring);
444
+ outline-offset: 2px;
445
+ }
446
+
447
+ .ds-lib-stop {
448
+ background: var(--ds-color-foreground);
449
+ color: var(--ds-color-background);
450
+ }
451
+
452
+ .ds-lib-send {
453
+ background: var(--ds-color-primary);
454
+ color: var(--ds-color-primary-foreground);
455
+ }
456
+
457
+ .ds-lib-send:hover:not(:disabled),
458
+ .ds-lib-stop:hover {
459
+ opacity: 0.9;
460
+ }
461
+
462
+ /* Three chips in a narrow column clipped all three to fragments
463
+ ("defence-s…", "All colle…"). Wrapping beats truncating: a chip a reader
464
+ cannot finish reading is not a control, it is decoration. */
465
+ .ds-lib-scopes {
466
+ display: flex;
467
+ min-width: 0;
468
+ flex-wrap: wrap;
469
+ align-items: center;
470
+ gap: 0.25rem;
471
+ }
472
+
473
+ .ds-lib-scope-chip {
474
+ max-width: 100%;
475
+ overflow: hidden;
476
+ border: 1px solid transparent;
477
+ border-radius: var(--ds-radius-full);
478
+ background: none;
479
+ padding: 0.125rem 0.5rem;
480
+ color: var(--ds-color-muted-foreground);
481
+ font: inherit;
482
+ font-size: var(--ds-text-2xs);
483
+ text-overflow: ellipsis;
484
+ white-space: nowrap;
485
+ cursor: pointer;
486
+ transition:
487
+ color 150ms ease,
488
+ background-color 150ms ease;
489
+ }
490
+
491
+ .ds-lib-scope-chip:hover {
492
+ color: var(--ds-color-foreground);
493
+ }
494
+
495
+ .ds-lib-scope-chip.is-chosen {
496
+ border-color: color-mix(in oklab, var(--ds-color-primary) 40%, transparent);
497
+ background: color-mix(in oklab, var(--ds-color-primary) 15%, transparent);
498
+ color: var(--ds-color-foreground);
499
+ }
500
+
501
+ .ds-lib-spacer {
502
+ flex: 1;
503
+ }
504
+
505
+ .ds-lib-rejected {
506
+ margin: 0;
507
+ padding: 0.375rem 0.25rem 0;
508
+ color: var(--ds-color-status-error);
509
+ font-size: var(--ds-text-2xs);
510
+ }
511
+
512
+ @media (prefers-reduced-motion: reduce) {
513
+ .ds-lib-composer-lift {
514
+ transition: none;
515
+ }
516
+ }
517
+ </style>
@@ -1,3 +1,4 @@
1
+ import { type LibrarianCopy } from '../../copy';
1
2
  export type Scope = 'document' | 'collection' | 'library';
2
3
  interface Props {
3
4
  value: string;
@@ -17,8 +18,11 @@ interface Props {
17
18
  * control renders — studying is a mode of asking, never a tab, so a
18
19
  * host with nothing to build offers none rather than a disabled one. */
19
20
  onbriefing?: () => void;
20
- /** The persona's display name; the placeholder in both states takes it. */
21
+ /** Who is being asked. A slug is fine — `penny` renders as "Penny" — and
22
+ * both placeholders are composed from it. */
21
23
  name?: string;
24
+ /** Overrides for the package's own words. */
25
+ copy?: Partial<LibrarianCopy>;
22
26
  }
23
27
  declare const Composer: import("svelte").Component<Props, {}, "value" | "files">;
24
28
  type Composer = ReturnType<typeof Composer>;