vira 33.2.0 → 33.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,68 @@
1
+ import { Directive, type PartInfo } from 'element-vir';
2
+ import { type DragReorderManager, type DragReorderMove } from '../util/drag-reorder-manager.js';
3
+ /**
4
+ * The directive class behind {@link dragReorderHandle}.
5
+ *
6
+ * @category Internal
7
+ */
8
+ export declare class DragReorderHandleDirective extends Directive {
9
+ protected manager: DragReorderManager | undefined;
10
+ protected element: HTMLElement | undefined;
11
+ constructor(partInfo: PartInfo);
12
+ /** Applies the latest directive arguments. */
13
+ update(partInfo: PartInfo, [manager]: [DragReorderManager]): undefined;
14
+ /** Renders nothing, this directive only attaches behavior to its element. */
15
+ render(manager: DragReorderManager): undefined;
16
+ }
17
+ /**
18
+ * Makes the element it is attached to start dragging the row that contains it. That row must be a
19
+ * direct child of the element that `dragReorderRows` with the same manager is attached to.
20
+ *
21
+ * @category Reorder
22
+ */
23
+ export declare const dragReorderHandle: (manager: DragReorderManager) => import("element-vir").DirectiveResult<typeof DragReorderHandleDirective>;
24
+ /**
25
+ * The directive class behind {@link dragReorderRows}.
26
+ *
27
+ * @category Internal
28
+ */
29
+ export declare class DragReorderRowsDirective extends Directive {
30
+ protected manager: DragReorderManager | undefined;
31
+ protected element: HTMLElement | undefined;
32
+ constructor(partInfo: PartInfo);
33
+ /** Applies the latest directive arguments. */
34
+ update(partInfo: PartInfo, [manager, onReorder,]: [
35
+ DragReorderManager,
36
+ (move: Readonly<DragReorderMove>) => void
37
+ ]): undefined;
38
+ /** Renders nothing, this directive only attaches behavior to its element. */
39
+ render(manager: DragReorderManager, onReorder: (move: Readonly<DragReorderMove>) => void): undefined;
40
+ }
41
+ /**
42
+ * Marks the element whose direct children are the reorderable rows, and receives every finished
43
+ * {@link DragReorderMove}. Indexes count those children, so render nothing (not an empty wrapper)
44
+ * for rows that should not take part.
45
+ *
46
+ * @category Reorder
47
+ */
48
+ export declare const dragReorderRows: (manager: DragReorderManager, onReorder: (move: Readonly<DragReorderMove>) => void) => import("element-vir").DirectiveResult<typeof DragReorderRowsDirective>;
49
+ /**
50
+ * The directive class behind {@link dragReorderGhost}.
51
+ *
52
+ * @category Internal
53
+ */
54
+ export declare class DragReorderGhostDirective extends Directive {
55
+ constructor(partInfo: PartInfo);
56
+ /** Applies the latest directive arguments. */
57
+ update(partInfo: PartInfo, [manager]: [DragReorderManager]): undefined;
58
+ /** Renders nothing, this directive only attaches behavior to its element. */
59
+ render(manager: DragReorderManager): undefined;
60
+ }
61
+ /**
62
+ * Turns the element it is attached to into the translucent copy of the dragged row: it is
63
+ * absolutely positioned over the dragged row and then follows the pointer. Render it only while the
64
+ * manager's `value.draggedIndex` is set, with a copy of that row inside it.
65
+ *
66
+ * @category Reorder
67
+ */
68
+ export declare const dragReorderGhost: (manager: DragReorderManager) => import("element-vir").DirectiveResult<typeof DragReorderGhostDirective>;
@@ -0,0 +1,114 @@
1
+ import { assertWrap } from '@augment-vir/assert';
2
+ import { assertIsElementPartInfo, directive, Directive } from 'element-vir';
3
+ /**
4
+ * The directive class behind {@link dragReorderHandle}.
5
+ *
6
+ * @category Internal
7
+ */
8
+ export class DragReorderHandleDirective extends Directive {
9
+ manager;
10
+ element;
11
+ constructor(partInfo) {
12
+ super(partInfo);
13
+ assertIsElementPartInfo(partInfo, 'dragReorderHandle');
14
+ }
15
+ /** Applies the latest directive arguments. */
16
+ update(partInfo, [manager]) {
17
+ assertIsElementPartInfo(partInfo, 'dragReorderHandle');
18
+ this.manager = manager;
19
+ const element = assertWrap.instanceOf(partInfo.element, HTMLElement);
20
+ if (element !== this.element) {
21
+ this.element = element;
22
+ element.draggable = true;
23
+ element.addEventListener('dragstart', (event) => {
24
+ this.manager?.startDrag(event, element);
25
+ });
26
+ element.addEventListener('dragend', () => this.manager?.endDrag());
27
+ }
28
+ return this.render(manager);
29
+ }
30
+ /** Renders nothing, this directive only attaches behavior to its element. */
31
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
32
+ render(manager) {
33
+ return undefined;
34
+ }
35
+ }
36
+ /**
37
+ * Makes the element it is attached to start dragging the row that contains it. That row must be a
38
+ * direct child of the element that `dragReorderRows` with the same manager is attached to.
39
+ *
40
+ * @category Reorder
41
+ */
42
+ export const dragReorderHandle = directive(DragReorderHandleDirective);
43
+ /**
44
+ * The directive class behind {@link dragReorderRows}.
45
+ *
46
+ * @category Internal
47
+ */
48
+ export class DragReorderRowsDirective extends Directive {
49
+ manager;
50
+ element;
51
+ constructor(partInfo) {
52
+ super(partInfo);
53
+ assertIsElementPartInfo(partInfo, 'dragReorderRows');
54
+ }
55
+ /** Applies the latest directive arguments. */
56
+ update(partInfo, [manager, onReorder,]) {
57
+ assertIsElementPartInfo(partInfo, 'dragReorderRows');
58
+ this.manager = manager;
59
+ const element = assertWrap.instanceOf(partInfo.element, HTMLElement);
60
+ manager.attachRows(element, onReorder);
61
+ if (element !== this.element) {
62
+ this.element = element;
63
+ element.addEventListener('dragover', (event) => this.manager?.dragOver(event));
64
+ element.addEventListener('drop', (event) => this.manager?.drop(event));
65
+ }
66
+ return this.render(manager, onReorder);
67
+ }
68
+ /** Renders nothing, this directive only attaches behavior to its element. */
69
+ render(
70
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
71
+ manager,
72
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
73
+ onReorder) {
74
+ return undefined;
75
+ }
76
+ }
77
+ /**
78
+ * Marks the element whose direct children are the reorderable rows, and receives every finished
79
+ * {@link DragReorderMove}. Indexes count those children, so render nothing (not an empty wrapper)
80
+ * for rows that should not take part.
81
+ *
82
+ * @category Reorder
83
+ */
84
+ export const dragReorderRows = directive(DragReorderRowsDirective);
85
+ /**
86
+ * The directive class behind {@link dragReorderGhost}.
87
+ *
88
+ * @category Internal
89
+ */
90
+ export class DragReorderGhostDirective extends Directive {
91
+ constructor(partInfo) {
92
+ super(partInfo);
93
+ assertIsElementPartInfo(partInfo, 'dragReorderGhost');
94
+ }
95
+ /** Applies the latest directive arguments. */
96
+ update(partInfo, [manager]) {
97
+ assertIsElementPartInfo(partInfo, 'dragReorderGhost');
98
+ manager.attachGhost(assertWrap.instanceOf(partInfo.element, HTMLElement));
99
+ return this.render(manager);
100
+ }
101
+ /** Renders nothing, this directive only attaches behavior to its element. */
102
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
103
+ render(manager) {
104
+ return undefined;
105
+ }
106
+ }
107
+ /**
108
+ * Turns the element it is attached to into the translucent copy of the dragged row: it is
109
+ * absolutely positioned over the dragged row and then follows the pointer. Render it only while the
110
+ * manager's `value.draggedIndex` is set, with a copy of that row inside it.
111
+ *
112
+ * @category Reorder
113
+ */
114
+ export const dragReorderGhost = directive(DragReorderGhostDirective);
@@ -1,2 +1,3 @@
1
+ export * from './drag-reorder.directive.js';
1
2
  export * from './popover.directive.js';
