mosage 0.2.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 (121) hide show
  1. package/README.md +16 -15
  2. package/bin.js +2 -0
  3. package/dist/build-C7NW_3Pk.js +14 -0
  4. package/dist/check-CP4873Wx.js +41 -0
  5. package/dist/cli/bin.d.ts +1 -0
  6. package/dist/cli/bin.js +228 -0
  7. package/dist/config-DPm1BBAb.js +2619 -0
  8. package/dist/config-TlTe7Ona.d.ts +24 -0
  9. package/dist/context-BqsdSrAQ.js +1084 -0
  10. package/dist/dev-Biz42qlu.js +17 -0
  11. package/dist/diagram-xlVDekYk.js +763 -0
  12. package/dist/export-Bi6nuxjT.js +31 -0
  13. package/dist/import-D2jNB07F.js +25 -0
  14. package/dist/index.d.ts +455 -0
  15. package/dist/index.js +693 -0
  16. package/dist/{cli.js → init-Bbtj2pxF.js} +34 -68
  17. package/dist/preview-CLm51aRt.js +19 -0
  18. package/dist/sdk-DjpX6mCv.js +51 -0
  19. package/dist/vite/index.d.ts +25 -0
  20. package/dist/vite/index.js +2 -0
  21. package/env.d.ts +83 -0
  22. package/package.json +59 -12
  23. package/{template/.agents/skills → skills}/create-doc/SKILL.md +1 -1
  24. package/{template/.agents/skills → skills}/create-theme/SKILL.md +1 -1
  25. package/{template/.agents/skills → skills}/doc-authoring/SKILL.md +8 -9
  26. package/{template/.agents/skills → skills}/doc-authoring/references/assets.md +1 -1
  27. package/{template/.agents/skills → skills}/doc-authoring/references/design-system.md +1 -1
  28. package/{template/.agents/skills → skills}/doc-authoring/references/long-form.md +4 -4
  29. package/{template/.agents/skills → skills}/doc-authoring/references/pagination.md +1 -1
  30. package/src/app/app.tsx +42 -0
  31. package/src/app/components/data-table.tsx +196 -0
  32. package/src/app/components/design-panel/design-panel.tsx +318 -0
  33. package/src/app/components/design-panel/design-provider.tsx +121 -0
  34. package/src/app/components/design-panel/use-design.ts +85 -0
  35. package/src/app/components/diagram.tsx +76 -0
  36. package/src/app/components/doc-assets.tsx +129 -0
  37. package/src/app/components/doc-search.tsx +248 -0
  38. package/src/app/components/doc-sidebar.tsx +162 -0
  39. package/src/app/components/flow-page.tsx +93 -0
  40. package/src/app/components/footnote.tsx +204 -0
  41. package/src/app/components/image-placeholder.tsx +50 -0
  42. package/src/app/components/inspector/inspector.tsx +518 -0
  43. package/src/app/components/numbering.tsx +224 -0
  44. package/src/app/components/page-frame.tsx +70 -0
  45. package/src/app/components/sidebar/folder-item.tsx +212 -0
  46. package/src/app/components/sidebar/icon-picker.tsx +99 -0
  47. package/src/app/components/sidebar/sidebar.tsx +252 -0
  48. package/src/app/components/table-of-contents.tsx +93 -0
  49. package/src/app/components/theme-toggle.tsx +50 -0
  50. package/src/app/components/themes/markdown.tsx +249 -0
  51. package/src/app/components/themes/theme-preview.tsx +74 -0
  52. package/src/app/components/ui/menu.tsx +143 -0
  53. package/src/app/index.html +12 -0
  54. package/src/app/lib/agent-bridge.ts +140 -0
  55. package/src/app/lib/assets.ts +151 -0
  56. package/src/app/lib/design-presets.ts +109 -0
  57. package/src/app/lib/design.ts +88 -0
  58. package/src/app/lib/diagnostics.ts +282 -0
  59. package/src/app/lib/doc-preview.tsx +29 -0
  60. package/src/app/lib/docs.ts +26 -0
  61. package/src/app/lib/docx/extract.ts +1623 -0
  62. package/src/app/lib/docx/fonts.test.ts +136 -0
  63. package/src/app/lib/docx/fonts.ts +166 -0
  64. package/src/app/lib/docx/media.ts +102 -0
  65. package/src/app/lib/docx/model.ts +206 -0
  66. package/src/app/lib/docx/paragraph.test.ts +92 -0
  67. package/src/app/lib/docx/paragraph.ts +107 -0
  68. package/src/app/lib/docx/props.ts +187 -0
  69. package/src/app/lib/docx/styles.ts +306 -0
  70. package/src/app/lib/docx/units.ts +35 -0
  71. package/src/app/lib/docx/write.test.ts +507 -0
  72. package/src/app/lib/docx/write.ts +581 -0
  73. package/src/app/lib/docx/xml.ts +39 -0
  74. package/src/app/lib/export-docx.ts +289 -0
  75. package/src/app/lib/export-dom.ts +318 -0
  76. package/src/app/lib/export-html.ts +156 -0
  77. package/src/app/lib/export-image.ts +70 -0
  78. package/src/app/lib/export-pdf.ts +165 -0
  79. package/src/app/lib/flow-measure.test.ts +31 -0
  80. package/src/app/lib/flow-measure.ts +183 -0
  81. package/src/app/lib/flow.test.ts +110 -0
  82. package/src/app/lib/flow.ts +136 -0
  83. package/src/app/lib/folders.ts +192 -0
  84. package/src/app/lib/footnotes.test.tsx +102 -0
  85. package/src/app/lib/footnotes.ts +94 -0
  86. package/src/app/lib/inspector/fiber.ts +99 -0
  87. package/src/app/lib/labels.test.ts +18 -0
  88. package/src/app/lib/labels.ts +181 -0
  89. package/src/app/lib/outline.ts +118 -0
  90. package/src/app/lib/page-context.tsx +43 -0
  91. package/src/app/lib/page-range.test.ts +95 -0
  92. package/src/app/lib/page-range.ts +90 -0
  93. package/src/app/lib/print-ready.ts +69 -0
  94. package/src/app/lib/rasterize.ts +173 -0
  95. package/src/app/lib/scan.ts +26 -0
  96. package/src/app/lib/sdk.test.ts +32 -0
  97. package/src/app/lib/sdk.ts +115 -0
  98. package/src/app/lib/themes.ts +31 -0
  99. package/src/app/lib/use-doc-module.ts +53 -0
  100. package/src/app/lib/use-doc-pages.ts +147 -0
  101. package/src/app/lib/utils.ts +6 -0
  102. package/src/app/lib/view-mode.test.ts +91 -0
  103. package/src/app/lib/view-mode.ts +104 -0
  104. package/src/app/main.tsx +14 -0
  105. package/src/app/routes/assets.tsx +257 -0
  106. package/src/app/routes/doc.tsx +877 -0
  107. package/src/app/routes/home-shell.tsx +203 -0
  108. package/src/app/routes/home.tsx +269 -0
  109. package/src/app/routes/themes.tsx +121 -0
  110. package/src/app/styles.css +97 -0
  111. package/src/app/virtual.d.ts +30 -0
  112. package/template/AGENTS.md +1 -1
  113. package/template/README.md +24 -52
  114. package/template/docs/getting-started/index.tsx +2 -2
  115. package/template/mosage.config.ts +1 -1
  116. package/template/package.json +1 -1
  117. package/template/tsconfig.json +1 -1
  118. package/README.zh-TW.md +0 -28
  119. /package/{template/.agents/skills → skills}/apply-comments/SKILL.md +0 -0
  120. /package/{template/.agents/skills → skills}/current-doc/SKILL.md +0 -0
  121. /package/{template/.agents/skills → skills}/doc-authoring/references/tables-and-charts.md +0 -0
