@simai-studio/ui 5.20261023.214 → 5.20261025.207

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 (50) hide show
  1. package/README.md +4 -4
  2. package/distr/component/button-group/css/button-group.min.css +1 -1
  3. package/distr/core/contracts/public-variables.v1.json +88 -0
  4. package/distr/core/contracts/third-party-notices.v1.json +1 -1
  5. package/distr/core/css/core.css +3 -2
  6. package/distr/core/js/composition/overlay.mjs +472 -24
  7. package/distr/core/js/composition/sortable-inserter.mjs +534 -0
  8. package/distr/core/js/core-loader.js +1 -1
  9. package/distr/core/js/core-loader.min.js +1 -1
  10. package/distr/core/js/core-rules.js +242 -185
  11. package/distr/core/js/core-rules.min.js +1 -1
  12. package/distr/core/js/open-surface-mark.js +88 -0
  13. package/distr/core/js/template-utilities.generated.json +3 -0
  14. package/distr/rule/js/rule.min.js +1 -1
  15. package/distr/rule/rule.json +47 -1
  16. package/distr/smart/color/css/color.min.css +1 -0
  17. package/distr/smart/color/js/color.min.js +36 -17
  18. package/distr/smart/color/smart.manifest.json +45 -4
  19. package/distr/smart/color/template/default.js +13 -2
  20. package/distr/smart/composition-overlay/css/composition-overlay.min.css +1 -1
  21. package/distr/smart/composition-overlay/js/composition-overlay.min.js +1 -1
  22. package/distr/smart/composition-overlay/smart.manifest.json +178 -11
  23. package/distr/smart/dropdown/css/dropdown.min.css +1 -0
  24. package/distr/smart/dropdown/js/dropdown.min.js +41 -21
  25. package/distr/smart/dropdown/smart.manifest.json +62 -3
  26. package/distr/smart/dropdown/template/default.js +5 -1
  27. package/distr/smart/field-renderers/js/field-renderers.min.js +54 -42
  28. package/distr/smart/image-field/css/image-field.min.css +1 -0
  29. package/distr/smart/image-field/js/image-field.min.js +62 -0
  30. package/distr/smart/image-field/smart.manifest.json +519 -0
  31. package/distr/smart/image-field/studio.fixture.json +15 -0
  32. package/distr/smart/image-field/studio.scenario.json +22 -0
  33. package/distr/smart/image-field/template/default.js +125 -0
  34. package/distr/smart/inputs/css/inputs.min.css +1 -0
  35. package/distr/smart/inputs/js/inputs.min.js +93 -75
  36. package/distr/smart/inputs/smart.manifest.json +62 -3
  37. package/distr/smart/inputs/template/default.js +8 -2
  38. package/distr/smart/sortable/css/sortable.min.css +1 -1
  39. package/distr/smart/sortable/js/sortable.min.js +1 -1
  40. package/distr/smart/sortable/smart.manifest.json +101 -14
  41. package/distr/smart/switch/css/switch.min.css +1 -0
  42. package/distr/smart/switch/js/switch.min.js +31 -13
  43. package/distr/smart/switch/smart.manifest.json +62 -3
  44. package/distr/smart/switch/template/default.js +2 -0
  45. package/distr/smart/textarea/css/textarea.min.css +1 -0
  46. package/distr/smart/textarea/js/textarea.min.js +51 -32
  47. package/distr/smart/textarea/smart.manifest.json +62 -3
  48. package/distr/smart/textarea/template/default.js +8 -1
  49. package/package-manifest.json +50 -39
  50. package/package.json +5 -4
