@sealpixel/ui 1.0.0-rc.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 (196) hide show
  1. package/LICENSE.md +59 -0
  2. package/README.md +10 -0
  3. package/dist/components/lumbra-button.d.ts +28 -0
  4. package/dist/components/lumbra-button.d.ts.map +1 -0
  5. package/dist/components/lumbra-button.js +87 -0
  6. package/dist/components/lumbra-button.js.map +1 -0
  7. package/dist/components/lumbra-editor.d.ts +226 -0
  8. package/dist/components/lumbra-editor.d.ts.map +1 -0
  9. package/dist/components/lumbra-editor.js +873 -0
  10. package/dist/components/lumbra-editor.js.map +1 -0
  11. package/dist/components/lumbra-nav.d.ts +48 -0
  12. package/dist/components/lumbra-nav.d.ts.map +1 -0
  13. package/dist/components/lumbra-nav.js +159 -0
  14. package/dist/components/lumbra-nav.js.map +1 -0
  15. package/dist/components/lumbra-slider.d.ts +48 -0
  16. package/dist/components/lumbra-slider.d.ts.map +1 -0
  17. package/dist/components/lumbra-slider.js +168 -0
  18. package/dist/components/lumbra-slider.js.map +1 -0
  19. package/dist/components/lumbra-stage.d.ts +92 -0
  20. package/dist/components/lumbra-stage.d.ts.map +1 -0
  21. package/dist/components/lumbra-stage.js +344 -0
  22. package/dist/components/lumbra-stage.js.map +1 -0
  23. package/dist/components/lumbra-toolbar.d.ts +30 -0
  24. package/dist/components/lumbra-toolbar.d.ts.map +1 -0
  25. package/dist/components/lumbra-toolbar.js +127 -0
  26. package/dist/components/lumbra-toolbar.js.map +1 -0
  27. package/dist/components/pincel-button.d.ts +28 -0
  28. package/dist/components/pincel-button.d.ts.map +1 -0
  29. package/dist/components/pincel-button.js +87 -0
  30. package/dist/components/pincel-button.js.map +1 -0
  31. package/dist/components/pincel-editor.d.ts +195 -0
  32. package/dist/components/pincel-editor.d.ts.map +1 -0
  33. package/dist/components/pincel-editor.js +794 -0
  34. package/dist/components/pincel-editor.js.map +1 -0
  35. package/dist/components/pincel-nav.d.ts +33 -0
  36. package/dist/components/pincel-nav.d.ts.map +1 -0
  37. package/dist/components/pincel-nav.js +147 -0
  38. package/dist/components/pincel-nav.js.map +1 -0
  39. package/dist/components/pincel-slider.d.ts +48 -0
  40. package/dist/components/pincel-slider.d.ts.map +1 -0
  41. package/dist/components/pincel-slider.js +168 -0
  42. package/dist/components/pincel-slider.js.map +1 -0
  43. package/dist/components/pincel-stage.d.ts +66 -0
  44. package/dist/components/pincel-stage.d.ts.map +1 -0
  45. package/dist/components/pincel-stage.js +233 -0
  46. package/dist/components/pincel-stage.js.map +1 -0
  47. package/dist/components/pincel-toolbar.d.ts +30 -0
  48. package/dist/components/pincel-toolbar.d.ts.map +1 -0
  49. package/dist/components/pincel-toolbar.js +127 -0
  50. package/dist/components/pincel-toolbar.js.map +1 -0
  51. package/dist/components/sealpixel-button.d.ts +28 -0
  52. package/dist/components/sealpixel-button.d.ts.map +1 -0
  53. package/dist/components/sealpixel-button.js +117 -0
  54. package/dist/components/sealpixel-button.js.map +1 -0
  55. package/dist/components/sealpixel-editor.d.ts +226 -0
  56. package/dist/components/sealpixel-editor.d.ts.map +1 -0
  57. package/dist/components/sealpixel-editor.js +1245 -0
  58. package/dist/components/sealpixel-editor.js.map +1 -0
  59. package/dist/components/sealpixel-nav.d.ts +48 -0
  60. package/dist/components/sealpixel-nav.d.ts.map +1 -0
  61. package/dist/components/sealpixel-nav.js +159 -0
  62. package/dist/components/sealpixel-nav.js.map +1 -0
  63. package/dist/components/sealpixel-slider.d.ts +75 -0
  64. package/dist/components/sealpixel-slider.d.ts.map +1 -0
  65. package/dist/components/sealpixel-slider.js +269 -0
  66. package/dist/components/sealpixel-slider.js.map +1 -0
  67. package/dist/components/sealpixel-stage.d.ts +118 -0
  68. package/dist/components/sealpixel-stage.d.ts.map +1 -0
  69. package/dist/components/sealpixel-stage.js +409 -0
  70. package/dist/components/sealpixel-stage.js.map +1 -0
  71. package/dist/components/sealpixel-stepper.d.ts +69 -0
  72. package/dist/components/sealpixel-stepper.d.ts.map +1 -0
  73. package/dist/components/sealpixel-stepper.js +248 -0
  74. package/dist/components/sealpixel-stepper.js.map +1 -0
  75. package/dist/components/sealpixel-toolbar.d.ts +30 -0
  76. package/dist/components/sealpixel-toolbar.d.ts.map +1 -0
  77. package/dist/components/sealpixel-toolbar.js +149 -0
  78. package/dist/components/sealpixel-toolbar.js.map +1 -0
  79. package/dist/controllers/gesture-controller.d.ts +65 -0
  80. package/dist/controllers/gesture-controller.d.ts.map +1 -0
  81. package/dist/controllers/gesture-controller.js +125 -0
  82. package/dist/controllers/gesture-controller.js.map +1 -0
  83. package/dist/controllers/session-controller.d.ts +15 -0
  84. package/dist/controllers/session-controller.d.ts.map +1 -0
  85. package/dist/controllers/session-controller.js +37 -0
  86. package/dist/controllers/session-controller.js.map +1 -0
  87. package/dist/define.d.ts +21 -0
  88. package/dist/define.d.ts.map +1 -0
  89. package/dist/define.js +27 -0
  90. package/dist/define.js.map +1 -0
  91. package/dist/i18n.d.ts +11 -0
  92. package/dist/i18n.d.ts.map +1 -0
  93. package/dist/i18n.js +27 -0
  94. package/dist/i18n.js.map +1 -0
  95. package/dist/icons.d.ts +32 -0
  96. package/dist/icons.d.ts.map +1 -0
  97. package/dist/icons.js +45 -0
  98. package/dist/icons.js.map +1 -0
  99. package/dist/index.d.ts +18 -0
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.js +17 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/locales/de.d.ts +3 -0
  104. package/dist/locales/de.d.ts.map +1 -0
  105. package/dist/locales/de.js +132 -0
  106. package/dist/locales/de.js.map +1 -0
  107. package/dist/locales/en.d.ts +3 -0
  108. package/dist/locales/en.d.ts.map +1 -0
  109. package/dist/locales/en.js +132 -0
  110. package/dist/locales/en.js.map +1 -0
  111. package/dist/locales/es.d.ts +3 -0
  112. package/dist/locales/es.d.ts.map +1 -0
  113. package/dist/locales/es.js +132 -0
  114. package/dist/locales/es.js.map +1 -0
  115. package/dist/locales/fr.d.ts +3 -0
  116. package/dist/locales/fr.d.ts.map +1 -0
  117. package/dist/locales/fr.js +132 -0
  118. package/dist/locales/fr.js.map +1 -0
  119. package/dist/locales/index.d.ts +6 -0
  120. package/dist/locales/index.d.ts.map +1 -0
  121. package/dist/locales/index.js +13 -0
  122. package/dist/locales/index.js.map +1 -0
  123. package/dist/locales/ja.d.ts +3 -0
  124. package/dist/locales/ja.d.ts.map +1 -0
  125. package/dist/locales/ja.js +132 -0
  126. package/dist/locales/ja.js.map +1 -0
  127. package/dist/locales/pt.d.ts +3 -0
  128. package/dist/locales/pt.d.ts.map +1 -0
  129. package/dist/locales/pt.js +132 -0
  130. package/dist/locales/pt.js.map +1 -0
  131. package/dist/theme.d.ts +19 -0
  132. package/dist/theme.d.ts.map +1 -0
  133. package/dist/theme.js +150 -0
  134. package/dist/theme.js.map +1 -0
  135. package/dist/tool.d.ts +70 -0
  136. package/dist/tool.d.ts.map +1 -0
  137. package/dist/tool.js +2 -0
  138. package/dist/tool.js.map +1 -0
  139. package/dist/tools/annotate.d.ts +3 -0
  140. package/dist/tools/annotate.d.ts.map +1 -0
  141. package/dist/tools/annotate.js +436 -0
  142. package/dist/tools/annotate.js.map +1 -0
  143. package/dist/tools/crop-mask.d.ts +29 -0
  144. package/dist/tools/crop-mask.d.ts.map +1 -0
  145. package/dist/tools/crop-mask.js +75 -0
  146. package/dist/tools/crop-mask.js.map +1 -0
  147. package/dist/tools/crop.d.ts +11 -0
  148. package/dist/tools/crop.d.ts.map +1 -0
  149. package/dist/tools/crop.js +361 -0
  150. package/dist/tools/crop.js.map +1 -0
  151. package/dist/tools/delete-handle.d.ts +18 -0
  152. package/dist/tools/delete-handle.d.ts.map +1 -0
  153. package/dist/tools/delete-handle.js +61 -0
  154. package/dist/tools/delete-handle.js.map +1 -0
  155. package/dist/tools/filter.d.ts +3 -0
  156. package/dist/tools/filter.d.ts.map +1 -0
  157. package/dist/tools/filter.js +174 -0
  158. package/dist/tools/filter.js.map +1 -0
  159. package/dist/tools/finetune.d.ts +11 -0
  160. package/dist/tools/finetune.d.ts.map +1 -0
  161. package/dist/tools/finetune.js +98 -0
  162. package/dist/tools/finetune.js.map +1 -0
  163. package/dist/tools/frame.d.ts +3 -0
  164. package/dist/tools/frame.d.ts.map +1 -0
  165. package/dist/tools/frame.js +57 -0
  166. package/dist/tools/frame.js.map +1 -0
  167. package/dist/tools/index.d.ts +14 -0
  168. package/dist/tools/index.d.ts.map +1 -0
  169. package/dist/tools/index.js +41 -0
  170. package/dist/tools/index.js.map +1 -0
  171. package/dist/tools/redact.d.ts +3 -0
  172. package/dist/tools/redact.d.ts.map +1 -0
  173. package/dist/tools/redact.js +252 -0
  174. package/dist/tools/redact.js.map +1 -0
  175. package/dist/tools/resize.d.ts +3 -0
  176. package/dist/tools/resize.d.ts.map +1 -0
  177. package/dist/tools/resize.js +91 -0
  178. package/dist/tools/resize.js.map +1 -0
  179. package/dist/tools/sticker.d.ts +8 -0
  180. package/dist/tools/sticker.d.ts.map +1 -0
  181. package/dist/tools/sticker.js +156 -0
  182. package/dist/tools/sticker.js.map +1 -0
  183. package/dist/tools/watermark.d.ts +3 -0
  184. package/dist/tools/watermark.d.ts.map +1 -0
  185. package/dist/tools/watermark.js +102 -0
  186. package/dist/tools/watermark.js.map +1 -0
  187. package/dist/view-mapping.d.ts +23 -0
  188. package/dist/view-mapping.d.ts.map +1 -0
  189. package/dist/view-mapping.js +72 -0
  190. package/dist/view-mapping.js.map +1 -0
  191. package/package.json +55 -0
  192. package/themes/default.css +23 -0
  193. package/themes/high-contrast.css +30 -0
  194. package/themes/minimal.css +32 -0
  195. package/themes/nocturne.css +21 -0
  196. package/themes/studio.css +23 -0
