@lvl99dotcom/paintcanvas 0.5.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 (97) hide show
  1. package/dist/lib/brushes/configRanges/index.d.ts +17 -0
  2. package/dist/lib/brushes/configRanges/index.js +31 -1
  3. package/dist/lib/brushes/params/index.d.ts +2 -1
  4. package/dist/lib/brushes/params/index.js +7 -4
  5. package/dist/lib/brushes/types/index.d.ts +8 -0
  6. package/dist/lib/classes/Layer/index.d.ts +10 -0
  7. package/dist/lib/classes/Layer/index.js +15 -1
  8. package/dist/lib/classes/LayerAutosave/index.d.ts +9 -15
  9. package/dist/lib/classes/LayerAutosave/index.js +30 -43
  10. package/dist/lib/classes/LayerStack/index.d.ts +15 -0
  11. package/dist/lib/classes/LayerStack/index.js +26 -2
  12. package/dist/lib/classes/PaintCanvas/index.d.ts +26 -3
  13. package/dist/lib/classes/PaintCanvas/index.js +39 -12
  14. package/dist/lib/classes/PngEncoder/index.d.ts +41 -0
  15. package/dist/lib/classes/PngEncoder/index.js +104 -0
  16. package/dist/lib/classes/ProfileGraphs/index.d.ts +87 -0
  17. package/dist/lib/classes/ProfileGraphs/index.js +148 -0
  18. package/dist/lib/classes/SettingsAutosave/index.d.ts +1 -1
  19. package/dist/lib/classes/SettingsAutosave/index.js +2 -1
  20. package/dist/lib/messages/schema.d.ts +720 -0
  21. package/dist/lib/presets/schema.d.ts +360 -0
  22. package/dist/lib/presets/schema.js +8 -1
  23. package/dist/lib/profiles/index.d.ts +61 -0
  24. package/dist/lib/profiles/index.js +131 -0
  25. package/dist/lib/profiles/schema.d.ts +73 -0
  26. package/dist/lib/profiles/schema.js +53 -0
  27. package/dist/lib/rendering/sample/index.d.ts +11 -0
  28. package/dist/lib/rendering/sample/index.js +36 -5
  29. package/dist/lib/settings/index.d.ts +2 -1
  30. package/dist/lib/settings/index.js +4 -1
  31. package/dist/lib/settings/schema.d.ts +49 -0
  32. package/dist/lib/settings/schema.js +6 -0
  33. package/dist/lib/validation/validators.generated.js +13064 -3696
  34. package/dist/locales/en-GB.js +30 -0
  35. package/dist/locales/en-US.js +30 -0
  36. package/dist/locales/fr.js +30 -0
  37. package/dist/locales/ja.js +30 -0
  38. package/dist/locales/ko.js +30 -0
  39. package/dist/locales/zh-Hans.js +30 -0
  40. package/dist/locales/zh-Hant.js +30 -0
  41. package/dist/package.js +1 -1
  42. package/dist/ui/ask/index.d.ts +48 -0
  43. package/dist/ui/ask/index.js +67 -0
  44. package/dist/ui/base/index.d.ts +8 -1
  45. package/dist/ui/base/index.js +19 -4
  46. package/dist/ui/elements/PaintCanvas/index.js +3 -1
  47. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  48. package/dist/ui/elements/PcAppSettings/index.d.ts +6 -6
  49. package/dist/ui/elements/PcAppSettings/index.js +21 -65
  50. package/dist/ui/elements/PcBrushConfig/index.d.ts +25 -14
  51. package/dist/ui/elements/PcBrushConfig/index.js +216 -146
  52. package/dist/ui/elements/PcBrushConfig/style.js +1 -1
  53. package/dist/ui/elements/PcBrushTest/index.js +7 -2
  54. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  55. package/dist/ui/elements/PcButton/index.d.ts +12 -3
  56. package/dist/ui/elements/PcButton/index.js +18 -10
  57. package/dist/ui/elements/PcCanvas/index.d.ts +6 -1
  58. package/dist/ui/elements/PcCanvas/index.js +10 -3
  59. package/dist/ui/elements/PcCanvas/style.js +1 -1
  60. package/dist/ui/elements/PcClipboardTools/index.d.ts +5 -4
  61. package/dist/ui/elements/PcClipboardTools/index.js +12 -5
  62. package/dist/ui/elements/PcDialog/index.d.ts +46 -10
  63. package/dist/ui/elements/PcDialog/index.js +102 -51
  64. package/dist/ui/elements/PcDialogWindow/index.d.ts +141 -0
  65. package/dist/ui/elements/PcDialogWindow/index.js +385 -0
  66. package/dist/ui/elements/PcPanelBrushes/index.d.ts +3 -2
  67. package/dist/ui/elements/PcPanelBrushes/index.js +7 -5
  68. package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
  69. package/dist/ui/elements/PcPanelColours/index.js +2 -2
  70. package/dist/ui/elements/PcPanelColours/style.js +1 -1
  71. package/dist/ui/elements/PcPanelLayers/index.js +1 -1
  72. package/dist/ui/elements/PcPanelLayers/style.js +1 -1
  73. package/dist/ui/elements/PcProfileEditor/index.d.ts +103 -0
  74. package/dist/ui/elements/PcProfileEditor/index.js +259 -0
  75. package/dist/ui/elements/PcProfileGraph/index.d.ts +84 -0
  76. package/dist/ui/elements/PcProfileGraph/index.js +317 -0
  77. package/dist/ui/elements/PcShortcutSettings/index.js +9 -13
  78. package/dist/ui/elements/PcSliderRange/index.d.ts +147 -0
  79. package/dist/ui/elements/PcSliderRange/index.js +307 -0
  80. package/dist/ui/elements/PcStats/index.d.ts +19 -4
  81. package/dist/ui/elements/PcStats/index.js +29 -13
  82. package/dist/ui/elements/PcToolSettings/index.js +6 -6
  83. package/dist/ui/icons/images/icon-profile-graph@2x.js +3 -0
  84. package/dist/ui/icons/index.d.ts +1 -1
  85. package/dist/ui/icons/index.js +2 -0
  86. package/dist/ui/index.d.ts +6 -1
  87. package/dist/ui/index.js +6 -1
  88. package/dist/ui/styles/index.d.ts +21 -1
  89. package/dist/ui/styles/index.js +73 -14
  90. package/locales/en-GB.json +30 -0
  91. package/locales/en-US.json +30 -0
  92. package/locales/fr.json +30 -0
  93. package/locales/ja.json +30 -0
  94. package/locales/ko.json +30 -0
  95. package/locales/zh-Hans.json +30 -0
  96. package/locales/zh-Hant.json +30 -0
  97. package/package.json +25 -23
