@yurikilian/lex4 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -779,6 +779,115 @@ const TranslationsProvider = ({
779
779
  );
780
780
  return /* @__PURE__ */ jsxRuntime.jsx(TranslationsContext.Provider, { value: merged, children });
781
781
  };
782
+ const createStoreImpl = (createState) => {
783
+ let state;
784
+ const listeners = /* @__PURE__ */ new Set();
785
+ const setState = (partial, replace) => {
786
+ const nextState = typeof partial === "function" ? partial(state) : partial;
787
+ if (!Object.is(nextState, state)) {
788
+ const previousState = state;
789
+ state = (replace != null ? replace : typeof nextState !== "object" || nextState === null) ? nextState : Object.assign({}, state, nextState);
790
+ listeners.forEach((listener) => listener(state, previousState));
791
+ }
792
+ };
793
+ const getState = () => state;
794
+ const getInitialState = () => initialState;
795
+ const subscribe = (listener) => {
796
+ listeners.add(listener);
797
+ return () => listeners.delete(listener);
798
+ };
799
+ const api = { setState, getState, getInitialState, subscribe };
800
+ const initialState = state = createState(setState, getState, api);
801
+ return api;
802
+ };
803
+ const createStore = (createState) => createState ? createStoreImpl(createState) : createStoreImpl;
804
+ const identity = (arg) => arg;
805
+ function useStore(api, selector = identity) {
806
+ const slice = React.useSyncExternalStore(
807
+ api.subscribe,
808
+ React.useCallback(() => selector(api.getState()), [api, selector]),
809
+ React.useCallback(() => selector(api.getInitialState()), [api, selector])
810
+ );
811
+ React.useDebugValue(slice);
812
+ return slice;
813
+ }
814
+ const SUPPORTED_FONT_SIZES = [
815
+ 8,
816
+ 9,
817
+ 10,
818
+ 11,
819
+ 12,
820
+ 14,
821
+ 16,
822
+ 18,
823
+ 20,
824
+ 24,
825
+ 28,
826
+ 32,
827
+ 36,
828
+ 48,
829
+ 72
830
+ ];
831
+ const DEFAULT_FONT_SIZE = 12;
832
+ function applyFontSize(editor, size) {
833
+ editor.update(() => {
834
+ const selection2 = lexical.$getSelection();
835
+ if (!lexical.$isRangeSelection(selection2)) return;
836
+ const nodes = selection2.getNodes();
837
+ for (const node of nodes) {
838
+ if (lexical.$isTextNode(node)) {
839
+ const existing = node.getStyle();
840
+ const updated = mergeFontSize(existing, size);
841
+ node.setStyle(updated);
842
+ }
843
+ }
844
+ });
845
+ }
846
+ function mergeFontSize(existingStyle, size) {
847
+ const stripped = existingStyle.replace(/font-size:\s*[^;]+;?\s*/g, "").trim();
848
+ const sizeDecl = `font-size: ${size}pt`;
849
+ return stripped ? `${stripped}; ${sizeDecl}` : sizeDecl;
850
+ }
851
+ const DEFAULT_TOOLBAR_STYLE_SNAPSHOT = {
852
+ blockType: "paragraph",
853
+ fontFamily: "Inter",
854
+ fontSize: DEFAULT_FONT_SIZE,
855
+ alignment: "left",
856
+ isBold: false,
857
+ isItalic: false,
858
+ isUnderline: false,
859
+ isStrikethrough: false,
860
+ hasSelectedVariable: false
861
+ };
862
+ function createToolbarStyleStore(initialSnapshot = DEFAULT_TOOLBAR_STYLE_SNAPSHOT) {
863
+ return createStore((set) => ({
864
+ ...initialSnapshot,
865
+ setSnapshot: (snapshot) => set(snapshot),
866
+ reset: () => set(DEFAULT_TOOLBAR_STYLE_SNAPSHOT)
867
+ }));
868
+ }
869
+ const ToolbarStyleStoreContext = React.createContext(null);
870
+ const ToolbarStyleStoreProvider = ({ children }) => {
871
+ const storeRef = React.useRef(null);
872
+ if (!storeRef.current) {
873
+ storeRef.current = createToolbarStyleStore();
874
+ }
875
+ return /* @__PURE__ */ jsxRuntime.jsx(ToolbarStyleStoreContext.Provider, { value: storeRef.current, children });
876
+ };
877
+ function useToolbarStyleStore(selector) {
878
+ const store = React.useContext(ToolbarStyleStoreContext);
879
+ if (!store) {
880
+ throw new Error("useToolbarStyleStore must be used within a ToolbarStyleStoreProvider");
881
+ }
882
+ return useStore(store, selector);
883
+ }
884
+ function useToolbarStyleStoreApi() {
885
+ const store = React.useContext(ToolbarStyleStoreContext);
886
+ if (!store) {
887
+ throw new Error("useToolbarStyleStoreApi must be used within a ToolbarStyleStoreProvider");
888
+ }
889
+ return store;
890
+ }
782
891
  const HISTORY_RESTORE_SUPPRESSION_MS = 100;
783
892
  const HISTORY_BATCH_FLUSH_MS = 16;
784
893
  function cloneDocumentSnapshot(document2) {
@@ -1335,7 +1444,7 @@ const DocumentProvider = ({
1335
1444
  undo,
1336
1445
  redo,
1337
1446
  editorRegistry
1338
- }, children });
1447
+ }, children: /* @__PURE__ */ jsxRuntime.jsx(ToolbarStyleStoreProvider, { children }) });
1339
1448
  };
