@lvl99dotcom/paintcanvas 0.5.0 → 0.7.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 (120) hide show
  1. package/README.md +2 -2
  2. package/dist/lib/brushes/configRanges/index.d.ts +17 -0
  3. package/dist/lib/brushes/configRanges/index.js +31 -1
  4. package/dist/lib/brushes/params/index.d.ts +2 -1
  5. package/dist/lib/brushes/params/index.js +7 -4
  6. package/dist/lib/brushes/types/index.d.ts +8 -0
  7. package/dist/lib/classes/Layer/index.d.ts +10 -0
  8. package/dist/lib/classes/Layer/index.js +15 -1
  9. package/dist/lib/classes/LayerAutosave/index.d.ts +9 -15
  10. package/dist/lib/classes/LayerAutosave/index.js +30 -43
  11. package/dist/lib/classes/LayerStack/index.d.ts +15 -0
  12. package/dist/lib/classes/LayerStack/index.js +26 -2
  13. package/dist/lib/classes/PaintCanvas/index.d.ts +36 -5
  14. package/dist/lib/classes/PaintCanvas/index.js +45 -14
  15. package/dist/lib/classes/PngEncoder/index.d.ts +41 -0
  16. package/dist/lib/classes/PngEncoder/index.js +104 -0
  17. package/dist/lib/classes/ProfileGraphs/index.d.ts +87 -0
  18. package/dist/lib/classes/ProfileGraphs/index.js +148 -0
  19. package/dist/lib/classes/SettingsAutosave/index.d.ts +1 -1
  20. package/dist/lib/classes/SettingsAutosave/index.js +2 -1
  21. package/dist/lib/classes/UiSettings/index.d.ts +24 -2
  22. package/dist/lib/classes/UiSettings/index.js +32 -7
  23. package/dist/lib/messages/schema.d.ts +720 -0
  24. package/dist/lib/presets/schema.d.ts +360 -0
  25. package/dist/lib/presets/schema.js +8 -1
  26. package/dist/lib/profiles/index.d.ts +61 -0
  27. package/dist/lib/profiles/index.js +131 -0
  28. package/dist/lib/profiles/schema.d.ts +73 -0
  29. package/dist/lib/profiles/schema.js +53 -0
  30. package/dist/lib/rendering/sample/index.d.ts +11 -0
  31. package/dist/lib/rendering/sample/index.js +36 -5
  32. package/dist/lib/settings/index.d.ts +2 -1
  33. package/dist/lib/settings/index.js +4 -1
  34. package/dist/lib/settings/schema.d.ts +53 -1
  35. package/dist/lib/settings/schema.js +9 -2
  36. package/dist/lib/storage/index.d.ts +26 -0
  37. package/dist/lib/storage/index.js +38 -1
  38. package/dist/lib/validation/validators.generated.js +13061 -3675
  39. package/dist/lib/version/index.d.ts +25 -0
  40. package/dist/lib/version/index.js +35 -0
  41. package/dist/locales/en-GB.js +35 -0
  42. package/dist/locales/en-US.js +35 -0
  43. package/dist/locales/fr.js +35 -0
  44. package/dist/locales/ja.js +35 -0
  45. package/dist/locales/ko.js +35 -0
  46. package/dist/locales/zh-Hans.js +35 -0
  47. package/dist/locales/zh-Hant.js +35 -0
  48. package/dist/package.js +1 -1
  49. package/dist/ui/ask/index.d.ts +48 -0
  50. package/dist/ui/ask/index.js +67 -0
  51. package/dist/ui/base/index.d.ts +8 -1
  52. package/dist/ui/base/index.js +19 -4
  53. package/dist/ui/elements/PaintCanvas/index.d.ts +13 -3
  54. package/dist/ui/elements/PaintCanvas/index.js +25 -6
  55. package/dist/ui/elements/PaintCanvas/style.js +1 -1
  56. package/dist/ui/elements/PcAppSettings/index.d.ts +15 -7
  57. package/dist/ui/elements/PcAppSettings/index.js +53 -62
  58. package/dist/ui/elements/PcBrushConfig/index.d.ts +26 -14
  59. package/dist/ui/elements/PcBrushConfig/index.js +214 -145
  60. package/dist/ui/elements/PcBrushConfig/style.js +1 -1
  61. package/dist/ui/elements/PcBrushTest/index.d.ts +1 -0
  62. package/dist/ui/elements/PcBrushTest/index.js +15 -9
  63. package/dist/ui/elements/PcBrushTest/style.js +1 -1
  64. package/dist/ui/elements/PcButton/index.d.ts +12 -3
  65. package/dist/ui/elements/PcButton/index.js +18 -10
  66. package/dist/ui/elements/PcButtonMenu/index.d.ts +88 -0
  67. package/dist/ui/elements/PcButtonMenu/index.js +125 -0
  68. package/dist/ui/elements/PcCanvas/index.d.ts +15 -1
  69. package/dist/ui/elements/PcCanvas/index.js +42 -19
  70. package/dist/ui/elements/PcCanvas/style.js +1 -1
  71. package/dist/ui/elements/PcClipboardTools/index.d.ts +11 -7
  72. package/dist/ui/elements/PcClipboardTools/index.js +30 -29
  73. package/dist/ui/elements/PcDialog/index.d.ts +46 -10
  74. package/dist/ui/elements/PcDialog/index.js +102 -51
  75. package/dist/ui/elements/PcDialogWindow/index.d.ts +141 -0
  76. package/dist/ui/elements/PcDialogWindow/index.js +385 -0
  77. package/dist/ui/elements/PcInputSettings/index.d.ts +2 -1
  78. package/dist/ui/elements/PcInputSettings/index.js +9 -15
  79. package/dist/ui/elements/PcLayerItem/index.d.ts +21 -6
  80. package/dist/ui/elements/PcLayerItem/index.js +53 -26
  81. package/dist/ui/elements/PcLayerItem/style.js +1 -1
  82. package/dist/ui/elements/PcPanelBrushes/index.d.ts +3 -2
  83. package/dist/ui/elements/PcPanelBrushes/index.js +7 -5
  84. package/dist/ui/elements/PcPanelBrushes/style.js +1 -1
  85. package/dist/ui/elements/PcPanelColours/index.js +2 -2
  86. package/dist/ui/elements/PcPanelColours/style.js +1 -1
  87. package/dist/ui/elements/PcPanelLayers/index.js +1 -1
  88. package/dist/ui/elements/PcPanelLayers/style.js +1 -1
  89. package/dist/ui/elements/PcProfileEditor/index.d.ts +103 -0
  90. package/dist/ui/elements/PcProfileEditor/index.js +259 -0
  91. package/dist/ui/elements/PcProfileGraph/index.d.ts +84 -0
  92. package/dist/ui/elements/PcProfileGraph/index.js +317 -0
  93. package/dist/ui/elements/PcShortcutSettings/index.js +9 -13
  94. package/dist/ui/elements/PcSliderRange/index.d.ts +147 -0
  95. package/dist/ui/elements/PcSliderRange/index.js +308 -0
  96. package/dist/ui/elements/PcStats/index.d.ts +19 -4
  97. package/dist/ui/elements/PcStats/index.js +29 -13
  98. package/dist/ui/elements/PcToggleCheckbox/index.d.ts +13 -2
  99. package/dist/ui/elements/PcToggleCheckbox/index.js +27 -8
  100. package/dist/ui/elements/PcToolSettings/index.d.ts +1 -0
  101. package/dist/ui/elements/PcToolSettings/index.js +14 -14
  102. package/dist/ui/elements/PcVersion/index.d.ts +12 -0
  103. package/dist/ui/elements/PcVersion/index.js +43 -0
  104. package/dist/ui/icons/images/icon-profile-graph@2x.js +3 -0
  105. package/dist/ui/icons/index.d.ts +1 -1
  106. package/dist/ui/icons/index.js +2 -0
  107. package/dist/ui/index.d.ts +9 -2
  108. package/dist/ui/index.js +9 -2
  109. package/dist/ui/layout/index.d.ts +2 -2
  110. package/dist/ui/layout/index.js +2 -2
  111. package/dist/ui/styles/index.d.ts +21 -1
  112. package/dist/ui/styles/index.js +73 -14
  113. package/locales/en-GB.json +35 -0
  114. package/locales/en-US.json +35 -0
  115. package/locales/fr.json +35 -0
  116. package/locales/ja.json +35 -0
  117. package/locales/ko.json +35 -0
  118. package/locales/zh-Hans.json +35 -0
  119. package/locales/zh-Hant.json +35 -0
  120. package/package.json +25 -23
