@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
@@ -0,0 +1,125 @@
1
+ import { PcElement, defineElement, emit } from "../../base/index.js";
2
+ import ChangeController from "../../ChangeController/index.js";
3
+ import { css, html } from "lit";
4
+ import { live } from "lit/directives/live.js";
5
+ // src/ui/elements/PcColourPair/index.ts
6
+ /** Event fired when the foreground or background colour is pressed (detail: `{ slot }`) */
7
+ var COLOUR_CLICK_EVENT = "pc-colour-click";
8
+ /** The two colours, foreground first */
9
+ var SLOTS = ["foreground", "background"];
10
+ /**
11
+ * The foreground and background colours as two overlapping circles in a square (the foreground at the top left), the
12
+ * one selected for configuration outlined and on top. Pressing either fires `pc-colour-click` with its `slot`; the
13
+ * implementer decides what it does (e.g. select it and open a colour palette). `openPicker()` opens the browser's colour
14
+ * picker for the selected colour, which sets it. Sized by `--pc-colour-pair-size` (50px by default).
15
+ *
16
+ * @example
17
+ * html`<pc-colour-pair .app=${app} @pc-colour-click=${(e) => app.selectColour(e.detail.slot)}></pc-colour-pair>`
18
+ */
19
+ var PcColourPair = class extends PcElement {
20
+ static properties = {
21
+ ...PcElement.properties,
22
+ app: { attribute: false }
23
+ };
24
+ static styles = [PcElement.styles, css`
25
+ :host {
26
+ --size: var(--pc-colour-pair-size, 50px);
27
+ position: relative;
28
+ display: block;
29
+ flex: none;
30
+ width: var(--size);
31
+ height: var(--size);
32
+ background: var(--pc-text-muted);
33
+ }
34
+ button {
35
+ position: absolute;
36
+ width: 56%;
37
+ height: 56%;
38
+ padding: 0;
39
+ border: 1px solid var(--pc-text);
40
+ border-radius: 50%;
41
+ background: var(--colour);
42
+ cursor: pointer;
43
+ }
44
+ .foreground {
45
+ top: 10%;
46
+ left: 10%;
47
+ z-index: 1;
48
+ }
49
+ .background {
50
+ right: 10%;
51
+ bottom: 10%;
52
+ }
53
+ /* The selected colour: on top, with a white ring outlined in black */
54
+ button[aria-pressed="true"] {
55
+ z-index: 2;
56
+ box-shadow:
57
+ 0 0 0 2px #ffffff,
58
+ 0 0 0 3px #000000;
59
+ }
60
+ button:focus-visible {
61
+ outline: var(--pc-focus-width) solid var(--pc-focus);
62
+ outline-offset: 4px;
63
+ }
64
+ /* Kept in place (invisible) so the browser's colour picker opens beside the colours */
65
+ input {
66
+ position: absolute;
67
+ bottom: 0;
68
+ left: 0;
69
+ width: 1px;
70
+ height: 1px;
71
+ padding: 0;
72
+ border: 0;
73
+ opacity: 0;
74
+ pointer-events: none;
75
+ }
76
+ `];
77
+ changes = new ChangeController(this, () => [this.app]);
78
+ /**
79
+ * Open the browser's colour picker for the selected colour (call it while handling a press, as browsers only open
80
+ * pickers for one).
81
+ */
82
+ openPicker() {
83
+ const input = this.renderRoot.querySelector("input");
84
+ if (!input) return;
85
+ try {
86
+ input.showPicker();
87
+ } catch {
88
+ input.click();
89
+ }
90
+ }
91
+ /** Tell listeners a colour was pressed */
92
+ onClick(slot) {
93
+ emit(this, COLOUR_CLICK_EVENT, { slot });
94
+ }
95
+ /** Set the selected colour from the browser's colour picker */
96
+ onPick(event) {
97
+ const app = this.app;
98
+ app?.setColour(event.target.value, app.selectedColour);
99
+ }
100
+ render() {
101
+ const app = this.app;
102
+ if (!app) return html``;
103
+ return html`${SLOTS.map((slot) => {
104
+ const colour = app.colours[slot];
105
+ return html`<button
106
+ type="button"
107
+ part=${slot}
108
+ class=${slot}
109
+ style="--colour: ${colour}"
110
+ title=${this.t(`paintCanvas.colours.${slot}`, { colour })}
111
+ aria-label=${this.t(`paintCanvas.colours.${slot}`, { colour })}
112
+ aria-pressed=${app.selectedColour === slot}
113
+ @click=${() => this.onClick(slot)}
114
+ ></button>`;
115
+ })}<input
116
+ type="color"
117
+ tabindex="-1"
118
+ aria-hidden="true"
119
+ .value=${live(app.colours[app.selectedColour])}
120
+ @input=${this.onPick}
121
+ />`;
122
+ }
123
+ };
124
+ defineElement("pc-colour-pair", PcColourPair);
125
+ export { COLOUR_CLICK_EVENT, PcColourPair };
@@ -1,23 +1,43 @@
1
1
  import { type PropertyValues } from "lit";
