@pixerate/editor-react 0.15.0 → 0.17.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.
Files changed (54) hide show
  1. package/README.md +6 -2
  2. package/dist/canvas/index.cjs +484 -119
  3. package/dist/canvas/index.cjs.map +1 -1
  4. package/dist/canvas/index.d.cts +122 -3
  5. package/dist/canvas/index.d.ts +122 -3
  6. package/dist/canvas/index.js +380 -0
  7. package/dist/canvas/index.js.map +1 -1
  8. package/dist/chunk-I3E4KSA2.js +515 -0
  9. package/dist/chunk-I3E4KSA2.js.map +1 -0
  10. package/dist/chunk-JR5ZBDR7.js +2012 -0
  11. package/dist/chunk-JR5ZBDR7.js.map +1 -0
  12. package/dist/chunk-XWFU2CSG.js +167 -0
  13. package/dist/chunk-XWFU2CSG.js.map +1 -0
  14. package/dist/chunk-ZU54BMDC.js +2128 -0
  15. package/dist/chunk-ZU54BMDC.js.map +1 -0
  16. package/dist/generation/index.cjs +525 -0
  17. package/dist/generation/index.cjs.map +1 -0
  18. package/dist/generation/index.d.cts +124 -0
  19. package/dist/generation/index.d.ts +124 -0
  20. package/dist/generation/index.js +19 -0
  21. package/dist/generation/index.js.map +1 -0
  22. package/dist/image-editor/index.cjs +988 -337
  23. package/dist/image-editor/index.cjs.map +1 -1
  24. package/dist/image-editor/index.d.cts +92 -2
  25. package/dist/image-editor/index.d.ts +92 -2
  26. package/dist/image-editor/index.js +17 -3
  27. package/dist/index.cjs +3628 -754
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +226 -5
  30. package/dist/index.d.ts +226 -5
  31. package/dist/index.js +868 -77
  32. package/dist/index.js.map +1 -1
  33. package/dist/persistence/index.cjs +191 -0
  34. package/dist/persistence/index.cjs.map +1 -0
  35. package/dist/persistence/index.d.cts +77 -0
  36. package/dist/persistence/index.d.ts +77 -0
  37. package/dist/persistence/index.js +9 -0
  38. package/dist/persistence/index.js.map +1 -0
  39. package/dist/spreadsheet/index.cjs +949 -111
  40. package/dist/spreadsheet/index.cjs.map +1 -1
  41. package/dist/spreadsheet/index.d.cts +121 -7
  42. package/dist/spreadsheet/index.d.ts +121 -7
  43. package/dist/spreadsheet/index.js +11 -1
  44. package/dist/tokens/index.cjs +395 -0
  45. package/dist/tokens/index.cjs.map +1 -0
  46. package/dist/tokens/index.d.cts +54 -0
  47. package/dist/tokens/index.d.ts +54 -0
  48. package/dist/tokens/index.js +375 -0
  49. package/dist/tokens/index.js.map +1 -0
  50. package/package.json +26 -2
  51. package/dist/chunk-LPFZMZ3L.js +0 -1461
  52. package/dist/chunk-LPFZMZ3L.js.map +0 -1
  53. package/dist/chunk-QVQYYHJ5.js +0 -1161
  54. package/dist/chunk-QVQYYHJ5.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,22 +1,47 @@
1
1
  import {
2
2
  FormulaHints,
3
+ NameManager,
4
+ SheetTabs,
5
+ SpreadsheetConditionalFormatPanel,
3
6
  SpreadsheetContextMenu,
7
+ SpreadsheetDataValidationPanel,
4
8
  SpreadsheetEditor,
5
9
  SpreadsheetGroupRow,
10
+ SpreadsheetValidationDialog,
6
11
  useFillHandle,
7
12
  useSpreadsheetClipboard,
8
13
  useSpreadsheetEditor
9
- } from "./chunk-QVQYYHJ5.js";
14
+ } from "./chunk-JR5ZBDR7.js";
10
15
  import {
16
+ AdjustmentLayersPanel,
17
+ ControlSlider,
18
+ CurvesEditor,
19
+ Histogram,
11
20
  ImageEditor,
12
- useImageEditor
13
- } from "./chunk-LPFZMZ3L.js";
21
+ RefineEdgePanel,
22
+ SelectionOptionsBar,
23
+ useImageEditor,
24
+ useImageHistogram
25
+ } from "./chunk-ZU54BMDC.js";
14
26
  import {
15
27
  CommandPalette,
16
28
  useCommandPalette,
17
29
  useCommandShortcuts,
18
30
  useRegisterCommands
19
31
  } from "./chunk-4ZC2WCUO.js";
32
+ import {
33
+ RecoveryPrompt,
34
+ useAutosave
35
+ } from "./chunk-XWFU2CSG.js";
36
+ import {
37
+ AssetMentionChips,
38
+ GenerationHistory,
39
+ GenerationSettings,
40
+ JobProgress,
41
+ useGenerationPlan,
42
+ useGenerationRecords,
43
+ useJobProgress
44
+ } from "./chunk-I3E4KSA2.js";
20
45
 
21
46
  // src/hooks/usePromptEditor.ts
22
47
  import { useMemo, useEffect, useRef } from "react";
