@bendyline/docblocks-react 2.6.2 → 2.7.1

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 (57) hide show
  1. package/README.md +3 -10
  2. package/THIRD_PARTY_NOTICES.txt +4284 -3651
  3. package/dist/AiDiagrams-LFV4YU6W.js +13 -0
  4. package/dist/AiEditorAssistant-GN2KXKZD.js +786 -0
  5. package/dist/AudioExportDialog-F23QXNPB.js +253 -0
  6. package/dist/DocumentConflictDialog-5LRVIGXS.js +111 -0
  7. package/dist/ExportDialog-IMBVGRYG.js +9 -0
  8. package/dist/{ExportToolbarControls-QYLBRWM7.js → ExportToolbarControls-MUM6HM3K.js} +117 -49
  9. package/dist/{GitToolbarControl-O5AXBC6T.js → GitToolbarControl-B7KG4MFP.js} +6 -6
  10. package/dist/{GitUI-L5T3XHZG.js → GitUI-RCWUZVS6.js} +6 -6
  11. package/dist/PresentationMaker-W2JIBHRN.js +9 -0
  12. package/dist/QuickOpenDialog-2KK4SEBU.js +356 -0
  13. package/dist/Settings-DuXk6baH.d.ts +144 -0
  14. package/dist/ShareDialog-XUA2OVGI.js +292 -0
  15. package/dist/SpeechToolbarControl-OSQS7NVY.js +434 -0
  16. package/dist/TextFromNarration-XYRAB6O2.js +598 -0
  17. package/dist/WorkspaceSearchPanel-BKZJRPMY.js +334 -0
  18. package/dist/chunk-2IYBBFQX.js +201 -0
  19. package/dist/{chunk-LG6HAWCK.js → chunk-56KO2LYN.js} +35 -4
  20. package/dist/chunk-6B7G33D5.js +88 -0
  21. package/dist/chunk-ARATARVJ.js +40 -0
  22. package/dist/{chunk-ODYNOPC3.js → chunk-BCQZTWE5.js} +57 -17
  23. package/dist/{chunk-FMDLZ4ZG.js → chunk-BD54Z5XP.js} +12 -3
  24. package/dist/chunk-EA4BO6E4.js +73 -0
  25. package/dist/chunk-EPOLZNFD.js +518 -0
  26. package/dist/chunk-G4MFB3IO.js +208 -0
  27. package/dist/{chunk-MRUK56JS.js → chunk-GMA4KGGO.js} +7 -3
  28. package/dist/chunk-I44HKDQX.js +314 -0
  29. package/dist/chunk-K4I3NJFL.js +49 -0
  30. package/dist/chunk-KBDZDFPU.js +61 -0
  31. package/dist/{chunk-FNSRCCBC.js → chunk-KWG6UP5O.js} +0 -10
  32. package/dist/chunk-NAH52736.js +1884 -0
  33. package/dist/chunk-RP3DVNTI.js +62 -0
  34. package/dist/chunk-SPUPWCSI.js +111 -0
  35. package/dist/chunk-TBLZBNQF.js +25 -0
  36. package/dist/{chunk-M5Y5WO7Z.js → chunk-UG4G7PJ4.js} +1 -1
  37. package/dist/chunk-UONFWKJ7.js +22 -0
  38. package/dist/chunk-VO2JYYQZ.js +40 -0
  39. package/dist/chunk-W55E6TE2.js +1714 -0
  40. package/dist/chunk-X3DDTJKR.js +574 -0
  41. package/dist/{chunk-EBWYGTN7.js → chunk-X437UAF7.js} +6 -0
  42. package/dist/editor.d.ts +36 -1
  43. package/dist/editor.js +16 -1
  44. package/dist/export/index.d.ts +27 -11
  45. package/dist/export/index.js +11 -7
  46. package/dist/index.d.ts +100 -13
  47. package/dist/index.js +3927 -1638
  48. package/dist/proofing/index.js +11 -1
  49. package/dist/{run-export-I2XOVGG4.js → run-export-47X5FTYZ.js} +3 -3
  50. package/dist/settings/index.d.ts +32 -101
  51. package/dist/settings/index.js +14 -2
  52. package/package.json +9 -9
  53. package/src/styles/docblocks.css +2344 -657
  54. package/dist/ExportDialog-Q4VWI43H.js +0 -9
  55. package/dist/ShareDialog-4OX3UPJ4.js +0 -359
  56. package/dist/chunk-JDJVRDOP.js +0 -92
  57. package/dist/chunk-Q6XNIKDG.js +0 -669