2
2
  import { PcElement } from "../../base";
3
- /** An item in a context menu */
4
- export interface MenuItem {
5
- /** What `pc-select` gives when it's chosen */
6
- value: string;
7
- label: string;
8
- disabled?: boolean;
9
- /** Shown in the cancelling colour, e.g. for deleting */
10
- danger?: boolean;
11
- }
3
+ import { PcMenu, type MenuEntry } from "../PcMenu";
4
+ import "../PcMenu";
5
+ /** Space kept between a context menu's menu and the window's edges */
6
+ export declare const MENU_MARGIN = 8;
7
+ /** Event fired when a context menu opens or closes (detail: `{ open }`) */
8
+ export declare const CONTEXT_MENU_TOGGLE_EVENT = "pc-toggle";
9
+ /** Where a context menu's menu opens: under its button, or beside it */
10
+ export type MenuPlacement = "below" | "beside";
11
+ /**
12
+ * Where a menu's top goes: level with its anchor, moved up as far as needed to stay in the window (but never above its
13
+ * top margin).
14
+ *
15
+ * @example
16
+ * menuTop(700, 300, 800); // 492 (moved up to fit)
17
+ * menuTop(100, 300, 800); // 100
18
+ */
19
+ export declare const menuTop: (anchorTop: number, menuHeight: number, viewportHeight: number, margin?: number) => number;
20
+ /**
21
+ * Where a menu's left edge goes: at `left`, moved left as far as needed to stay in the window (but never past its left
22
+ * margin).
23
+ *
24
+ * @example
25
+ * menuLeft(900, 200, 1000); // 792 (moved left to fit)
26
+ */
27
+ export declare const menuLeft: (left: number, menuWidth: number, viewportWidth: number, margin?: number) => number;
12
28
  /**
13
- * A menu of actions, e.g. for a layer. While `open`, it shows its items at `x`, `y` (in the page's viewport) and takes
14
- * keyboard focus: arrow keys, Home and End move between items, Enter or Space chooses one, and Escape or Tab closes it.
15
- * Fires `pc-select` with the chosen item's value, and `pc-close` when it should close (after choosing, Escape, Tab, or a
16
- * press outside it). The implementer sets `open` to false in response.
29
+ * A button that opens a menu (`pc-menu`): its `icon` slot is what the button shows (⋮ by default), and its `menu` slot
30
+ * takes the `pc-menu` to open. Without one, it opens its own menu of `items`. Pressing the button (or right-clicking
31
+ * it) opens the menu under it (or beside it, with `placement="beside"`), kept within the window; choosing an item,
32
+ * pressing outside, Escape, Tab, scrolling or pressing the button again closes it, and focus returns to the button.
33
+ *
34
+ * Fires `pc-toggle` (detail: `{ open }`) when it opens or closes. The menu's `pc-select` (detail: `{ value }`) reaches
35
+ * the context menu's listeners.
17
36
  *
18
37
  * @example
19
- * html`<pc-context-menu open .x=${10} .y=${20} label="Layer actions" .items=${items}
20
- * @pc-select=${(e) => act(e.detail.value)} @pc-close=${close}></pc-context-menu>`
38
+ * html`<pc-context-menu label="Layer actions" .items=${items} @pc-select=${(e) => act(e.detail.value)}>
39
+ * <pc-icon slot="icon" name="menu"></pc-icon>
40
+ * </pc-context-menu>`
21
41
  */