@@ -0,0 +1,93 @@
1
+ import type { CSSProperties, ReactNode } from 'react';
2
+ import type { DesignSystem } from '../lib/design';
3
+ import type { FlowSection } from '../lib/flow';
4
+ import type { ExtractedNote } from '../lib/footnotes';
5
+ import { PAGE_ATTR, PAGE_INDEX_ATTR } from '../lib/outline';
6
+ import { DocPageProvider, useDocPageCount } from '../lib/page-context';
7
+ import { Footnotes } from './footnote';
8
+
9
+ export const FLOW_BLOCK_ATTR = 'data-od-flow-block';
10
+ /** Around the section's footer, as `display: contents` so it lays out as if absent. */
11
+ export const FLOW_FOOTER_ATTR = 'data-od-flow-footer';
12
+
13
+ /**
14
+ * The page shell a flow section renders into. The framework owns the margin and
15
+ * base typography here — that is the trade for not hand-splitting pages — while
16
+ * the blocks keep their own styles.
17
+ *
18
+ * It is a column so the footnote area can sit at the foot of the sheet rather
19
+ * than immediately under the last paragraph. The blocks stay inside one block
20
+ * container, so their margins collapse exactly as they did when measured.
21
+ */
22
+ export function flowShellStyle(design: DesignSystem | undefined, padding?: number): CSSProperties {
23
+ return {
24
+ width: '100%',
25
+ height: '100%',
26
+ boxSizing: 'border-box',
27
+ padding: padding ?? design?.margin ?? 76,
28
+ background: 'var(--od-bg)',
29
+ color: 'var(--od-text)',
30
+ fontFamily: 'var(--od-font-body)',
31
+ fontSize: 'var(--od-size-body)',
32
+ lineHeight: 'var(--od-leading)',
33
+ position: 'relative',
34
+ overflow: 'hidden',
35
+ display: 'flex',
36
+ flexDirection: 'column',
37
+ };
38
+ }
39
+
40
+ /**
41
+ * With a `sheet`, the block is a page frame of its own: the outline and
42
+ * numbering scans, and `useDocPageNumber()`, see the sheet it printed on even
43
+ * when it is laid out in one continuous column.
44
+ */
45
+ export function FlowBlock({ children, sheet }: { children?: ReactNode; sheet?: number }) {
46
+ const total = useDocPageCount();
47
+ if (sheet === undefined) return <div {...{ [FLOW_BLOCK_ATTR]: '' }}>{children}</div>;
48
+ return (
49
+ <div {...{ [FLOW_BLOCK_ATTR]: '', [PAGE_ATTR]: '', [PAGE_INDEX_ATTR]: sheet }}>
50
+ <DocPageProvider index={sheet} total={total}>
51
+ {children}
52
+ </DocPageProvider>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export function FlowPage({
58
+ section,
59
+ design,
60
+ blockIndices,
61
+ blocks,
62
+ notes,
63
+ sheets,
64
+ }: {
65
+ section: FlowSection;
66
+ design: DesignSystem | undefined;
67
+ blockIndices: number[];
68
+ /** Blocks with footnotes already lifted out; falls back to the authored ones. */
69
+ blocks?: ReactNode[];
70
+ notes?: ExtractedNote[];
71
+ /** Block index → the sheet it printed on, for a copy laid out as one column. */
72
+ sheets?: ReadonlyMap<number, number>;
73
+ }) {
74
+ const Footer = section.footer;
75
+ const source = blocks ?? section.blocks;
76
+ return (
77
+ <div style={flowShellStyle(design, section.padding)}>
78
+ <div style={{ flex: 1, minHeight: 0 }}>
79
+ {blockIndices.map((index) => (
80
+ <FlowBlock key={index} sheet={sheets?.get(index)}>
81
+ {source[index]}
82
+ </FlowBlock>
83
+ ))}
84
+ </div>
85
+ {notes && notes.length > 0 && <Footnotes notes={notes} />}
86
+ {Footer && (
87
+ <div {...{ [FLOW_FOOTER_ATTR]: '' }} style={{ display: 'contents' }}>
88
+ <Footer />
89
+ </div>
90
+ )}
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,204 @@
1
+ import {
2
+ type Context,
3
+ type CSSProperties,
4
+ createContext,
5
+ type ReactNode,
6
+ useContext,
7
+ useEffect,
8
+ useId,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from 'react';
13
+ import { LABEL_ATTR, LABEL_ID_ATTR, useDocLabel, useLabelVocabulary } from '../lib/labels';
14
+
15
+ /** Stands in for a number the scan has not produced yet, or never will. */
16
+ const UNNUMBERED = '\u2022';
17
+
18
+ export type CollectedNote = { id: string; content: ReactNode };
19
+
20
+ type Collector = {
21
+ /** Registration order, which is document order. */
22
+ ids: string[];
23
+ /**
24
+ * Note bodies live outside React state on purpose. A `ReactNode` is a new
25
+ * object on every render, so storing one in state would make every commit
26
+ * look like a change — register, re-render, register — and never settle.
27
+ * Ids are stable, so only they drive a re-render; the bodies are read fresh
28
+ * from here in whatever render follows.
29
+ */
30
+ contents: Map<string, ReactNode>;
31
+ register: (id: string, content: ReactNode) => void;
32
+ unregister: (id: string) => void;
33
+ };
34
+
35
+ // Same globalThis treatment as the page context: a document imports the
36
+ // published bundle while the viewer runs the source copy, and a context created
37
+ // twice registers into one instance and reads from the other.
38
+ const GLOBAL_KEY = '__mosage_footnote_context__';
39
+ type GlobalWithCtx = typeof globalThis & { [GLOBAL_KEY]?: Context<Collector | null> };
40
+ const g = globalThis as GlobalWithCtx;
41
+ if (!g[GLOBAL_KEY]) {
42
+ g[GLOBAL_KEY] = createContext<Collector | null>(null);
43
+ }
44
+ const FootnoteContext = g[GLOBAL_KEY];
45
+
46
+ /**
47
+ * Collects the footnotes a fixed page renders, so `<Footnotes />` can print
48
+ * them at its foot. Flow sections never use this: their notes are lifted out of
49
+ * the blocks before measurement, because the space they take decides where the
50
+ * page breaks land.
51
+ */
52
+ export function FootnoteCollector({ children }: { children?: ReactNode }) {
53
+ const contents = useRef<Map<string, ReactNode>>(new Map()).current;
54
+ const [ids, setIds] = useState<string[]>([]);
55
+
56
+ const value = useMemo<Collector>(
57
+ () => ({
58
+ ids,
59
+ contents,
60
+ register: (id, content) => {
61
+ contents.set(id, content);
62
+ setIds((current) => (current.includes(id) ? current : [...current, id]));
63
+ },
64
+ unregister: (id) => {
65
+ contents.delete(id);
66
+ setIds((current) => (current.includes(id) ? current.filter((x) => x !== id) : current));
67
+ },
68
+ }),
69
+ [ids, contents],
70
+ );
71
+
72
+ return <FootnoteContext.Provider value={value}>{children}</FootnoteContext.Provider>;
73
+ }
74
+
75
+ export const FOOTNOTE_MARKER_FLAG = '__odFootnote';
76
+
77
+ export type FootnoteProps = {
78
+ /** Stable id, so `<Ref to>` can point at the note. Generated when omitted. */
79
+ id?: string;
80
+ children?: ReactNode;
81
+ };
82
+
83
+ export function markerStyle(): CSSProperties {
84
+ return {
85
+ fontSize: '0.7em',
86
+ lineHeight: 0,
87
+ verticalAlign: 'super',
88
+ fontVariantNumeric: 'tabular-nums',
89
+ color: 'var(--od-accent)',
90
+ padding: '0 1px',
91
+ };
92
+ }
93
+
94
+ /**
95
+ * The superscript in the text. Its position in the document decides the number.
96
+ *
97
+ * The placeholder is the one the printed note uses, and deliberately not an
98
+ * empty string: the two describe the same unresolved state, a note whose number
99
+ * never arrives has to be visible in both places rather than silently blank,
100
+ * and the packer measures this glyph — an empty marker is narrower than the
101
+ * digit that replaces it.
102
+ */
103
+ export function FootnoteMarker({ id }: { id: string }) {
104
+ const entry = useDocLabel(id);
105
+ return (
106
+ <sup {...{ [LABEL_ATTR]: 'footnote', [LABEL_ID_ATTR]: id }} style={markerStyle()}>
107
+ {entry?.number ?? UNNUMBERED}
108
+ </sup>
109
+ );
110
+ }
111
+
112
+ /**
113
+ * A note anchored to this point in the text. Inside a `flow()` section the
114
+ * framework lifts it to the foot of whatever page the marker lands on; on a
115
+ * fixed page, put a `<Footnotes />` where you want them printed.
116
+ */
117
+ export function Footnote({ id, children }: FootnoteProps) {
118
+ const generated = useId();
119
+ const noteId = id ?? generated;
120
+ const collector = useContext(FootnoteContext);
121
+ const latest = useRef<Collector | null>(null);
122
+
123
+ // Runs after every commit so the body stays current; only a new id changes
124
+ // state, so this settles instead of looping.
125
+ useEffect(() => {
126
+ latest.current = collector;
127
+ collector?.register(noteId, children);
128
+ });
129
+
130
+ useEffect(() => () => latest.current?.unregister(noteId), [noteId]);
131
+
132
+ return <FootnoteMarker id={noteId} />;
133
+ }
134
+
135
+ (Footnote as unknown as Record<string, boolean>)[FOOTNOTE_MARKER_FLAG] = true;
136
+
137
+ export type FootnotesProps = {
138
+ /** Notes to print. Omitted on a fixed page, where they are collected from it. */
139
+ notes?: CollectedNote[];
140
+ style?: CSSProperties;
141
+ className?: string;
142
+ };
143
+
144
+ /** Outside the area's box, so measurement has to add it back by hand. */
145
+ export const FOOTNOTE_AREA_MARGIN_TOP = 12;
146
+
147
+ export const FOOTNOTE_ROW_ATTR = 'data-od-footnote-row';
148
+ export const FOOTNOTE_BODY_ATTR = 'data-od-footnote-body';
149
+ export const FOOTNOTES_ATTR = 'data-od-footnotes';
150
+
151
+ export function footnoteAreaStyle(): CSSProperties {
152
+ return {
153
+ borderTop: '1px solid var(--od-rule)',
154
+ paddingTop: 6,
155
+ marginTop: FOOTNOTE_AREA_MARGIN_TOP,
156
+ fontSize: 'var(--od-size-caption)',
157
+ lineHeight: 1.45,
158
+ color: 'var(--od-muted)',
159
+ };
160
+ }
161
+
162
+ /** One note's row. Shared with the measurement pass so the reserved space is real. */
163
+ export function FootnoteRow({ id, content }: CollectedNote) {
164
+ const entry = useDocLabel(id);
165
+ return (
166
+ <div {...{ [FOOTNOTE_ROW_ATTR]: id }} style={{ display: 'flex', gap: 5, marginBottom: 3 }}>
167
+ <span
168
+ style={{
169
+ flex: 'none',
170
+ fontVariantNumeric: 'tabular-nums',
171
+ color: 'var(--od-accent)',
172
+ }}
173
+ >
174
+ {entry?.number ?? UNNUMBERED}
175
+ </span>
176
+ <span {...{ [FOOTNOTE_BODY_ATTR]: '' }} style={{ minWidth: 0 }}>
177
+ {content}
178
+ </span>
179
+ </div>
180
+ );
181
+ }
182
+
183
+ export function Footnotes({ notes, style, className }: FootnotesProps) {
184
+ const collector = useContext(FootnoteContext);
185
+ const vocabulary = useLabelVocabulary();
186
+ const list =
187
+ notes ?? collector?.ids.map((id) => ({ id, content: collector.contents.get(id) })) ?? [];
188
+ if (list.length === 0) return null;
189
+
190
+ return (
191
+ <div
192
+ {...{ [FOOTNOTES_ATTR]: '' }}
193
+ className={className}
194
+ style={{ ...footnoteAreaStyle(), ...style }}
195
+ >
196
+ {vocabulary.footnotes ? (
197
+ <div style={{ fontWeight: 600, marginBottom: 3 }}>{vocabulary.footnotes}</div>
198
+ ) : null}
199
+ {list.map((note) => (
200
+ <FootnoteRow key={note.id} id={note.id} content={note.content} />
201
+ ))}
202
+ </div>
203
+ );
204
+ }
@@ -0,0 +1,50 @@
1
+ import type { CSSProperties } from 'react';
2
+
3
+ export type ImagePlaceholderProps = {
4
+ /** What the final image should show — the user reads this when replacing it. */
5
+ hint: string;
6
+ width?: number | string;
7
+ height?: number | string;
8
+ style?: CSSProperties;
9
+ className?: string;
10
+ };
11
+
12
+ /**
13
+ * Stands in for an image the user has to supply (a chart from their data, a
14
+ * product screenshot). Use it instead of inventing filler — never for
15
+ * decoration.
16
+ */
17
+ export function ImagePlaceholder({
18
+ hint,
19
+ width = '100%',
20
+ height = 200,
21
+ style,
22
+ className,
23
+ }: ImagePlaceholderProps) {
24
+ return (
25
+ <div
26
+ data-od-image-placeholder=""
27
+ className={className}
28
+ style={{
29
+ width,
30
+ height,
31
+ display: 'flex',
32
+ alignItems: 'center',
33
+ justifyContent: 'center',
34
+ textAlign: 'center',
35
+ padding: 16,
36
+ boxSizing: 'border-box',
37
+ border: '1px dashed var(--od-rule, #d4d4d8)',
38
+ borderRadius: 'var(--od-radius, 6px)',
39
+ background: 'repeating-linear-gradient(45deg, rgba(0,0,0,.02) 0 8px, transparent 8px 16px)',
40
+ color: 'var(--od-muted, #6b7280)',
41
+ fontFamily: 'var(--od-font-body, system-ui, sans-serif)',
42
+ fontSize: 'var(--od-size-caption, 10px)',
43
+ lineHeight: 1.4,
44
+ ...style,
45
+ }}
46
+ >
47
+ {hint}
48
+ </div>
49
+ );
50
+ }