@kubex/zinc 1.0.24 → 1.0.98

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 (198) hide show
  1. package/.claude/project.md +556 -0
  2. package/.github/workflows/js_build_and_deploy.yaml +9 -10
  3. package/dist/custom-elements.json +13431 -8728
  4. package/dist/vscode.html-custom-data.json +571 -37
  5. package/dist/web-types.json +1439 -181
  6. package/dist/zn.d.ts +1642 -661
  7. package/dist/zn.min.css +1 -1
  8. package/dist/zn.min.js +1407 -887
  9. package/docs/_utilities/code-previews.cjs +10 -1
  10. package/docs/assets/scripts/docs.js +42 -18
  11. package/docs/data/data-table.json +229 -66
  12. package/docs/data/empty.json +6 -0
  13. package/docs/data/products-table.json +41 -193
  14. package/docs/pages/components/action-bar.md +121 -6
  15. package/docs/pages/components/alert.md +118 -15
  16. package/docs/pages/components/animated-button.md +299 -0
  17. package/docs/pages/components/audio-select.md +534 -0
  18. package/docs/pages/components/bulk-actions.md +79 -5
  19. package/docs/pages/components/button-group.md +235 -27
  20. package/docs/pages/components/button.md +139 -1
  21. package/docs/pages/components/chart.md +365 -9
  22. package/docs/pages/components/checkbox.md +387 -58
  23. package/docs/pages/components/chip.md +253 -33
  24. package/docs/pages/components/collapsible.md +393 -46
  25. package/docs/pages/components/cols.md +152 -30
  26. package/docs/pages/components/confirm.md +182 -16
  27. package/docs/pages/components/copy-button.md +169 -6
  28. package/docs/pages/components/data-select.md +194 -15
  29. package/docs/pages/components/data-table-filter.md +96 -5
  30. package/docs/pages/components/data-table-search.md +451 -0
  31. package/docs/pages/components/data-table-sort.md +66 -5
  32. package/docs/pages/components/data-table.md +727 -66
  33. package/docs/pages/components/datepicker.md +402 -7
  34. package/docs/pages/components/defined-label.md +120 -5
  35. package/docs/pages/components/dialog.md +339 -59
  36. package/docs/pages/components/dropdown.md +366 -11
  37. package/docs/pages/components/editor.md +604 -7
  38. package/docs/pages/components/empty-state.md +315 -16
  39. package/docs/pages/components/expanding-action.md +212 -28
  40. package/docs/pages/components/file.md +421 -12
  41. package/docs/pages/components/filter-container.md +158 -5
  42. package/docs/pages/components/filter-wrapper.md +167 -6
  43. package/docs/pages/components/form-group.md +383 -6
  44. package/docs/pages/components/hover-container.md +237 -6
  45. package/docs/pages/components/icon-picker.md +121 -0
  46. package/docs/pages/components/icon.md +454 -48
  47. package/docs/pages/components/inline-edit.md +787 -19
  48. package/docs/pages/components/input-group.md +207 -13
  49. package/docs/pages/components/input.md +194 -0
  50. package/docs/pages/components/key-container.md +115 -0
  51. package/docs/pages/components/key.md +101 -0
  52. package/docs/pages/components/menu.md +512 -6
  53. package/docs/pages/components/note.md +239 -36
  54. package/docs/pages/components/opt-group.md +107 -0
  55. package/docs/pages/components/pagination.md +332 -6
  56. package/docs/pages/components/pane.md +222 -6
  57. package/docs/pages/components/panel.md +213 -26
  58. package/docs/pages/components/popup.md +1354 -6
  59. package/docs/pages/components/priority-list.md +233 -0
  60. package/docs/pages/components/progress-bar.md +387 -6
  61. package/docs/pages/components/radio-group.md +242 -5
  62. package/docs/pages/components/radio.md +474 -6
  63. package/docs/pages/components/rating.md +331 -7
  64. package/docs/pages/components/reveal.md +524 -6
  65. package/docs/pages/components/scroll-container.md +552 -6
  66. package/docs/pages/components/select.md +762 -160
  67. package/docs/pages/components/skeleton.md +473 -30
  68. package/docs/pages/components/slideout.md +261 -5
  69. package/docs/pages/components/sp.md +99 -41
  70. package/docs/pages/components/split-button.md +347 -9
  71. package/docs/pages/components/split-pane.md +445 -6
  72. package/docs/pages/components/stat.md +549 -9
  73. package/docs/pages/components/status-indicator.md +230 -1
  74. package/docs/pages/components/stepper.md +563 -6
  75. package/docs/pages/components/tabs.md +439 -13
  76. package/docs/pages/components/textarea.md +415 -17
  77. package/docs/pages/components/tile.md +385 -26
  78. package/docs/pages/components/toggle.md +424 -6
  79. package/docs/pages/components/tooltip.md +367 -0
  80. package/docs/pages/components/translation-group.md +210 -0
  81. package/docs/pages/components/translations.md +319 -18
  82. package/docs/pages/components/vertical-stepper.md +401 -8
  83. package/docs/pages/components/well.md +300 -3
  84. package/docs/pages/getting-started/form-controls.md +162 -0
  85. package/docs/pages/index.md +116 -15
  86. package/package.json +5 -6
  87. package/scss/_global-spacing.scss +4 -0
  88. package/scss/_root.scss +2 -2
  89. package/scss/boot.scss +19 -0
  90. package/scss/shared/layout.scss +5 -0
  91. package/scss/themes/_light.scss +3 -3
  92. package/src/components/alert/alert.scss +1 -0
  93. package/src/components/animated-button/README.md +306 -0
  94. package/src/components/animated-button/animated-button.component.ts +229 -0
  95. package/src/components/animated-button/animated-button.scss +228 -0
  96. package/src/components/animated-button/animated-button.test.ts +143 -0
  97. package/src/components/animated-button/index.ts +12 -0
  98. package/src/components/button/button.component.ts +5 -0
  99. package/src/components/button-group/button-group.component.ts +25 -13
  100. package/src/components/button-group/button-group.scss +6 -0
  101. package/src/components/checkbox/checkbox.component.ts +4 -0
  102. package/src/components/checkbox/checkbox.scss +7 -4
  103. package/src/components/checkbox-group/checkbox-group.scss +7 -0
  104. package/src/components/chip/chip.component.ts +7 -2
  105. package/src/components/chip/chip.scss +16 -2
  106. package/src/components/collapsible/collapsible.component.ts +27 -20
  107. package/src/components/cols/cols.component.ts +14 -2
  108. package/src/components/cols/cols.scss +6 -12
  109. package/src/components/confirm/confirm.component.ts +7 -0
  110. package/src/components/copy-button/copy-button.component.ts +12 -11
  111. package/src/components/data-select/data-select.component.ts +215 -44
  112. package/src/components/data-select/data-select.scss +66 -6
  113. package/src/components/data-select/providers/currency-data-provider.ts +20 -6
  114. package/src/components/data-select/providers/provider.ts +1 -0
  115. package/src/components/data-select/providers/us-state-data-provider.ts +68 -0
  116. package/src/components/data-table/data-table.component.ts +82 -23
  117. package/src/components/data-table-search/data-table-search.component.ts +184 -0
  118. package/src/components/data-table-search/data-table-search.scss +17 -0
  119. package/src/components/data-table-search/data-table-search.test.ts +11 -0
  120. package/src/components/data-table-search/index.ts +12 -0
  121. package/src/components/datepicker/datepicker.component.ts +347 -13
  122. package/src/components/dialog/dialog.component.ts +1 -1
  123. package/src/components/empty-state/empty-state.scss +6 -1
  124. package/src/components/header/header.scss +3 -3
  125. package/src/components/icon/icon.component.ts +23 -1
  126. package/src/components/icon/icon.scss +23 -0
  127. package/src/components/icon-picker/brand-icons.ts +24 -0
  128. package/src/components/icon-picker/icon-picker.component.ts +407 -0
  129. package/src/components/icon-picker/icon-picker.scss +205 -0
  130. package/src/components/icon-picker/index.ts +12 -0
  131. package/src/components/icon-picker/line-icons.ts +612 -0
  132. package/src/components/icon-picker/material-icons.ts +14543 -0
  133. package/src/components/inline-edit/inline-edit.component.ts +190 -46
  134. package/src/components/inline-edit/inline-edit.scss +29 -0
  135. package/src/components/inline-edit/inline-edit.test.ts +314 -1
  136. package/src/components/input/input.component.ts +328 -5
  137. package/src/components/input/input.scss +100 -4
  138. package/src/components/input-group/input-group.component.ts +28 -7
  139. package/src/components/input-group/input-group.scss +48 -10
  140. package/src/components/item/item.component.ts +11 -2
  141. package/src/components/item/item.scss +7 -0
  142. package/src/components/key/index.ts +12 -0
  143. package/src/components/key/key.component.ts +47 -0
  144. package/src/components/key/key.scss +38 -0
  145. package/src/components/key-container/index.ts +12 -0
  146. package/src/components/key-container/key-container.component.ts +142 -0
  147. package/src/components/key-container/key-container.scss +33 -0
  148. package/src/components/menu-item/menu-item.component.ts +20 -0
  149. package/src/components/menu-item/menu-item.scss +69 -2
  150. package/src/components/navbar/navbar.component.ts +103 -8
  151. package/src/components/navbar/navbar.scss +6 -1
  152. package/src/components/opt-group/index.ts +12 -0
  153. package/src/components/opt-group/opt-group.component.ts +74 -0
  154. package/src/components/opt-group/opt-group.scss +37 -0
  155. package/src/components/opt-group/opt-group.test.ts +27 -0
  156. package/src/components/option/option.component.ts +1 -16
  157. package/src/components/option/option.scss +4 -0
  158. package/src/components/panel/panel.component.ts +10 -8
  159. package/src/components/panel/panel.scss +20 -0
  160. package/src/components/priority-list/index.ts +12 -0
  161. package/src/components/priority-list/priority-list.component.ts +611 -0
  162. package/src/components/priority-list/priority-list.scss +185 -0
  163. package/src/components/priority-list/priority-list.test.ts +139 -0
  164. package/src/components/progress-bar/progress-bar.component.ts +22 -16
  165. package/src/components/radio/radio.scss +10 -3
  166. package/src/components/select/select.component.ts +812 -72
  167. package/src/components/select/select.scss +108 -2
  168. package/src/components/select/select.test.ts +147 -2
  169. package/src/components/settings-container/settings-container.component.ts +44 -19
  170. package/src/components/settings-container/settings-container.scss +1 -1
  171. package/src/components/skeleton/skeleton.component.ts +6 -1
  172. package/src/components/slideout/slideout.scss +8 -3
  173. package/src/components/sp/sp.component.ts +3 -1
  174. package/src/components/sp/sp.scss +19 -0
  175. package/src/components/stepper/stepper.component.ts +16 -10
  176. package/src/components/textarea/textarea.scss +0 -4
  177. package/src/components/tile/tile.component.ts +12 -3
  178. package/src/components/tile/tile.scss +1 -1
  179. package/src/components/timer/timer.component.ts +15 -2
  180. package/src/components/toggle/toggle.component.ts +29 -26
  181. package/src/components/toggle/toggle.scss +13 -1
  182. package/src/components/translation-group/index.ts +12 -0
  183. package/src/components/translation-group/translation-group.component.ts +207 -0
  184. package/src/components/translation-group/translation-group.scss +8 -0
  185. package/src/components/translations/translations.component.ts +90 -42
  186. package/src/components/translations/translations.scss +5 -1
  187. package/src/components/vertical-stepper/vertical-stepper.component.ts +14 -9
  188. package/src/events/events.ts +12 -7
  189. package/src/events/zn-language-change.ts +7 -0
  190. package/src/events/zn-purchase.ts +11 -0
  191. package/src/events/zn-redirect.ts +7 -0
  192. package/src/events/zn-reorder.ts +7 -0
  193. package/src/events/zn-search-change.ts +11 -0
  194. package/src/events/zn-submit.ts +1 -1
  195. package/src/form-control.scss +4 -0
  196. package/src/internal/form-navigation.ts +510 -0
  197. package/src/internal/form.ts +6 -0
  198. package/src/zinc.ts +91 -83
