@yurikilian/lex4 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +236 -20
  2. package/dist/ast/block-mapper.d.ts +24 -0
  3. package/dist/ast/block-mapper.d.ts.map +1 -0
  4. package/dist/ast/content-mapper.d.ts +16 -0
  5. package/dist/ast/content-mapper.d.ts.map +1 -0
  6. package/dist/ast/document-serializer.d.ts +11 -0
  7. package/dist/ast/document-serializer.d.ts.map +1 -0
  8. package/dist/ast/index.d.ts +9 -0
  9. package/dist/ast/index.d.ts.map +1 -0
  10. package/dist/ast/inline-mapper.d.ts +41 -0
  11. package/dist/ast/inline-mapper.d.ts.map +1 -0
  12. package/dist/ast/payload-builder.d.ts +18 -0
  13. package/dist/ast/payload-builder.d.ts.map +1 -0
  14. package/dist/ast/types.d.ts +112 -0
  15. package/dist/ast/types.d.ts.map +1 -0
  16. package/dist/components/EditorSidebar.d.ts +1 -0
  17. package/dist/components/EditorSidebar.d.ts.map +1 -1
  18. package/dist/components/HistorySidebar.d.ts.map +1 -1
  19. package/dist/components/Lex4Editor.d.ts +9 -1
  20. package/dist/components/Lex4Editor.d.ts.map +1 -1
  21. package/dist/components/PageBody.d.ts +1 -2
  22. package/dist/components/PageBody.d.ts.map +1 -1
  23. package/dist/components/PageFooter.d.ts.map +1 -1
  24. package/dist/components/PageHeader.d.ts.map +1 -1
  25. package/dist/components/Toolbar.d.ts.map +1 -1
  26. package/dist/components/VariablePanel.d.ts +15 -0
  27. package/dist/components/VariablePanel.d.ts.map +1 -0
  28. package/dist/components/VariablePicker.d.ts +14 -0
  29. package/dist/components/VariablePicker.d.ts.map +1 -0
  30. package/dist/extensions/ast-extension.d.ts +16 -0
  31. package/dist/extensions/ast-extension.d.ts.map +1 -0
  32. package/dist/extensions/extension-context.d.ts +29 -0
  33. package/dist/extensions/extension-context.d.ts.map +1 -0
  34. package/dist/extensions/index.d.ts +6 -0
  35. package/dist/extensions/index.d.ts.map +1 -0
  36. package/dist/extensions/types.d.ts +74 -0
  37. package/dist/extensions/types.d.ts.map +1 -0
  38. package/dist/extensions/variables-extension.d.ts +31 -0
  39. package/dist/extensions/variables-extension.d.ts.map +1 -0
  40. package/dist/i18n/context.d.ts +28 -0
  41. package/dist/i18n/context.d.ts.map +1 -0
  42. package/dist/i18n/defaults.d.ts +3 -0
  43. package/dist/i18n/defaults.d.ts.map +1 -0
  44. package/dist/i18n/index.d.ts +4 -0
  45. package/dist/i18n/index.d.ts.map +1 -0
  46. package/dist/i18n/types.d.ts +82 -0
  47. package/dist/i18n/types.d.ts.map +1 -0
  48. package/dist/index.d.ts +10 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/lex4-editor.cjs +1113 -101
  51. package/dist/lex4-editor.cjs.map +1 -1
  52. package/dist/lex4-editor.js +1116 -104
  53. package/dist/lex4-editor.js.map +1 -1
  54. package/dist/lexical/editor-setup.d.ts +5 -1
  55. package/dist/lexical/editor-setup.d.ts.map +1 -1
  56. package/dist/lexical/plugins/font-plugin.d.ts +1 -1
  57. package/dist/lexical/plugins/font-plugin.d.ts.map +1 -1
  58. package/dist/lexical/plugins/font-size-plugin.d.ts +19 -0
  59. package/dist/lexical/plugins/font-size-plugin.d.ts.map +1 -0
  60. package/dist/lexical/plugins/index.d.ts +2 -0
  61. package/dist/lexical/plugins/index.d.ts.map +1 -1
  62. package/dist/style.css +532 -160
  63. package/dist/types/editor-handle.d.ts +14 -0
  64. package/dist/types/editor-handle.d.ts.map +1 -0
  65. package/dist/types/editor-props.d.ts +23 -0
  66. package/dist/types/editor-props.d.ts.map +1 -1
  67. package/dist/variables/index.d.ts +5 -0
  68. package/dist/variables/index.d.ts.map +1 -0
  69. package/dist/variables/types.d.ts +26 -0
  70. package/dist/variables/types.d.ts.map +1 -0
  71. package/dist/variables/variable-commands.d.ts +11 -0
  72. package/dist/variables/variable-commands.d.ts.map +1 -0
  73. package/dist/variables/variable-context.d.ts +21 -0
  74. package/dist/variables/variable-context.d.ts.map +1 -0
  75. package/dist/variables/variable-node.d.ts +34 -0
  76. package/dist/variables/variable-node.d.ts.map +1 -0
  77. package/dist/variables/variable-plugin.d.ts +8 -0
  78. package/dist/variables/variable-plugin.d.ts.map +1 -0
  79. package/package.json +1 -1
@@ -1,6 +1,9 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
1
4
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import React, { createContext, useContext, useReducer, useState, useRef, useMemo, useCallback, useEffect, forwardRef, createElement } from "react";
3
- import { $getRoot, $createRangeSelectionFromDom, $getSelection, $isRangeSelection, $isTextNode, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $selectAll, KEY_TAB_COMMAND, COMMAND_PRIORITY_LOW, $isElementNode, $isParagraphNode, $setSelection, FOCUS_COMMAND, CONTROLLED_TEXT_INSERTION_COMMAND, KEY_DOWN_COMMAND, PASTE_COMMAND, KEY_ENTER_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_CRITICAL } from "lexical";
5
+ import React, { createContext, useContext, useReducer, useState, useRef, useMemo, useCallback, useEffect, forwardRef, createElement, useImperativeHandle } from "react";
6
+ import { $getRoot, $createRangeSelectionFromDom, $getSelection, $isRangeSelection, $isTextNode, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $selectAll, KEY_TAB_COMMAND, COMMAND_PRIORITY_LOW, $isElementNode, $isParagraphNode, $setSelection, FOCUS_COMMAND, CONTROLLED_TEXT_INSERTION_COMMAND, KEY_DOWN_COMMAND, PASTE_COMMAND, KEY_ENTER_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_CRITICAL, $applyNodeReplacement, DecoratorNode, createCommand, $insertNodes, COMMAND_PRIORITY_EDITOR } from "lexical";
4
7
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
5
8
  import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListNode, ListItemNode } from "@lexical/list";
6
9
  import { LexicalComposer } from "@lexical/react/LexicalComposer";