@@ -0,0 +1,794 @@
1
+ import { canonicalJson, createAssetResolver, createEditorSession, } from '@pincel/core';
2
+ import { defaultPlugins } from '@pincel/plugins';
3
+ import { css, html, LitElement, nothing } from 'lit';
4
+ import { SessionController } from '../controllers/session-controller.js';
5
+ import { createI18n } from '../i18n.js';
6
+ import { resolveLocale } from '../locales/index.js';
7
+ import { base, tokens } from '../theme.js';
8
+ import { toolsFor } from '../tools/index.js';
9
+ /**
10
+ * The default editor. Headless `EditorSession` underneath; this element adds
11
+ * the responsive shell, tools and events.
12
+ *
13
+ * Events: `pincel:ready`, `pincel:load`, `pincel:change` ({ log, index }),
14
+ * `pincel:process` (ExportResult), `pincel:error` (PincelError).
15
+ */
16
+ export class PincelEditor extends LitElement {
17
+ static { this.styles = [
18
+ tokens,
19
+ base,
20
+ css `
21
+ :host {
22
+ display: block;
23
+ container-type: inline-size;
24
+ background: var(--pincel-color-bg);
25
+ color: var(--pincel-color-fg);
26
+ height: 100%;
27
+ min-height: 320px;
28
+ min-width: 0;
29
+ overflow: hidden;
30
+ }
31
+ :host(:focus) {
32
+ outline: none;
33
+ }
34
+ .root {
35
+ position: relative;
36
+ display: grid;
37
+ height: 100%;
38
+ min-width: 0;
39
+ grid-template:
40
+ 'nav' auto
41
+ 'stage' 1fr
42
+ 'panel' auto
43
+ 'toolbar' auto / 1fr;
44
+ padding-bottom: env(safe-area-inset-bottom);
45
+ }
46
+ .root > * {
47
+ min-width: 0;
48
+ min-height: 0;
49
+ }
50
+ pincel-stage {
51
+ grid-area: stage;
52
+ min-height: 160px;
53
+ }
54
+ pincel-nav {
55
+ grid-area: nav;
56
+ }
57
+ pincel-toolbar {
58
+ grid-area: toolbar;
59
+ }
60
+ .panel {
61
+ grid-area: panel;
62
+ background: var(--pincel-color-surface);
63
+ border-top: 1px solid var(--pincel-color-border);
64
+ padding: var(--pincel-space) calc(var(--pincel-space) * 1.5);
65
+ max-height: 34cqh;
66
+ overflow: auto;
67
+ display: grid;
68
+ gap: var(--pincel-space);
69
+ align-content: start;
70
+ scrollbar-width: thin;
71
+ }
72
+ /* Phones: option grids become one horizontal strip so the stage keeps its height. */
73
+ .panel .grid,
74
+ .panel .strip {
75
+ display: flex;
76
+ gap: var(--pincel-space);
77
+ overflow-x: auto;
78
+ scrollbar-width: none;
79
+ padding-bottom: 2px;
80
+ scroll-snap-type: x proximity;
81
+ }
82
+ .panel .grid::-webkit-scrollbar,
83
+ .panel .strip::-webkit-scrollbar {
84
+ display: none;
85
+ }
86
+ .panel .grid > *,
87
+ .panel .strip > * {
88
+ flex: 0 0 auto;
89
+ min-width: 96px;
90
+ scroll-snap-align: start;
91
+ }
92
+ .panel .strip > * {
93
+ min-width: 0;
94
+ }
95
+ .panel .row.compact > pincel-button {
96
+ flex: 1 1 0;
97
+ }
98
+ /* Phones: sibling groups share one line and hints make room for the stage. */
99
+ .panel .rows {
100
+ display: flex;
101
+ gap: var(--pincel-space);
102
+ align-items: end;
103
+ }
104
+ .panel .rows > * {
105
+ flex: 1 1 0;
106
+ min-width: 0;
107
+ }
108
+ .panel .rows .row {
109
+ flex-wrap: nowrap;
110
+ }
111
+ .panel .rows .row > pincel-button {
112
+ flex: 1 1 0;
113
+ min-width: 0;
114
+ }
115
+ .panel .rows select,
116
+ .panel .rows input:not([type='checkbox']):not([type='color']) {
117
+ width: 100%;
118
+ min-width: 0;
119
+ }
120
+ .panel .rows > .field:has(input[type='color']),
121
+ .panel .rows > .field.check {
122
+ flex: 0 0 auto;
123
+ }
124
+ .panel .rows > .field:has(input[type='color']) input {
125
+ width: var(--pincel-touch-target);
126
+ }
127
+ .panel .hint {
128
+ display: none;
129
+ }
130
+ .panel:empty {
131
+ display: none;
132
+ }
133
+ .toast {
134
+ position: absolute;
135
+ left: 50%;
136
+ bottom: calc(var(--pincel-toolbar-size) + var(--pincel-space) * 2);
137
+ transform: translateX(-50%);
138
+ display: flex;
139
+ align-items: center;
140
+ gap: var(--pincel-space);
141
+ max-width: calc(100% - var(--pincel-space) * 4);
142
+ padding: calc(var(--pincel-space) * 1.25) calc(var(--pincel-space) * 2);
143
+ border-radius: 999px;
144
+ background: color-mix(in srgb, var(--pincel-color-surface) 92%, transparent);
145
+ border: 1px solid var(--pincel-color-border);
146
+ box-shadow: var(--pincel-shadow, 0 8px 24px rgba(0, 0, 0, 0.3));
147
+ color: var(--pincel-color-fg);
148
+ font-size: 13px;
149
+ white-space: nowrap;
150
+ backdrop-filter: blur(8px);
151
+ cursor: pointer;
152
+ z-index: 2;
153
+ animation: pincel-toast-in var(--pincel-transition) both;
154
+ }
155
+ .toast code {
156
+ font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
157
+ color: var(--pincel-color-fg-muted);
158
+ padding: 2px 8px;
159
+ border-radius: 999px;
160
+ background: var(--pincel-color-surface-2);
161
+ }
162
+ @keyframes pincel-toast-in {
163
+ from {
164
+ opacity: 0;
165
+ transform: translate(-50%, 8px);
166
+ }
167
+ }
168
+ @container (min-width: 48rem) {
169
+ .root {
170
+ grid-template:
171
+ 'nav nav nav' auto
172
+ 'toolbar stage panel' 1fr / var(--pincel-toolbar-size) 1fr var(--pincel-panel-size);
173
+ }
174
+ .panel {
175
+ border-top: none;
176
+ border-left: 1px solid var(--pincel-color-border);
177
+ max-height: none;
178
+ padding: calc(var(--pincel-space) * 1.5) calc(var(--pincel-space) * 2);
179
+ gap: calc(var(--pincel-space) * 1.25);
180
+ }
181
+ .panel .grid {
182
+ display: grid;
183
+ grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
184
+ overflow: visible;
185
+ }
186
+ .panel .grid > * {
187
+ min-width: 0;
188
+ }
189
+ .panel .rows {
190
+ display: grid;
191
+ gap: calc(var(--pincel-space) * 1.25);
192
+ }
193
+ .panel .rows > .field:has(input[type='color']) input {
194
+ width: 100%;
195
+ }
196
+ .panel .hint {
197
+ display: block;
198
+ }
199
+ .toast {
200
+ left: calc(var(--pincel-toolbar-size) + (100% - var(--pincel-toolbar-size) - var(--pincel-panel-size)) / 2);
201
+ bottom: calc(var(--pincel-space) * 2);
202
+ }
203
+ }
204
+ .panel .row {
205
+ display: flex;
206
+ gap: var(--pincel-space);
207
+ flex-wrap: wrap;
208
+ }
209
+ .panel .field {
210
+ display: grid;
211
+ gap: 4px;
212
+ color: var(--pincel-color-fg-muted);
213
+ }
214
+ .panel .hint {
215
+ margin: 0;
216
+ color: var(--pincel-color-fg-muted);
217
+ font-size: 12px;
218
+ }
219
+ .panel .list {
220
+ list-style: none;
221
+ margin: 0;
222
+ padding: 0;
223
+ display: grid;
224
+ gap: 4px;
225
+ }
226
+ .panel .list li {
227
+ display: flex;
228
+ gap: 4px;
229
+ align-items: center;
230
+ }
231
+ .panel .list li > pincel-button:first-child {
232
+ flex: 1;
233
+ }
234
+ .panel .anchors {
235
+ display: grid;
236
+ grid-template-columns: repeat(3, 1fr);
237
+ gap: 4px;
238
+ max-width: 180px;
239
+ }
240
+ .panel .field.check {
241
+ grid-template-columns: auto 1fr;
242
+ align-items: center;
243
+ }
244
+ .panel textarea {
245
+ font: inherit;
246
+ color: var(--pincel-color-fg);
247
+ background: var(--pincel-color-surface-2);
248
+ border: 1px solid var(--pincel-color-border);
249
+ border-radius: var(--pincel-radius-sm);
250
+ padding: var(--pincel-space);
251
+ resize: vertical;
252
+ }
253
+ .panel input[type='color'] {
254
+ min-height: var(--pincel-touch-target);
255
+ padding: 2px;
256
+ width: 100%;
257
+ }
258
+ .panel input:not([type='checkbox']),
259
+ .panel select {
260
+ font: inherit;
261
+ color: var(--pincel-color-fg);
262
+ background: var(--pincel-color-surface-2);
263
+ border: 1px solid var(--pincel-color-border);
264
+ border-radius: var(--pincel-radius-sm);
265
+ min-height: var(--pincel-touch-target);
266
+ padding: 0 var(--pincel-space);
267
+ }
268
+ .empty {
269
+ grid-area: stage;
270
+ display: grid;
271
+ place-content: center;
272
+ gap: var(--pincel-space);
273
+ text-align: center;
274
+ color: var(--pincel-color-fg-muted);
275
+ padding: var(--pincel-space);
276
+ }
277
+ .empty.dragover {
278
+ outline: 2px dashed var(--pincel-color-accent);
279
+ outline-offset: -8px;
280
+ }
281
+ .error {
282
+ grid-area: nav;
283
+ align-self: end;
284
+ justify-self: center;
285
+ margin-top: calc(var(--pincel-toolbar-size) + 4px);
286
+ z-index: 1;
287
+ padding: 6px 10px;
288
+ border-radius: var(--pincel-radius);
289
+ background: var(--pincel-color-danger);
290
+ color: white;
291
+ font-size: 12px;
292
+ }
293
+ `,
294
+ ]; }
295
+ static { this.properties = {
296
+ plugins: { attribute: false },
297
+ session: { attribute: false },
298
+ renderer: { attribute: false },
299
+ assets: { attribute: false },
300
+ server: { attribute: false },
301
+ serverEndpoint: { type: String, attribute: 'server-endpoint' },
302
+ src: { attribute: false },
303
+ editLog: { attribute: false },
304
+ defaultEditLog: { attribute: false },
305
+ output: { attribute: false },
306
+ theme: { type: String, reflect: true },
307
+ locale: { type: String },
308
+ dictionary: { attribute: false },
309
+ tools: { attribute: false },
310
+ activeTool: { type: String, attribute: 'active-tool' },
311
+ showExport: { type: String, attribute: 'show-export' },
312
+ _busy: { state: true },
313
+ _processListeners: { state: true },
314
+ _toast: { state: true },
315
+ _dragover: { state: true },
316
+ _wide: { state: true },
317
+ _pluginTools: { state: true },
318
+ }; }
319
+ /**
320
+ * Used when no `renderer` property is set. `@pincel/element` installs a
321
+ * factory that starts the bundled worker; without it the editor renders inline.
322
+ */
323
+ static { this.defaultRendererFactory = null; }
324
+ constructor() {
325
+ super();
326
+ this.resizeObserver = null;
327
+ this.toolLoads = new Set();
328
+ this.ctl = new SessionController(this, null);
329
+ this.i18n = createI18n('en');
330
+ this.ownsSession = false;
331
+ this.lastEmittedVersion = -1;
332
+ this.lastAppliedControlled = '';
333
+ this.hint = null;
334
+ this.previousTool = null;
335
+ this.loadedSrc = null;
336
+ this.lastError = null;
337
+ this.sessionRenderer = null;
338
+ this.sessionAssets = null;
339
+ this.sessionServer = null;
340
+ this.onPointerDown = () => {
341
+ if (!this.matches(':focus-within'))
342
+ this.focus({ preventScroll: true });
343
+ };
344
+ this.onKeydown = (e) => {
345
+ const session = this.ctl.session;
346
+ if (!session || session.getState().status !== 'ready')
347
+ return;
348
+ const mod = e.metaKey || e.ctrlKey;
349
+ if (mod && e.key.toLowerCase() === 'z') {
350
+ e.preventDefault();
351
+ if (e.shiftKey)
352
+ session.redo();
353
+ else
354
+ session.undo();
355
+ }
356
+ else if (mod && e.key.toLowerCase() === 'y') {
357
+ e.preventDefault();
358
+ session.redo();
359
+ }
360
+ };
361
+ this.toastTimer = null;
362
+ this.plugins = null;
363
+ this.session = null;
364
+ this.renderer = null;
365
+ this.assets = null;
366
+ this.server = null;
367
+ this.serverEndpoint = null;
368
+ this.src = null;
369
+ this.editLog = undefined;
370
+ this.defaultEditLog = undefined;
371
+ this.output = { format: 'image/png' };
372
+ this.theme = 'dark';
373
+ this.locale = 'en';
374
+ this.dictionary = null;
375
+ this.tools = null;
376
+ this.activeTool = null;
377
+ this.showExport = 'auto';
378
+ this._busy = false;
379
+ this._processListeners = 0;
380
+ this._toast = null;
381
+ this._dragover = false;
382
+ this._wide = false;
383
+ this._pluginTools = {};
384
+ }
385
+ addEventListener(type, listener, options) {
386
+ super.addEventListener(type, listener, options);
387
+ if (type === 'pincel:process')
388
+ this._processListeners += 1;
389
+ }
390
+ removeEventListener(type, listener, options) {
391
+ super.removeEventListener(type, listener, options);
392
+ if (type === 'pincel:process' && this._processListeners > 0)
393
+ this._processListeners -= 1;
394
+ }
395
+ /** The live session, created on first connection when none was passed. */
396
+ getSession() {
397
+ if (!this.ctl.session)
398
+ this.ensureSession();
399
+ return this.ctl.session;
400
+ }
401
+ getEditLog() {
402
+ return this.getSession().getEditLog();
403
+ }
404
+ setEditLog(saved, index) {
405
+ this.getSession().setEditLog(saved, index);
406
+ }
407
+ undo() {
408
+ this.getSession().undo();
409
+ }
410
+ redo() {
411
+ this.getSession().redo();
412
+ }
413
+ timeTravel(index) {
414
+ this.getSession().timeTravel(index);
415
+ }
416
+ async export(output = this.output, options = {}) {
417
+ return this.getSession().export({ output, ...options });
418
+ }
419
+ async download(options = {}) {
420
+ return this.getSession().download({ output: this.output, ...options });
421
+ }
422
+ ensureSession() {
423
+ if (this.ctl.session)
424
+ return;
425
+ if (this.session) {
426
+ this.ctl.session = this.session;
427
+ return;
428
+ }
429
+ const plugins = this.plugins ?? defaultPlugins;
430
+ const renderer = this.renderer ?? PincelEditor.defaultRendererFactory?.(plugins) ?? null;
431
+ this.sessionRenderer = renderer;
432
+ const assets = this.assets ?? createAssetResolver({ assets: plugins.flatMap((p) => p.assets ?? []) });
433
+ const server = this.server ?? (this.serverEndpoint ? { endpoint: this.serverEndpoint } : null);
434
+ this.sessionServer = server;
435
+ this.sessionAssets = this.assets;
436
+ const session = createEditorSession({
437
+ plugins,
438
+ assets,
439
+ ...(renderer ? { renderer } : {}),
440
+ ...(server ? { server } : {}),
441
+ });
442
+ this.ownsSession = true;
443
+ this.ctl.session = session;
444
+ this.dispatchEvent(new CustomEvent('pincel:ready', { bubbles: true, composed: true }));
445
+ }
446
+ connectedCallback() {
447
+ super.connectedCallback();
448
+ this.ensureSession();
449
+ // Keyboard shortcuts need focus inside the editor. WebKit does not focus
450
+ // buttons on click, so pull focus in on any pointer interaction.
451
+ if (!this.hasAttribute('tabindex'))
452
+ this.tabIndex = -1;
453
+ this.addEventListener('keydown', this.onKeydown);
454
+ this.addEventListener('pointerdown', this.onPointerDown);
455
+ this.resizeObserver = new ResizeObserver((entries) => {
456
+ const width = entries[0]?.contentRect.width ?? this.clientWidth;
457
+ this._wide = width >= 768;
458
+ });
459
+ this.resizeObserver.observe(this);
460
+ }
461
+ disconnectedCallback() {
462
+ super.disconnectedCallback();
463
+ this.removeEventListener('keydown', this.onKeydown);
464
+ this.removeEventListener('pointerdown', this.onPointerDown);
465
+ this.resizeObserver?.disconnect();
466
+ if (this.ownsSession) {
467
+ this.ctl.session?.dispose();
468
+ this.ctl.session = null;
469
+ this.ownsSession = false;
470
+ }
471
+ }
472
+ willUpdate(changed) {
473
+ if (changed.has('locale') || changed.has('dictionary')) {
474
+ this.i18n = createI18n(this.locale, this.dictionary ?? resolveLocale(this.locale));
475
+ }
476
+ if (changed.has('session') && this.session && this.session !== this.ctl.session) {
477
+ if (this.ownsSession)
478
+ this.ctl.session?.dispose();
479
+ this.ownsSession = false;
480
+ this.ctl.session = this.session;
481
+ }
482
+ // A new plugin list or renderer needs a new session; reload the current source into it.
483
+ if ((changed.has('plugins') ||
484
+ changed.has('renderer') ||
485
+ changed.has('assets') ||
486
+ changed.has('server') ||
487
+ changed.has('serverEndpoint')) &&
488
+ this.ownsSession &&
489
+ !this.sessionMatchesProps()) {
490
+ this.ctl.session?.dispose();
491
+ this.ctl.session = null;
492
+ this.ownsSession = false;
493
+ this.toolLoads.clear();
494
+ this._pluginTools = {};
495
+ this.lastEmittedVersion = -1;
496
+ this.ensureSession();
497
+ if (this.loadedSrc)
498
+ void this.loadSource(this.loadedSrc);
499
+ }
500
+ if (changed.has('src') && this.src && this.src !== this.loadedSrc) {
501
+ this.loadedSrc = this.src;
502
+ void this.loadSource(this.src);
503
+ }
504
+ if (changed.has('editLog') && this.editLog !== undefined)
505
+ this.applyControlled();
506
+ }
507
+ updated() {
508
+ const state = this.ctl.state;
509
+ const session = this.ctl.session;
510
+ if (!state || !session)
511
+ return;
512
+ if (state.version !== this.lastEmittedVersion && state.status === 'ready') {
513
+ this.lastEmittedVersion = state.version;
514
+ const detail = session.getEditLog();
515
+ this.lastAppliedControlled = canonicalJson(detail);
516
+ this.dispatchEvent(new CustomEvent('pincel:change', { detail, bubbles: true, composed: true }));
517
+ }
518
+ if (state.error && state.error !== this.lastError) {
519
+ this.lastError = state.error;
520
+ this.dispatchEvent(new CustomEvent('pincel:error', { detail: state.error, bubbles: true, composed: true }));
521
+ }
522
+ this.syncActiveTool();
523
+ }
524
+ async loadSource(src) {
525
+ const session = this.getSession();
526
+ try {
527
+ const saved = this.editLog !== undefined ? this.editLog : this.defaultEditLog;
528
+ const bytes = src instanceof Uint8Array ? src : src;
529
+ const descriptor = await session.load(bytes, saved === undefined ? {} : { editLog: saved });
530
+ this.dispatchEvent(new CustomEvent('pincel:load', { detail: descriptor, bubbles: true, composed: true }));
531
+ if (!this.activeTool) {
532
+ const first = this.currentTools()[0];
533
+ if (first)
534
+ this.activeTool = first.id;
535
+ }
536
+ }
537
+ catch (error) {
538
+ this.dispatchEvent(new CustomEvent('pincel:error', { detail: error, bubbles: true, composed: true }));
539
+ }
540
+ }
541
+ applyControlled() {
542
+ const session = this.ctl.session;
543
+ if (!session || session.getState().status !== 'ready' || this.editLog === undefined)
544
+ return;
545
+ const wanted = this.editLog;
546
+ if (wanted === null)
547
+ return;
548
+ const key = canonicalJson(wanted);
549
+ if (key === this.lastAppliedControlled)
550
+ return;
551
+ try {
552
+ session.setEditLog(wanted, wanted.index);
553
+ this.lastAppliedControlled = key;
554
+ }
555
+ catch (error) {
556
+ this.dispatchEvent(new CustomEvent('pincel:error', { detail: error, bubbles: true, composed: true }));
557
+ }
558
+ }
559
+ /** Whether the owned session was built from the current `plugins` and `renderer`. */
560
+ sessionMatchesProps() {
561
+ const session = this.ctl.session;
562
+ if (!session)
563
+ return false;
564
+ if (this.renderer && this.renderer !== this.sessionRenderer)
565
+ return false;
566
+ if (this.assets && this.assets !== this.sessionAssets)
567
+ return false;
568
+ const wantServer = this.server ?? (this.serverEndpoint ? { endpoint: this.serverEndpoint } : null);
569
+ if (JSON.stringify(wantServer?.endpoint ?? null) !==
570
+ JSON.stringify(this.sessionServer?.endpoint ?? null))
571
+ return false;
572
+ if (wantServer !== this.sessionServer && wantServer?.fetch !== this.sessionServer?.fetch)
573
+ return false;
574
+ const want = (this.plugins ?? defaultPlugins).map((p) => p.id).join(',');
575
+ const have = session.registry.plugins
576
+ .map((p) => p.id)
577
+ .sort()
578
+ .join(',');
579
+ return want.split(',').sort().join(',') === have;
580
+ }
581
+ currentTools() {
582
+ const plugins = this.plugins ?? defaultPlugins;
583
+ this.loadPluginTools(plugins);
584
+ return toolsFor(plugins, { ...this._pluginTools, ...(this.tools ?? {}) });
585
+ }
586
+ /** Plugins may ship their own tool; load each once and re-render when it arrives. */
587
+ loadPluginTools(plugins) {
588
+ for (const plugin of plugins) {
589
+ if (!plugin.tool || this.toolLoads.has(plugin.id))
590
+ continue;
591
+ this.toolLoads.add(plugin.id);
592
+ plugin
593
+ .tool()
594
+ .then((tool) => {
595
+ this._pluginTools = { ...this._pluginTools, [plugin.id]: tool };
596
+ })
597
+ .catch((error) => {
598
+ this.dispatchEvent(new CustomEvent('pincel:error', { detail: error, bubbles: true, composed: true }));
599
+ });
600
+ }
601
+ }
602
+ get stage() {
603
+ return this.renderRoot.querySelector('pincel-stage');
604
+ }
605
+ toolContext(state) {
606
+ const session = this.getSession();
607
+ const stage = this.stage;
608
+ return {
609
+ session,
610
+ state,
611
+ document: state.document,
612
+ dispatch: (op) => session.dispatch(op),
613
+ t: this.i18n.t,
614
+ stage: {
615
+ width: state.preview?.image.width ?? 0,
616
+ height: state.preview?.image.height ?? 0,
617
+ zoom: stage?.zoom ?? 1,
618
+ geometry: state.preview?.geometry ?? null,
619
+ toImage: (x, y) => stage?.toImage(x, y) ?? { x: 0, y: 0 },
620
+ },
621
+ hint: (hint) => {
622
+ this.hint = hint;
623
+ const s = this.stage;
624
+ if (s) {
625
+ s.hintFilter = hint?.filter ?? '';
626
+ s.hintTransform = hint?.transform ?? '';
627
+ }
628
+ },
629
+ refresh: () => this.requestUpdate(),
630
+ layout: this._wide ? 'wide' : 'narrow',
631
+ };
632
+ }
633
+ syncActiveTool() {
634
+ const state = this.ctl.state;
635
+ if (!state || state.status !== 'ready')
636
+ return;
637
+ const tool = this.currentTools().find((t) => t.id === this.activeTool) ?? null;
638
+ if (tool === this.previousTool)
639
+ return;
640
+ const ctx = this.toolContext(state);
641
+ this.previousTool?.deactivate?.(ctx);
642
+ tool?.activate?.(ctx);
643
+ this.previousTool = tool;
644
+ }
645
+ async onExport() {
646
+ const session = this.getSession();
647
+ this._busy = true;
648
+ try {
649
+ const result = await session.export({ output: this.output });
650
+ this.dispatchEvent(new CustomEvent('pincel:process', { detail: result, bubbles: true, composed: true }));
651
+ this.toast('toast.exported', result);
652
+ }
653
+ catch (error) {
654
+ this.dispatchEvent(new CustomEvent('pincel:error', { detail: error, bubbles: true, composed: true }));
655
+ }
656
+ finally {
657
+ this._busy = false;
658
+ }
659
+ }
660
+ async onDownload() {
661
+ const session = this.getSession();
662
+ this._busy = true;
663
+ try {
664
+ const { result } = await session.download({ output: this.output });
665
+ this.dispatchEvent(new CustomEvent('pincel:process', { detail: result, bubbles: true, composed: true }));
666
+ this.toast('toast.downloaded', result);
667
+ }
668
+ catch (error) {
669
+ this.dispatchEvent(new CustomEvent('pincel:error', { detail: error, bubbles: true, composed: true }));
670
+ }
671
+ finally {
672
+ this._busy = false;
673
+ }
674
+ }
675
+ announce(text) {
676
+ const live = this.renderRoot.querySelector('#live');
677
+ if (live)
678
+ live.textContent = text;
679
+ }
680
+ /** Brief confirmation over the stage: format, size and hash of the result. */
681
+ toast(key, result) {
682
+ const format = result.mimeType.replace('image/', '').toUpperCase();
683
+ const size = result.bytes ? formatBytes(result.bytes.byteLength) : 'server';
684
+ const text = this.i18n.t(key, { format, width: result.width, height: result.height, size });
685
+ this._toast = { text, hash: result.hash.slice(0, 12) };
686
+ this.announce(text);
687
+ if (this.toastTimer)
688
+ clearTimeout(this.toastTimer);
689
+ this.toastTimer = setTimeout(() => {
690
+ this._toast = null;
691
+ this.toastTimer = null;
692
+ }, 5000);
693
+ }
694
+ onFiles(files) {
695
+ const file = files?.[0];
696
+ if (file)
697
+ this.src = file;
698
+ }
699
+ render() {
700
+ const state = this.ctl.state;
701
+ const tools = this.currentTools();
702
+ const tool = tools.find((t) => t.id === this.activeTool) ?? null;
703
+ const ready = state?.status === 'ready';
704
+ const ctx = state && ready ? this.toolContext(state) : null;
705
+ const wide = this._wide;
706
+ return html `
707
+ <div class="root" part="root">
708
+ <pincel-nav
709
+ part="nav"
710
+ exportparts="undo, redo, history, export, download, dimensions, button"
711
+ .state=${state}
712
+ .t=${this.i18n.t}
713
+ .busy=${this._busy}
714
+ .canExport=${(state?.resident ?? false) || this.server !== null || this.serverEndpoint !== null}
715
+ .showExport=${this.showExport === 'always' || (this.showExport === 'auto' && this._processListeners > 0)}
716
+ @nav-undo=${() => this.getSession().undo()}
717
+ @nav-redo=${() => this.getSession().redo()}
718
+ @nav-time-travel=${(e) => this.getSession().timeTravel(e.detail)}
719
+ @nav-export=${() => this.onExport()}
720
+ @nav-download=${() => this.onDownload()}
721
+ ></pincel-nav>
722
+ ${ready
723
+ ? html `<pincel-stage
724
+ part="stage"
725
+ exportparts="viewport, canvas, overlay, status"
726
+ .preview=${state?.preview ?? null}
727
+ .pending=${state?.previewPending ?? false}
728
+ .overlay=${ctx && tool?.overlay ? tool.overlay(ctx) : null}
729
+ @stage-view=${() => this.requestUpdate()}
730
+ ></pincel-stage>`
731
+ : this.renderEmpty(state)}
732
+ <div class="panel" part="panel">${ctx && tool ? tool.panel(ctx) : nothing}</div>
733
+ <pincel-toolbar
734
+ part="toolbar"
735
+ exportparts="tool"
736
+ .tools=${tools}
737
+ .active=${this.activeTool}
738
+ .t=${this.i18n.t}
739
+ .orientation=${wide ? 'vertical' : 'horizontal'}
740
+ @tool-select=${(e) => {
741
+ this.activeTool = e.detail;
742
+ }}
743
+ ></pincel-toolbar>
744
+ ${state?.error ? html `<div class="error" role="alert">${this.i18n.t('editor.error', { message: state.error.message })}</div>` : nothing}
745
+ ${this._toast
746
+ ? html `<div class="toast" part="toast" data-testid="toast" @click=${() => {
747
+ this._toast = null;
748
+ }}>
749
+ <span>${this._toast.text}</span>
750
+ <code>${this._toast.hash}</code>
751
+ </div>`
752
+ : nothing}
753
+ <span id="live" class="visually-hidden" role="status" aria-live="polite"></span>
754
+ </div>
755
+ `;
756
+ }
757
+ renderEmpty(state) {
758
+ if (state?.status === 'loading') {
759
+ return html `<div class="empty" part="empty" role="status">${this.i18n.t('editor.loading')}</div>`;
760
+ }
761
+ return html `<div
762
+ class="empty ${this._dragover ? 'dragover' : ''}"
763
+ part="empty"
764
+ @dragover=${(e) => {
765
+ e.preventDefault();
766
+ this._dragover = true;
767
+ }}
768
+ @dragleave=${() => {
769
+ this._dragover = false;
770
+ }}
771
+ @drop=${(e) => {
772
+ e.preventDefault();
773
+ this._dragover = false;
774
+ this.onFiles(e.dataTransfer?.files ?? null);
775
+ }}
776
+ >
777
+ <p>${this.i18n.t('editor.empty')}</p>
778
+ <label>
779
+ <input class="visually-hidden" type="file" accept="image/png,image/jpeg,image/webp" @change=${(e) => this.onFiles(e.target.files)} />
780
+ <pincel-button variant="primary" @click=${(e) => e.currentTarget.previousElementSibling.click()}>
781
+ ${this.i18n.t('editor.choose')}
782
+ </pincel-button>
783
+ </label>
784
+ </div>`;
785
+ }
786
+ }
787
+ function formatBytes(n) {
788
+ if (n < 1024)
789
+ return `${n} B`;
790
+ if (n < 1024 * 1024)
791
+ return `${(n / 1024).toFixed(n < 10240 ? 1 : 0)} KB`;
792
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
793
+ }
794
+ //# sourceMappingURL=pincel-editor.js.map