@sealpixel/ui 1.0.0-rc.0 → 1.0.0-rc.2

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 (51) hide show
  1. package/LICENSE.md +1 -1
  2. package/dist/theme.js +6 -6
  3. package/package.json +5 -5
  4. package/dist/components/lumbra-button.d.ts +0 -28
  5. package/dist/components/lumbra-button.d.ts.map +0 -1
  6. package/dist/components/lumbra-button.js +0 -87
  7. package/dist/components/lumbra-button.js.map +0 -1
  8. package/dist/components/lumbra-editor.d.ts +0 -226
  9. package/dist/components/lumbra-editor.d.ts.map +0 -1
  10. package/dist/components/lumbra-editor.js +0 -873
  11. package/dist/components/lumbra-editor.js.map +0 -1
  12. package/dist/components/lumbra-nav.d.ts +0 -48
  13. package/dist/components/lumbra-nav.d.ts.map +0 -1
  14. package/dist/components/lumbra-nav.js +0 -159
  15. package/dist/components/lumbra-nav.js.map +0 -1
  16. package/dist/components/lumbra-slider.d.ts +0 -48
  17. package/dist/components/lumbra-slider.d.ts.map +0 -1
  18. package/dist/components/lumbra-slider.js +0 -168
  19. package/dist/components/lumbra-slider.js.map +0 -1
  20. package/dist/components/lumbra-stage.d.ts +0 -92
  21. package/dist/components/lumbra-stage.d.ts.map +0 -1
  22. package/dist/components/lumbra-stage.js +0 -344
  23. package/dist/components/lumbra-stage.js.map +0 -1
  24. package/dist/components/lumbra-toolbar.d.ts +0 -30
  25. package/dist/components/lumbra-toolbar.d.ts.map +0 -1
  26. package/dist/components/lumbra-toolbar.js +0 -127
  27. package/dist/components/lumbra-toolbar.js.map +0 -1
  28. package/dist/components/pincel-button.d.ts +0 -28
  29. package/dist/components/pincel-button.d.ts.map +0 -1
  30. package/dist/components/pincel-button.js +0 -87
  31. package/dist/components/pincel-button.js.map +0 -1
  32. package/dist/components/pincel-editor.d.ts +0 -195
  33. package/dist/components/pincel-editor.d.ts.map +0 -1
  34. package/dist/components/pincel-editor.js +0 -794
  35. package/dist/components/pincel-editor.js.map +0 -1
  36. package/dist/components/pincel-nav.d.ts +0 -33
  37. package/dist/components/pincel-nav.d.ts.map +0 -1
  38. package/dist/components/pincel-nav.js +0 -147
  39. package/dist/components/pincel-nav.js.map +0 -1
  40. package/dist/components/pincel-slider.d.ts +0 -48
  41. package/dist/components/pincel-slider.d.ts.map +0 -1
  42. package/dist/components/pincel-slider.js +0 -168
  43. package/dist/components/pincel-slider.js.map +0 -1
  44. package/dist/components/pincel-stage.d.ts +0 -66
  45. package/dist/components/pincel-stage.d.ts.map +0 -1
  46. package/dist/components/pincel-stage.js +0 -233
  47. package/dist/components/pincel-stage.js.map +0 -1
  48. package/dist/components/pincel-toolbar.d.ts +0 -30
  49. package/dist/components/pincel-toolbar.d.ts.map +0 -1
  50. package/dist/components/pincel-toolbar.js +0 -127
  51. package/dist/components/pincel-toolbar.js.map +0 -1
@@ -1,794 +0,0 @@
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