1340
1449
  /**
1341
1450
  * @license lucide-react v1.8.0 - ISC
@@ -2083,43 +2192,6 @@ function applyFontFamily(editor, fontFamily) {
2083
2192
  }
2084
2193
  });
2085
2194
  }
2086
- const SUPPORTED_FONT_SIZES = [
2087
- 8,
2088
- 9,
2089
- 10,
2090
- 11,
2091
- 12,
2092
- 14,
2093
- 16,
2094
- 18,
2095
- 20,
2096
- 24,
2097
- 28,
2098
- 32,
2099
- 36,
2100
- 48,
2101
- 72
2102
- ];
2103
- const DEFAULT_FONT_SIZE = 12;
2104
- function applyFontSize(editor, size) {
2105
- editor.update(() => {
2106
- const selection2 = lexical.$getSelection();
2107
- if (!lexical.$isRangeSelection(selection2)) return;
2108
- const nodes = selection2.getNodes();
2109
- for (const node of nodes) {
2110
- if (lexical.$isTextNode(node)) {
2111
- const existing = node.getStyle();
2112
- const updated = mergeFontSize(existing, size);
2113
- node.setStyle(updated);
2114
- }
2115
- }
2116
- });
2117
- }
2118
- function mergeFontSize(existingStyle, size) {
2119
- const stripped = existingStyle.replace(/font-size:\s*[^;]+;?\s*/g, "").trim();
2120
- const sizeDecl = `font-size: ${size}pt`;
2121
- return stripped ? `${stripped}; ${sizeDecl}` : sizeDecl;
2122
- }
2123
2195
  function toggleFormat(editor, format) {
2124
2196
  editor.dispatchCommand(lexical.FORMAT_TEXT_COMMAND, format);
2125
2197
  }
