@lvl99dotcom/paintcanvas 0.3.0 → 0.4.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 (98) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/brushes/StrokeEngine/index.d.ts +2 -2
  3. package/dist/lib/brushes/StrokeEngine/index.js +26 -28
  4. package/dist/lib/classes/BrushSettings/index.d.ts +24 -0
  5. package/dist/lib/classes/BrushSettings/index.js +35 -1
  6. package/dist/lib/classes/KeyboardShortcuts/index.d.ts +17 -6
  7. package/dist/lib/classes/KeyboardShortcuts/index.js +81 -18
  8. package/dist/lib/classes/PaintCanvas/index.d.ts +77 -27
  9. package/dist/lib/classes/PaintCanvas/index.js +143 -42
  10. package/dist/lib/classes/PaintCanvas/tool.d.ts +15 -0
  11. package/dist/lib/classes/PaintCanvas/tool.js +18 -0
  12. package/dist/lib/classes/Palette/index.d.ts +58 -0
  13. package/dist/lib/classes/Palette/index.js +92 -0
  14. package/dist/lib/classes/SettingsAutosave/index.d.ts +52 -0
  15. package/dist/lib/classes/SettingsAutosave/index.js +96 -0
  16. package/dist/lib/classes/ShortcutSettings/index.d.ts +63 -0
  17. package/dist/lib/classes/ShortcutSettings/index.js +110 -0
  18. package/dist/lib/palette/index.d.ts +49 -0
  19. package/dist/lib/palette/index.js +82 -0
  20. package/dist/lib/palette/schema.d.ts +45 -0
  21. package/dist/lib/palette/schema.js +41 -0
  22. package/dist/lib/rendering/cursors/index.d.ts +2 -0
  23. package/dist/lib/rendering/cursors/index.js +3 -1
  24. package/dist/lib/rendering/sample/index.js +3 -2
  25. package/dist/lib/settings/index.d.ts +63 -0
  26. package/dist/lib/settings/index.js +91 -0
  27. package/dist/lib/settings/schema.d.ts +150 -0
  28. package/dist/lib/settings/schema.js +102 -0
  29. package/dist/lib/storage/index.d.ts +36 -5
  30. package/dist/lib/storage/index.js +87 -29
  31. package/dist/lib/utils/colour/index.d.ts +72 -0
  32. package/dist/lib/utils/colour/index.js +154 -0
  33. package/dist/lib/utils/shortcuts/index.d.ts +174 -11
  34. package/dist/lib/utils/shortcuts/index.js +391 -145
  35. package/dist/lib/validation/validators.generated.d.ts +5 -0
  36. package/dist/lib/validation/validators.generated.js +872 -1
  37. package/dist/locales/en-GB.js +87 -1
  38. package/dist/locales/en-US.js +87 -1
  39. package/dist/locales/fr.js +87 -1
  40. package/dist/locales/ja.js +87 -1
  41. package/dist/locales/ko.js +87 -1
  42. package/dist/locales/zh-Hans.js +87 -1
  43. package/dist/locales/zh-Hant.js +87 -1
  44. package/dist/package.js +1 -1
  45. package/dist/ui/elements/PaintCanvas/index.d.ts +49 -47
  46. package/dist/ui/elements/PaintCanvas/index.js +128 -169
  47. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  48. package/dist/ui/elements/PcAppSettings/index.d.ts +59 -0
  49. package/dist/ui/elements/PcAppSettings/index.js +222 -0
  50. package/dist/ui/elements/PcBrushConfig/index.js +1 -0
  51. package/dist/ui/elements/PcButton/index.js +1 -0
  52. package/dist/ui/elements/PcCanvas/index.d.ts +1 -1
  53. package/dist/ui/elements/PcCanvas/index.js +7 -5
  54. package/dist/ui/elements/PcColourPair/index.d.ts +43 -0
  55. package/dist/ui/elements/PcColourPair/index.js +125 -0
  56. package/dist/ui/elements/PcContextMenu/index.d.ts +70 -33
  57. package/dist/ui/elements/PcContextMenu/index.js +128 -97
  58. package/dist/ui/elements/PcDialog/index.js +1 -0
  59. package/dist/ui/elements/PcIconToggle/index.js +1 -0
  60. package/dist/ui/elements/PcLayerItem/index.d.ts +13 -34
  61. package/dist/ui/elements/PcLayerItem/index.js +39 -91
  62. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  63. package/dist/ui/elements/PcMenu/index.d.ts +93 -0
  64. package/dist/ui/elements/PcMenu/index.js +204 -0
  65. package/dist/ui/elements/PcPane/index.d.ts +118 -0
  66. package/dist/ui/elements/PcPane/index.js +357 -0
  67. package/dist/ui/elements/PcPanel/index.d.ts +26 -8
  68. package/dist/ui/elements/PcPanel/index.js +76 -8
  69. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.d.ts +20 -6
  70. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.js +20 -15
  71. package/dist/ui/elements/PcPanelBrushes/style.js +3 -0
  72. package/dist/ui/elements/PcPanelColours/index.d.ts +171 -0
  73. package/dist/ui/elements/PcPanelColours/index.js +650 -0
  74. package/dist/ui/elements/PcPanelColours/style.js +3 -0
  75. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.d.ts +17 -4
  76. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.js +17 -10
  77. package/dist/ui/elements/PcPanelLayers/style.js +3 -0
  78. package/dist/ui/elements/PcShortcutSettings/index.d.ts +69 -0
  79. package/dist/ui/elements/PcShortcutSettings/index.js +273 -0
  80. package/dist/ui/elements/PcThemePicker/index.js +1 -0
  81. package/dist/ui/elements/PcToast/index.js +1 -0
  82. package/dist/ui/elements/PcToolSettings/index.js +2 -1
  83. package/dist/ui/elements/PcToolbar/index.d.ts +47 -12
  84. package/dist/ui/elements/PcToolbar/index.js +77 -56
  85. package/dist/ui/elements/PcToolbar/style.js +1 -1
  86. package/dist/ui/index.d.ts +7 -1
  87. package/dist/ui/index.js +8 -2
  88. package/dist/ui/tokens/index.js +3 -1
  89. package/locales/en-GB.json +87 -1
  90. package/locales/en-US.json +87 -1
  91. package/locales/fr.json +87 -1
  92. package/locales/ja.json +87 -1
  93. package/locales/ko.json +87 -1
  94. package/locales/zh-Hans.json +87 -1
  95. package/locales/zh-Hant.json +87 -1
  96. package/package.json +1 -1
  97. package/dist/ui/elements/PcBrushMenu/style.js +0 -3
  98. package/dist/ui/elements/PcLayersMenu/style.js +0 -3
