@yurikilian/lex4 1.4.0 → 1.5.1

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 (43) 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/CanvasControls.d.ts.map +1 -1
  6. package/dist/components/HeaderFooterActions.d.ts.map +1 -1
  7. package/dist/components/HeaderFooterToggle.d.ts +1 -0
  8. package/dist/components/HeaderFooterToggle.d.ts.map +1 -1
  9. package/dist/components/Lex4Editor.d.ts.map +1 -1
  10. package/dist/components/Toolbar.d.ts.map +1 -1
  11. package/dist/context/document-context.d.ts +1 -0
  12. package/dist/context/document-context.d.ts.map +1 -1
  13. package/dist/context/document-provider.d.ts.map +1 -1
  14. package/dist/context/toolbar-config.d.ts +18 -0
  15. package/dist/context/toolbar-config.d.ts.map +1 -0
  16. package/dist/extensions/variables-extension.d.ts.map +1 -1
  17. package/dist/i18n/defaults.d.ts.map +1 -1
  18. package/dist/i18n/pt-BR.d.ts.map +1 -1
  19. package/dist/i18n/types.d.ts +25 -0
  20. package/dist/i18n/types.d.ts.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/lex4-editor.cjs +773 -178
  24. package/dist/lex4-editor.cjs.map +1 -1
  25. package/dist/lex4-editor.js +757 -162
  26. package/dist/lex4-editor.js.map +1 -1
  27. package/dist/lexical/commands/block-commands.d.ts +5 -0
  28. package/dist/lexical/commands/block-commands.d.ts.map +1 -0
  29. package/dist/lexical/theme.d.ts.map +1 -1
  30. package/dist/lexical/utils/import-document-content.d.ts +4 -0
  31. package/dist/lexical/utils/import-document-content.d.ts.map +1 -0
  32. package/dist/style.css +51 -23
  33. package/dist/types/editor-handle.d.ts +2 -0
  34. package/dist/types/editor-handle.d.ts.map +1 -1
  35. package/dist/types/editor-props.d.ts +16 -0
  36. package/dist/types/editor-props.d.ts.map +1 -1
  37. package/dist/utils/text-style.d.ts +7 -0
  38. package/dist/utils/text-style.d.ts.map +1 -0
  39. package/dist/variables/variable-formatting.d.ts +11 -0
  40. package/dist/variables/variable-formatting.d.ts.map +1 -0
  41. package/dist/variables/variable-node.d.ts +10 -2
  42. package/dist/variables/variable-node.d.ts.map +1 -1
  43. 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: {
@@ -550,7 +563,20 @@ const DEFAULT_TRANSLATIONS = {
550
563
  page: "Page {{page}}"
551
564
  },
552
565
  headerFooter: {
553
- label: "Headers & Footers"
566
+ label: "Headers & Footers",
567
+ settingsLabel: "Header and footer settings",
568
+ pageCounter: "Page counter",
569
+ pageCounterModes: {
570
+ none: "None",
571
+ header: "Header",
572
+ footer: "Footer",
573
+ both: "Both"
574
+ },
575
+ headerSection: "Header",
576
+ footerSection: "Footer",
577
+ copyToAllPages: "Copy to all pages",
578
+ clearThisPage: "Clear this page",
579
+ clearAll: "Clear all"
554
580
  },
555
581
  sidebar: {
556
582
  close: "Close sidebar"
@@ -591,6 +617,15 @@ const PT_BR_TRANSLATIONS = {
591
617
  bulletList: "Lista com Marcadores",
592
618
  indent: "Aumentar Recuo",
593
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",
594
629
  openHistory: "Abrir Histórico",
595
630
  closeHistory: "Fechar Histórico"
596
631
  },
@@ -622,7 +657,9 @@ const PT_BR_TRANSLATIONS = {
622
657
  indentedContent: "Conteúdo recuado",
623
658
  outdentedContent: "Recuo reduzido",
624
659
  fontChanged: "Fonte alterada para {{value}}",
625
- 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"
626
663
  }
627
664
  },
628
665
  variables: {
@@ -668,7 +705,20 @@ const PT_BR_TRANSLATIONS = {
668
705
  page: "Página {{page}}"
669
706
  },
670
707
  headerFooter: {
671
- label: "Cabeçalhos e Rodapés"
708
+ label: "Cabeçalhos e Rodapés",
709
+ settingsLabel: "Configurações de cabeçalho e rodapé",
710
+ pageCounter: "Contador de páginas",
711
+ pageCounterModes: {
712
+ none: "Nenhum",
713
+ header: "Cabeçalho",
714
+ footer: "Rodapé",
715
+ both: "Ambos"
716
+ },
717
+ headerSection: "Cabeçalho",
718
+ footerSection: "Rodapé",
719
+ copyToAllPages: "Copiar para todas as páginas",
720
+ clearThisPage: "Limpar esta página",
721
+ clearAll: "Limpar tudo"
672
722
  },
673
723
  sidebar: {
674
724
  close: "Fechar barra lateral"
@@ -771,25 +821,25 @@ function captureCaretSelection(editor) {
771
821
  };
772
822
  let caretSelection = null;
773
823
  editor.getEditorState().read(() => {
774
- const selection = lexical.$createRangeSelectionFromDom(window.getSelection(), editor) ?? lexical.$getSelection();
775
- if (!lexical.$isRangeSelection(selection)) {
824
+ const selection2 = lexical.$createRangeSelectionFromDom(window.getSelection(), editor) ?? lexical.$getSelection();
825
+ if (!lexical.$isRangeSelection(selection2)) {
776
826
  return;
777
827
  }
778
828
  caretSelection = {
779
829
  anchor: {
780
- key: selection.anchor.key,
781
- offset: selection.anchor.offset,
782
- type: selection.anchor.type
830
+ key: selection2.anchor.key,
831
+ offset: selection2.anchor.offset,
832
+ type: selection2.anchor.type
783
833
  },
784
834
  focus: {
785
- key: selection.focus.key,
786
- offset: selection.focus.offset,
787
- type: selection.focus.type
835
+ key: selection2.focus.key,
836
+ offset: selection2.focus.offset,
837
+ type: selection2.focus.type
788
838
  },
789
839
  anchorTextOffset: domSelection && selectionInRoot(domSelection.anchorNode) ? getTextOffset(domSelection.anchorNode, domSelection.anchorOffset) : 0,
790
840
  focusTextOffset: domSelection && selectionInRoot(domSelection.focusNode) ? getTextOffset(domSelection.focusNode, domSelection.focusOffset) : 0,
791
- format: selection.format,
792
- style: selection.style
841
+ format: selection2.format,
842
+ style: selection2.style
793
843
  };
794
844
  });
795
845
  return caretSelection;
@@ -1260,6 +1310,7 @@ const DocumentProvider = ({
1260
1310
  activePageId,
1261
1311
  setActivePageId,
1262
1312
  activeEditor: activeEditorRef.current,
1313
+ activeCaretPosition: activeCaretPositionRef.current,
1263
1314
  consumePendingCaretPosition,
1264
1315
  consumePendingFocusAtEnd,
1265
1316
  requestFocusAtEnd,
@@ -1866,6 +1917,36 @@ const HistorySidebar = () => {
1866
1917
  }
1867
1918
  );
1868
1919
  };
1920
+ const DEFAULT_TOOLBAR_CONTROL_CONFIG = {
1921
+ visible: true,
1922
+ showLabel: true
1923
+ };
1924
+ const DEFAULT_TOOLBAR_CONFIG = {
1925
+ history: DEFAULT_TOOLBAR_CONTROL_CONFIG,
1926
+ variables: DEFAULT_TOOLBAR_CONTROL_CONFIG,
1927
+ headerFooter: DEFAULT_TOOLBAR_CONTROL_CONFIG
1928
+ };
1929
+ const ToolbarConfigContext = React.createContext(DEFAULT_TOOLBAR_CONFIG);
1930
+ function resolveControlConfig(config) {
1931
+ return {
1932
+ visible: (config == null ? void 0 : config.visible) ?? true,
1933
+ showLabel: (config == null ? void 0 : config.showLabel) ?? true
1934
+ };
1935
+ }
1936
+ function normalizeToolbarConfig(config) {
1937
+ return {
1938
+ history: resolveControlConfig(config == null ? void 0 : config.history),
1939
+ variables: resolveControlConfig(config == null ? void 0 : config.variables),
1940
+ headerFooter: resolveControlConfig(config == null ? void 0 : config.headerFooter)
1941
+ };
1942
+ }
1943
+ const ToolbarConfigProvider = ({ toolbar, children }) => {
1944
+ const resolvedConfig = React.useMemo(() => normalizeToolbarConfig(toolbar), [toolbar]);
1945
+ return /* @__PURE__ */ jsxRuntime.jsx(ToolbarConfigContext.Provider, { value: resolvedConfig, children });
1946
+ };
1947
+ function useToolbarConfig() {
1948
+ return React.useContext(ToolbarConfigContext);
1949
+ }
1869
1950
  function resolveExtensions(extensions) {
1870
1951
  const resolved = {
1871
1952
  nodes: [],
@@ -1971,9 +2052,9 @@ const SUPPORTED_FONTS = [
1971
2052
  ];
1972
2053
  function applyFontFamily(editor, fontFamily) {
1973
2054
  editor.update(() => {
1974
- const selection = lexical.$getSelection();
1975
- if (!lexical.$isRangeSelection(selection)) return;
1976
- const nodes = selection.getNodes();
2055
+ const selection2 = lexical.$getSelection();
2056
+ if (!lexical.$isRangeSelection(selection2)) return;
2057
+ const nodes = selection2.getNodes();
1977
2058
  for (const node of nodes) {
1978
2059
  if (lexical.$isTextNode(node)) {
1979
2060
  node.setStyle(`font-family: ${fontFamily}`);
@@ -1998,11 +2079,12 @@ const SUPPORTED_FONT_SIZES = [
1998
2079
  48,
1999
2080
  72
2000
2081
  ];
2082
+ const DEFAULT_FONT_SIZE = 12;
2001
2083
  function applyFontSize(editor, size) {
2002
2084
  editor.update(() => {
2003
- const selection = lexical.$getSelection();
2004
- if (!lexical.$isRangeSelection(selection)) return;
2005
- const nodes = selection.getNodes();
2085
+ const selection2 = lexical.$getSelection();
2086
+ if (!lexical.$isRangeSelection(selection2)) return;
2087
+ const nodes = selection2.getNodes();
2006
2088
  for (const node of nodes) {
2007
2089
  if (lexical.$isTextNode(node)) {
2008
2090
  const existing = node.getStyle();
@@ -2048,9 +2130,366 @@ function indentContent(editor) {
2048
2130
  function outdentContent(editor) {
2049
2131
  editor.dispatchCommand(lexical.OUTDENT_CONTENT_COMMAND, void 0);
2050
2132
  }
2133
+ function setBlockType(editor, blockType) {
2134
+ editor.update(() => {
2135
+ const selection$1 = lexical.$getSelection();
2136
+ if (!lexical.$isRangeSelection(selection$1)) {
2137
+ return;
2138
+ }
2139
+ if (blockType === "paragraph") {
2140
+ selection.$setBlocksType(selection$1, () => lexical.$createParagraphNode());
2141
+ return;
2142
+ }
2143
+ selection.$setBlocksType(selection$1, () => richText.$createHeadingNode(blockType));
2144
+ });
2145
+ }
2146
+ function getActiveBlockType(editor) {
2147
+ let blockType = "paragraph";
2148
+ editor.getEditorState().read(() => {
2149
+ const selection2 = lexical.$getSelection();
2150
+ if (!lexical.$isRangeSelection(selection2)) {
2151
+ return;
2152
+ }
2153
+ const anchorNode = selection2.anchor.getNode();
2154
+ const topLevelElement = anchorNode.getTopLevelElementOrThrow();
2155
+ if (richText.$isHeadingNode(topLevelElement)) {
2156
+ blockType = topLevelElement.getTag();
2157
+ }
2158
+ });
2159
+ return blockType;
2160
+ }
2161
+ function extractStyleValue(style, property) {
2162
+ const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2163
+ const match = style.match(new RegExp(`${escapedProperty}:\\s*([^;]+)`));
2164
+ return match ? match[1].trim().replace(/['"]/g, "") : void 0;
2165
+ }
2166
+ function mergeStyleDeclaration(existingStyle, property, value) {
2167
+ const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2168
+ const stripped = existingStyle.replace(
2169
+ new RegExp(`${escapedProperty}:\\s*[^;]+;?\\s*`, "g"),
2170
+ ""
2171
+ ).trim();
2172
+ const declaration = `${property}: ${value}`;
2173
+ return stripped ? `${stripped}; ${declaration}` : declaration;
2174
+ }
2175
+ function extractFontFamilyFromStyle(style) {
2176
+ return extractStyleValue(style, "font-family");
2177
+ }
2178
+ function extractFontSizePtFromStyle(style) {
2179
+ const value = extractStyleValue(style, "font-size");
2180
+ if (!value) {
2181
+ return void 0;
2182
+ }
2183
+ const match = value.match(/^(\d+(?:\.\d+)?)\s*pt$/);
2184
+ return match ? parseFloat(match[1]) : void 0;
2185
+ }
2186
+ function mergeFontFamilyStyle(existingStyle, fontFamily) {
2187
+ return mergeStyleDeclaration(existingStyle, "font-family", fontFamily);
2188
+ }
2189
+ function mergeFontSizeStyle(existingStyle, size) {
2190
+ return mergeStyleDeclaration(existingStyle, "font-size", `${size}pt`);
2191
+ }
2192
+ const EMPTY_CONTEXT = {
2193
+ definitions: [],
2194
+ refreshDefinitions: () => {
2195
+ },
2196
+ getDefinition: () => void 0
2197
+ };
2198
+ const VariableContext = React.createContext(EMPTY_CONTEXT);
2199
+ const VariableProvider = ({
2200
+ initialDefinitions = [],
2201
+ children
2202
+ }) => {
2203
+ const [definitions, setDefinitions] = React.useState(initialDefinitions);
2204
+ const refresh = React.useCallback((newDefinitions) => {
2205
+ setDefinitions(newDefinitions);
2206
+ }, []);
2207
+ const getDefinition = React.useCallback(
2208
+ (key) => {
2209
+ return definitions.find((d) => d.key === key);
2210
+ },
2211
+ [definitions]
2212
+ );
2213
+ const value = React.useMemo(
2214
+ () => ({ definitions, refreshDefinitions: refresh, getDefinition }),
2215
+ [definitions, refresh, getDefinition]
2216
+ );
2217
+ return /* @__PURE__ */ jsxRuntime.jsx(VariableContext.Provider, { value, children });
2218
+ };
2219
+ function useVariables() {
2220
+ return React.useContext(VariableContext);
2221
+ }
2222
+ class VariableNode extends lexical.DecoratorNode {
2223
+ constructor(variableKey, format = 0, style = "", key) {
2224
+ super(key);
2225
+ __publicField(this, "__variableKey");
2226
+ __publicField(this, "__format");
2227
+ __publicField(this, "__style");
2228
+ this.__variableKey = variableKey;
2229
+ this.__format = format;
2230
+ this.__style = style;
2231
+ }
2232
+ static getType() {
2233
+ return "variable-node";
2234
+ }
2235
+ static clone(node) {
2236
+ return new VariableNode(node.__variableKey, node.__format, node.__style, node.__key);
2237
+ }
2238
+ getVariableKey() {
2239
+ return this.getLatest().__variableKey;
2240
+ }
2241
+ getFormat() {
2242
+ return this.getLatest().__format;
2243
+ }
2244
+ setFormat(format) {
2245
+ const writable = this.getWritable();
2246
+ writable.__format = format;
2247
+ return writable;
2248
+ }
2249
+ getStyle() {
2250
+ return this.getLatest().__style;
2251
+ }
2252
+ setStyle(style) {
2253
+ const writable = this.getWritable();
2254
+ writable.__style = style;
2255
+ return writable;
2256
+ }
2257
+ // -- Serialization --
2258
+ static importJSON(serializedNode) {
2259
+ return $createVariableNode(
2260
+ serializedNode.variableKey,
2261
+ serializedNode.format ?? 0,
2262
+ serializedNode.style ?? ""
2263
+ );
2264
+ }
2265
+ exportJSON() {
2266
+ return {
2267
+ type: "variable-node",
2268
+ version: 1,
2269
+ variableKey: this.__variableKey,
2270
+ format: this.__format,
2271
+ style: this.__style
2272
+ };
2273
+ }
2274
+ // -- DOM --
2275
+ createDOM() {
2276
+ const span = document.createElement("span");
2277
+ span.className = "lex4-variable";
2278
+ span.setAttribute("data-variable-key", this.__variableKey);
2279
+ span.setAttribute("data-testid", `variable-${this.__variableKey}`);
2280
+ span.contentEditable = "false";
2281
+ return span;
2282
+ }
2283
+ updateDOM() {
2284
+ return false;
2285
+ }
2286
+ exportDOM() {
2287
+ const span = document.createElement("span");
2288
+ span.setAttribute("data-variable-key", this.__variableKey);
2289
+ span.textContent = `{{${this.__variableKey}}}`;
2290
+ return { element: span };
2291
+ }
2292
+ static importDOM() {
2293
+ return null;
2294
+ }
2295
+ // -- Behavior --
2296
+ isInline() {
2297
+ return true;
2298
+ }
2299
+ isKeyboardSelectable() {
2300
+ return true;
2301
+ }
2302
+ getTextContent() {
2303
+ return `{{${this.__variableKey}}}`;
2304
+ }
2305
+ // -- Rendering --
2306
+ decorate() {
2307
+ return /* @__PURE__ */ jsxRuntime.jsx(
2308
+ VariableChip,
2309
+ {
2310
+ nodeKey: this.__key,
2311
+ variableKey: this.__variableKey,
2312
+ format: this.__format,
2313
+ styleValue: this.__style
2314
+ }
2315
+ );
2316
+ }
2317
+ }
2318
+ function VariableChip({
2319
+ nodeKey,
2320
+ variableKey,
2321
+ format,
2322
+ styleValue
2323
+ }) {
2324
+ const { getDefinition } = useVariables();
2325
+ const [editor] = LexicalComposerContext.useLexicalComposerContext();
2326
+ const [isSelected, setSelected, clearOtherSelections] = useLexicalNodeSelection.useLexicalNodeSelection(nodeKey);
2327
+ const def = getDefinition(variableKey);
2328
+ const label = (def == null ? void 0 : def.label) ?? variableKey;
2329
+ const group = def == null ? void 0 : def.group;
2330
+ const style = React.useMemo(() => {
2331
+ const fontFamily = extractFontFamilyFromStyle(styleValue);
2332
+ const fontSize = extractFontSizePtFromStyle(styleValue);
2333
+ return {
2334
+ ...fontFamily ? { fontFamily } : {},
2335
+ ...fontSize ? { fontSize: `${fontSize}pt` } : {}
2336
+ };
2337
+ }, [styleValue]);
2338
+ const className = [
2339
+ "lex4-variable-chip",
2340
+ isSelected && "lex4-variable-chip-selected",
2341
+ format & 1 ? "lex4-text-bold" : "",
2342
+ format & 2 ? "lex4-text-italic" : "",
2343
+ format & 8 ? "lex4-text-underline" : "",
2344
+ format & 4 ? "lex4-text-strikethrough" : ""
2345
+ ].filter(Boolean).join(" ");
2346
+ const handleClick = React.useCallback((event) => {
2347
+ event.preventDefault();
2348
+ if (!event.shiftKey) {
2349
+ clearOtherSelections();
2350
+ }
2351
+ setSelected(!isSelected);
2352
+ }, [clearOtherSelections, isSelected, setSelected]);
2353
+ React.useEffect(() => {
2354
+ const removeSelectedNodes = () => {
2355
+ editor.update(() => {
2356
+ const selection2 = lexical.$getSelection();
2357
+ if (!lexical.$isNodeSelection(selection2)) {
2358
+ const node = lexical.$getNodeByKey(nodeKey);
2359
+ if ($isVariableNode(node)) {
2360
+ node.remove();
2361
+ }
2362
+ return;
2363
+ }
2364
+ for (const node of selection2.getNodes()) {
2365
+ if ($isVariableNode(node)) {
2366
+ node.remove();
2367
+ }
2368
+ }
2369
+ });
2370
+ };
2371
+ const unregisterBackspace = editor.registerCommand(
2372
+ lexical.KEY_BACKSPACE_COMMAND,
2373
+ () => {
2374
+ if (!isSelected) {
2375
+ return false;
2376
+ }
2377
+ removeSelectedNodes();
2378
+ return true;
2379
+ },
2380
+ lexical.COMMAND_PRIORITY_LOW
2381
+ );
2382
+ const unregisterDelete = editor.registerCommand(
2383
+ lexical.KEY_DELETE_COMMAND,
2384
+ () => {
2385
+ if (!isSelected) {
2386
+ return false;
2387
+ }
2388
+ removeSelectedNodes();
2389
+ return true;
2390
+ },
2391
+ lexical.COMMAND_PRIORITY_LOW
2392
+ );
2393
+ return () => {
2394
+ unregisterBackspace();
2395
+ unregisterDelete();
2396
+ };
2397
+ }, [editor, isSelected, nodeKey]);
2398
+ return /* @__PURE__ */ jsxRuntime.jsx(
2399
+ "span",
2400
+ {
2401
+ className,
2402
+ "data-testid": `variable-chip-${variableKey}`,
2403
+ "data-variable-group": group,
2404
+ title: variableKey,
2405
+ style,
2406
+ onMouseDown: (event) => event.preventDefault(),
2407
+ onClick: handleClick,
2408
+ children: label
2409
+ }
2410
+ );
2411
+ }
2412
+ function $createVariableNode(variableKey, format = 0, style = "") {
2413
+ return lexical.$applyNodeReplacement(new VariableNode(variableKey, format, style));
2414
+ }
2415
+ function $isVariableNode(node) {
2416
+ return node instanceof VariableNode;
2417
+ }
2418
+ const FORMAT_MASKS = {
2419
+ bold: 1,
2420
+ italic: 2,
2421
+ strikethrough: 4,
2422
+ underline: 8
2423
+ };
2424
+ function withSelectedVariableNodes(editor, updater) {
2425
+ let updated = false;
2426
+ editor.update(() => {
2427
+ const selection2 = lexical.$getSelection();
2428
+ if (!lexical.$isNodeSelection(selection2)) {
2429
+ return;
2430
+ }
2431
+ const nodes = selection2.getNodes().filter($isVariableNode);
2432
+ if (nodes.length === 0) {
2433
+ return;
2434
+ }
2435
+ updater(nodes);
2436
+ updated = true;
2437
+ });
2438
+ return updated;
2439
+ }
2440
+ function getSelectedVariableNodes(editor) {
2441
+ let nodes = [];
2442
+ editor.getEditorState().read(() => {
2443
+ const selection2 = lexical.$getSelection();
2444
+ if (!lexical.$isNodeSelection(selection2)) {
2445
+ return;
2446
+ }
2447
+ nodes = selection2.getNodes().filter($isVariableNode);
2448
+ });
2449
+ return nodes;
2450
+ }
2451
+ function toggleSelectedVariableFormat(editor, format) {
2452
+ const mask = FORMAT_MASKS[format];
2453
+ if (!mask) {
2454
+ return false;
2455
+ }
2456
+ return withSelectedVariableNodes(editor, (nodes) => {
2457
+ const shouldEnable = nodes.some((node) => (node.getFormat() & mask) === 0);
2458
+ for (const node of nodes) {
2459
+ const nextFormat = shouldEnable ? node.getFormat() | mask : node.getFormat() & ~mask;
2460
+ node.setFormat(nextFormat);
2461
+ }
2462
+ });
2463
+ }
2464
+ function applyFontFamilyToSelectedVariables(editor, fontFamily) {
2465
+ return withSelectedVariableNodes(editor, (nodes) => {
2466
+ for (const node of nodes) {
2467
+ node.setStyle(mergeFontFamilyStyle(node.getStyle(), fontFamily));
2468
+ }
2469
+ });
2470
+ }
2471
+ function applyFontSizeToSelectedVariables(editor, size) {
2472
+ return withSelectedVariableNodes(editor, (nodes) => {
2473
+ for (const node of nodes) {
2474
+ node.setStyle(mergeFontSizeStyle(node.getStyle(), size));
2475
+ }
2476
+ });
2477
+ }
2478
+ function readSelectedVariableFormatting(editor) {
2479
+ const firstNode = getSelectedVariableNodes(editor)[0];
2480
+ if (!firstNode) {
2481
+ return {};
2482
+ }
2483
+ const style = firstNode.getStyle();
2484
+ return {
2485
+ fontFamily: extractFontFamilyFromStyle(style),
2486
+ fontSize: extractFontSizePtFromStyle(style)
2487
+ };
2488
+ }
2051
2489
  const HeaderFooterToggle = ({
2052
2490
  enabled,
2053
- onToggle
2491
+ onToggle,
2492
+ showLabel = true
2054
2493
  }) => {
2055
2494
  const t = useTranslations();
2056
2495
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2060,18 +2499,20 @@ const HeaderFooterToggle = ({
2060
2499
  "data-testid": "header-footer-toggle",
2061
2500
  children: [
2062
2501
  /* @__PURE__ */ jsxRuntime.jsx(FileText, { size: 14, className: "lex4-hf-toggle-icon" }),
2063
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lex4-hf-toggle-label", children: t.headerFooter.label }),
2502
+ showLabel && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lex4-hf-toggle-label", children: t.headerFooter.label }),
2064
2503
  /* @__PURE__ */ jsxRuntime.jsx(
2065
2504
  "button",
2066
2505
  {
2067
2506
  type: "button",
2068
2507
  role: "switch",
2069
2508
  "aria-checked": enabled,
2509
+ "aria-label": t.headerFooter.label,
2070
2510
  onMouseDown: (e) => e.preventDefault(),
2071
2511
  onClick: () => onToggle(!enabled),
2072
2512
  className: "lex4-hf-switch",
2073
2513
  style: { backgroundColor: enabled ? "var(--color-primary)" : "var(--color-muted)" },
2074
2514
  "data-testid": "header-footer-switch",
2515
+ title: t.headerFooter.label,
2075
2516
  children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lex4-hf-switch-knob" })
2076
2517
  }
2077
2518
  )
@@ -2079,12 +2520,7 @@ const HeaderFooterToggle = ({
2079
2520
  }
2080
2521
  );
2081
2522
  };
2082
- const PAGE_COUNTER_OPTIONS = [
2083
- { value: "none", label: "None" },
2084
- { value: "header", label: "Header" },
2085
- { value: "footer", label: "Footer" },
2086
- { value: "both", label: "Both" }
2087
- ];
2523
+ const PAGE_COUNTER_OPTIONS = ["none", "header", "footer", "both"];
2088
2524
  const HeaderFooterActions = ({
2089
2525
  activePageId,
2090
2526
  pageCounterMode,
@@ -2099,6 +2535,7 @@ const HeaderFooterActions = ({
2099
2535
  const [open, setOpen] = React.useState(false);
2100
2536
  const containerRef = React.useRef(null);
2101
2537
  const hasActivePage = activePageId !== null;
2538
+ const t = useTranslations();
2102
2539
  const close = React.useCallback(() => setOpen(false), []);
2103
2540
  React.useEffect(() => {
2104
2541
  if (!open) return;
@@ -2125,8 +2562,8 @@ const HeaderFooterActions = ({
2125
2562
  "data-testid": "header-footer-menu-trigger",
2126
2563
  "aria-expanded": open,
2127
2564
  "aria-haspopup": "true",
2128
- "aria-label": "Header and footer settings",
2129
- title: "Header and footer settings",
2565
+ "aria-label": t.headerFooter.settingsLabel,
2566
+ title: t.headerFooter.settingsLabel,
2130
2567
  children: /* @__PURE__ */ jsxRuntime.jsx(Settings2, { size: 14 })
2131
2568
  }
2132
2569
  ),
@@ -2137,8 +2574,8 @@ const HeaderFooterActions = ({
2137
2574
  role: "menu",
2138
2575
  "data-testid": "header-footer-menu",
2139
2576
  children: [
2140
- /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Page counter" }),
2141
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lex4-settings-counter-grid", "data-testid": "page-counter-mode", children: PAGE_COUNTER_OPTIONS.map(({ value, label }) => /* @__PURE__ */ jsxRuntime.jsx(
2577
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: t.headerFooter.pageCounter }),
2578
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lex4-settings-counter-grid", "data-testid": "page-counter-mode", children: PAGE_COUNTER_OPTIONS.map((value) => /* @__PURE__ */ jsxRuntime.jsx(
2142
2579
  "button",
2143
2580
  {
2144
2581
  type: "button",
@@ -2148,17 +2585,17 @@ const HeaderFooterActions = ({
2148
2585
  onClick: () => onPageCounterModeChange(value),
2149
2586
  className: "lex4-settings-counter-btn",
2150
2587
  "data-testid": `page-counter-${value}`,
2151
- children: label
2588
+ children: t.headerFooter.pageCounterModes[value]
2152
2589
  },
2153
2590
  value
2154
2591
  )) }),
2155
2592
  /* @__PURE__ */ jsxRuntime.jsx(MenuDivider, {}),
2156
- /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Header" }),
2593
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: t.headerFooter.headerSection }),
2157
2594
  /* @__PURE__ */ jsxRuntime.jsx(
2158
2595
  MenuItem,
2159
2596
  {
2160
2597
  icon: /* @__PURE__ */ jsxRuntime.jsx(CopyPlus, { size: 14 }),
2161
- label: "Copy to all pages",
2598
+ label: t.headerFooter.copyToAllPages,
2162
2599
  onClick: () => handleAction(onCopyHeaderToAll),
2163
2600
  disabled: !hasActivePage,
2164
2601
  testId: "copy-header-all"
@@ -2168,7 +2605,7 @@ const HeaderFooterActions = ({
2168
2605
  MenuItem,
2169
2606
  {
2170
2607
  icon: /* @__PURE__ */ jsxRuntime.jsx(Eraser, { size: 14 }),
2171
- label: "Clear this page",
2608
+ label: t.headerFooter.clearThisPage,
2172
2609
  onClick: () => handleAction(onClearHeader),
2173
2610
  disabled: !hasActivePage,
2174
2611
  testId: "clear-header"
@@ -2178,18 +2615,18 @@ const HeaderFooterActions = ({
2178
2615
  MenuItem,
2179
2616
  {
2180
2617
  icon: /* @__PURE__ */ jsxRuntime.jsx(Trash2, { size: 14 }),
2181
- label: "Clear all",
2618
+ label: t.headerFooter.clearAll,
2182
2619
  onClick: () => handleAction(onClearAllHeaders),
2183
2620
  testId: "clear-all-headers"
2184
2621
  }
2185
2622
  ),
2186
2623
  /* @__PURE__ */ jsxRuntime.jsx(MenuDivider, {}),
2187
- /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: "Footer" }),
2624
+ /* @__PURE__ */ jsxRuntime.jsx(SectionLabel, { children: t.headerFooter.footerSection }),
2188
2625
  /* @__PURE__ */ jsxRuntime.jsx(
2189
2626
  MenuItem,
2190
2627
  {
2191
2628
  icon: /* @__PURE__ */ jsxRuntime.jsx(CopyPlus, { size: 14 }),
2192
- label: "Copy to all pages",
2629
+ label: t.headerFooter.copyToAllPages,
2193
2630
  onClick: () => handleAction(onCopyFooterToAll),
2194
2631
  disabled: !hasActivePage,
2195
2632
  testId: "copy-footer-all"
@@ -2199,7 +2636,7 @@ const HeaderFooterActions = ({
2199
2636
  MenuItem,
2200
2637
  {
2201
2638
  icon: /* @__PURE__ */ jsxRuntime.jsx(Eraser, { size: 14 }),
2202
- label: "Clear this page",
2639
+ label: t.headerFooter.clearThisPage,
2203
2640
  onClick: () => handleAction(onClearFooter),
2204
2641
  disabled: !hasActivePage,
2205
2642
  testId: "clear-footer"
@@ -2209,7 +2646,7 @@ const HeaderFooterActions = ({
2209
2646
  MenuItem,
2210
2647
  {
2211
2648
  icon: /* @__PURE__ */ jsxRuntime.jsx(Trash2, { size: 14 }),
2212
- label: "Clear all",
2649
+ label: t.headerFooter.clearAll,
2213
2650
  onClick: () => handleAction(onClearAllFooters),
2214
2651
  testId: "clear-all-footers"
2215
2652
  }
@@ -2239,6 +2676,7 @@ const MenuItem = ({ icon, label, onClick, disabled, testId }) => /* @__PURE__ */
2239
2676
  const MenuDivider = () => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lex4-settings-divider" });
2240
2677
  const CanvasControls = () => {
2241
2678
  const { document: document2, dispatch, activePageId, runHistoryAction } = useDocument();
2679
+ const toolbarConfig = useToolbarConfig();
2242
2680
  const t = useTranslations();
2243
2681
  const runCanvasAction = React.useCallback((label, callback) => {
2244
2682
  runHistoryAction(
@@ -2305,12 +2743,16 @@ const CanvasControls = () => {
2305
2743
  dispatch({ type: "SET_PAGE_COUNTER_MODE", mode });
2306
2744
  });
2307
2745
  }, [dispatch, runCanvasAction, t.history.actions.pageCounterSet]);
2746
+ if (!toolbarConfig.headerFooter.visible) {
2747
+ return null;
2748
+ }
2308
2749
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lex4-toolbar-group lex4-toolbar-group-gap", "data-testid": "header-footer-controls", children: [
2309
2750
  /* @__PURE__ */ jsxRuntime.jsx(
2310
2751
  HeaderFooterToggle,
2311
2752
  {
2312
2753
  enabled: document2.headerFooterEnabled,
2313
- onToggle: handleToggle
2754
+ onToggle: handleToggle,
2755
+ showLabel: toolbarConfig.headerFooter.showLabel
2314
2756
  }
2315
2757
  ),
2316
2758
  document2.headerFooterEnabled && /* @__PURE__ */ jsxRuntime.jsx(
@@ -2343,7 +2785,17 @@ const Toolbar = () => {
2343
2785
  undo
2344
2786
  } = useDocument();
2345
2787
  const { toolbarItems, toolbarEndItems } = useExtensions();
2788
+ const toolbarConfig = useToolbarConfig();
2346
2789
  const t = useTranslations();
2790
+ const [activeBlockType, setActiveBlockType] = React.useState("paragraph");
2791
+ const [activeFontFamily, setActiveFontFamily] = React.useState("Calibri");
2792
+ const [activeFontSize, setActiveFontSize] = React.useState(DEFAULT_FONT_SIZE);
2793
+ const normalizeFontFamily = React.useCallback((fontFamily) => {
2794
+ if (fontFamily && SUPPORTED_FONTS.includes(fontFamily)) {
2795
+ return fontFamily;
2796
+ }
2797
+ return "Calibri";
2798
+ }, []);
2347
2799
  const withBodySelection = React.useCallback(
2348
2800
  (editor, action) => {
2349
2801
  editor.update(() => {
@@ -2379,27 +2831,91 @@ const Toolbar = () => {
2379
2831
  },
2380
2832
  [runHistoryAction]
2381
2833
  );
2834
+ React.useEffect(() => {
2835
+ if (!activeEditor) {
2836
+ setActiveBlockType("paragraph");
2837
+ setActiveFontFamily("Calibri");
2838
+ setActiveFontSize(DEFAULT_FONT_SIZE);
2839
+ return;
2840
+ }
2841
+ const updateSelectionState = () => {
2842
+ const selectedVariables = getSelectedVariableNodes(activeEditor);
2843
+ if (selectedVariables.length > 0) {
2844
+ const formatting = readSelectedVariableFormatting(activeEditor);
2845
+ setActiveBlockType("paragraph");
2846
+ setActiveFontFamily(normalizeFontFamily(formatting.fontFamily));
2847
+ setActiveFontSize(formatting.fontSize ?? DEFAULT_FONT_SIZE);
2848
+ return;
2849
+ }
2850
+ setActiveBlockType(getActiveBlockType(activeEditor));
2851
+ let nextFontFamily = "Calibri";
2852
+ let nextFontSize = DEFAULT_FONT_SIZE;
2853
+ activeEditor.getEditorState().read(() => {
2854
+ const selection2 = lexical.$getSelection();
2855
+ if (!lexical.$isRangeSelection(selection2)) {
2856
+ return;
2857
+ }
2858
+ const textNode = selection2.getNodes().find(lexical.$isTextNode);
2859
+ if (!textNode) {
2860
+ return;
2861
+ }
2862
+ const style = textNode.getStyle();
2863
+ nextFontFamily = normalizeFontFamily(extractFontFamilyFromStyle(style));
2864
+ nextFontSize = extractFontSizePtFromStyle(style) ?? DEFAULT_FONT_SIZE;
2865
+ });
2866
+ setActiveFontFamily(nextFontFamily);
2867
+ setActiveFontSize(nextFontSize);
2868
+ };
2869
+ updateSelectionState();
2870
+ const unregisterSelectionChange = activeEditor.registerCommand(
2871
+ lexical.SELECTION_CHANGE_COMMAND,
2872
+ () => {
2873
+ updateSelectionState();
2874
+ return false;
2875
+ },
2876
+ lexical.COMMAND_PRIORITY_LOW
2877
+ );
2878
+ const unregisterUpdateListener = activeEditor.registerUpdateListener(() => {
2879
+ updateSelectionState();
2880
+ });
2881
+ return () => {
2882
+ unregisterSelectionChange();
2883
+ unregisterUpdateListener();
2884
+ };
2885
+ }, [activeEditor, normalizeFontFamily]);
2382
2886
  const handleBold = React.useCallback(() => {
2383
2887
  debug("toolbar", `bold (globalSelection=${globalSelectionActive}, editors=${editorRegistry.all().length}, hasEditor=${!!activeEditor})`);
2384
2888
  runToolbarAction(t.history.actions.boldApplied, () => {
2889
+ if (activeEditor && toggleSelectedVariableFormat(activeEditor, "bold")) {
2890
+ return;
2891
+ }
2385
2892
  applyToBodyEditors(toggleBold);
2386
2893
  });
2387
2894
  }, [activeEditor, applyToBodyEditors, editorRegistry, globalSelectionActive, runToolbarAction, t.history.actions.boldApplied]);
2388
2895
  const handleItalic = React.useCallback(() => {
2389
2896
  debug("toolbar", `italic (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
2390
2897
  runToolbarAction(t.history.actions.italicApplied, () => {
2898
+ if (activeEditor && toggleSelectedVariableFormat(activeEditor, "italic")) {
2899
+ return;
2900
+ }
2391
2901
  applyToBodyEditors(toggleItalic);
2392
2902
  });
2393
2903
  }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction, t.history.actions.italicApplied]);
2394
2904
  const handleUnderline = React.useCallback(() => {
2395
2905
  debug("toolbar", `underline (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
2396
2906
  runToolbarAction(t.history.actions.underlineApplied, () => {
2907
+ if (activeEditor && toggleSelectedVariableFormat(activeEditor, "underline")) {
2908
+ return;
2909
+ }
2397
2910
  applyToBodyEditors(toggleUnderline);
2398
2911
  });
2399
2912
  }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction, t.history.actions.underlineApplied]);
2400
2913
  const handleStrikethrough = React.useCallback(() => {
2401
2914
  debug("toolbar", `strikethrough (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
2402
2915
  runToolbarAction(t.history.actions.strikethroughApplied, () => {
2916
+ if (activeEditor && toggleSelectedVariableFormat(activeEditor, "strikethrough")) {
2917
+ return;
2918
+ }
2403
2919
  applyToBodyEditors(toggleStrikethrough);
2404
2920
  });
2405
2921
  }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction, t.history.actions.strikethroughApplied]);
@@ -2445,20 +2961,59 @@ const Toolbar = () => {
2445
2961
  }, [applyToBodyEditors, runToolbarAction, t.history.actions.outdentedContent]);
2446
2962
  const handleFontChange = React.useCallback(
2447
2963
  (e) => {
2448
- runToolbarAction(interpolate(t.history.actions.fontChanged, { value: e.target.value }), () => {
2449
- applyToBodyEditors((editor) => applyFontFamily(editor, e.target.value));
2964
+ const fontFamily = e.target.value;
2965
+ runToolbarAction(interpolate(t.history.actions.fontChanged, { value: fontFamily }), () => {
2966
+ if (activeEditor && applyFontFamilyToSelectedVariables(activeEditor, fontFamily)) {
2967
+ return;
2968
+ }
2969
+ applyToBodyEditors((editor) => applyFontFamily(editor, fontFamily));
2450
2970
  });
2451
2971
  },
2452
- [applyToBodyEditors, runToolbarAction, t.history.actions.fontChanged]
2972
+ [activeEditor, applyToBodyEditors, runToolbarAction, t.history.actions.fontChanged]
2453
2973
  );
2454
2974
  const handleFontSizeChange = React.useCallback(
2455
2975
  (e) => {
2456
2976
  const size = parseInt(e.target.value, 10);
2457
2977
  runToolbarAction(interpolate(t.history.actions.fontSizeChanged, { value: String(size) }), () => {
2978
+ if (activeEditor && applyFontSizeToSelectedVariables(activeEditor, size)) {
2979
+ return;
2980
+ }
2458
2981
  applyToBodyEditors((editor) => applyFontSize(editor, size));
2459
2982
  });
2460
2983
  },
2461
- [applyToBodyEditors, runToolbarAction, t.history.actions.fontSizeChanged]
2984
+ [activeEditor, applyToBodyEditors, runToolbarAction, t.history.actions.fontSizeChanged]
2985
+ );
2986
+ const handleBlockTypeChange = React.useCallback(
2987
+ (e) => {
2988
+ const blockType = e.target.value;
2989
+ const labelMap = {
2990
+ paragraph: t.toolbar.paragraph,
2991
+ h1: t.toolbar.heading1,
2992
+ h2: t.toolbar.heading2,
2993
+ h3: t.toolbar.heading3,
2994
+ h4: t.toolbar.heading4,
2995
+ h5: t.toolbar.heading5,
2996
+ h6: t.toolbar.heading6
2997
+ };
2998
+ runToolbarAction(
2999
+ interpolate(t.history.actions.blockTypeChanged, { value: labelMap[blockType] }),
3000
+ () => {
3001
+ applyToBodyEditors((editor) => setBlockType(editor, blockType));
3002
+ }
3003
+ );
3004
+ },
3005
+ [
3006
+ applyToBodyEditors,
3007
+ runToolbarAction,
3008
+ t.history.actions.blockTypeChanged,
3009
+ t.toolbar.heading1,
3010
+ t.toolbar.heading2,
3011
+ t.toolbar.heading3,
3012
+ t.toolbar.heading4,
3013
+ t.toolbar.heading5,
3014
+ t.toolbar.heading6,
3015
+ t.toolbar.paragraph
3016
+ ]
2462
3017
  );
2463
3018
  const handleToggleHistory = React.useCallback(() => {
2464
3019
  setHistorySidebarOpen(!historySidebarOpen);
@@ -2492,6 +3047,26 @@ const Toolbar = () => {
2492
3047
  )
2493
3048
  ] }),
2494
3049
  /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
3050
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lex4-toolbar-group-gap lex4-toolbar-group-block", children: /* @__PURE__ */ jsxRuntime.jsxs(
3051
+ "select",
3052
+ {
3053
+ className: "lex4-toolbar-select lex4-toolbar-select-block",
3054
+ "data-testid": "block-type-selector",
3055
+ "aria-label": t.toolbar.blockType,
3056
+ value: activeBlockType,
3057
+ onChange: handleBlockTypeChange,
3058
+ children: [
3059
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "paragraph", children: t.toolbar.paragraph }),
3060
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "h1", children: t.toolbar.heading1 }),
3061
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "h2", children: t.toolbar.heading2 }),
3062
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "h3", children: t.toolbar.heading3 }),
3063
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "h4", children: t.toolbar.heading4 }),
3064
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "h5", children: t.toolbar.heading5 }),
3065
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "h6", children: t.toolbar.heading6 })
3066
+ ]
3067
+ }
3068
+ ) }),
3069
+ /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
2495
3070
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lex4-toolbar-group-gap", children: [
2496
3071
  /* @__PURE__ */ jsxRuntime.jsx(Type, { size: 14, className: "lex4-toolbar-icon" }),
2497
3072
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2499,7 +3074,7 @@ const Toolbar = () => {
2499
3074
  {
2500
3075
  className: "lex4-toolbar-select",
2501
3076
  "data-testid": "font-selector",
2502
- defaultValue: "Calibri",
3077
+ value: activeFontFamily,
2503
3078
  onChange: handleFontChange,
2504
3079
  children: SUPPORTED_FONTS.map((font) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: font, style: { fontFamily: font }, children: font }, font))
2505
3080
  }
@@ -2512,7 +3087,7 @@ const Toolbar = () => {
2512
3087
  {
2513
3088
  className: "lex4-toolbar-select lex4-toolbar-select-narrow",
2514
3089
  "data-testid": "font-size-selector",
2515
- defaultValue: "12",
3090
+ value: String(activeFontSize),
2516
3091
  onChange: handleFontSizeChange,
2517
3092
  children: SUPPORTED_FONT_SIZES.map((size) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: size, children: size }, size))
2518
3093
  }
@@ -2547,7 +3122,7 @@ const Toolbar = () => {
2547
3122
  /* @__PURE__ */ jsxRuntime.jsx(CanvasControls, {}),
2548
3123
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lex4-toolbar-end", children: [
2549
3124
  toolbarEndItems.map((EndItem, idx) => /* @__PURE__ */ jsxRuntime.jsx(EndItem, {}, idx)),
2550
- /* @__PURE__ */ jsxRuntime.jsxs(
3125
+ toolbarConfig.history.visible && /* @__PURE__ */ jsxRuntime.jsxs(
2551
3126
  "button",
2552
3127
  {
2553
3128
  type: "button",
@@ -2559,7 +3134,7 @@ const Toolbar = () => {
2559
3134
  "aria-label": historySidebarOpen ? t.toolbar.closeHistory : t.toolbar.openHistory,
2560
3135
  children: [
2561
3136
  /* @__PURE__ */ jsxRuntime.jsx(History, { size: 14 }),
2562
- "History"
3137
+ toolbarConfig.history.showLabel && t.toolbar.history
2563
3138
  ]
2564
3139
  }
2565
3140
  )
@@ -2903,7 +3478,8 @@ const lexicalTheme = {
2903
3478
  h2: "lex4-heading lex4-heading-h2",
2904
3479
  h3: "lex4-heading lex4-heading-h3",
2905
3480
  h4: "lex4-heading lex4-heading-h4",
2906
- h5: "lex4-heading lex4-heading-h5"
3481
+ h5: "lex4-heading lex4-heading-h5",
3482
+ h6: "lex4-heading lex4-heading-h6"
2907
3483
  },
2908
3484
  text: {
2909
3485
  bold: "lex4-text-bold",
@@ -4440,12 +5016,10 @@ function decodeFormatBitmask(format) {
4440
5016
  return marks;
4441
5017
  }
4442
5018
  function extractFontFamily(style) {
4443
- const match = style.match(/font-family:\s*([^;]+)/);
4444
- return match ? match[1].trim().replace(/['"]/g, "") : void 0;
5019
+ return extractFontFamilyFromStyle(style);
4445
5020
  }
4446
5021
  function extractFontSizePt(style) {
4447
- const match = style.match(/font-size:\s*(\d+(?:\.\d+)?)\s*pt/);
4448
- return match ? parseFloat(match[1]) : void 0;
5022
+ return extractFontSizePtFromStyle(style);
4449
5023
  }
4450
5024
  function buildTextMarks(format, style) {
4451
5025
  const formatMarks = decodeFormatBitmask(format);
@@ -4479,9 +5053,11 @@ function mapTextNode(node) {
4479
5053
  };
4480
5054
  }
4481
5055
  function mapVariableNode(node) {
5056
+ const marks = buildTextMarks(node.format ?? 0, node.style);
4482
5057
  return {
4483
5058
  type: "variable",
4484
- key: node.variableKey
5059
+ key: node.variableKey,
5060
+ ...marks ? { marks } : {}
4485
5061
  };
4486
5062
  }
4487
5063
  function mapLineBreak() {
@@ -4545,7 +5121,7 @@ function mapParagraph(node) {
4545
5121
  function mapHeading(node) {
4546
5122
  var _a;
4547
5123
  const alignment = decodeAlignment(node.format);
4548
- const tagMatch = (_a = node.tag) == null ? void 0 : _a.match(/^h(\d)$/);
5124
+ const tagMatch = (_a = node.tag) == null ? void 0 : _a.match(/^h([1-6])$/);
4549
5125
  const level = tagMatch ? parseInt(tagMatch[1], 10) : 1;
4550
5126
  return {
4551
5127
  type: "heading",
@@ -4670,6 +5246,105 @@ function buildSavePayload(ast, options) {
4670
5246
  function serializeDocumentJson(ast) {
4671
5247
  return JSON.stringify(ast, null, 2);
4672
5248
  }
5249
+ function appendChildren(parent, children = []) {
5250
+ const nodes = children.map(buildLexicalNode).filter((node) => node !== null);
5251
+ if (nodes.length > 0) {
5252
+ parent.append(...nodes);
5253
+ }
5254
+ }
5255
+ function applyElementFormatting(node, serializedNode) {
5256
+ if (typeof serializedNode.format === "string" && ["left", "center", "right", "justify"].includes(serializedNode.format) && node.setFormat) {
5257
+ node.setFormat(serializedNode.format);
5258
+ }
5259
+ if (typeof serializedNode.indent === "number" && serializedNode.indent > 0 && node.setIndent) {
5260
+ node.setIndent(serializedNode.indent);
5261
+ }
5262
+ }
5263
+ function buildLexicalNode(serializedNode) {
5264
+ switch (serializedNode.type) {
5265
+ case "paragraph": {
5266
+ const node = lexical.$createParagraphNode();
5267
+ applyElementFormatting(node, serializedNode);
5268
+ appendChildren(node, serializedNode.children);
5269
+ return node;
5270
+ }
5271
+ case "heading": {
5272
+ const tag = typeof serializedNode.tag === "string" && /^h[1-6]$/.test(serializedNode.tag) ? serializedNode.tag : "h1";
5273
+ const node = richText.$createHeadingNode(tag);
5274
+ applyElementFormatting(node, serializedNode);
5275
+ appendChildren(node, serializedNode.children);
5276
+ return node;
5277
+ }
5278
+ case "quote": {
5279
+ const node = richText.$createQuoteNode();
5280
+ applyElementFormatting(node, serializedNode);
5281
+ appendChildren(node, serializedNode.children);
5282
+ return node;
5283
+ }
5284
+ case "list": {
5285
+ const listType = serializedNode.listType === "number" ? "number" : "bullet";
5286
+ const node = list.$createListNode(listType, typeof serializedNode.start === "number" ? serializedNode.start : 1);
5287
+ appendChildren(node, serializedNode.children);
5288
+ return node;
5289
+ }
5290
+ case "listitem": {
5291
+ const node = list.$createListItemNode();
5292
+ appendChildren(node, serializedNode.children);
5293
+ return node;
5294
+ }
5295
+ case "text": {
5296
+ const node = lexical.$createTextNode(serializedNode.text ?? "");
5297
+ if (typeof serializedNode.format === "number" && serializedNode.format > 0) {
5298
+ node.setFormat(serializedNode.format);
5299
+ }
5300
+ if (typeof serializedNode.style === "string" && serializedNode.style.trim() !== "") {
5301
+ node.setStyle(serializedNode.style);
5302
+ }
5303
+ return node;
5304
+ }
5305
+ case "linebreak":
5306
+ return lexical.$createLineBreakNode();
5307
+ case "variable-node":
5308
+ return $createVariableNode(
5309
+ serializedNode.variableKey ?? "",
5310
+ typeof serializedNode.format === "number" ? serializedNode.format : 0,
5311
+ typeof serializedNode.style === "string" ? serializedNode.style : ""
5312
+ );
5313
+ default:
5314
+ return null;
5315
+ }
5316
+ }
5317
+ function getBodyChildren(pageState) {
5318
+ const root = pageState == null ? void 0 : pageState.root;
5319
+ return (root == null ? void 0 : root.children) ?? [];
5320
+ }
5321
+ function insertDocumentContent(editor, document2) {
5322
+ let inserted = false;
5323
+ editor.update(() => {
5324
+ const nodes = document2.pages.flatMap((page) => getBodyChildren(page.bodyState)).map(buildLexicalNode).filter((node) => node !== null);
5325
+ if (nodes.length === 0) {
5326
+ return;
5327
+ }
5328
+ const selection2 = lexical.$getSelection();
5329
+ if (lexical.$isRangeSelection(selection2)) {
5330
+ if (selection2.isCollapsed()) {
5331
+ selection2.insertParagraph();
5332
+ }
5333
+ const nextSelection = lexical.$getSelection();
5334
+ if (lexical.$isRangeSelection(nextSelection) || lexical.$isNodeSelection(nextSelection)) {
5335
+ nextSelection.insertNodes(nodes);
5336
+ } else {
5337
+ lexical.$insertNodes(nodes);
5338
+ }
5339
+ } else if (lexical.$isNodeSelection(selection2)) {
5340
+ selection2.insertNodes(nodes);
5341
+ } else {
5342
+ lexical.$insertNodes(nodes);
5343
+ }
5344
+ inserted = true;
5345
+ });
5346
+ return inserted;
5347
+ }
4673
5348
  function selectEntireDocument(rootElement, selectionBuffer) {
4674
5349
  if (!rootElement || !selectionBuffer) {
4675
5350
  return;
@@ -4876,10 +5551,13 @@ const EditorWithHandle = React.forwardRef(({ captureHistoryShortcutsOnWindow, on
4876
5551
  const {
4877
5552
  document: doc,
4878
5553
  activeEditor,
5554
+ activeCaretPosition,
4879
5555
  historySidebarOpen,
5556
+ runHistoryAction,
4880
5557
  setHistorySidebarOpen
4881
5558
  } = useDocument();
4882
5559
  const { handleFactories } = useExtensions();
5560
+ const t = useTranslations();
4883
5561
  const getDocument = React.useCallback(() => doc, [doc]);
4884
5562
  const getActiveEditor = React.useCallback(() => activeEditor, [activeEditor]);
4885
5563
  const extensionCtx = useExtensionContext(getDocument, getActiveEditor);
@@ -4890,6 +5568,23 @@ const EditorWithHandle = React.forwardRef(({ captureHistoryShortcutsOnWindow, on
4890
5568
  },
4891
5569
  toggleHistorySidebar: () => {
4892
5570
  setHistorySidebarOpen(!historySidebarOpen);
5571
+ },
5572
+ insertDocumentContent: (documentToInsert) => {
5573
+ if (!activeEditor || (activeCaretPosition == null ? void 0 : activeCaretPosition.region) !== "body") {
5574
+ return false;
5575
+ }
5576
+ let inserted = false;
5577
+ runHistoryAction(
5578
+ {
5579
+ label: t.history.actions.insertedDocumentContent,
5580
+ source: "toolbar",
5581
+ region: "document"
5582
+ },
5583
+ () => {
5584
+ inserted = insertDocumentContent(activeEditor, documentToInsert);
5585
+ }
5586
+ );
5587
+ return inserted;
4893
5588
  }
4894
5589
  };
4895
5590
  for (const factory of handleFactories) {
@@ -4897,7 +5592,16 @@ const EditorWithHandle = React.forwardRef(({ captureHistoryShortcutsOnWindow, on
4897
5592
  Object.assign(handle, methods);
4898
5593
  }
4899
5594
  return handle;
4900
- }, [extensionCtx, handleFactories, historySidebarOpen, setHistorySidebarOpen]);
5595
+ }, [
5596
+ activeCaretPosition == null ? void 0 : activeCaretPosition.region,
5597
+ activeEditor,
5598
+ extensionCtx,
5599
+ handleFactories,
5600
+ historySidebarOpen,
5601
+ runHistoryAction,
5602
+ setHistorySidebarOpen,
5603
+ t.history.actions.insertedDocumentContent
5604
+ ]);
4901
5605
  return /* @__PURE__ */ jsxRuntime.jsx(
4902
5606
  EditorChrome,
4903
5607
  {
@@ -4915,9 +5619,10 @@ const Lex4Editor = React.forwardRef(({
4915
5619
  onSave,
4916
5620
  extensions,
4917
5621
  translations,
5622
+ toolbar,
4918
5623
  className
4919
5624
  }, ref) => {
4920
- return /* @__PURE__ */ jsxRuntime.jsx(TranslationsProvider, { translations, children: /* @__PURE__ */ jsxRuntime.jsx(ExtensionStateProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(ExtensionProvider, { extensions, children: /* @__PURE__ */ jsxRuntime.jsx(
5625
+ 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(
4921
5626
  DocumentProvider,
4922
5627
  {
4923
5628
  initialDocument,
@@ -4932,7 +5637,7 @@ const Lex4Editor = React.forwardRef(({
4932
5637
  }
4933
5638
  )
4934
5639
  }
4935
- ) }) }) });
5640
+ ) }) }) }) });
4936
5641
  });
4937
5642
  Lex4Editor.displayName = "Lex4Editor";
4938
5643
  function useOverflowDetection(bodyHeight, onOverflow, debounceMs = 100) {
@@ -5026,120 +5731,6 @@ function astExtension() {
5026
5731
  }
5027
5732
  };
5028
5733
  }
5029
- const EMPTY_CONTEXT = {
5030
- definitions: [],
5031
- refreshDefinitions: () => {
5032
- },
5033
- getDefinition: () => void 0
5034
- };
5035
- const VariableContext = React.createContext(EMPTY_CONTEXT);
5036
- const VariableProvider = ({
5037
- initialDefinitions = [],
5038
- children
5039
- }) => {
5040
- const [definitions, setDefinitions] = React.useState(initialDefinitions);
5041
- const refresh = React.useCallback((newDefinitions) => {
5042
- setDefinitions(newDefinitions);
5043
- }, []);
5044
- const getDefinition = React.useCallback(
5045
- (key) => {
5046
- return definitions.find((d) => d.key === key);
5047
- },
5048
- [definitions]
5049
- );
5050
- const value = React.useMemo(
5051
- () => ({ definitions, refreshDefinitions: refresh, getDefinition }),
5052
- [definitions, refresh, getDefinition]
5053
- );
5054
- return /* @__PURE__ */ jsxRuntime.jsx(VariableContext.Provider, { value, children });
5055
- };
5056
- function useVariables() {
5057
- return React.useContext(VariableContext);
5058
- }
5059
- class VariableNode extends lexical.DecoratorNode {
5060
- constructor(variableKey, key) {
5061
- super(key);
5062
- __publicField(this, "__variableKey");
5063
- this.__variableKey = variableKey;
5064
- }
5065
- static getType() {
5066
- return "variable-node";
5067
- }
5068
- static clone(node) {
5069
- return new VariableNode(node.__variableKey, node.__key);
5070
- }
5071
- getVariableKey() {
5072
- return this.__variableKey;
5073
- }
5074
- // -- Serialization --
5075
- static importJSON(serializedNode) {
5076
- return $createVariableNode(serializedNode.variableKey);
5077
- }
5078
- exportJSON() {
5079
- return {
5080
- type: "variable-node",
5081
- version: 1,
5082
- variableKey: this.__variableKey
5083
- };
5084
- }
5085
- // -- DOM --
5086
- createDOM() {
5087
- const span = document.createElement("span");
5088
- span.className = "lex4-variable";
5089
- span.setAttribute("data-variable-key", this.__variableKey);
5090
- span.setAttribute("data-testid", `variable-${this.__variableKey}`);
5091
- span.contentEditable = "false";
5092
- return span;
5093
- }
5094
- updateDOM() {
5095
- return false;
5096
- }
5097
- exportDOM() {
5098
- const span = document.createElement("span");
5099
- span.setAttribute("data-variable-key", this.__variableKey);
5100
- span.textContent = `{{${this.__variableKey}}}`;
5101
- return { element: span };
5102
- }
5103
- static importDOM() {
5104
- return null;
5105
- }
5106
- // -- Behavior --
5107
- isInline() {
5108
- return true;
5109
- }
5110
- isKeyboardSelectable() {
5111
- return true;
5112
- }
5113
- getTextContent() {
5114
- return `{{${this.__variableKey}}}`;
5115
- }
5116
- // -- Rendering --
5117
- decorate() {
5118
- return /* @__PURE__ */ jsxRuntime.jsx(VariableChip, { variableKey: this.__variableKey });
5119
- }
5120
- }
5121
- function VariableChip({ variableKey }) {
5122
- const { getDefinition } = useVariables();
5123
- const def = getDefinition(variableKey);
5124
- const label = (def == null ? void 0 : def.label) ?? variableKey;
5125
- const group = def == null ? void 0 : def.group;
5126
- return /* @__PURE__ */ jsxRuntime.jsx(
5127
- "span",
5128
- {
5129
- className: "lex4-variable-chip",
5130
- "data-testid": `variable-chip-${variableKey}`,
5131
- "data-variable-group": group,
5132
- title: variableKey,
5133
- children: label
5134
- }
5135
- );
5136
- }
5137
- function $createVariableNode(variableKey) {
5138
- return lexical.$applyNodeReplacement(new VariableNode(variableKey));
5139
- }
5140
- function $isVariableNode(node) {
5141
- return node instanceof VariableNode;
5142
- }
5143
5734
  const INSERT_VARIABLE_COMMAND = lexical.createCommand("INSERT_VARIABLE");
5144
5735
  const VariablePlugin = () => {
5145
5736
  const [editor] = LexicalComposerContext.useLexicalComposerContext();
@@ -5148,8 +5739,8 @@ const VariablePlugin = () => {
5148
5739
  INSERT_VARIABLE_COMMAND,
5149
5740
  (variableKey) => {
5150
5741
  editor.update(() => {
5151
- const selection = lexical.$getSelection();
5152
- if (!lexical.$isRangeSelection(selection)) return;
5742
+ const selection2 = lexical.$getSelection();
5743
+ if (!lexical.$isRangeSelection(selection2)) return;
5153
5744
  const variableNode = $createVariableNode(variableKey);
5154
5745
  lexical.$insertNodes([variableNode]);
5155
5746
  });
@@ -5400,7 +5991,11 @@ const VariablePanelStateProvider = ({ children }) => {
5400
5991
  };
5401
5992
  const VariableToolbarToggle = () => {
5402
5993
  const { panelOpen, setPanelOpen } = useVariablePanelState();
5994
+ const toolbarConfig = useToolbarConfig();
5403
5995
  const t = useTranslations();
5996
+ if (!toolbarConfig.variables.visible) {
5997
+ return null;
5998
+ }
5404
5999
  return /* @__PURE__ */ jsxRuntime.jsxs(
5405
6000
  "button",
5406
6001
  {
@@ -5413,7 +6008,7 @@ const VariableToolbarToggle = () => {
5413
6008
  "aria-label": panelOpen ? t.variables.closePanel : t.variables.openPanel,
5414
6009
  children: [
5415
6010
  /* @__PURE__ */ jsxRuntime.jsx(Braces, { size: 14 }),
5416
- "Variables"
6011
+ toolbarConfig.variables.showLabel && t.variables.title
5417
6012
  ]
5418
6013
  }
5419
6014
  );