@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
@@ -0,0 +1,357 @@
1
+ import { PcElement, defineElement, emit } from "../../base/index.js";
2
+ import { PcPanel } from "../PcPanel/index.js";
3
+ import { css, html, nothing } from "lit";
4
+ // src/ui/elements/PcPane/index.ts
5
+ /** Event fired when a floating pane is shown or collapsed (detail: `{ collapsed }`) */
6
+ var PANE_COLLAPSE_EVENT = "pc-collapse";
7
+ /** Elements that can take keyboard focus */
8
+ var FOCUSABLE = "button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])";
9
+ /**
10
+ * An area where buttons and panels are shown or docked, e.g. a side of the `<paint-canvas>` layout. Its content goes in
11
+ * its `start`, `middle` and `end` slots (content without a slot goes at the start), which run down a `vertical` pane
12
+ * (the default) or across a `horizontal` one; the middle takes the space left.
13
+ *
14
+ * Its `mode` sets how it shows its panels (`PcPanel`s, e.g. `<pc-panel-layers>`):
15
+ * - `docked`: as they are.
16
+ * - `rail`: as a column of buttons (with each panel's icon, name and badge) that open one panel at a time over the
17
+ * canvas, beside the pane (away from its `edge`). Focus moves into the panel; Escape, a press outside it or its
18
+ * button again closes it, and focus goes back to the button.
19
+ * - `floating`: the pane floats over the canvas as a box that collapses to a button (`collapsed`, named with its
20
+ * `label`), firing `pc-collapse` when it's shown or collapsed.
21
+ *
22
+ * Panes made for one purpose extend it and put their own content in its regions (see `renderStart`, `renderMiddle`
23
+ * and `renderEnd`), e.g. `<pc-toolbar>`.
24
+ *
25
+ * @example
26
+ * html`<pc-pane edge="right" mode="rail" label="Panels"><pc-panel-layers .app=${app}></pc-panel-layers></pc-pane>`
27
+ */
28
+ var PcPane = class extends PcElement {
29
+ static properties = {
30
+ ...PcElement.properties,
31
+ orientation: {
32
+ type: String,
33
+ reflect: true
34
+ },
35
+ mode: {
36
+ type: String,
37
+ reflect: true
38
+ },
39
+ edge: {
40
+ type: String,
41
+ reflect: true
42
+ },
43
+ label: { type: String },
44
+ collapsed: {
45
+ type: Boolean,
46
+ reflect: true
47
+ },
48
+ openPanel: { state: true }
49
+ };
50
+ static styles = [PcElement.styles, css`
51
+ :host {
52
+ position: relative;
53
+ display: flex;
54
+ box-sizing: border-box;
55
+ min-width: 0;
56
+ min-height: 0;
57
+ font-family: var(--pc-font);
58
+ font-size: var(--pc-font-size);
59
+ }
60
+ :host([edge="right"]) {
61
+ flex-direction: row-reverse;
62
+ }
63
+ .regions {
64
+ display: flex;
65
+ flex: 1;
66
+ flex-direction: column;
67
+ min-width: 0;
68
+ min-height: 0;
69
+ }
70
+ :host([orientation="horizontal"]) .regions {
71
+ flex-direction: row;
72
+ }
73
+ .start,
74
+ .middle,
75
+ .end {
76
+ display: flex;
77
+ flex-direction: inherit;
78
+ }
79
+ /* The middle takes the space left, and gives it up first; the start and end keep their content's size */
80
+ .middle {
81
+ flex: 1;
82
+ min-width: 0;
83
+ min-height: 0;
84
+ }
85
+ .regions[hidden] {
86
+ display: none;
87
+ }
88
+
89
+ /* Rail: a column of buttons, one per panel, on the side facing the canvas */
90
+ .rail {
91
+ display: flex;
92
+ flex-direction: column;
93
+ gap: var(--pc-space-1);
94
+ padding: var(--pc-space-1);
95
+ border-inline: 1px solid var(--pc-border);
96
+ background: var(--pc-bar);
97
+ }
98
+ .rail button {
99
+ position: relative;
100
+ display: inline-flex;
101
+ padding: var(--pc-space-1);
102
+ border: 1px solid transparent;
103
+ border-radius: var(--pc-radius-small);
104
+ background: transparent;
105
+ cursor: pointer;
106
+ }
107
+ .rail button:hover {
108
+ background: var(--pc-surface-hover);
109
+ }
110
+ .rail button[aria-expanded="true"] {
111
+ border-color: var(--pc-accent);
112
+ background: var(--pc-accent);
113
+ }
114
+ .rail button:focus-visible {
115
+ outline: var(--pc-focus-width) solid var(--pc-focus);
116
+ }
117
+ .rail img {
118
+ width: var(--pc-icon-size);
119
+ height: var(--pc-icon-size);
120
+ filter: var(--pc-icon-filter);
121
+ }
122
+ .rail .badge {
123
+ position: absolute;
124
+ top: -2px;
125
+ right: -2px;
126
+ width: 10px;
127
+ height: 10px;
128
+ border: 2px solid var(--pc-bar);
129
+ border-radius: 50%;
130
+ background: var(--pc-red);
131
+ }
132
+
133
+ /* Floating: the pane floats over the canvas as a box, and collapses to a button */
134
+ :host([mode="floating"]) {
135
+ position: absolute;
136
+ z-index: 5;
137
+ top: var(--pc-space-2);
138
+ bottom: var(--pc-space-2);
139
+ flex-direction: column;
140
+ gap: var(--pc-space-1);
141
+ max-width: var(--pc-floating-panel-width, min(360px, 85vw));
142
+ border-radius: var(--pc-radius);
143
+ background: var(--pc-surface-raised);
144
+ box-shadow: var(--pc-shadow);
145
+ overflow: hidden;
146
+ }
147
+ :host([mode="floating"]:not([edge="right"])) {
148
+ left: var(--pc-space-2);
149
+ }
150
+ :host([mode="floating"][edge="right"]) {
151
+ right: var(--pc-space-2);
152
+ }
153
+ :host([mode="floating"][collapsed]) {
154
+ bottom: auto;
155
+ }
156
+ .collapse {
157
+ align-self: flex-end;
158
+ min-width: var(--pc-control-size);
159
+ min-height: var(--pc-control-size);
160
+ border: 0;
161
+ background: transparent;
162
+ color: var(--pc-text);
163
+ font-size: 18px;
164
+ cursor: pointer;
165
+ }
166
+ .collapse:focus-visible {
167
+ outline: var(--pc-focus-width) solid var(--pc-focus);
168
+ }
169
+ `];
170
+ /** The panels in it */
171
+ panels = [];
172
+ /** The rail button that opened the open panel, to return focus to */
173
+ opener;
174
+ panelObserver = new MutationObserver(() => this.requestUpdate());
175
+ onDocumentPointerDown = (event) => {
176
+ const panel = this.openPanel;
177
+ if (!panel) return;
178
+ const path = event.composedPath();
179
+ if (!path.includes(panel) && !(this.opener && path.includes(this.opener))) this.closePanel(false);
180
+ };
181
+ onKeyDown = (event) => {
182
+ if (event.key !== "Escape" || !this.openPanel) return;
183
+ event.preventDefault();
184
+ this.closePanel(true);
185
+ };
186
+ constructor() {
187
+ super();
188
+ this.orientation = "vertical";
189
+ this.mode = "docked";
190
+ this.edge = "left";
191
+ this.label = "";
192
+ this.collapsed = true;
193
+ }
194
+ connectedCallback() {
195
+ super.connectedCallback();
196
+ this.panelObserver.observe(this, {
197
+ subtree: true,
198
+ attributes: true,
199
+ attributeFilter: [
200
+ "icon",
201
+ "label",
202
+ "badge"
203
+ ]
204
+ });
205
+ this.addEventListener("keydown", this.onKeyDown);
206
+ document.addEventListener("pointerdown", this.onDocumentPointerDown, true);
207
+ }
208
+ disconnectedCallback() {
209
+ super.disconnectedCallback();
210
+ this.panelObserver.disconnect();
211
+ this.removeEventListener("keydown", this.onKeyDown);
212
+ document.removeEventListener("pointerdown", this.onDocumentPointerDown, true);
213
+ }
214
+ /**
215
+ * Open a panel over the canvas from its rail button (closing any other), or close it if it's open. Focus moves into
216
+ * the panel: to the first thing in it that takes focus, or the panel itself.
217
+ *
218
+ * @example
219
+ * pane.togglePanel(pane.querySelector("pc-panel-layers"));
220
+ */
221
+ togglePanel(panel, opener) {
222
+ if (this.openPanel === panel) {
223
+ this.closePanel(true);
224
+ return;
225
+ }
226
+ if (this.openPanel) this.closePanel(false);
227
+ this.openPanel = panel;
228
+ this.opener = opener;
229
+ this.syncPanels();
230
+ const focusable = panel.querySelector(FOCUSABLE);
231
+ if (focusable) focusable.focus();
232
+ else {
233
+ panel.tabIndex = -1;
234
+ panel.focus();
235
+ }
236
+ }
237
+ /**
238
+ * Close the panel open from the rail, moving focus back to its rail button if asked.
239
+ */
240
+ closePanel(returnFocus) {
241
+ if (!this.openPanel) return;
242
+ this.openPanel = void 0;
243
+ this.syncPanels();
244
+ if (returnFocus) this.opener?.focus();
245
+ this.opener = void 0;
246
+ }
247
+ /** Show or collapse the floating pane, telling listeners */
248
+ toggleCollapsed() {
249
+ this.collapsed = !this.collapsed;
250
+ emit(this, PANE_COLLAPSE_EVENT, { collapsed: this.collapsed });
251
+ }
252
+ /** Follow the panels slotted into it */
253
+ onSlotChange() {
254
+ this.panels = [...this.renderRoot.querySelectorAll("slot")].flatMap((slot) => slot.assignedElements({ flatten: true }).filter((element) => element instanceof PcPanel));
255
+ if (this.openPanel && !this.panels.includes(this.openPanel)) this.closePanel(false);
256
+ this.syncPanels();
257
+ this.requestUpdate();
258
+ }
259
+ /** Show the panels for the mode: hidden behind the rail (except the open one, beside the pane) or as they are */
260
+ syncPanels() {
261
+ const rail = this.mode === "rail";
262
+ for (const panel of this.panels) {
263
+ panel.toggleAttribute("data-pc-rail", rail);
264
+ if (rail && panel === this.openPanel) panel.setAttribute("data-pc-open", this.edge);
265
+ else panel.removeAttribute("data-pc-open");
266
+ }
267
+ }
268
+ updated(changed) {
269
+ if (!changed.has("mode") && !changed.has("edge")) return;
270
+ if (this.mode !== "rail") this.closePanel(false);
271
+ this.syncPanels();
272
+ }
273
+ /** Content of its own at its start, before what's slotted there */
274
+ renderStart() {
275
+ return nothing;
276
+ }
277
+ /** Content of its own in its middle, before what's slotted there */
278
+ renderMiddle() {
279
+ return nothing;
280
+ }
281
+ /** Content of its own at its end, before what's slotted there */
282
+ renderEnd() {
283
+ return nothing;
284
+ }
285
+ /** The rail: a button per panel */
286
+ renderRail() {
287
+ if (this.mode !== "rail" || this.panels.length === 0) return nothing;
288
+ return html`<div
289
+ class="rail"
290
+ part="rail"
291
+ role="toolbar"
292
+ aria-orientation="vertical"
293
+ aria-label=${this.label || nothing}
294
+ >
295
+ ${this.panels.map((panel) => {
296
+ const label = panel.panelLabel;
297
+ return html`<button
298
+ type="button"
299
+ title=${label}
300
+ aria-label=${panel.badge ? this.t("paintCanvas.layout.new", { name: label }) : label}
301
+ aria-expanded=${String(this.openPanel === panel)}
302
+ @click=${(event) => this.togglePanel(panel, event.currentTarget)}
303
+ >
304
+ <img src=${this.iconUrl(panel.panelIcon)} alt="" />
305
+ ${panel.badge ? html`<span class="badge"></span>` : nothing}
306
+ </button>`;
307
+ })}
308
+ </div>`;
309
+ }
310
+ /** The button that shows or collapses a floating pane */
311
+ renderCollapse() {
312
+ if (this.mode !== "floating") return nothing;
313
+ const collapsed = this.collapsed;
314
+ const arrows = this.edge === "right" ? ["«", "»"] : ["»", "«"];
315
+ return html`<button
316
+ type="button"
317
+ class="collapse"
318
+ part="collapse"
319
+ aria-expanded=${String(!collapsed)}
320
+ aria-label=${this.t(collapsed ? "paintCanvas.layout.show" : "paintCanvas.layout.hide", { name: this.label })}
321
+ @click=${this.toggleCollapsed}
322
+ >
323
+ ${collapsed ? arrows[0] : arrows[1]}
324
+ </button>`;
325
+ }
326
+ render() {
327
+ return html`${this.renderCollapse()}
328
+ <div
329
+ class="regions"
330
+ part="regions"
331
+ ?hidden=${this.mode === "floating" && this.collapsed}
332
+ >
333
+ <div class="start" part="start">
334
+ ${this.renderStart()}<slot
335
+ name="start"
336
+ @slotchange=${this.onSlotChange}
337
+ ></slot
338
+ ><slot @slotchange=${this.onSlotChange}></slot>
339
+ </div>
340
+ <div class="middle" part="middle">
341
+ ${this.renderMiddle()}<slot
342
+ name="middle"
343
+ @slotchange=${this.onSlotChange}
344
+ ></slot>
345
+ </div>
346
+ <div class="end" part="end">
347
+ ${this.renderEnd()}<slot
348
+ name="end"
349
+ @slotchange=${this.onSlotChange}
350
+ ></slot>
351
+ </div>
352
+ </div>
353
+ ${this.renderRail()}`;
354
+ }
355
+ };
356
+ defineElement("pc-pane", PcPane);
357
+ export { PANE_COLLAPSE_EVENT, PcPane };
@@ -1,12 +1,20 @@
1
+ import { nothing, type TemplateResult } from "lit";
1
2
  import { PcElement } from "../../base";
