@hwp-editor/react 1.0.0-rc.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/index.cjs ADDED
@@ -0,0 +1,1778 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ COMPOSE_PRESETS: () => COMPOSE_PRESETS,
24
+ ComposePanel: () => ComposePanel,
25
+ FieldsPanel: () => FieldsPanel,
26
+ HwpEditor: () => HwpEditor,
27
+ PageCanvas: () => PageCanvas,
28
+ SegmentInspector: () => SegmentInspector,
29
+ TableGrid: () => TableGrid,
30
+ bodyToBlocks: () => bodyToBlocks,
31
+ buildDocumentSpec: () => buildDocumentSpec,
32
+ classifyEngineError: () => classifyEngineError,
33
+ clickOffset: () => clickOffset,
34
+ createT: () => createT,
35
+ en: () => en,
36
+ extractFieldSlots: () => extractFieldSlots,
37
+ findTables: () => findTables,
38
+ isTableSlice: () => isTableSlice,
39
+ ko: () => ko,
40
+ nearestSegment: () => nearestSegment,
41
+ parseMarkdownTable: () => parseMarkdownTable,
42
+ plainSegmentText: () => plainSegmentText,
43
+ sanitizeSvg: () => sanitizeSvg,
44
+ segmentBand: () => segmentBand,
45
+ tableAtRef: () => tableAtRef,
46
+ useHwpEditorContext: () => useHwpEditorContext
47
+ });
48
+ module.exports = __toCommonJS(index_exports);
49
+
50
+ // src/HwpEditor.tsx
51
+ var import_react7 = require("react");
52
+ var import_core8 = require("@hwp-editor/core");
53
+
54
+ // src/context.ts
55
+ var import_react = require("react");
56
+ var HwpEditorContext = (0, import_react.createContext)(
57
+ null
58
+ );
59
+ function useHwpEditorContext() {
60
+ const value = (0, import_react.useContext)(HwpEditorContext);
61
+ if (value === null) {
62
+ throw new Error("hwp-editor panels must be rendered inside <HwpEditor>");
63
+ }
64
+ return value;
65
+ }
66
+
67
+ // src/PageCanvas.tsx
68
+ var import_react2 = require("react");
69
+ var import_core2 = require("@hwp-editor/core");
70
+
71
+ // src/geometry.ts
72
+ var import_core = require("@hwp-editor/core");
73
+ function clamp(value, min, max) {
74
+ return Math.min(max, Math.max(min, value));
75
+ }
76
+ function nearestSegment(envelope, pageIndex, yFraction, pageCount) {
77
+ if (envelope.segments.length === 0) return void 0;
78
+ const offset = clickOffset(envelope, pageIndex, yFraction, pageCount);
79
+ const hit = (0, import_core.segmentAtOffset)(envelope, offset);
80
+ if (hit !== void 0) return hit;
81
+ let best;
82
+ let bestDistance = Number.POSITIVE_INFINITY;
83
+ for (const segment of envelope.segments) {
84
+ const distance = Math.abs(segment.start - offset);
85
+ if (distance < bestDistance) {
86
+ best = segment;
87
+ bestDistance = distance;
88
+ }
89
+ }
90
+ return best;
91
+ }
92
+ function clickOffset(envelope, pageIndex, yFraction, pageCount) {
93
+ const total = Math.max(pageCount, 1);
94
+ const fraction = clamp(
95
+ (clamp(pageIndex, 0, total - 1) + clamp(yFraction, 0, 1)) / total,
96
+ 0,
97
+ 1
98
+ );
99
+ return Math.round(fraction * envelope.markdown.length);
100
+ }
101
+ function segmentBand(envelope, segment, pageCount) {
102
+ const length = envelope.markdown.length;
103
+ if (length === 0) return null;
104
+ const total = Math.max(pageCount, 1);
105
+ const center = (segment.start + segment.end) / 2;
106
+ const position = center / length * total;
107
+ const pageIndex = clamp(Math.floor(position), 0, total - 1);
108
+ const height = clamp((segment.end - segment.start) / length * total * 100, 2, 100);
109
+ const top = clamp((position - pageIndex) * 100 - height / 2, 0, 100 - height);
110
+ return { pageIndex, top, height };
111
+ }
112
+
113
+ // src/sanitize.ts
114
+ function sanitizeSvg(svgText) {
115
+ const doc = new DOMParser().parseFromString(svgText, "image/svg+xml");
116
+ const root = doc.documentElement;
117
+ if (root === null || root.nodeName === "parsererror" || doc.getElementsByTagName("parsererror").length > 0) {
118
+ return "";
119
+ }
120
+ const dangerous = root.querySelectorAll("script, foreignObject");
121
+ for (const node of Array.from(dangerous)) node.remove();
122
+ scrubElement(root);
123
+ return root.outerHTML;
124
+ }
125
+ function scrubElement(el) {
126
+ for (const attr of Array.from(el.attributes)) {
127
+ const name = attr.name.toLowerCase();
128
+ const value = attr.value.trim().toLowerCase();
129
+ if (name.startsWith("on")) {
130
+ el.removeAttribute(attr.name);
131
+ } else if ((name === "href" || name === "xlink:href") && value.startsWith("javascript:")) {
132
+ el.removeAttribute(attr.name);
133
+ }
134
+ }
135
+ for (const child of Array.from(el.children)) scrubElement(child);
136
+ }
137
+
138
+ // src/text.ts
139
+ function plainSegmentText(markdownSlice) {
140
+ return markdownSlice.split("\n").map(stripLine).join("\n").trim();
141
+ }
142
+ function stripLine(line) {
143
+ let out = line.trimEnd();
144
+ out = out.replace(/^#{1,6}\s+/, "");
145
+ out = out.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
146
+ out = out.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
147
+ out = out.replace(/(\*\*|__)(.*?)\1/g, "$2");
148
+ out = out.replace(/(\*|_)(.*?)\1/g, "$2");
149
+ out = out.replace(/`([^`]*)`/g, "$1");
150
+ return out;
151
+ }
152
+
153
+ // src/PageCanvas.tsx
154
+ var import_jsx_runtime = require("react/jsx-runtime");
155
+ function PageCanvas() {
156
+ const { state, store, envelope, t } = useHwpEditorContext();
157
+ const { pages, selection, pendingOps } = state;
158
+ const pendingSegments = (0, import_react2.useMemo)(() => {
159
+ if (envelope === null) return /* @__PURE__ */ new Set();
160
+ const keys = /* @__PURE__ */ new Set();
161
+ for (const op of pendingOps) {
162
+ const target = opSearchText(op);
163
+ if (target === null || target === "") continue;
164
+ for (const segment of envelope.segments) {
165
+ const plain = plainSegmentText((0, import_core2.segmentText)(envelope, segment));
166
+ if (plain.includes(target)) {
167
+ keys.add(`${segment.section}:${segment.para}`);
168
+ }
169
+ }
170
+ }
171
+ return keys;
172
+ }, [envelope, pendingOps]);
173
+ const selectedSegment = envelope !== null && selection !== null ? (0, import_core2.segmentAtRef)(envelope, selection) : void 0;
174
+ if (pages.length === 0) {
175
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "hwped-canvas", role: "main", "aria-label": t("canvas.aria"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "hwped-empty", children: t("canvas.emptyPages") }) });
176
+ }
177
+ const selectAt = (pageIndex, yFraction) => {
178
+ if (envelope === null) return;
179
+ const segment = nearestSegment(envelope, pageIndex, yFraction, pages.length);
180
+ if (segment !== void 0) {
181
+ store.dispatch({ type: "select", selection: (0, import_core2.segmentRef)(segment) });
182
+ }
183
+ };
184
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "hwped-canvas", role: "main", "aria-label": t("canvas.aria"), children: pages.map((page, pageIndex) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
185
+ PageView,
186
+ {
187
+ page,
188
+ onClick: (e) => {
189
+ const rect = e.currentTarget.getBoundingClientRect();
190
+ selectAt(
191
+ pageIndex,
192
+ rect.height > 0 ? (e.clientY - rect.top) / rect.height : 0.5
193
+ );
194
+ },
195
+ onActivate: () => selectAt(pageIndex, 0.5),
196
+ bands: bandsForPage(
197
+ envelope,
198
+ pageIndex,
199
+ pages.length,
200
+ selectedSegment,
201
+ pendingSegments
202
+ )
203
+ },
204
+ page.page
205
+ )) });
206
+ }
207
+ function bandsForPage(envelope, pageIndex, pageCount, selected, pendingKeys) {
208
+ if (envelope === null) return [];
209
+ const bands = [];
210
+ for (const segment of envelope.segments) {
211
+ const key = `${segment.section}:${segment.para}`;
212
+ const isSelected = selected === segment;
213
+ const isPending = !isSelected && pendingKeys.has(key);
214
+ if (!isSelected && !isPending) continue;
215
+ const band = segmentBand(envelope, segment, pageCount);
216
+ if (band === null || band.pageIndex !== pageIndex) continue;
217
+ bands.push({
218
+ key: `${isSelected ? "sel" : "pend"}-${key}`,
219
+ className: isSelected ? "hwped-band hwped-band-selected" : "hwped-band hwped-band-pending",
220
+ top: band.top,
221
+ height: band.height
222
+ });
223
+ }
224
+ return bands;
225
+ }
226
+ function PageView(props) {
227
+ const { page, onClick, onActivate, bands } = props;
228
+ const { t } = useHwpEditorContext();
229
+ const label = t("page.label", { page: page.page });
230
+ const svgHtml = (0, import_react2.useMemo)(
231
+ () => page.format === "svg" ? sanitizeSvg(new TextDecoder().decode(page.data)) : null,
232
+ [page]
233
+ );
234
+ const imgSrc = (0, import_react2.useMemo)(() => {
235
+ if (page.format === "svg") return null;
236
+ const mime = page.format === "jpeg" ? "image/jpeg" : `image/${page.format}`;
237
+ return `data:${mime};base64,${import_core2.base64.encode(page.data)}`;
238
+ }, [page]);
239
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
240
+ "div",
241
+ {
242
+ className: "hwped-page",
243
+ role: "button",
244
+ tabIndex: 0,
245
+ "aria-label": label,
246
+ style: { aspectRatio: `${page.width} / ${page.height}` },
247
+ onClick,
248
+ onKeyDown: (e) => {
249
+ if (e.key === "Enter" || e.key === " ") {
250
+ e.preventDefault();
251
+ onActivate();
252
+ }
253
+ },
254
+ children: [
255
+ svgHtml !== null ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
256
+ "div",
257
+ {
258
+ className: "hwped-page-svg",
259
+ dangerouslySetInnerHTML: { __html: svgHtml }
260
+ }
261
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
262
+ "img",
263
+ {
264
+ className: "hwped-page-img",
265
+ src: imgSrc ?? "",
266
+ alt: label,
267
+ width: page.width,
268
+ height: page.height
269
+ }
270
+ ),
271
+ bands.map((band) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
272
+ "div",
273
+ {
274
+ className: band.className,
275
+ style: { top: `${band.top}%`, height: `${band.height}%` }
276
+ },
277
+ band.key
278
+ ))
279
+ ]
280
+ }
281
+ );
282
+ }
283
+ function opSearchText(op) {
284
+ if ("find" in op && typeof op.find === "string") return op.find;
285
+ if ("anchor" in op && typeof op.anchor === "string") return op.anchor;
286
+ if (op.kind === "delete-para" && typeof op.text === "string") return op.text;
287
+ return null;
288
+ }
289
+
290
+ // src/SegmentInspector.tsx
291
+ var import_react3 = require("react");
292
+ var import_core3 = require("@hwp-editor/core");
293
+ var import_jsx_runtime2 = require("react/jsx-runtime");
294
+ var ALIGNMENTS = [
295
+ { value: "left", key: "segment.alignLeft" },
296
+ { value: "center", key: "segment.alignCenter" },
297
+ { value: "right", key: "segment.alignRight" },
298
+ { value: "justify", key: "segment.alignJustify" }
299
+ ];
300
+ function SegmentInspector() {
301
+ const { state, store, envelope, editable, t } = useHwpEditorContext();
302
+ const { selection } = state;
303
+ const segment = envelope !== null && selection !== null ? (0, import_core3.segmentAtRef)(envelope, selection) : void 0;
304
+ const currentText = envelope !== null && segment !== void 0 ? plainSegmentText((0, import_core3.segmentText)(envelope, segment)) : "";
305
+ const [replaceWith, setReplaceWith] = (0, import_react3.useState)("");
306
+ const [insertText, setInsertText] = (0, import_react3.useState)("");
307
+ const [bold, setBold] = (0, import_react3.useState)(false);
308
+ const [fontSize, setFontSize] = (0, import_react3.useState)("");
309
+ const [color, setColor] = (0, import_react3.useState)("");
310
+ (0, import_react3.useEffect)(() => {
311
+ setReplaceWith("");
312
+ setInsertText("");
313
+ setBold(false);
314
+ setFontSize("");
315
+ setColor("");
316
+ }, [selection]);
317
+ if (segment === void 0 || selection === null) {
318
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
319
+ "div",
320
+ {
321
+ className: "hwped-panel",
322
+ role: "region",
323
+ "aria-label": t("segment.panelAria"),
324
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "hwped-hint", children: t("segment.hint") })
325
+ }
326
+ );
327
+ }
328
+ const find = currentText;
329
+ const disabled = !editable;
330
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
331
+ "div",
332
+ {
333
+ className: "hwped-panel",
334
+ role: "region",
335
+ "aria-label": t("segment.panelAria"),
336
+ children: [
337
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hwped-field", children: [
338
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "hwped-label", children: t("segment.selectedPara", {
339
+ section: selection.section,
340
+ para: selection.para
341
+ }) }),
342
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hwped-quote", children: currentText || t("segment.emptyPara") })
343
+ ] }),
344
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hwped-field", children: [
345
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { className: "hwped-label", htmlFor: "hwped-replace", children: t("segment.replaceLabel") }),
346
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hwped-row", children: [
347
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
348
+ "input",
349
+ {
350
+ id: "hwped-replace",
351
+ className: "hwped-input",
352
+ type: "text",
353
+ value: replaceWith,
354
+ disabled,
355
+ onChange: (e) => setReplaceWith(e.target.value),
356
+ placeholder: t("segment.replacePlaceholder")
357
+ }
358
+ ),
359
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
360
+ "button",
361
+ {
362
+ type: "button",
363
+ className: "hwped-btn",
364
+ disabled: disabled || replaceWith === "" || find === "",
365
+ onClick: () => store.dispatch({
366
+ type: "queueOp",
367
+ op: { kind: "replace", find, replace: replaceWith }
368
+ }),
369
+ children: t("segment.replaceSubmit")
370
+ }
371
+ )
372
+ ] })
373
+ ] }),
374
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hwped-field", children: [
375
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { className: "hwped-label", htmlFor: "hwped-insert", children: t("segment.insertLabel") }),
376
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
377
+ "textarea",
378
+ {
379
+ id: "hwped-insert",
380
+ className: "hwped-textarea",
381
+ value: insertText,
382
+ disabled,
383
+ onChange: (e) => setInsertText(e.target.value),
384
+ placeholder: t("segment.insertPlaceholder"),
385
+ rows: 2
386
+ }
387
+ ),
388
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hwped-row", children: [
389
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
390
+ "button",
391
+ {
392
+ type: "button",
393
+ className: "hwped-btn",
394
+ disabled: disabled || insertText === "" || find === "",
395
+ onClick: () => store.dispatch({
396
+ type: "queueOp",
397
+ op: { kind: "insert-para-before", anchor: find, text: insertText }
398
+ }),
399
+ children: t("segment.insertBefore")
400
+ }
401
+ ),
402
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
403
+ "button",
404
+ {
405
+ type: "button",
406
+ className: "hwped-btn",
407
+ disabled: disabled || insertText === "" || find === "",
408
+ onClick: () => store.dispatch({
409
+ type: "queueOp",
410
+ op: { kind: "insert-para", anchor: find, text: insertText }
411
+ }),
412
+ children: t("segment.insertAfter")
413
+ }
414
+ ),
415
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
416
+ "button",
417
+ {
418
+ type: "button",
419
+ className: "hwped-btn hwped-btn-danger",
420
+ disabled: disabled || find === "",
421
+ onClick: () => store.dispatch({
422
+ type: "queueOp",
423
+ op: { kind: "delete-para", text: find }
424
+ }),
425
+ children: t("segment.deletePara")
426
+ }
427
+ )
428
+ ] })
429
+ ] }),
430
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hwped-field", children: [
431
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "hwped-label", children: t("segment.alignLabel") }),
432
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
433
+ "div",
434
+ {
435
+ className: "hwped-row",
436
+ role: "group",
437
+ "aria-label": t("segment.alignGroupAria"),
438
+ children: ALIGNMENTS.map((align) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
439
+ "button",
440
+ {
441
+ type: "button",
442
+ className: "hwped-btn",
443
+ disabled: disabled || find === "",
444
+ onClick: () => store.dispatch({
445
+ type: "queueOp",
446
+ op: { kind: "set-align", find, alignment: align.value }
447
+ }),
448
+ children: t(align.key)
449
+ },
450
+ align.value
451
+ ))
452
+ }
453
+ )
454
+ ] }),
455
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hwped-field", children: [
456
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "hwped-label", children: t("segment.formatLabel") }),
457
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hwped-row", children: [
458
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "hwped-check", children: [
459
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
460
+ "input",
461
+ {
462
+ type: "checkbox",
463
+ checked: bold,
464
+ disabled,
465
+ onChange: (e) => setBold(e.target.checked)
466
+ }
467
+ ),
468
+ t("segment.bold")
469
+ ] }),
470
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
471
+ "input",
472
+ {
473
+ className: "hwped-input hwped-input-narrow",
474
+ type: "number",
475
+ min: 1,
476
+ value: fontSize,
477
+ disabled,
478
+ onChange: (e) => setFontSize(e.target.value),
479
+ placeholder: t("segment.sizePlaceholder"),
480
+ "aria-label": t("segment.sizeAria")
481
+ }
482
+ ),
483
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
484
+ "input",
485
+ {
486
+ className: "hwped-input hwped-input-narrow",
487
+ type: "text",
488
+ value: color,
489
+ disabled,
490
+ onChange: (e) => setColor(e.target.value),
491
+ placeholder: "#FF0000",
492
+ "aria-label": t("segment.colorAria")
493
+ }
494
+ ),
495
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
496
+ "button",
497
+ {
498
+ type: "button",
499
+ className: "hwped-btn",
500
+ disabled: disabled || find === "" || !bold && fontSize === "" && color === "",
501
+ onClick: () => {
502
+ const props = {};
503
+ if (bold) props["bold"] = "on";
504
+ if (fontSize !== "") props["size"] = fontSize;
505
+ if (color !== "") props["color"] = color;
506
+ store.dispatch({
507
+ type: "queueOp",
508
+ op: { kind: "set-format", find, props }
509
+ });
510
+ },
511
+ children: t("segment.formatSubmit")
512
+ }
513
+ )
514
+ ] })
515
+ ] })
516
+ ]
517
+ }
518
+ );
519
+ }
520
+
521
+ // src/TableGrid.tsx
522
+ var import_react4 = require("react");
523
+
524
+ // src/tables.ts
525
+ var import_core4 = require("@hwp-editor/core");
526
+ function isTableSlice(slice) {
527
+ return parseMarkdownTable(slice) !== null;
528
+ }
529
+ function parseMarkdownTable(slice) {
530
+ const lines = slice.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("|") && line.endsWith("|"));
531
+ if (lines.length < 2) return null;
532
+ const rows = [];
533
+ let sawSeparator = false;
534
+ for (const line of lines) {
535
+ const cells = line.slice(1, -1).split("|").map((cell) => cell.trim());
536
+ if (cells.every((cell) => /^:?-{2,}:?$/.test(cell))) {
537
+ sawSeparator = true;
538
+ continue;
539
+ }
540
+ rows.push(cells);
541
+ }
542
+ return sawSeparator && rows.length > 0 ? rows : null;
543
+ }
544
+ function findTables(envelope) {
545
+ const tables = [];
546
+ for (const segment of envelope.segments) {
547
+ const rows = parseMarkdownTable((0, import_core4.segmentText)(envelope, segment));
548
+ if (rows !== null) {
549
+ tables.push({ tableIndex: tables.length, segment, rows });
550
+ }
551
+ }
552
+ return tables;
553
+ }
554
+ function tableAtRef(envelope, ref) {
555
+ return findTables(envelope).find(
556
+ (table) => table.segment.section === ref.section && table.segment.para === ref.para
557
+ );
558
+ }
559
+
560
+ // src/TableGrid.tsx
561
+ var import_jsx_runtime3 = require("react/jsx-runtime");
562
+ function TableGrid() {
563
+ const { state, store, envelope, editable, t } = useHwpEditorContext();
564
+ const { selection } = state;
565
+ const table = envelope !== null && selection !== null ? tableAtRef(envelope, selection) : void 0;
566
+ const [cell, setCell] = (0, import_react4.useState)(null);
567
+ const [mergeAnchor, setMergeAnchor] = (0, import_react4.useState)(null);
568
+ const [cellValue, setCellValue] = (0, import_react4.useState)("");
569
+ (0, import_react4.useEffect)(() => {
570
+ setCell(null);
571
+ setMergeAnchor(null);
572
+ setCellValue("");
573
+ }, [selection]);
574
+ if (table === void 0) {
575
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
576
+ "div",
577
+ {
578
+ className: "hwped-panel",
579
+ role: "region",
580
+ "aria-label": t("table.panelAria"),
581
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "hwped-hint", children: t("table.hint") })
582
+ }
583
+ );
584
+ }
585
+ const disabled = !editable;
586
+ const tableIndex = table.tableIndex;
587
+ const queue = store.dispatch.bind(store);
588
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hwped-panel", role: "region", "aria-label": t("table.panelAria"), children: [
589
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hwped-field", children: [
590
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "hwped-label", children: t("table.tableLabel", { index: tableIndex }) }),
591
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("table", { className: "hwped-grid", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("tbody", { children: table.rows.map((row, rowIndex) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("tr", { children: row.map((text, colIndex) => {
592
+ const active = cell !== null && cell.row === rowIndex && cell.col === colIndex;
593
+ const anchor = mergeAnchor !== null && mergeAnchor.row === rowIndex && mergeAnchor.col === colIndex;
594
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("td", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
595
+ "button",
596
+ {
597
+ type: "button",
598
+ className: "hwped-cell" + (active ? " hwped-cell-selected" : "") + (anchor ? " hwped-cell-anchor" : ""),
599
+ onClick: () => {
600
+ setCell({ row: rowIndex, col: colIndex });
601
+ setCellValue(plainSegmentText(text));
602
+ },
603
+ children: plainSegmentText(text) || " "
604
+ }
605
+ ) }, colIndex);
606
+ }) }, rowIndex)) }) })
607
+ ] }),
608
+ cell !== null && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hwped-field", children: [
609
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { className: "hwped-label", htmlFor: "hwped-cell", children: t("table.cellLabel", { row: cell.row, col: cell.col }) }),
610
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hwped-row", children: [
611
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
612
+ "input",
613
+ {
614
+ id: "hwped-cell",
615
+ className: "hwped-input",
616
+ type: "text",
617
+ value: cellValue,
618
+ disabled,
619
+ onChange: (e) => setCellValue(e.target.value)
620
+ }
621
+ ),
622
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
623
+ "button",
624
+ {
625
+ type: "button",
626
+ className: "hwped-btn",
627
+ disabled,
628
+ onClick: () => queue({
629
+ type: "queueOp",
630
+ op: {
631
+ kind: "set-cell",
632
+ table: tableIndex,
633
+ row: cell.row,
634
+ col: cell.col,
635
+ value: cellValue
636
+ }
637
+ }),
638
+ children: t("table.setCell")
639
+ }
640
+ )
641
+ ] }),
642
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hwped-row", children: [
643
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
644
+ "button",
645
+ {
646
+ type: "button",
647
+ className: "hwped-btn",
648
+ disabled,
649
+ onClick: () => setMergeAnchor({ ...cell }),
650
+ children: t("table.mergeAnchor")
651
+ }
652
+ ),
653
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
654
+ "button",
655
+ {
656
+ type: "button",
657
+ className: "hwped-btn",
658
+ disabled: disabled || mergeAnchor === null || mergeAnchor.row === cell.row && mergeAnchor.col === cell.col,
659
+ onClick: () => {
660
+ if (mergeAnchor === null) return;
661
+ queue({
662
+ type: "queueOp",
663
+ op: {
664
+ kind: "merge-cells",
665
+ table: tableIndex,
666
+ r1: Math.min(mergeAnchor.row, cell.row),
667
+ c1: Math.min(mergeAnchor.col, cell.col),
668
+ r2: Math.max(mergeAnchor.row, cell.row),
669
+ c2: Math.max(mergeAnchor.col, cell.col)
670
+ }
671
+ });
672
+ setMergeAnchor(null);
673
+ },
674
+ children: t("table.mergeWithAnchor")
675
+ }
676
+ ),
677
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
678
+ "button",
679
+ {
680
+ type: "button",
681
+ className: "hwped-btn",
682
+ disabled,
683
+ onClick: () => queue({
684
+ type: "queueOp",
685
+ op: {
686
+ kind: "split-cell",
687
+ table: tableIndex,
688
+ row: cell.row,
689
+ col: cell.col
690
+ }
691
+ }),
692
+ children: t("table.splitCell")
693
+ }
694
+ )
695
+ ] })
696
+ ] }),
697
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hwped-field", children: [
698
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "hwped-label", children: t("table.rowsCols") }),
699
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hwped-row", children: [
700
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
701
+ "button",
702
+ {
703
+ type: "button",
704
+ className: "hwped-btn",
705
+ disabled,
706
+ onClick: () => queue({
707
+ type: "queueOp",
708
+ op: cell === null ? { kind: "add-row", table: tableIndex } : { kind: "add-row", table: tableIndex, at: cell.row + 1 }
709
+ }),
710
+ children: t("table.addRow")
711
+ }
712
+ ),
713
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
714
+ "button",
715
+ {
716
+ type: "button",
717
+ className: "hwped-btn",
718
+ disabled,
719
+ onClick: () => queue({
720
+ type: "queueOp",
721
+ op: cell === null ? { kind: "add-col", table: tableIndex } : { kind: "add-col", table: tableIndex, at: cell.col + 1 }
722
+ }),
723
+ children: t("table.addCol")
724
+ }
725
+ ),
726
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
727
+ "button",
728
+ {
729
+ type: "button",
730
+ className: "hwped-btn hwped-btn-danger",
731
+ disabled: disabled || cell === null,
732
+ onClick: () => {
733
+ if (cell === null) return;
734
+ queue({
735
+ type: "queueOp",
736
+ op: { kind: "delete-row", table: tableIndex, row: cell.row }
737
+ });
738
+ },
739
+ children: t("table.deleteRow")
740
+ }
741
+ ),
742
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
743
+ "button",
744
+ {
745
+ type: "button",
746
+ className: "hwped-btn hwped-btn-danger",
747
+ disabled: disabled || cell === null,
748
+ onClick: () => {
749
+ if (cell === null) return;
750
+ queue({
751
+ type: "queueOp",
752
+ op: { kind: "delete-col", table: tableIndex, col: cell.col }
753
+ });
754
+ },
755
+ children: t("table.deleteCol")
756
+ }
757
+ )
758
+ ] })
759
+ ] }),
760
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "hwped-field", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
761
+ "button",
762
+ {
763
+ type: "button",
764
+ className: "hwped-btn hwped-btn-danger",
765
+ disabled,
766
+ onClick: () => queue({
767
+ type: "queueOp",
768
+ op: { kind: "delete-table", target: tableIndex }
769
+ }),
770
+ children: t("table.deleteTable")
771
+ }
772
+ ) })
773
+ ] });
774
+ }
775
+
776
+ // src/FieldsPanel.tsx
777
+ var import_react5 = require("react");
778
+ var import_core6 = require("@hwp-editor/core");
779
+
780
+ // src/fields.ts
781
+ var import_core5 = require("@hwp-editor/core");
782
+ var SLOT_PATTERN = /\{\{\s*([^{}]+?)\s*\}\}/g;
783
+ function extractFieldSlots(envelope) {
784
+ const slots = [];
785
+ const seen = /* @__PURE__ */ new Set();
786
+ for (const match of envelope.markdown.matchAll(SLOT_PATTERN)) {
787
+ const name = match[1] ?? "";
788
+ const offset = match.index ?? 0;
789
+ const segment = (0, import_core5.segmentAtOffset)(envelope, offset);
790
+ if (segment === void 0 || seen.has(name)) continue;
791
+ seen.add(name);
792
+ slots.push({
793
+ name,
794
+ segment: { section: segment.section, para: segment.para }
795
+ });
796
+ }
797
+ return slots;
798
+ }
799
+
800
+ // src/FieldsPanel.tsx
801
+ var import_jsx_runtime4 = require("react/jsx-runtime");
802
+ function FieldsPanel() {
803
+ const { store, envelope, editable, t } = useHwpEditorContext();
804
+ const [drafts, setDrafts] = (0, import_react5.useState)({});
805
+ const slots = (0, import_react5.useMemo)(
806
+ () => envelope === null ? [] : extractFieldSlots(envelope),
807
+ [envelope]
808
+ );
809
+ if (envelope === null || slots.length === 0) {
810
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
811
+ "div",
812
+ {
813
+ className: "hwped-panel",
814
+ role: "region",
815
+ "aria-label": t("fields.panelAria"),
816
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "hwped-hint", children: t("fields.hint") })
817
+ }
818
+ );
819
+ }
820
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
821
+ "div",
822
+ {
823
+ className: "hwped-panel",
824
+ role: "region",
825
+ "aria-label": t("fields.panelAria"),
826
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { className: "hwped-fields", children: slots.map((slot) => {
827
+ const segment = (0, import_core6.segmentAtRef)(envelope, slot.segment);
828
+ const snippet = segment === void 0 ? "" : (0, import_core6.segmentText)(envelope, segment).trim().slice(0, 40);
829
+ const draft = drafts[slot.name] ?? "";
830
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("li", { className: "hwped-field-item", children: [
831
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hwped-row", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
832
+ "button",
833
+ {
834
+ type: "button",
835
+ className: "hwped-btn hwped-btn-link",
836
+ title: snippet,
837
+ onClick: () => store.dispatch({ type: "select", selection: slot.segment }),
838
+ children: slot.name
839
+ }
840
+ ) }),
841
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hwped-row", children: [
842
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
843
+ "input",
844
+ {
845
+ className: "hwped-input",
846
+ type: "text",
847
+ value: draft,
848
+ disabled: !editable,
849
+ placeholder: t("fields.valuePlaceholder"),
850
+ "aria-label": t("fields.fieldValueAria", { name: slot.name }),
851
+ onChange: (e) => setDrafts((prev) => ({ ...prev, [slot.name]: e.target.value }))
852
+ }
853
+ ),
854
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
855
+ "button",
856
+ {
857
+ type: "button",
858
+ className: "hwped-btn",
859
+ disabled: !editable || draft === "",
860
+ onClick: () => store.dispatch({
861
+ type: "queueOp",
862
+ op: { kind: "set-field", name: slot.name, value: draft }
863
+ }),
864
+ children: t("fields.setValue")
865
+ }
866
+ )
867
+ ] })
868
+ ] }, slot.name);
869
+ }) })
870
+ }
871
+ );
872
+ }
873
+
874
+ // src/ComposePanel.tsx
875
+ var import_react6 = require("react");
876
+
877
+ // src/errors.ts
878
+ var import_core7 = require("@hwp-editor/core");
879
+ function toEditorError(e) {
880
+ return (0, import_core7.isHwpEngineError)(e) && (0, import_core7.isHwpErrorCode)(e.code) ? { code: e.code, message: e.message } : { message: e instanceof Error ? e.message : String(e) };
881
+ }
882
+ var KIND_BY_CODE = {
883
+ timeout: "timeout",
884
+ unavailable: "unavailable",
885
+ version: "unavailable",
886
+ protected: "protected",
887
+ failed: "generic",
888
+ bad_request: "generic",
889
+ unsupported_format: "generic",
890
+ output_too_large: "generic",
891
+ cancelled: "generic",
892
+ method_not_allowed: "generic",
893
+ not_found: "generic",
894
+ session_not_found: "generic",
895
+ path_traversal: "generic",
896
+ forbidden: "generic",
897
+ internal: "generic"
898
+ };
899
+ function engineErrorKind(error) {
900
+ if (error.code !== void 0 && Object.hasOwn(KIND_BY_CODE, error.code)) {
901
+ return KIND_BY_CODE[error.code];
902
+ }
903
+ return classifyEngineError(error.message);
904
+ }
905
+ function classifyEngineError(message) {
906
+ const m = message.toLowerCase();
907
+ if (m.includes("timed out") || m.includes("hwp_timeout") || m.includes("http 504")) {
908
+ return "timeout";
909
+ }
910
+ if (m.includes("binary not found") || m.includes("cli_missing") || m.includes("not executable") || m.includes("http 503")) {
911
+ return "unavailable";
912
+ }
913
+ if (m.includes("distribution") || m.includes("encrypted") || m.includes("protected") || m.includes("\uBC30\uD3EC\uC6A9")) {
914
+ return "protected";
915
+ }
916
+ return "generic";
917
+ }
918
+
919
+ // src/ErrorLine.tsx
920
+ var import_jsx_runtime5 = require("react/jsx-runtime");
921
+ var LABEL_KEY_BY_KIND = {
922
+ timeout: "error.kind.timeout",
923
+ unavailable: "error.kind.unavailable",
924
+ protected: "error.kind.protected",
925
+ generic: "error.kind.generic"
926
+ };
927
+ function ErrorLine(props) {
928
+ const { t } = useHwpEditorContext();
929
+ const kind = engineErrorKind({
930
+ // Conditional spread: exactOptionalPropertyTypes rejects an explicit
931
+ // `code: undefined` against the optional field.
932
+ ...props.code !== void 0 ? { code: props.code } : {},
933
+ message: props.message
934
+ });
935
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "hwped-error", role: "alert", "data-error-kind": kind, children: [
936
+ kind !== "generic" && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: `hwped-error-kind hwped-error-${kind}`, children: t(LABEL_KEY_BY_KIND[kind]) }),
937
+ props.prefix,
938
+ ": ",
939
+ props.message
940
+ ] });
941
+ }
942
+
943
+ // src/presets.ts
944
+ var COMPOSE_PRESETS = [
945
+ "official",
946
+ "report",
947
+ "plan",
948
+ "notice",
949
+ "minutes",
950
+ "gaejosik",
951
+ "press"
952
+ ];
953
+ var MALGUN = "\uB9D1\uC740 \uACE0\uB515";
954
+ var HCR_BATANG = "\uD568\uCD08\uB86C\uBC14\uD0D5";
955
+ var PRESET_PROFILES = {
956
+ official: { fontFamily: MALGUN, bodySizePt: 12, lineHeightPercent: 160, pageNumber: false },
957
+ report: { fontFamily: HCR_BATANG, bodySizePt: 15, lineHeightPercent: 160, pageNumber: true },
958
+ plan: { fontFamily: HCR_BATANG, bodySizePt: 15, lineHeightPercent: 160, pageNumber: true },
959
+ notice: { fontFamily: MALGUN, bodySizePt: 15, lineHeightPercent: 160, pageNumber: true },
960
+ minutes: { fontFamily: HCR_BATANG, bodySizePt: 14, lineHeightPercent: 130, pageNumber: false },
961
+ gaejosik: { fontFamily: MALGUN, bodySizePt: 15, lineHeightPercent: 160, pageNumber: true },
962
+ press: { fontFamily: HCR_BATANG, bodySizePt: 14, lineHeightPercent: 160, pageNumber: true }
963
+ };
964
+ function bodyToBlocks(body) {
965
+ const blocks = [];
966
+ for (const chunk of body.split(/\n\s*\n/)) {
967
+ const text = chunk.trim();
968
+ if (text === "") continue;
969
+ if (text.startsWith("# ")) {
970
+ blocks.push({
971
+ type: "paragraph",
972
+ style: "heading",
973
+ runs: [{ type: "text", text: text.slice(2).trim() }]
974
+ });
975
+ } else {
976
+ blocks.push({
977
+ type: "paragraph",
978
+ style: "body",
979
+ runs: [{ type: "text", text }]
980
+ });
981
+ }
982
+ }
983
+ return blocks;
984
+ }
985
+ function buildDocumentSpec(preset, input) {
986
+ const profile = PRESET_PROFILES[preset];
987
+ const blocks = [];
988
+ if (input.title.trim() !== "") {
989
+ blocks.push({
990
+ type: "paragraph",
991
+ style: "title",
992
+ runs: [{ type: "text", text: input.title.trim() }]
993
+ });
994
+ }
995
+ blocks.push(...bodyToBlocks(input.body));
996
+ if (blocks.length === 0) {
997
+ blocks.push({
998
+ type: "paragraph",
999
+ style: "body",
1000
+ runs: [{ type: "text", text: " " }]
1001
+ });
1002
+ }
1003
+ const section = {
1004
+ ...profile.pageNumber ? { page_number: { position: "bottom_center" } } : {},
1005
+ blocks
1006
+ };
1007
+ return {
1008
+ version: "2.0",
1009
+ document: {
1010
+ version: "1.0",
1011
+ metadata: {
1012
+ ...input.title.trim() !== "" ? { title: input.title.trim() } : {},
1013
+ ...input.author.trim() !== "" ? { author: input.author.trim() } : {}
1014
+ },
1015
+ styles: {
1016
+ body: {
1017
+ font_family: profile.fontFamily,
1018
+ font_size_pt: profile.bodySizePt,
1019
+ line_height_percent: profile.lineHeightPercent,
1020
+ align: "justify"
1021
+ },
1022
+ heading: {
1023
+ font_family: profile.fontFamily,
1024
+ font_size_pt: profile.bodySizePt + 3,
1025
+ bold: true,
1026
+ spacing_before_pt: 13,
1027
+ spacing_after_pt: 6
1028
+ },
1029
+ title: {
1030
+ font_family: profile.fontFamily,
1031
+ font_size_pt: profile.bodySizePt + 6,
1032
+ bold: true,
1033
+ align: "center",
1034
+ spacing_after_pt: 18
1035
+ }
1036
+ },
1037
+ sections: [section]
1038
+ }
1039
+ };
1040
+ }
1041
+
1042
+ // src/ComposePanel.tsx
1043
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1044
+ var PRESET_LABEL_KEYS = {
1045
+ official: "presets.official",
1046
+ report: "presets.report",
1047
+ plan: "presets.plan",
1048
+ notice: "presets.notice",
1049
+ minutes: "presets.minutes",
1050
+ gaejosik: "presets.gaejosik",
1051
+ press: "presets.press"
1052
+ };
1053
+ function ComposePanel(props) {
1054
+ const { engine, t, onError } = useHwpEditorContext();
1055
+ const [preset, setPreset] = (0, import_react6.useState)("official");
1056
+ const [title, setTitle] = (0, import_react6.useState)("");
1057
+ const [author, setAuthor] = (0, import_react6.useState)("");
1058
+ const [body, setBody] = (0, import_react6.useState)("");
1059
+ const [busy, setBusy] = (0, import_react6.useState)(false);
1060
+ const [error, setError] = (0, import_react6.useState)(null);
1061
+ const compose = async () => {
1062
+ setBusy(true);
1063
+ setError(null);
1064
+ try {
1065
+ const spec = buildDocumentSpec(preset, { title, author, body });
1066
+ const stem = title.trim() === "" ? t("compose.defaultFileStem") : title.trim();
1067
+ const result = await engine.compose(spec, `${stem}.hwpx`);
1068
+ props.onComposed(result.document);
1069
+ } catch (e) {
1070
+ setError(toEditorError(e));
1071
+ onError?.(e);
1072
+ } finally {
1073
+ setBusy(false);
1074
+ }
1075
+ };
1076
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "hwped-overlay", role: "presentation", onClick: props.onClose, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1077
+ "div",
1078
+ {
1079
+ className: "hwped-dialog",
1080
+ role: "dialog",
1081
+ "aria-modal": "true",
1082
+ "aria-label": t("compose.title"),
1083
+ onClick: (e) => e.stopPropagation(),
1084
+ children: [
1085
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h2", { className: "hwped-dialog-title", children: t("compose.title") }),
1086
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "hwped-field", children: [
1087
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "hwped-label", children: t("compose.docType") }),
1088
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1089
+ "div",
1090
+ {
1091
+ className: "hwped-presets",
1092
+ role: "radiogroup",
1093
+ "aria-label": t("compose.docTypeAria"),
1094
+ children: COMPOSE_PRESETS.map((value) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1095
+ "button",
1096
+ {
1097
+ type: "button",
1098
+ role: "radio",
1099
+ "aria-checked": preset === value,
1100
+ className: "hwped-preset" + (preset === value ? " hwped-preset-active" : ""),
1101
+ onClick: () => setPreset(value),
1102
+ children: t(PRESET_LABEL_KEYS[value])
1103
+ },
1104
+ value
1105
+ ))
1106
+ }
1107
+ )
1108
+ ] }),
1109
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "hwped-field", children: [
1110
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("label", { className: "hwped-label", htmlFor: "hwped-compose-title", children: t("compose.titleLabel") }),
1111
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1112
+ "input",
1113
+ {
1114
+ id: "hwped-compose-title",
1115
+ className: "hwped-input",
1116
+ type: "text",
1117
+ value: title,
1118
+ onChange: (e) => setTitle(e.target.value),
1119
+ placeholder: t("compose.titlePlaceholder")
1120
+ }
1121
+ )
1122
+ ] }),
1123
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "hwped-field", children: [
1124
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("label", { className: "hwped-label", htmlFor: "hwped-compose-author", children: t("compose.authorLabel") }),
1125
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1126
+ "input",
1127
+ {
1128
+ id: "hwped-compose-author",
1129
+ className: "hwped-input",
1130
+ type: "text",
1131
+ value: author,
1132
+ onChange: (e) => setAuthor(e.target.value),
1133
+ placeholder: t("compose.authorPlaceholder")
1134
+ }
1135
+ )
1136
+ ] }),
1137
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "hwped-field", children: [
1138
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("label", { className: "hwped-label", htmlFor: "hwped-compose-body", children: t("compose.bodyLabel") }),
1139
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1140
+ "textarea",
1141
+ {
1142
+ id: "hwped-compose-body",
1143
+ className: "hwped-textarea",
1144
+ rows: 8,
1145
+ value: body,
1146
+ onChange: (e) => setBody(e.target.value),
1147
+ placeholder: t("compose.bodyPlaceholder")
1148
+ }
1149
+ )
1150
+ ] }),
1151
+ error !== null && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ErrorLine, { prefix: t("error.prefix.compose"), ...error }),
1152
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "hwped-row hwped-dialog-actions", children: [
1153
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1154
+ "button",
1155
+ {
1156
+ type: "button",
1157
+ className: "hwped-btn",
1158
+ onClick: props.onClose,
1159
+ disabled: busy,
1160
+ children: t("compose.cancel")
1161
+ }
1162
+ ),
1163
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1164
+ "button",
1165
+ {
1166
+ type: "button",
1167
+ className: "hwped-btn hwped-btn-primary",
1168
+ onClick: () => void compose(),
1169
+ disabled: busy,
1170
+ children: busy ? t("compose.submitting") : t("compose.submit")
1171
+ }
1172
+ )
1173
+ ] })
1174
+ ]
1175
+ }
1176
+ ) });
1177
+ }
1178
+
1179
+ // src/messages.ts
1180
+ var en = {
1181
+ // Toolbar
1182
+ "toolbar.toolsAria": "Editing tools",
1183
+ "toolbar.readOnly": "Read-only",
1184
+ "toolbar.pendingEdits": (p) => p.count === 1 ? "1 pending edit" : `${p.count} pending edits`,
1185
+ "toolbar.revert": "Undo edit",
1186
+ "toolbar.apply": "Apply",
1187
+ "toolbar.applyWithCount": (p) => `Apply (${p.count})`,
1188
+ "toolbar.applying": "Applying\u2026",
1189
+ "toolbar.newDocument": "New document",
1190
+ // Validation badge
1191
+ "validation.aria": "Validation result",
1192
+ "validation.valid": "Valid",
1193
+ "validation.errors": (p) => p.count === 1 ? "1 error" : `${p.count} errors`,
1194
+ // Canvas
1195
+ "canvas.aria": "Document pages",
1196
+ "canvas.empty": "No document open.",
1197
+ "canvas.createCta": "Create a new document",
1198
+ "canvas.emptyPages": "No rendered pages.",
1199
+ "page.label": (p) => `Page ${p.page}`,
1200
+ // Side panel shell
1201
+ "side.panelAria": "Editing panels",
1202
+ "tabs.para": "Paragraph",
1203
+ "tabs.table": "Table",
1204
+ "tabs.fields": "Fields",
1205
+ // Error line — the prefix names which operation failed. The `{message}`
1206
+ // tail stays engine-authored and is never translated.
1207
+ "error.prefix.load": "Failed to open document",
1208
+ "error.prefix.apply": "Failed to apply edits",
1209
+ "error.prefix.compose": "Failed to create document",
1210
+ // Badge labels; "generic" renders no chip, hence the empty string.
1211
+ "error.kind.timeout": "Engine timed out",
1212
+ "error.kind.unavailable": "hwp binary unavailable",
1213
+ "error.kind.protected": "Protected document",
1214
+ "error.kind.generic": "",
1215
+ // Compose dialog
1216
+ "compose.title": "Create a new document",
1217
+ "compose.docType": "Document type",
1218
+ "compose.docTypeAria": "Document type",
1219
+ "compose.titleLabel": "Title",
1220
+ "compose.titlePlaceholder": "Document title",
1221
+ "compose.authorLabel": "Author",
1222
+ "compose.authorPlaceholder": "Author (optional)",
1223
+ "compose.bodyLabel": "Body",
1224
+ "compose.bodyPlaceholder": "Blank lines separate paragraphs. Lines starting with '# ' become subheadings.\n\nExample:\n# 1. Overview\nBody text...",
1225
+ "compose.cancel": "Cancel",
1226
+ "compose.submit": "Create document",
1227
+ "compose.submitting": "Creating\u2026",
1228
+ /** Filename stem used when the compose form has no title. */
1229
+ "compose.defaultFileStem": "New document",
1230
+ // Compose preset labels. The preset PROFILES (font names, sizes) are
1231
+ // document data and stay in presets.ts, untranslated.
1232
+ "presets.official": "Official document",
1233
+ "presets.report": "Report",
1234
+ "presets.plan": "Plan",
1235
+ "presets.notice": "Notice",
1236
+ "presets.minutes": "Minutes",
1237
+ "presets.gaejosik": "Outline document",
1238
+ "presets.press": "Press release",
1239
+ // SegmentInspector
1240
+ "segment.panelAria": "Paragraph editor",
1241
+ "segment.hint": "Click the page to select a paragraph to edit.",
1242
+ "segment.selectedPara": (p) => `Selected paragraph (section ${p.section}, paragraph ${p.para})`,
1243
+ "segment.emptyPara": "(empty paragraph)",
1244
+ "segment.replaceLabel": "Replace text",
1245
+ "segment.replacePlaceholder": "New text",
1246
+ "segment.replaceSubmit": "Replace",
1247
+ "segment.insertLabel": "Insert paragraph",
1248
+ "segment.insertPlaceholder": "Paragraph text to insert",
1249
+ "segment.insertBefore": "Insert before",
1250
+ "segment.insertAfter": "Insert after",
1251
+ "segment.deletePara": "Delete paragraph",
1252
+ "segment.alignLabel": "Alignment",
1253
+ "segment.alignGroupAria": "Paragraph alignment",
1254
+ "segment.alignLeft": "Left",
1255
+ "segment.alignCenter": "Center",
1256
+ "segment.alignRight": "Right",
1257
+ "segment.alignJustify": "Justify",
1258
+ "segment.formatLabel": "Character format",
1259
+ "segment.bold": "Bold",
1260
+ "segment.sizePlaceholder": "Size (pt)",
1261
+ "segment.sizeAria": "Font size (pt)",
1262
+ "segment.colorAria": "Font color",
1263
+ "segment.formatSubmit": "Apply format",
1264
+ // TableGrid
1265
+ "table.panelAria": "Table editor",
1266
+ "table.hint": "Select a region containing a table on the page. The current selection is not a table paragraph.",
1267
+ "table.tableLabel": (p) => `Table #${p.index}`,
1268
+ "table.cellLabel": (p) => `Cell (${p.row}, ${p.col})`,
1269
+ "table.setCell": "Set cell",
1270
+ "table.mergeAnchor": "Set merge anchor",
1271
+ "table.mergeWithAnchor": "Merge with anchor",
1272
+ "table.splitCell": "Split cell",
1273
+ "table.rowsCols": "Rows / columns",
1274
+ "table.addRow": "Add row",
1275
+ "table.addCol": "Add column",
1276
+ "table.deleteRow": "Delete row",
1277
+ "table.deleteCol": "Delete column",
1278
+ "table.deleteTable": "Delete table",
1279
+ // FieldsPanel
1280
+ "fields.panelAria": "Fields",
1281
+ "fields.hint": "No {{name}} field placeholders were found in the document.",
1282
+ "fields.valuePlaceholder": "New value",
1283
+ "fields.fieldValueAria": (p) => `Value for field ${p.name}`,
1284
+ "fields.setValue": "Set value"
1285
+ };
1286
+ var ko = {
1287
+ "toolbar.toolsAria": "\uD3B8\uC9D1 \uB3C4\uAD6C",
1288
+ "toolbar.readOnly": "\uC77D\uAE30 \uC804\uC6A9",
1289
+ "toolbar.pendingEdits": (p) => `\uB300\uAE30 \uD3B8\uC9D1 ${p.count}`,
1290
+ "toolbar.revert": "\uB418\uB3CC\uB9AC\uAE30",
1291
+ "toolbar.apply": "\uC801\uC6A9",
1292
+ "toolbar.applyWithCount": (p) => `\uC801\uC6A9 (${p.count})`,
1293
+ "toolbar.applying": "\uC801\uC6A9 \uC911...",
1294
+ "toolbar.newDocument": "\uC0C8 \uBB38\uC11C",
1295
+ "validation.aria": "\uAC80\uC99D \uACB0\uACFC",
1296
+ "validation.valid": "\uC720\uD6A8",
1297
+ "validation.errors": (p) => `\uC624\uB958 ${p.count}\uAC74`,
1298
+ "canvas.aria": "\uBB38\uC11C \uD398\uC774\uC9C0",
1299
+ "canvas.empty": "\uC5F4\uB9B0 \uBB38\uC11C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
1300
+ "canvas.createCta": "\uC0C8 \uBB38\uC11C \uB9CC\uB4E4\uAE30",
1301
+ "canvas.emptyPages": "\uB80C\uB354\uB9C1\uB41C \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
1302
+ "page.label": (p) => `\uD398\uC774\uC9C0 ${p.page}`,
1303
+ "side.panelAria": "\uD3B8\uC9D1 \uD328\uB110",
1304
+ "tabs.para": "\uBB38\uB2E8",
1305
+ "tabs.table": "\uD45C",
1306
+ "tabs.fields": "\uD544\uB4DC",
1307
+ "error.prefix.load": "\uBB38\uC11C \uC5F4\uAE30 \uC2E4\uD328",
1308
+ "error.prefix.apply": "\uD3B8\uC9D1 \uC801\uC6A9 \uC2E4\uD328",
1309
+ "error.prefix.compose": "\uBB38\uC11C \uC0DD\uC131 \uC2E4\uD328",
1310
+ "error.kind.timeout": "\uC5D4\uC9C4 \uC2DC\uAC04 \uCD08\uACFC",
1311
+ "error.kind.unavailable": "hwp \uC2E4\uD589 \uD30C\uC77C \uC5C6\uC74C",
1312
+ "error.kind.protected": "\uBCF4\uD638/\uBC30\uD3EC \uBB38\uC11C",
1313
+ "error.kind.generic": "",
1314
+ "compose.title": "\uC0C8 \uBB38\uC11C \uB9CC\uB4E4\uAE30",
1315
+ "compose.docType": "\uBB38\uC11C \uC720\uD615",
1316
+ "compose.docTypeAria": "\uBB38\uC11C \uC720\uD615",
1317
+ "compose.titleLabel": "\uC81C\uBAA9",
1318
+ "compose.titlePlaceholder": "\uBB38\uC11C \uC81C\uBAA9",
1319
+ "compose.authorLabel": "\uC791\uC131\uC790",
1320
+ "compose.authorPlaceholder": "\uC791\uC131\uC790 (\uC120\uD0DD)",
1321
+ "compose.bodyLabel": "\uBCF8\uBB38",
1322
+ "compose.bodyPlaceholder": "\uBE48 \uC904\uB85C \uBB38\uB2E8\uC744 \uAD6C\uBD84\uD569\uB2C8\uB2E4. '# '\uB85C \uC2DC\uC791\uD558\uB294 \uC904\uC740 \uC18C\uC81C\uBAA9\uC774 \uB429\uB2C8\uB2E4.\n\n\uC608)\n# 1. \uAC1C\uC694\n\uBCF8\uBB38 \uB0B4\uC6A9...",
1323
+ "compose.cancel": "\uCDE8\uC18C",
1324
+ "compose.submit": "\uBB38\uC11C \uC0DD\uC131",
1325
+ "compose.submitting": "\uC0DD\uC131 \uC911...",
1326
+ "compose.defaultFileStem": "\uC0C8 \uBB38\uC11C",
1327
+ "presets.official": "\uACF5\uBB38",
1328
+ "presets.report": "\uBCF4\uACE0\uC11C",
1329
+ "presets.plan": "\uACC4\uD68D\uC11C",
1330
+ "presets.notice": "\uC548\uB0B4\uBB38",
1331
+ "presets.minutes": "\uD68C\uC758\uB85D",
1332
+ "presets.gaejosik": "\uAC1C\uC870\uC2DD \uBB38\uC11C",
1333
+ "presets.press": "\uBCF4\uB3C4\uC790\uB8CC",
1334
+ "segment.panelAria": "\uBB38\uB2E8 \uD3B8\uC9D1",
1335
+ "segment.hint": "\uD398\uC774\uC9C0\uB97C \uD074\uB9AD\uD574 \uD3B8\uC9D1\uD560 \uBB38\uB2E8\uC744 \uC120\uD0DD\uD558\uC138\uC694.",
1336
+ "segment.selectedPara": (p) => `\uC120\uD0DD \uBB38\uB2E8 (\uAD6C\uC5ED ${p.section}, \uBB38\uB2E8 ${p.para})`,
1337
+ "segment.emptyPara": "(\uBE48 \uBB38\uB2E8)",
1338
+ "segment.replaceLabel": "\uD14D\uC2A4\uD2B8 \uAD50\uCCB4",
1339
+ "segment.replacePlaceholder": "\uC0C8 \uD14D\uC2A4\uD2B8",
1340
+ "segment.replaceSubmit": "\uAD50\uCCB4",
1341
+ "segment.insertLabel": "\uBB38\uB2E8 \uC0BD\uC785",
1342
+ "segment.insertPlaceholder": "\uC0BD\uC785\uD560 \uBB38\uB2E8 \uD14D\uC2A4\uD2B8",
1343
+ "segment.insertBefore": "\uC55E\uC5D0 \uC0BD\uC785",
1344
+ "segment.insertAfter": "\uB4A4\uC5D0 \uC0BD\uC785",
1345
+ "segment.deletePara": "\uBB38\uB2E8 \uC0AD\uC81C",
1346
+ "segment.alignLabel": "\uC815\uB82C",
1347
+ "segment.alignGroupAria": "\uBB38\uB2E8 \uC815\uB82C",
1348
+ "segment.alignLeft": "\uC67C\uCABD",
1349
+ "segment.alignCenter": "\uAC00\uC6B4\uB370",
1350
+ "segment.alignRight": "\uC624\uB978\uCABD",
1351
+ "segment.alignJustify": "\uC591\uCABD",
1352
+ "segment.formatLabel": "\uAE00\uC790 \uC11C\uC2DD",
1353
+ "segment.bold": "\uAD75\uAC8C",
1354
+ "segment.sizePlaceholder": "\uD06C\uAE30(pt)",
1355
+ "segment.sizeAria": "\uAE00\uC790 \uD06C\uAE30(pt)",
1356
+ "segment.colorAria": "\uAE00\uC790 \uC0C9\uC0C1",
1357
+ "segment.formatSubmit": "\uC11C\uC2DD \uC801\uC6A9",
1358
+ "table.panelAria": "\uD45C \uD3B8\uC9D1",
1359
+ "table.hint": "\uD45C\uAC00 \uD3EC\uD568\uB41C \uC601\uC5ED\uC744 \uD398\uC774\uC9C0\uC5D0\uC11C \uC120\uD0DD\uD558\uC138\uC694. \uD45C\uAC00 \uC544\uB2CC \uBB38\uB2E8\uC774 \uC120\uD0DD\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
1360
+ "table.tableLabel": (p) => `\uD45C #${p.index}`,
1361
+ "table.cellLabel": (p) => `\uC140 (${p.row}, ${p.col})`,
1362
+ "table.setCell": "\uC140 \uC124\uC815",
1363
+ "table.mergeAnchor": "\uBCD1\uD569 \uAE30\uC900 \uC9C0\uC815",
1364
+ "table.mergeWithAnchor": "\uAE30\uC900 \uC140\uACFC \uBCD1\uD569",
1365
+ "table.splitCell": "\uC140 \uBD84\uD560",
1366
+ "table.rowsCols": "\uD589/\uC5F4",
1367
+ "table.addRow": "\uD589 \uCD94\uAC00",
1368
+ "table.addCol": "\uC5F4 \uCD94\uAC00",
1369
+ "table.deleteRow": "\uD589 \uC0AD\uC81C",
1370
+ "table.deleteCol": "\uC5F4 \uC0AD\uC81C",
1371
+ "table.deleteTable": "\uD45C \uC0AD\uC81C",
1372
+ "fields.panelAria": "\uD544\uB4DC",
1373
+ "fields.hint": "\uBB38\uC11C\uC5D0\uC11C {{\uC774\uB984}} \uD615\uD0DC\uC758 \uD544\uB4DC \uC790\uB9AC\uD45C\uC2DC\uC790\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.",
1374
+ "fields.valuePlaceholder": "\uC0C8 \uAC12",
1375
+ "fields.fieldValueAria": (p) => `\uD544\uB4DC ${p.name} \uAC12`,
1376
+ "fields.setValue": "\uC124\uC815"
1377
+ };
1378
+ function createT(locale, messages) {
1379
+ const table = {
1380
+ ...en,
1381
+ ...locale === "ko" ? ko : {},
1382
+ ...messages
1383
+ };
1384
+ return ((key, params) => {
1385
+ const value = table[key];
1386
+ return typeof value === "function" ? value(params) : value;
1387
+ });
1388
+ }
1389
+
1390
+ // src/HwpEditor.tsx
1391
+ var import_core9 = require("@hwp-editor/core");
1392
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1393
+ var RENDER_SVG = { format: "svg" };
1394
+ var HwpEditor = (0, import_react7.forwardRef)(
1395
+ function HwpEditor2(props, ref) {
1396
+ const {
1397
+ engine,
1398
+ file,
1399
+ locale = "en",
1400
+ messages,
1401
+ readOnly = false,
1402
+ onReady,
1403
+ onError,
1404
+ onChange,
1405
+ onDirtyChange,
1406
+ className,
1407
+ style
1408
+ } = props;
1409
+ const cbRef = (0, import_react7.useRef)({ onReady, onError, onChange, onDirtyChange });
1410
+ (0, import_react7.useEffect)(() => {
1411
+ cbRef.current = { onReady, onError, onChange, onDirtyChange };
1412
+ });
1413
+ const emitReady = (0, import_react7.useCallback)((document) => {
1414
+ cbRef.current.onReady?.(document);
1415
+ }, []);
1416
+ const emitError = (0, import_react7.useCallback)((error) => {
1417
+ cbRef.current.onError?.(error);
1418
+ }, []);
1419
+ const emitChange = (0, import_react7.useCallback)((document) => {
1420
+ cbRef.current.onChange?.(document);
1421
+ }, []);
1422
+ const emitDirty = (0, import_react7.useCallback)((dirty2) => {
1423
+ cbRef.current.onDirtyChange?.(dirty2);
1424
+ }, []);
1425
+ const t = (0, import_react7.useMemo)(() => createT(locale, messages), [locale, messages]);
1426
+ const [store] = (0, import_react7.useState)(() => (0, import_core8.createStore)());
1427
+ const state = (0, import_react7.useSyncExternalStore)(store.subscribe, store.getState, store.getState);
1428
+ const [envelope, setEnvelope] = (0, import_react7.useState)(null);
1429
+ const [capabilities, setCapabilities] = (0, import_react7.useState)(null);
1430
+ const [validation, setValidation] = (0, import_react7.useState)(null);
1431
+ const [loadError, setLoadError] = (0, import_react7.useState)(null);
1432
+ const [tab, setTab] = (0, import_react7.useState)("para");
1433
+ const [composing, setComposing] = (0, import_react7.useState)(false);
1434
+ (0, import_react7.useEffect)(() => {
1435
+ let cancelled = false;
1436
+ if (file === null) {
1437
+ setEnvelope(null);
1438
+ setValidation(null);
1439
+ setLoadError(null);
1440
+ return;
1441
+ }
1442
+ (async () => {
1443
+ try {
1444
+ const caps = await engine.capabilities();
1445
+ const [nextEnvelope, pages, report] = await Promise.all([
1446
+ engine.read(file),
1447
+ engine.render(file, RENDER_SVG),
1448
+ engine.validate(file).catch(() => null)
1449
+ ]);
1450
+ if (cancelled) return;
1451
+ setCapabilities(caps);
1452
+ setEnvelope(nextEnvelope);
1453
+ setValidation(report);
1454
+ setLoadError(null);
1455
+ store.dispatch({ type: "load", document: file, pages });
1456
+ emitReady(file);
1457
+ } catch (e) {
1458
+ if (cancelled) return;
1459
+ setLoadError(toEditorError(e));
1460
+ emitError(e);
1461
+ }
1462
+ })();
1463
+ return () => {
1464
+ cancelled = true;
1465
+ };
1466
+ }, [engine, file, store]);
1467
+ const editable = !readOnly && (capabilities?.editable ?? true);
1468
+ (0, import_react7.useEffect)(() => {
1469
+ if (envelope === null || state.selection === null) return;
1470
+ const segment = (0, import_core8.segmentAtRef)(envelope, state.selection);
1471
+ if (segment === void 0) return;
1472
+ setTab(
1473
+ isTableSlice((0, import_core9.segmentText)(envelope, segment)) ? "table" : "para"
1474
+ );
1475
+ }, [envelope, state.selection]);
1476
+ const dirty = state.pendingOps.length > 0;
1477
+ (0, import_react7.useEffect)(() => {
1478
+ emitDirty(dirty);
1479
+ }, [dirty]);
1480
+ const applyPendingOps = (0, import_react7.useCallback)(() => {
1481
+ const snapshot = store.getState();
1482
+ if (snapshot.document === null || snapshot.pendingOps.length === 0 || snapshot.status === "applying" || !editable) {
1483
+ return Promise.resolve();
1484
+ }
1485
+ store.dispatch({ type: "applyStarted" });
1486
+ return (async () => {
1487
+ try {
1488
+ const next = await engine.edit(snapshot.document, snapshot.pendingOps, {
1489
+ verify: true
1490
+ });
1491
+ const [nextEnvelope, pages, report] = await Promise.all([
1492
+ engine.read(next),
1493
+ engine.render(next, RENDER_SVG),
1494
+ engine.validate(next).catch(() => null)
1495
+ ]);
1496
+ setEnvelope(nextEnvelope);
1497
+ setValidation(report);
1498
+ store.dispatch({ type: "applySucceeded", document: next, pages });
1499
+ emitChange(next);
1500
+ } catch (e) {
1501
+ store.dispatch({ type: "applyFailed", error: toEditorError(e) });
1502
+ emitError(e);
1503
+ }
1504
+ })();
1505
+ }, [editable, engine, store]);
1506
+ const revert = (0, import_react7.useCallback)(() => {
1507
+ const snapshot = store.getState();
1508
+ const previous = snapshot.snapshots[snapshot.snapshots.length - 1];
1509
+ if (previous === void 0) return Promise.resolve();
1510
+ return (async () => {
1511
+ try {
1512
+ const [nextEnvelope, pages, report] = await Promise.all([
1513
+ engine.read(previous),
1514
+ engine.render(previous, RENDER_SVG),
1515
+ engine.validate(previous).catch(() => null)
1516
+ ]);
1517
+ const now = store.getState();
1518
+ if (now.snapshots[now.snapshots.length - 1] !== previous) return;
1519
+ store.dispatch({ type: "undo" });
1520
+ setEnvelope(nextEnvelope);
1521
+ if (report !== null) setValidation(report);
1522
+ store.dispatch({ type: "setPages", pages });
1523
+ emitChange(previous);
1524
+ } catch (e) {
1525
+ setLoadError(toEditorError(e));
1526
+ emitError(e);
1527
+ }
1528
+ })();
1529
+ }, [engine, store]);
1530
+ const refresh = (0, import_react7.useCallback)(async () => {
1531
+ const snapshot = store.getState();
1532
+ if (snapshot.document === null) return;
1533
+ try {
1534
+ const [nextEnvelope, pages, report] = await Promise.all([
1535
+ engine.read(snapshot.document),
1536
+ engine.render(snapshot.document, RENDER_SVG),
1537
+ engine.validate(snapshot.document).catch(() => null)
1538
+ ]);
1539
+ setEnvelope(nextEnvelope);
1540
+ setValidation(report);
1541
+ store.dispatch({ type: "setPages", pages });
1542
+ } catch (e) {
1543
+ setLoadError(toEditorError(e));
1544
+ emitError(e);
1545
+ throw e;
1546
+ }
1547
+ }, [engine, store]);
1548
+ const openCompose = (0, import_react7.useCallback)(() => setComposing(true), []);
1549
+ (0, import_react7.useImperativeHandle)(
1550
+ ref,
1551
+ () => ({ apply: applyPendingOps, revert, refresh, openCompose }),
1552
+ [applyPendingOps, revert, refresh, openCompose]
1553
+ );
1554
+ const contextValue = (0, import_react7.useMemo)(
1555
+ () => ({
1556
+ engine,
1557
+ store,
1558
+ state,
1559
+ envelope,
1560
+ capabilities,
1561
+ editable,
1562
+ validation,
1563
+ applyPendingOps,
1564
+ refresh,
1565
+ openCompose,
1566
+ t,
1567
+ // Always defined now, and stable: a widening, since every consumer
1568
+ // already calls it as `onError?.(e)` and it no-ops when the host
1569
+ // supplied no callback. The payload is unchanged.
1570
+ onError: emitError
1571
+ }),
1572
+ [
1573
+ engine,
1574
+ store,
1575
+ state,
1576
+ envelope,
1577
+ capabilities,
1578
+ editable,
1579
+ validation,
1580
+ applyPendingOps,
1581
+ refresh,
1582
+ openCompose,
1583
+ t
1584
+ ]
1585
+ );
1586
+ const onKeyDown = (e) => {
1587
+ if (e.key === "Escape") {
1588
+ store.dispatch({ type: "select", selection: null });
1589
+ } else if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
1590
+ e.preventDefault();
1591
+ void applyPendingOps();
1592
+ }
1593
+ };
1594
+ const rootClass = "hwped hwped-root" + (className !== void 0 ? ` ${className}` : "");
1595
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(HwpEditorContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1596
+ "div",
1597
+ {
1598
+ className: rootClass,
1599
+ style,
1600
+ onKeyDown,
1601
+ "data-status": state.status,
1602
+ lang: locale,
1603
+ children: [
1604
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1605
+ "header",
1606
+ {
1607
+ className: "hwped-toolbar",
1608
+ role: "toolbar",
1609
+ "aria-label": t("toolbar.toolsAria"),
1610
+ children: [
1611
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "hwped-title", children: state.document?.name ?? "hwp-editor" }),
1612
+ !editable && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1613
+ "span",
1614
+ {
1615
+ className: "hwped-notice",
1616
+ role: "note",
1617
+ ...capabilities?.reason !== void 0 ? { title: capabilities.reason } : {},
1618
+ children: [
1619
+ t("toolbar.readOnly"),
1620
+ capabilities?.editable === false && (capabilities.reason !== void 0 ? `: ${capabilities.reason}` : ` (${t("error.kind.protected")})`)
1621
+ ]
1622
+ }
1623
+ ),
1624
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "hwped-spacer" }),
1625
+ validation !== null && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1626
+ "span",
1627
+ {
1628
+ className: validation.valid ? "hwped-badge hwped-badge-ok" : "hwped-badge hwped-badge-err",
1629
+ role: "status",
1630
+ "aria-label": t("validation.aria"),
1631
+ children: validation.valid ? t("validation.valid") : t("validation.errors", { count: validation.errors.length })
1632
+ }
1633
+ ),
1634
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1635
+ "span",
1636
+ {
1637
+ className: "hwped-count",
1638
+ "aria-label": t("toolbar.pendingEdits", {
1639
+ count: state.pendingOps.length
1640
+ }),
1641
+ children: t("toolbar.pendingEdits", { count: state.pendingOps.length })
1642
+ }
1643
+ ),
1644
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1645
+ "button",
1646
+ {
1647
+ type: "button",
1648
+ className: "hwped-btn",
1649
+ onClick: () => void revert(),
1650
+ disabled: state.snapshots.length === 0 || state.status === "applying",
1651
+ children: t("toolbar.revert")
1652
+ }
1653
+ ),
1654
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1655
+ "button",
1656
+ {
1657
+ type: "button",
1658
+ className: "hwped-btn hwped-btn-primary",
1659
+ onClick: () => void applyPendingOps(),
1660
+ disabled: !dirty || !editable || state.status === "applying",
1661
+ children: state.status === "applying" ? t("toolbar.applying") : t("toolbar.applyWithCount", {
1662
+ count: state.pendingOps.length
1663
+ })
1664
+ }
1665
+ ),
1666
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1667
+ "button",
1668
+ {
1669
+ type: "button",
1670
+ className: "hwped-btn",
1671
+ onClick: () => setComposing(true),
1672
+ children: t("toolbar.newDocument")
1673
+ }
1674
+ )
1675
+ ]
1676
+ }
1677
+ ),
1678
+ state.status === "error" && state.error !== null && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ErrorLine, { prefix: t("error.prefix.apply"), ...state.error }),
1679
+ loadError !== null && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ErrorLine, { prefix: t("error.prefix.load"), ...loadError }),
1680
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hwped-main", children: [
1681
+ state.document === null ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1682
+ "div",
1683
+ {
1684
+ className: "hwped-canvas",
1685
+ role: "main",
1686
+ "aria-label": t("canvas.aria"),
1687
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "hwped-empty", children: [
1688
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { children: t("canvas.empty") }),
1689
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1690
+ "button",
1691
+ {
1692
+ type: "button",
1693
+ className: "hwped-btn hwped-btn-primary",
1694
+ onClick: () => setComposing(true),
1695
+ children: t("canvas.createCta")
1696
+ }
1697
+ )
1698
+ ] })
1699
+ }
1700
+ ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PageCanvas, {}),
1701
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("aside", { className: "hwped-side", "aria-label": t("side.panelAria"), children: [
1702
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "hwped-tabs", role: "tablist", children: [
1703
+ ["para", t("tabs.para")],
1704
+ ["table", t("tabs.table")],
1705
+ ["fields", t("tabs.fields")]
1706
+ ].map(([value, label]) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1707
+ "button",
1708
+ {
1709
+ type: "button",
1710
+ role: "tab",
1711
+ "aria-selected": tab === value,
1712
+ className: "hwped-tab" + (tab === value ? " hwped-tab-active" : ""),
1713
+ onClick: () => setTab(value),
1714
+ children: label
1715
+ },
1716
+ value
1717
+ )) }),
1718
+ tab === "para" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SegmentInspector, {}),
1719
+ tab === "table" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TableGrid, {}),
1720
+ tab === "fields" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(FieldsPanel, {})
1721
+ ] })
1722
+ ] }),
1723
+ composing && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1724
+ ComposePanel,
1725
+ {
1726
+ onClose: () => setComposing(false),
1727
+ onComposed: (document) => {
1728
+ setComposing(false);
1729
+ emitChange(document);
1730
+ void (async () => {
1731
+ try {
1732
+ const [nextEnvelope, pages] = await Promise.all([
1733
+ engine.read(document),
1734
+ engine.render(document, RENDER_SVG)
1735
+ ]);
1736
+ setEnvelope(nextEnvelope);
1737
+ store.dispatch({ type: "load", document, pages });
1738
+ } catch (e) {
1739
+ setLoadError(toEditorError(e));
1740
+ emitError(e);
1741
+ }
1742
+ })();
1743
+ }
1744
+ }
1745
+ )
1746
+ ]
1747
+ }
1748
+ ) });
1749
+ }
1750
+ );
1751
+ // Annotate the CommonJS export names for ESM import in node:
1752
+ 0 && (module.exports = {
1753
+ COMPOSE_PRESETS,
1754
+ ComposePanel,
1755
+ FieldsPanel,
1756
+ HwpEditor,
1757
+ PageCanvas,
1758
+ SegmentInspector,
1759
+ TableGrid,
1760
+ bodyToBlocks,
1761
+ buildDocumentSpec,
1762
+ classifyEngineError,
1763
+ clickOffset,
1764
+ createT,
1765
+ en,
1766
+ extractFieldSlots,
1767
+ findTables,
1768
+ isTableSlice,
1769
+ ko,
1770
+ nearestSegment,
1771
+ parseMarkdownTable,
1772
+ plainSegmentText,
1773
+ sanitizeSvg,
1774
+ segmentBand,
1775
+ tableAtRef,
1776
+ useHwpEditorContext
1777
+ });
1778
+ //# sourceMappingURL=index.cjs.map