@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
@@ -1,15 +1,41 @@
1
1
  import { PcElement, defineElement, emit } from "../../base/index.js";
2
+ import { PcMenu } from "../PcMenu/index.js";
2
3
  import { css, html, nothing } from "lit";
3
4
  // src/ui/elements/PcContextMenu/index.ts
5
+ /** Space kept between a context menu's menu and the window's edges */
6
+ var MENU_MARGIN = 8;
7
+ /** Event fired when a context menu opens or closes (detail: `{ open }`) */
8
+ var CONTEXT_MENU_TOGGLE_EVENT = "pc-toggle";
4
9
  /**
5
- * A menu of actions, e.g. for a layer. While `open`, it shows its items at `x`, `y` (in the page's viewport) and takes
6
- * keyboard focus: arrow keys, Home and End move between items, Enter or Space chooses one, and Escape or Tab closes it.
7
- * Fires `pc-select` with the chosen item's value, and `pc-close` when it should close (after choosing, Escape, Tab, or a
8
- * press outside it). The implementer sets `open` to false in response.
10
+ * Where a menu's top goes: level with its anchor, moved up as far as needed to stay in the window (but never above its
11
+ * top margin).
9
12
  *
10
13
  * @example
11
- * html`<pc-context-menu open .x=${10} .y=${20} label="Layer actions" .items=${items}
12
- * @pc-select=${(e) => act(e.detail.value)} @pc-close=${close}></pc-context-menu>`
14
+ * menuTop(700, 300, 800); // 492 (moved up to fit)
15
+ * menuTop(100, 300, 800); // 100
16
+ */
17
+ var menuTop = (anchorTop, menuHeight, viewportHeight, margin = 8) => Math.max(margin, Math.min(anchorTop, viewportHeight - margin - menuHeight));
18
+ /**
19
+ * Where a menu's left edge goes: at `left`, moved left as far as needed to stay in the window (but never past its left
20
+ * margin).
21
+ *
22
+ * @example
23
+ * menuLeft(900, 200, 1000); // 792 (moved left to fit)
24
+ */
25
+ var menuLeft = (left, menuWidth, viewportWidth, margin = 8) => Math.max(margin, Math.min(left, viewportWidth - margin - menuWidth));
26
+ /**
27
+ * A button that opens a menu (`pc-menu`): its `icon` slot is what the button shows (⋮ by default), and its `menu` slot
28
+ * takes the `pc-menu` to open. Without one, it opens its own menu of `items`. Pressing the button (or right-clicking
29
+ * it) opens the menu under it (or beside it, with `placement="beside"`), kept within the window; choosing an item,
30
+ * pressing outside, Escape, Tab, scrolling or pressing the button again closes it, and focus returns to the button.
31
+ *
32
+ * Fires `pc-toggle` (detail: `{ open }`) when it opens or closes. The menu's `pc-select` (detail: `{ value }`) reaches
33
+ * the context menu's listeners.
34
+ *
35
+ * @example
36
+ * html`<pc-context-menu label="Layer actions" .items=${items} @pc-select=${(e) => act(e.detail.value)}>
37
+ * <pc-icon slot="icon" name="menu"></pc-icon>
38
+ * </pc-context-menu>`
13
39
  */
