@lvl99dotcom/paintcanvas 0.3.1 → 0.5.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 (212) hide show
  1. package/README.md +30 -15
  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/brushes/presets/round/index.d.ts +1 -1
  5. package/dist/lib/brushes/presets/round/index.js +12 -7
  6. package/dist/lib/brushes/presets/square/index.d.ts +1 -1
  7. package/dist/lib/brushes/presets/square/index.js +7 -3
  8. package/dist/lib/brushes/shapeImage/index.d.ts +7 -3
  9. package/dist/lib/brushes/shapeImage/index.js +15 -9
  10. package/dist/lib/brushes/shapes/index.d.ts +18 -0
  11. package/dist/lib/brushes/shapes/index.js +86 -0
  12. package/dist/lib/classes/BrushSettings/index.d.ts +53 -2
  13. package/dist/lib/classes/BrushSettings/index.js +85 -14
  14. package/dist/lib/classes/KeyboardShortcuts/index.d.ts +47 -7
  15. package/dist/lib/classes/KeyboardShortcuts/index.js +145 -20
  16. package/dist/lib/classes/PaintCanvas/index.d.ts +123 -27
  17. package/dist/lib/classes/PaintCanvas/index.js +208 -41
  18. package/dist/lib/classes/PaintCanvas/tool.d.ts +15 -0
  19. package/dist/lib/classes/PaintCanvas/tool.js +18 -0
  20. package/dist/lib/classes/Palette/index.d.ts +58 -0
  21. package/dist/lib/classes/Palette/index.js +92 -0
  22. package/dist/lib/classes/SettingsAutosave/index.d.ts +53 -0
  23. package/dist/lib/classes/SettingsAutosave/index.js +98 -0
  24. package/dist/lib/classes/ShortcutSettings/index.d.ts +63 -0
  25. package/dist/lib/classes/ShortcutSettings/index.js +110 -0
  26. package/dist/lib/classes/UiSettings/index.d.ts +68 -0
  27. package/dist/lib/classes/UiSettings/index.js +82 -0
  28. package/dist/lib/palette/index.d.ts +49 -0
  29. package/dist/lib/palette/index.js +82 -0
  30. package/dist/lib/palette/schema.d.ts +45 -0
  31. package/dist/lib/palette/schema.js +41 -0
  32. package/dist/lib/rendering/cursors/index.d.ts +2 -0
  33. package/dist/lib/rendering/cursors/index.js +3 -1
  34. package/dist/lib/rendering/sample/index.js +3 -2
  35. package/dist/lib/settings/index.d.ts +63 -0
  36. package/dist/lib/settings/index.js +93 -0
  37. package/dist/lib/settings/schema.d.ts +168 -0
  38. package/dist/lib/settings/schema.js +116 -0
  39. package/dist/lib/storage/index.d.ts +36 -5
  40. package/dist/lib/storage/index.js +87 -29
  41. package/dist/lib/utils/colour/index.d.ts +72 -0
  42. package/dist/lib/utils/colour/index.js +154 -0
  43. package/dist/lib/utils/shortcuts/index.d.ts +174 -11
  44. package/dist/lib/utils/shortcuts/index.js +391 -145
  45. package/dist/lib/validation/validators.generated.d.ts +5 -0
  46. package/dist/lib/validation/validators.generated.js +1040 -1
  47. package/dist/locales/en-GB.js +96 -3
  48. package/dist/locales/en-US.js +96 -3
  49. package/dist/locales/fr.js +96 -3
  50. package/dist/locales/ja.js +96 -3
  51. package/dist/locales/ko.js +96 -3
  52. package/dist/locales/zh-Hans.js +96 -3
  53. package/dist/locales/zh-Hant.js +96 -3
  54. package/dist/package.js +1 -1
  55. package/dist/testing/recordingCanvas.js +2 -1
  56. package/dist/ui/base/index.d.ts +2 -2
  57. package/dist/ui/dismiss/index.d.ts +21 -0
  58. package/dist/ui/dismiss/index.js +27 -0
  59. package/dist/ui/drag/index.d.ts +51 -0
  60. package/dist/ui/drag/index.js +81 -0
  61. package/dist/ui/elements/PaintCanvas/index.d.ts +57 -50
  62. package/dist/ui/elements/PaintCanvas/index.js +141 -174
  63. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  64. package/dist/ui/elements/PcAppSettings/index.d.ts +59 -0
  65. package/dist/ui/elements/PcAppSettings/index.js +222 -0
  66. package/dist/ui/elements/PcBrushConfig/index.d.ts +9 -2
  67. package/dist/ui/elements/PcBrushConfig/index.js +47 -7
  68. package/dist/ui/elements/PcBrushConfig/style.js +1 -1
  69. package/dist/ui/elements/PcBrushSize/index.d.ts +53 -0
  70. package/dist/ui/elements/PcBrushSize/index.js +66 -0
  71. package/dist/ui/elements/PcBrushTest/index.d.ts +26 -0
  72. package/dist/ui/elements/PcBrushTest/index.js +73 -5
  73. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  74. package/dist/ui/elements/PcCanvas/index.d.ts +22 -2
  75. package/dist/ui/elements/PcCanvas/index.js +60 -15
  76. package/dist/ui/elements/PcCanvasMap/style.js +1 -1
  77. package/dist/ui/elements/PcClipboardTools/index.d.ts +48 -0
  78. package/dist/ui/elements/PcClipboardTools/index.js +118 -0
  79. package/dist/ui/elements/PcColourPair/index.d.ts +43 -0
  80. package/dist/ui/elements/PcColourPair/index.js +125 -0
  81. package/dist/ui/elements/PcContextMenu/index.d.ts +71 -34
  82. package/dist/ui/elements/PcContextMenu/index.js +128 -97
  83. package/dist/ui/elements/PcInputSettings/index.d.ts +13 -0
  84. package/dist/ui/elements/PcInputSettings/index.js +16 -1
  85. package/dist/ui/elements/PcLayerItem/index.d.ts +13 -34
  86. package/dist/ui/elements/PcLayerItem/index.js +39 -91
  87. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  88. package/dist/ui/elements/PcMenu/index.d.ts +138 -0
  89. package/dist/ui/elements/PcMenu/index.js +296 -0
  90. package/dist/ui/elements/PcNumberSlider/index.d.ts +137 -0
  91. package/dist/ui/elements/PcNumberSlider/index.js +247 -0
  92. package/dist/ui/elements/PcPane/index.d.ts +120 -0
  93. package/dist/ui/elements/PcPane/index.js +394 -0
  94. package/dist/ui/elements/PcPanel/index.d.ts +26 -8
  95. package/dist/ui/elements/PcPanel/index.js +76 -8
  96. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.d.ts +22 -21
  97. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.js +24 -57
  98. package/dist/ui/elements/PcPanelBrushes/style.js +3 -0
  99. package/dist/ui/elements/PcPanelColours/index.d.ts +171 -0
  100. package/dist/ui/elements/PcPanelColours/index.js +650 -0
  101. package/dist/ui/elements/PcPanelColours/style.js +3 -0
  102. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.d.ts +17 -4
  103. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.js +17 -10
  104. package/dist/ui/elements/PcPanelLayers/style.js +3 -0
  105. package/dist/ui/elements/PcPopover/index.d.ts +39 -1
  106. package/dist/ui/elements/PcPopover/index.js +72 -4
  107. package/dist/ui/elements/PcShortcutSettings/index.d.ts +69 -0
  108. package/dist/ui/elements/PcShortcutSettings/index.js +273 -0
  109. package/dist/ui/elements/PcThemePicker/index.d.ts +17 -15
  110. package/dist/ui/elements/PcThemePicker/index.js +22 -21
  111. package/dist/ui/elements/PcToolSettings/index.d.ts +48 -9
  112. package/dist/ui/elements/PcToolSettings/index.js +384 -319
  113. package/dist/ui/elements/PcToolSettings/style.js +1 -1
  114. package/dist/ui/elements/PcToolbar/index.d.ts +62 -12
  115. package/dist/ui/elements/PcToolbar/index.js +119 -57
  116. package/dist/ui/elements/PcToolbar/style.js +1 -1
  117. package/dist/ui/icons/images/icon-brush@2x.js +3 -0
  118. package/dist/ui/icons/images/icon-circle-mask@2x.js +3 -0
  119. package/dist/ui/icons/images/icon-clipboard@2x.js +3 -0
  120. package/dist/ui/icons/images/icon-cog@2x.js +3 -0
  121. package/dist/ui/icons/images/icon-cursor@2x.js +3 -0
  122. package/dist/ui/icons/images/icon-diagonal-tlbr-size@2x.js +3 -0
  123. package/dist/ui/icons/images/icon-diagonal-trbl-size@2x.js +3 -0
  124. package/dist/ui/icons/images/icon-eraser@2x.js +3 -0
  125. package/dist/ui/icons/images/icon-eyedropper@2x.js +3 -0
  126. package/dist/ui/icons/images/icon-flip-horizontal@2x.js +3 -0
  127. package/dist/ui/icons/images/icon-flip-vertical@2x.js +3 -0
  128. package/dist/ui/icons/images/icon-focus-pen@2x.js +3 -0
  129. package/dist/ui/icons/images/icon-freehand-mask@2x.js +3 -0
  130. package/dist/ui/icons/images/icon-hidden@2x.js +3 -0
  131. package/dist/ui/icons/images/icon-horizontal-size@2x.js +3 -0
  132. package/dist/ui/icons/images/icon-layers@2x.js +3 -0
  133. package/dist/ui/icons/images/icon-linked@2x.js +3 -0
  134. package/dist/ui/icons/images/icon-locked@2x.js +3 -0
  135. package/dist/ui/icons/images/icon-moon@2x.js +3 -0
  136. package/dist/ui/icons/images/icon-pan@2x.js +3 -0
  137. package/dist/ui/icons/images/icon-polygon-mask@2x.js +3 -0
  138. package/dist/ui/icons/images/icon-pressure@2x.js +3 -0
  139. package/dist/ui/icons/images/icon-randomise@2x.js +3 -0
  140. package/dist/ui/icons/images/icon-rectangle-mask@2x.js +3 -0
  141. package/dist/ui/icons/images/icon-redo@2x.js +3 -0
  142. package/dist/ui/icons/images/icon-rotate-90-left@2x.js +3 -0
  143. package/dist/ui/icons/images/icon-rotate-90-right@2x.js +3 -0
  144. package/dist/ui/icons/images/icon-rotate@2x.js +3 -0
  145. package/dist/ui/icons/images/icon-smoothing-bezier-cubic@2x.js +3 -0
  146. package/dist/ui/icons/images/icon-smoothing-bezier-quad@2x.js +3 -0
  147. package/dist/ui/icons/images/icon-smoothing-linear@2x.js +3 -0
  148. package/dist/ui/icons/images/icon-star@2x.js +3 -0
  149. package/dist/ui/icons/images/icon-sun@2x.js +3 -0
  150. package/dist/ui/icons/images/icon-tilt@2x.js +3 -0
  151. package/dist/ui/icons/images/icon-undo@2x.js +3 -0
  152. package/dist/ui/icons/images/icon-unlinked@2x.js +3 -0
  153. package/dist/ui/icons/images/icon-unlocked@2x.js +3 -0
  154. package/dist/ui/icons/images/icon-vertical-size@2x.js +3 -0
  155. package/dist/ui/icons/images/icon-visible@2x.js +3 -0
  156. package/dist/ui/icons/images/icon-zoom-in@2x.js +3 -0
  157. package/dist/ui/icons/images/icon-zoom-out@2x.js +3 -0
  158. package/dist/ui/icons/images/icon-zoom@2x.js +3 -0
  159. package/dist/ui/icons/index.d.ts +1 -1
  160. package/dist/ui/icons/index.js +84 -72
  161. package/dist/ui/index.d.ts +13 -2
  162. package/dist/ui/index.js +16 -4
  163. package/dist/ui/layout/index.d.ts +9 -1
  164. package/dist/ui/layout/index.js +11 -2
  165. package/dist/ui/tokens/index.d.ts +5 -4
  166. package/dist/ui/tokens/index.js +15 -8
  167. package/locales/en-GB.json +96 -3
  168. package/locales/en-US.json +96 -3
  169. package/locales/fr.json +96 -3
  170. package/locales/ja.json +96 -3
  171. package/locales/ko.json +96 -3
  172. package/locales/zh-Hans.json +96 -3
  173. package/locales/zh-Hant.json +96 -3
  174. package/package.json +11 -4
  175. package/dist/ui/elements/PcBrushMenu/style.js +0 -3
  176. package/dist/ui/elements/PcLayersMenu/style.js +0 -3
  177. package/dist/ui/icons/images/brush.js +0 -3
  178. package/dist/ui/icons/images/circle-mask.js +0 -3
  179. package/dist/ui/icons/images/clipboard.js +0 -3
  180. package/dist/ui/icons/images/cursor.js +0 -3
  181. package/dist/ui/icons/images/eraser.js +0 -3
  182. package/dist/ui/icons/images/eyedropper.js +0 -3
  183. package/dist/ui/icons/images/flip-horizontal.js +0 -3
  184. package/dist/ui/icons/images/flip-vertical.js +0 -3
  185. package/dist/ui/icons/images/focus-pen.js +0 -3
  186. package/dist/ui/icons/images/freehand-mask.js +0 -3
  187. package/dist/ui/icons/images/hidden.js +0 -3
  188. package/dist/ui/icons/images/layers.js +0 -3
  189. package/dist/ui/icons/images/linked.js +0 -3
  190. package/dist/ui/icons/images/locked.js +0 -3
  191. package/dist/ui/icons/images/moon.js +0 -3
  192. package/dist/ui/icons/images/pan.js +0 -3
  193. package/dist/ui/icons/images/polygon-mask.js +0 -3
  194. package/dist/ui/icons/images/pressure.js +0 -3
  195. package/dist/ui/icons/images/randomise.js +0 -3
  196. package/dist/ui/icons/images/rectangle-mask.js +0 -3
  197. package/dist/ui/icons/images/redo.js +0 -3
  198. package/dist/ui/icons/images/rotate-90-left.js +0 -3
  199. package/dist/ui/icons/images/rotate-90-right.js +0 -3
  200. package/dist/ui/icons/images/smoothing-bezier-cubic.js +0 -3
  201. package/dist/ui/icons/images/smoothing-bezier-quad.js +0 -3
  202. package/dist/ui/icons/images/smoothing-linear.js +0 -3
  203. package/dist/ui/icons/images/star.js +0 -3
  204. package/dist/ui/icons/images/sun.js +0 -3
  205. package/dist/ui/icons/images/tilt.js +0 -3
  206. package/dist/ui/icons/images/undo.js +0 -3
  207. package/dist/ui/icons/images/unlinked.js +0 -3
  208. package/dist/ui/icons/images/unlocked.js +0 -3
  209. package/dist/ui/icons/images/visible.js +0 -3
  210. package/dist/ui/icons/images/zoom-in.js +0 -3
  211. package/dist/ui/icons/images/zoom-out.js +0 -3
  212. package/dist/ui/icons/images/zoom.js +0 -3
