sh3-core 0.19.3 → 0.19.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 (35) hide show
  1. package/dist/api.d.ts +4 -0
  2. package/dist/api.js +3 -0
  3. package/dist/chrome/CompactChrome.svelte +34 -1
  4. package/dist/chrome/CompactChrome.svelte.test.js +4 -2
  5. package/dist/chrome/FloatsSheet.svelte +236 -0
  6. package/dist/chrome/FloatsSheet.svelte.d.ts +7 -0
  7. package/dist/chrome/FloatsSheet.svelte.test.d.ts +1 -0
  8. package/dist/chrome/FloatsSheet.svelte.test.js +155 -0
  9. package/dist/layout/compact/CompactRenderer.svelte +8 -2
  10. package/dist/layout/compact/rootStore.svelte.d.ts +20 -0
  11. package/dist/layout/compact/rootStore.svelte.js +59 -0
  12. package/dist/layout/compact/rootStore.svelte.test.d.ts +1 -0
  13. package/dist/layout/compact/rootStore.svelte.test.js +54 -0
  14. package/dist/layout/floats.d.ts +27 -0
  15. package/dist/layout/floats.js +20 -0
  16. package/dist/layout/floats.test.js +34 -1
  17. package/dist/layout/inspection.js +25 -2
  18. package/dist/layout/inspection.svelte.test.js +49 -0
  19. package/dist/overlays/FloatLayer.svelte +12 -1
  20. package/dist/overlays/float.d.ts +7 -0
  21. package/dist/overlays/float.js +76 -6
  22. package/dist/overlays/float.test.js +170 -0
  23. package/dist/primitives/widgets/DocumentFilePicker.d.ts +25 -0
  24. package/dist/primitives/widgets/DocumentFilePicker.js +74 -0
  25. package/dist/primitives/widgets/DocumentFilePicker.svelte +144 -0
  26. package/dist/primitives/widgets/DocumentFilePicker.svelte.d.ts +18 -0
  27. package/dist/primitives/widgets/DocumentOpener.svelte +36 -0
  28. package/dist/primitives/widgets/DocumentOpener.svelte.d.ts +17 -0
  29. package/dist/primitives/widgets/DocumentSaver.svelte +36 -0
  30. package/dist/primitives/widgets/DocumentSaver.svelte.d.ts +17 -0
  31. package/dist/primitives/widgets/_DocumentBrowser.svelte +337 -0
  32. package/dist/primitives/widgets/_DocumentBrowser.svelte.d.ts +11 -0
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/package.json +1 -1
@@ -0,0 +1,74 @@
1
+ export function buildTree(docs, shardId, prefix) {
2
+ if (shardId === null) {
3
+ const shards = [...new Set(docs.map((d) => d.shardId))].sort();
4
+ return shards.map((s) => ({ kind: 'folder', name: s, fullPath: s }));
5
+ }
6
+ const shardDocs = docs.filter((d) => d.shardId === shardId);
7
+ const folders = new Map();
8
+ const files = [];
9
+ const normPrefix = prefix ? prefix + '/' : '';
10
+ const plen = normPrefix.length;
11
+ for (const doc of shardDocs) {
12
+ if (!doc.path.startsWith(normPrefix))
13
+ continue;
14
+ const relative = doc.path.slice(plen);
15
+ const slash = relative.indexOf('/');
16
+ if (slash >= 0) {
17
+ const name = relative.slice(0, slash);
18
+ const full = prefix ? `${prefix}/${name}` : name;
19
+ if (!folders.has(name))
20
+ folders.set(name, full);
21
+ }
22
+ else {
23
+ files.push({ kind: 'file', name: relative, doc });
24
+ }
25
+ }
26
+ const folderItems = [...folders.entries()]
27
+ .map(([name, fullPath]) => ({ kind: 'folder', name, fullPath }))
28
+ .sort((a, b) => a.name.localeCompare(b.name));
29
+ const fileItems = files.sort((a, b) => a.name.localeCompare(b.name));
30
+ return [...folderItems, ...fileItems];
31
+ }
32
+ export function formatSize(bytes) {
33
+ if (bytes < 1024)
34
+ return `${bytes} B`;
35
+ if (bytes < 1024 * 1024)
36
+ return `${(bytes / 1024).toFixed(1)} KB`;
37
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
38
+ }
39
+ export function formatDate(epochMs) {
40
+ const d = new Date(epochMs);
41
+ return d.toLocaleDateString('en-US', {
42
+ month: 'short',
43
+ day: 'numeric',
44
+ year: d.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined,
45
+ });
46
+ }
47
+ const EXT_ICONS = {
48
+ guml: '⬡', glsl: '◇', md: '≡', obj: '△', png: '▦',
49
+ };
50
+ export function iconForFile(name) {
51
+ var _a;
52
+ const ext = name.slice(name.lastIndexOf('.') + 1).toLowerCase();
53
+ return (_a = EXT_ICONS[ext]) !== null && _a !== void 0 ? _a : '▯';
54
+ }
55
+ export function breadcrumbSegments(shardId, prefix) {
56
+ const segs = [
57
+ { label: 'SH3', level: 0, targetShard: null, targetPrefix: '' },
58
+ ];
59
+ if (shardId) {
60
+ segs.push({ label: shardId, level: 1, targetShard: shardId, targetPrefix: '' });
61
+ if (prefix) {
62
+ const parts = prefix.split('/');
63
+ for (let i = 0; i < parts.length; i++) {
64
+ segs.push({
65
+ label: parts[i],
66
+ level: 2 + i,
67
+ targetShard: shardId,
68
+ targetPrefix: parts.slice(0, i + 1).join('/'),
69
+ });
70
+ }
71
+ }
72
+ }
73
+ return segs;
74
+ }
@@ -0,0 +1,144 @@
1
+ <script lang="ts">
2
+ import type { CommitOnlyEvents } from './_contract';
3
+ import { sh3 } from '../../sh3Runtime.svelte';
4
+ import DocumentBrowser from './_DocumentBrowser.svelte';
5
+ import type { DocumentMeta } from '../../documents/types';
6
+ import type { DocEntry, OpenerValue, SaverValue } from './DocumentFilePicker';
7
+
8
+ type DocListFn = () => Promise<Array<DocumentMeta & { shardId: string }>>;
9
+
10
+ let {
11
+ mode,
12
+ value = $bindable<OpenerValue | SaverValue>(null),
13
+ listDocuments,
14
+ disabled = false,
15
+ invalid = false,
16
+ size = 'md',
17
+ buttonLabel = 'Choose…',
18
+ onchange,
19
+ }: {
20
+ mode: 'open' | 'save';
21
+ value?: OpenerValue | SaverValue;
22
+ listDocuments: DocListFn;
23
+ disabled?: boolean;
24
+ invalid?: boolean;
25
+ size?: 'sm' | 'md';
26
+ buttonLabel?: string;
27
+ } & CommitOnlyEvents<OpenerValue | SaverValue> = $props();
28
+
29
+ let trigger = $state<HTMLButtonElement | undefined>(undefined);
30
+ let openFlag = $state(false);
31
+
32
+ const displayPath = $derived(
33
+ value
34
+ ? typeof value === 'string'
35
+ ? value
36
+ : `${value.shardId}/${value.path}`
37
+ : null,
38
+ );
39
+
40
+ function handleCommit(result: OpenerValue | SaverValue) {
41
+ value = result;
42
+ onchange?.(result);
43
+ }
44
+
45
+ function onOpenClosed() {
46
+ openFlag = false;
47
+ trigger?.focus();
48
+ }
49
+
50
+ async function open() {
51
+ if (disabled || openFlag || !trigger) return;
52
+ openFlag = true;
53
+ let docs: DocEntry[] = [];
54
+ try {
55
+ docs = await listDocuments();
56
+ } catch {
57
+ openFlag = false;
58
+ return;
59
+ }
60
+
61
+ const popupHandle = sh3.popup.show(
62
+ DocumentBrowser,
63
+ { anchor: trigger },
64
+ {
65
+ mode,
66
+ docs,
67
+ onCommit: (result: OpenerValue | SaverValue) => {
68
+ handleCommit(result);
69
+ },
70
+ onCancel: () => {},
71
+ },
72
+ );
73
+
74
+ const origClose = popupHandle.close;
75
+ popupHandle.close = () => {
76
+ origClose();
77
+ onOpenClosed();
78
+ };
79
+ }
80
+ </script>
81
+
82
+ <label class="sh3-dfp" class:sh3-dfp--sm={size === 'sm'} class:sh3-dfp--invalid={invalid}>
83
+ <button
84
+ type="button"
85
+ class="sh3-dfp__btn"
86
+ bind:this={trigger}
87
+ {disabled}
88
+ aria-haspopup="dialog"
89
+ aria-expanded={openFlag}
90
+ onclick={open}
91
+ >
92
+ <span class="sh3-dfp__label">{buttonLabel}</span>
93
+ <span class="sh3-dfp__path" class:sh3-dfp__path--empty={!displayPath}>
94
+ {displayPath ?? (mode === 'open' ? 'no document' : 'choose path…')}
95
+ </span>
96
+ <span class="sh3-dfp__chevron" aria-hidden="true">▾</span>
97
+ </button>
98
+ </label>
99
+
100
+ <style>
101
+ .sh3-dfp { display: inline-flex; font-size: 0.8125rem; }
102
+ .sh3-dfp__btn {
103
+ display: inline-flex; align-items: stretch;
104
+ height: var(--sh3-field-height-md);
105
+ min-width: 240px;
106
+ border: 1px solid var(--sh3-border);
107
+ border-radius: var(--sh3-widget-radius);
108
+ background: var(--sh3-input-bg);
109
+ cursor: pointer;
110
+ font: inherit;
111
+ text-align: left;
112
+ overflow: hidden;
113
+ padding: 0;
114
+ }
115
+ .sh3-dfp--sm .sh3-dfp__btn { height: var(--sh3-field-height-sm); }
116
+ .sh3-dfp--invalid .sh3-dfp__btn { border-color: var(--sh3-error); }
117
+ .sh3-dfp__btn:hover { border-color: var(--sh3-input-border-focus); }
118
+ .sh3-dfp__btn:focus-visible { outline: none; box-shadow: var(--sh3-focus-ring); border-color: var(--sh3-input-border-focus); }
119
+ .sh3-dfp__btn:disabled { opacity: 0.55; cursor: not-allowed; }
120
+ .sh3-dfp__label {
121
+ display: inline-flex; align-items: center;
122
+ padding: 0 var(--sh3-field-pad-x);
123
+ background: var(--sh3-bg-elevated);
124
+ color: var(--sh3-fg);
125
+ border-right: 1px solid var(--sh3-border);
126
+ white-space: nowrap;
127
+ flex-shrink: 0;
128
+ }
129
+ .sh3-dfp__path {
130
+ display: inline-flex; align-items: center;
131
+ padding: 0 var(--sh3-field-pad-x);
132
+ color: var(--sh3-fg);
133
+ flex: 1; min-width: 0;
134
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
135
+ font-family: var(--sh3-font-mono); font-size: 0.75rem;
136
+ }
137
+ .sh3-dfp__path--empty { color: var(--sh3-fg-muted); font-family: inherit; font-size: 0.8125rem; font-style: italic; }
138
+ .sh3-dfp__chevron {
139
+ display: inline-flex; align-items: center;
140
+ padding: 0 8px;
141
+ color: var(--sh3-fg-muted);
142
+ flex-shrink: 0;
143
+ }
144
+ </style>
@@ -0,0 +1,18 @@
1
+ import type { CommitOnlyEvents } from './_contract';
2
+ import type { DocumentMeta } from '../../documents/types';
3
+ import type { OpenerValue, SaverValue } from './DocumentFilePicker';
4
+ type DocListFn = () => Promise<Array<DocumentMeta & {
5
+ shardId: string;
6
+ }>>;
7
+ type $$ComponentProps = {
8
+ mode: 'open' | 'save';
9
+ value?: OpenerValue | SaverValue;
10
+ listDocuments: DocListFn;
11
+ disabled?: boolean;
12
+ invalid?: boolean;
13
+ size?: 'sm' | 'md';
14
+ buttonLabel?: string;
15
+ } & CommitOnlyEvents<OpenerValue | SaverValue>;
16
+ declare const DocumentFilePicker: import("svelte").Component<$$ComponentProps, {}, "value">;
17
+ type DocumentFilePicker = ReturnType<typeof DocumentFilePicker>;
18
+ export default DocumentFilePicker;
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import type { CommitOnlyEvents } from './_contract';
3
+ import DocumentFilePicker from './DocumentFilePicker.svelte';
4
+ import type { DocumentMeta } from '../../documents/types';
5
+ import type { OpenerValue } from './DocumentFilePicker';
6
+
7
+ type DocListFn = () => Promise<Array<DocumentMeta & { shardId: string }>>;
8
+
9
+ let {
10
+ value = $bindable<OpenerValue>(null),
11
+ listDocuments,
12
+ disabled = false,
13
+ invalid = false,
14
+ size = 'md',
15
+ buttonLabel = 'Open document…',
16
+ onchange,
17
+ }: {
18
+ value?: OpenerValue;
19
+ listDocuments: DocListFn;
20
+ disabled?: boolean;
21
+ invalid?: boolean;
22
+ size?: 'sm' | 'md';
23
+ buttonLabel?: string;
24
+ } & CommitOnlyEvents<OpenerValue> = $props();
25
+ </script>
26
+
27
+ <DocumentFilePicker
28
+ mode="open"
29
+ bind:value
30
+ {listDocuments}
31
+ {disabled}
32
+ {invalid}
33
+ {size}
34
+ {buttonLabel}
35
+ {onchange}
36
+ />
@@ -0,0 +1,17 @@
1
+ import type { CommitOnlyEvents } from './_contract';
2
+ import type { DocumentMeta } from '../../documents/types';
3
+ import type { OpenerValue } from './DocumentFilePicker';
4
+ type DocListFn = () => Promise<Array<DocumentMeta & {
5
+ shardId: string;
6
+ }>>;
7
+ type $$ComponentProps = {
8
+ value?: OpenerValue;
9
+ listDocuments: DocListFn;
10
+ disabled?: boolean;
11
+ invalid?: boolean;
12
+ size?: 'sm' | 'md';
13
+ buttonLabel?: string;
14
+ } & CommitOnlyEvents<OpenerValue>;
15
+ declare const DocumentOpener: import("svelte").Component<$$ComponentProps, {}, "value">;
16
+ type DocumentOpener = ReturnType<typeof DocumentOpener>;
17
+ export default DocumentOpener;
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import type { CommitOnlyEvents } from './_contract';
3
+ import DocumentFilePicker from './DocumentFilePicker.svelte';
4
+ import type { DocumentMeta } from '../../documents/types';
5
+ import type { SaverValue } from './DocumentFilePicker';
6
+
7
+ type DocListFn = () => Promise<Array<DocumentMeta & { shardId: string }>>;
8
+
9
+ let {
10
+ value = $bindable<SaverValue>(null),
11
+ listDocuments,
12
+ disabled = false,
13
+ invalid = false,
14
+ size = 'md',
15
+ buttonLabel = 'Save as…',
16
+ onchange,
17
+ }: {
18
+ value?: SaverValue;
19
+ listDocuments: DocListFn;
20
+ disabled?: boolean;
21
+ invalid?: boolean;
22
+ size?: 'sm' | 'md';
23
+ buttonLabel?: string;
24
+ } & CommitOnlyEvents<SaverValue> = $props();
25
+ </script>
26
+
27
+ <DocumentFilePicker
28
+ mode="save"
29
+ bind:value
30
+ {listDocuments}
31
+ {disabled}
32
+ {invalid}
33
+ {size}
34
+ {buttonLabel}
35
+ {onchange}
36
+ />
@@ -0,0 +1,17 @@
1
+ import type { CommitOnlyEvents } from './_contract';
2
+ import type { DocumentMeta } from '../../documents/types';
3
+ import type { SaverValue } from './DocumentFilePicker';
4
+ type DocListFn = () => Promise<Array<DocumentMeta & {
5
+ shardId: string;
6
+ }>>;
7
+ type $$ComponentProps = {
8
+ value?: SaverValue;
9
+ listDocuments: DocListFn;
10
+ disabled?: boolean;
11
+ invalid?: boolean;
12
+ size?: 'sm' | 'md';
13
+ buttonLabel?: string;
14
+ } & CommitOnlyEvents<SaverValue>;
15
+ declare const DocumentSaver: import("svelte").Component<$$ComponentProps, {}, "value">;
16
+ type DocumentSaver = ReturnType<typeof DocumentSaver>;
17
+ export default DocumentSaver;
@@ -0,0 +1,337 @@
1
+ <script lang="ts">
2
+ import {
3
+ buildTree,
4
+ formatSize,
5
+ formatDate,
6
+ iconForFile,
7
+ breadcrumbSegments,
8
+ type DocEntry,
9
+ type OpenerValue,
10
+ type SaverValue,
11
+ type FileItem,
12
+ } from './DocumentFilePicker';
13
+
14
+ let {
15
+ mode,
16
+ docs,
17
+ onCommit,
18
+ onCancel,
19
+ close,
20
+ }: {
21
+ mode: 'open' | 'save';
22
+ docs: DocEntry[];
23
+ onCommit: (value: OpenerValue | SaverValue) => void;
24
+ onCancel: () => void;
25
+ close: () => void;
26
+ } = $props();
27
+
28
+ let shardId = $state<string | null>(null);
29
+ let prefix = $state('');
30
+ let selectedFile = $state<DocEntry | null>(null);
31
+ let filename = $state('');
32
+ let activeIdx = $state(0);
33
+ let listEl = $state<HTMLElement | undefined>(undefined);
34
+
35
+ const items = $derived(buildTree(docs, shardId, prefix));
36
+ const crumbs = $derived(breadcrumbSegments(shardId, prefix));
37
+
38
+ $effect(() => {
39
+ items;
40
+ activeIdx = Math.min(activeIdx, items.length - 1);
41
+ });
42
+
43
+ function navigateShard(id: string) {
44
+ shardId = id;
45
+ prefix = '';
46
+ selectedFile = null;
47
+ filename = '';
48
+ activeIdx = 0;
49
+ }
50
+
51
+ function navigatePrefix(p: string) {
52
+ if (shardId === null) {
53
+ navigateShard(p);
54
+ } else {
55
+ prefix = p;
56
+ selectedFile = null;
57
+ filename = '';
58
+ activeIdx = 0;
59
+ }
60
+ }
61
+
62
+ function selectFile(item: FileItem) {
63
+ if (item.kind === 'folder') {
64
+ navigatePrefix(item.fullPath);
65
+ } else {
66
+ if (mode === 'open') {
67
+ selectedFile = item.doc;
68
+ }
69
+ if (mode === 'save') {
70
+ filename = item.name;
71
+ }
72
+ }
73
+ }
74
+
75
+ function commit() {
76
+ if (mode === 'open' && selectedFile) {
77
+ onCommit({ shardId: selectedFile.shardId, path: selectedFile.path });
78
+ close();
79
+ } else if (mode === 'save' && filename.trim() && shardId) {
80
+ const p = prefix ? `${shardId}/${prefix}/${filename}` : `${shardId}/${filename}`;
81
+ onCommit(p);
82
+ close();
83
+ }
84
+ }
85
+
86
+ function cancel() {
87
+ onCancel();
88
+ close();
89
+ }
90
+
91
+ function canCommit(): boolean {
92
+ if (mode === 'open') return selectedFile !== null;
93
+ return filename.trim().length > 0 && shardId !== null;
94
+ }
95
+
96
+ function onKey(e: KeyboardEvent) {
97
+ if (e.target instanceof HTMLInputElement) return;
98
+ switch (e.key) {
99
+ case 'ArrowDown':
100
+ e.preventDefault();
101
+ activeIdx = Math.min(activeIdx + 1, items.length - 1);
102
+ break;
103
+ case 'ArrowUp':
104
+ e.preventDefault();
105
+ activeIdx = Math.max(activeIdx - 1, 0);
106
+ break;
107
+ case 'Enter':
108
+ e.preventDefault();
109
+ if (activeIdx >= 0 && activeIdx < items.length) {
110
+ selectFile(items[activeIdx]);
111
+ }
112
+ break;
113
+ case 'Escape':
114
+ e.preventDefault();
115
+ cancel();
116
+ break;
117
+ case 'Backspace':
118
+ if (prefix) {
119
+ e.preventDefault();
120
+ const parts = prefix.split('/');
121
+ parts.pop();
122
+ prefix = parts.join('/');
123
+ activeIdx = 0;
124
+ } else if (shardId) {
125
+ e.preventDefault();
126
+ shardId = null;
127
+ activeIdx = 0;
128
+ }
129
+ break;
130
+ }
131
+ }
132
+
133
+ function onDblClick(item: FileItem) {
134
+ if (item.kind === 'folder') {
135
+ navigatePrefix(item.fullPath);
136
+ } else if (mode === 'open') {
137
+ onCommit({ shardId: item.doc.shardId, path: item.doc.path });
138
+ close();
139
+ }
140
+ }
141
+ </script>
142
+
143
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
144
+ <div class="sh3-doc-browser" onkeydown={onKey} tabindex="-1">
145
+ <div class="sh3-doc-browser__head">
146
+ <span class="sh3-doc-browser__title">
147
+ {mode === 'open' ? 'Open Document' : 'Save Document'}
148
+ </span>
149
+ </div>
150
+
151
+ <nav class="sh3-doc-browser__crumbs">
152
+ {#each crumbs as seg, i}
153
+ {#if i > 0}<span class="sh3-doc-browser__crumb-sep">/</span>{/if}
154
+ <button
155
+ type="button"
156
+ class="sh3-doc-browser__crumb"
157
+ class:sh3-doc-browser__crumb--last={i === crumbs.length - 1}
158
+ onclick={() => {
159
+ shardId = seg.targetShard;
160
+ prefix = seg.targetPrefix;
161
+ selectedFile = null;
162
+ filename = '';
163
+ activeIdx = 0;
164
+ }}
165
+ >{seg.label}</button>
166
+ {/each}
167
+ </nav>
168
+
169
+ <div class="sh3-doc-browser__list" bind:this={listEl}>
170
+ {#if items.length === 0}
171
+ <div class="sh3-doc-browser__empty">
172
+ {shardId === null ? 'No shards available.' : prefix ? 'Empty directory.' : 'No documents in this shard.'}
173
+ </div>
174
+ {:else}
175
+ {#each items as item, i}
176
+ {@const isActive = i === activeIdx}
177
+ {@const isSelected = item.kind === 'file' && mode === 'open' && selectedFile?.path === item.doc.path && selectedFile?.shardId === item.doc.shardId}
178
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
179
+ <div
180
+ class="sh3-doc-browser__item"
181
+ class:sh3-doc-browser__item--active={isActive}
182
+ class:sh3-doc-browser__item--selected={isSelected}
183
+ class:sh3-doc-browser__item--folder={item.kind === 'folder'}
184
+ role="option"
185
+ tabindex="-1"
186
+ aria-selected={isSelected}
187
+ onclick={() => selectFile(item)}
188
+ onkeydown={(e) => { if (e.key === 'Enter') selectFile(item); }}
189
+ ondblclick={() => onDblClick(item)}
190
+ onmouseenter={() => activeIdx = i}
191
+ >
192
+ {#if item.kind === 'folder'}
193
+ <span class="sh3-doc-browser__icon sh3-doc-browser__icon--folder" aria-hidden="true">📁</span>
194
+ <span class="sh3-doc-browser__name">{item.name}</span>
195
+ <span class="sh3-doc-browser__meta"></span>
196
+ {:else}
197
+ <span class="sh3-doc-browser__icon" aria-hidden="true">{iconForFile(item.name)}</span>
198
+ <span class="sh3-doc-browser__name">{item.name}</span>
199
+ <span class="sh3-doc-browser__meta">
200
+ {formatSize(item.doc.size)}
201
+ <span class="sh3-doc-browser__date">{formatDate(item.doc.lastModified)}</span>
202
+ </span>
203
+ {/if}
204
+ </div>
205
+ {/each}
206
+ {/if}
207
+ </div>
208
+
209
+ {#if mode === 'save' && shardId !== null}
210
+ <div class="sh3-doc-browser__save-row">
211
+ <span class="sh3-doc-browser__save-label">Filename:</span>
212
+ <input
213
+ class="sh3-doc-browser__save-input"
214
+ type="text"
215
+ placeholder="filename.ext"
216
+ bind:value={filename}
217
+ onkeydown={(e) => { if (e.key === 'Enter' && canCommit()) commit(); }}
218
+ />
219
+ </div>
220
+ {/if}
221
+
222
+ <div class="sh3-doc-browser__footer">
223
+ <button type="button" class="sh3-doc-browser__btn sh3-doc-browser__btn--cancel" onclick={cancel}>Cancel</button>
224
+ <button
225
+ type="button"
226
+ class="sh3-doc-browser__btn sh3-doc-browser__btn--primary"
227
+ disabled={!canCommit()}
228
+ onclick={commit}
229
+ >
230
+ {mode === 'open' ? 'Open' : 'Save'}
231
+ </button>
232
+ </div>
233
+ </div>
234
+
235
+ <style>
236
+ .sh3-doc-browser {
237
+ background: var(--sh3-bg-elevated);
238
+ border: 1px solid var(--sh3-border-strong);
239
+ border-radius: var(--sh3-widget-radius);
240
+ box-shadow: var(--sh3-shadow-lg);
241
+ width: 420px;
242
+ max-height: 480px;
243
+ display: flex; flex-direction: column;
244
+ overflow: hidden;
245
+ color: var(--sh3-fg);
246
+ font-size: 0.8125rem;
247
+ outline: none;
248
+ }
249
+ .sh3-doc-browser__head {
250
+ display: flex; align-items: center; justify-content: space-between;
251
+ padding: 8px 12px;
252
+ border-bottom: 1px solid var(--sh3-border);
253
+ }
254
+ .sh3-doc-browser__title { font-weight: 600; }
255
+ .sh3-doc-browser__crumbs {
256
+ display: flex; align-items: center; gap: 2px;
257
+ padding: 4px 8px;
258
+ border-bottom: 1px solid var(--sh3-border);
259
+ overflow-x: auto;
260
+ flex-shrink: 0;
261
+ }
262
+ .sh3-doc-browser__crumb {
263
+ display: inline-flex; align-items: center;
264
+ padding: 1px 4px;
265
+ border: none; background: none;
266
+ color: var(--sh3-fg-muted);
267
+ font: inherit; font-size: 0.75rem;
268
+ cursor: pointer;
269
+ border-radius: var(--sh3-radius-sm);
270
+ white-space: nowrap;
271
+ flex-shrink: 0;
272
+ }
273
+ .sh3-doc-browser__crumb:hover { background: var(--sh3-bg); color: var(--sh3-fg); }
274
+ .sh3-doc-browser__crumb--last { color: var(--sh3-fg); font-weight: 600; }
275
+ .sh3-doc-browser__crumb-sep { color: var(--sh3-fg-subtle); font-size: 0.75rem; flex-shrink: 0; }
276
+ .sh3-doc-browser__list { flex: 1; overflow-y: auto; padding: 4px 0; min-height: 0; }
277
+ .sh3-doc-browser__item { display: flex; align-items: center; gap: 8px; padding: 5px 12px; cursor: pointer; }
278
+ .sh3-doc-browser__item--active { background: var(--sh3-bg); }
279
+ .sh3-doc-browser__item--selected { background: var(--sh3-accent); color: var(--sh3-fg-on-accent); }
280
+ .sh3-doc-browser__item--active.sh3-doc-browser__item--selected { background: var(--sh3-accent); }
281
+ .sh3-doc-browser__icon { flex-shrink: 0; width: 18px; text-align: center; font-size: 0.875rem; }
282
+ .sh3-doc-browser__icon--folder { font-size: 0.75rem; }
283
+ .sh3-doc-browser__name {
284
+ flex: 1; min-width: 0;
285
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
286
+ font-family: var(--sh3-font-mono); font-size: 0.75rem;
287
+ }
288
+ .sh3-doc-browser__item--folder .sh3-doc-browser__name { font-family: inherit; font-size: 0.8125rem; }
289
+ .sh3-doc-browser__meta {
290
+ display: flex; align-items: center; gap: 10px;
291
+ flex-shrink: 0; font-size: 0.6875rem; color: var(--sh3-fg-muted);
292
+ }
293
+ .sh3-doc-browser__item--selected .sh3-doc-browser__meta { color: var(--sh3-fg-on-accent); opacity: 0.8; }
294
+ .sh3-doc-browser__date { color: var(--sh3-fg-subtle); }
295
+ .sh3-doc-browser__item--selected .sh3-doc-browser__date { color: var(--sh3-fg-on-accent); opacity: 0.65; }
296
+ .sh3-doc-browser__empty { padding: 20px 12px; text-align: center; color: var(--sh3-fg-muted); font-style: italic; }
297
+ .sh3-doc-browser__save-row {
298
+ display: flex; align-items: center; gap: 8px;
299
+ padding: 6px 12px; border-top: 1px solid var(--sh3-border);
300
+ }
301
+ .sh3-doc-browser__save-label { font-size: 0.75rem; color: var(--sh3-fg-muted); flex-shrink: 0; }
302
+ .sh3-doc-browser__save-input {
303
+ flex: 1;
304
+ height: var(--sh3-field-height-sm);
305
+ padding: 0 8px;
306
+ background: var(--sh3-input-bg);
307
+ border: 1px solid var(--sh3-border);
308
+ border-radius: var(--sh3-radius-sm);
309
+ color: var(--sh3-fg);
310
+ font: inherit; font-family: var(--sh3-font-mono); font-size: 0.75rem;
311
+ outline: none;
312
+ }
313
+ .sh3-doc-browser__save-input:focus { border-color: var(--sh3-input-border-focus); box-shadow: var(--sh3-focus-ring); }
314
+ .sh3-doc-browser__footer {
315
+ display: flex; justify-content: flex-end; gap: 8px;
316
+ padding: 8px 12px; border-top: 1px solid var(--sh3-border);
317
+ }
318
+ .sh3-doc-browser__btn {
319
+ display: inline-flex; align-items: center;
320
+ height: 26px; padding: 0 12px;
321
+ border: 1px solid var(--sh3-border);
322
+ border-radius: var(--sh3-radius-sm);
323
+ background: var(--sh3-bg-elevated);
324
+ color: var(--sh3-fg);
325
+ font: inherit; font-size: 0.75rem;
326
+ cursor: pointer;
327
+ }
328
+ .sh3-doc-browser__btn:hover { background: var(--sh3-bg); }
329
+ .sh3-doc-browser__btn--primary {
330
+ background: var(--sh3-accent);
331
+ color: var(--sh3-fg-on-accent);
332
+ border-color: var(--sh3-accent);
333
+ }
334
+ .sh3-doc-browser__btn--primary:hover { filter: brightness(1.1); }
335
+ .sh3-doc-browser__btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
336
+ .sh3-doc-browser__btn--primary:disabled:hover { filter: none; }
337
+ </style>
@@ -0,0 +1,11 @@
1
+ import { type DocEntry, type OpenerValue, type SaverValue } from './DocumentFilePicker';
2
+ type $$ComponentProps = {
3
+ mode: 'open' | 'save';
4
+ docs: DocEntry[];
5
+ onCommit: (value: OpenerValue | SaverValue) => void;
6
+ onCancel: () => void;
7
+ close: () => void;
8
+ };
9
+ declare const DocumentBrowser: import("svelte").Component<$$ComponentProps, {}, "">;
10
+ type DocumentBrowser = ReturnType<typeof DocumentBrowser>;
11
+ export default DocumentBrowser;