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

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