@lvl99dotcom/paintcanvas 0.4.0 → 0.6.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 (224) hide show
  1. package/README.md +30 -15
  2. package/dist/lib/brushes/configRanges/index.d.ts +17 -0
  3. package/dist/lib/brushes/configRanges/index.js +31 -1
  4. package/dist/lib/brushes/params/index.d.ts +2 -1
  5. package/dist/lib/brushes/params/index.js +7 -4
  6. package/dist/lib/brushes/presets/round/index.d.ts +1 -1
  7. package/dist/lib/brushes/presets/round/index.js +12 -7
  8. package/dist/lib/brushes/presets/square/index.d.ts +1 -1
  9. package/dist/lib/brushes/presets/square/index.js +7 -3
  10. package/dist/lib/brushes/shapeImage/index.d.ts +7 -3
  11. package/dist/lib/brushes/shapeImage/index.js +15 -9
  12. package/dist/lib/brushes/shapes/index.d.ts +18 -0
  13. package/dist/lib/brushes/shapes/index.js +86 -0
  14. package/dist/lib/brushes/types/index.d.ts +8 -0
  15. package/dist/lib/classes/BrushSettings/index.d.ts +29 -2
  16. package/dist/lib/classes/BrushSettings/index.js +51 -14
  17. package/dist/lib/classes/KeyboardShortcuts/index.d.ts +31 -2
  18. package/dist/lib/classes/KeyboardShortcuts/index.js +66 -4
  19. package/dist/lib/classes/Layer/index.d.ts +10 -0
  20. package/dist/lib/classes/Layer/index.js +15 -1
  21. package/dist/lib/classes/LayerAutosave/index.d.ts +9 -15
  22. package/dist/lib/classes/LayerAutosave/index.js +30 -43
  23. package/dist/lib/classes/LayerStack/index.d.ts +15 -0
  24. package/dist/lib/classes/LayerStack/index.js +26 -2
  25. package/dist/lib/classes/PaintCanvas/index.d.ts +72 -3
  26. package/dist/lib/classes/PaintCanvas/index.js +106 -12
  27. package/dist/lib/classes/PngEncoder/index.d.ts +41 -0
  28. package/dist/lib/classes/PngEncoder/index.js +104 -0
  29. package/dist/lib/classes/ProfileGraphs/index.d.ts +87 -0
  30. package/dist/lib/classes/ProfileGraphs/index.js +148 -0
  31. package/dist/lib/classes/SettingsAutosave/index.d.ts +3 -2
  32. package/dist/lib/classes/SettingsAutosave/index.js +6 -3
  33. package/dist/lib/classes/UiSettings/index.d.ts +68 -0
  34. package/dist/lib/classes/UiSettings/index.js +82 -0
  35. package/dist/lib/messages/schema.d.ts +720 -0
  36. package/dist/lib/presets/schema.d.ts +360 -0
  37. package/dist/lib/presets/schema.js +8 -1
  38. package/dist/lib/profiles/index.d.ts +61 -0
  39. package/dist/lib/profiles/index.js +131 -0
  40. package/dist/lib/profiles/schema.d.ts +73 -0
  41. package/dist/lib/profiles/schema.js +53 -0
  42. package/dist/lib/rendering/sample/index.d.ts +11 -0
  43. package/dist/lib/rendering/sample/index.js +36 -5
  44. package/dist/lib/settings/index.d.ts +2 -1
  45. package/dist/lib/settings/index.js +7 -2
  46. package/dist/lib/settings/schema.d.ts +67 -0
  47. package/dist/lib/settings/schema.js +20 -0
  48. package/dist/lib/validation/validators.generated.js +13191 -3655
  49. package/dist/locales/en-GB.js +39 -2
  50. package/dist/locales/en-US.js +39 -2
  51. package/dist/locales/fr.js +39 -2
  52. package/dist/locales/ja.js +39 -2
  53. package/dist/locales/ko.js +39 -2
  54. package/dist/locales/zh-Hans.js +39 -2
  55. package/dist/locales/zh-Hant.js +39 -2
  56. package/dist/package.js +1 -1
  57. package/dist/testing/recordingCanvas.js +2 -1
  58. package/dist/ui/ask/index.d.ts +48 -0
  59. package/dist/ui/ask/index.js +67 -0
  60. package/dist/ui/base/index.d.ts +10 -3
  61. package/dist/ui/base/index.js +19 -4
  62. package/dist/ui/dismiss/index.d.ts +21 -0
  63. package/dist/ui/dismiss/index.js +27 -0
  64. package/dist/ui/drag/index.d.ts +51 -0
  65. package/dist/ui/drag/index.js +81 -0
  66. package/dist/ui/elements/PaintCanvas/index.d.ts +17 -12
  67. package/dist/ui/elements/PaintCanvas/index.js +36 -17
  68. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  69. package/dist/ui/elements/PcAppSettings/index.d.ts +6 -6
  70. package/dist/ui/elements/PcAppSettings/index.js +21 -65
  71. package/dist/ui/elements/PcBrushConfig/index.d.ts +34 -16
  72. package/dist/ui/elements/PcBrushConfig/index.js +263 -153
  73. package/dist/ui/elements/PcBrushConfig/style.js +1 -1
  74. package/dist/ui/elements/PcBrushSize/index.d.ts +53 -0
  75. package/dist/ui/elements/PcBrushSize/index.js +66 -0
  76. package/dist/ui/elements/PcBrushTest/index.d.ts +26 -0
  77. package/dist/ui/elements/PcBrushTest/index.js +79 -6
  78. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  79. package/dist/ui/elements/PcButton/index.d.ts +12 -3
  80. package/dist/ui/elements/PcButton/index.js +18 -10
  81. package/dist/ui/elements/PcCanvas/index.d.ts +27 -2
  82. package/dist/ui/elements/PcCanvas/index.js +63 -13
  83. package/dist/ui/elements/PcCanvas/style.js +1 -1
  84. package/dist/ui/elements/PcCanvasMap/style.js +1 -1
  85. package/dist/ui/elements/PcClipboardTools/index.d.ts +49 -0
  86. package/dist/ui/elements/PcClipboardTools/index.js +125 -0
  87. package/dist/ui/elements/PcContextMenu/index.d.ts +2 -2
  88. package/dist/ui/elements/PcDialog/index.d.ts +46 -10
  89. package/dist/ui/elements/PcDialog/index.js +102 -51
  90. package/dist/ui/elements/PcDialogWindow/index.d.ts +141 -0
  91. package/dist/ui/elements/PcDialogWindow/index.js +385 -0
  92. package/dist/ui/elements/PcInputSettings/index.d.ts +13 -0
  93. package/dist/ui/elements/PcInputSettings/index.js +16 -1
  94. package/dist/ui/elements/PcMenu/index.d.ts +48 -3
  95. package/dist/ui/elements/PcMenu/index.js +111 -19
  96. package/dist/ui/elements/PcNumberSlider/index.d.ts +137 -0
  97. package/dist/ui/elements/PcNumberSlider/index.js +247 -0
  98. package/dist/ui/elements/PcPane/index.d.ts +3 -1
  99. package/dist/ui/elements/PcPane/index.js +44 -7
  100. package/dist/ui/elements/PcPanel/index.d.ts +2 -2
  101. package/dist/ui/elements/PcPanel/index.js +8 -8
  102. package/dist/ui/elements/PcPanelBrushes/index.d.ts +5 -17
  103. package/dist/ui/elements/PcPanelBrushes/index.js +13 -48
  104. package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
  105. package/dist/ui/elements/PcPanelColours/index.js +2 -2
  106. package/dist/ui/elements/PcPanelColours/style.js +1 -1
  107. package/dist/ui/elements/PcPanelLayers/index.js +1 -1
  108. package/dist/ui/elements/PcPanelLayers/style.js +1 -1
  109. package/dist/ui/elements/PcPopover/index.d.ts +39 -1
  110. package/dist/ui/elements/PcPopover/index.js +72 -4
  111. package/dist/ui/elements/PcProfileEditor/index.d.ts +103 -0
  112. package/dist/ui/elements/PcProfileEditor/index.js +259 -0
  113. package/dist/ui/elements/PcProfileGraph/index.d.ts +84 -0
  114. package/dist/ui/elements/PcProfileGraph/index.js +317 -0
  115. package/dist/ui/elements/PcShortcutSettings/index.js +9 -13
  116. package/dist/ui/elements/PcSliderRange/index.d.ts +147 -0
  117. package/dist/ui/elements/PcSliderRange/index.js +307 -0
  118. package/dist/ui/elements/PcStats/index.d.ts +19 -4
  119. package/dist/ui/elements/PcStats/index.js +29 -13
  120. package/dist/ui/elements/PcThemePicker/index.d.ts +17 -15
  121. package/dist/ui/elements/PcThemePicker/index.js +22 -21
  122. package/dist/ui/elements/PcToolSettings/index.d.ts +48 -9
  123. package/dist/ui/elements/PcToolSettings/index.js +382 -318
  124. package/dist/ui/elements/PcToolSettings/style.js +1 -1
  125. package/dist/ui/elements/PcToolbar/index.d.ts +15 -0
  126. package/dist/ui/elements/PcToolbar/index.js +55 -14
  127. package/dist/ui/elements/PcToolbar/style.js +1 -1
  128. package/dist/ui/icons/images/icon-brush@2x.js +3 -0
  129. package/dist/ui/icons/images/icon-circle-mask@2x.js +3 -0
  130. package/dist/ui/icons/images/icon-clipboard@2x.js +3 -0
  131. package/dist/ui/icons/images/icon-cog@2x.js +3 -0
  132. package/dist/ui/icons/images/icon-cursor@2x.js +3 -0
  133. package/dist/ui/icons/images/icon-diagonal-tlbr-size@2x.js +3 -0
  134. package/dist/ui/icons/images/icon-diagonal-trbl-size@2x.js +3 -0
  135. package/dist/ui/icons/images/icon-eraser@2x.js +3 -0
  136. package/dist/ui/icons/images/icon-eyedropper@2x.js +3 -0
  137. package/dist/ui/icons/images/icon-flip-horizontal@2x.js +3 -0
  138. package/dist/ui/icons/images/icon-flip-vertical@2x.js +3 -0
  139. package/dist/ui/icons/images/icon-focus-pen@2x.js +3 -0
  140. package/dist/ui/icons/images/icon-freehand-mask@2x.js +3 -0
  141. package/dist/ui/icons/images/icon-hidden@2x.js +3 -0
  142. package/dist/ui/icons/images/icon-horizontal-size@2x.js +3 -0
  143. package/dist/ui/icons/images/icon-layers@2x.js +3 -0
  144. package/dist/ui/icons/images/icon-linked@2x.js +3 -0
  145. package/dist/ui/icons/images/icon-locked@2x.js +3 -0
  146. package/dist/ui/icons/images/icon-moon@2x.js +3 -0
  147. package/dist/ui/icons/images/icon-pan@2x.js +3 -0
  148. package/dist/ui/icons/images/icon-polygon-mask@2x.js +3 -0
  149. package/dist/ui/icons/images/icon-pressure@2x.js +3 -0
  150. package/dist/ui/icons/images/icon-profile-graph@2x.js +3 -0
  151. package/dist/ui/icons/images/icon-randomise@2x.js +3 -0
  152. package/dist/ui/icons/images/icon-rectangle-mask@2x.js +3 -0
  153. package/dist/ui/icons/images/icon-redo@2x.js +3 -0
  154. package/dist/ui/icons/images/icon-rotate-90-left@2x.js +3 -0
  155. package/dist/ui/icons/images/icon-rotate-90-right@2x.js +3 -0
  156. package/dist/ui/icons/images/icon-rotate@2x.js +3 -0
  157. package/dist/ui/icons/images/icon-smoothing-bezier-cubic@2x.js +3 -0
  158. package/dist/ui/icons/images/icon-smoothing-bezier-quad@2x.js +3 -0
  159. package/dist/ui/icons/images/icon-smoothing-linear@2x.js +3 -0
  160. package/dist/ui/icons/images/icon-star@2x.js +3 -0
  161. package/dist/ui/icons/images/icon-sun@2x.js +3 -0
  162. package/dist/ui/icons/images/icon-tilt@2x.js +3 -0
  163. package/dist/ui/icons/images/icon-undo@2x.js +3 -0
  164. package/dist/ui/icons/images/icon-unlinked@2x.js +3 -0
  165. package/dist/ui/icons/images/icon-unlocked@2x.js +3 -0
  166. package/dist/ui/icons/images/icon-vertical-size@2x.js +3 -0
  167. package/dist/ui/icons/images/icon-visible@2x.js +3 -0
  168. package/dist/ui/icons/images/icon-zoom-in@2x.js +3 -0
  169. package/dist/ui/icons/images/icon-zoom-out@2x.js +3 -0
  170. package/dist/ui/icons/images/icon-zoom@2x.js +3 -0
  171. package/dist/ui/icons/index.d.ts +1 -1
  172. package/dist/ui/icons/index.js +86 -72
  173. package/dist/ui/index.d.ts +13 -3
  174. package/dist/ui/index.js +15 -4
  175. package/dist/ui/layout/index.d.ts +9 -1
  176. package/dist/ui/layout/index.js +11 -2
  177. package/dist/ui/styles/index.d.ts +21 -1
  178. package/dist/ui/styles/index.js +73 -14
  179. package/dist/ui/tokens/index.d.ts +5 -4
  180. package/dist/ui/tokens/index.js +12 -7
  181. package/locales/en-GB.json +39 -2
  182. package/locales/en-US.json +39 -2
  183. package/locales/fr.json +39 -2
  184. package/locales/ja.json +39 -2
  185. package/locales/ko.json +39 -2
  186. package/locales/zh-Hans.json +39 -2
  187. package/locales/zh-Hant.json +39 -2
  188. package/package.json +27 -18
  189. package/dist/ui/icons/images/brush.js +0 -3
  190. package/dist/ui/icons/images/circle-mask.js +0 -3
  191. package/dist/ui/icons/images/clipboard.js +0 -3
  192. package/dist/ui/icons/images/cursor.js +0 -3
  193. package/dist/ui/icons/images/eraser.js +0 -3
  194. package/dist/ui/icons/images/eyedropper.js +0 -3
  195. package/dist/ui/icons/images/flip-horizontal.js +0 -3
  196. package/dist/ui/icons/images/flip-vertical.js +0 -3
  197. package/dist/ui/icons/images/focus-pen.js +0 -3
  198. package/dist/ui/icons/images/freehand-mask.js +0 -3
  199. package/dist/ui/icons/images/hidden.js +0 -3
  200. package/dist/ui/icons/images/layers.js +0 -3
  201. package/dist/ui/icons/images/linked.js +0 -3
  202. package/dist/ui/icons/images/locked.js +0 -3
  203. package/dist/ui/icons/images/moon.js +0 -3
  204. package/dist/ui/icons/images/pan.js +0 -3
  205. package/dist/ui/icons/images/polygon-mask.js +0 -3
  206. package/dist/ui/icons/images/pressure.js +0 -3
  207. package/dist/ui/icons/images/randomise.js +0 -3
  208. package/dist/ui/icons/images/rectangle-mask.js +0 -3
  209. package/dist/ui/icons/images/redo.js +0 -3
  210. package/dist/ui/icons/images/rotate-90-left.js +0 -3
  211. package/dist/ui/icons/images/rotate-90-right.js +0 -3
  212. package/dist/ui/icons/images/smoothing-bezier-cubic.js +0 -3
  213. package/dist/ui/icons/images/smoothing-bezier-quad.js +0 -3
  214. package/dist/ui/icons/images/smoothing-linear.js +0 -3
  215. package/dist/ui/icons/images/star.js +0 -3
  216. package/dist/ui/icons/images/sun.js +0 -3
  217. package/dist/ui/icons/images/tilt.js +0 -3
  218. package/dist/ui/icons/images/undo.js +0 -3
  219. package/dist/ui/icons/images/unlinked.js +0 -3
  220. package/dist/ui/icons/images/unlocked.js +0 -3
  221. package/dist/ui/icons/images/visible.js +0 -3
  222. package/dist/ui/icons/images/zoom-in.js +0 -3
  223. package/dist/ui/icons/images/zoom-out.js +0 -3
  224. package/dist/ui/icons/images/zoom.js +0 -3
