@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,873 @@
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