2
3
  /**
3
- * Wraps content docked at a side of the `<paint-canvas>` layout (e.g. the layers menu or a chat window). Its `icon`,
4
- * `label` and `badge` are what the layout shows for it when the side narrows to a rail of buttons; the badge is a dot
5
- * meaning something new (e.g. unread messages). It names its region for screen readers. It's at most 290px wide
6
- * (`--pc-panel-max-width`).
4
+ * A panel: a box of settings or content for one purpose, docked in a `<pc-pane>` or floating over the canvas. It has
5
+ * `header`, `body` and `footer` slots; content without a slot goes in the body. It names its region with its `label`
6
+ * for screen readers.
7
+ *
8
+ * Its `icon`, `label` and `badge` are what a pane shows for it when the pane narrows to a rail of buttons; the badge is
9
+ * a dot meaning something new (e.g. unread messages). It's 290px wide (`--pc-panel-width`) and no wider than the space
10
+ * it has.
11
+ *
12
+ * Panels made for one purpose extend `PcPanel`, so they share its look: e.g. `<pc-panel-layers>`,
13
+ * `<pc-panel-brushes>` and `<pc-panel-colours>`. They put their own content in the body (see `renderBody`), and give
14
+ * their own `defaultLabel` and `defaultIcon`.
7
15
  *
8
16
  * @example
9
- * html`<pc-panel slot="right" icon="layers" label="Layers"><my-layers></my-layers></pc-panel>`
17
+ * html`<pc-panel icon="chat" label="Chat"><my-chat></my-chat></pc-panel>`
10
18
  */
