@rozie-ui/sortable-list-lit 0.1.5

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.
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@rozie-ui/sortable-list-lit",
3
+ "version": "0.1.5",
4
+ "type": "module",
5
+ "private": false,
6
+ "license": "MIT",
7
+ "description": "Idiomatic Lit drag-and-drop sortable list — one Rozie source compiled to Lit via SortableJS.",
8
+ "keywords": [
9
+ "rozie",
10
+ "rozie-ui",
11
+ "sortable",
12
+ "drag-and-drop",
13
+ "sortablejs",
14
+ "list",
15
+ "component",
16
+ "lit"
17
+ ],
18
+ "author": "One Learning Community (https://github.com/One-Learning-Community)",
19
+ "homepage": "https://github.com/One-Learning-Community/rozie.js#readme",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/One-Learning-Community/rozie.js.git",
23
+ "directory": "packages/ui/sortable-list/packages/lit"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/One-Learning-Community/rozie.js/issues"
27
+ },
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.mjs",
30
+ "types": "./dist/index.d.mts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.mts",
34
+ "import": "./dist/index.mjs",
35
+ "require": "./dist/index.cjs"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "src"
41
+ ],
42
+ "dependencies": {
43
+ "@rozie/runtime-lit": "0.1.1"
44
+ },
45
+ "peerDependencies": {
46
+ "lit": "^3.2",
47
+ "@lit-labs/preact-signals": "^1.0.0",
48
+ "@preact/signals-core": "^1.3.0",
49
+ "sortablejs": "^1.15"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "lit": {
53
+ "optional": false
54
+ },
55
+ "@lit-labs/preact-signals": {
56
+ "optional": false
57
+ },
58
+ "@preact/signals-core": {
59
+ "optional": false
60
+ },
61
+ "sortablejs": {
62
+ "optional": false
63
+ }
64
+ },
65
+ "publishConfig": {
66
+ "access": "public"
67
+ },
68
+ "scripts": {
69
+ "build": "tsdown",
70
+ "typecheck": "tsc --noEmit"
71
+ }
72
+ }
@@ -0,0 +1,419 @@
1
+ import { LitElement, css, html } from 'lit';
2
+ import { customElement, property, query, queryAssignedElements, state } from 'lit/decorators.js';
3
+ import { SignalWatcher, signal } from '@lit-labs/preact-signals';
4
+ import { __rozieReconcileAfterDomMutation, createLitControllableProperty, rozieAttr, rozieClass, rozieListeners, rozieSpread, rozieStyle } from '@rozie/runtime-lit';
5
+ import { repeat } from 'lit/directives/repeat.js';
6
+ import { keyed } from 'lit/directives/keyed.js';
7
+ import { useSortableJS } from './internal/useSortableJS';
8
+
9
+ interface RozieDefaultSlotCtx {
10
+ item: unknown;
11
+ index: unknown;
12
+ }
13
+
14
+ @customElement('rozie-sortable-list')
15
+ export default class SortableList extends SignalWatcher(LitElement) {
16
+ static styles = css`
17
+ .rozie-sortable-wrap[data-rozie-s-0af24eae] { display: block; }
18
+ .rozie-sortable-list[data-rozie-s-0af24eae] { display: block; }
19
+ .rozie-sortable-item[data-rozie-s-0af24eae] { display: block; outline: none; }
20
+ .rozie-sortable-item[data-rozie-s-0af24eae]:focus { outline: 2px solid rgba(0, 102, 204, 0.6); outline-offset: -2px; }
21
+ .rozie-sortable-item-lifted[data-rozie-s-0af24eae] {
22
+ background: rgba(0, 102, 204, 0.08);
23
+ box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.4) inset;
24
+ }
25
+ .rozie-sortable-aria-live[data-rozie-s-0af24eae] {
26
+ position: absolute;
27
+ width: 1px;
28
+ height: 1px;
29
+ padding: 0;
30
+ margin: -1px;
31
+ overflow: hidden;
32
+ clip: rect(0, 0, 0, 0);
33
+ white-space: nowrap;
34
+ border: 0;
35
+ }
36
+ `;
37
+
38
+ @property({ type: Array, attribute: 'items' }) _items_attr: any[] = [];
39
+ private _itemsControllable = createLitControllableProperty<any[]>({ host: this, eventName: 'items-change', defaultValue: [], initialControlledValue: undefined });
40
+ @property({ type: String }) itemKey: string | (((...args: unknown[]) => unknown) | null) = null;
41
+ @property({ type: String, reflect: true }) handle: string = null;
42
+ @property({ type: String, reflect: true }) group: string = null;
43
+ @property({ type: Number, reflect: true }) animation: number = 150;
44
+ @property({ type: Boolean, reflect: true }) disabled: boolean = false;
45
+ @property({ type: Boolean, reflect: true }) disableKeyboard: boolean = false;
46
+ @property({ type: Object }) options: any = {};
47
+ @property({ type: Function }) labelFor: ((...args: unknown[]) => unknown) | null = null;
48
+ @property({ type: String, reflect: true }) ghostClass: string = null;
49
+ @property({ type: String, reflect: true }) chosenClass: string = null;
50
+ @property({ type: String, reflect: true }) dragClass: string = null;
51
+ @property({ type: String, reflect: true }) filter: string = null;
52
+ @property({ type: String, reflect: true }) easing: string = null;
53
+ @property({ type: Boolean, reflect: true }) forceFallback: boolean = false;
54
+ @property({ type: Number, reflect: true }) swapThreshold: number = 1;
55
+ @property({ type: Boolean, reflect: true }) cloneable: boolean = false;
56
+ @property({ type: String }) listClass: string | any[] | any = '';
57
+ @property({ type: String }) itemClass: string | any[] | any | (((...args: unknown[]) => unknown) | null) = '';
58
+ @property({ type: String }) itemStyle: string | any | (((...args: unknown[]) => unknown) | null) = null;
59
+ private _liftedIndex = signal(null);
60
+ private _ariaLiveText = signal('');
61
+ @query('[data-rozie-ref="listEl"]') private _refListEl!: HTMLElement;
62
+ @query('[data-rozie-ref="__rozieRoot"]') private _ref__rozieRoot!: HTMLElement;
63
+ private __rozieFirstUpdateDone = false;
64
+
65
+ @state() private _hasSlotHeader = false;
66
+ @queryAssignedElements({ slot: 'header', flatten: true }) private _slotHeaderElements!: Element[];
67
+ @state() private _hasSlotDefault = false;
68
+ @queryAssignedElements({ flatten: true }) private _slotDefaultElements!: Element[];
69
+ @property({ attribute: false }) __rozieDefaultSlot__?: (scope: { item: unknown; index: unknown }) => unknown;
70
+ @state() private _hasSlotFooter = false;
71
+ @queryAssignedElements({ slot: 'footer', flatten: true }) private _slotFooterElements!: Element[];
72
+
73
+ private _disconnectCleanups: Array<() => void> = [];
74
+ // Re-parenting guard: set true once the deferred teardown has actually
75
+ // run (a genuine un-mount), so a subsequent reconnect knows to re-arm.
76
+ private _rozieTornDown = false;
77
+
78
+ _rozieReconcileSeq = 0;
79
+
80
+ private _armListeners(): void {
81
+ {
82
+ const slotEl = this.shadowRoot?.querySelector('slot[name="header"]');
83
+ if (slotEl !== null && slotEl !== undefined) {
84
+ const update = () => { this._hasSlotHeader = this._slotHeaderElements.length > 0; };
85
+ slotEl.addEventListener('slotchange', update);
86
+ // CR-05 fix: push cleanup so the listener is removed on disconnectedCallback.
87
+ this._disconnectCleanups.push(() => slotEl.removeEventListener('slotchange', update));
88
+ update();
89
+ }
90
+ }
91
+
92
+ {
93
+ const slotEl = this.shadowRoot?.querySelector('slot:not([name])');
94
+ if (slotEl !== null && slotEl !== undefined) {
95
+ const update = () => { this._hasSlotDefault = this._slotDefaultElements.length > 0; };
96
+ slotEl.addEventListener('slotchange', update);
97
+ // CR-05 fix: push cleanup so the listener is removed on disconnectedCallback.
98
+ this._disconnectCleanups.push(() => slotEl.removeEventListener('slotchange', update));
99
+ update();
100
+ }
101
+ }
102
+
103
+ {
104
+ const slotEl = this.shadowRoot?.querySelector('slot[name="footer"]');
105
+ if (slotEl !== null && slotEl !== undefined) {
106
+ const update = () => { this._hasSlotFooter = this._slotFooterElements.length > 0; };
107
+ slotEl.addEventListener('slotchange', update);
108
+ // CR-05 fix: push cleanup so the listener is removed on disconnectedCallback.
109
+ this._disconnectCleanups.push(() => slotEl.removeEventListener('slotchange', update));
110
+ update();
111
+ }
112
+ }
113
+ }
114
+
115
+ connectedCallback(): void {
116
+ // Phase 07.3.1 D-LIT-15 — pre-seed _hasSlot<X> from light DOM so first render isn't deadlocked.
117
+ this._hasSlotHeader = Array.from(this.children).some((el) => el.getAttribute('slot') === 'header');
118
+ this._hasSlotDefault = Array.from(this.children).some((el) => !el.hasAttribute('slot') && (el.nodeType !== 3 || (el.textContent?.trim().length ?? 0) > 0));
119
+ this._hasSlotFooter = Array.from(this.children).some((el) => el.getAttribute('slot') === 'footer');
120
+ super.connectedCallback();
121
+ if (this.hasUpdated && this._rozieTornDown) { this._rozieTornDown = false; this._armListeners(); }
122
+ }
123
+
124
+ firstUpdated(): void {
125
+ this._armListeners();
126
+
127
+ this._disconnectCleanups.push((() => this.instance?.destroy()));
128
+
129
+ // Named `sortable` (not `handle`) to avoid shadowing `$props.handle`
130
+ // when the options object below references it.
131
+ const sortable = useSortableJS(this._refListEl, {
132
+ items: () => this.items,
133
+ onCommit: (next: any) => {
134
+ this._itemsControllable.write(next);
135
+ },
136
+ options: {
137
+ animation: this.animation,
138
+ disabled: this.disabled,
139
+ // `cloneable` is a high-level Rozie prop that REPLACES a string
140
+ // `group` with SortableJS's `{ name, pull: 'clone', put: true }`
141
+ // object form. When `cloneable:false`, pass `$props.group` through
142
+ // verbatim. When `cloneable:true` AND `$props.group` is null,
143
+ // leave it null — a clone-mode list without a group name is not
144
+ // meaningful (no peer list can join the cross-list flow).
145
+ group: this.cloneable && typeof this.group === 'string' ? {
146
+ name: this.group,
147
+ pull: 'clone',
148
+ put: true
149
+ } : this.group,
150
+ handle: this.handle,
151
+ ghostClass: this.ghostClass,
152
+ chosenClass: this.chosenClass,
153
+ dragClass: this.dragClass,
154
+ filter: this.filter,
155
+ forceFallback: this.forceFallback,
156
+ swapThreshold: this.swapThreshold,
157
+ easing: this.easing,
158
+ ...this.options
159
+ },
160
+ // Lit lit-html `repeat` directive caches its part array by sentinel-
161
+ // comment node identity; SortableJS's physical DOM mutation desyncs
162
+ // that cache. The sigil lowers to `__rozieReconcileAfterDomMutation(this)`
163
+ // on Lit (real call) and `void 0` on the other 5 targets (no-op).
164
+ afterCommit: () => __rozieReconcileAfterDomMutation(this),
165
+ onChange: ({
166
+ kind,
167
+ oldIndex,
168
+ newIndex,
169
+ item
170
+ }: any) => {
171
+ if (kind === 'reorder') this.dispatchEvent(new CustomEvent("change", {
172
+ detail: {
173
+ oldIndex,
174
+ newIndex,
175
+ item
176
+ },
177
+ bubbles: true,
178
+ composed: true
179
+ }));else if (kind === 'add') this.dispatchEvent(new CustomEvent("add", {
180
+ detail: {
181
+ newIndex,
182
+ item
183
+ },
184
+ bubbles: true,
185
+ composed: true
186
+ }));else if (kind === 'remove') this.dispatchEvent(new CustomEvent("remove", {
187
+ detail: {
188
+ oldIndex,
189
+ item
190
+ },
191
+ bubbles: true,
192
+ composed: true
193
+ }));
194
+ },
195
+ onStart: (e: any) => this.dispatchEvent(new CustomEvent("start", {
196
+ detail: e,
197
+ bubbles: true,
198
+ composed: true
199
+ })),
200
+ onEnd: (e: any) => this.dispatchEvent(new CustomEvent("end", {
201
+ detail: e,
202
+ bubbles: true,
203
+ composed: true
204
+ }))
205
+ });
206
+ this.instance = sortable.instance;
207
+ // $onMount's cleanup-return: closing over a setup-local (`sortable`) does
208
+ // not survive the Solid emitter's setup/cleanup split — it scopes cleanup
209
+ // outside the setup IIFE. Closing over `instance` (a module-scope `let`)
210
+ // works on every target.
211
+ }
212
+
213
+ updated(changedProperties: Map<string, unknown>): void {
214
+ if (this.__rozieFirstUpdateDone && (changedProperties.has('disabled'))) { const __watchVal = (() => this.disabled)(); ((v: any) => this.instance?.option('disabled', v))(__watchVal); }
215
+ if (this.__rozieFirstUpdateDone && (changedProperties.has('group'))) { const __watchVal = (() => this.group)(); ((v: any) => this.instance?.option('group', v))(__watchVal); }
216
+ if (this.__rozieFirstUpdateDone && (changedProperties.has('handle'))) { const __watchVal = (() => this.handle)(); ((v: any) => this.instance?.option('handle', v))(__watchVal); }
217
+ if (this.__rozieFirstUpdateDone && (changedProperties.has('ghostClass'))) { const __watchVal = (() => this.ghostClass)(); ((v: any) => this.instance?.option('ghostClass', v))(__watchVal); }
218
+ if (this.__rozieFirstUpdateDone && (changedProperties.has('chosenClass'))) { const __watchVal = (() => this.chosenClass)(); ((v: any) => this.instance?.option('chosenClass', v))(__watchVal); }
219
+ if (this.__rozieFirstUpdateDone && (changedProperties.has('dragClass'))) { const __watchVal = (() => this.dragClass)(); ((v: any) => this.instance?.option('dragClass', v))(__watchVal); }
220
+ if (this.__rozieFirstUpdateDone && (changedProperties.has('filter'))) { const __watchVal = (() => this.filter)(); ((v: any) => this.instance?.option('filter', v))(__watchVal); }
221
+ if (this.__rozieFirstUpdateDone && (changedProperties.has('easing'))) { const __watchVal = (() => this.easing)(); ((v: any) => this.instance?.option('easing', v))(__watchVal); }
222
+ this.__rozieFirstUpdateDone = true;
223
+ }
224
+
225
+ disconnectedCallback(): void {
226
+ super.disconnectedCallback();
227
+ queueMicrotask(() => {
228
+ if (this.isConnected || this._rozieTornDown) return;
229
+ this._rozieTornDown = true;
230
+ for (const fn of this._disconnectCleanups) fn();
231
+ this._disconnectCleanups = [];
232
+ });
233
+ }
234
+
235
+ attributeChangedCallback(name: string, old: string | null, value: string | null): void {
236
+ super.attributeChangedCallback(name, old, value);
237
+ if (name === 'items') this._itemsControllable.notifyAttributeChange(value as unknown as any[]);
238
+ }
239
+
240
+ render() {
241
+ return html`
242
+ <div class="rozie-sortable-wrap" ${rozieSpread(this.$attrs)} ${rozieListeners(this.$listeners)} data-rozie-ref="__rozieRoot" data-rozie-s-0af24eae>
243
+ <div class="${(rozieClass(['rozie-sortable-list', this.listClass]))}" part="list" data-rozie-ref="listEl" data-rozie-s-0af24eae>${keyed(this._rozieReconcileSeq ?? 0, html`
244
+ <slot name="header"></slot>
245
+ ${repeat<any>(this.items, (item, index) => this.keyFor(item, index), (item, index) => html`<div class="${(rozieClass(['rozie-sortable-item', this.itemClassFor(item, index), { 'rozie-sortable-item-lifted': this._liftedIndex.value === index }]))}" key=${rozieAttr(this.keyFor(item, index))} style=${rozieStyle(this.itemStyleFor(item, index))} data-id=${rozieAttr(this.keyFor(item, index))} role="listitem" tabindex=${rozieAttr(this.keyboardEnabled() ? 0 : null)} @keydown=${($event: Event) => { this.onRowKeyDown($event, index); }} data-rozie-s-0af24eae>
246
+ ${this.__rozieDefaultSlot__ !== undefined ? this.__rozieDefaultSlot__({item: item, index: index}) : html`<slot data-rozie-params=${(() => { try { return JSON.stringify({item: item, index: index}); } catch { return '{}'; } })()}></slot>`}
247
+ </div>`)}
248
+ <slot name="footer"></slot>
249
+ `)}</div>
250
+ <div class="rozie-sortable-aria-live" data-rozie-sortable-aria-live="" aria-live="polite" aria-atomic="true" data-rozie-s-0af24eae>${this._ariaLiveText.value}</div>
251
+ </div>
252
+ `;
253
+ }
254
+
255
+ instance: any = null;
256
+
257
+ __rowKey = {
258
+ map: new WeakMap(),
259
+ seq: 0
260
+ };
261
+
262
+ keyFor = (item: any, index: any) => {
263
+ // (a) function itemKey: consumer-supplied (item, index) => key.
264
+ if (typeof this.itemKey === 'function') {
265
+ return this.itemKey(item, index);
266
+ }
267
+ // (b) string itemKey: a property name on a non-null object item.
268
+ if (typeof this.itemKey === 'string' && item !== null && typeof item === 'object' && item[this.itemKey] != null) {
269
+ return item[this.itemKey];
270
+ }
271
+ // (c) id-less object (or function) item: assign-on-first-sight WeakMap
272
+ // synthetic id. Survives reorder because it is keyed by object identity.
273
+ if (item !== null && typeof item === 'object' || typeof item === 'function') {
274
+ if (!this.__rowKey.map.has(item)) {
275
+ this.__rowKey.map.set(item, '__rk' + this.__rowKey.seq++);
276
+ }
277
+ return this.__rowKey.map.get(item);
278
+ }
279
+ // (d) primitive item: fall back to index. NOTE: duplicate primitives are
280
+ // unsafe to reorder this way — pass a function itemKey for those.
281
+ return index;
282
+ };
283
+
284
+ itemClassFor = (item: any, index: any) => {
285
+ const v = this.itemClass;
286
+ return typeof v === 'function' ? v(item, index) : v;
287
+ };
288
+
289
+ itemStyleFor = (item: any, index: any) => {
290
+ const s = typeof this.itemStyle === 'function' ? this.itemStyle(item, index) : this.itemStyle;
291
+ return s == null || s === '' ? null : s;
292
+ };
293
+
294
+ getLabel = (idx: any) => {
295
+ const item = this.items[idx];
296
+ if (this.labelFor !== null) return this.labelFor(item, idx);
297
+ if (item !== null && typeof item === 'object' && 'label' in item) return item.label;
298
+ return String(item);
299
+ };
300
+
301
+ keyboardEnabled = () => !this.disabled && !this.disableKeyboard;
302
+
303
+ onRowKeyDown = ($event: any, index: any) => {
304
+ // Defense-in-depth: when keyboard reordering is off the rows carry no
305
+ // tabindex and can't receive focus, but a consumer-focused row (or a
306
+ // programmatic .focus()) must still no-op here rather than reorder.
307
+ if (!this.keyboardEnabled()) return;
308
+ const key = $event.key;
309
+ // Space (' ' on browsers; KeyboardEvent.key === ' ') OR Enter — lift/drop.
310
+ if (key === ' ' || key === 'Spacebar' || key === 'Enter') {
311
+ $event.preventDefault();
312
+ if (this._liftedIndex.value === null) {
313
+ // LIFT
314
+ this._liftedIndex.value = index;
315
+ this._ariaLiveText.value = 'Lifted ' + this.getLabel(index);
316
+ return;
317
+ }
318
+ // DROP
319
+ const dropped = this.getLabel(this._liftedIndex.value);
320
+ const at = this._liftedIndex.value;
321
+ this._liftedIndex.value = null;
322
+ this._ariaLiveText.value = 'Dropped ' + dropped + ' at position ' + (at + 1);
323
+ return;
324
+ }
325
+ if (key === 'Escape') {
326
+ if (this._liftedIndex.value === null) return;
327
+ $event.preventDefault();
328
+ const cancelled = this.getLabel(this._liftedIndex.value);
329
+ this._liftedIndex.value = null;
330
+ this._ariaLiveText.value = 'Cancelled lift of ' + cancelled;
331
+ return;
332
+ }
333
+ if (key === 'ArrowDown' || key === 'ArrowUp') {
334
+ if (this._liftedIndex.value === null) return;
335
+ $event.preventDefault();
336
+ const dir = key === 'ArrowDown' ? 1 : -1;
337
+ const from = this._liftedIndex.value;
338
+ const to = from + dir;
339
+ if (to < 0 || to >= this.items.length) return;
340
+ const next = [...this.items];
341
+ const [moved] = next.splice(from, 1);
342
+ next.splice(to, 0, moved);
343
+ this._itemsControllable.write(next);
344
+ this._liftedIndex.value = to;
345
+ this._ariaLiveText.value = 'Moved ' + this.getLabel(to) + ' to position ' + (to + 1);
346
+ // After the keyed reorder write, restore focus to the moved row. No-op
347
+ // on React/Vue/Angular (DOM identity preserved); queueMicrotask +
348
+ // querySelectorAll + .focus() on Svelte/Solid/Lit (DOM re-created).
349
+ queueMicrotask(() => (this.renderRoot.querySelectorAll('[role="listitem"]')?.[to] as HTMLElement | undefined)?.focus?.());
350
+ this.dispatchEvent(new CustomEvent("change", {
351
+ detail: {
352
+ oldIndex: from,
353
+ newIndex: to,
354
+ item: moved
355
+ },
356
+ bubbles: true,
357
+ composed: true
358
+ }));
359
+ }
360
+ };
361
+
362
+ getInstance() {
363
+ return this.instance;
364
+ }
365
+
366
+ toArray() {
367
+ return this.instance ? this.instance.toArray() : [];
368
+ }
369
+
370
+ sort(order: any, useAnimation = true) {
371
+ this.instance?.sort(order, useAnimation);
372
+ }
373
+
374
+ option(name: any, value: any) {
375
+ if (!this.instance) return undefined;
376
+ if (value === undefined) return this.instance.option(name);
377
+ this.instance.option(name, value);
378
+ return value;
379
+ }
380
+
381
+ get items(): any[] { return this._itemsControllable.read(); }
382
+ set items(v: any[]) { this._itemsControllable.notifyPropertyWrite(v); }
383
+
384
+ /**
385
+ * Plan 14-05 — cross-framework attribute fallthrough source. Reads the
386
+ * host custom element's attributes on each call so a consumer-side bound
387
+ * attribute flows through on every render. The `rozieSpread` directive
388
+ * (D-02) does the cross-render diff downstream.
389
+ *
390
+ * Phase 15 follow-up Bug A — declared-prop attribute names are filtered
391
+ * out so `$attrs` returns "rest after declared props" (semantic parity
392
+ * with React/Vue/Svelte/Solid/Angular). Both Lit attribute-naming
393
+ * forms are folded into the skip set: kebab-case for model props
394
+ * (explicit `attribute:`) AND lowercased property name (Lit's default).
395
+ */
396
+ private get $attrs(): Record<string, string> {
397
+ const __skip = new Set<string>(['items', 'item-key', 'itemkey', 'handle', 'group', 'animation', 'disabled', 'disable-keyboard', 'disablekeyboard', 'options', 'label-for', 'labelfor', 'ghost-class', 'ghostclass', 'chosen-class', 'chosenclass', 'drag-class', 'dragclass', 'filter', 'easing', 'force-fallback', 'forcefallback', 'swap-threshold', 'swapthreshold', 'cloneable', 'list-class', 'listclass', 'item-class', 'itemclass', 'item-style', 'itemstyle']);
398
+ const out: Record<string, string> = {};
399
+ for (const a of Array.from(this.attributes)) {
400
+ if (__skip.has(a.name)) continue;
401
+ out[a.name] = a.value;
402
+ }
403
+ return out;
404
+ }
405
+
406
+ /**
407
+ * Phase 15 D-19 — consumer-passed listener cluster placeholder.
408
+ * Lit attaches event listeners directly on the host element via
409
+ * `addEventListener` (no per-instance prop rest binding), so the
410
+ * runtime value is undefined; the `rozieListeners` directive's
411
+ * nullish coercion (`obj ?? {}`) handles the no-op cleanly.
412
+ * The declaration exists to satisfy `tsc --noEmit` on consumer
413
+ * projects with strict mode — bare `$listeners` in `render()`
414
+ * would otherwise raise TS2304 (Cannot find name).
415
+ */
416
+ private get $listeners(): Record<string, EventListener> | undefined {
417
+ return undefined;
418
+ }
419
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { default as SortableList } from './SortableList';
2
+ export { default } from './SortableList';