@lvl99dotcom/paintcanvas 0.6.0 → 0.8.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 (119) hide show
  1. package/README.md +3 -2
  2. package/dist/lib/assets/index.d.ts +92 -0
  3. package/dist/lib/assets/index.js +106 -0
  4. package/dist/lib/assets/schema.d.ts +151 -0
  5. package/dist/lib/assets/schema.js +76 -0
  6. package/dist/lib/brushes/presetFile/index.d.ts +6 -27
  7. package/dist/lib/brushes/presetFile/index.js +34 -47
  8. package/dist/lib/brushes/types/index.d.ts +2 -11
  9. package/dist/lib/classes/AssetSync/index.d.ts +45 -0
  10. package/dist/lib/classes/AssetSync/index.js +80 -0
  11. package/dist/lib/classes/ColourPalettes/index.d.ts +83 -0
  12. package/dist/lib/classes/ColourPalettes/index.js +165 -0
  13. package/dist/lib/classes/CustomPresets/index.d.ts +14 -15
  14. package/dist/lib/classes/CustomPresets/index.js +25 -10
  15. package/dist/lib/classes/LayerAutosave/index.d.ts +9 -2
  16. package/dist/lib/classes/LayerAutosave/index.js +16 -6
  17. package/dist/lib/classes/PaintCanvas/index.d.ts +124 -28
  18. package/dist/lib/classes/PaintCanvas/index.js +228 -63
  19. package/dist/lib/classes/Palette/index.d.ts +3 -2
  20. package/dist/lib/classes/Palette/index.js +3 -2
  21. package/dist/lib/classes/ProfileGraphs/index.d.ts +13 -2
  22. package/dist/lib/classes/ProfileGraphs/index.js +22 -2
  23. package/dist/lib/classes/RemoteDocumentAutosave/index.d.ts +73 -0
  24. package/dist/lib/classes/RemoteDocumentAutosave/index.js +119 -0
  25. package/dist/lib/classes/SettingsAutosave/index.d.ts +7 -3
  26. package/dist/lib/classes/SettingsAutosave/index.js +28 -7
  27. package/dist/lib/classes/SettingsExtensions/index.d.ts +67 -0
  28. package/dist/lib/classes/SettingsExtensions/index.js +88 -0
  29. package/dist/lib/classes/UiSettings/index.d.ts +24 -2
  30. package/dist/lib/classes/UiSettings/index.js +32 -7
  31. package/dist/lib/document/index.d.ts +61 -4
  32. package/dist/lib/document/index.js +173 -4
  33. package/dist/lib/document/schema.d.ts +154 -39
  34. package/dist/lib/document/schema.js +91 -25
  35. package/dist/lib/messages/schema.d.ts +176 -74
  36. package/dist/lib/messages/schema.js +2 -1
  37. package/dist/lib/palette/index.d.ts +8 -42
  38. package/dist/lib/palette/index.js +26 -59
  39. package/dist/lib/palette/schema.d.ts +111 -22
  40. package/dist/lib/palette/schema.js +51 -26
  41. package/dist/lib/presets/index.d.ts +1 -9
  42. package/dist/lib/presets/index.js +4 -10
  43. package/dist/lib/presets/schema.d.ts +118 -73
  44. package/dist/lib/presets/schema.js +28 -60
  45. package/dist/lib/profiles/profileFile/index.d.ts +10 -0
  46. package/dist/lib/profiles/profileFile/index.js +19 -0
  47. package/dist/lib/profiles/schema.d.ts +96 -3
  48. package/dist/lib/profiles/schema.js +31 -7
  49. package/dist/lib/schemas/index.d.ts +42 -0
  50. package/dist/lib/schemas/index.js +60 -0
  51. package/dist/lib/settings/index.d.ts +37 -22
  52. package/dist/lib/settings/index.js +78 -37
  53. package/dist/lib/settings/schema.d.ts +202 -77
  54. package/dist/lib/settings/schema.js +93 -86
  55. package/dist/lib/storage/index.d.ts +108 -3
  56. package/dist/lib/storage/index.js +119 -5
  57. package/dist/lib/validation/validators.generated.d.ts +19 -8
  58. package/dist/lib/validation/validators.generated.js +44723 -18889
  59. package/dist/lib/version/index.d.ts +25 -0
  60. package/dist/lib/version/index.js +35 -0
  61. package/dist/locales/en-GB.js +28 -5
  62. package/dist/locales/en-US.js +28 -5
  63. package/dist/locales/fr.js +28 -5
  64. package/dist/locales/ja.js +28 -5
  65. package/dist/locales/ko.js +28 -5
  66. package/dist/locales/zh-Hans.js +28 -5
  67. package/dist/locales/zh-Hant.js +28 -5
  68. package/dist/package.js +1 -1
  69. package/dist/testing/app.d.ts +6 -2
  70. package/dist/testing/app.js +7 -2
  71. package/dist/testing/documentStore.d.ts +26 -0
  72. package/dist/testing/documentStore.js +31 -0
  73. package/dist/ui/elements/PaintCanvas/index.d.ts +19 -5
  74. package/dist/ui/elements/PaintCanvas/index.js +41 -10
  75. package/dist/ui/elements/PcAppSettings/index.d.ts +14 -7
  76. package/dist/ui/elements/PcAppSettings/index.js +58 -33
  77. package/dist/ui/elements/PcBrushConfig/index.d.ts +1 -0
  78. package/dist/ui/elements/PcBrushConfig/index.js +8 -9
  79. package/dist/ui/elements/PcBrushTest/index.d.ts +1 -0
  80. package/dist/ui/elements/PcBrushTest/index.js +8 -7
  81. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  82. package/dist/ui/elements/PcButtonMenu/index.d.ts +88 -0
  83. package/dist/ui/elements/PcButtonMenu/index.js +125 -0
  84. package/dist/ui/elements/PcCanvas/index.d.ts +9 -0
  85. package/dist/ui/elements/PcCanvas/index.js +33 -17
  86. package/dist/ui/elements/PcClipboardTools/index.d.ts +7 -4
  87. package/dist/ui/elements/PcClipboardTools/index.js +20 -26
  88. package/dist/ui/elements/PcInputSettings/index.d.ts +2 -1
  89. package/dist/ui/elements/PcInputSettings/index.js +9 -15
  90. package/dist/ui/elements/PcLayerItem/index.d.ts +21 -6
  91. package/dist/ui/elements/PcLayerItem/index.js +53 -26
  92. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  93. package/dist/ui/elements/PcPanelBrushes/index.d.ts +1 -1
  94. package/dist/ui/elements/PcPanelBrushes/index.js +22 -28
  95. package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
  96. package/dist/ui/elements/PcPanelColours/index.d.ts +56 -19
  97. package/dist/ui/elements/PcPanelColours/index.js +251 -104
  98. package/dist/ui/elements/PcPanelColours/style.js +1 -1
  99. package/dist/ui/elements/PcSliderRange/index.js +3 -2
  100. package/dist/ui/elements/PcStats/index.d.ts +8 -4
  101. package/dist/ui/elements/PcStats/index.js +39 -5
  102. package/dist/ui/elements/PcToggleCheckbox/index.d.ts +13 -2
  103. package/dist/ui/elements/PcToggleCheckbox/index.js +27 -8
  104. package/dist/ui/elements/PcToolSettings/index.d.ts +1 -0
  105. package/dist/ui/elements/PcToolSettings/index.js +8 -8
  106. package/dist/ui/elements/PcVersion/index.d.ts +12 -0
  107. package/dist/ui/elements/PcVersion/index.js +43 -0
  108. package/dist/ui/index.d.ts +3 -1
  109. package/dist/ui/index.js +4 -2
  110. package/dist/ui/layout/index.d.ts +2 -2
  111. package/dist/ui/layout/index.js +2 -2
  112. package/locales/en-GB.json +28 -5
  113. package/locales/en-US.json +28 -5
  114. package/locales/fr.json +28 -5
  115. package/locales/ja.json +28 -5
  116. package/locales/ko.json +28 -5
  117. package/locales/zh-Hans.json +28 -5
  118. package/locales/zh-Hant.json +28 -5
  119. package/package.json +3 -2
