@yurikilian/lex4 1.4.1 → 1.5.2

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 (44) hide show
  1. package/README.md +6 -4
  2. package/dist/ast/inline-mapper.d.ts.map +1 -1
  3. package/dist/ast/types.d.ts +1 -1
  4. package/dist/ast/types.d.ts.map +1 -1
  5. package/dist/components/BlockTypePicker.d.ts +11 -0
  6. package/dist/components/BlockTypePicker.d.ts.map +1 -0
  7. package/dist/components/CanvasControls.d.ts.map +1 -1
  8. package/dist/components/HeaderFooterToggle.d.ts +1 -0
  9. package/dist/components/HeaderFooterToggle.d.ts.map +1 -1
  10. package/dist/components/Lex4Editor.d.ts.map +1 -1
  11. package/dist/components/Toolbar.d.ts.map +1 -1
  12. package/dist/context/document-context.d.ts +1 -0
  13. package/dist/context/document-context.d.ts.map +1 -1
  14. package/dist/context/document-provider.d.ts.map +1 -1
  15. package/dist/context/toolbar-config.d.ts +18 -0
  16. package/dist/context/toolbar-config.d.ts.map +1 -0
  17. package/dist/extensions/variables-extension.d.ts.map +1 -1
  18. package/dist/i18n/defaults.d.ts.map +1 -1
  19. package/dist/i18n/pt-BR.d.ts.map +1 -1
  20. package/dist/i18n/types.d.ts +12 -0
  21. package/dist/i18n/types.d.ts.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/lex4-editor.cjs +845 -163
  25. package/dist/lex4-editor.cjs.map +1 -1
  26. package/dist/lex4-editor.js +829 -147
  27. package/dist/lex4-editor.js.map +1 -1
  28. package/dist/lexical/commands/block-commands.d.ts +5 -0
  29. package/dist/lexical/commands/block-commands.d.ts.map +1 -0
  30. package/dist/lexical/theme.d.ts.map +1 -1
  31. package/dist/lexical/utils/import-document-content.d.ts +4 -0
  32. package/dist/lexical/utils/import-document-content.d.ts.map +1 -0
  33. package/dist/style.css +156 -25
  34. package/dist/types/editor-handle.d.ts +2 -0
  35. package/dist/types/editor-handle.d.ts.map +1 -1
  36. package/dist/types/editor-props.d.ts +16 -0
  37. package/dist/types/editor-props.d.ts.map +1 -1
  38. package/dist/utils/text-style.d.ts +7 -0
  39. package/dist/utils/text-style.d.ts.map +1 -0
  40. package/dist/variables/variable-formatting.d.ts +11 -0
  41. package/dist/variables/variable-formatting.d.ts.map +1 -0
  42. package/dist/variables/variable-node.d.ts +10 -2
  43. package/dist/variables/variable-node.d.ts.map +1 -1
  44. package/package.json +1 -1
@@ -8,13 +8,15 @@ const React = require("react");
8
8
  const lexical = require("lexical");
9
9
  const LexicalComposerContext = require("@lexical/react/LexicalComposerContext");
10
10
  const list = require("@lexical/list");
11
+ const selection = require("@lexical/selection");
12
+ const richText = require("@lexical/rich-text");
13
+ const useLexicalNodeSelection = require("@lexical/react/useLexicalNodeSelection");
11
14
  const LexicalComposer = require("@lexical/react/LexicalComposer");
12
15
  const LexicalRichTextPlugin = require("@lexical/react/LexicalRichTextPlugin");
13
16
  const LexicalContentEditable = require("@lexical/react/LexicalContentEditable");
14
17
  const LexicalListPlugin = require("@lexical/react/LexicalListPlugin");
15
18
  const LexicalErrorBoundary = require("@lexical/react/LexicalErrorBoundary");
16
19
  const LexicalOnChangePlugin = require("@lexical/react/LexicalOnChangePlugin");
