@atom63/resume 0.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.
- package/dist/chunk-FL25EF7U.js +384 -0
- package/dist/chunk-FL25EF7U.js.map +1 -0
- package/dist/editor/index.css +225 -0
- package/dist/editor/index.css.map +1 -0
- package/dist/editor/index.d.ts +35 -0
- package/dist/editor/index.js +127 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/index.css +334 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +232 -0
- package/dist/index.js +638 -0
- package/dist/index.js.map +1 -0
- package/dist/vite/index.d.ts +26 -0
- package/dist/vite/index.js +73 -0
- package/dist/vite/index.js.map +1 -0
- package/package.json +108 -0
- package/src/styles/css-modules.d.ts +5 -0
- package/src/styles/document.css +170 -0
- package/src/styles/styles.css +9 -0
- package/src/styles/styles.d.ts +3 -0
- package/src/styles/tokens.css +48 -0
- package/src/styles/tokens.d.ts +3 -0
- package/src/styles/viewer.css +205 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/* src/styles/tokens.css */
|
|
2
|
+
[data-resume-document] {
|
|
3
|
+
--paper-width: 8.5in;
|
|
4
|
+
--paper-height: 11in;
|
|
5
|
+
--paper-pad-x: 0.7in;
|
|
6
|
+
--paper-pad-y: 0.6in;
|
|
7
|
+
--paper-gap: 32px;
|
|
8
|
+
--doc-meta-col: 11rem;
|
|
9
|
+
--doc-resume-cols: 1fr 2fr;
|
|
10
|
+
--doc-col-gap: 0.75rem;
|
|
11
|
+
--doc-col-pad: 1.5rem;
|
|
12
|
+
--doc-block-gap: 0.375rem;
|
|
13
|
+
--doc-section-gap: 0.5rem;
|
|
14
|
+
--doc-group-gap: 0.125rem;
|
|
15
|
+
--doc-text: 10px;
|
|
16
|
+
--doc-text-xs: 8px;
|
|
17
|
+
--doc-leading: 1.4;
|
|
18
|
+
--doc-leading-heading: 1.5;
|
|
19
|
+
--doc-ink: #000;
|
|
20
|
+
--doc-ink-body: #404040;
|
|
21
|
+
--doc-ink-muted: #737373;
|
|
22
|
+
--doc-ink-faint: #a3a3a3;
|
|
23
|
+
--doc-rule: #d4d4d4;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* src/styles/document.css */
|
|
27
|
+
.doc-meta-grid {
|
|
28
|
+
display: grid;
|
|
29
|
+
min-width: 0;
|
|
30
|
+
grid-template-columns: var(--doc-meta-col) minmax(0, 1fr);
|
|
31
|
+
gap: var(--doc-col-gap);
|
|
32
|
+
}
|
|
33
|
+
.doc-resume-grid {
|
|
34
|
+
display: grid;
|
|
35
|
+
min-width: 0;
|
|
36
|
+
grid-template-columns: var(--doc-resume-cols);
|
|
37
|
+
}
|
|
38
|
+
.doc-col {
|
|
39
|
+
display: flex;
|
|
40
|
+
min-width: 0;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
gap: var(--doc-block-gap);
|
|
43
|
+
}
|
|
44
|
+
.doc-group {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
gap: var(--doc-group-gap);
|
|
48
|
+
}
|
|
49
|
+
.doc-section {
|
|
50
|
+
margin-top: var(--doc-section-gap);
|
|
51
|
+
}
|
|
52
|
+
.doc-body {
|
|
53
|
+
font-size: var(--doc-text);
|
|
54
|
+
line-height: var(--doc-leading);
|
|
55
|
+
color: var(--doc-ink-body);
|
|
56
|
+
overflow-wrap: break-word;
|
|
57
|
+
}
|
|
58
|
+
.doc-meta {
|
|
59
|
+
font-size: var(--doc-text);
|
|
60
|
+
line-height: var(--doc-leading);
|
|
61
|
+
color: var(--doc-ink-muted);
|
|
62
|
+
}
|
|
63
|
+
.doc-heading {
|
|
64
|
+
font-size: var(--doc-text);
|
|
65
|
+
line-height: var(--doc-leading-heading);
|
|
66
|
+
color: var(--doc-ink);
|
|
67
|
+
font-weight: 700;
|
|
68
|
+
text-transform: uppercase;
|
|
69
|
+
}
|
|
70
|
+
.doc-title {
|
|
71
|
+
font-size: var(--doc-text);
|
|
72
|
+
line-height: 1;
|
|
73
|
+
color: var(--doc-ink);
|
|
74
|
+
font-weight: 700;
|
|
75
|
+
text-transform: uppercase;
|
|
76
|
+
}
|
|
77
|
+
.doc-subheading {
|
|
78
|
+
font-size: var(--doc-text);
|
|
79
|
+
line-height: 1;
|
|
80
|
+
color: var(--doc-ink);
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
}
|
|
83
|
+
.doc-strong {
|
|
84
|
+
color: var(--doc-ink);
|
|
85
|
+
font-weight: 600;
|
|
86
|
+
}
|
|
87
|
+
.doc-link {
|
|
88
|
+
color: var(--doc-ink-body);
|
|
89
|
+
text-decoration: underline;
|
|
90
|
+
touch-action: manipulation;
|
|
91
|
+
}
|
|
92
|
+
.doc-rule {
|
|
93
|
+
margin: 0.25rem 0;
|
|
94
|
+
border-bottom: 1px solid var(--doc-rule);
|
|
95
|
+
}
|
|
96
|
+
.doc-footer {
|
|
97
|
+
font-size: var(--doc-text-xs);
|
|
98
|
+
color: var(--doc-ink-faint);
|
|
99
|
+
margin-top: auto;
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: flex-end;
|
|
102
|
+
justify-content: space-between;
|
|
103
|
+
}
|
|
104
|
+
.doc-footer a {
|
|
105
|
+
color: inherit;
|
|
106
|
+
}
|
|
107
|
+
.doc-header {
|
|
108
|
+
margin-bottom: 0.75rem;
|
|
109
|
+
}
|
|
110
|
+
.doc-header-col {
|
|
111
|
+
display: flex;
|
|
112
|
+
min-width: 0;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
justify-content: space-between;
|
|
115
|
+
}
|
|
116
|
+
.doc-header-left {
|
|
117
|
+
padding-right: var(--doc-col-pad);
|
|
118
|
+
}
|
|
119
|
+
.doc-header-right {
|
|
120
|
+
padding-left: var(--doc-col-pad);
|
|
121
|
+
}
|
|
122
|
+
.doc-header-right > p + p {
|
|
123
|
+
margin-top: 0.125rem;
|
|
124
|
+
}
|
|
125
|
+
.doc-links {
|
|
126
|
+
margin-top: 0.75rem;
|
|
127
|
+
}
|
|
128
|
+
.doc-links > p {
|
|
129
|
+
margin-top: 0;
|
|
130
|
+
color: var(--doc-ink-muted);
|
|
131
|
+
}
|
|
132
|
+
.doc-links a {
|
|
133
|
+
color: inherit;
|
|
134
|
+
}
|
|
135
|
+
.doc-links em {
|
|
136
|
+
color: var(--doc-ink-faint);
|
|
137
|
+
}
|
|
138
|
+
.doc-entry-body {
|
|
139
|
+
min-width: 0;
|
|
140
|
+
}
|
|
141
|
+
.doc-col-sidebar {
|
|
142
|
+
grid-column-start: 1;
|
|
143
|
+
grid-row-start: 1;
|
|
144
|
+
padding-right: var(--doc-col-pad);
|
|
145
|
+
}
|
|
146
|
+
.doc-col-main {
|
|
147
|
+
grid-column-start: 2;
|
|
148
|
+
grid-row-start: 1;
|
|
149
|
+
padding-left: var(--doc-col-pad);
|
|
150
|
+
}
|
|
151
|
+
.doc-list {
|
|
152
|
+
list-style: disc;
|
|
153
|
+
padding-left: 0.75rem;
|
|
154
|
+
}
|
|
155
|
+
.doc-em {
|
|
156
|
+
font-style: normal;
|
|
157
|
+
}
|
|
158
|
+
.doc-hr {
|
|
159
|
+
margin: 0.75rem 0;
|
|
160
|
+
}
|
|
161
|
+
.doc-pages {
|
|
162
|
+
display: flex;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
}
|
|
165
|
+
.doc-page {
|
|
166
|
+
position: relative;
|
|
167
|
+
display: flex;
|
|
168
|
+
flex-direction: column;
|
|
169
|
+
background: #fff;
|
|
170
|
+
color: #000;
|
|
171
|
+
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* src/editor/editor.css */
|
|
175
|
+
.resume-editor {
|
|
176
|
+
display: flex;
|
|
177
|
+
flex-direction: row;
|
|
178
|
+
width: 100%;
|
|
179
|
+
height: 100%;
|
|
180
|
+
min-height: 0;
|
|
181
|
+
}
|
|
182
|
+
.resume-editor-pane {
|
|
183
|
+
flex: 1 1 50%;
|
|
184
|
+
min-width: 0;
|
|
185
|
+
min-height: 0;
|
|
186
|
+
overflow: auto;
|
|
187
|
+
}
|
|
188
|
+
.resume-editor-source {
|
|
189
|
+
display: block;
|
|
190
|
+
width: 100%;
|
|
191
|
+
height: 100%;
|
|
192
|
+
box-sizing: border-box;
|
|
193
|
+
resize: none;
|
|
194
|
+
border: none;
|
|
195
|
+
outline: none;
|
|
196
|
+
padding: 1rem;
|
|
197
|
+
font-family:
|
|
198
|
+
ui-monospace,
|
|
199
|
+
SFMono-Regular,
|
|
200
|
+
Menlo,
|
|
201
|
+
Monaco,
|
|
202
|
+
Consolas,
|
|
203
|
+
monospace;
|
|
204
|
+
font-size: 0.8125rem;
|
|
205
|
+
line-height: 1.5;
|
|
206
|
+
tab-size: 2;
|
|
207
|
+
}
|
|
208
|
+
.resume-editor-preview {
|
|
209
|
+
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
|
210
|
+
}
|
|
211
|
+
.resume-mdx-live-empty {
|
|
212
|
+
display: flex;
|
|
213
|
+
align-items: center;
|
|
214
|
+
justify-content: center;
|
|
215
|
+
height: 100%;
|
|
216
|
+
padding: 2rem;
|
|
217
|
+
font-size: 0.875rem;
|
|
218
|
+
opacity: 0.6;
|
|
219
|
+
}
|
|
220
|
+
.resume-mdx-live-error {
|
|
221
|
+
padding: 1rem;
|
|
222
|
+
font-size: 0.875rem;
|
|
223
|
+
color: #b00020;
|
|
224
|
+
}
|
|
225
|
+
/*# sourceMappingURL=index.css.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/styles/tokens.css","../../src/styles/document.css","../../src/editor/editor.css"],"sourcesContent":["/*\n * @atom63/resume — paper/document design tokens.\n *\n * The single source of truth for the resume/CV look. Override any token (e.g.\n * in your own stylesheet, scoped to [data-resume-document]) to restyle the\n * resume and CV together; the React primitives only compose these.\n *\n * --paper-* : page geometry (defaults: US Letter @ 96dpi; override for A4).\n * --doc-* : document language (layout, type, ink).\n *\n * Print-fixed by design: doc sizes are absolute px for PDF fidelity, independent\n * of any host typography-scale setting. The font *family* is inherited (so the\n * document follows the host font), which is why pagination re-measures on font\n * change.\n *\n * NOTE: the numeric --paper-* defaults and --doc-block-gap MUST stay in sync with\n * geometry.ts (the JS packer reads the same numbers to compute page breaks).\n */\n[data-resume-document] {\n /* Page geometry (US Letter @ 96dpi — see geometry.ts) */\n --paper-width: 8.5in;\n --paper-height: 11in;\n --paper-pad-x: 0.7in;\n --paper-pad-y: 0.6in;\n --paper-gap: 32px; /* screen-only gap between page frames */\n\n /* Document layout */\n --doc-meta-col: 11rem; /* left meta/label column (header name, section label, year|role) */\n --doc-resume-cols: 1fr 2fr; /* resume sidebar | main split */\n --doc-col-gap: 0.75rem; /* gap between the meta column and content */\n --doc-col-pad: 1.5rem; /* resume two-column inner gutter */\n --doc-block-gap: 0.375rem; /* between blocks within a column (== BLOCK_GAP_PX) */\n --doc-section-gap: 0.5rem; /* space above a section */\n --doc-group-gap: 0.125rem; /* within a tight group (title + meta line) */\n\n /* Type */\n --doc-text: 10px;\n --doc-text-xs: 8px;\n --doc-leading: 1.4;\n --doc-leading-heading: 1.5;\n\n /* Ink (on the white page) */\n --doc-ink: #000; /* headings, strong */\n --doc-ink-body: #404040; /* body copy (neutral-700) */\n --doc-ink-muted: #737373; /* meta / labels (neutral-500) */\n --doc-ink-faint: #a3a3a3; /* footer, faint detail (neutral-400) */\n --doc-rule: #d4d4d4; /* dividers (neutral-300) */\n}\n","/*\n * Resume & CV document layout + type primitives. These classes only COMPOSE the\n * design tokens — the token vocabulary itself (the paper and doc custom\n * properties) lives in tokens.css (imported separately, or together via\n * styles.css). Keep this file token-free so consumers can adopt just the tokens,\n * or restyle without forking these classes.\n */\n\n/* Layout primitives */\n.doc-meta-grid {\n display: grid;\n min-width: 0;\n grid-template-columns: var(--doc-meta-col) minmax(0, 1fr);\n gap: var(--doc-col-gap);\n}\n.doc-resume-grid {\n display: grid;\n min-width: 0;\n grid-template-columns: var(--doc-resume-cols);\n}\n.doc-col {\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: var(--doc-block-gap);\n}\n.doc-group {\n display: flex;\n flex-direction: column;\n gap: var(--doc-group-gap);\n}\n.doc-section {\n margin-top: var(--doc-section-gap);\n}\n\n/* Type primitives */\n.doc-body {\n font-size: var(--doc-text);\n line-height: var(--doc-leading);\n color: var(--doc-ink-body);\n overflow-wrap: break-word;\n}\n.doc-meta {\n font-size: var(--doc-text);\n line-height: var(--doc-leading);\n color: var(--doc-ink-muted);\n}\n.doc-heading {\n font-size: var(--doc-text);\n line-height: var(--doc-leading-heading);\n color: var(--doc-ink);\n font-weight: 700;\n text-transform: uppercase;\n}\n.doc-title {\n font-size: var(--doc-text);\n line-height: 1;\n color: var(--doc-ink);\n font-weight: 700;\n text-transform: uppercase;\n}\n.doc-subheading {\n font-size: var(--doc-text);\n line-height: 1;\n color: var(--doc-ink);\n font-weight: 600;\n}\n.doc-strong {\n color: var(--doc-ink);\n font-weight: 600;\n}\n.doc-link {\n color: var(--doc-ink-body);\n text-decoration: underline;\n /* touch-manipulation equivalent for links */\n touch-action: manipulation;\n}\n.doc-rule {\n margin: 0.25rem 0;\n border-bottom: 1px solid var(--doc-rule);\n}\n.doc-footer {\n font-size: var(--doc-text-xs);\n color: var(--doc-ink-faint);\n /* Layout (replacing Tailwind utilities) */\n margin-top: auto;\n display: flex;\n align-items: flex-end;\n justify-content: space-between;\n}\n.doc-footer a {\n color: inherit;\n}\n\n/* Header / layout helpers (replacing Tailwind utilities) */\n.doc-header {\n margin-bottom: 0.75rem;\n}\n.doc-header-col {\n display: flex;\n min-width: 0;\n flex-direction: column;\n justify-content: space-between;\n}\n.doc-header-left {\n padding-right: var(--doc-col-pad);\n}\n.doc-header-right {\n padding-left: var(--doc-col-pad);\n}\n.doc-header-right > p + p {\n margin-top: 0.125rem;\n}\n\n.doc-links {\n margin-top: 0.75rem;\n}\n.doc-links > p {\n margin-top: 0;\n color: var(--doc-ink-muted);\n}\n.doc-links a {\n color: inherit;\n}\n.doc-links em {\n color: var(--doc-ink-faint);\n}\n\n.doc-entry-body {\n min-width: 0;\n}\n\n/* Two-column grid placement (sidebar | main), used on every page incl. continuations */\n.doc-col-sidebar {\n grid-column-start: 1;\n grid-row-start: 1;\n padding-right: var(--doc-col-pad);\n}\n.doc-col-main {\n grid-column-start: 2;\n grid-row-start: 1;\n padding-left: var(--doc-col-pad);\n}\n\n.doc-list {\n list-style: disc;\n padding-left: 0.75rem;\n}\n.doc-em {\n font-style: normal;\n}\n.doc-hr {\n margin: 0.75rem 0;\n}\n\n/* Page frame (Tailwind shadow-lg + bg-white + text-black + flex column) */\n.doc-pages {\n display: flex;\n flex-direction: column;\n}\n.doc-page {\n position: relative;\n display: flex;\n flex-direction: column;\n background: #fff;\n color: #000;\n box-shadow:\n 0 10px 15px -3px rgb(0 0 0 / 0.1),\n 0 4px 6px -4px rgb(0 0 0 / 0.1);\n}\n","/* packages/resume/src/editor/editor.css\n Minimal, Tailwind-free layout for the split-pane resume editor. */\n\n.resume-editor {\n display: flex;\n flex-direction: row;\n width: 100%;\n height: 100%;\n min-height: 0;\n}\n\n.resume-editor-pane {\n flex: 1 1 50%;\n min-width: 0;\n min-height: 0;\n overflow: auto;\n}\n\n.resume-editor-source {\n display: block;\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n resize: none;\n border: none;\n outline: none;\n padding: 1rem;\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n font-size: 0.8125rem;\n line-height: 1.5;\n tab-size: 2;\n}\n\n.resume-editor-preview {\n border-left: 1px solid rgba(0, 0, 0, 0.1);\n}\n\n.resume-mdx-live-empty {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 2rem;\n font-size: 0.875rem;\n opacity: 0.6;\n}\n\n.resume-mdx-live-error {\n padding: 1rem;\n font-size: 0.875rem;\n color: #b00020;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ComponentType } from 'react';
|
|
3
|
+
|
|
4
|
+
interface MdxLivePreviewProps {
|
|
5
|
+
source: string;
|
|
6
|
+
components: Record<string, unknown>;
|
|
7
|
+
onError?: (error: Error | null) => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Renders runtime-compiled MDX inside the given component scope, isolating
|
|
11
|
+
* runtime throws in an error boundary. Compile errors are surfaced via onError
|
|
12
|
+
* while the last good render stays on screen.
|
|
13
|
+
*/
|
|
14
|
+
declare function MdxLivePreview({ source, components, onError }: MdxLivePreviewProps): react_jsx_runtime.JSX.Element;
|
|
15
|
+
|
|
16
|
+
interface ResumeEditorProps {
|
|
17
|
+
/** Current MDX source. */
|
|
18
|
+
source: string;
|
|
19
|
+
/** Called when the user edits the source. */
|
|
20
|
+
onChange: (next: string) => void;
|
|
21
|
+
/** Compile-error callback. */
|
|
22
|
+
onError?: (error: Error) => void;
|
|
23
|
+
/** Optional reset handler (host decides what reset means). */
|
|
24
|
+
onReset?: () => void;
|
|
25
|
+
/** Override the MDX component map; defaults to resumeMdxComponents. */
|
|
26
|
+
components?: Record<string, ComponentType<unknown>>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Host-agnostic, controlled split-pane MDX editor for resumes: a source
|
|
30
|
+
* textarea on the left and a live, paginated preview on the right. Carries no
|
|
31
|
+
* OS63 chrome — the host owns the surrounding shell.
|
|
32
|
+
*/
|
|
33
|
+
declare function ResumeEditor({ source, onChange, onError, components }: ResumeEditorProps): react_jsx_runtime.JSX.Element;
|
|
34
|
+
|
|
35
|
+
export { MdxLivePreview, ResumeEditor, type ResumeEditorProps };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { resumeMdxComponents } from '../chunk-FL25EF7U.js';
|
|
2
|
+
import { MDXProvider } from '@mdx-js/react';
|
|
3
|
+
import { useEffect, useState, useRef, Component } from 'react';
|
|
4
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
var evaluatePromise = null;
|
|
7
|
+
function loadEvaluate() {
|
|
8
|
+
if (!evaluatePromise) {
|
|
9
|
+
evaluatePromise = import('@mdx-js/mdx').then((m) => m.evaluate);
|
|
10
|
+
}
|
|
11
|
+
return evaluatePromise;
|
|
12
|
+
}
|
|
13
|
+
async function compileMdx(source) {
|
|
14
|
+
const [evaluate, runtime, mdxReact] = await Promise.all([
|
|
15
|
+
loadEvaluate(),
|
|
16
|
+
import('react/jsx-runtime'),
|
|
17
|
+
import('@mdx-js/react')
|
|
18
|
+
]);
|
|
19
|
+
const mod = await evaluate(source, {
|
|
20
|
+
...runtime,
|
|
21
|
+
useMDXComponents: mdxReact.useMDXComponents,
|
|
22
|
+
baseUrl: import.meta.url
|
|
23
|
+
});
|
|
24
|
+
return mod.default;
|
|
25
|
+
}
|
|
26
|
+
function useMdxRuntime(source, debounceMs = 300) {
|
|
27
|
+
const [state, setState] = useState({
|
|
28
|
+
Component: null,
|
|
29
|
+
error: null,
|
|
30
|
+
status: "idle"
|
|
31
|
+
});
|
|
32
|
+
const lastGood = useRef(null);
|
|
33
|
+
const reqId = useRef(0);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const trimmed = source.trim();
|
|
36
|
+
if (!trimmed) {
|
|
37
|
+
setState({ Component: null, error: null, status: "idle" });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const id = ++reqId.current;
|
|
41
|
+
setState((s) => ({ ...s, status: "compiling" }));
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
compileMdx(trimmed).then((Component) => {
|
|
44
|
+
if (id !== reqId.current) return;
|
|
45
|
+
lastGood.current = Component;
|
|
46
|
+
setState({ Component, error: null, status: "ready" });
|
|
47
|
+
}).catch((e) => {
|
|
48
|
+
if (id !== reqId.current) return;
|
|
49
|
+
setState({
|
|
50
|
+
Component: lastGood.current,
|
|
51
|
+
error: e instanceof Error ? e : new Error(String(e)),
|
|
52
|
+
status: "error"
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}, debounceMs);
|
|
56
|
+
return () => clearTimeout(timer);
|
|
57
|
+
}, [source, debounceMs]);
|
|
58
|
+
return state;
|
|
59
|
+
}
|
|
60
|
+
var MdxErrorBoundary = class extends Component {
|
|
61
|
+
state = { err: null };
|
|
62
|
+
static getDerivedStateFromError(err) {
|
|
63
|
+
return { err };
|
|
64
|
+
}
|
|
65
|
+
componentDidUpdate(prev) {
|
|
66
|
+
if (prev.resetKey !== this.props.resetKey && this.state.err) {
|
|
67
|
+
this.setState({ err: null });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
render() {
|
|
71
|
+
return this.state.err ? this.props.fallback(this.state.err) : this.props.children;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
function MdxLivePreview({ source, components, onError }) {
|
|
75
|
+
const { Component, error } = useMdxRuntime(source);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
onError?.(error);
|
|
78
|
+
}, [error]);
|
|
79
|
+
if (!Component) {
|
|
80
|
+
return /* @__PURE__ */ jsx("div", { className: "resume-mdx-live-empty", children: "Start typing MDX\u2026" });
|
|
81
|
+
}
|
|
82
|
+
return /* @__PURE__ */ jsx(
|
|
83
|
+
MdxErrorBoundary,
|
|
84
|
+
{
|
|
85
|
+
fallback: (e) => /* @__PURE__ */ jsxs("div", { className: "resume-mdx-live-error", children: [
|
|
86
|
+
"Runtime error: ",
|
|
87
|
+
e.message
|
|
88
|
+
] }),
|
|
89
|
+
resetKey: source,
|
|
90
|
+
children: /* @__PURE__ */ jsx(MDXProvider, { components, children: /* @__PURE__ */ jsx(Component, {}) })
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
function ensureResumeDocument(source) {
|
|
95
|
+
return source.includes("<ResumeDocument>") ? source : `<ResumeDocument>
|
|
96
|
+
|
|
97
|
+
${source.trim()}
|
|
98
|
+
|
|
99
|
+
</ResumeDocument>`;
|
|
100
|
+
}
|
|
101
|
+
function ResumeEditor({ source, onChange, onError, components }) {
|
|
102
|
+
const map = components ?? resumeMdxComponents;
|
|
103
|
+
return /* @__PURE__ */ jsxs("div", { className: "resume-editor", children: [
|
|
104
|
+
/* @__PURE__ */ jsx("div", { className: "resume-editor-pane", children: /* @__PURE__ */ jsx(
|
|
105
|
+
"textarea",
|
|
106
|
+
{
|
|
107
|
+
className: "resume-editor-source",
|
|
108
|
+
value: source,
|
|
109
|
+
onChange: (e) => onChange(e.target.value),
|
|
110
|
+
spellCheck: false,
|
|
111
|
+
"aria-label": "Resume MDX source"
|
|
112
|
+
}
|
|
113
|
+
) }),
|
|
114
|
+
/* @__PURE__ */ jsx("div", { className: "resume-editor-pane resume-editor-preview", children: /* @__PURE__ */ jsx(
|
|
115
|
+
MdxLivePreview,
|
|
116
|
+
{
|
|
117
|
+
source: ensureResumeDocument(source),
|
|
118
|
+
components: map,
|
|
119
|
+
onError: onError ? (e) => onError(e ?? new Error("MDX compile error")) : void 0
|
|
120
|
+
}
|
|
121
|
+
) })
|
|
122
|
+
] });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export { MdxLivePreview, ResumeEditor };
|
|
126
|
+
//# sourceMappingURL=index.js.map
|
|
127
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/editor/mdx-live-preview.tsx","../../src/editor/resume-editor.tsx"],"names":["ReactComponent","jsxs","jsx"],"mappings":"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","file":"index.js","sourcesContent":["// packages/resume/src/editor/mdx-live-preview.tsx\n//\n// Vendored from @atom63/mdx's MdxLivePreview (+ its useMdxRuntime helper),\n// with all @atom63/* imports stripped. Depends only on react, @mdx-js/mdx,\n// and @mdx-js/react — all already declared as resume package dependencies.\nimport { MDXProvider } from '@mdx-js/react'\nimport type { ComponentType, ReactNode } from 'react'\nimport { Component as ReactComponent, useEffect, useRef, useState } from 'react'\n\ntype MdxComponent = ComponentType<{ components?: Record<string, unknown> }>\n\n// biome-ignore lint/suspicious/noExplicitAny: @mdx-js/mdx evaluate is loaded dynamically\nlet evaluatePromise: Promise<any> | null = null\nfunction loadEvaluate() {\n if (!evaluatePromise) {\n evaluatePromise = import('@mdx-js/mdx').then(m => m.evaluate)\n }\n return evaluatePromise\n}\n\n/**\n * Compile an MDX string to a React component at runtime. Lazy-loads the MDX\n * compiler so it never enters the base bundle. The returned component renders\n * inside an `<MDXProvider>` (or accepts a `components` prop) for scope.\n */\nasync function compileMdx(source: string): Promise<MdxComponent> {\n const [evaluate, runtime, mdxReact] = await Promise.all([\n loadEvaluate(),\n import('react/jsx-runtime'),\n import('@mdx-js/react'),\n ])\n const mod = await evaluate(source, {\n ...(runtime as Record<string, unknown>),\n useMDXComponents: mdxReact.useMDXComponents,\n baseUrl: import.meta.url,\n })\n return mod.default as MdxComponent\n}\n\ninterface MdxRuntimeResult {\n Component: MdxComponent | null\n error: Error | null\n status: 'idle' | 'compiling' | 'ready' | 'error'\n}\n\n/**\n * Debounced runtime compile of an MDX string. Keeps the last successfully\n * compiled component while compiling and on error, so a transient typo never\n * blanks the preview.\n */\nfunction useMdxRuntime(source: string, debounceMs = 300): MdxRuntimeResult {\n const [state, setState] = useState<MdxRuntimeResult>({\n Component: null,\n error: null,\n status: 'idle',\n })\n const lastGood = useRef<MdxComponent | null>(null)\n const reqId = useRef(0)\n\n useEffect(() => {\n const trimmed = source.trim()\n if (!trimmed) {\n setState({ Component: null, error: null, status: 'idle' })\n return\n }\n const id = ++reqId.current\n setState(s => ({ ...s, status: 'compiling' }))\n const timer = setTimeout(() => {\n compileMdx(trimmed)\n .then(Component => {\n if (id !== reqId.current) return\n lastGood.current = Component\n setState({ Component, error: null, status: 'ready' })\n })\n .catch((e: unknown) => {\n if (id !== reqId.current) return\n setState({\n Component: lastGood.current,\n error: e instanceof Error ? e : new Error(String(e)),\n status: 'error',\n })\n })\n }, debounceMs)\n return () => clearTimeout(timer)\n }, [source, debounceMs])\n\n return state\n}\n\nclass MdxErrorBoundary extends ReactComponent<\n { children: ReactNode; resetKey: string; fallback: (e: Error) => ReactNode },\n { err: Error | null }\n> {\n state: { err: Error | null } = { err: null }\n static getDerivedStateFromError(err: Error) {\n return { err }\n }\n componentDidUpdate(prev: { resetKey: string }) {\n if (prev.resetKey !== this.props.resetKey && this.state.err) {\n this.setState({ err: null })\n }\n }\n render() {\n return this.state.err ? this.props.fallback(this.state.err) : this.props.children\n }\n}\n\nexport interface MdxLivePreviewProps {\n source: string\n components: Record<string, unknown>\n onError?: (error: Error | null) => void\n}\n\n/**\n * Renders runtime-compiled MDX inside the given component scope, isolating\n * runtime throws in an error boundary. Compile errors are surfaced via onError\n * while the last good render stays on screen.\n */\nexport function MdxLivePreview({ source, components, onError }: MdxLivePreviewProps) {\n const { Component, error } = useMdxRuntime(source)\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: onError is a stable callback from the host\n useEffect(() => {\n onError?.(error)\n }, [error])\n\n if (!Component) {\n return <div className=\"resume-mdx-live-empty\">Start typing MDX…</div>\n }\n\n return (\n <MdxErrorBoundary\n fallback={e => <div className=\"resume-mdx-live-error\">Runtime error: {e.message}</div>}\n resetKey={source}\n >\n <MDXProvider components={components as Record<string, ComponentType>}>\n <Component />\n </MDXProvider>\n </MdxErrorBoundary>\n )\n}\n","// packages/resume/src/editor/resume-editor.tsx\nimport type { ComponentType } from 'react'\nimport { resumeMdxComponents } from '../document/components'\nimport './editor.css'\nimport { MdxLivePreview } from './mdx-live-preview'\n\n// The pagination engine activates via the <ResumeDocument> wrapper. If an edited\n// draft drops it, wrap the whole document so the live preview still paginates\n// (instead of rendering as an unpaginated flow with no page background).\nfunction ensureResumeDocument(source: string): string {\n return source.includes('<ResumeDocument>')\n ? source\n : `<ResumeDocument>\\n\\n${source.trim()}\\n\\n</ResumeDocument>`\n}\n\nexport interface ResumeEditorProps {\n /** Current MDX source. */\n source: string\n /** Called when the user edits the source. */\n onChange: (next: string) => void\n /** Compile-error callback. */\n onError?: (error: Error) => void\n /** Optional reset handler (host decides what reset means). */\n onReset?: () => void\n /** Override the MDX component map; defaults to resumeMdxComponents. */\n components?: Record<string, ComponentType<unknown>>\n}\n\n/**\n * Host-agnostic, controlled split-pane MDX editor for resumes: a source\n * textarea on the left and a live, paginated preview on the right. Carries no\n * OS63 chrome — the host owns the surrounding shell.\n */\nexport function ResumeEditor({ source, onChange, onError, components }: ResumeEditorProps) {\n const map = components ?? (resumeMdxComponents as Record<string, ComponentType<unknown>>)\n\n return (\n <div className=\"resume-editor\">\n <div className=\"resume-editor-pane\">\n <textarea\n className=\"resume-editor-source\"\n value={source}\n onChange={e => onChange(e.target.value)}\n spellCheck={false}\n aria-label=\"Resume MDX source\"\n />\n </div>\n <div className=\"resume-editor-pane resume-editor-preview\">\n <MdxLivePreview\n source={ensureResumeDocument(source)}\n components={map}\n onError={onError ? e => onError(e ?? new Error('MDX compile error')) : undefined}\n />\n </div>\n </div>\n )\n}\n"]}
|