2
3
  export * from './tooltip.directive.js';
@@ -1,2 +1,3 @@
1
+ export * from './drag-reorder.directive.js';
1
2
  export * from './popover.directive.js';
2
3
  export * from './tooltip.directive.js';
@@ -19,6 +19,8 @@ export declare const ViraMenuItem: import("element-vir").DeclarativeElementDefin
19
19
  * _always_ be shown, even if `selected` is set to `false`.
20
20
  */
21
21
  iconOverride: ViraIconSvg;
22
+ /** Removes the icon entirely, including the space it reserves when not shown. */
23
+ hideIcon: boolean;
22
24
  /**
23
25
  * When `true`, activating this item will _not_ close the containing popover.
24
26
  *
@@ -1,5 +1,5 @@
1
1
  import { assertWrap } from '@augment-vir/assert';
2
- import { css, defineElementEvent, html } from 'element-vir';
2
+ import { css, defineElementEvent, html, nothing } from 'element-vir';
3
3
  import { listenTo } from 'typed-event-target';
4
4
  import { lucideIcons } from '../../icons/lucide-icons.js';
5
5
  import { createSizedIcon } from '../../icons/sized-icon.js';
@@ -241,9 +241,13 @@ export const ViraMenuItem = defineViraElement()({
241
241
  },
242
242
  render({ inputs }) {
243
243
  return html `
244
- <${ViraIcon.assign({
245
- icon: inputs.iconOverride || selectedCheckIcon,
246
- })}></${ViraIcon}>
244
+ ${inputs.hideIcon
245
+ ? nothing
246
+ : html `
247
+ <${ViraIcon.assign({
248
+ icon: inputs.iconOverride || selectedCheckIcon,
249
+ })}></${ViraIcon}>
250
+ `}
247
251
  <div class="slot-wrapper">
248
252
  <slot>&nbsp;</slot>
249
253
  </div>
@@ -7,7 +7,7 @@ import { ChevronUp16Icon } from '../icons/index.js';
7
7
  import { createFocusStyles } from '../styles/focus.js';
8
8
  import { viraFormCssVars } from '../styles/form-styles.js';
9
9
  import { ViraSize, viraSizeHeights } from '../styles/form-variants.js';
10
- import { noNativeFormStyles, noUserSelect, viraAnimationDurations } from '../styles/index.js';
10
+ import { noNativeFormStyles, noUserSelect, viraAnimationDurations, viraTheme, } from '../styles/index.js';
11
11
  import { defineViraElement } from '../util/define-vira-element.js';
12
12
  import { fuzzyMatch } from '../util/fuzzy-match.js';
13
13
  import { renderMenuItemEntries } from '../util/menu-helpers.js';
@@ -149,20 +149,21 @@ export const ViraDropdown = defineViraElement()({
149
149
  .option-group-label,
150
150
  .no-options {
151
151
  ${noUserSelect};
152
- /* Aligns with the label text of each ViraMenuItem, past its check icon. */
153
- padding: 8px 12px 4px
154
- calc(${ViraMenuItem.cssVars['vira-menu-item-icon-gap'].value} * 3 + 16px);
155
- opacity: 0.6;
156
152
  text-align: left;
157
153
  }
158
154
 
159
155
  .option-group-label {
156
+ color: ${viraTheme.colors['vira-grey-foreground-placeholder'].foreground.value};
157
+ padding: 8px 12px 4px calc(${ViraMenuItem.cssVars['vira-menu-item-icon-gap'].value} * 2);
160
158
  font-weight: ${viraFormCssVars['vira-form-label-font-weight'].value};
161
159
  }
162
160
 
163
161
  .no-options {
164
162
  cursor: default;
165
- padding-bottom: 8px;
163
+ opacity: 0.6;
164
+ /* Aligns with the label text of each ViraMenuItem, past its check icon. */
165
+ padding: 8px 12px 8px
166
+ calc(${ViraMenuItem.cssVars['vira-menu-item-icon-gap'].value} * 3 + 16px);
166
167
  }
167
168
 