@@ -15,6 +15,8 @@ export declare const HANDLE_GRAB_DISTANCE = 8;
15
15
  export declare const ANTS_INTERVAL = 100;
16
16
  /** In focus pen mode, how long after a pen lifts touches are still ignored (milliseconds) */
17
17
  export declare const PEN_LIFT_GRACE_MS = 500;
18
+ /** The most device pixels per CSS pixel the canvases on screen use (see `PcCanvas.pixelRatio`) */
19
+ export declare const MAX_PIXEL_RATIO = 2;
18
20
  /**
19
21
  * Space covered by the toolbar and chat on a screen of the given width.
20
22
  *
@@ -92,7 +94,10 @@ export declare class PcCanvas extends PcElement {
92
94
  insets?: Insets;
93
95
  /** The insets in effect (see `insets`) */
94
96
  private get activeInsets();
95
- /** Device pixels per CSS pixel (replaceable for tests) */
97
+ /**
98
+ * Device pixels per CSS pixel for the canvases on screen, at most `MAX_PIXEL_RATIO`: phones with 3 have 2.25 times
99
+ * the pixels to draw and put on screen for little visible difference (replaceable for tests).
100
+ */
96
101
  pixelRatio: () => number;
97
102
  private changes;
98
103
  /** Whether anything but the stroke in progress changed since the last update (so everything is redrawn) */
@@ -39,6 +39,8 @@ var HANDLE_GRAB_DISTANCE = 8;
39
39
  var ANTS_INTERVAL = 100;
40
40
  /** In focus pen mode, how long after a pen lifts touches are still ignored (milliseconds) */
41
41
  var PEN_LIFT_GRACE_MS = 500;
