mosage 0.1.0 → 0.8.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 (146) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +16 -217
  3. package/bin.js +2 -0
  4. package/dist/build-C7NW_3Pk.js +14 -0
  5. package/dist/check-CP4873Wx.js +41 -0
  6. package/dist/cli/bin.d.ts +1 -0
  7. package/dist/cli/bin.js +228 -0
  8. package/dist/config-DPm1BBAb.js +2619 -0
  9. package/dist/config-TlTe7Ona.d.ts +24 -0
  10. package/dist/context-BqsdSrAQ.js +1084 -0
  11. package/dist/dev-Biz42qlu.js +17 -0
  12. package/dist/diagram-xlVDekYk.js +763 -0
  13. package/dist/export-Bi6nuxjT.js +31 -0
  14. package/dist/import-D2jNB07F.js +25 -0
  15. package/dist/index.d.ts +455 -0
  16. package/dist/index.js +693 -0
  17. package/dist/init-Bbtj2pxF.js +262 -0
  18. package/dist/preview-CLm51aRt.js +19 -0
  19. package/dist/sdk-DjpX6mCv.js +51 -0
  20. package/dist/vite/index.d.ts +25 -0
  21. package/dist/vite/index.js +2 -0
  22. package/env.d.ts +83 -0
  23. package/package.json +84 -64
  24. package/skills/apply-comments/SKILL.md +43 -56
  25. package/skills/create-doc/SKILL.md +106 -0
  26. package/skills/create-theme/SKILL.md +184 -0
  27. package/skills/current-doc/SKILL.md +120 -0
  28. package/skills/doc-authoring/SKILL.md +434 -0
  29. package/skills/doc-authoring/references/assets.md +47 -0
  30. package/skills/doc-authoring/references/design-system.md +81 -0
  31. package/skills/doc-authoring/references/long-form.md +131 -0
  32. package/skills/doc-authoring/references/pagination.md +118 -0
  33. package/skills/doc-authoring/references/tables-and-charts.md +161 -0
  34. package/src/app/app.tsx +42 -0
  35. package/src/app/components/data-table.tsx +196 -0
  36. package/src/app/components/design-panel/design-panel.tsx +318 -0
  37. package/src/app/components/design-panel/design-provider.tsx +121 -0
  38. package/src/app/components/design-panel/use-design.ts +85 -0
  39. package/src/app/components/diagram.tsx +76 -0
  40. package/src/app/components/doc-assets.tsx +129 -0
  41. package/src/app/components/doc-search.tsx +248 -0
  42. package/src/app/components/doc-sidebar.tsx +162 -0
  43. package/src/app/components/flow-page.tsx +93 -0
  44. package/src/app/components/footnote.tsx +204 -0
  45. package/src/app/components/image-placeholder.tsx +50 -0
  46. package/src/app/components/inspector/inspector.tsx +518 -0
  47. package/src/app/components/numbering.tsx +224 -0
  48. package/src/app/components/page-frame.tsx +70 -0
  49. package/src/app/components/sidebar/folder-item.tsx +212 -0
  50. package/src/app/components/sidebar/icon-picker.tsx +99 -0
  51. package/src/app/components/sidebar/sidebar.tsx +252 -0
  52. package/src/app/components/table-of-contents.tsx +93 -0
  53. package/src/app/components/theme-toggle.tsx +50 -0
  54. package/src/app/components/themes/markdown.tsx +249 -0
  55. package/src/app/components/themes/theme-preview.tsx +74 -0
  56. package/src/app/components/ui/menu.tsx +143 -0
  57. package/src/app/index.html +12 -0
  58. package/src/app/lib/agent-bridge.ts +140 -0
  59. package/src/app/lib/assets.ts +151 -0
  60. package/src/app/lib/design-presets.ts +109 -0
  61. package/src/app/lib/design.ts +88 -0
  62. package/src/app/lib/diagnostics.ts +282 -0
  63. package/src/app/lib/doc-preview.tsx +29 -0
  64. package/src/app/lib/docs.ts +26 -0
  65. package/src/app/lib/docx/extract.ts +1623 -0
  66. package/src/app/lib/docx/fonts.test.ts +136 -0
  67. package/src/app/lib/docx/fonts.ts +166 -0
  68. package/src/app/lib/docx/media.ts +102 -0
  69. package/src/app/lib/docx/model.ts +206 -0
  70. package/src/app/lib/docx/paragraph.test.ts +92 -0
  71. package/src/app/lib/docx/paragraph.ts +107 -0
  72. package/src/app/lib/docx/props.ts +187 -0
  73. package/src/app/lib/docx/styles.ts +306 -0
  74. package/src/app/lib/docx/units.ts +35 -0
  75. package/src/app/lib/docx/write.test.ts +507 -0
  76. package/src/app/lib/docx/write.ts +581 -0
  77. package/src/app/lib/docx/xml.ts +39 -0
  78. package/src/app/lib/export-docx.ts +289 -0
  79. package/src/app/lib/export-dom.ts +318 -0
  80. package/src/app/lib/export-html.ts +156 -0
  81. package/src/app/lib/export-image.ts +70 -0
  82. package/src/app/lib/export-pdf.ts +165 -0
  83. package/src/app/lib/flow-measure.test.ts +31 -0
  84. package/src/app/lib/flow-measure.ts +183 -0
  85. package/src/app/lib/flow.test.ts +110 -0
  86. package/src/app/lib/flow.ts +136 -0
  87. package/src/app/lib/folders.ts +192 -0
  88. package/src/app/lib/footnotes.test.tsx +102 -0
  89. package/src/app/lib/footnotes.ts +94 -0
  90. package/src/app/lib/inspector/fiber.ts +99 -0
  91. package/src/app/lib/labels.test.ts +18 -0
  92. package/src/app/lib/labels.ts +181 -0
  93. package/src/app/lib/outline.ts +118 -0
  94. package/src/app/lib/page-context.tsx +43 -0
  95. package/src/app/lib/page-range.test.ts +95 -0
  96. package/src/app/lib/page-range.ts +90 -0
  97. package/src/app/lib/print-ready.ts +69 -0
  98. package/src/app/lib/rasterize.ts +173 -0
  99. package/src/app/lib/scan.ts +26 -0
  100. package/src/app/lib/sdk.test.ts +32 -0
  101. package/src/app/lib/sdk.ts +115 -0
  102. package/src/app/lib/themes.ts +31 -0
  103. package/src/app/lib/use-doc-module.ts +53 -0
  104. package/src/app/lib/use-doc-pages.ts +147 -0
  105. package/src/app/lib/utils.ts +6 -0
  106. package/src/app/lib/view-mode.test.ts +91 -0
  107. package/src/app/lib/view-mode.ts +104 -0
  108. package/src/app/main.tsx +14 -0
  109. package/src/app/routes/assets.tsx +257 -0
  110. package/src/app/routes/doc.tsx +877 -0
  111. package/src/app/routes/home-shell.tsx +203 -0
  112. package/src/app/routes/home.tsx +269 -0
  113. package/src/app/routes/themes.tsx +121 -0
  114. package/src/app/styles.css +97 -0
  115. package/src/app/virtual.d.ts +30 -0
  116. package/template/AGENTS.md +27 -0
  117. package/template/README.md +39 -0
  118. package/template/docs/getting-started/index.tsx +230 -0
  119. package/template/mosage.config.ts +5 -0
  120. package/template/package.json +24 -0
  121. package/template/tsconfig.json +17 -0
  122. package/README.en.md +0 -57
  123. package/dist/cli.js +0 -4545
  124. package/dist/web/assets/index-Czg2WeHe.js +0 -182
  125. package/dist/web/assets/index-DKAyt92W.css +0 -1
  126. package/dist/web/index.html +0 -15
  127. package/skills/current-position/SKILL.md +0 -65
  128. package/skills/kickoff/SKILL.md +0 -105
  129. package/skills/mosage-reference/SKILL.md +0 -164
  130. package/skills/outline/SKILL.md +0 -79
  131. package/skills/review/SKILL.md +0 -64
  132. package/skills/write-chapter/SKILL.md +0 -58
  133. package/template/book/STYLE.md +0 -6
  134. package/template/book/assets/.gitkeep +0 -0
  135. package/template/book/book.yaml +0 -42
  136. package/template/book/brief.md +0 -6
  137. package/template/book/chapters/.gitkeep +0 -0
  138. package/template/book/notes/README.md +0 -7
  139. package/template/project/AGENTS.md +0 -82
  140. package/template/project/CLAUDE.md +0 -1
  141. package/template/project/README.md +0 -48
  142. package/template/project/books/.gitkeep +0 -0
  143. package/template/project/gitignore +0 -5
  144. package/template/project/mosage.yaml +0 -23
  145. package/template/project/notes/README.md +0 -8
  146. package/template/project/package.json +0 -14
