@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
@@ -0,0 +1,650 @@
1
+ import style_default from "./style.js";
2
+ import { PALETTE_FILE_FORMAT } from "../../../lib/palette/schema.js";
3
+ import { ICON_CONTROLS_CSS, NO_TEXT_SELECTION_CSS, PILLS_CSS } from "../../styles/index.js";
4
+ import { defineElement, emit } from "../../base/index.js";
5
+ import { PcPanel } from "../PcPanel/index.js";
6
+ import ChangeController from "../../ChangeController/index.js";
7
+ import "../PcColourPair/index.js";
8
+ import "../PcMenu/index.js";
9
+ import { CANCEL_CHOICE } from "../PcDialog/index.js";
10
+ 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 { JSON_FILE_TYPE, saveFile } from "../../../lib/rendering/screenshot/index.js";
13
+ import { html, nothing, unsafeCSS } from "lit";
14
+ import { live } from "lit/directives/live.js";
15
+ // src/ui/elements/PcPanelColours/index.ts
16
+ /** Each colour model's channels */
17
+ var CHANNELS = {
18
+ rgb: [
19
+ {
20
+ key: "r",
21
+ name: "paintCanvas.palette.red",
22
+ max: 255
23
+ },
24
+ {
25
+ key: "g",
26
+ name: "paintCanvas.palette.green",
27
+ max: 255
28
+ },
29
+ {
30
+ key: "b",
31
+ name: "paintCanvas.palette.blue",
32
+ max: 255
33
+ }
34
+ ],
35
+ hsl: [
36
+ {
37
+ key: "h",
38
+ name: "paintCanvas.palette.hue",
39
+ max: 360
40
+ },
41
+ {
42
+ key: "s",
43
+ name: "paintCanvas.palette.saturation",
44
+ max: 100
45
+ },
46
+ {
47
+ key: "l",
48
+ name: "paintCanvas.palette.lightness",
49
+ max: 100
50
+ }
51
+ ]
52
+ };
53
+ /** Hues the hue slider's track passes through, every 60 degrees from red back to red */
54
+ var HUE_STOPS = [
55
+ 0,
56
+ 60,
57
+ 120,
58
+ 180,
59
+ 240,
60
+ 300,
61
+ 360
62
+ ].map((h) => hslToHex({
63
+ h,
64
+ s: 100,
65
+ l: 50
66
+ }));
67
+ /**
68
+ * The CSS gradient a channel's slider track shows: the colours its values give, from the lowest to the highest, keeping
69
+ * the colour's other values. Red, green and blue go from none to all of that channel (e.g. red from `rgb(0, g, b)` to
70
+ * `rgb(255, g, b)`), saturation from grey to full colour, and lightness from black through the colour to white. Hue goes
71
+ * through every hue at full saturation, so it never shows as grey.
72
+ *
73
+ * @example
74
+ * channelGradient("r", { r: 0, g: 128, b: 255 }, { h: 210, s: 100, l: 50 }); // "linear-gradient(to right, #0080ff, #ff80ff)"
75
+ * channelGradient("s", { r: 77, g: 179, b: 77 }, { h: 120, s: 40, l: 50 }); // "linear-gradient(to right, #808080, #00ff00)"
76
+ */
77
+ var channelGradient = (key, rgb, hsl) => {
78
+ const { h, s, l } = hsl;
79
+ /** The colour with no and all of an RGB channel */
80
+ const rgbStops = (channel) => [rgbToHex({
81
+ ...rgb,
82
+ [channel]: 0
83
+ }), rgbToHex({
84
+ ...rgb,
85
+ [channel]: 255
86
+ })];
87
+ return `linear-gradient(to right, ${{
88
+ r: rgbStops("r"),
89
+ g: rgbStops("g"),
90
+ b: rgbStops("b"),
91
+ h: HUE_STOPS,
92
+ s: [hslToHex({
93
+ h,
94
+ s: 0,
95
+ l
96
+ }), hslToHex({
97
+ h,
98
+ s: 100,
99
+ l
100
+ })],
101
+ l: [
102
+ 0,
103
+ 50,
104
+ 100
105
+ ].map((lightness) => hslToHex({
106
+ h,
107
+ s,
108
+ l: lightness
109
+ }))
110
+ }[key].join(", ")})`;
111
+ };
112
+ /** The colour models in tab order */
113
+ var MODELS = ["rgb", "hsl"];
114
+ /** How long a touch is held on a saved colour to open its menu (milliseconds) */
115
+ var LONG_PRESS_MS = 500;
116
+ /** How far a touch can move and still count as held still (CSS pixels) */
117
+ 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
+ };
123
+ /** The dialog's choice that deletes the colour */
124
+ var DELETE_CHOICE = "delete";
125
+ /**
126
+ * Keep a number within a range, rounded to a whole number.
127
+ */
128
+ var clampWhole = (value, max) => Math.round(Math.min(max, Math.max(0, value)));
129
+ /**
130
+ * 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.
132
+ *
133
+ * - 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
136
+ * menu (right-click, a long press on touch screens, or the context menu key), or with Delete or Backspace. Arrow keys,
137
+ * Home and End move between saved colours.
138
+ * - Escape closes it (closes the canvas's open panel).
139
+ *
140
+ * Fires `pc-toast` after importing, or when importing or exporting fails.
141
+ *
142
+ * @example
143
+ * html`<pc-panel-colours .app=${app}></pc-panel-colours>`
144
+ */
145
+ var PcPanelColours = class extends PcPanel {
146
+ static properties = {
147
+ ...PcPanel.properties,
148
+ app: { attribute: false },
149
+ model: { type: String },
150
+ hexDraft: { state: true },
151
+ menu: { state: true },
152
+ confirming: { state: true }
153
+ };
154
+ static styles = [
155
+ ...PcPanel.styles,
156
+ unsafeCSS(NO_TEXT_SELECTION_CSS),
157
+ unsafeCSS(ICON_CONTROLS_CSS),
158
+ unsafeCSS(PILLS_CSS),
159
+ unsafeCSS(style_default)
160
+ ];
161
+ /** Saves exported files (replaceable for tests) */
162
+ saveFile = (blob, name) => saveFile(blob, name, void 0, JSON_FILE_TYPE);
163
+ /** The HSL values shown, kept while they give the selected colour (so hue stays put for greys) */
164
+ hsl = {
165
+ h: 0,
166
+ s: 0,
167
+ l: 0
168
+ };
169
+ /** The saved colour that is reached with Tab (the others with arrow keys) */
170
+ focusIndex = 0;
171
+ /** A touch being held on a saved colour, until it opens its menu */
172
+ press;
173
+ /** Whether a long press opened a menu, so the click that follows is ignored */
174
+ longPressed = false;
175
+ changes = new ChangeController(this, () => [this.app, this.app?.palette]);
176
+ onKey = (event) => {
177
+ if (event.key === "Alt") this.toggleAttribute("deleting", event.type === "keydown");
178
+ };
179
+ onBlur = () => this.removeAttribute("deleting");
180
+ constructor() {
181
+ super();
182
+ this.model = "rgb";
183
+ }
184
+ connectedCallback() {
185
+ super.connectedCallback();
186
+ window.addEventListener("keydown", this.onKey);
187
+ window.addEventListener("keyup", this.onKey);
188
+ window.addEventListener("blur", this.onBlur);
189
+ }
190
+ disconnectedCallback() {
191
+ super.disconnectedCallback();
192
+ window.removeEventListener("keydown", this.onKey);
193
+ window.removeEventListener("keyup", this.onKey);
194
+ window.removeEventListener("blur", this.onBlur);
195
+ this.cancelPress();
196
+ }
197
+ /** The selected colour's value, e.g. `#000000` */
198
+ get colour() {
199
+ const app = this.app;
200
+ return app.colours[app.selectedColour];
201
+ }
202
+ willUpdate(changed) {
203
+ super.willUpdate(changed);
204
+ if (!this.app) return;
205
+ 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));
208
+ }
209
+ /** Set the selected colour */
210
+ setColour(colour) {
211
+ const app = this.app;
212
+ app.setColour(colour, app.selectedColour);
213
+ }
214
+ /** Select the pressed colour */
215
+ onColourClick(event) {
216
+ this.app?.selectColour(event.detail.slot);
217
+ }
218
+ /** Follow the hex value as it's typed, setting the colour once it's a full 6-digit value */
219
+ onHexInput(event) {
220
+ const value = event.target.value;
221
+ const hex = normaliseHex(value);
222
+ if (hex && value.replace(/[\s#]/g, "").length === 6) {
223
+ this.hexDraft = void 0;
224
+ this.setColour(hex);
225
+ return;
226
+ }
227
+ this.hexDraft = value;
228
+ }
229
+ /** Set the colour from a typed hex value (including 3 digits) when done, or show the colour again if it isn't one */
230
+ onHexChange(event) {
231
+ const hex = normaliseHex(event.target.value);
232
+ this.hexDraft = void 0;
233
+ if (hex) this.setColour(hex);
234
+ }
235
+ /** Switch colour model with the arrow keys, Home and End, as tabs do */
236
+ onTabKeyDown(event) {
237
+ const index = MODELS.indexOf(this.model);
238
+ const last = MODELS.length - 1;
239
+ const next = {
240
+ ArrowRight: index === last ? 0 : index + 1,
241
+ ArrowLeft: index === 0 ? last : index - 1,
242
+ Home: 0,
243
+ End: last
244
+ }[event.key];
245
+ if (next === void 0) return;
246
+ event.preventDefault();
247
+ this.selectModel(MODELS[next], true);
248
+ }
249
+ /** Show a colour model's sliders */
250
+ async selectModel(model, focus = false) {
251
+ this.model = model;
252
+ if (!focus) return;
253
+ await this.updateComplete;
254
+ this.renderRoot.querySelector(`[role="tab"][data-model="${model}"]`)?.focus();
255
+ }
256
+ /** Change one channel of the colour model shown */
257
+ onChannel(channel, event) {
258
+ const input = event.target;
259
+ if (input.value === "") return;
260
+ const value = clampWhole(input.valueAsNumber, channel.max);
261
+ if (this.model === "rgb") {
262
+ this.setColour(rgbToHex({
263
+ ...hexToRgb(this.colour),
264
+ [channel.key]: value
265
+ }));
266
+ return;
267
+ }
268
+ this.hsl = {
269
+ ...this.hsl,
270
+ [channel.key]: value
271
+ };
272
+ this.setColour(hslToHex(this.hsl));
273
+ this.requestUpdate();
274
+ }
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;
278
+ if (this.longPressed) {
279
+ this.longPressed = false;
280
+ return;
281
+ }
282
+ if (event.altKey) {
283
+ this.confirming = index;
284
+ return;
285
+ }
286
+ this.setColour(this.app?.palette.colours[index]);
287
+ }
288
+ /** Open a saved colour's menu where it was right-clicked */
289
+ onSwatchContextMenu(event, index) {
290
+ event.preventDefault();
291
+ this.focusIndex = index;
292
+ this.menu = {
293
+ index,
294
+ x: event.clientX,
295
+ y: event.clientY
296
+ };
297
+ }
298
+ /** Start waiting for a touch held on a saved colour to open its menu */
299
+ onSwatchPointerDown(event, index) {
300
+ this.longPressed = false;
301
+ if (event.pointerType !== "touch") return;
302
+ this.cancelPress();
303
+ const { clientX: x, clientY: y } = event;
304
+ this.press = {
305
+ x,
306
+ y,
307
+ timer: setTimeout(() => {
308
+ this.press = void 0;
309
+ this.longPressed = true;
310
+ this.focusIndex = index;
311
+ this.menu = {
312
+ index,
313
+ x,
314
+ y
315
+ };
316
+ }, 500)
317
+ };
318
+ }
319
+ /** Stop waiting for a long press when the touch moves away */
320
+ onSwatchPointerMove(event) {
321
+ const press = this.press;
322
+ if (press && Math.hypot(event.clientX - press.x, event.clientY - press.y) > LONG_PRESS_SLOP) this.cancelPress();
323
+ }
324
+ /** Stop waiting for a long press */
325
+ cancelPress() {
326
+ clearTimeout(this.press?.timer);
327
+ this.press = void 0;
328
+ }
329
+ /**
330
+ * Move between saved colours, ask to delete one, or open its menu, with the keyboard. Keys used here don't reach the
331
+ * keyboard shortcuts (e.g. Delete doesn't also delete the masked image data).
332
+ */
333
+ onSwatchKeyDown(event, index) {
334
+ if (event.key === "Delete" || event.key === "Backspace") {
335
+ event.preventDefault();
336
+ event.stopPropagation();
337
+ this.confirming = index;
338
+ return;
339
+ }
340
+ if (event.key === "ContextMenu" || event.shiftKey && event.key === "F10") {
341
+ event.preventDefault();
342
+ event.stopPropagation();
343
+ const rect = event.currentTarget.getBoundingClientRect();
344
+ this.menu = {
345
+ index,
346
+ x: rect.left,
347
+ y: rect.bottom
348
+ };
349
+ return;
350
+ }
351
+ const last = this.app.palette.colours.length - 1;
352
+ const columns = this.columns();
353
+ const next = {
354
+ ArrowRight: index + 1,
355
+ ArrowLeft: index - 1,
356
+ ArrowDown: index + columns,
357
+ ArrowUp: index - columns,
358
+ Home: 0,
359
+ End: last
360
+ }[event.key];
361
+ if (next === void 0 || next < 0 || next > last) return;
362
+ event.preventDefault();
363
+ event.stopPropagation();
364
+ this.focusSwatch(next);
365
+ }
366
+ /** How many saved colours fit in a row */
367
+ columns() {
368
+ const grid = this.renderRoot.querySelector(".swatches");
369
+ const width = grid.querySelector(".swatch").offsetWidth;
370
+ return width > 0 ? Math.max(1, Math.floor(grid.clientWidth / width)) : 1;
371
+ }
372
+ /** Focus a saved colour */
373
+ async focusSwatch(index) {
374
+ this.focusIndex = index;
375
+ this.requestUpdate();
376
+ await this.updateComplete;
377
+ this.renderRoot.querySelector(`.swatch[data-index="${index}"]`)?.focus();
378
+ }
379
+ /** Save the selected colour */
380
+ onAdd() {
381
+ this.app?.palette.add(this.colour);
382
+ }
383
+ /** Delete the saved colour that was asked about, if confirmed */
384
+ onConfirm(event) {
385
+ event.stopPropagation();
386
+ const index = this.confirming;
387
+ 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));
392
+ }
393
+ /** Close the palette with Escape (unless a dialog or menu is open, which closes first) */
394
+ onKeyDown(event) {
395
+ if (event.key !== "Escape" || this.confirming !== void 0 || this.menu !== void 0) return;
396
+ event.preventDefault();
397
+ event.stopPropagation();
398
+ this.app?.closePanel();
399
+ }
400
+ /**
401
+ * Import the colours in a colour palette file, adding them after the saved ones (leaving out ones already saved).
402
+ */
403
+ async importFile(file) {
404
+ const app = this.app;
405
+ 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
+ try {
414
+ const colours = parsePaletteFile(text, [app.names.paletteFileFormat, PALETTE_FILE_FORMAT]);
415
+ const count = app.palette.merge(colours);
416
+ emit(this, "pc-toast", {
417
+ message: this.t("paintCanvas.palette.imported", { count }),
418
+ kind: "success"
419
+ });
420
+ } catch (error) {
421
+ this.toastError(error instanceof PaletteFileError ? error.message : PALETTE_FILE_ERRORS.read);
422
+ }
423
+ }
424
+ /**
425
+ * Export the saved colours as a colour palette file.
426
+ */
427
+ async exportFile() {
428
+ const app = this.app;
429
+ if (!app) return;
430
+ 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 {
433
+ emit(this, "pc-toast", {
434
+ message: this.t("paintCanvas.palette.exportFailed"),
435
+ kind: "error"
436
+ });
437
+ }
438
+ }
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
+ /** The colour model tabs and the selected model's sliders */
447
+ renderChannels() {
448
+ const rgb = hexToRgb(this.colour);
449
+ const values = this.model === "rgb" ? rgb : this.hsl;
450
+ return html`<div class="channels">
451
+ <div
452
+ role="tablist"
453
+ aria-label=${this.t("paintCanvas.palette.model")}
454
+ @keydown=${this.onTabKeyDown}
455
+ >
456
+ ${MODELS.map((model) => html`<button
457
+ type="button"
458
+ role="tab"
459
+ id="tab-${model}"
460
+ data-model=${model}
461
+ aria-selected=${this.model === model}
462
+ aria-controls="sliders"
463
+ tabindex=${this.model === model ? 0 : -1}
464
+ @click=${() => void this.selectModel(model)}
465
+ >
466
+ ${this.t(`paintCanvas.palette.${model}`)}
467
+ </button>`)}
468
+ </div>
469
+ <div
470
+ id="sliders"
471
+ class="sliders"
472
+ role="tabpanel"
473
+ aria-labelledby="tab-${this.model}"
474
+ >
475
+ ${CHANNELS[this.model].map((channel) => {
476
+ const value = String(values[channel.key]);
477
+ const name = this.t(channel.name);
478
+ return html`<span class="letter" aria-hidden="true"
479
+ >${channel.key.toUpperCase()}</span
480
+ ><input
481
+ type="range"
482
+ data-channel=${channel.key}
483
+ style="--track: ${channelGradient(channel.key, rgb, this.hsl)}"
484
+ min="0"
485
+ max=${channel.max}
486
+ aria-label=${name}
487
+ .value=${live(value)}
488
+ @input=${(e) => this.onChannel(channel, e)}
489
+ /><input
490
+ type="number"
491
+ data-channel=${channel.key}
492
+ min="0"
493
+ max=${channel.max}
494
+ aria-label=${name}
495
+ .value=${live(value)}
496
+ @change=${(e) => this.onChannel(channel, e)}
497
+ />`;
498
+ })}
499
+ </div>
500
+ </div>`;
501
+ }
502
+ /** The saved colours and the [+] button */
503
+ renderSwatches(app) {
504
+ const colours = app.palette.colours;
505
+ 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
512
+ type="button"
513
+ class="swatch"
514
+ data-index=${index}
515
+ style="--colour: ${colour}"
516
+ title=${colour}
517
+ aria-label=${colour}
518
+ aria-haspopup="menu"
519
+ 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)}
524
+ @pointermove=${this.onSwatchPointerMove}
525
+ @pointerup=${this.cancelPress}
526
+ @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>`;
540
+ }
541
+ /** The saved colour's menu, while open */
542
+ renderMenu() {
543
+ const menu = this.menu;
544
+ if (!menu) return nothing;
545
+ const items = [{
546
+ value: DELETE_CHOICE,
547
+ label: this.t("paintCanvas.palette.deleteColour"),
548
+ danger: true
549
+ }];
550
+ return html`<pc-menu
551
+ open
552
+ .x=${menu.x}
553
+ .y=${menu.y}
554
+ label=${this.t("paintCanvas.palette.colourActions", { colour: this.app.palette.colours[menu.index] })}
555
+ .items=${items}
556
+ @pc-select=${() => this.confirming = menu.index}
557
+ @pc-close=${() => {
558
+ this.menu = void 0;
559
+ if (this.confirming === void 0) this.focusSwatch(menu.index);
560
+ }}
561
+ ></pc-menu>`;
562
+ }
563
+ /** The dialog asking before deleting a saved colour, while asking */
564
+ renderConfirmation() {
565
+ if (this.confirming === void 0) return nothing;
566
+ const choices = [{
567
+ value: DELETE_CHOICE,
568
+ label: this.t("paintCanvas.palette.deleteConfirm"),
569
+ cancelling: true
570
+ }, {
571
+ value: CANCEL_CHOICE,
572
+ label: this.t("paintCanvas.common.dismiss"),
573
+ variant: "dismiss"
574
+ }];
575
+ return html`<pc-dialog
576
+ heading=${this.t("paintCanvas.palette.deleteColour")}
577
+ message=${this.t("paintCanvas.palette.deleteMessage")}
578
+ .choices=${choices}
579
+ @pc-choose=${this.onConfirm}
580
+ ></pc-dialog>`;
581
+ }
582
+ get defaultIcon() {
583
+ return "";
584
+ }
585
+ get defaultLabel() {
586
+ return this.t("paintCanvas.palette.label");
587
+ }
588
+ renderBody() {
589
+ const app = this.app;
590
+ if (!app) return nothing;
591
+ return html`<div class="palette" @keydown=${this.onKeyDown}>
592
+ <div class="top">
593
+ <div class="current">
594
+ <pc-colour-pair
595
+ .app=${app}
596
+ @pc-colour-click=${this.onColourClick}
597
+ ></pc-colour-pair>
598
+ <input
599
+ class="hex"
600
+ spellcheck="false"
601
+ autocomplete="off"
602
+ maxlength="7"
603
+ aria-label=${this.t("paintCanvas.palette.hex")}
604
+ aria-invalid=${this.hexDraft !== void 0 && !normaliseHex(this.hexDraft)}
605
+ .value=${live(this.hexDraft ?? this.colour.toUpperCase())}
606
+ @input=${this.onHexInput}
607
+ @change=${this.onHexChange}
608
+ />
609
+ </div>
610
+ ${this.renderChannels()}
611
+ </div>
612
+ ${this.renderSwatches(app)}
613
+ <div class="actions">
614
+ <button
615
+ type="button"
616
+ class="pill import"
617
+ @click=${() => this.renderRoot.querySelector(".import-file")?.click()}
618
+ >
619
+ ${this.t("paintCanvas.palette.import")}
620
+ </button>
621
+ <button
622
+ type="button"
623
+ class="pill export"
624
+ @click=${() => void this.exportFile()}
625
+ >
626
+ ${this.t("paintCanvas.palette.export")}
627
+ </button>
628
+ <!-- Chosen through "Import" -->
629
+ <label class="visually-hidden">
630
+ ${this.t("paintCanvas.palette.importFile")}
631
+ <input
632
+ type="file"
633
+ class="import-file"
634
+ tabindex="-1"
635
+ accept="application/json,.json"
636
+ @change=${(e) => {
637
+ const input = e.target;
638
+ const file = input.files?.[0];
639
+ input.value = "";
640
+ if (file) this.importFile(file);
641
+ }}
642
+ />
643
+ </label>
644
+ </div>
645
+ </div>
646
+ ${this.renderMenu()} ${this.renderConfirmation()}`;
647
+ }
648
+ };
649
+ defineElement("pc-panel-colours", PcPanelColours);
650
+ export { LONG_PRESS_MS, PcPanelColours, channelGradient };
@@ -0,0 +1,3 @@
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/* Same size as the brush menu's Import and Export */\n.actions button.pill {\n flex: 1;\n padding: 4px 8px;\n}\n";
3
+ export { style_default as default };
@@ -1,4 +1,5 @@
1
- import { PcElement } from "../../base";
1
+ import { nothing } from "lit";
2
+ import { PcPanel } from "../PcPanel";
2
3
  import { type LayerDropAction } from "../PcLayerItem";
3
4
  import "../PcLayerItem";
4
5
  import type PaintCanvas from "../../../lib/classes/PaintCanvas";
@@ -52,9 +53,9 @@ export declare const dropSlot: (rows: readonly {
52
53
  * highlighted for touch). Escape cancels the drag. Moves and duplicates are announced to screen readers.
53
54
  *
54
55
  * @example
55
- * html`<pc-layers-menu .app=${app}></pc-layers-menu>`
56
+ * html`<pc-panel-layers .app=${app}></pc-panel-layers>`
56
57
  */
57
- export declare class PcLayersMenu extends PcElement {
58
+ export declare class PcPanelLayers extends PcPanel {
58
59
  static properties: {
59
60
  app: {
60
61
  attribute: boolean;
@@ -65,6 +66,16 @@ export declare class PcLayersMenu extends PcElement {
65
66
  announcement: {
66
67
  state: boolean;
67
68
  };
69
+ icon: {
70
+ type: StringConstructor;
71
+ };
72
+ label: {
73
+ type: StringConstructor;
74
+ };
75
+ badge: {
76
+ type: BooleanConstructor;
77
+ reflect: boolean;
78
+ };
68
79
  translations: {
69
80
  attribute: boolean;
70
81
  };
@@ -103,6 +114,8 @@ export declare class PcLayersMenu extends PcElement {
103
114
  * layer keeps its row (and a drag its handle) as the ghost moves.
104
115
  */
105
116
  private renderRows;
106
- render(): import("lit").TemplateResult<1>;
117
+ protected get defaultIcon(): string;
118
+ protected get defaultLabel(): string;
119
+ protected renderBody(): import("lit").TemplateResult<1> | typeof nothing;
107
120
  }
108
121
  export {};