@@ -561,7 +586,8 @@ function useHistoryShortcuts(manager, options = {}) {
561
586
  // src/hooks/useFileImport.ts
562
587
  import { useEffect as useEffect5, useMemo as useMemo3, useRef as useRef6, useSyncExternalStore as useSyncExternalStore3 } from "react";
563
588
  import {
564
- FileImportController
589
+ FileImportController,
590
+ summarizeImportStructure
565
591
  } from "@pixerate/editor";
566
592
  function useFileImport(editor, options = {}) {
567
593
  const editorRef = useRef6(editor);
@@ -577,8 +603,10 @@ function useFileImport(editor, options = {}) {
577
603
  controller.setOptions(options);
578
604
  });
579
605
  const state = useSyncExternalStore3(controller.subscribe, controller.getState, controller.getState);
606
+ const structureSummary = useMemo3(() => summarizeImportStructure(state.lastOutcomes), [state.lastOutcomes]);
580
607
  return {
581
608
  ...state,
609
+ structureSummary,
582
610
  accept: controller.accept,
583
611
  openFilePicker: controller.openFilePicker,
584
612
  importFiles: controller.importFiles
@@ -660,11 +688,12 @@ var BubbleMenu = ({
660
688
  // src/components/TemplateRenderer.tsx
661
689
  import { useMemo as useMemo4 } from "react";
662
690
  import {
691
+ describeConditionalTag,
663
692
  evaluatePromptTokens,
693
+ formatConditionalTag,
664
694
  resolveTemplates
665
695
  } from "@pixerate/editor";
666
696
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
667
- var CONDITIONAL_PREFIX = { if: "?", unless: "!", end: "/" };
668
697
  var TemplateRenderer = ({
669
698
  content,
670
699
  templates = [],
@@ -747,8 +776,8 @@ var TemplateRenderer = ({
747
776
  {
748
777
  "data-conditional": token.conditional,
749
778
  className: "inline-flex items-center px-1 py-0.5 mx-0.5 text-xs font-medium rounded-md border border-amber-400/40 bg-amber-400/15 text-amber-700 dark:text-amber-300",
750
- title: token.conditional === "end" ? `End of section: ${token.value}` : token.conditional === "if" ? `Shown when "${token.value}" has a value` : `Shown when "${token.value}" is empty`,
751
- children: `{${CONDITIONAL_PREFIX[token.conditional]}${token.value}}`
779
+ title: describeConditionalTag(token),
780
+ children: formatConditionalTag(token)
752
781
  },
753
782
  index
754
783
  );
@@ -856,17 +885,312 @@ var VariableForm = ({
856
885
  }) });
857
886
  };
858
887
 
888
+ // src/components/MergePreview.tsx
889
+ import { useId as useId2, useState as useState7 } from "react";
890
+
891
+ // src/hooks/useMergePreview.ts
892
+ import { useCallback as useCallback5, useMemo as useMemo6, useState as useState6 } from "react";
893
+ import {
894
+ extractVariables,
895
+ matchFields,
896
+ mergeRecords,
897
+ parseDataSource,
898
+ recordToValues,
899
+ serializeMergeOutputs
900
+ } from "@pixerate/editor";
901
+ function useMergePreview(options) {
902
+ const { template, templates, bindings, parseOptions } = options;
903
+ const [sourceText, setSourceTextState] = useState6(options.initialSource ?? "");
904
+ const [overrides, setOverrides] = useState6({});
905
+ const [index, setIndexState] = useState6(0);
906
+ const [result, setResult] = useState6(null);
907
+ const source = useMemo6(
908
+ () => parseDataSource(sourceText, parseOptions),
909
+ [sourceText, parseOptions]
910
+ );
911
+ const variables = useMemo6(
912
+ () => extractVariables(template, { templates, bindings }).map((v) => v.name),
913
+ [template, templates, bindings]
914
+ );
915
+ const mapping = useMemo6(() => {
916
+ const merged = {
917
+ ...matchFields(variables, source.fields).mapping
918
+ };
919
+ for (const [variable, field] of Object.entries(overrides)) {
920
+ if (variables.includes(variable) && (field === null || source.fields.includes(field))) {
921
+ merged[variable] = field;
922
+ }
923
+ }
924
+ return merged;
925
+ }, [variables, source.fields, overrides]);
926
+ const count = source.records.length;
927
+ const current = count === 0 ? 0 : Math.min(index, count - 1);
928
+ const record = source.records[current];
929
+ const values = useMemo6(
930
+ () => record ? recordToValues(record, mapping) : {},
931
+ [record, mapping]
932
+ );
933
+ const setSourceText = useCallback5((text) => {
934
+ setSourceTextState(text);
935
+ setIndexState(0);
936
+ setResult(null);
937
+ }, []);
938
+ const loadFile = useCallback5(
939
+ async (file) => setSourceText(await file.text()),
940
+ [setSourceText]
941
+ );
942
+ const setIndex = useCallback5(
943
+ (next) => setIndexState(Math.max(0, Math.min(next, count - 1))),
944
+ [count]
945
+ );
946
+ const setFieldMapping = useCallback5((variable, field) => {
947
+ setOverrides((previous) => ({ ...previous, [variable]: field }));
948
+ setResult(null);
949
+ }, []);
950
+ const resetMapping = useCallback5(() => setOverrides({}), []);
951
+ const merge = useCallback5(() => {
952
+ const merged = mergeRecords(template, source, {
953
+ templates,
954
+ bindings,
955
+ mapping,
956
+ skipIf: options.skipIf,
957
+ separator: options.separator,
958
+ maxOutputs: options.maxOutputs
959
+ });
960
+ setResult(merged);
961
+ return merged;
962
+ }, [template, source, templates, bindings, mapping, options.skipIf, options.separator, options.maxOutputs]);
963
+ const serialize = useCallback5(
964
+ (format) => result ? serializeMergeOutputs(result.outputs, format, options.separator) : "",
965
+ [result, options.separator]
966
+ );
967
+ return {
968
+ sourceText,
969
+ setSourceText,
970
+ loadFile,
971
+ source,
972
+ variables,
973
+ mapping,
974
+ setFieldMapping,
975
+ resetMapping,
976
+ index: current,
977
+ count,
978
+ setIndex,
979
+ next: () => setIndex(current + 1),
980
+ prev: () => setIndex(current - 1),
981
+ record,
982
+ values,
983
+ merge,
984
+ result,
985
+ serialize
986
+ };
987
+ }
988
+
989
+ // src/components/MergePreview.tsx
990
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
991
+ function downloadText(text, fileName, type) {
992
+ if (typeof document === "undefined" || typeof URL.createObjectURL !== "function") {
993
+ return;
994
+ }
995
+ const url = URL.createObjectURL(new Blob([text], { type }));
996
+ const link = document.createElement("a");
997
+ link.href = url;
998
+ link.download = fileName;
999
+ link.click();
1000
+ setTimeout(() => URL.revokeObjectURL(url), 0);
1001
+ }
1002
+ var MergePreview = ({
1003
+ content,
1004
+ templates,
1005
+ bindings,
1006
+ templateColorMap,
1007
+ defaultSource,
1008
+ parseOptions,
1009
+ skipIf,
1010
+ separator,
1011
+ maxOutputs,
1012
+ onMerge,
1013
+ downloadName = "merged",
1014
+ className = "flex flex-col gap-3 text-sm"
1015
+ }) => {
1016
+ const id = useId2();
1017
+ const merge = useMergePreview({
1018
+ template: content,
1019
+ templates,
1020
+ bindings,
1021
+ initialSource: defaultSource,
1022
+ parseOptions,
1023
+ skipIf,
1024
+ separator,
1025
+ maxOutputs
1026
+ });
1027
+ const [copied, setCopied] = useState7(false);
1028
+ const { source, result } = merge;
1029
+ const copy = async () => {
1030
+ try {
1031
+ await navigator.clipboard.writeText(merge.serialize("text"));
1032
+ setCopied(true);
1033
+ } catch {
1034
+ setCopied(false);
1035
+ }
1036
+ };
1037
+ return /* @__PURE__ */ jsxs3("div", { className, "data-merge-preview": "", children: [
1038
+ /* @__PURE__ */ jsxs3("label", { htmlFor: `${id}-source`, className: "flex flex-col gap-1", children: [
1039
+ /* @__PURE__ */ jsx4("span", { className: "font-medium", children: "Data source (CSV, TSV or JSON)" }),
1040
+ /* @__PURE__ */ jsx4(
1041
+ "textarea",
1042
+ {
1043
+ id: `${id}-source`,
1044
+ "data-merge-source": "",
1045
+ value: merge.sourceText,
1046
+ rows: 5,
1047
+ spellCheck: false,
1048
+ onChange: (event) => merge.setSourceText(event.target.value),
1049
+ className: "px-2 py-1 font-mono text-xs rounded-md border border-violet-400/40 bg-transparent"
1050
+ }
1051
+ )
1052
+ ] }),
1053
+ /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-2", children: [
1054
+ /* @__PURE__ */ jsx4(
1055
+ "input",
1056
+ {
1057
+ type: "file",
1058
+ accept: ".csv,.tsv,.txt,.json,text/csv,application/json",
1059
+ "aria-label": "Load data source file",
1060
+ "data-merge-file": "",
1061
+ onChange: (event) => {
1062
+ const file = event.target.files?.[0];
1063
+ if (file) void merge.loadFile(file);
1064
+ }
1065
+ }
1066
+ ),
1067
+ /* @__PURE__ */ jsx4("span", { "data-merge-summary": "", className: "opacity-70", children: `${source.records.length} record${source.records.length === 1 ? "" : "s"} \xB7 ${source.fields.length} field${source.fields.length === 1 ? "" : "s"}` })
1068
+ ] }),
1069
+ source.warnings.length > 0 && merge.sourceText.trim() !== "" && /* @__PURE__ */ jsx4("ul", { "data-merge-warnings": "", className: "text-amber-600 text-xs", children: source.warnings.map((warning, i) => /* @__PURE__ */ jsx4("li", { children: warning.message }, i)) }),
1070
+ merge.variables.length > 0 && /* @__PURE__ */ jsxs3("fieldset", { className: "flex flex-col gap-1", children: [
1071
+ /* @__PURE__ */ jsx4("legend", { className: "font-medium", children: "Field mapping" }),
1072
+ merge.variables.map((variable) => /* @__PURE__ */ jsxs3(
1073
+ "label",
1074
+ {
1075
+ className: "flex items-center gap-2",
1076
+ "data-merge-mapping": variable,
1077
+ children: [
1078
+ /* @__PURE__ */ jsx4("span", { className: "min-w-24 font-mono", children: `{${variable}}` }),
1079
+ /* @__PURE__ */ jsxs3(
1080
+ "select",
1081
+ {
1082
+ value: merge.mapping[variable] ?? "",
1083
+ "aria-label": `Field for ${variable}`,
1084
+ onChange: (event) => merge.setFieldMapping(variable, event.target.value || null),
1085
+ className: "px-1 py-0.5 rounded-md border border-violet-400/40 bg-transparent",
1086
+ children: [
1087
+ /* @__PURE__ */ jsx4("option", { value: "", children: "(none)" }),
1088
+ source.fields.map((field) => /* @__PURE__ */ jsx4("option", { value: field, children: field }, field))
1089
+ ]
1090
+ }
1091
+ )
1092
+ ]
1093
+ },
1094
+ variable
1095
+ ))
1096
+ ] }),
1097
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
1098
+ /* @__PURE__ */ jsx4(
1099
+ "button",
1100
+ {
1101
+ type: "button",
1102
+ "data-merge-prev": "",
1103
+ onClick: merge.prev,
1104
+ disabled: merge.index <= 0,
1105
+ children: "Prev"
1106
+ }
1107
+ ),
1108
+ /* @__PURE__ */ jsx4("span", { "data-merge-position": "", children: merge.count === 0 ? "No records" : `Record ${merge.index + 1} of ${merge.count}` }),
1109
+ /* @__PURE__ */ jsx4(
1110
+ "button",
1111
+ {
1112
+ type: "button",
1113
+ "data-merge-next": "",
1114
+ onClick: merge.next,
1115
+ disabled: merge.index >= merge.count - 1,
1116
+ children: "Next"
1117
+ }
1118
+ )
1119
+ ] }),
1120
+ /* @__PURE__ */ jsx4("div", { "data-merge-output": "", children: /* @__PURE__ */ jsx4(
1121
+ TemplateRenderer,
1122
+ {
1123
+ content,
1124
+ templates,
1125
+ bindings,
1126
+ templateColorMap,
1127
+ resolve: !!templates && templates.length > 0,
1128
+ values: merge.values
1129
+ }
1130
+ ) }),
1131
+ /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-2", children: [
1132
+ /* @__PURE__ */ jsx4(
1133
+ "button",
1134
+ {
1135
+ type: "button",
1136
+ "data-merge-all": "",
1137
+ disabled: merge.count === 0,
1138
+ onClick: () => {
1139
+ const merged = merge.merge();
1140
+ setCopied(false);
1141
+ onMerge?.(merged);
1142
+ },
1143
+ children: "Merge all"
1144
+ }
1145
+ ),
1146
+ result && /* @__PURE__ */ jsxs3(Fragment, { children: [
1147
+ /* @__PURE__ */ jsx4("span", { "data-merge-result": "", children: `${result.outputs.length} merged${result.skipped.length ? `, ${result.skipped.length} skipped` : ""}${result.truncated ? " (stopped early)" : ""}` }),
1148
+ /* @__PURE__ */ jsx4("button", { type: "button", "data-merge-copy": "", onClick: copy, children: copied ? "Copied" : "Copy" }),
1149
+ /* @__PURE__ */ jsx4(
1150
+ "button",
1151
+ {
1152
+ type: "button",
1153
+ "data-merge-download": "text",
1154
+ onClick: () => downloadText(merge.serialize("text"), `${downloadName}.txt`, "text/plain"),
1155
+ children: "Download .txt"
1156
+ }
1157
+ ),
1158
+ /* @__PURE__ */ jsx4(
1159
+ "button",
1160
+ {
1161
+ type: "button",
1162
+ "data-merge-download": "json",
1163
+ onClick: () => downloadText(merge.serialize("json"), `${downloadName}.json`, "application/json"),
1164
+ children: "Download .json"
1165
+ }
1166
+ )
1167
+ ] })
1168
+ ] }),
1169
+ result && /* @__PURE__ */ jsx4(
1170
+ "textarea",
1171
+ {
1172
+ readOnly: true,
1173
+ "aria-label": "Merged output",
1174
+ "data-merge-text": "",
1175
+ rows: 6,
1176
+ value: merge.serialize("text"),
1177
+ className: "px-2 py-1 font-mono text-xs rounded-md border border-violet-400/40 bg-transparent"
1178
+ }
1179
+ )
1180
+ ] });
1181
+ };
1182
+
859
1183
  // src/hooks/useOutline.ts
