@lvl99dotcom/paintcanvas 0.6.0 → 0.7.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 (59) hide show
  1. package/README.md +2 -2
  2. package/dist/lib/classes/PaintCanvas/index.d.ts +13 -5
  3. package/dist/lib/classes/PaintCanvas/index.js +10 -6
  4. package/dist/lib/classes/UiSettings/index.d.ts +24 -2
  5. package/dist/lib/classes/UiSettings/index.js +32 -7
  6. package/dist/lib/settings/schema.d.ts +4 -1
  7. package/dist/lib/settings/schema.js +3 -2
  8. package/dist/lib/storage/index.d.ts +26 -0
  9. package/dist/lib/storage/index.js +38 -1
  10. package/dist/lib/validation/validators.generated.js +20 -2
  11. package/dist/lib/version/index.d.ts +25 -0
  12. package/dist/lib/version/index.js +35 -0
  13. package/dist/locales/en-GB.js +5 -0
  14. package/dist/locales/en-US.js +5 -0
  15. package/dist/locales/fr.js +5 -0
  16. package/dist/locales/ja.js +5 -0
  17. package/dist/locales/ko.js +5 -0
  18. package/dist/locales/zh-Hans.js +5 -0
  19. package/dist/locales/zh-Hant.js +5 -0
  20. package/dist/package.js +1 -1
  21. package/dist/ui/elements/PaintCanvas/index.d.ts +13 -3
  22. package/dist/ui/elements/PaintCanvas/index.js +22 -5
  23. package/dist/ui/elements/PcAppSettings/index.d.ts +10 -2
  24. package/dist/ui/elements/PcAppSettings/index.js +42 -7
  25. package/dist/ui/elements/PcBrushConfig/index.d.ts +1 -0
  26. package/dist/ui/elements/PcBrushConfig/index.js +8 -9
  27. package/dist/ui/elements/PcBrushTest/index.d.ts +1 -0
  28. package/dist/ui/elements/PcBrushTest/index.js +8 -7
  29. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  30. package/dist/ui/elements/PcButtonMenu/index.d.ts +88 -0
  31. package/dist/ui/elements/PcButtonMenu/index.js +125 -0
  32. package/dist/ui/elements/PcCanvas/index.d.ts +9 -0
  33. package/dist/ui/elements/PcCanvas/index.js +33 -17
  34. package/dist/ui/elements/PcClipboardTools/index.d.ts +7 -4
  35. package/dist/ui/elements/PcClipboardTools/index.js +20 -26
  36. package/dist/ui/elements/PcInputSettings/index.d.ts +2 -1
  37. package/dist/ui/elements/PcInputSettings/index.js +9 -15
  38. package/dist/ui/elements/PcLayerItem/index.d.ts +21 -6
  39. package/dist/ui/elements/PcLayerItem/index.js +53 -26
  40. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  41. package/dist/ui/elements/PcSliderRange/index.js +3 -2
  42. package/dist/ui/elements/PcToggleCheckbox/index.d.ts +13 -2
  43. package/dist/ui/elements/PcToggleCheckbox/index.js +27 -8
  44. package/dist/ui/elements/PcToolSettings/index.d.ts +1 -0
  45. package/dist/ui/elements/PcToolSettings/index.js +8 -8
  46. package/dist/ui/elements/PcVersion/index.d.ts +12 -0
  47. package/dist/ui/elements/PcVersion/index.js +43 -0
  48. package/dist/ui/index.d.ts +3 -1
  49. package/dist/ui/index.js +4 -2
  50. package/dist/ui/layout/index.d.ts +2 -2
  51. package/dist/ui/layout/index.js +2 -2
  52. package/locales/en-GB.json +5 -0
  53. package/locales/en-US.json +5 -0
  54. package/locales/fr.json +5 -0
  55. package/locales/ja.json +5 -0
  56. package/locales/ko.json +5 -0
  57. package/locales/zh-Hans.json +5 -0
  58. package/locales/zh-Hant.json +5 -0
  59. package/package.json +2 -2
@@ -21,6 +21,7 @@ import "../../../lib/rendering/sample/index.js";
21
21
  import { gestureAction } from "../../../lib/utils/gestures/index.js";
22
22
  import { snapToPixelGrid } from "../../../lib/brushes/params/index.js";
23
23
  import { html, unsafeCSS } from "lit";
24
+ import { keyed } from "lit/directives/keyed.js";
24
25
  // src/ui/elements/PcCanvas/index.ts
25
26
  /** Toolbar width in CSS pixels (kept clear when fitting the canvas) */
26
27
  var TOOLBAR_WIDTH = 50;