22
42
  export declare class PcContextMenu extends PcElement {
23
43
  static properties: {
@@ -31,11 +51,16 @@ export declare class PcContextMenu extends PcElement {
31
51
  label: {
32
52
  type: StringConstructor;
33
53
  };
34
- x: {
35
- type: NumberConstructor;
54
+ menuLabel: {
55
+ type: StringConstructor;
56
+ attribute: string;
57
+ };
58
+ placement: {
59
+ type: StringConstructor;
36
60
  };
37
- y: {
38
- type: NumberConstructor;
61
+ disabled: {
62
+ type: BooleanConstructor;
63
+ reflect: boolean;
39
64
  };
40
65
  translations: {
41
66
  attribute: boolean;
@@ -49,24 +74,36 @@ export declare class PcContextMenu extends PcElement {
49
74
  };
50
75
  };
51
76
  static styles: import("lit").CSSResultGroup[];
77
+ /** Whether the menu is open */
52
78
  open: boolean;
53
- items: readonly MenuItem[];
54
- /** Names the menu for screen readers */
79
+ /** The items of its own menu, when no `pc-menu` is slotted */
80
+ items: readonly MenuEntry[];
81
+ /** Names the button for screen readers */
55
82
  label: string;
56
- x: number;
57
- y: number;
58
- private readonly onPointerDown;
83
+ /** Names the menu for screen readers (the button's `label` by default) */
84
+ menuLabel: string;
85
+ /** Where the menu opens: under the button (default) or beside it */
86
+ placement: MenuPlacement;
87
+ disabled: boolean;
88
+ private readonly onScroll;
59
89
  constructor();
60
- connectedCallback(): void;
61
90
  disconnectedCallback(): void;
62
- updated(changed: PropertyValues<this>): void;
63
- /** The enabled items' buttons */
64
- private buttons;
65
- /** Tell listeners the menu should close */
66
- close(): void;
67
- /** Choose an item, then close */
68
- private choose;
69
- /** Move focus between items, or close */
70
- private onKeyDown;
91
+ /** The menu it opens: the slotted `pc-menu`, or its own */
92
+ get menu(): PcMenu | undefined;
93
+ /** The button that opens the menu */
94
+ private get button();
95
+ /**
96
+ * Open or close the menu, telling listeners.
97
+ */
98
+ setOpen(open: boolean): void;
99
+ /** Open the menu on a right-click (instead of the browser's), or close it */
100
+ private onContextMenu;
101
+ /** Close the menu when it asks to, and return focus to the button */
102
+ private onClose;
103
+ /** Set up a newly slotted menu like the current one (open or closed) */
104
+ private onMenuSlotChange;
105
+ protected updated(changed: PropertyValues<this>): void;
106
+ /** Put the open menu under or beside the button, within the window */
107
+ private place;
71
108
  render(): import("lit").TemplateResult<1>;
72
109
  }
@@ -1,15 +1,41 @@
1
1
  import { PcElement, defineElement, emit } from "../../base/index.js";
2
+ import { PcMenu } from "../PcMenu/index.js";
2
3
  import { css, html, nothing } from "lit";
3
4
  // src/ui/elements/PcContextMenu/index.ts
5
+ /** Space kept between a context menu's menu and the window's edges */
6
+ var MENU_MARGIN = 8;
7
+ /** Event fired when a context menu opens or closes (detail: `{ open }`) */
8
+ var CONTEXT_MENU_TOGGLE_EVENT = "pc-toggle";
4
9
  /**
5
- * A menu of actions, e.g. for a layer. While `open`, it shows its items at `x`, `y` (in the page's viewport) and takes
6
- * keyboard focus: arrow keys, Home and End move between items, Enter or Space chooses one, and Escape or Tab closes it.
7
- * Fires `pc-select` with the chosen item's value, and `pc-close` when it should close (after choosing, Escape, Tab, or a
8
- * press outside it). The implementer sets `open` to false in response.
10
+ * Where a menu's top goes: level with its anchor, moved up as far as needed to stay in the window (but never above its
11
+ * top margin).
9
12
  *
10
13
  * @example
11
- * html`<pc-context-menu open .x=${10} .y=${20} label="Layer actions" .items=${items}
12
- * @pc-select=${(e) => act(e.detail.value)} @pc-close=${close}></pc-context-menu>`
14
+ * menuTop(700, 300, 800); // 492 (moved up to fit)
15
+ * menuTop(100, 300, 800); // 100
16
+ */
17
+ var menuTop = (anchorTop, menuHeight, viewportHeight, margin = 8) => Math.max(margin, Math.min(anchorTop, viewportHeight - margin - menuHeight));
18
+ /**
19
+ * Where a menu's left edge goes: at `left`, moved left as far as needed to stay in the window (but never past its left
20
+ * margin).
21
+ *
22
+ * @example
23
+ * menuLeft(900, 200, 1000); // 792 (moved left to fit)
24
+ */
25
+ var menuLeft = (left, menuWidth, viewportWidth, margin = 8) => Math.max(margin, Math.min(left, viewportWidth - margin - menuWidth));
26
+ /**
27
+ * A button that opens a menu (`pc-menu`): its `icon` slot is what the button shows (⋮ by default), and its `menu` slot
28
+ * takes the `pc-menu` to open. Without one, it opens its own menu of `items`. Pressing the button (or right-clicking
29
+ * it) opens the menu under it (or beside it, with `placement="beside"`), kept within the window; choosing an item,
30
+ * pressing outside, Escape, Tab, scrolling or pressing the button again closes it, and focus returns to the button.
31
+ *
32
+ * Fires `pc-toggle` (detail: `{ open }`) when it opens or closes. The menu's `pc-select` (detail: `{ value }`) reaches
33
+ * the context menu's listeners.
34
+ *
35
+ * @example
36
+ * html`<pc-context-menu label="Layer actions" .items=${items} @pc-select=${(e) => act(e.detail.value)}>
37
+ * <pc-icon slot="icon" name="menu"></pc-icon>
38
+ * </pc-context-menu>`
13
39
  */
14
40
  var PcContextMenu = class extends PcElement {
15
41
  static properties = {
@@ -20,129 +46,134 @@ var PcContextMenu = class extends PcElement {
20
46
  },
21
47
  items: { attribute: false },
22
48
  label: { type: String },
23
- x: { type: Number },
24
- y: { type: Number }
49
+ menuLabel: {
50
+ type: String,
51
+ attribute: "menu-label"
52
+ },
53
+ placement: { type: String },
54
+ disabled: {
55
+ type: Boolean,
56
+ reflect: true
57
+ }
25
58
  };
26
59
  static styles = [PcElement.styles, css`
27
60
  :host {
28
- position: fixed;
29
- z-index: 10;
30
- display: none;
31
- }
32
- :host([open]) {
33
- display: block;
34
- }
35
- [role="menu"] {
36
- display: flex;
37
- min-width: 160px;
38
- flex-direction: column;
39
- padding: var(--pc-space-1) 0;
40
- border-radius: var(--pc-radius);
41
- background: var(--pc-surface-raised);
42
- box-shadow: var(--pc-shadow);
61
+ display: inline-flex;
43
62
  }
44
63
  button {
45
- padding: var(--pc-space-2) var(--pc-space-3);
46
- border: 0;
47
- background: transparent;
64
+ display: inline-flex;
65
+ flex: 1;
66
+ align-items: center;
67
+ justify-content: center;
68
+ min-width: var(--pc-control-size);
69
+ min-height: var(--pc-control-size);
70
+ padding: 0 var(--pc-space-1);
71
+ border: 1px solid var(--pc-border);
72
+ border-radius: var(--pc-radius-small);
73
+ background: var(--pc-surface);
48
74
  color: var(--pc-text);
49
- font-family: var(--pc-font);
50
- font-size: var(--pc-font-size);
51
- text-align: start;
75
+ font: inherit;
52
76
  cursor: pointer;
53
77
  }
54
78
  button:hover:not(:disabled),
55
- button:focus-visible {
79
+ :host([open]) button {
56
80
  background: var(--pc-surface-hover);
57
- outline: none;
58
81
  }
59
82
  button:focus-visible {
60
- box-shadow: inset 0 0 0 var(--pc-focus-width) var(--pc-focus);
61
- }
62
- button.danger {
63
- color: var(--pc-dismiss-text);
83
+ outline: var(--pc-focus-width) solid var(--pc-focus);
64
84
  }
65
85
  button:disabled {
66
- color: var(--pc-text-muted);
86
+ opacity: 0.4;
67
87
  cursor: default;
68
88
  }
69
89
  `];
70
- onPointerDown = (event) => {
71
- if (this.open && !event.composedPath().includes(this)) this.close();
72
- };
90
+ onScroll = () => this.setOpen(false);
73
91
  constructor() {
74
92
  super();
75
93
  this.open = false;
76
94
  this.items = [];
77
95
  this.label = "";
78
- this.x = 0;
79
- this.y = 0;
80
- }
81
- connectedCallback() {
82
- super.connectedCallback();
83
- document.addEventListener("pointerdown", this.onPointerDown, true);
96
+ this.menuLabel = "";
97
+ this.placement = "below";
98
+ this.disabled = false;
84
99
  }
85
100
  disconnectedCallback() {
86
101
  super.disconnectedCallback();
87
- document.removeEventListener("pointerdown", this.onPointerDown, true);
102
+ window.removeEventListener("scroll", this.onScroll, true);
88
103
  }
89
- updated(changed) {
90
- this.style.left = `${this.x}px`;
91
- this.style.top = `${this.y}px`;
92
- if (changed.has("open") && this.open) this.buttons()[0]?.focus();
104
+ /** The menu it opens: the slotted `pc-menu`, or its own */
105
+ get menu() {
106
+ return this.renderRoot.querySelector("slot[name=\"menu\"]")?.assignedElements({ flatten: true }).find((element) => element instanceof PcMenu) ?? void 0;
93
107
  }
94
- /** The enabled items' buttons */
95
- buttons() {
96
- return [...this.renderRoot.querySelectorAll("button")].filter((button) => !button.disabled);
108
+ /** The button that opens the menu */
109
+ get button() {
110
+ return this.renderRoot.querySelector("button");
97
111
  }
98
- /** Tell listeners the menu should close */
99
- close() {
100
- emit(this, "pc-close");
112
+ /**
113
+ * Open or close the menu, telling listeners.
114
+ */
115
+ setOpen(open) {
116
+ if (open === this.open || open && this.disabled) return;
117
+ this.open = open;
118
+ emit(this, CONTEXT_MENU_TOGGLE_EVENT, { open });
101
119
  }
102
- /** Choose an item, then close */
103
- choose(item) {
104
- emit(this, "pc-select", { value: item.value });
105
- this.close();
120
+ /** Open the menu on a right-click (instead of the browser's), or close it */
121
+ onContextMenu(event) {
122
+ event.preventDefault();
123
+ this.setOpen(!this.open);
106
124
  }
107
- /** Move focus between items, or close */
108
- onKeyDown(event) {
109
- const buttons = this.buttons();
110
- const index = buttons.indexOf(this.renderRoot.activeElement);
111
- const last = buttons.length - 1;
112
- const target = {
113
- ArrowDown: index >= last ? 0 : index + 1,
114
- ArrowUp: index <= 0 ? last : index - 1,
115
- Home: 0,
116
- End: last
117
- }[event.key];
118
- if (target !== void 0) {
119
- event.preventDefault();
120
- buttons[target]?.focus();
121
- } else if (event.key === "Escape" || event.key === "Tab") {
122
- event.preventDefault();
123
- this.close();
124
- }
125
+ /** Close the menu when it asks to, and return focus to the button */
126
+ onClose() {
127
+ const focusWasInside = this.menu.matches(":focus-within");
128
+ this.setOpen(false);
129
+ if (focusWasInside) this.button.focus();
130
+ }
131
+ /** Set up a newly slotted menu like the current one (open or closed) */
132
+ onMenuSlotChange() {
133
+ this.requestUpdate("open", !this.open);
134
+ }
135
+ updated(changed) {
136
+ const menu = this.menu;
137
+ if (!menu) return;
138
+ menu.anchor = this;
139
+ menu.label ||= this.menuLabel || this.label;
140
+ if (!changed.has("open")) return;
141
+ window[this.open ? "addEventListener" : "removeEventListener"]("scroll", this.onScroll, true);
142
+ menu.open = this.open;
143
+ if (this.open) this.place(menu);
144
+ }
145
+ /** Put the open menu under or beside the button, within the window */
146
+ async place(menu) {
147
+ const anchor = this.button.getBoundingClientRect();
148
+ const beside = this.placement === "beside";
149
+ menu.x = beside ? anchor.right : anchor.left;
150
+ menu.y = beside ? anchor.top : anchor.bottom;
151
+ await menu.updateComplete;
152
+ menu.x = menuLeft(menu.x, menu.offsetWidth, window.innerWidth);
153
+ menu.y = menuTop(menu.y, menu.offsetHeight, window.innerHeight);
125
154
  }
126
155
  render() {
127
- return html`<div
128
- part="menu"
129
- role="menu"
130
- aria-label=${this.label}
131
- @keydown=${this.onKeyDown}
132
- >
133
- ${this.items.map((item) => html`<button
134
- part="item"
135
- type="button"
136
- role="menuitem"
137
- class=${item.danger ? "danger" : nothing}
138
- ?disabled=${item.disabled}
139
- data-value=${item.value}
140
- @click=${() => this.choose(item)}
141
- >
142
- ${item.label}
143
- </button>`)}
144
- </div>`;
156
+ return html`<button
157
+ type="button"
158
+ part="button"
159
+ aria-haspopup="menu"
160
+ aria-expanded=${String(this.open)}
161
+ aria-label=${this.label || nothing}
162
+ ?disabled=${this.disabled}
163
+ @click=${() => this.setOpen(!this.open)}
164
+ @contextmenu=${this.onContextMenu}
165
+ >
166
+ <slot name="icon">⋮</slot></button
167
+ ><slot
168
+ name="menu"
169
+ @slotchange=${this.onMenuSlotChange}
170
+ @pc-close=${this.onClose}
171
+ >${this.items.length ? html`<pc-menu
172
+ .items=${this.items}
173
+ label=${this.menuLabel || this.label}
174
+ ></pc-menu>` : nothing}</slot
175
+ >`;
145
176
  }
146
177
  };
147
178
  defineElement("pc-context-menu", PcContextMenu);
148
- export { PcContextMenu };
179
+ export { CONTEXT_MENU_TOGGLE_EVENT, MENU_MARGIN, PcContextMenu, menuLeft, menuTop };
@@ -1,6 +1,7 @@
1
1
  import { nothing } from "lit";
2
2
  import { PcElement } from "../../base";
3
3
  import { type InputSettingsData } from "../../../lib/classes/InputSettings";
4
+ import type { UiSettingsLike } from "../../../lib/classes/UiSettings";
4
5
  /** Settings `pc-input-settings` can change: PaintCanvas's `InputSettings`, or anything with the same data and `update` */
5
6
  export interface InputSettingsLike extends EventTarget {
6
7
  readonly data: Readonly<InputSettingsData>;
@@ -12,6 +13,9 @@ export interface InputSettingsLike extends EventTarget {
12
13
  * and the language. Changes apply straight away. Hide focus pen mode or the language with `focusPen` and `languages`
13
14
  * set to false. Fires `pc-language-change` (with the locale) when the user picks a language.
14
15
  *
16
+ * With `ui` (e.g. the canvas's `ui`), the picked language is kept there, and used whenever it changes (e.g. when
17
+ * restored), so it's kept with the other app settings.
18
+ *
15
19
  * @example
16
20
  * html`<pc-input-settings .settings=${canvas.input}></pc-input-settings>`
17
21
  */
@@ -20,6 +24,9 @@ export declare class PcInputSettings extends PcElement {
20
24
  settings: {
21
25
  attribute: boolean;
22
26
  };
27
+ ui: {
28
+ attribute: boolean;
29
+ };
23
30
  focusPen: {
24
31
  type: BooleanConstructor;
25
32
  };
@@ -46,6 +53,8 @@ export declare class PcInputSettings extends PcElement {
46
53
  static styles: import("lit").CSSResultGroup[];
47
54
  /** The settings shown and changed */
48
55
  settings?: InputSettingsLike;
56
+ /** Where the language is kept, if anywhere (e.g. the canvas's `ui`) */
57
+ ui?: UiSettingsLike;
49
58
  /** Whether to show focus pen mode (default true) */
50
59
  focusPen: boolean;
51
60
  /** Whether to show the language (default true) */
@@ -55,7 +64,11 @@ export declare class PcInputSettings extends PcElement {
55
64
  /** Name of the zoom modifier key ("Cmd" or "Ctrl", detected by default) */
56
65
  modifier: string;
57
66
  private changes;
67
+ /** The kept language last used, so it's only switched to once */
68
+ private usedLocale?;
58
69
  constructor();
70
+ /** Use the kept language when it changes */
71
+ protected willUpdate(): void;
59
72
  /** A checkbox for an on/off setting */
60
73
  private toggle;
61
74
  /**
@@ -18,6 +18,9 @@ var ACTION_KEYS = {
18
18
  * and the language. Changes apply straight away. Hide focus pen mode or the language with `focusPen` and `languages`
19
19
  * set to false. Fires `pc-language-change` (with the locale) when the user picks a language.
20
20
  *
21
+ * With `ui` (e.g. the canvas's `ui`), the picked language is kept there, and used whenever it changes (e.g. when
22
+ * restored), so it's kept with the other app settings.
23
+ *
21
24
  * @example
22
25
  * html`<pc-input-settings .settings=${canvas.input}></pc-input-settings>`
23
26
  */
@@ -25,6 +28,7 @@ var PcInputSettings = class extends PcElement {
25
28
  static properties = {
26
29
  ...PcElement.properties,
27
30
  settings: { attribute: false },
31
+ ui: { attribute: false },
28
32
  focusPen: { type: Boolean },
29
33
  languages: { type: Boolean },
30
34
  touch: { type: Boolean },
@@ -67,7 +71,9 @@ var PcInputSettings = class extends PcElement {
67
71
  outline-offset: 2px;
68
72
  }
69
73
  `];
70
- changes = new ChangeController(this, () => [this.settings]);
74
+ changes = new ChangeController(this, () => [this.settings, this.ui]);
75
+ /** The kept language last used, so it's only switched to once */
76
+ usedLocale;
71
77
  constructor() {
72
78
  super();
73
79
  this.focusPen = true;
@@ -75,6 +81,13 @@ var PcInputSettings = class extends PcElement {
75
81
  this.touch = isTouchDevice();
76
82
  this.modifier = zoomModifierName();
77
83
  }
84
+ /** Use the kept language when it changes */
85
+ willUpdate() {
86
+ const locale = this.ui?.data.locale;
87
+ if (locale === void 0 || locale === this.usedLocale) return;
88
+ this.usedLocale = locale;
89
+ if (locale !== this.translations.locale) this.translations.setLocale(locale);
90
+ }
78
91
  /** A checkbox for an on/off setting */
79
92
  toggle(data, key, label) {
80
93
  return html`<label class="toggle">
@@ -123,6 +136,8 @@ var PcInputSettings = class extends PcElement {
123
136
  name="language"
124
137
  @change=${(e) => {
125
138
  const locale = e.target.value;
139
+ this.usedLocale = locale;
140
+ this.ui?.update({ locale });
126
141
  translations.setLocale(locale).then(() => emit(this, "pc-language-change", { locale }));
127
142
  }}
128
143
  >