@poodle64/librarian 2026.10.0 → 2026.10.2
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 -83
- package/dist/components/composer/composer.svelte.d.ts +3 -7
- 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 +220 -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/source-list/source-list.svelte +9 -8
- package/dist/copy.d.ts +7 -2
- package/dist/copy.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ src/lib/
|
|
|
35
35
|
document-pane/ the cited document, open at the cited passage
|
|
36
36
|
artefact-card/ an artefact's card, in the host's words
|
|
37
37
|
artefact-pane/ an artefact that is its answer's prose, in the column
|
|
38
|
-
composer/ the input box: attachments, collection
|
|
38
|
+
composer/ the input box: attachments, one collection control, a note, send/stop
|
|
39
39
|
markdown/ sanitised, streaming-safe markdown + highlighting
|
|
40
40
|
activity-group/ a whole investigation, as one quiet line
|
|
41
41
|
tool-row/ one tool call
|
|
@@ -174,12 +174,13 @@ on it fires once and never again.
|
|
|
174
174
|
| Attachments | nothing: `ask()` posts multipart (`question`, `resume`, `collection`, `files[]`) whenever `files` is non-empty, and JSON when it is not. The route must accept both |
|
|
175
175
|
| Reading a cited document | `loadDocument(document_id) => Promise<{title, sections: [{anchor, heading, text}]}>`, proxied through the app's own authenticated route (caspo: `GET /api/sources/documents/{id}/content`) |
|
|
176
176
|
| Asking again | `onregenerate`: re-send the last question as a NEW turn; the package exposes the action and never re-asks by itself |
|
|
177
|
-
| Choosing a collection | `Composer`'s `collections` (`{ name,
|
|
178
|
-
| The empty state | `welcome` and up to three `examples`
|
|
177
|
+
| Choosing a collection | `Composer`'s `collections` (`{ name, title?, description? }[]`, the ones the person may read) and `collection` (the chosen name; undefined is all of them), with `oncollection(name \| undefined)`. From two collections up it is ONE control naming the choice, opening a listbox (searchable from eight) where each shows by its title (the name made readable where there is none) over its description, never a retrieval summary |
|
|
178
|
+
| The empty state | `welcome` and up to three `examples`. With a composer, the opening and the composer sit together in the middle of the panel, the examples, the folded scope and `lead` under it; the first turn docks the composer at the foot |
|
|
179
|
+
| Conversation controls | `toolbar`: a snippet at the measure's trailing edge above the transcript, outside its scroll (the room page's Past questions and New question) |
|
|
179
180
|
| What this surface covers | `scope`: one statement per persona, in the host's own words. Rendered above the first turn and folded to a line once the conversation starts |
|
|
180
181
|
| The words themselves | `copy`: a partial of `ChatCopy`. Every component resolves it itself, so overriding one line does not mean restating the rest |
|
|
181
182
|
| An answer its own way | `turn`: a snippet given each turn's `AgentTranscriptProps` in place of `AgentTranscript`. Render your own from the parts, or `AgentTranscript` inside your own to add to it |
|
|
182
|
-
| What leads a conversation | `lead`: a snippet inside the scroll, under the opening and the scope, so a tall one scrolls with the conversation instead of taking its height
|
|
183
|
+
| What leads a conversation | `lead`: a snippet inside the scroll, under the opening and the scope, so a tall one scrolls with the conversation instead of taking its height; under the centred composer before the first question |
|
|
183
184
|
|
|
184
185
|
Nothing here fetches on its own behalf. The document read is
|
|
185
186
|
authenticated, and a package that called it directly would be reaching past
|
|
@@ -319,8 +320,9 @@ the job may go on by itself; `sendWhileRunning` is true.
|
|
|
319
320
|
| Sharing an answer | `onshare(turn, share)` on `Conversation`, over `chat.share`: every settled answer offers Share, which copies the link `StoredTurn.shared` names; a shared one offers Copy link and Stop sharing. Nothing is shared unless the reader asks. A host's own `turn` places `AnswerShare` (`./answer-share`) with the `onshare` and `shared` it is handed |
|
|
320
321
|
| A shared answer, read | `Conversation` given `[storedTurn(id, 0, answer)]` from `./chat` and no handlers, no composer: the question, the answer and its sources, read-only, citations opening as in the chat |
|
|
321
322
|
| Still answering | `answering` on `Conversation`, from the controller |
|
|
323
|
+
| Reopening | `reopening` on `Conversation` while a reopened conversation has no turns yet (`chat.conversationId` set, `chat.turns` empty): docked and silent, never a new conversation's centred opening |
|
|
322
324
|
| The allowance | `FairUseNotice quota={…}` in the composer snippet: a count while there are questions left, a notice with `support_url` once there are none. Nothing for `exempt`, or a `limit` of 0 |
|
|
323
|
-
| Past conversations | `ConversationList`: newest first, `href` makes each row a link, and each act renders only when its handler is given; a delete asks first
|
|
325
|
+
| Past conversations | `ConversationList`: newest first, `href` makes each row a link, and each act renders only when its handler is given; a delete asks first. A host laying it over the page (a sheet) passes `onclose` for the header's close |
|
|
324
326
|
|
|
325
327
|
### Watching a session somebody else started
|
|
326
328
|
|
|
@@ -480,7 +482,7 @@ of them is composed from the persona's name:
|
|
|
480
482
|
|
|
481
483
|
That one prop carries the working line ("Penny is looking…"), both composer
|
|
482
484
|
placeholders, the uncited-answer note, both failure sentences, the scope
|
|
483
|
-
label, the collection picker's label, the opening line, the name signed on every answer
|
|
485
|
+
label, the collection picker's label, the opening line, the name signed on every answer and the
|
|
484
486
|
accessible name of the scroll region. A slug is fine: `penny` renders as
|
|
485
487
|
"Penny", `chief-engineer` as "Chief Engineer", and a name the host already
|
|
486
488
|
capitalised is left exactly as written.
|
|
@@ -511,8 +513,8 @@ settles, offering "Ask again".
|
|
|
511
513
|
|
|
512
514
|
### Timestamps
|
|
513
515
|
|
|
514
|
-
`Turn.at` (epoch ms) renders as a clock time on the question and the answer
|
|
515
|
-
|
|
516
|
+
`Turn.at` (epoch ms) renders as a clock time on the question and the answer,
|
|
517
|
+
which is a different fact from the duration badge beside the actions. A
|
|
516
518
|
host that persists conversations sets it; a host that does not gets one
|
|
517
519
|
stamped when the turn first appears, and a turn already on screen at mount —
|
|
518
520
|
one read back out of a stored conversation — deliberately shows none rather than claiming
|
|
@@ -541,8 +543,9 @@ running from its own static build. The same script asserts what a screenshot
|
|
|
541
543
|
cannot: that nothing scrolls sideways at any width, that the source pane
|
|
542
544
|
opens and closes from the keyboard with focus returning to the chip and is
|
|
543
545
|
really draggable, that the scope statement folds once there is a
|
|
544
|
-
conversation over it and reopens from that line, that the collection
|
|
545
|
-
starts on all of them
|
|
546
|
+
conversation over it and reopens from that line, that the collection control
|
|
547
|
+
starts on all of them, stays one control for 27 and takes a searched pick from
|
|
548
|
+
the keyboard, that past questions come over the page and hand focus back, that the persona's
|
|
546
549
|
between-tool narration is nowhere in the answer prose before the activity
|
|
547
550
|
line is opened, that a job's artefact and the pages it read open in the
|
|
548
551
|
host's own columns and never in a pane of the package's, and that the reading
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
One question and everything the persona did answering it.
|
|
3
3
|
|
|
4
|
-
Four things carry the shape. The question is a
|
|
5
|
-
edge, so a reader scanning back finds their own
|
|
6
|
-
the answer. The answer is
|
|
7
|
-
name and the time,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
utility class a consuming app's Tailwind has to discover in
|
|
12
|
-
that got away compiled to `max-width: none` in production
|
|
13
|
-
2302px-wide transcript.
|
|
4
|
+
Four things carry the shape, ChatGPT's and Claude's. The question is a quiet
|
|
5
|
+
bubble against the trailing edge, so a reader scanning back finds their own
|
|
6
|
+
words without reading any of the answer. The answer is prose under it, signed
|
|
7
|
+
with the persona's name and the time, in no box: space and type tell one turn
|
|
8
|
+
from the next (the operator's bar, 19/10/2026: "clean and elegant, like what
|
|
9
|
+
you get when you chat with ChatGPT or Claude"). Everything the persona DID is
|
|
10
|
+
one quiet line in the answer, never a stack. And the measure is the package's
|
|
11
|
+
own CSS, never a utility class a consuming app's Tailwind has to discover in
|
|
12
|
+
`dist` — the one that got away compiled to `max-width: none` in production
|
|
13
|
+
and left a 2302px-wide transcript.
|
|
14
14
|
-->
|
|
15
15
|
<script lang="ts">
|
|
16
16
|
import CheckIcon from '@lucide/svelte/icons/check';
|
|
@@ -364,7 +364,7 @@
|
|
|
364
364
|
.ds-lib-turn {
|
|
365
365
|
display: flex;
|
|
366
366
|
flex-direction: column;
|
|
367
|
-
gap:
|
|
367
|
+
gap: 1rem;
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
.ds-lib-ask {
|
|
@@ -376,27 +376,19 @@
|
|
|
376
376
|
align-self: flex-end;
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
/*
|
|
380
|
-
a
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
dark-mode palette lightens its primary so the SAME token can serve as
|
|
386
|
-
coloured text on a dark page. A fill behind light text and text on a dark
|
|
387
|
-
ground want opposite lightness, and a brand asked to be both comes out
|
|
388
|
-
washed — a deep red reads as salmon. An app with a fill-grade brand
|
|
389
|
-
colour declares the solid pair; one that has not is unaffected.
|
|
390
|
-
It stays on the SHORT side of the transcript on purpose — a question is
|
|
391
|
-
a line or two, an answer is paragraphs, so filling the question is an
|
|
392
|
-
accent and filling the answer would be a wall. */
|
|
379
|
+
/* A raised tone, not the brand: both references set the reader's words on
|
|
380
|
+
a quiet surface at the trailing edge, and position and shape are what a
|
|
381
|
+
reader scanning back finds them by. It stays on the SHORT side of the
|
|
382
|
+
transcript on purpose — a question is a line or two, an answer is
|
|
383
|
+
paragraphs, so the bubble is an accent and boxing the answer would be a
|
|
384
|
+
wall. */
|
|
393
385
|
.ds-lib-bubble {
|
|
394
386
|
max-width: 85%;
|
|
395
387
|
border: 1px solid transparent;
|
|
396
388
|
border-radius: var(--ds-radius-xl);
|
|
397
|
-
background: var(--ds-color-
|
|
389
|
+
background: var(--ds-color-surface-2);
|
|
398
390
|
padding: 0.625rem 1rem;
|
|
399
|
-
color: var(--ds-color-
|
|
391
|
+
color: var(--ds-color-foreground);
|
|
400
392
|
font-size: 1rem;
|
|
401
393
|
line-height: 1.5rem;
|
|
402
394
|
overflow-wrap: break-word;
|
|
@@ -410,18 +402,10 @@
|
|
|
410
402
|
font-variant-numeric: tabular-nums;
|
|
411
403
|
}
|
|
412
404
|
|
|
413
|
-
/* The card. A turn a reader can see the edges of is the whole of what the
|
|
414
|
-
director was asking for: one question and its answer, bounded, rather
|
|
415
|
-
than prose running edge to edge under the last lot of prose. */
|
|
416
405
|
.ds-lib-answer {
|
|
417
406
|
display: flex;
|
|
418
407
|
flex-direction: column;
|
|
419
408
|
gap: 0.75rem;
|
|
420
|
-
border: 1px solid var(--ds-color-border);
|
|
421
|
-
border-inline-start: 3px solid var(--ds-color-primary);
|
|
422
|
-
border-radius: var(--ds-radius-xl);
|
|
423
|
-
background: var(--ds-color-surface-2);
|
|
424
|
-
padding: 0.875rem 1rem 0.75rem;
|
|
425
409
|
}
|
|
426
410
|
|
|
427
411
|
.ds-lib-answer-head {
|
|
@@ -502,7 +486,7 @@
|
|
|
502
486
|
}
|
|
503
487
|
|
|
504
488
|
.ds-lib-action:hover {
|
|
505
|
-
background: var(--ds-color-surface-
|
|
489
|
+
background: var(--ds-color-surface-2);
|
|
506
490
|
color: var(--ds-color-foreground);
|
|
507
491
|
}
|
|
508
492
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** A collection a person may read, as the app lists it: `name` is what a
|
|
2
|
+
* question is narrowed to, `title` is what the picker shows (the name,
|
|
3
|
+
* made readable, where there is none) and `description` is its one line
|
|
4
|
+
* under the title. A retrieval summary is never a field here: it is filing
|
|
5
|
+
* history. */
|
|
6
|
+
export interface CollectionChoice {
|
|
7
|
+
name: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
/** `defence-housing` as a reader would first read it: "Defence housing". */
|
|
12
|
+
export declare function titleOf(choice: CollectionChoice): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** `defence-housing` as a reader would first read it: "Defence housing". */
|
|
2
|
+
export function titleOf(choice) {
|
|
3
|
+
const given = choice.title?.trim();
|
|
4
|
+
if (given)
|
|
5
|
+
return given;
|
|
6
|
+
const words = choice.name.replace(/[-_]+/g, ' ').trim();
|
|
7
|
+
return words ? words[0].toLocaleUpperCase() + words.slice(1) : choice.name;
|
|
8
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
The composer: one rounded box holding attachments, the input AND a footer row.
|
|
3
3
|
|
|
4
|
-
Two things carry it: the
|
|
5
|
-
than beside it,
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
Two things carry it: the quiet controls sit BELOW the input inside the same
|
|
5
|
+
box rather than beside it (attach, a briefing, and where the question looks
|
|
6
|
+
as one compact control), and the trailing control is a circular filled
|
|
7
|
+
accent button. The empty→typing transition lives almost entirely in that
|
|
8
|
+
control — the box chrome grows with the text and nothing else moves.
|
|
8
9
|
|
|
9
10
|
While the persona is answering the box is disabled and the send button becomes a
|
|
10
11
|
stop button: an ask route takes one question per request, and a box that
|
|
@@ -27,24 +28,23 @@
|
|
|
27
28
|
rejectionMessage
|
|
28
29
|
} from '../../attachments';
|
|
29
30
|
import { DEFAULT_PERSONA, resolveCopy, type ChatCopy } from '../../copy';
|
|
31
|
+
import type { CollectionChoice } from './choices';
|
|
32
|
+
import ScopePicker from './scope-picker.svelte';
|
|
30
33
|
|
|
31
34
|
/** Past this many turns, the long-conversation banner offers to start
|
|
32
35
|
* over: a follow-up re-reads the whole conversation, so each one costs
|
|
33
36
|
* more than the last (measured — the longest ran 2.05M cached tokens). */
|
|
34
37
|
const DEFAULT_TURN_LIMIT = 8;
|
|
35
38
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
export interface CollectionChoice {
|
|
39
|
-
name: string;
|
|
40
|
-
summary: string;
|
|
41
|
-
}
|
|
39
|
+
/** px kept between a lifted box and the keyboard. */
|
|
40
|
+
const KEYBOARD_GAP = 12;
|
|
42
41
|
|
|
43
42
|
interface Props {
|
|
44
43
|
value: string;
|
|
45
44
|
running: boolean;
|
|
46
45
|
/** The collections this person may read. The picker renders when there
|
|
47
|
-
* are two or more: with one, or none, there is nothing to choose.
|
|
46
|
+
* are two or more: with one, or none, there is nothing to choose. It
|
|
47
|
+
* is one control however many there are, searchable from eight. */
|
|
48
48
|
collections?: CollectionChoice[];
|
|
49
49
|
/** The collection the next question is narrowed to, by name; undefined
|
|
50
50
|
* asks across all of them. */
|
|
@@ -120,14 +120,11 @@
|
|
|
120
120
|
|
|
121
121
|
// Who the question is put to: every collection the person may read, or one
|
|
122
122
|
// of them. A person with one collection (or none) has nothing to pick, so
|
|
123
|
-
//
|
|
124
|
-
// A collection is shown by its summary, the words a reader knows it by.
|
|
123
|
+
// nothing renders rather than a control that only reselects itself.
|
|
125
124
|
const hasChoice = $derived(collections.length > 1);
|
|
126
125
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// cannot finish reading is not a control, it is decoration.
|
|
130
|
-
|
|
126
|
+
let box = $state<HTMLElement | null>(null);
|
|
127
|
+
let wrapper = $state<HTMLElement | null>(null);
|
|
131
128
|
let textarea = $state<HTMLTextAreaElement | null>(null);
|
|
132
129
|
let picker = $state<HTMLInputElement | null>(null);
|
|
133
130
|
let rejected = $state('');
|
|
@@ -204,26 +201,45 @@
|
|
|
204
201
|
|
|
205
202
|
// The keyboard does not shrink the layout viewport on iOS, so a composer at
|
|
206
203
|
// the bottom of a full-height column sits UNDER it. The visual viewport is
|
|
207
|
-
// the one that moves
|
|
208
|
-
// box
|
|
204
|
+
// the one that moves, and the box rises by however far its unlifted foot
|
|
205
|
+
// falls below it: the keyboard's height for a docked box, nothing for one
|
|
206
|
+
// centred above the keyboard on an empty conversation.
|
|
207
|
+
//
|
|
208
|
+
// The foot is read from layout, never from where the box is drawn: the
|
|
209
|
+
// transform's current value (mid-transition included) and the safe-area
|
|
210
|
+
// padding that lifting removes are both taken back out, so a second event
|
|
211
|
+
// inside the 150ms transition cannot read a half-lifted box and overshoot.
|
|
212
|
+
// Any scroll re-measures, since a scrolling stage moves the box too.
|
|
209
213
|
let lift = $state(0);
|
|
210
214
|
$effect(() => {
|
|
211
215
|
const viewport = globalThis.visualViewport;
|
|
212
216
|
if (!viewport) return;
|
|
213
217
|
const measure = () => {
|
|
214
|
-
|
|
218
|
+
const shown = viewport.offsetTop + viewport.height;
|
|
219
|
+
if (!wrapper || shown >= window.innerHeight) {
|
|
220
|
+
lift = 0;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const style = getComputedStyle(wrapper);
|
|
224
|
+
const drawn = style.transform === 'none' ? 0 : new DOMMatrixReadOnly(style.transform).m42;
|
|
225
|
+
const foot =
|
|
226
|
+
wrapper.getBoundingClientRect().bottom - drawn - (parseFloat(style.paddingBottom) || 0);
|
|
227
|
+
lift = Math.max(0, Math.round(foot + KEYBOARD_GAP - shown));
|
|
215
228
|
};
|
|
216
229
|
measure();
|
|
217
230
|
viewport.addEventListener('resize', measure);
|
|
218
231
|
viewport.addEventListener('scroll', measure);
|
|
232
|
+
document.addEventListener('scroll', measure, { capture: true, passive: true });
|
|
219
233
|
return () => {
|
|
220
234
|
viewport.removeEventListener('resize', measure);
|
|
221
235
|
viewport.removeEventListener('scroll', measure);
|
|
236
|
+
document.removeEventListener('scroll', measure, { capture: true });
|
|
222
237
|
};
|
|
223
238
|
});
|
|
224
239
|
</script>
|
|
225
240
|
|
|
226
241
|
<div
|
|
242
|
+
bind:this={wrapper}
|
|
227
243
|
class="ds-lib-composer-lift"
|
|
228
244
|
style="transform: translateY(-{lift}px); padding-bottom: {lift
|
|
229
245
|
? '0px'
|
|
@@ -239,7 +255,7 @@
|
|
|
239
255
|
</div>
|
|
240
256
|
{/if}
|
|
241
257
|
|
|
242
|
-
<div class="ds-lib-box">
|
|
258
|
+
<div class="ds-lib-box" bind:this={box}>
|
|
243
259
|
{#if files.length > 0}
|
|
244
260
|
<ul class="ds-lib-files">
|
|
245
261
|
{#each files as file, index (file.name + file.size)}
|
|
@@ -307,28 +323,16 @@
|
|
|
307
323
|
{/if}
|
|
308
324
|
|
|
309
325
|
{#if hasChoice}
|
|
310
|
-
<
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
{#each collections as choice (choice.name)}
|
|
321
|
-
<button
|
|
322
|
-
type="button"
|
|
323
|
-
class="ds-lib-scope-chip"
|
|
324
|
-
aria-pressed={collection === choice.name}
|
|
325
|
-
class:is-chosen={collection === choice.name}
|
|
326
|
-
onclick={() => oncollection?.(choice.name)}
|
|
327
|
-
>
|
|
328
|
-
{choice.summary || choice.name}
|
|
329
|
-
</button>
|
|
330
|
-
{/each}
|
|
331
|
-
</div>
|
|
326
|
+
<ScopePicker
|
|
327
|
+
{collections}
|
|
328
|
+
{collection}
|
|
329
|
+
{words}
|
|
330
|
+
anchor={box}
|
|
331
|
+
onchoose={(name) => {
|
|
332
|
+
oncollection?.(name);
|
|
333
|
+
textarea?.focus();
|
|
334
|
+
}}
|
|
335
|
+
/>
|
|
332
336
|
{:else if note}
|
|
333
337
|
<span class="ds-lib-note">{note}</span>
|
|
334
338
|
{/if}
|
|
@@ -362,7 +366,9 @@
|
|
|
362
366
|
transition: transform 150ms ease;
|
|
363
367
|
}
|
|
364
368
|
|
|
369
|
+
/* Positioned: the scope list is placed against it. */
|
|
365
370
|
.ds-lib-box {
|
|
371
|
+
position: relative;
|
|
366
372
|
border: 1px solid var(--ds-color-border);
|
|
367
373
|
border-radius: var(--ds-radius-xl);
|
|
368
374
|
background: var(--ds-color-surface-1);
|
|
@@ -500,7 +506,6 @@
|
|
|
500
506
|
.ds-lib-icon-button:focus-visible,
|
|
501
507
|
.ds-lib-stop:focus-visible,
|
|
502
508
|
.ds-lib-send:focus-visible,
|
|
503
|
-
.ds-lib-scope-chip:focus-visible,
|
|
504
509
|
.ds-lib-file-remove:focus-visible {
|
|
505
510
|
outline: 2px solid var(--ds-color-ring);
|
|
506
511
|
outline-offset: 2px;
|
|
@@ -521,45 +526,6 @@
|
|
|
521
526
|
opacity: 0.9;
|
|
522
527
|
}
|
|
523
528
|
|
|
524
|
-
/* Three chips in a narrow column clipped all three to fragments
|
|
525
|
-
("defence-s…", "All colle…"). Wrapping beats truncating: a chip a reader
|
|
526
|
-
cannot finish reading is not a control, it is decoration. */
|
|
527
|
-
.ds-lib-scopes {
|
|
528
|
-
display: flex;
|
|
529
|
-
min-width: 0;
|
|
530
|
-
flex-wrap: wrap;
|
|
531
|
-
align-items: center;
|
|
532
|
-
gap: 0.25rem;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
.ds-lib-scope-chip {
|
|
536
|
-
max-width: 100%;
|
|
537
|
-
overflow: hidden;
|
|
538
|
-
border: 1px solid transparent;
|
|
539
|
-
border-radius: var(--ds-radius-full);
|
|
540
|
-
background: none;
|
|
541
|
-
padding: 0.125rem 0.5rem;
|
|
542
|
-
color: var(--ds-color-muted-foreground);
|
|
543
|
-
font: inherit;
|
|
544
|
-
font-size: var(--ds-text-2xs);
|
|
545
|
-
text-overflow: ellipsis;
|
|
546
|
-
white-space: nowrap;
|
|
547
|
-
cursor: pointer;
|
|
548
|
-
transition:
|
|
549
|
-
color 150ms ease,
|
|
550
|
-
background-color 150ms ease;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
.ds-lib-scope-chip:hover {
|
|
554
|
-
color: var(--ds-color-foreground);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
.ds-lib-scope-chip.is-chosen {
|
|
558
|
-
border-color: color-mix(in oklab, var(--ds-color-primary) 40%, transparent);
|
|
559
|
-
background: color-mix(in oklab, var(--ds-color-primary) 15%, transparent);
|
|
560
|
-
color: var(--ds-color-foreground);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
529
|
.ds-lib-spacer {
|
|
564
530
|
flex: 1;
|
|
565
531
|
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { type ChatCopy } from '../../copy';
|
|
2
|
-
|
|
3
|
-
* question is narrowed to, `summary` is what the picker shows. */
|
|
4
|
-
export interface CollectionChoice {
|
|
5
|
-
name: string;
|
|
6
|
-
summary: string;
|
|
7
|
-
}
|
|
2
|
+
import type { CollectionChoice } from './choices';
|
|
8
3
|
interface Props {
|
|
9
4
|
value: string;
|
|
10
5
|
running: boolean;
|
|
11
6
|
/** The collections this person may read. The picker renders when there
|
|
12
|
-
* are two or more: with one, or none, there is nothing to choose.
|
|
7
|
+
* are two or more: with one, or none, there is nothing to choose. It
|
|
8
|
+
* is one control however many there are, searchable from eight. */
|
|
13
9
|
collections?: CollectionChoice[];
|
|
14
10
|
/** The collection the next question is narrowed to, by name; undefined
|
|
15
11
|
* asks across all of them. */
|