168
169
  ${ViraMenuItem}.option-group-label {
@@ -335,6 +336,7 @@ export const ViraDropdown = defineViraElement()({
335
336
  ? html `
336
337
  <${ViraMenuItem.assign({
337
338
  disablePointerStyles: true,
339
+ hideIcon: true,
338
340
  })}
339
341
  class="option-group-label"
340
342
  ${state.navController ? nav(state.navController) : nothing}
@@ -1,4 +1,5 @@
1
1
  import { type PartialWithUndefined } from '@augment-vir/common';
2
+ import { DragReorderManager } from '../util/drag-reorder-manager.js';
2
3
  import { type ViraFormField, type ViraFormFields } from '../util/vira-form-fields.js';
3
4
  /**
4
5
  * A form element.
@@ -44,8 +45,17 @@ export declare const ViraForm: import("element-vir").DeclarativeElementDefinitio
44
45
  * @default false
45
46
  */
46
47
  isReadonly: boolean;
48
+ /**
49
+ * When `true`, each visible field gets a drag handle. Dropping a field emits
50
+ * `fieldOrderChange` with every field key in its new order. The form does not reorder
51
+ * `fields` itself: pass them back in the emitted order.
52
+ *
53
+ * @default false
54
+ */
55
+ isReorderable: boolean;
47
56
  }>, {
48
57
  lastIsValid: boolean;
58
+ dragReorder: DragReorderManager;
49
59
  }, {
50
60
  valueChange: import("element-vir").DefineElementEvent<{
51
61
  key: string;
@@ -53,4 +63,10 @@ export declare const ViraForm: import("element-vir").DeclarativeElementDefinitio
53
63
  validChange: import("element-vir").DefineElementEvent<{
54
64
  allFieldsAreValid: boolean;
55
65
  }>;
56
- }, "vira-form-", "vira-form-", readonly [], readonly []>;
66
+ /** Emitted when focus leaves a field entirely, not when it moves within the field. */
67
+ fieldBlur: import("element-vir").DefineElementEvent<{
68
+ key: string;
69
+ } & ViraFormField>;
70
+ /** Every field key, hidden ones included, in the order the user dropped them. */
71
+ fieldOrderChange: import("element-vir").DefineElementEvent<string[]>;
72
+ }, "vira-form-", "vira-form-", readonly [], readonly ["dragHandle"]>;
@@ -1,12 +1,16 @@
1
1
  import { check } from '@augment-vir/assert';
2
2
  import { getObjectTypedEntries } from '@augment-vir/common';
3
- import { css, defineElementEvent, html, listen, nothing, testId, } from 'element-vir';
3
+ import { classMap, css, defineElementEvent, html, listen, nothing, testId, } from 'element-vir';
4
+ import { dragReorderGhost, dragReorderHandle, dragReorderRows, } from '../directives/drag-reorder.directive.js';
5
+ import { lucideIcons } from '../icons/lucide-icons.js';
4
6
  import { viraFormCssVars } from '../styles/form-styles.js';
5
7
  import { defineViraElement } from '../util/define-vira-element.js';
6
- import { applyRequiredLabel, areFormFieldsValid, ViraFormFieldType, } from '../util/vira-form-fields.js';
8
+ import { DragReorderManager } from '../util/drag-reorder-manager.js';
9
+ import { applyRequiredLabel, areFormFieldsValid, moveFormFieldKey, ViraFormFieldType, } from '../util/vira-form-fields.js';
7
10
  import { ViraCheckbox } from './vira-checkbox.element.js';
8
11
  import { ViraDateInput } from './vira-date-input.element.js';
9
12
  import { ViraDropdown } from './vira-dropdown.element.js';
13
+ import { ViraIcon } from './vira-icon.element.js';
10
14
  import { ViraInput, ViraInputType } from './vira-input.element.js';
11
15
  import { ViraTextArea } from './vira-text-area.element.js';
12
16
  /**
@@ -20,12 +24,20 @@ export const ViraForm = defineViraElement()({
20
24
  state() {
21
25
  return {
22
26
  lastIsValid: false,
27
+ dragReorder: new DragReorderManager(),
23
28
  };
24
29
  },
25
30
  events: {
26
31
  valueChange: defineElementEvent(),
27
32
  validChange: defineElementEvent(),
33
+ /** Emitted when focus leaves a field entirely, not when it moves within the field. */
34
+ fieldBlur: defineElementEvent(),
35
+ /** Every field key, hidden ones included, in the order the user dropped them. */
36
+ fieldOrderChange: defineElementEvent(),
28
37
  },
38
+ testIds: [
39
+ 'dragHandle',
40
+ ],
29
41
  styles: css `
30
42
  :host {
31
43
  display: flex;
@@ -43,6 +55,58 @@ export const ViraForm = defineViraElement()({
43
55
  }
44
56
  }
45
57
 
58
+ .drag-ghost {
59
+ opacity: 0.6;
60
+
61
+ & .drop-above,
62
+ & .drop-above > *,
63
+ & .drop-below,
64
+ & .drop-below > * {
65
+ box-shadow: none;
66
+ }
67
+
68
+ & .horizontal-fields {
69
+ border-spacing: 0;
70
+ table-layout: fixed;
71
+ }
72
+ }
73
+
74
+ .reorder-rows {
75
+ display: flex;
76
+ flex-direction: column;
77
+ gap: 10px;
78
+ }
79
+
80
+ .reorder-row {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 4px;
84
+
85
+ & > :last-child {
86
+ flex-grow: 1;
87
+ }
88
+ }
89
+
90
+ .drag-handle {
91
+ display: flex;
92
+ cursor: grab;
93
+ color: ${viraFormCssVars['vira-form-placeholder-color'].value};
94
+
95
+ &:active {
96
+ cursor: grabbing;
97
+ }
98
+ }
99
+
100
+ div.drop-above,
101
+ tr.drop-above > * {
102
+ box-shadow: 0 -5px 0 0 ${viraFormCssVars['vira-form-focus-outline-color'].value};
103
+ }
104
+
105
+ div.drop-below,
106
+ tr.drop-below > * {
107
+ box-shadow: 0 5px 0 0 ${viraFormCssVars['vira-form-focus-outline-color'].value};
108
+ }
109
+
46
110
  .horizontal-fields {
47
111
  width: 100%;
48
112
  border-collapse: separate;
@@ -53,6 +117,11 @@ export const ViraForm = defineViraElement()({
53
117
  padding: 0;
54
118
  }
55
119
 
120
+ & .handle-cell {
121
+ width: 0;
122
+ vertical-align: middle;
123
+ }
124
+
56
125
  & th {
57
126
  padding: 0 8px;
58
127
  vertical-align: middle;
@@ -80,7 +149,7 @@ export const ViraForm = defineViraElement()({
80
149
  }
81
150
  }
82
151
  `,
83
- render({ inputs, dispatch, events, state, updateState }) {
152
+ render({ inputs, dispatch, events, state, updateState, testIds }) {
84
153
  const currentIsValid = areFormFieldsValid(inputs.fields);
85
154
  if (currentIsValid !== state.lastIsValid) {
86
155
  updateState({
@@ -92,15 +161,75 @@ export const ViraForm = defineViraElement()({
92
161
  },
93
162
  }));
94
163
  }
95
- function wrapFormField({ fieldTemplate, label, }) {
164
+ const visibleKeys = getObjectTypedEntries(inputs.fields)
165
+ .filter(([, field,]) => !field.isHidden)
166
+ .map(([key]) => key);
167
+ function dropField({ fromIndex, toIndex }) {
168
+ const movedKey = visibleKeys[fromIndex];
169
+ if (movedKey == undefined) {
170
+ return;
171
+ }
172
+ dispatch(new events.fieldOrderChange({
173
+ detail: moveFormFieldKey({
174
+ keys: Object.keys(inputs.fields),
175
+ movedKey,
176
+ beforeKey: visibleKeys[toIndex],
177
+ }),
178
+ }));
179
+ }
180
+ function listenToFieldBlur(key, field) {
181
+ /**
182
+ * `blur` doesn't bubble out of the field's shadow root, but `focusout` does, and the
183
+ * browser skips it on the field's host when focus moves between elements inside it.
184
+ */
185
+ return listen('focusout', () => {
186
+ dispatch(new events.fieldBlur({
187
+ detail: {
188
+ key,
189
+ ...field,
190
+ },
191
+ }));
192
+ });
193
+ }
194
+ function wrapFormField({ fieldTemplate, label, key, }) {
195
+ const dropLine = state.dragReorder.readDropLine(visibleKeys.indexOf(key));
196
+ const rowClasses = classMap({
197
+ 'reorder-row': !inputs.useHorizontalLabels,
198
+ 'drop-above': dropLine.isAbove,
199
+ 'drop-below': dropLine.isBelow,
200
+ });
201
+ const dragHandle = inputs.isReorderable
202
+ ? html `
203
+ <div
204
+ class="drag-handle"
205
+ title="drag to reorder"
206
+ ${testId(testIds.dragHandle)}
207
+ ${dragReorderHandle(state.dragReorder)}
208
+ >
209
+ <${ViraIcon.assign({
210
+ icon: lucideIcons.GripVertical,
211
+ })}></${ViraIcon}>
212
+ </div>
213
+ `
214
+ : nothing;
96
215
  if (inputs.useHorizontalLabels) {
97
216
  return html `
98
- <tr>
217
+ <tr class=${rowClasses}>
218
+ ${inputs.isReorderable
219
+ ? html `
220
+ <td class="handle-cell">${dragHandle}</td>
221
+ `
222
+ : nothing}
99
223
  <th scope="row">${label}</th>
100
224
  <td>${fieldTemplate}</td>
101
225
  </tr>
102
226
  `;
103
227
  }
228
+ else if (inputs.isReorderable) {
229
+ return html `
230
+ <div class=${rowClasses}>${dragHandle} ${fieldTemplate}</div>
231
+ `;
232
+ }
104
233
  else {
105
234
  return fieldTemplate;
106
235
  }
@@ -114,6 +243,7 @@ export const ViraForm = defineViraElement()({
114
243
  else if (field.type === ViraFormFieldType.Checkbox) {
115
244
  const checkboxLabel = applyRequiredLabel(field.label, showRequiredMarker);
116
245
  return wrapFormField({
246
+ key,
117
247
  label: checkboxLabel,
118
248
  fieldTemplate: html `
119
249
  <${ViraCheckbox.assign({
@@ -125,6 +255,7 @@ export const ViraForm = defineViraElement()({
125
255
  fillWhenUnchecked: field.fillWhenUnchecked,
126
256
  })}
127
257
  ${field.testId ? testId(field.testId) : nothing}
258
+ ${listenToFieldBlur(key, field)}
128
259
  ${listen(ViraCheckbox.events.valueChange, (event) => {
129
260
  dispatch(new events.valueChange({
130
261
  detail: {
@@ -157,6 +288,7 @@ export const ViraForm = defineViraElement()({
157
288
  : {};
158
289
  if (field.type === ViraFormFieldType.Select) {
159
290
  return wrapFormField({
291
+ key,
160
292
  label,
161
293
  fieldTemplate: html `
162
294
  <${ViraDropdown.assign({
@@ -171,6 +303,7 @@ export const ViraForm = defineViraElement()({
171
303
  attributePassthrough: horizontalLabelAttributes,
172
304
  })}
173
305
  ${field.testId ? testId(field.testId) : nothing}
306
+ ${listenToFieldBlur(key, field)}
174
307
  ${listen(ViraDropdown.events.selectedValuesChange, (event) => {
175
308
  dispatch(new events.valueChange({
176
309
  detail: {
@@ -186,6 +319,7 @@ export const ViraForm = defineViraElement()({
186
319
  }
187
320
  else if (field.type === ViraFormFieldType.TextArea) {
188
321
  return wrapFormField({
322
+ key,
189
323
  label,
190
324
  fieldTemplate: html `
191
325
  <${ViraTextArea.assign({
@@ -200,6 +334,7 @@ export const ViraForm = defineViraElement()({
200
334
  attributePassthrough: horizontalLabelAttributes,
201
335
  })}
202
336
  ${field.testId ? testId(field.testId) : nothing}
337
+ ${listenToFieldBlur(key, field)}
203
338
  ${listen(ViraTextArea.events.valueChange, (event) => {
204
339
  dispatch(new events.valueChange({
205
340
  detail: {
@@ -215,6 +350,7 @@ export const ViraForm = defineViraElement()({
215
350
  }
216
351
  else if (field.type === ViraFormFieldType.Number) {
217
352
  return wrapFormField({
353
+ key,
218
354
  label,
219
355
  fieldTemplate: html `
220
356
  <${ViraInput.assign({
@@ -248,6 +384,7 @@ export const ViraForm = defineViraElement()({
248
384
  },
249
385
  })}
250
386
  ${field.testId ? testId(field.testId) : nothing}
387
+ ${listenToFieldBlur(key, field)}
251
388
  ${listen(ViraInput.events.valueChange, (event) => {
252
389
  const numericValue = event.detail === '' ? undefined : Number(event.detail);
253
390
  dispatch(new events.valueChange({
@@ -264,6 +401,7 @@ export const ViraForm = defineViraElement()({
264
401
  }
265
402
  else if (field.type === ViraFormFieldType.Date) {
266
403
  return wrapFormField({
404
+ key,
267
405
  label,
268
406
  fieldTemplate: html `
269
407
  <${ViraDateInput.assign({
@@ -279,6 +417,7 @@ export const ViraForm = defineViraElement()({
279
417
  label: childLabel,
280
418
  })}
281
419
  ${field.testId ? testId(field.testId) : nothing}
420
+ ${listenToFieldBlur(key, field)}
282
421
  ${listen(ViraDateInput.events.valueChange, (event) => {
283
422
  dispatch(new events.valueChange({
284
423
  detail: {
@@ -294,6 +433,7 @@ export const ViraForm = defineViraElement()({
294
433
  }
295
434
  else {
296
435
  return wrapFormField({
436
+ key,
297
437
  label,
298
438
  fieldTemplate: html `
299
439
  <${ViraInput.assign({
@@ -336,6 +476,7 @@ export const ViraForm = defineViraElement()({
336
476
  : ViraInputType.Default,
337
477
  })}
338
478
  ${field.testId ? testId(field.testId) : nothing}
479
+ ${listenToFieldBlur(key, field)}
339
480
  ${listen(ViraInput.events.valueChange, (event) => {
340
481
  dispatch(new events.valueChange({
341
482
  detail: {
@@ -353,14 +494,53 @@ export const ViraForm = defineViraElement()({
353
494
  const formFieldsWrapper = inputs.useHorizontalLabels
354
495
  ? html `
355
496
  <table class="horizontal-fields">
356
- <tbody>${formFieldTemplates}</tbody>
497
+ <tbody ${dragReorderRows(state.dragReorder, dropField)}>
498
+ ${formFieldTemplates}
499
+ </tbody>
357
500
  </table>
358
501
  `
359
- : formFieldTemplates;
502
+ : inputs.isReorderable
503
+ ? html `
504
+ <div class="reorder-rows" ${dragReorderRows(state.dragReorder, dropField)}>
505
+ ${formFieldTemplates}
506
+ </div>
507
+ `
508
+ : formFieldTemplates;
509
+ const draggedKey = state.dragReorder.value.draggedIndex == undefined
510
+ ? undefined
511
+ : visibleKeys[state.dragReorder.value.draggedIndex];
512
+ const draggedRow = draggedKey == undefined
513
+ ? undefined
514
+ : formFieldTemplates[Object.keys(inputs.fields).indexOf(draggedKey)];
515
+ const dragGhost = draggedRow
516
+ ? html `
517
+ <div class="drag-ghost" ${dragReorderGhost(state.dragReorder)}>
518
+ ${inputs.useHorizontalLabels
519
+ ? html `
520
+ <table class="horizontal-fields">
521
+ <colgroup>
522
+ ${state.dragReorder.value.draggedCellWidths.map((cellWidth) => {
523
+ return html `
524
+ <col
525
+ style=${css `
526
+ width: ${cellWidth}px;
527
+ `}
528
+ />
529
+ `;
530
+ })}
531
+ </colgroup>
532
+ <tbody>${draggedRow}</tbody>
533
+ </table>
534
+ `
535
+ : draggedRow}
536
+ </div>
537
+ `
538
+ : nothing;
360
539
  return html `
361
540
  <form ${listen('submit', (event) => event.preventDefault())}>
362
541
  ${formFieldsWrapper}
363
542
  <slot></slot>
543
+ ${dragGhost}
364
544
  </form>
365
545
  `;
366
546
  },
@@ -472,7 +472,7 @@ export const ViraJsonForm = defineViraElement()({
472
472
  selected: toSelectedValues(selectedType),
473
473
  })}
474
474
  ${listen(ViraDropdown.events.selectedValuesChange, (event) => {
475
- setPendingType(pathKey, assertWrap.isDefined(event.detail[0]));
475
+ setPendingType(pathKey, assertWrap.isEnumValue(event.detail[0], ViraJsonType));
476
476
  })}
477
477
  ></${ViraDropdown}>
478
478
  ${renderPlusButton({
@@ -536,7 +536,7 @@ export const ViraJsonForm = defineViraElement()({
536
536
  selected: toSelectedValues(selectedType),
537
537
  })}
538
538
  ${listen(ViraDropdown.events.selectedValuesChange, (event) => {
539
- setPendingType(pathKey, assertWrap.isDefined(event.detail[0]));
539
+ setPendingType(pathKey, assertWrap.isEnumValue(event.detail[0], ViraJsonType));
540
540
  })}
541
541
  ></${ViraDropdown}>
542
542
  ${renderPlusButton({
@@ -849,7 +849,7 @@ export const ViraJsonForm = defineViraElement()({
849
849
  })}
850
850
  title="Choose from options or enter a custom value"
851
851
  ${listen(ViraDropdown.events.selectedValuesChange, (event) => {
852
- setStringMode(pathKey, assertWrap.isDefined(event.detail[0]));
852
+ setStringMode(pathKey, assertWrap.isEnumValue(event.detail[0], ViraJsonStringMode));
853
853
  })}
854
854
  ></${ViraDropdown}>
855
855
  </div>
@@ -904,7 +904,7 @@ export const ViraJsonForm = defineViraElement()({
904
904
  })}
905
905
  title="Change type"
906
906
  ${listen(ViraDropdown.events.selectedValuesChange, (event) => {
907
- const newType = assertWrap.isDefined(event.detail[0]);
907
+ const newType = assertWrap.isEnumValue(event.detail[0], ViraJsonType);
908
908
  emitReplaceAt(path, createDefaultForJsonType(newType));
909
909
  })}
910
910
  ></${ViraDropdown}>
@@ -10,5 +10,5 @@ export declare const viraIconCssVars: import("lit-css-vars").CssVarDefinitions<{
10
10
  readonly 'vira-icon-stroke-color': "currentColor";
11
11
  /** To be used for coloring an icon's fill. */
12
12
  readonly 'vira-icon-fill-color': "none";
13
- readonly 'vira-icon-stroke-width': "1.5px";
13
+ readonly 'vira-icon-stroke-width': "1px";
14
14
  }>;
@@ -11,5 +11,5 @@ export const viraIconCssVars = defineCssVars({
11
11
  'vira-icon-stroke-color': 'currentColor',
12
12
  /** To be used for coloring an icon's fill. */
13
13
  'vira-icon-fill-color': 'none',
14
- 'vira-icon-stroke-width': '1.5px',
14
+ 'vira-icon-stroke-width': '1px',
15
15
  });
@@ -8,7 +8,8 @@ import { type ViraIconSvg } from './icon-svg.js';
8
8
  export type LucideIconKey = keyof typeof lucideStaticImport;
9
9
  /**
10
10
  * All [Lucide icons](https://lucide.dev) in a format compatible with `ViraIcon`. Each icon entry
11
- * can be accessed directly as a {@link ViraIconSvg}.
11
+ * can be accessed directly as a {@link ViraIconSvg}. Icons render at 16px instead of Lucide's
12
+ * default 24px; use `createSizedIcon` for other sizes.
12
13
  *
13
14
  * @category Icon
14
15
  */
@@ -5,6 +5,8 @@ const defaultLucideAttributes = {
5
5
  fill: String(viraIconCssVars['vira-icon-fill-color'].value),
6
6
  stroke: String(viraIconCssVars['vira-icon-stroke-color'].value),
7
7
  'stroke-width': String(viraIconCssVars['vira-icon-stroke-width'].value),
8
+ width: '16',
9
+ height: '16',
8
10
  };
9
11
  function setSvgAttribute({ svgString, attributeName, value, }) {
10
12
  const escapedName = attributeName.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
@@ -43,7 +45,8 @@ function createLucideIconEntry(iconKey) {
43
45
  const lucideIconCache = new Map();
44
46
  /**
45
47
  * All [Lucide icons](https://lucide.dev) in a format compatible with `ViraIcon`. Each icon entry
46
- * can be accessed directly as a {@link ViraIconSvg}.
48
+ * can be accessed directly as a {@link ViraIconSvg}. Icons render at 16px instead of Lucide's
49
+ * default 24px; use `createSizedIcon` for other sizes.
47
50
  *
48
51
  * @category Icon
49
52
  */
@@ -5,8 +5,8 @@ import { type CSSResult } from 'element-vir';
5
5
  * @category Styles
6
6
  */
7
7
  export declare const shadowCssVars: import("lit-css-vars").CssVarDefinitions<{
8
- readonly 'menu-shadow-color': "#e2e2e2";
9
- readonly 'modal-shadow-color': "#4f4f4f";
8
+ readonly 'menu-shadow-color': "rgba(0, 0, 0, 0.11)";
9
+ readonly 'modal-shadow-color': "rgba(0, 0, 0, 0.69)";
10
10
  }>;
11
11
  /**
12
12
  * CSS chunks for default Vira shadow styles.
@@ -6,8 +6,8 @@ import { defineCssVars } from 'lit-css-vars';
6
6
  * @category Styles
7
7
  */
8
8
  export const shadowCssVars = defineCssVars({
9
- 'menu-shadow-color': '#e2e2e2',
10
- 'modal-shadow-color': '#4f4f4f',
9
+ 'menu-shadow-color': 'rgba(0, 0, 0, 0.11)',
10
+ 'modal-shadow-color': 'rgba(0, 0, 0, 0.69)',
11
11
  });
12
12
  /**
13
13
  * CSS chunks for default Vira shadow styles.
@@ -0,0 +1,84 @@
1
+ import { Observable } from 'observavir';
2
+ /**
3
+ * A finished drag from {@link DragReorderManager}.
4
+ *
5
+ * @category Reorder
6
+ */
7
+ export type DragReorderMove = {
8
+ fromIndex: number;
9
+ /**
10
+ * Index, in the order before the move, of the row that the dragged row was dropped in front of.
11
+ * Equals the row count when it was dropped after the last row.
12
+ */
13
+ toIndex: number;
14
+ };
15
+ /**
16
+ * The current value of a {@link DragReorderManager}.
17
+ *
18
+ * @category Reorder
19
+ */
20
+ export type DragReorderState = {
21
+ draggedIndex: number | undefined;
22
+ /**
23
+ * Same meaning as {@link DragReorderMove.toIndex}. `undefined` while the pointer is where a drop
24
+ * would not move the row.
25
+ */
26
+ dropIndex: number | undefined;
27
+ rowCount: number;
28
+ /**
29
+ * Widths of the dragged row's direct children, so a ghost of a table row can keep the real
30
+ * column widths. Empty while nothing is dragged.
31
+ */
32
+ draggedCellWidths: number[];
33
+ };
34
+ /**
35
+ * Drag-to-reorder for rows that the host element renders itself, so the host keeps its own markup
36
+ * and styles (including table rows). Store one in an element's `state` so the element re-renders as
37
+ * the drag progresses, then attach `dragReorderRows` to the element whose direct children are the
38
+ * rows, `dragReorderHandle` to each row's handle, and `dragReorderGhost` to a copy of the dragged
39
+ * row rendered while `value.draggedIndex` is set. The ghost is positioned over the dragged row and
40
+ * follows the pointer; style its opacity yourself.
41
+ *
42
+ * Nothing is reordered for you: apply each {@link DragReorderMove} to your own data.
43
+ *
44
+ * @category Reorder
45
+ */
46
+ export declare class DragReorderManager extends Observable<DragReorderState> {
47
+ protected rowsElement: HTMLElement | undefined;
48
+ /** The callback given to `attachRows`. */
49
+ protected onReorder: ((move: Readonly<DragReorderMove>) => void) | undefined;
50
+ protected draggedRow: Element | undefined;
51
+ protected ghostElement: HTMLElement | undefined;
52
+ protected pointerStart: {
53
+ x: number;
54
+ y: number;
55
+ };
56
+ /** Removes the global listeners that move the ghost. Set only while a drag is in progress. */
57
+ protected stopTrackingPointer: (() => void) | undefined;
58
+ constructor();
59
+ /** Whether to draw the drop line above or below the row at `rowIndex`. */
60
+ readDropLine(rowIndex: number): {
61
+ isAbove: boolean;
62
+ isBelow: boolean;
63
+ };
64
+ /** Used by `dragReorderRows`. */
65
+ attachRows(rowsElement: HTMLElement, onReorder: (move: Readonly<DragReorderMove>) => void): void;
66
+ /** Used by `dragReorderGhost`. */
67
+ attachGhost(ghostElement: HTMLElement): void;
68
+ /** Used by `dragReorderHandle`. */
69
+ startDrag(event: DragEvent, handle: Element): void;
70
+ /** Used by `dragReorderRows`. */
71
+ dragOver(event: DragEvent): void;
72
+ /** Used by `dragReorderRows`. */
73
+ drop(event: DragEvent): void;
74
+ /**
75
+ * Used by `dragReorderHandle`. A release outside the rows still lands where the drop line was
76
+ * last drawn.
77
+ */
78
+ endDrag(): void;
79
+ destroy(): void;
80
+ /** Counts the rows whose vertical midpoint is above the pointer. */
81
+ protected readDropIndex(clientY: number): number | undefined;
82
+ /** Resets to the idle state and only calls `onReorder` when `dropIndex` is defined. */
83
+ protected finishDrag(dropIndex: number | undefined): void;
84
+ }
@@ -0,0 +1,213 @@
1
+ import { Observable } from 'observavir';
2
+ import { listenToGlobal } from 'typed-event-target';
3
+ const idleState = {
4
+ draggedIndex: undefined,
5
+ dropIndex: undefined,
6
+ rowCount: 0,
7
+ draggedCellWidths: [],
8
+ };
9
+ /**
10
+ * The ghost row replaces the browser's own drag preview, which only shows the handle that the drag
11
+ * started on.
12
+ */
13
+ function setEmptyDragImage(dataTransfer) {
14
+ const emptyImage = document.createElement('div');
15
+ emptyImage.style.position = 'fixed';
16
+ emptyImage.style.top = '0';
17
+ emptyImage.style.left = '0';
18
+ emptyImage.style.width = '1px';
19
+ emptyImage.style.height = '1px';
20
+ emptyImage.style.pointerEvents = 'none';
21
+ document.body.append(emptyImage);
22
+ dataTransfer.setDragImage(emptyImage, 0, 0);
23
+ globalThis.requestAnimationFrame(() => emptyImage.remove());
24
+ }
25
+ /**
26
+ * Drag-to-reorder for rows that the host element renders itself, so the host keeps its own markup
27
+ * and styles (including table rows). Store one in an element's `state` so the element re-renders as
28
+ * the drag progresses, then attach `dragReorderRows` to the element whose direct children are the
29
+ * rows, `dragReorderHandle` to each row's handle, and `dragReorderGhost` to a copy of the dragged
30
+ * row rendered while `value.draggedIndex` is set. The ghost is positioned over the dragged row and
31
+ * follows the pointer; style its opacity yourself.
32
+ *
33
+ * Nothing is reordered for you: apply each {@link DragReorderMove} to your own data.
34
+ *
35
+ * @category Reorder
36
+ */
37
+ export class DragReorderManager extends Observable {
38
+ rowsElement;
39
+ /** The callback given to `attachRows`. */
40
+ onReorder;
41
+ draggedRow;
42
+ ghostElement;
43
+ pointerStart = {
44
+ x: 0,
45
+ y: 0,
46
+ };
47
+ /** Removes the global listeners that move the ghost. Set only while a drag is in progress. */
48
+ stopTrackingPointer;
49
+ constructor() {
50
+ super({
51
+ defaultValue: idleState,
52
+ });
53
+ }
54
+ /** Whether to draw the drop line above or below the row at `rowIndex`. */
55
+ readDropLine(rowIndex) {
56
+ return {
57
+ isAbove: this.value.dropIndex === rowIndex,
58
+ isBelow: rowIndex === this.value.rowCount - 1 &&
59
+ this.value.dropIndex === this.value.rowCount,
60
+ };
61
+ }
62
+ /** Used by `dragReorderRows`. */
63
+ attachRows(rowsElement, onReorder) {
64
+ this.rowsElement = rowsElement;
65
+ this.onReorder = onReorder;
66
+ }
67
+ /** Used by `dragReorderGhost`. */
68
+ attachGhost(ghostElement) {
69
+ if (ghostElement === this.ghostElement) {
70
+ return;
71
+ }
72
+ this.ghostElement = ghostElement;
73
+ ghostElement.style.position = 'absolute';
74
+ ghostElement.style.boxSizing = 'border-box';
75
+ ghostElement.style.margin = '0';
76
+ ghostElement.style.pointerEvents = 'none';
77
+ ghostElement.style.visibility = 'hidden';
78
+ /** The ghost is not laid out until after the current render. */
79
+ globalThis.requestAnimationFrame(() => {
80
+ if (ghostElement !== this.ghostElement || !this.draggedRow) {
81
+ return;
82
+ }
83
+ const rowRect = this.draggedRow.getBoundingClientRect();
84
+ const translate = ghostElement.style.translate;
85
+ ghostElement.style.translate = '';
86
+ ghostElement.style.left = '0';
87
+ ghostElement.style.top = '0';
88
+ ghostElement.style.width = `${rowRect.width}px`;
89
+ /**
90
+ * Measuring from a zero offset works whatever the ghost's containing block is, so the
91
+ * host does not have to position anything.
92
+ */
93
+ const ghostRect = ghostElement.getBoundingClientRect();
94
+ ghostElement.style.left = `${rowRect.left - ghostRect.left}px`;
95
+ ghostElement.style.top = `${rowRect.top - ghostRect.top}px`;
96
+ ghostElement.style.translate = translate;
97
+ ghostElement.style.visibility = '';
98
+ });
99
+ }
100
+ /** Used by `dragReorderHandle`. */
101
+ startDrag(event, handle) {
102
+ const rows = [...(this.rowsElement?.children || [])];
103
+ const draggedIndex = rows.findIndex((row) => row.contains(handle));
104
+ const draggedRow = rows[draggedIndex];
105
+ if (!draggedRow) {
106
+ return;
107
+ }
108
+ this.stopTrackingPointer?.();
109
+ this.draggedRow = draggedRow;
110
+ this.pointerStart = {
111
+ x: event.clientX,
112
+ y: event.clientY,
113
+ };
114
+ if (event.dataTransfer) {
115
+ event.dataTransfer.setData('text/plain', '');
116
+ event.dataTransfer.effectAllowed = 'move';
117
+ setEmptyDragImage(event.dataTransfer);
118
+ }
119
+ const stopListeners = [
120
+ listenToGlobal('dragover', (dragEvent) => {
121
+ /**
122
+ * Accepting the drop everywhere stops the browser from animating its drag preview
123
+ * back to the start when the pointer is released.
124
+ */
125
+ dragEvent.preventDefault();
126
+ if (this.ghostElement) {
127
+ this.ghostElement.style.translate = [
128
+ `${dragEvent.clientX - this.pointerStart.x}px`,
129
+ `${dragEvent.clientY - this.pointerStart.y}px`,
130
+ ].join(' ');
131
+ }
132
+ }),
133
+ listenToGlobal('drop', (dropEvent) => dropEvent.preventDefault()),
134
+ ];
135
+ this.stopTrackingPointer = () => {
136
+ stopListeners.forEach((stopListener) => stopListener());
137
+ };
138
+ this.setValue({
139
+ draggedIndex,
140
+ dropIndex: undefined,
141
+ rowCount: rows.length,
142
+ draggedCellWidths: [...draggedRow.children].map((cell) => cell.getBoundingClientRect().width),
143
+ });
144
+ }
145
+ /** Used by `dragReorderRows`. */
146
+ dragOver(event) {
147
+ if (this.value.draggedIndex == undefined) {
148
+ return;
149
+ }
150
+ event.preventDefault();
151
+ if (event.dataTransfer) {
152
+ event.dataTransfer.dropEffect = 'move';
153
+ }
154
+ const dropIndex = this.readDropIndex(event.clientY);
155
+ if (dropIndex !== this.value.dropIndex) {
156
+ this.setValue({
157
+ ...this.value,
158
+ dropIndex,
159
+ });
160
+ }
161
+ }
162
+ /** Used by `dragReorderRows`. */
163
+ drop(event) {
164
+ if (this.value.draggedIndex == undefined) {
165
+ return;
166
+ }
167
+ event.preventDefault();
168
+ this.finishDrag(this.readDropIndex(event.clientY));
169
+ }
170
+ /**
171
+ * Used by `dragReorderHandle`. A release outside the rows still lands where the drop line was
172
+ * last drawn.
173
+ */
174
+ endDrag() {
175
+ this.finishDrag(this.value.dropIndex);
176
+ }
177
+ destroy() {
178
+ this.stopTrackingPointer?.();
179
+ super.destroy();
180
+ }
181
+ /** Counts the rows whose vertical midpoint is above the pointer. */
182
+ readDropIndex(clientY) {
183
+ const draggedIndex = this.value.draggedIndex;
184
+ const dropIndex = [...(this.rowsElement?.children || [])].filter((row) => {
185
+ const rect = row.getBoundingClientRect();
186
+ return clientY >= rect.top + rect.height / 2;
187
+ }).length;
188
+ /** Dropping a row directly above or below itself would not move it. */
189
+ return dropIndex === draggedIndex ||
190
+ draggedIndex == undefined ||
191
+ dropIndex === draggedIndex + 1
192
+ ? undefined
193
+ : dropIndex;
194
+ }
195
+ /** Resets to the idle state and only calls `onReorder` when `dropIndex` is defined. */
196
+ finishDrag(dropIndex) {
197
+ const fromIndex = this.value.draggedIndex;
198
+ this.stopTrackingPointer?.();
199
+ this.stopTrackingPointer = undefined;
200
+ this.draggedRow = undefined;
201
+ this.ghostElement = undefined;
202
+ if (fromIndex == undefined) {
203
+ return;
204
+ }
205
+ this.setValue(idleState);
206
+ if (dropIndex != undefined) {
207
+ this.onReorder?.({
208
+ fromIndex,
209
+ toIndex: dropIndex,
210
+ });
211
+ }
212
+ }
213
+ }
@@ -1,6 +1,7 @@
1
1
  export * from './anchored-popover.js';
2
2
  export * from './define-table.js';
3
3
  export * from './define-vira-element.js';
4
+ export * from './drag-reorder-manager.js';
4
5
  export * from './dynamic-element.js';
5
6
  export * from './fuzzy-match.js';
6
7
  export * from './menu-helpers.js';
@@ -1,6 +1,7 @@
1
1
  export * from './anchored-popover.js';
2
2
  export * from './define-table.js';
3
3
  export * from './define-vira-element.js';
4
+ export * from './drag-reorder-manager.js';
4
5
  export * from './dynamic-element.js';
5
6
  export * from './fuzzy-match.js';
6
7
  export * from './menu-helpers.js';
@@ -21,12 +21,11 @@ function doesMatch({ input, matcher }) {
21
21
  }
22
22
  function isAllowed({ value: rawValue, allowed, blocked }) {
23
23
  const value = String(rawValue);
24
- const isAllowedCharacter = allowed
25
- ? doesMatch({
24
+ const isAllowedCharacter = !allowed ||
25
+ doesMatch({
26
26
  input: value,
27
27
  matcher: allowed,
28
- })
29
- : true;
28
+ });
30
29
  const isBlockedCharacter = blocked
31
30
  ? doesMatch({
32
31
  input: value,
@@ -138,3 +138,15 @@ export declare function applyRequiredLabel(label: string | HtmlInterpolation | u
138
138
  * @category Internal
139
139
  */
140
140
  export declare function areFormFieldsValid(formFields: Readonly<ViraFormFields>): boolean;
141
+ /**
142
+ * Moves one field key in front of another, keeping every other key (including hidden fields) in
143
+ * place. Used by `ViraForm` when its fields are reordered.
144
+ *
145
+ * @category Internal
146
+ */
147
+ export declare function moveFormFieldKey({ keys, movedKey, beforeKey, }: Readonly<{
148
+ keys: ReadonlyArray<string>;
149
+ movedKey: string;
150
+ /** The key that `movedKey` lands in front of. `undefined` moves it to the end. */
151
+ beforeKey: string | undefined;
152
+ }>): string[];
@@ -68,3 +68,19 @@ export function areFormFieldsValid(formFields) {
68
68
  }
69
69
  });
70
70
  }
71
+ /**
72
+ * Moves one field key in front of another, keeping every other key (including hidden fields) in
73
+ * place. Used by `ViraForm` when its fields are reordered.
74
+ *
75
+ * @category Internal
76
+ */
77
+ export function moveFormFieldKey({ keys, movedKey, beforeKey, }) {
78
+ const remainingKeys = keys.filter((key) => key !== movedKey);
79
+ const insertIndex = beforeKey == undefined ? -1 : remainingKeys.indexOf(beforeKey);
80
+ return insertIndex === -1
81
+ ? [
82
+ ...remainingKeys,
83
+ movedKey,
84
+ ]
85
+ : remainingKeys.toSpliced(insertIndex, 0, movedKey);
86
+ }
@@ -83,10 +83,7 @@ export function normalizeSchema(schema) {
83
83
  }
84
84
  }
85
85
  function jsonTypeFromSchemaType(type) {
86
- if (allJsonTypes.includes(type)) {
87
- return type;
88
- }
89
- return undefined;
86
+ return check.isEnumValue(type, ViraJsonType) ? type : undefined;
90
87
  }
91
88
  /** @category Internal */
92
89
  export function createResolveContext(schema) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vira",
3
- "version": "33.2.0",
3
+ "version": "33.4.0",
4
4
  "description": "A simple and highly versatile design system using element-vir.",
5
5
  "keywords": [
6
6
  "design",
@@ -38,24 +38,24 @@
38
38
  "test:docs": "virmator docs check"
39
39
  },
40
40
  "dependencies": {
41
- "@augment-vir/assert": "^32.3.1",
42
- "@augment-vir/common": "^32.3.1",
43
- "@augment-vir/web": "^32.3.1",
41
+ "@augment-vir/assert": "^32.5.0",
42
+ "@augment-vir/common": "^32.5.0",
43
+ "@augment-vir/web": "^32.5.0",
44
44
  "@electrovir/color": "^2.0.0",
45
45
  "@electrovir/local-storage-client": "^0.1.1",
46
- "date-vir": "^9.1.1",
46
+ "date-vir": "^9.4.0",
47
47
  "device-navigation": "^5.0.0",
48
48
  "json-schema-to-ts": "^3.1.1",
49
49
  "lit": "^3.3.3",
50
50
  "lit-css-vars": "^3.6.3",
51
- "object-shape-tester": "^6.15.0",
52
- "observavir": "^2.3.3",
51
+ "object-shape-tester": "^6.15.1",
52
+ "observavir": "^2.4.2",
53
53
  "page-active": "^1.0.4",
54
54
  "spa-router-vir": "^6.8.0",
55
55
  "typed-event-target": "^4.3.4"
56
56
  },
57
57
  "devDependencies": {
58
- "@augment-vir/test": "^32.3.1",
58
+ "@augment-vir/test": "^32.5.0",
59
59
  "@web/dev-server-esbuild": "^2.0.0",
60
60
  "@web/test-runner": "^1.0.0",
61
61
  "@web/test-runner-commands": "^1.0.1",
@@ -63,12 +63,12 @@
63
63
  "@web/test-runner-visual-regression": "^1.0.1",
64
64
  "esbuild": "^0.28.2",
65
65
  "istanbul-smart-text-reporter": "^1.1.5",
66
- "lucide-static": "^1.46.0",
67
- "markdown-code-example-inserter": "^3.0.6",
68
- "theme-vir": "^29.0.0",
66
+ "lucide-static": "^1.54.0",
67
+ "markdown-code-example-inserter": "^3.1.1",
68
+ "theme-vir": "^29.0.1",
69
69
  "typedoc": "^0.28.20",
70
70
  "typescript": "^6.0.3",
71
- "vite": "^8.3.0",
71
+ "vite": "^8.3.4",
72
72
  "vite-tsconfig-paths": "^6.1.1"
73
73
  },
74
74
  "peerDependencies": {