@yurikilian/lex4 0.3.2 → 1.0.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.
@@ -1740,21 +1740,21 @@ const EditorSidebar = ({
1740
1740
  return /* @__PURE__ */ jsxs(
1741
1741
  "aside",
1742
1742
  {
1743
- className: "flex h-full w-[320px] shrink-0 flex-col border-l border-gray-200 bg-white",
1743
+ className: "lex4-sidebar",
1744
1744
  "data-testid": testId,
1745
1745
  children: [
1746
- /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between border-b border-gray-200 px-3 py-2.5", children: [
1746
+ /* @__PURE__ */ jsxs("div", { className: "lex4-sidebar-header", children: [
1747
1747
  /* @__PURE__ */ jsxs("div", { children: [
1748
- /* @__PURE__ */ jsx("h2", { className: "text-xs font-semibold text-gray-900", children: title }),
1749
- subtitle && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: subtitle })
1748
+ /* @__PURE__ */ jsx("h2", { className: "lex4-sidebar-title", children: title }),
1749
+ subtitle && /* @__PURE__ */ jsx("p", { className: "lex4-sidebar-subtitle", children: subtitle })
1750
1750
  ] }),
1751
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
1751
+ /* @__PURE__ */ jsxs("div", { className: "lex4-sidebar-actions", children: [
1752
1752
  headerActions,
1753
1753
  onClose && /* @__PURE__ */ jsx(
1754
1754
  "button",
1755
1755
  {
1756
1756
  type: "button",
1757
- className: "flex h-6 w-6 items-center justify-center rounded text-gray-400\n transition-colors hover:bg-gray-100 hover:text-gray-600",
1757
+ className: "lex4-sidebar-action-btn",
1758
1758
  "data-testid": "close-editor-sidebar",
1759
1759
  onClick: onClose,
1760
1760
  "aria-label": t.sidebar.close,
@@ -1763,7 +1763,7 @@ const EditorSidebar = ({
1763
1763
  )
1764
1764
  ] })
1765
1765
  ] }),
1766
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-auto", children })
1766
+ /* @__PURE__ */ jsx("div", { className: "lex4-sidebar-body", children })
1767
1767
  ]
1768
1768
  }
1769
1769
  );
@@ -1795,7 +1795,7 @@ const HistorySidebar = () => {
1795
1795
  "aria-label": t.history.clearHistory,
1796
1796
  onMouseDown: (e) => e.preventDefault(),
1797
1797
  onClick: () => clearHistory("manual"),
1798
- className: "flex h-6 w-6 items-center justify-center rounded text-gray-400\n transition-colors hover:bg-gray-100 hover:text-gray-600",
1798
+ className: "lex4-sidebar-action-btn",
1799
1799
  "data-testid": "clear-history",
1800
1800
  children: /* @__PURE__ */ jsx(Trash2, { size: 13 })
1801
1801
  }
@@ -1812,27 +1812,27 @@ const HistorySidebar = () => {
1812
1812
  children: visibleEntries.length === 0 ? /* @__PURE__ */ jsx(
1813
1813
  "div",
1814
1814
  {
1815
- className: "px-3 py-4 text-xs text-gray-500",
1815
+ className: "lex4-history-empty",
1816
1816
  "data-testid": "history-empty",
1817
1817
  children: t.history.empty
1818
1818
  }
1819
- ) : /* @__PURE__ */ jsx("ol", { className: "divide-y divide-gray-100", "data-testid": "history-entry-list", children: visibleEntries.map((entry, reversedIndex) => {
1819
+ ) : /* @__PURE__ */ jsx("ol", { className: "lex4-history-list", "data-testid": "history-entry-list", children: visibleEntries.map((entry, reversedIndex) => {
1820
1820
  const actualIndex = historyEntries.length - reversedIndex - 1;
1821
1821
  const isCurrent = actualIndex === historyCursor - 1;
1822
1822
  return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
1823
1823
  "button",
1824
1824
  {
1825
1825
  type: "button",
1826
- className: `w-full px-3 py-2 text-left transition-colors ${isCurrent ? "bg-blue-50" : "bg-white hover:bg-gray-50"}`,
1826
+ className: `lex4-history-entry ${isCurrent ? "active" : ""}`,
1827
1827
  "data-testid": `history-entry-${actualIndex}`,
1828
1828
  "data-history-current": isCurrent ? "true" : "false",
1829
1829
  onClick: () => jumpToHistoryEntry2(actualIndex),
1830
- children: /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-2", children: [
1831
- /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
1832
- /* @__PURE__ */ jsx("div", { className: `text-xs ${isCurrent ? "font-semibold text-blue-700" : "text-gray-900"}`, children: entry.label }),
1833
- /* @__PURE__ */ jsx("div", { className: "mt-0.5 text-xs text-gray-400", children: t.regions[entry.source] ?? entry.source })
1830
+ children: /* @__PURE__ */ jsxs("div", { className: "lex4-history-entry-row", children: [
1831
+ /* @__PURE__ */ jsxs("div", { className: "lex4-history-entry-content", children: [
1832
+ /* @__PURE__ */ jsx("div", { className: `lex4-history-entry-label${isCurrent ? " current" : ""}`, children: entry.label }),
1833
+ /* @__PURE__ */ jsx("div", { className: "lex4-history-entry-source", children: t.regions[entry.source] ?? entry.source })
1834
1834
  ] }),
1835
- /* @__PURE__ */ jsx("div", { className: "shrink-0 text-xs text-gray-400", children: formatTimestamp(entry.timestamp) })
1835
+ /* @__PURE__ */ jsx("div", { className: "lex4-history-entry-time", children: formatTimestamp(entry.timestamp) })
1836
1836
  ] })
1837
1837
  }
1838
1838
  ) }, entry.id);
@@ -1848,11 +1848,11 @@ const HeaderFooterToggle = ({
1848
1848
  return /* @__PURE__ */ jsxs(
1849
1849
  "label",
1850
1850
  {
1851
- className: "flex items-center gap-1.5 cursor-pointer select-none",
1851
+ className: "lex4-hf-toggle",
1852
1852
  "data-testid": "header-footer-toggle",
1853
1853
  children: [
1854
- /* @__PURE__ */ jsx(Rows3, { size: 14, className: "text-gray-500" }),
1855
- /* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-gray-600", children: t.headerFooter.label }),
1854
+ /* @__PURE__ */ jsx(Rows3, { size: 14, className: "lex4-hf-toggle-icon" }),
1855
+ /* @__PURE__ */ jsx("span", { className: "lex4-hf-toggle-label", children: t.headerFooter.label }),
1856
1856
  /* @__PURE__ */ jsx(
1857
1857
  "button",
1858
1858
  {
@@ -1861,22 +1861,10 @@ const HeaderFooterToggle = ({
1861
1861
  "aria-checked": enabled,
1862
1862
  onMouseDown: (e) => e.preventDefault(),
1863
1863
  onClick: () => onToggle(!enabled),
1864
- className: `
1865
- relative inline-flex h-4 w-7 items-center rounded-full
1866
- transition-colors duration-200
1867
- ${enabled ? "bg-blue-500" : "bg-gray-300"}
1868
- `,
1864
+ className: "lex4-hf-switch",
1865
+ style: { backgroundColor: enabled ? "var(--lex4-color-primary)" : "var(--lex4-color-text-disabled)" },
1869
1866
  "data-testid": "header-footer-switch",
1870
- children: /* @__PURE__ */ jsx(
1871
- "span",
1872
- {
1873
- className: `
1874
- inline-block h-3 w-3 rounded-full bg-white shadow-sm
1875
- transition-transform duration-200
1876
- ${enabled ? "translate-x-3.5" : "translate-x-0.5"}
1877
- `
1878
- }
1879
- )
1867
+ children: /* @__PURE__ */ jsx("span", { className: "lex4-hf-switch-knob" })
1880
1868
  }
1881
1869
  )
1882
1870
  ]
@@ -1918,17 +1906,14 @@ const HeaderFooterActions = ({
1918
1906
  action();
1919
1907
  close();
1920
1908
  };
1921
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative", "data-testid": "header-footer-actions", children: [
1909
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, style: { position: "relative" }, "data-testid": "header-footer-actions", children: [
1922
1910
  /* @__PURE__ */ jsxs(
1923
1911
  "button",
1924
1912
  {
1925
1913
  type: "button",
1926
1914
  onMouseDown: (e) => e.preventDefault(),
1927
1915
  onClick: () => setOpen(!open),
1928
- className: `
1929
- flex h-7 items-center gap-1 rounded px-1.5 text-xs transition-colors
1930
- ${open ? "bg-blue-50 text-blue-600" : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"}
1931
- `,
1916
+ className: "lex4-settings-trigger",
1932
1917
  "data-testid": "header-footer-menu-trigger",
1933
1918
  "aria-expanded": open,
1934
1919
  "aria-haspopup": "true",
@@ -1941,12 +1926,12 @@ const HeaderFooterActions = ({
1941
1926
  open && /* @__PURE__ */ jsxs(
1942
1927
  "div",
1943
1928
  {
1944
- className: "absolute left-0 top-full mt-1 z-50 w-56 rounded-lg border border-gray-200 bg-white py-1 shadow-lg",
1929
+ className: "lex4-settings-menu",
1945
1930
  role: "menu",
1946
1931
  "data-testid": "header-footer-menu",
1947
1932
  children: [
1948
1933
  /* @__PURE__ */ jsx(SectionLabel, { children: "Page counter" }),
1949
- /* @__PURE__ */ jsx("div", { className: "px-3 pb-2 grid grid-cols-2 gap-1", "data-testid": "page-counter-mode", children: PAGE_COUNTER_OPTIONS.map(({ value, label }) => /* @__PURE__ */ jsx(
1934
+ /* @__PURE__ */ jsx("div", { className: "lex4-settings-counter-grid", "data-testid": "page-counter-mode", children: PAGE_COUNTER_OPTIONS.map(({ value, label }) => /* @__PURE__ */ jsx(
1950
1935
  "button",
1951
1936
  {
1952
1937
  type: "button",
@@ -1954,10 +1939,7 @@ const HeaderFooterActions = ({
1954
1939
  "aria-checked": pageCounterMode === value,
1955
1940
  onMouseDown: (e) => e.preventDefault(),
1956
1941
  onClick: () => onPageCounterModeChange(value),
1957
- className: `
1958
- rounded px-2 py-1 text-xs text-left transition-colors
1959
- ${pageCounterMode === value ? "bg-blue-50 text-blue-700 font-medium" : "text-gray-600 hover:bg-gray-100"}
1960
- `,
1942
+ className: "lex4-settings-counter-btn",
1961
1943
  "data-testid": `page-counter-${value}`,
1962
1944
  children: label
1963
1945
  },
@@ -2030,7 +2012,7 @@ const HeaderFooterActions = ({
2030
2012
  )
2031
2013
  ] });
2032
2014
  };
2033
- const SectionLabel = ({ children }) => /* @__PURE__ */ jsx("div", { className: "px-3 pt-2 pb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400", children });
2015
+ const SectionLabel = ({ children }) => /* @__PURE__ */ jsx("div", { className: "lex4-settings-label", children });
2034
2016
  const MenuItem = ({ icon, label, onClick, disabled, testId }) => /* @__PURE__ */ jsxs(
2035
2017
  "button",
2036
2018
  {
@@ -2039,7 +2021,7 @@ const MenuItem = ({ icon, label, onClick, disabled, testId }) => /* @__PURE__ */
2039
2021
  onMouseDown: (e) => e.preventDefault(),
2040
2022
  onClick,
2041
2023
  disabled,
2042
- className: "flex w-full items-center gap-2 px-3 py-1.5 text-xs text-gray-700 transition-colors\n hover:bg-gray-50 disabled:cursor-not-allowed disabled:text-gray-300",
2024
+ className: "lex4-settings-item",
2043
2025
  "data-testid": testId,
2044
2026
  children: [
2045
2027
  icon,
@@ -2047,7 +2029,7 @@ const MenuItem = ({ icon, label, onClick, disabled, testId }) => /* @__PURE__ */
2047
2029
  ]
2048
2030
  }
2049
2031
  );
2050
- const MenuDivider = () => /* @__PURE__ */ jsx("div", { className: "my-1 h-px bg-gray-100" });
2032
+ const MenuDivider = () => /* @__PURE__ */ jsx("div", { className: "lex4-settings-divider" });
2051
2033
  function resolveExtensions(extensions) {
2052
2034
  const resolved = {
2053
2035
  nodes: [],
@@ -2056,6 +2038,8 @@ function resolveExtensions(extensions) {
2056
2038
  sidePanels: [],
2057
2039
  providers: [],
2058
2040
  themeOverrides: {},
2041
+ cssVariables: {},
2042
+ rootClassNames: [],
2059
2043
  handleFactories: []
2060
2044
  };
2061
2045
  for (const ext of extensions) {
@@ -2067,6 +2051,10 @@ function resolveExtensions(extensions) {
2067
2051
  if (ext.themeOverrides) {
2068
2052
  resolved.themeOverrides = { ...resolved.themeOverrides, ...ext.themeOverrides };
2069
2053
  }
2054
+ if (ext.cssVariables) {
2055
+ Object.assign(resolved.cssVariables, ext.cssVariables);
2056
+ }
2057
+ if (ext.rootClassName) resolved.rootClassNames.push(ext.rootClassName);
2070
2058
  if (ext.handleMethods) resolved.handleFactories.push(ext.handleMethods);
2071
2059
  }
2072
2060
  return resolved;
@@ -2078,14 +2066,27 @@ const EMPTY_RESOLVED = {
2078
2066
  sidePanels: [],
2079
2067
  providers: [],
2080
2068
  themeOverrides: {},
2069
+ cssVariables: {},
2070
+ rootClassNames: [],
2081
2071
  handleFactories: []
2082
2072
  };
2083
2073
  const ExtensionResolvedContext = createContext(EMPTY_RESOLVED);
2074
+ function arraysEqual(a, b) {
2075
+ if (a.length !== b.length) return false;
2076
+ for (let i = 0; i < a.length; i++) {
2077
+ if (a[i] !== b[i]) return false;
2078
+ }
2079
+ return true;
2080
+ }
2084
2081
  const ExtensionProvider = ({ extensions, children }) => {
2085
- const resolved = useMemo(
2086
- () => extensions && extensions.length > 0 ? resolveExtensions(extensions) : EMPTY_RESOLVED,
2087
- [extensions]
2088
- );
2082
+ const prevNamesRef = useRef([]);
2083
+ const resolvedRef = useRef(EMPTY_RESOLVED);
2084
+ const currentNames = (extensions ?? []).map((e) => e.name);
2085
+ if (!arraysEqual(currentNames, prevNamesRef.current)) {
2086
+ resolvedRef.current = extensions && extensions.length > 0 ? resolveExtensions(extensions) : EMPTY_RESOLVED;
2087
+ prevNamesRef.current = currentNames;
2088
+ }
2089
+ const resolved = resolvedRef.current;
2089
2090
  let wrapped = /* @__PURE__ */ jsx(Fragment, { children });
2090
2091
  for (let i = resolved.providers.length - 1; i >= 0; i--) {
2091
2092
  const Provider = resolved.providers[i];
@@ -2392,11 +2393,11 @@ const Toolbar = () => {
2392
2393
  return /* @__PURE__ */ jsxs(
2393
2394
  "div",
2394
2395
  {
2395
- className: "lex4-toolbar sticky top-0 z-10 bg-white border-b border-gray-200",
2396
+ className: "lex4-toolbar",
2396
2397
  "data-testid": "toolbar",
2397
2398
  children: [
2398
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 px-2 py-1.5", children: [
2399
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "history-controls", children: [
2399
+ /* @__PURE__ */ jsxs("div", { className: "lex4-toolbar-row", children: [
2400
+ /* @__PURE__ */ jsxs("div", { className: "lex4-toolbar-group", "data-testid": "history-controls", children: [
2400
2401
  /* @__PURE__ */ jsx(
2401
2402
  ToolbarIconButton,
2402
2403
  {
@@ -2419,12 +2420,12 @@ const Toolbar = () => {
2419
2420
  )
2420
2421
  ] }),
2421
2422
  /* @__PURE__ */ jsx(Divider, {}),
2422
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
2423
- /* @__PURE__ */ jsx(Type, { size: 14, className: "text-gray-500" }),
2423
+ /* @__PURE__ */ jsxs("div", { className: "lex4-toolbar-group-gap", children: [
2424
+ /* @__PURE__ */ jsx(Type, { size: 14, className: "lex4-toolbar-icon" }),
2424
2425
  /* @__PURE__ */ jsx(
2425
2426
  "select",
2426
2427
  {
2427
- className: "h-7 rounded border border-gray-200 bg-white px-2 text-xs text-gray-700\n focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400",
2428
+ className: "lex4-toolbar-select",
2428
2429
  "data-testid": "font-selector",
2429
2430
  defaultValue: "Calibri",
2430
2431
  onChange: handleFontChange,
@@ -2432,12 +2433,12 @@ const Toolbar = () => {
2432
2433
  }
2433
2434
  )
2434
2435
  ] }),
2435
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
2436
- /* @__PURE__ */ jsx(ALargeSmall, { size: 14, className: "text-gray-500" }),
2436
+ /* @__PURE__ */ jsxs("div", { className: "lex4-toolbar-group-gap", children: [
2437
+ /* @__PURE__ */ jsx(ALargeSmall, { size: 14, className: "lex4-toolbar-icon" }),
2437
2438
  /* @__PURE__ */ jsx(
2438
2439
  "select",
2439
2440
  {
2440
- className: "h-7 w-16 rounded border border-gray-200 bg-white px-1 text-xs text-gray-700\n focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400",
2441
+ className: "lex4-toolbar-select lex4-toolbar-select-narrow",
2441
2442
  "data-testid": "font-size-selector",
2442
2443
  defaultValue: "12",
2443
2444
  onChange: handleFontSizeChange,
@@ -2446,21 +2447,21 @@ const Toolbar = () => {
2446
2447
  )
2447
2448
  ] }),
2448
2449
  /* @__PURE__ */ jsx(Divider, {}),
2449
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "format-group", children: [
2450
+ /* @__PURE__ */ jsxs("div", { className: "lex4-toolbar-group", "data-testid": "format-group", children: [
2450
2451
  /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.bold, testId: "btn-bold", onClick: handleBold, children: /* @__PURE__ */ jsx(Bold, { size: 15 }) }),
2451
2452
  /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.italic, testId: "btn-italic", onClick: handleItalic, children: /* @__PURE__ */ jsx(Italic, { size: 15 }) }),
2452
2453
  /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.underline, testId: "btn-underline", onClick: handleUnderline, children: /* @__PURE__ */ jsx(Underline, { size: 15 }) }),
2453
2454
  /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.strikethrough, testId: "btn-strike", onClick: handleStrikethrough, children: /* @__PURE__ */ jsx(Strikethrough, { size: 15 }) })
2454
2455
  ] }),
2455
2456
  /* @__PURE__ */ jsx(Divider, {}),
2456
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "align-group", children: [
2457
+ /* @__PURE__ */ jsxs("div", { className: "lex4-toolbar-group", "data-testid": "align-group", children: [
2457
2458
  /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.alignLeft, testId: "btn-align-left", onClick: handleAlignLeft, children: /* @__PURE__ */ jsx(TextAlignStart, { size: 15 }) }),
2458
2459
  /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.alignCenter, testId: "btn-align-center", onClick: handleAlignCenter, children: /* @__PURE__ */ jsx(TextAlignCenter, { size: 15 }) }),
2459
2460
  /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.alignRight, testId: "btn-align-right", onClick: handleAlignRight, children: /* @__PURE__ */ jsx(TextAlignEnd, { size: 15 }) }),
2460
2461
  /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.justify, testId: "btn-align-justify", onClick: handleAlignJustify, children: /* @__PURE__ */ jsx(TextAlignJustify, { size: 15 }) })
2461
2462
  ] }),
2462
2463
  /* @__PURE__ */ jsx(Divider, {}),
2463
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5", "data-testid": "list-group", children: [
2464
+ /* @__PURE__ */ jsxs("div", { className: "lex4-toolbar-group", "data-testid": "list-group", children: [
2464
2465
  /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.numberedList, testId: "btn-list-number", onClick: handleListNumber, children: /* @__PURE__ */ jsx(ListOrdered, { size: 15 }) }),
2465
2466
  /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.bulletList, testId: "btn-list-bullet", onClick: handleListBullet, children: /* @__PURE__ */ jsx(List, { size: 15 }) }),
2466
2467
  /* @__PURE__ */ jsx(ToolbarIconButton, { title: t.toolbar.indent, testId: "btn-indent", onClick: handleIndent, children: /* @__PURE__ */ jsx(ListIndentIncrease, { size: 15 }) }),
@@ -2470,7 +2471,7 @@ const Toolbar = () => {
2470
2471
  /* @__PURE__ */ jsx(Divider, {}),
2471
2472
  toolbarItems.map((ToolbarItem, idx) => /* @__PURE__ */ jsx(ToolbarItem, {}, idx))
2472
2473
  ] }),
2473
- /* @__PURE__ */ jsx("div", { className: "ml-auto flex items-center", children: /* @__PURE__ */ jsx(
2474
+ /* @__PURE__ */ jsx("div", { className: "lex4-toolbar-end", children: /* @__PURE__ */ jsx(
2474
2475
  ToolbarIconButton,
2475
2476
  {
2476
2477
  title: historySidebarOpen ? t.toolbar.closeHistory : t.toolbar.openHistory,
@@ -2481,7 +2482,7 @@ const Toolbar = () => {
2481
2482
  }
2482
2483
  ) })
2483
2484
  ] }),
2484
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 px-2 py-1 border-t border-gray-100", children: [
2485
+ /* @__PURE__ */ jsxs("div", { className: "lex4-hf-row", children: [
2485
2486
  /* @__PURE__ */ jsx(
2486
2487
  HeaderFooterToggle,
2487
2488
  {
@@ -2524,15 +2525,12 @@ const ToolbarIconButton = ({
2524
2525
  disabled,
2525
2526
  onMouseDown: (e) => e.preventDefault(),
2526
2527
  onClick,
2527
- className: `
2528
- flex h-7 w-7 items-center justify-center rounded transition-colors
2529
- ${disabled ? "cursor-not-allowed text-gray-300" : active ? "bg-blue-50 text-blue-600" : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"}
2530
- `,
2528
+ className: `lex4-toolbar-btn${active ? " active" : ""}`,
2531
2529
  "data-testid": testId,
2532
2530
  children
2533
2531
  }
2534
2532
  );
2535
- const Divider = () => /* @__PURE__ */ jsx("div", { className: "mx-0.5 h-5 w-px bg-gray-200" });
2533
+ const Divider = () => /* @__PURE__ */ jsx("div", { className: "lex4-toolbar-separator" });
2536
2534
  function computeBodyHeight(headerHeight, footerHeight) {
2537
2535
  const verticalMargins = PAGE_MARGIN_PX * 2;
2538
2536
  return A4_HEIGHT_PX - headerHeight - footerHeight - verticalMargins;
@@ -2839,33 +2837,33 @@ function usePagination(document2, dispatch) {
2839
2837
  };
2840
2838
  }
2841
2839
  const lexicalTheme = {
2842
- root: "lex4-root outline-none",
2843
- paragraph: "lex4-paragraph text-justify",
2840
+ root: "lex4-root",
2841
+ paragraph: "lex4-paragraph",
2844
2842
  heading: {
2845
- h1: "text-3xl font-bold mb-2",
2846
- h2: "text-2xl font-bold mb-2",
2847
- h3: "text-xl font-semibold mb-1",
2848
- h4: "text-lg font-semibold mb-1",
2849
- h5: "text-base font-medium mb-1"
2843
+ h1: "lex4-heading lex4-heading-h1",
2844
+ h2: "lex4-heading lex4-heading-h2",
2845
+ h3: "lex4-heading lex4-heading-h3",
2846
+ h4: "lex4-heading lex4-heading-h4",
2847
+ h5: "lex4-heading lex4-heading-h5"
2850
2848
  },
2851
2849
  text: {
2852
- bold: "font-bold",
2853
- italic: "italic",
2854
- underline: "underline",
2855
- strikethrough: "line-through",
2856
- underlineStrikethrough: "underline line-through"
2850
+ bold: "lex4-text-bold",
2851
+ italic: "lex4-text-italic",
2852
+ underline: "lex4-text-underline",
2853
+ strikethrough: "lex4-text-strikethrough",
2854
+ underlineStrikethrough: "lex4-text-underline lex4-text-strikethrough"
2857
2855
  },
2858
2856
  list: {
2859
2857
  nested: {
2860
- listitem: "list-none"
2858
+ listitem: "lex4-listitem-nested"
2861
2859
  },
2862
- ol: "list-decimal ml-6",
2863
- ul: "list-disc ml-6",
2860
+ ol: "lex4-list lex4-list-ordered",
2861
+ ul: "lex4-list lex4-list-unordered",
2864
2862
  listitem: "lex4-listitem",
2865
2863
  listitemChecked: "lex4-listitem-checked",
2866
2864
  listitemUnchecked: "lex4-listitem-unchecked"
2867
2865
  },
2868
- quote: "border-l-4 border-gray-300 pl-4 italic text-gray-600"
2866
+ quote: "lex4-quote"
2869
2867
  };
2870
2868
  const DEFAULT_NODES = [HeadingNode, QuoteNode, ListNode, ListItemNode];
2871
2869
  function createEditorConfig(mode, pageId, extraNodes = [], themeOverrides = {}) {
@@ -3756,8 +3754,7 @@ const PageBody = ({
3756
3754
  return /* @__PURE__ */ jsx(
3757
3755
  "div",
3758
3756
  {
3759
- className: "lex4-page-body flex-1 min-h-0 relative",
3760
- style: { overflow: "hidden" },
3757
+ className: "lex4-page-body",
3761
3758
  "data-testid": `page-body-${pageId}`,
3762
3759
  onFocus,
3763
3760
  children: /* @__PURE__ */ jsxs(LexicalComposer, { initialConfig: config, children: [
@@ -3767,11 +3764,10 @@ const PageBody = ({
3767
3764
  contentEditable: /* @__PURE__ */ jsx(
3768
3765
  ContentEditable,
3769
3766
  {
3770
- className: "outline-none h-full p-0",
3771
- style: { overflow: "visible" }
3767
+ className: "lex4-page-body-editable"
3772
3768
  }
3773
3769
  ),
3774
- placeholder: /* @__PURE__ */ jsx("div", { className: "absolute top-0 left-0 text-gray-400 pointer-events-none select-none", children: t.body.placeholder }),
3770
+ placeholder: /* @__PURE__ */ jsx("div", { className: "lex4-page-placeholder", children: t.body.placeholder }),
3775
3771
  ErrorBoundary: LexicalErrorBoundary
3776
3772
  }
3777
3773
  ),
@@ -3899,7 +3895,7 @@ const PageHeader = ({
3899
3895
  return /* @__PURE__ */ jsxs(
3900
3896
  "div",
3901
3897
  {
3902
- className: "lex4-page-header bg-blue-50/60 border-t-2 border-t-blue-200 border-b border-dashed border-blue-100 relative flex-shrink-0",
3898
+ className: "lex4-page-header",
3903
3899
  style: { maxHeight: MAX_HEADER_HEIGHT_PX, overflow: "clip" },
3904
3900
  "data-testid": `page-header-${pageId}`,
3905
3901
  children: [
@@ -3911,10 +3907,10 @@ const PageHeader = ({
3911
3907
  ContentEditable,
3912
3908
  {
3913
3909
  ref: contentRef,
3914
- className: `outline-none p-2 text-gray-600 min-h-[24px] ${hasPageCounter ? "pr-24" : ""}`
3910
+ className: `lex4-page-hf-editable${hasPageCounter ? " lex4-page-hf-narrow" : ""}`
3915
3911
  }
3916
3912
  ),
3917
- placeholder: /* @__PURE__ */ jsx("div", { className: `absolute top-0 left-0 text-gray-400 pointer-events-none select-none p-2 ${hasPageCounter ? "pr-24" : ""}`, children: t.header.placeholder }),
3913
+ placeholder: /* @__PURE__ */ jsx("div", { className: `lex4-page-hf-placeholder${hasPageCounter ? " lex4-page-hf-narrow" : ""}`, children: t.header.placeholder }),
3918
3914
  ErrorBoundary: LexicalErrorBoundary
3919
3915
  }
3920
3916
  ),
@@ -3926,7 +3922,7 @@ const PageHeader = ({
3926
3922
  pageCounterLabel && /* @__PURE__ */ jsx(
3927
3923
  "div",
3928
3924
  {
3929
- className: "pointer-events-none absolute right-2 top-2 select-none text-xs text-gray-500",
3925
+ className: "lex4-page-counter",
3930
3926
  "data-testid": `page-counter-header-${pageId}`,
3931
3927
  children: pageCounterLabel
3932
3928
  }
@@ -3979,7 +3975,7 @@ const PageFooter = ({
3979
3975
  return /* @__PURE__ */ jsxs(
3980
3976
  "div",
3981
3977
  {
3982
- className: "lex4-page-footer bg-blue-50/60 border-b-2 border-b-blue-200 border-t border-dashed border-blue-100 relative flex-shrink-0",
3978
+ className: "lex4-page-footer",
3983
3979
  style: { maxHeight: MAX_FOOTER_HEIGHT_PX, overflow: "clip" },
3984
3980
  "data-testid": `page-footer-${pageId}`,
3985
3981
  children: [
@@ -3991,10 +3987,10 @@ const PageFooter = ({
3991
3987
  ContentEditable,
3992
3988
  {
3993
3989
  ref: contentRef,
3994
- className: `outline-none p-2 text-gray-600 min-h-[24px] ${hasPageCounter ? "pr-24" : ""}`
3990
+ className: `lex4-page-hf-editable${hasPageCounter ? " lex4-page-hf-narrow" : ""}`
3995
3991
  }
3996
3992
  ),
3997
- placeholder: /* @__PURE__ */ jsx("div", { className: `absolute top-0 left-0 text-gray-400 pointer-events-none select-none p-2 ${hasPageCounter ? "pr-24" : ""}`, children: t.footer.placeholder }),
3993
+ placeholder: /* @__PURE__ */ jsx("div", { className: `lex4-page-hf-placeholder${hasPageCounter ? " lex4-page-hf-narrow" : ""}`, children: t.footer.placeholder }),
3998
3994
  ErrorBoundary: LexicalErrorBoundary
3999
3995
  }
4000
3996
  ),
@@ -4006,7 +4002,7 @@ const PageFooter = ({
4006
4002
  pageCounterLabel && /* @__PURE__ */ jsx(
4007
4003
  "div",
4008
4004
  {
4009
- className: "pointer-events-none absolute right-2 top-2 select-none text-xs text-gray-500",
4005
+ className: "lex4-page-counter",
4010
4006
  "data-testid": `page-counter-footer-${pageId}`,
4011
4007
  children: pageCounterLabel
4012
4008
  }
@@ -4064,7 +4060,7 @@ const PageView = React.memo(({
4064
4060
  return /* @__PURE__ */ jsxs(
4065
4061
  "div",
4066
4062
  {
4067
- className: "lex4-page bg-white shadow-xl flex flex-col",
4063
+ className: "lex4-page",
4068
4064
  style: {
4069
4065
  width: A4_WIDTH_PX,
4070
4066
  height: A4_HEIGHT_PX,
@@ -4349,7 +4345,7 @@ const DocumentView = () => {
4349
4345
  return /* @__PURE__ */ jsx(
4350
4346
  "div",
4351
4347
  {
4352
- className: "lex4-document-view flex flex-col items-center gap-8 py-8 min-h-full",
4348
+ className: "lex4-document-view",
4353
4349
  "data-testid": "document-view",
4354
4350
  tabIndex: -1,
4355
4351
  children: document2.pages.map((page, index) => /* @__PURE__ */ jsx(
@@ -4368,6 +4364,247 @@ const DocumentView = () => {
4368
4364
  }
4369
4365
  );
4370
4366
  };
4367
+ const AST_VERSION = "1.0.0";
4368
+ const IS_BOLD = 1;
4369
+ const IS_ITALIC = 2;
4370
+ const IS_STRIKETHROUGH = 4;
4371
+ const IS_UNDERLINE = 8;
4372
+ function decodeFormatBitmask(format) {
4373
+ const marks = {};
4374
+ if (format & IS_BOLD) marks.bold = true;
4375
+ if (format & IS_ITALIC) marks.italic = true;
4376
+ if (format & IS_UNDERLINE) marks.underline = true;
4377
+ if (format & IS_STRIKETHROUGH) marks.strikethrough = true;
4378
+ return marks;
4379
+ }
4380
+ function extractFontFamily(style) {
4381
+ const match = style.match(/font-family:\s*([^;]+)/);
4382
+ return match ? match[1].trim().replace(/['"]/g, "") : void 0;
4383
+ }
4384
+ function extractFontSizePt(style) {
4385
+ const match = style.match(/font-size:\s*(\d+(?:\.\d+)?)\s*pt/);
4386
+ return match ? parseFloat(match[1]) : void 0;
4387
+ }
4388
+ function buildTextMarks(format, style) {
4389
+ const formatMarks = decodeFormatBitmask(format);
4390
+ const fontFamily = style ? extractFontFamily(style) : void 0;
4391
+ const fontSize = style ? extractFontSizePt(style) : void 0;
4392
+ const marks = {
4393
+ ...formatMarks,
4394
+ ...fontFamily ? { fontFamily } : {},
4395
+ ...fontSize ? { fontSize } : {}
4396
+ };
4397
+ return Object.keys(marks).length > 0 ? marks : void 0;
4398
+ }
4399
+ function mapInlineNode(node) {
4400
+ switch (node.type) {
4401
+ case "text":
4402
+ return mapTextNode(node);
4403
+ case "variable-node":
4404
+ return mapVariableNode(node);
4405
+ case "linebreak":
4406
+ return mapLineBreak();
4407
+ default:
4408
+ return { type: "text", text: "" };
4409
+ }
4410
+ }
4411
+ function mapTextNode(node) {
4412
+ const marks = buildTextMarks(node.format, node.style);
4413
+ return {
4414
+ type: "text",
4415
+ text: node.text,
4416
+ ...marks ? { marks } : {}
4417
+ };
4418
+ }
4419
+ function mapVariableNode(node) {
4420
+ return {
4421
+ type: "variable",
4422
+ key: node.variableKey
4423
+ };
4424
+ }
4425
+ function mapLineBreak() {
4426
+ return { type: "linebreak" };
4427
+ }
4428
+ function mapInlineNodes(nodes) {
4429
+ return nodes.map(mapInlineNode);
4430
+ }
4431
+ const ALIGN_LEFT = 1;
4432
+ const ALIGN_CENTER = 2;
4433
+ const ALIGN_RIGHT = 3;
4434
+ const ALIGN_JUSTIFY = 4;
4435
+ function decodeAlignment(format) {
4436
+ if (typeof format === "string") {
4437
+ if (["left", "center", "right", "justify"].includes(format)) {
4438
+ return format;
4439
+ }
4440
+ return void 0;
4441
+ }
4442
+ if (typeof format !== "number" || format === 0) return void 0;
4443
+ switch (format) {
4444
+ case ALIGN_LEFT:
4445
+ return "left";
4446
+ case ALIGN_CENTER:
4447
+ return "center";
4448
+ case ALIGN_RIGHT:
4449
+ return "right";
4450
+ case ALIGN_JUSTIFY:
4451
+ return "justify";
4452
+ default:
4453
+ return void 0;
4454
+ }
4455
+ }
4456
+ function mapBlockNode(node) {
4457
+ switch (node.type) {
4458
+ case "paragraph":
4459
+ return mapParagraph(node);
4460
+ case "heading":
4461
+ return mapHeading(node);
4462
+ case "list":
4463
+ return mapList(node);
4464
+ case "quote":
4465
+ return mapBlockQuote(node);
4466
+ default:
4467
+ return {
4468
+ type: "paragraph",
4469
+ children: mapInlineChildren(node)
4470
+ };
4471
+ }
4472
+ }
4473
+ function mapParagraph(node) {
4474
+ const alignment = decodeAlignment(node.format);
4475
+ const indent = node.indent && node.indent > 0 ? node.indent : void 0;
4476
+ return {
4477
+ type: "paragraph",
4478
+ ...alignment ? { alignment } : {},
4479
+ ...indent ? { indent } : {},
4480
+ children: mapInlineChildren(node)
4481
+ };
4482
+ }
4483
+ function mapHeading(node) {
4484
+ var _a;
4485
+ const alignment = decodeAlignment(node.format);
4486
+ const tagMatch = (_a = node.tag) == null ? void 0 : _a.match(/^h(\d)$/);
4487
+ const level = tagMatch ? parseInt(tagMatch[1], 10) : 1;
4488
+ return {
4489
+ type: "heading",
4490
+ level,
4491
+ ...alignment ? { alignment } : {},
4492
+ children: mapInlineChildren(node)
4493
+ };
4494
+ }
4495
+ function mapList(node) {
4496
+ const listType = node.listType === "number" ? "ordered" : "unordered";
4497
+ const items = (node.children ?? []).filter((c) => c.type === "listitem").map(mapListItem);
4498
+ return {
4499
+ type: "list",
4500
+ listType,
4501
+ items
4502
+ };
4503
+ }
4504
+ function mapListItem(node) {
4505
+ const inlineChildren = [];
4506
+ let nestedList;
4507
+ for (const child of node.children ?? []) {
4508
+ if (child.type === "list") {
4509
+ nestedList = mapList(child);
4510
+ } else {
4511
+ const mapped = mapInlineNodes([child]);
4512
+ inlineChildren.push(...mapped);
4513
+ }
4514
+ }
4515
+ return {
4516
+ type: "list-item",
4517
+ children: inlineChildren,
4518
+ ...nestedList ? { nestedList } : {}
4519
+ };
4520
+ }
4521
+ function mapBlockQuote(node) {
4522
+ return {
4523
+ type: "blockquote",
4524
+ children: mapInlineChildren(node)
4525
+ };
4526
+ }
4527
+ function mapInlineChildren(node) {
4528
+ if (!node.children || node.children.length === 0) return [];
4529
+ return mapInlineNodes(node.children);
4530
+ }
4531
+ function mapBlockNodes(nodes) {
4532
+ return nodes.map(mapBlockNode);
4533
+ }
4534
+ function mapEditorStateToContent(state) {
4535
+ if (!state || !state.root || !state.root.children) {
4536
+ return null;
4537
+ }
4538
+ const blocks = mapBlockNodes(state.root.children);
4539
+ return { blocks };
4540
+ }
4541
+ function mapEditorStateToBlocks(state) {
4542
+ if (!state || !state.root || !state.root.children) {
4543
+ return [];
4544
+ }
4545
+ return mapBlockNodes(state.root.children);
4546
+ }
4547
+ const MARGIN_MM = Math.round(PAGE_MARGIN_PX / PX_PER_MM * 10) / 10;
4548
+ function serializeDocument(document2, variableDefinitions = []) {
4549
+ const pages = document2.pages.map(
4550
+ (page, index) => serializePage(page, index)
4551
+ );
4552
+ const metadata = buildMetadata(variableDefinitions);
4553
+ return {
4554
+ version: AST_VERSION,
4555
+ page: {
4556
+ format: "A4",
4557
+ widthMm: 210,
4558
+ heightMm: 297,
4559
+ margins: {
4560
+ topMm: MARGIN_MM,
4561
+ rightMm: MARGIN_MM,
4562
+ bottomMm: MARGIN_MM,
4563
+ leftMm: MARGIN_MM
4564
+ }
4565
+ },
4566
+ headerFooter: {
4567
+ enabled: document2.headerFooterEnabled,
4568
+ pageCounterMode: document2.pageCounterMode,
4569
+ defaultHeader: document2.pages.length > 0 ? mapEditorStateToContent(document2.pages[0].headerState) : null,
4570
+ defaultFooter: document2.pages.length > 0 ? mapEditorStateToContent(document2.pages[0].footerState) : null
4571
+ },
4572
+ pages,
4573
+ metadata
4574
+ };
4575
+ }
4576
+ function serializePage(page, pageIndex) {
4577
+ return {
4578
+ pageIndex,
4579
+ body: mapEditorStateToBlocks(page.bodyState),
4580
+ header: mapEditorStateToContent(page.headerState),
4581
+ footer: mapEditorStateToContent(page.footerState)
4582
+ };
4583
+ }
4584
+ function buildMetadata(variableDefinitions) {
4585
+ const variables = {};
4586
+ for (const def of variableDefinitions) {
4587
+ variables[def.key] = {
4588
+ key: def.key,
4589
+ label: def.label,
4590
+ ...def.description ? { description: def.description } : {},
4591
+ ...def.valueType ? { valueType: def.valueType } : {},
4592
+ ...def.group ? { group: def.group } : {}
4593
+ };
4594
+ }
4595
+ return { variables };
4596
+ }
4597
+ function buildSavePayload(ast, options) {
4598
+ return {
4599
+ document: ast,
4600
+ ...(options == null ? void 0 : options.exportTarget) ? { exportTarget: options.exportTarget } : {},
4601
+ ...(options == null ? void 0 : options.documentId) ? { documentId: options.documentId } : {},
4602
+ ...(options == null ? void 0 : options.metadata) ? { metadata: options.metadata } : {}
4603
+ };
4604
+ }
4605
+ function serializeDocumentJson(ast) {
4606
+ return JSON.stringify(ast, null, 2);
4607
+ }
4371
4608
  function selectEntireDocument(rootElement, selectionBuffer) {
4372
4609
  if (!rootElement || !selectionBuffer) {
4373
4610
  return;
@@ -4391,6 +4628,7 @@ function isFormFieldTarget(target) {
4391
4628
  }
4392
4629
  const EditorChrome = ({
4393
4630
  captureHistoryShortcutsOnWindow,
4631
+ onSave,
4394
4632
  className
4395
4633
  }) => {
4396
4634
  const {
@@ -4401,7 +4639,7 @@ const EditorChrome = ({
4401
4639
  undo,
4402
4640
  redo
4403
4641
  } = useDocument();
4404
- const { sidePanels } = useExtensions();
4642
+ const { sidePanels, cssVariables, rootClassNames } = useExtensions();
4405
4643
  const rootRef = useRef(null);
4406
4644
  const selectionBufferRef = useRef(null);
4407
4645
  const clearGlobalSelection = useCallback(() => {
@@ -4463,12 +4701,22 @@ const EditorChrome = ({
4463
4701
  });
4464
4702
  return;
4465
4703
  }
4704
+ if (key === "s" && onSave) {
4705
+ event.preventDefault();
4706
+ event.stopPropagation();
4707
+ const ast = serializeDocument(document2);
4708
+ const json = serializeDocumentJson(ast);
4709
+ onSave({ document: document2, ast, json });
4710
+ return;
4711
+ }
4466
4712
  if (handleHistoryShortcut(event)) {
4467
4713
  return;
4468
4714
  }
4469
4715
  }, [
4716
+ document2,
4470
4717
  dispatch,
4471
4718
  handleHistoryShortcut,
4719
+ onSave,
4472
4720
  setGlobalSelectionActive
4473
4721
  ]);
4474
4722
  const handleMouseDownCapture = useCallback((event) => {
@@ -4486,9 +4734,13 @@ const EditorChrome = ({
4486
4734
  const editableRoots = ((_a = rootRef.current) == null ? void 0 : _a.querySelectorAll(
4487
4735
  '[data-testid^="page-body-"] [data-lexical-editor="true"]'
4488
4736
  )) ?? [];
4737
+ const root = rootRef.current;
4738
+ const styles = root ? getComputedStyle(root) : null;
4739
+ const selBg = (styles == null ? void 0 : styles.getPropertyValue("--lex4-color-selection-bg").trim()) || GLOBAL_SELECTION_BACKGROUND;
4740
+ const selFg = (styles == null ? void 0 : styles.getPropertyValue("--lex4-color-selection-text").trim()) || GLOBAL_SELECTION_FOREGROUND;
4489
4741
  editableRoots.forEach((editableRoot) => {
4490
- editableRoot.style.backgroundColor = globalSelectionActive ? GLOBAL_SELECTION_BACKGROUND : "";
4491
- editableRoot.style.color = globalSelectionActive ? GLOBAL_SELECTION_FOREGROUND : "";
4742
+ editableRoot.style.backgroundColor = globalSelectionActive ? selBg : "";
4743
+ editableRoot.style.color = globalSelectionActive ? selFg : "";
4492
4744
  editableRoot.style.caretColor = globalSelectionActive ? "transparent" : "";
4493
4745
  });
4494
4746
  }, [globalSelectionActive, document2.pages.length]);
@@ -4521,11 +4773,14 @@ const EditorChrome = ({
4521
4773
  window.removeEventListener("beforeinput", handleWindowBeforeInput, { capture: true });
4522
4774
  };
4523
4775
  }, [captureHistoryShortcutsOnWindow, clearGlobalSelection, handleHistoryShortcut, redo, undo]);
4776
+ const rootClassName = ["lex4-editor", ...rootClassNames, className].filter(Boolean).join(" ");
4777
+ const extensionStyle = Object.keys(cssVariables).length > 0 ? cssVariables : void 0;
4524
4778
  return /* @__PURE__ */ jsxs(
4525
4779
  "div",
4526
4780
  {
4527
4781
  ref: rootRef,
4528
- className: `lex4-editor flex flex-col h-full ${className ?? ""}`,
4782
+ className: rootClassName,
4783
+ style: extensionStyle,
4529
4784
  "data-testid": "lex4-editor",
4530
4785
  "data-global-selection-active": globalSelectionActive ? "true" : "false",
4531
4786
  onKeyDownCapture: handleKeyDownCapture,
@@ -4539,12 +4794,12 @@ const EditorChrome = ({
4539
4794
  "data-testid": "global-selection-buffer",
4540
4795
  readOnly: true,
4541
4796
  tabIndex: -1,
4542
- className: "pointer-events-none fixed -left-[9999px] top-0 h-0 w-0 opacity-0"
4797
+ className: "lex4-selection-buffer"
4543
4798
  }
4544
4799
  ),
4545
4800
  /* @__PURE__ */ jsx(Toolbar, {}),
4546
- /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 overflow-hidden bg-gray-200", children: [
4547
- /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1 overflow-auto", children: /* @__PURE__ */ jsx(DocumentView, {}) }),
4801
+ /* @__PURE__ */ jsxs("div", { className: "lex4-canvas", children: [
4802
+ /* @__PURE__ */ jsx("div", { className: "lex4-canvas-scroll", children: /* @__PURE__ */ jsx(DocumentView, {}) }),
4548
4803
  sidePanels.map((Panel, idx) => /* @__PURE__ */ jsx(Panel, {}, idx)),
4549
4804
  /* @__PURE__ */ jsx(HistorySidebar, {})
4550
4805
  ] })
@@ -4552,7 +4807,7 @@ const EditorChrome = ({
4552
4807
  }
4553
4808
  );
4554
4809
  };
4555
- const EditorWithHandle = forwardRef(({ captureHistoryShortcutsOnWindow, className }, ref) => {
4810
+ const EditorWithHandle = forwardRef(({ captureHistoryShortcutsOnWindow, onSave, className }, ref) => {
4556
4811
  const { document: doc, activeEditor } = useDocument();
4557
4812
  const { handleFactories } = useExtensions();
4558
4813
  const getDocument = useCallback(() => doc, [doc]);
@@ -4570,6 +4825,7 @@ const EditorWithHandle = forwardRef(({ captureHistoryShortcutsOnWindow, classNam
4570
4825
  EditorChrome,
4571
4826
  {
4572
4827
  captureHistoryShortcutsOnWindow,
4828
+ onSave,
4573
4829
  className
4574
4830
  }
4575
4831
  );
@@ -4579,6 +4835,7 @@ const Lex4Editor = forwardRef(({
4579
4835
  captureHistoryShortcutsOnWindow = true,
4580
4836
  initialDocument,
4581
4837
  onDocumentChange,
4838
+ onSave,
4582
4839
  extensions,
4583
4840
  translations,
4584
4841
  className
@@ -4593,6 +4850,7 @@ const Lex4Editor = forwardRef(({
4593
4850
  {
4594
4851
  ref,
4595
4852
  captureHistoryShortcutsOnWindow,
4853
+ onSave,
4596
4854
  className
4597
4855
  }
4598
4856
  )
@@ -4665,247 +4923,6 @@ function useHeaderFooter(maxHeight, onHeightChange) {
4665
4923
  }, []);
4666
4924
  return { attachRef };
4667
4925
  }
4668
- const AST_VERSION = "1.0.0";
4669
- const IS_BOLD = 1;
4670
- const IS_ITALIC = 2;
4671
- const IS_STRIKETHROUGH = 4;
4672
- const IS_UNDERLINE = 8;
4673
- function decodeFormatBitmask(format) {
4674
- const marks = {};
4675
- if (format & IS_BOLD) marks.bold = true;
4676
- if (format & IS_ITALIC) marks.italic = true;
4677
- if (format & IS_UNDERLINE) marks.underline = true;
4678
- if (format & IS_STRIKETHROUGH) marks.strikethrough = true;
4679
- return marks;
4680
- }
4681
- function extractFontFamily(style) {
4682
- const match = style.match(/font-family:\s*([^;]+)/);
4683
- return match ? match[1].trim().replace(/['"]/g, "") : void 0;
4684
- }
4685
- function extractFontSizePt(style) {
4686
- const match = style.match(/font-size:\s*(\d+(?:\.\d+)?)\s*pt/);
4687
- return match ? parseFloat(match[1]) : void 0;
4688
- }
4689
- function buildTextMarks(format, style) {
4690
- const formatMarks = decodeFormatBitmask(format);
4691
- const fontFamily = style ? extractFontFamily(style) : void 0;
4692
- const fontSize = style ? extractFontSizePt(style) : void 0;
4693
- const marks = {
4694
- ...formatMarks,
4695
- ...fontFamily ? { fontFamily } : {},
4696
- ...fontSize ? { fontSize } : {}
4697
- };
4698
- return Object.keys(marks).length > 0 ? marks : void 0;
4699
- }
4700
- function mapInlineNode(node) {
4701
- switch (node.type) {
4702
- case "text":
4703
- return mapTextNode(node);
4704
- case "variable-node":
4705
- return mapVariableNode(node);
4706
- case "linebreak":
4707
- return mapLineBreak();
4708
- default:
4709
- return { type: "text", text: "" };
4710
- }
4711
- }
4712
- function mapTextNode(node) {
4713
- const marks = buildTextMarks(node.format, node.style);
4714
- return {
4715
- type: "text",
4716
- text: node.text,
4717
- ...marks ? { marks } : {}
4718
- };
4719
- }
4720
- function mapVariableNode(node) {
4721
- return {
4722
- type: "variable",
4723
- key: node.variableKey
4724
- };
4725
- }
4726
- function mapLineBreak() {
4727
- return { type: "linebreak" };
4728
- }
4729
- function mapInlineNodes(nodes) {
4730
- return nodes.map(mapInlineNode);
4731
- }
4732
- const ALIGN_LEFT = 1;
4733
- const ALIGN_CENTER = 2;
4734
- const ALIGN_RIGHT = 3;
4735
- const ALIGN_JUSTIFY = 4;
4736
- function decodeAlignment(format) {
4737
- if (typeof format === "string") {
4738
- if (["left", "center", "right", "justify"].includes(format)) {
4739
- return format;
4740
- }
4741
- return void 0;
4742
- }
4743
- if (typeof format !== "number" || format === 0) return void 0;
4744
- switch (format) {
4745
- case ALIGN_LEFT:
4746
- return "left";
4747
- case ALIGN_CENTER:
4748
- return "center";
4749
- case ALIGN_RIGHT:
4750
- return "right";
4751
- case ALIGN_JUSTIFY:
4752
- return "justify";
4753
- default:
4754
- return void 0;
4755
- }
4756
- }
4757
- function mapBlockNode(node) {
4758
- switch (node.type) {
4759
- case "paragraph":
4760
- return mapParagraph(node);
4761
- case "heading":
4762
- return mapHeading(node);
4763
- case "list":
4764
- return mapList(node);
4765
- case "quote":
4766
- return mapBlockQuote(node);
4767
- default:
4768
- return {
4769
- type: "paragraph",
4770
- children: mapInlineChildren(node)
4771
- };
4772
- }
4773
- }
4774
- function mapParagraph(node) {
4775
- const alignment = decodeAlignment(node.format);
4776
- const indent = node.indent && node.indent > 0 ? node.indent : void 0;
4777
- return {
4778
- type: "paragraph",
4779
- ...alignment ? { alignment } : {},
4780
- ...indent ? { indent } : {},
4781
- children: mapInlineChildren(node)
4782
- };
4783
- }
4784
- function mapHeading(node) {
4785
- var _a;
4786
- const alignment = decodeAlignment(node.format);
4787
- const tagMatch = (_a = node.tag) == null ? void 0 : _a.match(/^h(\d)$/);
4788
- const level = tagMatch ? parseInt(tagMatch[1], 10) : 1;
4789
- return {
4790
- type: "heading",
4791
- level,
4792
- ...alignment ? { alignment } : {},
4793
- children: mapInlineChildren(node)
4794
- };
4795
- }
4796
- function mapList(node) {
4797
- const listType = node.listType === "number" ? "ordered" : "unordered";
4798
- const items = (node.children ?? []).filter((c) => c.type === "listitem").map(mapListItem);
4799
- return {
4800
- type: "list",
4801
- listType,
4802
- items
4803
- };
4804
- }
4805
- function mapListItem(node) {
4806
- const inlineChildren = [];
4807
- let nestedList;
4808
- for (const child of node.children ?? []) {
4809
- if (child.type === "list") {
4810
- nestedList = mapList(child);
4811
- } else {
4812
- const mapped = mapInlineNodes([child]);
4813
- inlineChildren.push(...mapped);
4814
- }
4815
- }
4816
- return {
4817
- type: "list-item",
4818
- children: inlineChildren,
4819
- ...nestedList ? { nestedList } : {}
4820
- };
4821
- }
4822
- function mapBlockQuote(node) {
4823
- return {
4824
- type: "blockquote",
4825
- children: mapInlineChildren(node)
4826
- };
4827
- }
4828
- function mapInlineChildren(node) {
4829
- if (!node.children || node.children.length === 0) return [];
4830
- return mapInlineNodes(node.children);
4831
- }
4832
- function mapBlockNodes(nodes) {
4833
- return nodes.map(mapBlockNode);
4834
- }
4835
- function mapEditorStateToContent(state) {
4836
- if (!state || !state.root || !state.root.children) {
4837
- return null;
4838
- }
4839
- const blocks = mapBlockNodes(state.root.children);
4840
- return { blocks };
4841
- }
4842
- function mapEditorStateToBlocks(state) {
4843
- if (!state || !state.root || !state.root.children) {
4844
- return [];
4845
- }
4846
- return mapBlockNodes(state.root.children);
4847
- }
4848
- const MARGIN_MM = Math.round(PAGE_MARGIN_PX / PX_PER_MM * 10) / 10;
4849
- function serializeDocument(document2, variableDefinitions = []) {
4850
- const pages = document2.pages.map(
4851
- (page, index) => serializePage(page, index)
4852
- );
4853
- const metadata = buildMetadata(variableDefinitions);
4854
- return {
4855
- version: AST_VERSION,
4856
- page: {
4857
- format: "A4",
4858
- widthMm: 210,
4859
- heightMm: 297,
4860
- margins: {
4861
- topMm: MARGIN_MM,
4862
- rightMm: MARGIN_MM,
4863
- bottomMm: MARGIN_MM,
4864
- leftMm: MARGIN_MM
4865
- }
4866
- },
4867
- headerFooter: {
4868
- enabled: document2.headerFooterEnabled,
4869
- pageCounterMode: document2.pageCounterMode,
4870
- defaultHeader: document2.pages.length > 0 ? mapEditorStateToContent(document2.pages[0].headerState) : null,
4871
- defaultFooter: document2.pages.length > 0 ? mapEditorStateToContent(document2.pages[0].footerState) : null
4872
- },
4873
- pages,
4874
- metadata
4875
- };
4876
- }
4877
- function serializePage(page, pageIndex) {
4878
- return {
4879
- pageIndex,
4880
- body: mapEditorStateToBlocks(page.bodyState),
4881
- header: mapEditorStateToContent(page.headerState),
4882
- footer: mapEditorStateToContent(page.footerState)
4883
- };
4884
- }
4885
- function buildMetadata(variableDefinitions) {
4886
- const variables = {};
4887
- for (const def of variableDefinitions) {
4888
- variables[def.key] = {
4889
- key: def.key,
4890
- label: def.label,
4891
- ...def.description ? { description: def.description } : {},
4892
- ...def.valueType ? { valueType: def.valueType } : {},
4893
- ...def.group ? { group: def.group } : {}
4894
- };
4895
- }
4896
- return { variables };
4897
- }
4898
- function buildSavePayload(ast, options) {
4899
- return {
4900
- document: ast,
4901
- ...(options == null ? void 0 : options.exportTarget) ? { exportTarget: options.exportTarget } : {},
4902
- ...(options == null ? void 0 : options.documentId) ? { documentId: options.documentId } : {},
4903
- ...(options == null ? void 0 : options.metadata) ? { metadata: options.metadata } : {}
4904
- };
4905
- }
4906
- function serializeDocumentJson(ast) {
4907
- return JSON.stringify(ast, null, 2);
4908
- }
4909
4926
  function astExtension() {
4910
4927
  return {
4911
4928
  name: "ast",
@@ -5031,7 +5048,7 @@ function VariableChip({ variableKey }) {
5031
5048
  return /* @__PURE__ */ jsx(
5032
5049
  "span",
5033
5050
  {
5034
- className: "lex4-variable-chip inline-flex items-center rounded-full border border-blue-300\n bg-white px-2 py-0.5 text-[11px] font-medium text-blue-700 select-none\n cursor-default whitespace-nowrap mx-0.5",
5051
+ className: "lex4-variable-chip",
5035
5052
  "data-testid": `variable-chip-${variableKey}`,
5036
5053
  title: variableKey,
5037
5054
  children: label
@@ -5104,12 +5121,12 @@ const VariablePicker = ({ onInsert, disabled = false }) => {
5104
5121
  document.addEventListener("mousedown", handler);
5105
5122
  return () => document.removeEventListener("mousedown", handler);
5106
5123
  }, [open]);
5107
- return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "relative inline-block", children: [
5124
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "lex4-variable-picker", children: [
5108
5125
  /* @__PURE__ */ jsx(
5109
5126
  "button",
5110
5127
  {
5111
5128
  type: "button",
5112
- className: "h-7 rounded border border-gray-200 bg-white px-2 text-xs font-medium text-gray-700\n hover:bg-gray-50 focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-400\n disabled:opacity-50 disabled:cursor-not-allowed",
5129
+ className: "lex4-variable-picker-btn",
5113
5130
  "data-testid": "variable-picker-button",
5114
5131
  disabled: disabled || definitions.length === 0,
5115
5132
  onClick: () => setOpen(!open),
@@ -5120,14 +5137,13 @@ const VariablePicker = ({ onInsert, disabled = false }) => {
5120
5137
  open && /* @__PURE__ */ jsxs(
5121
5138
  "div",
5122
5139
  {
5123
- className: "absolute left-0 top-full z-50 mt-1 w-64 rounded-md border border-gray-200\n bg-white shadow-lg",
5140
+ className: "lex4-variable-picker-dropdown",
5124
5141
  "data-testid": "variable-picker-dropdown",
5125
5142
  children: [
5126
- /* @__PURE__ */ jsx("div", { className: "border-b border-gray-100 p-2", children: /* @__PURE__ */ jsx(
5143
+ /* @__PURE__ */ jsx("div", { className: "lex4-variable-picker-search", children: /* @__PURE__ */ jsx(
5127
5144
  "input",
5128
5145
  {
5129
5146
  type: "text",
5130
- className: "w-full rounded border border-gray-200 px-2 py-1 text-xs\n focus:border-blue-400 focus:outline-none",
5131
5147
  placeholder: "Search variables...",
5132
5148
  "data-testid": "variable-picker-search",
5133
5149
  value: filter,
@@ -5135,20 +5151,20 @@ const VariablePicker = ({ onInsert, disabled = false }) => {
5135
5151
  autoFocus: true
5136
5152
  }
5137
5153
  ) }),
5138
- /* @__PURE__ */ jsxs("div", { className: "max-h-48 overflow-y-auto p-1", children: [
5139
- Object.keys(grouped).length === 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-1 text-xs text-gray-400", children: "No variables found" }),
5154
+ /* @__PURE__ */ jsxs("div", { className: "lex4-variable-picker-list", children: [
5155
+ Object.keys(grouped).length === 0 && /* @__PURE__ */ jsx("div", { className: "lex4-variable-picker-empty", children: "No variables found" }),
5140
5156
  Object.entries(grouped).map(([group, defs]) => /* @__PURE__ */ jsxs("div", { children: [
5141
- /* @__PURE__ */ jsx("div", { className: "px-2 py-1 text-[10px] font-semibold uppercase text-gray-400", children: group }),
5157
+ /* @__PURE__ */ jsx("div", { className: "lex4-variable-picker-group-label", children: group }),
5142
5158
  defs.map((def) => /* @__PURE__ */ jsxs(
5143
5159
  "button",
5144
5160
  {
5145
5161
  type: "button",
5146
- className: "flex w-full items-center gap-2 rounded px-2 py-1 text-left text-xs\n hover:bg-blue-50",
5162
+ className: "lex4-variable-picker-option",
5147
5163
  "data-testid": `variable-option-${def.key}`,
5148
5164
  onClick: () => handleInsert(def.key),
5149
5165
  children: [
5150
- /* @__PURE__ */ jsx("span", { className: "font-medium text-blue-700", children: `{{${def.key}}}` }),
5151
- /* @__PURE__ */ jsx("span", { className: "text-gray-500", children: def.label })
5166
+ /* @__PURE__ */ jsx("span", { className: "lex4-variable-picker-key", children: `{{${def.key}}}` }),
5167
+ /* @__PURE__ */ jsx("span", { className: "lex4-variable-picker-label", children: def.label })
5152
5168
  ]
5153
5169
  },
5154
5170
  def.key
@@ -5206,20 +5222,20 @@ const VariablePanel = ({ open, onClose }) => {
5206
5222
  "button",
5207
5223
  {
5208
5224
  type: "button",
5209
- className: "flex h-6 w-6 items-center justify-center rounded text-gray-400\n transition-colors hover:bg-gray-100 hover:text-gray-600",
5225
+ className: "lex4-sidebar-action-btn",
5210
5226
  title: t.variables.refreshVariables,
5211
5227
  "data-testid": "btn-refresh-variables",
5212
5228
  children: /* @__PURE__ */ jsx(RefreshCw, { size: 12 })
5213
5229
  }
5214
5230
  ),
5215
5231
  children: [
5216
- /* @__PURE__ */ jsx("div", { className: "p-3", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
5217
- /* @__PURE__ */ jsx(Search, { size: 14, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400" }),
5232
+ /* @__PURE__ */ jsx("div", { className: "lex4-variable-search-container", children: /* @__PURE__ */ jsxs("div", { className: "lex4-variable-search-wrapper", children: [
5233
+ /* @__PURE__ */ jsx(Search, { size: 14, className: "lex4-variable-search-icon" }),
5218
5234
  /* @__PURE__ */ jsx(
5219
5235
  "input",
5220
5236
  {
5221
5237
  type: "text",
5222
- className: "w-full rounded-lg border border-gray-200 bg-gray-50 py-1.5 pl-8 pr-3 text-xs\n placeholder-gray-400 focus:border-blue-400 focus:bg-white focus:outline-none\n focus:ring-1 focus:ring-blue-400",
5238
+ className: "lex4-variable-search-input",
5223
5239
  placeholder: t.variables.searchPlaceholder,
5224
5240
  "data-testid": "variable-panel-search",
5225
5241
  value: filter,
@@ -5227,25 +5243,19 @@ const VariablePanel = ({ open, onClose }) => {
5227
5243
  }
5228
5244
  )
5229
5245
  ] }) }),
5230
- /* @__PURE__ */ jsxs("div", { className: "px-3 pb-3", children: [
5231
- Object.keys(grouped).length === 0 && /* @__PURE__ */ jsx("div", { className: "py-4 text-center text-xs text-gray-400", children: t.variables.noVariablesFound }),
5232
- Object.entries(grouped).map(([group, defs]) => /* @__PURE__ */ jsx("div", { className: "mb-3", children: /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: defs.map((def) => /* @__PURE__ */ jsxs(
5246
+ /* @__PURE__ */ jsxs("div", { className: "lex4-variable-list", children: [
5247
+ Object.keys(grouped).length === 0 && /* @__PURE__ */ jsx("div", { className: "lex4-variable-list-empty", children: t.variables.noVariablesFound }),
5248
+ Object.entries(grouped).map(([group, defs]) => /* @__PURE__ */ jsx("div", { className: "lex4-variable-group", children: /* @__PURE__ */ jsx("div", { children: defs.map((def) => /* @__PURE__ */ jsxs(
5233
5249
  "button",
5234
5250
  {
5235
5251
  type: "button",
5236
- className: "flex items-center justify-between gap-2 rounded-lg px-2.5 py-1.5 text-left text-xs\n transition-colors hover:bg-blue-50 group",
5252
+ className: "lex4-variable-list-item",
5237
5253
  "data-testid": `variable-panel-${def.key}`,
5238
5254
  onClick: () => handleInsert(def.key),
5239
5255
  disabled: !activeEditor,
5240
5256
  children: [
5241
- /* @__PURE__ */ jsx(
5242
- "span",
5243
- {
5244
- className: "inline-flex items-center rounded-full border border-blue-300 bg-white\n px-2 py-0.5 text-[11px] font-medium text-blue-700\n group-hover:border-blue-400 group-hover:bg-blue-50",
5245
- children: def.label
5246
- }
5247
- ),
5248
- /* @__PURE__ */ jsx("span", { className: "text-[10px] font-semibold uppercase tracking-wider text-gray-400", children: group })
5257
+ /* @__PURE__ */ jsx("span", { className: "lex4-variable-badge", children: def.label }),
5258
+ /* @__PURE__ */ jsx("span", { className: "lex4-variable-group-label", children: group })
5249
5259
  ]
5250
5260
  },
5251
5261
  def.key
@@ -5299,10 +5309,7 @@ const VariablePanelToggle = () => {
5299
5309
  "aria-label": panelOpen ? t.variables.closePanel : t.variables.openPanel,
5300
5310
  onMouseDown: (e) => e.preventDefault(),
5301
5311
  onClick: () => setPanelOpen(!panelOpen),
5302
- className: `
5303
- flex h-7 w-7 items-center justify-center rounded transition-colors
5304
- ${panelOpen ? "bg-blue-50 text-blue-600" : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"}
5305
- `,
5312
+ className: `lex4-toolbar-btn${panelOpen ? " active" : ""}`,
5306
5313
  "data-testid": "toggle-variable-panel",
5307
5314
  children: /* @__PURE__ */ jsx(Braces, { size: 15 })
5308
5315
  }