42
+ /** The most device pixels per CSS pixel the canvases on screen use (see `PcCanvas.pixelRatio`) */
43
+ var MAX_PIXEL_RATIO = 2;
42
44
  /**
43
45
  * Space covered by the toolbar and chat on a screen of the given width.
44
46
  *
@@ -131,8 +133,11 @@ var PcCanvas = class extends PcElement {
131
133
  const app = this.app;
132
134
  return this.insets ?? interfaceInsets(app.viewport.width, app.interfaceVisible);
133
135
  }
134
- /** Device pixels per CSS pixel (replaceable for tests) */
135
- pixelRatio = () => window.devicePixelRatio || 1;
136
+ /**
137
+ * Device pixels per CSS pixel for the canvases on screen, at most `MAX_PIXEL_RATIO`: phones with 3 have 2.25 times
138
+ * the pixels to draw and put on screen for little visible difference (replaceable for tests).
139
+ */
140
+ pixelRatio = () => Math.min(window.devicePixelRatio || 1, 2);
136
141
  changes = new ChangeController(this, () => [
137
142
  this.app,
138
143
  this.app?.brush,
@@ -242,6 +247,7 @@ var PcCanvas = class extends PcElement {
242
247
  this.frame = 0;
243
248
  }
244
249
  firstUpdated() {
250
+ this.canvas?.getContext("2d", { willReadFrequently: true });
245
251
  if (typeof ResizeObserver !== "undefined") {
246
252
  this.resizeObserver = new ResizeObserver(() => this.syncSize());
247
253
  this.resizeObserver.observe(this);
@@ -884,6 +890,7 @@ var PcCanvas = class extends PcElement {
884
890
  if (!ctx) return;
885
891
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
886
892
  const sampling = this.sampling;
893
+ this.sampler.hidden = !sampling;
887
894
  if (!sampling) return;
888
895
  const ratio = this.pixelRatio();
889
896
  ctx.save();
@@ -1046,4 +1053,4 @@ var PcCanvas = class extends PcElement {
1046
1053
  }
1047
1054
  };
1048
1055
  defineElement("pc-canvas", PcCanvas);
1049
- export { ANTS_INTERVAL, CHAT_WIDTH_RATIO, HANDLE_GRAB_DISTANCE, LASSO_TOOLS, PEN_LIFT_GRACE_MS, PcCanvas, SAMPLER_HEIGHT, SAMPLER_WIDTH, TOOLBAR_WIDTH, TOUCH_SAMPLER_GAP, cursorFor, interfaceInsets, samplerPosition };
1056
+ export { ANTS_INTERVAL, CHAT_WIDTH_RATIO, HANDLE_GRAB_DISTANCE, LASSO_TOOLS, MAX_PIXEL_RATIO, PEN_LIFT_GRACE_MS, PcCanvas, SAMPLER_HEIGHT, SAMPLER_WIDTH, TOOLBAR_WIDTH, TOUCH_SAMPLER_GAP, cursorFor, interfaceInsets, samplerPosition };
@@ -1,3 +1,3 @@
1
1
  // src/ui/elements/PcCanvas/style.css?inline
2
- var style_default = ":host {\n display: block;\n position: absolute;\n inset: 0;\n touch-action: none;\n}\ncanvas {\n display: block;\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n}\ncanvas.outlines,\ncanvas.sampler {\n pointer-events: none;\n}\ncanvas.overlay {\n pointer-events: none;\n /* Inverts what is underneath so the brush outline is visible on any colour */\n mix-blend-mode: difference;\n}\n";
2
+ var style_default = ":host {\n display: block;\n position: absolute;\n inset: 0;\n touch-action: none;\n}\ncanvas {\n display: block;\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n}\ncanvas[hidden] {\n display: none;\n}\ncanvas.outlines,\ncanvas.sampler {\n pointer-events: none;\n}\ncanvas.overlay {\n pointer-events: none;\n /* Inverts what is underneath so the brush outline is visible on any colour */\n mix-blend-mode: difference;\n}\n";
3
3
  export { style_default as default };
@@ -5,9 +5,10 @@ import "../PcContextMenu";
5
5
  /** The tools that cut, copy and paste are shown for: the brush, eraser, lassos and Move tool */
6
6
  export declare const CLIPBOARD_TOOLS: readonly Tool[];
7
7
  /**
8
- * Cut, copy and paste buttons for the canvas's masked image data, shown for the brush, eraser, lassos and Move tool
9
- * (see `CLIPBOARD_TOOLS`). Cut and copy work while there's a mask and no transform is pending; paste while there's
10
- * copied image data. While `narrow`, they're one ⋮ menu (`pc-context-menu`) instead.
8
+ * Cut, copy, copy merged and paste buttons for the canvas's masked image data, shown for the brush, eraser, lassos and
9
+ * Move tool (see `CLIPBOARD_TOOLS`). "Copy merged" copies the shown layers mixed as shown (see `PaintCanvas.copyMerged`).
10
+ * Cut and both copies work while there's a mask and no transform is pending; paste while there's copied image data.
11
+ * While `narrow`, they're one ⋮ menu (`pc-context-menu`) instead.
11
12
  *
12
13
  * `<paint-canvas>` puts it at the end of its top pane, after the tool settings.
13
14
  *
@@ -40,7 +41,7 @@ export declare class PcClipboardTools extends PcElement {
40
41
  narrow: boolean;
41
42
  private changes;
42
43
  constructor();
43
- /** Cut, copy and paste, each with whether it can be done now */
44
+ /** Cut, copy, copy merged and paste, each with whether it can be done now */
44
45
  private actions;
45
46
  /** Hide for tools without clipboard actions (e.g. zoom) */
46
47
  protected willUpdate(): void;
@@ -17,9 +17,10 @@ var CLIPBOARD_TOOLS = [
17
17
  Tool.Move
18
18
  ];
19
19
  /**
20
- * Cut, copy and paste buttons for the canvas's masked image data, shown for the brush, eraser, lassos and Move tool
21
- * (see `CLIPBOARD_TOOLS`). Cut and copy work while there's a mask and no transform is pending; paste while there's
22
- * copied image data. While `narrow`, they're one ⋮ menu (`pc-context-menu`) instead.
20
+ * Cut, copy, copy merged and paste buttons for the canvas's masked image data, shown for the brush, eraser, lassos and
21
+ * Move tool (see `CLIPBOARD_TOOLS`). "Copy merged" copies the shown layers mixed as shown (see `PaintCanvas.copyMerged`).
22
+ * Cut and both copies work while there's a mask and no transform is pending; paste while there's copied image data.
23
+ * While `narrow`, they're one ⋮ menu (`pc-context-menu`) instead.
23
24
  *
24
25
  * `<paint-canvas>` puts it at the end of its top pane, after the tool settings.
25
26
  *
@@ -62,7 +63,7 @@ var PcClipboardTools = class extends PcElement {
62
63
  super();
63
64
  this.narrow = false;
64
65
  }
65
- /** Cut, copy and paste, each with whether it can be done now */
66
+ /** Cut, copy, copy merged and paste, each with whether it can be done now */
66
67
  actions(app) {
67
68
  const noCopy = !app.selection.active || app.transform.pending;
68
69
  return [
@@ -78,6 +79,12 @@ var PcClipboardTools = class extends PcElement {
78
79
  disabled: noCopy,
79
80
  run: () => app.copy()
80
81
  },
82
+ {
83
+ name: "copy-merged",
84
+ label: this.t("paintCanvas.common.copyMerged"),
85
+ disabled: noCopy,
86
+ run: () => app.copyMerged()
87
+ },
81
88
  {
82
89
  name: "paste",
83
90
  label: this.t("paintCanvas.common.paste"),
@@ -106,7 +113,7 @@ var PcClipboardTools = class extends PcElement {
106
113
  ></pc-context-menu>`;
107
114
  return actions.map((action) => html`<button
108
115
  type="button"
109
- class="pill ${action.name}"
116
+ class="pill-s ${action.name}"
110
117
  ?disabled=${action.disabled}
111
118
  @click=${action.run}
112
119
  >
@@ -1,6 +1,7 @@
1
1
  import { type PropertyValues } from "lit";
2
2
  import { PcElement } from "../../base";
3
3
  import "../PcButton";
4
+ import "../PcDialogWindow";
4
5
  import type { ButtonVariant } from "../PcButton";
5
6
  /** A button in a dialog */
6
7
  export interface DialogChoice {
@@ -12,14 +13,27 @@ export interface DialogChoice {
12
13
  /** The button's style instead (e.g. `dismiss`: pink text without a background) */
13
14
  variant?: ButtonVariant;
14
15
  }
15
- /** What `pc-choose` gives when the dialog is dismissed with Escape */
16
+ /** A text field in a dialog, e.g. for a name */
17
+ export interface DialogField {
18
+ label: string;
19
+ /** The text it starts with */
20
+ value: string;
21
+ }
22
+ /** What `pc-choose` gives: the chosen value, and the field's text when the dialog has a field */
23
+ export interface DialogChoiceDetail {
24
+ value: string;
25
+ text?: string;
26
+ }
27
+ /** What `pc-choose` gives when the dialog is dismissed (Escape, or a press outside it) */
16
28
  export declare const CANCEL_CHOICE = "cancel";
17
29
  /**
18
- * A modal window asking the user to choose, over a dimmed backdrop, with a heading, a message, any content of its own,
19
- * and its choices, each on its own row (the cancel choice and those marked `cancelling` in pink, the others purple,
20
- * unless a choice gives its own `variant`).
21
- * Focus moves to the first choice when it opens. Fires `pc-choose` with the chosen value (`cancel` for Escape, pressed
22
- * anywhere while it's open).
30
+ * A modal window asking the user to choose (a `<pc-dialog-window>` over a shaded page), with a heading, a message, an
31
+ * optional text field (`field`, e.g. for a name), any content of its own, and its choices, each on its own row (the
32
+ * cancel choice and those marked `cancelling` in pink, the others purple, unless a choice gives its own `variant`).
33
+ *
34
+ * Focus moves to the field, or else the first choice, when it opens. Fires `pc-choose` with the chosen value (and the
35
+ * field's `text`): Enter in the field chooses the first choice that isn't cancelling; Escape and a press outside it
36
+ * choose `cancel`, unless it's not `dismissible`. `inline` shows it over its nearest positioned ancestor only.
23
37
  *
24
38
  * @example
25
39
  * html`<pc-dialog heading="Delete layer" message='Delete "Sketch"?'
@@ -37,6 +51,16 @@ export declare class PcDialog extends PcElement {
37
51
  choices: {
38
52
  attribute: boolean;
39
53
  };
54
+ field: {
55
+ attribute: boolean;
56
+ };
57
+ dismissible: {
58
+ type: BooleanConstructor;
59
+ };
60
+ inline: {
61
+ type: BooleanConstructor;
62
+ reflect: boolean;
63
+ };
40
64
  translations: {
41
65
  attribute: boolean;
42
66
  };
@@ -52,12 +76,24 @@ export declare class PcDialog extends PcElement {
52
76
  heading: string;
53
77
  message: string;
54
78
  choices: readonly DialogChoice[];
79
+ /** A text field to fill in, if any */
80
+ field?: DialogField;
81
+ /** Whether Escape and a press outside it choose `cancel` */
82
+ dismissible: boolean;
83
+ /** Whether it covers only its nearest positioned ancestor, leaving the rest of the page usable */
84
+ inline: boolean;
55
85
  constructor();
56
86
  firstUpdated(changed: PropertyValues<this>): void;
57
- /** Choose one of the choices (or `cancel`) */
87
+ /**
88
+ * Move focus to the field (selecting its text), or else the first choice, once the window has opened (nothing in it
89
+ * can take focus before) and the choice has rendered its button.
90
+ */
91
+ private focusFirst;
92
+ /**
93
+ * Choose one of the choices (or `cancel`), giving the field's text when there's a field.
94
+ */
58
95
  choose(value: string): void;
59
- private readonly onKeyDown;
60
- connectedCallback(): void;
61
- disconnectedCallback(): void;
96
+ private readonly onFieldKeyDown;
97
+ private readonly onDismiss;
62
98
  render(): import("lit").TemplateResult<1>;
63
99
  }
@@ -1,15 +1,18 @@
1
1
  import { PcElement, defineElement, emit } from "../../base/index.js";
2
2
  import "../PcButton/index.js";
3
+ import "../PcDialogWindow/index.js";
3
4
  import { css, html, nothing } from "lit";
4
5
  // src/ui/elements/PcDialog/index.ts
5
- /** What `pc-choose` gives when the dialog is dismissed with Escape */
6
+ /** What `pc-choose` gives when the dialog is dismissed (Escape, or a press outside it) */
6
7
  var CANCEL_CHOICE = "cancel";
7
8
  /**
8
- * A modal window asking the user to choose, over a dimmed backdrop, with a heading, a message, any content of its own,
9
- * and its choices, each on its own row (the cancel choice and those marked `cancelling` in pink, the others purple,
10
- * unless a choice gives its own `variant`).
11
- * Focus moves to the first choice when it opens. Fires `pc-choose` with the chosen value (`cancel` for Escape, pressed
12
- * anywhere while it's open).
9
+ * A modal window asking the user to choose (a `<pc-dialog-window>` over a shaded page), with a heading, a message, an
10
+ * optional text field (`field`, e.g. for a name), any content of its own, and its choices, each on its own row (the
11
+ * cancel choice and those marked `cancelling` in pink, the others purple, unless a choice gives its own `variant`).
12
+ *
13
+ * Focus moves to the field, or else the first choice, when it opens. Fires `pc-choose` with the chosen value (and the
14
+ * field's `text`): Enter in the field chooses the first choice that isn't cancelling; Escape and a press outside it
15
+ * choose `cancel`, unless it's not `dismissible`. `inline` shows it over its nearest positioned ancestor only.
13
16
  *
14
17
  * @example
15
18
  * html`<pc-dialog heading="Delete layer" message='Delete "Sketch"?'
@@ -21,32 +24,20 @@ var PcDialog = class extends PcElement {
21
24
  ...PcElement.properties,
22
25
  heading: { type: String },
23
26
  message: { type: String },
24
- choices: { attribute: false }
27
+ choices: { attribute: false },
28
+ field: { attribute: false },
29
+ dismissible: { type: Boolean },
30
+ inline: {
31
+ type: Boolean,
32
+ reflect: true
33
+ }
25
34
  };
26
35
  static styles = [PcElement.styles, css`
27
36
  :host {
28
- position: fixed;
29
- z-index: 20;
30
- inset: 0;
31
- display: flex;
32
- align-items: center;
33
- justify-content: center;
34
- padding: var(--pc-space-3);
35
- background: var(--pc-backdrop);
37
+ display: contents;
36
38
  }
37
- [role="dialog"] {
38
- display: flex;
39
- width: 100%;
40
- max-width: 400px;
41
- flex-direction: column;
42
- gap: var(--pc-space-2);
43
- padding: var(--pc-space-3);
44
- border-radius: var(--pc-radius);
45
- background: var(--pc-surface-raised);
46
- color: var(--pc-text);
47
- box-shadow: var(--pc-shadow);
48
- font-family: var(--pc-font);
49
- font-size: var(--pc-font-size);
39
+ pc-dialog-window {
40
+ --pc-dialog-width: 400px;
50
41
  }
51
42
  h2 {
52
43
  margin: 0;
@@ -55,6 +46,30 @@ var PcDialog = class extends PcElement {
55
46
  p {
56
47
  margin: 0;
57
48
  }
49
+ .body {
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: var(--pc-space-2);
53
+ }
54
+ label {
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: var(--pc-space-1);
58
+ }
59
+ input {
60
+ box-sizing: border-box;
61
+ width: 100%;
62
+ min-height: var(--pc-control-size);
63
+ padding: 0 var(--pc-space-2);
64
+ border: 1px solid var(--pc-border);
65
+ border-radius: var(--pc-radius-small);
66
+ background: var(--pc-surface);
67
+ color: var(--pc-text);
68
+ font: inherit;
69
+ }
70
+ input:focus-visible {
71
+ outline: var(--pc-focus-width) solid var(--pc-focus);
72
+ }
58
73
  .choices {
59
74
  display: flex;
60
75
  flex-direction: column;
@@ -66,41 +81,77 @@ var PcDialog = class extends PcElement {
66
81
  this.heading = "";
67
82
  this.message = "";
68
83
  this.choices = [];
84
+ this.dismissible = true;
85
+ this.inline = false;
69
86
  }
70
87
  firstUpdated(changed) {
71
88
  super.firstUpdated(changed);
89
+ this.focusFirst();
90
+ }
91
+ /**
92
+ * Move focus to the field (selecting its text), or else the first choice, once the window has opened (nothing in it
93
+ * can take focus before) and the choice has rendered its button.
94
+ */
95
+ async focusFirst() {
96
+ await this.renderRoot.querySelector("pc-dialog-window").updateComplete;
97
+ const field = this.renderRoot.querySelector("input");
98
+ if (field) {
99
+ field.focus();
100
+ field.select();
101
+ return;
102
+ }
72
103
  const first = this.renderRoot.querySelector("pc-button");
73
- if (first) first.updateComplete.then(() => first.focus());
104
+ if (!first) return;
105
+ await first.updateComplete;
106
+ first.focus();
74
107
  }
75
- /** Choose one of the choices (or `cancel`) */
108
+ /**
109
+ * Choose one of the choices (or `cancel`), giving the field's text when there's a field.
110
+ */
76
111
  choose(value) {
77
- emit(this, "pc-choose", { value });
112
+ const text = this.renderRoot.querySelector("input")?.value;
113
+ emit(this, "pc-choose", text === void 0 ? { value } : {
114
+ value,
115
+ text
116
+ });
78
117
  }
79
- onKeyDown = (event) => {
80
- if (event.key !== "Escape") return;
118
+ onFieldKeyDown = (event) => {
119
+ if (event.key !== "Enter") return;
120
+ const choice = this.choices.find((item) => item.value !== "cancel" && !item.cancelling);
121
+ if (!choice) return;
122
+ event.preventDefault();
123
+ this.choose(choice.value);
124
+ };
125
+ onDismiss = (event) => {
81
126
  event.preventDefault();
82
127
  this.choose(CANCEL_CHOICE);
83
128
  };
84
- connectedCallback() {
85
- super.connectedCallback();
86
- window.addEventListener("keydown", this.onKeyDown);
87
- }
88
- disconnectedCallback() {
89
- super.disconnectedCallback();
90
- window.removeEventListener("keydown", this.onKeyDown);
91
- }
92
129
  render() {
93
- return html`<div
130
+ const field = this.field;
131
+ return html`<pc-dialog-window
94
132
  part="dialog"
95
- role="dialog"
96
- aria-modal="true"
97
- aria-labelledby="heading"
98
- aria-describedby=${this.message ? "message" : nothing}
133
+ open
134
+ shaded
135
+ closable="false"
136
+ .dismissible=${this.dismissible}
137
+ ?inline=${this.inline}
138
+ description=${this.message || nothing}
139
+ @pc-dismiss=${this.onDismiss}
99
140
  >
100
- <h2 id="heading" part="heading">${this.heading}</h2>
101
- ${this.message ? html`<p id="message" part="message">${this.message}</p>` : nothing}
102
- <slot></slot>
103
- <div class="choices" part="choices">
141
+ <h2 slot="header-middle" part="heading">${this.heading}</h2>
142
+ <div slot="body" class="body">
143
+ ${this.message ? html`<p part="message">${this.message}</p>` : nothing}
144
+ ${field ? html`<label part="field">
145
+ ${field.label}
146
+ <input
147
+ type="text"
148
+ .value=${field.value}
149
+ @keydown=${this.onFieldKeyDown}
150
+ />
151
+ </label>` : nothing}
152
+ <slot></slot>
153
+ </div>
154
+ <div slot="footer-middle" class="choices" part="choices">
104
155
  ${this.choices.map((choice) => html`<pc-button
105
156
  variant=${choice.variant ?? (choice.value === "cancel" || choice.cancelling ? "cancel" : "accent")}
106
157
  data-value=${choice.value}
@@ -108,7 +159,7 @@ var PcDialog = class extends PcElement {
108
159
  >${choice.label}</pc-button
109
160
  >`)}
110
161
  </div>
111
- </div>`;
162
+ </pc-dialog-window>`;
112
163
  }
113
164
  };
114
165
  defineElement("pc-dialog", PcDialog);
@@ -0,0 +1,141 @@
1
+ import { type PropertyValues } from "lit";
2
+ import { PcElement } from "../../base";
3
+ /** Why the user asked to close a dialog window: its ⨉ button, the Escape key, or a press outside it */
4
+ export type DialogCloseReason = "close" | "escape" | "outside";
5
+ /** Event fired when the user asks to close a dialog window (detail: `{ reason }`); cancel it to keep the window open */
6
+ export declare const DIALOG_DISMISS_EVENT = "pc-dismiss";
7
+ /**
8
+ * A window over the page, built on the browser's `<dialog>`: centred in the viewport, never bigger than it, with a
9
+ * header, a body and a footer. While it's `open`, the rest of the page can't be used or scrolled and keyboard focus stays
10
+ * in it (moving to its first control, and back to where it was once it closes).
11
+ *
12
+ * Content goes in its slots: `header-start`, `header-middle` (which names the window for screen readers, unless it has a
13
+ * `label`; `description` describes it), `header-end`, `body` (or no slot), `footer-start`, `footer-middle` and `footer-end`. The body scrolls when
14
+ * there's too much to fit, keeping the header and footer in view. The header and footer only show with content (or the
15
+ * ⨉ button).
16
+ *
17
+ * Options:
18
+ * - `closable` (on by default; `closable="false"` turns it off): a ⨉ button at the top right.
19
+ * - `shaded`: shade the page under it.
20
+ * - `dismissible` (on by default; `dismissible="false"` turns it off): a press outside it closes it.
21
+ * - `inline`: cover only its nearest positioned ancestor instead of the whole page, which stays usable (and scrolls).
22
+ *
23
+ * The ⨉ button, a press outside it (while `dismissible`) and Escape (while `closable` or `dismissible`) fire a cancellable
24
+ * `pc-dismiss` event (detail: `{ reason }`, one of `close`, `outside` or `escape`); unless it's cancelled, the window
25
+ * closes (`open` becomes false). `--pc-dialog-width` sets its width (fitted to its content by default).
26
+ *
27
+ * @example
28
+ * html`<pc-dialog-window open shaded label="Keyboard shortcuts" @pc-dismiss=${close}>
29
+ * <h2 slot="header-middle">Keyboard shortcuts</h2>
30
+ * <pc-shortcut-settings slot="body" .settings=${settings}></pc-shortcut-settings>
31
+ * <pc-button slot="footer-end" @click=${close}>Done</pc-button>
32
+ * </pc-dialog-window>`
33
+ */
34
+ export declare class PcDialogWindow extends PcElement {
35
+ static properties: {
36
+ open: {
37
+ type: BooleanConstructor;
38
+ reflect: boolean;
39
+ };
40
+ closable: {
41
+ converter: {
42
+ fromAttribute: (value: string | null) => boolean;
43
+ toAttribute: (value: boolean) => "" | "false";
44
+ };
45
+ reflect: boolean;
46
+ };
47
+ shaded: {
48
+ type: BooleanConstructor;
49
+ reflect: boolean;
50
+ };
51
+ dismissible: {
52
+ converter: {
53
+ fromAttribute: (value: string | null) => boolean;
54
+ toAttribute: (value: boolean) => "" | "false";
55
+ };
56
+ reflect: boolean;
57
+ };
58
+ inline: {
59
+ type: BooleanConstructor;
60
+ reflect: boolean;
61
+ };
62
+ label: {
63
+ type: StringConstructor;
64
+ };
65
+ description: {
66
+ type: StringConstructor;
67
+ };
68
+ filled: {
69
+ state: boolean;
70
+ };
71
+ translations: {
72
+ attribute: boolean;
73
+ };
74
+ icons: {
75
+ attribute: boolean;
76
+ };
77
+ theme: {
78
+ type: StringConstructor;
79
+ reflect: boolean;
80
+ };
81
+ };
82
+ static styles: import("lit").CSSResultGroup[];
83
+ /** Whether it's shown */
84
+ open: boolean;
85
+ /** Whether it has a ⨉ button at the top right */
86
+ closable: boolean;
87
+ /** Whether the page under it is shaded */
88
+ shaded: boolean;
89
+ /** Whether a press outside it closes it */
90
+ dismissible: boolean;
91
+ /** Whether it covers only its nearest positioned ancestor, leaving the rest of the page usable */
92
+ inline: boolean;
93
+ /** Its name for screen readers, instead of its `header-middle` content */
94
+ label?: string;
95
+ /** A description for screen readers, e.g. the question a prompt asks */
96
+ description?: string;
97
+ /** The names of its header and footer slots that have content */
98
+ filled: ReadonlySet<string>;
99
+ /** Whether it's keeping the page from scrolling */
100
+ private scrollLocked;
101
+ /** What had focus before it opened, to give it back to if it's taken out of the page while open */
102
+ private returnFocus;
103
+ /** Whether the `<dialog>` was last shown inline (not modal) */
104
+ private shownInline;
105
+ /** Whether the press being made started outside the window */
106
+ private pressedOutside;
107
+ constructor();
108
+ /** The `<dialog>` it's built on */
109
+ private get dialog();
110
+ /**
111
+ * Ask to close the window, as the user would: fires `pc-dismiss` (detail: `{ reason }`), and closes it unless that's
112
+ * cancelled.
113
+ *
114
+ * @example
115
+ * dialogWindow.requestClose("close");
116
+ */
117
+ requestClose(reason: DialogCloseReason): void;
118
+ connectedCallback(): void;
119
+ disconnectedCallback(): void;
120
+ protected firstUpdated(changed: PropertyValues<this>): void;
121
+ protected updated(changed: PropertyValues<this>): void;
122
+ /**
123
+ * Show or close the `<dialog>` to match `open` (modal unless `inline`), keeping the page from scrolling while a modal
124
+ * window is open.
125
+ */
126
+ private syncOpen;
127
+ /** Keep the page from scrolling while a modal window is open in it, and let it scroll again otherwise */
128
+ private syncScrollLock;
129
+ /** Note which header and footer slots have content: only those (and the header and footer with them) are shown */
130
+ private updateRegions;
131
+ /** Whether any of a header's or footer's slots have content */
132
+ private hasRegion;
133
+ private readonly onKeyDown;
134
+ private readonly onCancel;
135
+ private readonly onClose;
136
+ private readonly onPointerDown;
137
+ private readonly onClick;
138
+ /** A header or footer's start, middle and end slots */
139
+ private renderRegion;
140
+ render(): import("lit").TemplateResult<1>;
141
+ }