@poodle64/librarian 2026.10.4 → 2026.10.5

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 (50) hide show
  1. package/README.md +7 -5
  2. package/dist/components/activity-group/activity-group.svelte +99 -17
  3. package/dist/components/activity-group/activity-group.svelte.d.ts +4 -1
  4. package/dist/components/agent-transcript/agent-transcript.svelte +82 -26
  5. package/dist/components/agent-transcript/agent-transcript.svelte.d.ts +4 -3
  6. package/dist/components/artefact-pane/artefact-pane.svelte +278 -78
  7. package/dist/components/conversation/conversation.svelte +6 -6
  8. package/dist/components/conversation/conversation.svelte.d.ts +4 -4
  9. package/dist/components/markdown/markdown.d.ts +27 -10
  10. package/dist/components/markdown/markdown.js +99 -22
  11. package/dist/components/markdown/markdown.svelte +122 -28
  12. package/dist/components/markdown/markdown.svelte.d.ts +12 -4
  13. package/dist/components/source-fold/index.d.ts +2 -0
  14. package/dist/components/source-fold/index.js +2 -0
  15. package/dist/components/source-fold/source-fold.svelte +225 -0
  16. package/dist/components/source-fold/source-fold.svelte.d.ts +17 -0
  17. package/dist/components/source-list/source-list.svelte +5 -3
  18. package/dist/components/source-list/source-list.svelte.d.ts +2 -0
  19. package/dist/components/thinking-row/thinking-row.svelte +37 -16
  20. package/dist/components/thinking-row/thinking-row.svelte.d.ts +2 -0
  21. package/dist/components/tool-row/tool-row.svelte +130 -38
  22. package/dist/components/visual-block/entity-card.svelte +94 -0
  23. package/dist/components/visual-block/entity-card.svelte.d.ts +7 -0
  24. package/dist/components/visual-block/timeline.svelte +99 -0
  25. package/dist/components/visual-block/timeline.svelte.d.ts +7 -0
  26. package/dist/components/visual-block/vega-chart.svelte +110 -0
  27. package/dist/components/visual-block/vega-chart.svelte.d.ts +7 -0
  28. package/dist/components/visual-block/visual-block.svelte +53 -0
  29. package/dist/components/visual-block/visual-block.svelte.d.ts +13 -0
  30. package/dist/components/written-card/written-card.svelte +97 -49
  31. package/dist/components/written-card/written-card.svelte.d.ts +1 -1
  32. package/dist/copy.d.ts +54 -0
  33. package/dist/copy.js +44 -1
  34. package/dist/diff.d.ts +8 -0
  35. package/dist/diff.js +37 -0
  36. package/dist/steps.d.ts +28 -0
  37. package/dist/steps.js +168 -0
  38. package/dist/transcript.svelte.d.ts +4 -0
  39. package/dist/transcript.svelte.js +13 -2
  40. package/dist/visuals.d.ts +38 -0
  41. package/dist/visuals.js +82 -0
  42. package/dist/web-sources.d.ts +19 -3
  43. package/dist/web-sources.js +39 -7
  44. package/dist/written.d.ts +26 -1
  45. package/dist/written.js +42 -0
  46. package/package.json +10 -1
  47. package/dist/components/web-list/index.d.ts +0 -2
  48. package/dist/components/web-list/index.js +0 -2
  49. package/dist/components/web-list/web-list.svelte +0 -84
  50. package/dist/components/web-list/web-list.svelte.d.ts +0 -9
@@ -17,7 +17,8 @@
17
17
  */
18
18
  import DOMPurify from 'isomorphic-dompurify';
19
19
  import { marked } from 'marked';
20
- import { mount, unmount } from 'svelte';
20
+ import { markChanges } from '../../diff';
21
+ import { webKey } from '../../web-sources';
21
22
  marked.setOptions({ gfm: true, breaks: false });
22
23
  /** Close anything the stream has opened but not yet finished. */
23
24
  export function balance(markdown) {
@@ -71,17 +72,25 @@ function dropFetchers(root) {
71
72
  el.remove();
72
73
  }
73
74
  }
