@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.
- package/README.md +1 -1
- package/dist/lib/brushes/StrokeEngine/index.d.ts +2 -2
- package/dist/lib/brushes/StrokeEngine/index.js +26 -28
- package/dist/lib/classes/BrushSettings/index.d.ts +24 -0
- package/dist/lib/classes/BrushSettings/index.js +35 -1
- package/dist/lib/classes/KeyboardShortcuts/index.d.ts +17 -6
- package/dist/lib/classes/KeyboardShortcuts/index.js +81 -18
- package/dist/lib/classes/PaintCanvas/index.d.ts +77 -27
- package/dist/lib/classes/PaintCanvas/index.js +141 -41
- package/dist/lib/classes/PaintCanvas/tool.d.ts +15 -0
- package/dist/lib/classes/PaintCanvas/tool.js +18 -0
- package/dist/lib/classes/Palette/index.d.ts +58 -0
- package/dist/lib/classes/Palette/index.js +92 -0
- package/dist/lib/classes/SettingsAutosave/index.d.ts +52 -0
- package/dist/lib/classes/SettingsAutosave/index.js +96 -0
- package/dist/lib/classes/ShortcutSettings/index.d.ts +63 -0
- package/dist/lib/classes/ShortcutSettings/index.js +110 -0
- package/dist/lib/palette/index.d.ts +49 -0
- package/dist/lib/palette/index.js +82 -0
- package/dist/lib/palette/schema.d.ts +45 -0
- package/dist/lib/palette/schema.js +41 -0
- package/dist/lib/rendering/cursors/index.d.ts +2 -0
- package/dist/lib/rendering/cursors/index.js +3 -1
- package/dist/lib/rendering/sample/index.js +3 -2
- package/dist/lib/settings/index.d.ts +63 -0
- package/dist/lib/settings/index.js +91 -0
- package/dist/lib/settings/schema.d.ts +150 -0
- package/dist/lib/settings/schema.js +102 -0
- package/dist/lib/storage/index.d.ts +36 -5
- package/dist/lib/storage/index.js +87 -29
- package/dist/lib/utils/colour/index.d.ts +72 -0
- package/dist/lib/utils/colour/index.js +154 -0
- package/dist/lib/utils/shortcuts/index.d.ts +174 -11
- package/dist/lib/utils/shortcuts/index.js +391 -145
- package/dist/lib/validation/validators.generated.d.ts +5 -0
- package/dist/lib/validation/validators.generated.js +872 -1
- package/dist/locales/en-GB.js +87 -1
- package/dist/locales/en-US.js +87 -1
- package/dist/locales/fr.js +87 -1
- package/dist/locales/ja.js +87 -1
- package/dist/locales/ko.js +87 -1
- package/dist/locales/zh-Hans.js +87 -1
- package/dist/locales/zh-Hant.js +87 -1
- package/dist/package.js +1 -1
- package/dist/ui/elements/PaintCanvas/index.d.ts +49 -47
- package/dist/ui/elements/PaintCanvas/index.js +121 -171
- package/dist/ui/elements/PaintCanvas/style.js +1 -1
- package/dist/ui/elements/PcAppSettings/index.d.ts +59 -0
- package/dist/ui/elements/PcAppSettings/index.js +222 -0
- package/dist/ui/elements/PcCanvas/index.d.ts +1 -1
- package/dist/ui/elements/PcCanvas/index.js +7 -5
- package/dist/ui/elements/PcColourPair/index.d.ts +43 -0
- package/dist/ui/elements/PcColourPair/index.js +125 -0
- package/dist/ui/elements/PcContextMenu/index.d.ts +70 -33
- package/dist/ui/elements/PcContextMenu/index.js +128 -97
- package/dist/ui/elements/PcLayerItem/index.d.ts +13 -34
- package/dist/ui/elements/PcLayerItem/index.js +39 -91
- package/dist/ui/elements/PcLayerItem/style.js +1 -1
- package/dist/ui/elements/PcMenu/index.d.ts +93 -0
- package/dist/ui/elements/PcMenu/index.js +204 -0
- package/dist/ui/elements/PcPane/index.d.ts +118 -0
- package/dist/ui/elements/PcPane/index.js +357 -0
- package/dist/ui/elements/PcPanel/index.d.ts +26 -8
- package/dist/ui/elements/PcPanel/index.js +76 -8
- package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.d.ts +20 -6
- package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.js +18 -14
- package/dist/ui/elements/PcPanelBrushes/style.js +3 -0
- package/dist/ui/elements/PcPanelColours/index.d.ts +171 -0
- package/dist/ui/elements/PcPanelColours/index.js +650 -0
- package/dist/ui/elements/PcPanelColours/style.js +3 -0
- package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.d.ts +17 -4
- package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.js +17 -10
- package/dist/ui/elements/PcPanelLayers/style.js +3 -0
- package/dist/ui/elements/PcShortcutSettings/index.d.ts +69 -0
- package/dist/ui/elements/PcShortcutSettings/index.js +273 -0
- package/dist/ui/elements/PcToolSettings/index.js +2 -1
- package/dist/ui/elements/PcToolbar/index.d.ts +47 -12
- package/dist/ui/elements/PcToolbar/index.js +77 -56
- package/dist/ui/elements/PcToolbar/style.js +1 -1
- package/dist/ui/index.d.ts +7 -1
- package/dist/ui/index.js +8 -2
- package/dist/ui/tokens/index.js +3 -1
- package/locales/en-GB.json +87 -1
- package/locales/en-US.json +87 -1
- package/locales/fr.json +87 -1
- package/locales/ja.json +87 -1
- package/locales/ko.json +87 -1
- package/locales/zh-Hans.json +87 -1
- package/locales/zh-Hant.json +87 -1
- package/package.json +1 -1
- package/dist/ui/elements/PcBrushMenu/style.js +0 -3
- 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
|
-
*
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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:
|
|
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.
|
|
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 {
|
|
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
|
|
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-
|
|
40
|
+
* html`<pc-panel-brushes kind="eraser" .app=${app}></pc-panel-brushes>`
|
|
39
41
|
*/
|
|
40
|
-
export declare class
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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/
|
|
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-
|
|
64
|
+
* html`<pc-panel-brushes kind="eraser" .app=${app}></pc-panel-brushes>`
|
|
66
65
|
*/
|
|
67
|
-
var
|
|
66
|
+
var PcPanelBrushes = class extends PcPanel {
|
|
68
67
|
static properties = {
|
|
69
|
-
...
|
|
68
|
+
...PcPanel.properties,
|
|
70
69
|
app: { attribute: false },
|
|
71
70
|
kind: { type: String }
|
|
72
71
|
};
|
|
73
72
|
static styles = [
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
363
|
-
export { DELETE_CURSOR, MAX_PREVIEW_SIZE, MIN_PREVIEW_SIZE,
|
|
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 };
|