@@ -1,14 +1,18 @@
1
1
  import style_default from "./style.js";
2
- import { PALETTE_FILE_FORMAT } from "../../../lib/palette/schema.js";
3
2
  import { ICON_CONTROLS_CSS, NO_TEXT_SELECTION_CSS, PILLS_CSS } from "../../styles/index.js";
3
+ import "../../../lib/palette/schema.js";
4
4
  import { defineElement, emit } from "../../base/index.js";
5
5
  import { PcPanel } from "../PcPanel/index.js";
6
6
  import ChangeController from "../../ChangeController/index.js";
7
7
  import "../PcColourPair/index.js";
8
8
  import "../PcMenu/index.js";
9
+ import "../PcContextMenu/index.js";
9
10
  import { CANCEL_CHOICE } from "../PcDialog/index.js";
10
11
  import { hexToHsl, hexToRgb, hslToHex, normaliseHex, rgbToHex } from "../../../lib/utils/colour/index.js";
11
- import { PALETTE_FILE_ERRORS, PaletteFileError, exportPaletteFile, paletteFileName, parsePaletteFile } from "../../../lib/palette/index.js";
12
+ import { colourPaletteFile } from "../../../lib/palette/index.js";
13
+ import { DEFAULT_PALETTE_ID } from "../../../lib/classes/ColourPalettes/index.js";
14
+ import { AssetFileError } from "../../../lib/assets/index.js";
15
+ import { askConfirm, askText } from "../../ask/index.js";
12
16
  import { JSON_FILE_TYPE, saveFile } from "../../../lib/rendering/screenshot/index.js";
13
17
  import { html, nothing, unsafeCSS } from "lit";
14
18
  import { live } from "lit/directives/live.js";
@@ -115,11 +119,23 @@ var MODELS = ["rgb", "hsl"];
115
119
  var LONG_PRESS_MS = 500;
116
120
  /** How far a touch can move and still count as held still (CSS pixels) */
117
121
  var LONG_PRESS_SLOP = 10;