860
- import { useMemo as useMemo6, useSyncExternalStore as useSyncExternalStore4 } from "react";
1184
+ import { useMemo as useMemo7, useSyncExternalStore as useSyncExternalStore4 } from "react";
861
1185
  import { createOutlineStore } from "@pixerate/editor";
862
1186
  function useOutline(editor) {
863
- const store = useMemo6(() => createOutlineStore(editor), [editor]);
1187
+ const store = useMemo7(() => createOutlineStore(editor), [editor]);
864
1188
  return useSyncExternalStore4(store.subscribe, store.getSnapshot, store.getSnapshot);
865
1189
  }
866
1190
 
867
1191
  // src/components/OutlinePanel.tsx
868
1192
  import { navigateToHeading } from "@pixerate/editor";
869
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
1193
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
870
1194
  var OutlinePanel = ({
871
1195
  editor,
872
1196
  title = "Outline",
@@ -880,11 +1204,11 @@ var OutlinePanel = ({
880
1204
  }) => {
881
1205
  const { headings, activeId } = useOutline(editor);
882
1206
  const visible = headings.filter((h) => h.level <= maxLevel);
883
- return /* @__PURE__ */ jsxs3("nav", { className, "aria-label": "Document outline", "data-outline-panel": "", children: [
884
- title != null && /* @__PURE__ */ jsx4("div", { className: "pixerate-outline-title", children: title }),
885
- visible.length === 0 ? /* @__PURE__ */ jsx4("div", { className: "pixerate-outline-empty", children: emptyState }) : /* @__PURE__ */ jsx4("ul", { role: "list", style: { listStyle: "none", margin: 0, padding: 0 }, children: visible.map((heading) => {
1207
+ return /* @__PURE__ */ jsxs4("nav", { className, "aria-label": "Document outline", "data-outline-panel": "", children: [
1208
+ title != null && /* @__PURE__ */ jsx5("div", { className: "pixerate-outline-title", children: title }),
1209
+ visible.length === 0 ? /* @__PURE__ */ jsx5("div", { className: "pixerate-outline-empty", children: emptyState }) : /* @__PURE__ */ jsx5("ul", { role: "list", style: { listStyle: "none", margin: 0, padding: 0 }, children: visible.map((heading) => {
886
1210
  const active = heading.id === activeId;
887
- return /* @__PURE__ */ jsx4("li", { "data-level": heading.level, children: /* @__PURE__ */ jsx4(
1211
+ return /* @__PURE__ */ jsx5("li", { "data-level": heading.level, children: /* @__PURE__ */ jsx5(
888
1212
  "button",
889
1213
  {
890
1214
  type: "button",
@@ -905,6 +1229,168 @@ var OutlinePanel = ({
905
1229
  ] });
906
1230
  };
907
1231
 
1232
+ // src/hooks/useParagraphStyles.ts
1233
+ import { useMemo as useMemo8, useSyncExternalStore as useSyncExternalStore5 } from "react";
1234
+ import {
1235
+ createParagraphStylesStore,
1236
+ getStyleRegistry
1237
+ } from "@pixerate/editor";
1238
+ function useParagraphStyles(editor) {
1239
+ const store = useMemo8(() => createParagraphStylesStore(editor), [editor]);
1240
+ const snapshot = useSyncExternalStore5(store.subscribe, store.getSnapshot, store.getSnapshot);
1241
+ return useMemo8(() => {
1242
+ const registry = getStyleRegistry(editor);
1243
+ const live = () => editor && !editor.isDestroyed ? editor : null;
1244
+ return {
1245
+ ...snapshot,
1246
+ registry,
1247
+ currentStyle: snapshot.currentStyleId && registry?.get(snapshot.currentStyleId) || null,
1248
+ apply: (id) => !!live()?.chain().focus().setParagraphStyle(id).run(),
1249
+ clear: () => !!live()?.chain().focus().clearParagraphStyle().run(),
1250
+ updateFromSelection: (id) => !!live()?.chain().focus().updateParagraphStyleFromSelection(id).run(),
1251
+ createFromSelection: (name, options) => {
1252
+ const e = live();
1253
+ if (!e || !registry) return null;
1254
+ const id = options?.id ?? registry.newId(name.trim());
1255
+ const ok = e.chain().focus().createParagraphStyleFromSelection(name, { ...options, id }).run();
1256
+ return ok ? id : null;
1257
+ },
1258
+ applyCharacterStyle: (id) => !!live()?.chain().focus().setCharacterStyle(id).run()
1259
+ };
1260
+ }, [snapshot, editor]);
1261
+ }
1262
+
1263
+ // src/components/StylesGallery.tsx
1264
+ import { useState as useState8 } from "react";
1265
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1266
+ var camel = (property) => property.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1267
+ var keepSelection = (event) => event.preventDefault();
1268
+ var StylesGallery = ({
1269
+ editor,
1270
+ variant = "dropdown",
1271
+ showAllStyles = false,
1272
+ showActions = true,
1273
+ preview = true,
1274
+ className = "relative inline-flex flex-wrap items-center gap-1 text-sm",
1275
+ itemClassName = "rounded px-2 py-1 text-left text-xs text-foreground hover:bg-muted",
1276
+ activeItemClassName = "bg-primary/10 text-primary",
1277
+ onChange
1278
+ }) => {
1279
+ const styles = useParagraphStyles(editor);
1280
+ const [open, setOpen] = useState8(false);
1281
+ const [creating, setCreating] = useState8(false);
1282
+ const [name, setName] = useState8("");
1283
+ const list = showAllStyles ? styles.styles : styles.gallery;
1284
+ const current = styles.currentStyle;
1285
+ const expanded = variant === "gallery" || open;
1286
+ const close = () => {
1287
+ if (variant === "dropdown") setOpen(false);
1288
+ setCreating(false);
1289
+ setName("");
1290
+ };
1291
+ const previewStyle = (id) => {
1292
+ if (!preview || !styles.registry) return void 0;
1293
+ return Object.fromEntries(
1294
+ Object.entries(styles.registry.previewDeclarations(id)).map(([k, v]) => [camel(k), v])
1295
+ );
1296
+ };
1297
+ const apply = (id) => {
1298
+ if (styles.apply(id)) onChange?.({ type: "apply", id });
1299
+ close();
1300
+ };
1301
+ const create = (event) => {
1302
+ event.preventDefault();
1303
+ const id = name.trim() ? styles.createFromSelection(name) : null;
1304
+ if (id) onChange?.({ type: "create", id });
1305
+ close();
1306
+ };
1307
+ return /* @__PURE__ */ jsxs5("div", { className, role: "group", "aria-label": "Paragraph styles", "data-styles-gallery": variant, children: [
1308
+ variant === "dropdown" && /* @__PURE__ */ jsxs5(
1309
+ "button",
1310
+ {
1311
+ type: "button",
1312
+ className: itemClassName,
1313
+ "aria-haspopup": "listbox",
1314
+ "aria-expanded": open,
1315
+ disabled: !editor || !styles.registry,
1316
+ onMouseDown: keepSelection,
1317
+ onClick: () => setOpen((value) => !value),
1318
+ children: [
1319
+ current?.name ?? "Styles",
1320
+ " \u25BE"
1321
+ ]
1322
+ }
1323
+ ),
1324
+ expanded && /* @__PURE__ */ jsxs5(
1325
+ "div",
1326
+ {
1327
+ className: variant === "dropdown" ? "absolute left-0 top-full z-20 mt-1 flex min-w-[12rem] flex-col gap-0.5 rounded-md border border-border bg-background p-1 shadow-md" : "flex flex-wrap items-center gap-1",
1328
+ children: [
1329
+ /* @__PURE__ */ jsx6("div", { role: "listbox", "aria-label": "Styles", className: variant === "dropdown" ? "flex flex-col gap-0.5" : "flex flex-wrap gap-1", children: list.map((style) => {
1330
+ const active = style.id === styles.currentStyleId;
1331
+ return /* @__PURE__ */ jsx6(
1332
+ "button",
1333
+ {
1334
+ type: "button",
1335
+ role: "option",
1336
+ "aria-selected": active,
1337
+ "data-style-id": style.id,
1338
+ className: active ? `${itemClassName} ${activeItemClassName}` : itemClassName,
1339
+ style: previewStyle(style.id),
1340
+ onMouseDown: keepSelection,
1341
+ onClick: () => apply(style.id),
1342
+ children: style.name
1343
+ },
1344
+ style.id
1345
+ );
1346
+ }) }),
1347
+ showActions && /* @__PURE__ */ jsxs5("div", { className: variant === "dropdown" ? "mt-1 flex flex-col gap-0.5 border-t border-border pt-1" : "flex items-center gap-1", children: [
1348
+ /* @__PURE__ */ jsx6(
1349
+ "button",
1350
+ {
1351
+ type: "button",
1352
+ className: itemClassName,
1353
+ disabled: !current,
1354
+ onMouseDown: keepSelection,
1355
+ onClick: () => {
1356
+ if (current && styles.updateFromSelection(current.id)) {
1357
+ onChange?.({ type: "update", id: current.id });
1358
+ }
1359
+ close();
1360
+ },
1361
+ children: current ? `Update ${current.name} to match selection` : "Update to match selection"
1362
+ }
1363
+ ),
1364
+ creating ? /* @__PURE__ */ jsxs5("form", { className: "flex items-center gap-1", onSubmit: create, children: [
1365
+ /* @__PURE__ */ jsx6(
1366
+ "input",
1367
+ {
1368
+ "aria-label": "New style name",
1369
+ className: "w-32 rounded border border-border bg-background px-1 py-0.5 text-xs",
1370
+ value: name,
1371
+ autoFocus: true,
1372
+ onChange: (event) => setName(event.target.value)
1373
+ }
1374
+ ),
1375
+ /* @__PURE__ */ jsx6("button", { type: "submit", className: itemClassName, disabled: !name.trim(), children: "Create" })
1376
+ ] }) : /* @__PURE__ */ jsx6(
1377
+ "button",
1378
+ {
1379
+ type: "button",
1380
+ className: itemClassName,
1381
+ disabled: !editor || !styles.registry,
1382
+ onMouseDown: keepSelection,
1383
+ onClick: () => setCreating(true),
1384
+ children: "New style from selection"
1385
+ }
1386
+ )
1387
+ ] })
1388
+ ]
1389
+ }
1390
+ )
1391
+ ] });
1392
+ };
1393
+
908
1394
  // src/index.ts
909
1395
  import {
910
1396
  DirtyTracker as DirtyTracker2,
@@ -917,10 +1403,10 @@ import {
917
1403
  } from "@pixerate/editor";
918
1404
 
919
1405
  // src/slots.tsx
920
- import { createContext, useContext, useMemo as useMemo7 } from "react";
921
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1406
+ import { createContext, useContext, useMemo as useMemo9 } from "react";
1407
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
922
1408
  var defaultSlots = {
923
- Button: (props) => /* @__PURE__ */ jsx5(
1409
+ Button: (props) => /* @__PURE__ */ jsx7(
924
1410
  "button",
925
1411
  {
926
1412
  type: "button",
@@ -930,26 +1416,26 @@ var defaultSlots = {
930
1416
  children: props.children
931
1417
  }
932
1418
  ),
933
- Tooltip: (props) => /* @__PURE__ */ jsx5("div", { className: `pixerate-tooltip-container ${props.className || ""}`, title: String(props.content), children: props.children }),
934
- NodeHeader: (props) => /* @__PURE__ */ jsxs4("div", { className: `pixerate-node-header flex items-center justify-between ${props.className || ""}`, children: [
935
- /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2", children: [
1419
+ Tooltip: (props) => /* @__PURE__ */ jsx7("div", { className: `pixerate-tooltip-container ${props.className || ""}`, title: String(props.content), children: props.children }),
1420
+ NodeHeader: (props) => /* @__PURE__ */ jsxs6("div", { className: `pixerate-node-header flex items-center justify-between ${props.className || ""}`, children: [
1421
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
936
1422
  props.icon,
937
- /* @__PURE__ */ jsx5("span", { className: "font-semibold", children: props.title })
1423
+ /* @__PURE__ */ jsx7("span", { className: "font-semibold", children: props.title })
938
1424
  ] }),
939
- props.onClose && /* @__PURE__ */ jsx5("button", { type: "button", onClick: props.onClose, className: "opacity-70 hover:opacity-100", children: "\xD7" })
1425
+ props.onClose && /* @__PURE__ */ jsx7("button", { type: "button", onClick: props.onClose, className: "opacity-70 hover:opacity-100", children: "\xD7" })
940
1426
  ] })
941
1427
  };
942
1428
  var EditorSlotsContext = createContext(defaultSlots);
943
1429
  function EditorSlotsProvider({ slots, children }) {
944
- const mergedSlots = useMemo7(() => ({ ...defaultSlots, ...slots }), [slots]);
945
- return /* @__PURE__ */ jsx5(EditorSlotsContext.Provider, { value: mergedSlots, children });
1430
+ const mergedSlots = useMemo9(() => ({ ...defaultSlots, ...slots }), [slots]);
1431
+ return /* @__PURE__ */ jsx7(EditorSlotsContext.Provider, { value: mergedSlots, children });
946
1432
  }
947
1433
  function useEditorSlots() {
948
1434
  return useContext(EditorSlotsContext);
949
1435
  }
950
1436
 
951
1437
  // src/review/useReview.ts
952
- import { useCallback as useCallback5, useMemo as useMemo8, useSyncExternalStore as useSyncExternalStore5 } from "react";
1438
+ import { useCallback as useCallback6, useMemo as useMemo10, useSyncExternalStore as useSyncExternalStore6 } from "react";
953
1439
  import {
954
1440
  getCommentStore,
955
1441
  getTrackChangesStore
@@ -971,23 +1457,23 @@ function isCommentStore(value) {
971
1457
  }
972
1458
  function useComments(source) {
973
1459
  const store = isCommentStore(source) ? source : getCommentStore(source);
974
- const subscribe = useCallback5(
1460
+ const subscribe = useCallback6(
975
1461
  (onChange) => store ? store.subscribe(() => onChange()) : noopUnsubscribe,
976
1462
  [store]
977
1463
  );
978
1464
  const getSnapshot = () => store?.getSnapshot() ?? EMPTY_COMMENTS;
979
- const snapshot = useSyncExternalStore5(subscribe, getSnapshot, getSnapshot);
980
- return useMemo8(() => ({ ...snapshot, store }), [snapshot, store]);
1465
+ const snapshot = useSyncExternalStore6(subscribe, getSnapshot, getSnapshot);
1466
+ return useMemo10(() => ({ ...snapshot, store }), [snapshot, store]);
981
1467
  }
982
1468
  function useTrackChanges(editor) {
983
1469
  const store = getTrackChangesStore(editor ?? null);
984
- const subscribe = useCallback5(
1470
+ const subscribe = useCallback6(
985
1471
  (onChange) => store ? store.subscribe(() => onChange()) : noopUnsubscribe,
986
1472
  [store]
987
1473
  );
988
1474
  const getSnapshot = () => store?.getSnapshot() ?? EMPTY_TRACK_CHANGES;
989
- const snapshot = useSyncExternalStore5(subscribe, getSnapshot, getSnapshot);
990
- return useMemo8(() => {
1475
+ const snapshot = useSyncExternalStore6(subscribe, getSnapshot, getSnapshot);
1476
+ return useMemo10(() => {
991
1477
  const run = (fn) => () => {
992
1478
  if (editor && !editor.isDestroyed) fn(editor);
993
1479
  };
@@ -1001,18 +1487,21 @@ function useTrackChanges(editor) {
1001
1487
  acceptAll: run((e) => e.commands.acceptAllChanges()),
1002
1488
  rejectAll: run((e) => e.commands.rejectAllChanges()),
1003
1489
  next: run((e) => e.chain().focus().selectNextChange().run()),
1004
- previous: run((e) => e.chain().focus().selectPreviousChange().run())
1490
+ previous: run((e) => e.chain().focus().selectPreviousChange().run()),
1491
+ select: (id) => run((e) => e.chain().focus().selectChange(id).run())(),
1492
+ acceptMove: (id) => run((e) => e.commands.acceptMove(id))(),
1493
+ rejectMove: (id) => run((e) => e.commands.rejectMove(id))()
1005
1494
  };
1006
1495
  }, [snapshot, editor]);
1007
1496
  }
1008
1497
 
1009
1498
  // src/review/CommentsPanel.tsx
1010
- import { useState as useState6 } from "react";
1499
+ import { useState as useState9 } from "react";
1011
1500
  import {
1012
1501
  formatReviewTime,
1013
1502
  getCommentAuthor
1014
1503
  } from "@pixerate/editor";
1015
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
1504
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1016
1505
  var buttonClass = "rounded px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-50";
1017
1506
  var primaryButtonClass = "rounded px-2 py-1 text-xs bg-primary/10 text-primary hover:bg-primary/20 disabled:opacity-50";
1018
1507
  var textareaClass = "w-full resize-y rounded-md border border-input bg-background p-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary";
@@ -1026,18 +1515,18 @@ var CommentsPanel = ({
1026
1515
  emptyMessage = "No comments yet. Select text and add a comment."
1027
1516
  }) => {
1028
1517
  const { threads, activeThreadId, store } = useComments(storeProp ?? editor);
1029
- const [showResolved, setShowResolved] = useState6(showResolvedInitial);
1518
+ const [showResolved, setShowResolved] = useState9(showResolvedInitial);
1030
1519
  const author = authorProp ?? getCommentAuthor(editor) ?? { id: "anonymous", name: "Anonymous" };
1031
1520
  const visible = threads.filter((t) => showResolved || !t.resolved);
1032
1521
  const resolvedCount = threads.filter((t) => t.resolved).length;
1033
- return /* @__PURE__ */ jsxs5("aside", { className, "aria-label": "Comments", children: [
1034
- /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between gap-2", children: [
1035
- /* @__PURE__ */ jsxs5("span", { className: "text-xs font-medium text-muted-foreground", children: [
1522
+ return /* @__PURE__ */ jsxs7("aside", { className, "aria-label": "Comments", children: [
1523
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between gap-2", children: [
1524
+ /* @__PURE__ */ jsxs7("span", { className: "text-xs font-medium text-muted-foreground", children: [
1036
1525
  threads.length - resolvedCount,
1037
1526
  " open",
1038
1527
  resolvedCount > 0 ? ` \xB7 ${resolvedCount} resolved` : ""
1039
1528
  ] }),
1040
- resolvedCount > 0 && /* @__PURE__ */ jsx6(
1529
+ resolvedCount > 0 && /* @__PURE__ */ jsx8(
1041
1530
  "button",
1042
1531
  {
1043
1532
  type: "button",
@@ -1048,7 +1537,7 @@ var CommentsPanel = ({
1048
1537
  }
1049
1538
  )
1050
1539
  ] }),
1051
- visible.length === 0 ? /* @__PURE__ */ jsx6("p", { className: "text-xs text-muted-foreground", children: emptyMessage }) : /* @__PURE__ */ jsx6("ul", { className: "flex flex-col gap-2", role: "list", children: visible.map((thread) => /* @__PURE__ */ jsx6(
1540
+ visible.length === 0 ? /* @__PURE__ */ jsx8("p", { className: "text-xs text-muted-foreground", children: emptyMessage }) : /* @__PURE__ */ jsx8("ul", { className: "flex flex-col gap-2", role: "list", children: visible.map((thread) => /* @__PURE__ */ jsx8(
1052
1541
  CommentThreadCard,
1053
1542
  {
1054
1543
  thread,
@@ -1070,7 +1559,7 @@ var CommentThreadCard = ({
1070
1559
  author,
1071
1560
  formatTime
1072
1561
  }) => {
1073
- const [draft, setDraft] = useState6("");
1562
+ const [draft, setDraft] = useState9("");
1074
1563
  const isDraft = thread.body === "";
1075
1564
  const quote = thread.anchor?.quote ?? thread.lastQuote ?? "";
1076
1565
  const live = editor && !editor.isDestroyed ? editor : null;
@@ -1096,7 +1585,7 @@ var CommentThreadCard = ({
1096
1585
  setDraft("");
1097
1586
  };
1098
1587
  const stop = (e) => e.stopPropagation();
1099
- return /* @__PURE__ */ jsxs5(
1588
+ return /* @__PURE__ */ jsxs7(
1100
1589
  "li",
1101
1590
  {
1102
1591
  className: `flex flex-col gap-2 rounded-md border bg-background p-3 cursor-pointer ${active ? "border-primary ring-1 ring-primary" : "border-border"} ${thread.resolved ? "opacity-70" : ""}`,
@@ -1112,24 +1601,24 @@ var CommentThreadCard = ({
1112
1601
  }
1113
1602
  },
1114
1603
  children: [
1115
- /* @__PURE__ */ jsxs5("div", { className: "flex items-baseline justify-between gap-2", children: [
1116
- /* @__PURE__ */ jsx6("span", { className: "font-medium", children: thread.author.name }),
1117
- /* @__PURE__ */ jsx6("span", { className: "text-xs text-muted-foreground", children: formatTime(thread.createdAt) })
1604
+ /* @__PURE__ */ jsxs7("div", { className: "flex items-baseline justify-between gap-2", children: [
1605
+ /* @__PURE__ */ jsx8("span", { className: "font-medium", children: thread.author.name }),
1606
+ /* @__PURE__ */ jsx8("span", { className: "text-xs text-muted-foreground", children: formatTime(thread.createdAt) })
1118
1607
  ] }),
1119
- quote && /* @__PURE__ */ jsx6("blockquote", { className: "border-l-2 border-border pl-2 text-xs text-muted-foreground line-clamp-2", children: quote }),
1120
- thread.orphaned && /* @__PURE__ */ jsx6("span", { className: "text-xs text-destructive", children: "The commented text was deleted." }),
1121
- thread.resolved && /* @__PURE__ */ jsx6("span", { className: "text-xs text-muted-foreground", children: "Resolved" }),
1122
- !isDraft && /* @__PURE__ */ jsx6("p", { className: "whitespace-pre-wrap break-words", children: thread.body }),
1123
- thread.replies.length > 0 && /* @__PURE__ */ jsx6("ul", { className: "flex flex-col gap-2 border-l border-border pl-3", role: "list", children: thread.replies.map((reply) => /* @__PURE__ */ jsxs5("li", { className: "flex flex-col gap-0.5", "data-reply-id": reply.id, children: [
1124
- /* @__PURE__ */ jsxs5("span", { className: "text-xs", children: [
1125
- /* @__PURE__ */ jsx6("span", { className: "font-medium", children: reply.author.name }),
1608
+ quote && /* @__PURE__ */ jsx8("blockquote", { className: "border-l-2 border-border pl-2 text-xs text-muted-foreground line-clamp-2", children: quote }),
1609
+ thread.orphaned && /* @__PURE__ */ jsx8("span", { className: "text-xs text-destructive", children: "The commented text was deleted." }),
1610
+ thread.resolved && /* @__PURE__ */ jsx8("span", { className: "text-xs text-muted-foreground", children: "Resolved" }),
1611
+ !isDraft && /* @__PURE__ */ jsx8("p", { className: "whitespace-pre-wrap break-words", children: thread.body }),
1612
+ thread.replies.length > 0 && /* @__PURE__ */ jsx8("ul", { className: "flex flex-col gap-2 border-l border-border pl-3", role: "list", children: thread.replies.map((reply) => /* @__PURE__ */ jsxs7("li", { className: "flex flex-col gap-0.5", "data-reply-id": reply.id, children: [
1613
+ /* @__PURE__ */ jsxs7("span", { className: "text-xs", children: [
1614
+ /* @__PURE__ */ jsx8("span", { className: "font-medium", children: reply.author.name }),
1126
1615
  " ",
1127
- /* @__PURE__ */ jsx6("span", { className: "text-muted-foreground", children: formatTime(reply.createdAt) })
1616
+ /* @__PURE__ */ jsx8("span", { className: "text-muted-foreground", children: formatTime(reply.createdAt) })
1128
1617
  ] }),
1129
- /* @__PURE__ */ jsx6("p", { className: "whitespace-pre-wrap break-words", children: reply.body })
1618
+ /* @__PURE__ */ jsx8("p", { className: "whitespace-pre-wrap break-words", children: reply.body })
1130
1619
  ] }, reply.id)) }),
1131
- (active || isDraft) && !thread.resolved && /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-1", onClick: stop, children: [
1132
- /* @__PURE__ */ jsx6(
1620
+ (active || isDraft) && !thread.resolved && /* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-1", onClick: stop, children: [
1621
+ /* @__PURE__ */ jsx8(
1133
1622
  "textarea",
1134
1623
  {
1135
1624
  className: textareaClass,
@@ -1147,9 +1636,9 @@ var CommentThreadCard = ({
1147
1636
  }
1148
1637
  }
1149
1638
  ),
1150
- /* @__PURE__ */ jsxs5("div", { className: "flex justify-end gap-1", children: [
1151
- isDraft && /* @__PURE__ */ jsx6("button", { type: "button", className: buttonClass, onClick: remove, children: "Cancel" }),
1152
- /* @__PURE__ */ jsx6(
1639
+ /* @__PURE__ */ jsxs7("div", { className: "flex justify-end gap-1", children: [
1640
+ isDraft && /* @__PURE__ */ jsx8("button", { type: "button", className: buttonClass, onClick: remove, children: "Cancel" }),
1641
+ /* @__PURE__ */ jsx8(
1153
1642
  "button",
1154
1643
  {
1155
1644
  type: "button",
@@ -1161,9 +1650,9 @@ var CommentThreadCard = ({
1161
1650
  )
1162
1651
  ] })
1163
1652
  ] }),
1164
- !isDraft && /* @__PURE__ */ jsxs5("div", { className: "flex justify-end gap-1", onClick: stop, children: [
1165
- /* @__PURE__ */ jsx6("button", { type: "button", className: buttonClass, onClick: () => setResolved(!thread.resolved), children: thread.resolved ? "Reopen" : "Resolve" }),
1166
- /* @__PURE__ */ jsx6(
1653
+ !isDraft && /* @__PURE__ */ jsxs7("div", { className: "flex justify-end gap-1", onClick: stop, children: [
1654
+ /* @__PURE__ */ jsx8("button", { type: "button", className: buttonClass, onClick: () => setResolved(!thread.resolved), children: thread.resolved ? "Reopen" : "Resolve" }),
1655
+ /* @__PURE__ */ jsx8(
1167
1656
  "button",
1168
1657
  {
1169
1658
  type: "button",
@@ -1179,7 +1668,7 @@ var CommentThreadCard = ({
1179
1668
  };
1180
1669
 
1181
1670
  // src/review/ReviewToolbar.tsx
1182
- import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1671
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1183
1672
  var defaultDescriptionClass = "max-w-[16rem] truncate px-1 text-xs text-muted-foreground";
1184
1673
  var buttonClass2 = "rounded px-2 py-1 text-xs text-foreground hover:bg-muted disabled:opacity-40 disabled:hover:bg-transparent";
1185
1674
  var ReviewToolbar = ({
@@ -1192,8 +1681,8 @@ var ReviewToolbar = ({
1192
1681
  const count = review.changes.length;
1193
1682
  const current = review.currentChange;
1194
1683
  const index = current ? review.changes.findIndex((c) => c.id === current.id) + 1 : 0;
1195
- return /* @__PURE__ */ jsxs6("div", { className, role: "toolbar", "aria-label": "Review", children: [
1196
- /* @__PURE__ */ jsx7(
1684
+ return /* @__PURE__ */ jsxs8("div", { className, role: "toolbar", "aria-label": "Review", children: [
1685
+ /* @__PURE__ */ jsx9(
1197
1686
  "button",
1198
1687
  {
1199
1688
  type: "button",
@@ -1204,8 +1693,8 @@ var ReviewToolbar = ({
1204
1693
  children: review.enabled ? "Tracking on" : "Track changes"
1205
1694
  }
1206
1695
  ),
1207
- /* @__PURE__ */ jsx7("span", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": "true" }),
1208
- /* @__PURE__ */ jsx7(
1696
+ /* @__PURE__ */ jsx9("span", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": "true" }),
1697
+ /* @__PURE__ */ jsx9(
1209
1698
  "button",
1210
1699
  {
1211
1700
  type: "button",
@@ -1216,8 +1705,8 @@ var ReviewToolbar = ({
1216
1705
  children: "\u2039"
1217
1706
  }
1218
1707
  ),
1219
- /* @__PURE__ */ jsx7("span", { className: "min-w-[4rem] text-center text-xs text-muted-foreground", "aria-live": "polite", children: count === 0 ? "No changes" : current ? `${index} of ${count}` : `${count} changes` }),
1220
- /* @__PURE__ */ jsx7(
1708
+ /* @__PURE__ */ jsx9("span", { className: "min-w-[4rem] text-center text-xs text-muted-foreground", "aria-live": "polite", children: count === 0 ? "No changes" : current ? `${index} of ${count}` : `${count} changes` }),
1709
+ /* @__PURE__ */ jsx9(
1221
1710
  "button",
1222
1711
  {
1223
1712
  type: "button",
@@ -1228,7 +1717,7 @@ var ReviewToolbar = ({
1228
1717
  children: "\u203A"
1229
1718
  }
1230
1719
  ),
1231
- showDescription && current && /* @__PURE__ */ jsxs6(
1720
+ showDescription && current && /* @__PURE__ */ jsxs8(
1232
1721
  "span",
1233
1722
  {
1234
1723
  className: descriptionClassName,
@@ -1240,8 +1729,8 @@ var ReviewToolbar = ({
1240
1729
  ]
1241
1730
  }
1242
1731
  ),
1243
- /* @__PURE__ */ jsx7("span", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": "true" }),
1244
- /* @__PURE__ */ jsx7(
1732
+ /* @__PURE__ */ jsx9("span", { className: "mx-1 h-4 w-px bg-border", "aria-hidden": "true" }),
1733
+ /* @__PURE__ */ jsx9(
1245
1734
  "button",
1246
1735
  {
1247
1736
  type: "button",
@@ -1251,7 +1740,7 @@ var ReviewToolbar = ({
1251
1740
  children: "Accept"
1252
1741
  }
1253
1742
  ),
1254
- /* @__PURE__ */ jsx7(
1743
+ /* @__PURE__ */ jsx9(
1255
1744
  "button",
1256
1745
  {
1257
1746
  type: "button",
@@ -1261,18 +1750,18 @@ var ReviewToolbar = ({
1261
1750
  children: "Reject"
1262
1751
  }
1263
1752
  ),
1264
- /* @__PURE__ */ jsx7("button", { type: "button", className: buttonClass2, disabled: count === 0, onClick: review.acceptAll, children: "Accept all" }),
1265
- /* @__PURE__ */ jsx7("button", { type: "button", className: buttonClass2, disabled: count === 0, onClick: review.rejectAll, children: "Reject all" })
1753
+ /* @__PURE__ */ jsx9("button", { type: "button", className: buttonClass2, disabled: count === 0, onClick: review.acceptAll, children: "Accept all" }),
1754
+ /* @__PURE__ */ jsx9("button", { type: "button", className: buttonClass2, disabled: count === 0, onClick: review.rejectAll, children: "Reject all" })
1266
1755
  ] });
1267
1756
  };
1268
1757
 
1269
1758
  // src/review/CommentButton.tsx
1270
- import { jsx as jsx8 } from "react/jsx-runtime";
1759
+ import { jsx as jsx10 } from "react/jsx-runtime";
1271
1760
  var CommentButton = ({
1272
1761
  editor,
1273
1762
  className = "rounded px-2 py-1 text-xs text-popover-foreground hover:bg-muted",
1274
1763
  children = "Comment"
1275
- }) => /* @__PURE__ */ jsx8(
1764
+ }) => /* @__PURE__ */ jsx10(
1276
1765
  "button",
1277
1766
  {
1278
1767
  type: "button",
@@ -1282,43 +1771,345 @@ var CommentButton = ({
1282
1771
  children
1283
1772
  }
1284
1773
  );
1774
+
1775
+ // src/review/useDocumentCompare.ts
1776
+ import { useCallback as useCallback7, useMemo as useMemo11, useRef as useRef8, useState as useState10 } from "react";
1777
+ import {
1778
+ applyDocumentComparison
1779
+ } from "@pixerate/editor";
1780
+ function useDocumentCompare(editor, options) {
1781
+ const [summary, setSummary] = useState10(null);
1782
+ const defaults = useRef8(options);
1783
+ defaults.current = options;
1784
+ const compare = useCallback7(
1785
+ (original, revised, overrides) => {
1786
+ if (!editor || editor.isDestroyed) return null;
1787
+ const result = applyDocumentComparison(editor, original, revised, {
1788
+ ...defaults.current,
1789
+ ...overrides
1790
+ });
1791
+ setSummary(result.summary);
1792
+ return result.summary;
1793
+ },
1794
+ [editor]
1795
+ );
1796
+ const clear = useCallback7(() => setSummary(null), []);
1797
+ return useMemo11(() => ({ summary, compare, clear }), [summary, compare, clear]);
1798
+ }
1799
+
1800
+ // src/review/TrackedChangesList.tsx
1801
+ import { trackedChangeLabel } from "@pixerate/editor";
1802
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
1803
+ var buttonClass3 = "rounded px-1.5 py-0.5 text-xs text-foreground hover:bg-muted disabled:opacity-40";
1804
+ var TrackedChangesList = ({
1805
+ editor,
1806
+ className = "flex flex-col gap-1 text-sm",
1807
+ itemClassName = "flex items-center gap-2 rounded px-2 py-1 hover:bg-muted/50",
1808
+ activeItemClassName = "bg-primary/10",
1809
+ emptyState = "No tracked changes."
1810
+ }) => {
1811
+ const review = useTrackChanges(editor);
1812
+ if (review.changes.length === 0) {
1813
+ return /* @__PURE__ */ jsx11("div", { className, "data-tracked-changes-list": "", children: /* @__PURE__ */ jsx11("div", { className: "pixerate-changes-empty text-xs text-muted-foreground", children: emptyState }) });
1814
+ }
1815
+ return /* @__PURE__ */ jsx11("ul", { className, role: "list", "aria-label": "Tracked changes", "data-tracked-changes-list": "", children: review.changes.map((change) => {
1816
+ const active = change.id === review.currentChangeId;
1817
+ return /* @__PURE__ */ jsxs9(
1818
+ "li",
1819
+ {
1820
+ className: active ? `${itemClassName} ${activeItemClassName}` : itemClassName,
1821
+ "data-change-type": change.type,
1822
+ "data-change-id": change.id,
1823
+ children: [
1824
+ /* @__PURE__ */ jsxs9(
1825
+ "button",
1826
+ {
1827
+ type: "button",
1828
+ className: "min-w-0 flex-1 truncate text-left text-xs",
1829
+ title: `${change.author.name}: ${change.description}`,
1830
+ onClick: () => review.select(change.id),
1831
+ children: [
1832
+ /* @__PURE__ */ jsx11("span", { className: "font-medium", children: trackedChangeLabel(change) }),
1833
+ change.author.name ? ` \xB7 ${change.author.name}` : "",
1834
+ /* @__PURE__ */ jsx11("span", { className: "block truncate text-muted-foreground", children: change.description })
1835
+ ]
1836
+ }
1837
+ ),
1838
+ /* @__PURE__ */ jsx11("button", { type: "button", className: buttonClass3, onClick: () => review.accept(change.id), children: "Accept" }),
1839
+ /* @__PURE__ */ jsx11("button", { type: "button", className: buttonClass3, onClick: () => review.reject(change.id), children: "Reject" })
1840
+ ]
1841
+ },
1842
+ `${change.type}:${change.id}`
1843
+ );
1844
+ }) });
1845
+ };
1846
+
1847
+ // src/find-replace/useFindReplace.ts
1848
+ import { useCallback as useCallback8, useMemo as useMemo12, useSyncExternalStore as useSyncExternalStore7 } from "react";
1849
+ import {
1850
+ EMPTY_FIND_REPLACE_SNAPSHOT,
1851
+ getFindReplaceStore
1852
+ } from "@pixerate/editor";
1853
+ var noopUnsubscribe2 = () => {
1854
+ };
1855
+ function useFindReplace(editor) {
1856
+ const store = getFindReplaceStore(editor ?? null);
1857
+ const subscribe = useCallback8(
1858
+ (onChange) => store ? store.subscribe(() => onChange()) : noopUnsubscribe2,
1859
+ [store]
1860
+ );
1861
+ const getSnapshot = () => store?.getSnapshot() ?? EMPTY_FIND_REPLACE_SNAPSHOT;
1862
+ const snapshot = useSyncExternalStore7(subscribe, getSnapshot, getSnapshot);
1863
+ return useMemo12(() => {
1864
+ const run = (fn) => {
1865
+ if (editor && !editor.isDestroyed && store) fn(editor);
1866
+ };
1867
+ return {
1868
+ ...snapshot,
1869
+ available: !!store,
1870
+ matchCount: snapshot.matches.length,
1871
+ show: (mode = "find") => run((e) => e.commands.openFindReplace({ mode })),
1872
+ close: () => run((e) => e.commands.closeFindReplace()),
1873
+ setQuery: (query) => run((e) => e.commands.setSearch({ query })),
1874
+ setReplacement: (replacement) => run((e) => e.commands.setSearch({ replacement })),
1875
+ setOptions: (options) => run((e) => e.commands.setSearch({ options })),
1876
+ toggleOption: (option) => run((e) => e.commands.setSearch({ options: { [option]: !snapshot.options[option] } })),
1877
+ next: () => run((e) => e.commands.findNext()),
1878
+ previous: () => run((e) => e.commands.findPrevious()),
1879
+ replace: () => run((e) => e.commands.replaceCurrent()),
1880
+ replaceAll: () => run((e) => e.commands.replaceAll()),
1881
+ clear: () => run((e) => e.commands.clearSearch())
1882
+ };
1883
+ }, [snapshot, editor, store]);
1884
+ }
1885
+
1886
+ // src/find-replace/FindReplaceBar.tsx
1887
+ import { useEffect as useEffect8, useRef as useRef9, useState as useState11 } from "react";
1888
+ import { FIND_OPTION_TOGGLES, formatFindStatus } from "@pixerate/editor";
1889
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
1890
+ var inputClass = "min-w-0 flex-1 rounded border border-input bg-background px-2 py-1 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring";
1891
+ var buttonClass4 = "rounded px-2 py-1 text-xs text-foreground hover:bg-muted disabled:opacity-40 disabled:hover:bg-transparent";
1892
+ var toggleClass = (on) => `${buttonClass4} font-mono ${on ? "bg-primary/10 text-primary" : "text-muted-foreground"}`;
1893
+ var isMod = (e) => e.metaKey || e.ctrlKey;
1894
+ var FindReplaceBar = ({
1895
+ editor,
1896
+ className = "flex flex-col gap-1 rounded-md border border-border bg-background p-1 text-sm shadow-sm",
1897
+ alwaysOpen = false,
1898
+ showMoreOptions = true,
1899
+ onClose
1900
+ }) => {
1901
+ const find = useFindReplace(editor);
1902
+ const findInput = useRef9(null);
1903
+ const [moreOpen, setMoreOpen] = useState11(false);
1904
+ const visible = find.available && (alwaysOpen || find.open);
1905
+ useEffect8(() => {
1906
+ if (!visible || find.focusRequest === 0) return;
1907
+ findInput.current?.focus();
1908
+ findInput.current?.select();
1909
+ }, [visible, find.focusRequest]);
1910
+ if (!visible) return null;
1911
+ const showReplace = find.mode === "replace";
1912
+ const status = formatFindStatus(find);
1913
+ const hasMatches = find.matchCount > 0;
1914
+ const close = () => {
1915
+ find.close();
1916
+ editor?.commands.focus();
1917
+ onClose?.();
1918
+ };
1919
+ const onBarKeyDown = (e) => {
1920
+ if (e.key === "Escape") {
1921
+ e.preventDefault();
1922
+ close();
1923
+ } else if (isMod(e) && (e.key === "f" || e.key === "F")) {
1924
+ e.preventDefault();
1925
+ find.show("find");
1926
+ } else if (isMod(e) && (e.key === "h" || e.key === "H")) {
1927
+ e.preventDefault();
1928
+ find.show("replace");
1929
+ }
1930
+ };
1931
+ const onFindKeyDown = (e) => {
1932
+ if (e.key !== "Enter") return;
1933
+ e.preventDefault();
1934
+ if (e.shiftKey) find.previous();
1935
+ else find.next();
1936
+ };
1937
+ const onReplaceKeyDown = (e) => {
1938
+ if (e.key !== "Enter") return;
1939
+ e.preventDefault();
1940
+ if (isMod(e)) find.replaceAll();
1941
+ else find.replace();
1942
+ };
1943
+ return /* @__PURE__ */ jsxs10("div", { className, role: "search", "aria-label": "Find and replace", onKeyDown: onBarKeyDown, children: [
1944
+ /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-1", children: [
1945
+ /* @__PURE__ */ jsx12(
1946
+ "button",
1947
+ {
1948
+ type: "button",
1949
+ className: buttonClass4,
1950
+ "aria-label": "Toggle replace",
1951
+ "aria-expanded": showReplace,
1952
+ onClick: () => find.show(showReplace ? "find" : "replace"),
1953
+ children: showReplace ? "\u25BE" : "\u25B8"
1954
+ }
1955
+ ),
1956
+ /* @__PURE__ */ jsx12(
1957
+ "input",
1958
+ {
1959
+ ref: findInput,
1960
+ type: "text",
1961
+ className: inputClass,
1962
+ placeholder: "Find",
1963
+ "aria-label": "Find",
1964
+ "aria-invalid": !!find.error,
1965
+ value: find.query,
1966
+ onChange: (e) => find.setQuery(e.target.value),
1967
+ onKeyDown: onFindKeyDown
1968
+ }
1969
+ ),
1970
+ FIND_OPTION_TOGGLES.filter((t) => !t.advanced).map((t) => /* @__PURE__ */ jsx12(
1971
+ "button",
1972
+ {
1973
+ type: "button",
1974
+ className: toggleClass(!!find.options[t.key]),
1975
+ "aria-label": t.label,
1976
+ title: t.label,
1977
+ "aria-pressed": !!find.options[t.key],
1978
+ onClick: () => find.toggleOption(t.key),
1979
+ children: t.short
1980
+ },
1981
+ t.key
1982
+ )),
1983
+ /* @__PURE__ */ jsx12(
1984
+ "span",
1985
+ {
1986
+ className: `min-w-[5rem] px-1 text-center text-xs ${find.error ? "text-destructive" : "text-muted-foreground"}`,
1987
+ "aria-live": "polite",
1988
+ role: find.error ? "alert" : "status",
1989
+ "data-testid": "find-status",
1990
+ children: status
1991
+ }
1992
+ ),
1993
+ /* @__PURE__ */ jsx12("button", { type: "button", className: buttonClass4, "aria-label": "Previous match", disabled: !hasMatches, onClick: find.previous, children: "\u2191" }),
1994
+ /* @__PURE__ */ jsx12("button", { type: "button", className: buttonClass4, "aria-label": "Next match", disabled: !hasMatches, onClick: find.next, children: "\u2193" }),
1995
+ showMoreOptions && /* @__PURE__ */ jsx12(
1996
+ "button",
1997
+ {
1998
+ type: "button",
1999
+ className: buttonClass4,
2000
+ "aria-label": "More options",
2001
+ "aria-expanded": moreOpen,
2002
+ onClick: () => setMoreOpen((v) => !v),
2003
+ children: "\u2026"
2004
+ }
2005
+ ),
2006
+ /* @__PURE__ */ jsx12("button", { type: "button", className: buttonClass4, "aria-label": "Close find", onClick: close, children: "\xD7" })
2007
+ ] }),
2008
+ showReplace && /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-1 pl-7", children: [
2009
+ /* @__PURE__ */ jsx12(
2010
+ "input",
2011
+ {
2012
+ type: "text",
2013
+ className: inputClass,
2014
+ placeholder: "Replace",
2015
+ "aria-label": "Replace",
2016
+ value: find.replacement,
2017
+ onChange: (e) => find.setReplacement(e.target.value),
2018
+ onKeyDown: onReplaceKeyDown
2019
+ }
2020
+ ),
2021
+ /* @__PURE__ */ jsx12(
2022
+ "button",
2023
+ {
2024
+ type: "button",
2025
+ className: buttonClass4,
2026
+ disabled: !hasMatches || find.currentIndex < 0,
2027
+ onClick: find.replace,
2028
+ children: "Replace"
2029
+ }
2030
+ ),
2031
+ /* @__PURE__ */ jsx12("button", { type: "button", className: buttonClass4, disabled: !hasMatches, onClick: find.replaceAll, children: "Replace all" }),
2032
+ find.lastReplaceCount !== null && /* @__PURE__ */ jsx12("span", { className: "px-1 text-xs text-muted-foreground", "aria-live": "polite", children: find.lastReplaceCount === 1 ? "1 replaced" : `${find.lastReplaceCount} replaced` })
2033
+ ] }),
2034
+ showMoreOptions && moreOpen && /* @__PURE__ */ jsx12("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 pl-7 text-xs text-muted-foreground", children: FIND_OPTION_TOGGLES.filter((t) => t.advanced).map((t) => /* @__PURE__ */ jsxs10("label", { className: "flex items-center gap-1", children: [
2035
+ /* @__PURE__ */ jsx12(
2036
+ "input",
2037
+ {
2038
+ type: "checkbox",
2039
+ checked: !!find.options[t.key],
2040
+ onChange: () => find.toggleOption(t.key)
2041
+ }
2042
+ ),
2043
+ t.label
2044
+ ] }, t.key)) })
2045
+ ] });
2046
+ };
1285
2047
  export {
2048
+ AdjustmentLayersPanel,
2049
+ AssetMentionChips,
1286
2050
  BubbleMenu,
1287
2051
  CommandPalette,
1288
2052
  CommentButton,
1289
2053
  CommentsPanel,
2054
+ ControlSlider,
2055
+ CurvesEditor,
1290
2056
  DirtyTracker2 as DirtyTracker,
1291
2057
  EditorContent,
1292
2058
  EditorSlotsProvider,
2059
+ FindReplaceBar,
1293
2060
  FormulaHints,
2061
+ GenerationHistory,
2062
+ GenerationSettings,
2063
+ Histogram,
1294
2064
  HistoryManager2 as HistoryManager,
1295
2065
  ImageEditor,
2066
+ JobProgress,
2067
+ MergePreview,
2068
+ NameManager,
1296
2069
  OutlinePanel,
2070
+ RecoveryPrompt,
2071
+ RefineEdgePanel,
1297
2072
  ReviewToolbar,
2073
+ SelectionOptionsBar,
2074
+ SheetTabs,
2075
+ SpreadsheetConditionalFormatPanel,
1298
2076
  SpreadsheetContextMenu,
2077
+ SpreadsheetDataValidationPanel,
1299
2078
  SpreadsheetEditor,
1300
2079
  SpreadsheetGroupRow,
2080
+ SpreadsheetValidationDialog,
2081
+ StylesGallery,
1301
2082
  TemplateRenderer,
2083
+ TrackedChangesList,
1302
2084
  VariableForm,
1303
2085
  bindDirtyToHistory2 as bindDirtyToHistory,
1304
2086
  createDirtyTracker,
1305
2087
  createHistoryManager2 as createHistoryManager,
1306
2088
  defaultIsEqual,
1307
2089
  useAgentToolProvider,
2090
+ useAutosave,
1308
2091
  useCommandPalette,
1309
2092
  useCommandShortcuts,
1310
2093
  useComments,
1311
2094
  useDismissGuard,
2095
+ useDocumentCompare,
1312
2096
  useEditor2 as useEditor,
1313
2097
  useEditorSlots,
1314
2098
  useFileImport,
1315
2099
  useFillHandle,
2100
+ useFindReplace,
2101
+ useGenerationPlan,
2102
+ useGenerationRecords,
1316
2103
  useHistory,
1317
2104
  useHistoryDirty,
1318
2105
  useHistoryShortcuts,
1319
2106
  useImageEditor,
2107
+ useImageHistogram,
2108
+ useJobProgress,
2109
+ useMergePreview,
1320
2110
  useNavigationGuard,
1321
2111
  useOutline,
2112
+ useParagraphStyles,
1322
2113
  usePromptEditor,
1323
2114
  useRegisterCommands,
1324
2115
  useSpreadsheetClipboard,