@@ -0,0 +1,611 @@
1
+ import {classMap} from "lit/directives/class-map.js";
2
+ import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
3
+ import {
4
+ FormControlController,
5
+ validValidityState,
6
+ } from "../../internal/form";
7
+ import {HasSlotController} from '../../internal/slot';
8
+ import {property, query, state} from 'lit/decorators.js';
9
+ import {repeat} from "lit/directives/repeat.js";
10
+ import ZincElement from '../../internal/zinc-element';
11
+ import ZnIcon from '../icon';
12
+ import type {ZincFormControl} from '../../internal/zinc-element';
13
+
14
+ import styles from './priority-list.scss';
15
+
16
+ export interface PriorityItem {
17
+ key: string;
18
+ priority: number;
19
+ }
20
+
21
+ /**
22
+ * @summary A reorderable list where each item receives a numerical priority based on its position. Supports drag-and-drop
23
+ * and keyboard reordering. Priority values are submitted as form data via hidden inputs.
24
+ *
25
+ * @documentation https://zinc.style/components/priority-list
26
+ * @status experimental
27
+ * @since 1.0
28
+ *
29
+ * @dependency zn-icon
30
+ *
31
+ * @slot - The default slot where list items are placed. Each slotted element must have a `value` attribute that
32
+ * uniquely identifies the item. The component automatically assigns slot names to project each item into the
33
+ * correct position. The item's position in the list determines its priority (top = `priority-start`, incrementing).
34
+ * @slot label - The component's label. Required for proper accessibility. Alternatively, use the `label` attribute.
35
+ * @slot label-tooltip - Used to add text displayed in a tooltip next to the label.
36
+ * @slot help-text - Text that describes how to use the component. Alternatively, use the `help-text` attribute.
37
+ *
38
+ * @event zn-change - Emitted when the order of items changes.
39
+ * @event zn-reorder - Emitted when items are reordered. Call `getPriorityMap()` on the component to get
40
+ * the updated `{ key: string, priority: number }[]` array.
41
+ *
42
+ * @csspart form-control - The form control wrapper.
43
+ * @csspart form-control-label - The label wrapper.
44
+ * @csspart form-control-input - The input area wrapper.
45
+ * @csspart form-control-help-text - The help text wrapper.
46
+ * @csspart list - The list container.
47
+ * @csspart item - An individual list item row.
48
+ * @csspart drag-handle - The drag handle icon.
49
+ * @csspart priority - The priority number badge.
50
+ * @csspart content - The content area of an item.
51
+ *
52
+ * @cssproperty --zn-priority-list-item-gap - The gap between list items. Defaults to `var(--zn-spacing-2x-small)`.
53
+ * @cssproperty --zn-priority-list-item-padding - The padding inside each item. Defaults to `var(--zn-spacing-small) var(--zn-spacing-medium)`.
54
+ * @cssproperty --zn-priority-list-handle-color - The color of the drag handle. Defaults to `var(--zn-color-neutral-500)`.
55
+ * @cssproperty --zn-priority-list-priority-color - The color of the priority number. Defaults to `var(--zn-color-neutral-600)`.
56
+ */
57
+ export default class ZnPriorityList extends ZincElement implements ZincFormControl {
58
+ static dependencies = {
59
+ 'zn-icon': ZnIcon,
60
+ };
61
+
62
+ static styles: CSSResultGroup = unsafeCSS(styles);
63
+
64
+ protected readonly formControlController = new FormControlController(this, {
65
+ value: (control: ZnPriorityList) => {
66
+ return JSON.stringify(control.getPriorityMap());
67
+ },
68
+ defaultValue: (control: ZnPriorityList) => control.defaultValue,
69
+ setValue: (control: ZnPriorityList, value: string) => {
70
+ if (value) {
71
+ try {
72
+ const parsed: unknown = JSON.parse(value);
73
+ if (Array.isArray(parsed) && parsed.every((item: unknown): item is string => typeof item === 'string')) {
74
+ control.value = parsed as string[];
75
+ }
76
+ } catch {
77
+ // ignore parse errors
78
+ }
79
+ }
80
+ },
81
+ });
82
+
83
+ private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
84
+
85
+ @query('.priority-list__hidden-inputs') hiddenInputsContainer: HTMLDivElement;
86
+
87
+ /**
88
+ * The name prefix for form submission. Each item generates a hidden input named `{name}[{itemValue}]`
89
+ * with the priority number as its value.
90
+ */
91
+ @property() name = '';
92
+
93
+ /**
94
+ * The current ordered list of item keys, reflecting the visual order. This is the source of truth for ordering.
95
+ * Each entry should match a `value` attribute on a slotted child element.
96
+ */
97
+ @property({type: Array})
98
+ get value(): string[] {
99
+ return this._value;
100
+ }
101
+
102
+ set value(val: string[]) {
103
+ const oldVal = this._value;
104
+ this._value = [...val];
105
+ this._assignSlotNames();
106
+ this.requestUpdate('value', oldVal);
107
+ }
108
+
109
+ private _value: string[] = [];
110
+
111
+ @state() defaultValue: string[] = [];
112
+
113
+ /** The component's label. Required for proper accessibility. */
114
+ @property() label = '';
115
+
116
+ /** Text that appears in a tooltip next to the label. */
117
+ @property({attribute: 'label-tooltip'}) labelTooltip = '';
118
+
119
+ /** Help text that describes how to use the component. */
120
+ @property({attribute: 'help-text'}) helpText = '';
121
+
122
+ /**
123
+ * Associates the control with a form by id. The form must be in the same document or shadow root.
124
+ */
125
+ @property({reflect: true}) form = '';
126
+
127
+ /** The size of the list items. */
128
+ @property({reflect: true}) size: 'small' | 'medium' | 'large' = 'medium';
129
+
130
+ /** Whether the priority list is disabled. */
131
+ @property({type: Boolean, reflect: true}) disabled = false;
132
+
133
+ /** Ensures the form control has a value before allowing submission. */
134
+ @property({type: Boolean, reflect: true}) required = false;
135
+
136
+ /** The starting priority number. Defaults to 1. */
137
+ @property({type: Number, attribute: 'priority-start'}) priorityStart = 1;
138
+
139
+ /**
140
+ * When set, the associated form will be submitted with the given action URL whenever items are reordered.
141
+ * If set to an empty string, the form will be submitted using its existing action.
142
+ */
143
+ @property({attribute: 'formaction'}) formAction: string;
144
+
145
+ /**
146
+ * A comma-separated list of item keys defining the initial display order.
147
+ * Keys must match the `value` attributes on slotted children.
148
+ * Any slotted items not listed are appended at the end in DOM order.
149
+ */
150
+ @property() order = '';
151
+
152
+ // Drag state
153
+ @state() private draggedKey: string | null = null;
154
+ @state() private isDragging = false;
155
+ private dragOverKey: string | null = null;
156
+
157
+ /** Gets the validity state object. */
158
+ get validity(): ValidityState {
159
+ return validValidityState;
160
+ }
161
+
162
+ /** Gets the validation message. */
163
+ get validationMessage(): string {
164
+ return '';
165
+ }
166
+
167
+ connectedCallback() {
168
+ super.connectedCallback();
169
+ this.defaultValue = [...this._value];
170
+ }
171
+
172
+ protected firstUpdated(_changedProperties: PropertyValues) {
173
+ super.firstUpdated(_changedProperties);
174
+ this.formControlController.updateValidity();
175
+ this._initializeOrderFromSlot();
176
+ }
177
+
178
+ /**
179
+ * Initialize item order from slotted children if no explicit value was provided.
180
+ * If an `order` attribute is set, use it to define the initial order,
181
+ * appending any slotted items not listed in the order at the end.
182
+ */
183
+ private _initializeOrderFromSlot() {
184
+ if (this._value.length === 0) {
185
+ const items = this._getSlottedItems();
186
+ const domKeys = items.map(el => el.getAttribute('value') || '').filter(v => v !== '');
187
+
188
+ if (this.order) {
189
+ const orderedKeys = this.order.split(',').map(k => k.trim()).filter(k => k !== '');
190
+ const domKeySet = new Set(domKeys);
191
+ // Only include keys that exist in the DOM
192
+ const validOrdered = orderedKeys.filter(k => domKeySet.has(k));
193
+ // Append any DOM items not listed in the order attribute
194
+ const orderedSet = new Set(validOrdered);
195
+ const remaining = domKeys.filter(k => !orderedSet.has(k));
196
+ this._value = [...validOrdered, ...remaining];
197
+ } else {
198
+ this._value = domKeys;
199
+ }
200
+
201
+ this.defaultValue = [...this._value];
202
+ }
203
+ this._assignSlotNames();
204
+ this._syncHiddenInputs();
205
+ this.requestUpdate();
206
+ }
207
+
208
+ /**
209
+ * Gets all direct children with a `value` attribute (excluding those in reserved slots).
210
+ */
211
+ private _getSlottedItems(): HTMLElement[] {
212
+ const reservedSlots = ['label', 'label-tooltip', 'help-text'];
213
+ return [...this.querySelectorAll<HTMLElement>(':scope > [value]')].filter(el => {
214
+ const slot = el.getAttribute('slot');
215
+ // Exclude elements in reserved semantic slots; allow unslotted or internally assigned items
216
+ return !slot || slot.startsWith('_item-') || !reservedSlots.includes(slot);
217
+ });
218
+ }
219
+
220
+ /**
221
+ * Assigns `slot` attributes to light DOM children based on their `value` attribute,
222
+ * so they project into the correct named slot in the shadow DOM.
223
+ */
224
+ private _assignSlotNames() {
225
+ const items = [...this.querySelectorAll<HTMLElement>(':scope > [value]')];
226
+ items.forEach(el => {
227
+ const itemValue = el.getAttribute('value');
228
+ if (itemValue && !['label', 'label-tooltip', 'help-text'].includes(itemValue)) {
229
+ el.setAttribute('slot', `_item-${itemValue}`);
230
+ }
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Returns the priority map: an array of { key, priority } objects.
236
+ */
237
+ getPriorityMap(): PriorityItem[] {
238
+ return this._value.map((key, index) => ({
239
+ key,
240
+ priority: this.priorityStart + index,
241
+ }));
242
+ }
243
+
244
+ /**
245
+ * Syncs hidden inputs so form data includes priority values.
246
+ */
247
+ private _syncHiddenInputs() {
248
+ if (!this.hiddenInputsContainer) return;
249
+
250
+ this.hiddenInputsContainer.innerHTML = '';
251
+
252
+ if (!this.name) return;
253
+
254
+ this._value.forEach((key, index) => {
255
+ const input = document.createElement('input');
256
+ input.type = 'hidden';
257
+ input.name = `${this.name}[${key}]`;
258
+ input.value = String(this.priorityStart + index);
259
+ this.hiddenInputsContainer.appendChild(input);
260
+ });
261
+ }
262
+
263
+ /**
264
+ * Moves an item from one index to another.
265
+ */
266
+ private _moveItem(fromIndex: number, toIndex: number) {
267
+ if (fromIndex === toIndex) return;
268
+ if (fromIndex < 0 || toIndex < 0) return;
269
+ if (fromIndex >= this._value.length || toIndex >= this._value.length) return;
270
+
271
+ const newOrder = [...this._value];
272
+ const [moved] = newOrder.splice(fromIndex, 1);
273
+ newOrder.splice(toIndex, 0, moved);
274
+
275
+ this._value = newOrder;
276
+ this._assignSlotNames();
277
+ this._syncHiddenInputs();
278
+ this.requestUpdate();
279
+
280
+ this.emit('zn-change');
281
+ this.emit('zn-reorder');
282
+ this._submitForm();
283
+ }
284
+
285
+ /**
286
+ * Submits the associated form if a `formaction` attribute is set.
287
+ */
288
+ private _submitForm() {
289
+ if (this.formAction === undefined || this.formAction === null) return;
290
+
291
+ const form = this.getForm();
292
+ if (form) {
293
+ const submitter = document.createElement('button');
294
+ submitter.type = 'submit';
295
+ submitter.hidden = true;
296
+ if (this.formAction) {
297
+ submitter.setAttribute('formaction', this.formAction);
298
+ }
299
+ form.appendChild(submitter);
300
+ form.requestSubmit(submitter);
301
+ submitter.remove();
302
+ } else if (this.formAction) {
303
+ // No parent form — create a temporary one to submit the priority data
304
+ const tempForm = document.createElement('form');
305
+ tempForm.method = 'post';
306
+ tempForm.action = this.formAction;
307
+ tempForm.hidden = true;
308
+
309
+ this._value.forEach((key, index) => {
310
+ if (this.name) {
311
+ const input = document.createElement('input');
312
+ input.type = 'hidden';
313
+ input.name = `${this.name}[${key}]`;
314
+ input.value = String(this.priorityStart + index);
315
+ tempForm.appendChild(input);
316
+ }
317
+ });
318
+
319
+ document.body.appendChild(tempForm);
320
+ tempForm.submit();
321
+ tempForm.remove();
322
+ }
323
+ }
324
+
325
+ // --- Drag and Drop Handlers ---
326
+
327
+ private _handleDragStart(event: DragEvent, key: string) {
328
+ if (this.disabled) {
329
+ event.preventDefault();
330
+ return;
331
+ }
332
+
333
+ this.draggedKey = key;
334
+ this.isDragging = true;
335
+
336
+ if (event.dataTransfer) {
337
+ event.dataTransfer.effectAllowed = 'move';
338
+ event.dataTransfer.setData('text/plain', key);
339
+ }
340
+ }
341
+
342
+ private _handleDragEnd() {
343
+ if (this.isDragging) {
344
+ // Emit events for the final position change
345
+ this._syncHiddenInputs();
346
+ this.emit('zn-change');
347
+ this.emit('zn-reorder');
348
+ this._submitForm();
349
+ }
350
+
351
+ this.draggedKey = null;
352
+ this.dragOverKey = null;
353
+ this.isDragging = false;
354
+ }
355
+
356
+ private _handleDragOver(event: DragEvent, key: string) {
357
+ event.preventDefault();
358
+
359
+ if (!this.draggedKey || this.draggedKey === key) return;
360
+
361
+ if (event.dataTransfer) {
362
+ event.dataTransfer.dropEffect = 'move';
363
+ }
364
+
365
+ // Move the item in real-time so other items shift around
366
+ const fromIndex = this._value.indexOf(this.draggedKey);
367
+ const toIndex = this._value.indexOf(key);
368
+
369
+ if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return;
370
+
371
+ const newOrder = [...this._value];
372
+ const [moved] = newOrder.splice(fromIndex, 1);
373
+ newOrder.splice(toIndex, 0, moved);
374
+ this._value = newOrder;
375
+ this._assignSlotNames();
376
+ this.requestUpdate();
377
+
378
+ this.dragOverKey = key;
379
+ }
380
+
381
+ private _handleDragLeave(event: DragEvent, key: string) {
382
+ const related = event.relatedTarget as Node | null;
383
+ const target = event.currentTarget as HTMLElement;
384
+ if (related && target.contains(related)) return;
385
+
386
+ if (this.dragOverKey === key) {
387
+ this.dragOverKey = null;
388
+ }
389
+ }
390
+
391
+ private _handleDrop(event: DragEvent) {
392
+ event.preventDefault();
393
+ // Item is already in position from real-time dragover moves
394
+ // dragend handles cleanup and event emission
395
+ }
396
+
397
+ // --- Keyboard Handlers ---
398
+
399
+ private _handleKeyDown(event: KeyboardEvent, key: string) {
400
+ if (this.disabled) return;
401
+
402
+ const index = this._value.indexOf(key);
403
+ if (index === -1) return;
404
+
405
+ let newIndex: number | null = null;
406
+
407
+ switch (event.key) {
408
+ case 'ArrowUp':
409
+ if (index > 0) {
410
+ newIndex = index - 1;
411
+ }
412
+ break;
413
+ case 'ArrowDown':
414
+ if (index < this._value.length - 1) {
415
+ newIndex = index + 1;
416
+ }
417
+ break;
418
+ case 'Home':
419
+ if (index > 0) {
420
+ newIndex = 0;
421
+ }
422
+ break;
423
+ case 'End':
424
+ if (index < this._value.length - 1) {
425
+ newIndex = this._value.length - 1;
426
+ }
427
+ break;
428
+ default:
429
+ return;
430
+ }
431
+
432
+ if (newIndex !== null) {
433
+ event.preventDefault();
434
+ this._moveItem(index, newIndex);
435
+
436
+ // Focus the moved item after re-render
437
+ this.updateComplete.then(() => {
438
+ const items = this.shadowRoot?.querySelectorAll<HTMLElement>('.priority-list__item');
439
+ items?.[newIndex!]?.focus();
440
+ });
441
+ }
442
+ }
443
+
444
+ // --- Slot Change Handler ---
445
+
446
+ private _handleSlotChange() {
447
+ // Query all items with a value attribute (including those already assigned to _item- slots)
448
+ const allItems = [...this.querySelectorAll<HTMLElement>(':scope > [value]')];
449
+ const reservedSlots = ['label', 'label-tooltip', 'help-text'];
450
+ const slottedKeys = allItems
451
+ .filter(el => {
452
+ const slot = el.getAttribute('slot');
453
+ return !slot || slot.startsWith('_item-') || !reservedSlots.includes(slot);
454
+ })
455
+ .map(el => el.getAttribute('value') || '')
456
+ .filter(v => v !== '');
457
+
458
+ // If no items found but we already have an order, skip — this is likely a
459
+ // slotchange firing because items moved from the default slot to named slots
460
+ if (slottedKeys.length === 0 && this._value.length > 0) return;
461
+
462
+ const currentKeys = new Set(this._value);
463
+ const slottedKeysSet = new Set(slottedKeys);
464
+
465
+ // Add new keys that are not in the current order
466
+ const newKeys = slottedKeys.filter(k => !currentKeys.has(k));
467
+ // Remove keys that are no longer in the DOM
468
+ const remainingKeys = this._value.filter(k => slottedKeysSet.has(k));
469
+
470
+ if (newKeys.length > 0 || remainingKeys.length !== this._value.length) {
471
+ this._value = [...remainingKeys, ...newKeys];
472
+ this._assignSlotNames();
473
+ this._syncHiddenInputs();
474
+ this.requestUpdate();
475
+ }
476
+ }
477
+
478
+ // --- Form Control Methods ---
479
+
480
+ /** Checks for validity but does not show a validation message. */
481
+ checkValidity(): boolean {
482
+ return true;
483
+ }
484
+
485
+ /** Gets the associated form, if one exists. */
486
+ getForm(): HTMLFormElement | null {
487
+ return this.formControlController.getForm();
488
+ }
489
+
490
+ /** Checks for validity and shows the browser's validation message if the control is invalid. */
491
+ reportValidity(): boolean {
492
+ return true;
493
+ }
494
+
495
+ /** Sets a custom validation message. Pass an empty string to restore validity. */
496
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
497
+ setCustomValidity(_message = '') {
498
+ // No custom validation needed for this component
499
+ }
500
+
501
+ protected updated(changedProperties: PropertyValues) {
502
+ super.updated(changedProperties);
503
+
504
+ if (changedProperties.has('value') || changedProperties.has('name') || changedProperties.has('priorityStart')) {
505
+ this._syncHiddenInputs();
506
+ }
507
+ }
508
+
509
+ render() {
510
+ const hasLabelSlot = this.hasSlotController.test('label');
511
+ const hasLabelTooltipSlot = this.hasSlotController.test('label-tooltip');
512
+ const hasHelpTextSlot = this.hasSlotController.test('help-text');
513
+ const hasLabel = this.label ? true : hasLabelSlot;
514
+ const hasLabelTooltip = this.labelTooltip ? true : hasLabelTooltipSlot;
515
+ const hasHelpText = this.helpText ? true : hasHelpTextSlot;
516
+
517
+ return html`
518
+ <fieldset
519
+ part="form-control"
520
+ class=${classMap({
521
+ 'form-control': true,
522
+ 'form-control--small': this.size === 'small',
523
+ 'form-control--medium': this.size === 'medium',
524
+ 'form-control--large': this.size === 'large',
525
+ 'form-control--has-label': hasLabel,
526
+ 'form-control--has-label-tooltip': hasLabelTooltip,
527
+ 'form-control--has-help-text': hasHelpText,
528
+ })}
529
+ aria-labelledby="label"
530
+ aria-describedby="help-text"
531
+ ?disabled=${this.disabled}>
532
+ <label
533
+ part="form-control-label"
534
+ id="label"
535
+ class="form-control__label"
536
+ aria-hidden=${hasLabel ? 'false' : 'true'}>
537
+ <slot name="label">${this.label}</slot>
538
+ ${hasLabelTooltip
539
+ ? html`
540
+ <zn-tooltip class="form-control--label-tooltip">
541
+ <div slot="content">
542
+ <slot name="label-tooltip">${this.labelTooltip}</slot>
543
+ </div>
544
+ <zn-icon src="info"></zn-icon>
545
+ </zn-tooltip>`
546
+ : ''}
547
+ </label>
548
+
549
+ <div part="form-control-input" class="form-control-input">
550
+ <div
551
+ part="list"
552
+ class=${classMap({
553
+ 'priority-list': true,
554
+ 'priority-list--disabled': this.disabled,
555
+ 'priority-list--drag-active': this.isDragging,
556
+ })}
557
+ role="listbox"
558
+ aria-label=${this.label || 'Reorderable list'}
559
+ aria-orientation="vertical">
560
+ ${repeat(this._value, (key) => key, (key, index) => html`
561
+ <div
562
+ part="item"
563
+ class=${classMap({
564
+ 'priority-list__item': true,
565
+ 'priority-list__item--dragging': this.draggedKey === key,
566
+ 'priority-list__item--disabled': this.disabled,
567
+ })}
568
+ role="option"
569
+ tabindex=${this.disabled ? '-1' : '0'}
570
+ aria-selected="false"
571
+ aria-label="${key} - priority ${this.priorityStart + index}"
572
+ draggable=${this.disabled ? 'false' : 'true'}
573
+ @dragstart=${(e: DragEvent) => this._handleDragStart(e, key)}
574
+ @dragend=${this._handleDragEnd}
575
+ @dragover=${(e: DragEvent) => this._handleDragOver(e, key)}
576
+ @dragleave=${(e: DragEvent) => this._handleDragLeave(e, key)}
577
+ @drop=${(e: DragEvent) => this._handleDrop(e)}
578
+ @keydown=${(e: KeyboardEvent) => this._handleKeyDown(e, key)}>
579
+ <span class="priority-list__handle" part="drag-handle">
580
+ <zn-icon src="drag_indicator" size="16"></zn-icon>
581
+ </span>
582
+ <span class="priority-list__priority" part="priority">${this.priorityStart + index}</span>
583
+ <span class="priority-list__content" part="content">
584
+ <slot name="_item-${key}"></slot>
585
+ </span>
586
+ <span class="priority-list__actions" part="actions">
587
+ <slot name="action-${key}"></slot>
588
+ </span>
589
+ </div>
590
+ `)}
591
+ </div>
592
+
593
+ <!-- Hidden default slot to capture light DOM children for initial detection -->
594
+ <div class="priority-list__collector">
595
+ <slot @slotchange=${this._handleSlotChange}></slot>
596
+ </div>
597
+
598
+ <div class="priority-list__hidden-inputs"></div>
599
+ </div>
600
+
601
+ <div
602
+ part="form-control-help-text"
603
+ id="help-text"
604
+ class="form-control__help-text"
605
+ aria-hidden=${hasHelpText ? 'false' : 'true'}>
606
+ <slot name="help-text">${this.helpText}</slot>
607
+ </div>
608
+ </fieldset>
609
+ `;
610
+ }
611
+ }