@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
@@ -3,16 +3,18 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
5
5
  import React, { createContext, useContext, useMemo, useReducer, useState, useRef, useCallback, useEffect, forwardRef, createElement, useImperativeHandle } from "react";
6
- import { $getRoot, $createRangeSelectionFromDom, $getSelection, $isRangeSelection, $isTextNode, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $selectAll, KEY_TAB_COMMAND, COMMAND_PRIORITY_LOW, $isElementNode, $isParagraphNode, $setSelection, FOCUS_COMMAND, $splitNode, $getNearestNodeFromDOMNode, CONTROLLED_TEXT_INSERTION_COMMAND, KEY_DOWN_COMMAND, PASTE_COMMAND, KEY_ENTER_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_CRITICAL, $applyNodeReplacement, DecoratorNode, createCommand, $insertNodes, COMMAND_PRIORITY_EDITOR } from "lexical";
6
+ import { $getRoot, $createRangeSelectionFromDom, $getSelection, $isRangeSelection, $isTextNode, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, OUTDENT_CONTENT_COMMAND, INDENT_CONTENT_COMMAND, $createParagraphNode, $applyNodeReplacement, DecoratorNode, KEY_BACKSPACE_COMMAND, COMMAND_PRIORITY_LOW, KEY_DELETE_COMMAND, $isNodeSelection, $getNodeByKey, $selectAll, SELECTION_CHANGE_COMMAND, KEY_TAB_COMMAND, $isElementNode, $isParagraphNode, $setSelection, FOCUS_COMMAND, $splitNode, $getNearestNodeFromDOMNode, CONTROLLED_TEXT_INSERTION_COMMAND, KEY_DOWN_COMMAND, PASTE_COMMAND, KEY_ENTER_COMMAND, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_CRITICAL, $insertNodes, $createLineBreakNode, $createTextNode, createCommand, COMMAND_PRIORITY_EDITOR } from "lexical";
7
7
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
8
- import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListNode, ListItemNode, $isListNode } from "@lexical/list";
8
+ import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListNode, ListItemNode, $isListNode, $createListItemNode, $createListNode } from "@lexical/list";
9
+ import { $setBlocksType } from "@lexical/selection";
10
+ import { $createHeadingNode, $isHeadingNode, HeadingNode, QuoteNode, $createQuoteNode } from "@lexical/rich-text";
11
+ import { useLexicalNodeSelection } from "@lexical/react/useLexicalNodeSelection";
9
12
  import { LexicalComposer } from "@lexical/react/LexicalComposer";
10
13
  import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
11
14
  import { ContentEditable } from "@lexical/react/LexicalContentEditable";
12
15
  import { ListPlugin } from "@lexical/react/LexicalListPlugin";
13
16
  import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
14
17
  import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