@@ -1,5 +1,7 @@
1
1
  import { PcElement, defineElement, emit } from "../../base/index.js";
2
+ import { markDismissingPress } from "../../dismiss/index.js";
2
3
  import { PcPanel } from "../PcPanel/index.js";
4
+ import { showsRail } from "../../layout/index.js";
3
5
  import { css, html, nothing } from "lit";
4
6
  // src/ui/elements/PcPane/index.ts
5
7
  /** Event fired when a floating pane is shown or collapsed (detail: `{ collapsed }`) */
@@ -18,6 +20,8 @@ var FOCUSABLE = "button, [href], input, select, textarea, [tabindex]:not([tabind
18
20
  * button again closes it, and focus goes back to the button.
19
21
  * - `floating`: the pane floats over the canvas as a box that collapses to a button (`collapsed`, named with its
20
22
  * `label`), firing `pc-collapse` when it's shown or collapsed.
23
+ * - `overlay`: a rail over the canvas, without a background: each button has its own, and presses between them reach
24
+ * the canvas. Its panels stay solid.
21
25
  *
22
26
  * Panes made for one purpose extend it and put their own content in its regions (see `renderStart`, `renderMiddle`
23
27
  * and `renderEnd`), e.g. `<pc-toolbar>`.
@@ -95,10 +99,16 @@ var PcPane = class extends PcElement {
95
99
  border-inline: 1px solid var(--pc-border);
96
100
  background: var(--pc-bar);
97
101
  }
