@lvl99dotcom/paintcanvas 0.3.1 → 0.5.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 (212) hide show
  1. package/README.md +30 -15
  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/brushes/presets/round/index.d.ts +1 -1
  5. package/dist/lib/brushes/presets/round/index.js +12 -7
  6. package/dist/lib/brushes/presets/square/index.d.ts +1 -1
  7. package/dist/lib/brushes/presets/square/index.js +7 -3
  8. package/dist/lib/brushes/shapeImage/index.d.ts +7 -3
  9. package/dist/lib/brushes/shapeImage/index.js +15 -9
  10. package/dist/lib/brushes/shapes/index.d.ts +18 -0
  11. package/dist/lib/brushes/shapes/index.js +86 -0
  12. package/dist/lib/classes/BrushSettings/index.d.ts +53 -2
  13. package/dist/lib/classes/BrushSettings/index.js +85 -14
  14. package/dist/lib/classes/KeyboardShortcuts/index.d.ts +47 -7
  15. package/dist/lib/classes/KeyboardShortcuts/index.js +145 -20
  16. package/dist/lib/classes/PaintCanvas/index.d.ts +123 -27
  17. package/dist/lib/classes/PaintCanvas/index.js +208 -41
  18. package/dist/lib/classes/PaintCanvas/tool.d.ts +15 -0
  19. package/dist/lib/classes/PaintCanvas/tool.js +18 -0
  20. package/dist/lib/classes/Palette/index.d.ts +58 -0
  21. package/dist/lib/classes/Palette/index.js +92 -0
  22. package/dist/lib/classes/SettingsAutosave/index.d.ts +53 -0
  23. package/dist/lib/classes/SettingsAutosave/index.js +98 -0
  24. package/dist/lib/classes/ShortcutSettings/index.d.ts +63 -0
  25. package/dist/lib/classes/ShortcutSettings/index.js +110 -0
  26. package/dist/lib/classes/UiSettings/index.d.ts +68 -0
  27. package/dist/lib/classes/UiSettings/index.js +82 -0
  28. package/dist/lib/palette/index.d.ts +49 -0
  29. package/dist/lib/palette/index.js +82 -0
  30. package/dist/lib/palette/schema.d.ts +45 -0
  31. package/dist/lib/palette/schema.js +41 -0
  32. package/dist/lib/rendering/cursors/index.d.ts +2 -0
  33. package/dist/lib/rendering/cursors/index.js +3 -1
  34. package/dist/lib/rendering/sample/index.js +3 -2
  35. package/dist/lib/settings/index.d.ts +63 -0
  36. package/dist/lib/settings/index.js +93 -0
  37. package/dist/lib/settings/schema.d.ts +168 -0
  38. package/dist/lib/settings/schema.js +116 -0
  39. package/dist/lib/storage/index.d.ts +36 -5
  40. package/dist/lib/storage/index.js +87 -29
  41. package/dist/lib/utils/colour/index.d.ts +72 -0
  42. package/dist/lib/utils/colour/index.js +154 -0
  43. package/dist/lib/utils/shortcuts/index.d.ts +174 -11
  44. package/dist/lib/utils/shortcuts/index.js +391 -145
  45. package/dist/lib/validation/validators.generated.d.ts +5 -0
  46. package/dist/lib/validation/validators.generated.js +1040 -1
  47. package/dist/locales/en-GB.js +96 -3
  48. package/dist/locales/en-US.js +96 -3
  49. package/dist/locales/fr.js +96 -3
  50. package/dist/locales/ja.js +96 -3
  51. package/dist/locales/ko.js +96 -3
  52. package/dist/locales/zh-Hans.js +96 -3
  53. package/dist/locales/zh-Hant.js +96 -3
  54. package/dist/package.js +1 -1
  55. package/dist/testing/recordingCanvas.js +2 -1
  56. package/dist/ui/base/index.d.ts +2 -2
  57. package/dist/ui/dismiss/index.d.ts +21 -0
  58. package/dist/ui/dismiss/index.js +27 -0
  59. package/dist/ui/drag/index.d.ts +51 -0
  60. package/dist/ui/drag/index.js +81 -0
  61. package/dist/ui/elements/PaintCanvas/index.d.ts +57 -50
  62. package/dist/ui/elements/PaintCanvas/index.js +141 -174
  63. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  64. package/dist/ui/elements/PcAppSettings/index.d.ts +59 -0
  65. package/dist/ui/elements/PcAppSettings/index.js +222 -0
  66. package/dist/ui/elements/PcBrushConfig/index.d.ts +9 -2
  67. package/dist/ui/elements/PcBrushConfig/index.js +47 -7
  68. package/dist/ui/elements/PcBrushConfig/style.js +1 -1
  69. package/dist/ui/elements/PcBrushSize/index.d.ts +53 -0
  70. package/dist/ui/elements/PcBrushSize/index.js +66 -0
  71. package/dist/ui/elements/PcBrushTest/index.d.ts +26 -0
  72. package/dist/ui/elements/PcBrushTest/index.js +73 -5
  73. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  74. package/dist/ui/elements/PcCanvas/index.d.ts +22 -2
  75. package/dist/ui/elements/PcCanvas/index.js +60 -15
  76. package/dist/ui/elements/PcCanvasMap/style.js +1 -1
  77. package/dist/ui/elements/PcClipboardTools/index.d.ts +48 -0
  78. package/dist/ui/elements/PcClipboardTools/index.js +118 -0
  79. package/dist/ui/elements/PcColourPair/index.d.ts +43 -0
  80. package/dist/ui/elements/PcColourPair/index.js +125 -0
  81. package/dist/ui/elements/PcContextMenu/index.d.ts +71 -34
  82. package/dist/ui/elements/PcContextMenu/index.js +128 -97
  83. package/dist/ui/elements/PcInputSettings/index.d.ts +13 -0
  84. package/dist/ui/elements/PcInputSettings/index.js +16 -1
  85. package/dist/ui/elements/PcLayerItem/index.d.ts +13 -34
  86. package/dist/ui/elements/PcLayerItem/index.js +39 -91
  87. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  88. package/dist/ui/elements/PcMenu/index.d.ts +138 -0
  89. package/dist/ui/elements/PcMenu/index.js +296 -0
  90. package/dist/ui/elements/PcNumberSlider/index.d.ts +137 -0
  91. package/dist/ui/elements/PcNumberSlider/index.js +247 -0
  92. package/dist/ui/elements/PcPane/index.d.ts +120 -0
  93. package/dist/ui/elements/PcPane/index.js +394 -0
  94. package/dist/ui/elements/PcPanel/index.d.ts +26 -8
  95. package/dist/ui/elements/PcPanel/index.js +76 -8
  96. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.d.ts +22 -21
  97. package/dist/ui/elements/{PcBrushMenu → PcPanelBrushes}/index.js +24 -57
  98. package/dist/ui/elements/PcPanelBrushes/style.js +3 -0
  99. package/dist/ui/elements/PcPanelColours/index.d.ts +171 -0
  100. package/dist/ui/elements/PcPanelColours/index.js +650 -0
  101. package/dist/ui/elements/PcPanelColours/style.js +3 -0
  102. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.d.ts +17 -4
  103. package/dist/ui/elements/{PcLayersMenu → PcPanelLayers}/index.js +17 -10
  104. package/dist/ui/elements/PcPanelLayers/style.js +3 -0
  105. package/dist/ui/elements/PcPopover/index.d.ts +39 -1
  106. package/dist/ui/elements/PcPopover/index.js +72 -4
  107. package/dist/ui/elements/PcShortcutSettings/index.d.ts +69 -0
  108. package/dist/ui/elements/PcShortcutSettings/index.js +273 -0
  109. package/dist/ui/elements/PcThemePicker/index.d.ts +17 -15
  110. package/dist/ui/elements/PcThemePicker/index.js +22 -21
  111. package/dist/ui/elements/PcToolSettings/index.d.ts +48 -9
  112. package/dist/ui/elements/PcToolSettings/index.js +384 -319
  113. package/dist/ui/elements/PcToolSettings/style.js +1 -1
  114. package/dist/ui/elements/PcToolbar/index.d.ts +62 -12
  115. package/dist/ui/elements/PcToolbar/index.js +119 -57
  116. package/dist/ui/elements/PcToolbar/style.js +1 -1
  117. package/dist/ui/icons/images/icon-brush@2x.js +3 -0
  118. package/dist/ui/icons/images/icon-circle-mask@2x.js +3 -0
  119. package/dist/ui/icons/images/icon-clipboard@2x.js +3 -0
  120. package/dist/ui/icons/images/icon-cog@2x.js +3 -0
  121. package/dist/ui/icons/images/icon-cursor@2x.js +3 -0
  122. package/dist/ui/icons/images/icon-diagonal-tlbr-size@2x.js +3 -0
  123. package/dist/ui/icons/images/icon-diagonal-trbl-size@2x.js +3 -0
  124. package/dist/ui/icons/images/icon-eraser@2x.js +3 -0
  125. package/dist/ui/icons/images/icon-eyedropper@2x.js +3 -0
  126. package/dist/ui/icons/images/icon-flip-horizontal@2x.js +3 -0
  127. package/dist/ui/icons/images/icon-flip-vertical@2x.js +3 -0
  128. package/dist/ui/icons/images/icon-focus-pen@2x.js +3 -0
  129. package/dist/ui/icons/images/icon-freehand-mask@2x.js +3 -0
  130. package/dist/ui/icons/images/icon-hidden@2x.js +3 -0
  131. package/dist/ui/icons/images/icon-horizontal-size@2x.js +3 -0
  132. package/dist/ui/icons/images/icon-layers@2x.js +3 -0
  133. package/dist/ui/icons/images/icon-linked@2x.js +3 -0
  134. package/dist/ui/icons/images/icon-locked@2x.js +3 -0
  135. package/dist/ui/icons/images/icon-moon@2x.js +3 -0
  136. package/dist/ui/icons/images/icon-pan@2x.js +3 -0
  137. package/dist/ui/icons/images/icon-polygon-mask@2x.js +3 -0
  138. package/dist/ui/icons/images/icon-pressure@2x.js +3 -0
  139. package/dist/ui/icons/images/icon-randomise@2x.js +3 -0
  140. package/dist/ui/icons/images/icon-rectangle-mask@2x.js +3 -0
  141. package/dist/ui/icons/images/icon-redo@2x.js +3 -0
  142. package/dist/ui/icons/images/icon-rotate-90-left@2x.js +3 -0
  143. package/dist/ui/icons/images/icon-rotate-90-right@2x.js +3 -0
  144. package/dist/ui/icons/images/icon-rotate@2x.js +3 -0
  145. package/dist/ui/icons/images/icon-smoothing-bezier-cubic@2x.js +3 -0
  146. package/dist/ui/icons/images/icon-smoothing-bezier-quad@2x.js +3 -0
  147. package/dist/ui/icons/images/icon-smoothing-linear@2x.js +3 -0
  148. package/dist/ui/icons/images/icon-star@2x.js +3 -0
  149. package/dist/ui/icons/images/icon-sun@2x.js +3 -0
  150. package/dist/ui/icons/images/icon-tilt@2x.js +3 -0
  151. package/dist/ui/icons/images/icon-undo@2x.js +3 -0
  152. package/dist/ui/icons/images/icon-unlinked@2x.js +3 -0
  153. package/dist/ui/icons/images/icon-unlocked@2x.js +3 -0
  154. package/dist/ui/icons/images/icon-vertical-size@2x.js +3 -0
  155. package/dist/ui/icons/images/icon-visible@2x.js +3 -0
  156. package/dist/ui/icons/images/icon-zoom-in@2x.js +3 -0
  157. package/dist/ui/icons/images/icon-zoom-out@2x.js +3 -0
  158. package/dist/ui/icons/images/icon-zoom@2x.js +3 -0
  159. package/dist/ui/icons/index.d.ts +1 -1
  160. package/dist/ui/icons/index.js +84 -72
  161. package/dist/ui/index.d.ts +13 -2
  162. package/dist/ui/index.js +16 -4
  163. package/dist/ui/layout/index.d.ts +9 -1
  164. package/dist/ui/layout/index.js +11 -2
  165. package/dist/ui/tokens/index.d.ts +5 -4
  166. package/dist/ui/tokens/index.js +15 -8
  167. package/locales/en-GB.json +96 -3
  168. package/locales/en-US.json +96 -3
  169. package/locales/fr.json +96 -3
  170. package/locales/ja.json +96 -3
  171. package/locales/ko.json +96 -3
  172. package/locales/zh-Hans.json +96 -3
  173. package/locales/zh-Hant.json +96 -3
  174. package/package.json +11 -4
  175. package/dist/ui/elements/PcBrushMenu/style.js +0 -3
  176. package/dist/ui/elements/PcLayersMenu/style.js +0 -3
  177. package/dist/ui/icons/images/brush.js +0 -3
  178. package/dist/ui/icons/images/circle-mask.js +0 -3
  179. package/dist/ui/icons/images/clipboard.js +0 -3
  180. package/dist/ui/icons/images/cursor.js +0 -3
  181. package/dist/ui/icons/images/eraser.js +0 -3
  182. package/dist/ui/icons/images/eyedropper.js +0 -3
  183. package/dist/ui/icons/images/flip-horizontal.js +0 -3
  184. package/dist/ui/icons/images/flip-vertical.js +0 -3
  185. package/dist/ui/icons/images/focus-pen.js +0 -3
  186. package/dist/ui/icons/images/freehand-mask.js +0 -3
  187. package/dist/ui/icons/images/hidden.js +0 -3
  188. package/dist/ui/icons/images/layers.js +0 -3
  189. package/dist/ui/icons/images/linked.js +0 -3
  190. package/dist/ui/icons/images/locked.js +0 -3
  191. package/dist/ui/icons/images/moon.js +0 -3
  192. package/dist/ui/icons/images/pan.js +0 -3
  193. package/dist/ui/icons/images/polygon-mask.js +0 -3
  194. package/dist/ui/icons/images/pressure.js +0 -3
  195. package/dist/ui/icons/images/randomise.js +0 -3
  196. package/dist/ui/icons/images/rectangle-mask.js +0 -3
  197. package/dist/ui/icons/images/redo.js +0 -3
  198. package/dist/ui/icons/images/rotate-90-left.js +0 -3
  199. package/dist/ui/icons/images/rotate-90-right.js +0 -3
  200. package/dist/ui/icons/images/smoothing-bezier-cubic.js +0 -3
  201. package/dist/ui/icons/images/smoothing-bezier-quad.js +0 -3
  202. package/dist/ui/icons/images/smoothing-linear.js +0 -3
  203. package/dist/ui/icons/images/star.js +0 -3
  204. package/dist/ui/icons/images/sun.js +0 -3
  205. package/dist/ui/icons/images/tilt.js +0 -3
  206. package/dist/ui/icons/images/undo.js +0 -3
  207. package/dist/ui/icons/images/unlinked.js +0 -3
  208. package/dist/ui/icons/images/unlocked.js +0 -3
  209. package/dist/ui/icons/images/visible.js +0 -3
  210. package/dist/ui/icons/images/zoom-in.js +0 -3
  211. package/dist/ui/icons/images/zoom-out.js +0 -3
  212. package/dist/ui/icons/images/zoom.js +0 -3
