@lvl99dotcom/paintcanvas 0.3.1 → 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 (92) 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 +141 -41
  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 +121 -171
  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/PcCanvas/index.d.ts +1 -1
  51. package/dist/ui/elements/PcCanvas/index.js +7 -5
  52. package/dist/ui/elements/PcColourPair/index.d.ts +43 -0
  53. package/dist/ui/elements/PcColourPair/index.js +125 -0
  54. package/dist/ui/elements/PcContextMenu/index.d.ts +70 -33
  55. package/dist/ui/elements/PcContextMenu/index.js +128 -97
  56. package/dist/ui/elements/PcLayerItem/index.d.ts +13 -34
  57. package/dist/ui/elements/PcLayerItem/index.js +39 -91
  58. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  59. package/dist/ui/elements/PcMenu/index.d.ts +93 -0
  60. package/dist/ui/elements/PcMenu/index.js +204 -0
  61. package/dist/ui/elements/PcPane/index.d.ts +118 -0
  62. package/dist/ui/elements/PcPane/index.js +357 -0
  63. package/dist/ui/elements/PcPanel/index.d.ts +26 -8
  64. package/dist/ui/elements/PcPanel/index.js +76 -8
  65. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.d.ts +20 -6
  66. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.js +18 -14
  67. package/dist/ui/elements/PcPanelBrushes/style.js +3 -0
  68. package/dist/ui/elements/PcPanelColours/index.d.ts +171 -0
  69. package/dist/ui/elements/PcPanelColours/index.js +650 -0
  70. package/dist/ui/elements/PcPanelColours/style.js +3 -0
  71. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.d.ts +17 -4
  72. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.js +17 -10
  73. package/dist/ui/elements/PcPanelLayers/style.js +3 -0
  74. package/dist/ui/elements/PcShortcutSettings/index.d.ts +69 -0
  75. package/dist/ui/elements/PcShortcutSettings/index.js +273 -0
  76. package/dist/ui/elements/PcToolSettings/index.js +2 -1
  77. package/dist/ui/elements/PcToolbar/index.d.ts +47 -12
  78. package/dist/ui/elements/PcToolbar/index.js +77 -56
  79. package/dist/ui/elements/PcToolbar/style.js +1 -1
  80. package/dist/ui/index.d.ts +7 -1
  81. package/dist/ui/index.js +8 -2
  82. package/dist/ui/tokens/index.js +3 -1
  83. package/locales/en-GB.json +87 -1
  84. package/locales/en-US.json +87 -1
  85. package/locales/fr.json +87 -1
  86. package/locales/ja.json +87 -1
  87. package/locales/ko.json +87 -1
  88. package/locales/zh-Hans.json +87 -1
  89. package/locales/zh-Hant.json +87 -1
  90. package/package.json +1 -1
  91. package/dist/ui/elements/PcBrushMenu/style.js +0 -3
  92. package/dist/ui/elements/PcLayersMenu/style.js +0 -3
@@ -4,23 +4,28 @@ import ChangeController from "../../ChangeController/index.js";
4
4
  import PaintCanvas, { Panel } from "../../../lib/classes/PaintCanvas/index.js";
5
5
  import KeyboardShortcuts, { SHORTCUT_EVENT } from "../../../lib/classes/KeyboardShortcuts/index.js";
6
6
  import { isTouchDevice } from "../../../lib/classes/InputSettings/index.js";
7
- import IndexedDbLayerStore from "../../../lib/storage/index.js";
7
+ import IndexedDbLayerStore, { IndexedDbSettingsStore } from "../../../lib/storage/index.js";
8
+ import SettingsAutosave, { SETTINGS_AUTOSAVE_ERROR_EVENT } from "../../../lib/classes/SettingsAutosave/index.js";
8
9
  import { AUTOSAVE_ERROR_EVENT, AutosaveState } from "../../../lib/classes/LayerAutosave/index.js";
9
10
  import { isOwnAuthor } from "../../../lib/brushes/authors/index.js";
10
11
  import { renderScreenshot, saveFile, screenshotName } from "../../../lib/rendering/screenshot/index.js";
11
12
  import { DEFAULT_SIDE_MODES, STATE_EVENT, parseSideModes, sideModeFor } from "../../layout/index.js";
12
13
  import { hasToolSettings } from "../PcToolSettings/index.js";
13
14
  import "../PcPanel/index.js";
15
+ import { PANE_COLLAPSE_EVENT, PcPane } from "../PcPane/index.js";
14
16
  import "../PcToolbar/index.js";
15
17
  import "../PcCanvas/index.js";
16
18
  import "../PcCanvasMap/index.js";
17
- import "../PcBrushMenu/index.js";
18
- import "../PcLayersMenu/index.js";
19
+ import "../PcPanelBrushes/index.js";
20
+ import "../PcPanelLayers/index.js";
19
21
  import "../PcBrushConfig/index.js";
20
22
  import "../PcStats/index.js";
23
+ import "../PcPanelColours/index.js";
21
24
  import "../PcDialog/index.js";
