@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
@@ -4,14 +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";
14
+ import "../PcPanel/index.js";
15
+ import { PANE_COLLAPSE_EVENT, PcPane } from "../PcPane/index.js";
16
+ import "../PcToolbar/index.js";
17
+ import "../PcCanvas/index.js";
18
+ import "../PcCanvasMap/index.js";
19
+ import "../PcPanelBrushes/index.js";
20
+ import "../PcPanelLayers/index.js";
21
+ import "../PcBrushConfig/index.js";
22
+ import "../PcStats/index.js";
23
+ import "../PcPanelColours/index.js";
24
+ import "../PcDialog/index.js";
13
25
  import { html, nothing, unsafeCSS } from "lit";
14
26
  // src/ui/elements/PaintCanvas/index.ts
27
+ /** The sides */
28
+ var SIDES = ["left", "right"];
15
29
  /** No space kept clear */
16
30
  var NO_INSETS = {
17
31
  top: 0,
@@ -21,29 +35,28 @@ var NO_INSETS = {
21
35
  };
22
36
  /** The choice that takes the drawing over from another window */
23
37
  var TAKE_OVER_CHOICE = "take-over";
24
- /** Elements that can take keyboard focus */
25
- var FOCUSABLE = "button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])";
26
38
  /**
27
39
  * A ready-made painting app: the drawing elements arranged around the canvas, sized to its container (not the
28
40
  * window), with slots to add or replace content:
29
41
  *
30
42
  * - `header`: across the top (empty by default)
31
- * - `left`: `<pc-toolbar>` by default
32
- * - `top`: `<pc-tool-settings>` (for tools that have settings) by default
33
- * - `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
34
46
  * - `center`: `<pc-canvas>` by default
35
47
  * - `overlay`: drawn over the canvas; `<pc-canvas-map>` by default
36
48
  * - `bottom`: under the canvas (empty by default)
37
49
  * - `footer`: across the bottom; `<pc-stats>` by default
38
- * - `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)
39
51
  *
40
52
  * Content given for a slot replaces its default; an empty element clears it. Empty slots take no space. Hiding the
41
- * interface (Tab key) leaves only the canvas and overlay. A `<pc-layers-menu>` given in a side (e.g. in a `<pc-panel>`)
42
- * 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.
43
55
  *
44
56
  * Each side has a mode, chosen by the layout's width from `left-modes` and `right-modes` (default `"docked,
45
- * rail@600"`): `docked` beside the canvas; `rail`, a column of buttons (one per `<pc-panel>`) that open each panel over
46
- * 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.
47
60
  *
48
61
  * Reports its state for styling as attributes (`narrow`, `input="pen|touch|mouse"`, `ui-hidden`, `left-mode`,
49
62
  * `right-mode`) and as `pc-state-change` events (see `LayoutState`). Creates its own canvas unless given one, and
@@ -53,10 +66,16 @@ var FOCUSABLE = "button, [href], input, select, textarea, [tabindex]:not([tabind
53
66
  * reloading the page (see `LayerAutosave`). While another window is drawing on it, the layout is blocked by a message
54
67
  * that offers to draw here instead. Fires `pc-toast` when the drawing can't be loaded or saved.
55
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
+ *
56
72
  * @example
57
73
  * html`<paint-canvas style="height: 600px"></paint-canvas>`
58
74
  * html`<paint-canvas .app=${canvas} right-modes="docked, rail@800">
59
- * <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>
60
79
  * </paint-canvas>`
61
80
  */
62
81
  var PaintCanvasElement = class extends PcElement {
@@ -85,11 +104,13 @@ var PaintCanvasElement = class extends PcElement {
85
104
  },
86
105
  shortcuts: { type: Boolean },
87
106
  store: { type: String },
107
+ settingsStore: {
108
+ type: String,
109
+ attribute: "settings-store"
110
+ },
88
111
  width: { state: true },
89
112
  input: { state: true },
90
- configuring: { state: true },
91
- collapsed: { state: true },
92
- openPanel: { state: true }
113
+ configuring: { state: true }
93
114
  };
94
115
  static styles = [PcElement.styles, unsafeCSS(style_default)];
95
116
  /** Saves files (replaceable for tests) */
@@ -99,13 +120,10 @@ var PaintCanvasElement = class extends PcElement {
99
120
  ownAutosave;
100
121
  /** The autosave whose errors are shown */
101
122
  watchedAutosave;
123
+ /** Keeps the app settings under the `settings-store` name, if any */
124
+ ownSettingsAutosave;
102
125
  /** Whether drawing was blocked when shortcuts were last watched */
103
126
  blockedShortcuts = false;
104
- /** The panels in each side's slot */
105
- panels = {
106
- left: [],
107
- right: []
108
- };
109
127
  /** Whether each side's slot has a layers menu (the toolbar's isn't needed then) */
110
128
  sideLayers = {
111
129
  left: false,
@@ -121,12 +139,9 @@ var PaintCanvasElement = class extends PcElement {
121
139
  reported;
122
140
  /** Space the floating sides cover over the canvas */
123
141
  floatingInsets = NO_INSETS;
124
- /** The rail button that opened the open panel, to return focus to */
125
- opener;
126
142
  resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(([entry]) => {
127
143
  this.width = Math.round(entry?.contentRect.width ?? 0);
128
144
  }) : void 0;
129
- panelObserver = new MutationObserver(() => this.requestUpdate());
130
145
  onPointerDown = (event) => {
131
146
  const kind = event.pointerType;
132
147
  this.input = kind === "pen" || kind === "touch" ? kind : "mouse";
@@ -135,20 +150,15 @@ var PaintCanvasElement = class extends PcElement {
135
150
  message: this.t("paintCanvas.autosave.failed"),
136
151
  kind: "error"
137
152
  });
153
+ onSettingsAutosaveError = () => emit(this, "pc-toast", {
154
+ message: this.t("paintCanvas.settings.saveFailed"),
155
+ kind: "error"
156
+ });
138
157
  onShortcut = (event) => {
139
158
  if (event.detail.type === "save") this.saveScreenshot();
140
159
  };
141
- onDocumentPointerDown = (event) => {
142
- const panel = this.openPanel;
143
- if (!panel) return;
144
- const path = event.composedPath();
145
- if (!path.includes(panel) && !(this.opener && path.includes(this.opener))) this.closePanel(false);
146
- };
147
- onKeyDown = (event) => {
148
- if (event.key !== "Escape" || !this.openPanel) return;
149
- event.preventDefault();
150
- this.closePanel(true);
151
- };
160
+ onDocumentPointerDown = (event) => this.closePaletteOutside(event.composedPath());
161
+ onPaneCollapse = () => this.requestUpdate();
152
162
  constructor() {
153
163
  super();
154
164
  this.leftModes = DEFAULT_SIDE_MODES;
@@ -158,10 +168,6 @@ var PaintCanvasElement = class extends PcElement {
158
168
  this.shortcuts = true;
159
169
  this.width = 0;
160
170
  this.input = isTouchDevice() ? "touch" : "mouse";
161
- this.collapsed = {
162
- left: true,
163
- right: true
164
- };
165
171
  }
166
172
  /** Whether another window is drawing on the stored drawing, so this one is blocked */
167
173
  get blocked() {
@@ -186,36 +192,25 @@ var PaintCanvasElement = class extends PcElement {
186
192
  this.app ??= new PaintCanvas();
187
193
  super.connectedCallback();
188
194
  this.resizeObserver?.observe(this);
189
- this.panelObserver.observe(this, {
190
- subtree: true,
191
- attributes: true,
192
- attributeFilter: [
193
- "icon",
194
- "label",
195
- "badge"
196
- ]
197
- });
198
195
  this.addEventListener("pointerdown", this.onPointerDown, true);
199
- this.addEventListener("keydown", this.onKeyDown);
196
+ this.addEventListener(PANE_COLLAPSE_EVENT, this.onPaneCollapse);
200
197
  document.addEventListener("pointerdown", this.onDocumentPointerDown, true);
201
198
  this.watchShortcuts();
202
199
  }
203
200
  disconnectedCallback() {
204
201
  super.disconnectedCallback();
205
202
  this.resizeObserver?.disconnect();
206
- this.panelObserver.disconnect();
207
203
  this.removeEventListener("pointerdown", this.onPointerDown, true);
208
- this.removeEventListener("keydown", this.onKeyDown);
204
+ this.removeEventListener(PANE_COLLAPSE_EVENT, this.onPaneCollapse);
209
205
  document.removeEventListener("pointerdown", this.onDocumentPointerDown, true);
210
206
  this.unwatchShortcuts();
211
207
  }
212
208
  willUpdate(changed) {
213
209
  super.willUpdate(changed);
214
210
  if (changed.has("app") || changed.has("store")) this.useStore(changed.get("app"));
211
+ if (changed.has("app") || changed.has("settingsStore")) this.useSettingsStore();
215
212
  this.watchAutosaveErrors();
216
213
  if (this.isConnected && (changed.has("app") || changed.has("shortcuts") || this.blocked !== this.blockedShortcuts)) this.watchShortcuts();
217
- const panel = this.openPanel;
218
- if (panel && this.modeOf(this.sideOf(panel)) !== "rail") this.closePanel(false);
219
214
  }
220
215
  updated() {
221
216
  const state = this.state;
@@ -229,11 +224,25 @@ var PaintCanvasElement = class extends PcElement {
229
224
  this.reported = report;
230
225
  this.dispatchEvent(new CustomEvent(STATE_EVENT, { detail: state }));
231
226
  }
227
+ this.setPaneModes();
232
228
  this.measureFloatingSides();
233
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
+ }
234
243
  /** Keep the canvas clear of floating sides when fitting it */
235
244
  measureFloatingSides() {
236
- 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);
237
246
  const insets = {
238
247
  ...NO_INSETS,
239
248
  left: width("left"),
@@ -258,6 +267,20 @@ var PaintCanvasElement = class extends PcElement {
258
267
  }
259
268
  if (app.autosave?.store.name !== name) this.ownAutosave = app.useLayerStore(new IndexedDbLayerStore(name));
260
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
+ }
261
284
  /** Show errors from the canvas's current autosave */
262
285
  watchAutosaveErrors() {
263
286
  const autosave = this.app?.autosave;
@@ -330,40 +353,6 @@ var PaintCanvasElement = class extends PcElement {
330
353
  }
331
354
  };
332
355
  }
333
- /** Which side a panel is in */
334
- sideOf(panel) {
335
- return panel.slot === "left" ? "left" : "right";
336
- }
337
- /**
338
- * Open a panel from its rail button (closing any other), or close it if it's open. Focus moves into the panel.
339
- */
340
- togglePanel(panel, opener) {
341
- if (this.openPanel === panel) {
342
- this.closePanel(true);
343
- return;
344
- }
345
- if (this.openPanel) this.closePanel(false);
346
- this.openPanel = panel;
347
- this.opener = opener;
348
- panel.toggleAttribute("data-pc-open", true);
349
- const focusable = panel.querySelector(FOCUSABLE);
350
- if (focusable) focusable.focus();
351
- else {
352
- panel.tabIndex = -1;
353
- panel.focus();
354
- }
355
- }
356
- /**
357
- * Close the panel open from a rail, moving focus back to its rail button if asked.
358
- */
359
- closePanel(returnFocus) {
360
- const panel = this.openPanel;
361
- if (!panel) return;
362
- panel.removeAttribute("data-pc-open");
363
- this.openPanel = void 0;
364
- if (returnFocus) this.opener?.focus();
365
- this.opener = void 0;
366
- }
367
356
  /** Follow which slots have content given (their defaults aren't needed) */
368
357
  onSlotChange(event) {
369
358
  const slot = event.target;
@@ -379,16 +368,12 @@ var PaintCanvasElement = class extends PcElement {
379
368
  >${this.filled[name] ? nothing : fallback}</slot
380
369
  >`;
381
370
  }
382
- /** 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 */
383
372
  onSideSlotChange(side, event) {
384
373
  const assigned = event.target.assignedElements();
385
- this.panels = {
386
- ...this.panels,
387
- [side]: assigned.filter((element) => element.localName === "pc-panel")
388
- };
389
374
  this.sideLayers = {
390
375
  ...this.sideLayers,
391
- [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)"))
392
377
  };
393
378
  this.onSlotChange(event);
394
379
  }
@@ -399,99 +384,70 @@ var PaintCanvasElement = class extends PcElement {
399
384
  }
400
385
  /** Whether a side has a layers menu, replacing the toolbar's */
401
386
  get layersInSide() {
402
- return this.sideLayers.left || this.sideLayers.right;
387
+ return this.sideLayers.left || this.sideLayers.right || !this.filled.right;
403
388
  }
404
- /** The toolbar shown on the left unless the implementer gives other content */
405
- renderToolbar(app) {
406
- return html`<pc-toolbar
407
- .app=${app}
408
- .menu=${this.hasMenu}
409
- .layers=${!this.layersInSide}
410
- .menuText=${this.menuText}
411
- .menuLabel=${this.menuLabel}
412
- ></pc-toolbar>`;
413
- }
414
- /** A side's rail buttons, one per panel */
415
- renderRail(side, label) {
416
- const panels = this.panels[side];
417
- if (panels.length === 0) return nothing;
418
- return html`<div
419
- class="rail"
420
- part="${side}-rail"
421
- role="toolbar"
422
- aria-orientation="vertical"
423
- aria-label=${label}
424
- >
425
- ${panels.map((panel) => html`<button
426
- type="button"
427
- title=${panel.label}
428
- aria-label=${panel.badge ? this.t("paintCanvas.layout.new", { name: panel.label }) : panel.label}
429
- aria-expanded=${String(this.openPanel === panel)}
430
- @click=${(event) => this.togglePanel(panel, event.currentTarget)}
431
- >
432
- <img src=${this.iconUrl(panel.icon)} alt="" />
433
- ${panel.badge ? html`<span class="badge"></span>` : nothing}
434
- </button>`)}
435
- </div>`;
436
- }
437
- /** 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) */
438
404
  renderSide(side, app, blocked) {
439
- const mode = this.modeOf(side);
440
405
  const label = this.t(side === "left" ? "paintCanvas.layout.tools" : "paintCanvas.layout.panels");
441
- const slot = html`<slot
442
- name=${side}
443
- @slotchange=${(event) => this.onSideSlotChange(side, event)}
444
- >${side === "left" && !this.filled.left ? this.renderToolbar(app) : nothing}</slot
445
- >`;
446
- let content = slot;
447
- if (mode === "rail") content = side === "left" ? html`${slot}${this.renderRail(side, label)}` : html`${this.renderRail(side, label)}${slot}`;
448
- else if (mode === "floating") {
449
- const collapsed = this.collapsed[side];
450
- const arrows = side === "left" ? ["»", "«"] : ["«", "»"];
451
- content = html`<div
452
- class=${collapsed ? "float collapsed" : "float"}
453
- part="${side}-float"
454
- >
455
- <button
456
- type="button"
457
- class="collapse"
458
- aria-expanded=${String(!collapsed)}
459
- aria-label=${this.t(collapsed ? "paintCanvas.layout.show" : "paintCanvas.layout.hide", { name: label })}
460
- @click=${() => this.collapsed = {
461
- ...this.collapsed,
462
- [side]: !collapsed
463
- }}
464
- >
465
- ${collapsed ? arrows[0] : arrows[1]}
466
- </button>
467
- <div class="content" ?hidden=${collapsed}>${slot}</div>
468
- </div>`;
469
- }
470
406
  return html`<div
471
407
  class="side ${side}"
472
408
  part=${side}
473
- data-mode=${mode}
409
+ data-mode=${this.modeOf(side)}
474
410
  role="region"
475
411
  aria-label=${label}
476
412
  ?inert=${blocked}
477
413
  >
478
- ${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
+ >
479
419
  </div>`;
480
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
+ }
481
429
  /** The menus the toolbar opens, floating over the canvas beside it */
482
430
  renderPanels(app) {
483
- const brushMenu = (kind) => html`<pc-brush-menu
431
+ const brushMenu = (kind) => html`<pc-panel-brushes
484
432
  class="panel top-panel"
485
433
  kind=${kind}
486
434
  .app=${app}
487
435
  @pc-new-preset=${() => this.newPreset(kind)}
488
- ></pc-brush-menu>`;
436
+ ></pc-panel-brushes>`;
489
437
  return html`${app.panel === Panel.Brush ? brushMenu("brush") : nothing}
490
438
  ${app.panel === Panel.Eraser ? brushMenu("eraser") : nothing}
491
- ${app.panel === Panel.Layers && !this.layersInSide ? html`<pc-layers-menu class="panel" .app=${app}></pc-layers-menu>` : nothing}
492
- <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
+ >
493
449
  <slot name="menu" @slotchange=${this.onMenuSlotChange}></slot>
494
- </div>`;
450
+ </pc-panel>`;
495
451
  }
496
452
  /** The brush configuration window, while open */
497
453
  renderConfig(app) {
@@ -540,10 +496,13 @@ var PaintCanvasElement = class extends PcElement {
540
496
  aria-label=${this.t("paintCanvas.layout.toolSettings")}
541
497
  ?inert=${blocked}
542
498
  >
543
- ${this.slotWithDefault("top", showsToolSettings ? html`<pc-tool-settings
544
- .app=${app}
545
- @pc-configure-brush=${(event) => this.configure(event.detail)}
546
- ></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)}
547
506
  </div>
548
507
  <div
549
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
+ }