15
- import { HeadingNode, QuoteNode } from "@lexical/rich-text";
16
18
  function createEmptyPage(id) {
17
19
  return {
18
20
  id: id ?? crypto.randomUUID(),
@@ -471,6 +473,15 @@ const DEFAULT_TRANSLATIONS = {
471
473
  bulletList: "Bullet List",
472
474
  indent: "Indent",
473
475
  outdent: "Outdent",
476
+ history: "History",
477
+ blockType: "Block type",
478
+ paragraph: "Paragraph",
479
+ heading1: "Heading 1",
480
+ heading2: "Heading 2",
481
+ heading3: "Heading 3",
482
+ heading4: "Heading 4",
483
+ heading5: "Heading 5",
484
+ heading6: "Heading 6",
474
485
  openHistory: "Open History",
475
486
  closeHistory: "Close History"
476
487
  },
@@ -502,7 +513,9 @@ const DEFAULT_TRANSLATIONS = {
502
513
  indentedContent: "Indented content",
503
514
  outdentedContent: "Outdented content",
504
515
  fontChanged: "Font changed to {{value}}",
505
- fontSizeChanged: "Font size changed to {{value}}pt"
516
+ fontSizeChanged: "Font size changed to {{value}}pt",
517
+ blockTypeChanged: "Block type changed to {{value}}",
518
+ insertedDocumentContent: "Inserted document content"
506
519
  }
507
520
  },
508
521
  variables: {
@@ -548,7 +561,20 @@ const DEFAULT_TRANSLATIONS = {
548
561
  page: "Page {{page}}"
549
562
  },
550
563
  headerFooter: {
551
- label: "Headers & Footers"
564
+ label: "Headers & Footers",
565
+ settingsLabel: "Header and footer settings",
566
+ pageCounter: "Page counter",
567
+ pageCounterModes: {
568
+ none: "None",
569
+ header: "Header",
570
+ footer: "Footer",
571
+ both: "Both"
572
+ },
573
+ headerSection: "Header",
574
+ footerSection: "Footer",
575
+ copyToAllPages: "Copy to all pages",
576
+ clearThisPage: "Clear this page",
577
+ clearAll: "Clear all"
552
578
  },
553
579
  sidebar: {
554
580
  close: "Close sidebar"
@@ -589,6 +615,15 @@ const PT_BR_TRANSLATIONS = {
589
615
  bulletList: "Lista com Marcadores",
590
616
  indent: "Aumentar Recuo",
591
617
  outdent: "Diminuir Recuo",
618
+ history: "Histórico",
619
+ blockType: "Tipo de bloco",
620
+ paragraph: "Parágrafo",
621
+ heading1: "Título 1",
622
+ heading2: "Título 2",
623
+ heading3: "Título 3",
624
+ heading4: "Título 4",
625
+ heading5: "Título 5",
626
+ heading6: "Título 6",
592
627
  openHistory: "Abrir Histórico",
593
628
  closeHistory: "Fechar Histórico"
594
629
  },
@@ -620,7 +655,9 @@ const PT_BR_TRANSLATIONS = {
620
655
  indentedContent: "Conteúdo recuado",
621
656
  outdentedContent: "Recuo reduzido",
622
657
  fontChanged: "Fonte alterada para {{value}}",
623
- fontSizeChanged: "Tamanho da fonte alterado para {{value}}pt"
658
+ fontSizeChanged: "Tamanho da fonte alterado para {{value}}pt",
659
+ blockTypeChanged: "Tipo de bloco alterado para {{value}}",
660
+ insertedDocumentContent: "Conteúdo do documento inserido"
624
661
  }
625
662
  },
626
663
  variables: {
@@ -666,7 +703,20 @@ const PT_BR_TRANSLATIONS = {
666
703
  page: "Página {{page}}"
667
704
  },
668
705
  headerFooter: {
669
- label: "Cabeçalhos e Rodapés"
706
+ label: "Cabeçalhos e Rodapés",
707
+ settingsLabel: "Configurações de cabeçalho e rodapé",
708
+ pageCounter: "Contador de páginas",
709
+ pageCounterModes: {
710
+ none: "Nenhum",
711
+ header: "Cabeçalho",
712
+ footer: "Rodapé",
713
+ both: "Ambos"
714
+ },
715
+ headerSection: "Cabeçalho",
716
+ footerSection: "Rodapé",
717
+ copyToAllPages: "Copiar para todas as páginas",
718
+ clearThisPage: "Limpar esta página",
719
+ clearAll: "Limpar tudo"
670
720
  },
671
721
  sidebar: {
672
722
  close: "Fechar barra lateral"
@@ -1258,6 +1308,7 @@ const DocumentProvider = ({
1258
1308
  activePageId,
1259
1309
  setActivePageId,
1260
1310
  activeEditor: activeEditorRef.current,
1311
+ activeCaretPosition: activeCaretPositionRef.current,
1261
1312
  consumePendingCaretPosition,
1262
1313
  consumePendingFocusAtEnd,
1263
1314
  requestFocusAtEnd,
@@ -1864,6 +1915,36 @@ const HistorySidebar = () => {
1864
1915
  }
1865
1916
  );
1866
1917
  };
1918
+ const DEFAULT_TOOLBAR_CONTROL_CONFIG = {
1919
+ visible: true,
1920
+ showLabel: true
1921
+ };
1922
+ const DEFAULT_TOOLBAR_CONFIG = {
1923
+ history: DEFAULT_TOOLBAR_CONTROL_CONFIG,
1924
+ variables: DEFAULT_TOOLBAR_CONTROL_CONFIG,
1925
+ headerFooter: DEFAULT_TOOLBAR_CONTROL_CONFIG
1926
+ };
1927
+ const ToolbarConfigContext = createContext(DEFAULT_TOOLBAR_CONFIG);
1928
+ function resolveControlConfig(config) {
1929
+ return {
1930
+ visible: (config == null ? void 0 : config.visible) ?? true,
1931
+ showLabel: (config == null ? void 0 : config.showLabel) ?? true
1932
+ };
1933
+ }
1934
+ function normalizeToolbarConfig(config) {
1935
+ return {
1936
+ history: resolveControlConfig(config == null ? void 0 : config.history),
1937
+ variables: resolveControlConfig(config == null ? void 0 : config.variables),
1938
+ headerFooter: resolveControlConfig(config == null ? void 0 : config.headerFooter)
1939
+ };
1940
+ }
1941
+ const ToolbarConfigProvider = ({ toolbar, children }) => {
1942
+ const resolvedConfig = useMemo(() => normalizeToolbarConfig(toolbar), [toolbar]);
1943
+ return /* @__PURE__ */ jsx(ToolbarConfigContext.Provider, { value: resolvedConfig, children });
1944
+ };
1945
+ function useToolbarConfig() {
1946
+ return useContext(ToolbarConfigContext);
1947
+ }
1867
1948
  function resolveExtensions(extensions) {
1868
1949
  const resolved = {
1869
1950
  nodes: [],
@@ -1996,6 +2077,7 @@ const SUPPORTED_FONT_SIZES = [
1996
2077
  48,
1997
2078
  72
1998
2079
  ];
2080
+ const DEFAULT_FONT_SIZE = 12;
1999
2081
  function applyFontSize(editor, size) {
2000
2082
  editor.update(() => {
2001
2083
  const selection = $getSelection();
@@ -2046,9 +2128,366 @@ function indentContent(editor) {
2046
2128
  function outdentContent(editor) {
2047
2129
  editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, void 0);
2048
2130
  }
2131
+ function setBlockType(editor, blockType) {
2132
+ editor.update(() => {
2133
+ const selection = $getSelection();
2134
+ if (!$isRangeSelection(selection)) {
2135
+ return;
2136
+ }
2137
+ if (blockType === "paragraph") {
2138
+ $setBlocksType(selection, () => $createParagraphNode());
2139
+ return;
2140
+ }
2141
+ $setBlocksType(selection, () => $createHeadingNode(blockType));
2142
+ });
2143
+ }
2144
+ function getActiveBlockType(editor) {
2145
+ let blockType = "paragraph";
2146
+ editor.getEditorState().read(() => {
2147
+ const selection = $getSelection();
2148
+ if (!$isRangeSelection(selection)) {
2149
+ return;
2150
+ }
2151
+ const anchorNode = selection.anchor.getNode();
2152
+ const topLevelElement = anchorNode.getTopLevelElementOrThrow();
2153
+ if ($isHeadingNode(topLevelElement)) {
2154
+ blockType = topLevelElement.getTag();
2155
+ }
2156
+ });
2157
+ return blockType;
2158
+ }
2159
+ function extractStyleValue(style, property) {
2160
+ const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2161
+ const match = style.match(new RegExp(`${escapedProperty}:\\s*([^;]+)`));
2162
+ return match ? match[1].trim().replace(/['"]/g, "") : void 0;
2163
+ }
2164
+ function mergeStyleDeclaration(existingStyle, property, value) {
2165
+ const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2166
+ const stripped = existingStyle.replace(
2167
+ new RegExp(`${escapedProperty}:\\s*[^;]+;?\\s*`, "g"),
2168
+ ""
2169
+ ).trim();
2170
+ const declaration = `${property}: ${value}`;
2171
+ return stripped ? `${stripped}; ${declaration}` : declaration;
2172
+ }
2173
+ function extractFontFamilyFromStyle(style) {
2174
+ return extractStyleValue(style, "font-family");
2175
+ }
2176
+ function extractFontSizePtFromStyle(style) {
2177
+ const value = extractStyleValue(style, "font-size");
2178
+ if (!value) {
2179
+ return void 0;
2180
+ }
2181
+ const match = value.match(/^(\d+(?:\.\d+)?)\s*pt$/);
2182
+ return match ? parseFloat(match[1]) : void 0;
2183
+ }
2184
+ function mergeFontFamilyStyle(existingStyle, fontFamily) {
2185
+ return mergeStyleDeclaration(existingStyle, "font-family", fontFamily);
2186
+ }
2187
+ function mergeFontSizeStyle(existingStyle, size) {
2188
+ return mergeStyleDeclaration(existingStyle, "font-size", `${size}pt`);
2189
+ }
2190
+ const EMPTY_CONTEXT = {
2191
+ definitions: [],
2192
+ refreshDefinitions: () => {
2193
+ },
2194
+ getDefinition: () => void 0
2195
+ };
2196
+ const VariableContext = createContext(EMPTY_CONTEXT);
2197
+ const VariableProvider = ({
2198
+ initialDefinitions = [],
2199
+ children
2200
+ }) => {
2201
+ const [definitions, setDefinitions] = useState(initialDefinitions);
2202
+ const refresh = useCallback((newDefinitions) => {
2203
+ setDefinitions(newDefinitions);
2204
+ }, []);
2205
+ const getDefinition = useCallback(
2206
+ (key) => {
2207
+ return definitions.find((d) => d.key === key);
2208
+ },
2209
+ [definitions]
2210
+ );
2211
+ const value = useMemo(
2212
+ () => ({ definitions, refreshDefinitions: refresh, getDefinition }),
2213
+ [definitions, refresh, getDefinition]
2214
+ );
2215
+ return /* @__PURE__ */ jsx(VariableContext.Provider, { value, children });
2216
+ };
2217
+ function useVariables() {
2218
+ return useContext(VariableContext);
2219
+ }
2220
+ class VariableNode extends DecoratorNode {
2221
+ constructor(variableKey, format = 0, style = "", key) {
2222
+ super(key);
2223
+ __publicField(this, "__variableKey");
2224
+ __publicField(this, "__format");
2225
+ __publicField(this, "__style");
2226
+ this.__variableKey = variableKey;
2227
+ this.__format = format;
2228
+ this.__style = style;
2229
+ }
2230
+ static getType() {
2231
+ return "variable-node";
2232
+ }
2233
+ static clone(node) {
2234
+ return new VariableNode(node.__variableKey, node.__format, node.__style, node.__key);
2235
+ }
2236
+ getVariableKey() {
2237
+ return this.getLatest().__variableKey;
2238
+ }
2239
+ getFormat() {
2240
+ return this.getLatest().__format;
2241
+ }
2242
+ setFormat(format) {
2243
+ const writable = this.getWritable();
2244
+ writable.__format = format;
2245
+ return writable;
2246
+ }
2247
+ getStyle() {
2248
+ return this.getLatest().__style;
2249
+ }
2250
+ setStyle(style) {
2251
+ const writable = this.getWritable();
2252
+ writable.__style = style;
2253
+ return writable;
2254
+ }
2255
+ // -- Serialization --
2256
+ static importJSON(serializedNode) {
2257
+ return $createVariableNode(
2258
+ serializedNode.variableKey,
2259
+ serializedNode.format ?? 0,
2260
+ serializedNode.style ?? ""
2261
+ );
2262
+ }
2263
+ exportJSON() {
2264
+ return {
2265
+ type: "variable-node",
2266
+ version: 1,
2267
+ variableKey: this.__variableKey,
2268
+ format: this.__format,
2269
+ style: this.__style
2270
+ };
2271
+ }
2272
+ // -- DOM --
2273
+ createDOM() {
2274
+ const span = document.createElement("span");
2275
+ span.className = "lex4-variable";
2276
+ span.setAttribute("data-variable-key", this.__variableKey);
2277
+ span.setAttribute("data-testid", `variable-${this.__variableKey}`);
2278
+ span.contentEditable = "false";
2279
+ return span;
2280
+ }
2281
+ updateDOM() {
2282
+ return false;
2283
+ }
2284
+ exportDOM() {
2285
+ const span = document.createElement("span");
2286
+ span.setAttribute("data-variable-key", this.__variableKey);
2287
+ span.textContent = `{{${this.__variableKey}}}`;
2288
+ return { element: span };
2289
+ }
2290
+ static importDOM() {
2291
+ return null;
2292
+ }
2293
+ // -- Behavior --
2294
+ isInline() {
2295
+ return true;
2296
+ }
2297
+ isKeyboardSelectable() {
2298
+ return true;
2299
+ }
2300
+ getTextContent() {
2301
+ return `{{${this.__variableKey}}}`;
2302
+ }
2303
+ // -- Rendering --
2304
+ decorate() {
2305
+ return /* @__PURE__ */ jsx(
2306
+ VariableChip,
2307
+ {
2308
+ nodeKey: this.__key,
2309
+ variableKey: this.__variableKey,
2310
+ format: this.__format,
2311
+ styleValue: this.__style
2312
+ }
2313
+ );
2314
+ }
2315
+ }
2316
+ function VariableChip({
2317
+ nodeKey,
2318
+ variableKey,
2319
+ format,
2320
+ styleValue
2321
+ }) {
2322
+ const { getDefinition } = useVariables();
2323
+ const [editor] = useLexicalComposerContext();
2324
+ const [isSelected, setSelected, clearOtherSelections] = useLexicalNodeSelection(nodeKey);
2325
+ const def = getDefinition(variableKey);
2326
+ const label = (def == null ? void 0 : def.label) ?? variableKey;
2327
+ const group = def == null ? void 0 : def.group;
2328
+ const style = useMemo(() => {
2329
+ const fontFamily = extractFontFamilyFromStyle(styleValue);
2330
+ const fontSize = extractFontSizePtFromStyle(styleValue);
2331
+ return {
2332
+ ...fontFamily ? { fontFamily } : {},
2333
+ ...fontSize ? { fontSize: `${fontSize}pt` } : {}
2334
+ };
2335
+ }, [styleValue]);
2336
+ const className = [
2337
+ "lex4-variable-chip",
2338
+ isSelected && "lex4-variable-chip-selected",
2339
+ format & 1 ? "lex4-text-bold" : "",
2340
+ format & 2 ? "lex4-text-italic" : "",
2341
+ format & 8 ? "lex4-text-underline" : "",
2342
+ format & 4 ? "lex4-text-strikethrough" : ""
2343
+ ].filter(Boolean).join(" ");
2344
+ const handleClick = useCallback((event) => {
2345
+ event.preventDefault();
2346
+ if (!event.shiftKey) {
2347
+ clearOtherSelections();
2348
+ }
2349
+ setSelected(!isSelected);
2350
+ }, [clearOtherSelections, isSelected, setSelected]);
2351
+ useEffect(() => {
2352
+ const removeSelectedNodes = () => {
2353
+ editor.update(() => {
2354
+ const selection = $getSelection();
2355
+ if (!$isNodeSelection(selection)) {
2356
+ const node = $getNodeByKey(nodeKey);
2357
+ if ($isVariableNode(node)) {
2358
+ node.remove();
2359
+ }
2360
+ return;
2361
+ }
2362
+ for (const node of selection.getNodes()) {
2363
+ if ($isVariableNode(node)) {
2364
+ node.remove();
2365
+ }
2366
+ }
2367
+ });
2368
+ };
2369
+ const unregisterBackspace = editor.registerCommand(
2370
+ KEY_BACKSPACE_COMMAND,
2371
+ () => {
2372
+ if (!isSelected) {
2373
+ return false;
2374
+ }
2375
+ removeSelectedNodes();
2376
+ return true;
2377
+ },
2378
+ COMMAND_PRIORITY_LOW
2379
+ );
2380
+ const unregisterDelete = editor.registerCommand(
2381
+ KEY_DELETE_COMMAND,
2382
+ () => {
2383
+ if (!isSelected) {
2384
+ return false;
2385
+ }
2386
+ removeSelectedNodes();
2387
+ return true;
2388
+ },
2389
+ COMMAND_PRIORITY_LOW
2390
+ );
2391
+ return () => {
2392
+ unregisterBackspace();
2393
+ unregisterDelete();
2394
+ };
2395
+ }, [editor, isSelected, nodeKey]);
2396
+ return /* @__PURE__ */ jsx(
2397
+ "span",
2398
+ {
2399
+ className,
2400
+ "data-testid": `variable-chip-${variableKey}`,
2401
+ "data-variable-group": group,
2402
+ title: variableKey,
2403
+ style,
2404
+ onMouseDown: (event) => event.preventDefault(),
2405
+ onClick: handleClick,
2406
+ children: label
2407
+ }
2408
+ );
2409
+ }
2410
+ function $createVariableNode(variableKey, format = 0, style = "") {
2411
+ return $applyNodeReplacement(new VariableNode(variableKey, format, style));
2412
+ }
2413
+ function $isVariableNode(node) {
2414
+ return node instanceof VariableNode;
2415
+ }
2416
+ const FORMAT_MASKS = {
2417
+ bold: 1,
2418
+ italic: 2,
2419
+ strikethrough: 4,
2420
+ underline: 8
2421
+ };
2422
+ function withSelectedVariableNodes(editor, updater) {
2423
+ let updated = false;
2424
+ editor.update(() => {
2425
+ const selection = $getSelection();
2426
+ if (!$isNodeSelection(selection)) {
2427
+ return;
2428
+ }
2429
+ const nodes = selection.getNodes().filter($isVariableNode);
2430
+ if (nodes.length === 0) {
2431
+ return;
2432
+ }
2433
+ updater(nodes);
2434
+ updated = true;
2435
+ });
2436
+ return updated;
2437
+ }
2438
+ function getSelectedVariableNodes(editor) {
2439
+ let nodes = [];
2440
+ editor.getEditorState().read(() => {
2441
+ const selection = $getSelection();
2442
+ if (!$isNodeSelection(selection)) {
2443
+ return;
2444
+ }
2445
+ nodes = selection.getNodes().filter($isVariableNode);
2446
+ });
2447
+ return nodes;
2448
+ }
2449
+ function toggleSelectedVariableFormat(editor, format) {
2450
+ const mask = FORMAT_MASKS[format];
2451
+ if (!mask) {
2452
+ return false;
2453
+ }
2454
+ return withSelectedVariableNodes(editor, (nodes) => {
2455
+ const shouldEnable = nodes.some((node) => (node.getFormat() & mask) === 0);
2456
+ for (const node of nodes) {
2457
+ const nextFormat = shouldEnable ? node.getFormat() | mask : node.getFormat() & ~mask;
2458
+ node.setFormat(nextFormat);
2459
+ }
2460
+ });
2461
+ }
2462
+ function applyFontFamilyToSelectedVariables(editor, fontFamily) {
2463
+ return withSelectedVariableNodes(editor, (nodes) => {
2464
+ for (const node of nodes) {
2465
+ node.setStyle(mergeFontFamilyStyle(node.getStyle(), fontFamily));
2466
+ }
2467
+ });
2468
+ }
2469
+ function applyFontSizeToSelectedVariables(editor, size) {
2470
+ return withSelectedVariableNodes(editor, (nodes) => {
2471
+ for (const node of nodes) {
2472
+ node.setStyle(mergeFontSizeStyle(node.getStyle(), size));
2473
+ }
2474
+ });
2475
+ }
2476
+ function readSelectedVariableFormatting(editor) {
2477
+ const firstNode = getSelectedVariableNodes(editor)[0];
2478
+ if (!firstNode) {
2479
+ return {};
2480
+ }
2481
+ const style = firstNode.getStyle();
2482
+ return {
2483
+ fontFamily: extractFontFamilyFromStyle(style),
2484
+ fontSize: extractFontSizePtFromStyle(style)
2485
+ };
2486
+ }
2049
2487
  const HeaderFooterToggle = ({
2050
2488
  enabled,
2051
- onToggle
2489
+ onToggle,
2490
+ showLabel = true
2052
2491
  }) => {
2053
2492
  const t = useTranslations();
2054
2493
  return /* @__PURE__ */ jsxs(
@@ -2058,18 +2497,20 @@ const HeaderFooterToggle = ({
2058
2497
  "data-testid": "header-footer-toggle",
2059
2498
  children: [
2060
2499
  /* @__PURE__ */ jsx(FileText, { size: 14, className: "lex4-hf-toggle-icon" }),
2061
- /* @__PURE__ */ jsx("span", { className: "lex4-hf-toggle-label", children: t.headerFooter.label }),
2500
+ showLabel && /* @__PURE__ */ jsx("span", { className: "lex4-hf-toggle-label", children: t.headerFooter.label }),
2062
2501
  /* @__PURE__ */ jsx(
2063
2502
  "button",
2064
2503
  {
2065
2504
  type: "button",
2066
2505
  role: "switch",
2067
2506
  "aria-checked": enabled,
2507
+ "aria-label": t.headerFooter.label,
2068
2508
  onMouseDown: (e) => e.preventDefault(),
2069
2509
  onClick: () => onToggle(!enabled),
2070
2510
  className: "lex4-hf-switch",
2071
2511
  style: { backgroundColor: enabled ? "var(--color-primary)" : "var(--color-muted)" },
2072
2512
  "data-testid": "header-footer-switch",
2513
+ title: t.headerFooter.label,
2073
2514
  children: /* @__PURE__ */ jsx("span", { className: "lex4-hf-switch-knob" })
2074
2515
  }
2075
2516
  )
@@ -2077,12 +2518,7 @@ const HeaderFooterToggle = ({
2077
2518
  }
2078
2519
  );
2079
2520
  };
2080
- const PAGE_COUNTER_OPTIONS = [
2081
- { value: "none", label: "None" },
2082
- { value: "header", label: "Header" },
2083
- { value: "footer", label: "Footer" },
2084
- { value: "both", label: "Both" }
2085
- ];
2521
+ const PAGE_COUNTER_OPTIONS = ["none", "header", "footer", "both"];
2086
2522
  const HeaderFooterActions = ({
2087
2523
  activePageId,
2088
2524
  pageCounterMode,
@@ -2097,6 +2533,7 @@ const HeaderFooterActions = ({
2097
2533
  const [open, setOpen] = useState(false);
2098
2534
  const containerRef = useRef(null);
2099
2535
  const hasActivePage = activePageId !== null;
2536
+ const t = useTranslations();
2100
2537
  const close = useCallback(() => setOpen(false), []);
2101
2538
  useEffect(() => {
2102
2539
  if (!open) return;
@@ -2123,8 +2560,8 @@ const HeaderFooterActions = ({
2123
2560
  "data-testid": "header-footer-menu-trigger",
2124
2561
  "aria-expanded": open,
2125
2562
  "aria-haspopup": "true",
2126
- "aria-label": "Header and footer settings",
2127
- title: "Header and footer settings",
2563
+ "aria-label": t.headerFooter.settingsLabel,
2564
+ title: t.headerFooter.settingsLabel,
2128
2565
  children: /* @__PURE__ */ jsx(Settings2, { size: 14 })
2129
2566
  }
2130
2567
  ),
@@ -2135,8 +2572,8 @@ const HeaderFooterActions = ({
2135
2572
  role: "menu",
2136
2573
  "data-testid": "header-footer-menu",
2137
2574
  children: [
2138
- /* @__PURE__ */ jsx(SectionLabel, { children: "Page counter" }),
2139
- /* @__PURE__ */ jsx("div", { className: "lex4-settings-counter-grid", "data-testid": "page-counter-mode", children: PAGE_COUNTER_OPTIONS.map(({ value, label }) => /* @__PURE__ */ jsx(
2575
+ /* @__PURE__ */ jsx(SectionLabel, { children: t.headerFooter.pageCounter }),
2576
+ /* @__PURE__ */ jsx("div", { className: "lex4-settings-counter-grid", "data-testid": "page-counter-mode", children: PAGE_COUNTER_OPTIONS.map((value) => /* @__PURE__ */ jsx(
2140
2577
  "button",
2141
2578
  {
2142
2579
  type: "button",
@@ -2146,17 +2583,17 @@ const HeaderFooterActions = ({
2146
2583
  onClick: () => onPageCounterModeChange(value),
2147
2584
  className: "lex4-settings-counter-btn",
2148
2585
  "data-testid": `page-counter-${value}`,
2149
- children: label
2586
+ children: t.headerFooter.pageCounterModes[value]
2150
2587
  },
2151
2588
  value
2152
2589
  )) }),
2153
2590
  /* @__PURE__ */ jsx(MenuDivider, {}),
2154
- /* @__PURE__ */ jsx(SectionLabel, { children: "Header" }),
2591
+ /* @__PURE__ */ jsx(SectionLabel, { children: t.headerFooter.headerSection }),
2155
2592
  /* @__PURE__ */ jsx(
2156
2593
  MenuItem,
2157
2594
  {
2158
2595
  icon: /* @__PURE__ */ jsx(CopyPlus, { size: 14 }),
2159
- label: "Copy to all pages",
2596
+ label: t.headerFooter.copyToAllPages,
2160
2597
  onClick: () => handleAction(onCopyHeaderToAll),
2161
2598
  disabled: !hasActivePage,
2162
2599
  testId: "copy-header-all"
@@ -2166,7 +2603,7 @@ const HeaderFooterActions = ({
2166
2603
  MenuItem,
2167
2604
  {
2168
2605
  icon: /* @__PURE__ */ jsx(Eraser, { size: 14 }),
2169
- label: "Clear this page",
2606
+ label: t.headerFooter.clearThisPage,
2170
2607
  onClick: () => handleAction(onClearHeader),
2171
2608
  disabled: !hasActivePage,
2172
2609
  testId: "clear-header"
@@ -2176,18 +2613,18 @@ const HeaderFooterActions = ({
2176
2613
  MenuItem,
2177
2614
  {
2178
2615
  icon: /* @__PURE__ */ jsx(Trash2, { size: 14 }),
2179
- label: "Clear all",
2616
+ label: t.headerFooter.clearAll,
2180
2617
  onClick: () => handleAction(onClearAllHeaders),
2181
2618
  testId: "clear-all-headers"
2182
2619
  }
2183
2620
  ),
2184
2621
  /* @__PURE__ */ jsx(MenuDivider, {}),
2185
- /* @__PURE__ */ jsx(SectionLabel, { children: "Footer" }),
2622
+ /* @__PURE__ */ jsx(SectionLabel, { children: t.headerFooter.footerSection }),
2186
2623
  /* @__PURE__ */ jsx(
2187
2624
  MenuItem,
2188
2625
  {
2189
2626
  icon: /* @__PURE__ */ jsx(CopyPlus, { size: 14 }),
2190
- label: "Copy to all pages",
2627
+ label: t.headerFooter.copyToAllPages,
2191
2628
  onClick: () => handleAction(onCopyFooterToAll),
2192
2629
  disabled: !hasActivePage,
2193
2630
  testId: "copy-footer-all"
@@ -2197,7 +2634,7 @@ const HeaderFooterActions = ({
2197
2634
  MenuItem,
2198
2635
  {
2199
2636
  icon: /* @__PURE__ */ jsx(Eraser, { size: 14 }),
2200
- label: "Clear this page",
2637
+ label: t.headerFooter.clearThisPage,
2201
2638
  onClick: () => handleAction(onClearFooter),
2202
2639
  disabled: !hasActivePage,
2203
2640
  testId: "clear-footer"
@@ -2207,7 +2644,7 @@ const HeaderFooterActions = ({
2207
2644
  MenuItem,
2208
2645
  {
2209
2646
  icon: /* @__PURE__ */ jsx(Trash2, { size: 14 }),
2210
- label: "Clear all",
2647
+ label: t.headerFooter.clearAll,
2211
2648
  onClick: () => handleAction(onClearAllFooters),
2212
2649
  testId: "clear-all-footers"
2213
2650
  }
@@ -2237,6 +2674,7 @@ const MenuItem = ({ icon, label, onClick, disabled, testId }) => /* @__PURE__ */
2237
2674
  const MenuDivider = () => /* @__PURE__ */ jsx("div", { className: "lex4-settings-divider" });
2238
2675
  const CanvasControls = () => {
2239
2676
  const { document: document2, dispatch, activePageId, runHistoryAction } = useDocument();
2677
+ const toolbarConfig = useToolbarConfig();
2240
2678
  const t = useTranslations();
2241
2679
  const runCanvasAction = useCallback((label, callback) => {
2242
2680
  runHistoryAction(
@@ -2303,12 +2741,16 @@ const CanvasControls = () => {
2303
2741
  dispatch({ type: "SET_PAGE_COUNTER_MODE", mode });
2304
2742
  });
2305
2743
  }, [dispatch, runCanvasAction, t.history.actions.pageCounterSet]);
2744
+ if (!toolbarConfig.headerFooter.visible) {
2745
+ return null;
2746
+ }
2306
2747
  return /* @__PURE__ */ jsxs("div", { className: "lex4-toolbar-group lex4-toolbar-group-gap", "data-testid": "header-footer-controls", children: [
2307
2748
  /* @__PURE__ */ jsx(
2308
2749
  HeaderFooterToggle,
2309
2750
  {
2310
2751
  enabled: document2.headerFooterEnabled,
2311
- onToggle: handleToggle
2752
+ onToggle: handleToggle,
2753
+ showLabel: toolbarConfig.headerFooter.showLabel
2312
2754
  }
2313
2755
  ),
2314
2756
  document2.headerFooterEnabled && /* @__PURE__ */ jsx(
@@ -2341,7 +2783,17 @@ const Toolbar = () => {
2341
2783
  undo
2342
2784
  } = useDocument();
2343
2785
  const { toolbarItems, toolbarEndItems } = useExtensions();
2786
+ const toolbarConfig = useToolbarConfig();
2344
2787
  const t = useTranslations();
2788
+ const [activeBlockType, setActiveBlockType] = useState("paragraph");
2789
+ const [activeFontFamily, setActiveFontFamily] = useState("Calibri");
2790
+ const [activeFontSize, setActiveFontSize] = useState(DEFAULT_FONT_SIZE);
2791
+ const normalizeFontFamily = useCallback((fontFamily) => {
2792
+ if (fontFamily && SUPPORTED_FONTS.includes(fontFamily)) {
2793
+ return fontFamily;
2794
+ }
2795
+ return "Calibri";
2796
+ }, []);
2345
2797
  const withBodySelection = useCallback(
2346
2798
  (editor, action) => {
2347
2799
  editor.update(() => {
@@ -2377,27 +2829,91 @@ const Toolbar = () => {
2377
2829
  },
2378
2830
  [runHistoryAction]
2379
2831
  );
2832
+ useEffect(() => {
2833
+ if (!activeEditor) {
2834
+ setActiveBlockType("paragraph");
2835
+ setActiveFontFamily("Calibri");
2836
+ setActiveFontSize(DEFAULT_FONT_SIZE);
2837
+ return;
2838
+ }
2839
+ const updateSelectionState = () => {
2840
+ const selectedVariables = getSelectedVariableNodes(activeEditor);
2841
+ if (selectedVariables.length > 0) {
2842
+ const formatting = readSelectedVariableFormatting(activeEditor);
2843
+ setActiveBlockType("paragraph");
2844
+ setActiveFontFamily(normalizeFontFamily(formatting.fontFamily));
2845
+ setActiveFontSize(formatting.fontSize ?? DEFAULT_FONT_SIZE);
2846
+ return;
2847
+ }
2848
+ setActiveBlockType(getActiveBlockType(activeEditor));
2849
+ let nextFontFamily = "Calibri";
2850
+ let nextFontSize = DEFAULT_FONT_SIZE;
2851
+ activeEditor.getEditorState().read(() => {
2852
+ const selection = $getSelection();
2853
+ if (!$isRangeSelection(selection)) {
2854
+ return;
2855
+ }
2856
+ const textNode = selection.getNodes().find($isTextNode);
2857
+ if (!textNode) {
2858
+ return;
2859
+ }
2860
+ const style = textNode.getStyle();
2861
+ nextFontFamily = normalizeFontFamily(extractFontFamilyFromStyle(style));
2862
+ nextFontSize = extractFontSizePtFromStyle(style) ?? DEFAULT_FONT_SIZE;
2863
+ });
2864
+ setActiveFontFamily(nextFontFamily);
2865
+ setActiveFontSize(nextFontSize);
2866
+ };
2867
+ updateSelectionState();
2868
+ const unregisterSelectionChange = activeEditor.registerCommand(
2869
+ SELECTION_CHANGE_COMMAND,
2870
+ () => {
2871
+ updateSelectionState();
2872
+ return false;
2873
+ },
2874
+ COMMAND_PRIORITY_LOW
2875
+ );
2876
+ const unregisterUpdateListener = activeEditor.registerUpdateListener(() => {
2877
+ updateSelectionState();
2878
+ });
2879
+ return () => {
2880
+ unregisterSelectionChange();
2881
+ unregisterUpdateListener();
2882
+ };
2883
+ }, [activeEditor, normalizeFontFamily]);
2380
2884
  const handleBold = useCallback(() => {
2381
2885
  debug("toolbar", `bold (globalSelection=${globalSelectionActive}, editors=${editorRegistry.all().length}, hasEditor=${!!activeEditor})`);
2382
2886
  runToolbarAction(t.history.actions.boldApplied, () => {
2887
+ if (activeEditor && toggleSelectedVariableFormat(activeEditor, "bold")) {
2888
+ return;
2889
+ }
2383
2890
  applyToBodyEditors(toggleBold);
2384
2891
  });
2385
2892
  }, [activeEditor, applyToBodyEditors, editorRegistry, globalSelectionActive, runToolbarAction, t.history.actions.boldApplied]);
2386
2893
  const handleItalic = useCallback(() => {
2387
2894
  debug("toolbar", `italic (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
2388
2895
  runToolbarAction(t.history.actions.italicApplied, () => {
2896
+ if (activeEditor && toggleSelectedVariableFormat(activeEditor, "italic")) {
2897
+ return;
2898
+ }
2389
2899
  applyToBodyEditors(toggleItalic);
2390
2900
  });
2391
2901
  }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction, t.history.actions.italicApplied]);
2392
2902
  const handleUnderline = useCallback(() => {
2393
2903
  debug("toolbar", `underline (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
2394
2904
  runToolbarAction(t.history.actions.underlineApplied, () => {
2905
+ if (activeEditor && toggleSelectedVariableFormat(activeEditor, "underline")) {
2906
+ return;
2907
+ }
2395
2908
  applyToBodyEditors(toggleUnderline);
2396
2909
  });
2397
2910
  }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction, t.history.actions.underlineApplied]);
2398
2911
  const handleStrikethrough = useCallback(() => {
2399
2912
  debug("toolbar", `strikethrough (globalSelection=${globalSelectionActive}, hasEditor=${!!activeEditor})`);
2400
2913
  runToolbarAction(t.history.actions.strikethroughApplied, () => {
2914
+ if (activeEditor && toggleSelectedVariableFormat(activeEditor, "strikethrough")) {
2915
+ return;
2916
+ }
2401
2917
  applyToBodyEditors(toggleStrikethrough);
2402
2918
  });
2403
2919
  }, [activeEditor, applyToBodyEditors, globalSelectionActive, runToolbarAction, t.history.actions.strikethroughApplied]);
@@ -2443,20 +2959,59 @@ const Toolbar = () => {
2443
2959
  }, [applyToBodyEditors, runToolbarAction, t.history.actions.outdentedContent]);
2444
2960
  const handleFontChange = useCallback(
2445
2961
  (e) => {
2446
- runToolbarAction(interpolate(t.history.actions.fontChanged, { value: e.target.value }), () => {
2447
- applyToBodyEditors((editor) => applyFontFamily(editor, e.target.value));
2962
+ const fontFamily = e.target.value;
2963
+ runToolbarAction(interpolate(t.history.actions.fontChanged, { value: fontFamily }), () => {
2964
+ if (activeEditor && applyFontFamilyToSelectedVariables(activeEditor, fontFamily)) {
2965
+ return;
2966
+ }
2967
+ applyToBodyEditors((editor) => applyFontFamily(editor, fontFamily));
2448
2968
  });
2449
2969
  },
2450
- [applyToBodyEditors, runToolbarAction, t.history.actions.fontChanged]
2970
+ [activeEditor, applyToBodyEditors, runToolbarAction, t.history.actions.fontChanged]
2451
2971
  );
2452
2972
  const handleFontSizeChange = useCallback(
2453
2973
  (e) => {
2454
2974
  const size = parseInt(e.target.value, 10);
2455
2975
  runToolbarAction(interpolate(t.history.actions.fontSizeChanged, { value: String(size) }), () => {
2976
+ if (activeEditor && applyFontSizeToSelectedVariables(activeEditor, size)) {
2977
+ return;
2978
+ }
2456
2979
  applyToBodyEditors((editor) => applyFontSize(editor, size));
2457
2980
  });
2458
2981
  },
2459
- [applyToBodyEditors, runToolbarAction, t.history.actions.fontSizeChanged]
2982
+ [activeEditor, applyToBodyEditors, runToolbarAction, t.history.actions.fontSizeChanged]
2983
+ );
2984
+ const handleBlockTypeChange = useCallback(
2985
+ (e) => {
2986
+ const blockType = e.target.value;
2987
+ const labelMap = {
2988
+ paragraph: t.toolbar.paragraph,
2989
+ h1: t.toolbar.heading1,
2990
+ h2: t.toolbar.heading2,
2991
+ h3: t.toolbar.heading3,
2992
+ h4: t.toolbar.heading4,
2993
+ h5: t.toolbar.heading5,
2994
+ h6: t.toolbar.heading6
2995
+ };
2996
+ runToolbarAction(
2997
+ interpolate(t.history.actions.blockTypeChanged, { value: labelMap[blockType] }),
2998
+ () => {
2999
+ applyToBodyEditors((editor) => setBlockType(editor, blockType));
3000
+ }
3001
+ );
3002
+ },
3003
+ [
3004
+ applyToBodyEditors,
3005
+ runToolbarAction,
3006
+ t.history.actions.blockTypeChanged,
3007
+ t.toolbar.heading1,
3008
+ t.toolbar.heading2,
3009
+ t.toolbar.heading3,
3010
+ t.toolbar.heading4,
3011
+ t.toolbar.heading5,
3012
+ t.toolbar.heading6,
3013
+ t.toolbar.paragraph
3014
+ ]
2460
3015
  );
2461
3016
  const handleToggleHistory = useCallback(() => {
2462
3017
  setHistorySidebarOpen(!historySidebarOpen);
@@ -2490,6 +3045,26 @@ const Toolbar = () => {
2490
3045
  )
2491
3046
  ] }),
2492
3047
  /* @__PURE__ */ jsx(Divider, {}),
3048
+ /* @__PURE__ */ jsx("div", { className: "lex4-toolbar-group-gap lex4-toolbar-group-block", children: /* @__PURE__ */ jsxs(
3049
+ "select",
3050
+ {
3051
+ className: "lex4-toolbar-select lex4-toolbar-select-block",
3052
+ "data-testid": "block-type-selector",
3053
+ "aria-label": t.toolbar.blockType,
3054
+ value: activeBlockType,
3055
+ onChange: handleBlockTypeChange,
3056
+ children: [
3057
+ /* @__PURE__ */ jsx("option", { value: "paragraph", children: t.toolbar.paragraph }),
3058
+ /* @__PURE__ */ jsx("option", { value: "h1", children: t.toolbar.heading1 }),
3059
+ /* @__PURE__ */ jsx("option", { value: "h2", children: t.toolbar.heading2 }),
3060
+ /* @__PURE__ */ jsx("option", { value: "h3", children: t.toolbar.heading3 }),
3061
+ /* @__PURE__ */ jsx("option", { value: "h4", children: t.toolbar.heading4 }),
3062
+ /* @__PURE__ */ jsx("option", { value: "h5", children: t.toolbar.heading5 }),
3063
+ /* @__PURE__ */ jsx("option", { value: "h6", children: t.toolbar.heading6 })
3064
+ ]
3065
+ }
3066
+ ) }),
3067
+ /* @__PURE__ */ jsx(Divider, {}),
2493
3068
  /* @__PURE__ */ jsxs("div", { className: "lex4-toolbar-group-gap", children: [
2494
3069
  /* @__PURE__ */ jsx(Type, { size: 14, className: "lex4-toolbar-icon" }),
2495
3070
  /* @__PURE__ */ jsx(
@@ -2497,7 +3072,7 @@ const Toolbar = () => {
2497
3072
  {
2498
3073
  className: "lex4-toolbar-select",
2499
3074
  "data-testid": "font-selector",
2500
- defaultValue: "Calibri",
3075
+ value: activeFontFamily,
2501
3076
  onChange: handleFontChange,
2502
3077
  children: SUPPORTED_FONTS.map((font) => /* @__PURE__ */ jsx("option", { value: font, style: { fontFamily: font }, children: font }, font))
2503
3078
  }
@@ -2510,7 +3085,7 @@ const Toolbar = () => {
2510
3085
  {
2511
3086
  className: "lex4-toolbar-select lex4-toolbar-select-narrow",
2512
3087
  "data-testid": "font-size-selector",
2513
- defaultValue: "12",
3088
+ value: String(activeFontSize),
2514
3089
  onChange: handleFontSizeChange,
2515
3090
  children: SUPPORTED_FONT_SIZES.map((size) => /* @__PURE__ */ jsx("option", { value: size, children: size }, size))
2516
3091
  }
@@ -2545,7 +3120,7 @@ const Toolbar = () => {
2545
3120
  /* @__PURE__ */ jsx(CanvasControls, {}),
2546
3121
  /* @__PURE__ */ jsxs("div", { className: "lex4-toolbar-end", children: [
2547
3122
  toolbarEndItems.map((EndItem, idx) => /* @__PURE__ */ jsx(EndItem, {}, idx)),
2548
- /* @__PURE__ */ jsxs(
3123
+ toolbarConfig.history.visible && /* @__PURE__ */ jsxs(
2549
3124
  "button",
2550
3125
  {
2551
3126
  type: "button",
@@ -2557,7 +3132,7 @@ const Toolbar = () => {
2557
3132
  "aria-label": historySidebarOpen ? t.toolbar.closeHistory : t.toolbar.openHistory,
2558
3133
  children: [
2559
3134
  /* @__PURE__ */ jsx(History, { size: 14 }),
2560
- "History"
3135
+ toolbarConfig.history.showLabel && t.toolbar.history
2561
3136
  ]
2562
3137
  }
2563
3138
  )
@@ -2901,7 +3476,8 @@ const lexicalTheme = {
2901
3476
  h2: "lex4-heading lex4-heading-h2",
2902
3477
  h3: "lex4-heading lex4-heading-h3",
2903
3478
  h4: "lex4-heading lex4-heading-h4",
2904
- h5: "lex4-heading lex4-heading-h5"
3479
+ h5: "lex4-heading lex4-heading-h5",
3480
+ h6: "lex4-heading lex4-heading-h6"
2905
3481
  },
2906
3482
  text: {
2907
3483
  bold: "lex4-text-bold",
@@ -4438,12 +5014,10 @@ function decodeFormatBitmask(format) {
4438
5014
  return marks;
4439
5015
  }
4440
5016
  function extractFontFamily(style) {
4441
- const match = style.match(/font-family:\s*([^;]+)/);
4442
- return match ? match[1].trim().replace(/['"]/g, "") : void 0;
5017
+ return extractFontFamilyFromStyle(style);
4443
5018
  }
4444
5019
  function extractFontSizePt(style) {
4445
- const match = style.match(/font-size:\s*(\d+(?:\.\d+)?)\s*pt/);
4446
- return match ? parseFloat(match[1]) : void 0;
5020
+ return extractFontSizePtFromStyle(style);
4447
5021
  }
4448
5022
  function buildTextMarks(format, style) {
4449
5023
  const formatMarks = decodeFormatBitmask(format);
@@ -4477,9 +5051,11 @@ function mapTextNode(node) {
4477
5051
  };
4478
5052
  }
4479
5053
  function mapVariableNode(node) {
5054
+ const marks = buildTextMarks(node.format ?? 0, node.style);
4480
5055
  return {
4481
5056
  type: "variable",
4482
- key: node.variableKey
5057
+ key: node.variableKey,
5058
+ ...marks ? { marks } : {}
4483
5059
  };
4484
5060
  }
4485
5061
  function mapLineBreak() {
@@ -4543,7 +5119,7 @@ function mapParagraph(node) {
4543
5119
  function mapHeading(node) {
4544
5120
  var _a;
4545
5121
  const alignment = decodeAlignment(node.format);
4546
- const tagMatch = (_a = node.tag) == null ? void 0 : _a.match(/^h(\d)$/);
5122
+ const tagMatch = (_a = node.tag) == null ? void 0 : _a.match(/^h([1-6])$/);
4547
5123
  const level = tagMatch ? parseInt(tagMatch[1], 10) : 1;
4548
5124
  return {
4549
5125
  type: "heading",
@@ -4668,6 +5244,105 @@ function buildSavePayload(ast, options) {
4668
5244
  function serializeDocumentJson(ast) {
4669
5245
  return JSON.stringify(ast, null, 2);
4670
5246
  }
5247
+ function appendChildren(parent, children = []) {
5248
+ const nodes = children.map(buildLexicalNode).filter((node) => node !== null);
5249
+ if (nodes.length > 0) {
5250
+ parent.append(...nodes);
5251
+ }
5252
+ }
5253
+ function applyElementFormatting(node, serializedNode) {
5254
+ if (typeof serializedNode.format === "string" && ["left", "center", "right", "justify"].includes(serializedNode.format) && node.setFormat) {
5255
+ node.setFormat(serializedNode.format);
5256
+ }
5257
+ if (typeof serializedNode.indent === "number" && serializedNode.indent > 0 && node.setIndent) {
5258
+ node.setIndent(serializedNode.indent);
5259
+ }
5260
+ }
5261
+ function buildLexicalNode(serializedNode) {
5262
+ switch (serializedNode.type) {
5263
+ case "paragraph": {
5264
+ const node = $createParagraphNode();
5265
+ applyElementFormatting(node, serializedNode);
5266
+ appendChildren(node, serializedNode.children);
5267
+ return node;
5268
+ }
5269
+ case "heading": {
5270
+ const tag = typeof serializedNode.tag === "string" && /^h[1-6]$/.test(serializedNode.tag) ? serializedNode.tag : "h1";
5271
+ const node = $createHeadingNode(tag);
5272
+ applyElementFormatting(node, serializedNode);
5273
+ appendChildren(node, serializedNode.children);
5274
+ return node;
5275
+ }
5276
+ case "quote": {
5277
+ const node = $createQuoteNode();
5278
+ applyElementFormatting(node, serializedNode);
5279
+ appendChildren(node, serializedNode.children);
5280
+ return node;
5281
+ }
5282
+ case "list": {
5283
+ const listType = serializedNode.listType === "number" ? "number" : "bullet";
5284
+ const node = $createListNode(listType, typeof serializedNode.start === "number" ? serializedNode.start : 1);
5285
+ appendChildren(node, serializedNode.children);
5286
+ return node;
5287
+ }
5288
+ case "listitem": {
5289
+ const node = $createListItemNode();
5290
+ appendChildren(node, serializedNode.children);
5291
+ return node;
5292
+ }
5293
+ case "text": {
5294
+ const node = $createTextNode(serializedNode.text ?? "");
5295
+ if (typeof serializedNode.format === "number" && serializedNode.format > 0) {
5296
+ node.setFormat(serializedNode.format);
5297
+ }
5298
+ if (typeof serializedNode.style === "string" && serializedNode.style.trim() !== "") {
5299
+ node.setStyle(serializedNode.style);
5300
+ }
5301
+ return node;
5302
+ }
5303
+ case "linebreak":
5304
+ return $createLineBreakNode();
5305
+ case "variable-node":
5306
+ return $createVariableNode(
5307
+ serializedNode.variableKey ?? "",
5308
+ typeof serializedNode.format === "number" ? serializedNode.format : 0,
5309
+ typeof serializedNode.style === "string" ? serializedNode.style : ""
5310
+ );
5311
+ default:
5312
+ return null;
5313
+ }
5314
+ }
5315
+ function getBodyChildren(pageState) {
5316
+ const root = pageState == null ? void 0 : pageState.root;
5317
+ return (root == null ? void 0 : root.children) ?? [];
5318
+ }
5319
+ function insertDocumentContent(editor, document2) {
5320
+ let inserted = false;
5321
+ editor.update(() => {
5322
+ const nodes = document2.pages.flatMap((page) => getBodyChildren(page.bodyState)).map(buildLexicalNode).filter((node) => node !== null);
5323
+ if (nodes.length === 0) {
5324
+ return;
5325
+ }
5326
+ const selection = $getSelection();
5327
+ if ($isRangeSelection(selection)) {
5328
+ if (selection.isCollapsed()) {
5329
+ selection.insertParagraph();
5330
+ }
5331
+ const nextSelection = $getSelection();
5332
+ if ($isRangeSelection(nextSelection) || $isNodeSelection(nextSelection)) {
5333
+ nextSelection.insertNodes(nodes);
5334
+ } else {
5335
+ $insertNodes(nodes);
5336
+ }
5337
+ } else if ($isNodeSelection(selection)) {
5338
+ selection.insertNodes(nodes);
5339
+ } else {
5340
+ $insertNodes(nodes);
5341
+ }
5342
+ inserted = true;
5343
+ });
5344
+ return inserted;
5345
+ }
4671
5346
  function selectEntireDocument(rootElement, selectionBuffer) {
4672
5347
  if (!rootElement || !selectionBuffer) {
4673
5348
  return;
@@ -4874,10 +5549,13 @@ const EditorWithHandle = forwardRef(({ captureHistoryShortcutsOnWindow, onSave,
4874
5549
  const {
4875
5550
  document: doc,
4876
5551
  activeEditor,
5552
+ activeCaretPosition,
4877
5553
  historySidebarOpen,
5554
+ runHistoryAction,
4878
5555
  setHistorySidebarOpen
4879
5556
  } = useDocument();
4880
5557
  const { handleFactories } = useExtensions();
5558
+ const t = useTranslations();
4881
5559
  const getDocument = useCallback(() => doc, [doc]);
4882
5560
  const getActiveEditor = useCallback(() => activeEditor, [activeEditor]);
4883
5561
  const extensionCtx = useExtensionContext(getDocument, getActiveEditor);
@@ -4888,6 +5566,23 @@ const EditorWithHandle = forwardRef(({ captureHistoryShortcutsOnWindow, onSave,
4888
5566
  },
4889
5567
  toggleHistorySidebar: () => {
4890
5568
  setHistorySidebarOpen(!historySidebarOpen);
5569
+ },
5570
+ insertDocumentContent: (documentToInsert) => {
5571
+ if (!activeEditor || (activeCaretPosition == null ? void 0 : activeCaretPosition.region) !== "body") {
5572
+ return false;
5573
+ }
5574
+ let inserted = false;
5575
+ runHistoryAction(
5576
+ {
5577
+ label: t.history.actions.insertedDocumentContent,
5578
+ source: "toolbar",
5579
+ region: "document"
5580
+ },
5581
+ () => {
5582
+ inserted = insertDocumentContent(activeEditor, documentToInsert);
5583
+ }
5584
+ );
5585
+ return inserted;
4891
5586
  }
4892
5587
  };
4893
5588
  for (const factory of handleFactories) {
@@ -4895,7 +5590,16 @@ const EditorWithHandle = forwardRef(({ captureHistoryShortcutsOnWindow, onSave,
4895
5590
  Object.assign(handle, methods);
4896
5591
  }
4897
5592
  return handle;
4898
- }, [extensionCtx, handleFactories, historySidebarOpen, setHistorySidebarOpen]);
5593
+ }, [
5594
+ activeCaretPosition == null ? void 0 : activeCaretPosition.region,
5595
+ activeEditor,
5596
+ extensionCtx,
5597
+ handleFactories,
5598
+ historySidebarOpen,
5599
+ runHistoryAction,
5600
+ setHistorySidebarOpen,
5601
+ t.history.actions.insertedDocumentContent
5602
+ ]);
4899
5603
  return /* @__PURE__ */ jsx(
4900
5604
  EditorChrome,
4901
5605
  {
@@ -4913,9 +5617,10 @@ const Lex4Editor = forwardRef(({
4913
5617
  onSave,
4914
5618
  extensions,
4915
5619
  translations,
5620
+ toolbar,
4916
5621
  className
4917
5622
  }, ref) => {
4918
- return /* @__PURE__ */ jsx(TranslationsProvider, { translations, children: /* @__PURE__ */ jsx(ExtensionStateProvider, { children: /* @__PURE__ */ jsx(ExtensionProvider, { extensions, children: /* @__PURE__ */ jsx(
5623
+ return /* @__PURE__ */ jsx(TranslationsProvider, { translations, children: /* @__PURE__ */ jsx(ToolbarConfigProvider, { toolbar, children: /* @__PURE__ */ jsx(ExtensionStateProvider, { children: /* @__PURE__ */ jsx(ExtensionProvider, { extensions, children: /* @__PURE__ */ jsx(
4919
5624
  DocumentProvider,
4920
5625
  {
4921
5626
  initialDocument,
@@ -4930,7 +5635,7 @@ const Lex4Editor = forwardRef(({
4930
5635
  }
4931
5636
  )
4932
5637
  }
4933
- ) }) }) });
5638
+ ) }) }) }) });
4934
5639
  });
4935
5640
  Lex4Editor.displayName = "Lex4Editor";
4936
5641
  function useOverflowDetection(bodyHeight, onOverflow, debounceMs = 100) {
@@ -5024,120 +5729,6 @@ function astExtension() {
5024
5729
  }
5025
5730
  };
5026
5731
  }
5027
- const EMPTY_CONTEXT = {
5028
- definitions: [],
5029
- refreshDefinitions: () => {
5030
- },
5031
- getDefinition: () => void 0
5032
- };
5033
- const VariableContext = createContext(EMPTY_CONTEXT);
5034
- const VariableProvider = ({
5035
- initialDefinitions = [],
5036
- children
5037
- }) => {
5038
- const [definitions, setDefinitions] = useState(initialDefinitions);
5039
- const refresh = useCallback((newDefinitions) => {
5040
- setDefinitions(newDefinitions);
5041
- }, []);
5042
- const getDefinition = useCallback(
5043
- (key) => {
5044
- return definitions.find((d) => d.key === key);
5045
- },
5046
- [definitions]
5047
- );
5048
- const value = useMemo(
5049
- () => ({ definitions, refreshDefinitions: refresh, getDefinition }),
5050
- [definitions, refresh, getDefinition]
5051
- );
5052
- return /* @__PURE__ */ jsx(VariableContext.Provider, { value, children });
5053
- };
5054
- function useVariables() {
5055
- return useContext(VariableContext);
5056
- }
5057
- class VariableNode extends DecoratorNode {
5058
- constructor(variableKey, key) {
5059
- super(key);
5060
- __publicField(this, "__variableKey");
5061
- this.__variableKey = variableKey;
5062
- }
5063
- static getType() {
5064
- return "variable-node";
5065
- }
5066
- static clone(node) {
5067
- return new VariableNode(node.__variableKey, node.__key);
5068
- }
5069
- getVariableKey() {
5070
- return this.__variableKey;
5071
- }
5072
- // -- Serialization --
5073
- static importJSON(serializedNode) {
5074
- return $createVariableNode(serializedNode.variableKey);
5075
- }
5076
- exportJSON() {
5077
- return {
5078
- type: "variable-node",
5079
- version: 1,
5080
- variableKey: this.__variableKey
5081
- };
5082
- }
5083
- // -- DOM --
5084
- createDOM() {
5085
- const span = document.createElement("span");
5086
- span.className = "lex4-variable";
5087
- span.setAttribute("data-variable-key", this.__variableKey);
5088
- span.setAttribute("data-testid", `variable-${this.__variableKey}`);
5089
- span.contentEditable = "false";
5090
- return span;
5091
- }
5092
- updateDOM() {
5093
- return false;
5094
- }
5095
- exportDOM() {
5096
- const span = document.createElement("span");
5097
- span.setAttribute("data-variable-key", this.__variableKey);
5098
- span.textContent = `{{${this.__variableKey}}}`;
5099
- return { element: span };
5100
- }
5101
- static importDOM() {
5102
- return null;
5103
- }
5104
- // -- Behavior --
5105
- isInline() {
5106
- return true;
5107
- }
5108
- isKeyboardSelectable() {
5109
- return true;
5110
- }
5111
- getTextContent() {
5112
- return `{{${this.__variableKey}}}`;
5113
- }
5114
- // -- Rendering --
5115
- decorate() {
5116
- return /* @__PURE__ */ jsx(VariableChip, { variableKey: this.__variableKey });
5117
- }
5118
- }
5119
- function VariableChip({ variableKey }) {
5120
- const { getDefinition } = useVariables();
5121
- const def = getDefinition(variableKey);
5122
- const label = (def == null ? void 0 : def.label) ?? variableKey;
5123
- const group = def == null ? void 0 : def.group;
5124
- return /* @__PURE__ */ jsx(
5125
- "span",
5126
- {
5127
- className: "lex4-variable-chip",
5128
- "data-testid": `variable-chip-${variableKey}`,
5129
- "data-variable-group": group,
5130
- title: variableKey,
5131
- children: label
5132
- }
5133
- );
5134
- }
5135
- function $createVariableNode(variableKey) {
5136
- return $applyNodeReplacement(new VariableNode(variableKey));
5137
- }
5138
- function $isVariableNode(node) {
5139
- return node instanceof VariableNode;
5140
- }
5141
5732
  const INSERT_VARIABLE_COMMAND = createCommand("INSERT_VARIABLE");
5142
5733
  const VariablePlugin = () => {
5143
5734
  const [editor] = useLexicalComposerContext();
@@ -5398,7 +5989,11 @@ const VariablePanelStateProvider = ({ children }) => {
5398
5989
  };
5399
5990
  const VariableToolbarToggle = () => {
5400
5991
  const { panelOpen, setPanelOpen } = useVariablePanelState();
5992
+ const toolbarConfig = useToolbarConfig();
5401
5993
  const t = useTranslations();
5994
+ if (!toolbarConfig.variables.visible) {
5995
+ return null;
5996
+ }
5402
5997
  return /* @__PURE__ */ jsxs(
5403
5998
  "button",
5404
5999
  {
@@ -5411,7 +6006,7 @@ const VariableToolbarToggle = () => {
5411
6006
  "aria-label": panelOpen ? t.variables.closePanel : t.variables.openPanel,
5412
6007
  children: [
5413
6008
  /* @__PURE__ */ jsx(Braces, { size: 14 }),
5414
- "Variables"
6009
+ toolbarConfig.variables.showLabel && t.variables.title
5415
6010
  ]
5416
6011
  }
5417
6012
  );