@@ -1,3 +1,3 @@
1
1
  // src/ui/elements/PcLayerItem/style.css?inline
2
- var style_default = "/* A white row with space around its parts. The middle column can shrink, so the row never grows wider than its panel */\n:host {\n display: grid;\n grid-template-columns: 30px minmax(0, 1fr) 30px 30px;\n align-items: center;\n column-gap: 6px;\n position: relative;\n padding: 6px;\n border: 1px solid var(--pc-surface-active);\n background: var(--pc-surface);\n}\n.name.active {\n font-weight: bold;\n}\n.thumbnail {\n width: 30px;\n height: 30px;\n}\n/* Thumbnail on the left and name on the right; under them, opacity on the left and blend mode on the right. The blend\n mode gets a set share of the width rather than its own: browsers size dropdowns differently (Safari and Firefox as\n wide as the longest option, Chrome as the chosen one), which would otherwise push the row wider than its panel */\n.details {\n display: grid;\n grid-template-columns: 30px minmax(0, 1fr) minmax(0, 2fr);\n align-items: center;\n gap: 4px 6px;\n min-width: 0;\n}\n.details .name {\n grid-column: 2 / -1;\n}\n.details .name,\n.details select {\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n}\n.details input[type=\"range\"] {\n grid-column: 1 / 3;\n min-width: 0;\n margin: 0;\n}\n/* The reorder handle drags with mouse, pen or touch (touch drags it rather than scrolling the panel) */\n.handle {\n /* As tall as the row */\n align-self: stretch;\n}\n.handle::part(button) {\n min-width: 0;\n padding: 0;\n cursor: grab;\n touch-action: none;\n}\n/* While dragged: the row floats with the pointer, out of the list (the layers menu places it), and the cursor shows\n what dropping would do (\"+\" duplicates on \"New Layer\") */\n:host([dragging]) {\n position: fixed;\n z-index: 10;\n opacity: 0.9;\n background: Canvas;\n box-shadow: var(--pc-shadow);\n}\n:host([dragging]) .handle::part(button) {\n cursor: grabbing;\n}\n:host([drop-action=\"duplicate\"]) .handle::part(button) {\n cursor: copy;\n}\n:host([drop-action=\"none\"]) .handle::part(button) {\n cursor: no-drop;\n}\n/* Alpha lock and visibility: boxed icon toggles like the settings bar's, darker while on */\n.alpha-lock,\n.visibility {\n width: 30px;\n height: 30px;\n padding: 2px;\n border: 1px solid var(--pc-border);\n border-radius: 4px;\n background: var(--pc-surface);\n cursor: pointer;\n}\n.alpha-lock:hover,\n.visibility:hover {\n background: var(--pc-surface-hover);\n}\n.alpha-lock[aria-pressed=\"true\"],\n.visibility[aria-pressed=\"true\"] {\n background: var(--pc-surface-active);\n}\n.alpha-lock:focus-visible,\n.visibility:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n}\n.alpha-lock img,\n.visibility img {\n display: block;\n width: 24px;\n height: 24px;\n margin: auto;\n}\n";
2
+ var style_default = "/* A white row with space around its parts. The middle column can shrink, so the row never grows wider than its panel */\n:host {\n display: grid;\n grid-template-columns: 30px minmax(0, 1fr) 30px 30px;\n align-items: center;\n column-gap: 6px;\n position: relative;\n padding: 6px;\n border: 1px solid var(--pc-surface-active);\n background: var(--pc-surface);\n}\n/* The selected layer: purple, so it stands out */\n:host([active]) {\n border-color: var(--pc-accent);\n background: var(--pc-accent);\n color: var(--pc-accent-text);\n}\n:host([active]) .name {\n font-weight: bold;\n}\n/* The opacity slider's track and handle in the row's text colour, so they show on purple */\n:host([active]) .opacity {\n --pc-accent: var(--pc-accent-text);\n --pc-border: rgb(255 255 255 / 45%);\n}\n/* The name, on one line (cut short with \"…\"); renamed from the actions menu */\n.name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.thumbnail {\n width: 30px;\n height: 30px;\n}\n/* Thumbnail on the left and name on the right; under them, opacity, then blend mode, each the full width. The blend\n mode never sets its own width: browsers size dropdowns differently (Safari and Firefox as wide as the longest option,\n Chrome as the chosen one), which would otherwise push the row wider than its panel */\n.details {\n display: grid;\n grid-template-columns: 30px minmax(0, 1fr);\n align-items: center;\n gap: 4px 6px;\n min-width: 0;\n}\n.details .opacity,\n.details select {\n grid-column: 1 / -1;\n box-sizing: border-box;\n width: 100%;\n min-width: 0;\n}\n/* The reorder handle drags with mouse, pen or touch (touch drags it rather than scrolling the panel) */\n.handle {\n /* As tall as the row */\n align-self: stretch;\n}\n.handle::part(button) {\n min-width: 0;\n padding: 0;\n cursor: grab;\n touch-action: none;\n}\n/* While dragged: the row floats with the pointer, out of the list (the layers menu places it), and the cursor shows\n what dropping would do (\"+\" duplicates on \"New Layer\") */\n:host([dragging]) {\n position: fixed;\n z-index: 10;\n opacity: 0.9;\n background: Canvas;\n box-shadow: var(--pc-shadow);\n}\n:host([dragging]) .handle::part(button) {\n cursor: grabbing;\n}\n:host([drop-action=\"duplicate\"]) .handle::part(button) {\n cursor: copy;\n}\n:host([drop-action=\"none\"]) .handle::part(button) {\n cursor: no-drop;\n}\n/* Alpha lock and visibility: boxed icon toggles like the settings bar's, darker while on */\n.alpha-lock,\n.visibility {\n width: 30px;\n height: 30px;\n padding: 2px;\n border: 1px solid var(--pc-border);\n border-radius: 4px;\n background: var(--pc-surface);\n cursor: pointer;\n}\n.alpha-lock:hover,\n.visibility:hover {\n background: var(--pc-surface-hover);\n}\n.alpha-lock[aria-pressed=\"true\"],\n.visibility[aria-pressed=\"true\"] {\n background: var(--pc-surface-active);\n}\n.alpha-lock:focus-visible,\n.visibility:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n}\n.alpha-lock img,\n.visibility img {\n display: block;\n width: 24px;\n height: 24px;\n margin: auto;\n}\n";
3
3
  export { style_default as default };