11
19
  export declare class PcPanel extends PcElement {
12
20
  static properties: {
@@ -32,12 +40,22 @@ export declare class PcPanel extends PcElement {
32
40
  };
33
41
  };
34
42
  static styles: import("lit").CSSResultGroup[];
35
- /** Name of the icon for its rail button */
43
+ /** Name of the icon for its rail button (`defaultIcon` if not given) */
36
44
  icon: string;
37
- /** Names the panel (its rail button and region) */
45
+ /** Names the panel: its region and rail button (`defaultLabel` if not given) */
38
46
  label: string;
39
47
  /** Whether there's something new in it */
40
48
  badge: boolean;
41
49
  constructor();
42
- render(): import("lit").TemplateResult<1>;
50
+ /** The icon a panel made for a purpose shows on its rail button when none is given */
51
+ protected get defaultIcon(): string;
52
+ /** The name a panel made for a purpose has when none is given, in the chosen language */
53
+ protected get defaultLabel(): string;
54
+ /** The icon shown on its rail button */
55
+ get panelIcon(): string;
56
+ /** The panel's name, for its region and rail button */
57
+ get panelLabel(): string;
58
+ /** The panel's own body content, before what's slotted into its body (none for a plain `<pc-panel>`) */
59
+ protected renderBody(): TemplateResult | typeof nothing;
60
+ render(): TemplateResult<1>;
43
61
  }