22
25
  import { html, nothing, unsafeCSS } from "lit";
23
26
  // src/ui/elements/PaintCanvas/index.ts
27
+ /** The sides */
28
+ var SIDES = ["left", "right"];
24
29
  /** No space kept clear */
25
30
  var NO_INSETS = {
26
31
  top: 0,
@@ -30,29 +35,28 @@ var NO_INSETS = {
30
35
  };
31
36
  /** The choice that takes the drawing over from another window */
32
37
  var TAKE_OVER_CHOICE = "take-over";
33
- /** Elements that can take keyboard focus */
34
- var FOCUSABLE = "button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])";
35
38
  /**
36
39
  * A ready-made painting app: the drawing elements arranged around the canvas, sized to its container (not the
37
40
  * window), with slots to add or replace content:
38
41
  *
39
42
  * - `header`: across the top (empty by default)
40
- * - `left`: `<pc-toolbar>` by default
41
- * - `top`: `<pc-tool-settings>` (for tools that have settings) by default
42
- * - `right`: empty by default (e.g. a `<pc-panel>` with a layers menu or chat)
43
+ * - `left`: `<pc-toolbar>` (a pane) by default
44
+ * - `top`: a horizontal `<pc-pane>` with `<pc-tool-settings>` (for tools that have settings) by default
45
+ * - `right`: a `<pc-pane>` with `<pc-panel-layers>` by default
43
46
  * - `center`: `<pc-canvas>` by default
44
47
  * - `overlay`: drawn over the canvas; `<pc-canvas-map>` by default
45
48
  * - `bottom`: under the canvas (empty by default)
46
49
  * - `footer`: across the bottom; `<pc-stats>` by default
47
- * - `menu`: the app menu the toolbar's menu button opens (the button shows only with a menu)
50
+ * - `menu`: the app menu the toolbar's menu button opens, shown in a `<pc-panel>` (the button shows only with a menu)
48
51
  *
49
52
  * Content given for a slot replaces its default; an empty element clears it. Empty slots take no space. Hiding the
50
- * interface (Tab key) leaves only the canvas and overlay. A `<pc-layers-menu>` given in a side (e.g. in a `<pc-panel>`)
51
- * replaces the toolbar's layers menu button.
53
+ * interface (Tab key) leaves only the canvas and overlay. While a side has a `<pc-panel-layers>` (as the right side
54
+ * does by default), the toolbar has no layers menu button.
52
55
  *
53
56
  * Each side has a mode, chosen by the layout's width from `left-modes` and `right-modes` (default `"docked,
54
- * rail@600"`): `docked` beside the canvas; `rail`, a column of buttons (one per `<pc-panel>`) that open each panel over
55
- * the canvas; or `floating`, the side over the canvas as a palette that collapses to a button.
57
+ * rail@600"`), which the layout gives the side's `<pc-pane>`s (see `PcPane`): `docked` beside the canvas; `rail`, a
58
+ * column of buttons (one per panel) that open each panel over the canvas; or `floating`, the pane over the canvas as a
59
+ * box that collapses to a button. Give side content in a `<pc-pane>` to have it follow the side's mode.
56
60
  *
57
61
  * Reports its state for styling as attributes (`narrow`, `input="pen|touch|mouse"`, `ui-hidden`, `left-mode`,
58
62
  * `right-mode`) and as `pc-state-change` events (see `LayoutState`). Creates its own canvas unless given one, and
@@ -62,10 +66,16 @@ var FOCUSABLE = "button, [href], input, select, textarea, [tabindex]:not([tabind
62
66
  * reloading the page (see `LayerAutosave`). While another window is drawing on it, the layout is blocked by a message
63
67
  * that offers to draw here instead. Fires `pc-toast` when the drawing can't be loaded or saved.
64
68
  *
69
+ * With a `settings-store` name, the user's app settings (input settings, keyboard shortcuts, colour palette and
70
+ * colours) are kept in the browser under that name (see `SettingsAutosave`). Fires `pc-toast` when they can't be.
71
+ *
65
72
  * @example
66
73
  * html`<paint-canvas style="height: 600px"></paint-canvas>`
67
74
  * html`<paint-canvas .app=${canvas} right-modes="docked, rail@800">
68
- * <pc-panel slot="right" icon="layers" label="Layers"><pc-layers-menu .app=${canvas}></pc-layers-menu></pc-panel>
75
+ * <pc-pane slot="right">
76
+ * <pc-panel-layers .app=${canvas}></pc-panel-layers>
77
+ * <pc-panel icon="chat" label="Chat"><my-chat></my-chat></pc-panel>
78
+ * </pc-pane>
69
79
  * </paint-canvas>`
70
80
  */
71
81
  var PaintCanvasElement = class extends PcElement {
@@ -94,11 +104,13 @@ var PaintCanvasElement = class extends PcElement {
94
104
  },
95
105
  shortcuts: { type: Boolean },
96
106
  store: { type: String },
107
+ settingsStore: {
108
+ type: String,
109
+ attribute: "settings-store"
110
+ },
97
111
  width: { state: true },
98
112
  input: { state: true },
99
- configuring: { state: true },
100
- collapsed: { state: true },
101
- openPanel: { state: true }
113
+ configuring: { state: true }
102
114
  };
103
115
  static styles = [PcElement.styles, unsafeCSS(style_default)];
104
116
  /** Saves files (replaceable for tests) */
@@ -108,13 +120,10 @@ var PaintCanvasElement = class extends PcElement {
108
120
  ownAutosave;
109
121
  /** The autosave whose errors are shown */
110
122
  watchedAutosave;
123
+ /** Keeps the app settings under the `settings-store` name, if any */
124
+ ownSettingsAutosave;
111
125
  /** Whether drawing was blocked when shortcuts were last watched */
112
126
  blockedShortcuts = false;
113
- /** The panels in each side's slot */
114
- panels = {
115
- left: [],
116
- right: []
117
- };
118
127
  /** Whether each side's slot has a layers menu (the toolbar's isn't needed then) */
119
128
  sideLayers = {
120
129
  left: false,
@@ -130,12 +139,9 @@ var PaintCanvasElement = class extends PcElement {
130
139
  reported;
131
140
  /** Space the floating sides cover over the canvas */
132
141
  floatingInsets = NO_INSETS;
133
- /** The rail button that opened the open panel, to return focus to */
134
- opener;
135
142
  resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(([entry]) => {
136
143
  this.width = Math.round(entry?.contentRect.width ?? 0);
137
144
  }) : void 0;
138
- panelObserver = new MutationObserver(() => this.requestUpdate());
139
145
  onPointerDown = (event) => {
140
146
  const kind = event.pointerType;
141
147
  this.input = kind === "pen" || kind === "touch" ? kind : "mouse";
@@ -144,20 +150,15 @@ var PaintCanvasElement = class extends PcElement {
144
150
  message: this.t("paintCanvas.autosave.failed"),
145
151
  kind: "error"
146
152
  });
153
+ onSettingsAutosaveError = () => emit(this, "pc-toast", {
154
+ message: this.t("paintCanvas.settings.saveFailed"),
155
+ kind: "error"
156
+ });
147
157
  onShortcut = (event) => {
148
158
  if (event.detail.type === "save") this.saveScreenshot();
149
159
  };
150
- onDocumentPointerDown = (event) => {
151
- const panel = this.openPanel;
152
- if (!panel) return;
153
- const path = event.composedPath();
154
- if (!path.includes(panel) && !(this.opener && path.includes(this.opener))) this.closePanel(false);
155
- };
156
- onKeyDown = (event) => {
157
- if (event.key !== "Escape" || !this.openPanel) return;
158
- event.preventDefault();
159
- this.closePanel(true);
160
- };
160
+ onDocumentPointerDown = (event) => this.closePaletteOutside(event.composedPath());
161
+ onPaneCollapse = () => this.requestUpdate();
161
162
  constructor() {
162
163
  super();
163
164
  this.leftModes = DEFAULT_SIDE_MODES;
@@ -167,10 +168,6 @@ var PaintCanvasElement = class extends PcElement {
167
168
  this.shortcuts = true;
168
169
  this.width = 0;
169
170
  this.input = isTouchDevice() ? "touch" : "mouse";
170
- this.collapsed = {
171
- left: true,
172
- right: true
173
- };
174
171
  }
175
172
  /** Whether another window is drawing on the stored drawing, so this one is blocked */
176
173
  get blocked() {
@@ -195,36 +192,25 @@ var PaintCanvasElement = class extends PcElement {
195
192
  this.app ??= new PaintCanvas();
196
193
  super.connectedCallback();
197
194
  this.resizeObserver?.observe(this);
198
- this.panelObserver.observe(this, {
199
- subtree: true,
200
- attributes: true,
201
- attributeFilter: [
202
- "icon",
203
- "label",
204
- "badge"
205
- ]
206
- });
207
195
  this.addEventListener("pointerdown", this.onPointerDown, true);
208
- this.addEventListener("keydown", this.onKeyDown);
196
+ this.addEventListener(PANE_COLLAPSE_EVENT, this.onPaneCollapse);
209
197
  document.addEventListener("pointerdown", this.onDocumentPointerDown, true);
210
198
  this.watchShortcuts();
211
199
  }
212
200
  disconnectedCallback() {
213
201
  super.disconnectedCallback();
214
202
  this.resizeObserver?.disconnect();
215
- this.panelObserver.disconnect();
216
203
  this.removeEventListener("pointerdown", this.onPointerDown, true);
217
- this.removeEventListener("keydown", this.onKeyDown);
204
+ this.removeEventListener(PANE_COLLAPSE_EVENT, this.onPaneCollapse);
218
205
  document.removeEventListener("pointerdown", this.onDocumentPointerDown, true);
219
206
  this.unwatchShortcuts();
220
207
  }
221
208
  willUpdate(changed) {
222
209
  super.willUpdate(changed);
223
210
  if (changed.has("app") || changed.has("store")) this.useStore(changed.get("app"));
211
+ if (changed.has("app") || changed.has("settingsStore")) this.useSettingsStore();
224
212
  this.watchAutosaveErrors();
225
213
  if (this.isConnected && (changed.has("app") || changed.has("shortcuts") || this.blocked !== this.blockedShortcuts)) this.watchShortcuts();
226
- const panel = this.openPanel;
227
- if (panel && this.modeOf(this.sideOf(panel)) !== "rail") this.closePanel(false);
228
214
  }
229
215
  updated() {
230
216
  const state = this.state;
@@ -238,11 +224,25 @@ var PaintCanvasElement = class extends PcElement {
238
224
  this.reported = report;
239
225
  this.dispatchEvent(new CustomEvent(STATE_EVENT, { detail: state }));
240
226
  }
227
+ this.setPaneModes();
241
228
  this.measureFloatingSides();
242
229
  }
230
+ /** The panes in a side: the default one, or those given for it */
231
+ panesIn(side) {
232
+ const slot = this.renderRoot.querySelector(`slot[name="${side}"]`);
233
+ const given = slot.assignedElements();
234
+ return (given.length ? given : [...slot.children]).filter((element) => element instanceof PcPane);
235
+ }
236
+ /** Show each side's panes in the side's mode, facing the canvas */
237
+ setPaneModes() {
238
+ for (const side of SIDES) for (const pane of this.panesIn(side)) {
239
+ pane.mode = this.modeOf(side);
240
+ pane.edge = side;
241
+ }
242
+ }
243
243
  /** Keep the canvas clear of floating sides when fitting it */
244
244
  measureFloatingSides() {
245
- const width = (side) => this.renderRoot.querySelector(`.side.${side} .float:not(.collapsed)`)?.getBoundingClientRect().width ?? 0;
245
+ const width = (side) => this.panesIn(side).filter((pane) => pane.mode === "floating" && !pane.collapsed).reduce((widest, pane) => Math.max(widest, pane.getBoundingClientRect().width), 0);
246
246
  const insets = {
247
247
  ...NO_INSETS,
248
248
  left: width("left"),
@@ -267,6 +267,20 @@ var PaintCanvasElement = class extends PcElement {
267
267
  }
268
268
  if (app.autosave?.store.name !== name) this.ownAutosave = app.useLayerStore(new IndexedDbLayerStore(name));
269
269
  }
270
+ /** Keeps the app settings under the `settings-store` name, if any (e.g. to wait until they are restored with `ready`) */
271
+ get settingsAutosave() {
272
+ return this.ownSettingsAutosave;
273
+ }
274
+ /**
275
+ * Keep the canvas's app settings under the `settings-store` name, stopping the previous settings autosave (which
276
+ * saves any change waiting to be saved).
277
+ */
278
+ useSettingsStore() {
279
+ this.ownSettingsAutosave?.stop();
280
+ const name = this.settingsStore || void 0;
281
+ this.ownSettingsAutosave = name === void 0 ? void 0 : new SettingsAutosave(this.app, new IndexedDbSettingsStore(name));
282
+ this.ownSettingsAutosave?.addEventListener(SETTINGS_AUTOSAVE_ERROR_EVENT, this.onSettingsAutosaveError);
283
+ }
270
284
  /** Show errors from the canvas's current autosave */
271
285
  watchAutosaveErrors() {
272
286
  const autosave = this.app?.autosave;
@@ -339,40 +353,6 @@ var PaintCanvasElement = class extends PcElement {
339
353
  }
340
354
  };
341
355
  }
342
- /** Which side a panel is in */
343
- sideOf(panel) {
344
- return panel.slot === "left" ? "left" : "right";
345
- }
346
- /**
347
- * Open a panel from its rail button (closing any other), or close it if it's open. Focus moves into the panel.
348
- */
349
- togglePanel(panel, opener) {
350
- if (this.openPanel === panel) {
351
- this.closePanel(true);
352
- return;
353
- }
354
- if (this.openPanel) this.closePanel(false);
355
- this.openPanel = panel;
356
- this.opener = opener;
357
- panel.toggleAttribute("data-pc-open", true);
358
- const focusable = panel.querySelector(FOCUSABLE);
359
- if (focusable) focusable.focus();
360
- else {
361
- panel.tabIndex = -1;
362
- panel.focus();
363
- }
364
- }
365
- /**
366
- * Close the panel open from a rail, moving focus back to its rail button if asked.
367
- */
368
- closePanel(returnFocus) {
369
- const panel = this.openPanel;
370
- if (!panel) return;
371
- panel.removeAttribute("data-pc-open");
372
- this.openPanel = void 0;
373
- if (returnFocus) this.opener?.focus();
374
- this.opener = void 0;
375
- }
376
356
  /** Follow which slots have content given (their defaults aren't needed) */
377
357
  onSlotChange(event) {
378
358
  const slot = event.target;
@@ -388,16 +368,12 @@ var PaintCanvasElement = class extends PcElement {
388
368
  >${this.filled[name] ? nothing : fallback}</slot
389
369
  >`;
390
370
  }
391
- /** Follow the panels given for a side, and whether it has a layers menu */
371
+ /** Follow whether a side given content has a layers menu */
392
372
  onSideSlotChange(side, event) {
393
373
  const assigned = event.target.assignedElements();
394
- this.panels = {
395
- ...this.panels,
396
- [side]: assigned.filter((element) => element.localName === "pc-panel")
397
- };
398
374
  this.sideLayers = {
399
375
  ...this.sideLayers,
400
- [side]: assigned.some((element) => element.matches("pc-layers-menu, :has(pc-layers-menu)"))
376
+ [side]: assigned.some((element) => element.matches("pc-panel-layers, :has(pc-panel-layers)"))
401
377
  };
402
378
  this.onSlotChange(event);
403
379
  }
@@ -408,99 +384,70 @@ var PaintCanvasElement = class extends PcElement {
408
384
  }
409
385
  /** Whether a side has a layers menu, replacing the toolbar's */
410
386
  get layersInSide() {
411
- return this.sideLayers.left || this.sideLayers.right;
387
+ return this.sideLayers.left || this.sideLayers.right || !this.filled.right;
412
388
  }
413
- /** The toolbar shown on the left unless the implementer gives other content */
414
- renderToolbar(app) {
415
- return html`<pc-toolbar
416
- .app=${app}
417
- .menu=${this.hasMenu}
418
- .layers=${!this.layersInSide}
419
- .menuText=${this.menuText}
420
- .menuLabel=${this.menuLabel}
421
- ></pc-toolbar>`;
422
- }
423
- /** A side's rail buttons, one per panel */
424
- renderRail(side, label) {
425
- const panels = this.panels[side];
426
- if (panels.length === 0) return nothing;
427
- return html`<div
428
- class="rail"
429
- part="${side}-rail"
430
- role="toolbar"
431
- aria-orientation="vertical"
432
- aria-label=${label}
433
- >
434
- ${panels.map((panel) => html`<button
435
- type="button"
436
- title=${panel.label}
437
- aria-label=${panel.badge ? this.t("paintCanvas.layout.new", { name: panel.label }) : panel.label}
438
- aria-expanded=${String(this.openPanel === panel)}
439
- @click=${(event) => this.togglePanel(panel, event.currentTarget)}
440
- >
441
- <img src=${this.iconUrl(panel.icon)} alt="" />
442
- ${panel.badge ? html`<span class="badge"></span>` : nothing}
443
- </button>`)}
444
- </div>`;
445
- }
446
- /** A side, in its mode */
389
+ /** A side's default content: the toolbar on the left, and the layers on the right */
390
+ renderSideDefault(side, app, label) {
391
+ if (side === "left") return html`<pc-toolbar
392
+ .app=${app}
393
+ .menu=${this.hasMenu}
394
+ .layers=${!this.layersInSide}
395
+ .menuText=${this.menuText}
396
+ .menuLabel=${this.menuLabel}
397
+ label=${label}
398
+ ></pc-toolbar>`;
399
+ return html`<pc-pane label=${label}
400
+ ><pc-panel-layers slot="start" .app=${app}></pc-panel-layers
401
+ ></pc-pane>`;
402
+ }
403
+ /** A side: its panes, in its mode (set on them after rendering) */
447
404
  renderSide(side, app, blocked) {
448
- const mode = this.modeOf(side);
449
405
  const label = this.t(side === "left" ? "paintCanvas.layout.tools" : "paintCanvas.layout.panels");
450
- const slot = html`<slot
451
- name=${side}
452
- @slotchange=${(event) => this.onSideSlotChange(side, event)}
453
- >${side === "left" && !this.filled.left ? this.renderToolbar(app) : nothing}</slot
454
- >`;
455
- let content = slot;
456
- if (mode === "rail") content = side === "left" ? html`${slot}${this.renderRail(side, label)}` : html`${this.renderRail(side, label)}${slot}`;
457
- else if (mode === "floating") {
458
- const collapsed = this.collapsed[side];
459
- const arrows = side === "left" ? ["»", "«"] : ["«", "»"];
460
- content = html`<div
461
- class=${collapsed ? "float collapsed" : "float"}
462
- part="${side}-float"
463
- >
464
- <button
465
- type="button"
466
- class="collapse"
467
- aria-expanded=${String(!collapsed)}
468
- aria-label=${this.t(collapsed ? "paintCanvas.layout.show" : "paintCanvas.layout.hide", { name: label })}
469
- @click=${() => this.collapsed = {
470
- ...this.collapsed,
471
- [side]: !collapsed
472
- }}
473
- >
474
- ${collapsed ? arrows[0] : arrows[1]}
475
- </button>
476
- <div class="content" ?hidden=${collapsed}>${slot}</div>
477
- </div>`;
478
- }
479
406
  return html`<div
480
407
  class="side ${side}"
481
408
  part=${side}
482
- data-mode=${mode}
409
+ data-mode=${this.modeOf(side)}
483
410
  role="region"
484
411
  aria-label=${label}
485
412
  ?inert=${blocked}
486
413
  >
487
- ${content}
414
+ <slot
415
+ name=${side}
416
+ @slotchange=${(event) => this.onSideSlotChange(side, event)}
417
+ >${this.filled[side] ? nothing : this.renderSideDefault(side, app, label)}</slot
418
+ >
488
419
  </div>`;
489
420
  }
421
+ /**
422
+ * Close the colour palette on a press outside it and the colours that open it.
423
+ */
424
+ closePaletteOutside(path) {
425
+ const app = this.app;
426
+ if (app?.panel !== Panel.Palette) return;
427
+ if (!path.some((target) => target instanceof Element && (target.localName === "pc-panel-colours" || target.localName === "pc-colour-pair"))) app.closePanel();
428
+ }
490
429
  /** The menus the toolbar opens, floating over the canvas beside it */
491
430
  renderPanels(app) {
492
- const brushMenu = (kind) => html`<pc-brush-menu
431
+ const brushMenu = (kind) => html`<pc-panel-brushes
493
432
  class="panel top-panel"
494
433
  kind=${kind}
495
434
  .app=${app}
496
435
  @pc-new-preset=${() => this.newPreset(kind)}
497
- ></pc-brush-menu>`;
436
+ ></pc-panel-brushes>`;
498
437
  return html`${app.panel === Panel.Brush ? brushMenu("brush") : nothing}
499
438
  ${app.panel === Panel.Eraser ? brushMenu("eraser") : nothing}
500
- ${app.panel === Panel.Layers && !this.layersInSide ? html`<pc-layers-menu class="panel" .app=${app}></pc-layers-menu>` : nothing}
501
- <div class="panel menu-panel" ?open=${app.panel === Panel.Menu}>
439
+ ${app.panel === Panel.Layers && !this.layersInSide ? html`<pc-panel-layers class="panel" .app=${app}></pc-panel-layers>` : nothing}
440
+ ${app.panel === Panel.Palette ? html`<pc-panel-colours
441
+ class="panel top-panel"
442
+ .app=${app}
443
+ ></pc-panel-colours>` : nothing}
444
+ <pc-panel
445
+ class="panel menu-panel"
446
+ label=${this.menuLabel || this.t("paintCanvas.toolbar.menu")}
447
+ ?open=${app.panel === Panel.Menu}
448
+ >
502
449
  <slot name="menu" @slotchange=${this.onMenuSlotChange}></slot>
503
- </div>`;
450
+ </pc-panel>`;
504
451
  }
505
452
  /** The brush configuration window, while open */
506
453
  renderConfig(app) {
@@ -549,10 +496,13 @@ var PaintCanvasElement = class extends PcElement {
549
496
  aria-label=${this.t("paintCanvas.layout.toolSettings")}
550
497
  ?inert=${blocked}
551
498
  >
552
- ${this.slotWithDefault("top", showsToolSettings ? html`<pc-tool-settings
553
- .app=${app}
554
- @pc-configure-brush=${(event) => this.configure(event.detail)}
555
- ></pc-tool-settings>` : nothing)}
499
+ ${this.slotWithDefault("top", showsToolSettings ? html`<pc-pane orientation="horizontal" edge="top"
500
+ ><pc-tool-settings
501
+ slot="middle"
502
+ .app=${app}
503
+ @pc-configure-brush=${(event) => this.configure(event.detail)}
504
+ ></pc-tool-settings
505
+ ></pc-pane>` : nothing)}
556
506
  </div>
557
507
  <div
558
508
  class="center"
@@ -1,3 +1,3 @@
1
1
  // src/ui/elements/PaintCanvas/style.css?inline
2
- var style_default = "/* The layout: header across the top, footer across the bottom, sides left and right, and between them the tool\n settings, the canvas and the bottom bar. Empty areas take no space. */\n:host {\n position: relative;\n display: grid;\n grid-template:\n \"header header header\" auto\n \"left top right\" auto\n \"left center right\" 1fr\n \"left bottom right\" auto\n \"footer footer footer\" auto / auto 1fr auto;\n overflow: hidden;\n background: var(--pc-bar);\n font-family: var(--pc-font);\n font-size: var(--pc-font-size);\n}\nheader {\n grid-area: header;\n}\n.top {\n grid-area: top;\n min-width: 0;\n}\n.center {\n position: relative;\n grid-area: center;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n.bottom {\n grid-area: bottom;\n min-width: 0;\n}\nfooter {\n grid-area: footer;\n}\n.side {\n position: relative;\n z-index: 3;\n display: flex;\n min-height: 0;\n}\n.side.left {\n grid-area: left;\n}\n.side.right {\n grid-area: right;\n}\n\n/* Hiding the interface (Tab key) leaves only the canvas */\n:host([ui-hidden]) header,\n:host([ui-hidden]) .top,\n:host([ui-hidden]) .bottom,\n:host([ui-hidden]) footer,\n:host([ui-hidden]) .side {\n display: none;\n}\n\n/* The canvas fills the middle; things drawn over it go in the overlay */\n.center ::slotted([slot=\"center\"]),\n.center pc-canvas {\n position: absolute;\n inset: 0;\n}\n.overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n}\n.overlay > * {\n pointer-events: auto;\n}\npc-canvas-map {\n position: absolute;\n right: 20px;\n bottom: 20px;\n}\n\n/* Menus opened from the toolbar float over the canvas, beside the toolbar */\n.panel {\n position: absolute;\n z-index: 4;\n left: 0;\n bottom: 0;\n max-height: 100%;\n overflow: auto;\n}\n.panel.top-panel {\n top: 0;\n bottom: auto;\n}\n.menu-panel:not([open]) {\n display: none;\n}\n\n/* Rail: a column of buttons, one per panel; the open panel floats over the canvas beside it */\n.rail {\n display: flex;\n flex-direction: column;\n gap: var(--pc-space-1);\n padding: var(--pc-space-1);\n border-inline: 1px solid var(--pc-border);\n background: var(--pc-bar);\n}\n.rail button {\n position: relative;\n display: inline-flex;\n padding: var(--pc-space-1);\n border: 1px solid transparent;\n border-radius: var(--pc-radius-small);\n background: transparent;\n cursor: pointer;\n}\n.rail button:hover {\n background: var(--pc-surface-hover);\n}\n.rail button[aria-expanded=\"true\"] {\n border-color: var(--pc-accent);\n background: var(--pc-accent);\n}\n.rail button:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n}\n.rail img {\n width: var(--pc-icon-size);\n height: var(--pc-icon-size);\n filter: var(--pc-icon-filter);\n}\n.rail .badge {\n position: absolute;\n top: -2px;\n right: -2px;\n width: 10px;\n height: 10px;\n border: 2px solid var(--pc-bar);\n border-radius: 50%;\n background: var(--pc-red);\n}\n.side[data-mode=\"rail\"] ::slotted(pc-panel) {\n display: none;\n}\n.side[data-mode=\"rail\"] ::slotted(pc-panel[data-pc-open]) {\n position: absolute;\n z-index: 5;\n top: 0;\n bottom: 0;\n display: flex;\n width: var(--pc-floating-panel-width, min(360px, 85vw));\n background: var(--pc-surface-raised);\n box-shadow: var(--pc-shadow);\n}\n/* When narrow, a layers menu fills the panel opened from the rail (a floating side sizes itself to its content) */\n:host([narrow]) .side[data-mode=\"rail\"] ::slotted(pc-panel) {\n --pc-layers-menu-width: 100%;\n}\n.side.left[data-mode=\"rail\"] ::slotted(pc-panel[data-pc-open]) {\n left: 100%;\n}\n.side.right[data-mode=\"rail\"] ::slotted(pc-panel[data-pc-open]) {\n right: 100%;\n}\n\n/* Floating: the side floats over the canvas as a palette, and collapses to a button */\n.side[data-mode=\"floating\"] {\n /* Beside the canvas only, so it floats over the canvas and not the tool settings */\n grid-row: 3;\n width: 0;\n}\n.float {\n position: absolute;\n z-index: 5;\n top: var(--pc-space-2);\n bottom: var(--pc-space-2);\n display: flex;\n flex-direction: column;\n gap: var(--pc-space-1);\n max-width: var(--pc-floating-panel-width, min(360px, 85vw));\n border-radius: var(--pc-radius);\n background: var(--pc-surface-raised);\n box-shadow: var(--pc-shadow);\n overflow: hidden;\n}\n.side.left .float {\n left: var(--pc-space-2);\n}\n.side.right .float {\n right: var(--pc-space-2);\n}\n.float.collapsed {\n bottom: auto;\n}\n.float .collapse {\n align-self: flex-end;\n min-width: var(--pc-control-size);\n min-height: var(--pc-control-size);\n border: 0;\n background: transparent;\n color: var(--pc-text);\n font-size: 18px;\n cursor: pointer;\n}\n.float .collapse:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n}\n.float .content {\n display: flex;\n min-height: 0;\n flex: 1;\n}\n.float .content[hidden] {\n display: none;\n}\n\n/* While another window is drawing, its message covers only the layout (not the whole page) */\npc-dialog.elsewhere {\n position: absolute;\n}\n";
2
+ var style_default = "/* The layout: header across the top, footer across the bottom, sides left and right, and between them the tool\n settings, the canvas and the bottom bar. Empty areas take no space. */\n:host {\n position: relative;\n display: grid;\n grid-template:\n \"header header header\" auto\n \"left top right\" auto\n \"left center right\" 1fr\n \"left bottom right\" auto\n \"footer footer footer\" auto / auto 1fr auto;\n overflow: hidden;\n background: var(--pc-bar);\n font-family: var(--pc-font);\n font-size: var(--pc-font-size);\n}\nheader {\n grid-area: header;\n}\n.top {\n grid-area: top;\n min-width: 0;\n}\n/* The tool settings fill the top pane */\n.top pc-tool-settings {\n flex: 1;\n}\n.center {\n position: relative;\n grid-area: center;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n.bottom {\n grid-area: bottom;\n min-width: 0;\n}\nfooter {\n grid-area: footer;\n}\n.side {\n position: relative;\n z-index: 3;\n display: flex;\n min-height: 0;\n}\n/* A side's panes fill it */\n.side pc-pane,\n.side pc-toolbar,\n.side ::slotted(*) {\n flex: 1;\n}\n.side.left {\n grid-area: left;\n}\n.side.right {\n grid-area: right;\n}\n\n/* Hiding the interface (Tab key) leaves only the canvas */\n:host([ui-hidden]) header,\n:host([ui-hidden]) .top,\n:host([ui-hidden]) .bottom,\n:host([ui-hidden]) footer,\n:host([ui-hidden]) .side {\n display: none;\n}\n\n/* The canvas fills the middle; things drawn over it go in the overlay */\n.center ::slotted([slot=\"center\"]),\n.center pc-canvas {\n position: absolute;\n inset: 0;\n}\n.overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n}\n.overlay > * {\n pointer-events: auto;\n}\npc-canvas-map {\n position: absolute;\n right: 20px;\n bottom: 20px;\n}\n\n/* Menus opened from the toolbar float over the canvas, beside the toolbar */\n.panel {\n position: absolute;\n z-index: 4;\n left: 0;\n bottom: 0;\n max-height: 100%;\n overflow: auto;\n}\n.panel.top-panel {\n top: 0;\n bottom: auto;\n}\n/* The app menu: the implementer's content in a panel */\n.menu-panel:not([open]) {\n display: none;\n}\n\n/* Floating: the side's panes float over the canvas (see pc-pane), so the side takes no space */\n.side[data-mode=\"floating\"] {\n /* Beside the canvas only, so it floats over the canvas and not the tool settings */\n grid-row: 3;\n width: 0;\n}\n/* While another window is drawing, its message covers only the layout (not the whole page) */\npc-dialog.elsewhere {\n position: absolute;\n}\n";
3
3
  export { style_default as default };
@@ -0,0 +1,59 @@
1
+ import { nothing, type PropertyValues } from "lit";
2
+ import { PcElement } from "../../base";
3
+ import "../PcShortcutSettings";
4
+ import type PaintCanvas from "../../../lib/classes/PaintCanvas";
5
+ import { type SaveMethod } from "../../../lib/rendering/screenshot";
6
+ /**
7
+ * Buttons for the user's app settings: "Keyboard shortcuts", which opens a window to change them (see
8
+ * `pc-shortcut-settings`), and "Import settings" and "Export settings", which read and write a settings JSON file (input
9
+ * settings, keyboard shortcuts, colour palette and colours). The window is a modal `<dialog>` above the whole page; it
10
+ * closes with its Close button, Escape, or a press on the backdrop, and the browser gives focus back to the button. Fires `pc-toast` after importing, or when importing or
11
+ * exporting fails.
12
+ *
13
+ * @example
14
+ * html`<pc-app-settings .app=${canvas}></pc-app-settings>`
15
+ */
16
+ export declare class PcAppSettings extends PcElement {
17
+ static properties: {
18
+ app: {
19
+ attribute: boolean;
20
+ };
21
+ shortcutsOpen: {
22
+ type: BooleanConstructor;
23
+ attribute: string;
24
+ };
25
+ translations: {
26
+ attribute: boolean;
27
+ };
28
+ icons: {
29
+ attribute: boolean;
30
+ };
31
+ theme: {
32
+ type: StringConstructor;
33
+ reflect: boolean;
34
+ };
35
+ };
36
+ static styles: import("lit").CSSResultGroup[];
37
+ app?: PaintCanvas;
38
+ /** Whether the keyboard shortcuts window is open */
39
+ shortcutsOpen: boolean;
40
+ /** Saves exported files (replaceable for tests) */
41
+ saveFile: (blob: Blob, name: string) => Promise<SaveMethod | undefined>;
42
+ constructor();
43
+ protected updated(changed: PropertyValues<this>): void;
44
+ /** Close the keyboard shortcuts window */
45
+ closeShortcuts(): void;
46
+ /**
47
+ * Import the settings in a settings file, using the parts it has.
48
+ */
49
+ importFile(file: File): Promise<void>;
50
+ /**
51
+ * Export the settings as a settings file.
52
+ */
53
+ exportFile(): Promise<void>;
54
+ /** Show a settings file error (one of `SETTINGS_FILE_ERRORS`) in the chosen language */
55
+ private toastError;
56
+ /** The keyboard shortcuts window: a modal dialog, with its contents only while open */
57
+ private renderShortcuts;
58
+ render(): import("lit").TemplateResult<1> | typeof nothing;
59
+ }