17
- const richText = require("@lexical/rich-text");
18
20
  function createEmptyPage(id) {
19
21
  return {
20
22
  id: id ?? crypto.randomUUID(),
@@ -473,6 +475,15 @@ const DEFAULT_TRANSLATIONS = {
473
475
  bulletList: "Bullet List",
474
476
  indent: "Indent",
475
477
  outdent: "Outdent",
478
+ history: "History",
479
+ blockType: "Block type",
480
+ paragraph: "Paragraph",
481
+ heading1: "Heading 1",
482
+ heading2: "Heading 2",
483
+ heading3: "Heading 3",
484
+ heading4: "Heading 4",
485
+ heading5: "Heading 5",
486
+ heading6: "Heading 6",
476
487
  openHistory: "Open History",
477
488
  closeHistory: "Close History"
478
489
  },
@@ -504,7 +515,9 @@ const DEFAULT_TRANSLATIONS = {
504
515
  indentedContent: "Indented content",
505
516
  outdentedContent: "Outdented content",
506
517
  fontChanged: "Font changed to {{value}}",
507
- fontSizeChanged: "Font size changed to {{value}}pt"
518
+ fontSizeChanged: "Font size changed to {{value}}pt",
519
+ blockTypeChanged: "Block type changed to {{value}}",
520
+ insertedDocumentContent: "Inserted document content"
508
521
  }
509
522
  },
510
523
  variables: {
@@ -604,6 +617,15 @@ const PT_BR_TRANSLATIONS = {
604
617
  bulletList: "Lista com Marcadores",
605
618
  indent: "Aumentar Recuo",
606
619
  outdent: "Diminuir Recuo",
620
+ history: "Histórico",
621
+ blockType: "Tipo de bloco",
622
+ paragraph: "Parágrafo",
623
+ heading1: "Título 1",
624
+ heading2: "Título 2",
625
+ heading3: "Título 3",
626
+ heading4: "Título 4",
627
+ heading5: "Título 5",
628
+ heading6: "Título 6",
607
629
  openHistory: "Abrir Histórico",
608
630
  closeHistory: "Fechar Histórico"
609
631
  },
@@ -635,7 +657,9 @@ const PT_BR_TRANSLATIONS = {
635
657
  indentedContent: "Conteúdo recuado",
636
658
  outdentedContent: "Recuo reduzido",
637
659
  fontChanged: "Fonte alterada para {{value}}",
638
- fontSizeChanged: "Tamanho da fonte alterado para {{value}}pt"
660
+ fontSizeChanged: "Tamanho da fonte alterado para {{value}}pt",
661
+ blockTypeChanged: "Tipo de bloco alterado para {{value}}",
662
+ insertedDocumentContent: "Conteúdo do documento inserido"
639
663
  }
640
664
  },
641
665
  variables: {
@@ -797,25 +821,25 @@ function captureCaretSelection(editor) {
797
821
  };
798
822
  let caretSelection = null;
799
823
  editor.getEditorState().read(() => {
800
- const selection = lexical.$createRangeSelectionFromDom(window.getSelection(), editor) ?? lexical.$getSelection();
801
- if (!lexical.$isRangeSelection(selection)) {
824
+ const selection2 = lexical.$createRangeSelectionFromDom(window.getSelection(), editor) ?? lexical.$getSelection();
825
+ if (!lexical.$isRangeSelection(selection2)) {
802
826
  return;
803
827
  }
804
828
  caretSelection = {
805
829
  anchor: {
806
- key: selection.anchor.key,
807
- offset: selection.anchor.offset,
808
- type: selection.anchor.type
830
+ key: selection2.anchor.key,
831
+ offset: selection2.anchor.offset,
832
+ type: selection2.anchor.type
809
833
  },
810
834
  focus: {
811
- key: selection.focus.key,
812
- offset: selection.focus.offset,
813
- type: selection.focus.type
835
+ key: selection2.focus.key,
836
+ offset: selection2.focus.offset,
837
+ type: selection2.focus.type
814
838
  },
815
839
  anchorTextOffset: domSelection && selectionInRoot(domSelection.anchorNode) ? getTextOffset(domSelection.anchorNode, domSelection.anchorOffset) : 0,
816
840
  focusTextOffset: domSelection && selectionInRoot(domSelection.focusNode) ? getTextOffset(domSelection.focusNode, domSelection.focusOffset) : 0,
817
- format: selection.format,
818
- style: selection.style
841
+ format: selection2.format,
842
+ style: selection2.style
819
843
  };
820
844
  });
821
845
  return caretSelection;
@@ -1286,6 +1310,7 @@ const DocumentProvider = ({
1286
1310
  activePageId,
1287
1311
  setActivePageId,
1288
1312
  activeEditor: activeEditorRef.current,
1313
+ activeCaretPosition: activeCaretPositionRef.current,
1289
1314
  consumePendingCaretPosition,
1290
1315
  consumePendingFocusAtEnd,
1291
1316
  requestFocusAtEnd,
@@ -1435,33 +1460,33 @@ const createLucideIcon = (iconName, iconNode) => {
1435
1460
  * This source code is licensed under the ISC license.
1436
1461
  * See the LICENSE file in the root directory of this source tree.
1437
1462
  */
1438
- const __iconNode$p = [
1463
+ const __iconNode$q = [
1439
1464
  ["path", { d: "m15 16 2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16", key: "xik6mr" }],
1440
1465
  ["path", { d: "M15.697 14h5.606", key: "1stdlc" }],
1441
1466
  ["path", { d: "m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16", key: "d5nyq2" }],
1442
1467
  ["path", { d: "M3.304 13h6.392", key: "1q3zxz" }]
1443
1468
  ];
1444
- const ALargeSmall = createLucideIcon("a-large-small", __iconNode$p);
1469
+ const ALargeSmall = createLucideIcon("a-large-small", __iconNode$q);
1445
1470
  /**
1446
1471
  * @license lucide-react v1.8.0 - ISC
1447
1472
  *
1448
1473
  * This source code is licensed under the ISC license.
1449
1474
  * See the LICENSE file in the root directory of this source tree.
1450
1475
  */
1451
- const __iconNode$o = [
1476
+ const __iconNode$p = [
1452
1477
  [
1453
1478
  "path",
1454
1479
  { 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" }
1455
1480
  ]
1456
1481
  ];
1457
- const Bold = createLucideIcon("bold", __iconNode$o);
1482
+ const Bold = createLucideIcon("bold", __iconNode$p);
1458
1483
  /**
1459
1484
  * @license lucide-react v1.8.0 - ISC
1460
1485
  *
1461
1486
  * This source code is licensed under the ISC license.
1462
1487
  * See the LICENSE file in the root directory of this source tree.
1463
1488
  */
1464
- const __iconNode$n = [
1489
+ const __iconNode$o = [
1465
1490
  [
1466
1491
  "path",
1467
1492
  { 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" }
@@ -1474,7 +1499,15 @@ const __iconNode$n = [
1474
1499
  }
1475
1500
  ]
1476
1501
  ];
1477
- const Braces = createLucideIcon("braces", __iconNode$n);
1502
+ const Braces = createLucideIcon("braces", __iconNode$o);
1503
+ /**
1504
+ * @license lucide-react v1.8.0 - ISC
1505
+ *
1506
+ * This source code is licensed under the ISC license.
1507
+ * See the LICENSE file in the root directory of this source tree.
1508
+ */
1509
+ const __iconNode$n = [["path", { d: "m6 9 6 6 6-6", key: "qrunsl" }]];
1510
+ const ChevronDown = createLucideIcon("chevron-down", __iconNode$n);
1478
1511
  /**
1479
1512
  * @license lucide-react v1.8.0 - ISC
1480
1513
  *
@@ -1892,6 +1925,36 @@ const HistorySidebar = () => {
1892
1925
  }
1893
1926
  );
1894
1927
  };
1928
+ const DEFAULT_TOOLBAR_CONTROL_CONFIG = {
1929
+ visible: true,
1930
+ showLabel: true
1931
+ };
1932
+ const DEFAULT_TOOLBAR_CONFIG = {
1933
+ history: DEFAULT_TOOLBAR_CONTROL_CONFIG,
1934
+ variables: DEFAULT_TOOLBAR_CONTROL_CONFIG,
1935
+ headerFooter: DEFAULT_TOOLBAR_CONTROL_CONFIG
1936
+ };
1937
+ const ToolbarConfigContext = React.createContext(DEFAULT_TOOLBAR_CONFIG);
1938
+ function resolveControlConfig(config) {
1939
+ return {
1940
+ visible: (config == null ? void 0 : config.visible) ?? true,
1941
+ showLabel: (config == null ? void 0 : config.showLabel) ?? true
1942
+ };
1943
+ }
1944
+ function normalizeToolbarConfig(config) {
1945
+ return {
1946
+ history: resolveControlConfig(config == null ? void 0 : config.history),
1947
+ variables: resolveControlConfig(config == null ? void 0 : config.variables),
1948
+ headerFooter: resolveControlConfig(config == null ? void 0 : config.headerFooter)
1949
+ };
1950
+ }
1951
+ const ToolbarConfigProvider = ({ toolbar, children }) => {
1952
+ const resolvedConfig = React.useMemo(() => normalizeToolbarConfig(toolbar), [toolbar]);
1953
+ return /* @__PURE__ */ jsxRuntime.jsx(ToolbarConfigContext.Provider, { value: resolvedConfig, children });
1954
+ };
1955
+ function useToolbarConfig() {
1956
+ return React.useContext(ToolbarConfigContext);
1957
+ }
1895
1958
  function resolveExtensions(extensions) {
1896
1959
  const resolved = {
1897
1960
  nodes: [],
@@ -1997,9 +2060,9 @@ const SUPPORTED_FONTS = [
1997
2060
  ];
1998
2061
  function applyFontFamily(editor, fontFamily) {
1999
2062
  editor.update(() => {
2000
- const selection = lexical.$getSelection();
2001
- if (!lexical.$isRangeSelection(selection)) return;
2002
- const nodes = selection.getNodes();
2063
+ const selection2 = lexical.$getSelection();
2064
+ if (!lexical.$isRangeSelection(selection2)) return;
2065
+ const nodes = selection2.getNodes();
2003
2066
  for (const node of nodes) {
2004
2067
  if (lexical.$isTextNode(node)) {
2005
2068
  node.setStyle(`font-family: ${fontFamily}`);
@@ -2024,11 +2087,12 @@ const SUPPORTED_FONT_SIZES = [
2024
2087
  48,
2025
2088
  72
2026
2089
  ];
2090
+ const DEFAULT_FONT_SIZE = 12;
2027
2091
  function applyFontSize(editor, size) {
2028
2092
  editor.update(() => {
2029
- const selection = lexical.$getSelection();
2030
- if (!lexical.$isRangeSelection(selection)) return;
2031
- const nodes = selection.getNodes();
2093
+ const selection2 = lexical.$getSelection();
2094
+ if (!lexical.$isRangeSelection(selection2)) return;
2095
+ const nodes = selection2.getNodes();
2032
2096
  for (const node of nodes) {
2033
2097
  if (lexical.$isTextNode(node)) {
2034
2098
  const existing = node.getStyle();
@@ -2074,9 +2138,495 @@ function indentContent(editor) {
2074
2138
  function outdentContent(editor) {
2075
2139
  editor.dispatchCommand(lexical.OUTDENT_CONTENT_COMMAND, void 0);
2076
2140
  }
2141
+ function setBlockType(editor, blockType) {
2142
+ editor.update(() => {
2143
+ const selection$1 = lexical.$getSelection();
2144
+ if (!lexical.$isRangeSelection(selection$1)) {
2145
+ return;
2146
+ }
2147
+ if (blockType === "paragraph") {
2148
+ selection.$setBlocksType(selection$1, () => lexical.$createParagraphNode());
2149
+ return;
2150
+ }
2151
+ selection.$setBlocksType(selection$1, () => richText.$createHeadingNode(blockType));
2152
+ });
2153
+ }
2154
+ function getActiveBlockType(editor) {
2155
+ let blockType = "paragraph";
2156
+ editor.getEditorState().read(() => {
2157
+ const selection2 = lexical.$getSelection();
2158
+ if (!lexical.$isRangeSelection(selection2)) {
2159
+ return;
2160
+ }
2161
+ const anchorNode = selection2.anchor.getNode();
2162
+ const topLevelElement = anchorNode.getTopLevelElementOrThrow();
2163
+ if (richText.$isHeadingNode(topLevelElement)) {
2164
+ blockType = topLevelElement.getTag();
2165
+ }
2166
+ });
2167
+ return blockType;
2168
+ }
2169
+ function extractStyleValue(style, property) {
2170
+ const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2171
+ const match = style.match(new RegExp(`${escapedProperty}:\\s*([^;]+)`));
2172
+ return match ? match[1].trim().replace(/['"]/g, "") : void 0;
2173
+ }
2174
+ function mergeStyleDeclaration(existingStyle, property, value) {
2175
+ const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2176
+ const stripped = existingStyle.replace(
2177
+ new RegExp(`${escapedProperty}:\\s*[^;]+;?\\s*`, "g"),
2178
+ ""
2179
+ ).trim();
2180
+ const declaration = `${property}: ${value}`;
2181
+ return stripped ? `${stripped}; ${declaration}` : declaration;
2182
+ }
2183
+ function extractFontFamilyFromStyle(style) {
2184
+ return extractStyleValue(style, "font-family");
2185
+ }
2186
+ function extractFontSizePtFromStyle(style) {
2187
+ const value = extractStyleValue(style, "font-size");
2188
+ if (!value) {
2189
+ return void 0;
2190
+ }
2191
+ const match = value.match(/^(\d+(?:\.\d+)?)\s*pt$/);
2192
+ return match ? parseFloat(match[1]) : void 0;
2193
+ }
2194
+ function mergeFontFamilyStyle(existingStyle, fontFamily) {
2195
+ return mergeStyleDeclaration(existingStyle, "font-family", fontFamily);
2196
+ }
2197
+ function mergeFontSizeStyle(existingStyle, size) {
2198
+ return mergeStyleDeclaration(existingStyle, "font-size", `${size}pt`);
2199
+ }
2200
+ const EMPTY_CONTEXT = {
2201
+ definitions: [],
2202
+ refreshDefinitions: () => {
2203
+ },
2204
+ getDefinition: () => void 0
2205
+ };
2206
+ const VariableContext = React.createContext(EMPTY_CONTEXT);
2207
+ const VariableProvider = ({
2208
+ initialDefinitions = [],
2209
+ children
2210
+ }) => {
2211
+ const [definitions, setDefinitions] = React.useState(initialDefinitions);
2212
+ const refresh = React.useCallback((newDefinitions) => {
2213
+ setDefinitions(newDefinitions);
2214
+ }, []);
2215
+ const getDefinition = React.useCallback(
2216
+ (key) => {
2217
+ return definitions.find((d) => d.key === key);
2218
+ },
2219
+ [definitions]
2220
+ );
2221
+ const value = React.useMemo(
2222
+ () => ({ definitions, refreshDefinitions: refresh, getDefinition }),
2223
+ [definitions, refresh, getDefinition]
2224
+ );
2225
+ return /* @__PURE__ */ jsxRuntime.jsx(VariableContext.Provider, { value, children });
2226
+ };
2227
+ function useVariables() {
2228
+ return React.useContext(VariableContext);
2229
+ }
2230
+ class VariableNode extends lexical.DecoratorNode {
2231
+ constructor(variableKey, format = 0, style = "", key) {
2232
+ super(key);
2233
+ __publicField(this, "__variableKey");
2234
+ __publicField(this, "__format");
2235
+ __publicField(this, "__style");
2236
+ this.__variableKey = variableKey;
2237
+ this.__format = format;
2238
+ this.__style = style;
2239
+ }
2240
+ static getType() {
2241
+ return "variable-node";
2242
+ }
2243
+ static clone(node) {
2244
+ return new VariableNode(node.__variableKey, node.__format, node.__style, node.__key);
2245
+ }
2246
+ getVariableKey() {
2247
+ return this.getLatest().__variableKey;
2248
+ }
2249
+ getFormat() {
2250
+ return this.getLatest().__format;
2251
+ }
2252
+ setFormat(format) {
2253
+ const writable = this.getWritable();
2254
+ writable.__format = format;
2255
+ return writable;
2256
+ }
2257
+ getStyle() {
2258
+ return this.getLatest().__style;
2259
+ }
2260
+ setStyle(style) {
2261
+ const writable = this.getWritable();
2262
+ writable.__style = style;
2263
+ return writable;
2264
+ }
2265
+ // -- Serialization --
2266
+ static importJSON(serializedNode) {
2267
+ return $createVariableNode(
2268
+ serializedNode.variableKey,
2269
+ serializedNode.format ?? 0,
2270
+ serializedNode.style ?? ""
2271
+ );
2272
+ }
2273
+ exportJSON() {
2274
+ return {
2275
+ type: "variable-node",
2276
+ version: 1,
2277
+ variableKey: this.__variableKey,
2278
+ format: this.__format,
2279
+ style: this.__style
2280
+ };
2281
+ }
2282
+ // -- DOM --
2283
+ createDOM() {
2284
+ const span = document.createElement("span");
2285
+ span.className = "lex4-variable";
2286
+ span.setAttribute("data-variable-key", this.__variableKey);
2287
+ span.setAttribute("data-testid", `variable-${this.__variableKey}`);
2288
+ span.contentEditable = "false";
2289
+ return span;
2290
+ }
2291
+ updateDOM() {
2292
+ return false;
2293
+ }
2294
+ exportDOM() {
2295
+ const span = document.createElement("span");
2296
+ span.setAttribute("data-variable-key", this.__variableKey);
2297
+ span.textContent = `{{${this.__variableKey}}}`;
2298
+ return { element: span };
2299
+ }
2300
+ static importDOM() {
2301
+ return null;
2302
+ }
2303
+ // -- Behavior --
2304
+ isInline() {
2305
+ return true;
2306
+ }
2307
+ isKeyboardSelectable() {
2308
+ return true;
2309
+ }
2310
+ getTextContent() {
2311
+ return `{{${this.__variableKey}}}`;
2312
+ }
2313
+ // -- Rendering --
2314
+ decorate() {
2315
+ return /* @__PURE__ */ jsxRuntime.jsx(
2316
+ VariableChip,
2317
+ {
2318
+ nodeKey: this.__key,
2319
+ variableKey: this.__variableKey,
2320
+ format: this.__format,
2321
+ styleValue: this.__style
2322
+ }
2323
+ );
2324
+ }
2325
+ }
2326
+ function VariableChip({
2327
+ nodeKey,
2328
+ variableKey,
2329
+ format,
2330
+ styleValue
2331
+ }) {
2332
+ const { getDefinition } = useVariables();
2333
+ const [editor] = LexicalComposerContext.useLexicalComposerContext();
2334
+ const [isSelected, setSelected, clearOtherSelections] = useLexicalNodeSelection.useLexicalNodeSelection(nodeKey);
2335
+ const def = getDefinition(variableKey);
2336
+ const label = (def == null ? void 0 : def.label) ?? variableKey;
2337
+ const group = def == null ? void 0 : def.group;
2338
+ const style = React.useMemo(() => {
2339
+ const fontFamily = extractFontFamilyFromStyle(styleValue);
2340
+ const fontSize = extractFontSizePtFromStyle(styleValue);
2341
+ return {
2342
+ ...fontFamily ? { fontFamily } : {},
2343
+ ...fontSize ? { fontSize: `${fontSize}pt` } : {}
2344
+ };
2345
+ }, [styleValue]);
2346
+ const className = [
2347
+ "lex4-variable-chip",
2348
+ isSelected && "lex4-variable-chip-selected",
2349
+ format & 1 ? "lex4-text-bold" : "",
2350
+ format & 2 ? "lex4-text-italic" : "",
2351
+ format & 8 ? "lex4-text-underline" : "",
2352
+ format & 4 ? "lex4-text-strikethrough" : ""
2353
+ ].filter(Boolean).join(" ");
2354
+ const handleClick = React.useCallback((event) => {
2355
+ event.preventDefault();
2356
+ if (!event.shiftKey) {
2357
+ clearOtherSelections();
2358
+ }
2359
+ setSelected(!isSelected);
2360
+ }, [clearOtherSelections, isSelected, setSelected]);
2361
+ React.useEffect(() => {
2362
+ const removeSelectedNodes = () => {
2363
+ editor.update(() => {
2364
+ const selection2 = lexical.$getSelection();
2365
+ if (!lexical.$isNodeSelection(selection2)) {
2366
+ const node = lexical.$getNodeByKey(nodeKey);
2367
+ if ($isVariableNode(node)) {
2368
+ node.remove();
2369
+ }
2370
+ return;
2371
+ }
2372
+ for (const node of selection2.getNodes()) {
2373
+ if ($isVariableNode(node)) {
2374
+ node.remove();
2375
+ }
2376
+ }
2377
+ });
2378
+ };
2379
+ const unregisterBackspace = editor.registerCommand(
2380
+ lexical.KEY_BACKSPACE_COMMAND,
2381
+ () => {
2382
+ if (!isSelected) {
2383
+ return false;
2384
+ }
2385
+ removeSelectedNodes();
2386
+ return true;
2387
+ },
2388
+ lexical.COMMAND_PRIORITY_LOW
2389
+ );
2390
+ const unregisterDelete = editor.registerCommand(
2391
+ lexical.KEY_DELETE_COMMAND,
2392
+ () => {
2393
+ if (!isSelected) {
2394
+ return false;
2395
+ }
2396
+ removeSelectedNodes();
2397
+ return true;
2398
+ },
2399
+ lexical.COMMAND_PRIORITY_LOW
2400
+ );
2401
+ return () => {
2402
+ unregisterBackspace();
2403
+ unregisterDelete();
2404
+ };
2405
+ }, [editor, isSelected, nodeKey]);
2406
+ return /* @__PURE__ */ jsxRuntime.jsx(
2407
+ "span",
2408
+ {
2409
+ className,
2410
+ "data-testid": `variable-chip-${variableKey}`,
2411
+ "data-variable-group": group,
2412
+ title: variableKey,
2413
+ style,
2414
+ onMouseDown: (event) => event.preventDefault(),
2415
+ onClick: handleClick,
2416
+ children: label
2417
+ }
2418
+ );
2419
+ }
2420
+ function $createVariableNode(variableKey, format = 0, style = "") {
2421
+ return lexical.$applyNodeReplacement(new VariableNode(variableKey, format, style));
2422
+ }
2423
+ function $isVariableNode(node) {
2424
+ return node instanceof VariableNode;
2425
+ }
2426
+ const FORMAT_MASKS = {
2427
+ bold: 1,
2428
+ italic: 2,
2429
+ strikethrough: 4,
2430
+ underline: 8
2431
+ };
2432
+ function withSelectedVariableNodes(editor, updater) {
2433
+ let updated = false;
2434
+ editor.update(() => {
2435
+ const selection2 = lexical.$getSelection();
2436
+ if (!lexical.$isNodeSelection(selection2)) {
2437
+ return;
2438
+ }
2439
+ const nodes = selection2.getNodes().filter($isVariableNode);
2440
+ if (nodes.length === 0) {
2441
+ return;
2442
+ }
2443
+ updater(nodes);
2444
+ updated = true;
2445
+ });
2446
+ return updated;
2447
+ }
2448
+ function getSelectedVariableNodes(editor) {
2449
+ let nodes = [];
2450
+ editor.getEditorState().read(() => {
2451
+ const selection2 = lexical.$getSelection();
2452
+ if (!lexical.$isNodeSelection(selection2)) {
2453
+ return;
2454
+ }
2455
+ nodes = selection2.getNodes().filter($isVariableNode);
2456
+ });
2457
+ return nodes;
2458
+ }
2459
+ function toggleSelectedVariableFormat(editor, format) {
2460
+ const mask = FORMAT_MASKS[format];
2461
+ if (!mask) {
2462
+ return false;
2463
+ }
2464
+ return withSelectedVariableNodes(editor, (nodes) => {
2465
+ const shouldEnable = nodes.some((node) => (node.getFormat() & mask) === 0);
2466
+ for (const node of nodes) {
2467
+ const nextFormat = shouldEnable ? node.getFormat() | mask : node.getFormat() & ~mask;
2468
+ node.setFormat(nextFormat);
2469
+ }
2470
+ });
2471
+ }
2472
+ function applyFontFamilyToSelectedVariables(editor, fontFamily) {
2473
+ return withSelectedVariableNodes(editor, (nodes) => {
2474
+ for (const node of nodes) {
2475
+ node.setStyle(mergeFontFamilyStyle(node.getStyle(), fontFamily));
2476
+ }
2477
+ });
2478
+ }
2479
+ function applyFontSizeToSelectedVariables(editor, size) {
2480
+ return withSelectedVariableNodes(editor, (nodes) => {
2481
+ for (const node of nodes) {
2482
+ node.setStyle(mergeFontSizeStyle(node.getStyle(), size));
2483
+ }
2484
+ });
2485
+ }
2486
+ function readSelectedVariableFormatting(editor) {
2487
+ const firstNode = getSelectedVariableNodes(editor)[0];
2488
+ if (!firstNode) {
2489
+ return {};
2490
+ }
2491
+ const style = firstNode.getStyle();
2492
+ return {
2493
+ fontFamily: extractFontFamilyFromStyle(style),
2494
+ fontSize: extractFontSizePtFromStyle(style)
2495
+ };
2496
+ }
2497
+ const BLOCK_TYPE_OPTIONS = [
2498
+ { value: "paragraph", shortLabel: "P" },
2499
+ { value: "h1", shortLabel: "H1" },
2500
+ { value: "h2", shortLabel: "H2" },
2501
+ { value: "h3", shortLabel: "H3" },
2502
+ { value: "h4", shortLabel: "H4" },
2503
+ { value: "h5", shortLabel: "H5" },
2504
+ { value: "h6", shortLabel: "H6" }
2505
+ ];
2506
+ function getBlockTypeLabel(t, blockType) {
2507
+ switch (blockType) {
2508
+ case "h1":
2509
+ return t.toolbar.heading1;
2510
+ case "h2":
2511
+ return t.toolbar.heading2;
2512
+ case "h3":
2513
+ return t.toolbar.heading3;
2514
+ case "h4":
2515
+ return t.toolbar.heading4;
2516
+ case "h5":
2517
+ return t.toolbar.heading5;
2518
+ case "h6":
2519
+ return t.toolbar.heading6;
2520
+ default:
2521
+ return t.toolbar.paragraph;
2522
+ }
2523
+ }
2524
+ function getBlockTypeShortLabel(blockType) {
2525
+ var _a;
2526
+ return ((_a = BLOCK_TYPE_OPTIONS.find((option) => option.value === blockType)) == null ? void 0 : _a.shortLabel) ?? "P";
2527
+ }
2528
+ const BlockTypePicker = ({ value, onChange }) => {
2529
+ const t = useTranslations();
2530
+ const [open, setOpen] = React.useState(false);
2531
+ const containerRef = React.useRef(null);
2532
+ React.useEffect(() => {
2533
+ if (!open) {
2534
+ return;
2535
+ }
2536
+ const handleClickOutside = (event) => {
2537
+ if (containerRef.current && !containerRef.current.contains(event.target)) {
2538
+ setOpen(false);
2539
+ }
2540
+ };
2541
+ const handleEscape = (event) => {
2542
+ if (event.key === "Escape") {
2543
+ setOpen(false);
2544
+ }
2545
+ };
2546
+ document.addEventListener("mousedown", handleClickOutside);
2547
+ document.addEventListener("keydown", handleEscape);
2548
+ return () => {
2549
+ document.removeEventListener("mousedown", handleClickOutside);
2550
+ document.removeEventListener("keydown", handleEscape);
2551
+ };
2552
+ }, [open]);
2553
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2554
+ "div",
2555
+ {
2556
+ ref: containerRef,
2557
+ className: "lex4-block-type-picker",
2558
+ "data-testid": "block-type-picker",
2559
+ children: [
2560
+ /* @__PURE__ */ jsxRuntime.jsxs(
2561
+ "button",
2562
+ {
2563
+ type: "button",
2564
+ className: `lex4-block-type-trigger${open ? " open" : ""}`,
2565
+ "data-testid": "block-type-selector",
2566
+ "aria-label": t.toolbar.blockType,
2567
+ "aria-expanded": open,
2568
+ "aria-haspopup": "menu",
2569
+ title: getBlockTypeLabel(t, value),
2570
+ onMouseDown: (event) => event.preventDefault(),
2571
+ onClick: () => setOpen((current) => !current),
2572
+ children: [
2573
+ /* @__PURE__ */ jsxRuntime.jsx(
2574
+ "span",
2575
+ {
2576
+ className: `lex4-block-type-trigger-code${value === "paragraph" ? "" : " heading"}`,
2577
+ children: getBlockTypeShortLabel(value)
2578
+ }
2579
+ ),
2580
+ /* @__PURE__ */ jsxRuntime.jsx(ChevronDown, { size: 14, className: `lex4-block-type-trigger-chevron${open ? " open" : ""}` })
2581
+ ]
2582
+ }
2583
+ ),
2584
+ open && /* @__PURE__ */ jsxRuntime.jsx(
2585
+ "div",
2586
+ {
2587
+ className: "lex4-block-type-menu",
2588
+ role: "menu",
2589
+ "data-testid": "block-type-menu",
2590
+ children: BLOCK_TYPE_OPTIONS.map((option) => {
2591
+ const label = getBlockTypeLabel(t, option.value);
2592
+ const active = option.value === value;
2593
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2594
+ "button",
2595
+ {
2596
+ type: "button",
2597
+ role: "menuitemradio",
2598
+ "aria-checked": active,
2599
+ className: `lex4-block-type-item${active ? " active" : ""}`,
2600
+ "data-testid": `block-type-option-${option.value}`,
2601
+ onMouseDown: (event) => event.preventDefault(),
2602
+ onClick: () => {
2603
+ onChange(option.value);
2604
+ setOpen(false);
2605
+ },
2606
+ children: [
2607
+ /* @__PURE__ */ jsxRuntime.jsx(
2608
+ "span",
2609
+ {
2610
+ className: `lex4-block-type-item-code${option.value === "paragraph" ? "" : " heading"}${active ? " active" : ""}`,
2611
+ children: option.shortLabel
2612
+ }
2613
+ ),
2614
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lex4-block-type-item-label", children: label })
2615
+ ]
2616
+ },
2617
+ option.value
2618
+ );
2619
+ })
2620
+ }
2621
+ )
2622
+ ]
2623
+ }
2624
+ );
2625
+ };
2077
2626
  const HeaderFooterToggle = ({
2078
2627
  enabled,
2079
- onToggle
2628
+ onToggle,
2629
+ showLabel = true
2080
2630
  }) => {
2081
2631
  const t = useTranslations();
2082
2632
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2086,18 +2636,20 @@ const HeaderFooterToggle = ({
2086
2636
  "data-testid": "header-footer-toggle",
2087
2637
  children: [
2088
2638
  /* @__PURE__ */ jsxRuntime.jsx(FileText, { size: 14, className: "lex4-hf-toggle-icon" }),
2089
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lex4-hf-toggle-label", children: t.headerFooter.label }),
2639
+ showLabel && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lex4-hf-toggle-label", children: t.headerFooter.label }),
2090
2640
  /* @__PURE__ */ jsxRuntime.jsx(
2091
2641
  "button",
2092
2642
  {
2093
2643
  type: "button",
2094
2644
  role: "switch",
2095
2645
  "aria-checked": enabled,
2646
+ "aria-label": t.headerFooter.label,
2096
2647
  onMouseDown: (e) => e.preventDefault(),
2097
2648
  onClick: () => onToggle(!enabled),
2098
2649
  className: "lex4-hf-switch",
2099
2650
  style: { backgroundColor: enabled ? "var(--color-primary)" : "var(--color-muted)" },
2100
2651
  "data-testid": "header-footer-switch",
2652
+ title: t.headerFooter.label,
2101
2653
  children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lex4-hf-switch-knob" })
2102
2654
  }
2103
2655
  )
@@ -2261,6 +2813,7 @@ const MenuItem = ({ icon, label, onClick, disabled, testId }) => /* @__PURE__ */
2261
2813
  const MenuDivider = () => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lex4-settings-divider" });
2262
2814
  const CanvasControls = () => {
2263
2815
  const { document: document2, dispatch, activePageId, runHistoryAction } = useDocument();
2816
+ const toolbarConfig = useToolbarConfig();
2264
2817
  const t = useTranslations();
2265
2818
  const runCanvasAction = React.useCallback((label, callback) => {
2266
2819
  runHistoryAction(
@@ -2327,12 +2880,16 @@ const CanvasControls = () => {
2327
2880
  dispatch({ type: "SET_PAGE_COUNTER_MODE", mode });
2328
2881
  });
2329
2882
  }, [dispatch, runCanvasAction, t.history.actions.pageCounterSet]);
2883
+ if (!toolbarConfig.headerFooter.visible) {
2884
+ return null;
2885
+ }
2330
2886
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lex4-toolbar-group lex4-toolbar-group-gap", "data-testid": "header-footer-controls", children: [
2331
2887
  /* @__PURE__ */ jsxRuntime.jsx(
2332
2888
  HeaderFooterToggle,
2333
2889
  {
2334
2890
  enabled: document2.headerFooterEnabled,
2335
- onToggle: handleToggle
2891
+ onToggle: handleToggle,
2892
+ showLabel: toolbarConfig.headerFooter.showLabel
2336
2893
  }
2337
2894
  ),
2338
2895
  document2.headerFooterEnabled && /* @__PURE__ */ jsxRuntime.jsx(
@@ -2365,7 +2922,17 @@ const Toolbar = () => {
2365
2922
  undo
2366
2923
  } = useDocument();
2367
2924
  const { toolbarItems, toolbarEndItems } = useExtensions();
2925
+ const toolbarConfig = useToolbarConfig();
2368
2926
  const t = useTranslations();
2927
+ const [activeBlockType, setActiveBlockType] = React.useState("paragraph");
2928
+ const [activeFontFamily, setActiveFontFamily] = React.useState("Calibri");
2929
+ const [activeFontSize, setActiveFontSize] = React.useState(DEFAULT_FONT_SIZE);
2930
+ const normalizeFontFamily = React.useCallback((fontFamily) => {
2931
+ if (fontFamily && SUPPORTED_FONTS.includes(fontFamily)) {
2932
+ return fontFamily;
2933
+ }
2934
+ return "Calibri";
2935
+ }, []);
2369
2936
  const withBodySelection = React.useCallback(
2370
2937
  (editor, action) => {
2371
2938
  editor.update(() => {
@@ -2401,27 +2968,91 @@ const Toolbar = () => {
2401
2968
  },
2402
2969
  [runHistoryAction]
2403
2970
  );
2971
+ React.useEffect(() => {
2972
+ if (!activeEditor) {
2973
+ setActiveBlockType("paragraph");
2974
+ setActiveFontFamily("Calibri");
2975
+ setActiveFontSize(DEFAULT_FONT_SIZE);
2976
+ return;
2977
+ }
2978
+ const updateSelectionState = () => {
2979
+ const selectedVariables = getSelectedVariableNodes(activeEditor);
2980
+ if (selectedVariables.length > 0) {
2981
+ const formatting = readSelectedVariableFormatting(activeEditor);
2982
+ setActiveBlockType("paragraph");
2983
+ setActiveFontFamily(normalizeFontFamily(formatting.fontFamily));
2984
+ setActiveFontSize(formatting.fontSize ?? DEFAULT_FONT_SIZE);
2985
+ return;
2986
+ }
2987
+ setActiveBlockType(getActiveBlockType(activeEditor));
2988
+ let nextFontFamily = "Calibri";
2989
+ let nextFontSize = DEFAULT_FONT_SIZE;
2990
+ activeEditor.getEditorState().read(() => {
2991
+ const selection2 = lexical.$getSelection();
2992
+ if (!lexical.$isRangeSelection(selection2)) {
2993
+ return;
2994
+ }
2995
+ const textNode = selection2.getNodes().find(lexical.$isTextNode);
2996
+ if (!textNode) {
2997
+ return;
2998
+ }
2999
+ const style = textNode.getStyle();
3000
+ nextFontFamily = normalizeFontFamily(extractFontFamilyFromStyle(style));
3001
+ nextFontSize = extractFontSizePtFromStyle(style) ?? DEFAULT_FONT_SIZE;
3002
+ });
3003
+ setActiveFontFamily(nextFontFamily);
3004
+ setActiveFontSize(nextFontSize);
3005
+ };
3006
+ updateSelectionState();
3007
+ const unregisterSelectionChange = activeEditor.registerCommand(
3008
+ lexical.SELECTION_CHANGE_COMMAND,
3009
+ () => {
3010
+ updateSelectionState();
3011
+ return false;
3012
+ },
3013
+ lexical.COMMAND_PRIORITY_LOW
3014
+ );
3015
+ const unregisterUpdateListener = activeEditor.registerUpdateListener(() => {
3016
+ updateSelectionState();
3017
+ });
3018
+ return () => {
3019
+ unregisterSelectionChange();
3020
+ unregisterUpdateListener();
3021
+ };
3022
+ }, [activeEditor, normalizeFontFamily]);
2404
3023
  const handleBold = React.useCallback(() => {
2405
3024
  debug("toolbar", `bold (globalSelection=${globalSelectionActive}, editors=${editorRegistry.all().length}, hasEditor=${!!activeEditor})`);
2406
3025
  runToolbarAction(t.history.actions.boldApplied, () => {
3026
+ if (activeEditor && toggleSelectedVariableFormat(activeEditor, "bold")) {
3027
+ return;
3028
+ }
2407
3029
  applyToBodyEditors(toggleBold);
2408
3030
  });
2409
3031
  }, [activeEditor, applyToBodyEditors, editorRegistry, globalSelectionActive, runToolbarAction, t.history.actions.boldApplied]);
2410
3032
  const handleItalic = React.useCallback(() => {
2411
3033
  debug("toolbar", `italic (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
2412
3034
  runToolbarAction(t.history.actions.italicApplied, () => {
3035
+ if (activeEditor && toggleSelectedVariableFormat(activeEditor, "italic")) {
3036
+ return;
3037
+ }
2413
3038
  applyToBodyEditors(toggleItalic);
2414
3039
  });
2415
3040
  }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction, t.history.actions.italicApplied]);
2416
3041
  const handleUnderline = React.useCallback(() => {
2417
3042
  debug("toolbar", `underline (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
2418
3043
  runToolbarAction(t.history.actions.underlineApplied, () => {
3044
+ if (activeEditor && toggleSelectedVariableFormat(activeEditor, "underline")) {
3045
+ return;
3046
+ }
2419
3047
  applyToBodyEditors(toggleUnderline);
2420
3048
  });
2421
3049
  }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction, t.history.actions.underlineApplied]);
2422
3050
  const handleStrikethrough = React.useCallback(() => {
2423
3051
  debug("toolbar", `strikethrough (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
2424
3052
  runToolbarAction(t.history.actions.strikethroughApplied, () => {
3053
+ if (activeEditor && toggleSelectedVariableFormat(activeEditor, "strikethrough")) {
3054
+ return;
3055
+ }
2425
3056
  applyToBodyEditors(toggleStrikethrough);
2426
3057
  });
2427
3058
  }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction, t.history.actions.strikethroughApplied]);
@@ -2467,20 +3098,43 @@ const Toolbar = () => {
2467
3098
  }, [applyToBodyEditors, runToolbarAction, t.history.actions.outdentedContent]);
2468
3099
  const handleFontChange = React.useCallback(
2469
3100
  (e) => {
2470
- runToolbarAction(interpolate(t.history.actions.fontChanged, { value: e.target.value }), () => {
2471
- applyToBodyEditors((editor) => applyFontFamily(editor, e.target.value));
3101
+ const fontFamily = e.target.value;
3102
+ runToolbarAction(interpolate(t.history.actions.fontChanged, { value: fontFamily }), () => {
3103
+ if (activeEditor && applyFontFamilyToSelectedVariables(activeEditor, fontFamily)) {
3104
+ return;
3105
+ }
3106
+ applyToBodyEditors((editor) => applyFontFamily(editor, fontFamily));
2472
3107
  });
2473
3108
  },
2474
- [applyToBodyEditors, runToolbarAction, t.history.actions.fontChanged]
3109
+ [activeEditor, applyToBodyEditors, runToolbarAction, t.history.actions.fontChanged]
2475
3110
  );
2476
3111
  const handleFontSizeChange = React.useCallback(
2477
3112
  (e) => {
2478
3113
  const size = parseInt(e.target.value, 10);
2479
3114
  runToolbarAction(interpolate(t.history.actions.fontSizeChanged, { value: String(size) }), () => {
3115
+ if (activeEditor && applyFontSizeToSelectedVariables(activeEditor, size)) {
3116
+ return;
3117
+ }
2480
3118
  applyToBodyEditors((editor) => applyFontSize(editor, size));
2481
3119
  });
2482
3120
  },
2483
- [applyToBodyEditors, runToolbarAction, t.history.actions.fontSizeChanged]
3121
+ [activeEditor, applyToBodyEditors, runToolbarAction, t.history.actions.fontSizeChanged]
3122
+ );
3123
+ const handleBlockTypeChange = React.useCallback(
3124
+ (blockType) => {
3125
+ runToolbarAction(
3126
+ interpolate(t.history.actions.blockTypeChanged, { value: getBlockTypeLabel(t, blockType) }),
3127
+ () => {
3128
+ applyToBodyEditors((editor) => setBlockType(editor, blockType));
3129
+ }
3130
+ );
3131
+ },
3132
+ [
3133
+ applyToBodyEditors,
3134
+ runToolbarAction,
3135
+ t.history.actions.blockTypeChanged,
3136
+ t
3137
+ ]
2484
3138
  );
2485
3139
  const handleToggleHistory = React.useCallback(() => {
2486
3140
  setHistorySidebarOpen(!historySidebarOpen);
@@ -2514,6 +3168,14 @@ const Toolbar = () => {
2514
3168
  )
2515
3169
  ] }),
2516
3170
  /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
3171
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lex4-toolbar-group", children: /* @__PURE__ */ jsxRuntime.jsx(
3172
+ BlockTypePicker,
3173
+ {
3174
+ value: activeBlockType,
3175
+ onChange: handleBlockTypeChange
3176
+ }
3177
+ ) }),
3178
+ /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
2517
3179
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lex4-toolbar-group-gap", children: [
2518
3180
  /* @__PURE__ */ jsxRuntime.jsx(Type, { size: 14, className: "lex4-toolbar-icon" }),
2519
3181
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2521,7 +3183,7 @@ const Toolbar = () => {
2521
3183
  {
2522
3184
  className: "lex4-toolbar-select",
2523
3185
  "data-testid": "font-selector",
2524
- defaultValue: "Calibri",
3186
+ value: activeFontFamily,
2525
3187
  onChange: handleFontChange,
2526
3188
  children: SUPPORTED_FONTS.map((font) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: font, style: { fontFamily: font }, children: font }, font))
2527
3189
  }
@@ -2534,7 +3196,7 @@ const Toolbar = () => {
2534
3196
  {
2535
3197
  className: "lex4-toolbar-select lex4-toolbar-select-narrow",
2536
3198
  "data-testid": "font-size-selector",
2537
- defaultValue: "12",
3199
+ value: String(activeFontSize),
2538
3200
  onChange: handleFontSizeChange,
2539
3201
  children: SUPPORTED_FONT_SIZES.map((size) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: size, children: size }, size))
2540
3202
  }
@@ -2569,7 +3231,7 @@ const Toolbar = () => {
2569
3231
  /* @__PURE__ */ jsxRuntime.jsx(CanvasControls, {}),
2570
3232
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lex4-toolbar-end", children: [
2571
3233
  toolbarEndItems.map((EndItem, idx) => /* @__PURE__ */ jsxRuntime.jsx(EndItem, {}, idx)),
2572
- /* @__PURE__ */ jsxRuntime.jsxs(
3234
+ toolbarConfig.history.visible && /* @__PURE__ */ jsxRuntime.jsxs(
2573
3235
  "button",
2574
3236
  {
2575
3237
  type: "button",
@@ -2581,7 +3243,7 @@ const Toolbar = () => {
2581
3243
  "aria-label": historySidebarOpen ? t.toolbar.closeHistory : t.toolbar.openHistory,
2582
3244
  children: [
2583
3245
  /* @__PURE__ */ jsxRuntime.jsx(History, { size: 14 }),
2584
- "History"
3246
+ toolbarConfig.history.showLabel && t.toolbar.history
2585
3247
  ]
2586
3248
  }
2587
3249
  )
@@ -2925,7 +3587,8 @@ const lexicalTheme = {
2925
3587
  h2: "lex4-heading lex4-heading-h2",
2926
3588
  h3: "lex4-heading lex4-heading-h3",
2927
3589
  h4: "lex4-heading lex4-heading-h4",
2928
- h5: "lex4-heading lex4-heading-h5"
3590
+ h5: "lex4-heading lex4-heading-h5",
3591
+ h6: "lex4-heading lex4-heading-h6"
2929
3592
  },
2930
3593
  text: {
2931
3594
  bold: "lex4-text-bold",
@@ -4462,12 +5125,10 @@ function decodeFormatBitmask(format) {
4462
5125
  return marks;
4463
5126
  }
4464
5127
  function extractFontFamily(style) {
4465
- const match = style.match(/font-family:\s*([^;]+)/);
4466
- return match ? match[1].trim().replace(/['"]/g, "") : void 0;
5128
+ return extractFontFamilyFromStyle(style);
4467
5129
  }
4468
5130
  function extractFontSizePt(style) {
4469
- const match = style.match(/font-size:\s*(\d+(?:\.\d+)?)\s*pt/);
4470
- return match ? parseFloat(match[1]) : void 0;
5131
+ return extractFontSizePtFromStyle(style);
4471
5132
  }
4472
5133
  function buildTextMarks(format, style) {
4473
5134
  const formatMarks = decodeFormatBitmask(format);
@@ -4501,9 +5162,11 @@ function mapTextNode(node) {
4501
5162
  };
4502
5163
  }
4503
5164
  function mapVariableNode(node) {
5165
+ const marks = buildTextMarks(node.format ?? 0, node.style);
4504
5166
  return {
4505
5167
  type: "variable",
4506
- key: node.variableKey
5168
+ key: node.variableKey,
5169
+ ...marks ? { marks } : {}
4507
5170
  };
4508
5171
  }
4509
5172
  function mapLineBreak() {
@@ -4567,7 +5230,7 @@ function mapParagraph(node) {
4567
5230
  function mapHeading(node) {
4568
5231
  var _a;
4569
5232
  const alignment = decodeAlignment(node.format);
4570
- const tagMatch = (_a = node.tag) == null ? void 0 : _a.match(/^h(\d)$/);
5233
+ const tagMatch = (_a = node.tag) == null ? void 0 : _a.match(/^h([1-6])$/);
4571
5234
  const level = tagMatch ? parseInt(tagMatch[1], 10) : 1;
4572
5235
  return {
4573
5236
  type: "heading",
@@ -4692,6 +5355,105 @@ function buildSavePayload(ast, options) {
4692
5355
  function serializeDocumentJson(ast) {
4693
5356
  return JSON.stringify(ast, null, 2);
4694
5357
  }
5358
+ function appendChildren(parent, children = []) {
5359
+ const nodes = children.map(buildLexicalNode).filter((node) => node !== null);
5360
+ if (nodes.length > 0) {
5361
+ parent.append(...nodes);
5362
+ }
5363
+ }
5364
+ function applyElementFormatting(node, serializedNode) {
5365
+ if (typeof serializedNode.format === "string" && ["left", "center", "right", "justify"].includes(serializedNode.format) && node.setFormat) {
5366
+ node.setFormat(serializedNode.format);
5367
+ }
5368
+ if (typeof serializedNode.indent === "number" && serializedNode.indent > 0 && node.setIndent) {
5369
+ node.setIndent(serializedNode.indent);
5370
+ }
5371
+ }
5372
+ function buildLexicalNode(serializedNode) {
5373
+ switch (serializedNode.type) {
5374
+ case "paragraph": {
5375
+ const node = lexical.$createParagraphNode();
5376
+ applyElementFormatting(node, serializedNode);
5377
+ appendChildren(node, serializedNode.children);
5378
+ return node;
5379
+ }
5380
+ case "heading": {
5381
+ const tag = typeof serializedNode.tag === "string" && /^h[1-6]$/.test(serializedNode.tag) ? serializedNode.tag : "h1";
5382
+ const node = richText.$createHeadingNode(tag);
5383
+ applyElementFormatting(node, serializedNode);
5384
+ appendChildren(node, serializedNode.children);
5385
+ return node;
5386
+ }
5387
+ case "quote": {
5388
+ const node = richText.$createQuoteNode();
5389
+ applyElementFormatting(node, serializedNode);
5390
+ appendChildren(node, serializedNode.children);
5391
+ return node;
5392
+ }
5393
+ case "list": {
5394
+ const listType = serializedNode.listType === "number" ? "number" : "bullet";
5395
+ const node = list.$createListNode(listType, typeof serializedNode.start === "number" ? serializedNode.start : 1);
5396
+ appendChildren(node, serializedNode.children);
5397
+ return node;
5398
+ }
5399
+ case "listitem": {
5400
+ const node = list.$createListItemNode();
5401
+ appendChildren(node, serializedNode.children);
5402
+ return node;
5403
+ }
5404
+ case "text": {
5405
+ const node = lexical.$createTextNode(serializedNode.text ?? "");
5406
+ if (typeof serializedNode.format === "number" && serializedNode.format > 0) {
5407
+ node.setFormat(serializedNode.format);
5408
+ }
5409
+ if (typeof serializedNode.style === "string" && serializedNode.style.trim() !== "") {
5410
+ node.setStyle(serializedNode.style);
5411
+ }
5412
+ return node;
5413
+ }
5414
+ case "linebreak":
5415
+ return lexical.$createLineBreakNode();
5416
+ case "variable-node":
5417
+ return $createVariableNode(
5418
+ serializedNode.variableKey ?? "",
5419
+ typeof serializedNode.format === "number" ? serializedNode.format : 0,
5420
+ typeof serializedNode.style === "string" ? serializedNode.style : ""
5421
+ );
5422
+ default:
5423
+ return null;
5424
+ }
5425
+ }
5426
+ function getBodyChildren(pageState) {
5427
+ const root = pageState == null ? void 0 : pageState.root;
5428
+ return (root == null ? void 0 : root.children) ?? [];
5429
+ }
5430
+ function insertDocumentContent(editor, document2) {
5431
+ let inserted = false;
5432
+ editor.update(() => {
5433
+ const nodes = document2.pages.flatMap((page) => getBodyChildren(page.bodyState)).map(buildLexicalNode).filter((node) => node !== null);
5434
+ if (nodes.length === 0) {
5435
+ return;
5436
+ }
5437
+ const selection2 = lexical.$getSelection();
5438
+ if (lexical.$isRangeSelection(selection2)) {
5439
+ if (selection2.isCollapsed()) {
5440
+ selection2.insertParagraph();
5441
+ }
5442
+ const nextSelection = lexical.$getSelection();
5443
+ if (lexical.$isRangeSelection(nextSelection) || lexical.$isNodeSelection(nextSelection)) {
5444
+ nextSelection.insertNodes(nodes);
5445
+ } else {
5446
+ lexical.$insertNodes(nodes);
5447
+ }
5448
+ } else if (lexical.$isNodeSelection(selection2)) {
5449
+ selection2.insertNodes(nodes);
5450
+ } else {
5451
+ lexical.$insertNodes(nodes);
5452
+ }
5453
+ inserted = true;
5454
+ });
5455
+ return inserted;
5456
+ }
4695
5457
  function selectEntireDocument(rootElement, selectionBuffer) {
4696
5458
  if (!rootElement || !selectionBuffer) {
4697
5459
  return;
@@ -4898,10 +5660,13 @@ const EditorWithHandle = React.forwardRef(({ captureHistoryShortcutsOnWindow, on
4898
5660
  const {
4899
5661
  document: doc,
4900
5662
  activeEditor,
5663
+ activeCaretPosition,
4901
5664
  historySidebarOpen,
5665
+ runHistoryAction,
4902
5666
  setHistorySidebarOpen
4903
5667
  } = useDocument();
4904
5668
  const { handleFactories } = useExtensions();
5669
+ const t = useTranslations();
4905
5670
  const getDocument = React.useCallback(() => doc, [doc]);
4906
5671
  const getActiveEditor = React.useCallback(() => activeEditor, [activeEditor]);
4907
5672
  const extensionCtx = useExtensionContext(getDocument, getActiveEditor);
@@ -4912,6 +5677,23 @@ const EditorWithHandle = React.forwardRef(({ captureHistoryShortcutsOnWindow, on
4912
5677
  },
4913
5678
  toggleHistorySidebar: () => {
4914
5679
  setHistorySidebarOpen(!historySidebarOpen);
5680
+ },
5681
+ insertDocumentContent: (documentToInsert) => {
5682
+ if (!activeEditor || (activeCaretPosition == null ? void 0 : activeCaretPosition.region) !== "body") {
5683
+ return false;
5684
+ }
5685
+ let inserted = false;
5686
+ runHistoryAction(
5687
+ {
5688
+ label: t.history.actions.insertedDocumentContent,
5689
+ source: "toolbar",
5690
+ region: "document"
5691
+ },
5692
+ () => {
5693
+ inserted = insertDocumentContent(activeEditor, documentToInsert);
5694
+ }
5695
+ );
5696
+ return inserted;
4915
5697
  }
4916
5698
  };
4917
5699
  for (const factory of handleFactories) {
@@ -4919,7 +5701,16 @@ const EditorWithHandle = React.forwardRef(({ captureHistoryShortcutsOnWindow, on
4919
5701
  Object.assign(handle, methods);
4920
5702
  }
4921
5703
  return handle;
4922
- }, [extensionCtx, handleFactories, historySidebarOpen, setHistorySidebarOpen]);
5704
+ }, [
5705
+ activeCaretPosition == null ? void 0 : activeCaretPosition.region,
5706
+ activeEditor,
5707
+ extensionCtx,
5708
+ handleFactories,
5709
+ historySidebarOpen,
5710
+ runHistoryAction,
5711
+ setHistorySidebarOpen,
5712
+ t.history.actions.insertedDocumentContent
5713
+ ]);
4923
5714
  return /* @__PURE__ */ jsxRuntime.jsx(
4924
5715
  EditorChrome,
4925
5716
  {
@@ -4937,9 +5728,10 @@ const Lex4Editor = React.forwardRef(({
4937
5728
  onSave,
4938
5729
  extensions,
4939
5730
  translations,
5731
+ toolbar,
4940
5732
  className
4941
5733
  }, ref) => {
4942
- return /* @__PURE__ */ jsxRuntime.jsx(TranslationsProvider, { translations, children: /* @__PURE__ */ jsxRuntime.jsx(ExtensionStateProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(ExtensionProvider, { extensions, children: /* @__PURE__ */ jsxRuntime.jsx(
5734
+ return /* @__PURE__ */ jsxRuntime.jsx(TranslationsProvider, { translations, children: /* @__PURE__ */ jsxRuntime.jsx(ToolbarConfigProvider, { toolbar, children: /* @__PURE__ */ jsxRuntime.jsx(ExtensionStateProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(ExtensionProvider, { extensions, children: /* @__PURE__ */ jsxRuntime.jsx(
4943
5735
  DocumentProvider,
4944
5736
  {
4945
5737
  initialDocument,
@@ -4954,7 +5746,7 @@ const Lex4Editor = React.forwardRef(({
4954
5746
  }
4955
5747
  )
4956
5748
  }
4957
- ) }) }) });
5749
+ ) }) }) }) });
4958
5750
  });
4959
5751
  Lex4Editor.displayName = "Lex4Editor";
4960
5752
  function useOverflowDetection(bodyHeight, onOverflow, debounceMs = 100) {
@@ -5048,120 +5840,6 @@ function astExtension() {
5048
5840
  }
5049
5841
  };
5050
5842
  }
5051
- const EMPTY_CONTEXT = {
5052
- definitions: [],
5053
- refreshDefinitions: () => {
5054
- },
5055
- getDefinition: () => void 0
5056
- };
5057
- const VariableContext = React.createContext(EMPTY_CONTEXT);
5058
- const VariableProvider = ({
5059
- initialDefinitions = [],
5060
- children
5061
- }) => {
5062
- const [definitions, setDefinitions] = React.useState(initialDefinitions);
5063
- const refresh = React.useCallback((newDefinitions) => {
5064
- setDefinitions(newDefinitions);
5065
- }, []);
5066
- const getDefinition = React.useCallback(
5067
- (key) => {
5068
- return definitions.find((d) => d.key === key);
5069
- },
5070
- [definitions]
5071
- );
5072
- const value = React.useMemo(
5073
- () => ({ definitions, refreshDefinitions: refresh, getDefinition }),
5074
- [definitions, refresh, getDefinition]
5075
- );
5076
- return /* @__PURE__ */ jsxRuntime.jsx(VariableContext.Provider, { value, children });
5077
- };
5078
- function useVariables() {
5079
- return React.useContext(VariableContext);
5080
- }
5081
- class VariableNode extends lexical.DecoratorNode {
5082
- constructor(variableKey, key) {
5083
- super(key);
5084
- __publicField(this, "__variableKey");
5085
- this.__variableKey = variableKey;
5086
- }
5087
- static getType() {
5088
- return "variable-node";
5089
- }
5090
- static clone(node) {
5091
- return new VariableNode(node.__variableKey, node.__key);
5092
- }
5093
- getVariableKey() {
5094
- return this.__variableKey;
5095
- }
5096
- // -- Serialization --
5097
- static importJSON(serializedNode) {
5098
- return $createVariableNode(serializedNode.variableKey);
5099
- }
5100
- exportJSON() {
5101
- return {
5102
- type: "variable-node",
5103
- version: 1,
5104
- variableKey: this.__variableKey
5105
- };
5106
- }
5107
- // -- DOM --
5108
- createDOM() {
5109
- const span = document.createElement("span");
5110
- span.className = "lex4-variable";
5111
- span.setAttribute("data-variable-key", this.__variableKey);
5112
- span.setAttribute("data-testid", `variable-${this.__variableKey}`);
5113
- span.contentEditable = "false";
5114
- return span;
5115
- }
5116
- updateDOM() {
5117
- return false;
5118
- }
5119
- exportDOM() {
5120
- const span = document.createElement("span");
5121
- span.setAttribute("data-variable-key", this.__variableKey);
5122
- span.textContent = `{{${this.__variableKey}}}`;
5123
- return { element: span };
5124
- }
5125
- static importDOM() {
5126
- return null;
5127
- }
5128
- // -- Behavior --
5129
- isInline() {
5130
- return true;
5131
- }
5132
- isKeyboardSelectable() {
5133
- return true;
5134
- }
5135
- getTextContent() {
5136
- return `{{${this.__variableKey}}}`;
5137
- }
5138
- // -- Rendering --
5139
- decorate() {
5140
- return /* @__PURE__ */ jsxRuntime.jsx(VariableChip, { variableKey: this.__variableKey });
5141
- }
5142
- }
5143
- function VariableChip({ variableKey }) {
5144
- const { getDefinition } = useVariables();
5145
- const def = getDefinition(variableKey);
5146
- const label = (def == null ? void 0 : def.label) ?? variableKey;
5147
- const group = def == null ? void 0 : def.group;
5148
- return /* @__PURE__ */ jsxRuntime.jsx(
5149
- "span",
5150
- {
5151
- className: "lex4-variable-chip",
5152
- "data-testid": `variable-chip-${variableKey}`,
5153
- "data-variable-group": group,
5154
- title: variableKey,
5155
- children: label
5156
- }
5157
- );
5158
- }
5159
- function $createVariableNode(variableKey) {
5160
- return lexical.$applyNodeReplacement(new VariableNode(variableKey));
5161
- }
5162
- function $isVariableNode(node) {
5163
- return node instanceof VariableNode;
5164
- }
5165
5843
  const INSERT_VARIABLE_COMMAND = lexical.createCommand("INSERT_VARIABLE");
5166
5844
  const VariablePlugin = () => {
5167
5845
  const [editor] = LexicalComposerContext.useLexicalComposerContext();
@@ -5170,8 +5848,8 @@ const VariablePlugin = () => {
5170
5848
  INSERT_VARIABLE_COMMAND,
5171
5849
  (variableKey) => {
5172
5850
  editor.update(() => {
5173
- const selection = lexical.$getSelection();
5174
- if (!lexical.$isRangeSelection(selection)) return;
5851
+ const selection2 = lexical.$getSelection();
5852
+ if (!lexical.$isRangeSelection(selection2)) return;
5175
5853
  const variableNode = $createVariableNode(variableKey);
5176
5854
  lexical.$insertNodes([variableNode]);
5177
5855
  });
@@ -5422,7 +6100,11 @@ const VariablePanelStateProvider = ({ children }) => {
5422
6100
  };
5423
6101
  const VariableToolbarToggle = () => {
5424
6102
  const { panelOpen, setPanelOpen } = useVariablePanelState();
6103
+ const toolbarConfig = useToolbarConfig();
5425
6104
  const t = useTranslations();
6105
+ if (!toolbarConfig.variables.visible) {
6106
+ return null;
6107
+ }
5426
6108
  return /* @__PURE__ */ jsxRuntime.jsxs(
5427
6109
  "button",
5428
6110
  {
@@ -5435,7 +6117,7 @@ const VariableToolbarToggle = () => {
5435
6117
  "aria-label": panelOpen ? t.variables.closePanel : t.variables.openPanel,
5436
6118
  children: [
5437
6119
  /* @__PURE__ */ jsxRuntime.jsx(Braces, { size: 14 }),
5438
- t.variables.title
6120
+ toolbarConfig.variables.showLabel && t.variables.title
5439
6121
  ]
5440
6122
  }
5441
6123
  );