@@ -2151,48 +2223,77 @@ function indentContent(editor) {
2151
2223
  function outdentContent(editor) {
2152
2224
  editor.dispatchCommand(lexical.OUTDENT_CONTENT_COMMAND, void 0);
2153
2225
  }
2154
- function setBlockType(editor, blockType) {
2155
- editor.update(() => {
2156
- const selection$1 = lexical.$getSelection();
2157
- if (!lexical.$isRangeSelection(selection$1)) {
2158
- return;
2159
- }
2160
- if (blockType === "paragraph") {
2161
- selection.$setBlocksType(selection$1, () => lexical.$createParagraphNode());
2162
- return;
2163
- }
2164
- selection.$setBlocksType(selection$1, () => richText.$createHeadingNode(blockType));
2165
- });
2226
+ const INLINE_BLOCK_STYLE_PROPERTY = "--lex4-block-type";
2227
+ const INLINE_BLOCK_STYLE_PRESETS = {
2228
+ paragraph: {
2229
+ [INLINE_BLOCK_STYLE_PROPERTY]: "paragraph",
2230
+ "font-size": "12pt",
2231
+ "font-weight": "400"
2232
+ },
2233
+ h1: {
2234
+ [INLINE_BLOCK_STYLE_PROPERTY]: "h1",
2235
+ "font-size": "22.5pt",
2236
+ "font-weight": "700"
2237
+ },
2238
+ h2: {
2239
+ [INLINE_BLOCK_STYLE_PROPERTY]: "h2",
2240
+ "font-size": "18pt",
2241
+ "font-weight": "700"
2242
+ },
2243
+ h3: {
2244
+ [INLINE_BLOCK_STYLE_PROPERTY]: "h3",
2245
+ "font-size": "15pt",
2246
+ "font-weight": "600"
2247
+ },
2248
+ h4: {
2249
+ [INLINE_BLOCK_STYLE_PROPERTY]: "h4",
2250
+ "font-size": "13.5pt",
2251
+ "font-weight": "600"
2252
+ },
2253
+ h5: {
2254
+ [INLINE_BLOCK_STYLE_PROPERTY]: "h5",
2255
+ "font-size": "12pt",
2256
+ "font-weight": "500"
2257
+ },
2258
+ h6: {
2259
+ [INLINE_BLOCK_STYLE_PROPERTY]: "h6",
2260
+ "font-size": "11.25pt",
2261
+ "font-weight": "500"
2262
+ }
2263
+ };
2264
+ function escapeStyleProperty(property) {
2265
+ const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2266
+ return escapedProperty;
2166
2267
  }
2167
- function getActiveBlockType(editor) {
2168
- let blockType = "paragraph";
2169
- editor.getEditorState().read(() => {
2170
- const selection2 = lexical.$getSelection();
2171
- if (!lexical.$isRangeSelection(selection2)) {
2172
- return;
2173
- }
2174
- const anchorNode = selection2.anchor.getNode();
2175
- const topLevelElement = anchorNode.getTopLevelElementOrThrow();
2176
- if (richText.$isHeadingNode(topLevelElement)) {
2177
- blockType = topLevelElement.getTag();
2178
- }
2179
- });
2180
- return blockType;
2268
+ function stripStyleDeclaration(existingStyle, property) {
2269
+ const escapedProperty = escapeStyleProperty(property);
2270
+ return existingStyle.replace(
2271
+ new RegExp(`${escapedProperty}:\\s*[^;]+;?\\s*`, "g"),
2272
+ ""
2273
+ ).trim();
2274
+ }
2275
+ function isSupportedInlineBlockType(value) {
2276
+ return value === "paragraph" || value === "h1" || value === "h2" || value === "h3" || value === "h4" || value === "h5" || value === "h6";
2181
2277
  }
2182
2278
  function extractStyleValue(style, property) {
2183
- const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2279
+ const escapedProperty = escapeStyleProperty(property);
2184
2280
  const match = style.match(new RegExp(`${escapedProperty}:\\s*([^;]+)`));
2185
2281
  return match ? match[1].trim().replace(/['"]/g, "") : void 0;
2186
2282
  }
2283
+ function removeStyleDeclaration(existingStyle, property) {
2284
+ return stripStyleDeclaration(existingStyle, property);
2285
+ }
2187
2286
  function mergeStyleDeclaration(existingStyle, property, value) {
2188
- const escapedProperty = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2189
- const stripped = existingStyle.replace(
2190
- new RegExp(`${escapedProperty}:\\s*[^;]+;?\\s*`, "g"),
2191
- ""
2192
- ).trim();
2287
+ const stripped = stripStyleDeclaration(existingStyle, property);
2193
2288
  const declaration = `${property}: ${value}`;
2194
2289
  return stripped ? `${stripped}; ${declaration}` : declaration;
2195
2290
  }
2291
+ function mergeStyleDeclarations(existingStyle, declarations) {
2292
+ return Object.entries(declarations).reduce(
2293
+ (style, [property, value]) => mergeStyleDeclaration(style, property, value),
2294
+ existingStyle
2295
+ );
2296
+ }
2196
2297
  function extractFontFamilyFromStyle(style) {
2197
2298
  return extractStyleValue(style, "font-family");
2198
2299
  }
@@ -2210,6 +2311,21 @@ function mergeFontFamilyStyle(existingStyle, fontFamily) {
2210
2311
  function mergeFontSizeStyle(existingStyle, size) {
2211
2312
  return mergeStyleDeclaration(existingStyle, "font-size", `${size}pt`);
2212
2313
  }
2314
+ function extractInlineBlockTypeFromStyle(style) {
2315
+ const value = extractStyleValue(style, INLINE_BLOCK_STYLE_PROPERTY);
2316
+ return isSupportedInlineBlockType(value) ? value : void 0;
2317
+ }
2318
+ function createInlineBlockTypeStylePatch(blockType) {
2319
+ return INLINE_BLOCK_STYLE_PRESETS[blockType];
2320
+ }
2321
+ function mergeInlineBlockTypeStyle(existingStyle, blockType) {
2322
+ const baseStyle = [
2323
+ INLINE_BLOCK_STYLE_PROPERTY,
2324
+ "font-size",
2325
+ "font-weight"
2326
+ ].reduce((style, property) => removeStyleDeclaration(style, property), existingStyle);
2327
+ return mergeStyleDeclarations(baseStyle, createInlineBlockTypeStylePatch(blockType));
2328
+ }
2213
2329
  const EMPTY_CONTEXT = {
2214
2330
  definitions: [],
2215
2331
  refreshDefinitions: () => {
@@ -2297,6 +2413,7 @@ class VariableNode extends lexical.DecoratorNode {
2297
2413
  const span = document.createElement("span");
2298
2414
  span.className = "lex4-variable";
2299
2415
  span.setAttribute("data-variable-key", this.__variableKey);
2416
+ span.setAttribute("data-node-key", this.__key);
2300
2417
  span.setAttribute("data-testid", `variable-${this.__variableKey}`);
2301
2418
  span.contentEditable = "false";
2302
2419
  return span;
@@ -2344,16 +2461,18 @@ function VariableChip({
2344
2461
  }) {
2345
2462
  const { getDefinition } = useVariables();
2346
2463
  const [editor] = LexicalComposerContext.useLexicalComposerContext();
2347
- const [isSelected, setSelected, clearOtherSelections] = useLexicalNodeSelection.useLexicalNodeSelection(nodeKey);
2464
+ const [isSelected] = useLexicalNodeSelection.useLexicalNodeSelection(nodeKey);
2348
2465
  const def = getDefinition(variableKey);
2349
2466
  const label = (def == null ? void 0 : def.label) ?? variableKey;
2350
2467
  const group = def == null ? void 0 : def.group;
2351
2468
  const style = React.useMemo(() => {
2352
2469
  const fontFamily = extractFontFamilyFromStyle(styleValue);
2353
2470
  const fontSize = extractFontSizePtFromStyle(styleValue);
2471
+ const fontWeight = extractStyleValue(styleValue, "font-weight");
2354
2472
  return {
2355
2473
  ...fontFamily ? { fontFamily } : {},
2356
- ...fontSize ? { fontSize: `${fontSize}pt` } : {}
2474
+ ...fontSize ? { fontSize: `${fontSize}pt` } : {},
2475
+ ...fontWeight ? { fontWeight } : {}
2357
2476
  };
2358
2477
  }, [styleValue]);
2359
2478
  const className = [
@@ -2364,13 +2483,48 @@ function VariableChip({
2364
2483
  format & 8 ? "lex4-text-underline" : "",
2365
2484
  format & 4 ? "lex4-text-strikethrough" : ""
2366
2485
  ].filter(Boolean).join(" ");
2367
- const handleClick = React.useCallback((event) => {
2486
+ const clearDomSelection = React.useCallback(() => {
2487
+ var _a;
2488
+ if (typeof window === "undefined") {
2489
+ return;
2490
+ }
2491
+ (_a = window.getSelection()) == null ? void 0 : _a.removeAllRanges();
2492
+ }, []);
2493
+ const selectNode = React.useCallback((extendSelection) => {
2494
+ editor.focus();
2495
+ editor.update(() => {
2496
+ const nextSelection = lexical.$createNodeSelection();
2497
+ if (extendSelection) {
2498
+ const currentSelection = lexical.$getSelection();
2499
+ if (lexical.$isNodeSelection(currentSelection)) {
2500
+ for (const node of currentSelection.getNodes()) {
2501
+ if ($isVariableNode(node)) {
2502
+ nextSelection.add(node.getKey());
2503
+ }
2504
+ }
2505
+ }
2506
+ }
2507
+ nextSelection.add(nodeKey);
2508
+ lexical.$setSelection(nextSelection);
2509
+ });
2510
+ clearDomSelection();
2511
+ }, [clearDomSelection, editor, nodeKey]);
2512
+ const handleMouseDown = React.useCallback((event) => {
2368
2513
  event.preventDefault();
2369
- if (!event.shiftKey) {
2370
- clearOtherSelections();
2514
+ event.stopPropagation();
2515
+ if (!isSelected || event.shiftKey) {
2516
+ selectNode(event.shiftKey);
2371
2517
  }
2372
- setSelected(!isSelected);
2373
- }, [clearOtherSelections, isSelected, setSelected]);
2518
+ }, [isSelected, selectNode]);
2519
+ const handleClick = React.useCallback((event) => {
2520
+ event.preventDefault();
2521
+ event.stopPropagation();
2522
+ }, []);
2523
+ const handleMouseUp = React.useCallback((event) => {
2524
+ event.preventDefault();
2525
+ event.stopPropagation();
2526
+ clearDomSelection();
2527
+ }, [clearDomSelection]);
2374
2528
  React.useEffect(() => {
2375
2529
  const removeSelectedNodes = () => {
2376
2530
  editor.update(() => {
@@ -2458,7 +2612,8 @@ function VariableChip({
2458
2612
  "data-variable-group": group,
2459
2613
  title: variableKey,
2460
2614
  style,
2461
- onMouseDown: (event) => event.preventDefault(),
2615
+ onMouseDown: handleMouseDown,
2616
+ onMouseUp: handleMouseUp,
2462
2617
  onClick: handleClick,
2463
2618
  children: label
2464
2619
  }
@@ -2470,41 +2625,58 @@ function $createVariableNode(variableKey, format = 0, style = "") {
2470
2625
  function $isVariableNode(node) {
2471
2626
  return node instanceof VariableNode;
2472
2627
  }
2473
- const FORMAT_MASKS = {
2628
+ const FORMAT_MASKS$1 = {
2474
2629
  bold: 1,
2475
2630
  italic: 2,
2476
2631
  strikethrough: 4,
2477
2632
  underline: 8
2478
2633
  };
2479
- function withSelectedVariableNodes(editor, updater) {
2480
- let updated = false;
2481
- editor.update(() => {
2482
- const selection2 = lexical.$getSelection();
2483
- if (!lexical.$isNodeSelection(selection2)) {
2634
+ function dedupeVariableNodes(nodes) {
2635
+ return Array.from(new Map(nodes.map((node) => [node.getKey(), node])).values());
2636
+ }
2637
+ function getSelectedVariableNodesFromSelection(selection2) {
2638
+ if (!selection2 || typeof selection2 !== "object" || !("getNodes" in selection2) || typeof selection2.getNodes !== "function") {
2639
+ return [];
2640
+ }
2641
+ return dedupeVariableNodes(selection2.getNodes().filter($isVariableNode));
2642
+ }
2643
+ function getVisuallySelectedVariableNodes(editor) {
2644
+ const rootElement = editor.getRootElement();
2645
+ if (!rootElement) {
2646
+ return [];
2647
+ }
2648
+ const nodes = [];
2649
+ rootElement.querySelectorAll(".lex4-variable-chip-selected").forEach((chip) => {
2650
+ const variableElement = chip.closest("[data-node-key]");
2651
+ const nodeKey = variableElement == null ? void 0 : variableElement.dataset.nodeKey;
2652
+ if (!nodeKey) {
2484
2653
  return;
2485
2654
  }
2486
- const nodes = selection2.getNodes().filter($isVariableNode);
2487
- if (nodes.length === 0) {
2488
- return;
2655
+ const node = lexical.$getNodeByKey(nodeKey);
2656
+ if ($isVariableNode(node)) {
2657
+ nodes.push(node);
2489
2658
  }
2490
- updater(nodes);
2491
- updated = true;
2492
2659
  });
2493
- return updated;
2660
+ return dedupeVariableNodes(nodes);
2494
2661
  }
2495
- function getSelectedVariableNodes(editor) {
2496
- let nodes = [];
2497
- editor.getEditorState().read(() => {
2498
- const selection2 = lexical.$getSelection();
2499
- if (!lexical.$isNodeSelection(selection2)) {
2662
+ function withSelectedVariableNodes(editor, updater) {
2663
+ let updated = false;
2664
+ editor.update(() => {
2665
+ const nodes = [
2666
+ ...getSelectedVariableNodesFromSelection(lexical.$getSelection()),
2667
+ ...getVisuallySelectedVariableNodes(editor)
2668
+ ];
2669
+ const uniqueNodes = dedupeVariableNodes(nodes);
2670
+ if (uniqueNodes.length === 0) {
2500
2671
  return;
2501
2672
  }
2502
- nodes = selection2.getNodes().filter($isVariableNode);
2673
+ updater(uniqueNodes);
2674
+ updated = true;
2503
2675
  });
2504
- return nodes;
2676
+ return updated;
2505
2677
  }
2506
2678
  function toggleSelectedVariableFormat(editor, format) {
2507
- const mask = FORMAT_MASKS[format];
2679
+ const mask = FORMAT_MASKS$1[format];
2508
2680
  if (!mask) {
2509
2681
  return false;
2510
2682
  }
@@ -2530,24 +2702,159 @@ function applyFontSizeToSelectedVariables(editor, size) {
2530
2702
  }
2531
2703
  });
2532
2704
  }
2533
- function readSelectedVariableFormatting(editor) {
2534
- let formatting = {};
2535
- editor.getEditorState().read(() => {
2536
- const selection2 = lexical.$getSelection();
2537
- if (!lexical.$isNodeSelection(selection2)) {
2705
+ function getElementBlockType$1(element) {
2706
+ if (richText.$isHeadingNode(element)) {
2707
+ return element.getTag();
2708
+ }
2709
+ return "paragraph";
2710
+ }
2711
+ function getVariableTopLevelElement(variable) {
2712
+ const topLevelElement = variable.getTopLevelElementOrThrow();
2713
+ return lexical.$isElementNode(topLevelElement) ? topLevelElement : null;
2714
+ }
2715
+ function replaceTopLevelBlockType(element, blockType) {
2716
+ const currentType = getElementBlockType$1(element);
2717
+ if (currentType === blockType) {
2718
+ return;
2719
+ }
2720
+ const nextElement = blockType === "paragraph" ? lexical.$createParagraphNode() : richText.$createHeadingNode(blockType);
2721
+ nextElement.setFormat(element.getFormatType());
2722
+ nextElement.setIndent(element.getIndent());
2723
+ const children = element.getChildren();
2724
+ for (const child of children) {
2725
+ nextElement.append(child);
2726
+ }
2727
+ element.replace(nextElement);
2728
+ }
2729
+ function isPartialSingleBlockSelection(selection2) {
2730
+ if (selection2.isCollapsed()) {
2731
+ return false;
2732
+ }
2733
+ const anchorTopLevel = selection2.anchor.getNode().getTopLevelElementOrThrow();
2734
+ const focusTopLevel = selection2.focus.getNode().getTopLevelElementOrThrow();
2735
+ if (!anchorTopLevel.is(focusTopLevel)) {
2736
+ return false;
2737
+ }
2738
+ const selectedText = selection2.getTextContent().trim();
2739
+ const blockText = anchorTopLevel.getTextContent().trim();
2740
+ return selectedText.length > 0 && selectedText.length < blockText.length;
2741
+ }
2742
+ function applySemanticBlockType(selection$1, blockType) {
2743
+ if (blockType === "paragraph") {
2744
+ selection.$setBlocksType(selection$1, () => lexical.$createParagraphNode());
2745
+ return;
2746
+ }
2747
+ selection.$setBlocksType(selection$1, () => richText.$createHeadingNode(blockType));
2748
+ }
2749
+ function selectedVariablesOccupyEntireBlock(variables, topLevelElement) {
2750
+ const selectedKeys = new Set(variables.map((variable) => variable.getKey()));
2751
+ const meaningfulChildren = topLevelElement.getChildren().filter(
2752
+ (child) => !(lexical.$isTextNode(child) && child.getTextContent().trim() === "")
2753
+ );
2754
+ if (meaningfulChildren.length === 0) {
2755
+ return false;
2756
+ }
2757
+ return meaningfulChildren.every(
2758
+ (child) => $isVariableNode(child) && selectedKeys.has(child.getKey())
2759
+ );
2760
+ }
2761
+ function getStandaloneVariableChildren(topLevelElement) {
2762
+ const meaningfulChildren = topLevelElement.getChildren().filter(
2763
+ (child) => !(lexical.$isTextNode(child) && child.getTextContent().trim() === "")
2764
+ );
2765
+ if (meaningfulChildren.length === 0 || !meaningfulChildren.every($isVariableNode)) {
2766
+ return null;
2767
+ }
2768
+ return meaningfulChildren;
2769
+ }
2770
+ function getSelectedVariableNodesFromRangeSelection(editor, selection2) {
2771
+ const seen = /* @__PURE__ */ new Set();
2772
+ const variables = [];
2773
+ for (const node of [
2774
+ selection2.anchor.getNode(),
2775
+ selection2.focus.getNode(),
2776
+ ...selection2.getNodes(),
2777
+ ...getVisuallySelectedVariableNodes(editor)
2778
+ ]) {
2779
+ if (!$isVariableNode(node) || seen.has(node.getKey())) {
2780
+ continue;
2781
+ }
2782
+ seen.add(node.getKey());
2783
+ variables.push(node);
2784
+ }
2785
+ return variables;
2786
+ }
2787
+ function setBlockType(editor, blockType) {
2788
+ editor.update(() => {
2789
+ const visuallySelectedVariables = getVisuallySelectedVariableNodes(editor);
2790
+ const currentSelection = lexical.$getSelection();
2791
+ const selection$1 = lexical.$isNodeSelection(currentSelection) ? currentSelection : lexical.$createRangeSelectionFromDom(window.getSelection(), editor) ?? currentSelection;
2792
+ if (lexical.$isRangeSelection(selection$1)) {
2793
+ lexical.$setSelection(selection$1);
2794
+ }
2795
+ if (lexical.$isNodeSelection(selection$1)) {
2796
+ const variables = Array.from(new Map(
2797
+ [
2798
+ ...selection$1.getNodes().filter($isVariableNode),
2799
+ ...visuallySelectedVariables
2800
+ ].map((variable) => [variable.getKey(), variable])
2801
+ ).values());
2802
+ if (variables.length === 0) {
2803
+ return;
2804
+ }
2805
+ const firstTopLevelElement = getVariableTopLevelElement(variables[0]);
2806
+ if (!firstTopLevelElement) {
2807
+ return;
2808
+ }
2809
+ const sameTopLevelElement = variables.every(
2810
+ (variable) => {
2811
+ var _a;
2812
+ return ((_a = getVariableTopLevelElement(variable)) == null ? void 0 : _a.is(firstTopLevelElement)) ?? false;
2813
+ }
2814
+ );
2815
+ if (sameTopLevelElement && selectedVariablesOccupyEntireBlock(variables, firstTopLevelElement)) {
2816
+ for (const variable of variables) {
2817
+ variable.setStyle(mergeInlineBlockTypeStyle(variable.getStyle(), blockType));
2818
+ }
2819
+ replaceTopLevelBlockType(firstTopLevelElement, blockType);
2820
+ const nextSelection = lexical.$createNodeSelection();
2821
+ for (const variable of variables) {
2822
+ nextSelection.add(variable.getKey());
2823
+ }
2824
+ lexical.$setSelection(nextSelection);
2825
+ return;
2826
+ }
2827
+ for (const variable of variables) {
2828
+ variable.setStyle(mergeInlineBlockTypeStyle(variable.getStyle(), blockType));
2829
+ }
2538
2830
  return;
2539
2831
  }
2540
- const firstNode = selection2.getNodes().filter($isVariableNode)[0];
2541
- if (!firstNode) {
2832
+ if (!lexical.$isRangeSelection(selection$1)) {
2833
+ if (visuallySelectedVariables.length === 0) {
2834
+ return;
2835
+ }
2836
+ for (const variable of visuallySelectedVariables) {
2837
+ variable.setStyle(mergeInlineBlockTypeStyle(variable.getStyle(), blockType));
2838
+ }
2542
2839
  return;
2543
2840
  }
2544
- const style = firstNode.getStyle();
2545
- formatting = {
2546
- fontFamily: extractFontFamilyFromStyle(style),
2547
- fontSize: extractFontSizePtFromStyle(style)
2548
- };
2841
+ const anchorTopLevel = selection$1.anchor.getNode().getTopLevelElementOrThrow();
2842
+ const standaloneVariables = lexical.$isElementNode(anchorTopLevel) ? getStandaloneVariableChildren(anchorTopLevel) : null;
2843
+ const selectedVariables = getSelectedVariableNodesFromRangeSelection(editor, selection$1);
2844
+ if (isPartialSingleBlockSelection(selection$1)) {
2845
+ selection.$patchStyleText(selection$1, createInlineBlockTypeStylePatch(blockType));
2846
+ for (const variable of selectedVariables) {
2847
+ variable.setStyle(mergeInlineBlockTypeStyle(variable.getStyle(), blockType));
2848
+ }
2849
+ return;
2850
+ }
2851
+ for (const variable of new Map(
2852
+ [...standaloneVariables ?? [], ...selectedVariables].map((variable2) => [variable2.getKey(), variable2])
2853
+ ).values()) {
2854
+ variable.setStyle(mergeInlineBlockTypeStyle(variable.getStyle(), blockType));
2855
+ }
2856
+ applySemanticBlockType(selection$1, blockType);
2549
2857
  });
2550
- return formatting;
2551
2858
  }
2552
2859
  const BLOCK_TYPE_OPTIONS = [
2553
2860
  { value: "paragraph", shortLabel: "P" },
@@ -2964,6 +3271,108 @@ const CanvasControls = () => {
2964
3271
  )
2965
3272
  ] });
2966
3273
  };
3274
+ const FORMAT_MASKS = {
3275
+ bold: 1,
3276
+ italic: 2,
3277
+ strikethrough: 4,
3278
+ underline: 8
3279
+ };
3280
+ function normalizeFontFamily(fontFamily) {
3281
+ if (fontFamily && SUPPORTED_FONTS.includes(fontFamily)) {
3282
+ return fontFamily;
3283
+ }
3284
+ return "Inter";
3285
+ }
3286
+ function normalizeAlignment(alignment) {
3287
+ if (alignment === "center" || alignment === "right" || alignment === "justify") {
3288
+ return alignment;
3289
+ }
3290
+ return "left";
3291
+ }
3292
+ function getElementBlockType(node) {
3293
+ const topLevelElement = node.getTopLevelElementOrThrow();
3294
+ if (richText.$isHeadingNode(topLevelElement)) {
3295
+ return topLevelElement.getTag();
3296
+ }
3297
+ return "paragraph";
3298
+ }
3299
+ function getElementAlignment(node) {
3300
+ const topLevelElement = node.getTopLevelElementOrThrow();
3301
+ if (lexical.$isElementNode(topLevelElement)) {
3302
+ return normalizeAlignment(topLevelElement.getFormatType());
3303
+ }
3304
+ return "left";
3305
+ }
3306
+ function getInlineStyleTarget(nodes, anchorNode) {
3307
+ if (lexical.$isTextNode(anchorNode) || $isVariableNode(anchorNode)) {
3308
+ return anchorNode;
3309
+ }
3310
+ return nodes.find((node) => lexical.$isTextNode(node) || $isVariableNode(node)) ?? null;
3311
+ }
3312
+ function getInlineStyleFromNode(node) {
3313
+ if (lexical.$isTextNode(node) || $isVariableNode(node)) {
3314
+ return node.getStyle();
3315
+ }
3316
+ return "";
3317
+ }
3318
+ function hasInlineFormat(node, format) {
3319
+ if ($isVariableNode(node)) {
3320
+ return (node.getFormat() & FORMAT_MASKS[format]) !== 0;
3321
+ }
3322
+ if (lexical.$isTextNode(node) && "hasFormat" in node && typeof node.hasFormat === "function") {
3323
+ return node.hasFormat(format);
3324
+ }
3325
+ if (lexical.$isTextNode(node) && "getFormat" in node && typeof node.getFormat === "function") {
3326
+ return (node.getFormat() & FORMAT_MASKS[format]) !== 0;
3327
+ }
3328
+ return false;
3329
+ }
3330
+ function readToolbarStyleSnapshot(editor, editorState = editor.getEditorState()) {
3331
+ let snapshot = DEFAULT_TOOLBAR_STYLE_SNAPSHOT;
3332
+ editorState.read(() => {
3333
+ const currentSelection = lexical.$getSelection();
3334
+ const selection2 = lexical.$isNodeSelection(currentSelection) ? currentSelection : lexical.$createRangeSelectionFromDom(window.getSelection(), editor) ?? currentSelection;
3335
+ if (lexical.$isNodeSelection(selection2)) {
3336
+ const variableNodes = selection2.getNodes().filter($isVariableNode);
3337
+ if (variableNodes.length === 0) {
3338
+ return;
3339
+ }
3340
+ const firstVariableNode = variableNodes[0];
3341
+ const style2 = firstVariableNode.getStyle();
3342
+ snapshot = {
3343
+ blockType: extractInlineBlockTypeFromStyle(style2) ?? getElementBlockType(firstVariableNode),
3344
+ fontFamily: normalizeFontFamily(extractFontFamilyFromStyle(style2)),
3345
+ fontSize: extractFontSizePtFromStyle(style2) ?? DEFAULT_FONT_SIZE,
3346
+ alignment: getElementAlignment(firstVariableNode),
3347
+ isBold: variableNodes.every((node) => (node.getFormat() & FORMAT_MASKS.bold) !== 0),
3348
+ isItalic: variableNodes.every((node) => (node.getFormat() & FORMAT_MASKS.italic) !== 0),
3349
+ isUnderline: variableNodes.every((node) => (node.getFormat() & FORMAT_MASKS.underline) !== 0),
3350
+ isStrikethrough: variableNodes.every((node) => (node.getFormat() & FORMAT_MASKS.strikethrough) !== 0),
3351
+ hasSelectedVariable: true
3352
+ };
3353
+ return;
3354
+ }
3355
+ if (!lexical.$isRangeSelection(selection2)) {
3356
+ return;
3357
+ }
3358
+ const anchorNode = selection2.anchor.getNode();
3359
+ const inlineStyleTarget = getInlineStyleTarget(selection2.getNodes(), anchorNode);
3360
+ const style = selection2.style || getInlineStyleFromNode(inlineStyleTarget);
3361
+ const isCollapsed = selection2.isCollapsed();
3362
+ snapshot = {
3363
+ blockType: extractInlineBlockTypeFromStyle(style) ?? getElementBlockType(anchorNode),
3364
+ fontFamily: normalizeFontFamily(extractFontFamilyFromStyle(style)),
3365
+ fontSize: extractFontSizePtFromStyle(style) ?? DEFAULT_FONT_SIZE,
3366
+ alignment: getElementAlignment(anchorNode),
3367
+ isBold: selection2.hasFormat("bold") || isCollapsed && hasInlineFormat(inlineStyleTarget, "bold"),
3368
+ isItalic: selection2.hasFormat("italic") || isCollapsed && hasInlineFormat(inlineStyleTarget, "italic"),
3369
+ isUnderline: selection2.hasFormat("underline") || isCollapsed && hasInlineFormat(inlineStyleTarget, "underline"),
3370
+ isStrikethrough: selection2.hasFormat("strikethrough") || isCollapsed && hasInlineFormat(inlineStyleTarget, "strikethrough"),
3371
+ hasSelectedVariable: false
3372
+ };
3373
+ });
3374
+ return snapshot;
3375
+ }
2967
3376
  const Toolbar = () => {
2968
3377
  const {
2969
3378
  activeEditor,
@@ -2980,15 +3389,15 @@ const Toolbar = () => {
2980
3389
  const { toolbarItems, toolbarEndItems } = useExtensions();
2981
3390
  const toolbarConfig = useToolbarConfig();
2982
3391
  const t = useTranslations();
2983
- const [activeBlockType, setActiveBlockType] = React.useState("paragraph");
2984
- const [activeFontFamily, setActiveFontFamily] = React.useState("Calibri");
2985
- const [activeFontSize, setActiveFontSize] = React.useState(DEFAULT_FONT_SIZE);
2986
- const normalizeFontFamily = React.useCallback((fontFamily) => {
2987
- if (fontFamily && SUPPORTED_FONTS.includes(fontFamily)) {
2988
- return fontFamily;
2989
- }
2990
- return "Calibri";
2991
- }, []);
3392
+ const toolbarStyleStore = useToolbarStyleStoreApi();
3393
+ const activeBlockType = useToolbarStyleStore((state) => state.blockType);
3394
+ const activeFontFamily = useToolbarStyleStore((state) => state.fontFamily);
3395
+ const activeFontSize = useToolbarStyleStore((state) => state.fontSize);
3396
+ const activeAlignment = useToolbarStyleStore((state) => state.alignment);
3397
+ const isBoldActive = useToolbarStyleStore((state) => state.isBold);
3398
+ const isItalicActive = useToolbarStyleStore((state) => state.isItalic);
3399
+ const isUnderlineActive = useToolbarStyleStore((state) => state.isUnderline);
3400
+ const isStrikethroughActive = useToolbarStyleStore((state) => state.isStrikethrough);
2992
3401
  const withBodySelection = React.useCallback(
2993
3402
  (editor, action) => {
2994
3403
  editor.update(() => {
@@ -3026,38 +3435,11 @@ const Toolbar = () => {
3026
3435
  );
3027
3436
  React.useEffect(() => {
3028
3437
  if (!activeEditor) {
3029
- setActiveBlockType("paragraph");
3030
- setActiveFontFamily("Calibri");
3031
- setActiveFontSize(DEFAULT_FONT_SIZE);
3438
+ toolbarStyleStore.getState().reset();
3032
3439
  return;
3033
3440
  }
3034
- const updateSelectionState = () => {
3035
- const selectedVariables = getSelectedVariableNodes(activeEditor);
3036
- if (selectedVariables.length > 0) {
3037
- const formatting = readSelectedVariableFormatting(activeEditor);
3038
- setActiveBlockType("paragraph");
3039
- setActiveFontFamily(normalizeFontFamily(formatting.fontFamily));
3040
- setActiveFontSize(formatting.fontSize ?? DEFAULT_FONT_SIZE);
3041
- return;
3042
- }
3043
- setActiveBlockType(getActiveBlockType(activeEditor));
3044
- let nextFontFamily = "Calibri";
3045
- let nextFontSize = DEFAULT_FONT_SIZE;
3046
- activeEditor.getEditorState().read(() => {
3047
- const selection2 = lexical.$getSelection();
3048
- if (!lexical.$isRangeSelection(selection2)) {
3049
- return;
3050
- }
3051
- const textNode = selection2.getNodes().find(lexical.$isTextNode);
3052
- if (!textNode) {
3053
- return;
3054
- }
3055
- const style = textNode.getStyle();
3056
- nextFontFamily = normalizeFontFamily(extractFontFamilyFromStyle(style));
3057
- nextFontSize = extractFontSizePtFromStyle(style) ?? DEFAULT_FONT_SIZE;
3058
- });
3059
- setActiveFontFamily(nextFontFamily);
3060
- setActiveFontSize(nextFontSize);
3441
+ const updateSelectionState = (editorState = activeEditor.getEditorState()) => {
3442
+ toolbarStyleStore.getState().setSnapshot(readToolbarStyleSnapshot(activeEditor, editorState));
3061
3443
  };
3062
3444
  updateSelectionState();
3063
3445
  const unregisterSelectionChange = activeEditor.registerCommand(
@@ -3068,14 +3450,14 @@ const Toolbar = () => {
3068
3450
  },
3069
3451
  lexical.COMMAND_PRIORITY_LOW
3070
3452
  );
3071
- const unregisterUpdateListener = activeEditor.registerUpdateListener(() => {
3072
- updateSelectionState();
3453
+ const unregisterUpdateListener = activeEditor.registerUpdateListener(({ editorState }) => {
3454
+ updateSelectionState(editorState);
3073
3455
  });
3074
3456
  return () => {
3075
3457
  unregisterSelectionChange();
3076
3458
  unregisterUpdateListener();
3077
3459
  };
3078
- }, [activeEditor, normalizeFontFamily]);
3460
+ }, [activeEditor, toolbarStyleStore]);
3079
3461
  const handleBold = React.useCallback(() => {
3080
3462
  debug("toolbar", `bold (globalSelection=${globalSelectionActive}, editors=${editorRegistry.all().length}, hasEditor=${!!activeEditor})`);
3081
3463
  runToolbarAction(t.history.actions.boldApplied, () => {
@@ -3260,17 +3642,17 @@ const Toolbar = () => {
3260
3642
  ] }),
3261
3643
  /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
3262
3644
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lex4-toolbar-group", "data-testid": "format-group", children: [
3263
- /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.bold, testId: "btn-bold", onClick: handleBold, children: /* @__PURE__ */ jsxRuntime.jsx(Bold, { size: 15 }) }),
3264
- /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.italic, testId: "btn-italic", onClick: handleItalic, children: /* @__PURE__ */ jsxRuntime.jsx(Italic, { size: 15 }) }),
3265
- /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.underline, testId: "btn-underline", onClick: handleUnderline, children: /* @__PURE__ */ jsxRuntime.jsx(Underline, { size: 15 }) }),
3266
- /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.strikethrough, testId: "btn-strike", onClick: handleStrikethrough, children: /* @__PURE__ */ jsxRuntime.jsx(Strikethrough, { size: 15 }) })
3645
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.bold, testId: "btn-bold", active: isBoldActive, onClick: handleBold, children: /* @__PURE__ */ jsxRuntime.jsx(Bold, { size: 15 }) }),
3646
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.italic, testId: "btn-italic", active: isItalicActive, onClick: handleItalic, children: /* @__PURE__ */ jsxRuntime.jsx(Italic, { size: 15 }) }),
3647
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.underline, testId: "btn-underline", active: isUnderlineActive, onClick: handleUnderline, children: /* @__PURE__ */ jsxRuntime.jsx(Underline, { size: 15 }) }),
3648
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.strikethrough, testId: "btn-strike", active: isStrikethroughActive, onClick: handleStrikethrough, children: /* @__PURE__ */ jsxRuntime.jsx(Strikethrough, { size: 15 }) })
3267
3649
  ] }),
3268
3650
  /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
3269
3651
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lex4-toolbar-group", "data-testid": "align-group", children: [
3270
- /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.alignLeft, testId: "btn-align-left", onClick: handleAlignLeft, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignStart, { size: 15 }) }),
3271
- /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.alignCenter, testId: "btn-align-center", onClick: handleAlignCenter, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignCenter, { size: 15 }) }),
3272
- /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.alignRight, testId: "btn-align-right", onClick: handleAlignRight, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignEnd, { size: 15 }) }),
3273
- /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.justify, testId: "btn-align-justify", onClick: handleAlignJustify, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignJustify, { size: 15 }) })
3652
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.alignLeft, testId: "btn-align-left", active: activeAlignment === "left", onClick: handleAlignLeft, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignStart, { size: 15 }) }),
3653
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.alignCenter, testId: "btn-align-center", active: activeAlignment === "center", onClick: handleAlignCenter, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignCenter, { size: 15 }) }),
3654
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.alignRight, testId: "btn-align-right", active: activeAlignment === "right", onClick: handleAlignRight, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignEnd, { size: 15 }) }),
3655
+ /* @__PURE__ */ jsxRuntime.jsx(ToolbarIconButton, { title: t.toolbar.justify, testId: "btn-align-justify", active: activeAlignment === "justify", onClick: handleAlignJustify, children: /* @__PURE__ */ jsxRuntime.jsx(TextAlignJustify, { size: 15 }) })
3274
3656
  ] }),
3275
3657
  /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
3276
3658
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lex4-toolbar-group", "data-testid": "list-group", children: [
@@ -3321,6 +3703,7 @@ const ToolbarIconButton = ({
3321
3703
  type: "button",
3322
3704
  title,
3323
3705
  "aria-label": title,
3706
+ "aria-pressed": active,
3324
3707
  disabled,
3325
3708
  onMouseDown: (e) => e.preventDefault(),
3326
3709
  onClick,