@@ -1131,33 +1134,53 @@ const createLucideIcon = (iconName, iconNode) => {
1131
1134
  * This source code is licensed under the ISC license.
1132
1135
  * See the LICENSE file in the root directory of this source tree.
1133
1136
  */
1134
- const __iconNode$j = [
1137
+ const __iconNode$m = [
1135
1138
  [
1136
1139
  "path",
1137
1140
  { d: "M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8", key: "mg9rjx" }
1138
1141
  ]
1139
1142
  ];
1140
- const Bold = createLucideIcon("bold", __iconNode$j);
1143
+ const Bold = createLucideIcon("bold", __iconNode$m);
1141
1144
  /**
1142
1145
  * @license lucide-react v1.8.0 - ISC
1143
1146
  *
1144
1147
  * This source code is licensed under the ISC license.
1145
1148
  * See the LICENSE file in the root directory of this source tree.
1146
1149
  */
1147
- const __iconNode$i = [
1150
+ const __iconNode$l = [
1151
+ [
1152
+ "path",
1153
+ { d: "M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1", key: "ezmyqa" }
1154
+ ],
1155
+ [
1156
+ "path",
1157
+ {
1158
+ d: "M16 21h1a2 2 0 0 0 2-2v-5c0-1.1.9-2 2-2a2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1",
1159
+ key: "e1hn23"
1160
+ }
1161
+ ]
1162
+ ];
1163
+ const Braces = createLucideIcon("braces", __iconNode$l);
1164
+ /**
1165
+ * @license lucide-react v1.8.0 - ISC
1166
+ *
1167
+ * This source code is licensed under the ISC license.
1168
+ * See the LICENSE file in the root directory of this source tree.
1169
+ */
1170
+ const __iconNode$k = [
1148
1171
  ["line", { x1: "15", x2: "15", y1: "12", y2: "18", key: "1p7wdc" }],
1149
1172
  ["line", { x1: "12", x2: "18", y1: "15", y2: "15", key: "1nscbv" }],
1150
1173
  ["rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2", key: "17jyea" }],
1151
1174
  ["path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2", key: "zix9uf" }]
1152
1175
  ];
1153
- const CopyPlus = createLucideIcon("copy-plus", __iconNode$i);
1176
+ const CopyPlus = createLucideIcon("copy-plus", __iconNode$k);
1154
1177
  /**
1155
1178
  * @license lucide-react v1.8.0 - ISC
1156
1179
  *
1157
1180
  * This source code is licensed under the ISC license.
1158
1181
  * See the LICENSE file in the root directory of this source tree.
1159
1182
  */
1160
- const __iconNode$h = [
1183
+ const __iconNode$j = [
1161
1184
  [
1162
1185
  "path",
1163
1186
  {
@@ -1167,65 +1190,65 @@ const __iconNode$h = [
1167
1190
  ],
1168
1191
  ["path", { d: "m5.082 11.09 8.828 8.828", key: "1wx5vj" }]
1169
1192
  ];
1170
- const Eraser = createLucideIcon("eraser", __iconNode$h);
1193
+ const Eraser = createLucideIcon("eraser", __iconNode$j);
1171
1194
  /**
1172
1195
  * @license lucide-react v1.8.0 - ISC
1173
1196
  *
1174
1197
  * This source code is licensed under the ISC license.
1175
1198
  * See the LICENSE file in the root directory of this source tree.
1176
1199
  */
1177
- const __iconNode$g = [
1200
+ const __iconNode$i = [
1178
1201
  ["line", { x1: "4", x2: "20", y1: "9", y2: "9", key: "4lhtct" }],
1179
1202
  ["line", { x1: "4", x2: "20", y1: "15", y2: "15", key: "vyu0kd" }],
1180
1203
  ["line", { x1: "10", x2: "8", y1: "3", y2: "21", key: "1ggp8o" }],
1181
1204
  ["line", { x1: "16", x2: "14", y1: "3", y2: "21", key: "weycgp" }]
1182
1205
  ];
1183
- const Hash = createLucideIcon("hash", __iconNode$g);
1206
+ const Hash = createLucideIcon("hash", __iconNode$i);
1184
1207
  /**
1185
1208
  * @license lucide-react v1.8.0 - ISC
1186
1209
  *
1187
1210
  * This source code is licensed under the ISC license.
1188
1211
  * See the LICENSE file in the root directory of this source tree.
1189
1212
  */
1190
- const __iconNode$f = [
1213
+ const __iconNode$h = [
1191
1214
  ["line", { x1: "19", x2: "10", y1: "4", y2: "4", key: "15jd3p" }],
1192
1215
  ["line", { x1: "14", x2: "5", y1: "20", y2: "20", key: "bu0au3" }],
1193
1216
  ["line", { x1: "15", x2: "9", y1: "4", y2: "20", key: "uljnxc" }]
1194
1217
  ];
1195
- const Italic = createLucideIcon("italic", __iconNode$f);
1218
+ const Italic = createLucideIcon("italic", __iconNode$h);
1196
1219
  /**
1197
1220
  * @license lucide-react v1.8.0 - ISC
1198
1221
  *
1199
1222
  * This source code is licensed under the ISC license.
1200
1223
  * See the LICENSE file in the root directory of this source tree.
1201
1224
  */
1202
- const __iconNode$e = [
1225
+ const __iconNode$g = [
1203
1226
  ["path", { d: "M21 5H11", key: "us1j55" }],
1204
1227
  ["path", { d: "M21 12H11", key: "wd7e0v" }],
1205
1228
  ["path", { d: "M21 19H11", key: "saa85w" }],
1206
1229
  ["path", { d: "m7 8-4 4 4 4", key: "o5hrat" }]
1207
1230
  ];
1208
- const ListIndentDecrease = createLucideIcon("list-indent-decrease", __iconNode$e);
1231
+ const ListIndentDecrease = createLucideIcon("list-indent-decrease", __iconNode$g);
1209
1232
  /**
1210
1233
  * @license lucide-react v1.8.0 - ISC
1211
1234
  *
1212
1235
  * This source code is licensed under the ISC license.
1213
1236
  * See the LICENSE file in the root directory of this source tree.
1214
1237
  */
1215
- const __iconNode$d = [
1238
+ const __iconNode$f = [
1216
1239
  ["path", { d: "M21 5H11", key: "us1j55" }],
1217
1240
  ["path", { d: "M21 12H11", key: "wd7e0v" }],
1218
1241
  ["path", { d: "M21 19H11", key: "saa85w" }],
1219
1242
  ["path", { d: "m3 8 4 4-4 4", key: "1a3j6y" }]
1220
1243
  ];
1221
- const ListIndentIncrease = createLucideIcon("list-indent-increase", __iconNode$d);
1244
+ const ListIndentIncrease = createLucideIcon("list-indent-increase", __iconNode$f);
1222
1245
  /**
1223
1246
  * @license lucide-react v1.8.0 - ISC
1224
1247
  *
1225
1248
  * This source code is licensed under the ISC license.
1226
1249
  * See the LICENSE file in the root directory of this source tree.
1227
1250
  */
1228
- const __iconNode$c = [
1251
+ const __iconNode$e = [
1229
1252
  ["path", { d: "M11 5h10", key: "1cz7ny" }],
1230
1253
  ["path", { d: "M11 12h10", key: "1438ji" }],
1231
1254
  ["path", { d: "M11 19h10", key: "11t30w" }],
@@ -1233,14 +1256,14 @@ const __iconNode$c = [
1233
1256
  ["path", { d: "M4 9h2", key: "r1h2o0" }],
1234
1257
  ["path", { d: "M6.5 20H3.4c0-1 2.6-1.925 2.6-3.5a1.5 1.5 0 0 0-2.6-1.02", key: "xtkcd5" }]
1235
1258
  ];
1236
- const ListOrdered = createLucideIcon("list-ordered", __iconNode$c);
1259
+ const ListOrdered = createLucideIcon("list-ordered", __iconNode$e);
1237
1260
  /**
1238
1261
  * @license lucide-react v1.8.0 - ISC
1239
1262
  *
1240
1263
  * This source code is licensed under the ISC license.
1241
1264
  * See the LICENSE file in the root directory of this source tree.
1242
1265
  */
1243
- const __iconNode$b = [
1266
+ const __iconNode$d = [
1244
1267
  ["path", { d: "M3 5h.01", key: "18ugdj" }],
1245
1268
  ["path", { d: "M3 12h.01", key: "nlz23k" }],
1246
1269
  ["path", { d: "M3 19h.01", key: "noohij" }],
@@ -1248,29 +1271,53 @@ const __iconNode$b = [
1248
1271
  ["path", { d: "M8 12h13", key: "1za7za" }],
1249
1272
  ["path", { d: "M8 19h13", key: "m83p4d" }]
1250
1273
  ];
1251
- const List = createLucideIcon("list", __iconNode$b);
1274
+ const List = createLucideIcon("list", __iconNode$d);
1252
1275
  /**
1253
1276
  * @license lucide-react v1.8.0 - ISC
1254
1277
  *
1255
1278
  * This source code is licensed under the ISC license.
1256
1279
  * See the LICENSE file in the root directory of this source tree.
1257
1280
  */
1258
- const __iconNode$a = [
1281
+ const __iconNode$c = [
1259
1282
  ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", key: "afitv7" }],
1260
1283
  ["path", { d: "M15 3v18", key: "14nvp0" }]
1261
1284
  ];
1262
- const PanelRight = createLucideIcon("panel-right", __iconNode$a);
1285
+ const PanelRight = createLucideIcon("panel-right", __iconNode$c);
1263
1286
  /**
1264
1287
  * @license lucide-react v1.8.0 - ISC
1265
1288
  *
1266
1289
  * This source code is licensed under the ISC license.
1267
1290
  * See the LICENSE file in the root directory of this source tree.
1268
1291
  */
1269
- const __iconNode$9 = [
1292
+ const __iconNode$b = [
1270
1293
  ["path", { d: "m15 14 5-5-5-5", key: "12vg1m" }],
1271
1294
  ["path", { d: "M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13", key: "6uklza" }]
1272
1295
  ];
1273
- const Redo2 = createLucideIcon("redo-2", __iconNode$9);
1296
+ const Redo2 = createLucideIcon("redo-2", __iconNode$b);
1297
+ /**
1298
+ * @license lucide-react v1.8.0 - ISC
1299
+ *
1300
+ * This source code is licensed under the ISC license.
1301
+ * See the LICENSE file in the root directory of this source tree.
1302
+ */
1303
+ const __iconNode$a = [
1304
+ ["path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8", key: "v9h5vc" }],
1305
+ ["path", { d: "M21 3v5h-5", key: "1q7to0" }],
1306
+ ["path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16", key: "3uifl3" }],
1307
+ ["path", { d: "M8 16H3v5", key: "1cv678" }]
1308
+ ];
1309
+ const RefreshCw = createLucideIcon("refresh-cw", __iconNode$a);
1310
+ /**
1311
+ * @license lucide-react v1.8.0 - ISC
1312
+ *
1313
+ * This source code is licensed under the ISC license.
1314
+ * See the LICENSE file in the root directory of this source tree.
1315
+ */
1316
+ const __iconNode$9 = [
1317
+ ["path", { d: "m21 21-4.34-4.34", key: "14j7rj" }],
1318
+ ["circle", { cx: "11", cy: "11", r: "8", key: "4ej97u" }]
1319
+ ];
1320
+ const Search = createLucideIcon("search", __iconNode$9);
1274
1321
  /**
1275
1322
  * @license lucide-react v1.8.0 - ISC
1276
1323
  *
@@ -1378,14 +1425,122 @@ const __iconNode = [
1378
1425
  ["path", { d: "m6 6 12 12", key: "d8bk6v" }]
1379
1426
  ];
1380
1427
  const X = createLucideIcon("x", __iconNode);
1428
+ const DEFAULT_TRANSLATIONS = {
1429
+ toolbar: {
1430
+ undo: "Undo",
1431
+ redo: "Redo",
1432
+ bold: "Bold (Ctrl+B)",
1433
+ italic: "Italic (Ctrl+I)",
1434
+ underline: "Underline (Ctrl+U)",
1435
+ strikethrough: "Strikethrough",
1436
+ alignLeft: "Align Left",
1437
+ alignCenter: "Align Center",
1438
+ alignRight: "Align Right",
1439
+ justify: "Justify",
1440
+ numberedList: "Numbered List",
1441
+ bulletList: "Bullet List",
1442
+ indent: "Indent",
1443
+ outdent: "Outdent",
1444
+ openHistory: "Open History",
1445
+ closeHistory: "Close History"
1446
+ },
1447
+ history: {
1448
+ title: "History",
1449
+ subtitle: "Word-style session history (last 100 actions)",
1450
+ empty: "No history yet.",
1451
+ clearHistory: "Clear History",
1452
+ actions: {
1453
+ enabledHeadersFooters: "Enabled headers and footers",
1454
+ disabledHeadersFooters: "Disabled headers and footers",
1455
+ copiedHeaderToAll: "Copied header to all pages",
1456
+ copiedFooterToAll: "Copied footer to all pages",
1457
+ clearedHeader: "Cleared header",
1458
+ clearedFooter: "Cleared footer",
1459
+ clearedAllHeaders: "Cleared all headers",
1460
+ clearedAllFooters: "Cleared all footers",
1461
+ pageCounterSet: "Page counter set to {{value}}",
1462
+ boldApplied: "Bold applied",
1463
+ italicApplied: "Italic applied",
1464
+ underlineApplied: "Underline applied",
1465
+ strikethroughApplied: "Strikethrough applied",
1466
+ alignedLeft: "Aligned left",
1467
+ alignedCenter: "Aligned center",
1468
+ alignedRight: "Aligned right",
1469
+ justifiedText: "Justified text",
1470
+ insertedNumberedList: "Inserted numbered list",
1471
+ insertedBulletList: "Inserted bullet list",
1472
+ indentedContent: "Indented content",
1473
+ outdentedContent: "Outdented content",
1474
+ fontChanged: "Font changed to {{value}}",
1475
+ fontSizeChanged: "Font size changed to {{value}}pt"
1476
+ }
1477
+ },
1478
+ variables: {
1479
+ title: "Variables",
1480
+ available: "{{count}} available",
1481
+ refreshVariables: "Refresh variables",
1482
+ searchPlaceholder: "Search variables...",
1483
+ noVariablesFound: "No variables found",
1484
+ insertVariable: "Insert variable {{key}}",
1485
+ openPanel: "Open Variables",
1486
+ closePanel: "Close Variables"
1487
+ },
1488
+ header: {
1489
+ placeholder: "Header"
1490
+ },
1491
+ footer: {
1492
+ placeholder: "Footer"
1493
+ },
1494
+ sidebar: {
1495
+ close: "Close sidebar"
1496
+ }
1497
+ };
1498
+ function deepMerge(target, source) {
1499
+ if (typeof target !== "object" || target === null) {
1500
+ return source ?? target;
1501
+ }
1502
+ const result = { ...target };
1503
+ for (const key of Object.keys(source)) {
1504
+ const sourceVal = source[key];
1505
+ const targetVal = target[key];
1506
+ if (sourceVal !== void 0 && typeof sourceVal === "object" && sourceVal !== null && !Array.isArray(sourceVal) && typeof targetVal === "object" && targetVal !== null) {
1507
+ result[key] = deepMerge(targetVal, sourceVal);
1508
+ } else if (sourceVal !== void 0) {
1509
+ result[key] = sourceVal;
1510
+ }
1511
+ }
1512
+ return result;
1513
+ }
1514
+ const TranslationsContext = createContext(DEFAULT_TRANSLATIONS);
1515
+ function useTranslations() {
1516
+ return useContext(TranslationsContext);
1517
+ }
1518
+ function interpolate(template, params) {
1519
+ return template.replace(
1520
+ /\{\{(\w+)\}\}/g,
1521
+ (_, key) => String(params[key] ?? `{{${key}}}`)
1522
+ );
1523
+ }
1524
+ const TranslationsProvider = ({
1525
+ translations,
1526
+ children
1527
+ }) => {
1528
+ const merged = useMemo(
1529
+ () => translations ? deepMerge(DEFAULT_TRANSLATIONS, translations) : DEFAULT_TRANSLATIONS,
1530
+ [translations]
1531
+ );
1532
+ return /* @__PURE__ */ jsx(TranslationsContext.Provider, { value: merged, children });
1533
+ };
1381
1534
  const EditorSidebar = ({
1382
1535
  title,
1383
1536
  subtitle,
1384
1537
  open,
1385
1538
  onClose,
1386
1539
  headerActions,
1540
+ testId = "editor-sidebar",
1387
1541
  children
1388
1542
  }) => {
1543
+ const t = useTranslations();
1389
1544
  if (!open) {
1390
1545
  return null;
1391
1546
  }
@@ -1393,7 +1548,7 @@ const EditorSidebar = ({
1393
1548
  "aside",
1394
1549
  {
1395
1550
  className: "flex h-full w-[320px] shrink-0 flex-col border-l border-gray-200 bg-white",
1396
- "data-testid": "editor-sidebar",
1551
+ "data-testid": testId,
1397
1552
  children: [
1398
1553
  /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between border-b border-gray-200 px-3 py-2.5", children: [
1399
1554
  /* @__PURE__ */ jsxs("div", { children: [
@@ -1409,7 +1564,7 @@ const EditorSidebar = ({
1409
1564
  className: "flex h-6 w-6 items-center justify-center rounded text-gray-400\n transition-colors hover:bg-gray-100 hover:text-gray-600",
1410
1565
  "data-testid": "close-editor-sidebar",
1411
1566
  onClick: onClose,
1412
- "aria-label": "Close sidebar",
1567
+ "aria-label": t.sidebar.close,
1413
1568
  children: /* @__PURE__ */ jsx(X, { size: 14 })
1414
1569
  }
1415
1570
  )
@@ -1437,13 +1592,14 @@ const HistorySidebar = () => {
1437
1592
  jumpToHistoryEntry: jumpToHistoryEntry2,
1438
1593
  setHistorySidebarOpen
1439
1594
  } = useDocument();
1595
+ const t = useTranslations();
1440
1596
  const visibleEntries = historyEntries.slice().reverse();
1441
1597
  const headerActions = /* @__PURE__ */ jsx(
1442
1598
  "button",
1443
1599
  {
1444
1600
  type: "button",
1445
- title: "Clear History",
1446
- "aria-label": "Clear History",
1601
+ title: t.history.clearHistory,
1602
+ "aria-label": t.history.clearHistory,
1447
1603
  onMouseDown: (e) => e.preventDefault(),
1448
1604
  onClick: () => clearHistory("manual"),
1449
1605
  className: "flex h-6 w-6 items-center justify-center rounded text-gray-400\n transition-colors hover:bg-gray-100 hover:text-gray-600",
@@ -1454,17 +1610,18 @@ const HistorySidebar = () => {
1454
1610
  return /* @__PURE__ */ jsx(
1455
1611
  EditorSidebar,
1456
1612
  {
1457
- title: "History",
1458
- subtitle: "Word-style session history (last 100 actions)",
1613
+ title: t.history.title,
1614
+ subtitle: t.history.subtitle,
1459
1615
  open: historySidebarOpen,
1460
1616
  onClose: () => setHistorySidebarOpen(false),
1461
1617
  headerActions,
1618
+ testId: "history-sidebar",
1462
1619
  children: visibleEntries.length === 0 ? /* @__PURE__ */ jsx(
1463
1620
  "div",
1464
1621
  {
1465
1622
  className: "px-3 py-4 text-xs text-gray-500",
1466
1623
  "data-testid": "history-empty",
1467
- children: "No history yet."
1624
+ children: t.history.empty
1468
1625
  }
1469
1626
  ) : /* @__PURE__ */ jsx("ol", { className: "divide-y divide-gray-100", "data-testid": "history-entry-list", children: visibleEntries.map((entry, reversedIndex) => {
1470
1627
  const actualIndex = historyEntries.length - reversedIndex - 1;
@@ -1655,7 +1812,81 @@ const ActionIconButton = ({ title, icon, onClick, disabled, testId }) => /* @__P
1655
1812
  }
1656
1813
  );
1657
1814
  const ActionDivider = () => /* @__PURE__ */ jsx("div", { className: "mx-0.5 h-4 w-px bg-gray-300/60" });
1815
+ function resolveExtensions(extensions) {
1816
+ const resolved = {
1817
+ nodes: [],
1818
+ bodyPlugins: [],
1819
+ toolbarItems: [],
1820
+ sidePanels: [],
1821
+ providers: [],
1822
+ themeOverrides: {},
1823
+ handleFactories: []
1824
+ };
1825
+ for (const ext of extensions) {
1826
+ if (ext.nodes) resolved.nodes.push(...ext.nodes);
1827
+ if (ext.bodyPlugins) resolved.bodyPlugins.push(...ext.bodyPlugins);
1828
+ if (ext.toolbarItems) resolved.toolbarItems.push(...ext.toolbarItems);
1829
+ if (ext.sidePanel) resolved.sidePanels.push(ext.sidePanel);
1830
+ if (ext.provider) resolved.providers.push(ext.provider);
1831
+ if (ext.themeOverrides) {
1832
+ resolved.themeOverrides = { ...resolved.themeOverrides, ...ext.themeOverrides };
1833
+ }
1834
+ if (ext.handleMethods) resolved.handleFactories.push(ext.handleMethods);
1835
+ }
1836
+ return resolved;
1837
+ }
1838
+ const EMPTY_RESOLVED = {
1839
+ nodes: [],
1840
+ bodyPlugins: [],
1841
+ toolbarItems: [],
1842
+ sidePanels: [],
1843
+ providers: [],
1844
+ themeOverrides: {},
1845
+ handleFactories: []
1846
+ };
1847
+ const ExtensionResolvedContext = createContext(EMPTY_RESOLVED);
1848
+ const ExtensionProvider = ({ extensions, children }) => {
1849
+ const resolved = useMemo(
1850
+ () => extensions && extensions.length > 0 ? resolveExtensions(extensions) : EMPTY_RESOLVED,
1851
+ [extensions]
1852
+ );
1853
+ let wrapped = /* @__PURE__ */ jsx(Fragment, { children });
1854
+ for (let i = resolved.providers.length - 1; i >= 0; i--) {
1855
+ const Provider = resolved.providers[i];
1856
+ wrapped = /* @__PURE__ */ jsx(Provider, { children: wrapped });
1857
+ }
1858
+ return /* @__PURE__ */ jsx(ExtensionResolvedContext.Provider, { value: resolved, children: wrapped });
1859
+ };
1860
+ function useExtensions() {
1861
+ return useContext(ExtensionResolvedContext);
1862
+ }
1863
+ const ExtensionStateContext = createContext(null);
1864
+ const ExtensionStateProvider = ({ children }) => {
1865
+ const stateRef = useRef({});
1866
+ return /* @__PURE__ */ jsx(ExtensionStateContext.Provider, { value: stateRef, children });
1867
+ };
1868
+ function useExtensionState() {
1869
+ const stateRef = useContext(ExtensionStateContext);
1870
+ return {
1871
+ get: (key) => stateRef == null ? void 0 : stateRef.current[key],
1872
+ set: (key, value) => {
1873
+ if (stateRef) stateRef.current[key] = value;
1874
+ }
1875
+ };
1876
+ }
1877
+ function useExtensionContext(getDocument, getActiveEditor) {
1878
+ const stateRef = useContext(ExtensionStateContext);
1879
+ return useMemo(() => ({
1880
+ getDocument,
1881
+ getActiveEditor,
1882
+ getExtensionState: (key) => stateRef == null ? void 0 : stateRef.current[key],
1883
+ setExtensionState: (key, value) => {
1884
+ if (stateRef) stateRef.current[key] = value;
1885
+ }
1886
+ }), [getDocument, getActiveEditor, stateRef]);
1887
+ }
1658
1888
  const SUPPORTED_FONTS = [
1889
+ "Inter",
1659
1890
  "Times New Roman",
1660
1891
  "Arial",
1661
1892
  "Calibri",
@@ -1674,6 +1905,42 @@ function applyFontFamily(editor, fontFamily) {
1674
1905
  }
1675
1906
  });
1676
1907
  }
1908
+ const SUPPORTED_FONT_SIZES = [
1909
+ 8,
1910
+ 9,
1911
+ 10,
1912
+ 11,
1913
+ 12,
1914
+ 14,
1915
+ 16,
1916
+ 18,
1917
+ 20,
1918
+ 24,
1919
+ 28,
1920
+ 32,
1921
+ 36,
1922
+ 48,
1923
+ 72
1924
+ ];
1925
+ function applyFontSize(editor, size) {
1926
+ editor.update(() => {
1927
+ const selection = $getSelection();
1928
+ if (!$isRangeSelection(selection)) return;
1929
+ const nodes = selection.getNodes();
1930
+ for (const node of nodes) {
1931
+ if ($isTextNode(node)) {
1932
+ const existing = node.getStyle();
1933
+ const updated = mergeFontSize(existing, size);
1934
+ node.setStyle(updated);
1935
+ }
1936
+ }
1937
+ });
1938
+ }
1939
+ function mergeFontSize(existingStyle, size) {
1940
+ const stripped = existingStyle.replace(/font-size:\s*[^;]+;?\s*/g, "").trim();
1941
+ const sizeDecl = `font-size: ${size}pt`;
1942
+ return stripped ? `${stripped}; ${sizeDecl}` : sizeDecl;
1943
+ }
1677
1944
  function toggleFormat(editor, format) {
1678
1945
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
1679
1946
  }
@@ -1721,6 +1988,8 @@ const Toolbar = () => {
1721
1988
  setHistorySidebarOpen,
1722
1989
  undo
1723
1990
  } = useDocument();
1991
+ const { toolbarItems } = useExtensions();
1992
+ const t = useTranslations();
1724
1993
  const withBodySelection = useCallback(
1725
1994
  (editor, action) => {
1726
1995
  editor.update(() => {
@@ -1758,7 +2027,7 @@ const Toolbar = () => {
1758
2027
  );
1759
2028
  const handleToggle = (enabled) => {
1760
2029
  runToolbarAction(
1761
- enabled ? "Enabled headers and footers" : "Disabled headers and footers",
2030
+ enabled ? t.history.actions.enabledHeadersFooters : t.history.actions.disabledHeadersFooters,
1762
2031
  () => {
1763
2032
  dispatch({ type: "SET_HEADER_FOOTER_ENABLED", enabled });
1764
2033
  }
@@ -1766,114 +2035,123 @@ const Toolbar = () => {
1766
2035
  };
1767
2036
  const handleCopyHeaderToAll = () => {
1768
2037
  if (activePageId) {
1769
- runToolbarAction("Copied header to all pages", () => {
2038
+ runToolbarAction(t.history.actions.copiedHeaderToAll, () => {
1770
2039
  dispatch({ type: "COPY_HEADER_TO_ALL", sourcePageId: activePageId });
1771
2040
  });
1772
2041
  }
1773
2042
  };
1774
2043
  const handleCopyFooterToAll = () => {
1775
2044
  if (activePageId) {
1776
- runToolbarAction("Copied footer to all pages", () => {
2045
+ runToolbarAction(t.history.actions.copiedFooterToAll, () => {
1777
2046
  dispatch({ type: "COPY_FOOTER_TO_ALL", sourcePageId: activePageId });
1778
2047
  });
1779
2048
  }
1780
2049
  };
1781
2050
  const handleClearHeader = () => {
1782
2051
  if (activePageId) {
1783
- runToolbarAction("Cleared header", () => {
2052
+ runToolbarAction(t.history.actions.clearedHeader, () => {
1784
2053
  dispatch({ type: "CLEAR_HEADER", pageId: activePageId });
1785
2054
  });
1786
2055
  }
1787
2056
  };
1788
2057
  const handleClearFooter = () => {
1789
2058
  if (activePageId) {
1790
- runToolbarAction("Cleared footer", () => {
2059
+ runToolbarAction(t.history.actions.clearedFooter, () => {
1791
2060
  dispatch({ type: "CLEAR_FOOTER", pageId: activePageId });
1792
2061
  });
1793
2062
  }
1794
2063
  };
1795
- const handleClearAllHeaders = () => runToolbarAction("Cleared all headers", () => {
2064
+ const handleClearAllHeaders = () => runToolbarAction(t.history.actions.clearedAllHeaders, () => {
1796
2065
  dispatch({ type: "CLEAR_ALL_HEADERS" });
1797
2066
  });
1798
- const handleClearAllFooters = () => runToolbarAction("Cleared all footers", () => {
2067
+ const handleClearAllFooters = () => runToolbarAction(t.history.actions.clearedAllFooters, () => {
1799
2068
  dispatch({ type: "CLEAR_ALL_FOOTERS" });
1800
2069
  });
1801
2070
  const handlePageCounterModeChange = useCallback((mode) => {
1802
- runToolbarAction(`Page counter set to ${mode}`, () => {
2071
+ runToolbarAction(interpolate(t.history.actions.pageCounterSet, { value: mode }), () => {
1803
2072
  dispatch({ type: "SET_PAGE_COUNTER_MODE", mode });
1804
2073
  });
1805
- }, [dispatch, runToolbarAction]);
2074
+ }, [dispatch, runToolbarAction, t.history.actions.pageCounterSet]);
1806
2075
  const handleBold = useCallback(() => {
1807
2076
  debug("toolbar", `bold (globalSelection=${globalSelectionActive}, editors=${editorRegistry.all().length}, hasEditor=${!!activeEditor})`);
1808
- runToolbarAction("Bold applied", () => {
2077
+ runToolbarAction(t.history.actions.boldApplied, () => {
1809
2078
  applyToBodyEditors(toggleBold);
1810
2079
  });
1811
- }, [activeEditor, applyToBodyEditors, editorRegistry, globalSelectionActive, runToolbarAction]);
2080
+ }, [activeEditor, applyToBodyEditors, editorRegistry, globalSelectionActive, runToolbarAction, t.history.actions.boldApplied]);
1812
2081
  const handleItalic = useCallback(() => {
1813
2082
  debug("toolbar", `italic (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
1814
- runToolbarAction("Italic applied", () => {
2083
+ runToolbarAction(t.history.actions.italicApplied, () => {
1815
2084
  applyToBodyEditors(toggleItalic);
1816
2085
  });
1817
- }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction]);
2086
+ }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction, t.history.actions.italicApplied]);
1818
2087
  const handleUnderline = useCallback(() => {
1819
2088
  debug("toolbar", `underline (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
1820
- runToolbarAction("Underline applied", () => {
2089
+ runToolbarAction(t.history.actions.underlineApplied, () => {
1821
2090
  applyToBodyEditors(toggleUnderline);
1822
2091
  });
1823
- }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction]);
2092
+ }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction, t.history.actions.underlineApplied]);
1824
2093
  const handleStrikethrough = useCallback(() => {
1825
2094
  debug("toolbar", `strikethrough (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
1826
- runToolbarAction("Strikethrough applied", () => {
2095
+ runToolbarAction(t.history.actions.strikethroughApplied, () => {
1827
2096
  applyToBodyEditors(toggleStrikethrough);
1828
2097
  });
1829
- }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction]);
2098
+ }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction, t.history.actions.strikethroughApplied]);
1830
2099
  const handleAlignLeft = useCallback(() => {
1831
- runToolbarAction("Aligned left", () => {
2100
+ runToolbarAction(t.history.actions.alignedLeft, () => {
1832
2101
  applyToBodyEditors((editor) => setAlignment(editor, "left"));
1833
2102
  });
1834
- }, [applyToBodyEditors, runToolbarAction]);
2103
+ }, [applyToBodyEditors, runToolbarAction, t.history.actions.alignedLeft]);
1835
2104
  const handleAlignCenter = useCallback(() => {
1836
- runToolbarAction("Aligned center", () => {
2105
+ runToolbarAction(t.history.actions.alignedCenter, () => {
1837
2106
  applyToBodyEditors((editor) => setAlignment(editor, "center"));
1838
2107
  });
1839
- }, [applyToBodyEditors, runToolbarAction]);
2108
+ }, [applyToBodyEditors, runToolbarAction, t.history.actions.alignedCenter]);
1840
2109
  const handleAlignRight = useCallback(() => {
1841
- runToolbarAction("Aligned right", () => {
2110
+ runToolbarAction(t.history.actions.alignedRight, () => {
1842
2111
  applyToBodyEditors((editor) => setAlignment(editor, "right"));
1843
2112
  });
1844
- }, [applyToBodyEditors, runToolbarAction]);
2113
+ }, [applyToBodyEditors, runToolbarAction, t.history.actions.alignedRight]);
1845
2114
  const handleAlignJustify = useCallback(() => {
1846
- runToolbarAction("Justified text", () => {
2115
+ runToolbarAction(t.history.actions.justifiedText, () => {
1847
2116
  applyToBodyEditors((editor) => setAlignment(editor, "justify"));
1848
2117
  });
1849
- }, [applyToBodyEditors, runToolbarAction]);
2118
+ }, [applyToBodyEditors, runToolbarAction, t.history.actions.justifiedText]);
1850
2119
  const handleListNumber = useCallback(() => {
1851
- runToolbarAction("Inserted numbered list", () => {
2120
+ runToolbarAction(t.history.actions.insertedNumberedList, () => {
1852
2121
  applyToBodyEditors((editor) => insertList(editor, "number"));
1853
2122
  });
1854
- }, [applyToBodyEditors, runToolbarAction]);
2123
+ }, [applyToBodyEditors, runToolbarAction, t.history.actions.insertedNumberedList]);
1855
2124
  const handleListBullet = useCallback(() => {
1856
- runToolbarAction("Inserted bullet list", () => {
2125
+ runToolbarAction(t.history.actions.insertedBulletList, () => {
1857
2126
  applyToBodyEditors((editor) => insertList(editor, "bullet"));
1858
2127
  });
1859
- }, [applyToBodyEditors, runToolbarAction]);
2128
+ }, [applyToBodyEditors, runToolbarAction, t.history.actions.insertedBulletList]);
1860
2129
  const handleIndent = useCallback(() => {
1861
- runToolbarAction("Indented content", () => {
2130
+ runToolbarAction(t.history.actions.indentedContent, () => {
1862
2131
  applyToBodyEditors(indentContent);
1863
2132
  });
1864
- }, [applyToBodyEditors, runToolbarAction]);
2133
+ }, [applyToBodyEditors, runToolbarAction, t.history.actions.indentedContent]);
1865
2134
  const handleOutdent = useCallback(() => {
1866
- runToolbarAction("Outdented content", () => {
2135
+ runToolbarAction(t.history.actions.outdentedContent, () => {
1867
2136
  applyToBodyEditors(outdentContent);
1868
2137
  });
1869
- }, [applyToBodyEditors, runToolbarAction]);
2138
+ }, [applyToBodyEditors, runToolbarAction, t.history.actions.outdentedContent]);
1870
2139
  const handleFontChange = useCallback(
1871
2140
  (e) => {
1872
- runToolbarAction(`Font changed to ${e.target.value}`, () => {
2141
+ runToolbarAction(interpolate(t.history.actions.fontChanged, { value: e.target.value }), () => {
1873
2142
  applyToBodyEditors((editor) => applyFontFamily(editor, e.target.value));
1874
2143
  });
1875
2144
  },
1876
- [applyToBodyEditors, runToolbarAction]
2145
+ [applyToBodyEditors, runToolbarAction, t.history.actions.fontChanged]
2146
+ );
2147
+ const handleFontSizeChange = useCallback(
2148
+ (e) => {
2149
+ const size = parseInt(e.target.value, 10);
2150
+ runToolbarAction(interpolate(t.history.actions.fontSizeChanged, { value: String(size) }), () => {
2151
+ applyToBodyEditors((editor) => applyFontSize(editor, size));
2152
+ });
2153
+ },
2154
+ [applyToBodyEditors, runToolbarAction, t.history.actions.fontSizeChanged]
1877
2155
  );
1878
2156
  return /* @__PURE__ */ jsx(
1879
2157
  "div",
@@ -1885,7 +2163,7 @@ const Toolbar = () => {
1885
2163
  /* @__PURE__ */ jsx(
1886
2164
  ToolbarIconButton,
1887
2165
  {
1888
- title: "Undo",
2166
+ title: t.toolbar.undo,
1889
2167
  testId: "btn-undo",
1890
2168
  disabled: !canUndo,
1891
2169
  onClick: undo,
@@ -1895,7 +2173,7 @@ const Toolbar = () => {
1895
2173
  /* @__PURE__ */ jsx(
1896
2174
  ToolbarIconButton,
1897
2175
  {
1898
- title: "Redo",
2176
+ title: t.toolbar.redo,
1899
2177
  testId: "btn-redo",
1900
2178
  disabled: !canRedo,
1901
2179
  onClick: redo,
@@ -1909,31 +2187,41 @@ const Toolbar = () => {
1909
2187
  {
1910
2188
  className: "h-7 rounded border border-gray-200 bg-white px-2 text-xs text-gray-700\n focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400",
1911
2189
  "data-testid": "font-selector",
1912
- defaultValue: "Arial",
2190
+ defaultValue: "Inter",
1913
2191
  onChange: handleFontChange,
1914
2192
  children: SUPPORTED_FONTS.map((font) => /* @__PURE__ */ jsx("option", { value: font, style: { fontFamily: font }, children: font }, font))
1915
2193
  }
1916
2194
  ),
2195
+ /* @__PURE__ */ jsx(
2196
+ "select",
2197
+ {
2198
+ className: "h-7 w-16 rounded border border-gray-200 bg-white px-1 text-xs text-gray-700\n focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400",
2199
+ "data-testid": "font-size-selector",
2200
+ defaultValue: "12",
2201
+ onChange: handleFontSizeChange,
2202
+ children: SUPPORTED_FONT_SIZES.map((size) => /* @__PURE__ */ jsx("option", { value: size, children: size }, size))
2203
+ }
2204
+ ),
1917
2205
  /* @__PURE__ */ jsx(Divider, {}),
1918
2206
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "format-group", children: [
1919
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: "Bold (Ctrl+B)", testId: "btn-bold", onClick: handleBold, children: /* @__PURE__ */ jsx(Bold, { size: 15 }) }),
1920
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: "Italic (Ctrl+I)", testId: "btn-italic", onClick: handleItalic, children: /* @__PURE__ */ jsx(Italic, { size: 15 }) }),
1921
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: "Underline (Ctrl+U)", testId: "btn-underline", onClick: handleUnderline, children: /* @__PURE__ */ jsx(Underline, { size: 15 }) }),
1922
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: "Strikethrough", testId: "btn-strike", onClick: handleStrikethrough, children: /* @__PURE__ */ jsx(Strikethrough, { size: 15 }) })
2207
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.bold, testId: "btn-bold", onClick: handleBold, children: /* @__PURE__ */ jsx(Bold, { size: 15 }) }),
2208
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.italic, testId: "btn-italic", onClick: handleItalic, children: /* @__PURE__ */ jsx(Italic, { size: 15 }) }),
2209
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.underline, testId: "btn-underline", onClick: handleUnderline, children: /* @__PURE__ */ jsx(Underline, { size: 15 }) }),
2210
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.strikethrough, testId: "btn-strike", onClick: handleStrikethrough, children: /* @__PURE__ */ jsx(Strikethrough, { size: 15 }) })
1923
2211
  ] }),
1924
2212
  /* @__PURE__ */ jsx(Divider, {}),
1925
2213
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "align-group", children: [
1926
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: "Align Left", testId: "btn-align-left", onClick: handleAlignLeft, children: /* @__PURE__ */ jsx(TextAlignStart, { size: 15 }) }),
1927
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: "Align Center", testId: "btn-align-center", onClick: handleAlignCenter, children: /* @__PURE__ */ jsx(TextAlignCenter, { size: 15 }) }),
1928
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: "Align Right", testId: "btn-align-right", onClick: handleAlignRight, children: /* @__PURE__ */ jsx(TextAlignEnd, { size: 15 }) }),
1929
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: "Justify", testId: "btn-align-justify", onClick: handleAlignJustify, children: /* @__PURE__ */ jsx(TextAlignJustify, { size: 15 }) })
2214
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.alignLeft, testId: "btn-align-left", onClick: handleAlignLeft, children: /* @__PURE__ */ jsx(TextAlignStart, { size: 15 }) }),
2215
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.alignCenter, testId: "btn-align-center", onClick: handleAlignCenter, children: /* @__PURE__ */ jsx(TextAlignCenter, { size: 15 }) }),
2216
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.alignRight, testId: "btn-align-right", onClick: handleAlignRight, children: /* @__PURE__ */ jsx(TextAlignEnd, { size: 15 }) }),
2217
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.justify, testId: "btn-align-justify", onClick: handleAlignJustify, children: /* @__PURE__ */ jsx(TextAlignJustify, { size: 15 }) })
1930
2218
  ] }),
1931
2219
  /* @__PURE__ */ jsx(Divider, {}),
1932
2220
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "list-group", children: [
1933
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: "Numbered List", testId: "btn-list-number", onClick: handleListNumber, children: /* @__PURE__ */ jsx(ListOrdered, { size: 15 }) }),
1934
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: "Bullet List", testId: "btn-list-bullet", onClick: handleListBullet, children: /* @__PURE__ */ jsx(List, { size: 15 }) }),
1935
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: "Indent", testId: "btn-indent", onClick: handleIndent, children: /* @__PURE__ */ jsx(ListIndentIncrease, { size: 15 }) }),
1936
- /* @__PURE__ */ jsx(ToolbarIconButton, { title: "Outdent", testId: "btn-outdent", onClick: handleOutdent, children: /* @__PURE__ */ jsx(ListIndentDecrease, { size: 15 }) })
2221
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.numberedList, testId: "btn-list-number", onClick: handleListNumber, children: /* @__PURE__ */ jsx(ListOrdered, { size: 15 }) }),
2222
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.bulletList, testId: "btn-list-bullet", onClick: handleListBullet, children: /* @__PURE__ */ jsx(List, { size: 15 }) }),
2223
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.indent, testId: "btn-indent", onClick: handleIndent, children: /* @__PURE__ */ jsx(ListIndentIncrease, { size: 15 }) }),
2224
+ /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.outdent, testId: "btn-outdent", onClick: handleOutdent, children: /* @__PURE__ */ jsx(ListIndentDecrease, { size: 15 }) })
1937
2225
  ] }),
1938
2226
  /* @__PURE__ */ jsx(Divider, {}),
1939
2227
  /* @__PURE__ */ jsx(
@@ -1960,10 +2248,14 @@ const Toolbar = () => {
1960
2248
  }
1961
2249
  )
1962
2250
  ] }),
2251
+ toolbarItems.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
2252
+ /* @__PURE__ */ jsx(Divider, {}),
2253
+ toolbarItems.map((ToolbarItem, idx) => /* @__PURE__ */ jsx(ToolbarItem, {}, idx))
2254
+ ] }),
1963
2255
  /* @__PURE__ */ jsx("div", { className: "ml-auto flex items-center", children: /* @__PURE__ */ jsx(
1964
2256
  ToolbarIconButton,
1965
2257
  {
1966
- title: historySidebarOpen ? "Close History" : "Open History",
2258
+ title: historySidebarOpen ? t.toolbar.closeHistory : t.toolbar.openHistory,
1967
2259
  testId: "toggle-history-sidebar",
1968
2260
  active: historySidebarOpen,
1969
2261
  onClick: () => setHistorySidebarOpen(!historySidebarOpen),
@@ -2266,12 +2558,13 @@ const lexicalTheme = {
2266
2558
  },
2267
2559
  quote: "border-l-4 border-gray-300 pl-4 italic text-gray-600"
2268
2560
  };
2269
- function createEditorConfig(mode, pageId) {
2561
+ const DEFAULT_NODES = [HeadingNode, QuoteNode, ListNode, ListItemNode];
2562
+ function createEditorConfig(mode, pageId, extraNodes = [], themeOverrides = {}) {
2270
2563
  const namespace = pageId ? `lex4-${mode}-${pageId}` : `lex4-${mode}`;
2271
2564
  return {
2272
2565
  namespace,
2273
- theme: lexicalTheme,
2274
- nodes: [HeadingNode, QuoteNode, ListNode, ListItemNode],
2566
+ theme: { ...lexicalTheme, ...themeOverrides },
2567
+ nodes: [...DEFAULT_NODES, ...extraNodes],
2275
2568
  onError: (error) => {
2276
2569
  console.error(`[Lex4 ${mode} editor error]`, error);
2277
2570
  }
@@ -2849,11 +3142,12 @@ const PageBody = ({
2849
3142
  onMoveToNextPage,
2850
3143
  readOnly = false
2851
3144
  }) => {
3145
+ const { nodes, bodyPlugins, themeOverrides } = useExtensions();
2852
3146
  const config = useMemo(
2853
3147
  () => {
2854
3148
  var _a, _b;
2855
3149
  const baseConfig = {
2856
- ...createEditorConfig("body", pageId),
3150
+ ...createEditorConfig("body", pageId, nodes, themeOverrides),
2857
3151
  editable: !readOnly
2858
3152
  };
2859
3153
  if (initialBodyState) {
@@ -2867,7 +3161,7 @@ const PageBody = ({
2867
3161
  return baseConfig;
2868
3162
  },
2869
3163
  // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally only use initialBodyState at mount time
2870
- [pageId, readOnly]
3164
+ [pageId, readOnly, nodes, themeOverrides]
2871
3165
  );
2872
3166
  const handleChange = useCallback(
2873
3167
  (editorState) => {
@@ -2923,6 +3217,7 @@ const PageBody = ({
2923
3217
  }
2924
3218
  ),
2925
3219
  /* @__PURE__ */ jsx(OverflowPlugin, { onOverflow: handleOverflow }),
3220
+ bodyPlugins.map((Plugin, idx) => /* @__PURE__ */ jsx(Plugin, {}, idx)),
2926
3221
  /* @__PURE__ */ jsx(OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true })
2927
3222
  ] })
2928
3223
  }
@@ -2992,16 +3287,18 @@ const PageHeader = ({
2992
3287
  onHeaderChange
2993
3288
  }) => {
2994
3289
  const hasPageCounter = !!pageCounterLabel;
3290
+ const { nodes, themeOverrides } = useExtensions();
3291
+ const t = useTranslations();
2995
3292
  const config = useMemo(
2996
3293
  () => {
2997
- const baseConfig = createEditorConfig("header", pageId);
3294
+ const baseConfig = createEditorConfig("header", pageId, nodes, themeOverrides);
2998
3295
  if (initialHeaderState) {
2999
3296
  return { ...baseConfig, editorState: JSON.stringify(initialHeaderState) };
3000
3297
  }
3001
3298
  return baseConfig;
3002
3299
  },
3003
3300
  // eslint-disable-next-line react-hooks/exhaustive-deps -- syncVersion forces remount via key, only used at init
3004
- [pageId]
3301
+ [pageId, nodes, themeOverrides]
3005
3302
  );
3006
3303
  const contentRef = useRef(null);
3007
3304
  const handleChange = useCallback(
@@ -3027,7 +3324,7 @@ const PageHeader = ({
3027
3324
  return /* @__PURE__ */ jsxs(
3028
3325
  "div",
3029
3326
  {
3030
- className: "lex4-page-header border-b border-dashed border-gray-200 relative flex-shrink-0",
3327
+ className: "lex4-page-header bg-blue-50/60 border-t-2 border-t-blue-200 border-b border-dashed border-blue-100 relative flex-shrink-0",
3031
3328
  style: { maxHeight: MAX_HEADER_HEIGHT_PX, overflow: "clip" },
3032
3329
  "data-testid": `page-header-${pageId}`,
3033
3330
  children: [
@@ -3042,7 +3339,7 @@ const PageHeader = ({
3042
3339
  className: `outline-none p-2 text-sm text-gray-600 min-h-[24px] ${hasPageCounter ? "pr-24" : ""}`
3043
3340
  }
3044
3341
  ),
3045
- placeholder: /* @__PURE__ */ jsx("div", { className: `absolute top-0 left-0 text-gray-400 pointer-events-none select-none p-2 text-sm ${hasPageCounter ? "pr-24" : ""}`, children: "Header" }),
3342
+ placeholder: /* @__PURE__ */ jsx("div", { className: `absolute top-0 left-0 text-gray-400 pointer-events-none select-none p-2 text-sm ${hasPageCounter ? "pr-24" : ""}`, children: t.header.placeholder }),
3046
3343
  ErrorBoundary: LexicalErrorBoundary
3047
3344
  }
3048
3345
  ),
@@ -3070,16 +3367,18 @@ const PageFooter = ({
3070
3367
  onFooterChange
3071
3368
  }) => {
3072
3369
  const hasPageCounter = !!pageCounterLabel;
3370
+ const { nodes, themeOverrides } = useExtensions();
3371
+ const t = useTranslations();
3073
3372
  const config = useMemo(
3074
3373
  () => {
3075
- const baseConfig = createEditorConfig("footer", pageId);
3374
+ const baseConfig = createEditorConfig("footer", pageId, nodes, themeOverrides);
3076
3375
  if (initialFooterState) {
3077
3376
  return { ...baseConfig, editorState: JSON.stringify(initialFooterState) };
3078
3377
  }
3079
3378
  return baseConfig;
3080
3379
  },
3081
3380
  // eslint-disable-next-line react-hooks/exhaustive-deps -- syncVersion forces remount via key, only used at init
3082
- [pageId]
3381
+ [pageId, nodes, themeOverrides]
3083
3382
  );
3084
3383
  const contentRef = useRef(null);
3085
3384
  const handleChange = useCallback(
@@ -3105,7 +3404,7 @@ const PageFooter = ({
3105
3404
  return /* @__PURE__ */ jsxs(
3106
3405
  "div",
3107
3406
  {
3108
- className: "lex4-page-footer border-t border-dashed border-gray-200 relative flex-shrink-0",
3407
+ className: "lex4-page-footer bg-blue-50/60 border-b-2 border-b-blue-200 border-t border-dashed border-blue-100 relative flex-shrink-0",
3109
3408
  style: { maxHeight: MAX_FOOTER_HEIGHT_PX, overflow: "clip" },
3110
3409
  "data-testid": `page-footer-${pageId}`,
3111
3410
  children: [
@@ -3120,7 +3419,7 @@ const PageFooter = ({
3120
3419
  className: `outline-none p-2 text-sm text-gray-600 min-h-[24px] ${hasPageCounter ? "pr-24" : ""}`
3121
3420
  }
3122
3421
  ),
3123
- placeholder: /* @__PURE__ */ jsx("div", { className: `absolute top-0 left-0 text-gray-400 pointer-events-none select-none p-2 text-sm ${hasPageCounter ? "pr-24" : ""}`, children: "Footer" }),
3422
+ placeholder: /* @__PURE__ */ jsx("div", { className: `absolute top-0 left-0 text-gray-400 pointer-events-none select-none p-2 text-sm ${hasPageCounter ? "pr-24" : ""}`, children: t.footer.placeholder }),
3124
3423
  ErrorBoundary: LexicalErrorBoundary
3125
3424
  }
3126
3425
  ),
@@ -3186,7 +3485,7 @@ const PageView = React.memo(({
3186
3485
  return /* @__PURE__ */ jsxs(
3187
3486
  "div",
3188
3487
  {
3189
- className: "lex4-page bg-white shadow-sm flex flex-col",
3488
+ className: "lex4-page bg-white shadow-xl flex flex-col",
3190
3489
  style: {
3191
3490
  width: A4_WIDTH_PX,
3192
3491
  height: A4_HEIGHT_PX,
@@ -3522,6 +3821,7 @@ const EditorChrome = ({
3522
3821
  undo,
3523
3822
  redo
3524
3823
  } = useDocument();
3824
+ const { sidePanels } = useExtensions();
3525
3825
  const rootRef = useRef(null);
3526
3826
  const selectionBufferRef = useRef(null);
3527
3827
  const clearGlobalSelection = useCallback(() => {
@@ -3663,35 +3963,63 @@ const EditorChrome = ({
3663
3963
  }
3664
3964
  ),
3665
3965
  /* @__PURE__ */ jsx(Toolbar, {}),
3666
- /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden bg-gray-200", children: [
3966
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden bg-gray-700", children: [
3667
3967
  /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1 overflow-auto", children: /* @__PURE__ */ jsx(DocumentView, {}) }),
3968
+ sidePanels.map((Panel, idx) => /* @__PURE__ */ jsx(Panel, {}, idx)),
3668
3969
  /* @__PURE__ */ jsx(HistorySidebar, {})
3669
3970
  ] })
3670
3971
  ]
3671
3972
  }
3672
3973
  );
3673
3974
  };
3674
- const Lex4Editor = ({
3975
+ const EditorWithHandle = forwardRef(({ captureHistoryShortcutsOnWindow, className }, ref) => {
3976
+ const { document: doc, activeEditor } = useDocument();
3977
+ const { handleFactories } = useExtensions();
3978
+ const getDocument = useCallback(() => doc, [doc]);
3979
+ const getActiveEditor = useCallback(() => activeEditor, [activeEditor]);
3980
+ const extensionCtx = useExtensionContext(getDocument, getActiveEditor);
3981
+ useImperativeHandle(ref, () => {
3982
+ const handle = {};
3983
+ for (const factory of handleFactories) {
3984
+ const methods = factory(extensionCtx);
3985
+ Object.assign(handle, methods);
3986
+ }
3987
+ return handle;
3988
+ }, [extensionCtx, handleFactories]);
3989
+ return /* @__PURE__ */ jsx(
3990
+ EditorChrome,
3991
+ {
3992
+ captureHistoryShortcutsOnWindow,
3993
+ className
3994
+ }
3995
+ );
3996
+ });
3997
+ EditorWithHandle.displayName = "EditorWithHandle";
3998
+ const Lex4Editor = forwardRef(({
3675
3999
  captureHistoryShortcutsOnWindow = true,
3676
4000
  initialDocument,
3677
4001
  onDocumentChange,
4002
+ extensions,
4003
+ translations,
3678
4004
  className
3679
- }) => {
3680
- return /* @__PURE__ */ jsx(
4005
+ }, ref) => {
4006
+ return /* @__PURE__ */ jsx(TranslationsProvider, { translations, children: /* @__PURE__ */ jsx(ExtensionStateProvider, { children: /* @__PURE__ */ jsx(ExtensionProvider, { extensions, children: /* @__PURE__ */ jsx(
3681
4007
  DocumentProvider,
3682
4008
  {
3683
4009
  initialDocument,
3684
4010
  onDocumentChange,
3685
4011
  children: /* @__PURE__ */ jsx(
3686
- EditorChrome,
4012
+ EditorWithHandle,
3687
4013
  {
4014
+ ref,
3688
4015
  captureHistoryShortcutsOnWindow,
3689
4016
  className
3690
4017
  }
3691
4018
  )
3692
4019
  }
3693
- );
3694
- };
4020
+ ) }) }) });
4021
+ });
4022
+ Lex4Editor.displayName = "Lex4Editor";
3695
4023
  function useOverflowDetection(bodyHeight, onOverflow, debounceMs = 100) {
3696
4024
  const timerRef = useRef(null);
3697
4025
  const observerRef = useRef(null);
@@ -3757,18 +4085,702 @@ function useHeaderFooter(maxHeight, onHeightChange) {
3757
4085
  }, []);
3758
4086
  return { attachRef };
3759
4087
  }
4088
+ const AST_VERSION = "1.0.0";
4089
+ const IS_BOLD = 1;
4090
+ const IS_ITALIC = 2;
4091
+ const IS_STRIKETHROUGH = 4;
4092
+ const IS_UNDERLINE = 8;
4093
+ function decodeFormatBitmask(format) {
4094
+ const marks = {};
4095
+ if (format & IS_BOLD) marks.bold = true;
4096
+ if (format & IS_ITALIC) marks.italic = true;
4097
+ if (format & IS_UNDERLINE) marks.underline = true;
4098
+ if (format & IS_STRIKETHROUGH) marks.strikethrough = true;
4099
+ return marks;
4100
+ }
4101
+ function extractFontFamily(style) {
4102
+ const match = style.match(/font-family:\s*([^;]+)/);
4103
+ return match ? match[1].trim().replace(/['"]/g, "") : void 0;
4104
+ }
4105
+ function extractFontSizePt(style) {
4106
+ const match = style.match(/font-size:\s*(\d+(?:\.\d+)?)\s*pt/);
4107
+ return match ? parseFloat(match[1]) : void 0;
4108
+ }
4109
+ function buildTextMarks(format, style) {
4110
+ const formatMarks = decodeFormatBitmask(format);
4111
+ const fontFamily = style ? extractFontFamily(style) : void 0;
4112
+ const fontSize = style ? extractFontSizePt(style) : void 0;
4113
+ const marks = {
4114
+ ...formatMarks,
4115
+ ...fontFamily ? { fontFamily } : {},
4116
+ ...fontSize ? { fontSize } : {}
4117
+ };
4118
+ return Object.keys(marks).length > 0 ? marks : void 0;
4119
+ }
4120
+ function mapInlineNode(node) {
4121
+ switch (node.type) {
4122
+ case "text":
4123
+ return mapTextNode(node);
4124
+ case "variable-node":
4125
+ return mapVariableNode(node);
4126
+ case "linebreak":
4127
+ return mapLineBreak();
4128
+ default:
4129
+ return { type: "text", text: "" };
4130
+ }
4131
+ }
4132
+ function mapTextNode(node) {
4133
+ const marks = buildTextMarks(node.format, node.style);
4134
+ return {
4135
+ type: "text",
4136
+ text: node.text,
4137
+ ...marks ? { marks } : {}
4138
+ };
4139
+ }
4140
+ function mapVariableNode(node) {
4141
+ return {
4142
+ type: "variable",
4143
+ key: node.variableKey
4144
+ };
4145
+ }
4146
+ function mapLineBreak() {
4147
+ return { type: "linebreak" };
4148
+ }
4149
+ function mapInlineNodes(nodes) {
4150
+ return nodes.map(mapInlineNode);
4151
+ }
4152
+ const ALIGN_LEFT = 1;
4153
+ const ALIGN_CENTER = 2;
4154
+ const ALIGN_RIGHT = 3;
4155
+ const ALIGN_JUSTIFY = 4;
4156
+ function decodeAlignment(format) {
4157
+ if (typeof format === "string") {
4158
+ if (["left", "center", "right", "justify"].includes(format)) {
4159
+ return format;
4160
+ }
4161
+ return void 0;
4162
+ }
4163
+ if (typeof format !== "number" || format === 0) return void 0;
4164
+ switch (format) {
4165
+ case ALIGN_LEFT:
4166
+ return "left";
4167
+ case ALIGN_CENTER:
4168
+ return "center";
4169
+ case ALIGN_RIGHT:
4170
+ return "right";
4171
+ case ALIGN_JUSTIFY:
4172
+ return "justify";
4173
+ default:
4174
+ return void 0;
4175
+ }
4176
+ }
4177
+ function mapBlockNode(node) {
4178
+ switch (node.type) {
4179
+ case "paragraph":
4180
+ return mapParagraph(node);
4181
+ case "heading":
4182
+ return mapHeading(node);
4183
+ case "list":
4184
+ return mapList(node);
4185
+ case "quote":
4186
+ return mapBlockQuote(node);
4187
+ default:
4188
+ return {
4189
+ type: "paragraph",
4190
+ children: mapInlineChildren(node)
4191
+ };
4192
+ }
4193
+ }
4194
+ function mapParagraph(node) {
4195
+ const alignment = decodeAlignment(node.format);
4196
+ const indent = node.indent && node.indent > 0 ? node.indent : void 0;
4197
+ return {
4198
+ type: "paragraph",
4199
+ ...alignment ? { alignment } : {},
4200
+ ...indent ? { indent } : {},
4201
+ children: mapInlineChildren(node)
4202
+ };
4203
+ }
4204
+ function mapHeading(node) {
4205
+ var _a;
4206
+ const alignment = decodeAlignment(node.format);
4207
+ const tagMatch = (_a = node.tag) == null ? void 0 : _a.match(/^h(\d)$/);
4208
+ const level = tagMatch ? parseInt(tagMatch[1], 10) : 1;
4209
+ return {
4210
+ type: "heading",
4211
+ level,
4212
+ ...alignment ? { alignment } : {},
4213
+ children: mapInlineChildren(node)
4214
+ };
4215
+ }
4216
+ function mapList(node) {
4217
+ const listType = node.listType === "number" ? "ordered" : "unordered";
4218
+ const items = (node.children ?? []).filter((c) => c.type === "listitem").map(mapListItem);
4219
+ return {
4220
+ type: "list",
4221
+ listType,
4222
+ items
4223
+ };
4224
+ }
4225
+ function mapListItem(node) {
4226
+ const inlineChildren = [];
4227
+ let nestedList;
4228
+ for (const child of node.children ?? []) {
4229
+ if (child.type === "list") {
4230
+ nestedList = mapList(child);
4231
+ } else {
4232
+ const mapped = mapInlineNodes([child]);
4233
+ inlineChildren.push(...mapped);
4234
+ }
4235
+ }
4236
+ return {
4237
+ type: "list-item",
4238
+ children: inlineChildren,
4239
+ ...nestedList ? { nestedList } : {}
4240
+ };
4241
+ }
4242
+ function mapBlockQuote(node) {
4243
+ return {
4244
+ type: "blockquote",
4245
+ children: mapInlineChildren(node)
4246
+ };
4247
+ }
4248
+ function mapInlineChildren(node) {
4249
+ if (!node.children || node.children.length === 0) return [];
4250
+ return mapInlineNodes(node.children);
4251
+ }
4252
+ function mapBlockNodes(nodes) {
4253
+ return nodes.map(mapBlockNode);
4254
+ }
4255
+ function mapEditorStateToContent(state) {
4256
+ if (!state || !state.root || !state.root.children) {
4257
+ return null;
4258
+ }
4259
+ const blocks = mapBlockNodes(state.root.children);
4260
+ return { blocks };
4261
+ }
4262
+ function mapEditorStateToBlocks(state) {
4263
+ if (!state || !state.root || !state.root.children) {
4264
+ return [];
4265
+ }
4266
+ return mapBlockNodes(state.root.children);
4267
+ }
4268
+ const MARGIN_MM = Math.round(PAGE_MARGIN_PX / PX_PER_MM * 10) / 10;
4269
+ function serializeDocument(document2, variableDefinitions = []) {
4270
+ const pages = document2.pages.map(
4271
+ (page, index) => serializePage(page, index)
4272
+ );
4273
+ const metadata = buildMetadata(variableDefinitions);
4274
+ return {
4275
+ version: AST_VERSION,
4276
+ page: {
4277
+ format: "A4",
4278
+ widthMm: 210,
4279
+ heightMm: 297,
4280
+ margins: {
4281
+ topMm: MARGIN_MM,
4282
+ rightMm: MARGIN_MM,
4283
+ bottomMm: MARGIN_MM,
4284
+ leftMm: MARGIN_MM
4285
+ }
4286
+ },
4287
+ headerFooter: {
4288
+ enabled: document2.headerFooterEnabled,
4289
+ pageCounterMode: document2.pageCounterMode,
4290
+ defaultHeader: document2.pages.length > 0 ? mapEditorStateToContent(document2.pages[0].headerState) : null,
4291
+ defaultFooter: document2.pages.length > 0 ? mapEditorStateToContent(document2.pages[0].footerState) : null
4292
+ },
4293
+ pages,
4294
+ metadata
4295
+ };
4296
+ }
4297
+ function serializePage(page, pageIndex) {
4298
+ return {
4299
+ pageIndex,
4300
+ body: mapEditorStateToBlocks(page.bodyState),
4301
+ header: mapEditorStateToContent(page.headerState),
4302
+ footer: mapEditorStateToContent(page.footerState)
4303
+ };
4304
+ }
4305
+ function buildMetadata(variableDefinitions) {
4306
+ const variables = {};
4307
+ for (const def of variableDefinitions) {
4308
+ variables[def.key] = {
4309
+ key: def.key,
4310
+ label: def.label,
4311
+ ...def.description ? { description: def.description } : {},
4312
+ ...def.valueType ? { valueType: def.valueType } : {},
4313
+ ...def.group ? { group: def.group } : {}
4314
+ };
4315
+ }
4316
+ return { variables };
4317
+ }
4318
+ function buildSavePayload(ast, options) {
4319
+ return {
4320
+ document: ast,
4321
+ ...(options == null ? void 0 : options.exportTarget) ? { exportTarget: options.exportTarget } : {},
4322
+ ...(options == null ? void 0 : options.documentId) ? { documentId: options.documentId } : {},
4323
+ ...(options == null ? void 0 : options.metadata) ? { metadata: options.metadata } : {}
4324
+ };
4325
+ }
4326
+ function serializeDocumentJson(ast) {
4327
+ return JSON.stringify(ast, null, 2);
4328
+ }
4329
+ function astExtension() {
4330
+ return {
4331
+ name: "ast",
4332
+ handleMethods: (ctx) => {
4333
+ const getDefinitions = () => {
4334
+ return ctx.getExtensionState("variableDefinitions") ?? [];
4335
+ };
4336
+ return {
4337
+ getDocumentAst: () => {
4338
+ const doc = ctx.getDocument();
4339
+ return serializeDocument(doc, getDefinitions());
4340
+ },
4341
+ getDocumentJson: () => {
4342
+ const doc = ctx.getDocument();
4343
+ const ast = serializeDocument(doc, getDefinitions());
4344
+ return serializeDocumentJson(ast);
4345
+ },
4346
+ buildSavePayload: (options) => {
4347
+ const doc = ctx.getDocument();
4348
+ const ast = serializeDocument(doc, getDefinitions());
4349
+ return buildSavePayload(ast, options);
4350
+ }
4351
+ };
4352
+ }
4353
+ };
4354
+ }
4355
+ class VariableNode extends DecoratorNode {
4356
+ constructor(variableKey, key) {
4357
+ super(key);
4358
+ __publicField(this, "__variableKey");
4359
+ this.__variableKey = variableKey;
4360
+ }
4361
+ static getType() {
4362
+ return "variable-node";
4363
+ }
4364
+ static clone(node) {
4365
+ return new VariableNode(node.__variableKey, node.__key);
4366
+ }
4367
+ getVariableKey() {
4368
+ return this.__variableKey;
4369
+ }
4370
+ // -- Serialization --
4371
+ static importJSON(serializedNode) {
4372
+ return $createVariableNode(serializedNode.variableKey);
4373
+ }
4374
+ exportJSON() {
4375
+ return {
4376
+ type: "variable-node",
4377
+ version: 1,
4378
+ variableKey: this.__variableKey
4379
+ };
4380
+ }
4381
+ // -- DOM --
4382
+ createDOM() {
4383
+ const span = document.createElement("span");
4384
+ span.className = "lex4-variable";
4385
+ span.setAttribute("data-variable-key", this.__variableKey);
4386
+ span.setAttribute("data-testid", `variable-${this.__variableKey}`);
4387
+ span.contentEditable = "false";
4388
+ return span;
4389
+ }
4390
+ updateDOM() {
4391
+ return false;
4392
+ }
4393
+ exportDOM() {
4394
+ const span = document.createElement("span");
4395
+ span.setAttribute("data-variable-key", this.__variableKey);
4396
+ span.textContent = `{{${this.__variableKey}}}`;
4397
+ return { element: span };
4398
+ }
4399
+ static importDOM() {
4400
+ return null;
4401
+ }
4402
+ // -- Behavior --
4403
+ isInline() {
4404
+ return true;
4405
+ }
4406
+ isKeyboardSelectable() {
4407
+ return true;
4408
+ }
4409
+ getTextContent() {
4410
+ return `{{${this.__variableKey}}}`;
4411
+ }
4412
+ // -- Rendering --
4413
+ decorate() {
4414
+ return /* @__PURE__ */ jsx(
4415
+ "span",
4416
+ {
4417
+ className: "lex4-variable-chip inline-flex items-center rounded bg-white px-1.5 py-0.5\n text-xs font-medium text-blue-700 border border-blue-300 select-none\n cursor-default whitespace-nowrap mx-0.5",
4418
+ "data-testid": `variable-chip-${this.__variableKey}`,
4419
+ title: this.__variableKey,
4420
+ children: `{{${this.__variableKey}}}`
4421
+ }
4422
+ );
4423
+ }
4424
+ }
4425
+ function $createVariableNode(variableKey) {
4426
+ return $applyNodeReplacement(new VariableNode(variableKey));
4427
+ }
4428
+ function $isVariableNode(node) {
4429
+ return node instanceof VariableNode;
4430
+ }
4431
+ const INSERT_VARIABLE_COMMAND = createCommand("INSERT_VARIABLE");
4432
+ const VariablePlugin = () => {
4433
+ const [editor] = useLexicalComposerContext();
4434
+ useEffect(() => {
4435
+ return editor.registerCommand(
4436
+ INSERT_VARIABLE_COMMAND,
4437
+ (variableKey) => {
4438
+ editor.update(() => {
4439
+ const selection = $getSelection();
4440
+ if (!$isRangeSelection(selection)) return;
4441
+ const variableNode = $createVariableNode(variableKey);
4442
+ $insertNodes([variableNode]);
4443
+ });
4444
+ return true;
4445
+ },
4446
+ COMMAND_PRIORITY_EDITOR
4447
+ );
4448
+ }, [editor]);
4449
+ return null;
4450
+ };
4451
+ const EMPTY_CONTEXT = {
4452
+ definitions: [],
4453
+ refreshDefinitions: () => {
4454
+ },
4455
+ getDefinition: () => void 0
4456
+ };
4457
+ const VariableContext = createContext(EMPTY_CONTEXT);
4458
+ const VariableProvider = ({
4459
+ initialDefinitions = [],
4460
+ children
4461
+ }) => {
4462
+ const [definitions, setDefinitions] = useState(initialDefinitions);
4463
+ const refresh = useCallback((newDefinitions) => {
4464
+ setDefinitions(newDefinitions);
4465
+ }, []);
4466
+ const getDefinition = useCallback(
4467
+ (key) => {
4468
+ return definitions.find((d) => d.key === key);
4469
+ },
4470
+ [definitions]
4471
+ );
4472
+ const value = useMemo(
4473
+ () => ({ definitions, refreshDefinitions: refresh, getDefinition }),
4474
+ [definitions, refresh, getDefinition]
4475
+ );
4476
+ return /* @__PURE__ */ jsx(VariableContext.Provider, { value, children });
4477
+ };
4478
+ function useVariables() {
4479
+ return useContext(VariableContext);
4480
+ }
4481
+ const VariablePicker = ({ onInsert, disabled = false }) => {
4482
+ const { definitions } = useVariables();
4483
+ const [open, setOpen] = useState(false);
4484
+ const [filter, setFilter] = useState("");
4485
+ const containerRef = useRef(null);
4486
+ const filtered = useMemo(() => {
4487
+ if (!filter) return definitions;
4488
+ const lower = filter.toLowerCase();
4489
+ return definitions.filter(
4490
+ (d) => d.key.toLowerCase().includes(lower) || d.label.toLowerCase().includes(lower)
4491
+ );
4492
+ }, [definitions, filter]);
4493
+ const grouped = useMemo(() => {
4494
+ const groups = {};
4495
+ for (const def of filtered) {
4496
+ const g = def.group ?? "Other";
4497
+ if (!groups[g]) groups[g] = [];
4498
+ groups[g].push(def);
4499
+ }
4500
+ return groups;
4501
+ }, [filtered]);
4502
+ const handleInsert = useCallback(
4503
+ (key) => {
4504
+ onInsert(key);
4505
+ setOpen(false);
4506
+ setFilter("");
4507
+ },
4508
+ [onInsert]
4509
+ );
4510
+ useEffect(() => {
4511
+ if (!open) return;
4512
+ const handler = (e) => {
4513
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
4514
+ setOpen(false);
4515
+ setFilter("");
4516
+ }
4517
+ };
4518
+ document.addEventListener("mousedown", handler);
4519
+ return () => document.removeEventListener("mousedown", handler);
4520
+ }, [open]);
4521
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative inline-block", children: [
4522
+ /* @__PURE__ */ jsx(
4523
+ "button",
4524
+ {
4525
+ type: "button",
4526
+ className: "h-7 rounded border border-gray-200 bg-white px-2 text-xs font-medium text-gray-700\n hover:bg-gray-50 focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400\n disabled:opacity-50 disabled:cursor-not-allowed",
4527
+ "data-testid": "variable-picker-button",
4528
+ disabled: disabled || definitions.length === 0,
4529
+ onClick: () => setOpen(!open),
4530
+ title: "Insert variable",
4531
+ children: "{x}"
4532
+ }
4533
+ ),
4534
+ open && /* @__PURE__ */ jsxs(
4535
+ "div",
4536
+ {
4537
+ className: "absolute left-0 top-full z-50 mt-1 w-64 rounded-md border border-gray-200\n bg-white shadow-lg",
4538
+ "data-testid": "variable-picker-dropdown",
4539
+ children: [
4540
+ /* @__PURE__ */ jsx("div", { className: "border-b border-gray-100 p-2", children: /* @__PURE__ */ jsx(
4541
+ "input",
4542
+ {
4543
+ type: "text",
4544
+ className: "w-full rounded border border-gray-200 px-2 py-1 text-xs\n focus:border-blue-400 focus:outline-none",
4545
+ placeholder: "Search variables...",
4546
+ "data-testid": "variable-picker-search",
4547
+ value: filter,
4548
+ onChange: (e) => setFilter(e.target.value),
4549
+ autoFocus: true
4550
+ }
4551
+ ) }),
4552
+ /* @__PURE__ */ jsxs("div", { className: "max-h-48 overflow-y-auto p-1", children: [
4553
+ Object.keys(grouped).length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-1 text-xs text-gray-400", children: "No variables found" }),
4554
+ Object.entries(grouped).map(([group, defs]) => /* @__PURE__ */ jsxs("div", { children: [
4555
+ /* @__PURE__ */ jsx("div", { className: "px-2 py-1 text-[10px] font-semibold uppercase text-gray-400", children: group }),
4556
+ defs.map((def) => /* @__PURE__ */ jsxs(
4557
+ "button",
4558
+ {
4559
+ type: "button",
4560
+ className: "flex w-full items-center gap-2 rounded px-2 py-1 text-left text-xs\n hover:bg-blue-50",
4561
+ "data-testid": `variable-option-${def.key}`,
4562
+ onClick: () => handleInsert(def.key),
4563
+ children: [
4564
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-blue-700", children: `{{${def.key}}}` }),
4565
+ /* @__PURE__ */ jsx("span", { className: "text-gray-500", children: def.label })
4566
+ ]
4567
+ },
4568
+ def.key
4569
+ ))
4570
+ ] }, group))
4571
+ ] })
4572
+ ]
4573
+ }
4574
+ )
4575
+ ] });
4576
+ };
4577
+ const VariablePanel = ({ open, onClose }) => {
4578
+ const { definitions } = useVariables();
4579
+ const { activeEditor, runHistoryAction } = useDocument();
4580
+ const t = useTranslations();
4581
+ const [filter, setFilter] = useState("");
4582
+ const filtered = useMemo(() => {
4583
+ if (!filter) return definitions;
4584
+ const lower = filter.toLowerCase();
4585
+ return definitions.filter(
4586
+ (d) => d.key.toLowerCase().includes(lower) || d.label.toLowerCase().includes(lower)
4587
+ );
4588
+ }, [definitions, filter]);
4589
+ const grouped = useMemo(() => {
4590
+ const groups = {};
4591
+ for (const def of filtered) {
4592
+ const g = def.group ?? "Other";
4593
+ if (!groups[g]) groups[g] = [];
4594
+ groups[g].push(def);
4595
+ }
4596
+ return groups;
4597
+ }, [filtered]);
4598
+ const handleInsert = useCallback(
4599
+ (key) => {
4600
+ if (!activeEditor) return;
4601
+ runHistoryAction(
4602
+ { label: interpolate(t.variables.insertVariable, { key }), source: "toolbar", region: "document" },
4603
+ () => {
4604
+ activeEditor.dispatchCommand(INSERT_VARIABLE_COMMAND, key);
4605
+ }
4606
+ );
4607
+ },
4608
+ [activeEditor, runHistoryAction]
4609
+ );
4610
+ if (definitions.length === 0) return null;
4611
+ return /* @__PURE__ */ jsxs(
4612
+ EditorSidebar,
4613
+ {
4614
+ title: t.variables.title,
4615
+ subtitle: interpolate(t.variables.available, { count: String(definitions.length) }),
4616
+ open,
4617
+ onClose,
4618
+ testId: "variable-panel",
4619
+ headerActions: /* @__PURE__ */ jsx(
4620
+ "button",
4621
+ {
4622
+ type: "button",
4623
+ className: "flex h-6 w-6 items-center justify-center rounded text-gray-400\n transition-colors hover:bg-gray-100 hover:text-gray-600",
4624
+ title: t.variables.refreshVariables,
4625
+ "data-testid": "btn-refresh-variables",
4626
+ children: /* @__PURE__ */ jsx(RefreshCw, { size: 12 })
4627
+ }
4628
+ ),
4629
+ children: [
4630
+ /* @__PURE__ */ jsx("div", { className: "p-3", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
4631
+ /* @__PURE__ */ jsx(Search, { size: 14, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400" }),
4632
+ /* @__PURE__ */ jsx(
4633
+ "input",
4634
+ {
4635
+ type: "text",
4636
+ className: "w-full rounded-lg border border-gray-200 bg-gray-50 py-1.5 pl-8 pr-3 text-xs\n placeholder-gray-400 focus:border-blue-400 focus:bg-white focus:outline-none\n focus:ring-1 focus:ring-blue-400",
4637
+ placeholder: t.variables.searchPlaceholder,
4638
+ "data-testid": "variable-panel-search",
4639
+ value: filter,
4640
+ onChange: (e) => setFilter(e.target.value)
4641
+ }
4642
+ )
4643
+ ] }) }),
4644
+ /* @__PURE__ */ jsxs("div", { className: "px-3 pb-3", children: [
4645
+ Object.keys(grouped).length === 0 && /* @__PURE__ */ jsx("div", { className: "py-4 text-center text-xs text-gray-400", children: t.variables.noVariablesFound }),
4646
+ Object.entries(grouped).map(([group, defs]) => /* @__PURE__ */ jsx("div", { className: "mb-3", children: /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: defs.map((def) => /* @__PURE__ */ jsxs(
4647
+ "button",
4648
+ {
4649
+ type: "button",
4650
+ className: "flex items-center justify-between gap-2 rounded-lg px-2.5 py-1.5 text-left text-xs\n transition-colors hover:bg-blue-50 group",
4651
+ "data-testid": `variable-panel-${def.key}`,
4652
+ onClick: () => handleInsert(def.key),
4653
+ disabled: !activeEditor,
4654
+ children: [
4655
+ /* @__PURE__ */ jsx(
4656
+ "span",
4657
+ {
4658
+ className: "inline-flex items-center rounded-full border border-blue-300 bg-white\n px-2 py-0.5 text-[11px] font-medium text-blue-700\n group-hover:border-blue-400 group-hover:bg-blue-50",
4659
+ children: def.label
4660
+ }
4661
+ ),
4662
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold uppercase tracking-wider text-gray-400", children: group })
4663
+ ]
4664
+ },
4665
+ def.key
4666
+ )) }) }, group))
4667
+ ] })
4668
+ ]
4669
+ }
4670
+ );
4671
+ };
4672
+ const VariablePanelContext = createContext({
4673
+ panelOpen: false,
4674
+ setPanelOpen: () => {
4675
+ }
4676
+ });
4677
+ function useVariablePanelState() {
4678
+ return useContext(VariablePanelContext);
4679
+ }
4680
+ const VariablePanelStateProvider = ({ children }) => {
4681
+ const [panelOpen, setPanelOpen] = useState(false);
4682
+ return /* @__PURE__ */ jsx(VariablePanelContext.Provider, { value: { panelOpen, setPanelOpen }, children });
4683
+ };
4684
+ const VariableToolbarItem = () => {
4685
+ const { activeEditor, runHistoryAction } = useDocument();
4686
+ const t = useTranslations();
4687
+ const handleInsert = useCallback((variableKey) => {
4688
+ runHistoryAction(
4689
+ { label: interpolate(t.variables.insertVariable, { key: variableKey }), source: "toolbar", region: "document" },
4690
+ () => {
4691
+ if (activeEditor) {
4692
+ activeEditor.dispatchCommand(INSERT_VARIABLE_COMMAND, variableKey);
4693
+ }
4694
+ }
4695
+ );
4696
+ }, [activeEditor, runHistoryAction]);
4697
+ return /* @__PURE__ */ jsx(
4698
+ VariablePicker,
4699
+ {
4700
+ onInsert: handleInsert,
4701
+ disabled: !activeEditor
4702
+ }
4703
+ );
4704
+ };
4705
+ const VariablePanelToggle = () => {
4706
+ const { panelOpen, setPanelOpen } = useVariablePanelState();
4707
+ const t = useTranslations();
4708
+ return /* @__PURE__ */ jsx(
4709
+ "button",
4710
+ {
4711
+ type: "button",
4712
+ title: panelOpen ? t.variables.closePanel : t.variables.openPanel,
4713
+ "aria-label": panelOpen ? t.variables.closePanel : t.variables.openPanel,
4714
+ onMouseDown: (e) => e.preventDefault(),
4715
+ onClick: () => setPanelOpen(!panelOpen),
4716
+ className: `
4717
+ flex h-7 w-7 items-center justify-center rounded transition-colors
4718
+ ${panelOpen ? "bg-blue-50 text-blue-600" : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"}
4719
+ `,
4720
+ "data-testid": "toggle-variable-panel",
4721
+ children: /* @__PURE__ */ jsx(Braces, { size: 15 })
4722
+ }
4723
+ );
4724
+ };
4725
+ const VariablePanelWithState = () => {
4726
+ const { panelOpen, setPanelOpen } = useVariablePanelState();
4727
+ return /* @__PURE__ */ jsx(VariablePanel, { open: panelOpen, onClose: () => setPanelOpen(false) });
4728
+ };
4729
+ const VariableDefinitionsSync = ({ children }) => {
4730
+ const { definitions } = useVariables();
4731
+ const extState = useExtensionState();
4732
+ useEffect(() => {
4733
+ extState.set("variableDefinitions", definitions);
4734
+ }, [definitions, extState]);
4735
+ return /* @__PURE__ */ jsx(Fragment, { children });
4736
+ };
4737
+ function variablesExtension(definitions = []) {
4738
+ const ProviderWrapper = ({ children }) => {
4739
+ return /* @__PURE__ */ jsx(VariablePanelStateProvider, { children: /* @__PURE__ */ jsx(VariableProvider, { initialDefinitions: definitions, children: /* @__PURE__ */ jsx(VariableDefinitionsSync, { children }) }) });
4740
+ };
4741
+ return {
4742
+ name: "variables",
4743
+ nodes: [VariableNode],
4744
+ bodyPlugins: [VariablePlugin],
4745
+ toolbarItems: [VariableToolbarItem, VariablePanelToggle],
4746
+ sidePanel: VariablePanelWithState,
4747
+ provider: ProviderWrapper,
4748
+ handleMethods: (ctx) => ({
4749
+ insertVariable: (key) => {
4750
+ const editor = ctx.getActiveEditor();
4751
+ if (editor) {
4752
+ editor.dispatchCommand(INSERT_VARIABLE_COMMAND, key);
4753
+ }
4754
+ },
4755
+ refreshVariables: (newDefs) => {
4756
+ ctx.setExtensionState("variableDefinitions", newDefs);
4757
+ }
4758
+ })
4759
+ };
4760
+ }
3760
4761
  export {
4762
+ $createVariableNode,
4763
+ $isVariableNode,
3761
4764
  A4_HEIGHT_MM,
3762
4765
  A4_HEIGHT_PX,
3763
4766
  A4_WIDTH_MM,
3764
4767
  A4_WIDTH_PX,
4768
+ AST_VERSION,
4769
+ DEFAULT_TRANSLATIONS,
4770
+ INSERT_VARIABLE_COMMAND,
3765
4771
  Lex4Editor,
3766
4772
  MAX_FOOTER_HEIGHT_PX,
3767
4773
  MAX_HEADER_HEIGHT_PX,
4774
+ VariableNode,
4775
+ astExtension,
4776
+ buildSavePayload,
3768
4777
  createEmptyDocument,
3769
4778
  createEmptyPage,
4779
+ serializeDocument,
4780
+ serializeDocumentJson,
3770
4781
  useHeaderFooter,
3771
4782
  useOverflowDetection,
3772
- usePagination
4783
+ usePagination,
4784
+ variablesExtension
3773
4785
  };
3774
4786
  //# sourceMappingURL=lex4-editor.js.map