@10x-media/undo-redo 0.1.0-beta.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 (60) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +54 -0
  4. package/dist/client/HistoryDebugOverlay.d.ts +30 -0
  5. package/dist/client/HistoryDebugOverlay.js +201 -0
  6. package/dist/client/HistoryDebugOverlay.js.map +1 -0
  7. package/dist/client/UndoRedoControls.d.ts +18 -0
  8. package/dist/client/UndoRedoControls.js +155 -0
  9. package/dist/client/UndoRedoControls.js.map +1 -0
  10. package/dist/client/formatShortcut.js +113 -0
  11. package/dist/client/formatShortcut.js.map +1 -0
  12. package/dist/client/historyDebugOverlay.css +206 -0
  13. package/dist/client/undoRedoControls.css +58 -0
  14. package/dist/client/useUndoRedo.d.ts +70 -0
  15. package/dist/client/useUndoRedo.js +259 -0
  16. package/dist/client/useUndoRedo.js.map +1 -0
  17. package/dist/exports/client.d.ts +4 -0
  18. package/dist/exports/client.js +5 -0
  19. package/dist/exports/i18n.d.ts +3 -0
  20. package/dist/exports/i18n.js +3 -0
  21. package/dist/exports/types.d.ts +5 -0
  22. package/dist/exports/types.js +1 -0
  23. package/dist/history/historyCore.d.ts +85 -0
  24. package/dist/history/historyCore.js +318 -0
  25. package/dist/history/historyCore.js.map +1 -0
  26. package/dist/history/pathPatterns.d.ts +6 -0
  27. package/dist/history/pathPatterns.js +45 -0
  28. package/dist/history/pathPatterns.js.map +1 -0
  29. package/dist/history/volatileValues.js +51 -0
  30. package/dist/history/volatileValues.js.map +1 -0
  31. package/dist/index.d.ts +20 -0
  32. package/dist/index.js +28 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/plugin/options.d.ts +109 -0
  35. package/dist/plugin/options.js +52 -0
  36. package/dist/plugin/options.js.map +1 -0
  37. package/dist/plugin/registerTranslations.js +19 -0
  38. package/dist/plugin/registerTranslations.js.map +1 -0
  39. package/dist/plugin/withUndoRedo.d.ts +34 -0
  40. package/dist/plugin/withUndoRedo.js +72 -0
  41. package/dist/plugin/withUndoRedo.js.map +1 -0
  42. package/dist/schema/fieldConfig.d.ts +36 -0
  43. package/dist/schema/fieldConfig.js +28 -0
  44. package/dist/schema/fieldConfig.js.map +1 -0
  45. package/dist/schema/fieldSchema.d.ts +63 -0
  46. package/dist/schema/fieldSchema.js +122 -0
  47. package/dist/schema/fieldSchema.js.map +1 -0
  48. package/dist/translations/de.js +19 -0
  49. package/dist/translations/de.js.map +1 -0
  50. package/dist/translations/en.js +23 -0
  51. package/dist/translations/en.js.map +1 -0
  52. package/dist/translations/index.d.ts +15 -0
  53. package/dist/translations/index.js +30 -0
  54. package/dist/translations/index.js.map +1 -0
  55. package/dist/translations/keys.d.ts +22 -0
  56. package/dist/translations/keys.js +22 -0
  57. package/dist/translations/keys.js.map +1 -0
  58. package/dist/translations/useTranslation.js +12 -0
  59. package/dist/translations/useTranslation.js.map +1 -0
  60. package/package.json +109 -0
