@poodle64/librarian 2026.9.13 → 2026.9.15
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/README.md +77 -19
- package/dist/client.d.ts +16 -1
- package/dist/client.js +55 -8
- package/dist/components/activity-group/activity-group.svelte +76 -8
- package/dist/components/agent-transcript/agent-transcript.svelte +346 -150
- package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +7 -0
- package/dist/components/artefact-card/artefact-card.svelte +30 -2
- package/dist/components/artefact-pane/artefact-pane.svelte +141 -50
- package/dist/components/composer/composer.svelte +255 -38
- package/dist/components/composer/composer.svelte.d.ts +6 -0
- package/dist/components/conversation/conversation.svelte +254 -81
- package/dist/components/conversation/conversation.svelte.d.ts +7 -0
- package/dist/components/document-pane/document-pane.svelte +216 -33
- package/dist/components/markdown/markdown.svelte +73 -24
- package/dist/components/scope-statement/scope-statement.svelte +79 -12
- package/dist/components/source-list/index.d.ts +2 -0
- package/dist/components/source-list/index.js +2 -0
- package/dist/components/source-list/source-list.svelte +137 -0
- package/dist/components/source-list/source-list.svelte.d.ts +13 -0
- package/dist/components/thinking-row/thinking-row.svelte +69 -18
- package/dist/components/tool-row/tool-row.svelte +132 -26
- package/dist/components/working/working.svelte +51 -7
- package/dist/components/working/working.svelte.d.ts +3 -0
- package/dist/copy.d.ts +45 -10
- package/dist/copy.js +77 -32
- package/dist/transcript.svelte.d.ts +10 -0
- package/dist/transcript.svelte.js +4 -1
- package/package.json +4 -2
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
One question and everything
|
|
2
|
+
One question and everything the persona did answering it.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
reader scanning back finds their own words without reading any of
|
|
6
|
-
The answer is
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
Four things carry the shape. The question is a bubble against the trailing
|
|
5
|
+
edge, so a reader scanning back finds their own words without reading any of
|
|
6
|
+
the answer. The answer is a bounded card under it, signed with the persona's
|
|
7
|
+
name and the time, because a transcript of unbounded prose reads as a log
|
|
8
|
+
and the director's first words about the live screen were "it doesn't look
|
|
9
|
+
like a ChatGPT window". Everything the persona DID is one quiet line inside
|
|
10
|
+
that card, never a stack. And the measure is the package's own CSS, never a
|
|
11
|
+
utility class a consuming app's Tailwind has to discover in `dist` — the one
|
|
12
|
+
that got away compiled to `max-width: none` in production and left a
|
|
13
|
+
2302px-wide transcript.
|
|
9
14
|
-->
|
|
10
15
|
<script lang="ts">
|
|
11
16
|
import CheckIcon from '@lucide/svelte/icons/check';
|
|
@@ -22,14 +27,14 @@
|
|
|
22
27
|
citationMarkers,
|
|
23
28
|
resolveCitations,
|
|
24
29
|
splitSources,
|
|
25
|
-
trustMark,
|
|
26
30
|
type Citation
|
|
27
31
|
} from '../../citations';
|
|
28
|
-
import { resolveCopy, type LibrarianCopy } from '../../copy';
|
|
32
|
+
import { DEFAULT_PERSONA, personaName, resolveCopy, type LibrarianCopy } from '../../copy';
|
|
29
33
|
import Working from '../working/working.svelte';
|
|
30
34
|
import ActivityGroup from '../activity-group/activity-group.svelte';
|
|
31
35
|
import ArtefactCard from '../artefact-card/artefact-card.svelte';
|
|
32
36
|
import Markdown from '../markdown/markdown.svelte';
|
|
37
|
+
import SourceList from '../source-list/source-list.svelte';
|
|
33
38
|
|
|
34
39
|
interface Props {
|
|
35
40
|
question: string;
|
|
@@ -52,6 +57,13 @@
|
|
|
52
57
|
* no chip. */
|
|
53
58
|
onsuggest?: (question: string) => void;
|
|
54
59
|
copy?: Partial<LibrarianCopy>;
|
|
60
|
+
/** The persona's display name, signed on the answer card. */
|
|
61
|
+
name?: string;
|
|
62
|
+
/** When the question was asked, epoch ms. Rendered as a clock time —
|
|
63
|
+
* which is a different fact from the duration badge beside the actions,
|
|
64
|
+
* and a reader scanning back for "what did I ask after lunch" needs the
|
|
65
|
+
* first one. Absent renders no time rather than a guess. */
|
|
66
|
+
at?: number;
|
|
55
67
|
/** A study artefact rather than an ordinary answer: once settled, this
|
|
56
68
|
* renders as a card instead of prose. */
|
|
57
69
|
kind?: 'answer' | 'artefact';
|
|
@@ -74,6 +86,8 @@
|
|
|
74
86
|
onregenerate,
|
|
75
87
|
onsuggest,
|
|
76
88
|
copy,
|
|
89
|
+
name = DEFAULT_PERSONA,
|
|
90
|
+
at,
|
|
77
91
|
kind = 'answer',
|
|
78
92
|
title,
|
|
79
93
|
onopenartefact
|
|
@@ -81,10 +95,28 @@
|
|
|
81
95
|
|
|
82
96
|
const isArtefact = $derived(kind === 'artefact');
|
|
83
97
|
|
|
84
|
-
const words = $derived(resolveCopy(copy));
|
|
98
|
+
const words = $derived(resolveCopy(copy, name));
|
|
99
|
+
const who = $derived(personaName(name));
|
|
85
100
|
|
|
86
101
|
const asked = $derived(readerQuestion(question));
|
|
87
102
|
|
|
103
|
+
// The reader's own locale and their own clock. A date is added only once
|
|
104
|
+
// the turn is not from today, because "14:32" is what a reader scanning
|
|
105
|
+
// this morning's conversation is actually matching against.
|
|
106
|
+
const clock = $derived.by(() => {
|
|
107
|
+
if (at === undefined) return null;
|
|
108
|
+
const when = new Date(at);
|
|
109
|
+
const sameDay = new Date().toDateString() === when.toDateString();
|
|
110
|
+
return {
|
|
111
|
+
iso: when.toISOString(),
|
|
112
|
+
text: when.toLocaleTimeString(undefined, {
|
|
113
|
+
hour: 'numeric',
|
|
114
|
+
minute: '2-digit',
|
|
115
|
+
...(sameDay ? {} : { day: 'numeric', month: 'short' })
|
|
116
|
+
})
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
88
120
|
// An activity group stays live — and therefore labelled "Working…" — only
|
|
89
121
|
// while it is the last thing in the turn. As soon as prose arrives, it
|
|
90
122
|
// settles into its count.
|
|
@@ -124,18 +156,23 @@
|
|
|
124
156
|
* What went wrong, if anything did — in words, whether or not the run gave
|
|
125
157
|
* any.
|
|
126
158
|
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* a
|
|
134
|
-
*
|
|
135
|
-
*
|
|
159
|
+
* Three shapes reach a turn and only one of them carries a message.
|
|
160
|
+
* `unreachable` is a stream that never opened or died half-way, which
|
|
161
|
+
* `client.ts` raises without words because that layer does not know whose
|
|
162
|
+
* voice to say them in. A run that opened and then failed sets `is_error`
|
|
163
|
+
* on its terminal frame and says nothing at all — Claude Code's own
|
|
164
|
+
* `error_max_turns` and `error_during_execution` are exactly that shape.
|
|
165
|
+
* Only a run that volunteered a message gets its own words through.
|
|
166
|
+
*
|
|
167
|
+
* Reading none of this left a failed turn rendering as a clean, complete
|
|
168
|
+
* answer: no banner, a duration badge, and — worse — the "answered without
|
|
169
|
+
* a source" line, which is a claim about the shelf made off a run that
|
|
170
|
+
* never finished looking.
|
|
136
171
|
*/
|
|
137
172
|
const failure = $derived(
|
|
138
|
-
outcome?.
|
|
173
|
+
outcome?.unreachable
|
|
174
|
+
? words.unreachable
|
|
175
|
+
: (outcome?.error ?? (outcome?.isError ? words.answerFailed : null))
|
|
139
176
|
);
|
|
140
177
|
|
|
141
178
|
// A failed turn settles too, and "Ask again" is the one thing a reader wants
|
|
@@ -143,7 +180,7 @@
|
|
|
143
180
|
const settled = $derived(!running && (hasAnswer || Boolean(failure)));
|
|
144
181
|
|
|
145
182
|
/**
|
|
146
|
-
*
|
|
183
|
+
* The persona answered and cited nothing, and we WATCHED it happen.
|
|
147
184
|
*
|
|
148
185
|
* `outcome` is the gate, not merely an empty source list: a turn read back
|
|
149
186
|
* out of the conversation history is prose with no citations and no
|
|
@@ -178,25 +215,34 @@
|
|
|
178
215
|
}
|
|
179
216
|
</script>
|
|
180
217
|
|
|
181
|
-
<article class="
|
|
182
|
-
<!--
|
|
183
|
-
panel: this surface is read by people who arrive with
|
|
184
|
-
their model of what a chat looks like. -->
|
|
185
|
-
<div
|
|
186
|
-
class="
|
|
187
|
-
|
|
188
|
-
|
|
218
|
+
<article class="ds-lib-turn">
|
|
219
|
+
<!-- Trailing-edge bubble, following Claude and ChatGPT rather than a
|
|
220
|
+
VS Code-style panel: this surface is read by people who arrive with
|
|
221
|
+
those two as their model of what a chat looks like. -->
|
|
222
|
+
<div class="ds-lib-ask">
|
|
223
|
+
<div class="ds-lib-bubble">{asked}</div>
|
|
224
|
+
{#if clock}
|
|
225
|
+
<time class="ds-lib-ask-time" datetime={clock.iso}>{clock.text}</time>
|
|
226
|
+
{/if}
|
|
189
227
|
</div>
|
|
190
228
|
|
|
191
|
-
|
|
192
|
-
<
|
|
193
|
-
|
|
229
|
+
<div class="ds-lib-answer">
|
|
230
|
+
<header class="ds-lib-answer-head">
|
|
231
|
+
<span class="ds-lib-avatar" aria-hidden="true">{who.slice(0, 1)}</span>
|
|
232
|
+
<span class="ds-lib-who">{who}</span>
|
|
233
|
+
{#if clock}
|
|
234
|
+
<time class="ds-lib-answer-time" datetime={clock.iso}>{clock.text}</time>
|
|
235
|
+
{/if}
|
|
236
|
+
</header>
|
|
194
237
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
238
|
+
{#if running && segments.length === 0}
|
|
239
|
+
<Working copy={words} />
|
|
240
|
+
{/if}
|
|
241
|
+
|
|
242
|
+
{#each segments as seg (seg.index)}
|
|
243
|
+
{#if seg.kind === 'activity'}
|
|
244
|
+
<ActivityGroup group={seg} live={running && seg.index === lastIndex} />
|
|
245
|
+
{:else if !isArtefact}
|
|
200
246
|
<Markdown
|
|
201
247
|
content={seg.index === lastTextIndex && split.citations.length > 0
|
|
202
248
|
? split.body
|
|
@@ -209,120 +255,270 @@
|
|
|
209
255
|
if (found) oncite?.(found);
|
|
210
256
|
}}
|
|
211
257
|
/>
|
|
212
|
-
|
|
258
|
+
{/if}
|
|
259
|
+
{/each}
|
|
260
|
+
|
|
261
|
+
{#if isArtefact && settled}
|
|
262
|
+
<ArtefactCard
|
|
263
|
+
title={title ?? 'Briefing'}
|
|
264
|
+
citationCount={sources.length}
|
|
265
|
+
onopen={() => onopenartefact?.()}
|
|
266
|
+
/>
|
|
213
267
|
{/if}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
268
|
+
|
|
269
|
+
{#if !isArtefact && sources.length > 0}
|
|
270
|
+
<SourceList {sources} {words} {oncite} />
|
|
271
|
+
{/if}
|
|
272
|
+
|
|
273
|
+
{#if !isArtefact && notHeld}
|
|
274
|
+
<p class="ds-lib-not-held">{words.notHeld}</p>
|
|
275
|
+
{/if}
|
|
276
|
+
|
|
277
|
+
{#if !isArtefact && followUps.length > 0}
|
|
278
|
+
<section>
|
|
279
|
+
<h2 class="ds-lib-follow-heading">{words.suggestions}</h2>
|
|
280
|
+
<!-- Wrapping, not scrolling: at 390 a question is most of a line, so
|
|
281
|
+
a row of three would clip two of them to fragments. -->
|
|
282
|
+
<ul class="ds-lib-chips">
|
|
283
|
+
{#each followUps as followUp (followUp)}
|
|
284
|
+
<li>
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
class="ds-lib-chip"
|
|
288
|
+
onclick={() => {
|
|
289
|
+
used = true;
|
|
290
|
+
onsuggest?.(followUp);
|
|
291
|
+
}}
|
|
238
292
|
>
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
{
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
<
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
</ul>
|
|
285
|
-
</section>
|
|
286
|
-
{/if}
|
|
287
|
-
|
|
288
|
-
{#if failure}
|
|
289
|
-
<p
|
|
290
|
-
class="border-status-error/40 bg-status-error/10 text-foreground max-w-[72ch] rounded-lg border px-3 py-2 text-sm"
|
|
291
|
-
role="alert"
|
|
292
|
-
>
|
|
293
|
-
{failure}
|
|
294
|
-
</p>
|
|
295
|
-
{/if}
|
|
296
|
-
|
|
297
|
-
{#if !isArtefact && settled}
|
|
298
|
-
<div class="text-muted-foreground -ml-1.5 flex items-center gap-1">
|
|
299
|
-
{#if hasAnswer}
|
|
300
|
-
<button
|
|
301
|
-
type="button"
|
|
302
|
-
onclick={copyToClipboard}
|
|
303
|
-
aria-label={copied ? words.copiedAnswer : words.copyAnswer}
|
|
304
|
-
class="hover:text-foreground hover:bg-surface-2 focus-visible:ring-ring flex items-center gap-1.5 rounded-md px-1.5 py-1 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
305
|
-
>
|
|
306
|
-
{#if copied}<CheckIcon class="size-3.5" />{:else}<CopyIcon class="size-3.5" />{/if}
|
|
307
|
-
<span>{copied ? words.copiedAnswer : words.copyAnswer}</span>
|
|
308
|
-
</button>
|
|
309
|
-
{/if}
|
|
310
|
-
{#if onregenerate}
|
|
311
|
-
<button
|
|
312
|
-
type="button"
|
|
313
|
-
onclick={onregenerate}
|
|
314
|
-
aria-label={words.askAgain}
|
|
315
|
-
class="hover:text-foreground hover:bg-surface-2 focus-visible:ring-ring flex items-center gap-1.5 rounded-md px-1.5 py-1 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
|
316
|
-
>
|
|
317
|
-
<RefreshCwIcon class="size-3.5" />
|
|
318
|
-
<span>{words.askAgain}</span>
|
|
319
|
-
</button>
|
|
320
|
-
{/if}
|
|
321
|
-
{#if outcome && !failure}
|
|
322
|
-
<span class="pl-1 font-mono text-xs tabular-nums opacity-70">
|
|
323
|
-
{((outcome.durationMs ?? 0) / 1000).toFixed(1)}s
|
|
324
|
-
</span>
|
|
325
|
-
{/if}
|
|
326
|
-
</div>
|
|
327
|
-
{/if}
|
|
293
|
+
{followUp}
|
|
294
|
+
</button>
|
|
295
|
+
</li>
|
|
296
|
+
{/each}
|
|
297
|
+
</ul>
|
|
298
|
+
</section>
|
|
299
|
+
{/if}
|
|
300
|
+
|
|
301
|
+
{#if failure}
|
|
302
|
+
<p class="ds-lib-failure" role="alert">{failure}</p>
|
|
303
|
+
{/if}
|
|
304
|
+
|
|
305
|
+
{#if !isArtefact && settled}
|
|
306
|
+
<div class="ds-lib-actions">
|
|
307
|
+
{#if hasAnswer}
|
|
308
|
+
<button
|
|
309
|
+
type="button"
|
|
310
|
+
class="ds-lib-action"
|
|
311
|
+
onclick={copyToClipboard}
|
|
312
|
+
aria-label={copied ? words.copiedAnswer : words.copyAnswer}
|
|
313
|
+
>
|
|
314
|
+
{#if copied}<CheckIcon size={14} />{:else}<CopyIcon size={14} />{/if}
|
|
315
|
+
<span>{copied ? words.copiedAnswer : words.copyAnswer}</span>
|
|
316
|
+
</button>
|
|
317
|
+
{/if}
|
|
318
|
+
{#if onregenerate}
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
class="ds-lib-action"
|
|
322
|
+
onclick={onregenerate}
|
|
323
|
+
aria-label={words.askAgain}
|
|
324
|
+
>
|
|
325
|
+
<RefreshCwIcon size={14} />
|
|
326
|
+
<span>{words.askAgain}</span>
|
|
327
|
+
</button>
|
|
328
|
+
{/if}
|
|
329
|
+
<!-- How long it took, which is not the same fact as when it was
|
|
330
|
+
asked; both are on the card and neither stands in for the
|
|
331
|
+
other. -->
|
|
332
|
+
{#if outcome && !failure}
|
|
333
|
+
<span class="ds-lib-duration">{((outcome.durationMs ?? 0) / 1000).toFixed(1)}s</span>
|
|
334
|
+
{/if}
|
|
335
|
+
</div>
|
|
336
|
+
{/if}
|
|
337
|
+
</div>
|
|
328
338
|
</article>
|
|
339
|
+
|
|
340
|
+
<style>
|
|
341
|
+
.ds-lib-turn {
|
|
342
|
+
display: flex;
|
|
343
|
+
flex-direction: column;
|
|
344
|
+
gap: 0.5rem;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.ds-lib-ask {
|
|
348
|
+
display: flex;
|
|
349
|
+
max-width: 100%;
|
|
350
|
+
flex-direction: column;
|
|
351
|
+
align-items: flex-end;
|
|
352
|
+
gap: 0.25rem;
|
|
353
|
+
align-self: flex-end;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.ds-lib-bubble {
|
|
357
|
+
max-width: 85%;
|
|
358
|
+
border: 1px solid var(--ds-color-border);
|
|
359
|
+
border-radius: var(--ds-radius-xl);
|
|
360
|
+
background: var(--ds-color-surface-3);
|
|
361
|
+
padding: 0.625rem 1rem;
|
|
362
|
+
color: var(--ds-color-foreground);
|
|
363
|
+
font-size: 1rem;
|
|
364
|
+
line-height: 1.5rem;
|
|
365
|
+
overflow-wrap: break-word;
|
|
366
|
+
white-space: pre-wrap;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.ds-lib-ask-time,
|
|
370
|
+
.ds-lib-answer-time {
|
|
371
|
+
color: var(--ds-color-muted-foreground);
|
|
372
|
+
font-size: var(--ds-text-2xs);
|
|
373
|
+
font-variant-numeric: tabular-nums;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* The card. A turn a reader can see the edges of is the whole of what the
|
|
377
|
+
director was asking for: one question and its answer, bounded, rather
|
|
378
|
+
than prose running edge to edge under the last lot of prose. */
|
|
379
|
+
.ds-lib-answer {
|
|
380
|
+
display: flex;
|
|
381
|
+
flex-direction: column;
|
|
382
|
+
gap: 0.75rem;
|
|
383
|
+
border: 1px solid var(--ds-color-border);
|
|
384
|
+
border-radius: var(--ds-radius-xl);
|
|
385
|
+
background: var(--ds-color-surface-2);
|
|
386
|
+
padding: 0.875rem 1rem 0.75rem;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.ds-lib-answer-head {
|
|
390
|
+
display: flex;
|
|
391
|
+
align-items: center;
|
|
392
|
+
gap: 0.5rem;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.ds-lib-avatar {
|
|
396
|
+
display: flex;
|
|
397
|
+
width: 1.375rem;
|
|
398
|
+
height: 1.375rem;
|
|
399
|
+
flex: none;
|
|
400
|
+
align-items: center;
|
|
401
|
+
justify-content: center;
|
|
402
|
+
border-radius: var(--ds-radius-full);
|
|
403
|
+
background: color-mix(in oklab, var(--ds-color-primary) 18%, transparent);
|
|
404
|
+
color: var(--ds-color-foreground);
|
|
405
|
+
font-size: var(--ds-text-2xs);
|
|
406
|
+
font-weight: 600;
|
|
407
|
+
text-transform: uppercase;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.ds-lib-who {
|
|
411
|
+
flex: 1;
|
|
412
|
+
color: var(--ds-color-foreground);
|
|
413
|
+
font-size: 0.8125rem;
|
|
414
|
+
font-weight: 600;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.ds-lib-not-held {
|
|
418
|
+
margin: 0;
|
|
419
|
+
color: var(--ds-color-muted-foreground);
|
|
420
|
+
font-size: 0.875rem;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.ds-lib-follow-heading {
|
|
424
|
+
margin: 0 0 0.375rem;
|
|
425
|
+
color: var(--ds-color-muted-foreground);
|
|
426
|
+
font-family: inherit;
|
|
427
|
+
font-size: var(--ds-text-2xs);
|
|
428
|
+
font-weight: 500;
|
|
429
|
+
letter-spacing: 0.05em;
|
|
430
|
+
text-transform: uppercase;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.ds-lib-chips {
|
|
434
|
+
display: flex;
|
|
435
|
+
flex-wrap: wrap;
|
|
436
|
+
gap: 0.5rem;
|
|
437
|
+
margin: 0;
|
|
438
|
+
padding: 0;
|
|
439
|
+
list-style: none;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.ds-lib-chips li {
|
|
443
|
+
max-width: 100%;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.ds-lib-chip {
|
|
447
|
+
max-width: 100%;
|
|
448
|
+
border: 1px solid var(--ds-color-border);
|
|
449
|
+
border-radius: var(--ds-radius-full);
|
|
450
|
+
background: none;
|
|
451
|
+
padding: 0.5rem 0.875rem;
|
|
452
|
+
color: var(--ds-color-foreground);
|
|
453
|
+
font: inherit;
|
|
454
|
+
font-size: 0.875rem;
|
|
455
|
+
text-align: start;
|
|
456
|
+
cursor: pointer;
|
|
457
|
+
transition:
|
|
458
|
+
background-color 150ms ease,
|
|
459
|
+
border-color 150ms ease;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.ds-lib-chip:hover {
|
|
463
|
+
border-color: var(--ds-color-border-strong);
|
|
464
|
+
background: var(--ds-color-surface-1);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.ds-lib-chip:focus-visible {
|
|
468
|
+
outline: 2px solid var(--ds-color-ring);
|
|
469
|
+
outline-offset: 2px;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.ds-lib-failure {
|
|
473
|
+
margin: 0;
|
|
474
|
+
border: 1px solid color-mix(in oklab, var(--ds-color-status-error) 40%, transparent);
|
|
475
|
+
border-radius: var(--ds-radius-lg);
|
|
476
|
+
background: color-mix(in oklab, var(--ds-color-status-error) 10%, transparent);
|
|
477
|
+
padding: 0.5rem 0.75rem;
|
|
478
|
+
color: var(--ds-color-foreground);
|
|
479
|
+
font-size: 0.875rem;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.ds-lib-actions {
|
|
483
|
+
display: flex;
|
|
484
|
+
align-items: center;
|
|
485
|
+
gap: 0.25rem;
|
|
486
|
+
margin-inline-start: -0.375rem;
|
|
487
|
+
color: var(--ds-color-muted-foreground);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.ds-lib-action {
|
|
491
|
+
display: flex;
|
|
492
|
+
align-items: center;
|
|
493
|
+
gap: 0.375rem;
|
|
494
|
+
border: 0;
|
|
495
|
+
border-radius: var(--ds-radius-md);
|
|
496
|
+
background: none;
|
|
497
|
+
padding: 0.25rem 0.375rem;
|
|
498
|
+
color: inherit;
|
|
499
|
+
font: inherit;
|
|
500
|
+
font-size: var(--ds-text-2xs);
|
|
501
|
+
cursor: pointer;
|
|
502
|
+
transition:
|
|
503
|
+
color 150ms ease,
|
|
504
|
+
background-color 150ms ease;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.ds-lib-action:hover {
|
|
508
|
+
background: var(--ds-color-surface-1);
|
|
509
|
+
color: var(--ds-color-foreground);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.ds-lib-action:focus-visible {
|
|
513
|
+
outline: 2px solid var(--ds-color-ring);
|
|
514
|
+
outline-offset: 2px;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.ds-lib-duration {
|
|
518
|
+
padding-inline-start: 0.25rem;
|
|
519
|
+
font-family: var(--ds-font-code);
|
|
520
|
+
font-size: var(--ds-text-2xs);
|
|
521
|
+
font-variant-numeric: tabular-nums;
|
|
522
|
+
opacity: 0.7;
|
|
523
|
+
}
|
|
524
|
+
</style>
|
|
@@ -22,6 +22,13 @@ interface Props {
|
|
|
22
22
|
* no chip. */
|
|
23
23
|
onsuggest?: (question: string) => void;
|
|
24
24
|
copy?: Partial<LibrarianCopy>;
|
|
25
|
+
/** The persona's display name, signed on the answer card. */
|
|
26
|
+
name?: string;
|
|
27
|
+
/** When the question was asked, epoch ms. Rendered as a clock time —
|
|
28
|
+
* which is a different fact from the duration badge beside the actions,
|
|
29
|
+
* and a reader scanning back for "what did I ask after lunch" needs the
|
|
30
|
+
* first one. Absent renders no time rather than a guess. */
|
|
31
|
+
at?: number;
|
|
25
32
|
/** A study artefact rather than an ordinary answer: once settled, this
|
|
26
33
|
* renders as a card instead of prose. */
|
|
27
34
|
kind?: 'answer' | 'artefact';
|
|
@@ -30,11 +30,39 @@
|
|
|
30
30
|
event.preventDefault();
|
|
31
31
|
onopen();
|
|
32
32
|
}}
|
|
33
|
-
class="
|
|
33
|
+
class="ds-lib-artefact-card"
|
|
34
34
|
>
|
|
35
35
|
{#snippet children()}
|
|
36
|
-
<p class="
|
|
36
|
+
<p class="ds-lib-artefact-count">
|
|
37
37
|
{citationCount} source{citationCount === 1 ? '' : 's'}
|
|
38
38
|
</p>
|
|
39
39
|
{/snippet}
|
|
40
40
|
</Panel>
|
|
41
|
+
|
|
42
|
+
<style>
|
|
43
|
+
/* `:global`, and the only one in this package that is not prose inside
|
|
44
|
+
`{@html}`: the class goes to another component's root element, which
|
|
45
|
+
Svelte's scoping hash never reaches. */
|
|
46
|
+
:global(.ds-lib-artefact-card) {
|
|
47
|
+
width: 100%;
|
|
48
|
+
max-width: 28rem;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
text-align: start;
|
|
51
|
+
transition: border-color 150ms ease;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:global(.ds-lib-artefact-card:hover) {
|
|
55
|
+
border-color: var(--ds-color-border-strong);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:global(.ds-lib-artefact-card:focus-visible) {
|
|
59
|
+
outline: 2px solid var(--ds-color-ring);
|
|
60
|
+
outline-offset: 2px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.ds-lib-artefact-count {
|
|
64
|
+
margin: 0;
|
|
65
|
+
color: var(--ds-color-muted-foreground);
|
|
66
|
+
font-size: var(--ds-text-2xs);
|
|
67
|
+
}
|
|
68
|
+
</style>
|