@bendyline/docblocks-react 1.1.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/README.md +37 -5
  2. package/dist/ExportDialog-7LDIXJZ3.js +10 -0
  3. package/dist/ExportDialog-7LDIXJZ3.js.map +1 -0
  4. package/dist/ExportToolbarControls-GHAOJMH6.js +508 -0
  5. package/dist/ExportToolbarControls-GHAOJMH6.js.map +1 -0
  6. package/dist/GitToolbarControl-GNMRVKWQ.js +136 -0
  7. package/dist/GitToolbarControl-GNMRVKWQ.js.map +1 -0
  8. package/dist/GitUI-VHOBQY43.js +1133 -0
  9. package/dist/GitUI-VHOBQY43.js.map +1 -0
  10. package/dist/LazyEditorShell-EF2BWRXU.js +6 -0
  11. package/dist/LazyEditorShell-EF2BWRXU.js.map +1 -0
  12. package/dist/ShareDialog-63ENBEDZ.js +213 -0
  13. package/dist/ShareDialog-63ENBEDZ.js.map +1 -0
  14. package/dist/chunk-4XXSWN3T.js +13 -0
  15. package/dist/chunk-4XXSWN3T.js.map +1 -0
  16. package/dist/chunk-57ZJJOC5.js +89 -0
  17. package/dist/chunk-57ZJJOC5.js.map +1 -0
  18. package/dist/chunk-5A4KEWKT.js +97 -0
  19. package/dist/chunk-5A4KEWKT.js.map +1 -0
  20. package/dist/chunk-DD4VFJQI.js +51 -0
  21. package/dist/chunk-DD4VFJQI.js.map +1 -0
  22. package/dist/chunk-FEWQUQML.js +256 -0
  23. package/dist/chunk-FEWQUQML.js.map +1 -0
  24. package/dist/chunk-FMGTHSTX.js +110 -0
  25. package/dist/chunk-FMGTHSTX.js.map +1 -0
  26. package/dist/chunk-UNYZQXWR.js +324 -0
  27. package/dist/chunk-UNYZQXWR.js.map +1 -0
  28. package/dist/chunk-VHDP4IPL.js +70 -0
  29. package/dist/chunk-VHDP4IPL.js.map +1 -0
  30. package/dist/chunk-ZI56BNB7.js +270 -0
  31. package/dist/chunk-ZI56BNB7.js.map +1 -0
  32. package/dist/editor.d.ts +11 -0
  33. package/dist/editor.js +9 -0
  34. package/dist/editor.js.map +1 -0
  35. package/dist/export/index.d.ts +93 -0
  36. package/dist/export/index.js +27 -0
  37. package/dist/export/index.js.map +1 -0
  38. package/dist/index.d.ts +346 -18
  39. package/dist/index.js +6169 -18
  40. package/dist/index.js.map +1 -1
  41. package/dist/run-export-ICTZITXC.js +10 -0
  42. package/dist/run-export-ICTZITXC.js.map +1 -0
  43. package/dist/settings/index.d.ts +36 -0
  44. package/dist/settings/index.js +14 -0
  45. package/dist/settings/index.js.map +1 -0
  46. package/package.json +33 -13
  47. package/src/styles/docblocks.css +2738 -132
  48. package/dist/AppMenu/AppMenu.d.ts +0 -26
  49. package/dist/AppMenu/AppMenu.d.ts.map +0 -1
  50. package/dist/AppMenu/AppMenu.js +0 -28
  51. package/dist/AppMenu/AppMenu.js.map +0 -1
  52. package/dist/AppMenu/index.d.ts +0 -3
  53. package/dist/AppMenu/index.d.ts.map +0 -1
  54. package/dist/AppMenu/index.js +0 -2
  55. package/dist/AppMenu/index.js.map +0 -1
  56. package/dist/DocBlocksShell/DocBlocksShell.d.ts +0 -36
  57. package/dist/DocBlocksShell/DocBlocksShell.d.ts.map +0 -1
  58. package/dist/DocBlocksShell/DocBlocksShell.js +0 -1351
  59. package/dist/DocBlocksShell/DocBlocksShell.js.map +0 -1
  60. package/dist/DocBlocksShell/index.d.ts +0 -3
  61. package/dist/DocBlocksShell/index.d.ts.map +0 -1
  62. package/dist/DocBlocksShell/index.js +0 -2
  63. package/dist/DocBlocksShell/index.js.map +0 -1
  64. package/dist/Export/ExportDialog.d.ts +0 -16
  65. package/dist/Export/ExportDialog.d.ts.map +0 -1
  66. package/dist/Export/ExportDialog.js +0 -158
  67. package/dist/Export/ExportDialog.js.map +0 -1
  68. package/dist/Export/ExportToolbarControls.d.ts +0 -17
  69. package/dist/Export/ExportToolbarControls.d.ts.map +0 -1
  70. package/dist/Export/ExportToolbarControls.js +0 -200
  71. package/dist/Export/ExportToolbarControls.js.map +0 -1
  72. package/dist/Export/export-options.d.ts +0 -53
  73. package/dist/Export/export-options.d.ts.map +0 -1
  74. package/dist/Export/export-options.js +0 -49
  75. package/dist/Export/export-options.js.map +0 -1
  76. package/dist/Export/index.d.ts +0 -6
  77. package/dist/Export/index.d.ts.map +0 -1
  78. package/dist/Export/index.js +0 -3
  79. package/dist/Export/index.js.map +0 -1
  80. package/dist/Export/run-export.d.ts +0 -8
  81. package/dist/Export/run-export.d.ts.map +0 -1
  82. package/dist/Export/run-export.js +0 -296
  83. package/dist/Export/run-export.js.map +0 -1
  84. package/dist/Export/transform-summaries.d.ts +0 -7
  85. package/dist/Export/transform-summaries.d.ts.map +0 -1
  86. package/dist/Export/transform-summaries.js +0 -6
  87. package/dist/Export/transform-summaries.js.map +0 -1
  88. package/dist/FileExplorer/FileExplorer.d.ts +0 -21
  89. package/dist/FileExplorer/FileExplorer.d.ts.map +0 -1
  90. package/dist/FileExplorer/FileExplorer.js +0 -140
  91. package/dist/FileExplorer/FileExplorer.js.map +0 -1
  92. package/dist/FileExplorer/FileTreeNode.d.ts +0 -18
  93. package/dist/FileExplorer/FileTreeNode.d.ts.map +0 -1
  94. package/dist/FileExplorer/FileTreeNode.js +0 -112
  95. package/dist/FileExplorer/FileTreeNode.js.map +0 -1
  96. package/dist/FileExplorer/index.d.ts +0 -6
  97. package/dist/FileExplorer/index.d.ts.map +0 -1
  98. package/dist/FileExplorer/index.js +0 -4
  99. package/dist/FileExplorer/index.js.map +0 -1
  100. package/dist/FileExplorer/useFileTree.d.ts +0 -35
  101. package/dist/FileExplorer/useFileTree.d.ts.map +0 -1
  102. package/dist/FileExplorer/useFileTree.js +0 -124
  103. package/dist/FileExplorer/useFileTree.js.map +0 -1
  104. package/dist/WorkspacePicker/WorkspacePicker.d.ts +0 -17
  105. package/dist/WorkspacePicker/WorkspacePicker.d.ts.map +0 -1
  106. package/dist/WorkspacePicker/WorkspacePicker.js +0 -63
  107. package/dist/WorkspacePicker/WorkspacePicker.js.map +0 -1
  108. package/dist/WorkspacePicker/WorkspaceSettingsButton.d.ts +0 -11
  109. package/dist/WorkspacePicker/WorkspaceSettingsButton.d.ts.map +0 -1
  110. package/dist/WorkspacePicker/WorkspaceSettingsButton.js +0 -27
  111. package/dist/WorkspacePicker/WorkspaceSettingsButton.js.map +0 -1
  112. package/dist/WorkspacePicker/WorkspaceSettingsDialog.d.ts +0 -20
  113. package/dist/WorkspacePicker/WorkspaceSettingsDialog.d.ts.map +0 -1
  114. package/dist/WorkspacePicker/WorkspaceSettingsDialog.js +0 -36
  115. package/dist/WorkspacePicker/WorkspaceSettingsDialog.js.map +0 -1
  116. package/dist/WorkspacePicker/index.d.ts +0 -3
  117. package/dist/WorkspacePicker/index.d.ts.map +0 -1
  118. package/dist/WorkspacePicker/index.js +0 -2
  119. package/dist/WorkspacePicker/index.js.map +0 -1
  120. package/dist/hooks/index.d.ts +0 -2
  121. package/dist/hooks/index.d.ts.map +0 -1
  122. package/dist/hooks/index.js +0 -2
  123. package/dist/hooks/index.js.map +0 -1
  124. package/dist/hooks/useAutoSave.d.ts +0 -15
  125. package/dist/hooks/useAutoSave.d.ts.map +0 -1
  126. package/dist/hooks/useAutoSave.js +0 -83
  127. package/dist/hooks/useAutoSave.js.map +0 -1
  128. package/dist/icons.d.ts +0 -7
  129. package/dist/icons.d.ts.map +0 -1
  130. package/dist/icons.js +0 -27
  131. package/dist/icons.js.map +0 -1
  132. package/dist/index.d.ts.map +0 -1
  133. package/dist/monaco-slim.d.ts +0 -19
  134. package/dist/monaco-slim.d.ts.map +0 -1
  135. package/dist/monaco-slim.js +0 -19
  136. package/dist/monaco-slim.js.map +0 -1
  137. package/dist/preferences/versioning.d.ts +0 -27
  138. package/dist/preferences/versioning.d.ts.map +0 -1
  139. package/dist/preferences/versioning.js +0 -62
  140. package/dist/preferences/versioning.js.map +0 -1
  141. package/src/AppMenu/AppMenu.tsx +0 -264
  142. package/src/AppMenu/index.ts +0 -2
  143. package/src/DocBlocksShell/DocBlocksShell.tsx +0 -1778
  144. package/src/DocBlocksShell/index.ts +0 -2
  145. package/src/Export/ExportDialog.tsx +0 -383
  146. package/src/Export/ExportToolbarControls.tsx +0 -313
  147. package/src/Export/export-options.ts +0 -99
  148. package/src/Export/index.ts +0 -5
  149. package/src/Export/run-export.ts +0 -349
  150. package/src/Export/transform-summaries.ts +0 -14
  151. package/src/FileExplorer/FileExplorer.tsx +0 -282
  152. package/src/FileExplorer/FileTreeNode.tsx +0 -214
  153. package/src/FileExplorer/index.ts +0 -5
  154. package/src/FileExplorer/useFileTree.ts +0 -178
  155. package/src/WorkspacePicker/WorkspacePicker.tsx +0 -137
  156. package/src/WorkspacePicker/WorkspaceSettingsButton.tsx +0 -88
  157. package/src/WorkspacePicker/WorkspaceSettingsDialog.tsx +0 -121
  158. package/src/WorkspacePicker/index.ts +0 -2
  159. package/src/fonts/fonts.css +0 -362
  160. package/src/fonts/licenses/LICENSE-cormorant-garamond.txt +0 -93
  161. package/src/fonts/licenses/LICENSE-crimson-text.txt +0 -93
  162. package/src/fonts/licenses/LICENSE-dm-sans.txt +0 -93
  163. package/src/fonts/licenses/LICENSE-dm-serif-display.txt +0 -93
  164. package/src/fonts/licenses/LICENSE-hanken-grotesk.txt +0 -94
  165. package/src/fonts/licenses/LICENSE-ibm-plex-sans.txt +0 -93
  166. package/src/fonts/licenses/LICENSE-inter.txt +0 -93
  167. package/src/fonts/licenses/LICENSE-jetbrains-mono.txt +0 -93
  168. package/src/fonts/licenses/LICENSE-lora.txt +0 -93
  169. package/src/fonts/licenses/LICENSE-merriweather.txt +0 -93
  170. package/src/fonts/licenses/LICENSE-oswald.txt +0 -93
  171. package/src/fonts/licenses/LICENSE-playfair-display.txt +0 -93
  172. package/src/fonts/licenses/LICENSE-pt-serif.txt +0 -93
  173. package/src/fonts/licenses/LICENSE-roboto.txt +0 -93
  174. package/src/fonts/licenses/LICENSE-source-serif-4.txt +0 -93
  175. package/src/fonts/woff2/cormorant-garamond-400.woff2 +0 -0
  176. package/src/fonts/woff2/cormorant-garamond-700.woff2 +0 -0
  177. package/src/fonts/woff2/crimson-text-400.woff2 +0 -0
  178. package/src/fonts/woff2/crimson-text-400i.woff2 +0 -0
  179. package/src/fonts/woff2/crimson-text-700.woff2 +0 -0
  180. package/src/fonts/woff2/dm-sans-400.woff2 +0 -0
  181. package/src/fonts/woff2/dm-sans-400i.woff2 +0 -0
  182. package/src/fonts/woff2/dm-sans-500.woff2 +0 -0
  183. package/src/fonts/woff2/dm-sans-700.woff2 +0 -0
  184. package/src/fonts/woff2/dm-serif-display-400.woff2 +0 -0
  185. package/src/fonts/woff2/hanken-grotesk-400.woff2 +0 -0
  186. package/src/fonts/woff2/hanken-grotesk-400i.woff2 +0 -0
  187. package/src/fonts/woff2/hanken-grotesk-500.woff2 +0 -0
  188. package/src/fonts/woff2/hanken-grotesk-600.woff2 +0 -0
  189. package/src/fonts/woff2/hanken-grotesk-700.woff2 +0 -0
  190. package/src/fonts/woff2/ibm-plex-sans-400.woff2 +0 -0
  191. package/src/fonts/woff2/ibm-plex-sans-400i.woff2 +0 -0
  192. package/src/fonts/woff2/ibm-plex-sans-500.woff2 +0 -0
  193. package/src/fonts/woff2/ibm-plex-sans-600.woff2 +0 -0
  194. package/src/fonts/woff2/ibm-plex-sans-700.woff2 +0 -0
  195. package/src/fonts/woff2/inter-400.woff2 +0 -0
  196. package/src/fonts/woff2/inter-400i.woff2 +0 -0
  197. package/src/fonts/woff2/inter-500.woff2 +0 -0
  198. package/src/fonts/woff2/inter-600.woff2 +0 -0
  199. package/src/fonts/woff2/inter-700.woff2 +0 -0
  200. package/src/fonts/woff2/jetbrains-mono-400.woff2 +0 -0
  201. package/src/fonts/woff2/jetbrains-mono-400i.woff2 +0 -0
  202. package/src/fonts/woff2/jetbrains-mono-700.woff2 +0 -0
  203. package/src/fonts/woff2/lora-400.woff2 +0 -0
  204. package/src/fonts/woff2/lora-400i.woff2 +0 -0
  205. package/src/fonts/woff2/lora-700.woff2 +0 -0
  206. package/src/fonts/woff2/merriweather-400.woff2 +0 -0
  207. package/src/fonts/woff2/merriweather-700.woff2 +0 -0
  208. package/src/fonts/woff2/oswald-400.woff2 +0 -0
  209. package/src/fonts/woff2/oswald-700.woff2 +0 -0
  210. package/src/fonts/woff2/playfair-display-400.woff2 +0 -0
  211. package/src/fonts/woff2/playfair-display-700.woff2 +0 -0
  212. package/src/fonts/woff2/pt-serif-400.woff2 +0 -0
  213. package/src/fonts/woff2/pt-serif-700.woff2 +0 -0
  214. package/src/fonts/woff2/roboto-400.woff2 +0 -0
  215. package/src/fonts/woff2/roboto-400i.woff2 +0 -0
  216. package/src/fonts/woff2/roboto-700.woff2 +0 -0
  217. package/src/fonts/woff2/source-serif-4-400.woff2 +0 -0
  218. package/src/fonts/woff2/source-serif-4-400i.woff2 +0 -0
  219. package/src/fonts/woff2/source-serif-4-700.woff2 +0 -0
  220. package/src/hooks/index.ts +0 -1
  221. package/src/hooks/useAutoSave.ts +0 -116
  222. package/src/icons.tsx +0 -70
  223. package/src/index.ts +0 -31
  224. package/src/monaco-slim.ts +0 -20
  225. package/src/preferences/versioning.ts +0 -69