@@ -0,0 +1,534 @@
1
+ // The insertion point of sf-sortable and the inserter it opens. A block editor
2
+ // asks for a "+" exactly where the next block goes -- between two rows, before
3
+ // the first, after the last -- that opens a searchable list of the kinds of
4
+ // thing that can go there (simai/ui-source#21; ui-mail redesign F4, 2026-10-22:
5
+ // "Добавить" always appended to the end, so every insert needed a later move).
6
+ // It is the same need in every editor built on a list -- mail, Docara studio,
7
+ // admin forms -- so it belongs to the list, not to each editor.
8
+ //
9
+ // The component reports the choice and creates nothing: the host hears
10
+ // sf-sortable-insert with the place and the kind, builds the item and puts it
11
+ // in the list, exactly as it applies sf-sortable-intent. The inserter is the
12
+ // Framework's standard menu (sf-context-menu, opened with openFor and its own
13
+ // local search), so its rows, its keys, its top layer and its Escape are the
14
+ // ones every other menu has; nothing here draws a second popover.
15
+ //
16
+ // It is opt-in: a list without inserter items gets a controller that draws
17
+ // nothing and listens to nothing, so every list written before is the list it
18
+ // was, its drag included. The drag module itself (sortable.mjs) is not changed:
19
+ // the element is extended here (withSortableInserter).
20
+ import { pageText } from '../page-language.js';
21
+ import { activeSortableSession } from './sortable.mjs';
22
+
23
+ export const SORTABLE_INSERT_EVENT = 'sf-sortable-insert';
24
+
25
+ const ITEM = '[data-sf-sortable-item]';
26
+
27
+ const MESSAGES = {
28
+ ru: {
29
+ point: 'Вставить: позиция {index} из {total}',
30
+ menu: 'Вставить',
31
+ search: 'Найти',
32
+ empty: 'Ничего не найдено',
33
+ inserted: 'Вставлено: {item}, позиция {index} в «{list}».',
34
+ hint: 'Плюс — вставить после.',
35
+ },
36
+ en: {
37
+ point: 'Insert: position {index} of {total}',
38
+ menu: 'Insert',
39
+ search: 'Find',
40
+ empty: 'Nothing found',
41
+ inserted: 'Inserted {item} at position {index} in “{list}”.',
42
+ hint: 'Plus to insert after.',
43
+ },
44
+ };
45
+
46
+ const format = (template, values) => template.replace(/\{(\w+)\}/gu, (_, key) => String(values[key] ?? ''));
47
+ const text = (key) => pageText(MESSAGES, key, undefined, 'sf-sortable');
48
+
49
+ function items(list) {
50
+ return [...list.querySelectorAll(ITEM)].filter((item) => item.closest('sf-sortable') === list);
51
+ }
52
+
53
+ function listName(list) {
54
+ return list.getAttribute('label') || list.getAttribute('aria-label') || list.getAttribute('group') || '';
55
+ }
56
+
57
+ // The same test the drag uses: two items side by side on one row make a grid,
58
+ // and a grid is read row by row.
59
+ function isWrapped(all) {
60
+ for (let position = 1; position < all.length; position += 1) {
61
+ const previous = all[position - 1].getBoundingClientRect();
62
+ const current = all[position].getBoundingClientRect();
63
+ if (current.top < previous.bottom - 1 && current.left >= previous.right - 1) return true;
64
+ }
65
+ return false;
66
+ }
67
+
68
+ /**
69
+ * Where the place `index` is, in viewport coordinates: a line across the list
70
+ * (axis "y", a vertical list), down it (axis "x", a horizontal one), or down the
71
+ * side of one cell of a grid. `at` is the line's position on its axis -- the
72
+ * middle of the gap between two items, so the point sits on the gap and not on
73
+ * either item -- and `start`/`end` its extent across.
74
+ */
75
+ export function insertionGeometry(list, all, index) {
76
+ const box = list.getBoundingClientRect();
77
+ const inner = {
78
+ left: box.left + list.clientLeft, top: box.top + list.clientTop,
79
+ width: list.clientWidth, height: list.clientHeight,
80
+ };
81
+ const horizontal = list.getAttribute('orientation') === 'horizontal';
82
+ const before = all[index]?.getBoundingClientRect();
83
+ const after = all[index - 1]?.getBoundingClientRect();
84
+ if (!horizontal && isWrapped(all)) {
85
+ const cell = before ?? after;
86
+ if (!cell) return { axis: 'y', at: inner.top + inner.height / 2, start: inner.left, end: inner.left + inner.width };
87
+ const sameRow = before && after && after.top < before.bottom - 1 && before.left >= after.right - 1;
88
+ const at = before ? (sameRow ? (after.right + before.left) / 2 : before.left) : after.right;
89
+ return { axis: 'x', at, start: cell.top, end: cell.bottom };
90
+ }
91
+ if (horizontal) {
92
+ const at = before && after ? (after.right + before.left) / 2 : before ? before.left : after ? after.right : inner.left + inner.width / 2;
93
+ return { axis: 'x', at, start: inner.top, end: inner.top + inner.height };
94
+ }
95
+ const at = before && after ? (after.bottom + before.top) / 2 : before ? before.top : after ? after.bottom : inner.top + inner.height / 2;
96
+ return { axis: 'y', at, start: inner.left, end: inner.left + inner.width };
97
+ }
98
+
99
+ /**
100
+ * The place nearest a pointer, and how far it is: across a vertical list only
101
+ * the height counts, along a horizontal one only the width; a grid's place is
102
+ * a side of a cell and counts only on that cell's row.
103
+ */
104
+ export function nearestInsertion(list, all, x, y) {
105
+ let best = null;
106
+ for (let index = 0; index <= all.length; index += 1) {
107
+ const place = insertionGeometry(list, all, index);
108
+ const along = place.axis === 'y' ? y : x;
109
+ const across = place.axis === 'y' ? x : y;
110
+ if (across < place.start || across > place.end) continue;
111
+ const distance = Math.abs(along - place.at);
112
+ if (!best || distance < best.distance) best = { index, distance, place };
113
+ }
114
+ return best;
115
+ }
116
+
117
+ function toEntries(list) {
118
+ return list.inserterItems.flatMap((item) => {
119
+ if (!item || typeof item !== 'object') return [];
120
+ if (item.type === 'separator') return [{ type: 'separator' }];
121
+ if (item.type === 'heading') return [{ type: 'heading', text: String(item.label ?? '') }];
122
+ if (item.value === undefined || item.value === null) return [];
123
+ return [{
124
+ id: String(item.value), text: String(item.label ?? item.value),
125
+ icon: item.icon || undefined, caption: item.caption || undefined,
126
+ keywords: item.keywords || undefined, disabled: Boolean(item.disabled),
127
+ }];
128
+ });
129
+ }
130
+
131
+ /**
132
+ * The controller one list owns. connect/disconnect follow the list; refresh
133
+ * follows its inserter items; ensure follows a host redraw of its children.
134
+ */
135
+ export function createSortableInserter(list) {
136
+ let connected = false;
137
+ let active = false;
138
+ let wiring = null;
139
+ let control = null;
140
+ let button = null;
141
+ let menu = null;
142
+ let menuIndex = 0;
143
+ // The place the point stands at, and why it is shown: a pointer near a gap,
144
+ // a focused row (its place after it), the point's own focus or its menu.
145
+ let point = 0;
146
+ let hover = null;
147
+ let rowFocus = null;
148
+
149
+ const document_ = () => globalThis.document;
150
+ const count = () => items(list).length;
151
+ const clamp = (index) => Math.max(0, Math.min(count(), index));
152
+
153
+ function build() {
154
+ control = document_().createElement('div');
155
+ control.className = 'sf-sortable-insert';
156
+ control.setAttribute('data-sf-sortable-insert', '');
157
+ // A native icon button of the Framework's smallest rung: its height, its
158
+ // glyph and its padding are the ladder's (1/3), and its look is the quiet
159
+ // surface one a row action has, so it reads the same on any canvas.
160
+ button = document_().createElement('button');
161
+ button.type = 'button';
162
+ button.className = 'sf-icon-button sf-icon-button--icon sf-icon-button--surface sf-icon-button--on-surface sf-icon-button--size-1/3 sf-sortable-insert-button';
163
+ button.setAttribute('aria-haspopup', 'menu');
164
+ button.setAttribute('aria-expanded', 'false');
165
+ const glyph = document_().createElement('i');
166
+ glyph.className = 'sf-icon';
167
+ glyph.setAttribute('aria-hidden', 'true');
168
+ glyph.textContent = 'add';
169
+ button.append(glyph);
170
+ control.append(button);
171
+ }
172
+
173
+ // The point after the last item in the document, so it is the list's last tab
174
+ // stop: Tab from the last row reaches it and Tab from it leaves the list, and
175
+ // nothing is trapped. A host that appends its redrawn items after the
176
+ // component's own nodes would otherwise put the point in front of them.
177
+ function ensure() {
178
+ if (!active || !control) return;
179
+ const last = items(list).at(-1);
180
+ const misplaced = control.parentNode !== list
181
+ || Boolean(last && (control.compareDocumentPosition(last) & globalThis.Node.DOCUMENT_POSITION_FOLLOWING));
182
+ if (misplaced) {
183
+ const focused = document_().activeElement === button;
184
+ list.append(control);
185
+ // Moving a focused node drops its focus; the person keeps their place.
186
+ if (focused) button.focus({ preventScroll: true });
187
+ }
188
+ // The rows moved under it: the place is measured again.
189
+ place();
190
+ }
191
+
192
+ function shownReason() {
193
+ if (activeSortableSession()) return null;
194
+ if (menu?.isOpen) return 'menu';
195
+ if (document_().activeElement === button) return 'focus';
196
+ if (hover !== null) return 'hover';
197
+ if (rowFocus !== null) return 'row';
198
+ return null;
199
+ }
200
+
201
+ function place() {
202
+ if (!active || !control) return;
203
+ const reason = shownReason();
204
+ if (reason === 'hover') point = hover;
205
+ else if (reason === 'row') point = rowFocus;
206
+ point = clamp(point);
207
+ const all = items(list);
208
+ const geometry = insertionGeometry(list, all, point);
209
+ const box = list.getBoundingClientRect();
210
+ const originX = box.left + list.clientLeft - list.scrollLeft;
211
+ const originY = box.top + list.clientTop - list.scrollTop;
212
+ control.dataset.axis = geometry.axis;
213
+ if (geometry.axis === 'y') {
214
+ Object.assign(control.style, { left: `${geometry.start - originX}px`, top: `${geometry.at - originY}px`, width: `${geometry.end - geometry.start}px`, height: '0px' });
215
+ } else {
216
+ Object.assign(control.style, { left: `${geometry.at - originX}px`, top: `${geometry.start - originY}px`, width: '0px', height: `${geometry.end - geometry.start}px` });
217
+ }
218
+ control.toggleAttribute('data-shown', Boolean(reason));
219
+ control.dataset.index = String(point);
220
+ button.setAttribute('aria-label', format(text('point'), { index: point + 1, total: all.length + 1 }));
221
+ }
222
+
223
+ // Pointer: the point comes to the gap the pointer is near, within half the
224
+ // point's own height of it -- near enough to mean the gap, not the row.
225
+ function onPointerMove(event) {
226
+ if (event.buttons || activeSortableSession() || menu?.isOpen) return;
227
+ if (control.contains(event.target)) return;
228
+ const zone = Math.max(button.offsetHeight, button.offsetWidth) / 2;
229
+ const nearest = nearestInsertion(list, items(list), event.clientX, event.clientY);
230
+ const next = nearest && nearest.distance <= zone ? nearest.index : null;
231
+ if (next === hover) return;
232
+ hover = next;
233
+ place();
234
+ }
235
+
236
+ function onPointerLeave() {
237
+ if (hover === null) return;
238
+ hover = null;
239
+ place();
240
+ }
241
+
242
+ // A focused row shows the place after it: the place its "+" key opens, and
243
+ // the one a tap on a touch screen, which has no hover, can reach.
244
+ function onFocusIn(event) {
245
+ const item = event.target.closest?.(ITEM);
246
+ if (event.target === button) {
247
+ place();
248
+ return;
249
+ }
250
+ rowFocus = item && item.closest('sf-sortable') === list ? items(list).indexOf(item) + 1 : null;
251
+ place();
252
+ }
253
+
254
+ function onFocusOut() {
255
+ globalThis.setTimeout(() => {
256
+ if (!connected) return;
257
+ if (!list.contains(document_().activeElement)) rowFocus = null;
258
+ place();
259
+ }, 0);
260
+ }
261
+
262
+ // The point's own keys: the arrows of the list's direction walk the places,
263
+ // Home and End go to the first and the last, and each step is said. Enter
264
+ // and Space are the button's own.
265
+ function onButtonKeyDown(event) {
266
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
267
+ const horizontal = list.getAttribute('orientation') === 'horizontal';
268
+ const rtl = globalThis.getComputedStyle(list).direction === 'rtl';
269
+ const steps = horizontal
270
+ ? { ArrowLeft: rtl ? 1 : -1, ArrowRight: rtl ? -1 : 1, ArrowUp: -1, ArrowDown: 1 }
271
+ : { ArrowUp: -1, ArrowDown: 1, ArrowLeft: -1, ArrowRight: 1 };
272
+ let next = null;
273
+ if (event.key in steps) next = point + steps[event.key];
274
+ else if (event.key === 'Home') next = 0;
275
+ else if (event.key === 'End') next = count();
276
+ if (next === null) return;
277
+ event.preventDefault();
278
+ hover = null;
279
+ rowFocus = null;
280
+ point = clamp(next);
281
+ place();
282
+ list.announce?.(format(text('point'), { index: point + 1, total: count() + 1 }));
283
+ }
284
+
285
+ function onButtonClick(event) {
286
+ event.preventDefault();
287
+ if (menu?.isOpen) {
288
+ menu.close();
289
+ return;
290
+ }
291
+ open(point);
292
+ }
293
+
294
+ async function standardMenu() {
295
+ if (menu?.isConnected) return menu;
296
+ const registry = globalThis.customElements;
297
+ if (!registry.get('sf-context-menu')) {
298
+ globalThis.SF?.Loader?.ensurePlugin?.('cl-context-menu');
299
+ await Promise.race([registry.whenDefined('sf-context-menu'), new Promise((done) => { globalThis.setTimeout(done, 3000); })]);
300
+ }
301
+ if (!registry.get('sf-context-menu')) return null;
302
+ menu = document_().createElement('sf-context-menu');
303
+ menu.setAttribute('popup', '');
304
+ menu.addEventListener('sf-context-menu-select', onSelect);
305
+ menu.addEventListener('sf-context-menu-close', () => { button?.setAttribute('aria-expanded', 'false'); place(); });
306
+ // Enter in the search takes the first row it left: whoever typed the name
307
+ // of what they want has chosen it. The menu's own Enter is a row's.
308
+ menu.addEventListener('keydown', (event) => {
309
+ if (event.key !== 'Enter' || event.defaultPrevented || event.target !== menu.searchField?.()) return;
310
+ event.preventDefault();
311
+ menu.syncMenuItems?.();
312
+ menu.availableMenuItems?.()[0]?.click();
313
+ });
314
+ (list.closest('dialog[open]') || document_().body).append(menu);
315
+ return menu;
316
+ }
317
+
318
+ /** Opens the inserter for the place `index` (0 before the first item). */
319
+ async function open(index) {
320
+ if (!active || activeSortableSession()) return false;
321
+ point = clamp(index);
322
+ hover = null;
323
+ place();
324
+ const surface = await standardMenu();
325
+ if (!surface || !active) return false;
326
+ menuIndex = point;
327
+ surface.setAttribute('aria-label', listName(list) ? `${text('menu')}: ${listName(list)}` : text('menu'));
328
+ surface.search = { placeholder: text('search'), empty: text('empty') };
329
+ surface.entries = toEntries(list);
330
+ const opened = await surface.openFor(button, { side: 'block-end', align: 'start', focus: 'first' });
331
+ place();
332
+ return opened;
333
+ }
334
+
335
+ // A choice is the host's to apply: it hears the place and the kind, and a
336
+ // refusal (preventDefault) leaves the inserter open for another choice. The
337
+ // menu is closed here rather than by itself, so a host that moved focus to
338
+ // the item it just inserted keeps it there.
339
+ function onSelect(event) {
340
+ event.preventDefault();
341
+ const entry = event.detail?.entry;
342
+ if (!entry || entry.id === undefined || entry.id === null) return;
343
+ const index = menuIndex;
344
+ const accepted = list.dispatchEvent(new globalThis.CustomEvent(SORTABLE_INSERT_EVENT, {
345
+ bubbles: true, composed: true, cancelable: true,
346
+ detail: Object.freeze({ index, item: entry.id, to: list.getAttribute('id') || list.getAttribute('group') || '' }),
347
+ }));
348
+ if (!accepted) return;
349
+ list.announce?.(format(text('inserted'), { item: entry.text ?? entry.id, index: index + 1, list: listName(list) }));
350
+ // The next insert goes after this one, so a run of choices builds in order.
351
+ point = index + 1;
352
+ const focusMoved = !menu.contains(document_().activeElement);
353
+ menu.close({ restoreFocus: !focusMoved });
354
+ }
355
+
356
+ function onItemKeyDown(event, item) {
357
+ if (!active || event.key !== '+' || event.altKey || event.ctrlKey || event.metaKey) return false;
358
+ event.preventDefault();
359
+ open(items(list).indexOf(item) + 1);
360
+ return true;
361
+ }
362
+
363
+ function onSortableState() {
364
+ hover = null;
365
+ place();
366
+ }
367
+
368
+ function mount() {
369
+ if (!control) build();
370
+ wiring = new globalThis.AbortController();
371
+ const options = { signal: wiring.signal };
372
+ list.setAttribute('data-sf-sortable-inserter', '');
373
+ list.addEventListener('pointermove', onPointerMove, options);
374
+ list.addEventListener('pointerleave', onPointerLeave, options);
375
+ list.addEventListener('focusin', onFocusIn, options);
376
+ list.addEventListener('focusout', onFocusOut, options);
377
+ list.addEventListener('sf-sortable-state', onSortableState, options);
378
+ button.addEventListener('keydown', onButtonKeyDown, options);
379
+ button.addEventListener('click', onButtonClick, options);
380
+ // The icon button's look and the glyph's font are component stylesheets:
381
+ // asked for here, and named as relations of the rule, so a page without
382
+ // them still gets them.
383
+ globalThis.SF?.Loader?.ensurePlugin?.('icon-buttons');
384
+ globalThis.SF?.Loader?.ensurePlugin?.('icons');
385
+ active = true;
386
+ ensure();
387
+ place();
388
+ }
389
+
390
+ function unmount() {
391
+ active = false;
392
+ wiring?.abort();
393
+ wiring = null;
394
+ menu?.close?.({ restoreFocus: false });
395
+ control?.remove();
396
+ list.removeAttribute('data-sf-sortable-inserter');
397
+ hover = null;
398
+ rowFocus = null;
399
+ }
400
+
401
+ function refresh() {
402
+ const wanted = connected && list.inserterItems.length > 0;
403
+ if (wanted && !active) mount();
404
+ else if (!wanted && active) unmount();
405
+ else if (wanted && menu?.isOpen) menu.entries = toEntries(list);
406
+ list.updateInstructions?.();
407
+ }
408
+
409
+ return {
410
+ connect() { connected = true; refresh(); },
411
+ disconnect() {
412
+ connected = false;
413
+ if (active) unmount();
414
+ menu?.remove();
415
+ menu = null;
416
+ },
417
+ refresh,
418
+ ensure,
419
+ open,
420
+ onItemKeyDown,
421
+ instructions: () => (active ? text('hint') : ''),
422
+ get active() { return active; },
423
+ get index() { return point; },
424
+ };
425
+ }
426
+
427
+ /**
428
+ * sf-sortable with an inserter: the drag element of sortable.mjs, extended.
429
+ * It is an extension rather than an edit of that module because the
430
+ * composition overlay's bundle carries sortable.mjs too (for the shared drag
431
+ * session) and has no use for an inserter; extending it here keeps the
432
+ * inserter in the sf-sortable bundle alone and the drag module as it was.
433
+ */
434
+ export function withSortableInserter(Base) {
435
+ return class SfSortable extends Base {
436
+ static get observedAttributes() {
437
+ return [...(super.observedAttributes ?? []), 'inserter-items'];
438
+ }
439
+
440
+ constructor() {
441
+ super();
442
+ this.inserterController = null;
443
+ this._inserterHint = '';
444
+ // Set before the element was defined, the value sat on the instance and
445
+ // would hide the accessor; it is taken over here, as sf-context-menu
446
+ // takes over its entries.
447
+ if (Object.prototype.hasOwnProperty.call(this, 'inserterItems')) {
448
+ const early = this.inserterItems;
449
+ delete this.inserterItems;
450
+ this._inserterItems = Array.isArray(early) ? early : null;
451
+ }
452
+ }
453
+
454
+ // What the inserter offers: [{value, label, icon?, caption?, keywords?,
455
+ // disabled?}], as a property or as JSON in the inserter-items attribute.
456
+ // An empty list is no inserter, and the list is the list it was before.
457
+ get inserterItems() {
458
+ if (Array.isArray(this._inserterItems)) return this._inserterItems;
459
+ const raw = this.getAttribute('inserter-items');
460
+ if (!raw) return [];
461
+ try {
462
+ const parsed = JSON.parse(raw);
463
+ return Array.isArray(parsed) ? parsed : [];
464
+ } catch {
465
+ return [];
466
+ }
467
+ }
468
+
469
+ set inserterItems(value) {
470
+ this._inserterItems = Array.isArray(value) ? value : null;
471
+ this.inserterController?.refresh();
472
+ }
473
+
474
+ attributeChangedCallback(name, previous, next) {
475
+ super.attributeChangedCallback?.(name, previous, next);
476
+ if (name === 'inserter-items' && previous !== next) this.inserterController?.refresh();
477
+ }
478
+
479
+ connectedCallback() {
480
+ super.connectedCallback();
481
+ if (!this.inserterController) this.inserterController = createSortableInserter(this);
482
+ this.inserterController.connect();
483
+ }
484
+
485
+ disconnectedCallback() {
486
+ this.inserterController?.disconnect();
487
+ super.disconnectedCallback();
488
+ }
489
+
490
+ // The instructions every item points at say the inserter's key while the
491
+ // list has one. The drag module writes its own sentence; the hint follows it.
492
+ updateInstructions() {
493
+ if (!this.instructions) return;
494
+ const current = this.instructions.textContent;
495
+ const own = this._inserterHint && current.endsWith(` ${this._inserterHint}`)
496
+ ? current.slice(0, -(this._inserterHint.length + 1))
497
+ : current;
498
+ const hint = this.inserterController?.instructions() || '';
499
+ const next = hint ? `${own} ${hint}` : own;
500
+ this._inserterHint = hint;
501
+ // Written only when it changes: the list observes its own subtree, and a
502
+ // write would wake the observer that called this.
503
+ if (next !== current) this.instructions.textContent = next;
504
+ }
505
+
506
+ // A host redraw takes the component's nodes out with its own, or leaves the
507
+ // insertion point in front of the items it appends: both come back.
508
+ ensureAccessibilityNodes() {
509
+ super.ensureAccessibilityNodes();
510
+ this.updateInstructions();
511
+ this.inserterController?.ensure();
512
+ }
513
+
514
+ // The inserter's key on a row ("+": insert after it) comes before the
515
+ // drag's keys; anything else is the drag's.
516
+ onKeyDown(event) {
517
+ const item = event.target.closest?.(ITEM);
518
+ if (!activeSortableSession() && item && item.closest('sf-sortable') === this && event.target === item
519
+ && this.inserterController?.onItemKeyDown(event, item)) return;
520
+ super.onKeyDown(event);
521
+ }
522
+ };
523
+ }
524
+
525
+ // The registry sf-sortable is defined in, through which it is defined with its
526
+ // inserter: defineSortable takes the registry it defines into.
527
+ export function inserterRegistry(registry = globalThis.customElements) {
528
+ return {
529
+ get: (name) => registry.get(name),
530
+ define: (name, Base, options) => registry.define(name, withSortableInserter(Base), options),
531
+ };
532
+ }
533
+
534
+ export default { SORTABLE_INSERT_EVENT, createSortableInserter, inserterRegistry, insertionGeometry, nearestInsertion, withSortableInserter };
@@ -7086,7 +7086,7 @@ module.exports = /*#__PURE__*/JSON.parse('{"version":1,"aliases":{"column-gap/de
7086
7086
  /***/ "e7acc025fab1"
7087
7087
  (module) {
7088
7088
 
7089
- module.exports = /*#__PURE__*/JSON.parse('{"cl-alert":["display/default","flex-direction/default","flex-wrap/default","flex/default","place-items/default","position/default"],"cl-avatar":["align-items/default","background-color/default","display/default","element-position/default","flex-direction/default","flex/default","height/default","justify-content/default","overflow/default","place-items/default","position/default","text-overflow/default","transition-property/default","white-space/default","width/default"],"cl-avatars":["align-items/default","display/default","flex-direction/default","position/default"],"cl-badges":["display/default","flex-direction/default","flex-wrap/default","justify-content/default","place-items/default","position/default","sr-only/default"],"cl-breadcrumbs":["align-items/default","container/default","display/default","position/default","visibility/default"],"cl-button-group":["align-items/default","display/default","isolate/default","pointer-events/default","position/default","text-align/default"],"cl-buttons":["align-items/default","display/default","flex/default","pointer-events/default","position/default"],"cl-checkbox":["cursor/default","display/default","flex-direction/default","position/default","transition-property/default"],"cl-close":["display/default","justify-content/default","place-items/default","position/default"],"cl-color":["display/default","position/default"],"cl-context-menu":["display/default","flex-direction/default","flex/default","position/default","text-decoration/default"],"cl-copy-button":["display/default","position/default"],"cl-country-code":["display/default","flex-direction/default","place-items/default","position/default","transition-property/default"],"cl-datepicker":["align-content/default","align-items/default","display/default","element-position/default","flex-direction/default","font-transform/default","gap/default","grid-template-columns/default","isolate/default","justify-content/default","overflow/default","position/default","resize/default"],"cl-download-file":["cursor/default","display/default","position/default","transition-property/default"],"cl-drawer":["display/default","isolate/default","justify-content/default","margin/default","place-items/default","position/default","resize/default","visibility/default"],"cl-dropdown":["align-items/default","background-color/default","background-size/default","cursor/default","display/default","flex-direction/default","flex-wrap/default","flex/default","place-items/default","position/default","resize/default","transition-property/default","visibility/default"],"cl-empty-state":["display/default","isolate/default","position/default"],"cl-file-preview":["isolate/default","position/default"],"cl-file-upload":["align-items/default","align-self/default","cursor/default","display/default","flex-direction/default","flex/default","gap/default","grid-template-columns/default","justify-content/default","justify-items/default","position/default","width/default"],"cl-gallery":["align-items/default","background-color/default","border-radius/default","border-width/default","box-shadow/default","display/default","filter-blur/default","flex-direction/default","gap/default","height/default","justify-content/default","margin/default","max-width/default","min-height/default","min-width/default","padding/default","position/default","text-color/default","width/default"],"cl-help-mark":["display/default","isolate/default","position/default"],"cl-icon-buttons":["align-items/default","display/default","pointer-events/default","position/default"],"cl-icons":["font-weight/default","position/default"],"cl-inputs":["align-items/default","display/default","flex-direction/default","position/default","text-align/default","transition-property/default"],"cl-list-item":["align-items/default","background-color/default","background-size/default","cursor/default","display/default","flex-direction/default","flex-wrap/default","flex/default","gap/default","justify-content/default","place-items/default","position/default"],"cl-modal":["align-items/default","backdrop-filter-blur/default","border-radius/default","border-width/default","display/default","element-position/default","filter-blur/default","flex-direction/default","flex-wrap/default","font-size/default","gap/default","height/default","justify-content/default","margin/default","min-width/default","overflow/default","padding/default","place-items/default","position/default","text-color/default","title/default","width/default","z-index/default"],"cl-page-header":["align-items/default","display/default","flex-direction/default","flex-grow/default","flex-wrap/default","gap/default","margin/default","min-width/default","position/default","text-overflow/default"],"cl-pagination":["align-items/default","display/default","flex-direction/default","justify-content/default","position/default"],"cl-popover":["display/default","position/default"],"cl-profile-picker":["display/default","position/default"],"cl-progress-bar":["align-items/default","display/default","flex-direction/default","position/default","transition-property/default"],"cl-progress-scale":["align-items/default","display/default","flex-direction/default","grid-template-columns/default","overflow/default","position/default"],"cl-radio":["align-items/default","display/default","flex-direction/default","justify-content/default","position/default","transition-property/default"],"cl-range-slider":["flex/default","isolate/default","position/default"],"cl-reference-link":["align-items/default","display/default","position/default"],"cl-resizer":["display/default","position/default","resize/default","text-max-width/default"],"cl-skeleton":["animation/default","display/default","flex/default","position/default"],"cl-slider":["background-color/default","border-radius/default","display/default","padding/default","position/default","text-color/default"],"cl-spinner":["animation/default","position/default","stroke-color/default"],"cl-steps":["cursor/default","display/default","flex-direction/default","justify-content/default","place-items/default","position/default"],"cl-switch":["align-items/default","display/default","flex-direction/default","justify-content/default","position/default","transition-property/default"],"cl-tabs":["container/default","display/default","flex-direction/default","position/default","resize/default","visibility/default"],"cl-tags":["background-size/default","display/default","flex-direction/default","flex-wrap/default","justify-content/default","min-width/default","place-items/default","position/default"],"cl-textarea":["display/default","flex-direction/default","isolate/default","position/default","transition-property/default"],"cl-theme-generator":["position/default"],"cl-toast":["align-items/default","display/default","flex-direction/default","flex/default","justify-content/default","place-items/default","position/default"],"cl-toggle":["display/default","justify-content/default","place-items/default","position/default","transition-property/default"],"cl-tooltip":["display/default","flex-direction/default","position/default"],"cl-tree":["display/default","flex-direction/default","position/default","visibility/default"],"cl-tree-item":["align-items/default","container/default","display/default","flex-direction/default","position/default"]}');
7089
+ module.exports = /*#__PURE__*/JSON.parse('{"cl-alert":["display/default","flex-direction/default","flex-wrap/default","flex/default","place-items/default","position/default"],"cl-avatar":["align-items/default","background-color/default","display/default","element-position/default","flex-direction/default","flex/default","height/default","justify-content/default","overflow/default","place-items/default","position/default","text-overflow/default","transition-property/default","white-space/default","width/default"],"cl-avatars":["align-items/default","display/default","flex-direction/default","position/default"],"cl-badges":["display/default","flex-direction/default","flex-wrap/default","justify-content/default","place-items/default","position/default","sr-only/default"],"cl-breadcrumbs":["align-items/default","container/default","display/default","position/default","visibility/default"],"cl-button-group":["align-items/default","display/default","isolate/default","pointer-events/default","position/default","text-align/default"],"cl-buttons":["align-items/default","display/default","flex/default","pointer-events/default","position/default"],"cl-checkbox":["cursor/default","display/default","flex-direction/default","position/default","transition-property/default"],"cl-close":["display/default","justify-content/default","place-items/default","position/default"],"cl-color":["display/default","position/default"],"cl-context-menu":["display/default","flex-direction/default","flex/default","position/default","text-decoration/default"],"cl-copy-button":["display/default","position/default"],"cl-country-code":["display/default","flex-direction/default","place-items/default","position/default","transition-property/default"],"cl-datepicker":["align-content/default","align-items/default","display/default","element-position/default","flex-direction/default","font-transform/default","gap/default","grid-template-columns/default","isolate/default","justify-content/default","overflow/default","position/default","resize/default"],"cl-download-file":["cursor/default","display/default","position/default","transition-property/default"],"cl-drawer":["display/default","isolate/default","justify-content/default","margin/default","place-items/default","position/default","resize/default","visibility/default"],"cl-dropdown":["align-items/default","background-color/default","background-size/default","cursor/default","display/default","flex-direction/default","flex-wrap/default","flex/default","place-items/default","position/default","resize/default","transition-property/default","visibility/default"],"cl-empty-state":["display/default","isolate/default","position/default"],"cl-file-preview":["isolate/default","position/default"],"cl-file-upload":["align-items/default","align-self/default","cursor/default","display/default","flex-direction/default","flex/default","gap/default","grid-template-columns/default","justify-content/default","justify-items/default","position/default","width/default"],"cl-gallery":["align-items/default","background-color/default","border-radius/default","border-width/default","box-shadow/default","display/default","filter-blur/default","flex-direction/default","gap/default","height/default","justify-content/default","margin/default","max-width/default","min-height/default","min-width/default","padding/default","position/default","text-color/default","width/default"],"cl-help-mark":["display/default","isolate/default","position/default"],"cl-icon-buttons":["align-items/default","display/default","pointer-events/default","position/default"],"cl-icons":["font-weight/default","position/default"],"cl-image-field":["position/default"],"cl-inputs":["align-items/default","display/default","flex-direction/default","position/default","text-align/default","transition-property/default"],"cl-list-item":["align-items/default","background-color/default","background-size/default","cursor/default","display/default","flex-direction/default","flex-wrap/default","flex/default","gap/default","justify-content/default","place-items/default","position/default"],"cl-modal":["align-items/default","backdrop-filter-blur/default","border-radius/default","border-width/default","display/default","element-position/default","filter-blur/default","flex-direction/default","flex-wrap/default","font-size/default","gap/default","height/default","justify-content/default","margin/default","min-width/default","overflow/default","padding/default","place-items/default","position/default","text-color/default","title/default","width/default","z-index/default"],"cl-page-header":["align-items/default","display/default","flex-direction/default","flex-grow/default","flex-wrap/default","gap/default","margin/default","min-width/default","position/default","text-overflow/default"],"cl-pagination":["align-items/default","display/default","flex-direction/default","justify-content/default","position/default"],"cl-popover":["display/default","position/default"],"cl-profile-picker":["display/default","position/default"],"cl-progress-bar":["align-items/default","display/default","flex-direction/default","position/default","transition-property/default"],"cl-progress-scale":["align-items/default","display/default","flex-direction/default","grid-template-columns/default","overflow/default","position/default"],"cl-radio":["align-items/default","display/default","flex-direction/default","justify-content/default","position/default","transition-property/default"],"cl-range-slider":["flex/default","isolate/default","position/default"],"cl-reference-link":["align-items/default","display/default","position/default"],"cl-resizer":["display/default","position/default","resize/default","text-max-width/default"],"cl-skeleton":["animation/default","display/default","flex/default","position/default"],"cl-slider":["background-color/default","border-radius/default","display/default","padding/default","position/default","text-color/default"],"cl-spinner":["animation/default","position/default","stroke-color/default"],"cl-steps":["cursor/default","display/default","flex-direction/default","justify-content/default","place-items/default","position/default"],"cl-switch":["align-items/default","display/default","flex-direction/default","justify-content/default","position/default","transition-property/default"],"cl-tabs":["container/default","display/default","flex-direction/default","position/default","resize/default","visibility/default"],"cl-tags":["background-size/default","display/default","flex-direction/default","flex-wrap/default","justify-content/default","min-width/default","place-items/default","position/default"],"cl-textarea":["display/default","flex-direction/default","isolate/default","position/default","transition-property/default"],"cl-theme-generator":["position/default"],"cl-toast":["align-items/default","display/default","flex-direction/default","flex/default","justify-content/default","place-items/default","position/default"],"cl-toggle":["display/default","justify-content/default","place-items/default","position/default","transition-property/default"],"cl-tooltip":["display/default","flex-direction/default","position/default"],"cl-tree":["display/default","flex-direction/default","position/default","visibility/default"],"cl-tree-item":["align-items/default","container/default","display/default","flex-direction/default","position/default"]}');
7090
7090
 
7091
7091
  /***/ }
7092
7092