@getdom/studio 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE.md +109 -0
  2. package/README.md +44 -0
  3. package/SKILL.md +83 -0
  4. package/package.json +73 -0
  5. package/public/llms.txt +57 -0
  6. package/public/skills.sh +11 -0
  7. package/src/pages/_layout/componentCatalog.generated.js +4514 -0
  8. package/src/pages/_layout/componentInspector.js +60 -0
  9. package/src/pages/_layout/componentManager.js +192 -0
  10. package/src/pages/_layout/componentRuntimeRecords.js +49 -0
  11. package/src/pages/_layout/inspector/componentRegistry.js +639 -0
  12. package/src/pages/_layout/inspector/serialize.js +134 -0
  13. package/src/pages/_layout/inspector/useInspector.js +293 -0
  14. package/src/pages/components/accordion/DomAccordion.vue +39 -0
  15. package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
  16. package/src/pages/components/button/DomButton.vue +132 -0
  17. package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
  18. package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
  19. package/src/pages/components/dialog/DomDialog.vue +271 -0
  20. package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
  21. package/src/pages/components/dialog/DomDialogStack.vue +289 -0
  22. package/src/pages/components/dialog/useDialogs.js +253 -0
  23. package/src/pages/components/drawer/DomDrawer.vue +159 -0
  24. package/src/pages/components/dropdown/DomDropdown.vue +261 -0
  25. package/src/pages/components/icon-button/DomIconButton.vue +155 -0
  26. package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
  27. package/src/pages/components/menu/DomMenu.vue +359 -0
  28. package/src/pages/components/menu/DomMenuItem.vue +233 -0
  29. package/src/pages/components/menu/DomMenuLabel.vue +19 -0
  30. package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
  31. package/src/pages/components/menu/menuContext.js +11 -0
  32. package/src/pages/components/menu/menuPlacement.js +26 -0
  33. package/src/pages/components/menu/submenu.js +58 -0
  34. package/src/pages/components/pagination/DomPagination.vue +265 -0
  35. package/src/pages/components/playground/DomRenderer.vue +48 -0
  36. package/src/pages/components/popover/DomPopover.vue +244 -0
  37. package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
  38. package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
  39. package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
  40. package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
  41. package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
  42. package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
  43. package/src/pages/components/stepper/DomStepper.vue +386 -0
  44. package/src/pages/components/tabs/DomTabs.vue +149 -0
  45. package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
  46. package/src/pages/components/toast/DomToastItem.vue +52 -0
  47. package/src/pages/components/toast/DomToastStack.vue +768 -0
  48. package/src/pages/components/toast/useToasts.js +237 -0
  49. package/src/pages/components/toolbar/DomToolbar.vue +158 -0
  50. package/src/pages/components/tooltip/DomTooltip.vue +69 -0
  51. package/src/pages/components/tooltip/vDomTooltip.js +106 -0
  52. package/src/pages/components/tree-view/DomTreeView.vue +961 -0
  53. package/src/pages/components/tree-view/treeSchemas.js +129 -0
  54. package/src/pages/components/tree-view/treeUtils.js +228 -0
  55. package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
  56. package/src/pages/data/_shared/resourceRecord.js +29 -0
  57. package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
  58. package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
  59. package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
  60. package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
  61. package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
  62. package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
  63. package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
  64. package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
  65. package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
  66. package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
  67. package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
  68. package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
  69. package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
  70. package/src/pages/data/kanban/DomKanban.vue +1703 -0
  71. package/src/pages/data/kanban/kanbanApi.js +80 -0
  72. package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
  73. package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
  74. package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
  75. package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
  76. package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
  77. package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
  78. package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
  79. package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
  80. package/src/pages/forms/_shared/options.js +61 -0
  81. package/src/pages/forms/_shared/useOptionMenu.js +59 -0
  82. package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
  83. package/src/pages/forms/calendar/DomCalendar.vue +442 -0
  84. package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
  85. package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
  86. package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
  87. package/src/pages/forms/color-input/DomColorInput.vue +118 -0
  88. package/src/pages/forms/combobox/DomCombobox.vue +237 -0
  89. package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
  90. package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
  91. package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
  92. package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
  93. package/src/pages/forms/field/DomField.vue +267 -0
  94. package/src/pages/forms/field/fieldLayout.js +1 -0
  95. package/src/pages/forms/field/fieldProps.js +114 -0
  96. package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
  97. package/src/pages/forms/field/useField.js +343 -0
  98. package/src/pages/forms/field/validators.js +364 -0
  99. package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
  100. package/src/pages/forms/form/DomForm.vue +985 -0
  101. package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
  102. package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
  103. package/src/pages/forms/form/adapters/shared.js +53 -0
  104. package/src/pages/forms/form/adapters/zod.js +235 -0
  105. package/src/pages/forms/form/formApi.js +38 -0
  106. package/src/pages/forms/form/formDefinition.js +392 -0
  107. package/src/pages/forms/form/schemaAdapter.js +54 -0
  108. package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
  109. package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
  110. package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
  111. package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
  112. package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
  113. package/src/pages/forms/listbox/DomListbox.vue +105 -0
  114. package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
  115. package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
  116. package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
  117. package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
  118. package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
  119. package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
  120. package/src/pages/forms/password-strength/passwordStrength.js +92 -0
  121. package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
  122. package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
  123. package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
  124. package/src/pages/forms/select/DomSelect.vue +479 -0
  125. package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
  126. package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
  127. package/src/pages/forms/text-input/DomTextInput.vue +48 -0
  128. package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
  129. package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
  130. package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
  131. package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
  132. package/src/pages/forms/time-selector/timeUtils.js +194 -0
  133. package/src/pages/forms/toggle/DomToggle.vue +158 -0
  134. package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
  135. package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
  136. package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
  137. package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
  138. package/src/pages/lib/css/color.js +245 -0
  139. package/src/pages/lib/headless/accordion.d.ts +7 -0
  140. package/src/pages/lib/headless/accordion.js +246 -0
  141. package/src/pages/lib/headless/autocomplete.d.ts +4 -0
  142. package/src/pages/lib/headless/autocomplete.js +33 -0
  143. package/src/pages/lib/headless/base.d.ts +25 -0
  144. package/src/pages/lib/headless/base.js +127 -0
  145. package/src/pages/lib/headless/checkbox.d.ts +19 -0
  146. package/src/pages/lib/headless/checkbox.js +99 -0
  147. package/src/pages/lib/headless/combobox.d.ts +7 -0
  148. package/src/pages/lib/headless/combobox.js +488 -0
  149. package/src/pages/lib/headless/dialog.d.ts +23 -0
  150. package/src/pages/lib/headless/dialog.js +384 -0
  151. package/src/pages/lib/headless/drawer.d.ts +21 -0
  152. package/src/pages/lib/headless/drawer.js +682 -0
  153. package/src/pages/lib/headless/dropdown.d.ts +19 -0
  154. package/src/pages/lib/headless/dropdown.js +412 -0
  155. package/src/pages/lib/headless/floating-effect.js +118 -0
  156. package/src/pages/lib/headless/floating.d.ts +34 -0
  157. package/src/pages/lib/headless/floating.js +382 -0
  158. package/src/pages/lib/headless/index.d.ts +19 -0
  159. package/src/pages/lib/headless/index.js +19 -0
  160. package/src/pages/lib/headless/listbox.d.ts +19 -0
  161. package/src/pages/lib/headless/listbox.js +126 -0
  162. package/src/pages/lib/headless/menu.d.ts +18 -0
  163. package/src/pages/lib/headless/menu.js +117 -0
  164. package/src/pages/lib/headless/popover-panel.d.ts +18 -0
  165. package/src/pages/lib/headless/popover-panel.js +309 -0
  166. package/src/pages/lib/headless/popover.d.ts +19 -0
  167. package/src/pages/lib/headless/popover.js +391 -0
  168. package/src/pages/lib/headless/radio-group.d.ts +19 -0
  169. package/src/pages/lib/headless/radio-group.js +100 -0
  170. package/src/pages/lib/headless/tabs.d.ts +18 -0
  171. package/src/pages/lib/headless/tabs.js +119 -0
  172. package/src/pages/lib/headless/toast.d.ts +13 -0
  173. package/src/pages/lib/headless/toast.js +65 -0
  174. package/src/pages/lib/headless/toggle.d.ts +19 -0
  175. package/src/pages/lib/headless/toggle.js +162 -0
  176. package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
  177. package/src/pages/lib/headless/tooltip-controller.js +549 -0
  178. package/src/pages/lib/headless/tooltip.d.ts +19 -0
  179. package/src/pages/lib/headless/tooltip.js +153 -0
  180. package/src/pages/lib/headless/transitions.d.ts +5 -0
  181. package/src/pages/lib/headless/transitions.js +89 -0
  182. package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
  183. package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
  184. package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
  185. package/src/pages/lib/vue/codeHighlighter.js +132 -0
  186. package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
  187. package/src/pages/lib/vue/index.d.ts +6806 -0
  188. package/src/pages/lib/vue/index.js +218 -0
  189. package/src/pages/lib/vue/useTheme.js +116 -0
  190. package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
  191. package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
  192. package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
  193. package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
  194. package/src/pages/mobile/shell/DomAppShell.vue +51 -0
  195. package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
  196. package/src/pages/mobile/stack/DomAppStack.vue +711 -0
  197. package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
  198. package/src/pages/visual/alert/DomAlert.vue +363 -0
  199. package/src/pages/visual/avatar/DomAvatar.vue +110 -0
  200. package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
  201. package/src/pages/visual/badge/DomBadge.vue +105 -0
  202. package/src/pages/visual/card/DomCard.vue +65 -0
  203. package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
  204. package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
  205. package/src/pages/visual/progress/DomProgress.vue +212 -0
  206. package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
  207. package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
  208. package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
  209. package/src/pages/visual/spinner/DomSpinner.vue +122 -0
  210. package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
  211. package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
  212. package/src/pages/visual/text-diff/textDiff.js +300 -0
  213. package/src/style.css +446 -0
