@lvl99dotcom/paintcanvas 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/brushes/StrokeEngine/index.d.ts +2 -2
  3. package/dist/lib/brushes/StrokeEngine/index.js +26 -28
  4. package/dist/lib/classes/BrushSettings/index.d.ts +24 -0
  5. package/dist/lib/classes/BrushSettings/index.js +35 -1
  6. package/dist/lib/classes/KeyboardShortcuts/index.d.ts +17 -6
  7. package/dist/lib/classes/KeyboardShortcuts/index.js +81 -18
  8. package/dist/lib/classes/PaintCanvas/index.d.ts +77 -27
  9. package/dist/lib/classes/PaintCanvas/index.js +143 -42
  10. package/dist/lib/classes/PaintCanvas/tool.d.ts +15 -0
  11. package/dist/lib/classes/PaintCanvas/tool.js +18 -0
  12. package/dist/lib/classes/Palette/index.d.ts +58 -0
  13. package/dist/lib/classes/Palette/index.js +92 -0
  14. package/dist/lib/classes/SettingsAutosave/index.d.ts +52 -0
  15. package/dist/lib/classes/SettingsAutosave/index.js +96 -0
  16. package/dist/lib/classes/ShortcutSettings/index.d.ts +63 -0
  17. package/dist/lib/classes/ShortcutSettings/index.js +110 -0
  18. package/dist/lib/palette/index.d.ts +49 -0
  19. package/dist/lib/palette/index.js +82 -0
  20. package/dist/lib/palette/schema.d.ts +45 -0
  21. package/dist/lib/palette/schema.js +41 -0
  22. package/dist/lib/rendering/cursors/index.d.ts +2 -0
  23. package/dist/lib/rendering/cursors/index.js +3 -1
  24. package/dist/lib/rendering/sample/index.js +3 -2
  25. package/dist/lib/settings/index.d.ts +63 -0
  26. package/dist/lib/settings/index.js +91 -0
  27. package/dist/lib/settings/schema.d.ts +150 -0
  28. package/dist/lib/settings/schema.js +102 -0
  29. package/dist/lib/storage/index.d.ts +36 -5
  30. package/dist/lib/storage/index.js +87 -29
  31. package/dist/lib/utils/colour/index.d.ts +72 -0
  32. package/dist/lib/utils/colour/index.js +154 -0
  33. package/dist/lib/utils/shortcuts/index.d.ts +174 -11
  34. package/dist/lib/utils/shortcuts/index.js +391 -145
  35. package/dist/lib/validation/validators.generated.d.ts +5 -0
  36. package/dist/lib/validation/validators.generated.js +872 -1
  37. package/dist/locales/en-GB.js +87 -1
  38. package/dist/locales/en-US.js +87 -1
  39. package/dist/locales/fr.js +87 -1
  40. package/dist/locales/ja.js +87 -1
  41. package/dist/locales/ko.js +87 -1
  42. package/dist/locales/zh-Hans.js +87 -1
  43. package/dist/locales/zh-Hant.js +87 -1
  44. package/dist/package.js +1 -1
  45. package/dist/ui/elements/PaintCanvas/index.d.ts +49 -47
  46. package/dist/ui/elements/PaintCanvas/index.js +128 -169
  47. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  48. package/dist/ui/elements/PcAppSettings/index.d.ts +59 -0
  49. package/dist/ui/elements/PcAppSettings/index.js +222 -0
  50. package/dist/ui/elements/PcBrushConfig/index.js +1 -0
  51. package/dist/ui/elements/PcButton/index.js +1 -0
  52. package/dist/ui/elements/PcCanvas/index.d.ts +1 -1
  53. package/dist/ui/elements/PcCanvas/index.js +7 -5
  54. package/dist/ui/elements/PcColourPair/index.d.ts +43 -0
  55. package/dist/ui/elements/PcColourPair/index.js +125 -0
  56. package/dist/ui/elements/PcContextMenu/index.d.ts +70 -33
  57. package/dist/ui/elements/PcContextMenu/index.js +128 -97
  58. package/dist/ui/elements/PcDialog/index.js +1 -0
  59. package/dist/ui/elements/PcIconToggle/index.js +1 -0
  60. package/dist/ui/elements/PcLayerItem/index.d.ts +13 -34
  61. package/dist/ui/elements/PcLayerItem/index.js +39 -91
  62. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  63. package/dist/ui/elements/PcMenu/index.d.ts +93 -0
  64. package/dist/ui/elements/PcMenu/index.js +204 -0
  65. package/dist/ui/elements/PcPane/index.d.ts +118 -0
  66. package/dist/ui/elements/PcPane/index.js +357 -0
  67. package/dist/ui/elements/PcPanel/index.d.ts +26 -8
  68. package/dist/ui/elements/PcPanel/index.js +76 -8
  69. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.d.ts +20 -6
  70. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.js +20 -15
  71. package/dist/ui/elements/PcPanelBrushes/style.js +3 -0
  72. package/dist/ui/elements/PcPanelColours/index.d.ts +171 -0
  73. package/dist/ui/elements/PcPanelColours/index.js +650 -0
  74. package/dist/ui/elements/PcPanelColours/style.js +3 -0
  75. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.d.ts +17 -4
  76. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.js +17 -10
  77. package/dist/ui/elements/PcPanelLayers/style.js +3 -0
  78. package/dist/ui/elements/PcShortcutSettings/index.d.ts +69 -0
  79. package/dist/ui/elements/PcShortcutSettings/index.js +273 -0
  80. package/dist/ui/elements/PcThemePicker/index.js +1 -0
  81. package/dist/ui/elements/PcToast/index.js +1 -0
  82. package/dist/ui/elements/PcToolSettings/index.js +2 -1
  83. package/dist/ui/elements/PcToolbar/index.d.ts +47 -12
  84. package/dist/ui/elements/PcToolbar/index.js +77 -56
  85. package/dist/ui/elements/PcToolbar/style.js +1 -1
  86. package/dist/ui/index.d.ts +7 -1
  87. package/dist/ui/index.js +8 -2
  88. package/dist/ui/tokens/index.js +3 -1
  89. package/locales/en-GB.json +87 -1
  90. package/locales/en-US.json +87 -1
  91. package/locales/fr.json +87 -1
  92. package/locales/ja.json +87 -1
  93. package/locales/ko.json +87 -1
  94. package/locales/zh-Hans.json +87 -1
  95. package/locales/zh-Hant.json +87 -1
  96. package/package.json +1 -1
  97. package/dist/ui/elements/PcBrushMenu/style.js +0 -3
  98. package/dist/ui/elements/PcLayersMenu/style.js +0 -3