@@ -0,0 +1,113 @@
1
+ //#region src/client/formatShortcut.ts
2
+ /**
3
+ * Render a `react-hotkeys-hook` chord the way the host platform writes it, so
4
+ * tooltips describe the shortcut that is actually bound. A hardcoded "Ctrl+Z" is
5
+ * wrong on macOS, and wrong everywhere once a host overrides `shortcuts`.
6
+ *
7
+ * Parsing mirrors react-hotkeys-hook's own `parseHotkeys`: the same split keys,
8
+ * the same alias table, the same reserved modifier list. Whatever the library
9
+ * binds as a modifier is drawn as one, and whatever it binds as a key is drawn
10
+ * as a key, so the label can never disagree with the binding.
11
+ */
12
+ /** Chord parts read as modifiers rather than keys. */
13
+ const MODIFIERS = [
14
+ "shift",
15
+ "alt",
16
+ "meta",
17
+ "mod",
18
+ "ctrl",
19
+ "control"
20
+ ];
21
+ /** Modifier order both Apple and Microsoft use in their own shortcut labels. */
22
+ const MODIFIER_ORDER = [
23
+ "ctrl",
24
+ "alt",
25
+ "shift",
26
+ "meta"
27
+ ];
28
+ /**
29
+ * Key aliases react-hotkeys-hook accepts, normalized to its canonical names.
30
+ * Its alias table also covers physical codes (`ControlLeft`, `ShiftRight`),
31
+ * which exist for matching `event.code` and are not a form anyone writes in a
32
+ * config, so they are left out here.
33
+ */
34
+ const KEY_ALIASES = {
35
+ down: "arrowdown",
36
+ esc: "escape",
37
+ left: "arrowleft",
38
+ return: "enter",
39
+ right: "arrowright",
40
+ up: "arrowup"
41
+ };
42
+ const MAC_MODIFIER_LABELS = {
43
+ alt: "⌥",
44
+ ctrl: "⌃",
45
+ meta: "⌘",
46
+ shift: "⇧"
47
+ };
48
+ const MODIFIER_LABELS = {
49
+ alt: "Alt",
50
+ ctrl: "Ctrl",
51
+ meta: "Meta",
52
+ shift: "Shift"
53
+ };
54
+ /** Keys whose name reads worse than the glyph every platform prints on the cap. */
55
+ const KEY_LABELS = {
56
+ arrowdown: "↓",
57
+ arrowleft: "←",
58
+ arrowright: "→",
59
+ arrowup: "↑",
60
+ escape: "Esc"
61
+ };
62
+ const MAC_KEY_LABELS = {
63
+ backspace: "⌫",
64
+ delete: "⌦",
65
+ enter: "↩",
66
+ escape: "⎋",
67
+ tab: "⇥"
68
+ };
69
+ const SPLIT_KEY = "+";
70
+ const SEQUENCE_SPLIT_KEY = ">";
71
+ /**
72
+ * Lowercased before the alias lookup, not after, because the library lowercases
73
+ * the whole chord before it maps anything. Looking `Left` up in a lowercase
74
+ * alias table misses, and `mod+Left` would then be drawn as `Left` while the
75
+ * binding listens for `arrowleft`.
76
+ */
77
+ const normalizePart = (part) => {
78
+ const key = part.trim().toLowerCase();
79
+ return (KEY_ALIASES[key] ?? key).replace(/key|digit|numpad/, "");
80
+ };
81
+ const isModifier = (part) => MODIFIERS.includes(part);
82
+ const keyLabel = (key, isMac) => {
83
+ const named = (isMac ? MAC_KEY_LABELS[key] : void 0) ?? KEY_LABELS[key];
84
+ if (named) return named;
85
+ return key.charAt(0).toUpperCase() + key.slice(1);
86
+ };
87
+ const formatChord = (chord, isMac) => {
88
+ const parts = chord.split(SPLIT_KEY).map(normalizePart).filter(Boolean);
89
+ const present = new Set(parts.filter(isModifier));
90
+ if (present.has("mod")) present.add(isMac ? "meta" : "ctrl");
91
+ if (present.has("control")) present.add("ctrl");
92
+ const modifiers = MODIFIER_ORDER.filter((modifier) => present.has(modifier)).map((modifier) => (isMac ? MAC_MODIFIER_LABELS : MODIFIER_LABELS)[modifier] ?? modifier);
93
+ const pressed = parts.filter((part) => !isModifier(part)).map((key) => keyLabel(key, isMac));
94
+ return [...modifiers, ...pressed].join(isMac ? "" : "+");
95
+ };
96
+ /**
97
+ * Human-readable form of one chord, e.g. `mod+shift+z` as `⇧⌘Z` on macOS and
98
+ * `Ctrl+Shift+Z` elsewhere. Sequences (`g>h`) render as their steps in order.
99
+ */
100
+ const formatShortcut = (chord, isMac) => chord.split(SEQUENCE_SPLIT_KEY).map((step) => formatChord(step, isMac)).filter(Boolean).join(" ");
101
+ /**
102
+ * The platform test react-hotkeys-hook itself applies to resolve `mod`, copied
103
+ * rather than imported because the library does not export it. Diverging here
104
+ * would label a chord with a modifier other than the one it listens for.
105
+ */
106
+ const isMacPlatform = () => {
107
+ if (typeof navigator === "undefined") return false;
108
+ return /mac/i.test(navigator.userAgent) && !/iphone|ipad|ipod/i.test(navigator.userAgent);
109
+ };
110
+ //#endregion
111
+ export { formatShortcut, isMacPlatform };
112
+
113
+ //# sourceMappingURL=formatShortcut.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatShortcut.js","names":[],"sources":["../../src/client/formatShortcut.ts"],"sourcesContent":["/**\n * Render a `react-hotkeys-hook` chord the way the host platform writes it, so\n * tooltips describe the shortcut that is actually bound. A hardcoded \"Ctrl+Z\" is\n * wrong on macOS, and wrong everywhere once a host overrides `shortcuts`.\n *\n * Parsing mirrors react-hotkeys-hook's own `parseHotkeys`: the same split keys,\n * the same alias table, the same reserved modifier list. Whatever the library\n * binds as a modifier is drawn as one, and whatever it binds as a key is drawn\n * as a key, so the label can never disagree with the binding.\n */\n\n/** Chord parts read as modifiers rather than keys. */\nconst MODIFIERS = ['shift', 'alt', 'meta', 'mod', 'ctrl', 'control'] as const\n\ntype Modifier = (typeof MODIFIERS)[number]\n\n/** Modifier order both Apple and Microsoft use in their own shortcut labels. */\nconst MODIFIER_ORDER = ['ctrl', 'alt', 'shift', 'meta'] as const\n\n/**\n * Key aliases react-hotkeys-hook accepts, normalized to its canonical names.\n * Its alias table also covers physical codes (`ControlLeft`, `ShiftRight`),\n * which exist for matching `event.code` and are not a form anyone writes in a\n * config, so they are left out here.\n */\nconst KEY_ALIASES: Record<string, string> = {\n\tdown: 'arrowdown',\n\tesc: 'escape',\n\tleft: 'arrowleft',\n\treturn: 'enter',\n\tright: 'arrowright',\n\tup: 'arrowup',\n}\n\nconst MAC_MODIFIER_LABELS: Record<string, string> = {\n\talt: '⌥',\n\tctrl: '⌃',\n\tmeta: '⌘',\n\tshift: '⇧',\n}\n\nconst MODIFIER_LABELS: Record<string, string> = {\n\talt: 'Alt',\n\tctrl: 'Ctrl',\n\tmeta: 'Meta',\n\tshift: 'Shift',\n}\n\n/** Keys whose name reads worse than the glyph every platform prints on the cap. */\nconst KEY_LABELS: Record<string, string> = {\n\tarrowdown: '↓',\n\tarrowleft: '←',\n\tarrowright: '→',\n\tarrowup: '↑',\n\tescape: 'Esc',\n}\n\nconst MAC_KEY_LABELS: Record<string, string> = {\n\tbackspace: '⌫',\n\tdelete: '⌦',\n\tenter: '↩',\n\tescape: '⎋',\n\ttab: '⇥',\n}\n\nconst SPLIT_KEY = '+'\nconst SEQUENCE_SPLIT_KEY = '>'\n\n/**\n * Lowercased before the alias lookup, not after, because the library lowercases\n * the whole chord before it maps anything. Looking `Left` up in a lowercase\n * alias table misses, and `mod+Left` would then be drawn as `Left` while the\n * binding listens for `arrowleft`.\n */\nconst normalizePart = (part: string): string => {\n\tconst key = part.trim().toLowerCase()\n\treturn (KEY_ALIASES[key] ?? key).replace(/key|digit|numpad/, '')\n}\n\nconst isModifier = (part: string): part is Modifier => MODIFIERS.includes(part as Modifier)\n\nconst keyLabel = (key: string, isMac: boolean): string => {\n\tconst named = (isMac ? MAC_KEY_LABELS[key] : undefined) ?? KEY_LABELS[key]\n\tif (named) return named\n\treturn key.charAt(0).toUpperCase() + key.slice(1)\n}\n\nconst formatChord = (chord: string, isMac: boolean): string => {\n\tconst parts = chord.split(SPLIT_KEY).map(normalizePart).filter(Boolean)\n\tconst present = new Set(parts.filter(isModifier))\n\t// `mod` is the library's platform alias and `control` its long spelling;\n\t// both collapse onto a canonical modifier before anything is drawn.\n\tif (present.has('mod')) present.add(isMac ? 'meta' : 'ctrl')\n\tif (present.has('control')) present.add('ctrl')\n\n\tconst modifiers = MODIFIER_ORDER.filter((modifier) => present.has(modifier)).map(\n\t\t(modifier) => (isMac ? MAC_MODIFIER_LABELS : MODIFIER_LABELS)[modifier] ?? modifier\n\t)\n\tconst pressed = parts.filter((part) => !isModifier(part)).map((key) => keyLabel(key, isMac))\n\n\t// macOS prints modifier glyphs run together, everything else separates them.\n\treturn [...modifiers, ...pressed].join(isMac ? '' : '+')\n}\n\n/**\n * Human-readable form of one chord, e.g. `mod+shift+z` as `⇧⌘Z` on macOS and\n * `Ctrl+Shift+Z` elsewhere. Sequences (`g>h`) render as their steps in order.\n */\nexport const formatShortcut = (chord: string, isMac: boolean): string =>\n\tchord\n\t\t.split(SEQUENCE_SPLIT_KEY)\n\t\t.map((step) => formatChord(step, isMac))\n\t\t.filter(Boolean)\n\t\t.join(' ')\n\n/**\n * The platform test react-hotkeys-hook itself applies to resolve `mod`, copied\n * rather than imported because the library does not export it. Diverging here\n * would label a chord with a modifier other than the one it listens for.\n */\nexport const isMacPlatform = (): boolean => {\n\tif (typeof navigator === 'undefined') return false\n\treturn /mac/i.test(navigator.userAgent) && !/iphone|ipad|ipod/i.test(navigator.userAgent)\n}\n"],"mappings":";;;;;;;;;;;;AAYA,MAAM,YAAY;CAAC;CAAS;CAAO;CAAQ;CAAO;CAAQ;AAAS;;AAKnE,MAAM,iBAAiB;CAAC;CAAQ;CAAO;CAAS;AAAM;;;;;;;AAQtD,MAAM,cAAsC;CAC3C,MAAM;CACN,KAAK;CACL,MAAM;CACN,QAAQ;CACR,OAAO;CACP,IAAI;AACL;AAEA,MAAM,sBAA8C;CACnD,KAAK;CACL,MAAM;CACN,MAAM;CACN,OAAO;AACR;AAEA,MAAM,kBAA0C;CAC/C,KAAK;CACL,MAAM;CACN,MAAM;CACN,OAAO;AACR;;AAGA,MAAM,aAAqC;CAC1C,WAAW;CACX,WAAW;CACX,YAAY;CACZ,SAAS;CACT,QAAQ;AACT;AAEA,MAAM,iBAAyC;CAC9C,WAAW;CACX,QAAQ;CACR,OAAO;CACP,QAAQ;CACR,KAAK;AACN;AAEA,MAAM,YAAY;AAClB,MAAM,qBAAqB;;;;;;;AAQ3B,MAAM,iBAAiB,SAAyB;CAC/C,MAAM,MAAM,KAAK,KAAK,EAAE,YAAY;CACpC,QAAQ,YAAY,QAAQ,KAAK,QAAQ,oBAAoB,EAAE;AAChE;AAEA,MAAM,cAAc,SAAmC,UAAU,SAAS,IAAgB;AAE1F,MAAM,YAAY,KAAa,UAA2B;CACzD,MAAM,SAAS,QAAQ,eAAe,OAAO,KAAA,MAAc,WAAW;CACtE,IAAI,OAAO,OAAO;CAClB,OAAO,IAAI,OAAO,CAAC,EAAE,YAAY,IAAI,IAAI,MAAM,CAAC;AACjD;AAEA,MAAM,eAAe,OAAe,UAA2B;CAC9D,MAAM,QAAQ,MAAM,MAAM,SAAS,EAAE,IAAI,aAAa,EAAE,OAAO,OAAO;CACtE,MAAM,UAAU,IAAI,IAAI,MAAM,OAAO,UAAU,CAAC;CAGhD,IAAI,QAAQ,IAAI,KAAK,GAAG,QAAQ,IAAI,QAAQ,SAAS,MAAM;CAC3D,IAAI,QAAQ,IAAI,SAAS,GAAG,QAAQ,IAAI,MAAM;CAE9C,MAAM,YAAY,eAAe,QAAQ,aAAa,QAAQ,IAAI,QAAQ,CAAC,EAAE,KAC3E,cAAc,QAAQ,sBAAsB,iBAAiB,aAAa,QAC5E;CACA,MAAM,UAAU,MAAM,QAAQ,SAAS,CAAC,WAAW,IAAI,CAAC,EAAE,KAAK,QAAQ,SAAS,KAAK,KAAK,CAAC;CAG3F,OAAO,CAAC,GAAG,WAAW,GAAG,OAAO,EAAE,KAAK,QAAQ,KAAK,GAAG;AACxD;;;;;AAMA,MAAa,kBAAkB,OAAe,UAC7C,MACE,MAAM,kBAAkB,EACxB,KAAK,SAAS,YAAY,MAAM,KAAK,CAAC,EACtC,OAAO,OAAO,EACd,KAAK,GAAG;;;;;;AAOX,MAAa,sBAA+B;CAC3C,IAAI,OAAO,cAAc,aAAa,OAAO;CAC7C,OAAO,OAAO,KAAK,UAAU,SAAS,KAAK,CAAC,oBAAoB,KAAK,UAAU,SAAS;AACzF"}
@@ -0,0 +1,206 @@
1
+ /* Developer overlay, not editor chrome: it deliberately floats above the admin
2
+ UI instead of docking into a Payload slot, so it stays usable while the form
3
+ below is scrolled and edited. Colors come from Payload's theme tokens so it
4
+ follows light/dark without a second stylesheet. */
5
+ .undo-redo-debug {
6
+ position: fixed;
7
+ right: 1rem;
8
+ bottom: 1rem;
9
+ z-index: 100;
10
+ display: flex;
11
+ flex-direction: column;
12
+ width: min(560px, calc(100vw - 2rem));
13
+ max-height: min(60vh, 640px);
14
+ overflow: hidden;
15
+ border: 1px solid var(--theme-elevation-150);
16
+ border-radius: var(--style-radius-m);
17
+ background: var(--theme-elevation-0);
18
+ color: var(--theme-elevation-800);
19
+ font-family: var(--font-mono);
20
+ font-size: 12px;
21
+ line-height: 1.5;
22
+ box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.35);
23
+ resize: both;
24
+ }
25
+
26
+ .undo-redo-debug__header {
27
+ display: flex;
28
+ flex-shrink: 0;
29
+ align-items: center;
30
+ gap: 8px;
31
+ padding: 8px 10px;
32
+ border-bottom: 1px solid var(--theme-elevation-150);
33
+ background: var(--theme-elevation-50);
34
+ }
35
+
36
+ .undo-redo-debug__title {
37
+ font-family: var(--font-body);
38
+ }
39
+
40
+ .undo-redo-debug__meta {
41
+ margin-right: auto;
42
+ color: var(--theme-elevation-500);
43
+ }
44
+
45
+ .undo-redo-debug__action {
46
+ padding: 2px 8px;
47
+ border: 1px solid var(--theme-elevation-150);
48
+ border-radius: var(--style-radius-s);
49
+ background: var(--theme-elevation-0);
50
+ color: inherit;
51
+ font-family: inherit;
52
+ font-size: inherit;
53
+ cursor: pointer;
54
+ }
55
+
56
+ .undo-redo-debug__action:hover {
57
+ border-color: var(--theme-elevation-250);
58
+ background: var(--theme-elevation-100);
59
+ }
60
+
61
+ .undo-redo-debug__body {
62
+ flex: 1;
63
+ overflow-y: auto;
64
+ padding: 4px 0;
65
+ }
66
+
67
+ .undo-redo-debug__empty {
68
+ margin: 0;
69
+ padding: 12px 10px;
70
+ color: var(--theme-elevation-500);
71
+ }
72
+
73
+ .undo-redo-debug__entry {
74
+ border-bottom: 1px solid var(--theme-elevation-100);
75
+ }
76
+
77
+ /* The entry matching the form state on screen right now. */
78
+ .undo-redo-debug__entry--current {
79
+ background: var(--theme-elevation-50);
80
+ box-shadow: inset 3px 0 0 var(--theme-success-500);
81
+ }
82
+
83
+ /* Edits made but still inside the capture debounce, so not an entry yet. */
84
+ .undo-redo-debug__entry--pending {
85
+ box-shadow: inset 3px 0 0 var(--theme-warning-500);
86
+ }
87
+
88
+ .undo-redo-debug__entryHead {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 6px;
92
+ padding-right: 8px;
93
+ }
94
+
95
+ .undo-redo-debug__toggle {
96
+ display: flex;
97
+ flex: 1;
98
+ align-items: center;
99
+ gap: 6px;
100
+ min-width: 0;
101
+ padding: 6px 8px;
102
+ border: 0;
103
+ background: none;
104
+ color: inherit;
105
+ font-family: inherit;
106
+ font-size: inherit;
107
+ text-align: left;
108
+ cursor: pointer;
109
+ }
110
+
111
+ .undo-redo-debug__caret {
112
+ width: 10px;
113
+ color: var(--theme-elevation-400);
114
+ }
115
+
116
+ .undo-redo-debug__index {
117
+ flex-shrink: 0;
118
+ color: var(--theme-elevation-500);
119
+ }
120
+
121
+ .undo-redo-debug__summary {
122
+ overflow: hidden;
123
+ white-space: nowrap;
124
+ text-overflow: ellipsis;
125
+ }
126
+
127
+ .undo-redo-debug__count {
128
+ flex-shrink: 0;
129
+ margin-left: auto;
130
+ padding: 0 6px;
131
+ border-radius: var(--style-radius-s);
132
+ background: var(--theme-elevation-100);
133
+ color: var(--theme-elevation-600);
134
+ }
135
+
136
+ .undo-redo-debug__jump {
137
+ flex-shrink: 0;
138
+ padding: 2px 6px;
139
+ border: 1px solid var(--theme-elevation-150);
140
+ border-radius: var(--style-radius-s);
141
+ background: var(--theme-elevation-0);
142
+ color: inherit;
143
+ font-family: inherit;
144
+ font-size: 11px;
145
+ cursor: pointer;
146
+ }
147
+
148
+ .undo-redo-debug__jump:disabled {
149
+ opacity: 0.4;
150
+ cursor: default;
151
+ }
152
+
153
+ .undo-redo-debug__diff {
154
+ width: 100%;
155
+ margin: 0 0 6px;
156
+ padding: 0 8px 0 24px;
157
+ border-collapse: collapse;
158
+ table-layout: fixed;
159
+ }
160
+
161
+ .undo-redo-debug__diff td {
162
+ padding: 1px 4px;
163
+ overflow: hidden;
164
+ vertical-align: top;
165
+ white-space: nowrap;
166
+ text-overflow: ellipsis;
167
+ }
168
+
169
+ .undo-redo-debug__path {
170
+ width: 34%;
171
+ color: var(--theme-elevation-700);
172
+ }
173
+
174
+ .undo-redo-debug__presence {
175
+ margin-left: 4px;
176
+ padding: 0 4px;
177
+ border-radius: var(--style-radius-s);
178
+ background: var(--theme-elevation-100);
179
+ color: var(--theme-elevation-500);
180
+ font-size: 10px;
181
+ }
182
+
183
+ .undo-redo-debug__from {
184
+ width: 29%;
185
+ color: var(--theme-error-500);
186
+ }
187
+
188
+ .undo-redo-debug__arrow {
189
+ width: 16px;
190
+ color: var(--theme-elevation-400);
191
+ }
192
+
193
+ .undo-redo-debug__to {
194
+ width: 29%;
195
+ color: var(--theme-success-500);
196
+ }
197
+
198
+ /* The entry that matches the persisted document. */
199
+ .undo-redo-debug__saved {
200
+ flex-shrink: 0;
201
+ padding: 0 4px;
202
+ border-radius: var(--style-radius-s);
203
+ background: var(--theme-success-100);
204
+ color: var(--theme-success-600);
205
+ font-size: 10px;
206
+ }
@@ -0,0 +1,58 @@
1
+ /* The controls mount inside Payload's document controls, next to Save/Publish.
2
+ Payload's Button sizes itself from text padding, which makes an icon-only
3
+ button a wide rounded rectangle. These are squared off to the same box as the
4
+ row's own icon button (the kebab menu: 1.6 * base, matching the height of a
5
+ medium button) so the group reads as toolbar chrome rather than as three text
6
+ buttons that happen to hold icons.
7
+
8
+ `--undo-redo-control-size` is the one knob for hosts mounting the controls
9
+ somewhere tighter than the document controls. */
10
+ .undo-redo-controls {
11
+ --undo-redo-control-size: calc(var(--base) * 1.6);
12
+ --undo-redo-icon-size: calc(var(--base) * 0.9);
13
+
14
+ display: flex;
15
+ align-items: center;
16
+ gap: calc(var(--base) * 0.2);
17
+ }
18
+
19
+ /* Doubled class so the padding reset outranks Payload's own `.btn--size-*`
20
+ rules whichever order the two stylesheets end up in. */
21
+ .undo-redo-controls__button.btn {
22
+ --btn-padding-block-start: 0;
23
+ --btn-padding-block-end: 0;
24
+ --btn-padding-inline-start: 0;
25
+ --btn-padding-inline-end: 0;
26
+
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ width: var(--undo-redo-control-size);
31
+ height: var(--undo-redo-control-size);
32
+ }
33
+
34
+ .undo-redo-controls__button.btn svg {
35
+ width: var(--undo-redo-icon-size);
36
+ height: var(--undo-redo-icon-size);
37
+ }
38
+
39
+ /* Disabled has to be readable across three squares sitting side by side, and a
40
+ muted icon alone is not enough at this size. So the fill and the enabled
41
+ border are dropped while the outline stays: the square keeps its place in the
42
+ row, but reads as an empty slot rather than a pressable control.
43
+
44
+ Payload's subtle style derives its hover variables from `--bg-color`,
45
+ `--btn-border` and `--color`, all overridden here, so hover stays flat
46
+ without restating it. */
47
+ .undo-redo-controls__button.btn.btn--disabled {
48
+ --bg-color: transparent;
49
+ --btn-border: 1px solid var(--theme-elevation-100);
50
+ --color: var(--theme-elevation-350);
51
+ }
52
+
53
+ /* Held-open state, mirroring how the document controls mark their own popup
54
+ trigger while its menu is showing. */
55
+ .undo-redo-controls__button--active.btn {
56
+ --bg-color: var(--theme-elevation-150);
57
+ --btn-border: 1px solid var(--theme-elevation-400);
58
+ }
@@ -0,0 +1,70 @@
1
+ import { UndoHistory } from "../history/historyCore.js";
2
+ import { ControlsClientProps } from "../plugin/options.js";
3
+ import { FormState } from "payload";
4
+
5
+ //#region src/client/useUndoRedo.d.ts
6
+ /**
7
+ * Every setting is optional so the hook stays usable when a host calls it by
8
+ * hand rather than through the plugin, which always passes a fully resolved set.
9
+ */
10
+ type UseUndoRedoOptions = Partial<ControlsClientProps>;
11
+ /** The chords actually bound, or null when keyboard handling is off. */
12
+ interface BoundChords {
13
+ undo: string[];
14
+ redo: string[];
15
+ }
16
+ interface UseUndoRedoResult {
17
+ /** True when there is an earlier entry to step back to. */
18
+ canUndo: boolean;
19
+ /** True when there is a later entry to step forward to. */
20
+ canRedo: boolean;
21
+ undo: () => void;
22
+ redo: () => void;
23
+ /** Restore the entry at `index` in `history.stack`. Out-of-range indexes no-op. */
24
+ jumpTo: (index: number) => void;
25
+ /** Live form state, as `useAllFormFields` reports it. */
26
+ fields: FormState;
27
+ /**
28
+ * The mutable history object backing this instance.
29
+ *
30
+ * Exposed so a custom control can mount `<HistoryDebugOverlay />` or inspect
31
+ * the stack. It is an internal structure, not a stable contract: treat it as
32
+ * read-only and expect its shape to change in a minor release.
33
+ */
34
+ history: UndoHistory;
35
+ /**
36
+ * Counter bumped on every capture and restore. Only moves when `debug` is on,
37
+ * since it exists to pull the mutable history into React's render cycle for
38
+ * the inspector and nothing else needs the re-renders.
39
+ */
40
+ revision: number;
41
+ /** False under autosave, where the saved baseline is deliberately not tracked. */
42
+ tracksSavedState: boolean;
43
+ /** Resolved keyboard chords, for labelling. Null when `shortcuts` is false. */
44
+ chords: BoundChords | null;
45
+ }
46
+ /**
47
+ * Undo/redo for the surrounding Payload form: a client-side history of
48
+ * form-state snapshots, independent of Payload's document versions, with
49
+ * nothing read from or written to the server until the user saves.
50
+ *
51
+ * Backs `<UndoRedoControls />` and is exported so a host can build its own
52
+ * controls instead (pair it with `autoMount: false`). Must be called from
53
+ * inside a document edit form; outside one it finds no fields and stays
54
+ * disabled.
55
+ *
56
+ * One history per call. Two instances on the same form keep two independent
57
+ * stacks and each captures the other's restores as fresh edits, so mount either
58
+ * ours or yours, not both.
59
+ */
60
+ declare const useUndoRedo: ({
61
+ captureDebounce,
62
+ debug,
63
+ ignoreFieldTypes,
64
+ ignorePaths,
65
+ maxHistory,
66
+ shortcuts
67
+ }?: UseUndoRedoOptions) => UseUndoRedoResult;
68
+ //#endregion
69
+ export { BoundChords, UseUndoRedoOptions, UseUndoRedoResult, useUndoRedo };
70
+ //# sourceMappingURL=useUndoRedo.d.ts.map