@ubermensch1218/hwpxeditor 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/index.js ADDED
@@ -0,0 +1,2563 @@
1
+ "use client";
2
+ var __defProp = Object.defineProperty;
3
+ var __defProps = Object.defineProperties;
4
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
5
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __spreadValues = (a, b) => {
10
+ for (var prop in b || (b = {}))
11
+ if (__hasOwnProp.call(b, prop))
12
+ __defNormalProp(a, prop, b[prop]);
13
+ if (__getOwnPropSymbols)
14
+ for (var prop of __getOwnPropSymbols(b)) {
15
+ if (__propIsEnum.call(b, prop))
16
+ __defNormalProp(a, prop, b[prop]);
17
+ }
18
+ return a;
19
+ };
20
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
21
+
22
+ // src/components/editor/Editor.tsx
23
+ import { useEffect as useEffect3 } from "react";
24
+
25
+ // src/lib/store.ts
26
+ import { create } from "zustand";
27
+
28
+ // src/lib/view-model.ts
29
+ import { parseHeaderXml, serializeXml } from "@ubermensch1218/hwpxcore";
30
+
31
+ // src/lib/hwp-units.ts
32
+ var HWP_UNITS_PER_INCH = 7200;
33
+ var PX_PER_INCH = 96;
34
+ var MM_PER_INCH = 25.4;
35
+ function hwpToPx(hwpUnit) {
36
+ return hwpUnit * PX_PER_INCH / HWP_UNITS_PER_INCH;
37
+ }
38
+ function pxToHwp(px) {
39
+ return Math.round(px * HWP_UNITS_PER_INCH / PX_PER_INCH);
40
+ }
41
+ function hwpToMm(hwpUnit) {
42
+ return hwpUnit * MM_PER_INCH / HWP_UNITS_PER_INCH;
43
+ }
44
+ function mmToHwp(mm) {
45
+ return Math.round(mm * HWP_UNITS_PER_INCH / MM_PER_INCH);
46
+ }
47
+
48
+ // src/lib/image-extractor.ts
49
+ function extractImages(pkg) {
50
+ var _a, _b;
51
+ const images = /* @__PURE__ */ new Map();
52
+ const partNames = pkg.partNames();
53
+ for (const name of partNames) {
54
+ if (!name.startsWith("BinData/")) continue;
55
+ const ext = (_b = (_a = name.split(".").pop()) == null ? void 0 : _a.toLowerCase()) != null ? _b : "";
56
+ let mediaType = "application/octet-stream";
57
+ if (ext === "png") mediaType = "image/png";
58
+ else if (ext === "jpg" || ext === "jpeg") mediaType = "image/jpeg";
59
+ else if (ext === "gif") mediaType = "image/gif";
60
+ else if (ext === "bmp") mediaType = "image/bmp";
61
+ else if (ext === "svg") mediaType = "image/svg+xml";
62
+ else if (ext === "webp") mediaType = "image/webp";
63
+ else if (ext === "tif" || ext === "tiff") mediaType = "image/tiff";
64
+ else if (ext === "emf") mediaType = "image/x-emf";
65
+ else if (ext === "wmf") mediaType = "image/x-wmf";
66
+ try {
67
+ const data = pkg.getPart(name);
68
+ const base64 = uint8ToBase64(data);
69
+ const dataUrl = `data:${mediaType};base64,${base64}`;
70
+ const fileName = name.replace("BinData/", "");
71
+ images.set(fileName, dataUrl);
72
+ } catch (e) {
73
+ }
74
+ }
75
+ return images;
76
+ }
77
+ function uint8ToBase64(bytes) {
78
+ let binary = "";
79
+ for (let i = 0; i < bytes.length; i++) {
80
+ binary += String.fromCharCode(bytes[i]);
81
+ }
82
+ return btoa(binary);
83
+ }
84
+
85
+ // src/lib/view-model.ts
86
+ function extractRunStyle(style) {
87
+ var _a, _b, _c, _d;
88
+ if (!style) {
89
+ return {
90
+ bold: false,
91
+ italic: false,
92
+ underline: false,
93
+ strikethrough: false,
94
+ color: null,
95
+ fontFamily: null,
96
+ fontSize: null,
97
+ highlightColor: null,
98
+ letterSpacing: null
99
+ };
100
+ }
101
+ const bold = style.attributes["bold"] === "1";
102
+ const italic = style.attributes["italic"] === "1";
103
+ const underlineChild = style.childAttributes["underline"];
104
+ const underline = underlineChild != null && underlineChild["type"] != null && underlineChild["type"] !== "NONE";
105
+ const strikeChild = style.childAttributes["strikeout"];
106
+ const strikethrough = strikeChild != null && strikeChild["type"] != null && strikeChild["type"] !== "NONE";
107
+ const color = (_a = style.attributes["textColor"]) != null ? _a : null;
108
+ const highlightColor = (_b = style.attributes["shadeColor"]) != null ? _b : null;
109
+ let fontFamily = null;
110
+ const fontRef = style.childAttributes["fontRef"];
111
+ if (fontRef) {
112
+ fontFamily = (_d = (_c = fontRef["hangul"]) != null ? _c : fontRef["latin"]) != null ? _d : null;
113
+ }
114
+ let fontSize = null;
115
+ const sizeStr = style.attributes["height"];
116
+ if (sizeStr) {
117
+ const hwpVal = parseInt(sizeStr, 10);
118
+ if (!isNaN(hwpVal)) fontSize = hwpVal / 100;
119
+ }
120
+ let letterSpacing = null;
121
+ const spacingStr = style.attributes["spacing"];
122
+ if (spacingStr) {
123
+ const val = parseInt(spacingStr, 10);
124
+ if (!isNaN(val) && val !== 0) letterSpacing = val;
125
+ }
126
+ return { bold, italic, underline, strikethrough, color, fontFamily, fontSize, highlightColor, letterSpacing };
127
+ }
128
+ function findPictureRefs(runElement) {
129
+ var _a, _b;
130
+ const results = [];
131
+ const children = runElement.childNodes;
132
+ for (let i = 0; i < children.length; i++) {
133
+ const child = children.item(i);
134
+ if (!child || child.nodeType !== 1) continue;
135
+ const el = child;
136
+ const localName = el.localName || el.nodeName.split(":").pop() || "";
137
+ if (localName !== "pic") continue;
138
+ let width = 0;
139
+ let height = 0;
140
+ const picChildren = el.childNodes;
141
+ for (let j = 0; j < picChildren.length; j++) {
142
+ const pc = picChildren.item(j);
143
+ if (!pc || pc.nodeType !== 1) continue;
144
+ const pel = pc;
145
+ const pName = pel.localName || pel.nodeName.split(":").pop() || "";
146
+ if (pName === "curSz") {
147
+ width = parseInt((_a = pel.getAttribute("width")) != null ? _a : "0", 10);
148
+ height = parseInt((_b = pel.getAttribute("height")) != null ? _b : "0", 10);
149
+ }
150
+ if (pName === "img") {
151
+ const ref = pel.getAttribute("binaryItemIDRef");
152
+ if (ref) results.push({ binaryItemIdRef: ref, width, height });
153
+ }
154
+ }
155
+ const allDescendants = el.getElementsByTagName("*");
156
+ for (let k = 0; k < allDescendants.length; k++) {
157
+ const desc = allDescendants.item(k);
158
+ if (!desc) continue;
159
+ const dName = desc.localName || desc.nodeName.split(":").pop() || "";
160
+ if (dName === "img") {
161
+ const ref = desc.getAttribute("binaryItemIDRef");
162
+ if (ref && !results.some((r) => r.binaryItemIdRef === ref)) {
163
+ results.push({ binaryItemIdRef: ref, width, height });
164
+ }
165
+ }
166
+ }
167
+ }
168
+ return results;
169
+ }
170
+ function buildParaPrLookup(doc) {
171
+ var _a;
172
+ const lookup = /* @__PURE__ */ new Map();
173
+ try {
174
+ const headers = doc.headers;
175
+ if (headers.length === 0) return lookup;
176
+ const headerEl = headers[0].element;
177
+ const xml = serializeXml(headerEl);
178
+ const parsed = parseHeaderXml(xml);
179
+ if (!((_a = parsed.refList) == null ? void 0 : _a.paraProperties)) return lookup;
180
+ for (const prop of parsed.refList.paraProperties.properties) {
181
+ if (prop.rawId) lookup.set(prop.rawId, prop);
182
+ if (prop.id != null) lookup.set(String(prop.id), prop);
183
+ }
184
+ } catch (e) {
185
+ }
186
+ return lookup;
187
+ }
188
+ function buildViewModel(doc) {
189
+ var _a, _b, _c;
190
+ const imageMap = extractImages(doc.package);
191
+ const paraPrLookup = buildParaPrLookup(doc);
192
+ const sections = [];
193
+ let globalParaIndex = 0;
194
+ for (let sIdx = 0; sIdx < doc.sections.length; sIdx++) {
195
+ const section = doc.sections[sIdx];
196
+ const props = section.properties;
197
+ const pageSize = props.pageSize;
198
+ const pageMargins = props.pageMargins;
199
+ const sectionVM = {
200
+ pageWidthPx: hwpToPx(pageSize.width),
201
+ pageHeightPx: hwpToPx(pageSize.height),
202
+ marginTopPx: hwpToPx(pageMargins.top),
203
+ marginBottomPx: hwpToPx(pageMargins.bottom),
204
+ marginLeftPx: hwpToPx(pageMargins.left),
205
+ marginRightPx: hwpToPx(pageMargins.right),
206
+ paragraphs: [],
207
+ sectionIndex: sIdx
208
+ };
209
+ const paragraphs = section.paragraphs;
210
+ for (const para of paragraphs) {
211
+ const runs = [];
212
+ const images = [];
213
+ for (const run of para.runs) {
214
+ const style = run.style;
215
+ const {
216
+ bold,
217
+ italic,
218
+ underline,
219
+ strikethrough,
220
+ color,
221
+ fontFamily,
222
+ fontSize,
223
+ highlightColor,
224
+ letterSpacing
225
+ } = extractRunStyle(style);
226
+ const text = run.text;
227
+ if (text) {
228
+ runs.push({
229
+ text,
230
+ bold,
231
+ italic,
232
+ underline,
233
+ strikethrough,
234
+ color,
235
+ fontFamily,
236
+ fontSize,
237
+ highlightColor,
238
+ letterSpacing,
239
+ charPrIdRef: run.charPrIdRef
240
+ });
241
+ }
242
+ const picRefs = findPictureRefs(run.element);
243
+ for (const ref of picRefs) {
244
+ const dataUrl = imageMap.get(ref.binaryItemIdRef);
245
+ if (dataUrl) {
246
+ images.push({
247
+ dataUrl,
248
+ widthPx: hwpToPx(ref.width),
249
+ heightPx: hwpToPx(ref.height),
250
+ binaryItemIdRef: ref.binaryItemIdRef
251
+ });
252
+ }
253
+ }
254
+ }
255
+ const tables = [];
256
+ const paraTables = para.tables;
257
+ for (let tIdx = 0; tIdx < paraTables.length; tIdx++) {
258
+ const table = paraTables[tIdx];
259
+ const rowCount = table.rowCount;
260
+ const colCount = table.columnCount;
261
+ let cellGrid = [];
262
+ try {
263
+ cellGrid = table.getCellMap();
264
+ } catch (e) {
265
+ continue;
266
+ }
267
+ const cellsVM = [];
268
+ for (let r = 0; r < rowCount; r++) {
269
+ const row = [];
270
+ for (let c = 0; c < colCount; c++) {
271
+ const pos = (_a = cellGrid[r]) == null ? void 0 : _a[c];
272
+ if (!pos) {
273
+ row.push({
274
+ row: r,
275
+ col: c,
276
+ rowSpan: 1,
277
+ colSpan: 1,
278
+ widthPx: 0,
279
+ heightPx: 0,
280
+ text: "",
281
+ isAnchor: false
282
+ });
283
+ continue;
284
+ }
285
+ const isAnchor = pos.anchor[0] === r && pos.anchor[1] === c;
286
+ row.push({
287
+ row: r,
288
+ col: c,
289
+ rowSpan: pos.span[0],
290
+ colSpan: pos.span[1],
291
+ widthPx: hwpToPx(pos.cell.width),
292
+ heightPx: hwpToPx(pos.cell.height),
293
+ text: isAnchor ? pos.cell.text : "",
294
+ isAnchor
295
+ });
296
+ }
297
+ cellsVM.push(row);
298
+ }
299
+ tables.push({
300
+ rowCount,
301
+ colCount,
302
+ cells: cellsVM,
303
+ tableIndex: tIdx
304
+ });
305
+ }
306
+ let alignment = "LEFT";
307
+ let lineSpacing = 1.6;
308
+ let spacingBefore = 0;
309
+ let spacingAfter = 0;
310
+ let firstLineIndent = 0;
311
+ let marginLeftPx = 0;
312
+ let marginRightPx = 0;
313
+ const paraPrIdRef = para.paraPrIdRef;
314
+ if (paraPrIdRef) {
315
+ const paraPr = paraPrLookup.get(paraPrIdRef);
316
+ if (paraPr) {
317
+ if ((_b = paraPr.align) == null ? void 0 : _b.horizontal) {
318
+ alignment = paraPr.align.horizontal.toUpperCase();
319
+ }
320
+ if (((_c = paraPr.lineSpacing) == null ? void 0 : _c.value) != null) {
321
+ lineSpacing = paraPr.lineSpacing.value / 100;
322
+ }
323
+ if (paraPr.margin) {
324
+ if (paraPr.margin.left) {
325
+ marginLeftPx = hwpToPx(parseInt(paraPr.margin.left, 10) || 0);
326
+ }
327
+ if (paraPr.margin.right) {
328
+ marginRightPx = hwpToPx(parseInt(paraPr.margin.right, 10) || 0);
329
+ }
330
+ if (paraPr.margin.intent) {
331
+ firstLineIndent = hwpToPx(parseInt(paraPr.margin.intent, 10) || 0);
332
+ }
333
+ if (paraPr.margin.prev) {
334
+ spacingBefore = hwpToPx(parseInt(paraPr.margin.prev, 10) || 0);
335
+ }
336
+ if (paraPr.margin.next) {
337
+ spacingAfter = hwpToPx(parseInt(paraPr.margin.next, 10) || 0);
338
+ }
339
+ }
340
+ }
341
+ }
342
+ sectionVM.paragraphs.push({
343
+ runs,
344
+ tables,
345
+ images,
346
+ alignment,
347
+ lineSpacing,
348
+ spacingBefore,
349
+ spacingAfter,
350
+ firstLineIndent,
351
+ marginLeftPx,
352
+ marginRightPx,
353
+ paragraphIndex: globalParaIndex
354
+ });
355
+ globalParaIndex++;
356
+ }
357
+ sections.push(sectionVM);
358
+ }
359
+ return { sections };
360
+ }
361
+
362
+ // src/lib/format-bridge.ts
363
+ import {
364
+ parseHeaderXml as parseHeaderXml2
365
+ } from "@ubermensch1218/hwpxcore";
366
+ import { serializeXml as serializeXml2 } from "@ubermensch1218/hwpxcore";
367
+ function getParsedHeader(doc) {
368
+ const headers = doc.headers;
369
+ if (headers.length === 0) return null;
370
+ const headerEl = headers[0].element;
371
+ try {
372
+ const xml = serializeXml2(headerEl);
373
+ return parseHeaderXml2(xml);
374
+ } catch (e) {
375
+ return null;
376
+ }
377
+ }
378
+ function readCharFormat(style) {
379
+ var _a, _b, _c, _d;
380
+ const result = {
381
+ bold: false,
382
+ italic: false,
383
+ underline: false,
384
+ strikethrough: false,
385
+ fontFamily: null,
386
+ fontSize: null,
387
+ textColor: null,
388
+ highlightColor: null,
389
+ letterSpacing: null
390
+ };
391
+ if (!style) return result;
392
+ result.bold = style.attributes["bold"] === "1";
393
+ result.italic = style.attributes["italic"] === "1";
394
+ const underlineChild = style.childAttributes["underline"];
395
+ result.underline = underlineChild != null && underlineChild["type"] != null && underlineChild["type"] !== "NONE";
396
+ const strikeChild = style.childAttributes["strikeout"];
397
+ result.strikethrough = strikeChild != null && strikeChild["type"] != null && strikeChild["type"] !== "NONE";
398
+ result.textColor = (_a = style.attributes["textColor"]) != null ? _a : null;
399
+ result.highlightColor = (_b = style.attributes["shadeColor"]) != null ? _b : null;
400
+ const spacing = style.attributes["spacing"];
401
+ if (spacing) {
402
+ const val = parseInt(spacing, 10);
403
+ if (!isNaN(val)) result.letterSpacing = val;
404
+ }
405
+ const fontRef = style.childAttributes["fontRef"];
406
+ if (fontRef) {
407
+ result.fontFamily = (_d = (_c = fontRef["hangul"]) != null ? _c : fontRef["latin"]) != null ? _d : null;
408
+ }
409
+ const sizeStr = style.attributes["height"];
410
+ if (sizeStr) {
411
+ const hwpVal = parseInt(sizeStr, 10);
412
+ if (!isNaN(hwpVal)) result.fontSize = hwpVal / 100;
413
+ }
414
+ return result;
415
+ }
416
+ function readParaFormat(doc, paragraph) {
417
+ var _a, _b, _c;
418
+ const result = {
419
+ alignment: "LEFT",
420
+ lineSpacing: 1.6,
421
+ spacingBefore: 0,
422
+ spacingAfter: 0,
423
+ indentLeft: 0,
424
+ indentRight: 0,
425
+ firstLineIndent: 0
426
+ };
427
+ const paraPrIdRef = paragraph.paraPrIdRef;
428
+ if (!paraPrIdRef) return result;
429
+ const header = getParsedHeader(doc);
430
+ if (!header) return result;
431
+ const paraPr = findParaPr(header, paraPrIdRef);
432
+ if (!paraPr) return result;
433
+ if ((_a = paraPr.align) == null ? void 0 : _a.horizontal) {
434
+ const h = paraPr.align.horizontal.toUpperCase();
435
+ if (h === "LEFT" || h === "CENTER" || h === "RIGHT" || h === "JUSTIFY" || h === "DISTRIBUTE") {
436
+ result.alignment = h;
437
+ }
438
+ }
439
+ if (((_b = paraPr.lineSpacing) == null ? void 0 : _b.value) != null) {
440
+ const spacingType = (_c = paraPr.lineSpacing.spacingType) == null ? void 0 : _c.toUpperCase();
441
+ if (spacingType === "PERCENT" || spacingType === "PROPORTIONAL") {
442
+ result.lineSpacing = paraPr.lineSpacing.value / 100;
443
+ } else {
444
+ result.lineSpacing = paraPr.lineSpacing.value / 100;
445
+ }
446
+ }
447
+ if (paraPr.margin) {
448
+ if (paraPr.margin.left) {
449
+ result.indentLeft = parseInt(paraPr.margin.left, 10) || 0;
450
+ }
451
+ if (paraPr.margin.right) {
452
+ result.indentRight = parseInt(paraPr.margin.right, 10) || 0;
453
+ }
454
+ if (paraPr.margin.intent) {
455
+ result.firstLineIndent = parseInt(paraPr.margin.intent, 10) || 0;
456
+ }
457
+ if (paraPr.margin.prev) {
458
+ result.spacingBefore = parseInt(paraPr.margin.prev, 10) || 0;
459
+ }
460
+ if (paraPr.margin.next) {
461
+ result.spacingAfter = parseInt(paraPr.margin.next, 10) || 0;
462
+ }
463
+ }
464
+ return result;
465
+ }
466
+ function findParaPr(header, paraPrIdRef) {
467
+ var _a;
468
+ if (!((_a = header.refList) == null ? void 0 : _a.paraProperties)) return null;
469
+ for (const prop of header.refList.paraProperties.properties) {
470
+ if (prop.rawId === paraPrIdRef) return prop;
471
+ if (prop.id != null && String(prop.id) === paraPrIdRef) return prop;
472
+ }
473
+ return null;
474
+ }
475
+ function readStyleInfo(doc, paragraph) {
476
+ var _a;
477
+ const styleIdRef = paragraph.styleIdRef;
478
+ if (!styleIdRef) return { styleName: null, styleId: null };
479
+ const header = getParsedHeader(doc);
480
+ if (!((_a = header == null ? void 0 : header.refList) == null ? void 0 : _a.styles)) return { styleName: null, styleId: null };
481
+ for (const style of header.refList.styles.styles) {
482
+ if (style.rawId === styleIdRef || style.id != null && String(style.id) === styleIdRef) {
483
+ return { styleName: style.name, styleId: styleIdRef };
484
+ }
485
+ }
486
+ return { styleName: null, styleId: styleIdRef };
487
+ }
488
+ function getDocumentStyles(doc) {
489
+ var _a;
490
+ const header = getParsedHeader(doc);
491
+ if (!((_a = header == null ? void 0 : header.refList) == null ? void 0 : _a.styles)) return [];
492
+ return header.refList.styles.styles;
493
+ }
494
+ function readFormatFromSelection(doc, sectionIndex, paragraphIndex) {
495
+ const section = doc.sections[sectionIndex];
496
+ if (!section) {
497
+ return {
498
+ char: readCharFormat(null),
499
+ para: {
500
+ alignment: "LEFT",
501
+ lineSpacing: 1.6,
502
+ spacingBefore: 0,
503
+ spacingAfter: 0,
504
+ indentLeft: 0,
505
+ indentRight: 0,
506
+ firstLineIndent: 0
507
+ }
508
+ };
509
+ }
510
+ const paras = section.paragraphs;
511
+ const para = paras[paragraphIndex];
512
+ if (!para) {
513
+ return {
514
+ char: readCharFormat(null),
515
+ para: {
516
+ alignment: "LEFT",
517
+ lineSpacing: 1.6,
518
+ spacingBefore: 0,
519
+ spacingAfter: 0,
520
+ indentLeft: 0,
521
+ indentRight: 0,
522
+ firstLineIndent: 0
523
+ }
524
+ };
525
+ }
526
+ const runs = para.runs;
527
+ let charStyle = null;
528
+ if (runs.length > 0) {
529
+ charStyle = runs[0].style;
530
+ } else {
531
+ const charPrIdRef = para.charPrIdRef;
532
+ if (charPrIdRef) {
533
+ charStyle = doc.charProperty(charPrIdRef);
534
+ }
535
+ }
536
+ return {
537
+ char: readCharFormat(charStyle),
538
+ para: readParaFormat(doc, para)
539
+ };
540
+ }
541
+
542
+ // src/lib/store.ts
543
+ var defaultCharFormat = {
544
+ bold: false,
545
+ italic: false,
546
+ underline: false,
547
+ strikethrough: false,
548
+ fontFamily: null,
549
+ fontSize: null,
550
+ textColor: null,
551
+ highlightColor: null,
552
+ letterSpacing: null
553
+ };
554
+ var defaultParaFormat = {
555
+ alignment: "LEFT",
556
+ lineSpacing: 1.6,
557
+ spacingBefore: 0,
558
+ spacingAfter: 0,
559
+ indentLeft: 0,
560
+ indentRight: 0,
561
+ firstLineIndent: 0
562
+ };
563
+ var useEditorStore = create((set, get) => ({
564
+ doc: null,
565
+ viewModel: null,
566
+ revision: 0,
567
+ selection: null,
568
+ activeFormat: { bold: false, italic: false, underline: false },
569
+ extendedFormat: { char: defaultCharFormat, para: defaultParaFormat },
570
+ uiState: { sidebarOpen: true, sidebarTab: "char" },
571
+ loading: false,
572
+ error: null,
573
+ setDocument: (doc) => {
574
+ const viewModel = buildViewModel(doc);
575
+ set({ doc, viewModel, revision: get().revision + 1, error: null });
576
+ },
577
+ rebuild: () => {
578
+ const { doc } = get();
579
+ if (!doc) return;
580
+ const viewModel = buildViewModel(doc);
581
+ set({ viewModel, revision: get().revision + 1 });
582
+ },
583
+ setSelection: (selection) => {
584
+ set({ selection });
585
+ if (selection) {
586
+ setTimeout(() => get().refreshExtendedFormat(), 0);
587
+ }
588
+ },
589
+ setActiveFormat: (fmt) => set((s) => ({ activeFormat: __spreadValues(__spreadValues({}, s.activeFormat), fmt) })),
590
+ refreshExtendedFormat: () => {
591
+ const { doc, selection } = get();
592
+ if (!doc || !selection) return;
593
+ try {
594
+ const fmt = readFormatFromSelection(
595
+ doc,
596
+ selection.sectionIndex,
597
+ selection.paragraphIndex
598
+ );
599
+ set({
600
+ extendedFormat: fmt,
601
+ activeFormat: {
602
+ bold: fmt.char.bold,
603
+ italic: fmt.char.italic,
604
+ underline: fmt.char.underline
605
+ }
606
+ });
607
+ } catch (e) {
608
+ console.error("refreshExtendedFormat failed:", e);
609
+ }
610
+ },
611
+ // UI actions
612
+ toggleSidebar: () => set((s) => ({
613
+ uiState: __spreadProps(__spreadValues({}, s.uiState), { sidebarOpen: !s.uiState.sidebarOpen })
614
+ })),
615
+ setSidebarTab: (tab) => set((s) => ({
616
+ uiState: __spreadProps(__spreadValues({}, s.uiState), { sidebarTab: tab })
617
+ })),
618
+ setLoading: (loading) => set({ loading }),
619
+ setError: (error) => set({ error }),
620
+ updateParagraphText: (sectionIndex, paragraphIndex, text) => {
621
+ const { doc } = get();
622
+ if (!doc) return;
623
+ try {
624
+ const section = doc.sections[sectionIndex];
625
+ if (!section) return;
626
+ const paras = section.paragraphs;
627
+ const para = paras[paragraphIndex];
628
+ if (!para) return;
629
+ para.text = text;
630
+ get().rebuild();
631
+ } catch (e) {
632
+ console.error("updateParagraphText failed:", e);
633
+ }
634
+ },
635
+ updateCellText: (sectionIndex, paragraphIndex, tableIndex, row, col, text) => {
636
+ const { doc } = get();
637
+ if (!doc) return;
638
+ try {
639
+ const section = doc.sections[sectionIndex];
640
+ if (!section) return;
641
+ const paras = section.paragraphs;
642
+ const para = paras[paragraphIndex];
643
+ if (!para) return;
644
+ const table = para.tables[tableIndex];
645
+ if (!table) return;
646
+ table.setCellText(row, col, text);
647
+ get().rebuild();
648
+ } catch (e) {
649
+ console.error("updateCellText failed:", e);
650
+ }
651
+ },
652
+ toggleBold: () => {
653
+ const { doc, activeFormat, selection } = get();
654
+ if (!doc || !selection) return;
655
+ const newBold = !activeFormat.bold;
656
+ try {
657
+ const charPrIdRef = doc.ensureRunStyle({
658
+ bold: newBold,
659
+ italic: activeFormat.italic,
660
+ underline: activeFormat.underline
661
+ });
662
+ const section = doc.sections[selection.sectionIndex];
663
+ if (!section) return;
664
+ const para = section.paragraphs[selection.paragraphIndex];
665
+ if (!para) return;
666
+ para.charPrIdRef = charPrIdRef;
667
+ set({ activeFormat: __spreadProps(__spreadValues({}, activeFormat), { bold: newBold }) });
668
+ get().rebuild();
669
+ } catch (e) {
670
+ console.error("toggleBold failed:", e);
671
+ }
672
+ },
673
+ toggleItalic: () => {
674
+ const { doc, activeFormat, selection } = get();
675
+ if (!doc || !selection) return;
676
+ const newItalic = !activeFormat.italic;
677
+ try {
678
+ const charPrIdRef = doc.ensureRunStyle({
679
+ bold: activeFormat.bold,
680
+ italic: newItalic,
681
+ underline: activeFormat.underline
682
+ });
683
+ const section = doc.sections[selection.sectionIndex];
684
+ if (!section) return;
685
+ const para = section.paragraphs[selection.paragraphIndex];
686
+ if (!para) return;
687
+ para.charPrIdRef = charPrIdRef;
688
+ set({ activeFormat: __spreadProps(__spreadValues({}, activeFormat), { italic: newItalic }) });
689
+ get().rebuild();
690
+ } catch (e) {
691
+ console.error("toggleItalic failed:", e);
692
+ }
693
+ },
694
+ toggleUnderline: () => {
695
+ const { doc, activeFormat, selection } = get();
696
+ if (!doc || !selection) return;
697
+ const newUnderline = !activeFormat.underline;
698
+ try {
699
+ const charPrIdRef = doc.ensureRunStyle({
700
+ bold: activeFormat.bold,
701
+ italic: activeFormat.italic,
702
+ underline: newUnderline
703
+ });
704
+ const section = doc.sections[selection.sectionIndex];
705
+ if (!section) return;
706
+ const para = section.paragraphs[selection.paragraphIndex];
707
+ if (!para) return;
708
+ para.charPrIdRef = charPrIdRef;
709
+ set({ activeFormat: __spreadProps(__spreadValues({}, activeFormat), { underline: newUnderline }) });
710
+ get().rebuild();
711
+ } catch (e) {
712
+ console.error("toggleUnderline failed:", e);
713
+ }
714
+ },
715
+ addParagraph: (text = "") => {
716
+ const { doc } = get();
717
+ if (!doc) return;
718
+ try {
719
+ doc.addParagraph(text);
720
+ get().rebuild();
721
+ } catch (e) {
722
+ console.error("addParagraph failed:", e);
723
+ }
724
+ },
725
+ addTable: (sectionIndex, paragraphIndex, rows, cols) => {
726
+ const { doc } = get();
727
+ if (!doc) return;
728
+ try {
729
+ const section = doc.sections[sectionIndex];
730
+ if (!section) return;
731
+ const para = section.paragraphs[paragraphIndex];
732
+ if (!para) return;
733
+ para.addTable(rows, cols);
734
+ get().rebuild();
735
+ } catch (e) {
736
+ console.error("addTable failed:", e);
737
+ }
738
+ },
739
+ insertImage: (data, mediaType, widthMm, heightMm) => {
740
+ const { doc } = get();
741
+ if (!doc) return;
742
+ try {
743
+ doc.addImage(data, { mediaType, widthMm, heightMm });
744
+ get().rebuild();
745
+ } catch (e) {
746
+ console.error("insertImage failed:", e);
747
+ }
748
+ },
749
+ saveDocument: async () => {
750
+ const { doc } = get();
751
+ if (!doc) return;
752
+ try {
753
+ set({ loading: true });
754
+ const bytes = await doc.save();
755
+ const blob = new Blob([bytes], {
756
+ type: "application/vnd.hancom.hwpx"
757
+ });
758
+ const url = URL.createObjectURL(blob);
759
+ const a = document.createElement("a");
760
+ a.href = url;
761
+ a.download = "document.hwpx";
762
+ a.click();
763
+ URL.revokeObjectURL(url);
764
+ } catch (e) {
765
+ console.error("save failed:", e);
766
+ set({ error: "\uBB38\uC11C \uC800\uC7A5\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4." });
767
+ } finally {
768
+ set({ loading: false });
769
+ }
770
+ }
771
+ }));
772
+
773
+ // src/lib/skeleton-loader.ts
774
+ import { setSkeletonHwpx, HwpxDocument } from "@ubermensch1218/hwpxcore";
775
+ var loaded = false;
776
+ async function ensureSkeletonLoaded() {
777
+ if (loaded) return;
778
+ const res = await fetch("/Skeleton.hwpx");
779
+ if (!res.ok) throw new Error(`Failed to fetch Skeleton.hwpx: ${res.status}`);
780
+ const buf = await res.arrayBuffer();
781
+ setSkeletonHwpx(new Uint8Array(buf));
782
+ loaded = true;
783
+ }
784
+ async function createNewDocument() {
785
+ await ensureSkeletonLoaded();
786
+ const res = await fetch("/Skeleton.hwpx");
787
+ const buf = await res.arrayBuffer();
788
+ return HwpxDocument.open(buf);
789
+ }
790
+
791
+ // src/components/toolbar/ClipboardGroup.tsx
792
+ import { Clipboard, Copy, ClipboardPaste, Paintbrush } from "lucide-react";
793
+
794
+ // src/components/toolbar/ToolbarButton.tsx
795
+ import { jsx, jsxs } from "react/jsx-runtime";
796
+ function ToolbarButton({
797
+ icon,
798
+ label,
799
+ active,
800
+ disabled,
801
+ onClick,
802
+ title,
803
+ size = "sm",
804
+ className = ""
805
+ }) {
806
+ const sizeClass = size === "md" ? "p-2" : "p-1.5";
807
+ return /* @__PURE__ */ jsxs(
808
+ "button",
809
+ {
810
+ disabled,
811
+ onClick,
812
+ title,
813
+ className: `${sizeClass} rounded transition-colors flex items-center gap-1 ${active ? "bg-blue-100 text-blue-700" : "text-gray-600 hover:bg-gray-100"} disabled:opacity-40 disabled:cursor-not-allowed ${className}`,
814
+ children: [
815
+ icon,
816
+ label && /* @__PURE__ */ jsx("span", { className: "text-xs", children: label })
817
+ ]
818
+ }
819
+ );
820
+ }
821
+
822
+ // src/components/toolbar/RibbonGroup.tsx
823
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
824
+ function RibbonGroup({ label, children }) {
825
+ return /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center border-r border-gray-200 px-2 last:border-r-0", children: [
826
+ /* @__PURE__ */ jsx2("div", { className: "flex items-center gap-0.5 py-1", children }),
827
+ /* @__PURE__ */ jsx2("span", { className: "text-[10px] text-gray-400 pb-0.5", children: label })
828
+ ] });
829
+ }
830
+
831
+ // src/components/toolbar/ClipboardGroup.tsx
832
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
833
+ function ClipboardGroup() {
834
+ return /* @__PURE__ */ jsxs3(RibbonGroup, { label: "\uD074\uB9BD\uBCF4\uB4DC", children: [
835
+ /* @__PURE__ */ jsx3(
836
+ ToolbarButton,
837
+ {
838
+ icon: /* @__PURE__ */ jsx3(Clipboard, { className: "w-4 h-4" }),
839
+ title: "\uC624\uB824\uB450\uAE30 (Ctrl+X)",
840
+ onClick: () => document.execCommand("cut")
841
+ }
842
+ ),
843
+ /* @__PURE__ */ jsx3(
844
+ ToolbarButton,
845
+ {
846
+ icon: /* @__PURE__ */ jsx3(Copy, { className: "w-4 h-4" }),
847
+ title: "\uBCF5\uC0AC (Ctrl+C)",
848
+ onClick: () => document.execCommand("copy")
849
+ }
850
+ ),
851
+ /* @__PURE__ */ jsx3(
852
+ ToolbarButton,
853
+ {
854
+ icon: /* @__PURE__ */ jsx3(ClipboardPaste, { className: "w-4 h-4" }),
855
+ title: "\uBD99\uC774\uAE30 (Ctrl+V)",
856
+ onClick: () => document.execCommand("paste")
857
+ }
858
+ ),
859
+ /* @__PURE__ */ jsx3(
860
+ ToolbarButton,
861
+ {
862
+ icon: /* @__PURE__ */ jsx3(Paintbrush, { className: "w-4 h-4" }),
863
+ title: "\uBAA8\uC591 \uBCF5\uC0AC",
864
+ disabled: true
865
+ }
866
+ )
867
+ ] });
868
+ }
869
+
870
+ // src/components/toolbar/InsertGroup.tsx
871
+ import { useCallback, useRef, useState } from "react";
872
+ import { Table, BarChart3, Shapes, ImageIcon, Save } from "lucide-react";
873
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
874
+ function InsertGroup() {
875
+ const doc = useEditorStore((s) => s.doc);
876
+ const selection = useEditorStore((s) => s.selection);
877
+ const addTable = useEditorStore((s) => s.addTable);
878
+ const insertImage = useEditorStore((s) => s.insertImage);
879
+ const saveDocument = useEditorStore((s) => s.saveDocument);
880
+ const loading = useEditorStore((s) => s.loading);
881
+ const [showTableDialog, setShowTableDialog] = useState(false);
882
+ const [tableRows, setTableRows] = useState(3);
883
+ const [tableCols, setTableCols] = useState(3);
884
+ const imageInputRef = useRef(null);
885
+ const disabled = !doc;
886
+ const handleAddTable = useCallback(() => {
887
+ if (!selection) return;
888
+ addTable(selection.sectionIndex, selection.paragraphIndex, tableRows, tableCols);
889
+ setShowTableDialog(false);
890
+ }, [selection, tableRows, tableCols, addTable]);
891
+ const handleImageSelect = useCallback(
892
+ async (e) => {
893
+ var _a;
894
+ const file = (_a = e.target.files) == null ? void 0 : _a[0];
895
+ if (!file) return;
896
+ const buffer = await file.arrayBuffer();
897
+ const data = new Uint8Array(buffer);
898
+ const img = new window.Image();
899
+ const url = URL.createObjectURL(file);
900
+ img.onload = () => {
901
+ const widthMm = img.naturalWidth / 96 * 25.4;
902
+ const heightMm = img.naturalHeight / 96 * 25.4;
903
+ insertImage(data, file.type, widthMm, heightMm);
904
+ URL.revokeObjectURL(url);
905
+ };
906
+ img.src = url;
907
+ e.target.value = "";
908
+ },
909
+ [insertImage]
910
+ );
911
+ return /* @__PURE__ */ jsxs4(Fragment, { children: [
912
+ /* @__PURE__ */ jsxs4(RibbonGroup, { label: "\uC0BD\uC785", children: [
913
+ /* @__PURE__ */ jsx4(
914
+ ToolbarButton,
915
+ {
916
+ icon: /* @__PURE__ */ jsx4(Table, { className: "w-4 h-4" }),
917
+ title: "\uD45C \uC0BD\uC785",
918
+ disabled: disabled || !selection,
919
+ onClick: () => setShowTableDialog(true)
920
+ }
921
+ ),
922
+ /* @__PURE__ */ jsx4(
923
+ ToolbarButton,
924
+ {
925
+ icon: /* @__PURE__ */ jsx4(BarChart3, { className: "w-4 h-4" }),
926
+ title: "\uCC28\uD2B8 \uC0BD\uC785",
927
+ disabled: true
928
+ }
929
+ ),
930
+ /* @__PURE__ */ jsx4(
931
+ ToolbarButton,
932
+ {
933
+ icon: /* @__PURE__ */ jsx4(Shapes, { className: "w-4 h-4" }),
934
+ title: "\uB3C4\uD615 \uC0BD\uC785",
935
+ disabled: true
936
+ }
937
+ ),
938
+ /* @__PURE__ */ jsx4(
939
+ ToolbarButton,
940
+ {
941
+ icon: /* @__PURE__ */ jsx4(ImageIcon, { className: "w-4 h-4" }),
942
+ title: "\uADF8\uB9BC \uC0BD\uC785",
943
+ disabled,
944
+ onClick: () => {
945
+ var _a;
946
+ return (_a = imageInputRef.current) == null ? void 0 : _a.click();
947
+ }
948
+ }
949
+ ),
950
+ /* @__PURE__ */ jsx4(
951
+ "input",
952
+ {
953
+ ref: imageInputRef,
954
+ type: "file",
955
+ accept: "image/*",
956
+ className: "hidden",
957
+ onChange: handleImageSelect
958
+ }
959
+ )
960
+ ] }),
961
+ /* @__PURE__ */ jsx4(RibbonGroup, { label: "\uD30C\uC77C", children: /* @__PURE__ */ jsx4(
962
+ ToolbarButton,
963
+ {
964
+ icon: /* @__PURE__ */ jsx4(Save, { className: "w-4 h-4" }),
965
+ title: "\uC800\uC7A5 (Ctrl+S)",
966
+ disabled: disabled || loading,
967
+ onClick: () => saveDocument()
968
+ }
969
+ ) }),
970
+ showTableDialog && /* @__PURE__ */ jsx4("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/30", children: /* @__PURE__ */ jsxs4("div", { className: "bg-white rounded-lg shadow-xl p-6 min-w-[280px]", children: [
971
+ /* @__PURE__ */ jsx4("h3", { className: "font-semibold mb-4", children: "\uD45C \uC0BD\uC785" }),
972
+ /* @__PURE__ */ jsxs4("div", { className: "flex gap-4 mb-4", children: [
973
+ /* @__PURE__ */ jsxs4("label", { className: "flex flex-col gap-1", children: [
974
+ /* @__PURE__ */ jsx4("span", { className: "text-sm text-gray-600", children: "\uD589" }),
975
+ /* @__PURE__ */ jsx4(
976
+ "input",
977
+ {
978
+ type: "number",
979
+ min: 1,
980
+ max: 50,
981
+ value: tableRows,
982
+ onChange: (e) => setTableRows(Number(e.target.value)),
983
+ className: "border rounded px-2 py-1 w-20"
984
+ }
985
+ )
986
+ ] }),
987
+ /* @__PURE__ */ jsxs4("label", { className: "flex flex-col gap-1", children: [
988
+ /* @__PURE__ */ jsx4("span", { className: "text-sm text-gray-600", children: "\uC5F4" }),
989
+ /* @__PURE__ */ jsx4(
990
+ "input",
991
+ {
992
+ type: "number",
993
+ min: 1,
994
+ max: 20,
995
+ value: tableCols,
996
+ onChange: (e) => setTableCols(Number(e.target.value)),
997
+ className: "border rounded px-2 py-1 w-20"
998
+ }
999
+ )
1000
+ ] })
1001
+ ] }),
1002
+ /* @__PURE__ */ jsxs4("div", { className: "flex justify-end gap-2", children: [
1003
+ /* @__PURE__ */ jsx4(
1004
+ "button",
1005
+ {
1006
+ onClick: () => setShowTableDialog(false),
1007
+ className: "px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded",
1008
+ children: "\uCDE8\uC18C"
1009
+ }
1010
+ ),
1011
+ /* @__PURE__ */ jsx4(
1012
+ "button",
1013
+ {
1014
+ onClick: handleAddTable,
1015
+ className: "px-4 py-2 text-sm bg-blue-500 text-white rounded hover:bg-blue-600",
1016
+ children: "\uC0BD\uC785"
1017
+ }
1018
+ )
1019
+ ] })
1020
+ ] }) })
1021
+ ] });
1022
+ }
1023
+
1024
+ // src/components/toolbar/RibbonToolbar.tsx
1025
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1026
+ function RibbonToolbar() {
1027
+ return /* @__PURE__ */ jsxs5("div", { className: "flex items-stretch bg-white border-b border-gray-200 px-1 min-h-[52px]", children: [
1028
+ /* @__PURE__ */ jsx5(ClipboardGroup, {}),
1029
+ /* @__PURE__ */ jsx5(InsertGroup, {})
1030
+ ] });
1031
+ }
1032
+
1033
+ // src/lib/constants.ts
1034
+ var FONT_FAMILIES = [
1035
+ "\uB9D1\uC740 \uACE0\uB515",
1036
+ "\uD568\uCD08\uB86C\uB3CB\uC6C0",
1037
+ "\uD568\uCD08\uB86C\uBC14\uD0D5",
1038
+ "\uB098\uB214\uACE0\uB515",
1039
+ "\uB098\uB214\uBA85\uC870",
1040
+ "\uB098\uB214\uBC14\uB978\uACE0\uB515",
1041
+ "\uBC14\uD0D5",
1042
+ "\uB3CB\uC6C0",
1043
+ "\uAD74\uB9BC",
1044
+ "\uAD81\uC11C",
1045
+ "Arial",
1046
+ "Times New Roman",
1047
+ "Courier New",
1048
+ "Verdana",
1049
+ "Georgia"
1050
+ ];
1051
+ var FONT_SIZES = [
1052
+ 8,
1053
+ 9,
1054
+ 10,
1055
+ 10.5,
1056
+ 11,
1057
+ 12,
1058
+ 14,
1059
+ 16,
1060
+ 18,
1061
+ 20,
1062
+ 22,
1063
+ 24,
1064
+ 28,
1065
+ 32,
1066
+ 36,
1067
+ 48,
1068
+ 72
1069
+ ];
1070
+ var STYLE_PRESETS = [
1071
+ { id: "0", name: "\uBC14\uD0D5\uAE00", engName: "Normal" },
1072
+ { id: "1", name: "\uBCF8\uBB38", engName: "Body" },
1073
+ { id: "2", name: "\uAC1C\uC694 1", engName: "Outline 1" },
1074
+ { id: "3", name: "\uAC1C\uC694 2", engName: "Outline 2" },
1075
+ { id: "4", name: "\uAC1C\uC694 3", engName: "Outline 3" },
1076
+ { id: "5", name: "\uCABD \uBC88\uD638", engName: "Page Number" },
1077
+ { id: "6", name: "\uBA38\uB9AC\uB9D0", engName: "Header" },
1078
+ { id: "7", name: "\uAF2C\uB9AC\uB9D0", engName: "Footer" },
1079
+ { id: "8", name: "\uAC01\uC8FC", engName: "Footnote" },
1080
+ { id: "9", name: "\uBBF8\uC8FC", engName: "Endnote" }
1081
+ ];
1082
+ var ALIGNMENT_OPTIONS = [
1083
+ { value: "LEFT", label: "\uC67C\uCABD \uC815\uB82C", shortcut: "Ctrl+L" },
1084
+ { value: "CENTER", label: "\uAC00\uC6B4\uB370 \uC815\uB82C", shortcut: "Ctrl+E" },
1085
+ { value: "RIGHT", label: "\uC624\uB978\uCABD \uC815\uB82C", shortcut: "Ctrl+R" },
1086
+ { value: "JUSTIFY", label: "\uC591\uCABD \uC815\uB82C", shortcut: "Ctrl+J" },
1087
+ { value: "DISTRIBUTE", label: "\uBC30\uBD84 \uC815\uB82C" }
1088
+ ];
1089
+ var LINE_SPACING_OPTIONS = [
1090
+ { value: 1, label: "1.0" },
1091
+ { value: 1.15, label: "1.15" },
1092
+ { value: 1.5, label: "1.5" },
1093
+ { value: 1.6, label: "1.6" },
1094
+ { value: 2, label: "2.0" },
1095
+ { value: 2.5, label: "2.5" },
1096
+ { value: 3, label: "3.0" }
1097
+ ];
1098
+ var UNDERLINE_TYPES = [
1099
+ "NONE",
1100
+ "SOLID",
1101
+ "DASH",
1102
+ "DOT",
1103
+ "DASH_DOT",
1104
+ "DASH_DOT_DOT",
1105
+ "LONG_DASH",
1106
+ "DOUBLE",
1107
+ "WAVE",
1108
+ "HEAVY_WAVE",
1109
+ "DOUBLE_WAVE"
1110
+ ];
1111
+ var COLOR_PRESETS = [
1112
+ // Row 1: Primary colors
1113
+ "#000000",
1114
+ "#333333",
1115
+ "#555555",
1116
+ "#777777",
1117
+ "#999999",
1118
+ "#BBBBBB",
1119
+ "#DDDDDD",
1120
+ "#FFFFFF",
1121
+ // Row 2: Vivid colors
1122
+ "#FF0000",
1123
+ "#FF6600",
1124
+ "#FFCC00",
1125
+ "#33CC33",
1126
+ "#0099FF",
1127
+ "#3366FF",
1128
+ "#6633CC",
1129
+ "#CC33CC",
1130
+ // Row 3: Pastel colors
1131
+ "#FF9999",
1132
+ "#FFCC99",
1133
+ "#FFFF99",
1134
+ "#CCFFCC",
1135
+ "#99CCFF",
1136
+ "#9999FF",
1137
+ "#CC99FF",
1138
+ "#FF99CC",
1139
+ // Row 4: Dark colors
1140
+ "#CC0000",
1141
+ "#CC6600",
1142
+ "#CC9900",
1143
+ "#009900",
1144
+ "#006699",
1145
+ "#003399",
1146
+ "#330099",
1147
+ "#990066"
1148
+ ];
1149
+ var HIGHLIGHT_COLORS = [
1150
+ { value: "#FFFF00", label: "\uB178\uB791" },
1151
+ { value: "#00FF00", label: "\uC5F0\uB450" },
1152
+ { value: "#00FFFF", label: "\uD558\uB298" },
1153
+ { value: "#FF00FF", label: "\uBD84\uD64D" },
1154
+ { value: "#0000FF", label: "\uD30C\uB791" },
1155
+ { value: "#FF0000", label: "\uBE68\uAC15" },
1156
+ { value: "#008000", label: "\uCD08\uB85D" },
1157
+ { value: "#800080", label: "\uBCF4\uB77C" },
1158
+ { value: "#808080", label: "\uD68C\uC0C9" },
1159
+ { value: "none", label: "\uC5C6\uC74C" }
1160
+ ];
1161
+
1162
+ // src/components/toolbar/ToolbarDropdown.tsx
1163
+ import { useState as useState2, useRef as useRef2, useEffect } from "react";
1164
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1165
+ function ToolbarDropdown({
1166
+ value,
1167
+ options,
1168
+ onChange,
1169
+ disabled,
1170
+ title,
1171
+ className = "",
1172
+ width = "w-28",
1173
+ icon
1174
+ }) {
1175
+ var _a, _b;
1176
+ const [open, setOpen] = useState2(false);
1177
+ const ref = useRef2(null);
1178
+ useEffect(() => {
1179
+ if (!open) return;
1180
+ const handler = (e) => {
1181
+ if (ref.current && !ref.current.contains(e.target)) {
1182
+ setOpen(false);
1183
+ }
1184
+ };
1185
+ document.addEventListener("mousedown", handler);
1186
+ return () => document.removeEventListener("mousedown", handler);
1187
+ }, [open]);
1188
+ const selectedLabel = (_b = (_a = options.find((o) => o.value === value)) == null ? void 0 : _a.label) != null ? _b : value;
1189
+ return /* @__PURE__ */ jsxs6("div", { ref, className: `relative ${className}`, title, children: [
1190
+ /* @__PURE__ */ jsxs6(
1191
+ "button",
1192
+ {
1193
+ disabled,
1194
+ onClick: () => setOpen(!open),
1195
+ className: `flex items-center gap-1 ${width} h-7 px-2 text-xs border border-gray-300 rounded bg-white hover:bg-gray-50 text-left disabled:opacity-40 disabled:cursor-not-allowed`,
1196
+ children: [
1197
+ icon,
1198
+ /* @__PURE__ */ jsx6("span", { className: "flex-1 truncate", children: selectedLabel }),
1199
+ /* @__PURE__ */ jsx6("svg", { className: "w-3 h-3 text-gray-400 flex-shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx6("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }) })
1200
+ ]
1201
+ }
1202
+ ),
1203
+ open && /* @__PURE__ */ jsx6("div", { className: "absolute top-full left-0 mt-1 z-50 bg-white border border-gray-200 rounded shadow-lg max-h-64 overflow-auto min-w-full", children: options.map((opt) => /* @__PURE__ */ jsx6(
1204
+ "button",
1205
+ {
1206
+ onClick: () => {
1207
+ onChange(opt.value);
1208
+ setOpen(false);
1209
+ },
1210
+ className: `block w-full text-left px-3 py-1.5 text-xs hover:bg-blue-50 ${opt.value === value ? "bg-blue-50 text-blue-700 font-medium" : "text-gray-700"}`,
1211
+ children: opt.label
1212
+ },
1213
+ opt.value
1214
+ )) })
1215
+ ] });
1216
+ }
1217
+
1218
+ // src/components/toolbar/StyleSelector.tsx
1219
+ import { jsx as jsx7 } from "react/jsx-runtime";
1220
+ function StyleSelector({ value, onChange, disabled }) {
1221
+ const options = STYLE_PRESETS.map((s) => ({
1222
+ value: s.id,
1223
+ label: s.name
1224
+ }));
1225
+ return /* @__PURE__ */ jsx7(
1226
+ ToolbarDropdown,
1227
+ {
1228
+ value,
1229
+ options,
1230
+ onChange,
1231
+ disabled,
1232
+ title: "\uC2A4\uD0C0\uC77C",
1233
+ width: "w-24"
1234
+ }
1235
+ );
1236
+ }
1237
+
1238
+ // src/components/toolbar/FontSelector.tsx
1239
+ import { jsx as jsx8 } from "react/jsx-runtime";
1240
+ function FontSelector({ value, onChange, disabled }) {
1241
+ const options = FONT_FAMILIES.map((f) => ({
1242
+ value: f,
1243
+ label: f
1244
+ }));
1245
+ return /* @__PURE__ */ jsx8(
1246
+ ToolbarDropdown,
1247
+ {
1248
+ value,
1249
+ options,
1250
+ onChange,
1251
+ disabled,
1252
+ title: "\uAE00\uAF34",
1253
+ width: "w-32"
1254
+ }
1255
+ );
1256
+ }
1257
+
1258
+ // src/components/toolbar/FontSizeInput.tsx
1259
+ import { useState as useState3, useCallback as useCallback2 } from "react";
1260
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1261
+ function FontSizeInput({ value, onChange, disabled }) {
1262
+ const [editing, setEditing] = useState3(false);
1263
+ const [inputValue, setInputValue] = useState3(String(value));
1264
+ const handleBlur = useCallback2(() => {
1265
+ setEditing(false);
1266
+ const num = parseFloat(inputValue);
1267
+ if (!isNaN(num) && num > 0 && num <= 200) {
1268
+ onChange(num);
1269
+ } else {
1270
+ setInputValue(String(value));
1271
+ }
1272
+ }, [inputValue, value, onChange]);
1273
+ const handleKeyDown = useCallback2(
1274
+ (e) => {
1275
+ if (e.key === "Enter") {
1276
+ e.target.blur();
1277
+ } else if (e.key === "Escape") {
1278
+ setInputValue(String(value));
1279
+ setEditing(false);
1280
+ }
1281
+ },
1282
+ [value]
1283
+ );
1284
+ if (editing) {
1285
+ return /* @__PURE__ */ jsx9(
1286
+ "input",
1287
+ {
1288
+ type: "text",
1289
+ value: inputValue,
1290
+ onChange: (e) => setInputValue(e.target.value),
1291
+ onBlur: handleBlur,
1292
+ onKeyDown: handleKeyDown,
1293
+ autoFocus: true,
1294
+ className: "w-14 h-7 px-2 text-xs border border-blue-400 rounded text-center focus:outline-none"
1295
+ }
1296
+ );
1297
+ }
1298
+ return /* @__PURE__ */ jsx9("div", { className: "relative", children: /* @__PURE__ */ jsxs7(
1299
+ "select",
1300
+ {
1301
+ value: String(value),
1302
+ onChange: (e) => {
1303
+ const num = parseFloat(e.target.value);
1304
+ if (!isNaN(num)) onChange(num);
1305
+ },
1306
+ disabled,
1307
+ className: "w-14 h-7 px-1 text-xs border border-gray-300 rounded bg-white text-center appearance-none cursor-pointer disabled:opacity-40",
1308
+ title: "\uAE00\uC790 \uD06C\uAE30",
1309
+ onDoubleClick: () => {
1310
+ setInputValue(String(value));
1311
+ setEditing(true);
1312
+ },
1313
+ children: [
1314
+ FONT_SIZES.map((s) => /* @__PURE__ */ jsx9("option", { value: String(s), children: s }, s)),
1315
+ !FONT_SIZES.includes(value) && /* @__PURE__ */ jsx9("option", { value: String(value), children: value })
1316
+ ]
1317
+ }
1318
+ ) });
1319
+ }
1320
+
1321
+ // src/components/toolbar/CharFormatButtons.tsx
1322
+ import {
1323
+ Bold,
1324
+ Italic,
1325
+ Underline,
1326
+ Strikethrough,
1327
+ Highlighter,
1328
+ Type
1329
+ } from "lucide-react";
1330
+
1331
+ // src/components/toolbar/ColorPicker.tsx
1332
+ import { useState as useState4, useRef as useRef3, useEffect as useEffect2 } from "react";
1333
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
1334
+ function ColorPicker({
1335
+ color,
1336
+ onChange,
1337
+ icon,
1338
+ title,
1339
+ disabled
1340
+ }) {
1341
+ const [open, setOpen] = useState4(false);
1342
+ const ref = useRef3(null);
1343
+ useEffect2(() => {
1344
+ if (!open) return;
1345
+ const handler = (e) => {
1346
+ if (ref.current && !ref.current.contains(e.target)) {
1347
+ setOpen(false);
1348
+ }
1349
+ };
1350
+ document.addEventListener("mousedown", handler);
1351
+ return () => document.removeEventListener("mousedown", handler);
1352
+ }, [open]);
1353
+ return /* @__PURE__ */ jsxs8("div", { ref, className: "relative", children: [
1354
+ /* @__PURE__ */ jsxs8(
1355
+ "button",
1356
+ {
1357
+ disabled,
1358
+ onClick: () => setOpen(!open),
1359
+ title,
1360
+ className: "p-1.5 rounded transition-colors text-gray-600 hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed flex flex-col items-center",
1361
+ children: [
1362
+ icon,
1363
+ /* @__PURE__ */ jsx10(
1364
+ "div",
1365
+ {
1366
+ className: "w-4 h-0.5 mt-0.5 rounded-sm",
1367
+ style: { backgroundColor: color || "#000000" }
1368
+ }
1369
+ )
1370
+ ]
1371
+ }
1372
+ ),
1373
+ open && /* @__PURE__ */ jsxs8("div", { className: "absolute top-full left-0 mt-1 z-50 bg-white border border-gray-200 rounded-lg shadow-lg p-2", children: [
1374
+ /* @__PURE__ */ jsx10("div", { className: "grid grid-cols-8 gap-1", children: COLOR_PRESETS.map((c) => /* @__PURE__ */ jsx10(
1375
+ "button",
1376
+ {
1377
+ onClick: () => {
1378
+ onChange(c);
1379
+ setOpen(false);
1380
+ },
1381
+ className: `w-5 h-5 rounded border ${c === color ? "border-blue-500 ring-1 ring-blue-300" : "border-gray-300"} hover:scale-110 transition-transform`,
1382
+ style: { backgroundColor: c },
1383
+ title: c
1384
+ },
1385
+ c
1386
+ )) }),
1387
+ /* @__PURE__ */ jsxs8("div", { className: "mt-2 pt-2 border-t border-gray-100 flex items-center gap-2", children: [
1388
+ /* @__PURE__ */ jsx10("label", { className: "text-xs text-gray-500", children: "\uCEE4\uC2A4\uD140:" }),
1389
+ /* @__PURE__ */ jsx10(
1390
+ "input",
1391
+ {
1392
+ type: "color",
1393
+ value: color || "#000000",
1394
+ onChange: (e) => {
1395
+ onChange(e.target.value);
1396
+ setOpen(false);
1397
+ },
1398
+ className: "w-6 h-6 cursor-pointer border-0"
1399
+ }
1400
+ )
1401
+ ] })
1402
+ ] })
1403
+ ] });
1404
+ }
1405
+
1406
+ // src/components/toolbar/CharFormatButtons.tsx
1407
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
1408
+ function CharFormatButtons() {
1409
+ const activeFormat = useEditorStore((s) => s.activeFormat);
1410
+ const extendedFormat = useEditorStore((s) => s.extendedFormat);
1411
+ const selection = useEditorStore((s) => s.selection);
1412
+ const doc = useEditorStore((s) => s.doc);
1413
+ const toggleBold = useEditorStore((s) => s.toggleBold);
1414
+ const toggleItalic = useEditorStore((s) => s.toggleItalic);
1415
+ const toggleUnderline = useEditorStore((s) => s.toggleUnderline);
1416
+ const disabled = !doc || !selection;
1417
+ return /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-0.5", children: [
1418
+ /* @__PURE__ */ jsx11(
1419
+ ColorPicker,
1420
+ {
1421
+ color: extendedFormat.char.textColor || "#000000",
1422
+ onChange: () => {
1423
+ },
1424
+ icon: /* @__PURE__ */ jsx11(Type, { className: "w-3.5 h-3.5" }),
1425
+ title: "\uAE00\uC790\uC0C9",
1426
+ disabled
1427
+ }
1428
+ ),
1429
+ /* @__PURE__ */ jsx11(
1430
+ ColorPicker,
1431
+ {
1432
+ color: extendedFormat.char.highlightColor || "#FFFF00",
1433
+ onChange: () => {
1434
+ },
1435
+ icon: /* @__PURE__ */ jsx11(Highlighter, { className: "w-3.5 h-3.5" }),
1436
+ title: "\uD615\uAD11\uD39C",
1437
+ disabled
1438
+ }
1439
+ ),
1440
+ /* @__PURE__ */ jsx11(
1441
+ ToolbarButton,
1442
+ {
1443
+ icon: /* @__PURE__ */ jsx11(Bold, { className: "w-3.5 h-3.5" }),
1444
+ active: activeFormat.bold,
1445
+ disabled,
1446
+ onClick: toggleBold,
1447
+ title: "\uAD75\uAC8C (Ctrl+B)"
1448
+ }
1449
+ ),
1450
+ /* @__PURE__ */ jsx11(
1451
+ ToolbarButton,
1452
+ {
1453
+ icon: /* @__PURE__ */ jsx11(Italic, { className: "w-3.5 h-3.5" }),
1454
+ active: activeFormat.italic,
1455
+ disabled,
1456
+ onClick: toggleItalic,
1457
+ title: "\uAE30\uC6B8\uC784 (Ctrl+I)"
1458
+ }
1459
+ ),
1460
+ /* @__PURE__ */ jsx11(
1461
+ ToolbarButton,
1462
+ {
1463
+ icon: /* @__PURE__ */ jsx11(Underline, { className: "w-3.5 h-3.5" }),
1464
+ active: activeFormat.underline,
1465
+ disabled,
1466
+ onClick: toggleUnderline,
1467
+ title: "\uBC11\uC904 (Ctrl+U)"
1468
+ }
1469
+ ),
1470
+ /* @__PURE__ */ jsx11(
1471
+ ToolbarButton,
1472
+ {
1473
+ icon: /* @__PURE__ */ jsx11(Strikethrough, { className: "w-3.5 h-3.5" }),
1474
+ active: extendedFormat.char.strikethrough,
1475
+ disabled,
1476
+ title: "\uCDE8\uC18C\uC120"
1477
+ }
1478
+ )
1479
+ ] });
1480
+ }
1481
+
1482
+ // src/components/toolbar/AlignmentButtons.tsx
1483
+ import {
1484
+ AlignLeft,
1485
+ AlignCenter,
1486
+ AlignRight,
1487
+ AlignJustify,
1488
+ Columns2
1489
+ } from "lucide-react";
1490
+ import { jsx as jsx12 } from "react/jsx-runtime";
1491
+ var ALIGNMENT_ICONS = {
1492
+ LEFT: /* @__PURE__ */ jsx12(AlignLeft, { className: "w-3.5 h-3.5" }),
1493
+ CENTER: /* @__PURE__ */ jsx12(AlignCenter, { className: "w-3.5 h-3.5" }),
1494
+ RIGHT: /* @__PURE__ */ jsx12(AlignRight, { className: "w-3.5 h-3.5" }),
1495
+ JUSTIFY: /* @__PURE__ */ jsx12(AlignJustify, { className: "w-3.5 h-3.5" }),
1496
+ DISTRIBUTE: /* @__PURE__ */ jsx12(Columns2, { className: "w-3.5 h-3.5" })
1497
+ };
1498
+ var ALIGNMENT_LABELS = {
1499
+ LEFT: "\uC67C\uCABD \uC815\uB82C (Ctrl+L)",
1500
+ CENTER: "\uAC00\uC6B4\uB370 \uC815\uB82C (Ctrl+E)",
1501
+ RIGHT: "\uC624\uB978\uCABD \uC815\uB82C (Ctrl+R)",
1502
+ JUSTIFY: "\uC591\uCABD \uC815\uB82C (Ctrl+J)",
1503
+ DISTRIBUTE: "\uBC30\uBD84 \uC815\uB82C"
1504
+ };
1505
+ function AlignmentButtons() {
1506
+ const extendedFormat = useEditorStore((s) => s.extendedFormat);
1507
+ const doc = useEditorStore((s) => s.doc);
1508
+ const selection = useEditorStore((s) => s.selection);
1509
+ const disabled = !doc || !selection;
1510
+ const currentAlignment = extendedFormat.para.alignment;
1511
+ return /* @__PURE__ */ jsx12("div", { className: "flex items-center gap-0.5", children: Object.keys(ALIGNMENT_ICONS).map((align) => /* @__PURE__ */ jsx12(
1512
+ ToolbarButton,
1513
+ {
1514
+ icon: ALIGNMENT_ICONS[align],
1515
+ active: currentAlignment === align,
1516
+ disabled,
1517
+ title: ALIGNMENT_LABELS[align]
1518
+ },
1519
+ align
1520
+ )) });
1521
+ }
1522
+
1523
+ // src/components/toolbar/LineSpacingControl.tsx
1524
+ import { jsx as jsx13 } from "react/jsx-runtime";
1525
+ function LineSpacingControl() {
1526
+ const extendedFormat = useEditorStore((s) => s.extendedFormat);
1527
+ const doc = useEditorStore((s) => s.doc);
1528
+ const selection = useEditorStore((s) => s.selection);
1529
+ const disabled = !doc || !selection;
1530
+ const currentSpacing = extendedFormat.para.lineSpacing;
1531
+ const closestValue = LINE_SPACING_OPTIONS.reduce(
1532
+ (prev, curr) => Math.abs(curr.value - currentSpacing) < Math.abs(prev.value - currentSpacing) ? curr : prev
1533
+ ).value;
1534
+ const options = LINE_SPACING_OPTIONS.map((o) => ({
1535
+ value: String(o.value),
1536
+ label: o.label
1537
+ }));
1538
+ return /* @__PURE__ */ jsx13(
1539
+ ToolbarDropdown,
1540
+ {
1541
+ value: String(closestValue),
1542
+ options,
1543
+ onChange: () => {
1544
+ },
1545
+ disabled,
1546
+ title: "\uC904 \uAC04\uACA9",
1547
+ width: "w-14"
1548
+ }
1549
+ );
1550
+ }
1551
+
1552
+ // src/components/toolbar/ToolbarDivider.tsx
1553
+ import { jsx as jsx14 } from "react/jsx-runtime";
1554
+ function ToolbarDivider() {
1555
+ return /* @__PURE__ */ jsx14("div", { className: "h-6 w-px bg-gray-200 mx-1 self-center" });
1556
+ }
1557
+
1558
+ // src/components/toolbar/SecondaryToolbar.tsx
1559
+ import { Plus } from "lucide-react";
1560
+ import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
1561
+ function SecondaryToolbar() {
1562
+ const doc = useEditorStore((s) => s.doc);
1563
+ const extendedFormat = useEditorStore((s) => s.extendedFormat);
1564
+ const addParagraph = useEditorStore((s) => s.addParagraph);
1565
+ const disabled = !doc;
1566
+ return /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-1 bg-gray-50 border-b border-gray-200 px-3 py-1 flex-wrap min-h-[36px]", children: [
1567
+ /* @__PURE__ */ jsx15(
1568
+ StyleSelector,
1569
+ {
1570
+ value: "0",
1571
+ onChange: () => {
1572
+ },
1573
+ disabled
1574
+ }
1575
+ ),
1576
+ /* @__PURE__ */ jsx15(ToolbarDivider, {}),
1577
+ /* @__PURE__ */ jsx15(
1578
+ FontSelector,
1579
+ {
1580
+ value: extendedFormat.char.fontFamily || "\uB9D1\uC740 \uACE0\uB515",
1581
+ onChange: () => {
1582
+ },
1583
+ disabled
1584
+ }
1585
+ ),
1586
+ /* @__PURE__ */ jsx15(
1587
+ FontSizeInput,
1588
+ {
1589
+ value: extendedFormat.char.fontSize || 10,
1590
+ onChange: () => {
1591
+ },
1592
+ disabled
1593
+ }
1594
+ ),
1595
+ /* @__PURE__ */ jsx15(ToolbarDivider, {}),
1596
+ /* @__PURE__ */ jsx15(CharFormatButtons, {}),
1597
+ /* @__PURE__ */ jsx15(ToolbarDivider, {}),
1598
+ /* @__PURE__ */ jsx15(AlignmentButtons, {}),
1599
+ /* @__PURE__ */ jsx15(ToolbarDivider, {}),
1600
+ /* @__PURE__ */ jsx15(LineSpacingControl, {}),
1601
+ /* @__PURE__ */ jsx15("div", { className: "flex-1" }),
1602
+ /* @__PURE__ */ jsxs10(
1603
+ "button",
1604
+ {
1605
+ disabled,
1606
+ onClick: () => addParagraph(""),
1607
+ className: "flex items-center gap-1 px-2.5 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-40 disabled:cursor-not-allowed",
1608
+ children: [
1609
+ /* @__PURE__ */ jsx15(Plus, { className: "w-3 h-3" }),
1610
+ "\uBB38\uB2E8 \uCD94\uAC00"
1611
+ ]
1612
+ }
1613
+ )
1614
+ ] });
1615
+ }
1616
+
1617
+ // src/components/ruler/HorizontalRuler.tsx
1618
+ import { jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
1619
+ function HorizontalRuler() {
1620
+ const viewModel = useEditorStore((s) => s.viewModel);
1621
+ if (!viewModel || viewModel.sections.length === 0) return null;
1622
+ const section = viewModel.sections[0];
1623
+ const pageWidthPx = section.pageWidthPx;
1624
+ const marginLeftPx = section.marginLeftPx;
1625
+ const marginRightPx = section.marginRightPx;
1626
+ const contentWidthPx = pageWidthPx - marginLeftPx - marginRightPx;
1627
+ const cmPerPx = 25.4 / 96 / 10;
1628
+ const totalCm = contentWidthPx * cmPerPx;
1629
+ const ticks = [];
1630
+ for (let cm = 0; cm <= totalCm + 0.5; cm += 0.5) {
1631
+ const px = cm / cmPerPx;
1632
+ const major = cm % 1 === 0;
1633
+ ticks.push({
1634
+ position: px,
1635
+ label: major ? String(Math.round(cm)) : "",
1636
+ major
1637
+ });
1638
+ }
1639
+ return /* @__PURE__ */ jsx16("div", { className: "bg-white border-b border-gray-200 overflow-hidden flex-shrink-0", children: /* @__PURE__ */ jsx16("div", { className: "flex justify-center", children: /* @__PURE__ */ jsxs11(
1640
+ "div",
1641
+ {
1642
+ className: "relative h-6",
1643
+ style: { width: pageWidthPx },
1644
+ children: [
1645
+ /* @__PURE__ */ jsx16(
1646
+ "div",
1647
+ {
1648
+ className: "absolute top-0 left-0 h-full bg-gray-100",
1649
+ style: { width: marginLeftPx }
1650
+ }
1651
+ ),
1652
+ /* @__PURE__ */ jsx16(
1653
+ "div",
1654
+ {
1655
+ className: "absolute top-0 right-0 h-full bg-gray-100",
1656
+ style: { width: marginRightPx }
1657
+ }
1658
+ ),
1659
+ /* @__PURE__ */ jsx16(
1660
+ "div",
1661
+ {
1662
+ className: "absolute top-0 h-full",
1663
+ style: { left: marginLeftPx, width: contentWidthPx },
1664
+ children: ticks.map((tick, i) => /* @__PURE__ */ jsxs11(
1665
+ "div",
1666
+ {
1667
+ className: "absolute bottom-0",
1668
+ style: { left: tick.position },
1669
+ children: [
1670
+ /* @__PURE__ */ jsx16(
1671
+ "div",
1672
+ {
1673
+ className: `w-px ${tick.major ? "h-3 bg-gray-500" : "h-1.5 bg-gray-300"}`
1674
+ }
1675
+ ),
1676
+ tick.label && /* @__PURE__ */ jsx16("span", { className: "absolute -top-0.5 left-1/2 -translate-x-1/2 text-[8px] text-gray-400 leading-none", children: tick.label })
1677
+ ]
1678
+ },
1679
+ i
1680
+ ))
1681
+ }
1682
+ )
1683
+ ]
1684
+ }
1685
+ ) }) });
1686
+ }
1687
+
1688
+ // src/components/sidebar/SidebarSection.tsx
1689
+ import { useState as useState5 } from "react";
1690
+ import { ChevronDown, ChevronRight } from "lucide-react";
1691
+ import { jsx as jsx17, jsxs as jsxs12 } from "react/jsx-runtime";
1692
+ function SidebarSection({
1693
+ title,
1694
+ children,
1695
+ defaultOpen = true
1696
+ }) {
1697
+ const [open, setOpen] = useState5(defaultOpen);
1698
+ return /* @__PURE__ */ jsxs12("div", { className: "border-b border-gray-100", children: [
1699
+ /* @__PURE__ */ jsxs12(
1700
+ "button",
1701
+ {
1702
+ onClick: () => setOpen(!open),
1703
+ className: "flex items-center gap-1 w-full px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-50",
1704
+ children: [
1705
+ open ? /* @__PURE__ */ jsx17(ChevronDown, { className: "w-3.5 h-3.5 text-gray-400" }) : /* @__PURE__ */ jsx17(ChevronRight, { className: "w-3.5 h-3.5 text-gray-400" }),
1706
+ title
1707
+ ]
1708
+ }
1709
+ ),
1710
+ open && /* @__PURE__ */ jsx17("div", { className: "px-3 pb-3", children })
1711
+ ] });
1712
+ }
1713
+
1714
+ // src/components/sidebar/SidebarField.tsx
1715
+ import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
1716
+ function SidebarField({ label, children }) {
1717
+ return /* @__PURE__ */ jsxs13("div", { className: "flex items-center gap-2 mb-2", children: [
1718
+ /* @__PURE__ */ jsx18("span", { className: "text-[11px] text-gray-500 w-14 flex-shrink-0", children: label }),
1719
+ /* @__PURE__ */ jsx18("div", { className: "flex-1", children })
1720
+ ] });
1721
+ }
1722
+
1723
+ // src/components/sidebar/BorderSettings.tsx
1724
+ import { jsx as jsx19, jsxs as jsxs14 } from "react/jsx-runtime";
1725
+ function BorderSettings({ disabled }) {
1726
+ return /* @__PURE__ */ jsxs14("div", { className: "space-y-1", children: [
1727
+ /* @__PURE__ */ jsx19(SidebarField, { label: "\uC885\uB958", children: /* @__PURE__ */ jsxs14(
1728
+ "select",
1729
+ {
1730
+ disabled,
1731
+ className: "w-full h-6 px-1 text-[11px] border border-gray-300 rounded bg-white disabled:opacity-40",
1732
+ children: [
1733
+ /* @__PURE__ */ jsx19("option", { value: "none", children: "\uC5C6\uC74C" }),
1734
+ /* @__PURE__ */ jsx19("option", { value: "solid", children: "\uC2E4\uC120" }),
1735
+ /* @__PURE__ */ jsx19("option", { value: "dashed", children: "\uC810\uC120" }),
1736
+ /* @__PURE__ */ jsx19("option", { value: "double", children: "\uC774\uC911\uC120" })
1737
+ ]
1738
+ }
1739
+ ) }),
1740
+ /* @__PURE__ */ jsx19(SidebarField, { label: "\uB450\uAED8", children: /* @__PURE__ */ jsxs14(
1741
+ "select",
1742
+ {
1743
+ disabled,
1744
+ className: "w-full h-6 px-1 text-[11px] border border-gray-300 rounded bg-white disabled:opacity-40",
1745
+ children: [
1746
+ /* @__PURE__ */ jsx19("option", { value: "0.1mm", children: "0.1mm" }),
1747
+ /* @__PURE__ */ jsx19("option", { value: "0.12mm", children: "0.12mm" }),
1748
+ /* @__PURE__ */ jsx19("option", { value: "0.3mm", children: "0.3mm" }),
1749
+ /* @__PURE__ */ jsx19("option", { value: "0.4mm", children: "0.4mm" }),
1750
+ /* @__PURE__ */ jsx19("option", { value: "0.5mm", children: "0.5mm" }),
1751
+ /* @__PURE__ */ jsx19("option", { value: "1.0mm", children: "1.0mm" })
1752
+ ]
1753
+ }
1754
+ ) }),
1755
+ /* @__PURE__ */ jsx19(SidebarField, { label: "\uC0C9", children: /* @__PURE__ */ jsx19(
1756
+ "input",
1757
+ {
1758
+ type: "color",
1759
+ defaultValue: "#000000",
1760
+ disabled,
1761
+ className: "w-6 h-6 border border-gray-300 rounded cursor-pointer disabled:opacity-40"
1762
+ }
1763
+ ) })
1764
+ ] });
1765
+ }
1766
+
1767
+ // src/components/sidebar/BackgroundSettings.tsx
1768
+ import { jsx as jsx20, jsxs as jsxs15 } from "react/jsx-runtime";
1769
+ function BackgroundSettings({ disabled }) {
1770
+ return /* @__PURE__ */ jsxs15("div", { className: "space-y-1", children: [
1771
+ /* @__PURE__ */ jsx20(SidebarField, { label: "\uCC44\uC6B0\uAE30", children: /* @__PURE__ */ jsxs15(
1772
+ "select",
1773
+ {
1774
+ disabled,
1775
+ className: "w-full h-6 px-1 text-[11px] border border-gray-300 rounded bg-white disabled:opacity-40",
1776
+ children: [
1777
+ /* @__PURE__ */ jsx20("option", { value: "none", children: "\uC5C6\uC74C" }),
1778
+ /* @__PURE__ */ jsx20("option", { value: "color", children: "\uC0C9 \uCC44\uC6B0\uAE30" }),
1779
+ /* @__PURE__ */ jsx20("option", { value: "gradient", children: "\uADF8\uB77C\uB370\uC774\uC158" }),
1780
+ /* @__PURE__ */ jsx20("option", { value: "pattern", children: "\uBB34\uB2AC" })
1781
+ ]
1782
+ }
1783
+ ) }),
1784
+ /* @__PURE__ */ jsx20(SidebarField, { label: "\uBC30\uACBD\uC0C9", children: /* @__PURE__ */ jsx20(
1785
+ "input",
1786
+ {
1787
+ type: "color",
1788
+ defaultValue: "#ffffff",
1789
+ disabled,
1790
+ className: "w-6 h-6 border border-gray-300 rounded cursor-pointer disabled:opacity-40"
1791
+ }
1792
+ ) })
1793
+ ] });
1794
+ }
1795
+
1796
+ // src/components/sidebar/CharFormatPanel.tsx
1797
+ import { jsx as jsx21, jsxs as jsxs16 } from "react/jsx-runtime";
1798
+ function CharFormatPanel() {
1799
+ const extendedFormat = useEditorStore((s) => s.extendedFormat);
1800
+ const activeFormat = useEditorStore((s) => s.activeFormat);
1801
+ const doc = useEditorStore((s) => s.doc);
1802
+ const selection = useEditorStore((s) => s.selection);
1803
+ const disabled = !doc || !selection;
1804
+ const cf = extendedFormat.char;
1805
+ return /* @__PURE__ */ jsxs16("div", { className: "text-xs", children: [
1806
+ /* @__PURE__ */ jsxs16(SidebarSection, { title: "\uAE30\uBCF8", children: [
1807
+ /* @__PURE__ */ jsx21(SidebarField, { label: "\uC2A4\uD0C0\uC77C", children: /* @__PURE__ */ jsxs16(
1808
+ "select",
1809
+ {
1810
+ disabled,
1811
+ className: "w-full h-6 px-1 text-[11px] border border-gray-300 rounded bg-white disabled:opacity-40",
1812
+ defaultValue: "0",
1813
+ children: [
1814
+ /* @__PURE__ */ jsx21("option", { value: "0", children: "\uBC14\uD0D5\uAE00" }),
1815
+ /* @__PURE__ */ jsx21("option", { value: "1", children: "\uBCF8\uBB38" }),
1816
+ /* @__PURE__ */ jsx21("option", { value: "2", children: "\uAC1C\uC694 1" }),
1817
+ /* @__PURE__ */ jsx21("option", { value: "3", children: "\uAC1C\uC694 2" })
1818
+ ]
1819
+ }
1820
+ ) }),
1821
+ /* @__PURE__ */ jsx21(SidebarField, { label: "\uAE00\uAF34", children: /* @__PURE__ */ jsx21(
1822
+ "select",
1823
+ {
1824
+ disabled,
1825
+ value: cf.fontFamily || "\uB9D1\uC740 \uACE0\uB515",
1826
+ onChange: () => {
1827
+ },
1828
+ className: "w-full h-6 px-1 text-[11px] border border-gray-300 rounded bg-white disabled:opacity-40",
1829
+ children: FONT_FAMILIES.map((f) => /* @__PURE__ */ jsx21("option", { value: f, children: f }, f))
1830
+ }
1831
+ ) }),
1832
+ /* @__PURE__ */ jsx21(SidebarField, { label: "\uD06C\uAE30", children: /* @__PURE__ */ jsx21(
1833
+ "select",
1834
+ {
1835
+ disabled,
1836
+ value: String(cf.fontSize || 10),
1837
+ onChange: () => {
1838
+ },
1839
+ className: "w-full h-6 px-1 text-[11px] border border-gray-300 rounded bg-white disabled:opacity-40",
1840
+ children: FONT_SIZES.map((s) => /* @__PURE__ */ jsxs16("option", { value: String(s), children: [
1841
+ s,
1842
+ "pt"
1843
+ ] }, s))
1844
+ }
1845
+ ) }),
1846
+ /* @__PURE__ */ jsx21(SidebarField, { label: "\uC0C9\uC0C1", children: /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2", children: [
1847
+ /* @__PURE__ */ jsx21(
1848
+ "input",
1849
+ {
1850
+ type: "color",
1851
+ value: cf.textColor || "#000000",
1852
+ onChange: () => {
1853
+ },
1854
+ disabled,
1855
+ className: "w-6 h-6 border border-gray-300 rounded cursor-pointer disabled:opacity-40"
1856
+ }
1857
+ ),
1858
+ /* @__PURE__ */ jsx21("span", { className: "text-[10px] text-gray-400", children: cf.textColor || "#000000" })
1859
+ ] }) })
1860
+ ] }),
1861
+ /* @__PURE__ */ jsx21(SidebarSection, { title: "\uAFB8\uBC08", children: /* @__PURE__ */ jsxs16("div", { className: "flex flex-wrap gap-1 mb-2", children: [
1862
+ /* @__PURE__ */ jsx21(FormatTag, { label: "\uAD75\uAC8C", active: activeFormat.bold }),
1863
+ /* @__PURE__ */ jsx21(FormatTag, { label: "\uAE30\uC6B8\uC784", active: activeFormat.italic }),
1864
+ /* @__PURE__ */ jsx21(FormatTag, { label: "\uBC11\uC904", active: activeFormat.underline }),
1865
+ /* @__PURE__ */ jsx21(FormatTag, { label: "\uCDE8\uC18C\uC120", active: cf.strikethrough })
1866
+ ] }) }),
1867
+ /* @__PURE__ */ jsx21(SidebarSection, { title: "\uD615\uAD11\uD39C", children: /* @__PURE__ */ jsx21(SidebarField, { label: "\uC0C9\uC0C1", children: /* @__PURE__ */ jsxs16("div", { className: "flex items-center gap-2", children: [
1868
+ /* @__PURE__ */ jsx21(
1869
+ "input",
1870
+ {
1871
+ type: "color",
1872
+ value: cf.highlightColor && cf.highlightColor !== "none" ? cf.highlightColor : "#FFFF00",
1873
+ onChange: () => {
1874
+ },
1875
+ disabled,
1876
+ className: "w-6 h-6 border border-gray-300 rounded cursor-pointer disabled:opacity-40"
1877
+ }
1878
+ ),
1879
+ /* @__PURE__ */ jsx21("span", { className: "text-[10px] text-gray-400", children: cf.highlightColor || "\uC5C6\uC74C" })
1880
+ ] }) }) }),
1881
+ /* @__PURE__ */ jsx21(SidebarSection, { title: "\uD14C\uB450\uB9AC", defaultOpen: false, children: /* @__PURE__ */ jsx21(BorderSettings, { disabled }) }),
1882
+ /* @__PURE__ */ jsx21(SidebarSection, { title: "\uBC30\uACBD", defaultOpen: false, children: /* @__PURE__ */ jsx21(BackgroundSettings, { disabled }) })
1883
+ ] });
1884
+ }
1885
+ function FormatTag({ label, active }) {
1886
+ return /* @__PURE__ */ jsx21(
1887
+ "span",
1888
+ {
1889
+ className: `px-2 py-0.5 rounded text-[10px] border ${active ? "bg-blue-50 border-blue-300 text-blue-700" : "bg-gray-50 border-gray-200 text-gray-400"}`,
1890
+ children: label
1891
+ }
1892
+ );
1893
+ }
1894
+
1895
+ // src/components/sidebar/ParaFormatPanel.tsx
1896
+ import { jsx as jsx22, jsxs as jsxs17 } from "react/jsx-runtime";
1897
+ function ParaFormatPanel() {
1898
+ const extendedFormat = useEditorStore((s) => s.extendedFormat);
1899
+ const doc = useEditorStore((s) => s.doc);
1900
+ const selection = useEditorStore((s) => s.selection);
1901
+ const disabled = !doc || !selection;
1902
+ const pf = extendedFormat.para;
1903
+ return /* @__PURE__ */ jsxs17("div", { className: "text-xs", children: [
1904
+ /* @__PURE__ */ jsx22(SidebarSection, { title: "\uC815\uB82C", children: /* @__PURE__ */ jsx22("div", { className: "flex gap-1 mb-2", children: ALIGNMENT_OPTIONS.map((opt) => /* @__PURE__ */ jsxs17(
1905
+ "button",
1906
+ {
1907
+ disabled,
1908
+ className: `flex-1 py-1 rounded text-[10px] border transition-colors ${pf.alignment === opt.value ? "bg-blue-50 border-blue-300 text-blue-700" : "bg-white border-gray-200 text-gray-500 hover:bg-gray-50"} disabled:opacity-40 disabled:cursor-not-allowed`,
1909
+ title: opt.label,
1910
+ children: [
1911
+ opt.value === "LEFT" && "\uC67C",
1912
+ opt.value === "CENTER" && "\uC911",
1913
+ opt.value === "RIGHT" && "\uC624",
1914
+ opt.value === "JUSTIFY" && "\uC591",
1915
+ opt.value === "DISTRIBUTE" && "\uBC30"
1916
+ ]
1917
+ },
1918
+ opt.value
1919
+ )) }) }),
1920
+ /* @__PURE__ */ jsxs17(SidebarSection, { title: "\uC5EC\uBC31", children: [
1921
+ /* @__PURE__ */ jsx22(SidebarField, { label: "\uC67C\uCABD", children: /* @__PURE__ */ jsx22(
1922
+ "input",
1923
+ {
1924
+ type: "number",
1925
+ value: pf.indentLeft,
1926
+ disabled,
1927
+ onChange: () => {
1928
+ },
1929
+ className: "w-full h-6 px-1 text-[11px] border border-gray-300 rounded bg-white disabled:opacity-40"
1930
+ }
1931
+ ) }),
1932
+ /* @__PURE__ */ jsx22(SidebarField, { label: "\uC624\uB978\uCABD", children: /* @__PURE__ */ jsx22(
1933
+ "input",
1934
+ {
1935
+ type: "number",
1936
+ value: pf.indentRight,
1937
+ disabled,
1938
+ onChange: () => {
1939
+ },
1940
+ className: "w-full h-6 px-1 text-[11px] border border-gray-300 rounded bg-white disabled:opacity-40"
1941
+ }
1942
+ ) }),
1943
+ /* @__PURE__ */ jsx22(SidebarField, { label: "\uCCAB\uC904", children: /* @__PURE__ */ jsx22(
1944
+ "input",
1945
+ {
1946
+ type: "number",
1947
+ value: pf.firstLineIndent,
1948
+ disabled,
1949
+ onChange: () => {
1950
+ },
1951
+ className: "w-full h-6 px-1 text-[11px] border border-gray-300 rounded bg-white disabled:opacity-40"
1952
+ }
1953
+ ) })
1954
+ ] }),
1955
+ /* @__PURE__ */ jsxs17(SidebarSection, { title: "\uAC04\uACA9", children: [
1956
+ /* @__PURE__ */ jsx22(SidebarField, { label: "\uC904 \uAC04\uACA9", children: /* @__PURE__ */ jsx22(
1957
+ "select",
1958
+ {
1959
+ disabled,
1960
+ value: String(
1961
+ LINE_SPACING_OPTIONS.reduce(
1962
+ (prev, curr) => Math.abs(curr.value - pf.lineSpacing) < Math.abs(prev.value - pf.lineSpacing) ? curr : prev
1963
+ ).value
1964
+ ),
1965
+ onChange: () => {
1966
+ },
1967
+ className: "w-full h-6 px-1 text-[11px] border border-gray-300 rounded bg-white disabled:opacity-40",
1968
+ children: LINE_SPACING_OPTIONS.map((o) => /* @__PURE__ */ jsx22("option", { value: String(o.value), children: o.label }, o.value))
1969
+ }
1970
+ ) }),
1971
+ /* @__PURE__ */ jsx22(SidebarField, { label: "\uBB38\uB2E8 \uC55E", children: /* @__PURE__ */ jsx22(
1972
+ "input",
1973
+ {
1974
+ type: "number",
1975
+ value: pf.spacingBefore,
1976
+ disabled,
1977
+ onChange: () => {
1978
+ },
1979
+ className: "w-full h-6 px-1 text-[11px] border border-gray-300 rounded bg-white disabled:opacity-40"
1980
+ }
1981
+ ) }),
1982
+ /* @__PURE__ */ jsx22(SidebarField, { label: "\uBB38\uB2E8 \uB4A4", children: /* @__PURE__ */ jsx22(
1983
+ "input",
1984
+ {
1985
+ type: "number",
1986
+ value: pf.spacingAfter,
1987
+ disabled,
1988
+ onChange: () => {
1989
+ },
1990
+ className: "w-full h-6 px-1 text-[11px] border border-gray-300 rounded bg-white disabled:opacity-40"
1991
+ }
1992
+ ) })
1993
+ ] }),
1994
+ /* @__PURE__ */ jsx22(SidebarSection, { title: "\uC904 \uB098\uB214", defaultOpen: false, children: /* @__PURE__ */ jsxs17("div", { className: "space-y-1", children: [
1995
+ /* @__PURE__ */ jsxs17("label", { className: "flex items-center gap-2 text-[11px] text-gray-600", children: [
1996
+ /* @__PURE__ */ jsx22("input", { type: "checkbox", disabled, className: "w-3 h-3" }),
1997
+ "\uD55C\uAE00 \uB2E8\uC5B4 \uC798\uB9BC \uD5C8\uC6A9"
1998
+ ] }),
1999
+ /* @__PURE__ */ jsxs17("label", { className: "flex items-center gap-2 text-[11px] text-gray-600", children: [
2000
+ /* @__PURE__ */ jsx22("input", { type: "checkbox", disabled, className: "w-3 h-3" }),
2001
+ "\uC601\uC5B4 \uB2E8\uC5B4 \uC798\uB9BC \uD5C8\uC6A9"
2002
+ ] }),
2003
+ /* @__PURE__ */ jsxs17("label", { className: "flex items-center gap-2 text-[11px] text-gray-600", children: [
2004
+ /* @__PURE__ */ jsx22("input", { type: "checkbox", disabled, className: "w-3 h-3" }),
2005
+ "\uC678\uD1A8\uC774\uC904 \uBC29\uC9C0"
2006
+ ] })
2007
+ ] }) }),
2008
+ /* @__PURE__ */ jsx22(SidebarSection, { title: "\uD14C\uB450\uB9AC", defaultOpen: false, children: /* @__PURE__ */ jsx22(BorderSettings, { disabled }) }),
2009
+ /* @__PURE__ */ jsx22(SidebarSection, { title: "\uBC30\uACBD", defaultOpen: false, children: /* @__PURE__ */ jsx22(BackgroundSettings, { disabled }) })
2010
+ ] });
2011
+ }
2012
+
2013
+ // src/components/sidebar/FormatSidebar.tsx
2014
+ import { PanelRightClose } from "lucide-react";
2015
+ import { jsx as jsx23, jsxs as jsxs18 } from "react/jsx-runtime";
2016
+ function FormatSidebar() {
2017
+ const uiState = useEditorStore((s) => s.uiState);
2018
+ const setSidebarTab = useEditorStore((s) => s.setSidebarTab);
2019
+ const toggleSidebar = useEditorStore((s) => s.toggleSidebar);
2020
+ if (!uiState.sidebarOpen) return null;
2021
+ return /* @__PURE__ */ jsxs18("div", { className: "w-72 border-l border-gray-200 bg-white flex flex-col overflow-hidden flex-shrink-0", children: [
2022
+ /* @__PURE__ */ jsxs18("div", { className: "flex items-center border-b border-gray-200", children: [
2023
+ /* @__PURE__ */ jsx23(
2024
+ "button",
2025
+ {
2026
+ onClick: () => setSidebarTab("char"),
2027
+ className: `flex-1 py-2 text-xs font-medium text-center transition-colors ${uiState.sidebarTab === "char" ? "text-blue-600 border-b-2 border-blue-600 bg-blue-50/50" : "text-gray-500 hover:text-gray-700 hover:bg-gray-50"}`,
2028
+ children: "\uAE00\uC790 \uBAA8\uC591"
2029
+ }
2030
+ ),
2031
+ /* @__PURE__ */ jsx23(
2032
+ "button",
2033
+ {
2034
+ onClick: () => setSidebarTab("para"),
2035
+ className: `flex-1 py-2 text-xs font-medium text-center transition-colors ${uiState.sidebarTab === "para" ? "text-blue-600 border-b-2 border-blue-600 bg-blue-50/50" : "text-gray-500 hover:text-gray-700 hover:bg-gray-50"}`,
2036
+ children: "\uBB38\uB2E8 \uBAA8\uC591"
2037
+ }
2038
+ ),
2039
+ /* @__PURE__ */ jsx23(
2040
+ "button",
2041
+ {
2042
+ onClick: toggleSidebar,
2043
+ className: "p-1.5 mr-1 rounded text-gray-400 hover:text-gray-600 hover:bg-gray-100",
2044
+ title: "\uC0AC\uC774\uB4DC\uBC14 \uB2EB\uAE30",
2045
+ children: /* @__PURE__ */ jsx23(PanelRightClose, { className: "w-4 h-4" })
2046
+ }
2047
+ )
2048
+ ] }),
2049
+ /* @__PURE__ */ jsx23("div", { className: "flex-1 overflow-auto", children: uiState.sidebarTab === "char" ? /* @__PURE__ */ jsx23(CharFormatPanel, {}) : /* @__PURE__ */ jsx23(ParaFormatPanel, {}) })
2050
+ ] });
2051
+ }
2052
+
2053
+ // src/components/editor/ParagraphBlock.tsx
2054
+ import { useRef as useRef5, useCallback as useCallback4 } from "react";
2055
+
2056
+ // src/components/editor/RunSpan.tsx
2057
+ import { jsx as jsx24 } from "react/jsx-runtime";
2058
+ function RunSpan({ run }) {
2059
+ const style = {};
2060
+ if (run.bold) style.fontWeight = "bold";
2061
+ if (run.italic) style.fontStyle = "italic";
2062
+ const decorations = [];
2063
+ if (run.underline) decorations.push("underline");
2064
+ if (run.strikethrough) decorations.push("line-through");
2065
+ if (decorations.length > 0) style.textDecoration = decorations.join(" ");
2066
+ if (run.color && run.color !== "#000000") style.color = run.color;
2067
+ if (run.fontFamily) style.fontFamily = run.fontFamily;
2068
+ if (run.fontSize) style.fontSize = `${run.fontSize}pt`;
2069
+ if (run.highlightColor) style.backgroundColor = run.highlightColor;
2070
+ if (run.letterSpacing) {
2071
+ style.letterSpacing = `${run.letterSpacing / 100}px`;
2072
+ }
2073
+ return /* @__PURE__ */ jsx24("span", { style, children: run.text });
2074
+ }
2075
+
2076
+ // src/components/editor/TableCell.tsx
2077
+ import { useRef as useRef4, useCallback as useCallback3 } from "react";
2078
+ import { jsx as jsx25 } from "react/jsx-runtime";
2079
+ function TableCell({
2080
+ cell,
2081
+ sectionIndex,
2082
+ paragraphIndex,
2083
+ tableIndex
2084
+ }) {
2085
+ const updateCellText = useEditorStore((s) => s.updateCellText);
2086
+ const setSelection = useEditorStore((s) => s.setSelection);
2087
+ const ref = useRef4(null);
2088
+ const handleBlur = useCallback3(() => {
2089
+ var _a;
2090
+ if (!ref.current) return;
2091
+ const newText = (_a = ref.current.textContent) != null ? _a : "";
2092
+ if (newText !== cell.text) {
2093
+ updateCellText(
2094
+ sectionIndex,
2095
+ paragraphIndex,
2096
+ tableIndex,
2097
+ cell.row,
2098
+ cell.col,
2099
+ newText
2100
+ );
2101
+ }
2102
+ }, [cell, sectionIndex, paragraphIndex, tableIndex, updateCellText]);
2103
+ const handleFocus = useCallback3(() => {
2104
+ setSelection({
2105
+ sectionIndex,
2106
+ paragraphIndex,
2107
+ type: "cell",
2108
+ tableIndex,
2109
+ row: cell.row,
2110
+ col: cell.col
2111
+ });
2112
+ }, [sectionIndex, paragraphIndex, tableIndex, cell.row, cell.col, setSelection]);
2113
+ if (!cell.isAnchor) return null;
2114
+ return /* @__PURE__ */ jsx25(
2115
+ "td",
2116
+ {
2117
+ ref,
2118
+ contentEditable: true,
2119
+ suppressContentEditableWarning: true,
2120
+ colSpan: cell.colSpan > 1 ? cell.colSpan : void 0,
2121
+ rowSpan: cell.rowSpan > 1 ? cell.rowSpan : void 0,
2122
+ onBlur: handleBlur,
2123
+ onFocus: handleFocus,
2124
+ className: "border border-gray-300 px-2 py-1 text-sm outline-none focus:bg-blue-50",
2125
+ style: {
2126
+ minWidth: cell.widthPx > 0 ? cell.widthPx : 40,
2127
+ minHeight: cell.heightPx > 0 ? cell.heightPx : 24
2128
+ },
2129
+ children: cell.text
2130
+ }
2131
+ );
2132
+ }
2133
+
2134
+ // src/components/editor/TableBlock.tsx
2135
+ import { jsx as jsx26 } from "react/jsx-runtime";
2136
+ function TableBlock({ table, sectionIndex, paragraphIndex }) {
2137
+ return /* @__PURE__ */ jsx26("div", { className: "my-2 overflow-x-auto", children: /* @__PURE__ */ jsx26("table", { className: "border-collapse border border-gray-400", children: /* @__PURE__ */ jsx26("tbody", { children: table.cells.map((row, rIdx) => /* @__PURE__ */ jsx26("tr", { children: row.map(
2138
+ (cell) => cell.isAnchor ? /* @__PURE__ */ jsx26(
2139
+ TableCell,
2140
+ {
2141
+ cell,
2142
+ sectionIndex,
2143
+ paragraphIndex,
2144
+ tableIndex: table.tableIndex
2145
+ },
2146
+ `${cell.row}-${cell.col}`
2147
+ ) : null
2148
+ ) }, rIdx)) }) }) });
2149
+ }
2150
+
2151
+ // src/components/editor/ImageBlock.tsx
2152
+ import { jsx as jsx27 } from "react/jsx-runtime";
2153
+ function ImageBlock({ image }) {
2154
+ return /* @__PURE__ */ jsx27("div", { className: "my-2 flex justify-center", children: /* @__PURE__ */ jsx27(
2155
+ "img",
2156
+ {
2157
+ src: image.dataUrl,
2158
+ alt: "",
2159
+ style: {
2160
+ width: image.widthPx > 0 ? image.widthPx : void 0,
2161
+ height: image.heightPx > 0 ? image.heightPx : void 0,
2162
+ maxWidth: "100%"
2163
+ }
2164
+ }
2165
+ ) });
2166
+ }
2167
+
2168
+ // src/components/editor/ParagraphBlock.tsx
2169
+ import { jsx as jsx28, jsxs as jsxs19 } from "react/jsx-runtime";
2170
+ function alignmentToCSS(alignment) {
2171
+ switch (alignment.toUpperCase()) {
2172
+ case "CENTER":
2173
+ return "center";
2174
+ case "RIGHT":
2175
+ return "right";
2176
+ case "JUSTIFY":
2177
+ case "DISTRIBUTE":
2178
+ return "justify";
2179
+ default:
2180
+ return "left";
2181
+ }
2182
+ }
2183
+ function ParagraphBlock({
2184
+ paragraph,
2185
+ sectionIndex,
2186
+ localIndex
2187
+ }) {
2188
+ const updateParagraphText = useEditorStore((s) => s.updateParagraphText);
2189
+ const setSelection = useEditorStore((s) => s.setSelection);
2190
+ const ref = useRef5(null);
2191
+ const hasTables = paragraph.tables.length > 0;
2192
+ const hasImages = paragraph.images.length > 0;
2193
+ const hasText = paragraph.runs.length > 0;
2194
+ const handleBlur = useCallback4(() => {
2195
+ var _a;
2196
+ if (!ref.current) return;
2197
+ const newText = (_a = ref.current.textContent) != null ? _a : "";
2198
+ if (!hasTables && !hasImages) {
2199
+ const oldText = paragraph.runs.map((r) => r.text).join("");
2200
+ if (newText !== oldText) {
2201
+ updateParagraphText(sectionIndex, localIndex, newText);
2202
+ }
2203
+ }
2204
+ }, [paragraph, sectionIndex, localIndex, hasTables, hasImages, updateParagraphText]);
2205
+ const handleFocus = useCallback4(() => {
2206
+ setSelection({
2207
+ sectionIndex,
2208
+ paragraphIndex: localIndex,
2209
+ type: "paragraph"
2210
+ });
2211
+ }, [sectionIndex, localIndex, setSelection]);
2212
+ const paraStyle = {
2213
+ textAlign: alignmentToCSS(paragraph.alignment),
2214
+ lineHeight: paragraph.lineSpacing,
2215
+ marginLeft: paragraph.marginLeftPx || void 0,
2216
+ marginRight: paragraph.marginRightPx || void 0,
2217
+ textIndent: paragraph.firstLineIndent || void 0,
2218
+ paddingTop: paragraph.spacingBefore || void 0,
2219
+ paddingBottom: paragraph.spacingAfter || void 0
2220
+ };
2221
+ if (hasTables) {
2222
+ return /* @__PURE__ */ jsxs19("div", { className: "my-1", style: paraStyle, children: [
2223
+ hasText && /* @__PURE__ */ jsx28(
2224
+ "div",
2225
+ {
2226
+ ref,
2227
+ contentEditable: true,
2228
+ suppressContentEditableWarning: true,
2229
+ onBlur: handleBlur,
2230
+ onFocus: handleFocus,
2231
+ className: "outline-none leading-relaxed",
2232
+ children: paragraph.runs.map((run, i) => /* @__PURE__ */ jsx28(RunSpan, { run }, i))
2233
+ }
2234
+ ),
2235
+ paragraph.tables.map((table) => /* @__PURE__ */ jsx28(
2236
+ TableBlock,
2237
+ {
2238
+ table,
2239
+ sectionIndex,
2240
+ paragraphIndex: localIndex
2241
+ },
2242
+ table.tableIndex
2243
+ ))
2244
+ ] });
2245
+ }
2246
+ if (hasImages) {
2247
+ return /* @__PURE__ */ jsx28("div", { className: "my-1", style: paraStyle, children: paragraph.images.map((img, i) => /* @__PURE__ */ jsx28(ImageBlock, { image: img }, i)) });
2248
+ }
2249
+ return /* @__PURE__ */ jsx28(
2250
+ "div",
2251
+ {
2252
+ ref,
2253
+ contentEditable: true,
2254
+ suppressContentEditableWarning: true,
2255
+ onBlur: handleBlur,
2256
+ onFocus: handleFocus,
2257
+ className: "outline-none min-h-[1.5em]",
2258
+ style: paraStyle,
2259
+ children: paragraph.runs.length > 0 ? paragraph.runs.map((run, i) => /* @__PURE__ */ jsx28(RunSpan, { run }, i)) : /* @__PURE__ */ jsx28("br", {})
2260
+ }
2261
+ );
2262
+ }
2263
+
2264
+ // src/components/editor/Page.tsx
2265
+ import { jsx as jsx29 } from "react/jsx-runtime";
2266
+ function Page({ section }) {
2267
+ const addParagraph = useEditorStore((s) => s.addParagraph);
2268
+ const revision = useEditorStore((s) => s.revision);
2269
+ const handlePageClick = (e) => {
2270
+ if (e.target !== e.currentTarget) return;
2271
+ if (section.paragraphs.length === 0) {
2272
+ addParagraph("");
2273
+ return;
2274
+ }
2275
+ const container = e.currentTarget;
2276
+ const editables = container.querySelectorAll("[contenteditable]");
2277
+ const last = editables[editables.length - 1];
2278
+ if (last) {
2279
+ last.focus();
2280
+ const sel = window.getSelection();
2281
+ if (sel) {
2282
+ sel.selectAllChildren(last);
2283
+ sel.collapseToEnd();
2284
+ }
2285
+ }
2286
+ };
2287
+ return /* @__PURE__ */ jsx29(
2288
+ "div",
2289
+ {
2290
+ className: "bg-white shadow-lg mx-auto mb-8 cursor-text",
2291
+ onClick: handlePageClick,
2292
+ style: {
2293
+ width: section.pageWidthPx,
2294
+ minHeight: section.pageHeightPx,
2295
+ paddingTop: section.marginTopPx,
2296
+ paddingBottom: section.marginBottomPx,
2297
+ paddingLeft: section.marginLeftPx,
2298
+ paddingRight: section.marginRightPx
2299
+ },
2300
+ children: section.paragraphs.map((para, idx) => /* @__PURE__ */ jsx29(
2301
+ ParagraphBlock,
2302
+ {
2303
+ paragraph: para,
2304
+ sectionIndex: section.sectionIndex,
2305
+ localIndex: idx
2306
+ },
2307
+ `${section.sectionIndex}-${idx}-r${revision}`
2308
+ ))
2309
+ }
2310
+ );
2311
+ }
2312
+
2313
+ // src/components/editor/PageView.tsx
2314
+ import { jsx as jsx30 } from "react/jsx-runtime";
2315
+ function PageView() {
2316
+ const viewModel = useEditorStore((s) => s.viewModel);
2317
+ useEditorStore((s) => s.revision);
2318
+ if (!viewModel) {
2319
+ return /* @__PURE__ */ jsx30("div", { className: "flex-1 flex items-center justify-center text-gray-400", children: "\uBB38\uC11C\uB97C \uC5F4\uAC70\uB098 \uC0C8 \uBB38\uC11C\uB97C \uB9CC\uB4DC\uC138\uC694." });
2320
+ }
2321
+ return /* @__PURE__ */ jsx30("div", { className: "flex-1 overflow-auto bg-gray-200 py-8", children: viewModel.sections.map((section) => /* @__PURE__ */ jsx30(Page, { section }, section.sectionIndex)) });
2322
+ }
2323
+
2324
+ // src/components/upload/FileUpload.tsx
2325
+ import { useCallback as useCallback5, useState as useState6, useRef as useRef6 } from "react";
2326
+ import { Upload } from "lucide-react";
2327
+ import { HwpxDocument as HwpxDocument2 } from "@ubermensch1218/hwpxcore";
2328
+ import { jsx as jsx31, jsxs as jsxs20 } from "react/jsx-runtime";
2329
+ function FileUpload() {
2330
+ const setDocument = useEditorStore((s) => s.setDocument);
2331
+ const setLoading = useEditorStore((s) => s.setLoading);
2332
+ const setError = useEditorStore((s) => s.setError);
2333
+ const [dragging, setDragging] = useState6(false);
2334
+ const inputRef = useRef6(null);
2335
+ const openFile = useCallback5(
2336
+ async (file) => {
2337
+ setLoading(true);
2338
+ setError(null);
2339
+ try {
2340
+ const buffer = await file.arrayBuffer();
2341
+ const doc = await HwpxDocument2.open(buffer);
2342
+ setDocument(doc);
2343
+ } catch (e) {
2344
+ console.error("Failed to open file:", e);
2345
+ setError("\uD30C\uC77C\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. HWPX \uD30C\uC77C\uC778\uC9C0 \uD655\uC778\uD558\uC138\uC694.");
2346
+ } finally {
2347
+ setLoading(false);
2348
+ }
2349
+ },
2350
+ [setDocument, setLoading, setError]
2351
+ );
2352
+ const handleDrop = useCallback5(
2353
+ (e) => {
2354
+ e.preventDefault();
2355
+ setDragging(false);
2356
+ const file = e.dataTransfer.files[0];
2357
+ if (file) openFile(file);
2358
+ },
2359
+ [openFile]
2360
+ );
2361
+ const handleDragOver = useCallback5((e) => {
2362
+ e.preventDefault();
2363
+ setDragging(true);
2364
+ }, []);
2365
+ const handleDragLeave = useCallback5(() => {
2366
+ setDragging(false);
2367
+ }, []);
2368
+ const handleChange = useCallback5(
2369
+ (e) => {
2370
+ var _a;
2371
+ const file = (_a = e.target.files) == null ? void 0 : _a[0];
2372
+ if (file) openFile(file);
2373
+ e.target.value = "";
2374
+ },
2375
+ [openFile]
2376
+ );
2377
+ return /* @__PURE__ */ jsxs20(
2378
+ "div",
2379
+ {
2380
+ onDrop: handleDrop,
2381
+ onDragOver: handleDragOver,
2382
+ onDragLeave: handleDragLeave,
2383
+ onClick: () => {
2384
+ var _a;
2385
+ return (_a = inputRef.current) == null ? void 0 : _a.click();
2386
+ },
2387
+ className: `cursor-pointer border-2 border-dashed rounded-lg p-8 text-center transition-colors ${dragging ? "border-blue-400 bg-blue-50" : "border-gray-300 hover:border-gray-400"}`,
2388
+ children: [
2389
+ /* @__PURE__ */ jsx31(Upload, { className: "w-10 h-10 mx-auto mb-3 text-gray-400" }),
2390
+ /* @__PURE__ */ jsx31("p", { className: "text-gray-600 font-medium", children: "HWPX \uD30C\uC77C\uC744 \uB4DC\uB798\uADF8\uD558\uAC70\uB098 \uD074\uB9AD\uD558\uC5EC \uC120\uD0DD" }),
2391
+ /* @__PURE__ */ jsx31("p", { className: "text-sm text-gray-400 mt-1", children: ".hwpx \uD30C\uC77C\uB9CC \uC9C0\uC6D0" }),
2392
+ /* @__PURE__ */ jsx31(
2393
+ "input",
2394
+ {
2395
+ ref: inputRef,
2396
+ type: "file",
2397
+ accept: ".hwpx",
2398
+ className: "hidden",
2399
+ onChange: handleChange
2400
+ }
2401
+ )
2402
+ ]
2403
+ }
2404
+ );
2405
+ }
2406
+
2407
+ // src/components/upload/NewDocumentButton.tsx
2408
+ import { useCallback as useCallback6 } from "react";
2409
+ import { FilePlus } from "lucide-react";
2410
+ import { jsx as jsx32, jsxs as jsxs21 } from "react/jsx-runtime";
2411
+ function NewDocumentButton() {
2412
+ const setDocument = useEditorStore((s) => s.setDocument);
2413
+ const setLoading = useEditorStore((s) => s.setLoading);
2414
+ const setError = useEditorStore((s) => s.setError);
2415
+ const handleClick = useCallback6(async () => {
2416
+ setLoading(true);
2417
+ setError(null);
2418
+ try {
2419
+ const doc = await createNewDocument();
2420
+ setDocument(doc);
2421
+ } catch (e) {
2422
+ console.error("Failed to create new document:", e);
2423
+ setError("\uC0C8 \uBB38\uC11C\uB97C \uB9CC\uB4E4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
2424
+ } finally {
2425
+ setLoading(false);
2426
+ }
2427
+ }, [setDocument, setLoading, setError]);
2428
+ return /* @__PURE__ */ jsxs21(
2429
+ "button",
2430
+ {
2431
+ onClick: handleClick,
2432
+ className: "flex items-center gap-2 px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors",
2433
+ children: [
2434
+ /* @__PURE__ */ jsx32(FilePlus, { className: "w-5 h-5" }),
2435
+ "\uC0C8 \uBB38\uC11C"
2436
+ ]
2437
+ }
2438
+ );
2439
+ }
2440
+
2441
+ // src/components/editor/Editor.tsx
2442
+ import { PanelRight } from "lucide-react";
2443
+ import { jsx as jsx33, jsxs as jsxs22 } from "react/jsx-runtime";
2444
+ function Editor() {
2445
+ const doc = useEditorStore((s) => s.doc);
2446
+ const loading = useEditorStore((s) => s.loading);
2447
+ const error = useEditorStore((s) => s.error);
2448
+ const uiState = useEditorStore((s) => s.uiState);
2449
+ const toggleSidebar = useEditorStore((s) => s.toggleSidebar);
2450
+ useEffect3(() => {
2451
+ ensureSkeletonLoaded().catch(console.error);
2452
+ }, []);
2453
+ useEffect3(() => {
2454
+ const handler = (e) => {
2455
+ const store = useEditorStore.getState();
2456
+ if (!store.doc || !store.selection) return;
2457
+ if ((e.ctrlKey || e.metaKey) && e.key === "b") {
2458
+ e.preventDefault();
2459
+ store.toggleBold();
2460
+ } else if ((e.ctrlKey || e.metaKey) && e.key === "i") {
2461
+ e.preventDefault();
2462
+ store.toggleItalic();
2463
+ } else if ((e.ctrlKey || e.metaKey) && e.key === "u") {
2464
+ e.preventDefault();
2465
+ store.toggleUnderline();
2466
+ } else if ((e.ctrlKey || e.metaKey) && e.key === "s") {
2467
+ e.preventDefault();
2468
+ store.saveDocument();
2469
+ }
2470
+ };
2471
+ window.addEventListener("keydown", handler);
2472
+ return () => window.removeEventListener("keydown", handler);
2473
+ }, []);
2474
+ if (loading) {
2475
+ return /* @__PURE__ */ jsx33("div", { className: "flex h-screen items-center justify-center", children: /* @__PURE__ */ jsx33("div", { className: "text-gray-500", children: "\uB85C\uB529 \uC911..." }) });
2476
+ }
2477
+ if (!doc) {
2478
+ return /* @__PURE__ */ jsxs22("div", { className: "flex h-screen flex-col items-center justify-center gap-6 bg-gray-100 p-8", children: [
2479
+ /* @__PURE__ */ jsx33("h1", { className: "text-2xl font-bold text-gray-800", children: "HWPX Editor" }),
2480
+ /* @__PURE__ */ jsx33("p", { className: "text-gray-500 max-w-md text-center", children: "HWPX \uBB38\uC11C\uB97C \uC5F4\uAC70\uB098 \uC0C8 \uBB38\uC11C\uB97C \uB9CC\uB4E4\uC5B4 \uD3B8\uC9D1\uD558\uC138\uC694." }),
2481
+ /* @__PURE__ */ jsx33("div", { className: "w-full max-w-md", children: /* @__PURE__ */ jsx33(FileUpload, {}) }),
2482
+ /* @__PURE__ */ jsx33("div", { className: "flex gap-3", children: /* @__PURE__ */ jsx33(NewDocumentButton, {}) }),
2483
+ error && /* @__PURE__ */ jsx33("div", { className: "bg-red-50 text-red-600 px-4 py-2 rounded-lg text-sm", children: error })
2484
+ ] });
2485
+ }
2486
+ return /* @__PURE__ */ jsxs22("div", { className: "flex h-screen flex-col", children: [
2487
+ /* @__PURE__ */ jsx33(RibbonToolbar, {}),
2488
+ /* @__PURE__ */ jsx33(SecondaryToolbar, {}),
2489
+ /* @__PURE__ */ jsx33(HorizontalRuler, {}),
2490
+ error && /* @__PURE__ */ jsx33("div", { className: "bg-red-50 text-red-600 px-4 py-2 text-sm border-b border-red-200", children: error }),
2491
+ /* @__PURE__ */ jsxs22("div", { className: "flex flex-1 overflow-hidden", children: [
2492
+ /* @__PURE__ */ jsx33(PageView, {}),
2493
+ /* @__PURE__ */ jsx33(FormatSidebar, {}),
2494
+ !uiState.sidebarOpen && /* @__PURE__ */ jsx33(
2495
+ "button",
2496
+ {
2497
+ onClick: toggleSidebar,
2498
+ className: "absolute right-2 top-[140px] z-10 p-1.5 bg-white border border-gray-200 rounded shadow-sm text-gray-400 hover:text-gray-600 hover:bg-gray-50",
2499
+ title: "\uC11C\uC2DD \uC0AC\uC774\uB4DC\uBC14 \uC5F4\uAE30",
2500
+ children: /* @__PURE__ */ jsx33(PanelRight, { className: "w-4 h-4" })
2501
+ }
2502
+ )
2503
+ ] })
2504
+ ] });
2505
+ }
2506
+ export {
2507
+ ALIGNMENT_OPTIONS,
2508
+ AlignmentButtons,
2509
+ BackgroundSettings,
2510
+ BorderSettings,
2511
+ COLOR_PRESETS,
2512
+ CharFormatButtons,
2513
+ CharFormatPanel,
2514
+ ClipboardGroup,
2515
+ ColorPicker,
2516
+ Editor,
2517
+ FONT_FAMILIES,
2518
+ FONT_SIZES,
2519
+ FileUpload,
2520
+ FontSelector,
2521
+ FontSizeInput,
2522
+ FormatSidebar,
2523
+ HIGHLIGHT_COLORS,
2524
+ HorizontalRuler,
2525
+ ImageBlock,
2526
+ InsertGroup,
2527
+ LINE_SPACING_OPTIONS,
2528
+ LineSpacingControl,
2529
+ NewDocumentButton,
2530
+ Page,
2531
+ PageView,
2532
+ ParaFormatPanel,
2533
+ ParagraphBlock,
2534
+ RibbonGroup,
2535
+ RibbonToolbar,
2536
+ RunSpan,
2537
+ STYLE_PRESETS,
2538
+ SecondaryToolbar,
2539
+ SidebarField,
2540
+ SidebarSection,
2541
+ StyleSelector,
2542
+ TableBlock,
2543
+ TableCell,
2544
+ ToolbarButton,
2545
+ ToolbarDivider,
2546
+ ToolbarDropdown,
2547
+ UNDERLINE_TYPES,
2548
+ buildViewModel,
2549
+ createNewDocument,
2550
+ ensureSkeletonLoaded,
2551
+ extractImages,
2552
+ getDocumentStyles,
2553
+ hwpToMm,
2554
+ hwpToPx,
2555
+ mmToHwp,
2556
+ pxToHwp,
2557
+ readCharFormat,
2558
+ readFormatFromSelection,
2559
+ readParaFormat,
2560
+ readStyleInfo,
2561
+ useEditorStore
2562
+ };
2563
+ //# sourceMappingURL=index.js.map