@@ -0,0 +1,222 @@
1
+ import { SETTINGS_FILE_FORMAT } from "../../../lib/settings/schema.js";
2
+ import { ICON_CONTROLS_CSS, PILLS_CSS, WINDOWS_CSS } from "../../styles/index.js";
3
+ import { PcElement, defineElement, emit } from "../../base/index.js";
4
+ import "../PcShortcutSettings/index.js";
5
+ import { SETTINGS_FILE_ERRORS, SettingsFileError, applySettings, collectSettings, exportSettingsFile, parseSettingsFile, settingsFileName } from "../../../lib/settings/index.js";
6
+ import { JSON_FILE_TYPE, saveFile } from "../../../lib/rendering/screenshot/index.js";
7
+ import { css, html, nothing, unsafeCSS } from "lit";
8
+ // src/ui/elements/PcAppSettings/index.ts
9
+ /** Translation keys of the settings file error messages */
10
+ var SETTINGS_FILE_ERROR_KEYS = {
11
+ [SETTINGS_FILE_ERRORS.incompatible]: "paintCanvas.settings.fileIncompatible",
12
+ [SETTINGS_FILE_ERRORS.read]: "paintCanvas.settings.fileReadFailed"
13
+ };
14
+ /**
15
+ * Buttons for the user's app settings: "Keyboard shortcuts", which opens a window to change them (see
16
+ * `pc-shortcut-settings`), and "Import settings" and "Export settings", which read and write a settings JSON file (input
17
+ * settings, keyboard shortcuts, colour palette and colours). The window is a modal `<dialog>` above the whole page; it
18
+ * 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
19
+ * exporting fails.
20
+ *
21
+ * @example
22
+ * html`<pc-app-settings .app=${canvas}></pc-app-settings>`
23
+ */
24
+ var PcAppSettings = class extends PcElement {
25
+ static properties = {
26
+ ...PcElement.properties,
27
+ app: { attribute: false },
28
+ shortcutsOpen: {
29
+ type: Boolean,
30
+ attribute: "shortcuts-open"
31
+ }
32
+ };
33
+ static styles = [
34
+ PcElement.styles,
35
+ unsafeCSS(WINDOWS_CSS),
36
+ unsafeCSS(ICON_CONTROLS_CSS),
37
+ unsafeCSS(PILLS_CSS),
38
+ css`
39
+ :host {
40
+ display: flex;
41
+ flex-direction: column;
42
+ gap: var(--pc-space-2);
43
+ font-family: var(--pc-font);
44
+ font-size: var(--pc-font-size);
45
+ }
46
+ .actions {
47
+ display: flex;
48
+ flex-wrap: wrap;
49
+ gap: var(--pc-space-2);
50
+ }
51
+ /* A modal window in the browser's top layer, above everything on the page */
52
+ dialog {
53
+ box-sizing: border-box;
54
+ width: min(480px, calc(100vw - 2 * var(--pc-space-3)));
55
+ max-height: calc(100vh - 2 * var(--pc-space-3));
56
+ padding: 0;
57
+ border: 0;
58
+ border-radius: var(--pc-radius);
59
+ background: var(--pc-surface-raised);
60
+ color: var(--pc-text);
61
+ box-shadow: var(--pc-shadow);
62
+ }
63
+ dialog::backdrop {
64
+ background: var(--pc-backdrop);
65
+ }
66
+ .window {
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: var(--pc-space-3);
70
+ box-shadow: none;
71
+ }
72
+ .header {
73
+ display: flex;
74
+ gap: var(--pc-space-2);
75
+ align-items: center;
76
+ justify-content: space-between;
77
+ }
78
+ h2 {
79
+ margin: 0;
80
+ font-size: 1.2em;
81
+ }
82
+ `
83
+ ];
84
+ /** Saves exported files (replaceable for tests) */
85
+ saveFile = (blob, name) => saveFile(blob, name, void 0, JSON_FILE_TYPE);
86
+ constructor() {
87
+ super();
88
+ this.shortcutsOpen = false;
89
+ }
90
+ updated(changed) {
91
+ if (!changed.has("shortcutsOpen")) return;
92
+ const dialog = this.renderRoot.querySelector("dialog");
93
+ if (!dialog) return;
94
+ if (this.shortcutsOpen && !dialog.open) dialog.showModal();
95
+ else if (!this.shortcutsOpen && dialog.open) dialog.close();
96
+ }
97
+ /** Close the keyboard shortcuts window */
98
+ closeShortcuts() {
99
+ this.shortcutsOpen = false;
100
+ }
101
+ /**
102
+ * Import the settings in a settings file, using the parts it has.
103
+ */
104
+ async importFile(file) {
105
+ const app = this.app;
106
+ if (!app) return;
107
+ let text;
108
+ try {
109
+ text = await file.text();
110
+ } catch {
111
+ this.toastError(SETTINGS_FILE_ERRORS.read);
112
+ return;
113
+ }
114
+ try {
115
+ applySettings(app, parseSettingsFile(text, [app.names.settingsFileFormat, SETTINGS_FILE_FORMAT]));
116
+ emit(this, "pc-toast", {
117
+ message: this.t("paintCanvas.settings.imported"),
118
+ kind: "success"
119
+ });
120
+ } catch (error) {
121
+ this.toastError(error instanceof SettingsFileError ? error.message : SETTINGS_FILE_ERRORS.read);
122
+ }
123
+ }
124
+ /**
125
+ * Export the settings as a settings file.
126
+ */
127
+ async exportFile() {
128
+ const app = this.app;
129
+ if (!app) return;
130
+ try {
131
+ await this.saveFile(new Blob([exportSettingsFile(collectSettings(app), app.names.settingsFileFormat)], { type: "application/json" }), settingsFileName(void 0, app.names.filePrefix));
132
+ } catch {
133
+ emit(this, "pc-toast", {
134
+ message: this.t("paintCanvas.settings.exportFailed"),
135
+ kind: "error"
136
+ });
137
+ }
138
+ }
139
+ /** Show a settings file error (one of `SETTINGS_FILE_ERRORS`) in the chosen language */
140
+ toastError(message) {
141
+ emit(this, "pc-toast", {
142
+ message: this.t(SETTINGS_FILE_ERROR_KEYS[message]),
143
+ kind: "error"
144
+ });
145
+ }
146
+ /** The keyboard shortcuts window: a modal dialog, with its contents only while open */
147
+ renderShortcuts(app) {
148
+ return html`<dialog
149
+ aria-labelledby="shortcuts-title"
150
+ @close=${() => this.closeShortcuts()}
151
+ @click=${(e) => {
152
+ if (e.target === e.currentTarget) this.closeShortcuts();
153
+ }}
154
+ >
155
+ ${this.shortcutsOpen ? html`<div class="window">
156
+ <div class="header">
157
+ <h2 id="shortcuts-title">
158
+ ${this.t("paintCanvas.shortcuts.title")}
159
+ </h2>
160
+ <button
161
+ type="button"
162
+ class="close dismiss"
163
+ autofocus
164
+ @click=${() => this.closeShortcuts()}
165
+ >
166
+ ${this.t("paintCanvas.common.close")}
167
+ </button>
168
+ </div>
169
+ <pc-shortcut-settings
170
+ .settings=${app.shortcuts}
171
+ ></pc-shortcut-settings>
172
+ </div>` : nothing}
173
+ </dialog>`;
174
+ }
175
+ render() {
176
+ const app = this.app;
177
+ if (!app) return nothing;
178
+ return html`<div class="actions">
179
+ <button
180
+ type="button"
181
+ class="pill shortcuts"
182
+ aria-haspopup="dialog"
183
+ @click=${() => this.shortcutsOpen = true}
184
+ >
185
+ ${this.t("paintCanvas.shortcuts.title")}
186
+ </button>
187
+ <button
188
+ type="button"
189
+ class="pill import"
190
+ @click=${() => this.renderRoot.querySelector(".import-file")?.click()}
191
+ >
192
+ ${this.t("paintCanvas.settings.import")}
193
+ </button>
194
+ <button
195
+ type="button"
196
+ class="pill export"
197
+ @click=${() => void this.exportFile()}
198
+ >
199
+ ${this.t("paintCanvas.settings.export")}
200
+ </button>
201
+ <!-- Chosen through "Import settings" -->
202
+ <label class="visually-hidden">
203
+ ${this.t("paintCanvas.settings.importFile")}
204
+ <input
205
+ type="file"
206
+ class="import-file"
207
+ tabindex="-1"
208
+ accept="application/json,.json"
209
+ @change=${(e) => {
210
+ const input = e.target;
211
+ const file = input.files?.[0];
212
+ input.value = "";
213
+ if (file) this.importFile(file);
214
+ }}
215
+ />
216
+ </label>
217
+ </div>
218
+ ${this.renderShortcuts(app)}`;
219
+ }
220
+ };
221
+ defineElement("pc-app-settings", PcAppSettings);
222
+ export { PcAppSettings };
@@ -2,6 +2,7 @@ import style_default from "./style.js";
2
2
  import { ICON_CONTROLS_CSS, PILLS_CSS, WINDOWS_CSS } from "../../styles/index.js";