@@ -0,0 +1,1714 @@
1
+ import {
2
+ Dialog
3
+ } from "./chunk-56KO2LYN.js";
4
+
5
+ // src/Settings/AiSettings.tsx
6
+ import { useCallback, useEffect as useEffect2, useId, useRef as useRef2, useState as useState2 } from "react";
7
+
8
+ // src/Settings/AiKnowledgeSettings.tsx
9
+ import { useEffect, useRef, useState } from "react";
10
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
+ function sizedArticle(bytes) {
12
+ const megabytes = String(Math.max(1, Math.round(bytes / 1024 ** 2)));
13
+ const an = megabytes.startsWith("8") || megabytes.length % 3 === 2 && (megabytes.startsWith("11") || megabytes.startsWith("18"));
14
+ return `${an ? "an" : "a"} ${megabytes} MB`;
15
+ }
16
+ function AiKnowledgeSettings({ knowledge }) {
17
+ const [state, setState] = useState(null);
18
+ const [error, setError] = useState(null);
19
+ const [loadError, setLoadError] = useState(null);
20
+ const [unsupported, setUnsupported] = useState(null);
21
+ const [busy, setBusy] = useState(false);
22
+ const [revision, setRevision] = useState(0);
23
+ const [browse, setBrowse] = useState(false);
24
+ const [query, setQuery] = useState("");
25
+ const [removing, setRemoving] = useState(null);
26
+ const generation = useRef(0);
27
+ useEffect(
28
+ () => () => {
29
+ generation.current++;
30
+ },
31
+ [knowledge]
32
+ );
33
+ useEffect(() => {
34
+ let live = true;
35
+ let timer;
36
+ const refresh = async () => {
37
+ try {
38
+ const result = await knowledge.state();
39
+ if (!live) return;
40
+ if (!result.ok) {
41
+ if (result.error.code === "unsupported") setUnsupported(result.error.message);
42
+ else setLoadError(result.error.message);
43
+ return;
44
+ }
45
+ setState(result.value);
46
+ setLoadError(null);
47
+ setUnsupported(null);
48
+ if (result.value.improvement?.downloading || result.value.catalogs.some((catalog) => catalog.state === "downloading")) {
49
+ timer = setTimeout(() => void refresh(), 1500);
50
+ }
51
+ } catch {
52
+ if (live) setLoadError("Could not refresh knowledge catalogs. Try again.");
53
+ }
54
+ };
55
+ void refresh();
56
+ return () => {
57
+ live = false;
58
+ if (timer) clearTimeout(timer);
59
+ };
60
+ }, [knowledge, revision]);
61
+ const update = async (action) => {
62
+ const current = generation.current;
63
+ setBusy(true);
64
+ setError(null);
65
+ try {
66
+ const result = await knowledge.update(action);
67
+ if (current !== generation.current) return;
68
+ if (!result.ok) setError(result.error.message);
69
+ else {
70
+ setRemoving(null);
71
+ setRevision((value) => value + 1);
72
+ }
73
+ } catch {
74
+ if (current === generation.current) setError("Could not change this catalog. Try again.");
75
+ } finally {
76
+ if (current === generation.current) setBusy(false);
77
+ }
78
+ };
79
+ const visible = state?.catalogs.filter(
80
+ (catalog) => (browse || catalog.installedVersion !== null || catalog.state === "downloading") && `${catalog.name} ${catalog.description}`.toLowerCase().includes(query.toLowerCase())
81
+ ) ?? [];
82
+ if (unsupported)
83
+ return /* @__PURE__ */ jsxs("section", { className: "db-settings-ai-knowledge", "aria-label": "Knowledge catalogs", children: [
84
+ /* @__PURE__ */ jsx("h3", { className: "db-settings-select-header", children: "Knowledge catalogs" }),
85
+ /* @__PURE__ */ jsx("p", { className: "db-settings-hint", children: unsupported })
86
+ ] });
87
+ return /* @__PURE__ */ jsxs("section", { className: "db-settings-ai-knowledge", "aria-label": "Knowledge catalogs", children: [
88
+ /* @__PURE__ */ jsx("h3", { className: "db-settings-select-header", children: "Knowledge catalogs" }),
89
+ /* @__PURE__ */ jsx("p", { className: "db-settings-hint", children: "Add reference material for AI writing, review, and chat. Relevant passages from enabled catalogs are included in each request." }),
90
+ (error ?? loadError) && /* @__PURE__ */ jsx("p", { className: "db-settings-hint db-settings-ai-error", role: "alert", children: error ?? loadError }),
91
+ !state && !loadError && /* @__PURE__ */ jsx("p", { className: "db-settings-hint", role: "status", children: "Loading knowledge catalogs\u2026" }),
92
+ state?.improvement && !state.improvement.downloading && /* @__PURE__ */ jsx("p", { className: "db-settings-hint", children: /* @__PURE__ */ jsxs(
93
+ "button",
94
+ {
95
+ type: "button",
96
+ className: "db-ai-link-button",
97
+ disabled: busy,
98
+ onClick: () => void update({ action: "improve" }),
99
+ children: [
100
+ "Improve knowledge results with ",
101
+ sizedArticle(state.improvement.downloadBytes),
102
+ " model download"
103
+ ]
104
+ }
105
+ ) }),
106
+ /* @__PURE__ */ jsxs("div", { className: "db-settings-ai-knowledge-actions", children: [
107
+ /* @__PURE__ */ jsx(
108
+ "button",
109
+ {
110
+ type: "button",
111
+ className: "db-settings-action db-settings-action--secondary",
112
+ disabled: busy,
113
+ onClick: () => setBrowse(!browse),
114
+ children: browse ? "Show downloaded catalogs" : "Browse additional catalogs"
115
+ }
116
+ ),
117
+ /* @__PURE__ */ jsx(
118
+ "button",
119
+ {
120
+ type: "button",
121
+ className: "db-settings-action db-settings-action--secondary",
122
+ disabled: busy,
123
+ onClick: () => setRevision((value) => value + 1),
124
+ children: "Refresh catalogs"
125
+ }
126
+ )
127
+ ] }),
128
+ state && /* @__PURE__ */ jsxs("label", { className: "db-settings-select", children: [
129
+ /* @__PURE__ */ jsx("span", { className: "db-settings-select-header", children: "Find a catalog" }),
130
+ /* @__PURE__ */ jsx(
131
+ "input",
132
+ {
133
+ className: "db-settings-select-input",
134
+ type: "search",
135
+ value: query,
136
+ onChange: (event) => setQuery(event.currentTarget.value)
137
+ }
138
+ )
139
+ ] }),
140
+ state && visible.length === 0 && /* @__PURE__ */ jsx("p", { className: "db-settings-hint", children: browse ? "No matching catalogs are available." : "No downloaded catalogs match. Browse additional catalogs to add reference material." }),
141
+ /* @__PURE__ */ jsx("ul", { className: "db-settings-ai-knowledge-list", children: visible.map((catalog) => /* @__PURE__ */ jsxs("li", { children: [
142
+ /* @__PURE__ */ jsx("strong", { children: catalog.name }),
143
+ /* @__PURE__ */ jsx("p", { className: "db-settings-hint", children: catalog.description }),
144
+ /* @__PURE__ */ jsxs("p", { className: "db-settings-hint", children: [
145
+ "Version ",
146
+ catalog.installedVersion ?? catalog.version,
147
+ catalog.documents === null ? "" : ` \xB7 ${catalog.documents.toLocaleString()} documents`,
148
+ catalog.downloadBytes === null ? "" : ` \xB7 ${(catalog.downloadBytes / 1024 ** 2).toFixed(1)} MB download`
149
+ ] }),
150
+ catalog.message && /* @__PURE__ */ jsx("p", { className: "db-settings-hint", role: catalog.state === "error" ? "alert" : "status", children: catalog.message }),
151
+ catalog.state === "downloading" ? /* @__PURE__ */ jsxs(Fragment, { children: [
152
+ /* @__PURE__ */ jsx(
153
+ "progress",
154
+ {
155
+ max: 100,
156
+ value: catalog.percent ?? void 0,
157
+ "aria-label": `${catalog.name} download progress`
158
+ }
159
+ ),
160
+ /* @__PURE__ */ jsx(
161
+ "button",
162
+ {
163
+ type: "button",
164
+ className: "db-settings-action db-settings-action--secondary",
165
+ disabled: busy,
166
+ onClick: () => void update({ action: "cancel", catalogId: catalog.id }),
167
+ children: "Cancel download"
168
+ }
169
+ )
170
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "db-settings-ai-knowledge-actions", children: [
171
+ catalog.installedVersion !== null && /* @__PURE__ */ jsxs("label", { className: "db-settings-checkbox", children: [
172
+ /* @__PURE__ */ jsx(
173
+ "input",
174
+ {
175
+ type: "checkbox",
176
+ checked: catalog.enabled,
177
+ disabled: busy,
178
+ onChange: (event) => void update({
179
+ action: event.currentTarget.checked ? "enable" : "disable",
180
+ catalogId: catalog.id
181
+ })
182
+ }
183
+ ),
184
+ "Use ",
185
+ catalog.name
186
+ ] }),
187
+ (catalog.installedVersion === null || catalog.updateAvailable) && /* @__PURE__ */ jsx(
188
+ "button",
189
+ {
190
+ type: "button",
191
+ className: "db-settings-action",
192
+ disabled: busy,
193
+ onClick: () => void update({ action: "install", catalogId: catalog.id }),
194
+ children: catalog.updateAvailable ? `Update to ${catalog.version}` : "Download catalog"
195
+ }
196
+ ),
197
+ catalog.installedVersion !== null && /* @__PURE__ */ jsxs(
198
+ "button",
199
+ {
200
+ type: "button",
201
+ className: "db-settings-action db-settings-action--secondary",
202
+ disabled: busy,
203
+ onClick: () => setRemoving(catalog.id),
204
+ children: [
205
+ "Remove ",
206
+ catalog.name,
207
+ "\u2026"
208
+ ]
209
+ }
210
+ )
211
+ ] }),
212
+ removing === catalog.id && /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": `Confirm removal of ${catalog.name}`, children: [
213
+ /* @__PURE__ */ jsxs("p", { className: "db-settings-hint", children: [
214
+ "Remove ",
215
+ catalog.name,
216
+ " from the connected AI service? Other apps using that service will also lose access to this catalog."
217
+ ] }),
218
+ /* @__PURE__ */ jsx(
219
+ "button",
220
+ {
221
+ type: "button",
222
+ className: "db-settings-action",
223
+ disabled: busy,
224
+ onClick: () => void update({ action: "remove", catalogId: catalog.id }),
225
+ children: "Confirm removal"
226
+ }
227
+ ),
228
+ /* @__PURE__ */ jsx(
229
+ "button",
230
+ {
231
+ type: "button",
232
+ className: "db-settings-action db-settings-action--secondary",
233
+ onClick: () => setRemoving(null),
234
+ children: "Keep catalog"
235
+ }
236
+ )
237
+ ] })
238
+ ] }, catalog.id)) })
239
+ ] });
240
+ }
241
+
242
+ // src/Settings/AiSettings.tsx
243
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
244
+ function statusSentence(status) {
245
+ switch (status.kind) {
246
+ case "unavailable":
247
+ switch (status.reason) {
248
+ case "opt-out":
249
+ return "AI features are off.";
250
+ case "platform-unsupported":
251
+ return "AI features are not available in this build of DocBlocks.";
252
+ case "not-installed":
253
+ return "DocBlocks could not start its built-in AI service.";
254
+ case "not-running":
255
+ return "DocBlocks is restarting its built-in AI service\u2026";
256
+ case "disconnected":
257
+ return "DocBlocks is starting its built-in AI service\u2026";
258
+ }
259
+ break;
260
+ case "connecting":
261
+ switch (status.step) {
262
+ case "detecting":
263
+ return "Looking for Gezel\u2026";
264
+ case "awaiting-approval":
265
+ return "Waiting for approval in Gezel\u2026";
266
+ case "preparing-model":
267
+ case "preparing-workspace": {
268
+ const progress = status.progress;
269
+ if (!progress?.message) return "Getting ready\u2026";
270
+ const percent = progress.percent === null ? "" : ` (${Math.round(progress.percent)}%)`;
271
+ return `Getting ready: ${progress.message}${percent}`;
272
+ }
273
+ }
274
+ break;
275
+ case "ready": {
276
+ const version = status.provider.version ? ` ${status.provider.version}` : "";
277
+ if (status.provider.mode === "hosted") {
278
+ if (!status.model) return "Add an on-device model to start using AI.";
279
+ return "";
280
+ }
281
+ return `Connected to ${status.provider.name}${version}.`;
282
+ }
283
+ case "error":
284
+ return status.error.message;
285
+ }
286
+ return "";
287
+ }
288
+ function modelLabel(model) {
289
+ return model.local ? `${model.label} (on this device)` : model.label;
290
+ }
291
+ function modelReady(model) {
292
+ return model.availability === void 0 || model.availability === "available";
293
+ }
294
+ function readinessLabel(model) {
295
+ switch (model.availability) {
296
+ case "download-required":
297
+ return "download required";
298
+ case "downloading":
299
+ return "downloading";
300
+ default:
301
+ return "not available";
302
+ }
303
+ }
304
+ function formatDownloadSize(bytes) {
305
+ if (bytes === null) return null;
306
+ const gibibytes = bytes / 1024 ** 3;
307
+ if (gibibytes >= 0.95) return `${gibibytes.toFixed(gibibytes >= 10 ? 0 : 1)} GB`;
308
+ return `${Math.max(1, Math.round(bytes / 1024 ** 2))} MB`;
309
+ }
310
+ function AiSettingsControls({ ai }) {
311
+ const [status, setStatus] = useState2(null);
312
+ const [preferences, setPreferences] = useState2(null);
313
+ const [providerInstalled, setProviderInstalled] = useState2(false);
314
+ const [models, setModels] = useState2([]);
315
+ const [availableModels, setAvailableModels] = useState2([]);
316
+ const [showModelDownloads, setShowModelDownloads] = useState2(false);
317
+ const [downloadModelId, setDownloadModelId] = useState2("");
318
+ const [downloadProgress, setDownloadProgress] = useState2(null);
319
+ const [loadingAvailableModels, setLoadingAvailableModels] = useState2(false);
320
+ const [installingModel, setInstallingModel] = useState2(false);
321
+ const [busy, setBusy] = useState2(false);
322
+ const [saving, setSaving] = useState2(false);
323
+ const [failure, setFailure] = useState2(null);
324
+ const mounted = useRef2(true);
325
+ const installHandle = useRef2(null);
326
+ const modelSelectId = useId();
327
+ useEffect2(() => {
328
+ mounted.current = true;
329
+ setProviderInstalled(false);
330
+ const unsubscribe = ai.onStatus((next) => {
331
+ if (mounted.current) setStatus(next);
332
+ });
333
+ void ai.providerInstalled().then(
334
+ (installed) => {
335
+ if (mounted.current) setProviderInstalled(installed);
336
+ },
337
+ () => {
338
+ if (mounted.current) setProviderInstalled(false);
339
+ }
340
+ );
341
+ void Promise.all([ai.status(), ai.getPreferences()]).then(([initialStatus, initial]) => {
342
+ if (!mounted.current) return;
343
+ setStatus((current) => current ?? initialStatus);
344
+ setPreferences(initial);
345
+ });
346
+ return () => {
347
+ mounted.current = false;
348
+ installHandle.current?.cancel();
349
+ unsubscribe();
350
+ };
351
+ }, [ai]);
352
+ const ready = status?.kind === "ready";
353
+ const refreshModels = useCallback(async () => {
354
+ const result = await ai.models();
355
+ if (mounted.current && result.ok) setModels(result.value);
356
+ return result;
357
+ }, [ai]);
358
+ const refreshAvailableModels = useCallback(async () => {
359
+ if (!ai.availableModels) return;
360
+ setLoadingAvailableModels(true);
361
+ try {
362
+ const result = await ai.availableModels();
363
+ if (!mounted.current) return;
364
+ if (result.ok) {
365
+ setAvailableModels(result.value);
366
+ setDownloadModelId(
367
+ (current) => result.value.some((model) => model.id === current) ? current : result.value[0]?.id ?? ""
368
+ );
369
+ } else {
370
+ setFailure(result.error);
371
+ }
372
+ } finally {
373
+ if (mounted.current) setLoadingAvailableModels(false);
374
+ }
375
+ }, [ai]);
376
+ useEffect2(() => {
377
+ if (!ready) {
378
+ setModels([]);
379
+ setAvailableModels([]);
380
+ setShowModelDownloads(false);
381
+ return;
382
+ }
383
+ void refreshModels();
384
+ }, [ready, refreshModels]);
385
+ const connect = useCallback(async () => {
386
+ setBusy(true);
387
+ setFailure(null);
388
+ try {
389
+ const result = await ai.connect();
390
+ if (mounted.current && !result.ok && result.error.code !== "cancelled") {
391
+ setFailure(result.error);
392
+ }
393
+ } finally {
394
+ if (mounted.current) setBusy(false);
395
+ }
396
+ }, [ai]);
397
+ const switchToBuiltIn = useCallback(async () => {
398
+ setBusy(true);
399
+ setFailure(null);
400
+ try {
401
+ await ai.disconnect();
402
+ } finally {
403
+ if (mounted.current) setBusy(false);
404
+ }
405
+ }, [ai]);
406
+ const setEnabled = useCallback(
407
+ async (enabled2) => {
408
+ const previous = preferences;
409
+ setFailure(null);
410
+ setSaving(true);
411
+ if (previous) setPreferences({ ...previous, enabled: enabled2 });
412
+ let next;
413
+ try {
414
+ next = await ai.setPreferences({ enabled: enabled2 });
415
+ } catch {
416
+ if (mounted.current && previous) setPreferences(previous);
417
+ return;
418
+ } finally {
419
+ if (mounted.current) setSaving(false);
420
+ }
421
+ if (!mounted.current) return;
422
+ setPreferences(next);
423
+ },
424
+ [ai, preferences]
425
+ );
426
+ const retryBuiltIn = useCallback(async () => {
427
+ setBusy(true);
428
+ setFailure(null);
429
+ try {
430
+ await ai.setPreferences({ enabled: true });
431
+ } finally {
432
+ if (mounted.current) setBusy(false);
433
+ }
434
+ }, [ai]);
435
+ const setModel = useCallback(
436
+ async (model) => {
437
+ const next = await ai.setPreferences({ model });
438
+ if (mounted.current) setPreferences(next);
439
+ },
440
+ [ai]
441
+ );
442
+ const addModel = useCallback(async () => {
443
+ if (!downloadModelId || !ai.installModel) return;
444
+ setFailure(null);
445
+ setDownloadProgress({ phase: "starting", message: "Starting download\u2026", percent: null });
446
+ setInstallingModel(true);
447
+ const handle = ai.installModel(downloadModelId, (progress) => {
448
+ if (mounted.current) setDownloadProgress(progress);
449
+ });
450
+ installHandle.current = handle;
451
+ try {
452
+ const result = await handle.done;
453
+ if (!mounted.current) return;
454
+ if (!result.ok) {
455
+ if (result.error.code !== "cancelled") setFailure(result.error);
456
+ return;
457
+ }
458
+ const nextPreferences = await ai.setPreferences({ model: result.value.id });
459
+ if (!mounted.current) return;
460
+ setPreferences(nextPreferences);
461
+ await Promise.all([refreshModels(), refreshAvailableModels()]);
462
+ } finally {
463
+ if (installHandle.current === handle) installHandle.current = null;
464
+ if (mounted.current) {
465
+ setInstallingModel(false);
466
+ setDownloadProgress(null);
467
+ }
468
+ }
469
+ }, [ai, downloadModelId, refreshAvailableModels, refreshModels]);
470
+ if (!preferences || !status) return null;
471
+ const enabled = preferences.enabled;
472
+ const connecting = status.kind === "connecting";
473
+ const code = status.kind === "connecting" ? status.verificationCode : null;
474
+ const defaultModel = models.find((model) => model.isDefault) ?? null;
475
+ const hasReadyModels = models.some(modelReady);
476
+ const unavailableModels = models.filter((model) => !modelReady(model));
477
+ const modelManagement = Boolean(ai.availableModels && ai.installModel);
478
+ const selectedDownload = availableModels.find((model) => model.id === downloadModelId) ?? null;
479
+ const failureMessage = failure && !(status.kind === "error" && status.error.message === failure.message) && !(status.kind === "unavailable" && failure.code === "provider-unavailable") ? failure.message : null;
480
+ return /* @__PURE__ */ jsxs2("fieldset", { className: "db-settings-fieldset", children: [
481
+ /* @__PURE__ */ jsx2("legend", { className: "db-settings-legend", children: "AI assistance" }),
482
+ /* @__PURE__ */ jsxs2("label", { className: "db-settings-checkbox", children: [
483
+ /* @__PURE__ */ jsx2(
484
+ "input",
485
+ {
486
+ type: "checkbox",
487
+ checked: enabled,
488
+ disabled: saving,
489
+ onChange: (event) => void setEnabled(event.currentTarget.checked)
490
+ }
491
+ ),
492
+ "Use AI features"
493
+ ] }),
494
+ /* @__PURE__ */ jsx2("p", { className: "db-settings-hint", children: "DocBlocks can offer AI features that run entirely on this device \u2014 no account needed. Model downloads start only when you choose to add one." }),
495
+ enabled && /* @__PURE__ */ jsxs2(Fragment2, { children: [
496
+ /* @__PURE__ */ jsx2("p", { className: "db-settings-hint", role: "status", "aria-live": "polite", children: statusSentence(status) }),
497
+ code && /* @__PURE__ */ jsxs2("p", { className: "db-settings-hint", children: [
498
+ "Type this code in Gezel to approve DocBlocks:",
499
+ " ",
500
+ /* @__PURE__ */ jsx2("strong", { className: "db-settings-ai-code", children: code })
501
+ ] }),
502
+ failureMessage && /* @__PURE__ */ jsx2("p", { className: "db-settings-hint db-settings-ai-error", role: "alert", children: failureMessage }),
503
+ ready && /* @__PURE__ */ jsxs2(Fragment2, { children: [
504
+ /* @__PURE__ */ jsxs2("label", { className: "db-settings-select", htmlFor: modelSelectId, children: [
505
+ /* @__PURE__ */ jsx2("span", { className: "db-settings-select-header", children: "Model" }),
506
+ /* @__PURE__ */ jsxs2(
507
+ "select",
508
+ {
509
+ id: modelSelectId,
510
+ className: "db-settings-select-input",
511
+ value: preferences.model ?? "",
512
+ disabled: !hasReadyModels || installingModel,
513
+ onChange: (event) => void setModel(event.currentTarget.value || null),
514
+ children: [
515
+ !hasReadyModels ? /* @__PURE__ */ jsx2("option", { value: "", children: models.length ? "No models ready" : "No models installed" }) : /* @__PURE__ */ jsx2("option", { value: "", children: defaultModel ? `Default: ${modelLabel(defaultModel)}` : "Default" }),
516
+ preferences.model && !models.some((model) => model.id === preferences.model) && /* @__PURE__ */ jsx2("option", { value: preferences.model, disabled: true, children: "Previously selected model (not available)" }),
517
+ models.map((model) => /* @__PURE__ */ jsx2("option", { value: model.id, disabled: !modelReady(model), children: modelReady(model) ? modelLabel(model) : `${model.label} (${readinessLabel(model)})` }, model.id))
518
+ ]
519
+ }
520
+ )
521
+ ] }),
522
+ unavailableModels.map((model) => /* @__PURE__ */ jsxs2("p", { className: "db-settings-hint", children: [
523
+ model.label,
524
+ ":",
525
+ " ",
526
+ model.unavailableReason ?? "This model is not available right now."
527
+ ] }, model.id)),
528
+ modelManagement && /* @__PURE__ */ jsxs2("div", { className: "db-settings-ai-models", children: [
529
+ /* @__PURE__ */ jsx2(
530
+ "button",
531
+ {
532
+ type: "button",
533
+ className: "db-settings-action db-settings-action--secondary",
534
+ disabled: installingModel,
535
+ onClick: () => {
536
+ const next = !showModelDownloads;
537
+ setShowModelDownloads(next);
538
+ if (next) void refreshAvailableModels();
539
+ },
540
+ children: showModelDownloads ? "Hide model downloads" : "Add model\u2026"
541
+ }
542
+ ),
543
+ showModelDownloads && /* @__PURE__ */ jsxs2("div", { className: "db-settings-ai-model-downloads", children: [
544
+ loadingAvailableModels ? /* @__PURE__ */ jsx2("p", { className: "db-settings-hint", role: "status", children: "Looking for models\u2026" }) : availableModels.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "db-settings-hint", children: "No downloadable models are available for this device." }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
545
+ /* @__PURE__ */ jsxs2("label", { className: "db-settings-select", children: [
546
+ /* @__PURE__ */ jsx2("span", { className: "db-settings-select-header", children: "Model to download" }),
547
+ /* @__PURE__ */ jsx2(
548
+ "select",
549
+ {
550
+ className: "db-settings-select-input",
551
+ value: downloadModelId,
552
+ disabled: installingModel,
553
+ onChange: (event) => setDownloadModelId(event.currentTarget.value),
554
+ children: availableModels.map((model) => {
555
+ const size = formatDownloadSize(model.downloadBytes);
556
+ return /* @__PURE__ */ jsxs2("option", { value: model.id, children: [
557
+ model.label,
558
+ size ? ` \u2014 ${size}` : ""
559
+ ] }, model.id);
560
+ })
561
+ }
562
+ )
563
+ ] }),
564
+ selectedDownload && /* @__PURE__ */ jsxs2("p", { className: "db-settings-hint", children: [
565
+ "Downloads and runs on this device",
566
+ formatDownloadSize(selectedDownload.downloadBytes) ? ` \xB7 ${formatDownloadSize(selectedDownload.downloadBytes)}` : ""
567
+ ] }),
568
+ /* @__PURE__ */ jsx2(
569
+ "button",
570
+ {
571
+ type: "button",
572
+ className: "db-settings-action",
573
+ disabled: !downloadModelId || installingModel,
574
+ onClick: () => void addModel(),
575
+ children: installingModel ? "Downloading\u2026" : "Download model"
576
+ }
577
+ )
578
+ ] }),
579
+ installingModel && downloadProgress && /* @__PURE__ */ jsxs2("div", { className: "db-settings-ai-download-progress", role: "status", children: [
580
+ /* @__PURE__ */ jsx2(
581
+ "progress",
582
+ {
583
+ max: 100,
584
+ value: downloadProgress.percent ?? void 0,
585
+ "aria-label": "Model download progress"
586
+ }
587
+ ),
588
+ /* @__PURE__ */ jsx2(
589
+ "button",
590
+ {
591
+ type: "button",
592
+ className: "db-settings-action db-settings-action--secondary",
593
+ onClick: () => installHandle.current?.cancel(),
594
+ children: "Cancel download"
595
+ }
596
+ ),
597
+ /* @__PURE__ */ jsxs2("span", { children: [
598
+ downloadProgress.message,
599
+ downloadProgress.percent === null ? "" : ` (${Math.round(downloadProgress.percent)}%)`
600
+ ] })
601
+ ] })
602
+ ] })
603
+ ] })
604
+ ] }),
605
+ ready && ai.knowledge && /* @__PURE__ */ jsx2(AiKnowledgeSettings, { knowledge: ai.knowledge }, status.provider.mode),
606
+ ready && status.provider.mode === "installed" ? /* @__PURE__ */ jsx2(
607
+ "button",
608
+ {
609
+ type: "button",
610
+ className: "db-settings-action db-settings-action--secondary",
611
+ disabled: busy,
612
+ onClick: () => void switchToBuiltIn(),
613
+ children: "Use built-in AI"
614
+ }
615
+ ) : ready && status.provider.mode === "hosted" && providerInstalled ? /* @__PURE__ */ jsx2(
616
+ "button",
617
+ {
618
+ type: "button",
619
+ className: "db-settings-action",
620
+ disabled: busy,
621
+ onClick: () => void connect(),
622
+ children: "Connect Gezel app\u2026"
623
+ }
624
+ ) : status.kind === "error" && status.retryable ? /* @__PURE__ */ jsx2(
625
+ "button",
626
+ {
627
+ type: "button",
628
+ className: "db-settings-action",
629
+ disabled: busy || connecting,
630
+ onClick: () => void retryBuiltIn(),
631
+ children: "Try built-in AI again"
632
+ }
633
+ ) : null
634
+ ] })
635
+ ] });
636
+ }
637
+
638
+ // src/Settings/WorkspaceSettingsControls.tsx
639
+ import { useEffect as useEffect3, useId as useId2, useState as useState3 } from "react";
640
+ import {
641
+ DEFAULT_CATALOG_HTML_PATH,
642
+ DEFAULT_CATALOG_JSON_PATH,
643
+ DEFAULT_CATALOG_TITLE
644
+ } from "@bendyline/docblocks/workspace-settings";
645
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
646
+ function compactObject(value) {
647
+ const entries = Object.entries(value).filter(([, field]) => field !== void 0);
648
+ return entries.length > 0 ? Object.fromEntries(entries) : void 0;
649
+ }
650
+ function withSection(settings, key, section) {
651
+ const next = { ...settings };
652
+ if (section === void 0) delete next[key];
653
+ else next[key] = section;
654
+ return next;
655
+ }
656
+ function withCatalog(settings, patch) {
657
+ return withSection(
658
+ settings,
659
+ "catalog",
660
+ compactObject({ ...settings.catalog ?? {}, ...patch })
661
+ );
662
+ }
663
+ function withCatalogHtml(settings, patch) {
664
+ return withCatalog(settings, {
665
+ html: compactObject({
666
+ ...settings.catalog?.html ?? {},
667
+ ...patch
668
+ })
669
+ });
670
+ }
671
+ function withCatalogJson(settings, patch) {
672
+ return withCatalog(settings, {
673
+ json: compactObject({
674
+ ...settings.catalog?.json ?? {},
675
+ ...patch
676
+ })
677
+ });
678
+ }
679
+ function optionalText(value) {
680
+ return value.trim() ? value : void 0;
681
+ }
682
+ function WorkspaceSettingsControls({
683
+ value,
684
+ onChange,
685
+ themes,
686
+ disabled = false,
687
+ versionHistory,
688
+ catalogStatus,
689
+ onRegenerate,
690
+ regenerating = false
691
+ }) {
692
+ const id = useId2();
693
+ const htmlEnabled = value.catalog?.html?.enabled === true;
694
+ const jsonEnabled = value.catalog?.json?.enabled === true;
695
+ const catalogEnabled = htmlEnabled || jsonEnabled;
696
+ const excludeText = (value.catalog?.exclude ?? []).join("\n");
697
+ const [excludeDraft, setExcludeDraft] = useState3(excludeText);
698
+ useEffect3(() => {
699
+ setExcludeDraft(
700
+ (current) => current.split("\n").map((line) => line.trim()).filter(Boolean).join("\n") === excludeText ? current : excludeText
701
+ );
702
+ }, [excludeText]);
703
+ const versioning = value.versionHistory?.enabled === void 0 ? "inherit" : value.versionHistory.enabled ? "on" : "off";
704
+ const themeSelect = (selectId, selected, emptyLabel, onSelect) => /* @__PURE__ */ jsxs3(
705
+ "select",
706
+ {
707
+ id: selectId,
708
+ className: "db-settings-select-input",
709
+ value: selected ?? "",
710
+ disabled,
711
+ onChange: (event) => onSelect(event.currentTarget.value || void 0),
712
+ children: [
713
+ /* @__PURE__ */ jsx3("option", { value: "", children: emptyLabel }),
714
+ themes.map((theme) => /* @__PURE__ */ jsx3("option", { value: theme.id, children: theme.name }, theme.id)),
715
+ selected && !themes.some((theme) => theme.id === selected) && /* @__PURE__ */ jsxs3("option", { value: selected, children: [
716
+ selected,
717
+ " (not installed)"
718
+ ] })
719
+ ]
720
+ }
721
+ );
722
+ return /* @__PURE__ */ jsxs3("div", { className: "db-workspace-settings", children: [
723
+ /* @__PURE__ */ jsxs3("fieldset", { className: "db-settings-fieldset", disabled, children: [
724
+ /* @__PURE__ */ jsx3("legend", { className: "db-settings-legend", children: "Documents" }),
725
+ /* @__PURE__ */ jsxs3("div", { className: "db-settings-select", children: [
726
+ /* @__PURE__ */ jsx3("label", { className: "db-settings-select-header", htmlFor: `${id}-theme`, children: "Default theme" }),
727
+ themeSelect(
728
+ `${id}-theme`,
729
+ value.documents?.defaultTheme,
730
+ "Standard",
731
+ (themeId) => onChange(
732
+ withSection(
733
+ value,
734
+ "documents",
735
+ compactObject({ ...value.documents ?? {}, defaultTheme: themeId })
736
+ )
737
+ )
738
+ )
739
+ ] }),
740
+ /* @__PURE__ */ jsx3("p", { className: "db-settings-hint", children: "Used by documents in this folder that don\u2019t choose a theme of their own. Web pages pick up a change the next time they\u2019re saved." })
741
+ ] }),
742
+ /* @__PURE__ */ jsxs3("fieldset", { className: "db-settings-fieldset", disabled, children: [
743
+ /* @__PURE__ */ jsx3("legend", { className: "db-settings-legend", children: "Catalog" }),
744
+ /* @__PURE__ */ jsx3("p", { className: "db-settings-hint", children: "Keep a list of every document in this folder up to date as you save. DocBlocks only ever replaces files it created." }),
745
+ /* @__PURE__ */ jsxs3("label", { className: "db-settings-checkbox", children: [
746
+ /* @__PURE__ */ jsx3(
747
+ "input",
748
+ {
749
+ type: "checkbox",
750
+ checked: htmlEnabled,
751
+ onChange: (event) => onChange(
752
+ withCatalogHtml(value, { enabled: event.currentTarget.checked ? true : void 0 })
753
+ )
754
+ }
755
+ ),
756
+ "Catalog page"
757
+ ] }),
758
+ htmlEnabled && /* @__PURE__ */ jsxs3("div", { className: "db-settings-subfields", children: [
759
+ /* @__PURE__ */ jsxs3("div", { className: "db-settings-text", children: [
760
+ /* @__PURE__ */ jsx3("label", { htmlFor: `${id}-html-path`, children: "Page file" }),
761
+ /* @__PURE__ */ jsx3(
762
+ "input",
763
+ {
764
+ id: `${id}-html-path`,
765
+ className: "db-settings-text-input",
766
+ type: "text",
767
+ spellCheck: false,
768
+ placeholder: DEFAULT_CATALOG_HTML_PATH,
769
+ value: value.catalog?.html?.path ?? "",
770
+ onChange: (event) => onChange(
771
+ withCatalogHtml(value, { path: optionalText(event.currentTarget.value) })
772
+ )
773
+ }
774
+ )
775
+ ] }),
776
+ /* @__PURE__ */ jsxs3("div", { className: "db-settings-text", children: [
777
+ /* @__PURE__ */ jsx3("label", { htmlFor: `${id}-html-theme`, children: "Page theme" }),
778
+ themeSelect(
779
+ `${id}-html-theme`,
780
+ value.catalog?.html?.theme,
781
+ "Same as default theme",
782
+ (themeId) => onChange(withCatalogHtml(value, { theme: themeId }))
783
+ )
784
+ ] })
785
+ ] }),
786
+ /* @__PURE__ */ jsxs3("label", { className: "db-settings-checkbox", children: [
787
+ /* @__PURE__ */ jsx3(
788
+ "input",
789
+ {
790
+ type: "checkbox",
791
+ checked: jsonEnabled,
792
+ onChange: (event) => onChange(
793
+ withCatalogJson(value, { enabled: event.currentTarget.checked ? true : void 0 })
794
+ )
795
+ }
796
+ ),
797
+ "Catalog data (JSON)"
798
+ ] }),
799
+ jsonEnabled && /* @__PURE__ */ jsxs3("div", { className: "db-settings-subfields", children: [
800
+ /* @__PURE__ */ jsxs3("div", { className: "db-settings-text", children: [
801
+ /* @__PURE__ */ jsx3("label", { htmlFor: `${id}-json-path`, children: "Data file" }),
802
+ /* @__PURE__ */ jsx3(
803
+ "input",
804
+ {
805
+ id: `${id}-json-path`,
806
+ className: "db-settings-text-input",
807
+ type: "text",
808
+ spellCheck: false,
809
+ placeholder: DEFAULT_CATALOG_JSON_PATH,
810
+ value: value.catalog?.json?.path ?? "",
811
+ onChange: (event) => onChange(
812
+ withCatalogJson(value, { path: optionalText(event.currentTarget.value) })
813
+ )
814
+ }
815
+ )
816
+ ] }),
817
+ /* @__PURE__ */ jsx3(
818
+ "div",
819
+ {
820
+ className: "db-settings-radio-row",
821
+ role: "radiogroup",
822
+ "aria-label": "Catalog data content",
823
+ children: [
824
+ ["metadata", "Titles, descriptions and outlines"],
825
+ ["markdown", "Also include each document\u2019s Markdown"]
826
+ ].map(([content, label]) => /* @__PURE__ */ jsxs3("label", { className: "db-settings-radio db-settings-radio--inline", children: [
827
+ /* @__PURE__ */ jsx3(
828
+ "input",
829
+ {
830
+ type: "radio",
831
+ name: `${id}-json-content`,
832
+ checked: (value.catalog?.json?.content ?? "metadata") === content,
833
+ onChange: () => onChange(
834
+ withCatalogJson(value, {
835
+ content: content === "metadata" ? void 0 : content
836
+ })
837
+ )
838
+ }
839
+ ),
840
+ label
841
+ ] }, content))
842
+ }
843
+ )
844
+ ] }),
845
+ catalogEnabled && /* @__PURE__ */ jsxs3("div", { className: "db-settings-subfields", children: [
846
+ /* @__PURE__ */ jsxs3("div", { className: "db-settings-text", children: [
847
+ /* @__PURE__ */ jsx3("label", { htmlFor: `${id}-title`, children: "Title" }),
848
+ /* @__PURE__ */ jsx3(
849
+ "input",
850
+ {
851
+ id: `${id}-title`,
852
+ className: "db-settings-text-input",
853
+ type: "text",
854
+ placeholder: DEFAULT_CATALOG_TITLE,
855
+ value: value.catalog?.title ?? "",
856
+ onChange: (event) => onChange(withCatalog(value, { title: optionalText(event.currentTarget.value) }))
857
+ }
858
+ )
859
+ ] }),
860
+ /* @__PURE__ */ jsxs3("div", { className: "db-settings-text", children: [
861
+ /* @__PURE__ */ jsx3("label", { htmlFor: `${id}-sort`, children: "Order" }),
862
+ /* @__PURE__ */ jsxs3(
863
+ "select",
864
+ {
865
+ id: `${id}-sort`,
866
+ className: "db-settings-select-input",
867
+ value: value.catalog?.sort ?? "title",
868
+ onChange: (event) => {
869
+ const sort = event.currentTarget.value;
870
+ onChange(withCatalog(value, { sort: sort === "title" ? void 0 : sort }));
871
+ },
872
+ children: [
873
+ /* @__PURE__ */ jsx3("option", { value: "title", children: "By title" }),
874
+ /* @__PURE__ */ jsx3("option", { value: "date", children: "Newest first (frontmatter date)" }),
875
+ /* @__PURE__ */ jsx3("option", { value: "path", children: "By file path" })
876
+ ]
877
+ }
878
+ )
879
+ ] }),
880
+ /* @__PURE__ */ jsxs3("div", { className: "db-settings-text", children: [
881
+ /* @__PURE__ */ jsx3("label", { htmlFor: `${id}-exclude`, children: "Leave out" }),
882
+ /* @__PURE__ */ jsx3(
883
+ "textarea",
884
+ {
885
+ id: `${id}-exclude`,
886
+ className: "db-settings-text-input db-settings-textarea",
887
+ rows: 2,
888
+ spellCheck: false,
889
+ placeholder: "drafts/",
890
+ value: excludeDraft,
891
+ onChange: (event) => {
892
+ const text = event.currentTarget.value;
893
+ setExcludeDraft(text);
894
+ const exclude = text.split("\n").map((line) => line.trim()).filter(Boolean);
895
+ onChange(
896
+ withCatalog(value, { exclude: exclude.length > 0 ? exclude : void 0 })
897
+ );
898
+ }
899
+ }
900
+ )
901
+ ] }),
902
+ /* @__PURE__ */ jsx3("p", { className: "db-settings-hint", children: "One folder or file per line." }),
903
+ (catalogStatus || onRegenerate) && /* @__PURE__ */ jsxs3("div", { className: "db-settings-status-row", children: [
904
+ catalogStatus && /* @__PURE__ */ jsx3(
905
+ "p",
906
+ {
907
+ className: `db-settings-status db-settings-status--${catalogStatus.tone}`,
908
+ role: "status",
909
+ children: catalogStatus.message
910
+ }
911
+ ),
912
+ onRegenerate && /* @__PURE__ */ jsx3(
913
+ "button",
914
+ {
915
+ type: "button",
916
+ className: "db-git-secondary-btn",
917
+ disabled: regenerating,
918
+ onClick: onRegenerate,
919
+ children: regenerating ? "Updating\u2026" : "Update now"
920
+ }
921
+ )
922
+ ] })
923
+ ] })
924
+ ] }),
925
+ versionHistory && /* @__PURE__ */ jsxs3("fieldset", { className: "db-settings-fieldset", disabled, children: [
926
+ /* @__PURE__ */ jsx3("legend", { className: "db-settings-legend", children: "Version history" }),
927
+ /* @__PURE__ */ jsxs3("p", { className: "db-settings-hint", children: [
928
+ "Keeps prior revisions in ",
929
+ /* @__PURE__ */ jsx3("code", { children: "<name>_files/.versions/" }),
930
+ " for documents in this folder."
931
+ ] }),
932
+ [
933
+ ["inherit", versionHistory.inheritLabel],
934
+ ["on", "On for this folder"],
935
+ ["off", "Off for this folder"]
936
+ ].map(([option, label]) => /* @__PURE__ */ jsxs3("label", { className: "db-settings-radio", children: [
937
+ /* @__PURE__ */ jsx3(
938
+ "input",
939
+ {
940
+ type: "radio",
941
+ name: `${id}-versioning`,
942
+ value: option,
943
+ checked: versioning === option,
944
+ onChange: () => onChange(
945
+ withSection(
946
+ value,
947
+ "versionHistory",
948
+ compactObject({
949
+ ...value.versionHistory ?? {},
950
+ enabled: option === "inherit" ? void 0 : option === "on"
951
+ })
952
+ )
953
+ )
954
+ }
955
+ ),
956
+ label
957
+ ] }, option)),
958
+ /* @__PURE__ */ jsxs3("div", { className: "db-settings-text", children: [
959
+ /* @__PURE__ */ jsx3("label", { htmlFor: `${id}-keep`, children: "Revisions to keep per document" }),
960
+ /* @__PURE__ */ jsx3(
961
+ "input",
962
+ {
963
+ id: `${id}-keep`,
964
+ className: "db-settings-text-input db-settings-number-input",
965
+ type: "number",
966
+ inputMode: "numeric",
967
+ min: 1,
968
+ max: 1e3,
969
+ step: 1,
970
+ placeholder: "50",
971
+ value: value.versionHistory?.keep ?? "",
972
+ onChange: (event) => {
973
+ const raw = event.currentTarget.value;
974
+ const keep = raw === "" ? void 0 : Number(raw);
975
+ onChange(
976
+ withSection(
977
+ value,
978
+ "versionHistory",
979
+ compactObject({ ...value.versionHistory ?? {}, keep })
980
+ )
981
+ );
982
+ }
983
+ }
984
+ )
985
+ ] })
986
+ ] })
987
+ ] });
988
+ }
989
+
990
+ // src/Settings/Settings.tsx
991
+ import {
992
+ useCallback as useCallback2,
993
+ useEffect as useEffect4,
994
+ useId as useId3,
995
+ useLayoutEffect,
996
+ useRef as useRef3,
997
+ useState as useState4
998
+ } from "react";
999
+
1000
+ // src/preferences/theme.ts
1001
+ var ACCENT_COLORS = [
1002
+ "brown",
1003
+ "green",
1004
+ "blue",
1005
+ "purple",
1006
+ "maroon",
1007
+ "orange",
1008
+ "gray"
1009
+ ];
1010
+ var DB_CHROME_COLORS = {
1011
+ light: "#f3eede",
1012
+ dark: "#262219"
1013
+ };
1014
+ var DEFAULT_INTERFACE_FONT = "system";
1015
+ var THEME_STORAGE_KEY = "docblocks:themePreference";
1016
+ var ACCENT_STORAGE_KEY = "docblocks:accentColor";
1017
+ var INTERFACE_FONT_STORAGE_KEY = "docblocks:interfaceFont";
1018
+ function loadThemePreference() {
1019
+ try {
1020
+ const raw = localStorage.getItem(THEME_STORAGE_KEY);
1021
+ if (raw === "light" || raw === "dark" || raw === "auto") return raw;
1022
+ } catch {
1023
+ }
1024
+ return "auto";
1025
+ }
1026
+ function saveThemePreference(value) {
1027
+ try {
1028
+ localStorage.setItem(THEME_STORAGE_KEY, value);
1029
+ } catch {
1030
+ }
1031
+ }
1032
+ function loadAccentColor() {
1033
+ try {
1034
+ const raw = localStorage.getItem(ACCENT_STORAGE_KEY);
1035
+ if (ACCENT_COLORS.some((color) => color === raw)) return raw;
1036
+ } catch {
1037
+ }
1038
+ return "brown";
1039
+ }
1040
+ function saveAccentColor(value) {
1041
+ try {
1042
+ localStorage.setItem(ACCENT_STORAGE_KEY, value);
1043
+ } catch {
1044
+ }
1045
+ }
1046
+ function loadInterfaceFont() {
1047
+ try {
1048
+ const raw = localStorage.getItem(INTERFACE_FONT_STORAGE_KEY);
1049
+ if (raw === "system" || raw === "fixed") return raw;
1050
+ } catch {
1051
+ }
1052
+ return DEFAULT_INTERFACE_FONT;
1053
+ }
1054
+ function saveInterfaceFont(value) {
1055
+ try {
1056
+ localStorage.setItem(INTERFACE_FONT_STORAGE_KEY, value);
1057
+ } catch {
1058
+ }
1059
+ }
1060
+
1061
+ // src/preferences/write-canvas.ts
1062
+ var SANS_STACK = 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
1063
+ var SERIF_STACK = 'Georgia, "Times New Roman", serif';
1064
+ var WRITE_CANVAS_FONT_SCHEMES = [
1065
+ { id: "theme", label: "Inherit from theme", group: "theme" },
1066
+ {
1067
+ id: "serif-sans",
1068
+ label: "Serif headings \xB7 Sans body",
1069
+ group: "system",
1070
+ headerFont: SERIF_STACK,
1071
+ bodyFont: SANS_STACK
1072
+ },
1073
+ {
1074
+ id: "sans-sans",
1075
+ label: "Sans headings \xB7 Sans body",
1076
+ group: "system",
1077
+ headerFont: SANS_STACK,
1078
+ bodyFont: SANS_STACK
1079
+ },
1080
+ {
1081
+ id: "serif-serif",
1082
+ label: "Serif headings \xB7 Serif body",
1083
+ group: "system",
1084
+ headerFont: SERIF_STACK,
1085
+ bodyFont: SERIF_STACK
1086
+ },
1087
+ {
1088
+ id: "pt-serif",
1089
+ label: "PT Serif",
1090
+ group: "curated",
1091
+ headerFont: '"PT Serif", Georgia, serif',
1092
+ bodyFont: '"PT Serif", Georgia, serif'
1093
+ },
1094
+ {
1095
+ id: "hanken",
1096
+ label: "Hanken Grotesk",
1097
+ group: "curated",
1098
+ headerFont: '"Hanken Grotesk", system-ui, sans-serif',
1099
+ bodyFont: '"Hanken Grotesk", system-ui, sans-serif'
1100
+ },
1101
+ {
1102
+ id: "playfair-pt-serif",
1103
+ label: "Playfair Display \xB7 PT Serif",
1104
+ group: "curated",
1105
+ headerFont: '"Playfair Display", Georgia, serif',
1106
+ bodyFont: '"PT Serif", Georgia, serif'
1107
+ },
1108
+ {
1109
+ id: "hanken-lora",
1110
+ label: "Hanken Grotesk \xB7 Lora",
1111
+ group: "curated",
1112
+ headerFont: '"Hanken Grotesk", system-ui, sans-serif',
1113
+ bodyFont: '"Lora", Georgia, serif'
1114
+ },
1115
+ {
1116
+ id: "dm-serif-dm-sans",
1117
+ label: "DM Serif Display \xB7 DM Sans",
1118
+ group: "curated",
1119
+ headerFont: '"DM Serif Display", Georgia, serif',
1120
+ bodyFont: '"DM Sans", system-ui, sans-serif'
1121
+ },
1122
+ {
1123
+ id: "inter",
1124
+ label: "Inter",
1125
+ group: "curated",
1126
+ headerFont: '"Inter", system-ui, sans-serif',
1127
+ bodyFont: '"Inter", system-ui, sans-serif'
1128
+ }
1129
+ ];
1130
+ var FONT_SCHEMES_BY_ID = new Map(WRITE_CANVAS_FONT_SCHEMES.map((scheme) => [scheme.id, scheme]));
1131
+ var DEFAULT_WRITE_CANVAS_FONT_SCHEME = "theme";
1132
+ function resolveWriteCanvasFonts(scheme) {
1133
+ const option = FONT_SCHEMES_BY_ID.get(scheme);
1134
+ const resolved = {};
1135
+ if (option?.headerFont) resolved.headerFont = option.headerFont;
1136
+ if (option?.bodyFont) resolved.bodyFont = option.bodyFont;
1137
+ return resolved;
1138
+ }
1139
+ function isWriteCanvasFontScheme(value) {
1140
+ return typeof value === "string" && FONT_SCHEMES_BY_ID.has(value);
1141
+ }
1142
+ var WRITE_CANVAS_TEXT_SIZE_MIN = 12;
1143
+ var WRITE_CANVAS_TEXT_SIZE_MAX = 32;
1144
+ var WRITE_CANVAS_LINE_SPACING_MIN = 1;
1145
+ var WRITE_CANVAS_LINE_SPACING_MAX = 2.4;
1146
+ var DEFAULT_WRITE_CANVAS_PREFERENCES = {
1147
+ textSize: 16,
1148
+ lineSpacing: 1.7,
1149
+ fontScheme: DEFAULT_WRITE_CANVAS_FONT_SCHEME
1150
+ };
1151
+ var WRITE_CANVAS_STORAGE_KEY = "docblocks:writeCanvasSettings";
1152
+ function loadWriteCanvasPreferences() {
1153
+ try {
1154
+ const raw = localStorage.getItem(WRITE_CANVAS_STORAGE_KEY);
1155
+ if (!raw) return { ...DEFAULT_WRITE_CANVAS_PREFERENCES };
1156
+ const stored = JSON.parse(raw);
1157
+ if (!isRecord(stored)) return { ...DEFAULT_WRITE_CANVAS_PREFERENCES };
1158
+ return {
1159
+ textSize: numberInRange(
1160
+ stored.textSize,
1161
+ WRITE_CANVAS_TEXT_SIZE_MIN,
1162
+ WRITE_CANVAS_TEXT_SIZE_MAX
1163
+ ) ? stored.textSize : DEFAULT_WRITE_CANVAS_PREFERENCES.textSize,
1164
+ lineSpacing: numberInRange(
1165
+ stored.lineSpacing,
1166
+ WRITE_CANVAS_LINE_SPACING_MIN,
1167
+ WRITE_CANVAS_LINE_SPACING_MAX
1168
+ ) ? stored.lineSpacing : DEFAULT_WRITE_CANVAS_PREFERENCES.lineSpacing,
1169
+ // Older stored preferences predate fontScheme; fall back to the default.
1170
+ fontScheme: isWriteCanvasFontScheme(stored.fontScheme) ? stored.fontScheme : DEFAULT_WRITE_CANVAS_PREFERENCES.fontScheme
1171
+ };
1172
+ } catch {
1173
+ return { ...DEFAULT_WRITE_CANVAS_PREFERENCES };
1174
+ }
1175
+ }
1176
+ function saveWriteCanvasPreferences(value) {
1177
+ try {
1178
+ localStorage.setItem(WRITE_CANVAS_STORAGE_KEY, JSON.stringify(value));
1179
+ } catch {
1180
+ }
1181
+ }
1182
+ function numberInRange(value, min, max) {
1183
+ return typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
1184
+ }
1185
+ function isRecord(value) {
1186
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1187
+ }
1188
+
1189
+ // src/Settings/Settings.tsx
1190
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1191
+ var ACCENT_LABELS = {
1192
+ brown: "Brown",
1193
+ green: "Green",
1194
+ blue: "Blue",
1195
+ purple: "Purple",
1196
+ maroon: "Maroon",
1197
+ orange: "Orange",
1198
+ gray: "Gray"
1199
+ };
1200
+ function SettingsDialog({ title = "Settings", onClose, children }) {
1201
+ return /* @__PURE__ */ jsx4(Dialog, { title, onClose, size: "wide", children });
1202
+ }
1203
+ function ThemeSettings({ value, onChange, name = "theme" }) {
1204
+ return /* @__PURE__ */ jsxs4("fieldset", { className: "db-settings-fieldset", children: [
1205
+ /* @__PURE__ */ jsx4("legend", { className: "db-settings-legend", children: "Theme" }),
1206
+ /* @__PURE__ */ jsxs4("div", { className: "db-settings-radio-row", children: [
1207
+ /* @__PURE__ */ jsxs4("label", { className: "db-settings-radio db-settings-radio--inline", children: [
1208
+ /* @__PURE__ */ jsx4(
1209
+ "input",
1210
+ {
1211
+ type: "radio",
1212
+ name,
1213
+ value: "auto",
1214
+ checked: value === "auto",
1215
+ onChange: () => onChange("auto")
1216
+ }
1217
+ ),
1218
+ "System default"
1219
+ ] }),
1220
+ /* @__PURE__ */ jsxs4("label", { className: "db-settings-radio db-settings-radio--inline", children: [
1221
+ /* @__PURE__ */ jsx4(
1222
+ "input",
1223
+ {
1224
+ type: "radio",
1225
+ name,
1226
+ value: "light",
1227
+ checked: value === "light",
1228
+ onChange: () => onChange("light")
1229
+ }
1230
+ ),
1231
+ "Light"
1232
+ ] }),
1233
+ /* @__PURE__ */ jsxs4("label", { className: "db-settings-radio db-settings-radio--inline", children: [
1234
+ /* @__PURE__ */ jsx4(
1235
+ "input",
1236
+ {
1237
+ type: "radio",
1238
+ name,
1239
+ value: "dark",
1240
+ checked: value === "dark",
1241
+ onChange: () => onChange("dark")
1242
+ }
1243
+ ),
1244
+ "Dark"
1245
+ ] })
1246
+ ] })
1247
+ ] });
1248
+ }
1249
+ function InterfaceFontSettings({
1250
+ value,
1251
+ onChange,
1252
+ name = "interface-font"
1253
+ }) {
1254
+ return /* @__PURE__ */ jsxs4("fieldset", { className: "db-settings-fieldset", children: [
1255
+ /* @__PURE__ */ jsx4("legend", { className: "db-settings-legend", children: "Interface font" }),
1256
+ /* @__PURE__ */ jsx4("p", { className: "db-settings-hint", children: "Applies to menus and panels, never to your documents." }),
1257
+ /* @__PURE__ */ jsxs4("div", { className: "db-settings-radio-row", children: [
1258
+ /* @__PURE__ */ jsxs4("label", { className: "db-settings-radio db-settings-radio--inline", children: [
1259
+ /* @__PURE__ */ jsx4(
1260
+ "input",
1261
+ {
1262
+ type: "radio",
1263
+ name,
1264
+ value: "system",
1265
+ checked: value !== "fixed",
1266
+ onChange: () => onChange("system")
1267
+ }
1268
+ ),
1269
+ "System default"
1270
+ ] }),
1271
+ /* @__PURE__ */ jsxs4("label", { className: "db-settings-radio db-settings-radio--inline", children: [
1272
+ /* @__PURE__ */ jsx4(
1273
+ "input",
1274
+ {
1275
+ type: "radio",
1276
+ name,
1277
+ value: "fixed",
1278
+ checked: value === "fixed",
1279
+ onChange: () => onChange("fixed")
1280
+ }
1281
+ ),
1282
+ "Bundled"
1283
+ ] })
1284
+ ] })
1285
+ ] });
1286
+ }
1287
+ function AccentColorSettings({
1288
+ value,
1289
+ onChange,
1290
+ name = "accent-color"
1291
+ }) {
1292
+ return /* @__PURE__ */ jsxs4("fieldset", { className: "db-settings-fieldset", children: [
1293
+ /* @__PURE__ */ jsx4("legend", { className: "db-settings-legend", children: "Accent color" }),
1294
+ /* @__PURE__ */ jsx4("p", { className: "db-settings-hint", children: "Used in both light and dark appearances." }),
1295
+ /* @__PURE__ */ jsx4("div", { className: "db-settings-accent-grid", children: ACCENT_COLORS.map((color) => /* @__PURE__ */ jsxs4(
1296
+ "label",
1297
+ {
1298
+ className: `db-settings-accent${value === color ? " db-settings-accent--selected" : ""}`,
1299
+ children: [
1300
+ /* @__PURE__ */ jsx4(
1301
+ "input",
1302
+ {
1303
+ className: "db-settings-accent-input",
1304
+ type: "radio",
1305
+ name,
1306
+ value: color,
1307
+ checked: value === color,
1308
+ onChange: () => onChange(color)
1309
+ }
1310
+ ),
1311
+ /* @__PURE__ */ jsx4(
1312
+ "span",
1313
+ {
1314
+ className: `db-settings-accent-swatch db-settings-accent-swatch--${color}`,
1315
+ "aria-hidden": "true"
1316
+ }
1317
+ ),
1318
+ /* @__PURE__ */ jsx4("span", { children: ACCENT_LABELS[color] })
1319
+ ]
1320
+ },
1321
+ color
1322
+ )) })
1323
+ ] });
1324
+ }
1325
+ function ProofingSettingsControls({ value, onChange }) {
1326
+ return /* @__PURE__ */ jsxs4("fieldset", { className: "db-settings-fieldset", children: [
1327
+ /* @__PURE__ */ jsx4("legend", { className: "db-settings-legend", children: "Spelling and grammar" }),
1328
+ /* @__PURE__ */ jsx4("p", { className: "db-settings-hint", children: "Checking runs on this device \u2014 nothing you write is sent anywhere." }),
1329
+ /* @__PURE__ */ jsxs4("label", { className: "db-settings-checkbox", children: [
1330
+ /* @__PURE__ */ jsx4(
1331
+ "input",
1332
+ {
1333
+ type: "checkbox",
1334
+ checked: value.spelling,
1335
+ onChange: (event) => onChange({ ...value, spelling: event.currentTarget.checked })
1336
+ }
1337
+ ),
1338
+ "Show inline spell checking"
1339
+ ] }),
1340
+ /* @__PURE__ */ jsxs4("label", { className: "db-settings-checkbox", children: [
1341
+ /* @__PURE__ */ jsx4(
1342
+ "input",
1343
+ {
1344
+ type: "checkbox",
1345
+ checked: value.grammar,
1346
+ onChange: (event) => onChange({ ...value, grammar: event.currentTarget.checked })
1347
+ }
1348
+ ),
1349
+ "Show inline grammar checking"
1350
+ ] }),
1351
+ /* @__PURE__ */ jsx4("p", { className: "db-settings-hint", children: "(Grammar checking is currently only available for English)" })
1352
+ ] });
1353
+ }
1354
+ function WriteCanvasSettingsControls({ value, onChange }) {
1355
+ return /* @__PURE__ */ jsxs4("fieldset", { className: "db-settings-fieldset", children: [
1356
+ /* @__PURE__ */ jsx4("legend", { className: "db-settings-legend", children: "Write canvas" }),
1357
+ /* @__PURE__ */ jsx4("p", { className: "db-settings-hint", children: "Adjust the writing view without changing the document or its exported text." }),
1358
+ /* @__PURE__ */ jsxs4("label", { className: "db-settings-slider", children: [
1359
+ /* @__PURE__ */ jsxs4("span", { className: "db-settings-slider-header", children: [
1360
+ /* @__PURE__ */ jsx4("span", { children: "Text size" }),
1361
+ /* @__PURE__ */ jsxs4("output", { className: "db-settings-slider-value", "aria-hidden": "true", children: [
1362
+ value.textSize,
1363
+ "px"
1364
+ ] })
1365
+ ] }),
1366
+ /* @__PURE__ */ jsx4(
1367
+ "input",
1368
+ {
1369
+ type: "range",
1370
+ min: WRITE_CANVAS_TEXT_SIZE_MIN,
1371
+ max: WRITE_CANVAS_TEXT_SIZE_MAX,
1372
+ step: 1,
1373
+ value: value.textSize,
1374
+ "aria-label": "Text size",
1375
+ "aria-valuetext": `${value.textSize} pixels`,
1376
+ onChange: (event) => onChange({ ...value, textSize: Number(event.currentTarget.value) })
1377
+ }
1378
+ )
1379
+ ] }),
1380
+ /* @__PURE__ */ jsxs4("label", { className: "db-settings-slider", children: [
1381
+ /* @__PURE__ */ jsxs4("span", { className: "db-settings-slider-header", children: [
1382
+ /* @__PURE__ */ jsx4("span", { children: "Line spacing" }),
1383
+ /* @__PURE__ */ jsx4("output", { className: "db-settings-slider-value", "aria-hidden": "true", children: formatLineSpacing(value.lineSpacing) })
1384
+ ] }),
1385
+ /* @__PURE__ */ jsx4(
1386
+ "input",
1387
+ {
1388
+ type: "range",
1389
+ min: WRITE_CANVAS_LINE_SPACING_MIN,
1390
+ max: WRITE_CANVAS_LINE_SPACING_MAX,
1391
+ step: 0.1,
1392
+ value: value.lineSpacing,
1393
+ "aria-label": "Line spacing",
1394
+ "aria-valuetext": `${value.lineSpacing} times`,
1395
+ onChange: (event) => onChange({ ...value, lineSpacing: Number(event.currentTarget.value) })
1396
+ }
1397
+ )
1398
+ ] }),
1399
+ /* @__PURE__ */ jsx4(
1400
+ FontSchemePicker,
1401
+ {
1402
+ value: value.fontScheme,
1403
+ onChange: (fontScheme) => onChange({ ...value, fontScheme })
1404
+ }
1405
+ )
1406
+ ] });
1407
+ }
1408
+ function FontSchemePicker({ value, onChange }) {
1409
+ const selectedIndex = Math.max(
1410
+ 0,
1411
+ WRITE_CANVAS_FONT_SCHEMES.findIndex((scheme) => scheme.id === value)
1412
+ );
1413
+ const selectedScheme = WRITE_CANVAS_FONT_SCHEMES[selectedIndex];
1414
+ const [isOpen, setIsOpen] = useState4(false);
1415
+ const [activeIndex, setActiveIndex] = useState4(selectedIndex);
1416
+ const [menuPosition, setMenuPosition] = useState4(null);
1417
+ const rootRef = useRef3(null);
1418
+ const triggerRef = useRef3(null);
1419
+ const listboxRef = useRef3(null);
1420
+ const optionRefs = useRef3([]);
1421
+ const labelId = useId3();
1422
+ const listboxId = useId3();
1423
+ const close = useCallback2((returnFocus) => {
1424
+ setIsOpen(false);
1425
+ setMenuPosition(null);
1426
+ if (returnFocus) triggerRef.current?.focus({ preventScroll: true });
1427
+ }, []);
1428
+ const updateMenuPosition = useCallback2(() => {
1429
+ const trigger = triggerRef.current;
1430
+ if (!trigger) return;
1431
+ const rect = trigger.getBoundingClientRect();
1432
+ const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
1433
+ const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
1434
+ const gutter = 8;
1435
+ const gap = 4;
1436
+ const width = Math.min(rect.width, viewportWidth - gutter * 2);
1437
+ const left = Math.min(Math.max(gutter, rect.left), viewportWidth - width - gutter);
1438
+ const spaceBelow = viewportHeight - rect.bottom - gap - gutter;
1439
+ const spaceAbove = rect.top - gap - gutter;
1440
+ const openAbove = spaceBelow < 260 && spaceAbove > spaceBelow;
1441
+ const availableHeight = openAbove ? spaceAbove : spaceBelow;
1442
+ const maxHeight = Math.max(80, Math.min(420, availableHeight));
1443
+ setMenuPosition(
1444
+ openAbove ? {
1445
+ left,
1446
+ bottom: viewportHeight - rect.top + gap,
1447
+ width,
1448
+ maxHeight
1449
+ } : {
1450
+ left,
1451
+ top: rect.bottom + gap,
1452
+ width,
1453
+ maxHeight
1454
+ }
1455
+ );
1456
+ }, []);
1457
+ const open = useCallback2(
1458
+ (index) => {
1459
+ setActiveIndex(index);
1460
+ updateMenuPosition();
1461
+ setIsOpen(true);
1462
+ },
1463
+ [updateMenuPosition]
1464
+ );
1465
+ useEffect4(() => {
1466
+ if (!isOpen) setActiveIndex(selectedIndex);
1467
+ }, [isOpen, selectedIndex]);
1468
+ useLayoutEffect(() => {
1469
+ if (!isOpen) return;
1470
+ updateMenuPosition();
1471
+ listboxRef.current?.focus({ preventScroll: true });
1472
+ optionRefs.current[activeIndex]?.scrollIntoView?.({ block: "nearest" });
1473
+ }, [activeIndex, isOpen, updateMenuPosition]);
1474
+ useEffect4(() => {
1475
+ if (!isOpen) return;
1476
+ const handleOutsidePointer = (event) => {
1477
+ if (!rootRef.current?.contains(event.target)) close(false);
1478
+ };
1479
+ const handleViewportChange = () => updateMenuPosition();
1480
+ document.addEventListener("mousedown", handleOutsidePointer);
1481
+ window.addEventListener("resize", handleViewportChange);
1482
+ window.addEventListener("scroll", handleViewportChange, true);
1483
+ return () => {
1484
+ document.removeEventListener("mousedown", handleOutsidePointer);
1485
+ window.removeEventListener("resize", handleViewportChange);
1486
+ window.removeEventListener("scroll", handleViewportChange, true);
1487
+ };
1488
+ }, [close, isOpen, updateMenuPosition]);
1489
+ const select = useCallback2(
1490
+ (index) => {
1491
+ const scheme = WRITE_CANVAS_FONT_SCHEMES[index];
1492
+ if (!scheme) return;
1493
+ onChange(scheme.id);
1494
+ close(true);
1495
+ },
1496
+ [close, onChange]
1497
+ );
1498
+ const handleTriggerKeyDown = (event) => {
1499
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1500
+ event.preventDefault();
1501
+ open(selectedIndex);
1502
+ }
1503
+ };
1504
+ const handleListboxKeyDown = (event) => {
1505
+ switch (event.key) {
1506
+ case "ArrowDown":
1507
+ event.preventDefault();
1508
+ setActiveIndex((current) => (current + 1) % WRITE_CANVAS_FONT_SCHEMES.length);
1509
+ break;
1510
+ case "ArrowUp":
1511
+ event.preventDefault();
1512
+ setActiveIndex(
1513
+ (current) => (current - 1 + WRITE_CANVAS_FONT_SCHEMES.length) % WRITE_CANVAS_FONT_SCHEMES.length
1514
+ );
1515
+ break;
1516
+ case "Home":
1517
+ event.preventDefault();
1518
+ setActiveIndex(0);
1519
+ break;
1520
+ case "End":
1521
+ event.preventDefault();
1522
+ setActiveIndex(WRITE_CANVAS_FONT_SCHEMES.length - 1);
1523
+ break;
1524
+ case "Enter":
1525
+ case " ":
1526
+ event.preventDefault();
1527
+ select(activeIndex);
1528
+ break;
1529
+ case "Escape":
1530
+ event.preventDefault();
1531
+ event.stopPropagation();
1532
+ close(true);
1533
+ break;
1534
+ case "Tab":
1535
+ close(false);
1536
+ break;
1537
+ }
1538
+ };
1539
+ const menuStyle = menuPosition ? {
1540
+ left: `${menuPosition.left}px`,
1541
+ top: menuPosition.top === void 0 ? void 0 : `${menuPosition.top}px`,
1542
+ bottom: menuPosition.bottom === void 0 ? void 0 : `${menuPosition.bottom}px`,
1543
+ width: `${menuPosition.width}px`,
1544
+ maxHeight: `${menuPosition.maxHeight}px`
1545
+ } : void 0;
1546
+ return /* @__PURE__ */ jsxs4("div", { ref: rootRef, className: "db-settings-select db-font-picker", children: [
1547
+ /* @__PURE__ */ jsx4("span", { className: "db-settings-select-header", children: /* @__PURE__ */ jsx4("span", { id: labelId, children: "Font" }) }),
1548
+ /* @__PURE__ */ jsxs4(
1549
+ "button",
1550
+ {
1551
+ ref: triggerRef,
1552
+ type: "button",
1553
+ className: "db-font-picker-trigger",
1554
+ "aria-label": `Font, ${selectedScheme.label}`,
1555
+ "aria-haspopup": "listbox",
1556
+ "aria-expanded": isOpen,
1557
+ "aria-controls": isOpen ? listboxId : void 0,
1558
+ onClick: () => isOpen ? close(false) : open(selectedIndex),
1559
+ onKeyDown: handleTriggerKeyDown,
1560
+ children: [
1561
+ /* @__PURE__ */ jsx4(FontSchemePreview, { scheme: selectedScheme, compact: true }),
1562
+ /* @__PURE__ */ jsx4(
1563
+ "span",
1564
+ {
1565
+ className: `db-font-picker-caret${isOpen ? " db-font-picker-caret--open" : ""}`,
1566
+ "aria-hidden": "true"
1567
+ }
1568
+ )
1569
+ ]
1570
+ }
1571
+ ),
1572
+ isOpen && /* @__PURE__ */ jsxs4(
1573
+ "div",
1574
+ {
1575
+ ref: listboxRef,
1576
+ id: listboxId,
1577
+ className: "db-font-picker-menu",
1578
+ style: menuStyle,
1579
+ role: "listbox",
1580
+ tabIndex: -1,
1581
+ "aria-labelledby": labelId,
1582
+ "aria-activedescendant": `${listboxId}-option-${activeIndex}`,
1583
+ onKeyDown: handleListboxKeyDown,
1584
+ children: [
1585
+ WRITE_CANVAS_FONT_SCHEMES.filter((scheme) => scheme.group === "theme").map(
1586
+ (scheme) => renderFontSchemeOption(scheme)
1587
+ ),
1588
+ FONT_SCHEME_GROUPS.map(({ group, label }) => /* @__PURE__ */ jsxs4("div", { role: "group", "aria-label": label, children: [
1589
+ /* @__PURE__ */ jsx4("div", { className: "db-font-picker-group", "aria-hidden": "true", children: label }),
1590
+ WRITE_CANVAS_FONT_SCHEMES.filter((scheme) => scheme.group === group).map(
1591
+ (scheme) => renderFontSchemeOption(scheme)
1592
+ )
1593
+ ] }, group))
1594
+ ]
1595
+ }
1596
+ )
1597
+ ] });
1598
+ function renderFontSchemeOption(scheme) {
1599
+ const index = WRITE_CANVAS_FONT_SCHEMES.indexOf(scheme);
1600
+ const selected = scheme.id === value;
1601
+ const active = index === activeIndex;
1602
+ return /* @__PURE__ */ jsxs4(
1603
+ "div",
1604
+ {
1605
+ ref: (element) => {
1606
+ optionRefs.current[index] = element;
1607
+ },
1608
+ id: `${listboxId}-option-${index}`,
1609
+ className: `db-font-picker-option${selected ? " db-font-picker-option--selected" : ""}${active ? " db-font-picker-option--active" : ""}`,
1610
+ role: "option",
1611
+ "aria-selected": selected,
1612
+ onMouseDown: (event) => event.preventDefault(),
1613
+ onMouseMove: () => setActiveIndex(index),
1614
+ onClick: () => select(index),
1615
+ children: [
1616
+ /* @__PURE__ */ jsx4(FontSchemePreview, { scheme }),
1617
+ /* @__PURE__ */ jsx4("span", { className: "db-font-picker-check", "aria-hidden": "true", children: selected ? "\u2713" : "" })
1618
+ ]
1619
+ },
1620
+ scheme.id
1621
+ );
1622
+ }
1623
+ }
1624
+ function FontSchemePreview({
1625
+ scheme,
1626
+ compact = false
1627
+ }) {
1628
+ const labels = scheme.label.split(/\s*\u00b7\s*/u);
1629
+ const headingLabel = labels[0] ?? scheme.label;
1630
+ const bodyLabel = labels[1] ?? (scheme.id === "theme" ? "Use the active theme" : "Headings & body");
1631
+ return /* @__PURE__ */ jsxs4("span", { className: `db-font-picker-preview${compact ? " db-font-picker-preview--compact" : ""}`, children: [
1632
+ /* @__PURE__ */ jsx4(
1633
+ "span",
1634
+ {
1635
+ className: "db-font-picker-heading",
1636
+ style: scheme.headerFont ? { fontFamily: scheme.headerFont } : void 0,
1637
+ children: headingLabel
1638
+ }
1639
+ ),
1640
+ /* @__PURE__ */ jsx4(
1641
+ "span",
1642
+ {
1643
+ className: "db-font-picker-body",
1644
+ style: scheme.bodyFont ? { fontFamily: scheme.bodyFont } : void 0,
1645
+ children: bodyLabel
1646
+ }
1647
+ )
1648
+ ] });
1649
+ }
1650
+ var FONT_SCHEME_GROUPS = [
1651
+ { group: "system", label: "System fonts" },
1652
+ { group: "curated", label: "Curated pairings" }
1653
+ ];
1654
+ function formatLineSpacing(value) {
1655
+ return `${value.toFixed(1).replace(/\.0$/, "")}\xD7`;
1656
+ }
1657
+
1658
+ // src/preferences/proofing.ts
1659
+ var DEFAULT_PROOFING_PREFERENCES = {
1660
+ spelling: true,
1661
+ grammar: true
1662
+ };
1663
+ var STORAGE_KEY = "docblocks:proofingPreferences";
1664
+ function loadProofingPreferences() {
1665
+ try {
1666
+ const raw = localStorage.getItem(STORAGE_KEY);
1667
+ if (!raw) return { ...DEFAULT_PROOFING_PREFERENCES };
1668
+ const stored = JSON.parse(raw);
1669
+ if (typeof stored !== "object" || stored === null) {
1670
+ return { ...DEFAULT_PROOFING_PREFERENCES };
1671
+ }
1672
+ const record = stored;
1673
+ return {
1674
+ spelling: typeof record.spelling === "boolean" ? record.spelling : DEFAULT_PROOFING_PREFERENCES.spelling,
1675
+ grammar: typeof record.grammar === "boolean" ? record.grammar : DEFAULT_PROOFING_PREFERENCES.grammar
1676
+ };
1677
+ } catch {
1678
+ }
1679
+ return { ...DEFAULT_PROOFING_PREFERENCES };
1680
+ }
1681
+ function saveProofingPreferences(value) {
1682
+ try {
1683
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
1684
+ } catch {
1685
+ }
1686
+ }
1687
+
1688
+ export {
1689
+ WRITE_CANVAS_FONT_SCHEMES,
1690
+ DEFAULT_WRITE_CANVAS_FONT_SCHEME,
1691
+ resolveWriteCanvasFonts,
1692
+ DEFAULT_WRITE_CANVAS_PREFERENCES,
1693
+ loadWriteCanvasPreferences,
1694
+ saveWriteCanvasPreferences,
1695
+ DEFAULT_PROOFING_PREFERENCES,
1696
+ loadProofingPreferences,
1697
+ saveProofingPreferences,
1698
+ DB_CHROME_COLORS,
1699
+ DEFAULT_INTERFACE_FONT,
1700
+ loadThemePreference,
1701
+ saveThemePreference,
1702
+ loadAccentColor,
1703
+ saveAccentColor,
1704
+ loadInterfaceFont,
1705
+ saveInterfaceFont,
1706
+ SettingsDialog,
1707
+ ThemeSettings,
1708
+ InterfaceFontSettings,
1709
+ AccentColorSettings,
1710
+ ProofingSettingsControls,
1711
+ WriteCanvasSettingsControls,
1712
+ AiSettingsControls,
1713
+ WorkspaceSettingsControls
1714
+ };