74
- export function render(markdown, { streaming = false } = {}) {
75
- const source = streaming ? balance(markdown) : markdown;
76
- const html = marked.parse(source, { async: false });
77
- const body = DOMPurify.sanitize(html, {
75
+ /**
76
+ * `markdown` as sanitised HTML. With `previous`, the version before it, each
77
+ * block it changed is marked on the sanitised DOM, before its one serialisation.
78
+ */
79
+ export function render(markdown, { streaming = false, previous } = {}) {
80
+ const body = sanitised(streaming ? balance(markdown) : markdown);
81
+ if (previous !== undefined && !streaming)
82
+ markChanges(body, sanitised(previous));
83
+ return body.innerHTML;
84
+ }
85
+ function sanitised(source) {
86
+ const body = DOMPurify.sanitize(marked.parse(source, { async: false }), {
78
87
  ADD_ATTR: ['target', 'rel', 'data-cite', 'tabindex', 'role'],
79
88
  FORBID_TAGS: ['style', 'form', 'input', 'button'],
80
89
  FORBID_ATTR: ['style', 'srcset', 'onerror', 'onload'],
81
90
  RETURN_DOM: true
82
91
  });
83
92
  dropFetchers(body);
84
- return body.innerHTML;
93
+ return body;
85
94
  }
86
95
  let highlighterPromise = null;
87
96
  /** Loaded once, lazily — shiki's engine and grammars are not small. */
@@ -176,26 +185,94 @@ export function markCitations(root, valid) {
176
185
  text.replaceWith(fragment);
177
186
  }
178
187
  }
188
+ const OPEN = /^ {0,3}(`{3,}|~{3,})[ \t]*([^\s`]*)[^`]*$/;
189
+ const CLOSE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
190
+ /**
191
+ * Cut `markdown` at each fenced block whose language `has` a renderer for.
192
+ * A fence of another language (or none) is ordinary code and stays in the
193
+ * prose, and so does anything inside one: an example of a visual's fence in a
194
+ * code block is text. A block still open at the end is `closed: false`.
195
+ */
196
+ export function splitVisuals(markdown, has) {
197
+ const parts = [];
198
+ let prose = [];
199
+ let code = null;
200
+ const flush = () => {
201
+ if (prose.join('').trim())
202
+ parts.push({ kind: 'md', text: prose.join('\n') });
203
+ prose = [];
204
+ };
205
+ for (const line of markdown.split('\n')) {
206
+ if (code) {
207
+ const close = CLOSE.exec(line);
208
+ if (close && close[1][0] === code.fence[0] && close[1].length >= code.fence.length) {
209
+ if (code.visual) {
210
+ parts.push({
211
+ kind: 'visual',
212
+ lang: code.visual.lang,
213
+ source: code.visual.lines.join('\n'),
214
+ closed: true
215
+ });
216
+ }
217
+ else
218
+ prose.push(line);
219
+ code = null;
220
+ }
221
+ else if (code.visual)
222
+ code.visual.lines.push(line);
223
+ else
224
+ prose.push(line);
225
+ continue;
226
+ }
227
+ const open = OPEN.exec(line);
228
+ if (open) {
229
+ const lang = open[2];
230
+ if (lang && has(lang)) {
231
+ flush();
232
+ code = { fence: open[1], visual: { lang, lines: [] } };
233
+ }
234
+ else {
235
+ code = { fence: open[1] };
236
+ prose.push(line);
237
+ }
238
+ continue;
239
+ }
240
+ prose.push(line);
241
+ }
242
+ if (code?.visual)
243
+ parts.push({
244
+ kind: 'visual',
245
+ lang: code.visual.lang,
246
+ source: code.visual.lines.join('\n'),
247
+ closed: false
248
+ });
249
+ else
250
+ flush();
251
+ return parts;
252
+ }
179
253
  /**
180
- * Draw every fenced block whose language the host registered with the host's
181
- * component, in place of the code block. The source is the block's text, never
182
- * HTML. Returns what unmounts them.
254
+ * Turn each link to a numbered web page into its citation chip: the link's
255
+ * text stays, unlinked, and the chip after it carries the page's number and
256
+ * opens the page. Against the rendered DOM, for the reason `markCitations` is.
183
257
  */
184
- export function mountFences(root, fences) {
185
- const drawn = [];
186
- for (const code of root.querySelectorAll('pre > code')) {
187
- const lang = [...code.classList].find((c) => c.startsWith('language-'))?.slice(9);
188
- const pre = code.parentElement;
189
- if (!lang || !pre || !Object.hasOwn(fences, lang))
258
+ export function markWebLinks(root, numbers) {
259
+ if (numbers.size === 0)
260
+ return;
261
+ for (const link of root.querySelectorAll('a[href]')) {
262
+ const href = link.getAttribute('href') ?? '';
263
+ const n = numbers.get(webKey(href));
264
+ if (n === undefined)
190
265
  continue;
191
- const target = document.createElement('div');
192
- pre.replaceWith(target);
193
- drawn.push(mount(fences[lang], { target, props: { source: code.textContent ?? '' } }));
266
+ const chip = document.createElement('sup');
267
+ chip.className = 'ds-cite';
268
+ chip.dataset.cite = String(n);
269
+ chip.dataset.web = href;
270
+ chip.setAttribute('role', 'button');
271
+ chip.setAttribute('tabindex', '0');
272
+ chip.setAttribute('aria-label', `Web page ${n}`);
273
+ chip.textContent = String(n);
274
+ link.replaceWith(...link.childNodes, chip);
194
275
  }
195
- return () => {
196
- for (const one of drawn)
197
- void unmount(one);
198
- };
199
276
  }
200
277
  /**
201
278
  * Replace every `<pre><code>` in a rendered fragment with a highlighted one.
@@ -7,13 +7,16 @@
7
7
  consuming app.
8
8
  -->
9
9
  <script lang="ts">
10
+ import { resolveCopy, type ChatCopy } from '../../copy';
11
+ import type { Visual } from '../../visuals';
12
+ import VisualBlock from '../visual-block/visual-block.svelte';
10
13
  import {
11
14
  highlight,
12
15
  markCitations,
13
16
  markCollections,
14
- mountFences,
17
+ markWebLinks,
15
18
  render,
16
- type Fences
19
+ splitVisuals
17
20
  } from './markdown';
18
21
 
19
22
  interface Props {
@@ -23,13 +26,20 @@
23
26
  collectionNames?: Set<string>;
24
27
  /** `[n]` markers to turn into chips; omit and the markers stay as text. */
25
28
  citationNumbers?: Set<number>;
29
+ /** Web pages by `webKey`, numbered after the library's: a link to one
30
+ * renders as its chip. */
31
+ webNumbers?: Map<string, number>;
26
32
  oncite?: (n: number) => void;
27
33
  /** A step down, for prose that is reference material beside an answer
28
34
  * rather than the answer itself — the source pane's sections. */
29
35
  dense?: boolean;
30
- /** A language to draw with the host's own component instead of as code.
31
- * Absent, every fence renders as code. */
32
- fences?: Fences;
36
+ /** Renderers by a fence's language: a block of one is drawn by its
37
+ * component in place of the code. Absent, every fence is code. */
38
+ visuals?: Record<string, Visual>;
39
+ /** The same document's earlier version, as Markdown: what differs here
40
+ * is marked with a bar. */
41
+ previous?: string;
42
+ copy?: Partial<ChatCopy>;
33
43
  }
34
44
 
35
45
  let {
@@ -37,53 +47,68 @@
37
47
  streaming = false,
38
48
  collectionNames = new Set(),
39
49
  citationNumbers = new Set(),
50
+ webNumbers = new Map(),
40
51
  oncite,
41
52
  dense = false,
42
- fences
53
+ visuals,
54
+ previous,
55
+ copy
43
56
  }: Props = $props();
44
57
  let host = $state<HTMLElement | null>(null);
45
58
 
46
- const html = $derived(render(content, { streaming }));
59
+ const words = $derived(resolveCopy(copy));
47
60
 
48
- // Once settled (a half-written fence is not a chart), and before the
49
- // highlight below, which would otherwise take the block first. Its own
50
- // effect, so a change to the chips never unmounts what it drew.
51
- $effect(() => {
52
- void html;
53
- if (!host || streaming || !fences) return;
54
- return mountFences(host, fences);
55
- });
61
+ // A visual is its own component between chunks of prose, so a token that
62
+ // grows the last chunk leaves the charts above it standing.
63
+ const parts = $derived(
64
+ visuals
65
+ ? splitVisuals(content, (lang) => Object.hasOwn(visuals, lang))
66
+ : [{ kind: 'md' as const, text: content }]
67
+ );
68
+ const chunked = $derived(parts.some((part) => part.kind === 'visual'));
69
+ const lastPart = $derived(parts.length - 1);
70
+
71
+ const prose = (text: string, last: boolean): string => render(text, { streaming: streaming && last, previous });
56
72
 
57
73
  // Highlight only once the text has settled. Re-running per token would
58
74
  // re-parse grammars for a block that changes again milliseconds later.
59
75
  $effect(() => {
60
- if (!host || !html) return;
76
+ void parts;
77
+ if (!host) return;
61
78
  // Chips are cheap and wanted DURING streaming; highlighting is not.
62
79
  markCollections(host, collectionNames);
63
80
  markCitations(host, citationNumbers);
81
+ markWebLinks(host, webNumbers);
64
82
  if (streaming) return;
65
83
  void highlight(host);
66
84
  });
67
85
 
68
86
  // Delegated, because the chips are created by the sanitiser's output rather
69
87
  // than by this template — there is no element here to put a handler on.
70
- function citeFrom(target: EventTarget | null): number | null {
71
- const chip = (target as HTMLElement | null)?.closest?.('[data-cite]');
72
- const n = Number((chip as HTMLElement | undefined)?.dataset.cite);
73
- return Number.isFinite(n) && n > 0 ? n : null;
88
+ function chipOf(target: EventTarget | null): HTMLElement | null {
89
+ return ((target as HTMLElement | null)?.closest?.('[data-cite]') as HTMLElement | null) ?? null;
90
+ }
91
+
92
+ function press(chip: HTMLElement) {
93
+ if (chip.dataset.web) {
94
+ window.open(chip.dataset.web, '_blank', 'noopener,noreferrer');
95
+ return;
96
+ }
97
+ const n = Number(chip.dataset.cite);
98
+ if (Number.isFinite(n) && n > 0) oncite?.(n);
74
99
  }
75
100
 
76
101
  function click(event: MouseEvent) {
77
- const n = citeFrom(event.target);
78
- if (n !== null) oncite?.(n);
102
+ const chip = chipOf(event.target);
103
+ if (chip) press(chip);
79
104
  }
80
105
 
81
106
  function keydown(event: KeyboardEvent) {
82
107
  if (event.key !== 'Enter' && event.key !== ' ') return;
83
- const n = citeFrom(event.target);
84
- if (n === null) return;
108
+ const chip = chipOf(event.target);
109
+ if (!chip) return;
85
110
  event.preventDefault();
86
- oncite?.(n);
111
+ press(chip);
87
112
  }
88
113
  </script>
89
114
 
@@ -97,9 +122,30 @@
97
122
  class="agent-prose"
98
123
  class:is-dense={dense}
99
124
  class:is-streaming={streaming}
125
+ class:is-chunked={chunked}
100
126
  >
101
- <!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in render() -->
102
- {@html html}
127
+ {#if !chunked}
128
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in render() -->
129
+ {@html prose(content, true)}
130
+ {:else}
131
+ {#each parts as part, i (i)}
132
+ {#if part.kind === 'md'}
133
+ <div class="ds-lib-chunk">
134
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitised in render() -->
135
+ {@html prose(part.text, i === lastPart)}
136
+ </div>
137
+ {:else if visuals}
138
+ <VisualBlock
139
+ lang={part.lang}
140
+ visual={visuals[part.lang]}
141
+ source={part.source}
142
+ closed={part.closed}
143
+ {streaming}
144
+ {words}
145
+ />
146
+ {/if}
147
+ {/each}
148
+ {/if}
103
149
  </div>
104
150
 
105
151
  <style>
@@ -131,6 +177,31 @@
131
177
  animation: ds-lib-caret 1.1s steps(1, end) infinite;
132
178
  }
133
179
 
180
+ /* Chunked, the caret rides the last prose chunk's last block, not the chunk. */
181
+ .agent-prose.is-chunked.is-streaming :global(> .ds-lib-chunk:last-child::after) {
182
+ content: none;
183
+ }
184
+
185
+ .agent-prose.is-chunked.is-streaming :global(> .ds-lib-chunk:last-child > *:last-child::after) {
186
+ content: '';
187
+ display: inline-block;
188
+ width: 0.45em;
189
+ height: 1.05em;
190
+ margin-inline-start: 0.12em;
191
+ border-radius: 1px;
192
+ background: var(--ds-color-primary);
193
+ vertical-align: -0.15em;
194
+ animation: ds-lib-caret 1.1s steps(1, end) infinite;
195
+ }
196
+
197
+ .agent-prose :global(.ds-lib-chunk > *:first-child) {
198
+ margin-top: 0;
199
+ }
200
+
201
+ .agent-prose :global(.ds-lib-chunk > *:last-child) {
202
+ margin-bottom: 0;
203
+ }
204
+
134
205
  @keyframes ds-lib-caret {
135
206
  50% {
136
207
  opacity: 0;
@@ -138,7 +209,8 @@
138
209
  }
139
210
 
140
211
  @media (prefers-reduced-motion: reduce) {
141
- .agent-prose.is-streaming :global(> *:last-child::after) {
212
+ .agent-prose.is-streaming :global(> *:last-child::after),
213
+ .agent-prose.is-chunked.is-streaming :global(> .ds-lib-chunk:last-child > *:last-child::after) {
142
214
  animation: none;
143
215
  }
144
216
  }
@@ -374,6 +446,28 @@
374
446
  outline-offset: 2px;
375
447
  }
376
448
 
449
+ /* What this version changed: a bar in the margin beside the block. */
450
+ .agent-prose :global([data-changed]) {
451
+ position: relative;
452
+ }
453
+
454
+ .agent-prose :global([data-changed]::before) {
455
+ content: '';
456
+ position: absolute;
457
+ inset-block: 0;
458
+ inset-inline-start: -0.75rem;
459
+ width: 2px;
460
+ background: var(--ds-color-foreground);
461
+ }
462
+
463
+ .agent-prose :global(tr[data-changed]::before) {
464
+ content: none;
465
+ }
466
+
467
+ .agent-prose :global(tr[data-changed] > td:first-child) {
468
+ box-shadow: inset 2px 0 0 var(--ds-color-foreground);
469
+ }
470
+
377
471
  .agent-prose :global(hr) {
378
472
  border: 0;
379
473
  border-top: 1px solid var(--ds-color-border);
@@ -1,4 +1,5 @@
1
- import { type Fences } from './markdown';
1
+ import { type ChatCopy } from '../../copy';
2
+ import type { Visual } from '../../visuals';
2
3
  interface Props {
3
4
  content: string;
4
5
  streaming?: boolean;
@@ -6,13 +7,20 @@ interface Props {
6
7
  collectionNames?: Set<string>;
7
8
  /** `[n]` markers to turn into chips; omit and the markers stay as text. */
8
9
  citationNumbers?: Set<number>;
10
+ /** Web pages by `webKey`, numbered after the library's: a link to one
11
+ * renders as its chip. */
12
+ webNumbers?: Map<string, number>;
9
13
  oncite?: (n: number) => void;
10
14
  /** A step down, for prose that is reference material beside an answer
11
15
  * rather than the answer itself — the source pane's sections. */
12
16
  dense?: boolean;
13
- /** A language to draw with the host's own component instead of as code.
14
- * Absent, every fence renders as code. */
15
- fences?: Fences;
17
+ /** Renderers by a fence's language: a block of one is drawn by its
18
+ * component in place of the code. Absent, every fence is code. */
19
+ visuals?: Record<string, Visual>;
20
+ /** The same document's earlier version, as Markdown: what differs here
21
+ * is marked with a bar. */
22
+ previous?: string;
23
+ copy?: Partial<ChatCopy>;
16
24
  }
17
25
  declare const Markdown: import("svelte").Component<Props, {}, "">;
18
26
  type Markdown = ReturnType<typeof Markdown>;
@@ -0,0 +1,2 @@
1
+ export { default } from './source-fold.svelte';
2
+ export { default as SourceFold } from './source-fold.svelte';
@@ -0,0 +1,2 @@
1
+ export { default } from './source-fold.svelte';
2
+ export { default as SourceFold } from './source-fold.svelte';
@@ -0,0 +1,225 @@
1
+ <!--
2
+ What an answer drew on, folded behind one line: "6 sources · 4 library · 2 web".
3
+
4
+ It opens on the library's numbered sources and then the web pages, numbered
5
+ on from them: the persona cites a page by its link, and the page numbers the
6
+ link with the same chip a library source gets.
7
+ -->
8
+ <script lang="ts">
9
+ import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
10
+ import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
11
+ import GlobeIcon from '@lucide/svelte/icons/globe';
12
+ import LibraryIcon from '@lucide/svelte/icons/library';
13
+ import type { Citation } from '../../citations';
14
+ import { fill, type ChatCopy } from '../../copy';
15
+ import type { WebSource } from '../../web-sources';
16
+ import SourceList from '../source-list/source-list.svelte';
17
+
18
+ interface Props {
19
+ library: Citation[];
20
+ /** Pages, each with the number the answer's chip for it carries. */
21
+ web: Array<WebSource & { n: number }>;
22
+ words: ChatCopy;
23
+ oncite?: (citation: Citation) => void;
24
+ /** When the pages were read, in words ("read today"), for a page the turn read. */
25
+ readWhen?: string;
26
+ }
27
+
28
+ let { library, web, words, oncite, readWhen = '' }: Props = $props();
29
+ let open = $state(false);
30
+
31
+ const total = $derived(library.length + web.length);
32
+
33
+ function when(page: WebSource): string {
34
+ if (page.date) {
35
+ const date = new Date(page.date);
36
+ return date.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' });
37
+ }
38
+ return page.read ? readWhen : '';
39
+ }
40
+ </script>
41
+
42
+ <section class="ds-lib-fold">
43
+ <button
44
+ type="button"
45
+ class="ds-lib-fold-head"
46
+ onclick={() => (open = !open)}
47
+ aria-expanded={open}
48
+ >
49
+ <span class="ds-lib-fold-icons" aria-hidden="true">
50
+ {#if library.length > 0}<span><LibraryIcon size={11} /></span>{/if}
51
+ {#if web.length > 0}<span><GlobeIcon size={11} /></span>{/if}
52
+ </span>
53
+ <span class="ds-lib-fold-count">
54
+ {total === 1 ? words.sourcesCountOne : fill(words.sourcesCount, { n: total })}
55
+ </span>
56
+ {#if library.length > 0 && web.length > 0}
57
+ <span class="ds-lib-fold-split">
58
+ {fill(words.sourcesLibrary, { n: library.length })} · {fill(words.sourcesWeb, { n: web.length })}
59
+ </span>
60
+ {/if}
61
+ <span class="ds-lib-fold-chevron" aria-hidden="true">
62
+ {#if open}<ChevronUpIcon size={15} />{:else}<ChevronDownIcon size={15} />{/if}
63
+ </span>
64
+ </button>
65
+
66
+ {#if open}
67
+ <div class="ds-lib-fold-body">
68
+ {#if library.length > 0}
69
+ <SourceList sources={library} {words} {oncite} heading={words.fromTheLibrary} />
70
+ {/if}
71
+ {#if web.length > 0}
72
+ <section>
73
+ <h2 class="ds-lib-fold-heading">{words.fromTheWeb}</h2>
74
+ <ol class="ds-lib-fold-pages">
75
+ {#each web as page (page.url)}
76
+ <li>
77
+ <a class="ds-lib-fold-page" href={page.url} target="_blank" rel="noopener noreferrer">
78
+ <span class="ds-lib-fold-n">{page.n}</span>
79
+ <span class="ds-lib-fold-page-text">
80
+ <span class="ds-lib-fold-page-title">{page.title}</span>
81
+ <span class="ds-lib-fold-page-meta">
82
+ {[page.host, when(page)].filter(Boolean).join(' · ')}
83
+ </span>
84
+ </span>
85
+ </a>
86
+ </li>
87
+ {/each}
88
+ </ol>
89
+ </section>
90
+ {/if}
91
+ </div>
92
+ {/if}
93
+ </section>
94
+
95
+ <style>
96
+ .ds-lib-fold-head {
97
+ display: flex;
98
+ min-height: 2.5rem;
99
+ align-items: center;
100
+ gap: 0.625rem;
101
+ border: 0;
102
+ background: none;
103
+ padding: 0;
104
+ color: var(--ds-color-muted-foreground);
105
+ font: inherit;
106
+ font-size: 0.875rem;
107
+ cursor: pointer;
108
+ }
109
+
110
+ .ds-lib-fold-head:hover {
111
+ color: var(--ds-color-foreground);
112
+ }
113
+
114
+ .ds-lib-fold-head:focus-visible {
115
+ outline: 2px solid var(--ds-color-ring);
116
+ outline-offset: 2px;
117
+ }
118
+
119
+ .ds-lib-fold-icons {
120
+ display: flex;
121
+ }
122
+
123
+ .ds-lib-fold-icons span {
124
+ display: flex;
125
+ width: 1.375rem;
126
+ height: 1.375rem;
127
+ align-items: center;
128
+ justify-content: center;
129
+ border: 1px solid var(--ds-color-border);
130
+ border-radius: var(--ds-radius-full);
131
+ background: var(--ds-color-surface-2);
132
+ }
133
+
134
+ .ds-lib-fold-icons span + span {
135
+ margin-inline-start: -0.375rem;
136
+ }
137
+
138
+ .ds-lib-fold-count {
139
+ color: var(--ds-color-foreground);
140
+ font-weight: 500;
141
+ }
142
+
143
+ .ds-lib-fold-split {
144
+ font-size: var(--ds-text-2xs);
145
+ }
146
+
147
+ .ds-lib-fold-chevron {
148
+ display: flex;
149
+ }
150
+
151
+ .ds-lib-fold-body {
152
+ display: flex;
153
+ flex-direction: column;
154
+ gap: 1rem;
155
+ margin-top: 0.5rem;
156
+ }
157
+
158
+ .ds-lib-fold-heading {
159
+ margin: 0 0 0.375rem;
160
+ color: var(--ds-color-muted-foreground);
161
+ font-family: inherit;
162
+ font-size: var(--ds-text-2xs);
163
+ font-weight: 500;
164
+ letter-spacing: 0.05em;
165
+ text-transform: uppercase;
166
+ }
167
+
168
+ .ds-lib-fold-pages {
169
+ display: flex;
170
+ flex-direction: column;
171
+ gap: 0.125rem;
172
+ margin: 0;
173
+ border-top: 1px solid var(--ds-color-border);
174
+ padding: 0.375rem 0 0;
175
+ list-style: none;
176
+ }
177
+
178
+ .ds-lib-fold-page {
179
+ display: flex;
180
+ align-items: baseline;
181
+ gap: 0.5rem;
182
+ border-radius: var(--ds-radius-md);
183
+ padding: 0.375rem 0.625rem;
184
+ color: inherit;
185
+ font-size: 0.875rem;
186
+ text-decoration: none;
187
+ }
188
+
189
+ .ds-lib-fold-page:hover {
190
+ background: var(--ds-color-surface-2);
191
+ }
192
+
193
+ .ds-lib-fold-page:focus-visible {
194
+ outline: 2px solid var(--ds-color-ring);
195
+ outline-offset: 2px;
196
+ }
197
+
198
+ .ds-lib-fold-n {
199
+ flex: none;
200
+ border-radius: var(--ds-radius-sm);
201
+ background: color-mix(in oklab, var(--ds-color-primary) 15%, transparent);
202
+ padding-inline: 0.375rem;
203
+ color: var(--ds-color-foreground);
204
+ font-family: var(--ds-font-code);
205
+ font-size: var(--ds-text-2xs);
206
+ font-variant-numeric: tabular-nums;
207
+ }
208
+
209
+ .ds-lib-fold-page-text {
210
+ display: flex;
211
+ min-width: 0;
212
+ flex-direction: column;
213
+ }
214
+
215
+ .ds-lib-fold-page-title {
216
+ color: var(--ds-color-foreground);
217
+ font-weight: 500;
218
+ overflow-wrap: anywhere;
219
+ }
220
+
221
+ .ds-lib-fold-page-meta {
222
+ color: var(--ds-color-muted-foreground);
223
+ font-size: var(--ds-text-2xs);
224
+ }
225
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { Citation } from '../../citations';
2
+ import { type ChatCopy } from '../../copy';
3
+ import type { WebSource } from '../../web-sources';
4
+ interface Props {
5
+ library: Citation[];
6
+ /** Pages, each with the number the answer's chip for it carries. */
7
+ web: Array<WebSource & {
8
+ n: number;
9
+ }>;
10
+ words: ChatCopy;
11
+ oncite?: (citation: Citation) => void;
12
+ /** When the pages were read, in words ("read today"), for a page the turn read. */
13
+ readWhen?: string;
14
+ }
15
+ declare const SourceFold: import("svelte").Component<Props, {}, "">;
16
+ type SourceFold = ReturnType<typeof SourceFold>;
17
+ export default SourceFold;