3
3
  import { iconLabel } from "../../icons/index.js";
4
4
  import { PcElement, defineElement } from "../../base/index.js";
5
+ import "../PcBrushTest/index.js";
5
6
  import { SmoothingType } from "../../../lib/brushes/types/index.js";
6
7
  import { PARAM_LIMITS, VARIATION_SOURCES, readRange, toggleSource, writeRange } from "../../../lib/brushes/configRanges/index.js";
7
8
  import { SHAPE_ERRORS, ShapeImageError, readClipboardShape, readShapeImage } from "../../../lib/brushes/shapeImage/index.js";
@@ -1,4 +1,5 @@
1
1
  import { PcElement, defineElement } from "../../base/index.js";
2
+ import "../PcIcon/index.js";
2
3
  import { css, html, nothing } from "lit";
3
4
  // src/ui/elements/PcButton/index.ts
4
5
  /**
@@ -301,7 +301,7 @@ export declare class PcCanvas extends PcElement {
301
301
  */
302
302
  private beginSample;
303
303
  /**
304
- * Set the current colour from the sample: the pixel under the pointer, or the average of every pixel in the area.
304
+ * Set the selected colour (foreground or background) from the sample: the pixel under the pointer, or the average of every pixel in the area.
305
305
  * Nothing changes if it's outside the canvas.
306
306
  */