@@ -172,6 +173,8 @@ var PcCanvas = class extends PcElement {
172
173
  antsOffset = 0;
173
174
  /** Viewport that has been fitted once (so resizing doesn't refit) */
174
175
  fittedViewport;
176
+ /** The screen canvas whose 2D context has been made (see `prepareScene`) */
177
+ preparedCanvas;
175
178
  frame = 0;
176
179
  /** Animation frame drawing only the brush cursor, if requested */
177
180
  overlayFrame = 0;
@@ -247,7 +250,7 @@ var PcCanvas = class extends PcElement {
247
250
  this.frame = 0;
248
251
  }
249
252
  firstUpdated() {
250
- this.canvas?.getContext("2d", { willReadFrequently: true });
253
+ this.prepareScene();
251
254
  if (typeof ResizeObserver !== "undefined") {
252
255
  this.resizeObserver = new ResizeObserver(() => this.syncSize());
253
256
  this.resizeObserver.observe(this);
@@ -255,6 +258,18 @@ var PcCanvas = class extends PcElement {
255
258
  this.syncSize();
256
259
  }
257
260
  /**
261
+ * Make a new screen canvas's 2D context where the app's canvases draw (see `PaintCanvas.rendering`). Drawn from layers
262
+ * kept in regular memory (`cpu`), keeping the screen canvas there too means each frame copies only the area that
263
+ * changed, instead of sending whole layers to the GPU. The first context decides that; later `getContext("2d")` calls
264
+ * get this one, so the screen canvas is made again when the app's rendering differs (see `render`).
265
+ */
266
+ prepareScene() {
267
+ const canvas = this.canvas;
268
+ if (!canvas || canvas === this.preparedCanvas) return;
269
+ this.preparedCanvas = canvas;
270
+ canvas.getContext("2d", { willReadFrequently: this.app?.rendering !== "gpu" });
271
+ }
272
+ /**
258
273
  * Note what changed: the stroke in progress (and the area it changed), or anything else.
259
274
  */
260
275
  sceneChanged(target) {
@@ -266,6 +281,7 @@ var PcCanvas = class extends PcElement {
266
281
  } else if (!(this.committing && target === app?.layers)) this.fullChange = true;
267
282
  }
268
283
  updated(changed) {
284
+ this.prepareScene();
269
285
  const resized = this.syncSize();
270
286
  const onlyStroke = changed.size === 0 && !this.fullChange && !resized;
271
287
  this.requestDraw(onlyStroke ? this.strokeArea : void 0);
@@ -1031,22 +1047,22 @@ var PcCanvas = class extends PcElement {
1031
1047
  });
1032
1048
  }
1033
1049
  render() {
1034
- return html`<canvas
1035
- class="scene"
1036
- aria-label=${this.t("paintCanvas.canvas.label")}
1037
- @pointerdown=${this.onPointerDown}
1038
- @pointermove=${this.onPointerMove}
1039
- @pointerup=${this.onPointerUp}
1040
- @pointercancel=${this.onPointerCancel}
1041
- @touchstart=${this.onTouchStart}
1042
- @pointerleave=${this.onPointerLeave}
1043
- @wheel=${this.onWheel}
1044
- @gesturestart=${this.onGestureStart}
1045
- @gesturechange=${this.onGestureChange}
1046
- @gestureend=${(e) => e.preventDefault()}
1047
- @dblclick=${() => this.app?.applyLasso(this.app.lasso.close())}
1048
- @contextmenu=${(e) => e.preventDefault()}
1049
- ></canvas>
1050
+ return html`${keyed(this.app?.rendering ?? "cpu", html`<canvas
1051
+ class="scene"
1052
+ aria-label=${this.t("paintCanvas.canvas.label")}
1053
+ @pointerdown=${this.onPointerDown}
1054
+ @pointermove=${this.onPointerMove}
1055
+ @pointerup=${this.onPointerUp}
1056
+ @pointercancel=${this.onPointerCancel}
1057
+ @touchstart=${this.onTouchStart}
1058
+ @pointerleave=${this.onPointerLeave}
1059
+ @wheel=${this.onWheel}
1060
+ @gesturestart=${this.onGestureStart}
1061
+ @gesturechange=${this.onGestureChange}
1062
+ @gestureend=${(e) => e.preventDefault()}
1063
+ @dblclick=${() => this.app?.applyLasso(this.app.lasso.close())}
1064
+ @contextmenu=${(e) => e.preventDefault()}
1065
+ ></canvas>`)}
1050
1066
  <canvas class="outlines" aria-hidden="true"></canvas>
1051
1067
  <canvas class="overlay" aria-hidden="true"></canvas>
1052
1068
  <canvas class="sampler" aria-hidden="true"></canvas>`;
@@ -1,14 +1,17 @@
1
1
  import { nothing } from "lit";
2
2
  import { PcElement } from "../../base";
3
3
  import PaintCanvas, { Tool } from "../../../lib/classes/PaintCanvas";
4
- import "../PcContextMenu";
4
+ import "../PcButtonMenu";
5
5
  /** The tools that cut, copy and paste are shown for: the brush, eraser, lassos and Move tool */
6
6
  export declare const CLIPBOARD_TOOLS: readonly Tool[];
7
+ /** Window width (CSS pixels) below which the clipboard tools are one ⋮ menu */
8
+ export declare const CLIPBOARD_MENU_BELOW = 900;
7
9
  /**
8
10
  * Cut, copy, copy merged and paste buttons for the canvas's masked image data, shown for the brush, eraser, lassos and
9
11
  * Move tool (see `CLIPBOARD_TOOLS`). "Copy merged" copies the shown layers mixed as shown (see `PaintCanvas.copyMerged`).
10
12
  * Cut and both copies work while there's a mask and no transform is pending; paste while there's copied image data.
11
- * While `narrow`, they're one ⋮ menu (`pc-context-menu`) instead.
13
+ * They're one ⋮ menu instead while the window is narrower than `CLIPBOARD_MENU_BELOW` (900px), or while `narrow` (see
14
+ * `pc-button-menu`).
12
15
  *
13
16
  * `<paint-canvas>` puts it at the end of its top pane, after the tool settings.
14
17
  *
@@ -37,7 +40,7 @@ export declare class PcClipboardTools extends PcElement {
37
40
  };
38
41
  static styles: import("lit").CSSResultGroup[];
39
42
  app?: PaintCanvas;
40
- /** Whether the buttons are one ⋮ menu (e.g. while the layout is narrow) */
43
+ /** Whether the buttons are one ⋮ menu whatever the window's width (e.g. while the layout is narrow) */
41
44
  narrow: boolean;
42
45
  private changes;
43
46
  constructor();
@@ -45,5 +48,5 @@ export declare class PcClipboardTools extends PcElement {
45
48
  private actions;
46
49
  /** Hide for tools without clipboard actions (e.g. zoom) */
47
50
  protected willUpdate(): void;
48
- render(): import("lit").TemplateResult<1> | typeof nothing | import("lit").TemplateResult<1>[];
51
+ render(): import("lit").TemplateResult<1> | typeof nothing;
49
52
  }
@@ -1,10 +1,9 @@
1
1
  import { Tool } from "../../../lib/classes/PaintCanvas/tool.js";
2
2
  import { PcElement, defineElement } from "../../base/index.js";
3
3
  import ChangeController from "../../ChangeController/index.js";
4
- import { PILLS_CSS } from "../../styles/index.js";
5
4
  import "../../../lib/classes/PaintCanvas/index.js";
6
- import "../PcContextMenu/index.js";
7
- import { css, html, nothing, unsafeCSS } from "lit";
5
+ import "../PcButtonMenu/index.js";
6
+ import { css, html, nothing } from "lit";
8
7
  // src/ui/elements/PcClipboardTools/index.ts
9
8
  /** The tools that cut, copy and paste are shown for: the brush, eraser, lassos and Move tool */
10
9
  var CLIPBOARD_TOOLS = [
@@ -16,11 +15,14 @@ var CLIPBOARD_TOOLS = [
16
15
  Tool.FreeformLasso,
17
16
  Tool.Move
18
17
  ];
18
+ /** Window width (CSS pixels) below which the clipboard tools are one ⋮ menu */
19
+ var CLIPBOARD_MENU_BELOW = 900;
19
20
  /**
20
21
  * Cut, copy, copy merged and paste buttons for the canvas's masked image data, shown for the brush, eraser, lassos and
21
22
  * Move tool (see `CLIPBOARD_TOOLS`). "Copy merged" copies the shown layers mixed as shown (see `PaintCanvas.copyMerged`).
22
23
  * Cut and both copies work while there's a mask and no transform is pending; paste while there's copied image data.
23
- * While `narrow`, they're one ⋮ menu (`pc-context-menu`) instead.
24
+ * They're one ⋮ menu instead while the window is narrower than `CLIPBOARD_MENU_BELOW` (900px), or while `narrow` (see
25
+ * `pc-button-menu`).
24
26
  *
25
27
  * `<paint-canvas>` puts it at the end of its top pane, after the tool settings.
26
28
  *
@@ -36,15 +38,11 @@ var PcClipboardTools = class extends PcElement {
36
38
  reflect: true
37
39
  }
38
40
  };
39
- static styles = [
40
- PcElement.styles,
41
- unsafeCSS(PILLS_CSS),
42
- css`
41
+ static styles = [PcElement.styles, css`
43
42
  :host {
44
43
  display: flex;
45
44
  flex: none;
46
45
  align-items: center;
47
- gap: 8px;
48
46
  padding: 4px 8px;
49
47
  background: var(--pc-bar);
50
48
  border-bottom: 1px solid var(--pc-surface-active);
@@ -52,8 +50,7 @@ var PcClipboardTools = class extends PcElement {
52
50
  :host([hidden]) {
53
51
  display: none;
54
52
  }
55
- `
56
- ];
53
+ `];
57
54
  changes = new ChangeController(this, () => this.app ? [
58
55
  this.app,
59
56
  this.app.selection,
@@ -101,25 +98,22 @@ var PcClipboardTools = class extends PcElement {
101
98
  const app = this.app;
102
99
  if (!app || this.hidden) return nothing;
103
100
  const actions = this.actions(app);
104
- if (this.narrow) return html`<pc-context-menu
105
- class="more"
106
- label=${this.t("paintCanvas.toolSettings.moreActions")}
107
- .items=${actions.map(({ name, label, disabled }) => ({
101
+ return html`<pc-button-menu
102
+ size="pill-s"
103
+ label=${this.t("paintCanvas.toolSettings.moreActions")}
104
+ menu-below=${900}
105
+ ?menu=${this.narrow}
106
+ .items=${actions.map(({ name, label, disabled }) => ({
108
107
  value: name,
109
108
  label,
110
109
  disabled
111
110
  }))}
112
- @pc-select=${(event) => actions.find((action) => action.name === event.detail.value)?.run()}
113
- ></pc-context-menu>`;
114
- return actions.map((action) => html`<button
115
- type="button"
116
- class="pill-s ${action.name}"
117
- ?disabled=${action.disabled}
118
- @click=${action.run}
119
- >
120
- ${action.label}
121
- </button>`);
111
+ @pc-select=${(event) => {
112
+ event.stopPropagation();
113
+ actions.find((action) => action.name === event.detail.value)?.run();
114
+ }}
115
+ ></pc-button-menu>`;
122
116
  }
123
117
  };
124
118
  defineElement("pc-clipboard-tools", PcClipboardTools);
125
- export { CLIPBOARD_TOOLS, PcClipboardTools };
119
+ export { CLIPBOARD_MENU_BELOW, CLIPBOARD_TOOLS, PcClipboardTools };
@@ -1,5 +1,6 @@
1
1
  import { nothing } from "lit";
2
2
  import { PcElement } from "../../base";
3
+ import "../PcToggleCheckbox";
3
4
  import { type InputSettingsData } from "../../../lib/classes/InputSettings";
4
5
  import type { UiSettingsLike } from "../../../lib/classes/UiSettings";
5
6
  /** Settings `pc-input-settings` can change: PaintCanvas's `InputSettings`, or anything with the same data and `update` */
@@ -69,7 +70,7 @@ export declare class PcInputSettings extends PcElement {
69
70
  constructor();
70
71
  /** Use the kept language when it changes */
71
72
  protected willUpdate(): void;
72
- /** A checkbox for an on/off setting */
73
+ /** A switch for an on/off setting */
73
74
  private toggle;
74
75
  /**
75
76
  * The touch gestures: which gesture undoes, redoes, copies and pastes. Choosing a gesture another action uses swaps
@@ -1,5 +1,6 @@
1
1
  import { PcElement, defineElement, emit } from "../../base/index.js";
2
2
  import ChangeController from "../../ChangeController/index.js";
3
+ import "../PcToggleCheckbox/index.js";
3
4
  import { languageName } from "../../../lib/i18n/index.js";
4
5
  import { isTouchDevice, zoomModifierName } from "../../../lib/classes/InputSettings/index.js";
5
6
  import { GESTURE_ACTIONS, TOUCH_GESTURES, assignGesture } from "../../../lib/utils/gestures/index.js";
@@ -57,13 +58,7 @@ var PcInputSettings = class extends PcElement {
57
58
  font-weight: bold;
58
59
  }
59
60
  .toggle {
60
- display: flex;
61
61
  grid-column: 1 / -1;
62
- gap: var(--pc-space-2);
63
- align-items: center;
64
- }
65
- input[type="checkbox"] {
66
- accent-color: var(--pc-accent);
67
62
  }
68
63
  input:focus-visible,
69
64
  select:focus-visible {
@@ -88,17 +83,16 @@ var PcInputSettings = class extends PcElement {
88
83
  this.usedLocale = locale;
89
84
  if (locale !== this.translations.locale) this.translations.setLocale(locale);
90
85
  }
91
- /** A checkbox for an on/off setting */
86
+ /** A switch for an on/off setting */
92
87
  toggle(data, key, label) {
93
- return html`<label class="toggle">
94
- <input
95
- type="checkbox"
96
- name=${key}
97
- .checked=${data[key]}
98
- @change=${(e) => this.settings?.update({ [key]: e.target.checked })}
99
- />
88
+ return html`<pc-toggle-checkbox
89
+ class="toggle"
90
+ data-setting=${key}
91
+ .checked=${data[key]}
92
+ @change=${(e) => this.settings?.update({ [key]: e.detail.checked })}
93
+ >
100
94
  ${label}
101
- </label>`;
95
+ </pc-toggle-checkbox>`;
102
96
  }
103
97
  /**
104
98
  * The touch gestures: which gesture undoes, redoes, copies and pastes. Choosing a gesture another action uses swaps
@@ -2,6 +2,8 @@ import { PcElement } from "../../base";
2
2
  import { BlendMode, type default as Layer } from "../../../lib/classes/Layer";
3
3
  import type LayerStack from "../../../lib/classes/LayerStack";
4
4
  import "../PcDialog";
5
+ import "../PcSliderRange";
6
+ import { type PromptFn } from "../../ask";
5
7
  import "../PcContextMenu";
6
8
  import { MENU_MARGIN, menuTop } from "../PcContextMenu";
7
9
  import { type DialogChoice } from "../PcDialog";
@@ -12,7 +14,7 @@ export declare const BLEND_MODE_OPTIONS: readonly {
12
14
  label: string;
13
15
  }[];
14
16
  /** Actions in the reorder handle's context menu */
15
- export type LayerAction = "mask" | "duplicate" | "merge" | "clear" | "delete" | "up" | "top" | "down" | "bottom";
17
+ export type LayerAction = "rename" | "mask" | "duplicate" | "merge" | "clear" | "delete" | "up" | "top" | "down" | "bottom";
16
18
  /** Context menu options in display order, with the translation keys of their labels */
17
19
  export declare const LAYER_ACTIONS: readonly {
18
20
  action: LayerAction;
@@ -61,7 +63,9 @@ export interface LayerDragDetail {
61
63
  /** Where the layer actions menu goes and how close to the window's edges (see `pc-context-menu`) */
62
64
  export { MENU_MARGIN, menuTop };
63
65
  /**
64
- * One row in the layers menu: reorder handle, thumbnail and name (then opacity and blend mode under them), alpha lock (padlock) and visibility (eye) icons.
66
+ * One row in the layers menu: reorder handle, thumbnail and name (then opacity and blend mode under them), alpha lock
67
+ * (padlock) and visibility (eye) icons. Pressing anywhere on the row (its controls too) selects the layer, which is then
68
+ * shown with a purple background (`active`). "Rename…" in the actions menu asks for a new name.
65
69
  * Dragging the handle (with mouse, pen or touch) reorders the layer: the row fires {@link LAYER_DRAG_EVENT} and the
66
70
  * layers menu shows where it will land. Clicking or tapping the handle, pressing Enter or Space on it, or right-clicking
67
71
  * it opens the layer's actions menu (which can also reorder it). Cmd/Ctrl + click on the thumbnail or name (or "Create
@@ -96,6 +100,10 @@ export declare class PcLayerItem extends PcElement {
96
100
  attribute: string;
97
101
  reflect: boolean;
98
102
  };
103
+ active: {
104
+ type: BooleanConstructor;
105
+ reflect: boolean;
106
+ };
99
107
  translations: {
100
108
  attribute: boolean;
101
109
  };
@@ -116,6 +124,10 @@ export declare class PcLayerItem extends PcElement {
116
124
  menuOpen: boolean;
117
125
  /** The action the user is being asked to confirm, if any */
118
126
  confirming?: ConfirmedAction;
127
+ /** Whether this row's layer is the selected one (shown with a purple background) */
128
+ active: boolean;
129
+ /** Asks the user for the layer's new name (replaceable for tests) */
130
+ prompt: PromptFn;
119
131
  /** Whether this row's layer is being dragged (set by the layers menu; the row floats with the pointer) */
120
132
  dragging: boolean;
121
133
  /** While this row's layer is dragged: what dropping it now would do (set by the layers menu; shown by the cursor) */
@@ -128,6 +140,8 @@ export declare class PcLayerItem extends PcElement {
128
140
  /** Device pixels per CSS pixel (replaceable for tests) */
129
141
  pixelRatio: () => number;
130
142
  constructor();
143
+ /** Pressing anywhere on the row selects its layer, except in the dialogs it shows (e.g. after deleting it) */
144
+ private readonly onClick;
131
145
  /**
132
146
  * Run a context menu action. Merging down, clearing and deleting ask for confirmation first (see `confirmed`).
133
147
  */
@@ -180,13 +194,14 @@ export declare class PcLayerItem extends PcElement {
180
194
  */
181
195
  private requestMask;
182
196
  /**
183
- * Clicking the row selects the layer. Cmd/Ctrl + click on the thumbnail or name also makes a mask from the layer's alpha.
197
+ * Cmd/Ctrl + click on the thumbnail or name makes a mask from the layer's alpha (the click also selects the layer).
184
198
  */
185
- private onRowClick;
199
+ private onDetailsClick;
186
200
  /**
187
- * Rename a layer when the name input changes. Empty names are reverted.
201
+ * Ask for the layer's new name and use it. Cancelling or an empty name leaves the name as it was.
188
202
  */
189
- private onRename;
203
+ private rename;
204
+ protected willUpdate(changed: Map<PropertyKey, unknown>): void;
190
205
  updated(): void;
191
206
  /**
192
207
  * Draw the layer's image, scaled to fit, on a checkerboard that shows transparency.
@@ -5,8 +5,11 @@ import ChangeController from "../../ChangeController/index.js";
5
5
  import { BlendMode } from "../../../lib/classes/Layer/index.js";
6
6
  import { drawCheckerboard, fitSize } from "../../../lib/rendering/checkerboard/index.js";
7
7
  import { CANCEL_CHOICE } from "../PcDialog/index.js";
8
+ import "../PcSliderRange/index.js";
9
+ import { askText } from "../../ask/index.js";
8
10
  import { MENU_MARGIN, menuTop } from "../PcContextMenu/index.js";
9
11
  import { html, nothing, unsafeCSS } from "lit";
12
+ import { live } from "lit/directives/live.js";
10
13
  // src/ui/elements/PcLayerItem/index.ts
11
14
  /** Blend mode options shown in the layer's select input */
12
15
  var BLEND_MODE_OPTIONS = [
@@ -37,6 +40,10 @@ var BLEND_MODE_OPTIONS = [
37
40
  ];
38
41
  /** Context menu options in display order, with the translation keys of their labels */
39
42
  var LAYER_ACTIONS = [
43
+ {
44
+ action: "rename",
45
+ label: "paintCanvas.layers.rename"
46
+ },
40
47
  {
41
48
  action: "mask",
42
49
  label: "paintCanvas.layers.createMask"
@@ -129,7 +136,9 @@ var DRAG_THRESHOLD = 4;
129
136
  /** Event a row dispatches while its layer is dragged by its reorder handle (detail: {@link LayerDragDetail}) */
130
137
  var LAYER_DRAG_EVENT = "pc-layer-drag";
131
138
  /**
132
- * One row in the layers menu: reorder handle, thumbnail and name (then opacity and blend mode under them), alpha lock (padlock) and visibility (eye) icons.
139
+ * One row in the layers menu: reorder handle, thumbnail and name (then opacity and blend mode under them), alpha lock
140
+ * (padlock) and visibility (eye) icons. Pressing anywhere on the row (its controls too) selects the layer, which is then
141
+ * shown with a purple background (`active`). "Rename…" in the actions menu asks for a new name.
133
142
  * Dragging the handle (with mouse, pen or touch) reorders the layer: the row fires {@link LAYER_DRAG_EVENT} and the
134
143
  * layers menu shows where it will land. Clicking or tapping the handle, pressing Enter or Space on it, or right-clicking
135
144
  * it opens the layer's actions menu (which can also reorder it). Cmd/Ctrl + click on the thumbnail or name (or "Create
@@ -154,6 +163,10 @@ var PcLayerItem = class extends PcElement {
154
163
  dropAction: {
155
164
  attribute: "drop-action",
156
165
  reflect: true
166
+ },
167
+ active: {
168
+ type: Boolean,
169
+ reflect: true
157
170
  }
158
171
  };
159
172
  static styles = [
@@ -162,6 +175,8 @@ var PcLayerItem = class extends PcElement {
162
175
  unsafeCSS(style_default),
163
176
  unsafeCSS(WINDOWS_CSS)
164
177
  ];
178
+ /** Asks the user for the layer's new name (replaceable for tests) */
179
+ prompt = (options) => askText(this.renderRoot, options);
165
180
  /** The pointer pressed on the reorder handle, where it was pressed, and whether it has started dragging */
166
181
  press;
167
182
  /** Whether to ignore the next click on the handle (it ended a drag) */
@@ -173,7 +188,15 @@ var PcLayerItem = class extends PcElement {
173
188
  super();
174
189
  this.menuOpen = false;
175
190
  this.dragging = false;
191
+ this.active = false;
192
+ this.addEventListener("click", this.onClick);
176
193
  }
194
+ /** Pressing anywhere on the row selects its layer, except in the dialogs it shows (e.g. after deleting it) */
195
+ onClick = (event) => {
196
+ const { stack, layer } = this;
197
+ if (!stack || !layer || !stack.layers.includes(layer)) return;
198
+ if (!event.composedPath().some((target) => target instanceof Element && target.matches("pc-dialog")) && stack.active !== layer) stack.select(layer.id);
199
+ };
177
200
  /**
178
201
  * Run a context menu action. Merging down, clearing and deleting ask for confirmation first (see `confirmed`).
179
202
  */
@@ -182,6 +205,9 @@ var PcLayerItem = class extends PcElement {
182
205
  this.menuOpen = false;
183
206
  if (!stack || !layer || !this.canRun(action)) return;
184
207
  switch (action) {
208
+ case "rename":
209
+ this.rename(stack, layer);
210
+ break;
185
211
  case "mask":
186
212
  this.requestMask(layer, {
187
213
  shiftKey: false,
@@ -360,10 +386,9 @@ var PcLayerItem = class extends PcElement {
360
386
  }));
361
387
  }
362
388
  /**
363
- * Clicking the row selects the layer. Cmd/Ctrl + click on the thumbnail or name also makes a mask from the layer's alpha.
389
+ * Cmd/Ctrl + click on the thumbnail or name makes a mask from the layer's alpha (the click also selects the layer).
364
390
  */
365
- onRowClick(event, stack, layer) {
366
- stack.select(layer.id);
391
+ onDetailsClick(event, layer) {
367
392
  if (!event.metaKey && !event.ctrlKey) return;
368
393
  const target = event.composedPath()[0];
369
394
  if (!(target instanceof Element) || !target.matches(".thumbnail, .name")) return;
@@ -371,15 +396,21 @@ var PcLayerItem = class extends PcElement {
371
396
  this.requestMask(layer, event);
372
397
  }
373
398
  /**
374
- * Rename a layer when the name input changes. Empty names are reverted.
399
+ * Ask for the layer's new name and use it. Cancelling or an empty name leaves the name as it was.
375
400
  */
376
- onRename(event, stack, layer) {
377
- const input = event.target;
378
- try {
379
- stack.rename(layer.id, input.value);
380
- } catch {
381
- input.value = layer.name;
382
- }
401
+ async rename(stack, layer) {
402
+ const name = (await this.prompt({
403
+ heading: this.t("paintCanvas.layers.renameTitle"),
404
+ label: this.t("paintCanvas.layers.name"),
405
+ value: layer.name,
406
+ confirm: this.t("paintCanvas.common.save"),
407
+ cancel: this.t("paintCanvas.common.cancel")
408
+ }))?.trim();
409
+ if (name && stack.layers.includes(layer)) stack.rename(layer.id, name);
410
+ }
411
+ willUpdate(changed) {
412
+ super.willUpdate(changed);
413
+ this.active = this.layer !== void 0 && this.stack?.active.id === this.layer.id;
383
414
  }
384
415
  updated() {
385
416
  const id = this.layer?.id;
@@ -434,7 +465,6 @@ var PcLayerItem = class extends PcElement {
434
465
  render() {
435
466
  const { stack, layer } = this;
436
467
  if (!stack || !layer) return html``;
437
- const active = stack.active.id === layer.id;
438
468
  return html`
439
469
  <pc-context-menu
440
470
  class="handle"
@@ -451,23 +481,20 @@ var PcLayerItem = class extends PcElement {
451
481
  >
452
482
  <div
453
483
  class="details"
454
- @click=${(e) => this.onRowClick(e, stack, layer)}
484
+ @click=${(e) => this.onDetailsClick(e, layer)}
455
485
  >
456
486
  <canvas class="thumbnail" aria-hidden="true"></canvas>
457
- <input
458
- class="name ${active ? "active" : ""}"
459
- aria-label=${this.t("paintCanvas.layers.name")}
460
- .value=${layer.name}
461
- @change=${(e) => this.onRename(e, stack, layer)}
462
- />
463
- <input
464
- type="range"
487
+ <span class="name" title=${layer.name}>${layer.name}</span>
488
+ <pc-slider-range
489
+ single
490
+ class="opacity"
491
+ label=${this.t("paintCanvas.common.opacity")}
465
492
  min="0"
466
493
  max="100"
467
- aria-label=${this.t("paintCanvas.common.opacity")}
468
- .value=${String(Math.round(layer.opacity * 100))}
469
- @change=${(e) => stack.update(layer.id, { opacity: Number(e.target.value) / 100 })}
470
- />
494
+ unit="%"
495
+ .value=${live(Math.round(layer.opacity * 100))}
496
+ @pc-change=${(e) => stack.update(layer.id, { opacity: e.detail.value / 100 })}
497
+ ></pc-slider-range>
471
498
  <select
472
499
  aria-label=${this.t("paintCanvas.layers.blendMode")}
473
500
  @change=${(e) => stack.update(layer.id, { blendMode: e.target.value })}
@@ -1,3 +1,3 @@
1
1
  // src/ui/elements/PcLayerItem/style.css?inline
2
- var style_default = "/* A white row with space around its parts. The middle column can shrink, so the row never grows wider than its panel */\n:host {\n display: grid;\n grid-template-columns: 30px minmax(0, 1fr) 30px 30px;\n align-items: center;\n column-gap: 6px;\n position: relative;\n padding: 6px;\n border: 1px solid var(--pc-surface-active);\n background: var(--pc-surface);\n}\n.name.active {\n font-weight: bold;\n}\n.thumbnail {\n width: 30px;\n height: 30px;\n}\n/* Thumbnail on the left and name on the right; under them, opacity on the left and blend mode on the right. The blend\n mode gets a set share of the width rather than its own: browsers size dropdowns differently (Safari and Firefox as\n wide as the longest option, Chrome as the chosen one), which would otherwise push the row wider than its panel */\n.details {\n display: grid;\n grid-template-columns: 30px minmax(0, 1fr) minmax(0, 2fr);\n align-items: center;\n gap: 4px 6px;\n min-width: 0;\n}\n.details .name {\n grid-column: 2 / -1;\n}\n.details .name,\n.details select {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n}\n.details input[type=\"range\"] {\n grid-column: 1 / 3;\n min-width: 0;\n margin: 0;\n}\n/* The reorder handle drags with mouse, pen or touch (touch drags it rather than scrolling the panel) */\n.handle {\n /* As tall as the row */\n align-self: stretch;\n}\n.handle::part(button) {\n min-width: 0;\n padding: 0;\n cursor: grab;\n touch-action: none;\n}\n/* While dragged: the row floats with the pointer, out of the list (the layers menu places it), and the cursor shows\n what dropping would do (\"+\" duplicates on \"New Layer\") */\n:host([dragging]) {\n position: fixed;\n z-index: 10;\n opacity: 0.9;\n background: Canvas;\n box-shadow: var(--pc-shadow);\n}\n:host([dragging]) .handle::part(button) {\n cursor: grabbing;\n}\n:host([drop-action=\"duplicate\"]) .handle::part(button) {\n cursor: copy;\n}\n:host([drop-action=\"none\"]) .handle::part(button) {\n cursor: no-drop;\n}\n/* Alpha lock and visibility: boxed icon toggles like the settings bar's, darker while on */\n.alpha-lock,\n.visibility {\n width: 30px;\n height: 30px;\n padding: 2px;\n border: 1px solid var(--pc-border);\n border-radius: 4px;\n background: var(--pc-surface);\n cursor: pointer;\n}\n.alpha-lock:hover,\n.visibility:hover {\n background: var(--pc-surface-hover);\n}\n.alpha-lock[aria-pressed=\"true\"],\n.visibility[aria-pressed=\"true\"] {\n background: var(--pc-surface-active);\n}\n.alpha-lock:focus-visible,\n.visibility:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n}\n.alpha-lock img,\n.visibility img {\n display: block;\n width: 24px;\n height: 24px;\n margin: auto;\n}\n";
2
+ var style_default = "/* A white row with space around its parts. The middle column can shrink, so the row never grows wider than its panel */\n:host {\n display: grid;\n grid-template-columns: 30px minmax(0, 1fr) 30px 30px;\n align-items: center;\n column-gap: 6px;\n position: relative;\n padding: 6px;\n border: 1px solid var(--pc-surface-active);\n background: var(--pc-surface);\n}\n/* The selected layer: purple, so it stands out */\n:host([active]) {\n border-color: var(--pc-accent);\n background: var(--pc-accent);\n color: var(--pc-accent-text);\n}\n:host([active]) .name {\n font-weight: bold;\n}\n/* The opacity slider's track and handle in the row's text colour, so they show on purple */\n:host([active]) .opacity {\n --pc-accent: var(--pc-accent-text);\n --pc-border: rgb(255 255 255 / 45%);\n}\n/* The name, on one line (cut short with \"…\"); renamed from the actions menu */\n.name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.thumbnail {\n width: 30px;\n height: 30px;\n}\n/* Thumbnail on the left and name on the right; under them, opacity, then blend mode, each the full width. The blend\n mode never sets its own width: browsers size dropdowns differently (Safari and Firefox as wide as the longest option,\n Chrome as the chosen one), which would otherwise push the row wider than its panel */\n.details {\n display: grid;\n grid-template-columns: 30px minmax(0, 1fr);\n align-items: center;\n gap: 4px 6px;\n min-width: 0;\n}\n.details .opacity,\n.details select {\n grid-column: 1 / -1;\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n}\n/* The reorder handle drags with mouse, pen or touch (touch drags it rather than scrolling the panel) */\n.handle {\n /* As tall as the row */\n align-self: stretch;\n}\n.handle::part(button) {\n min-width: 0;\n padding: 0;\n cursor: grab;\n touch-action: none;\n}\n/* While dragged: the row floats with the pointer, out of the list (the layers menu places it), and the cursor shows\n what dropping would do (\"+\" duplicates on \"New Layer\") */\n:host([dragging]) {\n position: fixed;\n z-index: 10;\n opacity: 0.9;\n background: Canvas;\n box-shadow: var(--pc-shadow);\n}\n:host([dragging]) .handle::part(button) {\n cursor: grabbing;\n}\n:host([drop-action=\"duplicate\"]) .handle::part(button) {\n cursor: copy;\n}\n:host([drop-action=\"none\"]) .handle::part(button) {\n cursor: no-drop;\n}\n/* Alpha lock and visibility: boxed icon toggles like the settings bar's, darker while on */\n.alpha-lock,\n.visibility {\n width: 30px;\n height: 30px;\n padding: 2px;\n border: 1px solid var(--pc-border);\n border-radius: 4px;\n background: var(--pc-surface);\n cursor: pointer;\n}\n.alpha-lock:hover,\n.visibility:hover {\n background: var(--pc-surface-hover);\n}\n.alpha-lock[aria-pressed=\"true\"],\n.visibility[aria-pressed=\"true\"] {\n background: var(--pc-surface-active);\n}\n.alpha-lock:focus-visible,\n.visibility:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n}\n.alpha-lock img,\n.visibility img {\n display: block;\n width: 24px;\n height: 24px;\n margin: auto;\n}\n";
3
3
  export { style_default as default };
@@ -94,9 +94,10 @@ var PcSliderRange = class extends PcElement {
94
94
  border-radius: var(--pc-radius-pill);
95
95
  transform: translateY(-50%);
96
96
  }
97
+ /* The whole range: the same area the fill covers from min to max */
97
98
  .rail {
98
- left: calc(var(--pc-range-thumb) / -2);
99
- right: calc(var(--pc-range-thumb) / -2);
99
+ left: 0;
100
+ right: 0;
100
101
  background: var(--pc-border);
101
102
  }
102
103
  .fill {
@@ -1,10 +1,15 @@
1
1
  import { PcElement } from "../../base";
2
+ /** Sizes of the switch: `m` (20px high, the default) and `s` (16px high, e.g. in toolbars) */
3
+ export declare const TOGGLE_SIZES: readonly ["m", "s"];
4
+ export type ToggleSize = (typeof TOGGLE_SIZES)[number];
2
5
  /**
3
- * An on/off switch with its text (the element's content) beside it, e.g. for settings. Fires `change` when the user
4
- * turns it on or off.
6
+ * An on/off switch with its text (the element's content) beside it, e.g. for settings. Use it instead of plain
7
+ * checkboxes. It comes in two sizes (`size`: `m`, 20px high, by default, or `s`, 16px high), both pressable over at
8
+ * least 24px of height. Fires `change` when the user turns it on or off.
5
9
  *
6
10
  * @example
7
11
  * html`<pc-toggle-checkbox .checked=${pinchZoom} @change=${toggle}>Pinch to zoom</pc-toggle-checkbox>`
12
+ * html`<pc-toggle-checkbox size="s">Show stroke points</pc-toggle-checkbox>`
8
13
  */
9
14
  export declare class PcToggleCheckbox extends PcElement {
10
15
  static properties: {
@@ -16,6 +21,10 @@ export declare class PcToggleCheckbox extends PcElement {
16
21
  type: BooleanConstructor;
17
22
  reflect: boolean;
18
23
  };
24
+ size: {
25
+ type: StringConstructor;
26
+ reflect: boolean;
27
+ };
19
28
  translations: {
20
29
  attribute: boolean;
21
30
  };
@@ -30,6 +39,8 @@ export declare class PcToggleCheckbox extends PcElement {
30
39
  static styles: import("lit").CSSResultGroup[];
31
40
  checked: boolean;
32
41
  disabled: boolean;
42
+ /** The switch's size: `m` (default) or `s` */
43
+ size: ToggleSize;
33
44
  constructor();
34
45
  /** Follow the user's change, and tell listeners */
35
46
  private onChange;