118
- /** Translation keys of the palette file error messages */
119
- var PALETTE_FILE_ERROR_KEYS = {
120
- [PALETTE_FILE_ERRORS.incompatible]: "paintCanvas.palette.fileIncompatible",
121
- [PALETTE_FILE_ERRORS.read]: "paintCanvas.palette.fileReadFailed"
122
+ /** Whether a saved colour is in one of the user's palettes (PaintCanvas's can't be changed) */
123
+ var isEditable = ({ palette }) => palette !== DEFAULT_PALETTE_ID;
124
+ /** Actions in a palette's menu */
125
+ var PALETTE_ACTIONS = [
126
+ "rename",
127
+ "export",
128
+ "delete"
129
+ ];
130
+ /** Translation keys of the messages shown when importing a palette file fails, by reason */
131
+ var IMPORT_ERROR_KEYS = {
132
+ invalid: "paintCanvas.palette.fileIncompatible",
133
+ unsupported: "paintCanvas.palette.fileUnsupported"
122
134
  };
135
+ /**
136
+ * The translation key of the message shown when importing a palette file fails.
137
+ */
138
+ var importErrorKey = (error) => error instanceof AssetFileError && IMPORT_ERROR_KEYS[error.reason] || "paintCanvas.palette.fileReadFailed";
123
139
  /** The dialog's choice that deletes the colour */
124
140
  var DELETE_CHOICE = "delete";
125
141
  /**
@@ -128,11 +144,14 @@ var DELETE_CHOICE = "delete";
128
144
  var clampWhole = (value, max) => Math.round(Math.min(max, Math.max(0, value)));
129
145
  /**
130
146
  * The colour palette menu: the foreground and background colours (the selected one is configured), its hex value, RGB
131
- * and HSL sliders, the saved colours, and buttons to import and export them as a JSON file.
147
+ * and HSL sliders, the colour palettes (the user's own above the one PaintCanvas comes with), and buttons to make a
148
+ * palette and to import and export the user's palettes as a JSON file.
132
149
  *
133
150
  * - Pressing the foreground or background colour selects it.
134
- * - Pressing a saved colour sets the selected colour; [+] saves the selected colour.
135
- * - A saved colour is deleted, after asking, by clicking it with Alt/Option held (the cursor shows an "X"), from its
151
+ * - Pressing a saved colour sets the selected colour (changing it afterwards doesn't change the palette); a palette's [+]
152
+ * saves the selected colour in it. PaintCanvas's palette can't be changed.
153
+ * - Each of the user's palettes has a ⋮ menu to rename, export or delete it (deleting asks first).
154
+ * - A colour in one of the user's palettes is deleted, after asking, by clicking it with Alt/Option held (the cursor shows an "X"), from its
136
155
  * menu (right-click, a long press on touch screens, or the context menu key), or with Delete or Backspace. Arrow keys,
137
156
  * Home and End move between saved colours.
138
157
  * - Escape closes it (closes the canvas's open panel).
@@ -158,6 +177,10 @@ var PcPanelColours = class extends PcPanel {
158
177
  unsafeCSS(PILLS_CSS),
159
178
  unsafeCSS(style_default)
160
179
  ];
180
+ /** Asks the user to confirm, in a dialog (replaceable for tests) */
181
+ confirm = (options) => askConfirm(this.renderRoot, options);
182
+ /** Asks the user for text, in a dialog (replaceable for tests) */
183
+ prompt = (options) => askText(this.renderRoot, options);
161
184
  /** Saves exported files (replaceable for tests) */
162
185
  saveFile = (blob, name) => saveFile(blob, name, void 0, JSON_FILE_TYPE);
163
186
  /** The HSL values shown, kept while they give the selected colour (so hue stays put for greys) */
@@ -166,13 +189,20 @@ var PcPanelColours = class extends PcPanel {
166
189
  s: 0,
167
190
  l: 0
168
191
  };
169
- /** The saved colour that is reached with Tab (the others with arrow keys) */
170
- focusIndex = 0;
192
+ /** The saved colour that is reached with Tab in its palette (the others with arrow keys; other palettes' first) */
193
+ focused = {
194
+ palette: DEFAULT_PALETTE_ID,
195
+ index: 0
196
+ };
171
197
  /** A touch being held on a saved colour, until it opens its menu */
172
198
  press;
173
199
  /** Whether a long press opened a menu, so the click that follows is ignored */
174
200
  longPressed = false;
175
- changes = new ChangeController(this, () => [this.app, this.app?.palette]);
201
+ changes = new ChangeController(this, () => [
202
+ this.app,
203
+ this.app?.palette,
204
+ this.app?.palettes
205
+ ]);
176
206
  onKey = (event) => {
177
207
  if (event.key === "Alt") this.toggleAttribute("deleting", event.type === "keydown");
178
208
  };
@@ -203,8 +233,36 @@ var PcPanelColours = class extends PcPanel {
203
233
  super.willUpdate(changed);
204
234
  if (!this.app) return;
205
235
  if (hslToHex(this.hsl) !== this.colour) this.hsl = hexToHsl(this.colour);
206
- const count = this.app.palette.colours.length;
207
- this.focusIndex = Math.max(0, Math.min(this.focusIndex, count - 1));
236
+ const shown = this.shown().find(({ id }) => id === this.focused.palette);
237
+ this.focused = shown ? {
238
+ palette: shown.id,
239
+ index: Math.max(0, Math.min(this.focused.index, shown.colours.length - 1))
240
+ } : {
241
+ palette: DEFAULT_PALETTE_ID,
242
+ index: 0
243
+ };
244
+ }
245
+ /** The palettes shown: the user's own, then the one PaintCanvas comes with (named after the app) */
246
+ shown() {
247
+ const app = this.app;
248
+ return [...app.palettes.items.map((palette) => ({
249
+ ...palette,
250
+ own: true
251
+ })), {
252
+ id: DEFAULT_PALETTE_ID,
253
+ name: app.names.app,
254
+ colours: app.palette.colours,
255
+ own: false
256
+ }];
257
+ }
258
+ /** A palette's colours */
259
+ coloursOf(palette) {
260
+ const app = this.app;
261
+ return palette === "paintcanvas" ? app.palette.colours : app.palettes.get(palette)?.colours ?? [];
262
+ }
263
+ /** The saved colour at a position */
264
+ colourAt({ palette, index }) {
265
+ return this.coloursOf(palette)[index];
208
266
  }
209
267
  /** Set the selected colour */
210
268
  setColour(colour) {
@@ -272,33 +330,34 @@ var PcPanelColours = class extends PcPanel {
272
330
  this.setColour(hslToHex(this.hsl));
273
331
  this.requestUpdate();
274
332
  }
275
- /** Set the selected colour from a saved colour, or ask to delete it with Alt/Option held */
276
- onSwatchClick(event, index) {
277
- this.focusIndex = index;
333
+ /** Set the selected colour from a saved colour, or ask to delete one of the user's with Alt/Option held */
334
+ onSwatchClick(event, swatch) {
335
+ this.focused = swatch;
278
336
  if (this.longPressed) {
279
337
  this.longPressed = false;
280
338
  return;
281
339
  }
282
- if (event.altKey) {
283
- this.confirming = index;
340
+ if (event.altKey && isEditable(swatch)) {
341
+ this.confirming = swatch;
284
342
  return;
285
343
  }
286
- this.setColour(this.app?.palette.colours[index]);
344
+ this.setColour(this.colourAt(swatch));
287
345
  }
288
- /** Open a saved colour's menu where it was right-clicked */
289
- onSwatchContextMenu(event, index) {
346
+ /** Open the menu of a colour in one of the user's palettes where it was right-clicked */
347
+ onSwatchContextMenu(event, swatch) {
348
+ if (!isEditable(swatch)) return;
290
349
  event.preventDefault();
291
- this.focusIndex = index;
350
+ this.focused = swatch;
292
351
  this.menu = {
293
- index,
352
+ ...swatch,
294
353
  x: event.clientX,
295
354
  y: event.clientY
296
355
  };
297
356
  }
298
357
  /** Start waiting for a touch held on a saved colour to open its menu */
299
- onSwatchPointerDown(event, index) {
358
+ onSwatchPointerDown(event, swatch) {
300
359
  this.longPressed = false;
301
- if (event.pointerType !== "touch") return;
360
+ if (event.pointerType !== "touch" || !isEditable(swatch)) return;
302
361
  this.cancelPress();
303
362
  const { clientX: x, clientY: y } = event;
304
363
  this.press = {
@@ -307,9 +366,9 @@ var PcPanelColours = class extends PcPanel {
307
366
  timer: setTimeout(() => {
308
367
  this.press = void 0;
309
368
  this.longPressed = true;
310
- this.focusIndex = index;
369
+ this.focused = swatch;
311
370
  this.menu = {
312
- index,
371
+ ...swatch,
313
372
  x,
314
373
  y
315
374
  };
@@ -330,26 +389,28 @@ var PcPanelColours = class extends PcPanel {
330
389
  * Move between saved colours, ask to delete one, or open its menu, with the keyboard. Keys used here don't reach the
331
390
  * keyboard shortcuts (e.g. Delete doesn't also delete the masked image data).
332
391
  */
333
- onSwatchKeyDown(event, index) {
334
- if (event.key === "Delete" || event.key === "Backspace") {
392
+ onSwatchKeyDown(event, swatch) {
393
+ const { palette, index } = swatch;
394
+ const editable = isEditable(swatch);
395
+ if (editable && (event.key === "Delete" || event.key === "Backspace")) {
335
396
  event.preventDefault();
336
397
  event.stopPropagation();
337
- this.confirming = index;
398
+ this.confirming = swatch;
338
399
  return;
339
400
  }
340
- if (event.key === "ContextMenu" || event.shiftKey && event.key === "F10") {
401
+ if (editable && (event.key === "ContextMenu" || event.shiftKey && event.key === "F10")) {
341
402
  event.preventDefault();
342
403
  event.stopPropagation();
343
404
  const rect = event.currentTarget.getBoundingClientRect();
344
405
  this.menu = {
345
- index,
406
+ ...swatch,
346
407
  x: rect.left,
347
408
  y: rect.bottom
348
409
  };
349
410
  return;
350
411
  }
351
- const last = this.app.palette.colours.length - 1;
352
- const columns = this.columns();
412
+ const last = this.coloursOf(palette).length - 1;
413
+ const columns = this.columns(palette);
353
414
  const next = {
354
415
  ArrowRight: index + 1,
355
416
  ArrowLeft: index - 1,
@@ -361,34 +422,85 @@ var PcPanelColours = class extends PcPanel {
361
422
  if (next === void 0 || next < 0 || next > last) return;
362
423
  event.preventDefault();
363
424
  event.stopPropagation();
364
- this.focusSwatch(next);
425
+ this.focusSwatch({
426
+ palette,
427
+ index: next
428
+ });
365
429
  }
366
- /** How many saved colours fit in a row */
367
- columns() {
368
- const grid = this.renderRoot.querySelector(".swatches");
430
+ /** How many saved colours fit in a row of a palette */
431
+ columns(palette) {
432
+ const grid = this.renderRoot.querySelector(`.swatches[data-palette="${palette}"]`);
369
433
  const width = grid.querySelector(".swatch").offsetWidth;
370
434
  return width > 0 ? Math.max(1, Math.floor(grid.clientWidth / width)) : 1;
371
435
  }
372
436
  /** Focus a saved colour */
373
- async focusSwatch(index) {
374
- this.focusIndex = index;
437
+ async focusSwatch(swatch) {
438
+ this.focused = swatch;
375
439
  this.requestUpdate();
376
440
  await this.updateComplete;
377
- this.renderRoot.querySelector(`.swatch[data-index="${index}"]`)?.focus();
441
+ this.renderRoot.querySelector(`.swatches[data-palette="${swatch.palette}"] .swatch[data-index="${swatch.index}"]`)?.focus();
378
442
  }
379
- /** Save the selected colour */
380
- onAdd() {
381
- this.app?.palette.add(this.colour);
443
+ /** Save the selected colour in one of the user's palettes */
444
+ onAdd(palette) {
445
+ this.app?.palettes.addColour(palette, this.colour);
382
446
  }
383
447
  /** Delete the saved colour that was asked about, if confirmed */
384
448
  onConfirm(event) {
385
449
  event.stopPropagation();
386
- const index = this.confirming;
450
+ const swatch = this.confirming;
387
451
  this.confirming = void 0;
388
- const { palette } = this.app;
389
- if (event.detail.value === DELETE_CHOICE) palette.remove(index);
390
- const count = palette.colours.length;
391
- if (count > 0) this.focusSwatch(Math.min(index, count - 1));
452
+ const app = this.app;
453
+ if (event.detail.value === DELETE_CHOICE) app.palettes.removeColour(swatch.palette, swatch.index);
454
+ const count = this.coloursOf(swatch.palette).length;
455
+ if (count > 0) this.focusSwatch({
456
+ palette: swatch.palette,
457
+ index: Math.min(swatch.index, count - 1)
458
+ });
459
+ }
460
+ /**
461
+ * Make a palette, asking for its name. The user is its author (see `PaintCanvas.author`).
462
+ */
463
+ async createPalette() {
464
+ const app = this.app;
465
+ if (!app) return void 0;
466
+ const name = await this.prompt({
467
+ heading: this.t("paintCanvas.palette.newPaletteHeading"),
468
+ label: this.t("paintCanvas.palette.name"),
469
+ value: this.t("paintCanvas.palette.defaultName"),
470
+ confirm: this.t("paintCanvas.common.save"),
471
+ cancel: this.t("paintCanvas.common.cancel")
472
+ });
473
+ return name === null ? void 0 : app.palettes.create(name, [], app.author);
474
+ }
475
+ /**
476
+ * Do one of a palette's menu actions: rename it (asking for a name), export it, or delete it (asking first).
477
+ */
478
+ async actOnPalette(id, action) {
479
+ const app = this.app;
480
+ const palette = app?.palettes.get(id);
481
+ if (!app || !palette) return;
482
+ if (action === "export") {
483
+ await this.exportFile([palette]);
484
+ return;
485
+ }
486
+ if (action === "rename") {
487
+ const name = await this.prompt({
488
+ heading: this.t("paintCanvas.palette.renameHeading"),
489
+ label: this.t("paintCanvas.palette.name"),
490
+ value: palette.name,
491
+ confirm: this.t("paintCanvas.common.save"),
492
+ cancel: this.t("paintCanvas.common.cancel")
493
+ });
494
+ if (name !== null) app.palettes.rename(id, name);
495
+ return;
496
+ }
497
+ if (await this.confirm({
498
+ heading: this.t("paintCanvas.palette.deletePaletteHeading"),
499
+ message: this.t("paintCanvas.palette.deletePaletteConfirm", { name: palette.name }),
500
+ confirm: this.t("paintCanvas.common.delete"),
501
+ cancel: this.t("paintCanvas.common.cancel"),
502
+ cancelling: true
503
+ })) app.palettes.remove(id);
392
504
  }
393
505
  /** Close the palette with Escape (unless a dialog or menu is open, which closes first) */
394
506
  onKeyDown(event) {
@@ -398,51 +510,40 @@ var PcPanelColours = class extends PcPanel {
398
510
  this.app?.closePanel();
399
511
  }
400
512
  /**
401
- * Import the colours in a colour palette file, adding them after the saved ones (leaving out ones already saved).
513
+ * Import the palettes in a colour palette file as the user's own, replacing ones with the same IDs.
402
514
  */
403
515
  async importFile(file) {
404
516
  const app = this.app;
405
517
  if (!app) return;
406
- let text;
407
- try {
408
- text = await file.text();
409
- } catch {
410
- this.toastError(PALETTE_FILE_ERRORS.read);
411
- return;
412
- }
413
518
  try {
414
- const colours = parsePaletteFile(text, [app.names.paletteFileFormat, PALETTE_FILE_FORMAT]);
415
- const count = app.palette.merge(colours);
519
+ const palettes = colourPaletteFile.parse(await file.text());
520
+ const count = app.palettes.merge(palettes);
416
521
  emit(this, "pc-toast", {
417
522
  message: this.t("paintCanvas.palette.imported", { count }),
418
523
  kind: "success"
419
524
  });
420
525
  } catch (error) {
421
- this.toastError(error instanceof PaletteFileError ? error.message : PALETTE_FILE_ERRORS.read);
526
+ emit(this, "pc-toast", {
527
+ message: this.t(importErrorKey(error)),
528
+ kind: "error"
529
+ });
422
530
  }
423
531
  }
424
532
  /**
425
- * Export the saved colours as a colour palette file.
533
+ * Export palettes (the user's own by default) as a colour palette file, or say there are none.
426
534
  */
427
- async exportFile() {
535
+ async exportFile(palettes = this.app?.palettes.items ?? []) {
428
536
  const app = this.app;
429
537
  if (!app) return;
430
538
  try {
431
- await this.saveFile(new Blob([exportPaletteFile(app.palette.colours, app.names.paletteFileFormat)], { type: "application/json" }), paletteFileName(void 0, app.names.filePrefix));
432
- } catch {
539
+ await this.saveFile(new Blob([colourPaletteFile.write(palettes)], { type: "application/json" }), colourPaletteFile.fileName(void 0, app.names.filePrefix));
540
+ } catch (error) {
433
541
  emit(this, "pc-toast", {
434
- message: this.t("paintCanvas.palette.exportFailed"),
542
+ message: this.t(error instanceof AssetFileError && error.reason === "empty" ? "paintCanvas.palette.nothingToExport" : "paintCanvas.palette.exportFailed"),
435
543
  kind: "error"
436
544
  });
437
545
  }
438
546
  }
439
- /** Show a palette file error (one of `PALETTE_FILE_ERRORS`) in the chosen language */
440
- toastError(message) {
441
- emit(this, "pc-toast", {
442
- message: this.t(PALETTE_FILE_ERROR_KEYS[message]),
443
- kind: "error"
444
- });
445
- }
446
547
  /** The colour model tabs and the selected model's sliders */
447
548
  renderChannels() {
448
549
  const rgb = hexToRgb(this.colour);
@@ -499,44 +600,71 @@ var PcPanelColours = class extends PcPanel {
499
600
  </div>
500
601
  </div>`;
501
602
  }
502
- /** The saved colours and the [+] button */
503
- renderSwatches(app) {
504
- const colours = app.palette.colours;
603
+ /** A palette: its name (and the ⋮ menu of the user's own), its colours and its [+] button */
604
+ renderPalette(palette) {
605
+ const { colours, id } = palette;
505
606
  const canAdd = !colours.includes(this.colour) && colours.length < 1e3;
506
- return html`<div
507
- class="swatches"
508
- role="group"
509
- aria-label=${this.t("paintCanvas.palette.saved")}
510
- >
511
- ${colours.map((colour, index) => html`<button
607
+ const focused = this.focused.palette === id ? this.focused.index : 0;
608
+ const add = this.t("paintCanvas.palette.add", {
609
+ colour: this.colour,
610
+ name: palette.name
611
+ });
612
+ const menu = PALETTE_ACTIONS.map((action) => ({
613
+ value: action,
614
+ label: this.t(`paintCanvas.palette.${action}Palette`),
615
+ danger: action === "delete"
616
+ }));
617
+ return html`<section class="palette-group" data-palette=${id}>
618
+ <div class="palette-heading">
619
+ <h3 id="palette-${id}">${palette.name}</h3>
620
+ ${palette.own ? html`<pc-context-menu
621
+ label=${this.t("paintCanvas.palette.paletteActions", { name: palette.name })}
622
+ .items=${menu}
623
+ @pc-select=${(event) => void this.actOnPalette(id, event.detail.value)}
624
+ ></pc-context-menu>` : nothing}
625
+ </div>
626
+ <div
627
+ class="swatches"
628
+ data-palette=${id}
629
+ role="group"
630
+ aria-labelledby="palette-${id}"
631
+ >
632
+ ${colours.map((colour, index) => {
633
+ const swatch = {
634
+ palette: id,
635
+ index
636
+ };
637
+ return html`<button
512
638
  type="button"
513
639
  class="swatch"
514
640
  data-index=${index}
515
641
  style="--colour: ${colour}"
516
642
  title=${colour}
517
643
  aria-label=${colour}
518
- aria-haspopup="menu"
644
+ aria-haspopup=${palette.own ? "menu" : nothing}
519
645
  aria-pressed=${colour === this.colour}
520
- tabindex=${index === this.focusIndex ? 0 : -1}
521
- @click=${(e) => this.onSwatchClick(e, index)}
522
- @contextmenu=${(e) => this.onSwatchContextMenu(e, index)}
523
- @pointerdown=${(e) => this.onSwatchPointerDown(e, index)}
646
+ tabindex=${index === focused ? 0 : -1}
647
+ @click=${(e) => this.onSwatchClick(e, swatch)}
648
+ @contextmenu=${(e) => this.onSwatchContextMenu(e, swatch)}
649
+ @pointerdown=${(e) => this.onSwatchPointerDown(e, swatch)}
524
650
  @pointermove=${this.onSwatchPointerMove}
525
651
  @pointerup=${this.cancelPress}
526
652
  @pointercancel=${this.cancelPress}
527
- @keydown=${(e) => this.onSwatchKeyDown(e, index)}
528
- ></button>`)}
529
- <button
530
- type="button"
531
- class="add"
532
- title=${this.t("paintCanvas.palette.add", { colour: this.colour })}
533
- aria-label=${this.t("paintCanvas.palette.add", { colour: this.colour })}
534
- ?disabled=${!canAdd}
535
- @click=${this.onAdd}
536
- >
537
- +
538
- </button>
539
- </div>`;
653
+ @keydown=${(e) => this.onSwatchKeyDown(e, swatch)}
654
+ ></button>`;
655
+ })}
656
+ ${palette.own ? html`<button
657
+ type="button"
658
+ class="add"
659
+ title=${add}
660
+ aria-label=${add}
661
+ ?disabled=${!canAdd}
662
+ @click=${() => this.onAdd(id)}
663
+ >
664
+ +
665
+ </button>` : nothing}
666
+ </div>
667
+ </section>`;
540
668
  }
541
669
  /** The saved colour's menu, while open */
542
670
  renderMenu() {
@@ -551,12 +679,18 @@ var PcPanelColours = class extends PcPanel {
551
679
  open
552
680
  .x=${menu.x}
553
681
  .y=${menu.y}
554
- label=${this.t("paintCanvas.palette.colourActions", { colour: this.app.palette.colours[menu.index] })}
682
+ label=${this.t("paintCanvas.palette.colourActions", { colour: this.colourAt(menu) })}
555
683
  .items=${items}
556
- @pc-select=${() => this.confirming = menu.index}
684
+ @pc-select=${() => this.confirming = {
685
+ palette: menu.palette,
686
+ index: menu.index
687
+ }}
557
688
  @pc-close=${() => {
558
689
  this.menu = void 0;
559
- if (this.confirming === void 0) this.focusSwatch(menu.index);
690
+ if (this.confirming === void 0) this.focusSwatch({
691
+ palette: menu.palette,
692
+ index: menu.index
693
+ });
560
694
  }}
561
695
  ></pc-menu>`;
562
696
  }
@@ -609,8 +743,21 @@ var PcPanelColours = class extends PcPanel {
609
743
  </div>
610
744
  ${this.renderChannels()}
611
745
  </div>
612
- ${this.renderSwatches(app)}
746
+ <div
747
+ class="palettes"
748
+ role="group"
749
+ aria-label=${this.t("paintCanvas.palette.saved")}
750
+ >
751
+ ${this.shown().map((palette) => this.renderPalette(palette))}
752
+ </div>
613
753
  <div class="actions">
754
+ <button
755
+ type="button"
756
+ class="pill-s new-palette"
757
+ @click=${() => void this.createPalette()}
758
+ >
759
+ ${this.t("paintCanvas.palette.newPalette")}
760
+ </button>
614
761
  <button
615
762
  type="button"
616
763
  class="pill-s import"
@@ -1,3 +1,3 @@
1
1
  // src/ui/elements/PcPanelColours/style.css?inline
2
- var style_default = ".palette {\n display: flex;\n flex-direction: column;\n gap: var(--pc-space-2);\n}\n\n/* The colours and their hex value beside the colour model's sliders */\n.top {\n display: flex;\n gap: var(--pc-space-2);\n}\n.current {\n display: flex;\n flex: none;\n flex-direction: column;\n width: 76px;\n border: 1px solid var(--pc-border);\n}\n.current pc-colour-pair {\n --pc-colour-pair-size: 76px;\n}\n.hex {\n box-sizing: border-box;\n width: 100%;\n min-height: var(--pc-control-size);\n padding: var(--pc-space-1);\n border: 0;\n border-top: 1px solid var(--pc-border);\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: var(--pc-font-size-small);\n text-align: center;\n font-variant-numeric: tabular-nums;\n}\n.hex[aria-invalid=\"true\"] {\n outline: 2px solid var(--pc-dismiss-text);\n outline-offset: -2px;\n}\n.hex:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n\n.channels {\n display: flex;\n flex: 1;\n flex-direction: column;\n min-width: 0;\n border: 1px solid var(--pc-border);\n}\n[role=\"tablist\"] {\n display: flex;\n}\n[role=\"tab\"] {\n flex: 1;\n min-height: 28px;\n border: 0;\n border-bottom: 1px solid var(--pc-border);\n background: var(--pc-surface-active);\n color: var(--pc-text);\n font: inherit;\n cursor: pointer;\n}\n[role=\"tab\"] + [role=\"tab\"] {\n border-left: 1px solid var(--pc-border);\n}\n[role=\"tab\"][aria-selected=\"true\"] {\n border-bottom-color: transparent;\n background: var(--pc-surface);\n font-weight: bold;\n}\n[role=\"tab\"]:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n.sliders {\n display: grid;\n flex: 1;\n grid-template-columns: auto minmax(0, 1fr) 3.6em;\n gap: var(--pc-space-1);\n align-items: center;\n padding: var(--pc-space-1) var(--pc-space-2);\n background: var(--pc-surface);\n}\n.letter {\n font-weight: bold;\n}\n/* Each slider's track shows the colours its values give (--track, set per slider), under a round white handle with a\n dark outline that shows on any colour */\n.sliders input[type=\"range\"] {\n width: 100%;\n min-width: 0;\n height: 20px;\n margin: 0;\n background: transparent;\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n}\n.sliders input[type=\"range\"]::-webkit-slider-runnable-track {\n height: 10px;\n border: 1px solid var(--pc-border);\n border-radius: var(--pc-radius-pill);\n background: var(--track);\n}\n.sliders input[type=\"range\"]::-moz-range-track {\n height: 10px;\n border: 1px solid var(--pc-border);\n border-radius: var(--pc-radius-pill);\n background: var(--track);\n}\n.sliders input[type=\"range\"]::-webkit-slider-thumb {\n width: 16px;\n height: 16px;\n margin-top: -4px;\n border: 2px solid #000000;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 0 0 1px #ffffff;\n -webkit-appearance: none;\n}\n.sliders input[type=\"range\"]::-moz-range-thumb {\n box-sizing: border-box;\n width: 16px;\n height: 16px;\n border: 2px solid #000000;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 0 0 1px #ffffff;\n}\n.sliders input[type=\"number\"] {\n box-sizing: border-box;\n width: 100%;\n padding: 2px var(--pc-space-1);\n border: 1px solid var(--pc-border);\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: var(--pc-font-size-small);\n font-variant-numeric: tabular-nums;\n}\n.sliders input:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: 1px;\n}\n\n/* Saved colours: square swatches in a grid that scrolls */\n.swatches {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(32px, 1fr));\n align-content: start;\n min-height: 64px;\n max-height: 132px;\n overflow-y: auto;\n border: 1px solid var(--pc-border);\n background: var(--pc-bar);\n}\n.swatch,\n.add {\n aspect-ratio: 1;\n min-width: 32px;\n padding: 0;\n border: 0;\n border-right: 1px solid var(--pc-border);\n border-bottom: 1px solid var(--pc-border);\n cursor: pointer;\n}\n.swatch {\n background: var(--colour);\n /* Long presses open the colour's menu instead of the browser's */\n -webkit-touch-callout: none;\n touch-action: manipulation;\n}\n.swatch[aria-pressed=\"true\"] {\n box-shadow:\n inset 0 0 0 2px #ffffff,\n inset 0 0 0 3px #000000;\n}\n:host([deleting]) .swatch {\n cursor: var(--pc-delete-cursor);\n}\n.add {\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: 20px;\n font-weight: bold;\n}\n.add:hover:not(:disabled) {\n background: var(--pc-surface-hover);\n}\n.add:disabled {\n color: var(--pc-disabled);\n cursor: default;\n}\n.swatch:focus-visible,\n.add:focus-visible {\n position: relative;\n z-index: 1;\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n\n.actions {\n display: flex;\n gap: var(--pc-space-2);\n}\n/* Import and Export share the row */\n.actions button.pill-s {\n flex: 1;\n}\n";
2
+ var style_default = ".palette {\n display: flex;\n flex-direction: column;\n gap: var(--pc-space-2);\n}\n\n/* The colours and their hex value beside the colour model's sliders */\n.top {\n display: flex;\n gap: var(--pc-space-2);\n}\n.current {\n display: flex;\n flex: none;\n flex-direction: column;\n width: 76px;\n border: 1px solid var(--pc-border);\n}\n.current pc-colour-pair {\n --pc-colour-pair-size: 76px;\n}\n.hex {\n box-sizing: border-box;\n width: 100%;\n min-height: var(--pc-control-size);\n padding: var(--pc-space-1);\n border: 0;\n border-top: 1px solid var(--pc-border);\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: var(--pc-font-size-small);\n text-align: center;\n font-variant-numeric: tabular-nums;\n}\n.hex[aria-invalid=\"true\"] {\n outline: 2px solid var(--pc-dismiss-text);\n outline-offset: -2px;\n}\n.hex:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n\n.channels {\n display: flex;\n flex: 1;\n flex-direction: column;\n min-width: 0;\n border: 1px solid var(--pc-border);\n}\n[role=\"tablist\"] {\n display: flex;\n}\n[role=\"tab\"] {\n flex: 1;\n min-height: 28px;\n border: 0;\n border-bottom: 1px solid var(--pc-border);\n background: var(--pc-surface-active);\n color: var(--pc-text);\n font: inherit;\n cursor: pointer;\n}\n[role=\"tab\"] + [role=\"tab\"] {\n border-left: 1px solid var(--pc-border);\n}\n[role=\"tab\"][aria-selected=\"true\"] {\n border-bottom-color: transparent;\n background: var(--pc-surface);\n font-weight: bold;\n}\n[role=\"tab\"]:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n.sliders {\n display: grid;\n flex: 1;\n grid-template-columns: auto minmax(0, 1fr) 3.6em;\n gap: var(--pc-space-1);\n align-items: center;\n padding: var(--pc-space-1) var(--pc-space-2);\n background: var(--pc-surface);\n}\n.letter {\n font-weight: bold;\n}\n/* Each slider's track shows the colours its values give (--track, set per slider), under a round white handle with a\n dark outline that shows on any colour */\n.sliders input[type=\"range\"] {\n width: 100%;\n min-width: 0;\n height: 20px;\n margin: 0;\n background: transparent;\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n}\n.sliders input[type=\"range\"]::-webkit-slider-runnable-track {\n height: 10px;\n border: 1px solid var(--pc-border);\n border-radius: var(--pc-radius-pill);\n background: var(--track);\n}\n.sliders input[type=\"range\"]::-moz-range-track {\n height: 10px;\n border: 1px solid var(--pc-border);\n border-radius: var(--pc-radius-pill);\n background: var(--track);\n}\n.sliders input[type=\"range\"]::-webkit-slider-thumb {\n width: 16px;\n height: 16px;\n margin-top: -4px;\n border: 2px solid #000000;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 0 0 1px #ffffff;\n -webkit-appearance: none;\n}\n.sliders input[type=\"range\"]::-moz-range-thumb {\n box-sizing: border-box;\n width: 16px;\n height: 16px;\n border: 2px solid #000000;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 0 0 1px #ffffff;\n}\n.sliders input[type=\"number\"] {\n box-sizing: border-box;\n width: 100%;\n padding: 2px var(--pc-space-1);\n border: 1px solid var(--pc-border);\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: var(--pc-font-size-small);\n font-variant-numeric: tabular-nums;\n}\n.sliders input:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: 1px;\n}\n\n/* The palettes, the user's own above PaintCanvas's, scrolling together */\n.palettes {\n display: flex;\n flex-direction: column;\n gap: var(--pc-space-2);\n max-height: 264px;\n overflow-y: auto;\n}\n.palette-heading {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--pc-space-1);\n min-height: var(--pc-control-size);\n}\n.palette-heading h3 {\n overflow: hidden;\n margin: 0;\n font-size: var(--pc-font-size-small);\n font-weight: 600;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* A palette's colours: square swatches in a grid */\n.swatches {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(32px, 1fr));\n align-content: start;\n border: 1px solid var(--pc-border);\n background: var(--pc-bar);\n}\n.swatch,\n.add {\n aspect-ratio: 1;\n min-width: 32px;\n padding: 0;\n border: 0;\n border-right: 1px solid var(--pc-border);\n border-bottom: 1px solid var(--pc-border);\n cursor: pointer;\n}\n.swatch {\n background: var(--colour);\n /* Long presses open the colour's menu instead of the browser's */\n -webkit-touch-callout: none;\n touch-action: manipulation;\n}\n.swatch[aria-pressed=\"true\"] {\n box-shadow:\n inset 0 0 0 2px #ffffff,\n inset 0 0 0 3px #000000;\n}\n:host([deleting]) .swatch {\n cursor: var(--pc-delete-cursor);\n}\n.add {\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: 20px;\n font-weight: bold;\n}\n.add:hover:not(:disabled) {\n background: var(--pc-surface-hover);\n}\n.add:disabled {\n color: var(--pc-disabled);\n cursor: default;\n}\n.swatch:focus-visible,\n.add:focus-visible {\n position: relative;\n z-index: 1;\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n\n.actions {\n display: flex;\n flex-wrap: wrap;\n gap: var(--pc-space-2);\n}\n/* New palette has a row of its own, above Import and Export */\n.actions button.pill-s {\n flex: 1;\n}\n.actions button.new-palette {\n flex-basis: 100%;\n}\n";
3
3
  export { style_default as default };
@@ -94,9 +94,10 @@ var PcSliderRange = class extends PcElement {
94
94
  border-radius: var(--pc-radius-pill);
95
95
  transform: translateY(-50%);
96
96
  }
97
+ /* The whole range: the same area the fill covers from min to max */
97
98
  .rail {
98
- left: calc(var(--pc-range-thumb) / -2);
99
- right: calc(var(--pc-range-thumb) / -2);
99
+ left: 0;
100
+ right: 0;
100
101
  background: var(--pc-border);
101
102
  }
102
103
  .fill {
@@ -16,10 +16,12 @@ export declare const COLOUR_INTERVAL_MS = 100;
16
16
  export declare const formatBytes: (bytes: number, locale?: string) => string;
17
17
  /**
18
18
  * A line of small text about the canvas: its size; while the drawing is kept in the browser, how much its layer
19
- * images take there; the zoom level; and the colour under the pointer (with a swatch; a dash when the pointer isn't
20
- * over the canvas). The colour follows the pointer at most every `COLOUR_INTERVAL_MS`, and isn't read while a stroke is
21
- * drawn (the next pointer move after it reads it). It's not announced to screen readers as it changes, as it changes
22
- * with every pointer move.
19
+ * images take there; the zoom level; the colour under the pointer (with a swatch; a dash when the pointer isn't over the
20
+ * canvas); and, on the right, while the drawing is being saved to a remote store (see
21
+ * `PaintCanvas.useRemoteDocumentStore`), a progress bar, or that saving there failed. The colour follows the pointer
22
+ * at most every `COLOUR_INTERVAL_MS`, and isn't read while a stroke is drawn (the next pointer move after it reads it).
23
+ * It's not announced to screen readers as it changes, as it changes with every pointer move; saving to the remote store
24
+ * is.
23
25
  *
24
26
  * @example
25
27
  * html`<pc-stats .app=${app}></pc-stats>`
@@ -66,4 +68,6 @@ export declare class PcStats extends PcElement {
66
68
  */
67
69
  private follow;
68
70
  render(): import("lit").TemplateResult<1> | typeof nothing;
71
+ /** Saving to the remote store, while under way or after it failed */
72
+ private renderRemote;
69
73
  }