@@ -0,0 +1,97 @@
1
+ // src/Git/GitContext.tsx
2
+ import { createContext, useContext } from "react";
3
+ var GitContext = createContext(null);
4
+ function useGitContext() {
5
+ return useContext(GitContext);
6
+ }
7
+
8
+ // src/Git/git-status.ts
9
+ var BADGE_GLYPHS = {
10
+ modified: "M",
11
+ added: "A",
12
+ untracked: "U",
13
+ renamed: "R",
14
+ deleted: "D",
15
+ conflicted: "!",
16
+ dir: "\u2022"
17
+ };
18
+ var BADGE_LABELS = {
19
+ modified: "modified",
20
+ added: "added",
21
+ untracked: "untracked",
22
+ renamed: "renamed",
23
+ deleted: "deleted",
24
+ conflicted: "has conflicts",
25
+ dir: "contains changes"
26
+ };
27
+ function normalise(path) {
28
+ return path.startsWith("/") ? path : `/${path}`;
29
+ }
30
+ function isDotPath(path) {
31
+ return normalise(path).split("/").some((segment) => segment.startsWith("."));
32
+ }
33
+ function badgeKindFor(change) {
34
+ if (change.conflicted) return "conflicted";
35
+ if (change.worktree === "untracked") return "untracked";
36
+ if (change.index === "deleted" || change.worktree === "deleted") return "deleted";
37
+ if (change.index === "renamed" || change.worktree === "renamed") return "renamed";
38
+ if (change.index === "added" || change.worktree === "added") return "added";
39
+ return "modified";
40
+ }
41
+ function buildBadgeMap(changes) {
42
+ const badges = /* @__PURE__ */ new Map();
43
+ const dirKinds = /* @__PURE__ */ new Map();
44
+ for (const change of changes) {
45
+ if (isDotPath(change.path)) continue;
46
+ const filePath = normalise(change.path);
47
+ const kind = badgeKindFor(change);
48
+ if (kind !== "deleted") {
49
+ badges.set(filePath, kind);
50
+ }
51
+ const segments = filePath.split("/").slice(1, -1);
52
+ let dir = "";
53
+ for (const segment of segments) {
54
+ dir += `/${segment}`;
55
+ const rollup = kind === "conflicted" ? "conflicted" : "dir";
56
+ if (dirKinds.get(dir) !== "conflicted") dirKinds.set(dir, rollup);
57
+ }
58
+ }
59
+ for (const [dir, kind] of dirKinds) {
60
+ if (!badges.has(dir)) badges.set(dir, kind);
61
+ }
62
+ return badges;
63
+ }
64
+ function formatAheadBehind(status) {
65
+ const parts = [];
66
+ if (status.ahead > 0) parts.push(`${status.ahead}\u2191`);
67
+ if (status.behind > 0) parts.push(`${status.behind}\u2193`);
68
+ return parts.join(" ");
69
+ }
70
+ function isFileDirty(status, path) {
71
+ if (!status || !path) return false;
72
+ const target = normalise(path);
73
+ return status.changes.some((c) => normalise(c.path) === target);
74
+ }
75
+ function conflictedPaths(status) {
76
+ if (!status) return [];
77
+ return status.changes.filter((c) => c.conflicted).map((c) => normalise(c.path));
78
+ }
79
+ function summarizeDirty(status) {
80
+ const count = status?.changes.length ?? 0;
81
+ if (count === 0) return "";
82
+ return count === 1 ? "1 change" : `${count} changes`;
83
+ }
84
+
85
+ export {
86
+ GitContext,
87
+ useGitContext,
88
+ BADGE_GLYPHS,
89
+ BADGE_LABELS,
90
+ badgeKindFor,
91
+ buildBadgeMap,
92
+ formatAheadBehind,
93
+ isFileDirty,
94
+ conflictedPaths,
95
+ summarizeDirty
96
+ };
97
+ //# sourceMappingURL=chunk-5A4KEWKT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/Git/GitContext.tsx","../src/Git/git-status.ts"],"sourcesContent":["/**\n * GitContext — the seam between DocBlocksShell and every git UI surface.\n *\n * The shell calls useGit() once and provides the value here; the status\n * bar, file-tree badges, toolbar control, and dialogs all consume it.\n * Consumers must tolerate a null context (FileExplorer renders standalone\n * in tests and on surfaces with no git).\n */\n\nimport { createContext, useContext } from 'react';\nimport type {\n DocBlocksHostGitAPI,\n GitCapabilities,\n GitRepoDetection,\n GitStatus,\n} from '@bendyline/docblocks/host';\nimport type { FileSystemProvider } from '@bendyline/docblocks/filesystem';\nimport type { GitBadgeKind } from './git-status.js';\n\nexport type GitDialogState =\n | { kind: 'none' }\n | { kind: 'commit' }\n | { kind: 'branches'; createFocus?: boolean }\n | { kind: 'history'; path?: string }\n /** `revision` absent → working tree vs HEAD; present → that commit vs its parent. */\n | { kind: 'diff'; path: string; revision?: { sha: string } }\n | { kind: 'clone' }\n | {\n kind: 'auth-guidance';\n operation: 'push' | 'pull' | 'fetch' | 'clone' | 'pr';\n detail?: string;\n }\n | { kind: 'conflicts'; paths: string[] };\n\nexport type GitBusyKind = 'push' | 'pull' | 'fetch' | 'commit' | 'branch' | 'pr' | null;\n\nexport interface GitInlineResult {\n tone: 'info' | 'error';\n message: string;\n}\n\nexport interface GitValue {\n /** Tier 1 — Electron host with a working git. Gates the clone entry. */\n available: boolean;\n capabilities: GitCapabilities | null;\n /** Tier 2 — the active workspace root is a repository. Gates everything else. */\n repo: GitRepoDetection | null;\n /** Main-owned repository capability; never a native path. */\n repositoryId: string | null;\n /** Parsed web location of the first remote, when one exists and parses. */\n remoteWeb: { host: string; owner: string; repo: string; webUrl: string } | null;\n /** Raw host API for read-heavy dialogs (log, branches, file-at-revision). */\n gitApi: DocBlocksHostGitAPI | null;\n provider: FileSystemProvider | null;\n theme: 'light' | 'dark';\n\n status: GitStatus | null;\n badges: ReadonlyMap<string, GitBadgeKind>;\n busy: GitBusyKind;\n /** Latest operation outcome, surfaced in an aria-live region. */\n lastResult: GitInlineResult | null;\n\n refresh: () => void;\n /** Debounced refresh — used after autosave writes. */\n scheduleRefresh: () => void;\n\n /** Actions route their typed errors to dialogs/lastResult internally. */\n commit: (message: string, paths: string[]) => Promise<boolean>;\n push: () => Promise<void>;\n pull: () => Promise<void>;\n fetchRemote: () => Promise<void>;\n createBranch: (name: string) => Promise<boolean>;\n switchBranch: (name: string) => Promise<boolean>;\n /** Open the repo (or a file within it) on the remote's web host. */\n openOnRemote: (filePath?: string) => void;\n createPullRequest: () => Promise<void>;\n\n dialog: GitDialogState;\n openDialog: (dialog: GitDialogState) => void;\n closeDialog: () => void;\n}\n\nexport const GitContext = createContext<GitValue | null>(null);\n\n/** Null when no provider is mounted — callers must degrade gracefully. */\nexport function useGitContext(): GitValue | null {\n return useContext(GitContext);\n}\n","/**\n * Pure helpers mapping GitStatus onto UI concerns — file-tree badges,\n * ahead/behind chips, dirty summaries. No React, no host access.\n */\n\nimport type { GitFileChange, GitStatus } from '@bendyline/docblocks/host';\n\nexport type GitBadgeKind =\n | 'modified'\n | 'added'\n | 'untracked'\n | 'renamed'\n | 'deleted'\n | 'conflicted'\n | 'dir';\n\nexport const BADGE_GLYPHS: Record<GitBadgeKind, string> = {\n modified: 'M',\n added: 'A',\n untracked: 'U',\n renamed: 'R',\n deleted: 'D',\n conflicted: '!',\n dir: '•',\n};\n\nexport const BADGE_LABELS: Record<GitBadgeKind, string> = {\n modified: 'modified',\n added: 'added',\n untracked: 'untracked',\n renamed: 'renamed',\n deleted: 'deleted',\n conflicted: 'has conflicts',\n dir: 'contains changes',\n};\n\nfunction normalise(path: string): string {\n return path.startsWith('/') ? path : `/${path}`;\n}\n\n/** True when any path segment is a dot-entry (.git, .github, .gitignore…). */\nfunction isDotPath(path: string): boolean {\n return normalise(path)\n .split('/')\n .some((segment) => segment.startsWith('.'));\n}\n\nexport function badgeKindFor(change: GitFileChange): GitBadgeKind {\n if (change.conflicted) return 'conflicted';\n if (change.worktree === 'untracked') return 'untracked';\n if (change.index === 'deleted' || change.worktree === 'deleted') return 'deleted';\n if (change.index === 'renamed' || change.worktree === 'renamed') return 'renamed';\n if (change.index === 'added' || change.worktree === 'added') return 'added';\n return 'modified';\n}\n\n/**\n * Slash-prefixed path → badge kind, including directory rollups: any\n * ancestor directory of a changed file gets a 'dir' dot (or 'conflicted'\n * when a conflicted descendant wins). Deleted files aren't in the tree, so\n * they contribute only their ancestor rollup. Dot-paths are excluded so a\n * `.gitignore` edit doesn't inexplicably dot the root.\n */\nexport function buildBadgeMap(changes: readonly GitFileChange[]): Map<string, GitBadgeKind> {\n const badges = new Map<string, GitBadgeKind>();\n const dirKinds = new Map<string, GitBadgeKind>();\n\n for (const change of changes) {\n if (isDotPath(change.path)) continue;\n const filePath = normalise(change.path);\n const kind = badgeKindFor(change);\n if (kind !== 'deleted') {\n badges.set(filePath, kind);\n }\n const segments = filePath.split('/').slice(1, -1);\n let dir = '';\n for (const segment of segments) {\n dir += `/${segment}`;\n const rollup = kind === 'conflicted' ? 'conflicted' : 'dir';\n if (dirKinds.get(dir) !== 'conflicted') dirKinds.set(dir, rollup);\n }\n }\n for (const [dir, kind] of dirKinds) {\n if (!badges.has(dir)) badges.set(dir, kind);\n }\n return badges;\n}\n\n/** \"2↑ 1↓\" — empty string when neither applies. */\nexport function formatAheadBehind(status: Pick<GitStatus, 'ahead' | 'behind'>): string {\n const parts: string[] = [];\n if (status.ahead > 0) parts.push(`${status.ahead}↑`);\n if (status.behind > 0) parts.push(`${status.behind}↓`);\n return parts.join(' ');\n}\n\nexport function isFileDirty(status: GitStatus | null, path: string | null): boolean {\n if (!status || !path) return false;\n const target = normalise(path);\n return status.changes.some((c) => normalise(c.path) === target);\n}\n\nexport function conflictedPaths(status: GitStatus | null): string[] {\n if (!status) return [];\n return status.changes.filter((c) => c.conflicted).map((c) => normalise(c.path));\n}\n\n/** \"3 changes\" / \"1 change\" / \"\" when clean. */\nexport function summarizeDirty(status: GitStatus | null): string {\n const count = status?.changes.length ?? 0;\n if (count === 0) return '';\n return count === 1 ? '1 change' : `${count} changes`;\n}\n"],"mappings":";AASA,SAAS,eAAe,kBAAkB;AAyEnC,IAAM,aAAa,cAA+B,IAAI;AAGtD,SAAS,gBAAiC;AAC/C,SAAO,WAAW,UAAU;AAC9B;;;ACvEO,IAAM,eAA6C;AAAA,EACxD,UAAU;AAAA,EACV,OAAO;AAAA,EACP,WAAW;AAAA,EACX,SAAS;AAAA,EACT,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,KAAK;AACP;AAEO,IAAM,eAA6C;AAAA,EACxD,UAAU;AAAA,EACV,OAAO;AAAA,EACP,WAAW;AAAA,EACX,SAAS;AAAA,EACT,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,KAAK;AACP;AAEA,SAAS,UAAU,MAAsB;AACvC,SAAO,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI,IAAI;AAC/C;AAGA,SAAS,UAAU,MAAuB;AACxC,SAAO,UAAU,IAAI,EAClB,MAAM,GAAG,EACT,KAAK,CAAC,YAAY,QAAQ,WAAW,GAAG,CAAC;AAC9C;AAEO,SAAS,aAAa,QAAqC;AAChE,MAAI,OAAO,WAAY,QAAO;AAC9B,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,MAAI,OAAO,UAAU,aAAa,OAAO,aAAa,UAAW,QAAO;AACxE,MAAI,OAAO,UAAU,aAAa,OAAO,aAAa,UAAW,QAAO;AACxE,MAAI,OAAO,UAAU,WAAW,OAAO,aAAa,QAAS,QAAO;AACpE,SAAO;AACT;AASO,SAAS,cAAc,SAA8D;AAC1F,QAAM,SAAS,oBAAI,IAA0B;AAC7C,QAAM,WAAW,oBAAI,IAA0B;AAE/C,aAAW,UAAU,SAAS;AAC5B,QAAI,UAAU,OAAO,IAAI,EAAG;AAC5B,UAAM,WAAW,UAAU,OAAO,IAAI;AACtC,UAAM,OAAO,aAAa,MAAM;AAChC,QAAI,SAAS,WAAW;AACtB,aAAO,IAAI,UAAU,IAAI;AAAA,IAC3B;AACA,UAAM,WAAW,SAAS,MAAM,GAAG,EAAE,MAAM,GAAG,EAAE;AAChD,QAAI,MAAM;AACV,eAAW,WAAW,UAAU;AAC9B,aAAO,IAAI,OAAO;AAClB,YAAM,SAAS,SAAS,eAAe,eAAe;AACtD,UAAI,SAAS,IAAI,GAAG,MAAM,aAAc,UAAS,IAAI,KAAK,MAAM;AAAA,IAClE;AAAA,EACF;AACA,aAAW,CAAC,KAAK,IAAI,KAAK,UAAU;AAClC,QAAI,CAAC,OAAO,IAAI,GAAG,EAAG,QAAO,IAAI,KAAK,IAAI;AAAA,EAC5C;AACA,SAAO;AACT;AAGO,SAAS,kBAAkB,QAAqD;AACrF,QAAM,QAAkB,CAAC;AACzB,MAAI,OAAO,QAAQ,EAAG,OAAM,KAAK,GAAG,OAAO,KAAK,QAAG;AACnD,MAAI,OAAO,SAAS,EAAG,OAAM,KAAK,GAAG,OAAO,MAAM,QAAG;AACrD,SAAO,MAAM,KAAK,GAAG;AACvB;AAEO,SAAS,YAAY,QAA0B,MAA8B;AAClF,MAAI,CAAC,UAAU,CAAC,KAAM,QAAO;AAC7B,QAAM,SAAS,UAAU,IAAI;AAC7B,SAAO,OAAO,QAAQ,KAAK,CAAC,MAAM,UAAU,EAAE,IAAI,MAAM,MAAM;AAChE;AAEO,SAAS,gBAAgB,QAAoC;AAClE,MAAI,CAAC,OAAQ,QAAO,CAAC;AACrB,SAAO,OAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,UAAU,EAAE,IAAI,CAAC;AAChF;AAGO,SAAS,eAAe,QAAkC;AAC/D,QAAM,QAAQ,QAAQ,QAAQ,UAAU;AACxC,MAAI,UAAU,EAAG,QAAO;AACxB,SAAO,UAAU,IAAI,aAAa,GAAG,KAAK;AAC5C;","names":[]}
@@ -0,0 +1,51 @@
1
+ // src/Export/export-options.ts
2
+ var FORMAT_LABELS = {
3
+ docx: "Word Document (.docx)",
4
+ pdf: "PDF (.pdf)",
5
+ pptx: "PowerPoint (.pptx)",
6
+ md: "Markdown (.md)",
7
+ html: "HTML (.html)"
8
+ };
9
+ var FORMAT_EXTENSIONS = {
10
+ docx: ".docx",
11
+ pdf: ".pdf",
12
+ pptx: ".pptx",
13
+ md: ".md",
14
+ html: ".html"
15
+ };
16
+ var STORAGE_KEY = "docblocks-export-options";
17
+ var DEFAULT_OPTIONS = {
18
+ format: "pdf",
19
+ themeId: "standard",
20
+ transformStyle: "documentary",
21
+ pageSize: "letter",
22
+ htmlStyle: "plain",
23
+ htmlBundle: "single",
24
+ includeLinkedDocs: false,
25
+ entryAsIndex: false
26
+ };
27
+ function loadLastExportOptions() {
28
+ try {
29
+ const raw = localStorage.getItem(STORAGE_KEY);
30
+ if (!raw) return null;
31
+ const parsed = JSON.parse(raw);
32
+ return { ...DEFAULT_OPTIONS, ...parsed };
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ function saveExportOptions(options) {
38
+ try {
39
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(options));
40
+ } catch {
41
+ }
42
+ }
43
+
44
+ export {
45
+ FORMAT_LABELS,
46
+ FORMAT_EXTENSIONS,
47
+ DEFAULT_OPTIONS,
48
+ loadLastExportOptions,
49
+ saveExportOptions
50
+ };
51
+ //# sourceMappingURL=chunk-DD4VFJQI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/Export/export-options.ts"],"sourcesContent":["/**\n * Export option types and localStorage persistence for quick-export.\n */\n\nexport type ExportFormat = 'docx' | 'pdf' | 'pptx' | 'md' | 'html';\n\n/** Visual style for HTML export. */\nexport type HtmlStyle =\n /** Render via SquisqPlayer (themed, embeds player bundle). */\n | 'rendered'\n /** Plain semantic HTML — small, no JS, no playback. */\n | 'plain';\n\n/** Packaging for HTML export. */\nexport type HtmlBundle =\n /** Single self-contained .html file (images base64-embedded). */\n | 'single'\n /** ZIP containing index.html + asset files. */\n | 'zip';\n\nexport interface ExportOptions {\n format: ExportFormat;\n themeId: string;\n /** Transform style for PPTX — controls how content is segmented and styled. */\n transformStyle: string;\n /** Only applies to PDF */\n pageSize: 'letter' | 'a4';\n /** HTML rendering style. Only applies to format=html. */\n htmlStyle: HtmlStyle;\n /** HTML bundle layout. Only applies to format=html. */\n htmlBundle: HtmlBundle;\n /**\n * Recursive HTML bundle: walk relative `.md` links from the entry\n * document, render every reachable sibling/child page, and rewrite\n * cross-doc links from `.md` → `.html`. The output is a ZIP\n * regardless of `htmlBundle` because multi-doc output needs a\n * directory tree. Supported for both plain (`markdownDocsToPlainHtmlBundle`)\n * and rendered (`markdownDocsToHtmlBundle`) HTML styles. Only\n * meaningful when `format === 'html'`.\n */\n includeLinkedDocs: boolean;\n /**\n * Rename the entry page from `<basename>.html` to `index.html` so the\n * export drops straight into a static-site host that expects an\n * `index.html` landing page. For recursive bundles, cross-doc links\n * targeting the entry also rewrite to `index.html` so siblings don't\n * 404. For a single-doc HTML download, this just renames the file.\n * Only meaningful when `format === 'html'`.\n */\n entryAsIndex: boolean;\n}\n\nexport const FORMAT_LABELS: Record<ExportFormat, string> = {\n docx: 'Word Document (.docx)',\n pdf: 'PDF (.pdf)',\n pptx: 'PowerPoint (.pptx)',\n md: 'Markdown (.md)',\n html: 'HTML (.html)',\n};\n\nexport const FORMAT_EXTENSIONS: Record<ExportFormat, string> = {\n docx: '.docx',\n pdf: '.pdf',\n pptx: '.pptx',\n md: '.md',\n html: '.html',\n};\n\nconst STORAGE_KEY = 'docblocks-export-options';\n\nexport const DEFAULT_OPTIONS: ExportOptions = {\n format: 'pdf',\n themeId: 'standard',\n transformStyle: 'documentary',\n pageSize: 'letter',\n htmlStyle: 'plain',\n htmlBundle: 'single',\n includeLinkedDocs: false,\n entryAsIndex: false,\n};\n\nexport function loadLastExportOptions(): ExportOptions | null {\n try {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (!raw) return null;\n const parsed = JSON.parse(raw) as Partial<ExportOptions>;\n return { ...DEFAULT_OPTIONS, ...parsed };\n } catch {\n return null;\n }\n}\n\nexport function saveExportOptions(options: ExportOptions): void {\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(options));\n } catch {\n // Ignore storage errors\n }\n}\n"],"mappings":";AAoDO,IAAM,gBAA8C;AAAA,EACzD,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,IAAI;AAAA,EACJ,MAAM;AACR;AAEO,IAAM,oBAAkD;AAAA,EAC7D,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,IAAI;AAAA,EACJ,MAAM;AACR;AAEA,IAAM,cAAc;AAEb,IAAM,kBAAiC;AAAA,EAC5C,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,UAAU;AAAA,EACV,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,mBAAmB;AAAA,EACnB,cAAc;AAChB;AAEO,SAAS,wBAA8C;AAC5D,MAAI;AACF,UAAM,MAAM,aAAa,QAAQ,WAAW;AAC5C,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,WAAO,EAAE,GAAG,iBAAiB,GAAG,OAAO;AAAA,EACzC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,kBAAkB,SAA8B;AAC9D,MAAI;AACF,iBAAa,QAAQ,aAAa,KAAK,UAAU,OAAO,CAAC;AAAA,EAC3D,QAAQ;AAAA,EAER;AACF;","names":[]}
@@ -0,0 +1,256 @@
1
+ import {
2
+ Dialog
3
+ } from "./chunk-FMGTHSTX.js";
4
+
5
+ // src/preferences/theme.ts
6
+ var ACCENT_COLORS = [
7
+ "brown",
8
+ "green",
9
+ "blue",
10
+ "purple",
11
+ "maroon",
12
+ "orange",
13
+ "gray"
14
+ ];
15
+ var DB_CHROME_COLORS = {
16
+ light: "#f3eede",
17
+ dark: "#262219"
18
+ };
19
+ var THEME_STORAGE_KEY = "docblocks:themePreference";
20
+ var ACCENT_STORAGE_KEY = "docblocks:accentColor";
21
+ function loadThemePreference() {
22
+ try {
23
+ const raw = localStorage.getItem(THEME_STORAGE_KEY);
24
+ if (raw === "light" || raw === "dark" || raw === "auto") return raw;
25
+ } catch {
26
+ }
27
+ return "auto";
28
+ }
29
+ function saveThemePreference(value) {
30
+ try {
31
+ localStorage.setItem(THEME_STORAGE_KEY, value);
32
+ } catch {
33
+ }
34
+ }
35
+ function loadAccentColor() {
36
+ try {
37
+ const raw = localStorage.getItem(ACCENT_STORAGE_KEY);
38
+ if (ACCENT_COLORS.some((color) => color === raw)) return raw;
39
+ } catch {
40
+ }
41
+ return "brown";
42
+ }
43
+ function saveAccentColor(value) {
44
+ try {
45
+ localStorage.setItem(ACCENT_STORAGE_KEY, value);
46
+ } catch {
47
+ }
48
+ }
49
+
50
+ // src/preferences/write-canvas.ts
51
+ var WRITE_CANVAS_TEXT_SIZE_MIN = 12;
52
+ var WRITE_CANVAS_TEXT_SIZE_MAX = 32;
53
+ var WRITE_CANVAS_LINE_SPACING_MIN = 1.2;
54
+ var WRITE_CANVAS_LINE_SPACING_MAX = 2.4;
55
+ var DEFAULT_WRITE_CANVAS_PREFERENCES = {
56
+ textSize: 16,
57
+ lineSpacing: 1.7
58
+ };
59
+ var WRITE_CANVAS_STORAGE_KEY = "docblocks:writeCanvasSettings";
60
+ function loadWriteCanvasPreferences() {
61
+ try {
62
+ const raw = localStorage.getItem(WRITE_CANVAS_STORAGE_KEY);
63
+ if (!raw) return { ...DEFAULT_WRITE_CANVAS_PREFERENCES };
64
+ const stored = JSON.parse(raw);
65
+ if (!isRecord(stored)) return { ...DEFAULT_WRITE_CANVAS_PREFERENCES };
66
+ return {
67
+ textSize: numberInRange(
68
+ stored.textSize,
69
+ WRITE_CANVAS_TEXT_SIZE_MIN,
70
+ WRITE_CANVAS_TEXT_SIZE_MAX
71
+ ) ? stored.textSize : DEFAULT_WRITE_CANVAS_PREFERENCES.textSize,
72
+ lineSpacing: numberInRange(
73
+ stored.lineSpacing,
74
+ WRITE_CANVAS_LINE_SPACING_MIN,
75
+ WRITE_CANVAS_LINE_SPACING_MAX
76
+ ) ? stored.lineSpacing : DEFAULT_WRITE_CANVAS_PREFERENCES.lineSpacing
77
+ };
78
+ } catch {
79
+ return { ...DEFAULT_WRITE_CANVAS_PREFERENCES };
80
+ }
81
+ }
82
+ function saveWriteCanvasPreferences(value) {
83
+ try {
84
+ localStorage.setItem(WRITE_CANVAS_STORAGE_KEY, JSON.stringify(value));
85
+ } catch {
86
+ }
87
+ }
88
+ function numberInRange(value, min, max) {
89
+ return typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
90
+ }
91
+ function isRecord(value) {
92
+ return typeof value === "object" && value !== null && !Array.isArray(value);
93
+ }
94
+
95
+ // src/Settings/Settings.tsx
96
+ import { jsx, jsxs } from "react/jsx-runtime";
97
+ var ACCENT_LABELS = {
98
+ brown: "Brown",
99
+ green: "Green",
100
+ blue: "Blue",
101
+ purple: "Purple",
102
+ maroon: "Maroon",
103
+ orange: "Orange",
104
+ gray: "Gray"
105
+ };
106
+ function SettingsDialog({ title = "Settings", onClose, children }) {
107
+ return /* @__PURE__ */ jsx(Dialog, { title, onClose, size: "wide", children });
108
+ }
109
+ function ThemeSettings({ value, onChange, name = "theme" }) {
110
+ return /* @__PURE__ */ jsxs("fieldset", { className: "db-settings-fieldset", children: [
111
+ /* @__PURE__ */ jsx("legend", { className: "db-settings-legend", children: "Theme" }),
112
+ /* @__PURE__ */ jsxs("label", { className: "db-settings-radio", children: [
113
+ /* @__PURE__ */ jsx(
114
+ "input",
115
+ {
116
+ type: "radio",
117
+ name,
118
+ value: "auto",
119
+ checked: value === "auto",
120
+ onChange: () => onChange("auto")
121
+ }
122
+ ),
123
+ "System default"
124
+ ] }),
125
+ /* @__PURE__ */ jsxs("label", { className: "db-settings-radio", children: [
126
+ /* @__PURE__ */ jsx(
127
+ "input",
128
+ {
129
+ type: "radio",
130
+ name,
131
+ value: "light",
132
+ checked: value === "light",
133
+ onChange: () => onChange("light")
134
+ }
135
+ ),
136
+ "Light"
137
+ ] }),
138
+ /* @__PURE__ */ jsxs("label", { className: "db-settings-radio", children: [
139
+ /* @__PURE__ */ jsx(
140
+ "input",
141
+ {
142
+ type: "radio",
143
+ name,
144
+ value: "dark",
145
+ checked: value === "dark",
146
+ onChange: () => onChange("dark")
147
+ }
148
+ ),
149
+ "Dark"
150
+ ] })
151
+ ] });
152
+ }
153
+ function AccentColorSettings({
154
+ value,
155
+ onChange,
156
+ name = "accent-color"
157
+ }) {
158
+ return /* @__PURE__ */ jsxs("fieldset", { className: "db-settings-fieldset", children: [
159
+ /* @__PURE__ */ jsx("legend", { className: "db-settings-legend", children: "Accent color" }),
160
+ /* @__PURE__ */ jsx("p", { className: "db-settings-hint", children: "Used in both light and dark appearances." }),
161
+ /* @__PURE__ */ jsx("div", { className: "db-settings-accent-grid", children: ACCENT_COLORS.map((color) => /* @__PURE__ */ jsxs(
162
+ "label",
163
+ {
164
+ className: `db-settings-accent${value === color ? " db-settings-accent--selected" : ""}`,
165
+ children: [
166
+ /* @__PURE__ */ jsx(
167
+ "input",
168
+ {
169
+ className: "db-settings-accent-input",
170
+ type: "radio",
171
+ name,
172
+ value: color,
173
+ checked: value === color,
174
+ onChange: () => onChange(color)
175
+ }
176
+ ),
177
+ /* @__PURE__ */ jsx(
178
+ "span",
179
+ {
180
+ className: `db-settings-accent-swatch db-settings-accent-swatch--${color}`,
181
+ "aria-hidden": "true"
182
+ }
183
+ ),
184
+ /* @__PURE__ */ jsx("span", { children: ACCENT_LABELS[color] })
185
+ ]
186
+ },
187
+ color
188
+ )) })
189
+ ] });
190
+ }
191
+ function WriteCanvasSettingsControls({ value, onChange }) {
192
+ return /* @__PURE__ */ jsxs("fieldset", { className: "db-settings-fieldset", children: [
193
+ /* @__PURE__ */ jsx("legend", { className: "db-settings-legend", children: "Write canvas" }),
194
+ /* @__PURE__ */ jsx("p", { className: "db-settings-hint", children: "Adjust the writing view without changing the document or its exported text." }),
195
+ /* @__PURE__ */ jsxs("label", { className: "db-settings-slider", children: [
196
+ /* @__PURE__ */ jsxs("span", { className: "db-settings-slider-header", children: [
197
+ /* @__PURE__ */ jsx("span", { children: "Text size" }),
198
+ /* @__PURE__ */ jsxs("output", { className: "db-settings-slider-value", "aria-hidden": "true", children: [
199
+ value.textSize,
200
+ "px"
201
+ ] })
202
+ ] }),
203
+ /* @__PURE__ */ jsx(
204
+ "input",
205
+ {
206
+ type: "range",
207
+ min: WRITE_CANVAS_TEXT_SIZE_MIN,
208
+ max: WRITE_CANVAS_TEXT_SIZE_MAX,
209
+ step: 1,
210
+ value: value.textSize,
211
+ "aria-label": "Text size",
212
+ "aria-valuetext": `${value.textSize} pixels`,
213
+ onChange: (event) => onChange({ ...value, textSize: Number(event.currentTarget.value) })
214
+ }
215
+ )
216
+ ] }),
217
+ /* @__PURE__ */ jsxs("label", { className: "db-settings-slider", children: [
218
+ /* @__PURE__ */ jsxs("span", { className: "db-settings-slider-header", children: [
219
+ /* @__PURE__ */ jsx("span", { children: "Line spacing" }),
220
+ /* @__PURE__ */ jsx("output", { className: "db-settings-slider-value", "aria-hidden": "true", children: formatLineSpacing(value.lineSpacing) })
221
+ ] }),
222
+ /* @__PURE__ */ jsx(
223
+ "input",
224
+ {
225
+ type: "range",
226
+ min: WRITE_CANVAS_LINE_SPACING_MIN,
227
+ max: WRITE_CANVAS_LINE_SPACING_MAX,
228
+ step: 0.1,
229
+ value: value.lineSpacing,
230
+ "aria-label": "Line spacing",
231
+ "aria-valuetext": `${value.lineSpacing} times`,
232
+ onChange: (event) => onChange({ ...value, lineSpacing: Number(event.currentTarget.value) })
233
+ }
234
+ )
235
+ ] })
236
+ ] });
237
+ }
238
+ function formatLineSpacing(value) {
239
+ return `${value.toFixed(1).replace(/\.0$/, "")}\xD7`;
240
+ }
241
+
242
+ export {
243
+ DEFAULT_WRITE_CANVAS_PREFERENCES,
244
+ loadWriteCanvasPreferences,
245
+ saveWriteCanvasPreferences,
246
+ DB_CHROME_COLORS,
247
+ loadThemePreference,
248
+ saveThemePreference,
249
+ loadAccentColor,
250
+ saveAccentColor,
251
+ SettingsDialog,
252
+ ThemeSettings,
253
+ AccentColorSettings,
254
+ WriteCanvasSettingsControls
255
+ };
256
+ //# sourceMappingURL=chunk-FEWQUQML.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/preferences/theme.ts","../src/preferences/write-canvas.ts","../src/Settings/Settings.tsx"],"sourcesContent":["/** Appearance preferences shared by the shell and settings UI. */\n\nexport type ThemePreference = 'auto' | 'light' | 'dark';\n\nexport const ACCENT_COLORS = [\n 'brown',\n 'green',\n 'blue',\n 'purple',\n 'maroon',\n 'orange',\n 'gray',\n] as const;\n\nexport type AccentColor = (typeof ACCENT_COLORS)[number];\n\n/**\n * Chrome (titlebar) background per resolved theme. Keep in sync with\n * `--db-bg-subtle` for the default accent in styles/docblocks.css and with\n * the static `<meta name=\"theme-color\">` values in the site's index.html.\n * The shell writes these into the theme-color metas so the installed web\n * app's titlebar (Window Controls Overlay caption area, Android status bar)\n * follows the resolved theme.\n */\nexport const DB_CHROME_COLORS: Record<'light' | 'dark', string> = {\n light: '#f3eede',\n dark: '#262219',\n};\n\nconst THEME_STORAGE_KEY = 'docblocks:themePreference';\nconst ACCENT_STORAGE_KEY = 'docblocks:accentColor';\n\nexport function loadThemePreference(): ThemePreference {\n try {\n const raw = localStorage.getItem(THEME_STORAGE_KEY);\n if (raw === 'light' || raw === 'dark' || raw === 'auto') return raw;\n } catch {\n // ignore unavailable storage\n }\n return 'auto';\n}\n\nexport function saveThemePreference(value: ThemePreference): void {\n try {\n localStorage.setItem(THEME_STORAGE_KEY, value);\n } catch {\n // ignore quota errors\n }\n}\n\nexport function loadAccentColor(): AccentColor {\n try {\n const raw = localStorage.getItem(ACCENT_STORAGE_KEY);\n if (ACCENT_COLORS.some((color) => color === raw)) return raw as AccentColor;\n } catch {\n // ignore unavailable storage\n }\n return 'brown';\n}\n\nexport function saveAccentColor(value: AccentColor): void {\n try {\n localStorage.setItem(ACCENT_STORAGE_KEY, value);\n } catch {\n // ignore quota errors\n }\n}\n","import type { WriteCanvasSettings } from '@bendyline/squisq-editor-react';\n\nexport type WriteCanvasPreferences = Required<WriteCanvasSettings>;\n\nexport const WRITE_CANVAS_TEXT_SIZE_MIN = 12;\nexport const WRITE_CANVAS_TEXT_SIZE_MAX = 32;\nexport const WRITE_CANVAS_LINE_SPACING_MIN = 1.2;\nexport const WRITE_CANVAS_LINE_SPACING_MAX = 2.4;\n\nexport const DEFAULT_WRITE_CANVAS_PREFERENCES: Readonly<WriteCanvasPreferences> = {\n textSize: 16,\n lineSpacing: 1.7,\n};\n\nconst WRITE_CANVAS_STORAGE_KEY = 'docblocks:writeCanvasSettings';\n\nexport function loadWriteCanvasPreferences(): WriteCanvasPreferences {\n try {\n const raw = localStorage.getItem(WRITE_CANVAS_STORAGE_KEY);\n if (!raw) return { ...DEFAULT_WRITE_CANVAS_PREFERENCES };\n\n const stored: unknown = JSON.parse(raw);\n if (!isRecord(stored)) return { ...DEFAULT_WRITE_CANVAS_PREFERENCES };\n\n return {\n textSize: numberInRange(\n stored.textSize,\n WRITE_CANVAS_TEXT_SIZE_MIN,\n WRITE_CANVAS_TEXT_SIZE_MAX,\n )\n ? stored.textSize\n : DEFAULT_WRITE_CANVAS_PREFERENCES.textSize,\n lineSpacing: numberInRange(\n stored.lineSpacing,\n WRITE_CANVAS_LINE_SPACING_MIN,\n WRITE_CANVAS_LINE_SPACING_MAX,\n )\n ? stored.lineSpacing\n : DEFAULT_WRITE_CANVAS_PREFERENCES.lineSpacing,\n };\n } catch {\n return { ...DEFAULT_WRITE_CANVAS_PREFERENCES };\n }\n}\n\nexport function saveWriteCanvasPreferences(value: WriteCanvasPreferences): void {\n try {\n localStorage.setItem(WRITE_CANVAS_STORAGE_KEY, JSON.stringify(value));\n } catch {\n // Ignore unavailable storage and quota errors; the in-memory choice still works.\n }\n}\n\nfunction numberInRange(value: unknown, min: number, max: number): value is number {\n return typeof value === 'number' && Number.isFinite(value) && value >= min && value <= max;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n","/** Shared controls used by the app and host-specific DocBlocks settings dialogs. */\n\nimport type { ReactNode } from 'react';\nimport { Dialog } from '../components/Dialog.js';\nimport { ACCENT_COLORS, type AccentColor, type ThemePreference } from '../preferences/theme.js';\nimport {\n WRITE_CANVAS_LINE_SPACING_MAX,\n WRITE_CANVAS_LINE_SPACING_MIN,\n WRITE_CANVAS_TEXT_SIZE_MAX,\n WRITE_CANVAS_TEXT_SIZE_MIN,\n type WriteCanvasPreferences,\n} from '../preferences/write-canvas.js';\n\nexport type { AccentColor, ThemePreference } from '../preferences/theme.js';\n\nconst ACCENT_LABELS: Record<AccentColor, string> = {\n brown: 'Brown',\n green: 'Green',\n blue: 'Blue',\n purple: 'Purple',\n maroon: 'Maroon',\n orange: 'Orange',\n gray: 'Gray',\n};\n\nexport interface SettingsDialogProps {\n title?: string;\n onClose: () => void;\n children: ReactNode;\n}\n\nexport function SettingsDialog({ title = 'Settings', onClose, children }: SettingsDialogProps) {\n return (\n <Dialog title={title} onClose={onClose} size=\"wide\">\n {children}\n </Dialog>\n );\n}\n\nexport interface ThemeSettingsProps {\n value: ThemePreference;\n onChange: (theme: ThemePreference) => void;\n name?: string;\n}\n\nexport function ThemeSettings({ value, onChange, name = 'theme' }: ThemeSettingsProps) {\n return (\n <fieldset className=\"db-settings-fieldset\">\n <legend className=\"db-settings-legend\">Theme</legend>\n <label className=\"db-settings-radio\">\n <input\n type=\"radio\"\n name={name}\n value=\"auto\"\n checked={value === 'auto'}\n onChange={() => onChange('auto')}\n />\n System default\n </label>\n <label className=\"db-settings-radio\">\n <input\n type=\"radio\"\n name={name}\n value=\"light\"\n checked={value === 'light'}\n onChange={() => onChange('light')}\n />\n Light\n </label>\n <label className=\"db-settings-radio\">\n <input\n type=\"radio\"\n name={name}\n value=\"dark\"\n checked={value === 'dark'}\n onChange={() => onChange('dark')}\n />\n Dark\n </label>\n </fieldset>\n );\n}\n\nexport interface AccentColorSettingsProps {\n value: AccentColor;\n onChange: (color: AccentColor) => void;\n name?: string;\n}\n\nexport function AccentColorSettings({\n value,\n onChange,\n name = 'accent-color',\n}: AccentColorSettingsProps) {\n return (\n <fieldset className=\"db-settings-fieldset\">\n <legend className=\"db-settings-legend\">Accent color</legend>\n <p className=\"db-settings-hint\">Used in both light and dark appearances.</p>\n <div className=\"db-settings-accent-grid\">\n {ACCENT_COLORS.map((color) => (\n <label\n className={`db-settings-accent${\n value === color ? ' db-settings-accent--selected' : ''\n }`}\n key={color}\n >\n <input\n className=\"db-settings-accent-input\"\n type=\"radio\"\n name={name}\n value={color}\n checked={value === color}\n onChange={() => onChange(color)}\n />\n <span\n className={`db-settings-accent-swatch db-settings-accent-swatch--${color}`}\n aria-hidden=\"true\"\n />\n <span>{ACCENT_LABELS[color]}</span>\n </label>\n ))}\n </div>\n </fieldset>\n );\n}\n\nexport interface WriteCanvasSettingsControlsProps {\n value: WriteCanvasPreferences;\n onChange: (settings: WriteCanvasPreferences) => void;\n}\n\nexport function WriteCanvasSettingsControls({ value, onChange }: WriteCanvasSettingsControlsProps) {\n return (\n <fieldset className=\"db-settings-fieldset\">\n <legend className=\"db-settings-legend\">Write canvas</legend>\n <p className=\"db-settings-hint\">\n Adjust the writing view without changing the document or its exported text.\n </p>\n <label className=\"db-settings-slider\">\n <span className=\"db-settings-slider-header\">\n <span>Text size</span>\n <output className=\"db-settings-slider-value\" aria-hidden=\"true\">\n {value.textSize}px\n </output>\n </span>\n <input\n type=\"range\"\n min={WRITE_CANVAS_TEXT_SIZE_MIN}\n max={WRITE_CANVAS_TEXT_SIZE_MAX}\n step={1}\n value={value.textSize}\n aria-label=\"Text size\"\n aria-valuetext={`${value.textSize} pixels`}\n onChange={(event) => onChange({ ...value, textSize: Number(event.currentTarget.value) })}\n />\n </label>\n <label className=\"db-settings-slider\">\n <span className=\"db-settings-slider-header\">\n <span>Line spacing</span>\n <output className=\"db-settings-slider-value\" aria-hidden=\"true\">\n {formatLineSpacing(value.lineSpacing)}\n </output>\n </span>\n <input\n type=\"range\"\n min={WRITE_CANVAS_LINE_SPACING_MIN}\n max={WRITE_CANVAS_LINE_SPACING_MAX}\n step={0.1}\n value={value.lineSpacing}\n aria-label=\"Line spacing\"\n aria-valuetext={`${value.lineSpacing} times`}\n onChange={(event) =>\n onChange({ ...value, lineSpacing: Number(event.currentTarget.value) })\n }\n />\n </label>\n </fieldset>\n );\n}\n\nfunction formatLineSpacing(value: number): string {\n return `${value.toFixed(1).replace(/\\.0$/, '')}\\u00d7`;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,110 @@
1
+ // src/components/Dialog.tsx
2
+ import { useCallback, useEffect, useId, useRef } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ function Dialog({
5
+ title,
6
+ onClose,
7
+ size = "default",
8
+ children,
9
+ footer,
10
+ initialFocusRef,
11
+ closeOnBackdrop = true,
12
+ className,
13
+ bodyClassName,
14
+ footerClassName
15
+ }) {
16
+ const closeButtonRef = useRef(null);
17
+ const dialogRef = useRef(null);
18
+ const titleId = useId();
19
+ useEffect(() => {
20
+ const onKey = (e) => {
21
+ if (e.key === "Escape") {
22
+ e.preventDefault();
23
+ onClose();
24
+ return;
25
+ }
26
+ if (e.key !== "Tab") return;
27
+ const dialog = dialogRef.current;
28
+ if (!dialog) return;
29
+ const focusable = [...dialog.querySelectorAll(FOCUSABLE_SELECTOR)].filter(
30
+ (element) => !element.hasAttribute("disabled") && element.getAttribute("aria-hidden") !== "true"
31
+ );
32
+ if (focusable.length === 0) {
33
+ e.preventDefault();
34
+ dialog.focus();
35
+ return;
36
+ }
37
+ const first = focusable[0];
38
+ const last = focusable[focusable.length - 1];
39
+ if (e.shiftKey && document.activeElement === first) {
40
+ e.preventDefault();
41
+ last.focus();
42
+ } else if (!e.shiftKey && document.activeElement === last) {
43
+ e.preventDefault();
44
+ first.focus();
45
+ }
46
+ };
47
+ window.addEventListener("keydown", onKey);
48
+ return () => window.removeEventListener("keydown", onKey);
49
+ }, [onClose]);
50
+ useEffect(() => {
51
+ const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
52
+ const target = initialFocusRef?.current ?? closeButtonRef.current;
53
+ target?.focus();
54
+ return () => {
55
+ if (previousFocus?.isConnected) previousFocus.focus();
56
+ };
57
+ }, []);
58
+ const handleBackdropClick = useCallback(
59
+ (e) => {
60
+ if (closeOnBackdrop && e.target === e.currentTarget) onClose();
61
+ },
62
+ [closeOnBackdrop, onClose]
63
+ );
64
+ const sizeClass = size === "wide" ? "db-dialog--wide" : size === "full" ? "db-dialog--full" : "";
65
+ const panelClassName = ["db-dialog", sizeClass, className].filter(Boolean).join(" ");
66
+ const bodyClasses = ["db-dialog-body", bodyClassName].filter(Boolean).join(" ");
67
+ const footerClasses = ["db-dialog-footer", footerClassName].filter(Boolean).join(" ");
68
+ return /* @__PURE__ */ jsx("div", { className: "db-dialog-overlay", onClick: handleBackdropClick, children: /* @__PURE__ */ jsxs(
69
+ "div",
70
+ {
71
+ ref: dialogRef,
72
+ className: panelClassName,
73
+ role: "dialog",
74
+ "aria-modal": "true",
75
+ "aria-labelledby": titleId,
76
+ tabIndex: -1,
77
+ children: [
78
+ /* @__PURE__ */ jsxs("div", { className: "db-dialog-header", children: [
79
+ /* @__PURE__ */ jsx("h2", { id: titleId, className: "db-dialog-title", children: title }),
80
+ /* @__PURE__ */ jsx(
81
+ "button",
82
+ {
83
+ ref: closeButtonRef,
84
+ type: "button",
85
+ className: "db-dialog-close",
86
+ onClick: onClose,
87
+ "aria-label": "Close",
88
+ children: "\xD7"
89
+ }
90
+ )
91
+ ] }),
92
+ /* @__PURE__ */ jsx("div", { className: bodyClasses, children }),
93
+ footer && /* @__PURE__ */ jsx("div", { className: footerClasses, children: footer })
94
+ ]
95
+ }
96
+ ) });
97
+ }
98
+ var FOCUSABLE_SELECTOR = [
99
+ "a[href]",
100
+ "button",
101
+ "input",
102
+ "select",
103
+ "textarea",
104
+ '[tabindex]:not([tabindex="-1"])'
105
+ ].join(",");
106
+
107
+ export {
108
+ Dialog
109
+ };
110
+ //# sourceMappingURL=chunk-FMGTHSTX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/Dialog.tsx"],"sourcesContent":["/**\n * Dialog — shared modal primitive.\n *\n * Promotes the `db-dialog-overlay` / `db-dialog` pattern used by the\n * settings dialogs into a reusable component, adding aria-modal and\n * initial focus. Escape and (by default) backdrop clicks close it.\n */\n\nimport React, { useCallback, useEffect, useId, useRef } from 'react';\n\nexport interface DialogProps {\n title: string;\n onClose: () => void;\n /** default 380px / wide 560px / full min(1100px, 94vw) column. */\n size?: 'default' | 'wide' | 'full';\n children: React.ReactNode;\n /** Right-aligned action row below the body. */\n footer?: React.ReactNode;\n /** Element to focus on open; defaults to the close button. */\n initialFocusRef?: React.RefObject<HTMLElement | null>;\n /** Set false when a stray backdrop click would lose user input. */\n closeOnBackdrop?: boolean;\n /** Additional class on the dialog panel. */\n className?: string;\n /** Additional class on the dialog body. */\n bodyClassName?: string;\n /** Additional class on the footer wrapper. */\n footerClassName?: string;\n}\n\nexport function Dialog({\n title,\n onClose,\n size = 'default',\n children,\n footer,\n initialFocusRef,\n closeOnBackdrop = true,\n className,\n bodyClassName,\n footerClassName,\n}: DialogProps) {\n const closeButtonRef = useRef<HTMLButtonElement>(null);\n const dialogRef = useRef<HTMLDivElement>(null);\n const titleId = useId();\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n onClose();\n return;\n }\n if (e.key !== 'Tab') return;\n\n const dialog = dialogRef.current;\n if (!dialog) return;\n const focusable = [...dialog.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)].filter(\n (element) =>\n !element.hasAttribute('disabled') && element.getAttribute('aria-hidden') !== 'true',\n );\n if (focusable.length === 0) {\n e.preventDefault();\n dialog.focus();\n return;\n }\n\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n if (e.shiftKey && document.activeElement === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n };\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [onClose]);\n\n useEffect(() => {\n const previousFocus =\n document.activeElement instanceof HTMLElement ? document.activeElement : null;\n const target = initialFocusRef?.current ?? closeButtonRef.current;\n target?.focus();\n return () => {\n if (previousFocus?.isConnected) previousFocus.focus();\n };\n // Focus only on mount — refocusing on every re-render would fight the user.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const handleBackdropClick = useCallback(\n (e: React.MouseEvent) => {\n if (closeOnBackdrop && e.target === e.currentTarget) onClose();\n },\n [closeOnBackdrop, onClose],\n );\n\n const sizeClass = size === 'wide' ? 'db-dialog--wide' : size === 'full' ? 'db-dialog--full' : '';\n const panelClassName = ['db-dialog', sizeClass, className].filter(Boolean).join(' ');\n const bodyClasses = ['db-dialog-body', bodyClassName].filter(Boolean).join(' ');\n const footerClasses = ['db-dialog-footer', footerClassName].filter(Boolean).join(' ');\n\n return (\n <div className=\"db-dialog-overlay\" onClick={handleBackdropClick}>\n <div\n ref={dialogRef}\n className={panelClassName}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={titleId}\n tabIndex={-1}\n >\n <div className=\"db-dialog-header\">\n <h2 id={titleId} className=\"db-dialog-title\">\n {title}\n </h2>\n <button\n ref={closeButtonRef}\n type=\"button\"\n className=\"db-dialog-close\"\n onClick={onClose}\n aria-label=\"Close\"\n >\n &times;\n </button>\n </div>\n <div className={bodyClasses}>{children}</div>\n {footer && <div className={footerClasses}>{footer}</div>}\n </div>\n </div>\n );\n}\n\nconst FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'button',\n 'input',\n 'select',\n 'textarea',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n"],"mappings":"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","names":[]}