@@ -0,0 +1,18 @@
1
+ // src/lib/classes/PaintCanvas/tool.ts
2
+ /** Drawing and navigation tools */
3
+ var Tool = /* @__PURE__ */ function(Tool) {
4
+ Tool["Brush"] = "brush";
5
+ Tool["Eraser"] = "eraser";
6
+ /** Move, scale and rotate */
7
+ Tool["Move"] = "move";
8
+ Tool["RectangleLasso"] = "rectangle-lasso";
9
+ Tool["CircleLasso"] = "circle-lasso";
10
+ Tool["PolygonLasso"] = "polygon-lasso";
11
+ Tool["FreeformLasso"] = "freeform-lasso";
12
+ Tool["Pan"] = "pan";
13
+ Tool["Zoom"] = "zoom";
14
+ /** Set the current colour from the canvas (eyedropper) */
15
+ Tool["ColourSample"] = "colour-sample";
16
+ return Tool;
17
+ }({});
18
+ export { Tool };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Keep only valid colours from an untrusted value (e.g. stored or imported): lowercase `#rrggbb` strings, each once, up
3
+ * to `MAX_PALETTE_COLOURS`. Undefined if the value isn't an array.
4
+ *
5
+ * @example
6
+ * sanitisePalette(["#FF0000", "red", "#ff0000", "#00ff00"]); // ["#ff0000", "#00ff00"]
7
+ * sanitisePalette("#ff0000"); // undefined
8
+ */
9
+ export declare const sanitisePalette: (input: unknown) => string[] | undefined;
10
+ /**
11
+ * The user's saved colours, shown as swatches. Each colour is kept once, as lowercase `#rrggbb`, up to
12
+ * `MAX_PALETTE_COLOURS`. Kept in memory; the implementer saves and restores them (see `set` and the `change` event).
13
+ *
14
+ * Dispatches a `change` event when the colours change.
15
+ *
16
+ * @example
17
+ * const palette = new Palette();
18
+ * palette.add("#123456");
19
+ * palette.colours.at(-1); // "#123456"
20
+ */
21
+ export default class Palette extends EventTarget {
22
+ private values;
23
+ /** Start with some colours (invalid ones are left out), or `DEFAULT_PALETTE` */
24
+ constructor(colours?: readonly string[]);
25
+ /** The colours in order (read-only copy) */
26
+ get colours(): readonly string[];
27
+ /**
28
+ * Add a colour at the end. Returns whether it was added: not if it's already there, the palette is full, or it isn't a
29
+ * `#rrggbb` colour.
30
+ *
31
+ * @example
32
+ * palette.add("#FF0000"); // true, added as "#ff0000"
33
+ */
34
+ add(colour: string): boolean;
35
+ /**
36
+ * Add colours at the end, e.g. from an imported palette file, leaving out ones already there. Returns how many were
37
+ * added.
38
+ *
39
+ * @example
40
+ * palette.merge(["#ff0000", "#00ff00"]); // 1, if "#ff0000" was already there
41
+ */
42
+ merge(colours: readonly string[]): number;
43
+ /**
44
+ * Remove the colour at a position.
45
+ *
46
+ * @example
47
+ * palette.remove(0);
48
+ */
49
+ remove(index: number): void;
50
+ /**
51
+ * Replace all the colours (e.g. when restoring saved ones), keeping only valid ones, and dispatch `change` if they
52
+ * changed.
53
+ *
54
+ * @example
55
+ * palette.set(["#000000", "#ffffff"]);
56
+ */
57
+ set(colours: unknown): void;
58
+ }
@@ -0,0 +1,92 @@
1
+ import { isHexColour } from "../../utils/colour/index.js";
2
+ import "../../palette/schema.js";
3
+ import { DEFAULT_PALETTE } from "../../palette/index.js";
4
+ // src/lib/classes/Palette/index.ts
5
+ /**
6
+ * Keep only valid colours from an untrusted value (e.g. stored or imported): lowercase `#rrggbb` strings, each once, up
7
+ * to `MAX_PALETTE_COLOURS`. Undefined if the value isn't an array.
8
+ *
9
+ * @example
10
+ * sanitisePalette(["#FF0000", "red", "#ff0000", "#00ff00"]); // ["#ff0000", "#00ff00"]
11
+ * sanitisePalette("#ff0000"); // undefined
12
+ */
13
+ var sanitisePalette = (input) => {
14
+ if (!Array.isArray(input)) return void 0;
15
+ const colours = /* @__PURE__ */ new Set();
16
+ for (const value of input) {
17
+ if (colours.size >= 1e3) break;
18
+ if (isHexColour(value)) colours.add(value.toLowerCase());
19
+ }
20
+ return [...colours];
21
+ };
22
+ /**
23
+ * The user's saved colours, shown as swatches. Each colour is kept once, as lowercase `#rrggbb`, up to
24
+ * `MAX_PALETTE_COLOURS`. Kept in memory; the implementer saves and restores them (see `set` and the `change` event).
25
+ *
26
+ * Dispatches a `change` event when the colours change.
27
+ *
28
+ * @example
29
+ * const palette = new Palette();
30
+ * palette.add("#123456");
31
+ * palette.colours.at(-1); // "#123456"
32
+ */
33
+ var Palette = class extends EventTarget {
34
+ values;
35
+ /** Start with some colours (invalid ones are left out), or `DEFAULT_PALETTE` */
36
+ constructor(colours = DEFAULT_PALETTE) {
37
+ super();
38
+ this.values = sanitisePalette(colours);
39
+ }
40
+ /** The colours in order (read-only copy) */
41
+ get colours() {
42
+ return this.values;
43
+ }
44
+ /**
45
+ * Add a colour at the end. Returns whether it was added: not if it's already there, the palette is full, or it isn't a
46
+ * `#rrggbb` colour.
47
+ *
48
+ * @example
49
+ * palette.add("#FF0000"); // true, added as "#ff0000"
50
+ */
51
+ add(colour) {
52
+ const before = this.values.length;
53
+ this.set([...this.values, colour]);
54
+ return this.values.length > before;
55
+ }
56
+ /**
57
+ * Add colours at the end, e.g. from an imported palette file, leaving out ones already there. Returns how many were
58
+ * added.
59
+ *
60
+ * @example
61
+ * palette.merge(["#ff0000", "#00ff00"]); // 1, if "#ff0000" was already there
62
+ */
63
+ merge(colours) {
64
+ const before = this.values.length;
65
+ this.set([...this.values, ...colours]);
66
+ return this.values.length - before;
67
+ }
68
+ /**
69
+ * Remove the colour at a position.
70
+ *
71
+ * @example
72
+ * palette.remove(0);
73
+ */
74
+ remove(index) {
75
+ if (index < 0 || index >= this.values.length) return;
76
+ this.set(this.values.filter((_, i) => i !== index));
77
+ }
78
+ /**
79
+ * Replace all the colours (e.g. when restoring saved ones), keeping only valid ones, and dispatch `change` if they
80
+ * changed.
81
+ *
82
+ * @example
83
+ * palette.set(["#000000", "#ffffff"]);
84
+ */
85
+ set(colours) {
86
+ const next = sanitisePalette(colours);
87
+ if (!next || next.length === this.values.length && next.every((colour, i) => colour === this.values[i])) return;
88
+ this.values = next;
89
+ this.dispatchEvent(new Event("change"));
90
+ }
91
+ };
92
+ export { Palette as default, sanitisePalette };
@@ -0,0 +1,53 @@
1
+ import type PaintCanvas from "../PaintCanvas";
2
+ import type { SettingsStoreLike } from "../../storage";
3
+ /** How long to wait after settings change before saving, so quick changes (e.g. dragging a colour slider) are saved together (milliseconds) */
4
+ export declare const SETTINGS_SAVE_DELAY_MS = 500;
5
+ /** Event dispatched when loading or saving settings fails (detail: the error) */
6
+ export declare const SETTINGS_AUTOSAVE_ERROR_EVENT = "error";
7
+ /** Options for `SettingsAutosave` */
8
+ export interface SettingsAutosaveOptions {
9
+ /** How long to wait after settings change before saving (default `SETTINGS_SAVE_DELAY_MS`) */
10
+ delayMs?: number;
11
+ }
12
+ /**
13
+ * Keeps a canvas's app settings (input settings, keyboard shortcuts, colour palette, colours, theme and language) in a
14
+ * store: the saved settings are restored when it starts (invalid ones are ignored), then changes are saved soon after
15
+ * they happen.
16
+ *
17
+ * Dispatches an `error` event (with the error as `detail`) when loading or saving fails.
18
+ *
19
+ * @example
20
+ * const autosave = new SettingsAutosave(canvas, new IndexedDbSettingsStore());
21
+ * await autosave.ready;
22
+ * autosave.stop(); // saves any pending change
23
+ */
24
+ export default class SettingsAutosave extends EventTarget {
25
+ /** Where the settings are kept */
26
+ readonly store: SettingsStoreLike;
27
+ /** Resolves once the saved settings are restored (or there were none) */
28
+ readonly ready: Promise<void>;
29
+ private readonly app;
30
+ private readonly delayMs;
31
+ private timer?;
32
+ /** The settings last saved or restored, as JSON, to skip saving unchanged settings */
33
+ private saved?;
34
+ private stopped;
35
+ private readonly onChange;
36
+ constructor(app: PaintCanvas, store: SettingsStoreLike, options?: SettingsAutosaveOptions);
37
+ /** The canvas parts whose changes are saved */
38
+ private get sources();
39
+ /**
40
+ * Stop saving changes, saving any that are waiting.
41
+ */
42
+ stop(): void;
43
+ /**
44
+ * Save the settings now if they changed since they were last saved.
45
+ */
46
+ save(): Promise<void>;
47
+ /** Restore the saved settings, then listen for changes */
48
+ private start;
49
+ /** Save soon, after changes stop for a moment */
50
+ private schedule;
51
+ /** Tell listeners loading or saving failed */
52
+ private fail;
53
+ }
@@ -0,0 +1,98 @@
1
+ import { applySettings, collectSettings, isValidSettings } from "../../settings/index.js";
2
+ // src/lib/classes/SettingsAutosave/index.ts
3
+ /** How long to wait after settings change before saving, so quick changes (e.g. dragging a colour slider) are saved together (milliseconds) */
4
+ var SETTINGS_SAVE_DELAY_MS = 500;
5
+ /** Event dispatched when loading or saving settings fails (detail: the error) */
6
+ var SETTINGS_AUTOSAVE_ERROR_EVENT = "error";
7
+ /**
8
+ * Keeps a canvas's app settings (input settings, keyboard shortcuts, colour palette, colours, theme and language) in a
9
+ * store: the saved settings are restored when it starts (invalid ones are ignored), then changes are saved soon after
10
+ * they happen.
11
+ *
12
+ * Dispatches an `error` event (with the error as `detail`) when loading or saving fails.
13
+ *
14
+ * @example
15
+ * const autosave = new SettingsAutosave(canvas, new IndexedDbSettingsStore());
16
+ * await autosave.ready;
17
+ * autosave.stop(); // saves any pending change
18
+ */
19
+ var SettingsAutosave = class extends EventTarget {
20
+ /** Where the settings are kept */
21
+ store;
22
+ /** Resolves once the saved settings are restored (or there were none) */
23
+ ready;
24
+ app;
25
+ delayMs;
26
+ timer;
27
+ /** The settings last saved or restored, as JSON, to skip saving unchanged settings */
28
+ saved;
29
+ stopped = false;
30
+ onChange = () => this.schedule();
31
+ constructor(app, store, options = {}) {
32
+ super();
33
+ this.app = app;
34
+ this.store = store;
35
+ this.delayMs = options.delayMs ?? 500;
36
+ this.ready = this.start();
37
+ }
38
+ /** The canvas parts whose changes are saved */
39
+ get sources() {
40
+ const { app } = this;
41
+ return [
42
+ app,
43
+ app.input,
44
+ app.shortcuts,
45
+ app.palette,
46
+ app.ui
47
+ ];
48
+ }
49
+ /**
50
+ * Stop saving changes, saving any that are waiting.
51
+ */
52
+ stop() {
53
+ if (this.stopped) return;
54
+ this.stopped = true;
55
+ for (const source of this.sources) source.removeEventListener("change", this.onChange);
56
+ if (this.timer === void 0) return;
57
+ clearTimeout(this.timer);
58
+ this.save();
59
+ }
60
+ /**
61
+ * Save the settings now if they changed since they were last saved.
62
+ */
63
+ async save() {
64
+ this.timer = void 0;
65
+ const settings = collectSettings(this.app);
66
+ const json = JSON.stringify(settings);
67
+ if (json === this.saved) return;
68
+ this.saved = json;
69
+ try {
70
+ await this.store.save(settings);
71
+ } catch (error) {
72
+ this.saved = void 0;
73
+ this.fail(error);
74
+ }
75
+ }
76
+ /** Restore the saved settings, then listen for changes */
77
+ async start() {
78
+ try {
79
+ const result = isValidSettings(await this.store.load());
80
+ if (result.valid && !this.stopped) applySettings(this.app, result.value);
81
+ } catch (error) {
82
+ this.fail(error);
83
+ }
84
+ if (this.stopped) return;
85
+ this.saved = JSON.stringify(collectSettings(this.app));
86
+ for (const source of this.sources) source.addEventListener("change", this.onChange);
87
+ }
88
+ /** Save soon, after changes stop for a moment */
89
+ schedule() {
90
+ clearTimeout(this.timer);
91
+ this.timer = setTimeout(() => void this.save(), this.delayMs);
92
+ }
93
+ /** Tell listeners loading or saving failed */
94
+ fail(error) {
95
+ this.dispatchEvent(new CustomEvent(SETTINGS_AUTOSAVE_ERROR_EVENT, { detail: error }));
96
+ }
97
+ };
98
+ export { SETTINGS_AUTOSAVE_ERROR_EVENT, SETTINGS_SAVE_DELAY_MS, SettingsAutosave as default };
@@ -0,0 +1,63 @@
1
+ import { type KeyCombo, type Shortcut, type ShortcutBindings } from "../../utils/shortcuts";
2
+ /** What happened when setting an action's key combination */
3
+ export interface ShortcutChangeResult {
4
+ /** Whether the key combination was set */
5
+ changed: boolean;
6
+ /** The action that had the key combination and lost it, if any */
7
+ movedFrom?: string;
8
+ /** The fixed action that uses the key combination, so it wasn't set */
9
+ fixed?: string;
10
+ }
11
+ /**
12
+ * The user's keyboard shortcuts: the key combination of each configurable action (see `CONFIGURABLE_SHORTCUTS`), each
13
+ * used by one action at most. Kept in memory; the implementer saves and restores them (see `load` and the `change`
14
+ * event).
15
+ *
16
+ * Dispatches a `change` event when a key combination changes.
17
+ *
18
+ * @example
19
+ * const shortcuts = new ShortcutSettings();
20
+ * shortcuts.set("tool-brush", { key: "p" });
21
+ * shortcuts.combo("tool-brush"); // { key: "p" }
22
+ */
23
+ export default class ShortcutSettings extends EventTarget {
24
+ private values;
25
+ private list;
26
+ constructor(initial?: unknown);
27
+ /** Each configurable action's key combination, by ID (read-only copy) */
28
+ get bindings(): Readonly<ShortcutBindings>;
29
+ /** The shortcuts key presses are matched against (except the hold-to-pan key) */
30
+ get shortcuts(): readonly Shortcut[];
31
+ /** The key held to pan, returning to the last tool when released (null when it has none) */
32
+ get holdToPan(): KeyCombo | null;
33
+ /**
34
+ * A configurable action's key combination (null when it has none or there's no such action).
35
+ *
36
+ * @example
37
+ * shortcuts.combo("deselect"); // { key: "d", cmd: true }
38
+ */
39
+ combo(id: string): KeyCombo | null;
40
+ /**
41
+ * Set or remove (null) a configurable action's key combination. If another configurable action has it, that action
42
+ * loses it (`movedFrom`). Nothing changes for a key combination a fixed action uses (`fixed`), or an unknown action.
43
+ *
44
+ * @example
45
+ * shortcuts.set("tool-brush", { key: "e" }); // { changed: true, movedFrom: "tool-eraser" }
46
+ * shortcuts.set("deselect", { key: "z", cmd: true }); // { changed: false, fixed: "undo" }
47
+ */
48
+ set(id: string, combo: KeyCombo | null): ShortcutChangeResult;
49
+ /**
50
+ * Go back to the default key combinations.
51
+ */
52
+ reset(): void;
53
+ /**
54
+ * Replace the key combinations, e.g. with saved or imported ones (see `sanitiseShortcutBindings`; actions left out
55
+ * get their defaults), and dispatch `change` if anything changed.
56
+ *
57
+ * @example
58
+ * shortcuts.load({ "tool-brush": { key: "p" } });
59
+ */
60
+ load(input: unknown): void;
61
+ /** Use new key combinations and tell listeners */
62
+ private apply;
63
+ }
@@ -0,0 +1,110 @@
1
+ import { CONFIGURABLE_SHORTCUTS, defaultShortcutBindings, fixedShortcutFor, normaliseCombo, sameCombo, sanitiseShortcutBindings, shortcutsFor } from "../../utils/shortcuts/index.js";
2
+ // src/lib/classes/ShortcutSettings/index.ts
3
+ /**
4
+ * The user's keyboard shortcuts: the key combination of each configurable action (see `CONFIGURABLE_SHORTCUTS`), each
5
+ * used by one action at most. Kept in memory; the implementer saves and restores them (see `load` and the `change`
6
+ * event).
7
+ *
8
+ * Dispatches a `change` event when a key combination changes.
9
+ *
10
+ * @example
11
+ * const shortcuts = new ShortcutSettings();
12
+ * shortcuts.set("tool-brush", { key: "p" });
13
+ * shortcuts.combo("tool-brush"); // { key: "p" }
14
+ */
15
+ var ShortcutSettings = class extends EventTarget {
16
+ values;
17
+ list;
18
+ constructor(initial) {
19
+ super();
20
+ this.values = sanitiseShortcutBindings(initial, defaultShortcutBindings());
21
+ this.list = shortcutsFor(this.values);
22
+ }
23
+ /** Each configurable action's key combination, by ID (read-only copy) */
24
+ get bindings() {
25
+ return this.values;
26
+ }
27
+ /** The shortcuts key presses are matched against (except the hold-to-pan key) */
28
+ get shortcuts() {
29
+ return this.list;
30
+ }
31
+ /** The key held to pan, returning to the last tool when released (null when it has none) */
32
+ get holdToPan() {
33
+ return this.values["pan"] ?? null;
34
+ }
35
+ /**
36
+ * A configurable action's key combination (null when it has none or there's no such action).
37
+ *
38
+ * @example
39
+ * shortcuts.combo("deselect"); // { key: "d", cmd: true }
40
+ */
41
+ combo(id) {
42
+ return this.values[id] ?? null;
43
+ }
44
+ /**
45
+ * Set or remove (null) a configurable action's key combination. If another configurable action has it, that action
46
+ * loses it (`movedFrom`). Nothing changes for a key combination a fixed action uses (`fixed`), or an unknown action.
47
+ *
48
+ * @example
49
+ * shortcuts.set("tool-brush", { key: "e" }); // { changed: true, movedFrom: "tool-eraser" }
50
+ * shortcuts.set("deselect", { key: "z", cmd: true }); // { changed: false, fixed: "undo" }
51
+ */
52
+ set(id, combo) {
53
+ if (!CONFIGURABLE_SHORTCUTS.some((definition) => definition.id === id)) return { changed: false };
54
+ const next = combo && normaliseCombo(combo);
55
+ const current = this.values[id] ?? null;
56
+ if (next ? current && sameCombo(current, next) : !current) return { changed: false };
57
+ if (!next) {
58
+ this.apply({
59
+ ...this.values,
60
+ [id]: null
61
+ });
62
+ return { changed: true };
63
+ }
64
+ const fixed = fixedShortcutFor(next);
65
+ if (fixed) return {
66
+ changed: false,
67
+ fixed: fixed.id
68
+ };
69
+ const movedFrom = Object.keys(this.values).find((other) => {
70
+ const otherCombo = this.values[other];
71
+ return other !== id && otherCombo && sameCombo(otherCombo, next);
72
+ });
73
+ this.apply({
74
+ ...this.values,
75
+ ...movedFrom && { [movedFrom]: null },
76
+ [id]: next
77
+ });
78
+ return movedFrom ? {
79
+ changed: true,
80
+ movedFrom
81
+ } : { changed: true };
82
+ }
83
+ /**
84
+ * Go back to the default key combinations.
85
+ */
86
+ reset() {
87
+ this.load(defaultShortcutBindings());
88
+ }
89
+ /**
90
+ * Replace the key combinations, e.g. with saved or imported ones (see `sanitiseShortcutBindings`; actions left out
91
+ * get their defaults), and dispatch `change` if anything changed.
92
+ *
93
+ * @example
94
+ * shortcuts.load({ "tool-brush": { key: "p" } });
95
+ */
96
+ load(input) {
97
+ const next = sanitiseShortcutBindings(input, defaultShortcutBindings());
98
+ if (Object.keys(next).some((id) => {
99
+ const [a, b] = [next[id], this.values[id]];
100
+ return a && b ? !sameCombo(a, b) : a !== b;
101
+ })) this.apply(next);
102
+ }
103
+ /** Use new key combinations and tell listeners */
104
+ apply(bindings) {
105
+ this.values = bindings;
106
+ this.list = shortcutsFor(bindings);
107
+ this.dispatchEvent(new Event("change"));
108
+ }
109
+ };
110
+ export { ShortcutSettings as default };
@@ -0,0 +1,68 @@
1
+ /** The themes: light, dark, or following the device's (system) */
2
+ export declare const THEMES: readonly ["light", "dark", "system"];
3
+ export type Theme = (typeof THEMES)[number];
4
+ /** Longest locale tag kept in UI settings (characters) */
5
+ export declare const MAX_LOCALE_LENGTH = 35;
6
+ /** A locale tag's characters: letters, digits and hyphens (e.g. `zh-Hant`) */
7
+ export declare const LOCALE_PATTERN = "^[A-Za-z0-9-]+$";
8
+ /**
9
+ * Whether a value is one of the themes.
10
+ *
11
+ * @example
12
+ * isTheme("dark"); // true
13
+ * isTheme(undefined); // false
14
+ */
15
+ export declare const isTheme: (value: unknown) => value is Theme;
16
+ /**
17
+ * Whether a value can be a locale tag (letters, digits and hyphens, at most `MAX_LOCALE_LENGTH` characters).
18
+ *
19
+ * @example
20
+ * isLocaleTag("zh-Hant"); // true
21
+ * isLocaleTag("<script>"); // false
22
+ */
23
+ export declare const isLocaleTag: (value: unknown) => value is string;
24
+ /** The user's interface choices */
25
+ export interface UiSettingsData {
26
+ /** The theme (`system` by default) */
27
+ theme: Theme;
28
+ /** The language's locale tag, e.g. `fr`, or undefined to follow the device's languages */
29
+ locale?: string;
30
+ }
31
+ /** UI settings the UI can show and change: PaintCanvas's `UiSettings`, or anything with the same data and `update` */
32
+ export interface UiSettingsLike extends EventTarget {
33
+ readonly data: Readonly<UiSettingsData>;
34
+ update(changes: Partial<UiSettingsData>): void;
35
+ }
36
+ /**
37
+ * Keep only valid UI settings from an untrusted value (e.g. stored or imported), using `defaults` for anything missing
38
+ * or invalid. Other properties are left out.
39
+ *
40
+ * @example
41
+ * sanitiseUiSettings({ theme: "dark", locale: 42 }, { theme: "system" }); // { theme: "dark" }
42
+ */
43
+ export declare const sanitiseUiSettings: (input: unknown, defaults: UiSettingsData) => UiSettingsData;
44
+ /**
45
+ * The user's interface choices: the theme and the language. Kept in memory; the implementer saves and restores them
46
+ * with the other app settings (see `SettingsAutosave`), and the UI shows and applies them (e.g. `<pc-theme-picker>` and
47
+ * `<pc-input-settings>`).
48
+ *
49
+ * Dispatches a `change` event when a setting changes.
50
+ *
51
+ * @example
52
+ * const ui = new UiSettings();
53
+ * ui.update({ theme: "dark", locale: "fr" });
54
+ * ui.data.theme; // "dark"
55
+ */
56
+ export default class UiSettings extends EventTarget {
57
+ private values;
58
+ constructor(initial?: unknown);
59
+ /** Current settings (read-only copy) */
60
+ get data(): Readonly<UiSettingsData>;
61
+ /**
62
+ * Change some settings and dispatch `change` if anything changed. Invalid values and other properties are ignored.
63
+ *
64
+ * @example
65
+ * ui.update({ locale: "ja" });
66
+ */
67
+ update(changes: Partial<UiSettingsData>): void;
68
+ }
@@ -0,0 +1,82 @@
1
+ // src/lib/classes/UiSettings/index.ts
2
+ /** The themes: light, dark, or following the device's (system) */
3
+ var THEMES = [
4
+ "light",
5
+ "dark",
6
+ "system"
7
+ ];
8
+ /** Longest locale tag kept in UI settings (characters) */
9
+ var MAX_LOCALE_LENGTH = 35;
10
+ /** A locale tag's characters: letters, digits and hyphens (e.g. `zh-Hant`) */
11
+ var LOCALE_PATTERN = "^[A-Za-z0-9-]+$";
12
+ /**
13
+ * Whether a value is one of the themes.
14
+ *
15
+ * @example
16
+ * isTheme("dark"); // true
17
+ * isTheme(undefined); // false
18
+ */
19
+ var isTheme = (value) => THEMES.includes(value);
20
+ /**
21
+ * Whether a value can be a locale tag (letters, digits and hyphens, at most `MAX_LOCALE_LENGTH` characters).
22
+ *
23
+ * @example
24
+ * isLocaleTag("zh-Hant"); // true
25
+ * isLocaleTag("<script>"); // false
26
+ */
27
+ var isLocaleTag = (value) => typeof value === "string" && value.length <= 35 && (/* @__PURE__ */ new RegExp("^[A-Za-z0-9-]+$")).test(value);
28
+ /**
29
+ * Keep only valid UI settings from an untrusted value (e.g. stored or imported), using `defaults` for anything missing
30
+ * or invalid. Other properties are left out.
31
+ *
32
+ * @example
33
+ * sanitiseUiSettings({ theme: "dark", locale: 42 }, { theme: "system" }); // { theme: "dark" }
34
+ */
35
+ var sanitiseUiSettings = (input, defaults) => {
36
+ const value = typeof input === "object" && input !== null ? input : {};
37
+ const theme = isTheme(value.theme) ? value.theme : defaults.theme;
38
+ const locale = isLocaleTag(value.locale) ? value.locale : defaults.locale;
39
+ return locale === void 0 ? { theme } : {
40
+ theme,
41
+ locale
42
+ };
43
+ };
44
+ /**
45
+ * The user's interface choices: the theme and the language. Kept in memory; the implementer saves and restores them
46
+ * with the other app settings (see `SettingsAutosave`), and the UI shows and applies them (e.g. `<pc-theme-picker>` and
47
+ * `<pc-input-settings>`).
48
+ *
49
+ * Dispatches a `change` event when a setting changes.
50
+ *
51
+ * @example
52
+ * const ui = new UiSettings();
53
+ * ui.update({ theme: "dark", locale: "fr" });
54
+ * ui.data.theme; // "dark"
55
+ */
56
+ var UiSettings = class extends EventTarget {
57
+ values;
58
+ constructor(initial) {
59
+ super();
60
+ this.values = sanitiseUiSettings(initial, { theme: "system" });
61
+ }
62
+ /** Current settings (read-only copy) */
63
+ get data() {
64
+ return this.values;
65
+ }
66
+ /**
67
+ * Change some settings and dispatch `change` if anything changed. Invalid values and other properties are ignored.
68
+ *
69
+ * @example
70
+ * ui.update({ locale: "ja" });
71
+ */
72
+ update(changes) {
73
+ const next = sanitiseUiSettings({
74
+ ...this.values,
75
+ ...changes
76
+ }, this.values);
77
+ if (next.theme === this.values.theme && next.locale === this.values.locale) return;
78
+ this.values = next;
79
+ this.dispatchEvent(new Event("change"));
80
+ }
81
+ };
82
+ export { LOCALE_PATTERN, MAX_LOCALE_LENGTH, THEMES, UiSettings as default, isLocaleTag, isTheme, sanitiseUiSettings };