@@ -1,5 +1,6 @@
1
1
  import { nothing } from "lit";
2
2
  import { PcPanel } from "../PcPanel";
3
+ import { type ConfirmFn } from "../../ask";
3
4
  import "../PcBrushSize";
4
5
  import type PaintCanvas from "../../../lib/classes/PaintCanvas";
5
6
  import type BrushSettings from "../../../lib/classes/BrushSettings";
@@ -8,8 +9,7 @@ import { DELETE_CURSOR } from "../../../lib/rendering/cursors";
8
9
  import { type SaveMethod } from "../../../lib/rendering/screenshot";
9
10
  /** Cursor over a preset that a click deletes (Alt/Option held): the `--pc-delete-cursor` token's default */
10
11
  export { DELETE_CURSOR };
11
- /** Asks the user to confirm; replaceable for tests */
12
- export type ConfirmFn = (message: string) => boolean;
12
+ export type { ConfirmFn } from "../../ask";
13
13
  /** Largest shape preview in a preset tile, in CSS pixels */
14
14
  export declare const MAX_PREVIEW_SIZE = 30;
15
15
  /** Smallest shape preview in a preset tile, in CSS pixels */
@@ -73,6 +73,7 @@ export declare class PcPanelBrushes extends PcPanel {
73
73
  app?: PaintCanvas;
74
74
  /** Which settings this menu controls */
75
75
  kind: "brush" | "eraser";
76
+ /** Asks the user to confirm, in a dialog (replaceable for tests) */
76
77
  confirm: ConfirmFn;
77
78
  /** Saves exported presets (replaceable for tests) */
78
79
  saveFile: (blob: Blob, name: string) => Promise<SaveMethod | undefined>;
@@ -3,6 +3,7 @@ import { BRUSH_PRESET_FILE_FORMAT } from "../../../lib/presets/schema.js";
3
3
  import { NO_TEXT_SELECTION_CSS, PILLS_CSS, WINDOWS_CSS } from "../../styles/index.js";
4
4
  import { defineElement } from "../../base/index.js";
5
5
  import { PcPanel } from "../PcPanel/index.js";
6
+ import { askConfirm, deletePresetQuestion } from "../../ask/index.js";
6
7
  import "../PcBrushSize/index.js";
7
8
  import ChangeController from "../../ChangeController/index.js";
8
9
  import { groupPresetsByAuthor } from "../../../lib/brushes/authors/index.js";
@@ -77,7 +78,8 @@ var PcPanelBrushes = class extends PcPanel {
77
78
  unsafeCSS(WINDOWS_CSS),
78
79
  unsafeCSS(PILLS_CSS)
79
80
  ];
80
- confirm = (message) => window.confirm(message);
81
+ /** Asks the user to confirm, in a dialog (replaceable for tests) */
82
+ confirm = (options) => askConfirm(this.renderRoot, options);
81
83
  /** Saves exported presets (replaceable for tests) */
82
84
  saveFile = (blob, name) => saveFile(blob, name, void 0, JSON_FILE_TYPE);
83
85
  changes = new ChangeController(this, () => [this.app, this.settings]);
@@ -127,7 +129,7 @@ var PcPanelBrushes = class extends PcPanel {
127
129
  const app = this.app;
128
130
  if (!app) return;
129
131
  const name = preset.name ?? preset.id;
130
- if (!this.confirm(this.t("paintCanvas.presets.deleteConfirm", { name }))) return;
132
+ if (!await this.confirm(deletePresetQuestion(this.translations, name))) return;
131
133
  try {
132
134
  await app.customPresets.remove(preset.id);
133
135
  this.emit("pc-toast", {
@@ -287,14 +289,14 @@ var PcPanelBrushes = class extends PcPanel {
287
289
  <div class="actions">
288
290
  <button
289
291
  type="button"
290
- class="pill new-preset"
292
+ class="pill-s new-preset"
291
293
  @click=${() => this.emit("pc-new-preset", this.kind)}
292
294
  >
293
295
  ${this.t("paintCanvas.brushMenu.create")}
294
296
  </button>
295
297
  <button
296
298
  type="button"
297
- class="pill import"
299
+ class="pill-s import"
298
300
  @click=${() => this.renderRoot.querySelector(".import-file")?.click()}
299
301
  >
300
302
  ${this.t("paintCanvas.brushMenu.import")}
@@ -317,7 +319,7 @@ var PcPanelBrushes = class extends PcPanel {
317
319
  </label>
318
320
  <button
319
321
  type="button"
320
- class="pill export"
322
+ class="pill-s export"
321
323
  @click=${() => void this.exportFile()}
322
324
  >
323
325
  ${this.t("paintCanvas.brushMenu.export")}
@@ -1,3 +1,3 @@
1
1
  // src/ui/elements/PcPanelBrushes/style.css?inline
2
- var style_default = "/* Narrower than other panels by default (set --pc-panel-width to change it) */\n:host {\n --pc-panel-default-width: 240px;\n}\n/* The current preset on one row: its shape, name and size */\n.current {\n display: flex;\n align-items: center;\n gap: 8px;\n min-height: 44px;\n padding: 4px 8px;\n border: 1px solid var(--pc-border);\n background: var(--pc-surface);\n}\n.current-shape {\n flex: none;\n width: 28px;\n height: 28px;\n border-radius: 50%;\n background: var(--pc-text);\n}\n.current-shape.image {\n border-radius: 0;\n mask-repeat: no-repeat;\n mask-size: contain;\n mask-position: center;\n}\n.name {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.size {\n flex: none;\n align-self: stretch;\n margin: -4px -8px -4px 0;\n border-left: 1px solid var(--pc-border);\n}\n/* The presets scroll, at most 400px tall, or less so the whole menu fits under the settings bar in a shorter window */\n.presets {\n flex: 1 1 auto;\n min-height: 0;\n max-height: min(400px, calc(100vh - 180px));\n overflow-y: auto;\n}\nh3 {\n margin: 4px 0;\n font-size: 14px;\n}\nul {\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n align-content: start;\n margin: 0;\n padding: 0;\n list-style: none;\n border: 1px solid var(--pc-border);\n background: var(--pc-bar);\n}\n:host([deleting]) li button.custom {\n cursor: var(--pc-delete-cursor);\n}\n/* Create, Import and Export side by side */\n.actions {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 6px;\n}\n.actions button.pill {\n padding: 4px 8px;\n}\n.visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\nli button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n aspect-ratio: 1;\n padding: 0;\n border: none;\n border-right: 1px solid var(--pc-border);\n border-bottom: 1px solid var(--pc-border);\n background: var(--pc-bar);\n cursor: pointer;\n}\nli button:hover {\n background: var(--pc-surface-hover);\n}\nli button[aria-pressed=\"true\"] {\n background: var(--pc-surface-active);\n}\n.shape {\n background: var(--pc-text);\n border-radius: 50%;\n}\n.shape.image {\n border-radius: 0;\n mask-repeat: no-repeat;\n mask-size: 100% 100%;\n}\n";
2
+ var style_default = "/* Narrower than other panels by default (set --pc-panel-width to change it) */\n:host {\n --pc-panel-default-width: 240px;\n}\n/* The current preset on one row: its shape, name and size */\n.current {\n display: flex;\n align-items: center;\n gap: 8px;\n min-height: 44px;\n padding: 4px 8px;\n border: 1px solid var(--pc-border);\n background: var(--pc-surface);\n}\n.current-shape {\n flex: none;\n width: 28px;\n height: 28px;\n border-radius: 50%;\n background: var(--pc-text);\n}\n.current-shape.image {\n border-radius: 0;\n mask-repeat: no-repeat;\n mask-size: contain;\n mask-position: center;\n}\n.name {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.size {\n flex: none;\n align-self: stretch;\n margin: -4px -8px -4px 0;\n border-left: 1px solid var(--pc-border);\n}\n/* The presets scroll, at most 400px tall, or less so the whole menu fits under the settings bar in a shorter window */\n.presets {\n flex: 1 1 auto;\n min-height: 0;\n max-height: min(400px, calc(100vh - 180px));\n overflow-y: auto;\n}\nh3 {\n margin: 4px 0;\n font-size: 14px;\n}\nul {\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n align-content: start;\n margin: 0;\n padding: 0;\n list-style: none;\n border: 1px solid var(--pc-border);\n background: var(--pc-bar);\n}\n:host([deleting]) li button.custom {\n cursor: var(--pc-delete-cursor);\n}\n/* Create, Import and Export side by side */\n.actions {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 6px;\n}\n.visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\nli button {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n aspect-ratio: 1;\n padding: 0;\n border: none;\n border-right: 1px solid var(--pc-border);\n border-bottom: 1px solid var(--pc-border);\n background: var(--pc-bar);\n cursor: pointer;\n}\nli button:hover {\n background: var(--pc-surface-hover);\n}\nli button[aria-pressed=\"true\"] {\n background: var(--pc-surface-active);\n}\n.shape {\n background: var(--pc-text);\n border-radius: 50%;\n}\n.shape.image {\n border-radius: 0;\n mask-repeat: no-repeat;\n mask-size: 100% 100%;\n}\n";
3
3
  export { style_default as default };
@@ -613,14 +613,14 @@ var PcPanelColours = class extends PcPanel {
613
613
  <div class="actions">
614
614
  <button
615
615
  type="button"
616
- class="pill import"
616
+ class="pill-s import"
617
617
  @click=${() => this.renderRoot.querySelector(".import-file")?.click()}
618
618
  >
619
619
  ${this.t("paintCanvas.palette.import")}
620
620
  </button>
621
621
  <button
622
622
  type="button"
623
- class="pill export"
623
+ class="pill-s export"
624
624
  @click=${() => void this.exportFile()}
625
625
  >
626
626
  ${this.t("paintCanvas.palette.export")}
@@ -1,3 +1,3 @@
1
1
  // src/ui/elements/PcPanelColours/style.css?inline
2
- var style_default = ".palette {\n display: flex;\n flex-direction: column;\n gap: var(--pc-space-2);\n}\n\n/* The colours and their hex value beside the colour model's sliders */\n.top {\n display: flex;\n gap: var(--pc-space-2);\n}\n.current {\n display: flex;\n flex: none;\n flex-direction: column;\n width: 76px;\n border: 1px solid var(--pc-border);\n}\n.current pc-colour-pair {\n --pc-colour-pair-size: 76px;\n}\n.hex {\n box-sizing: border-box;\n width: 100%;\n min-height: var(--pc-control-size);\n padding: var(--pc-space-1);\n border: 0;\n border-top: 1px solid var(--pc-border);\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: var(--pc-font-size-small);\n text-align: center;\n font-variant-numeric: tabular-nums;\n}\n.hex[aria-invalid=\"true\"] {\n outline: 2px solid var(--pc-dismiss-text);\n outline-offset: -2px;\n}\n.hex:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n\n.channels {\n display: flex;\n flex: 1;\n flex-direction: column;\n min-width: 0;\n border: 1px solid var(--pc-border);\n}\n[role=\"tablist\"] {\n display: flex;\n}\n[role=\"tab\"] {\n flex: 1;\n min-height: 28px;\n border: 0;\n border-bottom: 1px solid var(--pc-border);\n background: var(--pc-surface-active);\n color: var(--pc-text);\n font: inherit;\n cursor: pointer;\n}\n[role=\"tab\"] + [role=\"tab\"] {\n border-left: 1px solid var(--pc-border);\n}\n[role=\"tab\"][aria-selected=\"true\"] {\n border-bottom-color: transparent;\n background: var(--pc-surface);\n font-weight: bold;\n}\n[role=\"tab\"]:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n.sliders {\n display: grid;\n flex: 1;\n grid-template-columns: auto minmax(0, 1fr) 3.6em;\n gap: var(--pc-space-1);\n align-items: center;\n padding: var(--pc-space-1) var(--pc-space-2);\n background: var(--pc-surface);\n}\n.letter {\n font-weight: bold;\n}\n/* Each slider's track shows the colours its values give (--track, set per slider), under a round white handle with a\n dark outline that shows on any colour */\n.sliders input[type=\"range\"] {\n width: 100%;\n min-width: 0;\n height: 20px;\n margin: 0;\n background: transparent;\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n}\n.sliders input[type=\"range\"]::-webkit-slider-runnable-track {\n height: 10px;\n border: 1px solid var(--pc-border);\n border-radius: var(--pc-radius-pill);\n background: var(--track);\n}\n.sliders input[type=\"range\"]::-moz-range-track {\n height: 10px;\n border: 1px solid var(--pc-border);\n border-radius: var(--pc-radius-pill);\n background: var(--track);\n}\n.sliders input[type=\"range\"]::-webkit-slider-thumb {\n width: 16px;\n height: 16px;\n margin-top: -4px;\n border: 2px solid #000000;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 0 0 1px #ffffff;\n -webkit-appearance: none;\n}\n.sliders input[type=\"range\"]::-moz-range-thumb {\n box-sizing: border-box;\n width: 16px;\n height: 16px;\n border: 2px solid #000000;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 0 0 1px #ffffff;\n}\n.sliders input[type=\"number\"] {\n box-sizing: border-box;\n width: 100%;\n padding: 2px var(--pc-space-1);\n border: 1px solid var(--pc-border);\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: var(--pc-font-size-small);\n font-variant-numeric: tabular-nums;\n}\n.sliders input:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: 1px;\n}\n\n/* Saved colours: square swatches in a grid that scrolls */\n.swatches {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(32px, 1fr));\n align-content: start;\n min-height: 64px;\n max-height: 132px;\n overflow-y: auto;\n border: 1px solid var(--pc-border);\n background: var(--pc-bar);\n}\n.swatch,\n.add {\n aspect-ratio: 1;\n min-width: 32px;\n padding: 0;\n border: 0;\n border-right: 1px solid var(--pc-border);\n border-bottom: 1px solid var(--pc-border);\n cursor: pointer;\n}\n.swatch {\n background: var(--colour);\n /* Long presses open the colour's menu instead of the browser's */\n -webkit-touch-callout: none;\n touch-action: manipulation;\n}\n.swatch[aria-pressed=\"true\"] {\n box-shadow:\n inset 0 0 0 2px #ffffff,\n inset 0 0 0 3px #000000;\n}\n:host([deleting]) .swatch {\n cursor: var(--pc-delete-cursor);\n}\n.add {\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: 20px;\n font-weight: bold;\n}\n.add:hover:not(:disabled) {\n background: var(--pc-surface-hover);\n}\n.add:disabled {\n color: var(--pc-disabled);\n cursor: default;\n}\n.swatch:focus-visible,\n.add:focus-visible {\n position: relative;\n z-index: 1;\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n\n.actions {\n display: flex;\n gap: var(--pc-space-2);\n}\n/* Same size as the brush menu's Import and Export */\n.actions button.pill {\n flex: 1;\n padding: 4px 8px;\n}\n";
2
+ var style_default = ".palette {\n display: flex;\n flex-direction: column;\n gap: var(--pc-space-2);\n}\n\n/* The colours and their hex value beside the colour model's sliders */\n.top {\n display: flex;\n gap: var(--pc-space-2);\n}\n.current {\n display: flex;\n flex: none;\n flex-direction: column;\n width: 76px;\n border: 1px solid var(--pc-border);\n}\n.current pc-colour-pair {\n --pc-colour-pair-size: 76px;\n}\n.hex {\n box-sizing: border-box;\n width: 100%;\n min-height: var(--pc-control-size);\n padding: var(--pc-space-1);\n border: 0;\n border-top: 1px solid var(--pc-border);\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: var(--pc-font-size-small);\n text-align: center;\n font-variant-numeric: tabular-nums;\n}\n.hex[aria-invalid=\"true\"] {\n outline: 2px solid var(--pc-dismiss-text);\n outline-offset: -2px;\n}\n.hex:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n\n.channels {\n display: flex;\n flex: 1;\n flex-direction: column;\n min-width: 0;\n border: 1px solid var(--pc-border);\n}\n[role=\"tablist\"] {\n display: flex;\n}\n[role=\"tab\"] {\n flex: 1;\n min-height: 28px;\n border: 0;\n border-bottom: 1px solid var(--pc-border);\n background: var(--pc-surface-active);\n color: var(--pc-text);\n font: inherit;\n cursor: pointer;\n}\n[role=\"tab\"] + [role=\"tab\"] {\n border-left: 1px solid var(--pc-border);\n}\n[role=\"tab\"][aria-selected=\"true\"] {\n border-bottom-color: transparent;\n background: var(--pc-surface);\n font-weight: bold;\n}\n[role=\"tab\"]:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n.sliders {\n display: grid;\n flex: 1;\n grid-template-columns: auto minmax(0, 1fr) 3.6em;\n gap: var(--pc-space-1);\n align-items: center;\n padding: var(--pc-space-1) var(--pc-space-2);\n background: var(--pc-surface);\n}\n.letter {\n font-weight: bold;\n}\n/* Each slider's track shows the colours its values give (--track, set per slider), under a round white handle with a\n dark outline that shows on any colour */\n.sliders input[type=\"range\"] {\n width: 100%;\n min-width: 0;\n height: 20px;\n margin: 0;\n background: transparent;\n appearance: none;\n -webkit-appearance: none;\n cursor: pointer;\n}\n.sliders input[type=\"range\"]::-webkit-slider-runnable-track {\n height: 10px;\n border: 1px solid var(--pc-border);\n border-radius: var(--pc-radius-pill);\n background: var(--track);\n}\n.sliders input[type=\"range\"]::-moz-range-track {\n height: 10px;\n border: 1px solid var(--pc-border);\n border-radius: var(--pc-radius-pill);\n background: var(--track);\n}\n.sliders input[type=\"range\"]::-webkit-slider-thumb {\n width: 16px;\n height: 16px;\n margin-top: -4px;\n border: 2px solid #000000;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 0 0 1px #ffffff;\n -webkit-appearance: none;\n}\n.sliders input[type=\"range\"]::-moz-range-thumb {\n box-sizing: border-box;\n width: 16px;\n height: 16px;\n border: 2px solid #000000;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 0 0 1px #ffffff;\n}\n.sliders input[type=\"number\"] {\n box-sizing: border-box;\n width: 100%;\n padding: 2px var(--pc-space-1);\n border: 1px solid var(--pc-border);\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: var(--pc-font-size-small);\n font-variant-numeric: tabular-nums;\n}\n.sliders input:focus-visible {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: 1px;\n}\n\n/* Saved colours: square swatches in a grid that scrolls */\n.swatches {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(32px, 1fr));\n align-content: start;\n min-height: 64px;\n max-height: 132px;\n overflow-y: auto;\n border: 1px solid var(--pc-border);\n background: var(--pc-bar);\n}\n.swatch,\n.add {\n aspect-ratio: 1;\n min-width: 32px;\n padding: 0;\n border: 0;\n border-right: 1px solid var(--pc-border);\n border-bottom: 1px solid var(--pc-border);\n cursor: pointer;\n}\n.swatch {\n background: var(--colour);\n /* Long presses open the colour's menu instead of the browser's */\n -webkit-touch-callout: none;\n touch-action: manipulation;\n}\n.swatch[aria-pressed=\"true\"] {\n box-shadow:\n inset 0 0 0 2px #ffffff,\n inset 0 0 0 3px #000000;\n}\n:host([deleting]) .swatch {\n cursor: var(--pc-delete-cursor);\n}\n.add {\n background: var(--pc-surface);\n color: var(--pc-text);\n font: inherit;\n font-size: 20px;\n font-weight: bold;\n}\n.add:hover:not(:disabled) {\n background: var(--pc-surface-hover);\n}\n.add:disabled {\n color: var(--pc-disabled);\n cursor: default;\n}\n.swatch:focus-visible,\n.add:focus-visible {\n position: relative;\n z-index: 1;\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: -3px;\n}\n\n.actions {\n display: flex;\n gap: var(--pc-space-2);\n}\n/* Import and Export share the row */\n.actions button.pill-s {\n flex: 1;\n}\n";
3
3
  export { style_default as default };
@@ -199,7 +199,7 @@ var PcPanelLayers = class extends PcPanel {
199
199
  ${this.renderRows(layers)}
200
200
  <button
201
201
  type="button"
202
- class="pill new ${this.drag?.action === "duplicate" ? "drop-target" : ""}"
202
+ class="pill-s new ${this.drag?.action === "duplicate" ? "drop-target" : ""}"
203
203
  ?disabled=${!layers.canCreate}
204
204
  @click=${() => layers.create()}
205
205
  >
@@ -1,3 +1,3 @@
1
1
  // src/ui/elements/PcPanelLayers/style.css?inline
2
- var style_default = "/* \"New Layer\": a full-width purple pill */\nbutton.new {\n width: 100%;\n min-height: 40px;\n}\n/* Where a dragged layer would land: a full-width block with its name, as tall as the row (set by the menu) */\n.ghost {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n padding: 0 8px;\n border: 2px dashed;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n/* Dropping a dragged layer here duplicates it (highlighted for touch, where there is no \"+\" cursor) */\nbutton.new.drop-target {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: 2px;\n}\n/* For screen readers only */\n.announcement {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n";
2
+ var style_default = "/* \"New Layer\": a full-width purple pill */\nbutton.new {\n width: 100%;\n}\n/* Where a dragged layer would land: a full-width block with its name, as tall as the row (set by the menu) */\n.ghost {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n padding: 0 8px;\n border: 2px dashed;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n/* Dropping a dragged layer here duplicates it (highlighted for touch, where there is no \"+\" cursor) */\nbutton.new.drop-target {\n outline: var(--pc-focus-width) solid var(--pc-focus);\n outline-offset: 2px;\n}\n/* For screen readers only */\n.announcement {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n";
3
3
  export { style_default as default };
@@ -0,0 +1,103 @@
1
+ import { PcElement } from "../../base";
2
+ import { type ConfirmFn, type PromptFn } from "../../ask";
3
+ import "../PcDialogWindow";
4
+ import "../PcProfileGraph";
5
+ import "../PcContextMenu";
6
+ import "../PcButton";
7
+ import ProfileGraphs from "../../../lib/classes/ProfileGraphs";
8
+ import { type ProfilePoint } from "../../../lib/profiles";
9
+ /** What the editor's `pc-save` gives: the points to use and the graph they came from (undefined for an unsaved one) */
10
+ export interface ProfileSaveDetail {
11
+ points: ProfilePoint[];
12
+ profileId?: string;
13
+ }
14
+ /**
15
+ * A window for editing a brush setting's profile graph (see `<pc-profile-graph>`): a drop-down of the presets and the
16
+ * user's own graphs (it says "Custom" once the graph is edited, or for one that isn't saved), a ⋮ menu (New graph,
17
+ * Duplicate…, Rename… and Delete…, the last two for the user's own graphs), the graph, and Save, Reset and Dismiss.
18
+ *
19
+ * - Choosing a graph in the drop-down loads its points.
20
+ * - New graph starts from a straight line, and Duplicate… saves a copy of the current graph under a name ("Custom" to
21
+ * start with); Rename… and Delete… ask first.
22
+ * - Save uses the graph (firing `pc-save` with its points and ID); an edited or unsaved graph is first saved with a name
23
+ * it asks for, asking before replacing another of the user's graphs with that name.
24
+ * - Reset goes back to the graph it opened with; Dismiss, ⨉ and Escape close it (`pc-close`) without changing anything.
25
+ *
26
+ * Graphs are saved in `profiles` (the canvas's `ProfileGraphs`).
27
+ *
28
+ * @example
29
+ * html`<pc-profile-editor .profiles=${app.profiles} .points=${points} profile-id="overweight" label="Width"
30
+ * @pc-save=${(e) => use(e.detail)} @pc-close=${close}></pc-profile-editor>`
31
+ */
32
+ export declare class PcProfileEditor extends PcElement {
33
+ static properties: {
34
+ profiles: {
35
+ attribute: boolean;
36
+ };
37
+ points: {
38
+ attribute: boolean;
39
+ };
40
+ profileId: {
41
+ type: StringConstructor;
42
+ attribute: string;
43
+ };
44
+ label: {
45
+ type: StringConstructor;
46
+ };
47
+ draft: {
48
+ state: boolean;
49
+ };
50
+ sourceId: {
51
+ state: boolean;
52
+ };
53
+ translations: {
54
+ attribute: boolean;
55
+ };
56
+ icons: {
57
+ attribute: boolean;
58
+ };
59
+ theme: {
60
+ type: StringConstructor;
61
+ reflect: boolean;
62
+ };
63
+ };
64
+ static styles: import("lit").CSSResultGroup[];
65
+ /** Where graphs are saved (the canvas's `ProfileGraphs`) */
66
+ profiles: ProfileGraphs;
67
+ /** The points it opens with */
68
+ points?: ProfilePoint[];
69
+ /** The ID of the graph it opens with, if it's one of the presets or saved graphs */
70
+ profileId?: string;
71
+ /** The setting's name, e.g. "Width" */
72
+ label: string;
73
+ /** The points being edited */
74
+ draft: ProfilePoint[];
75
+ /** The graph the points came from, until they're edited */
76
+ sourceId?: string;
77
+ /** Asks the user to confirm, in a dialog (replaceable for tests) */
78
+ confirm: ConfirmFn;
79
+ /** Asks the user for text, in a dialog (replaceable for tests) */
80
+ prompt: PromptFn;
81
+ constructor();
82
+ protected willUpdate(changed: Map<PropertyKey, unknown>): void;
83
+ /** Go back to the graph it opened with */
84
+ reset(): void;
85
+ /** Load a graph's points */
86
+ load(id: string): void;
87
+ /** Whether the graph being edited is one of the user's own, unchanged */
88
+ private get ownSource();
89
+ /**
90
+ * Ask for a name and save the points as one of the user's graphs, asking before replacing another with that name.
91
+ * Returns the saved graph's ID, or undefined if it wasn't saved.
92
+ */
93
+ private saveAs;
94
+ /**
95
+ * Use the graph: save it first when it's edited or unsaved (asking for a name), then fire `pc-save`.
96
+ */
97
+ save(): Promise<void>;
98
+ /** Do one of the ⋮ menu's actions */
99
+ act(action: string): Promise<void>;
100
+ /** A graph's name: the presets' in the chosen language */
101
+ private graphName;
102
+ render(): import("lit").TemplateResult<1>;
103
+ }
@@ -0,0 +1,259 @@
1
+ import { PcElement, defineElement, emit } from "../../base/index.js";
2
+ import { askConfirm, askText } from "../../ask/index.js";
3
+ import "../PcDialogWindow/index.js";
4
+ import "../PcProfileGraph/index.js";
5
+ import "../PcContextMenu/index.js";
6
+ import "../PcButton/index.js";
7
+ import ProfileGraphs from "../../../lib/classes/ProfileGraphs/index.js";
8
+ import { linearPoints, samePoints } from "../../../lib/profiles/index.js";
9
+ import { css, html } from "lit";
10
+ import { live } from "lit/directives/live.js";
11
+ // src/ui/elements/PcProfileEditor/index.ts
12
+ /** The value of the drop-down's "Custom" option */
13
+ var CUSTOM = "";
14
+ /**
15
+ * A window for editing a brush setting's profile graph (see `<pc-profile-graph>`): a drop-down of the presets and the
16
+ * user's own graphs (it says "Custom" once the graph is edited, or for one that isn't saved), a ⋮ menu (New graph,
17
+ * Duplicate…, Rename… and Delete…, the last two for the user's own graphs), the graph, and Save, Reset and Dismiss.
18
+ *
19
+ * - Choosing a graph in the drop-down loads its points.
20
+ * - New graph starts from a straight line, and Duplicate… saves a copy of the current graph under a name ("Custom" to
21
+ * start with); Rename… and Delete… ask first.
22
+ * - Save uses the graph (firing `pc-save` with its points and ID); an edited or unsaved graph is first saved with a name
23
+ * it asks for, asking before replacing another of the user's graphs with that name.
24
+ * - Reset goes back to the graph it opened with; Dismiss, ⨉ and Escape close it (`pc-close`) without changing anything.
25
+ *
26
+ * Graphs are saved in `profiles` (the canvas's `ProfileGraphs`).
27
+ *
28
+ * @example
29
+ * html`<pc-profile-editor .profiles=${app.profiles} .points=${points} profile-id="overweight" label="Width"
30
+ * @pc-save=${(e) => use(e.detail)} @pc-close=${close}></pc-profile-editor>`
31
+ */
32
+ var PcProfileEditor = class extends PcElement {
33
+ static properties = {
34
+ ...PcElement.properties,
35
+ profiles: { attribute: false },
36
+ points: { attribute: false },
37
+ profileId: {
38
+ type: String,
39
+ attribute: "profile-id"
40
+ },
41
+ label: { type: String },
42
+ draft: { state: true },
43
+ sourceId: { state: true }
44
+ };
45
+ static styles = [PcElement.styles, css`
46
+ :host {
47
+ display: contents;
48
+ }
49
+ pc-dialog-window {
50
+ --pc-dialog-width: 340px;
51
+ }
52
+ .choose {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: var(--pc-space-2);
56
+ width: 100%;
57
+ }
58
+ select {
59
+ flex: 1;
60
+ min-width: 0;
61
+ min-height: var(--pc-control-size);
62
+ padding: 0 var(--pc-space-2);
63
+ border: 1px solid var(--pc-border);
64
+ border-radius: var(--pc-radius-small);
65
+ background: var(--pc-surface);
66
+ color: var(--pc-text);
67
+ font: inherit;
68
+ }
69
+ .actions {
70
+ display: flex;
71
+ flex-direction: column;
72
+ gap: var(--pc-space-2);
73
+ }
74
+ `];
75
+ /** Asks the user to confirm, in a dialog (replaceable for tests) */
76
+ confirm = (options) => askConfirm(this.renderRoot, options);
77
+ /** Asks the user for text, in a dialog (replaceable for tests) */
78
+ prompt = (options) => askText(this.renderRoot, options);
79
+ constructor() {
80
+ super();
81
+ this.profiles = new ProfileGraphs();
82
+ this.label = "";
83
+ this.draft = linearPoints();
84
+ }
85
+ willUpdate(changed) {
86
+ super.willUpdate(changed);
87
+ if (changed.has("points") || changed.has("profileId")) this.reset();
88
+ }
89
+ /** Go back to the graph it opened with */
90
+ reset() {
91
+ this.draft = structuredClone(this.points ?? linearPoints());
92
+ const source = this.profiles.get(this.profileId);
93
+ this.sourceId = source && samePoints(source.points, this.draft) ? source.id : void 0;
94
+ }
95
+ /** Load a graph's points */
96
+ load(id) {
97
+ const graph = this.profiles.get(id);
98
+ if (!graph) return;
99
+ this.draft = structuredClone(graph.points);
100
+ this.sourceId = graph.id;
101
+ }
102
+ /** Whether the graph being edited is one of the user's own, unchanged */
103
+ get ownSource() {
104
+ return this.sourceId !== void 0 && !this.profiles.isPreset(this.sourceId);
105
+ }
106
+ /**
107
+ * Ask for a name and save the points as one of the user's graphs, asking before replacing another with that name.
108
+ * Returns the saved graph's ID, or undefined if it wasn't saved.
109
+ */
110
+ async saveAs(value) {
111
+ const name = (await this.prompt({
112
+ heading: this.t("paintCanvas.profileEditor.saveHeading"),
113
+ label: this.t("paintCanvas.profileEditor.name"),
114
+ value,
115
+ confirm: this.t("paintCanvas.common.save"),
116
+ cancel: this.t("paintCanvas.common.cancel")
117
+ }))?.trim();
118
+ if (!name) return void 0;
119
+ const existing = this.profiles.findByName(name);
120
+ if (existing && !await this.confirm({
121
+ heading: this.t("paintCanvas.profileEditor.replaceHeading"),
122
+ message: this.t("paintCanvas.profileEditor.replaceConfirm", { name }),
123
+ confirm: this.t("paintCanvas.presets.replace"),
124
+ cancel: this.t("paintCanvas.common.cancel"),
125
+ cancelling: true
126
+ })) return void 0;
127
+ const saved = this.profiles.save({
128
+ id: existing?.id,
129
+ name,
130
+ points: this.draft
131
+ });
132
+ if (saved) this.sourceId = saved.id;
133
+ return saved?.id;
134
+ }
135
+ /**
136
+ * Use the graph: save it first when it's edited or unsaved (asking for a name), then fire `pc-save`.
137
+ */
138
+ async save() {
139
+ const id = this.sourceId ?? await this.saveAs(this.t("paintCanvas.profileEditor.custom"));
140
+ if (!id) return;
141
+ emit(this, "pc-save", {
142
+ points: structuredClone(this.draft),
143
+ profileId: id
144
+ });
145
+ }
146
+ /** Do one of the ⋮ menu's actions */
147
+ async act(action) {
148
+ const graph = this.profiles.get(this.sourceId);
149
+ if (action === "new") {
150
+ this.draft = linearPoints();
151
+ this.sourceId = void 0;
152
+ } else if (action === "duplicate") await this.saveAs(this.t("paintCanvas.profileEditor.custom"));
153
+ else if (action === "rename" && graph && this.ownSource) {
154
+ const name = await this.prompt({
155
+ heading: this.t("paintCanvas.profileEditor.renameHeading"),
156
+ label: this.t("paintCanvas.profileEditor.name"),
157
+ value: graph.name,
158
+ confirm: this.t("paintCanvas.common.save"),
159
+ cancel: this.t("paintCanvas.common.cancel")
160
+ });
161
+ if (name && this.profiles.rename(graph.id, name)) this.requestUpdate();
162
+ } else if (action === "delete" && graph && this.ownSource) {
163
+ if (await this.confirm({
164
+ heading: this.t("paintCanvas.profileEditor.deleteHeading"),
165
+ message: this.t("paintCanvas.profileEditor.deleteConfirm", { name: graph.name }),
166
+ confirm: this.t("paintCanvas.common.delete"),
167
+ cancel: this.t("paintCanvas.common.cancel"),
168
+ cancelling: true
169
+ }) && this.profiles.remove(graph.id)) this.sourceId = void 0;
170
+ }
171
+ }
172
+ /** A graph's name: the presets' in the chosen language */
173
+ graphName(id, name) {
174
+ return this.profiles.isPreset(id) ? this.t(`paintCanvas.profiles.${id}`) : name;
175
+ }
176
+ render() {
177
+ const own = this.ownSource;
178
+ const items = [
179
+ {
180
+ value: "new",
181
+ label: this.t("paintCanvas.profileEditor.new")
182
+ },
183
+ {
184
+ value: "duplicate",
185
+ label: this.t("paintCanvas.profileEditor.duplicate")
186
+ },
187
+ {
188
+ value: "rename",
189
+ label: this.t("paintCanvas.profileEditor.rename"),
190
+ disabled: !own
191
+ },
192
+ {
193
+ value: "delete",
194
+ label: this.t("paintCanvas.profileEditor.delete"),
195
+ disabled: !own,
196
+ danger: true
197
+ }
198
+ ];
199
+ const title = this.t("paintCanvas.profileEditor.title", { param: this.label });
200
+ return html`<pc-dialog-window
201
+ open
202
+ shaded
203
+ label=${title}
204
+ @pc-dismiss=${(event) => {
205
+ event.preventDefault();
206
+ emit(this, "pc-close");
207
+ }}
208
+ >
209
+ <div slot="header-middle" class="choose">
210
+ <select
211
+ aria-label=${title}
212
+ .value=${live(this.sourceId ?? CUSTOM)}
213
+ @change=${(event) => this.load(event.target.value)}
214
+ >
215
+ ${this.sourceId === void 0 ? html`<option value=${CUSTOM}>
216
+ ${this.t("paintCanvas.profileEditor.custom")}
217
+ </option>` : ""}
218
+ ${this.profiles.all.map((graph) => html`<option
219
+ value=${graph.id}
220
+ .selected=${graph.id === this.sourceId}
221
+ >
222
+ ${this.graphName(graph.id, graph.name)}
223
+ </option>`)}
224
+ </select>
225
+ <pc-context-menu
226
+ label=${this.t("paintCanvas.profileEditor.actions")}
227
+ .items=${items}
228
+ @pc-select=${(event) => void this.act(event.detail.value)}
229
+ ></pc-context-menu>
230
+ </div>
231
+ <pc-profile-graph
232
+ slot="body"
233
+ label=${title}
234
+ .points=${this.draft}
235
+ @pc-change=${(event) => {
236
+ this.draft = event.detail.points;
237
+ this.sourceId = void 0;
238
+ }}
239
+ ></pc-profile-graph>
240
+ <div slot="footer-middle" class="actions">
241
+ <pc-button class="save" @click=${() => void this.save()}>
242
+ ${this.t("paintCanvas.common.save")}
243
+ </pc-button>
244
+ <pc-button class="reset" variant="cancel" @click=${() => this.reset()}>
245
+ ${this.t("paintCanvas.profileEditor.reset")}
246
+ </pc-button>
247
+ <pc-button
248
+ class="dismiss"
249
+ variant="dismiss"
250
+ @click=${() => emit(this, "pc-close")}
251
+ >
252
+ ${this.t("paintCanvas.common.dismiss")}
253
+ </pc-button>
254
+ </div>
255
+ </pc-dialog-window>`;
256
+ }
257
+ };
258
+ defineElement("pc-profile-editor", PcProfileEditor);
259
+ export { PcProfileEditor };
@@ -0,0 +1,84 @@
1
+ import { PcElement } from "../../base";
2
+ import { Interpolation, type ProfilePoint } from "../../../lib/profiles";
3
+ /**
4
+ * The SVG path of a curve through points, drawn as short straight pieces (y is flipped: up is less negative).
5
+ *
6
+ * @example
7
+ * curvePath(linearPoints()); // "M0,10 L0.1,9.9 …"
8
+ */
9
+ export declare const curvePath: (points: readonly ProfilePoint[]) => string;
10
+ /**
11
+ * Move a point within the grid, keeping it between its neighbours (and the first and last points at the edges), to
12
+ * multiples of 0.1. Returns new points.
13
+ *
14
+ * @example
15
+ * movePoint(points, 1, 4.04, -3); // points[1] at { x: 4, y: -3 }
16
+ */
17
+ export declare const movePoint: (points: readonly ProfilePoint[], index: number, x: number, y: number) => ProfilePoint[];
18
+ /**
19
+ * A profile graph to edit (see `@lvl99dotcom/paintcanvas/lib/profiles`): a 10 × 10 grid with the curve through its
20
+ * points, from the setting's min at the bottom left to its max at the top right.
21
+ *
22
+ * - Pressing the line adds a point there (smooth), selected.
23
+ * - Pressing a point selects it; dragging moves it (between its neighbours; the first and last stay at the edges).
24
+ * - With a point selected, the arrow keys move it a grid unit, and the buttons below make it a sharp corner (linear)
25
+ * or smooth (bezier cubic), or remove it (−; not the first or last).
26
+ *
27
+ * Points are focusable buttons (Tab moves between them; focusing one selects it). Fires `pc-change` (detail:
28
+ * `{ points }`) whenever the points change, after changing `points` itself.
29
+ *
30
+ * @example
31
+ * html`<pc-profile-graph .points=${points} @pc-change=${(e) => (points = e.detail.points)}></pc-profile-graph>`
32
+ */
33
+ export declare class PcProfileGraph extends PcElement {
34
+ static properties: {
35
+ points: {
36
+ attribute: boolean;
37
+ };
38
+ selected: {
39
+ type: NumberConstructor;
40
+ };
41
+ label: {
42
+ type: StringConstructor;
43
+ };
44
+ translations: {
45
+ attribute: boolean;
46
+ };
47
+ icons: {
48
+ attribute: boolean;
49
+ };
50
+ theme: {
51
+ type: StringConstructor;
52
+ reflect: boolean;
53
+ };
54
+ };
55
+ static styles: import("lit").CSSResultGroup[];
56
+ /** The points, from left to right */
57
+ points: ProfilePoint[];
58
+ /** The selected point's index, if any */
59
+ selected?: number;
60
+ /** The graph's name for screen readers */
61
+ label: string;
62
+ /** The point being dragged */
63
+ private dragging?;
64
+ constructor();
65
+ /** Change the points and tell listeners */
66
+ private setPoints;
67
+ /** A pointer's position in grid units */
68
+ private gridAt;
69
+ /**
70
+ * Add a point (smooth) where a press on the line is, and select it. Does nothing away from the line or when full.
71
+ */
72
+ private onGraphDown;
73
+ /** Select a point and start dragging it */
74
+ private startDrag;
75
+ private onMove;
76
+ private onUp;
77
+ /** Move the selected point a grid unit with the arrow keys */
78
+ private onPointKeyDown;
79
+ /** Make the selected point a sharp corner or smooth */
80
+ setInterpolation(i: Interpolation): void;
81
+ /** Remove the selected point (not the first or last) */
82
+ removeSelected(): void;
83
+ render(): import("lit").TemplateResult<1>;
84
+ }