terminoir 0.7.0 → 0.9.0

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.
@@ -0,0 +1,362 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { defaultValueCtx, Editor, editorViewCtx, editorViewOptionsCtx, remarkStringifyOptionsCtx, rootCtx } from '@milkdown/kit/core';
4
+ import type { Ctx } from '@milkdown/kit/ctx';
5
+ import { block, BlockProvider } from '@milkdown/kit/plugin/block';
6
+ import { clipboard } from '@milkdown/kit/plugin/clipboard';
7
+ import { history } from '@milkdown/kit/plugin/history';
8
+ import { listener, listenerCtx } from '@milkdown/kit/plugin/listener';
9
+ import { commonmark } from '@milkdown/kit/preset/commonmark';
10
+ import { gfm } from '@milkdown/kit/preset/gfm';
11
+ import { TextSelection } from '@milkdown/kit/prose/state';
12
+ import { getMarkdown, replaceAll } from '@milkdown/kit/utils';
13
+ import { cn } from '../utils/cn.js';
14
+ import type { PickerOption } from '../utils/componentTypes.js';
15
+ import { moveIndex } from '../utils/listNav.js';
16
+ import { matchBlocks, placeholderPlugin, taskListItemView, tidyMarkdown, type BlockItem } from '../utils/markdownEditor/blocks.js';
17
+ import { triggerPlugin, type Trigger } from '../utils/markdownEditor/triggers.js';
18
+ import { wikilink, wikilinkNode } from '../utils/markdownEditor/wikilink.js';
19
+ import NavIcon from './NavIcon.svelte';
20
+
21
+ type MenuEntry = { id: string; label: string; icon?: string; iconColor?: string; description?: string; choose: () => void };
22
+
23
+ const LINK_SEARCH_DEBOUNCE_MS = 120;
24
+
25
+ let {
26
+ value,
27
+ onchange,
28
+ label,
29
+ placeholder = 'Write, or type / for blocks',
30
+ linkSearch,
31
+ onlinkopen,
32
+ class: className = ''
33
+ }: {
34
+ /** Markdown. Outside changes are applied while the editor is not focused. */
35
+ value: string;
36
+ /** Called with the edited markdown (only after a user edit, never on load). */
37
+ onchange?: (markdown: string) => void;
38
+ /** Accessible name of the writing surface. */
39
+ label: string;
40
+ placeholder?: string;
41
+ /** Enables `[[` links: options for the typed text. A chosen option is written as `[[id|label]]`. */
42
+ linkSearch?: (query: string) => Promise<PickerOption[]>;
43
+ /** A `[[link]]` was clicked; receives its target (the part before `|`). */
44
+ onlinkopen?: (target: string) => void;
45
+ class?: string;
46
+ } = $props();
47
+
48
+ const uid = $props.id();
49
+ let root = $state<HTMLDivElement>();
50
+ let host = $state<HTMLDivElement>();
51
+ let handle = $state<HTMLDivElement>();
52
+ let editor: Editor | undefined;
53
+ let provider: BlockProvider | undefined;
54
+ /** Normalized markdown of what the editor last loaded or emitted; changes equal to it are not edits. */
55
+ let known = '';
56
+
57
+ let trigger = $state<Trigger | null>(null);
58
+ let entries = $state<MenuEntry[]>([]);
59
+ let active = $state(0);
60
+ let searching = $state(false);
61
+ let searchSeq = 0;
62
+
63
+ function act<T>(fn: (ctx: Ctx) => T): T | undefined {
64
+ return editor?.action(fn);
65
+ }
66
+
67
+ function replaceTrigger(run: (ctx: Ctx) => void) {
68
+ const range = trigger;
69
+ if (!range) return;
70
+ act((ctx) => {
71
+ const view = ctx.get(editorViewCtx);
72
+ view.dispatch(view.state.tr.delete(range.from, range.to));
73
+ run(ctx);
74
+ view.focus();
75
+ });
76
+ }
77
+
78
+ function chooseBlock(item: BlockItem) {
79
+ replaceTrigger((ctx) => item.run(ctx));
80
+ }
81
+
82
+ function chooseLink(option: PickerOption) {
83
+ const range = trigger;
84
+ if (!range) return;
85
+ act((ctx) => {
86
+ const view = ctx.get(editorViewCtx);
87
+ const link = wikilinkNode.type(ctx).create({ target: option.id, label: option.label });
88
+ const tr = view.state.tr.replaceWith(range.from, range.to, link);
89
+ view.dispatch(tr.insertText(' ', range.from + link.nodeSize));
90
+ view.focus();
91
+ });
92
+ }
93
+
94
+ function onKey(key: string): boolean {
95
+ if (key === 'ArrowDown' || key === 'ArrowUp') {
96
+ active = moveIndex(active, key === 'ArrowDown' ? 1 : -1, entries.length);
97
+ return true;
98
+ }
99
+ const entry = entries[active];
100
+ if (!entry) return false;
101
+ entry.choose();
102
+ return true;
103
+ }
104
+
105
+ $effect(() => {
106
+ const current = trigger;
107
+ if (!current) {
108
+ entries = [];
109
+ return;
110
+ }
111
+ if (current.kind === 'slash') {
112
+ entries = matchBlocks(current.query).map((item) => ({ id: item.id, label: item.label, icon: item.icon, choose: () => chooseBlock(item) }));
113
+ active = 0;
114
+ return;
115
+ }
116
+ const query = current.query.trim();
117
+ if (!query || !linkSearch) {
118
+ entries = [];
119
+ return;
120
+ }
121
+ const seq = ++searchSeq;
122
+ const timer = setTimeout(async () => {
123
+ searching = true;
124
+ try {
125
+ const found = await linkSearch(query);
126
+ if (seq !== searchSeq) return;
127
+ entries = found.map((option) => ({ ...option, choose: () => chooseLink(option) }));
128
+ active = 0;
129
+ } catch {
130
+ if (seq === searchSeq) entries = [];
131
+ } finally {
132
+ if (seq === searchSeq) searching = false;
133
+ }
134
+ }, LINK_SEARCH_DEBOUNCE_MS);
135
+ return () => clearTimeout(timer);
136
+ });
137
+
138
+ function emit(markdown: string) {
139
+ const tidy = tidyMarkdown(markdown);
140
+ if (tidy === known) return;
141
+ known = tidy;
142
+ onchange?.(tidy);
143
+ }
144
+
145
+ /** Load outside changes, but never under the writer's caret. */
146
+ $effect(() => {
147
+ const next = value;
148
+ if (!editor || tidyMarkdown(next) === known) return;
149
+ const focused = act((ctx) => ctx.get(editorViewCtx).hasFocus());
150
+ if (focused) return;
151
+ act(replaceAll(next));
152
+ known = tidyMarkdown(act(getMarkdown()) ?? next);
153
+ });
154
+
155
+ /** The handle's `+`: open the `/` menu on a new line under the hovered block. */
156
+ function addBlockBelow() {
157
+ const target = provider?.active;
158
+ if (!target) return;
159
+ act((ctx) => {
160
+ const view = ctx.get(editorViewCtx);
161
+ const at = target.$pos.pos + target.node.nodeSize;
162
+ const paragraph = view.state.schema.nodes.paragraph.create(null, view.state.schema.text('/'));
163
+ const tr = view.state.tr.insert(at, paragraph);
164
+ view.dispatch(tr.setSelection(TextSelection.create(tr.doc, at + 2)).scrollIntoView());
165
+ view.focus();
166
+ });
167
+ }
168
+
169
+ function onHostClick(event: MouseEvent) {
170
+ const link = (event.target as HTMLElement).closest<HTMLElement>('[data-wikilink]');
171
+ if (link && onlinkopen) onlinkopen(link.dataset.wikilink ?? '');
172
+ }
173
+
174
+ onMount(() => {
175
+ let disposed = false;
176
+ const made = Editor.make()
177
+ .config((ctx) => {
178
+ ctx.set(rootCtx, host!);
179
+ ctx.set(defaultValueCtx, value);
180
+ ctx.update(remarkStringifyOptionsCtx, (options) => ({ ...options, bullet: '-' as const, rule: '-' as const }));
181
+ ctx.update(editorViewOptionsCtx, (options) => ({
182
+ ...options,
183
+ attributes: { class: 'terminoir-md__surface', 'aria-label': label, 'aria-multiline': 'true', role: 'textbox' }
184
+ }));
185
+ ctx.get(listenerCtx).markdownUpdated((_, markdown) => emit(markdown));
186
+ })
187
+ .use(commonmark)
188
+ .use(gfm)
189
+ .use(history)
190
+ .use(listener)
191
+ .use(clipboard)
192
+ .use(block)
193
+ .use(wikilink)
194
+ .use(taskListItemView)
195
+ .use(placeholderPlugin(placeholder))
196
+ .use(triggerPlugin({ onchange: (next) => (trigger = next), onkey: onKey }, Boolean(linkSearch)));
197
+
198
+ void made.create().then((created) => {
199
+ if (disposed) {
200
+ void created.destroy();
201
+ return;
202
+ }
203
+ editor = created;
204
+ known = tidyMarkdown(act(getMarkdown()) ?? value);
205
+ provider = new BlockProvider({ ctx: created.ctx, content: handle!, root, getOffset: () => 6 });
206
+ provider.update();
207
+ });
208
+
209
+ return () => {
210
+ disposed = true;
211
+ provider?.destroy();
212
+ void editor?.destroy();
213
+ };
214
+ });
215
+ </script>
216
+
217
+ <div class={cn('terminoir-md', className)} bind:this={root}>
218
+ <!-- Link chips are also reachable as text; the click only adds a shortcut to open them. -->
219
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
220
+ <div bind:this={host} onclick={onHostClick}></div>
221
+ <div class="terminoir-md__handle" bind:this={handle}>
222
+ <button type="button" class="terminoir-md__handle-button" aria-label="Add block below" onmousedown={(e) => e.preventDefault()} onclick={addBlockBelow}>
223
+ <NavIcon name="plus" />
224
+ </button>
225
+ <span class="terminoir-md__handle-grip" aria-hidden="true" title="Drag to move"><NavIcon name="grip-vertical" /></span>
226
+ </div>
227
+ </div>
228
+
229
+ {#if trigger && (entries.length || trigger.kind === 'link')}
230
+ <ul
231
+ id={`${uid}-menu`}
232
+ class="terminoir-md__menu glass-popover"
233
+ role="listbox"
234
+ aria-label={trigger.kind === 'slash' ? 'Blocks' : 'Link to'}
235
+ style:left={`${trigger.left}px`}
236
+ style:top={`${trigger.top + 4}px`}
237
+ >
238
+ {#each entries as entry, index (entry.id)}
239
+ <!-- Focus stays in the editor, which handles arrow keys and Enter. -->
240
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
241
+ <li
242
+ role="option"
243
+ aria-selected={index === active}
244
+ class="terminoir-md__option"
245
+ class:terminoir-md__option--active={index === active}
246
+ onpointerdown={(event) => event.preventDefault()}
247
+ onpointerenter={() => (active = index)}
248
+ onclick={() => entry.choose()}
249
+ >
250
+ {#if entry.icon}<NavIcon name={entry.icon} color={entry.iconColor} />{/if}
251
+ <span class="terminoir-md__option-label">{entry.label}</span>
252
+ {#if entry.description}<span class="terminoir-md__option-description">{entry.description}</span>{/if}
253
+ </li>
254
+ {:else}
255
+ <li class="terminoir-md__empty" role="presentation">
256
+ {searching ? 'Searching…' : trigger.query.trim() ? 'No matches' : 'Type to search'}
257
+ </li>
258
+ {/each}
259
+ </ul>
260
+ {/if}
261
+
262
+ <style>
263
+ .terminoir-md { position: relative; }
264
+ .terminoir-md :global(.terminoir-md__surface) {
265
+ outline: none;
266
+ min-height: 6rem;
267
+ padding: var(--space-1) 0;
268
+ color: var(--foreground);
269
+ font-size: 0.9375rem;
270
+ line-height: 1.65;
271
+ white-space: pre-wrap;
272
+ word-wrap: break-word;
273
+ }
274
+ .terminoir-md :global(.terminoir-md__surface > * + *) { margin-top: var(--space-2); }
275
+ .terminoir-md :global(p) { margin: 0; }
276
+ .terminoir-md :global(h1) { font-size: 1.5rem; font-weight: 600; line-height: 1.3; margin: var(--space-5) 0 var(--space-2); }
277
+ .terminoir-md :global(h2) { font-size: 1.2rem; font-weight: 600; line-height: 1.35; margin: var(--space-4) 0 var(--space-2); }
278
+ .terminoir-md :global(h3) { font-size: 1.05rem; font-weight: 600; margin: var(--space-3) 0 var(--space-1); }
279
+ .terminoir-md :global(.terminoir-md__surface > :first-child) { margin-top: 0; }
280
+ .terminoir-md :global(ul),
281
+ .terminoir-md :global(ol) { margin: 0; padding-left: 1.5rem; }
282
+ .terminoir-md :global(li > div > p),
283
+ .terminoir-md :global(li > p) { margin: 0; }
284
+ .terminoir-md :global(li[data-item-type='task']) { list-style: none; display: flex; align-items: flex-start; gap: var(--space-2); margin-left: -1.5rem; }
285
+ .terminoir-md :global(li[data-item-type='task'] > .terminoir-md-task-body) { flex: 1; min-width: 0; }
286
+ .terminoir-md :global(li[data-item-type='task'][data-checked='true'] > .terminoir-md-task-body) { color: var(--muted-foreground); text-decoration: line-through; }
287
+ .terminoir-md :global(.terminoir-md-task-box) { margin-top: 0.4em; accent-color: var(--primary); cursor: pointer; }
288
+ .terminoir-md :global(blockquote) { margin: 0; padding-left: var(--space-3); border-left: 2px solid var(--border); color: var(--muted-foreground); }
289
+ .terminoir-md :global(code) { font-family: var(--font-mono); font-size: 0.875em; padding: 0.1em 0.3em; border-radius: 0.3em; background: var(--muted); }
290
+ .terminoir-md :global(pre) { font-family: var(--font-mono); font-size: 0.8125rem; padding: var(--space-3); border-radius: var(--radius-control); background: var(--muted); overflow-x: auto; }
291
+ .terminoir-md :global(pre code) { padding: 0; background: none; }
292
+ .terminoir-md :global(hr) { border: none; border-top: 1px solid var(--border); margin: var(--space-4) 0; }
293
+ .terminoir-md :global(a) { color: var(--primary); text-underline-offset: 2px; }
294
+ .terminoir-md :global(table) { border-collapse: collapse; font-size: 0.875rem; }
295
+ .terminoir-md :global(th),
296
+ .terminoir-md :global(td) { border: 1px solid var(--border); padding: var(--space-1) var(--space-2); }
297
+ .terminoir-md :global(.terminoir-md-wikilink) {
298
+ display: inline-block;
299
+ padding: 0 0.4em;
300
+ border-radius: var(--radius-pill);
301
+ background: color-mix(in srgb, var(--primary) 16%, transparent);
302
+ color: var(--primary);
303
+ cursor: pointer;
304
+ }
305
+ .terminoir-md :global(.terminoir-md-wikilink.ProseMirror-selectednode) { outline: 1px solid var(--ring); }
306
+ .terminoir-md :global(.terminoir-md--empty::before) {
307
+ content: attr(data-placeholder);
308
+ position: absolute;
309
+ color: var(--muted-foreground);
310
+ pointer-events: none;
311
+ }
312
+
313
+ .terminoir-md__handle {
314
+ position: absolute;
315
+ display: flex;
316
+ align-items: center;
317
+ gap: 0.1rem;
318
+ color: var(--muted-foreground);
319
+ font-size: 0.75rem;
320
+ transition: opacity var(--duration-fast) ease;
321
+ }
322
+ .terminoir-md__handle:global([data-show='false']) { opacity: 0; pointer-events: none; }
323
+ .terminoir-md__handle-button,
324
+ .terminoir-md__handle-grip {
325
+ display: grid;
326
+ place-items: center;
327
+ width: 1.35rem;
328
+ height: 1.35rem;
329
+ border-radius: 0.3rem;
330
+ border: none;
331
+ background: none;
332
+ color: inherit;
333
+ cursor: pointer;
334
+ }
335
+ .terminoir-md__handle-grip { cursor: grab; }
336
+ .terminoir-md__handle-button:hover,
337
+ .terminoir-md__handle-grip:hover { background: color-mix(in srgb, var(--foreground) 8%, transparent); color: var(--foreground); }
338
+
339
+ .terminoir-md__menu {
340
+ position: fixed;
341
+ z-index: 60;
342
+ width: 16rem;
343
+ max-height: 18rem;
344
+ overflow-y: auto;
345
+ margin: 0;
346
+ padding: var(--space-1);
347
+ list-style: none;
348
+ }
349
+ .terminoir-md__option {
350
+ display: flex;
351
+ align-items: center;
352
+ gap: var(--space-2);
353
+ padding: 0.45rem 0.6rem;
354
+ border-radius: calc(var(--radius-control) - 0.2rem);
355
+ font-size: 0.875rem;
356
+ cursor: pointer;
357
+ }
358
+ .terminoir-md__option--active { background: color-mix(in srgb, var(--foreground) 8%, transparent); }
359
+ .terminoir-md__option-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
360
+ .terminoir-md__option-description { font-size: 0.75rem; color: var(--muted-foreground); flex-shrink: 0; }
361
+ .terminoir-md__empty { padding: 0.5rem 0.6rem; font-size: 0.8125rem; color: var(--muted-foreground); }
362
+ </style>
@@ -0,0 +1,18 @@
1
+ import type { PickerOption } from '../utils/componentTypes.js';
2
+ type $$ComponentProps = {
3
+ /** Markdown. Outside changes are applied while the editor is not focused. */
4
+ value: string;
5
+ /** Called with the edited markdown (only after a user edit, never on load). */
6
+ onchange?: (markdown: string) => void;
7
+ /** Accessible name of the writing surface. */
8
+ label: string;
9
+ placeholder?: string;
10
+ /** Enables `[[` links: options for the typed text. A chosen option is written as `[[id|label]]`. */
11
+ linkSearch?: (query: string) => Promise<PickerOption[]>;
12
+ /** A `[[link]]` was clicked; receives its target (the part before `|`). */
13
+ onlinkopen?: (target: string) => void;
14
+ class?: string;
15
+ };
16
+ declare const MarkdownEditor: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type MarkdownEditor = ReturnType<typeof MarkdownEditor>;
18
+ export default MarkdownEditor;
@@ -0,0 +1,130 @@
1
+ <script lang="ts">
2
+ import { cn } from '../utils/cn.js';
3
+ import { labelColor } from '../utils/palette.js';
4
+ import { timelineRows, timelineScale, todayOffset, type TimelineItem } from '../utils/timeline.js';
5
+
6
+ /** Gantt-style bars over a shared day scale; one row per item, grouped under headings. */
7
+ let {
8
+ items,
9
+ label,
10
+ onItemClick,
11
+ class: className = '',
12
+ }: {
13
+ items: TimelineItem[];
14
+ /** Accessible name for the chart. */
15
+ label: string;
16
+ onItemClick?: (item: TimelineItem) => void;
17
+ class?: string;
18
+ } = $props();
19
+
20
+ const scale = $derived(timelineScale(items));
21
+ const rows = $derived(timelineRows(items, scale));
22
+ const today = $derived(todayOffset(scale));
23
+
24
+ function day(d: Date): string {
25
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
26
+ }
27
+
28
+ function range(item: TimelineItem): string {
29
+ return item.end && item.end.getTime() !== item.start.getTime() ? `${day(item.start)} – ${day(item.end)}` : day(item.start);
30
+ }
31
+ </script>
32
+
33
+ <div class={cn('terminoir-timeline', className)} role="group" aria-label={label}>
34
+ <div class="terminoir-timeline__grid">
35
+ <div class="terminoir-timeline__corner" aria-hidden="true"></div>
36
+ <div class="terminoir-timeline__scale" aria-hidden="true">
37
+ {#each scale.ticks as tick (tick.date.getTime())}
38
+ <span class="terminoir-timeline__tick" style:left={`${tick.left}%`}>{day(tick.date)}</span>
39
+ {/each}
40
+ </div>
41
+ {#each rows as row (row.group ?? '')}
42
+ {#if row.group}<div class="terminoir-timeline__group">{row.group}</div>{/if}
43
+ {#each row.bars as bar (bar.item.id)}
44
+ <div class="terminoir-timeline__label" title={bar.item.title}>{bar.item.title}</div>
45
+ <div class="terminoir-timeline__track">
46
+ {#if today !== null}<div class="terminoir-timeline__today" style:left={`${today}%`} aria-hidden="true"></div>{/if}
47
+ <button
48
+ type="button"
49
+ class="terminoir-timeline__bar terminoir-focusable"
50
+ style:left={`${bar.left}%`}
51
+ style:width={`${bar.width}%`}
52
+ style:--bar-color={labelColor(bar.item.color) ?? 'var(--primary)'}
53
+ aria-label={`${bar.item.title}, ${range(bar.item)}`}
54
+ title={`${bar.item.title} · ${range(bar.item)}`}
55
+ disabled={!onItemClick}
56
+ onclick={() => onItemClick?.(bar.item)}
57
+ ></button>
58
+ </div>
59
+ {/each}
60
+ {/each}
61
+ </div>
62
+ </div>
63
+
64
+ <style>
65
+ .terminoir-timeline {
66
+ --timeline-row: 1.75rem;
67
+ --timeline-label: minmax(8rem, 14rem);
68
+ font-size: 0.8125rem;
69
+ border: 1px solid var(--border);
70
+ border-radius: var(--radius);
71
+ overflow-x: auto;
72
+ min-width: 0;
73
+ }
74
+ .terminoir-timeline__grid {
75
+ display: grid;
76
+ grid-template-columns: var(--timeline-label) minmax(24rem, 1fr);
77
+ min-width: 0;
78
+ padding-bottom: var(--space-1);
79
+ }
80
+ .terminoir-timeline__corner,
81
+ .terminoir-timeline__scale { height: 1.75rem; border-bottom: 1px solid var(--border); }
82
+ .terminoir-timeline__scale { position: relative; color: var(--muted-foreground); font-size: 0.6875rem; }
83
+ .terminoir-timeline__tick {
84
+ position: absolute;
85
+ top: 0.5rem;
86
+ padding-left: 0.25rem;
87
+ border-left: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
88
+ white-space: nowrap;
89
+ }
90
+ .terminoir-timeline__group {
91
+ grid-column: 1 / -1;
92
+ padding: var(--space-2) var(--space-2) var(--space-1);
93
+ color: var(--muted-foreground);
94
+ font-size: 0.6875rem;
95
+ text-transform: uppercase;
96
+ letter-spacing: 0.04em;
97
+ }
98
+ .terminoir-timeline__label {
99
+ display: flex;
100
+ align-items: center;
101
+ height: var(--timeline-row);
102
+ padding: 0 var(--space-2);
103
+ white-space: nowrap;
104
+ overflow: hidden;
105
+ text-overflow: ellipsis;
106
+ }
107
+ .terminoir-timeline__track { position: relative; height: var(--timeline-row); }
108
+ .terminoir-timeline__today {
109
+ position: absolute;
110
+ top: 0;
111
+ bottom: 0;
112
+ width: 1px;
113
+ background: var(--primary);
114
+ opacity: 0.5;
115
+ pointer-events: none;
116
+ }
117
+ .terminoir-timeline__bar {
118
+ position: absolute;
119
+ top: 0.375rem;
120
+ bottom: 0.375rem;
121
+ min-width: 0.5rem;
122
+ padding: 0;
123
+ border: 1px solid color-mix(in srgb, var(--bar-color) 70%, transparent);
124
+ border-radius: var(--radius-pill);
125
+ background: color-mix(in srgb, var(--bar-color) 45%, transparent);
126
+ cursor: pointer;
127
+ }
128
+ .terminoir-timeline__bar:disabled { cursor: default; }
129
+ .terminoir-timeline__bar:hover:not(:disabled) { background: color-mix(in srgb, var(--bar-color) 65%, transparent); }
130
+ </style>
@@ -0,0 +1,11 @@
1
+ import { type TimelineItem } from '../utils/timeline.js';
2
+ type $$ComponentProps = {
3
+ items: TimelineItem[];
4
+ /** Accessible name for the chart. */
5
+ label: string;
6
+ onItemClick?: (item: TimelineItem) => void;
7
+ class?: string;
8
+ };
9
+ declare const Timeline: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type Timeline = ReturnType<typeof Timeline>;
11
+ export default Timeline;
package/dist/index.d.ts CHANGED
@@ -47,6 +47,14 @@ export { default as Inline } from './components/Inline.svelte';
47
47
  export { default as Text } from './components/Text.svelte';
48
48
  export { default as Disclosure } from './components/Disclosure.svelte';
49
49
  export { default as GroupedList } from './components/GroupedList.svelte';
50
+ export { default as Timeline } from './components/Timeline.svelte';
51
+ export type { TimelineItem } from './utils/timeline.js';
52
+ export { default as BucketGrid } from './components/BucketGrid.svelte';
53
+ export type { Bucket } from './components/BucketGrid.svelte';
54
+ export { default as GridLayout } from './components/GridLayout.svelte';
55
+ export type { GridItem } from './utils/layout.js';
56
+ export { default as ExpressionEditor } from './components/ExpressionEditor.svelte';
57
+ export { describe as describeExpression, isComplete as isCompleteExpression, type ExpressionField, type ExpressionKind, type ExpressionNode, type ExpressionOp, type ExpressionSlot, } from './utils/expression.js';
50
58
  export type { GroupedListGroup, GroupedListItem } from './components/GroupedList.svelte';
51
59
  export { default as SegmentBar } from './components/SegmentBar.svelte';
52
60
  export type { Segment } from './components/SegmentBar.svelte';
@@ -71,6 +79,7 @@ export type { ChipSelectGroup, ChipSelectOption } from './utils/componentTypes.j
71
79
  export { default as DateField } from './components/DateField.svelte';
72
80
  export { dayLabel, dayUrgency, parseYmd, type DayUrgency } from './utils/dayLabel.js';
73
81
  export { default as SearchPicker } from './components/SearchPicker.svelte';
82
+ export { default as MarkdownEditor } from './components/MarkdownEditor.svelte';
74
83
  export { default as FileButton } from './components/FileButton.svelte';
75
84
  export { default as TitleInput } from './components/TitleInput.svelte';
76
85
  export type { PickerOption } from './utils/componentTypes.js';
package/dist/index.js CHANGED
@@ -46,6 +46,11 @@ export { default as Inline } from './components/Inline.svelte';
46
46
  export { default as Text } from './components/Text.svelte';
47
47
  export { default as Disclosure } from './components/Disclosure.svelte';
48
48
  export { default as GroupedList } from './components/GroupedList.svelte';
49
+ export { default as Timeline } from './components/Timeline.svelte';
50
+ export { default as BucketGrid } from './components/BucketGrid.svelte';
51
+ export { default as GridLayout } from './components/GridLayout.svelte';
52
+ export { default as ExpressionEditor } from './components/ExpressionEditor.svelte';
53
+ export { describe as describeExpression, isComplete as isCompleteExpression, } from './utils/expression.js';
49
54
  export { default as SegmentBar } from './components/SegmentBar.svelte';
50
55
  export { default as NavSection } from './components/NavSection.svelte';
51
56
  export { default as Spacer } from './components/Spacer.svelte';
@@ -66,6 +71,7 @@ export { default as ChipSelect } from './components/ChipSelect.svelte';
66
71
  export { default as DateField } from './components/DateField.svelte';
67
72
  export { dayLabel, dayUrgency, parseYmd } from './utils/dayLabel.js';
68
73
  export { default as SearchPicker } from './components/SearchPicker.svelte';
74
+ export { default as MarkdownEditor } from './components/MarkdownEditor.svelte';
69
75
  export { default as FileButton } from './components/FileButton.svelte';
70
76
  export { default as TitleInput } from './components/TitleInput.svelte';
71
77
  export { default as RadioCards } from './components/RadioCards.svelte';
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Typed expression trees for a block editor. The op catalogue (e.g. Oyster's
3
+ * `GET /expressions/ops`) says what each op returns and what its slots accept; the editor only
4
+ * offers ops whose result fits the slot, so users cannot build an ill-typed tree.
5
+ */
6
+ export type ExpressionKind = 'boolean' | 'number' | 'date' | 'text' | 'value' | 'ids';
7
+ export type ExpressionSlot = {
8
+ name: string;
9
+ kind: ExpressionKind;
10
+ many?: boolean;
11
+ };
12
+ export type ExpressionOp = {
13
+ op: string;
14
+ label: string;
15
+ group: string;
16
+ returns: ExpressionKind;
17
+ slots: ExpressionSlot[];
18
+ /** Plain fields on the node. A param named `key` picks a field; others are typed in. */
19
+ params: string[];
20
+ };
21
+ export type ExpressionField = {
22
+ key: string;
23
+ label?: string;
24
+ kind?: ExpressionKind;
25
+ };
26
+ export type ExpressionNode = {
27
+ op: string;
28
+ [key: string]: unknown;
29
+ };
30
+ /** A slot of `kind` accepts an op returning `returns` (`value` is the wildcard both ways, `ids` only fits `ids`). */
31
+ export declare function fits(kind: ExpressionKind, returns: ExpressionKind): boolean;
32
+ export declare function opsFor(kind: ExpressionKind, ops: ExpressionOp[]): ExpressionOp[];
33
+ /** A fresh node for an op: empty single slots, one empty entry in list slots, blank params. */
34
+ export declare function emptyNode(op: ExpressionOp): ExpressionNode;
35
+ /** Text typed into a param, as the value it stands for (numbers and true/false become typed). */
36
+ export declare function parseParam(param: string, text: string): unknown;
37
+ export declare function paramText(value: unknown): string;
38
+ /** True when every slot is filled and every key param names a field. */
39
+ export declare function isComplete(node: unknown, ops: ExpressionOp[]): boolean;
40
+ /** Plain-English reading of a tree, built from op labels and slot names (no per-op templates). */
41
+ export declare function describe(node: unknown, ops: ExpressionOp[], fields?: ExpressionField[]): string;
42
+ export type ExpressionPath = (string | number)[];
43
+ /** A copy of `root` with the value at `path` replaced (`undefined` removes a list entry). */
44
+ export declare function setAt(root: unknown, path: ExpressionPath, value: unknown): unknown;
45
+ /** Ops grouped by their `group`, in first-seen order, for an `<optgroup>` picker. */
46
+ export declare function groupOps(ops: ExpressionOp[]): {
47
+ group: string;
48
+ ops: ExpressionOp[];
49
+ }[];