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.
- package/README.md +16 -15
- package/bin.js +2 -0
- package/dist/build-C7NW_3Pk.js +14 -0
- package/dist/check-CP4873Wx.js +41 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +228 -0
- package/dist/config-DPm1BBAb.js +2619 -0
- package/dist/config-TlTe7Ona.d.ts +24 -0
- package/dist/context-BqsdSrAQ.js +1084 -0
- package/dist/dev-Biz42qlu.js +17 -0
- package/dist/diagram-xlVDekYk.js +763 -0
- package/dist/export-Bi6nuxjT.js +31 -0
- package/dist/import-D2jNB07F.js +25 -0
- package/dist/index.d.ts +455 -0
- package/dist/index.js +693 -0
- package/dist/{cli.js → init-Bbtj2pxF.js} +34 -68
- package/dist/preview-CLm51aRt.js +19 -0
- package/dist/sdk-DjpX6mCv.js +51 -0
- package/dist/vite/index.d.ts +25 -0
- package/dist/vite/index.js +2 -0
- package/env.d.ts +83 -0
- package/package.json +59 -12
- package/{template/.agents/skills → skills}/create-doc/SKILL.md +1 -1
- package/{template/.agents/skills → skills}/create-theme/SKILL.md +1 -1
- package/{template/.agents/skills → skills}/doc-authoring/SKILL.md +8 -9
- package/{template/.agents/skills → skills}/doc-authoring/references/assets.md +1 -1
- package/{template/.agents/skills → skills}/doc-authoring/references/design-system.md +1 -1
- package/{template/.agents/skills → skills}/doc-authoring/references/long-form.md +4 -4
- package/{template/.agents/skills → skills}/doc-authoring/references/pagination.md +1 -1
- package/src/app/app.tsx +42 -0
- package/src/app/components/data-table.tsx +196 -0
- package/src/app/components/design-panel/design-panel.tsx +318 -0
- package/src/app/components/design-panel/design-provider.tsx +121 -0
- package/src/app/components/design-panel/use-design.ts +85 -0
- package/src/app/components/diagram.tsx +76 -0
- package/src/app/components/doc-assets.tsx +129 -0
- package/src/app/components/doc-search.tsx +248 -0
- package/src/app/components/doc-sidebar.tsx +162 -0
- package/src/app/components/flow-page.tsx +93 -0
- package/src/app/components/footnote.tsx +204 -0
- package/src/app/components/image-placeholder.tsx +50 -0
- package/src/app/components/inspector/inspector.tsx +518 -0
- package/src/app/components/numbering.tsx +224 -0
- package/src/app/components/page-frame.tsx +70 -0
- package/src/app/components/sidebar/folder-item.tsx +212 -0
- package/src/app/components/sidebar/icon-picker.tsx +99 -0
- package/src/app/components/sidebar/sidebar.tsx +252 -0
- package/src/app/components/table-of-contents.tsx +93 -0
- package/src/app/components/theme-toggle.tsx +50 -0
- package/src/app/components/themes/markdown.tsx +249 -0
- package/src/app/components/themes/theme-preview.tsx +74 -0
- package/src/app/components/ui/menu.tsx +143 -0
- package/src/app/index.html +12 -0
- package/src/app/lib/agent-bridge.ts +140 -0
- package/src/app/lib/assets.ts +151 -0
- package/src/app/lib/design-presets.ts +109 -0
- package/src/app/lib/design.ts +88 -0
- package/src/app/lib/diagnostics.ts +282 -0
- package/src/app/lib/doc-preview.tsx +29 -0
- package/src/app/lib/docs.ts +26 -0
- package/src/app/lib/docx/extract.ts +1623 -0
- package/src/app/lib/docx/fonts.test.ts +136 -0
- package/src/app/lib/docx/fonts.ts +166 -0
- package/src/app/lib/docx/media.ts +102 -0
- package/src/app/lib/docx/model.ts +206 -0
- package/src/app/lib/docx/paragraph.test.ts +92 -0
- package/src/app/lib/docx/paragraph.ts +107 -0
- package/src/app/lib/docx/props.ts +187 -0
- package/src/app/lib/docx/styles.ts +306 -0
- package/src/app/lib/docx/units.ts +35 -0
- package/src/app/lib/docx/write.test.ts +507 -0
- package/src/app/lib/docx/write.ts +581 -0
- package/src/app/lib/docx/xml.ts +39 -0
- package/src/app/lib/export-docx.ts +289 -0
- package/src/app/lib/export-dom.ts +318 -0
- package/src/app/lib/export-html.ts +156 -0
- package/src/app/lib/export-image.ts +70 -0
- package/src/app/lib/export-pdf.ts +165 -0
- package/src/app/lib/flow-measure.test.ts +31 -0
- package/src/app/lib/flow-measure.ts +183 -0
- package/src/app/lib/flow.test.ts +110 -0
- package/src/app/lib/flow.ts +136 -0
- package/src/app/lib/folders.ts +192 -0
- package/src/app/lib/footnotes.test.tsx +102 -0
- package/src/app/lib/footnotes.ts +94 -0
- package/src/app/lib/inspector/fiber.ts +99 -0
- package/src/app/lib/labels.test.ts +18 -0
- package/src/app/lib/labels.ts +181 -0
- package/src/app/lib/outline.ts +118 -0
- package/src/app/lib/page-context.tsx +43 -0
- package/src/app/lib/page-range.test.ts +95 -0
- package/src/app/lib/page-range.ts +90 -0
- package/src/app/lib/print-ready.ts +69 -0
- package/src/app/lib/rasterize.ts +173 -0
- package/src/app/lib/scan.ts +26 -0
- package/src/app/lib/sdk.test.ts +32 -0
- package/src/app/lib/sdk.ts +115 -0
- package/src/app/lib/themes.ts +31 -0
- package/src/app/lib/use-doc-module.ts +53 -0
- package/src/app/lib/use-doc-pages.ts +147 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/lib/view-mode.test.ts +91 -0
- package/src/app/lib/view-mode.ts +104 -0
- package/src/app/main.tsx +14 -0
- package/src/app/routes/assets.tsx +257 -0
- package/src/app/routes/doc.tsx +877 -0
- package/src/app/routes/home-shell.tsx +203 -0
- package/src/app/routes/home.tsx +269 -0
- package/src/app/routes/themes.tsx +121 -0
- package/src/app/styles.css +97 -0
- package/src/app/virtual.d.ts +30 -0
- package/template/AGENTS.md +1 -1
- package/template/README.md +24 -52
- package/template/docs/getting-started/index.tsx +2 -2
- package/template/mosage.config.ts +1 -1
- package/template/package.json +1 -1
- package/template/tsconfig.json +1 -1
- package/README.zh-TW.md +0 -28
- /package/{template/.agents/skills → skills}/apply-comments/SKILL.md +0 -0
- /package/{template/.agents/skills → skills}/current-doc/SKILL.md +0 -0
- /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
|
+
}
|