@@ -0,0 +1,343 @@
1
+ import { computed, inject, onBeforeUnmount, ref, unref, useId, watch } from 'vue';
2
+ import { compileValidators, normalizeValidatorResult, requiredValidator } from './validators.js';
3
+
4
+ export const formFieldProviderKey = Symbol('domStudio.formFieldProvider');
5
+ export const fieldDisplayProviderKey = Symbol('domStudio.fieldDisplayProvider');
6
+
7
+ function providerValue(provider, name) {
8
+ if (!provider || !name) return undefined;
9
+ if (typeof provider.getValue === 'function') return unref(provider.getValue(name));
10
+ const values = provider.values || provider.data;
11
+ if (!values) return undefined;
12
+ return unref(values)?.[name];
13
+ }
14
+
15
+ function setProviderValue(provider, name, value) {
16
+ if (!provider || !name) return;
17
+ if (typeof provider.setValue === 'function') {
18
+ provider.setValue(name, value);
19
+ return;
20
+ }
21
+ const values = unref(provider.values || provider.data);
22
+ if (values && typeof values === 'object') values[name] = value;
23
+ }
24
+
25
+ function providerState(provider, name) {
26
+ if (!provider || !name) return {};
27
+ if (typeof provider.getFieldState === 'function') return unref(provider.getFieldState(name)) || {};
28
+ const states = unref(provider.fieldStates || provider.states);
29
+ return states?.[name] || {};
30
+ }
31
+
32
+ function setProviderState(provider, name, patch) {
33
+ if (!provider || !name) return;
34
+ if (typeof provider.setFieldState === 'function') {
35
+ provider.setFieldState(name, patch);
36
+ return;
37
+ }
38
+ const states = unref(provider.fieldStates || provider.states);
39
+ if (states && typeof states === 'object') {
40
+ states[name] = {
41
+ ...(states[name] || {}),
42
+ ...patch,
43
+ };
44
+ }
45
+ }
46
+
47
+ function providerPath(provider, name) {
48
+ if (!provider || !name) return name || '';
49
+ if (typeof provider.getFieldPath === 'function') return unref(provider.getFieldPath(name));
50
+ return name;
51
+ }
52
+
53
+ function providerHtmlName(provider, name) {
54
+ if (!provider || !name) return name || '';
55
+ if (typeof provider.getHtmlName === 'function') return unref(provider.getHtmlName(name));
56
+ return name;
57
+ }
58
+
59
+ function htmlIdFromPath(path) {
60
+ return String(path || '')
61
+ .split('.')
62
+ .filter(Boolean)
63
+ .join('_')
64
+ .replace(/[^A-Za-z0-9_]+/g, '_')
65
+ .replace(/^_+|_+$/g, '');
66
+ }
67
+
68
+ function providerHtmlId(provider, name) {
69
+ if (!provider || !name) return htmlIdFromPath(name);
70
+ if (typeof provider.getHtmlId === 'function') return unref(provider.getHtmlId(name));
71
+ return htmlIdFromPath(name);
72
+ }
73
+
74
+ export function normalizeErrors(errors) {
75
+ const value = unref(errors);
76
+ if (!value) return [];
77
+ if (typeof value === 'string') return value ? [value] : [];
78
+ if (Array.isArray(value)) {
79
+ return value
80
+ .flatMap((error) => normalizeErrors(error))
81
+ .filter(Boolean);
82
+ }
83
+ if (typeof value === 'object') {
84
+ if (value.message) return [value.message];
85
+ return Object.values(value)
86
+ .flatMap((error) => normalizeErrors(error))
87
+ .filter(Boolean);
88
+ }
89
+ return [];
90
+ }
91
+
92
+ export function useField(props, emit, options = {}) {
93
+ const injectedProvider = inject(formFieldProviderKey, null);
94
+ const injectedDisplayProvider = inject(fieldDisplayProviderKey, null);
95
+ const provider = options.register === false ? null : injectedProvider;
96
+ const displayProvider = options.register === false || options.display === false ? null : injectedDisplayProvider;
97
+ const displayRegistrationKey = Symbol(options.idPrefix || 'domStudio.fieldDisplayRegistration');
98
+ const generatedId = `${options.idPrefix || 'dom-field'}-${useId()}`;
99
+ const focused = ref(false);
100
+ const touchedState = ref(false);
101
+ const dirty = ref(false);
102
+ const validating = ref(false);
103
+ const internalValue = ref(props.modelValue);
104
+ const localErrors = ref([]);
105
+ let validationRun = 0;
106
+
107
+ const name = computed(() => props.name || props.id || generatedId);
108
+ const path = computed(() => providerPath(provider, name.value));
109
+ const htmlName = computed(() => providerHtmlName(provider, name.value));
110
+ const htmlId = computed(() => props.id || providerHtmlId(provider, name.value) || htmlIdFromPath(path.value) || generatedId);
111
+ const id = htmlId;
112
+ const descriptionId = computed(() => (props.description ? `${id.value}-description` : ''));
113
+ const errorId = computed(() => (errors.value.length ? `${id.value}-error` : ''));
114
+ const describedBy = computed(() => [descriptionId.value, errorId.value].filter(Boolean).join(' '));
115
+ const value = computed(() => {
116
+ const provided = providerValue(provider, name.value);
117
+ return provided !== undefined ? provided : internalValue.value;
118
+ });
119
+ const providerFieldState = computed(() => providerState(provider, name.value));
120
+ const errors = computed(() => [
121
+ ...normalizeErrors(props.errors),
122
+ ...normalizeErrors(providerFieldState.value.errors),
123
+ ...normalizeErrors(localErrors.value),
124
+ ]);
125
+ const invalid = computed(() => Boolean(props.invalid || providerFieldState.value.invalid || errors.value.length));
126
+ const interaction = computed(() => (
127
+ providerFieldState.value.interaction
128
+ || (providerFieldState.value.focused ? 'focused' : providerFieldState.value.touched ? 'blurred' : '')
129
+ || (focused.value ? 'focused' : touchedState.value ? 'blurred' : 'untouched')
130
+ ));
131
+ const modification = computed(() => providerFieldState.value.modification || (providerFieldState.value.dirty || dirty.value ? 'changed' : 'clean'));
132
+ const validation = computed(() => {
133
+ if (validating.value || providerFieldState.value.validating) return 'validating';
134
+ if (invalid.value) return 'invalid';
135
+ if (providerFieldState.value.valid) return 'valid';
136
+ if (providerFieldState.value.validation && providerFieldState.value.validation !== 'unknown') {
137
+ return providerFieldState.value.validation;
138
+ }
139
+ if (providerFieldState.value.validation === 'unknown') return 'unknown';
140
+ if (
141
+ 'invalid' in providerFieldState.value
142
+ || 'errors' in providerFieldState.value
143
+ || 'validating' in providerFieldState.value
144
+ || localErrors.value.length
145
+ ) return 'valid';
146
+ return 'unknown';
147
+ });
148
+ const valid = computed(() => validation.value === 'valid');
149
+ const touched = computed(() => interaction.value !== 'untouched');
150
+ const visible = computed(() => props.visible !== false && providerFieldState.value.visible !== false);
151
+ const disabled = computed(() => Boolean(props.disabled || providerFieldState.value.disabled));
152
+ const readOnly = computed(() => Boolean(props.readOnly || providerFieldState.value.readOnly));
153
+
154
+ const fieldAttrs = computed(() => ({
155
+ label: props.label,
156
+ description: props.description,
157
+ htmlFor: id.value,
158
+ descriptionId: descriptionId.value,
159
+ errorId: errorId.value,
160
+ invalid: invalid.value,
161
+ required: props.required,
162
+ errors: errors.value,
163
+ visible: visible.value,
164
+ }));
165
+
166
+ /**
167
+ * This helps decorate the input with the relevant html attributes.
168
+ */
169
+ const inputAttrs = computed(() => ({
170
+ id: id.value,
171
+ name: htmlName.value,
172
+ value: value.value,
173
+ placeholder: props.placeholder,
174
+ disabled: disabled.value || undefined,
175
+ readonly: readOnly.value || undefined,
176
+ required: props.required || undefined,
177
+ 'aria-invalid': invalid.value || undefined,
178
+ 'aria-describedby': describedBy.value || undefined,
179
+ 'aria-errormessage': errorId.value || undefined,
180
+ 'data-invalid': invalid.value ? '' : undefined,
181
+ }));
182
+
183
+ if (displayProvider?.registerField) {
184
+ displayProvider.registerField(displayRegistrationKey, {
185
+ id,
186
+ name,
187
+ path,
188
+ htmlName,
189
+ htmlId,
190
+ descriptionId,
191
+ errorId,
192
+ describedBy,
193
+ value,
194
+ errors,
195
+ invalid,
196
+ interaction,
197
+ modification,
198
+ validation,
199
+ valid,
200
+ focused,
201
+ touched,
202
+ dirty,
203
+ validating,
204
+ visible,
205
+ disabled,
206
+ readOnly,
207
+ fieldAttrs,
208
+ inputAttrs,
209
+ });
210
+ }
211
+
212
+ watch(() => props.modelValue, (nextValue) => {
213
+ internalValue.value = nextValue;
214
+ });
215
+
216
+ watch(value, () => {
217
+ if (!props.validateOnBlur) validate();
218
+ });
219
+
220
+ onBeforeUnmount(() => {
221
+ if (name.value && provider?.unregisterField) provider.unregisterField(name.value);
222
+ if (displayProvider?.unregisterField) displayProvider.unregisterField(displayRegistrationKey);
223
+ });
224
+
225
+ function setFieldState(patch) {
226
+ setProviderState(provider, name.value, patch);
227
+ }
228
+
229
+ function setValue(nextValue) {
230
+ internalValue.value = nextValue;
231
+ setProviderValue(provider, name.value, nextValue);
232
+ emit?.('update:modelValue', nextValue);
233
+ dirty.value = true;
234
+ setFieldState({ modification: 'changed', dirty: true, value: nextValue });
235
+ }
236
+
237
+ function onInput(nextValue) {
238
+ setValue(nextValue);
239
+ }
240
+
241
+ function onFocus(event) {
242
+ focused.value = true;
243
+ setFieldState({ interaction: 'focused', focused: true, touched: true });
244
+ emit?.('focus', event);
245
+ }
246
+
247
+ async function onBlur(event) {
248
+ focused.value = false;
249
+ touchedState.value = true;
250
+ setFieldState({ interaction: 'blurred', focused: false, touched: true });
251
+ emit?.('blur', event);
252
+ if (props.validateOnBlur) await validate();
253
+ }
254
+
255
+ async function validate(extraContext = {}) {
256
+ const currentRun = ++validationRun;
257
+ const nextErrors = [];
258
+ const validators = compileValidators([
259
+ ...(props.required ? [requiredValidator] : []),
260
+ ...(props.validators || []),
261
+ ]);
262
+ validating.value = true;
263
+ setFieldState({ validation: 'validating', validating: true });
264
+ for (const validator of validators) {
265
+ const validateFn = typeof validator === 'function' ? validator : validator.validate;
266
+ if (!validateFn) continue;
267
+ const result = await validateFn(value.value, {
268
+ id: id.value,
269
+ name: name.value,
270
+ path: path.value,
271
+ htmlName: htmlName.value,
272
+ htmlId: htmlId.value,
273
+ props,
274
+ provider,
275
+ ...extraContext,
276
+ });
277
+ const normalized = normalizeValidatorResult(result, validator);
278
+ if (normalized) nextErrors.push(normalized);
279
+ }
280
+ if (currentRun === validationRun) {
281
+ localErrors.value = provider ? [] : nextErrors;
282
+ validating.value = false;
283
+ setFieldState({
284
+ errors: nextErrors,
285
+ invalid: nextErrors.length > 0,
286
+ validation: nextErrors.length > 0 ? 'invalid' : 'valid',
287
+ validating: false,
288
+ });
289
+ }
290
+ return nextErrors.length === 0;
291
+ }
292
+
293
+ watch(name, (nextName, previousName) => {
294
+ if (previousName && provider?.unregisterField) provider.unregisterField(previousName);
295
+ if (nextName && provider?.registerField) {
296
+ provider.registerField({
297
+ id: id.value,
298
+ name: nextName,
299
+ path: path.value,
300
+ htmlName: htmlName.value,
301
+ htmlId: htmlId.value,
302
+ label: props.label,
303
+ required: props.required,
304
+ initialValue: internalValue.value,
305
+ validators: props.validators,
306
+ validate,
307
+ });
308
+ }
309
+ }, { immediate: true });
310
+
311
+ return {
312
+ id,
313
+ name,
314
+ path,
315
+ htmlName,
316
+ htmlId,
317
+ descriptionId,
318
+ errorId,
319
+ describedBy,
320
+ value,
321
+ errors,
322
+ invalid,
323
+ interaction,
324
+ modification,
325
+ validation,
326
+ valid,
327
+ focused,
328
+ touched,
329
+ dirty,
330
+ validating,
331
+ visible,
332
+ disabled,
333
+ readOnly,
334
+ fieldAttrs,
335
+ inputAttrs,
336
+ setFieldState,
337
+ setValue,
338
+ onInput,
339
+ onFocus,
340
+ onBlur,
341
+ validate,
342
+ };
343
+ }
@@ -0,0 +1,364 @@
1
+ export function createValidator(name, validate, message = 'This field is invalid.', options = {}) {
2
+ return {
3
+ ...options,
4
+ name,
5
+ message,
6
+ validate,
7
+ };
8
+ }
9
+
10
+ const validatorRegistry = new Map();
11
+
12
+ function valueFromProps(props = {}, names = ['value']) {
13
+ for (const name of names) {
14
+ if (props[name] !== undefined) return props[name];
15
+ }
16
+ return undefined;
17
+ }
18
+
19
+ function isEmptyValue(value) {
20
+ if (Array.isArray(value)) return value.length === 0;
21
+ return value === undefined || value === null || value === '';
22
+ }
23
+
24
+ function validatorProps(record = {}) {
25
+ return {
26
+ ...(record.props || {}),
27
+ ...(record.options || {}),
28
+ ...Object.fromEntries(Object.entries(record).filter(([key]) => ![
29
+ 'name',
30
+ 'rule',
31
+ 'type',
32
+ 'validator',
33
+ 'validate',
34
+ 'message',
35
+ 'props',
36
+ 'options',
37
+ ].includes(key))),
38
+ };
39
+ }
40
+
41
+ export function defineValidator(definition) {
42
+ if (!definition?.name || typeof definition.validate !== 'function') return null;
43
+ const {
44
+ name,
45
+ validate,
46
+ message,
47
+ ...metadata
48
+ } = definition;
49
+ const validator = createValidator(name, validate, message, metadata);
50
+ validatorRegistry.set(definition.name, validator);
51
+ return validator;
52
+ }
53
+
54
+ export function getValidator(name) {
55
+ return validatorRegistry.get(name) || null;
56
+ }
57
+
58
+ export function listValidators() {
59
+ return Array.from(validatorRegistry.values());
60
+ }
61
+
62
+ export function compileValidator(validator) {
63
+ if (!validator) return null;
64
+ if (typeof validator === 'function') return createValidator(validator.name || 'validator', validator);
65
+ if (typeof validator === 'string') return getValidator(validator);
66
+ if (typeof validator !== 'object') return null;
67
+ if (typeof validator.validate === 'function') return validator;
68
+
69
+ const name = validator.name || validator.rule || validator.type || validator.validator;
70
+ const registered = getValidator(name);
71
+ if (!registered) return null;
72
+ const props = validatorProps(validator);
73
+
74
+ return createValidator(
75
+ name,
76
+ async (value, context = {}) => {
77
+ const result = await registered.validate(value, {
78
+ ...context,
79
+ props,
80
+ validator,
81
+ });
82
+ if (result !== true && result !== undefined && result !== null && validator.message) {
83
+ return validator.message;
84
+ }
85
+ return result;
86
+ },
87
+ validator.message || registered.message,
88
+ );
89
+ }
90
+
91
+ export function compileValidators(validators = []) {
92
+ return validators.map((validator) => compileValidator(validator)).filter(Boolean);
93
+ }
94
+
95
+ export function normalizeValidatorResult(result, validator) {
96
+ if (result === true || result === undefined || result === null) {
97
+ return null;
98
+ }
99
+ if (typeof result === 'string') {
100
+ return {
101
+ name: validator.name || 'validator',
102
+ message: result,
103
+ };
104
+ }
105
+ if (typeof result === 'object') {
106
+ if (result.valid === true) return null;
107
+ return {
108
+ name: result.name || result.code || validator.name || 'validator',
109
+ message: result.message || validator.message || 'This field is invalid.',
110
+ code: result.code,
111
+ meta: result.meta,
112
+ };
113
+ }
114
+ return {
115
+ name: validator.name || 'validator',
116
+ message: validator.message || 'This field is invalid.',
117
+ };
118
+ }
119
+
120
+ export const requiredValidator = defineValidator({
121
+ name: 'required',
122
+ label: 'Required',
123
+ description: 'Requires a non-empty value.',
124
+ message: 'This field is required.',
125
+ validate(value) {
126
+ if (Array.isArray(value)) return value.length > 0;
127
+ return value !== undefined && value !== null && String(value).trim() !== '';
128
+ },
129
+ });
130
+
131
+ export const emailValidator = defineValidator({
132
+ name: 'email',
133
+ label: 'Email',
134
+ description: 'Requires an email address.',
135
+ message: 'Enter a valid email address.',
136
+ validate(value) {
137
+ if (!value) return true;
138
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value));
139
+ },
140
+ });
141
+
142
+ export const urlValidator = defineValidator({
143
+ name: 'url',
144
+ label: 'URL',
145
+ description: 'Requires an HTTP or HTTPS URL.',
146
+ message: 'Enter a valid URL.',
147
+ validate(value) {
148
+ if (!value) return true;
149
+ try {
150
+ const url = new URL(String(value));
151
+ return ['http:', 'https:'].includes(url.protocol);
152
+ } catch {
153
+ return false;
154
+ }
155
+ },
156
+ });
157
+
158
+ export const minLengthValidator = defineValidator({
159
+ name: 'minLength',
160
+ label: 'Minimum length',
161
+ description: 'Requires text to be at least this many characters.',
162
+ props: {
163
+ min: {
164
+ type: Number,
165
+ default: 2,
166
+ _edit: { label: 'Minimum', description: 'Minimum number of characters.' },
167
+ },
168
+ },
169
+ message: 'This field is too short.',
170
+ validate(value, { props = {} } = {}) {
171
+ if (isEmptyValue(value)) return true;
172
+ const min = Number(valueFromProps(props, ['min', 'value', 'length']));
173
+ if (!Number.isFinite(min)) return true;
174
+ return String(value).length >= min || `Use at least ${min} characters.`;
175
+ },
176
+ });
177
+
178
+ export const maxLengthValidator = defineValidator({
179
+ name: 'maxLength',
180
+ label: 'Maximum length',
181
+ description: 'Requires text to be no longer than this many characters.',
182
+ props: {
183
+ max: {
184
+ type: Number,
185
+ default: 120,
186
+ _edit: { label: 'Maximum', description: 'Maximum number of characters.' },
187
+ },
188
+ },
189
+ message: 'This field is too long.',
190
+ validate(value, { props = {} } = {}) {
191
+ if (isEmptyValue(value)) return true;
192
+ const max = Number(valueFromProps(props, ['max', 'value', 'length']));
193
+ if (!Number.isFinite(max)) return true;
194
+ return String(value).length <= max || `Use ${max} characters or fewer.`;
195
+ },
196
+ });
197
+
198
+ export const lengthValidator = defineValidator({
199
+ name: 'length',
200
+ label: 'Exact length',
201
+ description: 'Requires text to have exactly this many characters.',
202
+ props: {
203
+ length: {
204
+ type: Number,
205
+ default: 8,
206
+ _edit: { label: 'Length', description: 'Exact number of characters.' },
207
+ },
208
+ },
209
+ message: 'This field has the wrong length.',
210
+ validate(value, { props = {} } = {}) {
211
+ if (isEmptyValue(value)) return true;
212
+ const length = Number(valueFromProps(props, ['length', 'value']));
213
+ if (!Number.isFinite(length)) return true;
214
+ return String(value).length === length || `Use exactly ${length} characters.`;
215
+ },
216
+ });
217
+
218
+ export const patternValidator = defineValidator({
219
+ name: 'pattern',
220
+ label: 'Pattern',
221
+ description: 'Requires text to match a regular expression.',
222
+ props: {
223
+ pattern: {
224
+ type: String,
225
+ default: '',
226
+ _edit: { label: 'Pattern', description: 'Regular expression pattern.' },
227
+ },
228
+ flags: {
229
+ type: String,
230
+ default: '',
231
+ _edit: { label: 'Flags', description: 'Optional regular expression flags.' },
232
+ },
233
+ },
234
+ message: 'This field does not match the required pattern.',
235
+ validate(value, { props = {} } = {}) {
236
+ if (isEmptyValue(value)) return true;
237
+ const pattern = valueFromProps(props, ['pattern', 'regex', 'value']);
238
+ if (!pattern) return true;
239
+ const flags = props.flags || '';
240
+ try {
241
+ return new RegExp(pattern, flags).test(String(value));
242
+ } catch {
243
+ return true;
244
+ }
245
+ },
246
+ });
247
+
248
+ export const minValidator = defineValidator({
249
+ name: 'min',
250
+ label: 'Minimum value',
251
+ description: 'Requires a number to be at least this value.',
252
+ props: {
253
+ min: {
254
+ type: Number,
255
+ default: 0,
256
+ _edit: { label: 'Minimum', description: 'Minimum numeric value.' },
257
+ },
258
+ },
259
+ message: 'This field is too small.',
260
+ validate(value, { props = {} } = {}) {
261
+ if (isEmptyValue(value)) return true;
262
+ const min = Number(valueFromProps(props, ['min', 'value']));
263
+ if (!Number.isFinite(min)) return true;
264
+ return Number(value) >= min || `Use a value of ${min} or greater.`;
265
+ },
266
+ });
267
+
268
+ export const maxValidator = defineValidator({
269
+ name: 'max',
270
+ label: 'Maximum value',
271
+ description: 'Requires a number to be at most this value.',
272
+ props: {
273
+ max: {
274
+ type: Number,
275
+ default: 100,
276
+ _edit: { label: 'Maximum', description: 'Maximum numeric value.' },
277
+ },
278
+ },
279
+ message: 'This field is too large.',
280
+ validate(value, { props = {} } = {}) {
281
+ if (isEmptyValue(value)) return true;
282
+ const max = Number(valueFromProps(props, ['max', 'value']));
283
+ if (!Number.isFinite(max)) return true;
284
+ return Number(value) <= max || `Use a value of ${max} or less.`;
285
+ },
286
+ });
287
+
288
+ export const betweenValidator = defineValidator({
289
+ name: 'between',
290
+ label: 'Between',
291
+ description: 'Requires a number to stay inside a range.',
292
+ props: {
293
+ min: {
294
+ type: Number,
295
+ default: 0,
296
+ _edit: { label: 'Minimum', description: 'Minimum numeric value.' },
297
+ },
298
+ max: {
299
+ type: Number,
300
+ default: 100,
301
+ _edit: { label: 'Maximum', description: 'Maximum numeric value.' },
302
+ },
303
+ },
304
+ message: 'This field is outside the allowed range.',
305
+ validate(value, { props = {} } = {}) {
306
+ if (isEmptyValue(value)) return true;
307
+ const min = Number(valueFromProps(props, ['min', 'from']));
308
+ const max = Number(valueFromProps(props, ['max', 'to']));
309
+ const number = Number(value);
310
+ if (!Number.isFinite(number)) return false;
311
+ if (Number.isFinite(min) && number < min) return `Use a value of ${min} or greater.`;
312
+ if (Number.isFinite(max) && number > max) return `Use a value of ${max} or less.`;
313
+ return true;
314
+ },
315
+ });
316
+
317
+ export const numberValidator = defineValidator({
318
+ name: 'number',
319
+ label: 'Number',
320
+ description: 'Requires a numeric value.',
321
+ message: 'Use a number.',
322
+ validate(value) {
323
+ if (isEmptyValue(value)) return true;
324
+ return Number.isFinite(Number(value));
325
+ },
326
+ });
327
+
328
+ export const integerValidator = defineValidator({
329
+ name: 'integer',
330
+ label: 'Integer',
331
+ description: 'Requires a whole number.',
332
+ message: 'Use a whole number.',
333
+ validate(value) {
334
+ if (isEmptyValue(value)) return true;
335
+ return Number.isInteger(Number(value));
336
+ },
337
+ });
338
+
339
+ export const oneOfValidator = defineValidator({
340
+ name: 'oneOf',
341
+ label: 'One of',
342
+ description: 'Requires the value to match an allowed list.',
343
+ props: {
344
+ values: {
345
+ type: Array,
346
+ default: () => [],
347
+ _edit: {
348
+ component: 'DomJsonInput',
349
+ label: 'Values',
350
+ description: 'Allowed values.',
351
+ props: { rows: 4 },
352
+ },
353
+ },
354
+ },
355
+ message: 'Choose one of the allowed values.',
356
+ validate(value, { props = {} } = {}) {
357
+ if (isEmptyValue(value)) return true;
358
+ const values = valueFromProps(props, ['values', 'options', 'value']) || [];
359
+ const allowed = Array.isArray(values) ? values.map((item) => (
360
+ item && typeof item === 'object' && 'value' in item ? item.value : item
361
+ )) : [];
362
+ return allowed.includes(value);
363
+ },
364
+ });