@@ -1,150 +1,224 @@
1
- import { Tool } from "../../classes/PaintCanvas/index.js";
1
+ import { Tool } from "../../classes/PaintCanvas/tool.js";
2
2
  // src/lib/utils/shortcuts/index.ts
3
- /** All keyboard shortcuts */
4
- var SHORTCUTS = [
5
- {
6
- key: "b",
7
- action: {
8
- type: "tool",
9
- tool: Tool.Brush
10
- }
11
- },
12
- {
13
- key: "e",
14
- action: {
15
- type: "tool",
16
- tool: Tool.Eraser
17
- }
18
- },
19
- {
20
- key: "v",
21
- action: {
22
- type: "tool",
23
- tool: Tool.Move
24
- }
25
- },
26
- {
27
- key: "m",
28
- action: {
29
- type: "tool",
30
- tool: Tool.RectangleLasso
31
- }
32
- },
33
- {
3
+ /** ID of the action run while its key is held: pan, returning to the last tool when released */
4
+ var HOLD_TO_PAN = "pan";
5
+ /**
6
+ * Define a shortcut action.
7
+ */
8
+ var define = (id, group, action, combos, configurable = true) => ({
9
+ id,
10
+ group,
11
+ action,
12
+ combos,
13
+ configurable
14
+ });
15
+ /** Define a configurable tool shortcut */
16
+ var tool = (id, toolName, combo) => define(id, "tools", {
17
+ type: "tool",
18
+ tool: toolName
19
+ }, [combo]);
20
+ /** Layer opacity shortcuts: 1 for 10% to 9 for 90%, and 0 for 100% */
21
+ var LAYER_OPACITY_SHORTCUTS = Array.from({ length: 10 }, (_, i) => define(`layer-opacity-${(i + 1) * 10}`, "layers", {
22
+ type: "layer-opacity",
23
+ opacity: (i + 1) / 10
24
+ }, [{ key: String((i + 1) % 10) }], false));
25
+ /** Every keyboard shortcut action, in the order they are listed */
26
+ var SHORTCUT_DEFINITIONS = [
27
+ define("pan", "tools", { type: "pan" }, [{ key: " " }]),
28
+ tool("tool-move", Tool.Move, { key: "v" }),
29
+ tool("tool-brush", Tool.Brush, { key: "b" }),
30
+ tool("tool-eraser", Tool.Eraser, { key: "e" }),
31
+ tool("tool-rectangle-mask", Tool.RectangleLasso, { key: "m" }),
32
+ tool("tool-circle-mask", Tool.CircleLasso, {
34
33
  key: "m",
35
- shift: true,
36
- action: {
37
- type: "tool",
38
- tool: Tool.CircleLasso
39
- }
40
- },
41
- {
42
- key: "l",
43
- action: {
44
- type: "tool",
45
- tool: Tool.PolygonLasso
46
- }
47
- },
48
- {
34
+ shift: true
35
+ }),
36
+ tool("tool-polygon-mask", Tool.PolygonLasso, { key: "l" }),
37
+ tool("tool-freehand-mask", Tool.FreeformLasso, {
49
38
  key: "l",
50
- shift: true,
51
- action: {
52
- type: "tool",
53
- tool: Tool.FreeformLasso
54
- }
55
- },
56
- {
57
- key: "h",
58
- action: {
59
- type: "tool",
60
- tool: Tool.Pan
61
- }
62
- },
63
- {
64
- key: "z",
65
- action: {
66
- type: "tool",
67
- tool: Tool.Zoom
68
- }
69
- },
70
- {
71
- key: "i",
72
- action: {
73
- type: "tool",
74
- tool: Tool.ColourSample
75
- }
76
- },
77
- {
78
- key: "[",
79
- action: { type: "brush-smaller" }
80
- },
81
- {
82
- key: "]",
83
- action: { type: "brush-bigger" }
84
- },
85
- {
86
- key: "tab",
87
- action: { type: "toggle-interface" }
88
- },
89
- {
39
+ shift: true
40
+ }),
41
+ tool("tool-colour-sample", Tool.ColourSample, { key: "i" }),
42
+ tool("tool-zoom", Tool.Zoom, { key: "z" }),
43
+ define("brush-bigger", "brushes", { type: "brush-bigger" }, [{ key: "]" }]),
44
+ define("brush-smaller", "brushes", { type: "brush-smaller" }, [{ key: "[" }]),
45
+ define("brush-more-opaque", "brushes", { type: "brush-more-opaque" }, [{ key: ">" }]),
46
+ define("brush-less-opaque", "brushes", { type: "brush-less-opaque" }, [{ key: "<" }]),
47
+ define("select-all", "masking", { type: "select-all" }, [{
90
48
  key: "a",
49
+ cmd: true
50
+ }]),
51
+ define("invert-selection", "masking", { type: "invert-selection" }, [{
52
+ key: "i",
91
53
  cmd: true,
92
- action: { type: "select-all" }
93
- },
94
- {
54
+ shift: true
55
+ }]),
56
+ define("deselect", "masking", { type: "deselect" }, [{
95
57
  key: "d",
96
- cmd: true,
97
- action: { type: "deselect" }
98
- },
99
- {
100
- key: "s",
101
- cmd: true,
102
- action: { type: "save" }
103
- },
104
- {
105
- key: "z",
106
- cmd: true,
107
- action: { type: "undo" }
108
- },
109
- {
110
- key: "z",
111
- cmd: true,
112
- shift: true,
113
- action: { type: "redo" }
114
- },
115
- {
116
- key: "x",
117
- cmd: true,
118
- action: { type: "cut" }
119
- },
120
- {
121
- key: "delete",
122
- action: { type: "delete" }
123
- },
124
- {
58
+ cmd: true
59
+ }]),
60
+ define("fill-foreground", "fills", { type: "fill-foreground" }, [{
61
+ key: "backspace",
62
+ cmd: true
63
+ }]),
64
+ define("fill-background", "fills", { type: "fill-background" }, [{
125
65
  key: "backspace",
126
- action: { type: "delete" }
127
- },
128
- {
66
+ alt: true
67
+ }]),
68
+ define("swap-colours", "fills", { type: "swap-colours" }, [{ key: "x" }]),
69
+ define("zoom-in", "viewport", { type: "zoom-in" }, [{
70
+ key: "+",
71
+ cmd: true
72
+ }]),
73
+ define("zoom-out", "viewport", { type: "zoom-out" }, [{
74
+ key: "-",
75
+ cmd: true
76
+ }]),
77
+ define("reset-zoom", "viewport", { type: "reset-zoom" }, [{
78
+ key: "0",
79
+ cmd: true
80
+ }]),
81
+ ...LAYER_OPACITY_SHORTCUTS,
82
+ define("cut", "clipboard", { type: "cut" }, [{
83
+ key: "x",
84
+ cmd: true
85
+ }], false),
86
+ define("copy", "clipboard", { type: "copy" }, [{
129
87
  key: "c",
130
- cmd: true,
131
- action: { type: "copy" }
132
- },
133
- {
88
+ cmd: true
89
+ }], false),
90
+ define("paste", "clipboard", { type: "paste" }, [{
134
91
  key: "v",
92
+ cmd: true
93
+ }], false),
94
+ define("undo", "history", { type: "undo" }, [{
95
+ key: "z",
96
+ cmd: true
97
+ }], false),
98
+ define("redo", "history", { type: "redo" }, [{
99
+ key: "z",
135
100
  cmd: true,
136
- action: { type: "paste" }
137
- },
138
- {
139
- key: "escape",
140
- action: { type: "cancel" }
141
- },
142
- {
143
- key: "enter",
144
- action: { type: "commit" }
145
- }
101
+ shift: true
102
+ }], false),
103
+ define("toggle-interface", "general", { type: "toggle-interface" }, [{ key: "tab" }], false),
104
+ define("save", "general", { type: "save" }, [{
105
+ key: "s",
106
+ cmd: true
107
+ }], false),
108
+ define("delete", "general", { type: "delete" }, [{ key: "delete" }, { key: "backspace" }], false),
109
+ define("cancel", "general", { type: "cancel" }, [{ key: "escape" }], false),
110
+ define("commit", "general", { type: "commit" }, [{ key: "enter" }], false)
146
111
  ];
147
- /** Key held to pan temporarily (handled separately because it acts while held, not on press) */
112
+ /** The actions users can change the key combinations of */
113
+ var CONFIGURABLE_SHORTCUTS = SHORTCUT_DEFINITIONS.filter((definition) => definition.configurable);
114
+ /** The actions whose key combinations can't be changed */
115
+ var FIXED_SHORTCUTS = SHORTCUT_DEFINITIONS.filter((definition) => !definition.configurable);
116
+ /** Longest key name in a saved key combination (characters) */
117
+ var MAX_KEY_LENGTH = 32;
118
+ /**
119
+ * A key combination with only the modifiers that are on, e.g. to compare or save it.
120
+ *
121
+ * @example
122
+ * normaliseCombo({ key: "s", cmd: true, shift: false }); // { key: "s", cmd: true }
123
+ */
124
+ var normaliseCombo = ({ key, cmd, ctrl, shift, alt }) => ({
125
+ key,
126
+ ...cmd && { cmd: true },
127
+ ...ctrl && { ctrl: true },
128
+ ...shift && { shift: true },
129
+ ...alt && { alt: true }
130
+ });
131
+ /**
132
+ * Whether two key combinations are the same.
133
+ *
134
+ * @example
135
+ * sameCombo({ key: "s", cmd: true }, { key: "s", cmd: true, shift: false }); // true
136
+ */
137
+ var sameCombo = (a, b) => a.key === b.key && Boolean(a.cmd) === Boolean(b.cmd) && Boolean(a.ctrl) === Boolean(b.ctrl) && Boolean(a.shift) === Boolean(b.shift) && Boolean(a.alt) === Boolean(b.alt);
138
+ /**
139
+ * Keep a valid key combination from an untrusted value (e.g. stored or imported). Undefined if it isn't one.
140
+ *
141
+ * @example
142
+ * sanitiseCombo({ key: "s", cmd: true, extra: 1 }); // { key: "s", cmd: true }
143
+ * sanitiseCombo({ key: "" }); // undefined
144
+ */
145
+ var sanitiseCombo = (input) => {
146
+ if (typeof input !== "object" || input === null) return void 0;
147
+ const { key, cmd, ctrl, shift, alt } = input;
148
+ if (typeof key !== "string" || key.length === 0 || key.length > MAX_KEY_LENGTH || key !== key.toLowerCase() || ![
149
+ cmd,
150
+ ctrl,
151
+ shift,
152
+ alt
153
+ ].every((flag) => flag === void 0 || typeof flag === "boolean")) return void 0;
154
+ return normaliseCombo({
155
+ key,
156
+ cmd,
157
+ ctrl,
158
+ shift,
159
+ alt
160
+ });
161
+ };
162
+ /**
163
+ * The default key combination of each configurable action.
164
+ *
165
+ * @example
166
+ * defaultShortcutBindings()["tool-brush"]; // { key: "b" }
167
+ */
168
+ var defaultShortcutBindings = () => Object.fromEntries(CONFIGURABLE_SHORTCUTS.map(({ id, combos }) => [id, normaliseCombo(combos[0])]));
169
+ /**
170
+ * The fixed action that uses a key combination, if any.
171
+ *
172
+ * @example
173
+ * fixedShortcutFor({ key: "z", cmd: true })?.id; // "undo"
174
+ */
175
+ var fixedShortcutFor = (combo) => FIXED_SHORTCUTS.find(({ combos }) => combos.some((fixed) => sameCombo(fixed, combo)));
176
+ /**
177
+ * Keep valid key combinations from an untrusted value (e.g. stored or imported), using `defaults` for actions that are
178
+ * missing or invalid. `null` removes an action's key combination. A key combination a fixed action uses, or that an
179
+ * earlier action (in `SHORTCUT_DEFINITIONS` order) already has, is removed. Unknown actions are left out.
180
+ *
181
+ * @example
182
+ * sanitiseShortcutBindings({ "tool-brush": { key: "p" }, cut: null }, defaultShortcutBindings());
183
+ */
184
+ var sanitiseShortcutBindings = (input, defaults) => {
185
+ const given = typeof input === "object" && input !== null ? input : {};
186
+ const used = [];
187
+ return Object.fromEntries(CONFIGURABLE_SHORTCUTS.map(({ id }) => {
188
+ const value = given[id];
189
+ const combo = value === null ? null : sanitiseCombo(value) ?? defaults[id] ?? null;
190
+ if (!combo || fixedShortcutFor(combo) || used.some((other) => sameCombo(other, combo))) return [id, null];
191
+ used.push(combo);
192
+ return [id, combo];
193
+ }));
194
+ };
195
+ /**
196
+ * The shortcuts to match key presses against: the configurable actions' key combinations (except the hold-to-pan key,
197
+ * which acts while held) and the fixed ones.
198
+ *
199
+ * @example
200
+ * shortcutsFor(defaultShortcutBindings()).find((s) => s.key === "b")?.action; // { type: "tool", tool: Tool.Brush }
201
+ */
202
+ var shortcutsFor = (bindings) => [...CONFIGURABLE_SHORTCUTS.flatMap(({ id, action }) => {
203
+ const combo = bindings[id];
204
+ return combo && id !== "pan" ? [{
205
+ ...combo,
206
+ action
207
+ }] : [];
208
+ }), ...FIXED_SHORTCUTS.flatMap(({ action, combos }) => combos.map((combo) => ({
209
+ ...combo,
210
+ action
211
+ })))];
212
+ /** All keyboard shortcuts with their default key combinations */
213
+ var SHORTCUTS = shortcutsFor(defaultShortcutBindings());
214
+ /**
215
+ * Whether a platform (`navigator.platform` by default) is an Apple device, where "cmd" is the Command key.
216
+ *
217
+ * @example
218
+ * isApplePlatform("MacIntel"); // true
219
+ * isApplePlatform("Win32"); // false
220
+ */
221
+ var isApplePlatform = (platform = navigator.platform) => /Mac|iPhone|iPad|iPod/.test(platform);
148
222
  /**
149
223
  * The symbol for the "cmd" key in shortcut labels: ⌘ on Apple devices, ^ (Control) elsewhere.
150
224
  *
@@ -152,21 +226,193 @@ var SHORTCUTS = [
152
226
  * cmdSymbol("MacIntel"); // "⌘"
153
227
  * cmdSymbol("Win32"); // "^"
154
228
  */
155
- var cmdSymbol = (platform = navigator.platform) => /Mac|iPhone|iPad|iPod/.test(platform) ? "⌘" : "^";
156
- var HOLD_TO_PAN_KEY = " ";
229
+ var cmdSymbol = (platform) => isApplePlatform(platform) ? "⌘" : "^";
230
+ /** Symbols for keys in shortcut labels; other keys are shown in uppercase */
231
+ var KEY_SYMBOLS = {
232
+ backspace: "⌫",
233
+ delete: "⌦",
234
+ tab: "⇥",
235
+ escape: "⎋",
236
+ enter: "↵",
237
+ arrowleft: "←",
238
+ arrowright: "→",
239
+ arrowup: "↑",
240
+ arrowdown: "↓",
241
+ " ": "Space"
242
+ };
243
+ /** Names of keys for `aria-keyshortcuts`; other keys are named as they are, in uppercase */
244
+ var ARIA_KEY_NAMES = {
245
+ backspace: "Backspace",
246
+ delete: "Delete",
247
+ tab: "Tab",
248
+ escape: "Escape",
249
+ enter: "Enter",
250
+ arrowleft: "ArrowLeft",
251
+ arrowright: "ArrowRight",
252
+ arrowup: "ArrowUp",
253
+ arrowdown: "ArrowDown",
254
+ " ": "Space"
255
+ };
256
+ /**
257
+ * A short label for a key combination, e.g. for menus: modifier symbols (^ Control, ⌥ Alt, ⇧ Shift, then ⌘ for "cmd" on
258
+ * Apple devices or ^ elsewhere), then the key as a symbol or in uppercase.
259
+ *
260
+ * @example
261
+ * shortcutLabel({ key: "z", cmd: true, shift: true }, "MacIntel"); // "⇧⌘Z"
262
+ * shortcutLabel({ key: "backspace", alt: true }, "Win32"); // "⌥⌫"
263
+ */
264
+ var shortcutLabel = (combo, platform) => [
265
+ combo.ctrl && "^",
266
+ combo.alt && "⌥",
267
+ combo.shift && "⇧",
268
+ combo.cmd && cmdSymbol(platform),
269
+ KEY_SYMBOLS[combo.key] ?? combo.key.toUpperCase()
270
+ ].filter(Boolean).join("");
271
+ /**
272
+ * A key combination as an `aria-keyshortcuts` value, so screen readers can announce it.
273
+ *
274
+ * @example
275
+ * ariaKeyShortcut({ key: "s", cmd: true }, "MacIntel"); // "Meta+S"
276
+ * ariaKeyShortcut({ key: "backspace", alt: true }, "Win32"); // "Alt+Backspace"
277
+ */
278
+ var ariaKeyShortcut = (combo, platform) => [
279
+ (combo.ctrl || combo.cmd && !isApplePlatform(platform)) && "Control",
280
+ combo.cmd && isApplePlatform(platform) && "Meta",
281
+ combo.alt && "Alt",
282
+ combo.shift && "Shift",
283
+ ARIA_KEY_NAMES[combo.key] ?? combo.key.toUpperCase()
284
+ ].filter(Boolean).join("+");
285
+ /** Keys that are only modifiers, which don't make a key combination on their own (lowercase) */
286
+ var MODIFIER_KEYS = /* @__PURE__ */ new Set([
287
+ "shift",
288
+ "control",
289
+ "alt",
290
+ "altgraph",
291
+ "meta",
292
+ "os",
293
+ "super",
294
+ "hyper",
295
+ "fn",
296
+ "fnlock",
297
+ "capslock",
298
+ "numlock",
299
+ "scrolllock",
300
+ "symbol",
301
+ "symbollock"
302
+ ]);
303
+ /** Characters of physical keys (by `KeyboardEvent.code`) other than letters and digits, as on a US keyboard */
304
+ var CODE_CHARACTERS = {
305
+ Minus: "-",
306
+ Equal: "=",
307
+ BracketLeft: "[",
308
+ BracketRight: "]",
309
+ Backslash: "\\",
310
+ Semicolon: ";",
311
+ Quote: "'",
312
+ Backquote: "`",
313
+ Comma: ",",
314
+ Period: ".",
315
+ Slash: "/",
316
+ IntlBackslash: "<"
317
+ };
318
+ /** Keys treated as another key: `=` (typed without Shift on many keyboards) as `+` */
319
+ var KEY_ALIASES = { "=": "+" };
157
320
  /**
158
- * Find the action for a key press. Modifier keys must match exactly.
321
+ * The character of a physical key (by `KeyboardEvent.code`) as on a US keyboard, e.g. `KeyA` → `a`, `Digit5` → `5`.
159
322
  *
160
323
  * @example
161
- * matchShortcut({ key: "Z", metaKey: true, ctrlKey: false, shiftKey: true, altKey: false });
324
+ * codeCharacter("KeyG"); // "g"
325
+ * codeCharacter("BracketLeft"); // "["
326
+ */
327
+ var codeCharacter = (code) => {
328
+ const character = /^(?:Key([A-Z])|Digit(\d)|Numpad(\d))$/.exec(code ?? "")?.slice(1).find(Boolean);
329
+ if (character) return character.toLowerCase();
330
+ return code ? CODE_CHARACTERS[code] : void 0;
331
+ };
332
+ /**
333
+ * Whether a key is one printable basic Latin (ASCII) character.
334
+ */
335
+ var isBasicCharacter = (key) => key.length === 1 && key >= " " && key <= "~";
336
+ /**
337
+ * The key combination of a key press, matching keyboards with different layouts:
338
+ * - Keys are matched by the character they type, so letters follow the key's label (e.g. `a` on a French keyboard).
339
+ * - Digits and symbols ignore Shift and AltGr/Option, which are often needed to type them (e.g. `<` and `[` on a
340
+ * German keyboard).
341
+ * - When a key types a character that isn't basic Latin (another alphabet, or Option on macOS), or is a dead key, the
342
+ * physical key is used instead (e.g. `a` for `ф`, and `alt g` for Option+G typing `©`).
343
+ * - `=` counts as `+`, so `cmd +` works with or without Shift.
344
+ *
345
+ * `cmd` is Command on Apple devices and Ctrl elsewhere; `ctrl` is Control on Apple devices. Undefined while composing
346
+ * text (e.g. with an input method), for modifier keys alone, and for the Windows key elsewhere.
347
+ *
348
+ * @example
349
+ * comboFromEvent({ key: "Z", metaKey: true, ctrlKey: false, shiftKey: true, altKey: false }, "MacIntel");
350
+ * // { key: "z", cmd: true, shift: true }
351
+ */
352
+ var comboFromEvent = (event, platform) => {
353
+ const apple = isApplePlatform(platform);
354
+ const typed = event.key.toLowerCase();
355
+ if (event.isComposing || typed === "process" || typed === "unidentified" || MODIFIER_KEYS.has(typed) || !apple && event.metaKey) return void 0;
356
+ const character = [...event.key].length === 1;
357
+ const physical = character && !isBasicCharacter(typed) || typed === "dead" ? codeCharacter(event.code) : void 0;
358
+ if (typed === "dead" && !physical) return void 0;
359
+ const key = KEY_ALIASES[physical ?? typed] ?? physical ?? typed;
360
+ const altGraph = event.getModifierState?.("AltGraph") ?? false;
361
+ const typedSymbol = character && !physical && !/^[a-z ]$/.test(key);
362
+ return normaliseCombo({
363
+ key,
364
+ cmd: apple ? event.metaKey : event.ctrlKey && !altGraph,
365
+ ctrl: apple && event.ctrlKey,
366
+ shift: event.shiftKey && !typedSymbol,
367
+ alt: event.altKey && !altGraph && !typedSymbol
368
+ });
369
+ };
370
+ /**
371
+ * Find the action for a key press (see `comboFromEvent`). Modifier keys must match exactly.
372
+ *
373
+ * @example
374
+ * matchShortcut({ key: "Z", metaKey: true, ctrlKey: false, shiftKey: true, altKey: false }, SHORTCUTS, "MacIntel");
162
375
  * // { type: "redo" }
163
376
  */
164
- var matchShortcut = (event, shortcuts = SHORTCUTS) => {
165
- const key = event.key.toLowerCase();
166
- const cmd = event.metaKey || event.ctrlKey;
167
- return shortcuts.find((s) => s.key === key && Boolean(s.cmd) === cmd && Boolean(s.shift) === event.shiftKey && Boolean(s.alt) === event.altKey)?.action;
377
+ var matchShortcut = (event, shortcuts = SHORTCUTS, platform) => {
378
+ const combo = comboFromEvent(event, platform);
379
+ return combo && shortcuts.find((shortcut) => sameCombo(shortcut, combo))?.action;
168
380
  };
169
381
  /**
382
+ * The modifier keys held during a key press, as a key combination without a key (e.g. to show them while recording a
383
+ * key combination). Read like `comboFromEvent`: `cmd` is Command on Apple devices and Ctrl elsewhere.
384
+ *
385
+ * @example
386
+ * heldModifiers({ key: "Shift", metaKey: true, ctrlKey: false, shiftKey: true, altKey: false }, "MacIntel");
387
+ * // { key: "", cmd: true, shift: true }
388
+ */
389
+ var heldModifiers = (event, platform) => {
390
+ const apple = isApplePlatform(platform);
391
+ return normaliseCombo({
392
+ key: "",
393
+ cmd: apple ? event.metaKey : event.ctrlKey,
394
+ ctrl: apple && event.ctrlKey,
395
+ shift: event.shiftKey,
396
+ alt: event.altKey
397
+ });
398
+ };
399
+ /**
400
+ * A key combination as words, e.g. for a text field: modifiers in the order shift, alt, cmd, ctrl, then the key
401
+ * (Space as `space`). "cmd" is written `ctrl` except on Apple devices.
402
+ *
403
+ * @example
404
+ * comboText({ key: "g", shift: true, alt: true, cmd: true, ctrl: true }, "MacIntel"); // "shift alt cmd ctrl g"
405
+ * comboText({ key: "s", cmd: true }, "Win32"); // "ctrl s"
406
+ * comboText({ key: "", shift: true }, "MacIntel"); // "shift" (modifiers only)
407
+ */
408
+ var comboText = (combo, platform) => [
409
+ combo.shift && "shift",
410
+ combo.alt && "alt",
411
+ combo.cmd && (isApplePlatform(platform) ? "cmd" : "ctrl"),
412
+ combo.ctrl && "ctrl",
413
+ combo.key === " " ? "space" : combo.key
414
+ ].filter(Boolean).join(" ");
415
+ /**
170
416
  * Check if a key press happened while typing in a text field, where shortcuts must not apply.
171
417
  *
172
418
  * @example
@@ -186,4 +432,4 @@ var isTypingTarget = (target) => {
186
432
  ].includes(target.type);
187
433
  return false;
188
434
  };
189
- export { HOLD_TO_PAN_KEY, SHORTCUTS, cmdSymbol, isTypingTarget, matchShortcut };
435
+ export { CONFIGURABLE_SHORTCUTS, FIXED_SHORTCUTS, HOLD_TO_PAN, SHORTCUTS, SHORTCUT_DEFINITIONS, ariaKeyShortcut, cmdSymbol, comboFromEvent, comboText, defaultShortcutBindings, fixedShortcutFor, heldModifiers, isApplePlatform, isTypingTarget, matchShortcut, normaliseCombo, sameCombo, sanitiseCombo, sanitiseShortcutBindings, shortcutLabel, shortcutsFor };
@@ -1,6 +1,8 @@
1
1
  // Types for the generated validators (see validators.generated.js).
2
2
  import type { BrushPresetFile } from "../presets/schema";
3
3
  import type { PaintCanvasDocument } from "../document/schema";
4
+ import type { PaletteFile } from "../palette/schema";
5
+ import type { AppSettings, SettingsFile } from "../settings/schema";
4
6
  import type {
5
7
  HistoryMessage,
6
8
  PatchMessage,
@@ -25,3 +27,6 @@ export declare const validatePatchMessage: Validator<PatchMessage>;
25
27
  export declare const validateStrokeMessage: Validator<StrokeMessage>;
26
28
  export declare const validateHistoryMessage: Validator<HistoryMessage>;
27
29
  export declare const validateDocument: Validator<PaintCanvasDocument>;
30
+ export declare const validatePaletteFile: Validator<PaletteFile>;
31
+ export declare const validateSettings: Validator<AppSettings>;
32
+ export declare const validateSettingsFile: Validator<SettingsFile>;