@@ -2,13 +2,20 @@ import { PcElement, defineElement } from "../../base/index.js";
2
2
  import { css, html, nothing } from "lit";
3
3
  // src/ui/elements/PcPanel/index.ts
4
4
  /**
5
- * Wraps content docked at a side of the `<paint-canvas>` layout (e.g. the layers menu or a chat window). Its `icon`,
6
- * `label` and `badge` are what the layout shows for it when the side narrows to a rail of buttons; the badge is a dot
7
- * meaning something new (e.g. unread messages). It names its region for screen readers. It's at most 290px wide
8
- * (`--pc-panel-max-width`).
5
+ * A panel: a box of settings or content for one purpose, docked in a `<pc-pane>` or floating over the canvas. It has
6
+ * `header`, `body` and `footer` slots; content without a slot goes in the body. It names its region with its `label`
7
+ * for screen readers.
8
+ *
9
+ * Its `icon`, `label` and `badge` are what a pane shows for it when the pane narrows to a rail of buttons; the badge is
10
+ * a dot meaning something new (e.g. unread messages). It's 290px wide (`--pc-panel-width`) and no wider than the space
11
+ * it has.
12
+ *
13
+ * Panels made for one purpose extend `PcPanel`, so they share its look: e.g. `<pc-panel-layers>`,
14
+ * `<pc-panel-brushes>` and `<pc-panel-colours>`. They put their own content in the body (see `renderBody`), and give
15
+ * their own `defaultLabel` and `defaultIcon`.
9
16
  *
10
17
  * @example
11
- * html`<pc-panel slot="right" icon="layers" label="Layers"><my-layers></my-layers></pc-panel>`
18
+ * html`<pc-panel icon="chat" label="Chat"><my-chat></my-chat></pc-panel>`
12
19
  */
