@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.
- package/README.md +13 -10
- package/dist/components/agent-transcript/agent-transcript.svelte +20 -36
- package/dist/components/answer-mark/answer-mark.svelte +1 -1
- package/dist/components/answer-share/answer-share.svelte +1 -1
- package/dist/components/composer/choices.d.ts +12 -0
- package/dist/components/composer/choices.js +8 -0
- package/dist/components/composer/composer.svelte +49 -95
- package/dist/components/composer/composer.svelte.d.ts +3 -10
- package/dist/components/composer/index.d.ts +1 -1
- package/dist/components/composer/scope-picker.svelte +418 -0
- package/dist/components/composer/scope-picker.svelte.d.ts +14 -0
- package/dist/components/conversation/conversation.svelte +227 -99
- package/dist/components/conversation/conversation.svelte.d.ts +13 -3
- package/dist/components/conversation-list/conversation-list.svelte +22 -4
- package/dist/components/conversation-list/conversation-list.svelte.d.ts +2 -0
- package/dist/components/fair-use-notice/fair-use-notice.svelte +1 -1
- package/dist/components/markdown/markdown.svelte +2 -1
- package/dist/components/scope-statement/scope-statement.svelte +17 -20
- package/dist/components/source-list/source-list.svelte +9 -8
- package/dist/copy.d.ts +6 -1
- package/dist/copy.js +3 -0
- package/package.json +1 -1
|
@@ -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
|
-
/**
|
|
95
|
-
* the scope
|
|
96
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
334
|
-
<
|
|
335
|
-
<
|
|
336
|
-
|
|
337
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
/**
|
|
71
|
-
* the scope
|
|
72
|
-
*
|
|
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>;
|
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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.
|
|
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:
|
|
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
|
|
123
|
+
margin: 0;
|
|
127
124
|
color: var(--ds-color-muted-foreground);
|
|
128
125
|
font-size: 0.875rem;
|
|
129
126
|
line-height: 1.5rem;
|