@@ -1,13 +1,14 @@
1
1
  import style_default from "./style.js";
2
2
  import { NO_TEXT_SELECTION_CSS, PILLS_CSS, WINDOWS_CSS } from "../../styles/index.js";
3
- import { PcElement, defineElement } from "../../base/index.js";
3
+ import { defineElement } from "../../base/index.js";
4
+ import { PcPanel } from "../PcPanel/index.js";
4
5
  import ChangeController from "../../ChangeController/index.js";
5
6
  import { maskModeFor } from "../../../lib/selection/geometry/index.js";
6
7
  import { LAYER_ALPHA_MASK_EVENT, LAYER_DRAG_EVENT, LAYER_MENU_OPEN_EVENT } from "../PcLayerItem/index.js";
7
8
  import { html, nothing, unsafeCSS } from "lit";
8
9
  import { styleMap } from "lit/directives/style-map.js";
9
10
  import { repeat } from "lit/directives/repeat.js";
10
- // src/ui/elements/PcLayersMenu/index.ts
11
+ // src/ui/elements/PcPanelLayers/index.ts
11
12
  /**
12
13
  * Whether a point is inside a rectangle (edges included).
13
14
  *
@@ -41,17 +42,17 @@ var dropSlot = (rows, y, ghost) => {
41
42
  * highlighted for touch). Escape cancels the drag. Moves and duplicates are announced to screen readers.
42
43
  *
43
44
  * @example
44
- * html`<pc-layers-menu .app=${app}></pc-layers-menu>`
45
+ * html`<pc-panel-layers .app=${app}></pc-panel-layers>`
45
46
  */