@@ -0,0 +1,318 @@
1
+ import { Loader2, RotateCcw, Shuffle, X } from 'lucide-react';
2
+ import type { DesignSystem } from '../../lib/design';
3
+ import { useDesignPanelState } from './design-provider';
4
+
5
+ const FONT_OPTIONS: Array<{ label: string; value: string }> = [
6
+ {
7
+ label: 'System sans',
8
+ value: '-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif',
9
+ },
10
+ { label: 'Helvetica', value: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
11
+ { label: 'Georgia serif', value: 'Georgia, "Times New Roman", serif' },
12
+ { label: 'Times serif', value: '"Times New Roman", Times, serif' },
13
+ { label: 'Mono', value: 'ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace' },
14
+ ];
15
+
16
+ const PALETTE_KEYS: Array<{ key: keyof DesignSystem['palette']; label: string }> = [
17
+ { key: 'bg', label: 'Sheet' },
18
+ { key: 'text', label: 'Text' },
19
+ { key: 'muted', label: 'Muted' },
20
+ { key: 'accent', label: 'Accent' },
21
+ { key: 'rule', label: 'Rule' },
22
+ ];
23
+
24
+ const TYPE_KEYS: Array<{
25
+ key: keyof DesignSystem['typeScale'];
26
+ label: string;
27
+ min: number;
28
+ max: number;
29
+ }> = [
30
+ { key: 'title', label: 'Title', min: 24, max: 72 },
31
+ { key: 'h1', label: 'H1', min: 16, max: 48 },
32
+ { key: 'h2', label: 'H2', min: 14, max: 36 },
33
+ { key: 'h3', label: 'H3', min: 12, max: 28 },
34
+ { key: 'body', label: 'Body', min: 9, max: 20 },
35
+ { key: 'caption', label: 'Caption', min: 7, max: 16 },
36
+ ];
37
+
38
+ export function DesignPanel({ onClose }: { onClose: () => void }) {
39
+ const {
40
+ loaded,
41
+ exists,
42
+ warning,
43
+ draft,
44
+ dirty,
45
+ committing,
46
+ error,
47
+ update,
48
+ commit,
49
+ discard,
50
+ resetToDefaults,
51
+ shuffle,
52
+ } = useDesignPanelState();
53
+
54
+ return (
55
+ <aside className="flex w-72 flex-none flex-col border-border border-l bg-background">
56
+ <header className="flex h-10 flex-none items-center justify-between border-border border-b px-3">
57
+ <span className="font-medium text-xs uppercase tracking-wider">Design</span>
58
+ <div className="flex items-center gap-0.5">
59
+ <IconButton label="Shuffle preset" onClick={shuffle}>
60
+ <Shuffle className="size-3.5" />
61
+ </IconButton>
62
+ <IconButton label="Reset to defaults" onClick={resetToDefaults}>
63
+ <RotateCcw className="size-3.5" />
64
+ </IconButton>
65
+ <IconButton label="Close design panel" onClick={onClose}>
66
+ <X className="size-3.5" />
67
+ </IconButton>
68
+ </div>
69
+ </header>
70
+
71
+ <div className="min-h-0 flex-1 overflow-y-auto px-3 py-3">
72
+ {!loaded || !draft ? (
73
+ <div className="grid place-items-center py-10">
74
+ <Loader2 className="size-4 animate-spin text-muted-foreground" />
75
+ </div>
76
+ ) : (
77
+ <>
78
+ {warning && (
79
+ <p className="mb-3 rounded-md border border-border bg-muted px-2 py-1.5 text-[11px] text-muted-foreground">
80
+ {warning}
81
+ </p>
82
+ )}
83
+ {!exists && (
84
+ <p className="mb-3 rounded-md border border-border bg-muted px-2 py-1.5 text-[11px] text-muted-foreground">
85
+ This document has no <code className="font-mono">design</code> const yet. Saving
86
+ writes one into <code className="font-mono">index.tsx</code>.
87
+ </p>
88
+ )}
89
+
90
+ <Section title="Palette">
91
+ {PALETTE_KEYS.map(({ key, label }) => (
92
+ <Row key={key} label={label}>
93
+ <input
94
+ type="color"
95
+ value={draft.palette[key]}
96
+ onChange={(e) => {
97
+ const value = e.target.value;
98
+ update((d) => {
99
+ d.palette[key] = value;
100
+ });
101
+ }}
102
+ className="size-6 cursor-pointer rounded border border-border bg-transparent"
103
+ />
104
+ <input
105
+ type="text"
106
+ value={draft.palette[key]}
107
+ onChange={(e) => {
108
+ const value = e.target.value;
109
+ update((d) => {
110
+ d.palette[key] = value;
111
+ });
112
+ }}
113
+ className="w-20 rounded border border-border bg-transparent px-1.5 py-0.5 font-mono text-[11px]"
114
+ />
115
+ </Row>
116
+ ))}
117
+ </Section>
118
+
119
+ <Section title="Fonts">
120
+ {(['heading', 'body', 'mono'] as const).map((slot) => (
121
+ <Row key={slot} label={slot[0].toUpperCase() + slot.slice(1)}>
122
+ <select
123
+ value={
124
+ FONT_OPTIONS.some((o) => o.value === draft.fonts[slot])
125
+ ? draft.fonts[slot]
126
+ : 'custom'
127
+ }
128
+ onChange={(e) => {
129
+ const value = e.target.value;
130
+ if (value === 'custom') return;
131
+ update((d) => {
132
+ d.fonts[slot] = value;
133
+ });
134
+ }}
135
+ className="w-40 rounded border border-border bg-transparent px-1.5 py-0.5 text-[11px]"
136
+ >
137
+ {FONT_OPTIONS.map((option) => (
138
+ <option key={option.label} value={option.value}>
139
+ {option.label}
140
+ </option>
141
+ ))}
142
+ <option value="custom">Custom (from source)</option>
143
+ </select>
144
+ </Row>
145
+ ))}
146
+ </Section>
147
+
148
+ <Section title="Type scale">
149
+ {TYPE_KEYS.map(({ key, label, min, max }) => (
150
+ <SliderRow
151
+ key={key}
152
+ label={label}
153
+ value={draft.typeScale[key]}
154
+ min={min}
155
+ max={max}
156
+ step={1}
157
+ suffix="px"
158
+ onChange={(value) =>
159
+ update((d) => {
160
+ d.typeScale[key] = value;
161
+ })
162
+ }
163
+ />
164
+ ))}
165
+ </Section>
166
+
167
+ <Section title="Page">
168
+ <SliderRow
169
+ label="Margin"
170
+ value={draft.margin}
171
+ min={32}
172
+ max={140}
173
+ step={2}
174
+ suffix="px"
175
+ onChange={(value) =>
176
+ update((d) => {
177
+ d.margin = value;
178
+ })
179
+ }
180
+ />
181
+ <SliderRow
182
+ label="Leading"
183
+ value={draft.leading}
184
+ min={1.2}
185
+ max={2}
186
+ step={0.05}
187
+ onChange={(value) =>
188
+ update((d) => {
189
+ d.leading = Number(value.toFixed(2));
190
+ })
191
+ }
192
+ />
193
+ <SliderRow
194
+ label="Radius"
195
+ value={draft.radius}
196
+ min={0}
197
+ max={24}
198
+ step={1}
199
+ suffix="px"
200
+ onChange={(value) =>
201
+ update((d) => {
202
+ d.radius = value;
203
+ })
204
+ }
205
+ />
206
+ </Section>
207
+
208
+ {draft.typeScale.body < 12 && (
209
+ <p className="mt-3 rounded-md border border-border bg-muted px-2 py-1.5 text-[11px] text-muted-foreground">
210
+ Body under 12px prints below 9pt — hard to read on paper.
211
+ </p>
212
+ )}
213
+ </>
214
+ )}
215
+ </div>
216
+
217
+ <footer className="flex-none border-border border-t p-3">
218
+ {error && <p className="mb-2 text-[11px] text-muted-foreground">{error}</p>}
219
+ <div className="flex items-center gap-2">
220
+ <button
221
+ type="button"
222
+ onClick={commit}
223
+ disabled={!dirty || committing}
224
+ className="flex flex-1 items-center justify-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-primary-foreground text-xs transition-opacity hover:opacity-90 disabled:opacity-50"
225
+ >
226
+ {committing && <Loader2 className="size-3.5 animate-spin" />}
227
+ Save to source
228
+ </button>
229
+ <button
230
+ type="button"
231
+ onClick={discard}
232
+ disabled={!dirty || committing}
233
+ className="rounded-md border border-border px-3 py-1.5 text-xs transition-colors hover:bg-accent disabled:opacity-50"
234
+ >
235
+ Discard
236
+ </button>
237
+ </div>
238
+ </footer>
239
+ </aside>
240
+ );
241
+ }
242
+
243
+ function Section({ title, children }: { title: string; children: React.ReactNode }) {
244
+ return (
245
+ <section className="mb-4">
246
+ <h3 className="mb-1.5 text-[10px] text-muted-foreground uppercase tracking-wider">{title}</h3>
247
+ <div className="space-y-1.5">{children}</div>
248
+ </section>
249
+ );
250
+ }
251
+
252
+ function Row({ label, children }: { label: string; children: React.ReactNode }) {
253
+ return (
254
+ <div className="flex items-center justify-between gap-2">
255
+ <span className="text-xs">{label}</span>
256
+ <div className="flex items-center gap-1.5">{children}</div>
257
+ </div>
258
+ );
259
+ }
260
+
261
+ function SliderRow({
262
+ label,
263
+ value,
264
+ min,
265
+ max,
266
+ step,
267
+ suffix = '',
268
+ onChange,
269
+ }: {
270
+ label: string;
271
+ value: number;
272
+ min: number;
273
+ max: number;
274
+ step: number;
275
+ suffix?: string;
276
+ onChange: (value: number) => void;
277
+ }) {
278
+ return (
279
+ <div className="flex items-center gap-2">
280
+ <span className="w-16 flex-none text-xs">{label}</span>
281
+ <input
282
+ type="range"
283
+ min={min}
284
+ max={max}
285
+ step={step}
286
+ value={value}
287
+ onChange={(e) => onChange(Number(e.target.value))}
288
+ className="h-1 flex-1 cursor-pointer accent-foreground"
289
+ />
290
+ <span className="w-12 flex-none text-right font-mono text-[11px] tabular-nums">
291
+ {value}
292
+ {suffix}
293
+ </span>
294
+ </div>
295
+ );
296
+ }
297
+
298
+ function IconButton({
299
+ label,
300
+ onClick,
301
+ children,
302
+ }: {
303
+ label: string;
304
+ onClick: () => void;
305
+ children: React.ReactNode;
306
+ }) {
307
+ return (
308
+ <button
309
+ type="button"
310
+ aria-label={label}
311
+ title={label}
312
+ onClick={onClick}
313
+ className="flex size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
314
+ >
315
+ {children}
316
+ </button>
317
+ );
318
+ }
@@ -0,0 +1,121 @@
1
+ import {
2
+ createContext,
3
+ type ReactNode,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from 'react';
11
+ import { type DesignSystem, defaultDesign, designToCssVars } from '../../lib/design';
12
+ import { shuffleDesign } from '../../lib/design-presets';
13
+ import { useDesign as useDesignFetch } from './use-design';
14
+
15
+ type DesignCtx = {
16
+ docId: string;
17
+ loaded: boolean;
18
+ exists: boolean;
19
+ warning: string | null;
20
+ design: DesignSystem | null;
21
+ draft: DesignSystem | null;
22
+ dirty: boolean;
23
+ committing: boolean;
24
+ error: string | null;
25
+ update: (mut: (next: DesignSystem) => void) => void;
26
+ commit: () => Promise<void>;
27
+ discard: () => void;
28
+ resetToDefaults: () => void;
29
+ shuffle: () => void;
30
+ };
31
+
32
+ const Ctx = createContext<DesignCtx | null>(null);
33
+
34
+ export function useDesignPanelState(): DesignCtx {
35
+ const value = useContext(Ctx);
36
+ if (!value) throw new Error('useDesignPanelState must be used inside <DesignProvider>');
37
+ return value;
38
+ }
39
+
40
+ function clone<T>(value: T): T {
41
+ return JSON.parse(JSON.stringify(value)) as T;
42
+ }
43
+
44
+ export function DesignProvider({ docId, children }: { docId: string; children: ReactNode }) {
45
+ const { design, exists, warning, loaded, save } = useDesignFetch(docId);
46
+ const [draft, setDraft] = useState<DesignSystem | null>(null);
47
+ const [committing, setCommitting] = useState(false);
48
+ const [error, setError] = useState<string | null>(null);
49
+ const draftRef = useRef<DesignSystem | null>(null);
50
+ draftRef.current = draft;
51
+
52
+ useEffect(() => {
53
+ if (design) setDraft(clone(design));
54
+ }, [design]);
55
+
56
+ const dirty = useMemo(() => {
57
+ if (!draft || !design) return false;
58
+ return JSON.stringify(draft) !== JSON.stringify(design);
59
+ }, [draft, design]);
60
+
61
+ const update = useCallback((mut: (next: DesignSystem) => void) => {
62
+ const prev = draftRef.current;
63
+ if (!prev) return;
64
+ const next = clone(prev);
65
+ mut(next);
66
+ setDraft(next);
67
+ }, []);
68
+
69
+ const commit = useCallback(async () => {
70
+ if (!draft) return;
71
+ setCommitting(true);
72
+ const result = await save(draft);
73
+ setCommitting(false);
74
+ setError(result.ok ? null : (result.error ?? 'Failed to save'));
75
+ }, [draft, save]);
76
+
77
+ const discard = useCallback(() => {
78
+ if (design) setDraft(clone(design));
79
+ setError(null);
80
+ }, [design]);
81
+
82
+ const resetToDefaults = useCallback(() => setDraft(clone(defaultDesign)), []);
83
+ const shuffle = useCallback(() => setDraft(clone(shuffleDesign(draftRef.current))), []);
84
+
85
+ // PageFrame writes its design vars inline on each page root, so the draft
86
+ // overlay has to outrank inline styles — hence `!important`.
87
+ const previewCss = useMemo(() => {
88
+ if (!dirty || !draft) return '';
89
+ const lines = Object.entries(designToCssVars(draft))
90
+ .map(([k, v]) => ` ${k}: ${v} !important;`)
91
+ .join('\n');
92
+ return `[data-od-page] {\n${lines}\n}`;
93
+ }, [dirty, draft]);
94
+
95
+ const value: DesignCtx = {
96
+ docId,
97
+ loaded,
98
+ exists,
99
+ warning,
100
+ design,
101
+ draft,
102
+ dirty,
103
+ committing,
104
+ error,
105
+ update,
106
+ commit,
107
+ discard,
108
+ resetToDefaults,
109
+ shuffle,
110
+ };
111
+
112
+ return (
113
+ <Ctx.Provider value={value}>
114
+ {previewCss && (
115
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: trusted local css built from draft state
116
+ <style dangerouslySetInnerHTML={{ __html: previewCss }} />
117
+ )}
118
+ {children}
119
+ </Ctx.Provider>
120
+ );
121
+ }
@@ -0,0 +1,85 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import type { DesignSystem } from '../../lib/design';
3
+
4
+ type FetchedState = {
5
+ design: DesignSystem | null;
6
+ exists: boolean;
7
+ warning: string | null;
8
+ loaded: boolean;
9
+ };
10
+
11
+ export type UseDesignReturn = FetchedState & {
12
+ refresh: () => Promise<void>;
13
+ save: (patch: Partial<DesignSystem>) => Promise<{ ok: boolean; error?: string }>;
14
+ reset: () => Promise<{ ok: boolean; error?: string }>;
15
+ };
16
+
17
+ const INITIAL: FetchedState = { design: null, exists: false, warning: null, loaded: false };
18
+
19
+ export function useDesign(docId: string): UseDesignReturn {
20
+ const [state, setState] = useState<FetchedState>(INITIAL);
21
+ const docIdRef = useRef(docId);
22
+ docIdRef.current = docId;
23
+
24
+ const refresh = useCallback(async () => {
25
+ const id = docIdRef.current;
26
+ if (!id) return;
27
+ try {
28
+ const res = await fetch(`/__design?docId=${encodeURIComponent(id)}`);
29
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
30
+ const body = (await res.json()) as {
31
+ design: DesignSystem;
32
+ exists: boolean;
33
+ warning: string | null;
34
+ };
35
+ setState({ design: body.design, exists: body.exists, warning: body.warning, loaded: true });
36
+ } catch (err) {
37
+ setState((s) => ({ ...s, warning: String((err as Error).message), loaded: true }));
38
+ }
39
+ }, []);
40
+
41
+ useEffect(() => {
42
+ setState(INITIAL);
43
+ void refresh();
44
+ }, [refresh]);
45
+
46
+ const save = useCallback(async (patch: Partial<DesignSystem>) => {
47
+ const id = docIdRef.current;
48
+ if (!id) return { ok: false, error: 'no document id' };
49
+ try {
50
+ const res = await fetch(`/__design?docId=${encodeURIComponent(id)}`, {
51
+ method: 'PUT',
52
+ headers: { 'content-type': 'application/json' },
53
+ body: JSON.stringify({ patch }),
54
+ });
55
+ const body = (await res.json()) as { ok?: boolean; error?: string; design?: DesignSystem };
56
+ if (!res.ok || !body.ok) return { ok: false, error: body.error ?? `HTTP ${res.status}` };
57
+ if (body.design) {
58
+ setState((s) => ({ ...s, design: body.design ?? s.design, exists: true, warning: null }));
59
+ }
60
+ return { ok: true };
61
+ } catch (err) {
62
+ return { ok: false, error: String((err as Error).message) };
63
+ }
64
+ }, []);
65
+
66
+ const reset = useCallback(async () => {
67
+ const id = docIdRef.current;
68
+ if (!id) return { ok: false, error: 'no document id' };
69
+ try {
70
+ const res = await fetch(`/__design/reset?docId=${encodeURIComponent(id)}`, {
71
+ method: 'POST',
72
+ });
73
+ const body = (await res.json()) as { ok?: boolean; error?: string; design?: DesignSystem };
74
+ if (!res.ok || !body.ok) return { ok: false, error: body.error ?? `HTTP ${res.status}` };
75
+ if (body.design) {
76
+ setState((s) => ({ ...s, design: body.design ?? s.design, exists: true, warning: null }));
77
+ }
78
+ return { ok: true };
79
+ } catch (err) {
80
+ return { ok: false, error: String((err as Error).message) };
81
+ }
82
+ }, []);
83
+
84
+ return { ...state, refresh, save, reset };
85
+ }
@@ -0,0 +1,76 @@
1
+ import type { CSSProperties, ReactNode } from 'react';
2
+ import type { LabelKind } from '../lib/labels';
3
+ import { Figure } from './numbering';
4
+
5
+ export type DiagramSource = {
6
+ svg: string;
7
+ width: number;
8
+ height: number;
9
+ };
10
+
11
+ export type DiagramProps = {
12
+ /** The compiled module from `import chart from './architecture.mmd'`. */
13
+ chart: DiagramSource;
14
+ /** Caption text. Given one, the drawing is numbered like any other figure. */
15
+ caption?: ReactNode;
16
+ captionText?: string;
17
+ kind?: LabelKind;
18
+ id?: string;
19
+ /**
20
+ * Drawn width in CSS px. Defaults to the diagram's natural size, capped to
21
+ * the column — a diagram wider than the text block is a layout fault, and
22
+ * `mosage check` would report it as one.
23
+ */
24
+ width?: number;
25
+ align?: 'left' | 'center';
26
+ style?: CSSProperties;
27
+ className?: string;
28
+ };
29
+
30
+ /**
31
+ * A diagram compiled from Mermaid-flavoured text at build time. The SVG is
32
+ * inlined rather than referenced through `<img>` so it inherits the document's
33
+ * theme variables — an `<img src="data:…">` would be painted in a document of
34
+ * its own, with none of this one's colours or faces.
35
+ */
36
+ export function Diagram({
37
+ chart,
38
+ caption,
39
+ captionText,
40
+ kind = 'figure',
41
+ id,
42
+ width,
43
+ align = 'center',
44
+ style,
45
+ className,
46
+ }: DiagramProps) {
47
+ const drawing = (
48
+ <div
49
+ className={caption ? undefined : className}
50
+ style={{
51
+ width: width ?? chart.width,
52
+ maxWidth: '100%',
53
+ marginLeft: align === 'center' ? 'auto' : undefined,
54
+ marginRight: align === 'center' ? 'auto' : undefined,
55
+ ...(caption ? undefined : style),
56
+ }}
57
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: the SVG is produced by this package's own renderer at build time, never from document input at runtime
58
+ dangerouslySetInnerHTML={{ __html: chart.svg }}
59
+ />
60
+ );
61
+
62
+ if (!caption) return drawing;
63
+
64
+ return (
65
+ <Figure
66
+ {...(id ? { id } : {})}
67
+ caption={caption}
68
+ {...(captionText ? { captionText } : {})}
69
+ kind={kind}
70
+ className={className}
71
+ {...(style ? { style } : {})}
72
+ >
73
+ {drawing}
74
+ </Figure>
75
+ );
76
+ }