14
40
  var PcContextMenu = class extends PcElement {
15
41
  static properties = {
@@ -20,129 +46,134 @@ var PcContextMenu = class extends PcElement {
20
46
  },
21
47
  items: { attribute: false },
22
48
  label: { type: String },
23
- x: { type: Number },
24
- y: { type: Number }
49
+ menuLabel: {
50
+ type: String,
51
+ attribute: "menu-label"
52
+ },
53
+ placement: { type: String },
54
+ disabled: {
55
+ type: Boolean,
56
+ reflect: true
57
+ }
25
58
  };
26
59
  static styles = [PcElement.styles, css`
27
60
  :host {
28
- position: fixed;
29
- z-index: 10;
30
- display: none;
31
- }
32
- :host([open]) {
33
- display: block;
34
- }
35
- [role="menu"] {
36
- display: flex;
37
- min-width: 160px;
38
- flex-direction: column;
39
- padding: var(--pc-space-1) 0;
40
- border-radius: var(--pc-radius);
41
- background: var(--pc-surface-raised);
42
- box-shadow: var(--pc-shadow);
61
+ display: inline-flex;
43
62
  }
44
63
  button {
45
- padding: var(--pc-space-2) var(--pc-space-3);
46
- border: 0;
47
- background: transparent;
64
+ display: inline-flex;
65
+ flex: 1;
66
+ align-items: center;
67
+ justify-content: center;
68
+ min-width: var(--pc-control-size);
69
+ min-height: var(--pc-control-size);
70
+ padding: 0 var(--pc-space-1);
71
+ border: 1px solid var(--pc-border);
72
+ border-radius: var(--pc-radius-small);
73
+ background: var(--pc-surface);
48
74
  color: var(--pc-text);
49
- font-family: var(--pc-font);
50
- font-size: var(--pc-font-size);
51
- text-align: start;
75
+ font: inherit;
52
76
  cursor: pointer;
53
77
  }
54
78
  button:hover:not(:disabled),
55
- button:focus-visible {
79
+ :host([open]) button {
56
80
  background: var(--pc-surface-hover);
57
- outline: none;
58
81
  }
59
82
  button:focus-visible {
60
- box-shadow: inset 0 0 0 var(--pc-focus-width) var(--pc-focus);
61
- }
62
- button.danger {
63
- color: var(--pc-dismiss-text);
83
+ outline: var(--pc-focus-width) solid var(--pc-focus);
64
84
  }
65
85
  button:disabled {
66
- color: var(--pc-text-muted);
86
+ opacity: 0.4;
67
87
  cursor: default;
68
88
  }
69
89
  `];
70
- onPointerDown = (event) => {
71
- if (this.open && !event.composedPath().includes(this)) this.close();
72
- };
90
+ onScroll = () => this.setOpen(false);
73
91
  constructor() {
74
92
  super();
75
93
  this.open = false;
76
94
  this.items = [];
77
95
  this.label = "";
78
- this.x = 0;
79
- this.y = 0;
80
- }
81
- connectedCallback() {
82
- super.connectedCallback();
83
- document.addEventListener("pointerdown", this.onPointerDown, true);
96
+ this.menuLabel = "";
97
+ this.placement = "below";
98
+ this.disabled = false;
84
99
  }
85
100
  disconnectedCallback() {
86
101
  super.disconnectedCallback();
87
- document.removeEventListener("pointerdown", this.onPointerDown, true);
102
+ window.removeEventListener("scroll", this.onScroll, true);
88
103
  }
89
- updated(changed) {
90
- this.style.left = `${this.x}px`;
91
- this.style.top = `${this.y}px`;
92
- if (changed.has("open") && this.open) this.buttons()[0]?.focus();
104
+ /** The menu it opens: the slotted `pc-menu`, or its own */
105
+ get menu() {
106
+ return this.renderRoot.querySelector("slot[name=\"menu\"]")?.assignedElements({ flatten: true }).find((element) => element instanceof PcMenu) ?? void 0;
93
107
  }
94
- /** The enabled items' buttons */
95
- buttons() {
96
- return [...this.renderRoot.querySelectorAll("button")].filter((button) => !button.disabled);
108
+ /** The button that opens the menu */
109
+ get button() {
110
+ return this.renderRoot.querySelector("button");
97
111
  }
98
- /** Tell listeners the menu should close */
99
- close() {
100
- emit(this, "pc-close");
112
+ /**
113
+ * Open or close the menu, telling listeners.
114
+ */
115
+ setOpen(open) {
116
+ if (open === this.open || open && this.disabled) return;
117
+ this.open = open;
118
+ emit(this, CONTEXT_MENU_TOGGLE_EVENT, { open });
101
119
  }
102
- /** Choose an item, then close */
103
- choose(item) {
104
- emit(this, "pc-select", { value: item.value });
105
- this.close();
120
+ /** Open the menu on a right-click (instead of the browser's), or close it */
121
+ onContextMenu(event) {
122
+ event.preventDefault();
123
+ this.setOpen(!this.open);
106
124
  }
107
- /** Move focus between items, or close */
108
- onKeyDown(event) {
109
- const buttons = this.buttons();
110
- const index = buttons.indexOf(this.renderRoot.activeElement);
111
- const last = buttons.length - 1;
112
- const target = {
113
- ArrowDown: index >= last ? 0 : index + 1,
114
- ArrowUp: index <= 0 ? last : index - 1,
115
- Home: 0,
116
- End: last
117
- }[event.key];
118
- if (target !== void 0) {
119
- event.preventDefault();
120
- buttons[target]?.focus();
121
- } else if (event.key === "Escape" || event.key === "Tab") {
122
- event.preventDefault();
123
- this.close();
124
- }
125
+ /** Close the menu when it asks to, and return focus to the button */
126
+ onClose() {
127
+ const focusWasInside = this.menu.matches(":focus-within");
128
+ this.setOpen(false);
129
+ if (focusWasInside) this.button.focus();
130
+ }
131
+ /** Set up a newly slotted menu like the current one (open or closed) */
132
+ onMenuSlotChange() {
133
+ this.requestUpdate("open", !this.open);
134
+ }
135
+ updated(changed) {
136
+ const menu = this.menu;
137
+ if (!menu) return;
138
+ menu.anchor = this;
139
+ menu.label ||= this.menuLabel || this.label;
140
+ if (!changed.has("open")) return;
141
+ window[this.open ? "addEventListener" : "removeEventListener"]("scroll", this.onScroll, true);
142
+ menu.open = this.open;
143
+ if (this.open) this.place(menu);
144
+ }
145
+ /** Put the open menu under or beside the button, within the window */
146
+ async place(menu) {
147
+ const anchor = this.button.getBoundingClientRect();
148
+ const beside = this.placement === "beside";
149
+ menu.x = beside ? anchor.right : anchor.left;
150
+ menu.y = beside ? anchor.top : anchor.bottom;
151
+ await menu.updateComplete;
152
+ menu.x = menuLeft(menu.x, menu.offsetWidth, window.innerWidth);
153
+ menu.y = menuTop(menu.y, menu.offsetHeight, window.innerHeight);
125
154
  }
126
155
  render() {
127
- return html`<div
128
- part="menu"
129
- role="menu"
130
- aria-label=${this.label}
131
- @keydown=${this.onKeyDown}
132
- >
133
- ${this.items.map((item) => html`<button
134
- part="item"
135
- type="button"
136
- role="menuitem"
137
- class=${item.danger ? "danger" : nothing}
138
- ?disabled=${item.disabled}
139
- data-value=${item.value}
140
- @click=${() => this.choose(item)}
141
- >
142
- ${item.label}
143
- </button>`)}
144
- </div>`;
156
+ return html`<button
157
+ type="button"
158
+ part="button"
159
+ aria-haspopup="menu"
160
+ aria-expanded=${String(this.open)}
161
+ aria-label=${this.label || nothing}
162
+ ?disabled=${this.disabled}
163
+ @click=${() => this.setOpen(!this.open)}
164
+ @contextmenu=${this.onContextMenu}
165
+ >
166
+ <slot name="icon">⋮</slot></button
167
+ ><slot
168
+ name="menu"
169
+ @slotchange=${this.onMenuSlotChange}
170
+ @pc-close=${this.onClose}
171
+ >${this.items.length ? html`<pc-menu
172
+ .items=${this.items}
173
+ label=${this.menuLabel || this.label}
174
+ ></pc-menu>` : nothing}</slot
175
+ >`;
145
176
  }
146
177
  };
147
178
  defineElement("pc-context-menu", PcContextMenu);
148
- export { PcContextMenu };
179
+ export { CONTEXT_MENU_TOGGLE_EVENT, MENU_MARGIN, PcContextMenu, menuLeft, menuTop };
@@ -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 "../PcContextMenu";
6
+ import { MENU_MARGIN, menuTop } from "../PcContextMenu";
5
7
  import { type DialogChoice } from "../PcDialog";
6
8
  import type BackgroundWork from "../../../lib/classes/BackgroundWork";
7
9
  /** Blend mode options shown in the layer's select input */
@@ -56,17 +58,8 @@ export interface LayerDragDetail {
56
58
  x: number;
57
59
  y: number;
58
60
  }
59
- /** Space kept between the layer actions menu and the window's edges */
60
- export declare const MENU_MARGIN = 8;
61
- /**
62
- * Where the layer actions menu's top goes: level with the handle, moved up as far as needed to stay in the window
63
- * (but never above its top margin).
64
- *
65
- * @example
66
- * menuTop(700, 300, 800); // 492 (moved up to fit)
67
- * menuTop(100, 300, 800); // 100
68
- */
69
- export declare const menuTop: (anchorTop: number, menuHeight: number, viewportHeight: number, margin?: number) => number;
61
+ /** Where the layer actions menu goes and how close to the window's edges (see `pc-context-menu`) */
62
+ export { MENU_MARGIN, menuTop };
70
63
  /**
71
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.
72
65
  * Dragging the handle (with mouse, pen or touch) reorders the layer: the row fires {@link LAYER_DRAG_EVENT} and the
@@ -141,28 +134,22 @@ export declare class PcLayerItem extends PcElement {
141
134
  runAction(action: LayerAction): void;
142
135
  /**
143
136
  * Whether a context menu action can be done to the layer now: not merging down the bottom layer (or with a hidden
144
- * layer), deleting the only layer, or moving a layer further up or down than it can go.
137
+ * layer), deleting the only layer, moving a layer further up or down than it can go, or anything to a layer no longer
138
+ * in the stack (e.g. just deleted).
145
139
  */
146
140
  canRun(action: LayerAction): boolean;
147
141
  /**
148
142
  * Do an action the user confirmed.
149
143
  */
150
144
  confirmed(action: ConfirmedAction): void;
151
- private readonly onOutsidePointer;
152
- private readonly onKeyDown;
153
- /**
154
- * Open the context menu on right-click, or close it if open.
155
- */
156
- private onContextMenu;
157
145
  /**
158
- * Open the context menu on a click or tap on the handle (or Enter or Space), or close it if open; a click that ends a
159
- * drag does nothing.
146
+ * Follow the context menu opening or closing, telling other rows when it opens (so they close theirs).
160
147
  */
161
- private onHandleClick;
148
+ private onMenuToggle;
162
149
  /**
163
- * Open the context menu (closing other rows' menus), or close it if open.
150
+ * A click on the handle opens the menu, unless it ends a drag (then it's stopped before reaching the menu's button).
164
151
  */
165
- private toggleMenu;
152
+ private readonly onHandleClick;
166
153
  disconnectedCallback(): void;
167
154
  /** Tell the layers menu about a drag of this row's layer */
168
155
  private emitDrag;
@@ -188,16 +175,6 @@ export declare class PcLayerItem extends PcElement {
188
175
  private readonly onDragKeyDown;
189
176
  /** Stop following the pointer pressed on the handle */
190
177
  private endPress;
191
- /**
192
- * Start or stop listening for clicks outside and Escape while the menu is open.
193
- */
194
- private listenOutside;
195
- private readonly onScroll;
196
- /**
197
- * Place the open layer actions menu beside the reorder handle, floating over the layers panel (so the panel's edges
198
- * don't cut it off) and kept within the window.
199
- */
200
- private placeMenu;
201
178
  /**
202
179
  * Ask for a mask following the layer's alpha (Shift adds, Alt subtracts; otherwise it replaces the mask).
203
180
  */
@@ -210,12 +187,14 @@ export declare class PcLayerItem extends PcElement {
210
187
  * Rename a layer when the name input changes. Empty names are reverted.
211
188
  */
212
189
  private onRename;
213
- updated(changed: Map<PropertyKey, unknown>): void;
190
+ updated(): void;
214
191
  /**
215
192
  * Draw the layer's image, scaled to fit, on a checkerboard that shows transparency.
216
193
  */
217
194
  drawThumbnail(): void;
218
195
  /** The dialog asking the user to confirm an action, while asking */
219
196
  private renderConfirmation;
197
+ /** The layer's actions, for its context menu (those it can't do now are disabled) */
198
+ private menuItems;
220
199
  render(): import("lit").TemplateResult<1>;
221
200
  }
@@ -5,6 +5,7 @@ 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 { MENU_MARGIN, menuTop } from "../PcContextMenu/index.js";
8
9
  import { html, nothing, unsafeCSS } from "lit";
9
10
  // src/ui/elements/PcLayerItem/index.ts
10
11
  /** Blend mode options shown in the layer's select input */
@@ -127,17 +128,6 @@ var LAYER_ALPHA_MASK_EVENT = "pc-layer-alpha-mask";
127
128
  var DRAG_THRESHOLD = 4;
128
129
  /** Event a row dispatches while its layer is dragged by its reorder handle (detail: {@link LayerDragDetail}) */
129
130
  var LAYER_DRAG_EVENT = "pc-layer-drag";
130
- /** Space kept between the layer actions menu and the window's edges */
131
- var MENU_MARGIN = 8;
132
- /**
133
- * Where the layer actions menu's top goes: level with the handle, moved up as far as needed to stay in the window
134
- * (but never above its top margin).
135
- *
136
- * @example
137
- * menuTop(700, 300, 800); // 492 (moved up to fit)
138
- * menuTop(100, 300, 800); // 100
139
- */
140
- var menuTop = (anchorTop, menuHeight, viewportHeight, margin = 8) => Math.max(margin, Math.min(anchorTop, viewportHeight - margin - menuHeight));
141
131
  /**
142
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.
143
133
  * Dragging the handle (with mouse, pen or touch) reorders the layer: the row fires {@link LAYER_DRAG_EVENT} and the
@@ -220,11 +210,12 @@ var PcLayerItem = class extends PcElement {
220
210
  }
221
211
  /**
222
212
  * Whether a context menu action can be done to the layer now: not merging down the bottom layer (or with a hidden
223
- * layer), deleting the only layer, or moving a layer further up or down than it can go.
213
+ * layer), deleting the only layer, moving a layer further up or down than it can go, or anything to a layer no longer
214
+ * in the stack (e.g. just deleted).
224
215
  */
225
216
  canRun(action) {
226
217
  const { stack, layer } = this;
227
- if (!stack || !layer) return false;
218
+ if (!stack || !layer || !stack.layers.includes(layer)) return false;
228
219
  const index = stack.indexOf(layer.id);
229
220
  const last = stack.layers.length - 1;
230
221
  switch (action) {
@@ -248,43 +239,30 @@ var PcLayerItem = class extends PcElement {
248
239
  else if (action === "clear") stack.clear(layer.id);
249
240
  else stack.remove(layer.id);
250
241
  }
251
- onOutsidePointer = (event) => {
252
- if (!event.composedPath().includes(this)) this.menuOpen = false;
253
- };
254
- onKeyDown = (event) => {
255
- if (event.key === "Escape") this.menuOpen = false;
256
- };
257
242
  /**
258
- * Open the context menu on right-click, or close it if open.
243
+ * Follow the context menu opening or closing, telling other rows when it opens (so they close theirs).
259
244
  */
260
- onContextMenu(event) {
261
- event.preventDefault();
262
- this.toggleMenu();
263
- }
264
- /**
265
- * Open the context menu on a click or tap on the handle (or Enter or Space), or close it if open; a click that ends a
266
- * drag does nothing.
267
- */
268
- onHandleClick() {
269
- if (this.suppressClick) {
270
- this.suppressClick = false;
271
- return;
272
- }
273
- this.toggleMenu();
274
- }
275
- /**
276
- * Open the context menu (closing other rows' menus), or close it if open.
277
- */
278
- toggleMenu() {
279
- this.menuOpen = !this.menuOpen;
280
- if (this.menuOpen) this.dispatchEvent(new CustomEvent(LAYER_MENU_OPEN_EVENT, {
245
+ onMenuToggle(event) {
246
+ this.menuOpen = event.detail.open;
247
+ if (!this.menuOpen) return;
248
+ this.dispatchEvent(new CustomEvent(LAYER_MENU_OPEN_EVENT, {
281
249
  bubbles: true,
282
250
  composed: true
283
251
  }));
284
252
  }
253
+ /**
254
+ * A click on the handle opens the menu, unless it ends a drag (then it's stopped before reaching the menu's button).
255
+ */
256
+ onHandleClick = {
257
+ handleEvent: (event) => {
258
+ if (!this.suppressClick) return;
259
+ this.suppressClick = false;
260
+ event.stopPropagation();
261
+ },
262
+ capture: true
263
+ };
285
264
  disconnectedCallback() {
286
265
  super.disconnectedCallback();
287
- this.listenOutside(false);
288
266
  this.endPress();
289
267
  }
290
268
  /** Tell the layers menu about a drag of this row's layer */
@@ -313,7 +291,7 @@ var PcLayerItem = class extends PcElement {
313
291
  y: event.clientY,
314
292
  dragging: false
315
293
  };
316
- event.currentTarget.setPointerCapture(event.pointerId);
294
+ event.composedPath()[0].setPointerCapture(event.pointerId);
317
295
  window.addEventListener("pointermove", this.onHandlePointerMove);
318
296
  window.addEventListener("pointerup", this.onHandlePointerUp);
319
297
  window.addEventListener("pointercancel", this.onHandlePointerCancel);
@@ -368,29 +346,6 @@ var PcLayerItem = class extends PcElement {
368
346
  window.removeEventListener("pointercancel", this.onHandlePointerCancel);
369
347
  }
370
348
  /**
371
- * Start or stop listening for clicks outside and Escape while the menu is open.
372
- */
373
- listenOutside(listen) {
374
- const method = listen ? "addEventListener" : "removeEventListener";
375
- window[method]("pointerdown", this.onOutsidePointer, true);
376
- window[method]("keydown", this.onKeyDown);
377
- window[method]("scroll", this.onScroll, true);
378
- }
379
- onScroll = () => {
380
- this.menuOpen = false;
381
- };
382
- /**
383
- * Place the open layer actions menu beside the reorder handle, floating over the layers panel (so the panel's edges
384
- * don't cut it off) and kept within the window.
385
- */
386
- placeMenu() {
387
- const menu = this.renderRoot.querySelector("menu");
388
- if (!menu) return;
389
- const anchor = this.renderRoot.querySelector(".handle").getBoundingClientRect();
390
- menu.style.left = `${anchor.right}px`;
391
- menu.style.top = `${menuTop(anchor.top, menu.offsetHeight, window.innerHeight)}px`;
392
- }
393
- /**
394
349
  * Ask for a mask following the layer's alpha (Shift adds, Alt subtracts; otherwise it replaces the mask).
395
350
  */
396
351
  requestMask(layer, keys) {
@@ -426,11 +381,7 @@ var PcLayerItem = class extends PcElement {
426
381
  input.value = layer.name;
427
382
  }
428
383
  }
429
- updated(changed) {
430
- if (changed.has("menuOpen")) {
431
- this.listenOutside(this.menuOpen);
432
- if (this.menuOpen) this.placeMenu();
433
- }
384
+ updated() {
434
385
  const id = this.layer?.id;
435
386
  if (this.background && id) this.background.run(`thumbnail ${id}`, () => this.drawThumbnail());
436
387
  else this.drawThumbnail();
@@ -472,23 +423,32 @@ var PcLayerItem = class extends PcElement {
472
423
  }}
473
424
  ></pc-dialog>`;
474
425
  }
426
+ /** The layer's actions, for its context menu (those it can't do now are disabled) */
427
+ menuItems() {
428
+ return LAYER_ACTIONS.map(({ action, label }) => ({
429
+ value: action,
430
+ label: this.t(label),
431
+ disabled: !this.canRun(action)
432
+ }));
433
+ }
475
434
  render() {
476
435
  const { stack, layer } = this;
477
436
  if (!stack || !layer) return html``;
478
437
  const active = stack.active.id === layer.id;
479
438
  return html`
480
- <button
481
- type="button"
439
+ <pc-context-menu
482
440
  class="handle"
483
- aria-label=${this.t("paintCanvas.layers.reorder", { name: layer.name })}
484
- aria-haspopup="menu"
485
- aria-expanded=${this.menuOpen ? "true" : "false"}
486
- @contextmenu=${this.onContextMenu}
441
+ placement="beside"
442
+ label=${this.t("paintCanvas.layers.reorder", { name: layer.name })}
443
+ menu-label=${this.t("paintCanvas.layers.actions")}
444
+ .open=${this.menuOpen}
445
+ .items=${this.menuItems()}
446
+ @pc-toggle=${this.onMenuToggle}
447
+ @pc-select=${(e) => this.runAction(e.detail.value)}
487
448
  @click=${this.onHandleClick}
488
449
  @pointerdown=${this.onHandlePointerDown}
450
+ ><span slot="icon" aria-hidden="true">⠿</span></pc-context-menu
489
451
  >
490
- ⠿
491
- </button>
492
452
  <div
493
453
  class="details"
494
454
  @click=${(e) => this.onRowClick(e, stack, layer)}
@@ -541,18 +501,6 @@ var PcLayerItem = class extends PcElement {
541
501
  >
542
502
  <img src=${this.iconUrl(layer.visible ? "visible" : "hidden")} alt="" />
543
503
  </button>
544
- ${this.menuOpen ? html`<menu aria-label=${this.t("paintCanvas.layers.actions")}>
545
- ${LAYER_ACTIONS.map(({ action, label }) => html`<li>
546
- <button
547
- type="button"
548
- data-action=${action}
549
- ?disabled=${!this.canRun(action)}
550
- @click=${() => this.runAction(action)}
551
- >
552
- ${this.t(label)}
553
- </button>
554
- </li>`)}
555
- </menu>` : nothing}
556
504
  ${this.renderConfirmation(layer)}
557
505
  `;
558
506
  }
@@ -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 */\n:host {\n display: grid;\n grid-template-columns: 30px 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 */\n.details {\n display: grid;\n grid-template-columns: 30px minmax(0, 1fr) auto;\n align-items: center;\n gap: 4px 6px;\n}\n.details .name {\n grid-column: 2 / -1;\n min-width: 0;\n}\n.details input[type=\"range\"] {\n grid-column: 1 / 3;\n min-width: 0;\n margin: 0;\n}\n/* Like the chat's ⋮ menu: white and rounded, with plain items highlighted on hover. It floats beside the handle (placed\n by `placeMenu`), so the layers panel's edges don't cut it off */\nmenu {\n position: fixed;\n z-index: 20;\n min-width: 160px;\n margin: 0;\n padding: 4px 0;\n border-radius: 12px;\n list-style: none;\n background: var(--pc-surface);\n color: var(--pc-text);\n box-shadow: var(--pc-shadow);\n}\nmenu button {\n width: 100%;\n min-height: 32px;\n padding: 4px 16px;\n border: 0;\n background: none;\n color: inherit;\n font: inherit;\n text-align: left;\n white-space: nowrap;\n cursor: pointer;\n}\nmenu button:hover:not(:disabled),\nmenu button:focus-visible {\n background: var(--pc-bar);\n}\n/* Actions the layer can't take (e.g. moving the top layer up) */\nmenu button:disabled {\n opacity: 0.4;\n cursor: default;\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 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 {\n cursor: grabbing;\n}\n:host([drop-action=\"duplicate\"]) .handle {\n cursor: copy;\n}\n:host([drop-action=\"none\"]) .handle {\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.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";
3
3
  export { style_default as default };