307
307
  private finishSample;
@@ -1,9 +1,10 @@
1
+ import { Tool } from "../../../lib/classes/PaintCanvas/tool.js";
1
2
  import { unionRect } from "../../../lib/brushes/StrokeEngine/index.js";
2
3
  import style_default from "./style.js";
3
4
  import { NO_TEXT_SELECTION_CSS } from "../../styles/index.js";
4
5
  import { PcElement, defineElement } from "../../base/index.js";
5
6
  import ChangeController from "../../ChangeController/index.js";
6
- import { Tool } from "../../../lib/classes/PaintCanvas/index.js";
7
+ import "../../../lib/classes/PaintCanvas/index.js";
7
8
  import { PASTEBOARD_COLOUR, drawScene } from "../../../lib/rendering/compositing/index.js";
8
9
  import { drawBrushCursor } from "../../../lib/rendering/cursor/index.js";
9
10
  import { drawLassoPreview, drawMaskOutline } from "../../../lib/rendering/maskOutline/index.js";
@@ -563,15 +564,16 @@ var PcCanvas = class extends PcElement {
563
564
  layerId: layer.id
564
565
  };
565
566
  event.currentTarget.setPointerCapture(event.pointerId);
567
+ const erasing = app.activeTool === Tool.Eraser;
566
568
  app.strokes.begin({
567
569
  layer,
568
- mode: app.activeTool === Tool.Eraser ? "erase" : "paint",
570
+ mode: erasing ? "erase" : "paint",
569
571
  preset: settings.preset,
570
572
  size: {
571
573
  width: settings.width,
572
574
  height: settings.height
573
575
  },
574
- colour: app.colour,
576
+ colour: app.colours[erasing ? "background" : "foreground"],
575
577
  tip: app.tips.get(settings.preset),
576
578
  isPen: event.pointerType === "pen",
577
579
  mask: app.selection.mask
@@ -812,7 +814,7 @@ var PcCanvas = class extends PcElement {
812
814
  this.drawOverlay();
813
815
  }
814
816
  /**
815
- * Set the current colour from the sample: the pixel under the pointer, or the average of every pixel in the area.
817
+ * Set the selected colour (foreground or background) from the sample: the pixel under the pointer, or the average of every pixel in the area.
816
818
  * Nothing changes if it's outside the canvas.
817
819
  */
818
820
  finishSample() {
@@ -820,7 +822,7 @@ var PcCanvas = class extends PcElement {
820
822
  const { start, point } = this.sampling;
821
823
  const colour = app.sampleMode === "area" ? app.sampleAreaColour(start, point) : app.sampleColour(point);
822
824
  this.endSample();
823
- if (colour) app.setColour(colour);
825
+ if (colour) app.setColour(colour, app.selectedColour);
824
826
  }
825
827
  /** Stop sampling and close the sampling window */
826
828
  endSample() {
@@ -0,0 +1,43 @@
1
+ import { PcElement } from "../../base";
2
+ import type { default as PaintCanvas } from "../../../lib/classes/PaintCanvas";
3
+ /** Event fired when the foreground or background colour is pressed (detail: `{ slot }`) */
4
+ export declare const COLOUR_CLICK_EVENT = "pc-colour-click";
5
+ /**
6
+ * The foreground and background colours as two overlapping circles in a square (the foreground at the top left), the
7
+ * one selected for configuration outlined and on top. Pressing either fires `pc-colour-click` with its `slot`; the
8
+ * implementer decides what it does (e.g. select it and open a colour palette). `openPicker()` opens the browser's colour
9
+ * picker for the selected colour, which sets it. Sized by `--pc-colour-pair-size` (50px by default).
10
+ *
11
+ * @example
12
+ * html`<pc-colour-pair .app=${app} @pc-colour-click=${(e) => app.selectColour(e.detail.slot)}></pc-colour-pair>`
13
+ */
14
+ export declare class PcColourPair extends PcElement {
15
+ static properties: {
16
+ app: {
17
+ attribute: boolean;
18
+ };
19
+ translations: {
20
+ attribute: boolean;
21
+ };
22
+ icons: {
23
+ attribute: boolean;
24
+ };
25
+ theme: {
26
+ type: StringConstructor;
27
+ reflect: boolean;
28
+ };
29
+ };
30
+ static styles: import("lit").CSSResultGroup[];
31
+ app?: PaintCanvas;
32
+ private changes;
33
+ /**
34
+ * Open the browser's colour picker for the selected colour (call it while handling a press, as browsers only open
35
+ * pickers for one).
36
+ */
37
+ openPicker(): void;
38
+ /** Tell listeners a colour was pressed */
39
+ private onClick;
40
+ /** Set the selected colour from the browser's colour picker */
41
+ private onPick;
42
+ render(): import("lit").TemplateResult<1>;
43
+ }
@@ -0,0 +1,125 @@
1
+ import { PcElement, defineElement, emit } from "../../base/index.js";
2
+ import ChangeController from "../../ChangeController/index.js";
3
+ import { css, html } from "lit";
4
+ import { live } from "lit/directives/live.js";
5
+ // src/ui/elements/PcColourPair/index.ts
6
+ /** Event fired when the foreground or background colour is pressed (detail: `{ slot }`) */
7
+ var COLOUR_CLICK_EVENT = "pc-colour-click";
8
+ /** The two colours, foreground first */
9
+ var SLOTS = ["foreground", "background"];
10
+ /**
11
+ * The foreground and background colours as two overlapping circles in a square (the foreground at the top left), the
12
+ * one selected for configuration outlined and on top. Pressing either fires `pc-colour-click` with its `slot`; the
13
+ * implementer decides what it does (e.g. select it and open a colour palette). `openPicker()` opens the browser's colour
14
+ * picker for the selected colour, which sets it. Sized by `--pc-colour-pair-size` (50px by default).
15
+ *
16
+ * @example
17
+ * html`<pc-colour-pair .app=${app} @pc-colour-click=${(e) => app.selectColour(e.detail.slot)}></pc-colour-pair>`
18
+ */
19
+ var PcColourPair = class extends PcElement {
20
+ static properties = {
21
+ ...PcElement.properties,
22
+ app: { attribute: false }
23
+ };
24
+ static styles = [PcElement.styles, css`
25
+ :host {
26
+ --size: var(--pc-colour-pair-size, 50px);
27
+ position: relative;
28
+ display: block;
29
+ flex: none;
30
+ width: var(--size);
31
+ height: var(--size);
32
+ background: var(--pc-text-muted);
33
+ }
34
+ button {
35
+ position: absolute;
36
+ width: 56%;
37
+ height: 56%;
38
+ padding: 0;
39
+ border: 1px solid var(--pc-text);
40
+ border-radius: 50%;
41
+ background: var(--colour);
42
+ cursor: pointer;
43
+ }
44
+ .foreground {
45
+ top: 10%;
46
+ left: 10%;
47
+ z-index: 1;
48
+ }
49
+ .background {
50
+ right: 10%;
51
+ bottom: 10%;
52
+ }
53
+ /* The selected colour: on top, with a white ring outlined in black */
54
+ button[aria-pressed="true"] {
55
+ z-index: 2;
56
+ box-shadow:
57
+ 0 0 0 2px #ffffff,
58
+ 0 0 0 3px #000000;
59
+ }
60
+ button:focus-visible {
61
+ outline: var(--pc-focus-width) solid var(--pc-focus);
62
+ outline-offset: 4px;
63
+ }
64
+ /* Kept in place (invisible) so the browser's colour picker opens beside the colours */
65
+ input {
66
+ position: absolute;
67
+ bottom: 0;
68
+ left: 0;
69
+ width: 1px;
70
+ height: 1px;
71
+ padding: 0;
72
+ border: 0;
73
+ opacity: 0;
74
+ pointer-events: none;
75
+ }
76
+ `];
77
+ changes = new ChangeController(this, () => [this.app]);
78
+ /**
79
+ * Open the browser's colour picker for the selected colour (call it while handling a press, as browsers only open
80
+ * pickers for one).
81
+ */
82
+ openPicker() {
83
+ const input = this.renderRoot.querySelector("input");
84
+ if (!input) return;
85
+ try {
86
+ input.showPicker();
87
+ } catch {
88
+ input.click();
89
+ }
90
+ }
91
+ /** Tell listeners a colour was pressed */
92
+ onClick(slot) {
93
+ emit(this, COLOUR_CLICK_EVENT, { slot });
94
+ }
95
+ /** Set the selected colour from the browser's colour picker */
96
+ onPick(event) {
97
+ const app = this.app;
98
+ app?.setColour(event.target.value, app.selectedColour);
99
+ }
100
+ render() {
101
+ const app = this.app;
102
+ if (!app) return html``;
103
+ return html`${SLOTS.map((slot) => {
104
+ const colour = app.colours[slot];
105
+ return html`<button
106
+ type="button"
107
+ part=${slot}
108
+ class=${slot}
109
+ style="--colour: ${colour}"
110
+ title=${this.t(`paintCanvas.colours.${slot}`, { colour })}
111
+ aria-label=${this.t(`paintCanvas.colours.${slot}`, { colour })}
112
+ aria-pressed=${app.selectedColour === slot}
113
+ @click=${() => this.onClick(slot)}
114
+ ></button>`;
115
+ })}<input
116
+ type="color"
117
+ tabindex="-1"
118
+ aria-hidden="true"
119
+ .value=${live(app.colours[app.selectedColour])}
120
+ @input=${this.onPick}
121
+ />`;
122
+ }
123
+ };
124
+ defineElement("pc-colour-pair", PcColourPair);
125
+ export { COLOUR_CLICK_EVENT, PcColourPair };
@@ -1,23 +1,43 @@
1
1
  import { type PropertyValues } from "lit";
2
2
  import { PcElement } from "../../base";
3
- /** An item in a context menu */
4
- export interface MenuItem {
5
- /** What `pc-select` gives when it's chosen */
6
- value: string;
7
- label: string;
8
- disabled?: boolean;
9
- /** Shown in the cancelling colour, e.g. for deleting */
10
- danger?: boolean;
11
- }
3
+ import { PcMenu, type MenuItem } from "../PcMenu";
4
+ import "../PcMenu";
5
+ /** Space kept between a context menu's menu and the window's edges */
6
+ export declare const MENU_MARGIN = 8;
7
+ /** Event fired when a context menu opens or closes (detail: `{ open }`) */
8
+ export declare const CONTEXT_MENU_TOGGLE_EVENT = "pc-toggle";
9
+ /** Where a context menu's menu opens: under its button, or beside it */
10
+ export type MenuPlacement = "below" | "beside";
11
+ /**
12
+ * 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
13
+ * top margin).
14
+ *
15
+ * @example
16
+ * menuTop(700, 300, 800); // 492 (moved up to fit)
17
+ * menuTop(100, 300, 800); // 100
18
+ */
19
+ export declare const menuTop: (anchorTop: number, menuHeight: number, viewportHeight: number, margin?: number) => number;
20
+ /**
21
+ * 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
22
+ * margin).
23
+ *
24
+ * @example
25
+ * menuLeft(900, 200, 1000); // 792 (moved left to fit)
26
+ */
27
+ export declare const menuLeft: (left: number, menuWidth: number, viewportWidth: number, margin?: number) => number;
12
28
  /**
13
- * 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
14
- * keyboard focus: arrow keys, Home and End move between items, Enter or Space chooses one, and Escape or Tab closes it.
15
- * Fires `pc-select` with the chosen item's value, and `pc-close` when it should close (after choosing, Escape, Tab, or a
16
- * press outside it). The implementer sets `open` to false in response.
29
+ * A button that opens a menu (`pc-menu`): its `icon` slot is what the button shows (⋮ by default), and its `menu` slot
30
+ * takes the `pc-menu` to open. Without one, it opens its own menu of `items`. Pressing the button (or right-clicking
31
+ * it) opens the menu under it (or beside it, with `placement="beside"`), kept within the window; choosing an item,
32
+ * pressing outside, Escape, Tab, scrolling or pressing the button again closes it, and focus returns to the button.
33
+ *
34
+ * Fires `pc-toggle` (detail: `{ open }`) when it opens or closes. The menu's `pc-select` (detail: `{ value }`) reaches
35
+ * the context menu's listeners.
17
36
  *
18
37
  * @example
19
- * html`<pc-context-menu open .x=${10} .y=${20} label="Layer actions" .items=${items}
20
- * @pc-select=${(e) => act(e.detail.value)} @pc-close=${close}></pc-context-menu>`
38
+ * html`<pc-context-menu label="Layer actions" .items=${items} @pc-select=${(e) => act(e.detail.value)}>
39
+ * <pc-icon slot="icon" name="menu"></pc-icon>
40
+ * </pc-context-menu>`
21
41
  */
22
42
  export declare class PcContextMenu extends PcElement {
23
43
  static properties: {
@@ -31,11 +51,16 @@ export declare class PcContextMenu extends PcElement {
31
51
  label: {
32
52
  type: StringConstructor;
33
53
  };
34
- x: {
35
- type: NumberConstructor;
54
+ menuLabel: {
55
+ type: StringConstructor;
56
+ attribute: string;
57
+ };
58
+ placement: {
59
+ type: StringConstructor;
36
60
  };
37
- y: {
38
- type: NumberConstructor;
61
+ disabled: {
62
+ type: BooleanConstructor;
63
+ reflect: boolean;
39
64
  };
40
65
  translations: {
41
66
  attribute: boolean;
@@ -49,24 +74,36 @@ export declare class PcContextMenu extends PcElement {
49
74
  };
50
75
  };
51
76
  static styles: import("lit").CSSResultGroup[];
77
+ /** Whether the menu is open */
52
78
  open: boolean;
79
+ /** The items of its own menu, when no `pc-menu` is slotted */
53
80
  items: readonly MenuItem[];
54
- /** Names the menu for screen readers */
81
+ /** Names the button for screen readers */
55
82
  label: string;
56
- x: number;
57
- y: number;
58
- private readonly onPointerDown;
83
+ /** Names the menu for screen readers (the button's `label` by default) */
84
+ menuLabel: string;
85
+ /** Where the menu opens: under the button (default) or beside it */
86
+ placement: MenuPlacement;
87
+ disabled: boolean;
88
+ private readonly onScroll;
59
89
  constructor();
60
- connectedCallback(): void;
61
90
  disconnectedCallback(): void;
62
- updated(changed: PropertyValues<this>): void;
63
- /** The enabled items' buttons */
64
- private buttons;
65
- /** Tell listeners the menu should close */
66
- close(): void;
67
- /** Choose an item, then close */
68
- private choose;
69
- /** Move focus between items, or close */
70
- private onKeyDown;
91
+ /** The menu it opens: the slotted `pc-menu`, or its own */
92
+ get menu(): PcMenu | undefined;
93
+ /** The button that opens the menu */
94
+ private get button();
95
+ /**
96
+ * Open or close the menu, telling listeners.
97
+ */
98
+ setOpen(open: boolean): void;
99
+ /** Open the menu on a right-click (instead of the browser's), or close it */
100
+ private onContextMenu;
101
+ /** Close the menu when it asks to, and return focus to the button */
102
+ private onClose;
103
+ /** Set up a newly slotted menu like the current one (open or closed) */
104
+ private onMenuSlotChange;
105
+ protected updated(changed: PropertyValues<this>): void;
106
+ /** Put the open menu under or beside the button, within the window */
107
+ private place;
71
108
  render(): import("lit").TemplateResult<1>;
72
109
  }