46
- var PcLayersMenu = class extends PcElement {
47
+ var PcPanelLayers = class extends PcPanel {
47
48
  static properties = {
48
- ...PcElement.properties,
49
+ ...PcPanel.properties,
49
50
  app: { attribute: false },
50
51
  drag: { state: true },
51
52
  announcement: { state: true }
52
53
  };
53
54
  static styles = [
54
- PcElement.styles,
55
+ ...PcPanel.styles,
55
56
  unsafeCSS(NO_TEXT_SELECTION_CSS),
56
57
  unsafeCSS(style_default),
57
58
  unsafeCSS(WINDOWS_CSS),
@@ -185,9 +186,15 @@ var PcLayersMenu = class extends PcElement {
185
186
  if (slot === others) entries.push(ghost);
186
187
  return repeat(entries, (entry) => entry.key, (entry) => entry.view);
187
188
  }
188
- render() {
189
+ get defaultIcon() {
190
+ return "layers";
191
+ }
192
+ get defaultLabel() {
193
+ return this.t("paintCanvas.toolbar.layers");
194
+ }
195
+ renderBody() {
189
196
  const layers = this.app?.layers;
190
- if (!layers) return html``;
197
+ if (!layers) return nothing;
191
198
  return html`
192
199
  ${this.renderRows(layers)}
193
200
  <button
@@ -204,5 +211,5 @@ var PcLayersMenu = class extends PcElement {
204
211
  `;
205
212
  }
206
213
  };
207
- defineElement("pc-layers-menu", PcLayersMenu);
208
- export { PcLayersMenu, dropSlot, isInside };
214
+ defineElement("pc-panel-layers", PcPanelLayers);
215
+ export { PcPanelLayers, dropSlot, isInside };
@@ -0,0 +1,3 @@
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";
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 };
@@ -0,0 +1,69 @@
1
+ import { nothing } from "lit";
2
+ import { PcElement } from "../../base";
3
+ import { type KeyCombo } from "../../../lib/utils/shortcuts";
4
+ import type { ShortcutChangeResult } from "../../../lib/classes/ShortcutSettings";
5
+ /** Keyboard shortcut settings `pc-shortcut-settings` can change: PaintCanvas's `ShortcutSettings`, or anything like it */
6
+ export interface ShortcutSettingsLike extends EventTarget {
7
+ combo(id: string): KeyCombo | null;
8
+ set(id: string, combo: KeyCombo | null): ShortcutChangeResult;
9
+ reset(): void;
10
+ }
11
+ /**
12
+ * Every keyboard shortcut action and its key combination, in groups. A configurable action's key combination is changed
13
+ * by focusing its field and pressing the keys (the modifiers held show as they are pressed, e.g. `shift alt cmd ctrl
14
+ * g`); Tab and Escape aren't recorded, so they still move focus and close. Its ⨉ button removes it. A key combination
15
+ * another action has moves to this one; one a fixed action uses isn't set. Both are announced, and sent as
16
+ * `pc-toast`. Fixed actions (e.g. undo, and layer opacity on 1–0) are shown but can't be changed. "Reset" goes back to
17
+ * the default key combinations.
18
+ *
19
+ * @example
20
+ * html`<pc-shortcut-settings .settings=${canvas.shortcuts}></pc-shortcut-settings>`
21
+ */
22
+ export declare class PcShortcutSettings extends PcElement {
23
+ static properties: {
24
+ settings: {
25
+ attribute: boolean;
26
+ };
27
+ recording: {
28
+ state: boolean;
29
+ };
30
+ status: {
31
+ state: boolean;
32
+ };
33
+ translations: {
34
+ attribute: boolean;
35
+ };
36
+ icons: {
37
+ attribute: boolean;
38
+ };
39
+ theme: {
40
+ type: StringConstructor;
41
+ reflect: boolean;
42
+ };
43
+ };
44
+ static styles: import("lit").CSSResultGroup[];
45
+ /** The key combinations shown and changed */
46
+ settings?: ShortcutSettingsLike;
47
+ /** The action being recorded and the modifiers held so far */
48
+ recording?: {
49
+ id: string;
50
+ text: string;
51
+ };
52
+ /** The last change's message, for screen readers */
53
+ status: string;
54
+ private changes;
55
+ constructor();
56
+ /** A shortcut action's name in the chosen language */
57
+ private name;
58
+ /** Record the keys pressed in an action's field */
59
+ private onRecordKeyDown;
60
+ /** Show the key combination again once modifiers are let go without pressing a key */
61
+ private onRecordKeyUp;
62
+ /** Set or remove an action's key combination, saying what happened */
63
+ private change;
64
+ /** A configurable action's field and remove button */
65
+ private renderField;
66
+ /** A fixed action's key combinations */
67
+ private renderFixed;
68
+ render(): import("lit").TemplateResult<1> | typeof nothing;
69
+ }
@@ -0,0 +1,273 @@
1
+ import { PcElement, defineElement, emit } from "../../base/index.js";
2
+ import ChangeController from "../../ChangeController/index.js";
3
+ import { SHORTCUT_DEFINITIONS, comboFromEvent, comboText, heldModifiers } from "../../../lib/utils/shortcuts/index.js";
4
+ import { css, html, nothing } from "lit";
5
+ // src/ui/elements/PcShortcutSettings/index.ts
6
+ /** Keys used to move through and leave the settings, which are never recorded */
7
+ var PASSED_KEYS = /* @__PURE__ */ new Set(["Tab", "Escape"]);
8
+ /**
9
+ * Translation key and values of a shortcut action's name.
10
+ *
11
+ * @example
12
+ * actionName({ id: "layer-opacity-30", ... }); // ["paintCanvas.shortcuts.layerOpacity", { percent: 30 }]
13
+ */
14
+ var actionName = ({ id, action }) => action.type === "layer-opacity" ? ["paintCanvas.shortcuts.layerOpacity", { percent: Math.round(action.opacity * 100) }] : [`paintCanvas.shortcuts.${id}`];
15
+ /**
16
+ * The shortcut actions in groups, in the order they are listed.
17
+ */
18
+ var groups = () => {
19
+ const grouped = /* @__PURE__ */ new Map();
20
+ for (const definition of SHORTCUT_DEFINITIONS) grouped.set(definition.group, [...grouped.get(definition.group) ?? [], definition]);
21
+ return [...grouped];
22
+ };
23
+ /**
24
+ * Every keyboard shortcut action and its key combination, in groups. A configurable action's key combination is changed
25
+ * by focusing its field and pressing the keys (the modifiers held show as they are pressed, e.g. `shift alt cmd ctrl
26
+ * g`); Tab and Escape aren't recorded, so they still move focus and close. Its ⨉ button removes it. A key combination
27
+ * another action has moves to this one; one a fixed action uses isn't set. Both are announced, and sent as
28
+ * `pc-toast`. Fixed actions (e.g. undo, and layer opacity on 1–0) are shown but can't be changed. "Reset" goes back to
29
+ * the default key combinations.
30
+ *
31
+ * @example
32
+ * html`<pc-shortcut-settings .settings=${canvas.shortcuts}></pc-shortcut-settings>`
33
+ */
34
+ var PcShortcutSettings = class extends PcElement {
35
+ static properties = {
36
+ ...PcElement.properties,
37
+ settings: { attribute: false },
38
+ recording: { state: true },
39
+ status: { state: true }
40
+ };
41
+ static styles = [PcElement.styles, css`
42
+ :host {
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: var(--pc-space-3);
46
+ color: var(--pc-text);
47
+ font-family: var(--pc-font);
48
+ font-size: var(--pc-font-size);
49
+ }
50
+ section {
51
+ display: grid;
52
+ grid-template-columns: 1fr auto;
53
+ gap: var(--pc-space-1) var(--pc-space-2);
54
+ align-items: center;
55
+ }
56
+ h3 {
57
+ grid-column: 1 / -1;
58
+ margin: 0;
59
+ padding-bottom: var(--pc-space-1);
60
+ border-bottom: 1px solid var(--pc-border);
61
+ font-size: inherit;
62
+ }
63
+ .keys {
64
+ display: flex;
65
+ gap: var(--pc-space-1);
66
+ align-items: center;
67
+ justify-content: flex-end;
68
+ }
69
+ .combo {
70
+ box-sizing: border-box;
71
+ width: 11em;
72
+ min-height: var(--pc-control-size);
73
+ padding: var(--pc-space-1) var(--pc-space-2);
74
+ border: 1px solid var(--pc-border);
75
+ border-radius: var(--pc-radius-small);
76
+ background: var(--pc-surface);
77
+ color: var(--pc-text);
78
+ font: inherit;
79
+ font-variant-numeric: tabular-nums;
80
+ }
81
+ input.combo {
82
+ cursor: pointer;
83
+ }
84
+ input.combo:focus {
85
+ border-color: var(--pc-accent);
86
+ }
87
+ input.combo:focus-visible,
88
+ button:focus-visible {
89
+ outline: var(--pc-focus-width) solid var(--pc-focus);
90
+ outline-offset: 1px;
91
+ }
92
+ /* Fixed key combinations: shown like the fields, but muted */
93
+ span.combo {
94
+ display: inline-flex;
95
+ align-items: center;
96
+ margin-right: calc(var(--pc-control-size) + var(--pc-space-1));
97
+ background: var(--pc-bar);
98
+ color: var(--pc-text-muted);
99
+ }
100
+ button.remove {
101
+ width: var(--pc-control-size);
102
+ height: var(--pc-control-size);
103
+ padding: 0;
104
+ border: 1px solid var(--pc-border);
105
+ border-radius: var(--pc-radius-small);
106
+ background: var(--pc-surface);
107
+ color: var(--pc-text);
108
+ font: inherit;
109
+ cursor: pointer;
110
+ }
111
+ button.remove:hover:not(:disabled) {
112
+ background: var(--pc-surface-hover);
113
+ }
114
+ button.remove:disabled {
115
+ opacity: 0.4;
116
+ cursor: default;
117
+ }
118
+ .status:empty {
119
+ display: none;
120
+ }
121
+ /* Stays in view at the bottom of a scrolling window, where toasts may be hidden behind it */
122
+ .status {
123
+ position: sticky;
124
+ bottom: 0;
125
+ margin: 0;
126
+ padding: var(--pc-space-2);
127
+ border: 1px solid var(--pc-border);
128
+ border-radius: var(--pc-radius-small);
129
+ background: var(--pc-surface-raised);
130
+ box-shadow: var(--pc-shadow);
131
+ }
132
+ button.reset {
133
+ align-self: flex-start;
134
+ min-height: var(--pc-control-size);
135
+ padding: var(--pc-space-1) var(--pc-space-3);
136
+ border: 0;
137
+ border-radius: var(--pc-radius-pill);
138
+ background: var(--pc-accent);
139
+ color: var(--pc-accent-text);
140
+ font: inherit;
141
+ font-weight: bold;
142
+ cursor: pointer;
143
+ }
144
+ `];
145
+ changes = new ChangeController(this, () => [this.settings]);
146
+ constructor() {
147
+ super();
148
+ this.status = "";
149
+ }
150
+ /** A shortcut action's name in the chosen language */
151
+ name(definition) {
152
+ const [key, values] = actionName(definition);
153
+ return this.t(key, values);
154
+ }
155
+ /** Record the keys pressed in an action's field */
156
+ onRecordKeyDown(event, id) {
157
+ if (PASSED_KEYS.has(event.key)) return;
158
+ event.preventDefault();
159
+ event.stopPropagation();
160
+ const combo = comboFromEvent(event);
161
+ if (!combo) {
162
+ this.recording = {
163
+ id,
164
+ text: comboText(heldModifiers(event))
165
+ };
166
+ return;
167
+ }
168
+ this.recording = void 0;
169
+ this.change(id, combo);
170
+ }
171
+ /** Show the key combination again once modifiers are let go without pressing a key */
172
+ onRecordKeyUp(event) {
173
+ if (!this.recording) return;
174
+ const text = comboText(heldModifiers(event));
175
+ this.recording = text ? {
176
+ ...this.recording,
177
+ text
178
+ } : void 0;
179
+ }
180
+ /** Set or remove an action's key combination, saying what happened */
181
+ change(id, combo) {
182
+ const result = this.settings.set(id, combo);
183
+ const definition = (id) => SHORTCUT_DEFINITIONS.find((each) => each.id === id);
184
+ const keys = combo ? comboText(combo) : "";
185
+ const message = result.fixed ? this.t("paintCanvas.shortcuts.fixedUsed", {
186
+ keys,
187
+ action: this.name(definition(result.fixed))
188
+ }) : result.movedFrom ? this.t("paintCanvas.shortcuts.moved", {
189
+ keys,
190
+ from: this.name(definition(result.movedFrom)),
191
+ to: this.name(definition(id))
192
+ }) : "";
193
+ this.status = message;
194
+ if (message) emit(this, "pc-toast", {
195
+ message,
196
+ kind: "info"
197
+ });
198
+ }
199
+ /** A configurable action's field and remove button */
200
+ renderField(settings, definition) {
201
+ const { id } = definition;
202
+ const combo = settings.combo(id);
203
+ const name = this.name(definition);
204
+ const recording = this.recording?.id === id ? this.recording : void 0;
205
+ return html`<span class="keys">
206
+ <input
207
+ class="combo"
208
+ id="combo-${id}"
209
+ data-id=${id}
210
+ readonly
211
+ autocomplete="off"
212
+ aria-description=${this.t("paintCanvas.shortcuts.recordHint")}
213
+ placeholder=${this.t("paintCanvas.shortcuts.none")}
214
+ .value=${recording ? recording.text : combo ? comboText(combo) : ""}
215
+ @keydown=${(e) => this.onRecordKeyDown(e, id)}
216
+ @keyup=${this.onRecordKeyUp}
217
+ @blur=${() => this.recording = void 0}
218
+ />
219
+ <button
220
+ type="button"
221
+ class="remove"
222
+ data-id=${id}
223
+ title=${this.t("paintCanvas.shortcuts.remove", { action: name })}
224
+ aria-label=${this.t("paintCanvas.shortcuts.remove", { action: name })}
225
+ ?disabled=${!combo}
226
+ @click=${() => this.change(id, null)}
227
+ >
228
+ ⨉
229
+ </button>
230
+ </span>`;
231
+ }
232
+ /** A fixed action's key combinations */
233
+ renderFixed(definition) {
234
+ return html`<span class="keys">
235
+ <span
236
+ class="combo"
237
+ id="combo-${definition.id}"
238
+ data-id=${definition.id}
239
+ role="note"
240
+ aria-labelledby="name-${definition.id}"
241
+ aria-description=${this.t("paintCanvas.shortcuts.fixed")}
242
+ >${definition.combos.map((combo) => comboText(combo)).join(", ")}</span
243
+ >
244
+ </span>`;
245
+ }
246
+ render() {
247
+ const settings = this.settings;
248
+ if (!settings) return nothing;
249
+ return html`${groups().map(([group, definitions]) => html`<section aria-labelledby="group-${group}">
250
+ <h3 id="group-${group}">
251
+ ${this.t(`paintCanvas.shortcuts.group.${group}`)}
252
+ </h3>
253
+ ${definitions.map((definition) => definition.configurable ? html`<label for="combo-${definition.id}"
254
+ >${this.name(definition)}</label
255
+ >${this.renderField(settings, definition)}` : html`<span id="name-${definition.id}"
256
+ >${this.name(definition)}</span
257
+ >${this.renderFixed(definition)}`)}
258
+ </section>`)}
259
+ <p class="status" role="status">${this.status}</p>
260
+ <button
261
+ type="button"
262
+ class="reset"
263
+ @click=${() => {
264
+ settings.reset();
265
+ this.status = this.t("paintCanvas.shortcuts.wasReset");
266
+ }}
267
+ >
268
+ ${this.t("paintCanvas.shortcuts.reset")}
269
+ </button>`;
270
+ }
271
+ };
272
+ defineElement("pc-shortcut-settings", PcShortcutSettings);
273
+ export { PcShortcutSettings };
@@ -1,25 +1,19 @@
1
1
  import { PcElement } from "../../base";
2
2
  import type { IconName } from "../../icons";
3
+ import { THEMES, isTheme, type Theme, type UiSettingsLike } from "../../../lib/classes/UiSettings";
3
4
  import "../PcButtonGroup";
4
- /** The themes to pick from: light, dark, or following the device (auto) */
5
- export declare const THEMES: readonly ["light", "dark", "auto"];
6
- export type Theme = (typeof THEMES)[number];
5
+ export { THEMES, isTheme, type Theme };
7
6
  /** Each theme's icon */
8
7
  export declare const THEME_ICONS: Readonly<Record<Theme, IconName>>;
9
8
  /**
10
- * Whether a value is one of the themes.
11
- *
12
- * @example
13
- * isTheme("dark"); // true
14
- * isTheme(undefined); // false
15
- */
16
- export declare const isTheme: (value: unknown) => value is Theme;
17
- /**
18
- * Picks the light, dark or automatic theme (which follows the device), as a labelled group of three radios joined into
9
+ * Picks the light, dark or system theme (which follows the device's), as a labelled group of three radios joined into
19
10
  * one pill (a joined `<pc-button-group>`), each with its icon (sun, moon and star) and name; the picked one is purple.
20
- * Shows the theme of its `target` (the page's root element by default; light unless it has another), and sets the
11
+ * Shows the theme of its `target` (the page's root element by default; system unless it has another), and sets the
21
12
  * target's `data-pc-theme` when the user picks one, firing `change` (detail: the theme).
22
13
  *
14
+ * With `settings` (e.g. the canvas's `ui`), it shows and keeps the theme there instead, and sets the target's theme
15
+ * whenever theirs changes (e.g. when restored), so it's kept with the other app settings.
16
+ *
23
17
  * @example
24
18
  * html`<pc-theme-picker @change=${(e) => save(e.detail)}></pc-theme-picker>`
25
19
  */
@@ -29,6 +23,9 @@ export declare class PcThemePicker extends PcElement {
29
23
  type: StringConstructor;
30
24
  reflect: boolean;
31
25
  };
26
+ settings: {
27
+ attribute: boolean;
28
+ };
32
29
  translations: {
33
30
  attribute: boolean;
34
31
  };
@@ -41,15 +38,20 @@ export declare class PcThemePicker extends PcElement {
41
38
  theme: Theme;
42
39
  /** The element whose `data-pc-theme` is shown and set (the page's root element by default) */
43
40
  target?: HTMLElement;
41
+ /** Where the theme is kept, if anywhere (e.g. the canvas's `ui`) */
42
+ settings?: UiSettingsLike;
43
+ private changes;
44
44
  constructor();
45
45
  connectedCallback(): void;
46
+ /** Show the kept theme, and set it on the target */
47
+ protected willUpdate(): void;
46
48
  /** The element whose theme is shown and set */
47
49
  private get themeTarget();
48
50
  /**
49
- * Pick a theme: show it, set it on the target and tell listeners.
51
+ * Pick a theme: show it, keep it in the settings (if any), set it on the target and tell listeners.
50
52
  *
51
53
  * @example
52
- * picker.pick("auto");
54
+ * picker.pick("system");
53
55
  */
54
56
  pick(theme: Theme): void;
55
57
  render(): import("lit").TemplateResult<1>;