13
20
  var PcPanel = class extends PcElement {
14
21
  static properties = {
@@ -23,10 +30,38 @@ var PcPanel = class extends PcElement {
23
30
  static styles = [PcElement.styles, css`
24
31
  :host {
25
32
  display: flex;
33
+ box-sizing: border-box;
34
+ width: var(--pc-panel-width, var(--pc-panel-default-width, 290px));
26
35
  min-width: 0;
27
- max-width: var(--pc-panel-max-width, 290px);
36
+ max-width: 100%;
28
37
  min-height: 0;
38
+ max-height: 100%;
29
39
  flex-direction: column;
40
+ /* Light grey, like the bars */
41
+ background: var(--pc-bar);
42
+ color: var(--pc-text);
43
+ box-shadow: var(--pc-shadow);
44
+ font-family: var(--pc-font);
45
+ font-size: var(--pc-font-size);
46
+ }
47
+ /* In a pane's rail (set by the pane): hidden until opened from its button, then over the canvas beside the pane */
48
+ :host([data-pc-rail]:not([data-pc-open])) {
49
+ display: none;
50
+ }
51
+ :host([data-pc-open]) {
52
+ position: absolute;
53
+ z-index: 5;
54
+ top: 0;
55
+ bottom: 0;
56
+ width: var(--pc-floating-panel-width, min(360px, 85vw));
57
+ max-width: none;
58
+ max-height: none;
59
+ }
60
+ :host([data-pc-open="left"]) {
61
+ left: 100%;
62
+ }
63
+ :host([data-pc-open="right"]) {
64
+ right: 100%;
30
65
  }
31
66
  section {
32
67
  display: flex;
@@ -34,6 +69,15 @@ var PcPanel = class extends PcElement {
34
69
  min-height: 0;
35
70
  flex-direction: column;
36
71
  }
72
+ .body {
73
+ display: flex;
74
+ flex: 1;
75
+ min-height: 0;
76
+ flex-direction: column;
77
+ gap: var(--pc-space-2);
78
+ padding: var(--pc-space-2);
79
+ overflow: auto;
80
+ }
37
81
  `];
38
82
  constructor() {
39
83
  super();
@@ -41,9 +85,33 @@ var PcPanel = class extends PcElement {
41
85
  this.label = "";
42
86
  this.badge = false;
43
87
  }
88
+ /** The icon a panel made for a purpose shows on its rail button when none is given */
89
+ get defaultIcon() {
90
+ return "";
91
+ }
92
+ /** The name a panel made for a purpose has when none is given, in the chosen language */
93
+ get defaultLabel() {
94
+ return "";
95
+ }
96
+ /** The icon shown on its rail button */
97
+ get panelIcon() {
98
+ return this.icon || this.defaultIcon;
99
+ }
100
+ /** The panel's name, for its region and rail button */
101
+ get panelLabel() {
102
+ return this.label || this.defaultLabel;
103
+ }
104
+ /** The panel's own body content, before what's slotted into its body (none for a plain `<pc-panel>`) */
105
+ renderBody() {
106
+ return nothing;
107
+ }
44
108
  render() {
45
- return html`<section part="panel" aria-label=${this.label || nothing}>
46
- <slot></slot>
109
+ return html`<section part="panel" aria-label=${this.panelLabel || nothing}>
110
+ <slot name="header"></slot>
111
+ <div class="body" part="body">
112
+ ${this.renderBody()}<slot name="body"><slot></slot></slot>
113
+ </div>
114
+ <slot name="footer"></slot>
47
115
  </section>`;
48
116
  }
49
117
  };
@@ -1,10 +1,12 @@
1
- import { PcElement } from "../../base";
1
+ import { nothing } from "lit";
2
+ import { PcPanel } from "../PcPanel";
2
3
  import type PaintCanvas from "../../../lib/classes/PaintCanvas";
3
4
  import type BrushSettings from "../../../lib/classes/BrushSettings";
4
5
  import type { BrushConfig } from "../../../lib/brushes/types";
6
+ import { DELETE_CURSOR } from "../../../lib/rendering/cursors";
5
7
  import { type SaveMethod } from "../../../lib/rendering/screenshot";
6
- /** Cursor over a preset that a click deletes (Alt/Option held) */
7
- export declare const DELETE_CURSOR: string;
8
+ /** Cursor over a preset that a click deletes (Alt/Option held): the `--pc-delete-cursor` token's default */
9
+ export { DELETE_CURSOR };
8
10
  /** Asks the user to confirm; replaceable for tests */
9
11
  export type ConfirmFn = (message: string) => boolean;
10
12
  /** Largest shape preview in a preset tile, in CSS pixels */
@@ -35,9 +37,9 @@ export declare const previewSizes: (presets: BrushConfig[]) => Map<string, {
35
37
  * importing, exporting or deleting presets, or when that fails.
36
38
  *
37
39
  * @example
38
- * html`<pc-brush-menu kind="eraser" .app=${app}></pc-brush-menu>`
40
+ * html`<pc-panel-brushes kind="eraser" .app=${app}></pc-panel-brushes>`
39
41
  */
40
- export declare class PcBrushMenu extends PcElement {
42
+ export declare class PcPanelBrushes extends PcPanel {
41
43
  static properties: {
42
44
  app: {
43
45
  attribute: boolean;
@@ -45,6 +47,16 @@ export declare class PcBrushMenu extends PcElement {
45
47
  kind: {
46
48
  type: StringConstructor;
47
49
  };
50
+ icon: {
51
+ type: StringConstructor;
52
+ };
53
+ label: {
54
+ type: StringConstructor;
55
+ };
56
+ badge: {
57
+ type: BooleanConstructor;
58
+ reflect: boolean;
59
+ };
48
60
  translations: {
49
61
  attribute: boolean;
50
62
  };
@@ -117,5 +129,7 @@ export declare class PcBrushMenu extends PcElement {
117
129
  private onPointerUp;
118
130
  /** Render a preset's tile */
119
131
  private renderTile;
120
- render(): import("lit").TemplateResult<1>;
132
+ protected get defaultIcon(): string;
133
+ protected get defaultLabel(): string;
134
+ protected renderBody(): import("lit").TemplateResult<1> | typeof nothing;
121
135
  }
@@ -1,16 +1,17 @@
1
1
  import style_default from "./style.js";
2
2
  import { BRUSH_PRESET_FILE_FORMAT } from "../../../lib/presets/schema.js";
3
3
  import { NO_TEXT_SELECTION_CSS, PILLS_CSS, WINDOWS_CSS } from "../../styles/index.js";
4
- import { PcElement, defineElement } from "../../base/index.js";
4
+ import { defineElement } from "../../base/index.js";
5
+ import { PcPanel } from "../PcPanel/index.js";
5
6
  import ChangeController from "../../ChangeController/index.js";
6
7
  import { groupPresetsByAuthor } from "../../../lib/brushes/authors/index.js";
7
8
  import "../../../lib/presets/index.js";
8
9
  import { PRESET_FILE_ERRORS, PresetFileError, exportPresetFile, parsePresetFile, presetFileName } from "../../../lib/brushes/presetFile/index.js";
9
- import { svgCursor } from "../../../lib/rendering/cursors/index.js";
10
+ import { DELETE_CURSOR } from "../../../lib/rendering/cursors/index.js";
10
11
  import { JSON_FILE_TYPE, saveFile } from "../../../lib/rendering/screenshot/index.js";
11
12
  import { html, nothing, unsafeCSS } from "lit";
12
13
  import { styleMap } from "lit/directives/style-map.js";
13
- // src/ui/elements/PcBrushMenu/index.ts
14
+ // src/ui/elements/PcPanelBrushes/index.ts
14
15
  /** Translation keys of the preset file error messages */
15
16
  var PRESET_FILE_ERROR_KEYS = {
16
17
  [PRESET_FILE_ERRORS.incompatible]: "paintCanvas.presets.fileIncompatible",
@@ -18,8 +19,6 @@ var PRESET_FILE_ERROR_KEYS = {
18
19
  [PRESET_FILE_ERRORS.export]: "paintCanvas.presets.exportFailed",
19
20
  [PRESET_FILE_ERRORS.empty]: "paintCanvas.presets.nothingToExport"
20
21
  };
21
- /** Cursor over a preset that a click deletes (Alt/Option held) */
22
- var DELETE_CURSOR = svgCursor("M6 6 L18 18 M18 6 L6 18", [12, 12], "not-allowed");
23
22
  /** Size the previews of presets without a family are measured against, so a 50px preset fills its tile */
24
23
  var UNGROUPED_FULL_SIZE = 50;
25
24
  /** Largest shape preview in a preset tile, in CSS pixels */
@@ -62,16 +61,16 @@ var previewSizes = (presets) => {
62
61
  * importing, exporting or deleting presets, or when that fails.
63
62
  *
64
63
  * @example
65
- * html`<pc-brush-menu kind="eraser" .app=${app}></pc-brush-menu>`
64
+ * html`<pc-panel-brushes kind="eraser" .app=${app}></pc-panel-brushes>`
66
65
  */
67
- var PcBrushMenu = class extends PcElement {
66
+ var PcPanelBrushes = class extends PcPanel {
68
67
  static properties = {
69
- ...PcElement.properties,
68
+ ...PcPanel.properties,
70
69
  app: { attribute: false },
71
70
  kind: { type: String }
72
71
  };
73
72
  static styles = [
74
- PcElement.styles,
73
+ ...PcPanel.styles,
75
74
  unsafeCSS(NO_TEXT_SELECTION_CSS),
76
75
  unsafeCSS(style_default),
77
76
  unsafeCSS(WINDOWS_CSS),
@@ -93,7 +92,6 @@ var PcBrushMenu = class extends PcElement {
93
92
  }
94
93
  connectedCallback() {
95
94
  super.connectedCallback();
96
- this.style.setProperty("--delete-cursor", DELETE_CURSOR);
97
95
  window.addEventListener("keydown", this.onKey);
98
96
  window.addEventListener("keyup", this.onKey);
99
97
  window.addEventListener("blur", this.onBlur);
@@ -271,9 +269,15 @@ var PcBrushMenu = class extends PcElement {
271
269
  </button>
272
270
  </li>`;
273
271
  }
274
- render() {
272
+ get defaultIcon() {
273
+ return this.kind === "eraser" ? "eraser" : "brush";
274
+ }
275
+ get defaultLabel() {
276
+ return this.t(this.kind === "eraser" ? "paintCanvas.tool.eraser" : "paintCanvas.tool.brush");
277
+ }
278
+ renderBody() {
275
279
  const settings = this.settings;
276
- if (!settings) return html``;
280
+ if (!settings) return nothing;
277
281
  const title = this.t(this.kind === "eraser" ? "paintCanvas.tool.eraser" : "paintCanvas.tool.brush");
278
282
  const presets = Object.values(settings.presets);
279
283
  const sizes = previewSizes(presets);
@@ -359,5 +363,5 @@ var PcBrushMenu = class extends PcElement {
359
363
  `;
360
364
  }
361
365
  };
362
- defineElement("pc-brush-menu", PcBrushMenu);
363
- export { DELETE_CURSOR, MAX_PREVIEW_SIZE, MIN_PREVIEW_SIZE, PcBrushMenu, previewSizes };
366
+ defineElement("pc-panel-brushes", PcPanelBrushes);
367
+ export { DELETE_CURSOR, MAX_PREVIEW_SIZE, MIN_PREVIEW_SIZE, PcPanelBrushes, previewSizes };
@@ -0,0 +1,3 @@
1
+ // src/ui/elements/PcPanelBrushes/style.css?inline
2
+ var style_default = "/* Narrower than other panels by default (set --pc-panel-width to change it) */\n:host {\n --pc-panel-default-width: 240px;\n}\n/* The current preset on one row: its shape, name and size */\n.current {\n display: flex;\n align-items: center;\n gap: 8px;\n min-height: 44px;\n padding: 4px 8px;\n border: 1px solid var(--pc-border);\n background: var(--pc-surface);\n}\n.current-shape {\n flex: none;\n width: 28px;\n height: 28px;\n border-radius: 50%;\n background: var(--pc-text);\n}\n.current-shape.image {\n border-radius: 0;\n mask-repeat: no-repeat;\n mask-size: contain;\n mask-position: center;\n}\n.name {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.size {\n flex: none;\n align-self: stretch;\n display: flex;\n align-items: center;\n padding: 0 0 0 8px;\n border-left: 1px solid var(--pc-border);\n cursor: ew-resize;\n touch-action: none;\n}\n/* The presets scroll, at most 400px tall, or less so the whole menu fits under the settings bar in a shorter window */\n.presets {\n flex: 1 1 auto;\n min-height: 0;\n max-height: min(400px, calc(100vh - 180px));\n overflow-y: auto;\n}\nh3 {\n margin: 4px 0;\n font-size: 14px;\n}\nul {\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n align-content: start;\n margin: 0;\n padding: 0;\n list-style: none;\n border: 1px solid var(--pc-border);\n background: var(--pc-bar);\n}\n:host([deleting]) li button.custom {\n cursor: var(--pc-delete-cursor);\n}\n/* Create, Import and Export side by side */\n.actions {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 6px;\n}\n.actions button.pill {\n padding: 4px 8px;\n}\n.visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\nli button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n aspect-ratio: 1;\n padding: 0;\n border: none;\n border-right: 1px solid var(--pc-border);\n border-bottom: 1px solid var(--pc-border);\n background: var(--pc-bar);\n cursor: pointer;\n}\nli button:hover {\n background: var(--pc-surface-hover);\n}\nli button[aria-pressed=\"true\"] {\n background: var(--pc-surface-active);\n}\n.shape {\n background: var(--pc-text);\n border-radius: 50%;\n}\n.shape.image {\n border-radius: 0;\n mask-repeat: no-repeat;\n mask-size: 100% 100%;\n}\n";
3
+ export { style_default as default };