102
+ /* The same size as the toolbars' buttons (borders included) */
98
103
  .rail button {
99
104
  position: relative;
100
105
  display: inline-flex;
101
- padding: var(--pc-space-1);
106
+ align-items: center;
107
+ justify-content: center;
108
+ box-sizing: border-box;
109
+ width: var(--pc-side-button-size);
110
+ height: var(--pc-side-button-size);
111
+ padding: 0;
102
112
  border: 1px solid transparent;
103
113
  border-radius: var(--pc-radius-small);
104
114
  background: transparent;
@@ -115,8 +125,8 @@ var PcPane = class extends PcElement {
115
125
  outline: var(--pc-focus-width) solid var(--pc-focus);
116
126
  }
117
127
  .rail img {
118
- width: var(--pc-icon-size);
119
- height: var(--pc-icon-size);
128
+ width: var(--pc-side-icon-size);
129
+ height: var(--pc-side-icon-size);
120
130
  filter: var(--pc-icon-filter);
121
131
  }
122
132
  .rail .badge {
@@ -130,6 +140,31 @@ var PcPane = class extends PcElement {
130
140
  background: var(--pc-red);
131
141
  }
132
142
 
143
+ /* Overlay: the rail over the canvas without a background, so only its buttons (each with its own) take presses */
144
+ :host([mode="overlay"]) {
145
+ pointer-events: none;
146
+ }
147
+ :host([mode="overlay"]) .rail {
148
+ border-inline: 0;
149
+ background: transparent;
150
+ }
151
+ :host([mode="overlay"]) .rail button,
152
+ :host([mode="overlay"]) ::slotted(*) {
153
+ pointer-events: auto;
154
+ }
155
+ :host([mode="overlay"]) .rail button {
156
+ border-color: var(--pc-border);
157
+ background: var(--pc-surface-raised);
158
+ box-shadow: var(--pc-shadow);
159
+ }
160
+ :host([mode="overlay"]) .rail button:hover {
161
+ background: var(--pc-surface-hover);
162
+ }
163
+ :host([mode="overlay"]) .rail button[aria-expanded="true"] {
164
+ border-color: var(--pc-accent);
165
+ background: var(--pc-accent);
166
+ }
167
+
133
168
  /* Floating: the pane floats over the canvas as a box, and collapses to a button */
134
169
  :host([mode="floating"]) {
135
170
  position: absolute;
@@ -176,7 +211,9 @@ var PcPane = class extends PcElement {
176
211
  const panel = this.openPanel;
177
212
  if (!panel) return;
178
213
  const path = event.composedPath();
179
- if (!path.includes(panel) && !(this.opener && path.includes(this.opener))) this.closePanel(false);
214
+ if (path.includes(panel) || this.opener && path.includes(this.opener)) return;
215
+ markDismissingPress(event);
216
+ this.closePanel(false);
180
217
  };
181
218
  onKeyDown = (event) => {
182
219
  if (event.key !== "Escape" || !this.openPanel) return;
@@ -258,7 +295,7 @@ var PcPane = class extends PcElement {
258
295
  }
259
296
  /** Show the panels for the mode: hidden behind the rail (except the open one, beside the pane) or as they are */
260
297
  syncPanels() {
261
- const rail = this.mode === "rail";
298
+ const rail = showsRail(this.mode);
262
299
  for (const panel of this.panels) {
263
300
  panel.toggleAttribute("data-pc-rail", rail);
264
301
  if (rail && panel === this.openPanel) panel.setAttribute("data-pc-open", this.edge);
@@ -267,7 +304,7 @@ var PcPane = class extends PcElement {
267
304
  }
268
305
  updated(changed) {
269
306
  if (!changed.has("mode") && !changed.has("edge")) return;
270
- if (this.mode !== "rail") this.closePanel(false);
307
+ if (!showsRail(this.mode)) this.closePanel(false);
271
308
  this.syncPanels();
272
309
  }
273
310
  /** Content of its own at its start, before what's slotted there */
@@ -284,7 +321,7 @@ var PcPane = class extends PcElement {
284
321
  }
285
322
  /** The rail: a button per panel */
286
323
  renderRail() {
287
- if (this.mode !== "rail" || this.panels.length === 0) return nothing;
324
+ if (!showsRail(this.mode) || this.panels.length === 0) return nothing;
288
325
  return html`<div
289
326
  class="rail"
290
327
  part="rail"
@@ -6,8 +6,8 @@ import { PcElement } from "../../base";
6
6
  * for screen readers.
7
7
  *
8
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.
9
+ * a dot meaning something new (e.g. unread messages). It's 290px wide (`--pc-panel-width`), and in every layout it's
10
+ * never wider than 290px (`--pc-panel-max-width`) or the space it has.
11
11
  *
12
12
  * Panels made for one purpose extend `PcPanel`, so they share its look: e.g. `<pc-panel-layers>`,
13
13
  * `<pc-panel-brushes>` and `<pc-panel-colours>`. They put their own content in the body (see `renderBody`), and give
@@ -7,8 +7,8 @@ import { css, html, nothing } from "lit";
7
7
  * for screen readers.
8
8
  *
9
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.
10
+ * a dot meaning something new (e.g. unread messages). It's 290px wide (`--pc-panel-width`), and in every layout it's
11
+ * never wider than 290px (`--pc-panel-max-width`) or the space it has.
12
12
  *
13
13
  * Panels made for one purpose extend `PcPanel`, so they share its look: e.g. `<pc-panel-layers>`,
14
14
  * `<pc-panel-brushes>` and `<pc-panel-colours>`. They put their own content in the body (see `renderBody`), and give
@@ -33,7 +33,7 @@ var PcPanel = class extends PcElement {
33
33
  box-sizing: border-box;
34
34
  width: var(--pc-panel-width, var(--pc-panel-default-width, 290px));
35
35
  min-width: 0;
36
- max-width: 100%;
36
+ max-width: min(100%, var(--pc-panel-max-width, 290px));
37
37
  min-height: 0;
38
38
  max-height: 100%;
39
39
  flex-direction: column;
@@ -44,7 +44,8 @@ var PcPanel = class extends PcElement {
44
44
  font-family: var(--pc-font);
45
45
  font-size: var(--pc-font-size);
46
46
  }
47
- /* In a pane's rail (set by the pane): hidden until opened from its button, then over the canvas beside the pane */
47
+ /* In a pane's rail (set by the pane): hidden until opened from its button, then over the canvas beside the pane,
48
+ as tall as its content needs (at most 90% of the window's height, and never taller than the pane) */
48
49
  :host([data-pc-rail]:not([data-pc-open])) {
49
50
  display: none;
50
51
  }
@@ -52,10 +53,9 @@ var PcPanel = class extends PcElement {
52
53
  position: absolute;
53
54
  z-index: 5;
54
55
  top: 0;
55
- bottom: 0;
56
- width: var(--pc-floating-panel-width, min(360px, 85vw));
57
- max-width: none;
58
- max-height: none;
56
+ /* Its usual width, kept within the window (the pane it opens from is narrower than it) */
57
+ max-width: min(var(--pc-panel-max-width, 290px), 85vw);
58
+ max-height: min(90vh, 100%);
59
59
  }
60
60
  :host([data-pc-open="left"]) {
61
61
  left: 100%;
@@ -1,5 +1,7 @@
1
1
  import { nothing } from "lit";
2
2
  import { PcPanel } from "../PcPanel";
3
+ import { type ConfirmFn } from "../../ask";
4
+ import "../PcBrushSize";
3
5
  import type PaintCanvas from "../../../lib/classes/PaintCanvas";
4
6
  import type BrushSettings from "../../../lib/classes/BrushSettings";
5
7
  import type { BrushConfig } from "../../../lib/brushes/types";
@@ -7,8 +9,7 @@ import { DELETE_CURSOR } from "../../../lib/rendering/cursors";
7
9
  import { type SaveMethod } from "../../../lib/rendering/screenshot";
8
10
  /** Cursor over a preset that a click deletes (Alt/Option held): the `--pc-delete-cursor` token's default */
9
11
  export { DELETE_CURSOR };
10
- /** Asks the user to confirm; replaceable for tests */
11
- export type ConfirmFn = (message: string) => boolean;
12
+ export type { ConfirmFn } from "../../ask";
12
13
  /** Largest shape preview in a preset tile, in CSS pixels */
13
14
  export declare const MAX_PREVIEW_SIZE = 30;
14
15
  /** Smallest shape preview in a preset tile, in CSS pixels */
@@ -31,7 +32,7 @@ export declare const previewSizes: (presets: BrushConfig[]) => Map<string, {
31
32
  * tile showing its shape at its size relative to the rest of its family, five to a row. Picking a preset closes the
32
33
  * menu; holding Alt/Option shows an "X" over custom presets, and clicking one asks before deleting it.
33
34
  *
34
- * Dragging the size control up or right makes the brush bigger; down or left makes it smaller.
35
+ * Dragging the size control (`pc-brush-size`) up or right makes the brush bigger; down or left makes it smaller.
35
36
  *
36
37
  * Fires `pc-new-preset` (detail: "brush" or "eraser") for "New preset", and `pc-toast` (detail: the message) after
37
38
  * importing, exporting or deleting presets, or when that fails.
@@ -72,11 +73,10 @@ export declare class PcPanelBrushes extends PcPanel {
72
73
  app?: PaintCanvas;
73
74
  /** Which settings this menu controls */
74
75
  kind: "brush" | "eraser";
76
+ /** Asks the user to confirm, in a dialog (replaceable for tests) */
75
77
  confirm: ConfirmFn;
76
78
  /** Saves exported presets (replaceable for tests) */
77
79
  saveFile: (blob: Blob, name: string) => Promise<SaveMethod | undefined>;
78
- /** Last pointer position while dragging the size control */
79
- private dragPoint;
80
80
  private changes;
81
81
  private readonly onKey;
82
82
  private readonly onBlur;
@@ -115,18 +115,6 @@ export declare class PcPanelBrushes extends PcPanel {
115
115
  * Select a preset and close the menu.
116
116
  */
117
117
  selectPreset(id: string): void;
118
- /**
119
- * Start dragging the size control.
120
- */
121
- private onPointerDown;
122
- /**
123
- * Resize while dragging: right and up are positive, left and down are negative.
124
- */
125
- private onPointerMove;
126
- /**
127
- * Stop dragging the size control.
128
- */
129
- private onPointerUp;
130
118
  /** Render a preset's tile */
131
119
  private renderTile;
132
120
  protected get defaultIcon(): string;
@@ -3,6 +3,8 @@ 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
4
  import { defineElement } from "../../base/index.js";
5
5
  import { PcPanel } from "../PcPanel/index.js";
6
+ import { askConfirm, deletePresetQuestion } from "../../ask/index.js";
7
+ import "../PcBrushSize/index.js";
6
8
  import ChangeController from "../../ChangeController/index.js";
7
9
  import { groupPresetsByAuthor } from "../../../lib/brushes/authors/index.js";
8
10
  import "../../../lib/presets/index.js";
@@ -55,7 +57,7 @@ var previewSizes = (presets) => {
55
57
  * tile showing its shape at its size relative to the rest of its family, five to a row. Picking a preset closes the
56
58
  * menu; holding Alt/Option shows an "X" over custom presets, and clicking one asks before deleting it.
57
59
  *
58
- * Dragging the size control up or right makes the brush bigger; down or left makes it smaller.
60
+ * Dragging the size control (`pc-brush-size`) up or right makes the brush bigger; down or left makes it smaller.
59
61
  *
60
62
  * Fires `pc-new-preset` (detail: "brush" or "eraser") for "New preset", and `pc-toast` (detail: the message) after
61
63
  * importing, exporting or deleting presets, or when that fails.
@@ -76,11 +78,10 @@ var PcPanelBrushes = class extends PcPanel {
76
78
  unsafeCSS(WINDOWS_CSS),
77
79
  unsafeCSS(PILLS_CSS)
78
80
  ];
79
- confirm = (message) => window.confirm(message);
81
+ /** Asks the user to confirm, in a dialog (replaceable for tests) */
82
+ confirm = (options) => askConfirm(this.renderRoot, options);
80
83
  /** Saves exported presets (replaceable for tests) */
81
84
  saveFile = (blob, name) => saveFile(blob, name, void 0, JSON_FILE_TYPE);
82
- /** Last pointer position while dragging the size control */
83
- dragPoint = null;
84
85
  changes = new ChangeController(this, () => [this.app, this.settings]);
85
86
  onKey = (event) => {
86
87
  if (event.key === "Alt") this.toggleAttribute("deleting", event.type === "keydown");
@@ -128,7 +129,7 @@ var PcPanelBrushes = class extends PcPanel {
128
129
  const app = this.app;
129
130
  if (!app) return;
130
131
  const name = preset.name ?? preset.id;
131
- if (!this.confirm(this.t("paintCanvas.presets.deleteConfirm", { name }))) return;
132
+ if (!await this.confirm(deletePresetQuestion(this.translations, name))) return;
132
133
  try {
133
134
  await app.customPresets.remove(preset.id);
134
135
  this.emit("pc-toast", {
@@ -215,34 +216,6 @@ var PcPanelBrushes = class extends PcPanel {
215
216
  this.settings?.select(id);
216
217
  this.app?.closePanel();
217
218
  }
218
- /**
219
- * Start dragging the size control.
220
- */
221
- onPointerDown(event) {
222
- this.dragPoint = {
223
- x: event.clientX,
224
- y: event.clientY
225
- };
226
- event.currentTarget.setPointerCapture(event.pointerId);
227
- }
228
- /**
229
- * Resize while dragging: right and up are positive, left and down are negative.
230
- */
231
- onPointerMove(event) {
232
- if (!this.dragPoint || !this.settings) return;
233
- const delta = event.clientX - this.dragPoint.x + (this.dragPoint.y - event.clientY);
234
- this.dragPoint = {
235
- x: event.clientX,
236
- y: event.clientY
237
- };
238
- if (delta !== 0) this.settings.resize(delta);
239
- }
240
- /**
241
- * Stop dragging the size control.
242
- */
243
- onPointerUp() {
244
- this.dragPoint = null;
245
- }
246
219
  /** Render a preset's tile */
247
220
  renderTile(settings, preset, sizes) {
248
221
  const size = sizes.get(preset.id);
@@ -296,19 +269,11 @@ var PcPanelBrushes = class extends PcPanel {
296
269
  style=${styleMap({ maskImage: typeof preset.image === "string" ? `url("${preset.image}")` : void 0 })}
297
270
  ></span>
298
271
  <span class="name">${preset.name ?? preset.id}</span>
299
- <div
272
+ <pc-brush-size
300
273
  class="size"
301
- role="slider"
302
- aria-label=${this.t("paintCanvas.brushMenu.size", { tool: title })}
303
- aria-valuenow=${settings.width}
304
- title=${this.t("paintCanvas.brushMenu.dragToResize")}
305
- @pointerdown=${this.onPointerDown}
306
- @pointermove=${this.onPointerMove}
307
- @pointerup=${this.onPointerUp}
308
- @pointercancel=${this.onPointerUp}
309
- >
310
- ${settings.width === settings.height ? settings.width : `${settings.width} × ${settings.height}`}
311
- </div>
274
+ .settings=${settings}
275
+ label=${this.t("paintCanvas.brushMenu.size", { tool: title })}
276
+ ></pc-brush-size>
312
277
  </div>
313
278
  <div
314
279
  class="presets"
@@ -324,14 +289,14 @@ var PcPanelBrushes = class extends PcPanel {
324
289
  <div class="actions">
325
290
  <button
326
291
  type="button"
327
- class="pill new-preset"
292
+ class="pill-s new-preset"
328
293
  @click=${() => this.emit("pc-new-preset", this.kind)}
329
294
  >
330
295
  ${this.t("paintCanvas.brushMenu.create")}
331
296
  </button>
332
297
  <button
333
298
  type="button"
334
- class="pill import"
299
+ class="pill-s import"
335
300
  @click=${() => this.renderRoot.querySelector(".import-file")?.click()}
336
301
  >
337
302
  ${this.t("paintCanvas.brushMenu.import")}
@@ -354,7 +319,7 @@ var PcPanelBrushes = class extends PcPanel {
354
319
  </label>
355
320
  <button
356
321
  type="button"
357
- class="pill export"
322
+ class="pill-s export"
358
323
  @click=${() => void this.exportFile()}
359
324
  >
360
325
  ${this.t("paintCanvas.brushMenu.export")}
@@ -1,3 +1,3 @@
1
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";
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 margin: -4px -8px -4px 0;\n border-left: 1px solid var(--pc-border);\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.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
3
  export { style_default as default };
@@ -613,14 +613,14 @@ var PcPanelColours = class extends PcPanel {
613
613
  <div class="actions">
614
614
  <button
615
615
  type="button"
616
- class="pill import"
616
+ class="pill-s import"
617
617
  @click=${() => this.renderRoot.querySelector(".import-file")?.click()}
618
618
  >
619
619
  ${this.t("paintCanvas.palette.import")}
620
620
  </button>
621
621
  <button
622
622
  type="button"
623
- class="pill export"
623
+ class="pill-s export"
624
624
  @click=${() => void this.exportFile()}
625
625
  >
626
626
  ${this.t("paintCanvas.palette.export")}
@@ -1,3 +1,3 @@
1
1
  // src/ui/elements/PcPanelColours/style.css?inline
2
- var style_default = ".palette {\n display: flex;\n flex-direction: column;\n gap: var(--pc-space-2);\n}\n\n/* The colours and their hex value beside the colour model's sliders */\n.top {\n display: flex;\n gap: var(--pc-space-2);\n}\n.current {\n display: flex;\n flex: none;\n flex-direction: column;\n width: 76px;\n border: 1px solid var(--pc-border);\n}\n.current pc-colour-pair {\n --pc-colour-pair-size: 76px;\n}\n.hex {\n box-sizing: border-box;\n width: 100%;\n min-height: var(--pc-control-size);\n padding: var(--pc-space-1);\n border: 0;\n border-top: 1px solid var(--pc-border);\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: var(--pc-font-size-small);\n text-align: center;\n font-variant-numeric: tabular-nums;\n}\n.hex[aria-invalid=\"true\"] {\n outline: 2px solid var(--pc-dismiss-text);\n outline-offset: -2px;\n}\n.hex:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n\n.channels {\n display: flex;\n flex: 1;\n flex-direction: column;\n min-width: 0;\n border: 1px solid var(--pc-border);\n}\n[role=\"tablist\"] {\n display: flex;\n}\n[role=\"tab\"] {\n flex: 1;\n min-height: 28px;\n border: 0;\n border-bottom: 1px solid var(--pc-border);\n background: var(--pc-surface-active);\n color: var(--pc-text);\n font: inherit;\n cursor: pointer;\n}\n[role=\"tab\"] + [role=\"tab\"] {\n border-left: 1px solid var(--pc-border);\n}\n[role=\"tab\"][aria-selected=\"true\"] {\n border-bottom-color: transparent;\n background: var(--pc-surface);\n font-weight: bold;\n}\n[role=\"tab\"]:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n.sliders {\n display: grid;\n flex: 1;\n grid-template-columns: auto minmax(0, 1fr) 3.6em;\n gap: var(--pc-space-1);\n align-items: center;\n padding: var(--pc-space-1) var(--pc-space-2);\n background: var(--pc-surface);\n}\n.letter {\n font-weight: bold;\n}\n/* Each slider's track shows the colours its values give (--track, set per slider), under a round white handle with a\n dark outline that shows on any colour */\n.sliders input[type=\"range\"] {\n width: 100%;\n min-width: 0;\n height: 20px;\n margin: 0;\n background: transparent;\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n}\n.sliders input[type=\"range\"]::-webkit-slider-runnable-track {\n height: 10px;\n border: 1px solid var(--pc-border);\n border-radius: var(--pc-radius-pill);\n background: var(--track);\n}\n.sliders input[type=\"range\"]::-moz-range-track {\n height: 10px;\n border: 1px solid var(--pc-border);\n border-radius: var(--pc-radius-pill);\n background: var(--track);\n}\n.sliders input[type=\"range\"]::-webkit-slider-thumb {\n width: 16px;\n height: 16px;\n margin-top: -4px;\n border: 2px solid #000000;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 0 0 1px #ffffff;\n -webkit-appearance: none;\n}\n.sliders input[type=\"range\"]::-moz-range-thumb {\n box-sizing: border-box;\n width: 16px;\n height: 16px;\n border: 2px solid #000000;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 0 0 1px #ffffff;\n}\n.sliders input[type=\"number\"] {\n box-sizing: border-box;\n width: 100%;\n padding: 2px var(--pc-space-1);\n border: 1px solid var(--pc-border);\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: var(--pc-font-size-small);\n font-variant-numeric: tabular-nums;\n}\n.sliders input:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: 1px;\n}\n\n/* Saved colours: square swatches in a grid that scrolls */\n.swatches {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(32px, 1fr));\n align-content: start;\n min-height: 64px;\n max-height: 132px;\n overflow-y: auto;\n border: 1px solid var(--pc-border);\n background: var(--pc-bar);\n}\n.swatch,\n.add {\n aspect-ratio: 1;\n min-width: 32px;\n padding: 0;\n border: 0;\n border-right: 1px solid var(--pc-border);\n border-bottom: 1px solid var(--pc-border);\n cursor: pointer;\n}\n.swatch {\n background: var(--colour);\n /* Long presses open the colour's menu instead of the browser's */\n -webkit-touch-callout: none;\n touch-action: manipulation;\n}\n.swatch[aria-pressed=\"true\"] {\n box-shadow:\n inset 0 0 0 2px #ffffff,\n inset 0 0 0 3px #000000;\n}\n:host([deleting]) .swatch {\n cursor: var(--pc-delete-cursor);\n}\n.add {\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: 20px;\n font-weight: bold;\n}\n.add:hover:not(:disabled) {\n background: var(--pc-surface-hover);\n}\n.add:disabled {\n color: var(--pc-disabled);\n cursor: default;\n}\n.swatch:focus-visible,\n.add:focus-visible {\n position: relative;\n z-index: 1;\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n\n.actions {\n display: flex;\n gap: var(--pc-space-2);\n}\n/* Same size as the brush menu's Import and Export */\n.actions button.pill {\n flex: 1;\n padding: 4px 8px;\n}\n";
2
+ var style_default = ".palette {\n display: flex;\n flex-direction: column;\n gap: var(--pc-space-2);\n}\n\n/* The colours and their hex value beside the colour model's sliders */\n.top {\n display: flex;\n gap: var(--pc-space-2);\n}\n.current {\n display: flex;\n flex: none;\n flex-direction: column;\n width: 76px;\n border: 1px solid var(--pc-border);\n}\n.current pc-colour-pair {\n --pc-colour-pair-size: 76px;\n}\n.hex {\n box-sizing: border-box;\n width: 100%;\n min-height: var(--pc-control-size);\n padding: var(--pc-space-1);\n border: 0;\n border-top: 1px solid var(--pc-border);\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: var(--pc-font-size-small);\n text-align: center;\n font-variant-numeric: tabular-nums;\n}\n.hex[aria-invalid=\"true\"] {\n outline: 2px solid var(--pc-dismiss-text);\n outline-offset: -2px;\n}\n.hex:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n\n.channels {\n display: flex;\n flex: 1;\n flex-direction: column;\n min-width: 0;\n border: 1px solid var(--pc-border);\n}\n[role=\"tablist\"] {\n display: flex;\n}\n[role=\"tab\"] {\n flex: 1;\n min-height: 28px;\n border: 0;\n border-bottom: 1px solid var(--pc-border);\n background: var(--pc-surface-active);\n color: var(--pc-text);\n font: inherit;\n cursor: pointer;\n}\n[role=\"tab\"] + [role=\"tab\"] {\n border-left: 1px solid var(--pc-border);\n}\n[role=\"tab\"][aria-selected=\"true\"] {\n border-bottom-color: transparent;\n background: var(--pc-surface);\n font-weight: bold;\n}\n[role=\"tab\"]:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n.sliders {\n display: grid;\n flex: 1;\n grid-template-columns: auto minmax(0, 1fr) 3.6em;\n gap: var(--pc-space-1);\n align-items: center;\n padding: var(--pc-space-1) var(--pc-space-2);\n background: var(--pc-surface);\n}\n.letter {\n font-weight: bold;\n}\n/* Each slider's track shows the colours its values give (--track, set per slider), under a round white handle with a\n dark outline that shows on any colour */\n.sliders input[type=\"range\"] {\n width: 100%;\n min-width: 0;\n height: 20px;\n margin: 0;\n background: transparent;\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n}\n.sliders input[type=\"range\"]::-webkit-slider-runnable-track {\n height: 10px;\n border: 1px solid var(--pc-border);\n border-radius: var(--pc-radius-pill);\n background: var(--track);\n}\n.sliders input[type=\"range\"]::-moz-range-track {\n height: 10px;\n border: 1px solid var(--pc-border);\n border-radius: var(--pc-radius-pill);\n background: var(--track);\n}\n.sliders input[type=\"range\"]::-webkit-slider-thumb {\n width: 16px;\n height: 16px;\n margin-top: -4px;\n border: 2px solid #000000;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 0 0 1px #ffffff;\n -webkit-appearance: none;\n}\n.sliders input[type=\"range\"]::-moz-range-thumb {\n box-sizing: border-box;\n width: 16px;\n height: 16px;\n border: 2px solid #000000;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 0 0 1px #ffffff;\n}\n.sliders input[type=\"number\"] {\n box-sizing: border-box;\n width: 100%;\n padding: 2px var(--pc-space-1);\n border: 1px solid var(--pc-border);\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: var(--pc-font-size-small);\n font-variant-numeric: tabular-nums;\n}\n.sliders input:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: 1px;\n}\n\n/* Saved colours: square swatches in a grid that scrolls */\n.swatches {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(32px, 1fr));\n align-content: start;\n min-height: 64px;\n max-height: 132px;\n overflow-y: auto;\n border: 1px solid var(--pc-border);\n background: var(--pc-bar);\n}\n.swatch,\n.add {\n aspect-ratio: 1;\n min-width: 32px;\n padding: 0;\n border: 0;\n border-right: 1px solid var(--pc-border);\n border-bottom: 1px solid var(--pc-border);\n cursor: pointer;\n}\n.swatch {\n background: var(--colour);\n /* Long presses open the colour's menu instead of the browser's */\n -webkit-touch-callout: none;\n touch-action: manipulation;\n}\n.swatch[aria-pressed=\"true\"] {\n box-shadow:\n inset 0 0 0 2px #ffffff,\n inset 0 0 0 3px #000000;\n}\n:host([deleting]) .swatch {\n cursor: var(--pc-delete-cursor);\n}\n.add {\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: 20px;\n font-weight: bold;\n}\n.add:hover:not(:disabled) {\n background: var(--pc-surface-hover);\n}\n.add:disabled {\n color: var(--pc-disabled);\n cursor: default;\n}\n.swatch:focus-visible,\n.add:focus-visible {\n position: relative;\n z-index: 1;\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n\n.actions {\n display: flex;\n gap: var(--pc-space-2);\n}\n/* Import and Export share the row */\n.actions button.pill-s {\n flex: 1;\n}\n";
3
3
  export { style_default as default };
@@ -199,7 +199,7 @@ var PcPanelLayers = class extends PcPanel {
199
199
  ${this.renderRows(layers)}
200
200
  <button
201
201
  type="button"
202
- class="pill new ${this.drag?.action === "duplicate" ? "drop-target" : ""}"
202
+ class="pill-s new ${this.drag?.action === "duplicate" ? "drop-target" : ""}"
203
203
  ?disabled=${!layers.canCreate}
204
204
  @click=${() => layers.create()}
205
205
  >
@@ -1,3 +1,3 @@
1
1
  // src/ui/elements/PcPanelLayers/style.css?inline
2
- var style_default = "/* \"New Layer\": a full-width purple pill */\nbutton.new {\n width: 100%;\n min-height: 40px;\n}\n/* Where a dragged layer would land: a full-width block with its name, as tall as the row (set by the menu) */\n.ghost {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n padding: 0 8px;\n border: 2px dashed;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n/* Dropping a dragged layer here duplicates it (highlighted for touch, where there is no \"+\" cursor) */\nbutton.new.drop-target {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: 2px;\n}\n/* For screen readers only */\n.announcement {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n";
2
+ var style_default = "/* \"New Layer\": a full-width purple pill */\nbutton.new {\n width: 100%;\n}\n/* Where a dragged layer would land: a full-width block with its name, as tall as the row (set by the menu) */\n.ghost {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n padding: 0 8px;\n border: 2px dashed;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n/* Dropping a dragged layer here duplicates it (highlighted for touch, where there is no \"+\" cursor) */\nbutton.new.drop-target {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: 2px;\n}\n/* For screen readers only */\n.announcement {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n";
3
3
  export { style_default as default };
@@ -1,8 +1,30 @@
1
1
  import { type PropertyValues } from "lit";
2
2
  import { PcElement } from "../../base";
3
+ /** Height of a popover's arrow (pixels) */
4
+ export declare const POPOVER_ARROW_SIZE = 8;
5
+ /** Space kept between a placed popover and the window's edges (pixels) */
6
+ export declare const POPOVER_MARGIN = 8;
7
+ /**
8
+ * Where a popover goes under its anchor: centred below it (leaving room for the arrow), moved sideways as far as needed
9
+ * to stay in the window, with its arrow still pointing at the anchor's centre (`arrowX`, from the popover's left).
10
+ *
11
+ * @example
12
+ * popoverBelow({ left: 100, bottom: 40, width: 40 }, 200, 1000); // { left: 20, top: 48, arrowX: 100 }
13
+ * popoverBelow({ left: 0, bottom: 40, width: 40 }, 200, 1000); // { left: 8, top: 48, arrowX: 12 }
14
+ */
15
+ export declare const popoverBelow: (anchor: {
16
+ left: number;
17
+ bottom: number;
18
+ width: number;
19
+ }, popoverWidth: number, viewportWidth: number, margin?: number) => {
20
+ left: number;
21
+ top: number;
22
+ arrowX: number;
23
+ };
3
24
  /**
4
25
  * A floating box for a menu or settings (e.g. the brush presets), shown while `open`. The implementer places it (e.g.
5
- * next to the button that opens it). It isn't modal: Escape, or a press outside it and outside its `anchor`, fires
26
+ * next to the button that opens it), or `placeBelow` puts it under its `anchor`. With `arrow`, an arrow on its top edge
27
+ * points up at the anchor (at `--pc-popover-arrow-x` from its left, the middle by default). It isn't modal: Escape, or a press outside it and outside its `anchor`, fires
6
28
  * `pc-close`, and the implementer sets `open` to false in response. Focus moves into it when it opens.
7
29
  *
8
30
  * @example
@@ -20,6 +42,10 @@ export declare class PcPopover extends PcElement {
20
42
  anchor: {
21
43
  attribute: boolean;
22
44
  };
45
+ arrow: {
46
+ type: BooleanConstructor;
47
+ reflect: boolean;
48
+ };
23
49
  translations: {
24
50
  attribute: boolean;
25
51
  };
@@ -37,8 +63,20 @@ export declare class PcPopover extends PcElement {
37
63
  label: string;
38
64
  /** The element that opens it (pressing it doesn't count as pressing outside) */
39
65
  anchor?: Element;
66
+ /** Whether an arrow on its top edge points up at the anchor */
67
+ arrow: boolean;
40
68
  private readonly onPointerDown;
41
69
  constructor();
70
+ /**
71
+ * Put the popover under its anchor, in the window (see `popoverBelow`), with its arrow pointing at the anchor. Call it
72
+ * once open, so it can be measured.
73
+ *
74
+ * @example
75
+ * popover.open = true;
76
+ * await popover.updateComplete;
77
+ * popover.placeBelow();
78
+ */
79
+ placeBelow(): void;
42
80
  connectedCallback(): void;
43
81
  disconnectedCallback(): void;
44
82
  updated(changed: PropertyValues<this>): void;
@@ -1,9 +1,34 @@
1
1
  import { PcElement, defineElement, emit } from "../../base/index.js";
2
+ import { markDismissingPress } from "../../dismiss/index.js";
2
3
  import { css, html } from "lit";
3
4
  // src/ui/elements/PcPopover/index.ts
5
+ /** Height of a popover's arrow (pixels) */
6
+ var POPOVER_ARROW_SIZE = 8;
7
+ /** Side of the square turned 45° that makes the arrow (its diagonal is about twice the arrow's height) */
8
+ var ARROW_SQUARE = Math.round(8 * Math.SQRT2);
9
+ /** Space kept between a placed popover and the window's edges (pixels) */
10
+ var POPOVER_MARGIN = 8;
11
+ /**
12
+ * Where a popover goes under its anchor: centred below it (leaving room for the arrow), moved sideways as far as needed
13
+ * to stay in the window, with its arrow still pointing at the anchor's centre (`arrowX`, from the popover's left).
14
+ *
15
+ * @example
16
+ * popoverBelow({ left: 100, bottom: 40, width: 40 }, 200, 1000); // { left: 20, top: 48, arrowX: 100 }
17
+ * popoverBelow({ left: 0, bottom: 40, width: 40 }, 200, 1000); // { left: 8, top: 48, arrowX: 12 }
18
+ */
19
+ var popoverBelow = (anchor, popoverWidth, viewportWidth, margin = 8) => {
20
+ const centre = anchor.left + anchor.width / 2;
21
+ const left = Math.max(margin, Math.min(centre - popoverWidth / 2, viewportWidth - margin - popoverWidth));
22
+ return {
23
+ left,
24
+ top: anchor.bottom + 8,
25
+ arrowX: centre - left
26
+ };
27
+ };
4
28
  /**
5
29
  * A floating box for a menu or settings (e.g. the brush presets), shown while `open`. The implementer places it (e.g.
6
- * next to the button that opens it). It isn't modal: Escape, or a press outside it and outside its `anchor`, fires
30
+ * next to the button that opens it), or `placeBelow` puts it under its `anchor`. With `arrow`, an arrow on its top edge
31
+ * points up at the anchor (at `--pc-popover-arrow-x` from its left, the middle by default). It isn't modal: Escape, or a press outside it and outside its `anchor`, fires
7
32
  * `pc-close`, and the implementer sets `open` to false in response. Focus moves into it when it opens.
8
33
  *
9
34
  * @example
@@ -17,7 +42,11 @@ var PcPopover = class extends PcElement {
17
42
  reflect: true
18
43
  },
19
44
  label: { type: String },
20
- anchor: { attribute: false }
45
+ anchor: { attribute: false },
46
+ arrow: {
47
+ type: Boolean,
48
+ reflect: true
49
+ }
21
50
  };
22
51
  static styles = [PcElement.styles, css`
23
52
  :host {
@@ -32,6 +61,7 @@ var PcPopover = class extends PcElement {
32
61
  max-height: inherit;
33
62
  overflow: auto;
34
63
  padding: var(--pc-space-2);
64
+ border: 1px solid var(--pc-border);
35
65
  border-radius: var(--pc-radius);
36
66
  background: var(--pc-surface-raised);
37
67
  color: var(--pc-text);
@@ -39,6 +69,22 @@ var PcPopover = class extends PcElement {
39
69
  font-family: var(--pc-font);
40
70
  font-size: var(--pc-font-size);
41
71
  }
72
+ /* A square turned 45° with its top corner showing, outlined like the popover */
73
+ :host([arrow])::before {
74
+ content: "";
75
+ position: absolute;
76
+ z-index: 1;
77
+ top: -${ARROW_SQUARE / 2}px;
78
+ left: var(--pc-popover-arrow-x, 50%);
79
+ width: ${ARROW_SQUARE}px;
80
+ height: ${ARROW_SQUARE}px;
81
+ box-sizing: border-box;
82
+ border: 1px solid var(--pc-border);
83
+ border-right-color: transparent;
84
+ border-bottom-color: transparent;
85
+ background: var(--pc-surface-raised);
86
+ transform: translateX(-50%) rotate(45deg);
87
+ }
42
88
  [role="dialog"]:focus-visible {
43
89
  outline: var(--pc-focus-width) solid var(--pc-focus);
44
90
  }
@@ -46,12 +92,34 @@ var PcPopover = class extends PcElement {
46
92
  onPointerDown = (event) => {
47
93
  if (!this.open) return;
48
94
  const path = event.composedPath();
49
- if (!path.includes(this) && !(this.anchor && path.includes(this.anchor))) emit(this, "pc-close");
95
+ if (path.includes(this) || this.anchor && path.includes(this.anchor)) return;
96
+ markDismissingPress(event);
97
+ emit(this, "pc-close");
50
98
  };
51
99
  constructor() {
52
100
  super();
53
101
  this.open = false;
54
102
  this.label = "";
103
+ this.arrow = false;
104
+ }
105
+ /**
106
+ * Put the popover under its anchor, in the window (see `popoverBelow`), with its arrow pointing at the anchor. Call it
107
+ * once open, so it can be measured.
108
+ *
109
+ * @example
110
+ * popover.open = true;
111
+ * await popover.updateComplete;
112
+ * popover.placeBelow();
113
+ */
114
+ placeBelow() {
115
+ if (!this.anchor) return;
116
+ const { left, top, arrowX } = popoverBelow(this.anchor.getBoundingClientRect(), this.offsetWidth, window.innerWidth);
117
+ Object.assign(this.style, {
118
+ position: "fixed",
119
+ left: `${left}px`,
120
+ top: `${top}px`
121
+ });
122
+ this.style.setProperty("--pc-popover-arrow-x", `${arrowX}px`);
55
123
  }
56
124
  connectedCallback() {
57
125
  super.connectedCallback();
@@ -83,4 +151,4 @@ var PcPopover = class extends PcElement {
83
151
  }
84
152
  };
85
153
  defineElement("pc-popover", PcPopover);
86
- export { PcPopover };
154
+ export { POPOVER_ARROW_SIZE, POPOVER_MARGIN, PcPopover, popoverBelow };