@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,232 @@
1
+ <script setup>
2
+ import { computed, unref, useId } from 'vue';
3
+ import DomField from '../field/DomField.vue';
4
+ import { normalizeErrors } from '../field/useField.js';
5
+
6
+ defineOptions({
7
+ __doc: {
8
+ name: 'Input group',
9
+ tag: '<DomInputGroup>',
10
+ description: 'A display-only frame that presents several independent form controls as one grouped input, followed by optional feedback and their errors.',
11
+ icon: 'M5 5h14v14H5V5Zm0 7h14',
12
+ slots: [
13
+ { name: '(default)', description: 'Form controls using chrome="false". Each control keeps its own model, name, validation, and form registration.' },
14
+ { name: 'feedback', description: 'Supporting content shown outside the shared frame and before validation errors.' },
15
+ ],
16
+ },
17
+ });
18
+
19
+ const props = defineProps({
20
+ ariaLabel: {
21
+ type: String,
22
+ default: 'Grouped form fields',
23
+ _edit: { description: 'Accessible name for the shared control frame.' },
24
+ },
25
+ description: {
26
+ type: String,
27
+ default: '',
28
+ _edit: { description: 'Optional helper text shown below the grouped errors.' },
29
+ },
30
+ errors: {
31
+ type: [Array, Object, String],
32
+ default: () => [],
33
+ _edit: { component: 'DomJsonInput', description: 'Optional group-level validation errors shown after child field errors.' },
34
+ },
35
+ errorLabels: {
36
+ type: Boolean,
37
+ default: true,
38
+ _edit: { description: 'Prefix each child error with its field label so repeated messages remain distinguishable.' },
39
+ },
40
+ invalid: {
41
+ type: Boolean,
42
+ default: false,
43
+ _edit: { description: 'Mark the shared frame invalid in addition to any child field state.' },
44
+ },
45
+ visible: {
46
+ type: Boolean,
47
+ default: true,
48
+ _edit: { description: 'Show or hide the complete input group.' },
49
+ },
50
+ });
51
+
52
+ const inputGroupId = `dom-input-group-${useId()}`;
53
+ const descriptionId = `${inputGroupId}-description`;
54
+ const groupErrorId = `${inputGroupId}-error`;
55
+
56
+ /**
57
+ * Normalizes group-level errors without merging identical child messages.
58
+ */
59
+ const groupErrors = computed(() => normalizeErrors(props.errors));
60
+
61
+ /**
62
+ * Resolves a reactive field registration value or its matching field attribute.
63
+ *
64
+ * @param {Record<string, unknown>} field Registered child field.
65
+ * @param {string} key Registration property to resolve.
66
+ * @returns {unknown} Resolved property value.
67
+ */
68
+ function fieldValue(field, key) {
69
+ const directValue = unref(field?.[key]);
70
+ if (directValue !== undefined) return directValue;
71
+ return unref(unref(field?.fieldAttrs)?.[key]);
72
+ }
73
+
74
+ /**
75
+ * Returns the stable key for a registered child field.
76
+ *
77
+ * @param {Record<string, unknown>} field Registered child field.
78
+ * @param {number} index Child position used as a final fallback.
79
+ * @returns {string} Stable rendering key.
80
+ */
81
+ function fieldKey(field, index) {
82
+ return String(fieldValue(field, 'path') || fieldValue(field, 'htmlId') || fieldValue(field, 'id') || index);
83
+ }
84
+
85
+ /**
86
+ * Returns the visible and accessible label for a child field.
87
+ *
88
+ * @param {Record<string, unknown>} field Registered child field.
89
+ * @returns {string} Child field label.
90
+ */
91
+ function fieldLabel(field) {
92
+ return String(fieldValue(field, 'label') || fieldValue(field, 'name') || 'Field');
93
+ }
94
+
95
+ /**
96
+ * Returns the native input ID registered by a child field.
97
+ *
98
+ * @param {Record<string, unknown>} field Registered child field.
99
+ * @returns {string} Native input ID.
100
+ */
101
+ function fieldHtmlId(field) {
102
+ return String(fieldValue(field, 'htmlId') || fieldValue(field, 'id') || '');
103
+ }
104
+
105
+ /**
106
+ * Returns the error element ID expected by a child input's ARIA attributes.
107
+ *
108
+ * @param {Record<string, unknown>} field Registered child field.
109
+ * @returns {string} Child error element ID.
110
+ */
111
+ function fieldErrorId(field) {
112
+ return String(fieldValue(field, 'errorId') || '');
113
+ }
114
+
115
+ /**
116
+ * Returns every validation message registered for a child field.
117
+ *
118
+ * @param {Record<string, unknown>} field Registered child field.
119
+ * @returns {string[]} Normalized validation messages.
120
+ */
121
+ function fieldErrors(field) {
122
+ return normalizeErrors(fieldValue(field, 'errors'));
123
+ }
124
+
125
+ /**
126
+ * Reports whether any registered child field currently has validation errors.
127
+ *
128
+ * @param {Array<Record<string, unknown>>} fields Registered child fields.
129
+ * @returns {boolean} Whether child validation errors exist.
130
+ */
131
+ function hasFieldErrors(fields) {
132
+ return fields.some((field) => fieldErrors(field).length > 0);
133
+ }
134
+
135
+ /**
136
+ * Adds field context to an error when several controls share one error region.
137
+ *
138
+ * @param {Record<string, unknown>} field Registered child field.
139
+ * @param {string} error Validation message.
140
+ * @returns {string} Error copy for display.
141
+ */
142
+ function displayError(field, error) {
143
+ if (!props.errorLabels) return error;
144
+ return `${fieldLabel(field)}: ${error}`;
145
+ }
146
+ </script>
147
+
148
+ <template>
149
+ <DomField :chrome="false" :errors="errors" :invalid="invalid" :visible="visible">
150
+ <template #default="{ invalid: groupInvalid, fields }">
151
+ <div class="w-full space-y-1.5">
152
+ <div
153
+ :aria-label="ariaLabel"
154
+ :aria-describedby="description ? descriptionId : undefined"
155
+ :aria-errormessage="groupErrors.length ? groupErrorId : undefined"
156
+ :aria-invalid="groupInvalid || undefined"
157
+ role="group"
158
+ class="skin-input dom-input-group__controls overflow-hidden p-0 focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30 data-[invalid]:border-destructive data-[invalid]:focus-within:border-destructive data-[invalid]:focus-within:ring-destructive/25"
159
+ :data-invalid="groupInvalid ? '' : undefined"
160
+ >
161
+ <div class="sr-only">
162
+ <label
163
+ v-for="(field, fieldIndex) in fields"
164
+ :key="`label-${fieldKey(field, fieldIndex)}`"
165
+ :for="fieldHtmlId(field) || undefined"
166
+ >
167
+ {{ fieldLabel(field) }}
168
+ </label>
169
+ </div>
170
+ <slot />
171
+ </div>
172
+
173
+ <div v-if="$slots.feedback" class="dom-input-group__feedback pt-1">
174
+ <slot name="feedback" />
175
+ </div>
176
+
177
+ <div
178
+ v-if="hasFieldErrors(fields) || groupErrors.length"
179
+ class="space-y-1 text-[11px] leading-snug text-destructive"
180
+ aria-live="polite"
181
+ >
182
+ <template v-for="(field, fieldIndex) in fields" :key="`errors-${fieldKey(field, fieldIndex)}`">
183
+ <p
184
+ v-for="(error, errorIndex) in fieldErrors(field)"
185
+ :key="`${fieldKey(field, fieldIndex)}-${error}-${errorIndex}`"
186
+ :id="errorIndex === 0 ? fieldErrorId(field) || undefined : undefined"
187
+ >
188
+ {{ displayError(field, error) }}
189
+ </p>
190
+ </template>
191
+ <p
192
+ v-for="(error, errorIndex) in groupErrors"
193
+ :key="`group-${error}-${errorIndex}`"
194
+ :id="errorIndex === 0 ? groupErrorId : undefined"
195
+ >
196
+ {{ error }}
197
+ </p>
198
+ </div>
199
+
200
+ <p v-if="description" :id="descriptionId" class="text-[11px] leading-snug text-muted-fg">
201
+ {{ description }}
202
+ </p>
203
+ </div>
204
+ </template>
205
+ </DomField>
206
+ </template>
207
+
208
+ <style scoped>
209
+ .dom-input-group__controls :deep(.skin-input) {
210
+ background: transparent;
211
+ border: 0;
212
+ border-radius: 0;
213
+ box-shadow: none;
214
+ }
215
+
216
+ .dom-input-group__controls :deep(.skin-input:focus) {
217
+ border-color: transparent;
218
+ box-shadow: none;
219
+ }
220
+
221
+ .dom-input-group__controls :deep(button:active) {
222
+ scale: 1;
223
+ }
224
+
225
+ .dom-input-group__controls :deep(.dom-password-input__warning) {
226
+ margin: 0.75rem;
227
+ }
228
+
229
+ :global(.dom-input-group__controls > *:not(.sr-only) + *:not(.sr-only)) {
230
+ border-top: 1px solid var(--border);
231
+ }
232
+ </style>
@@ -0,0 +1,227 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from 'vue';
3
+ import DomCodeInput from '../code-input/DomCodeInput.vue';
4
+ import DomField from '../field/DomField.vue';
5
+ import DomForm from '../form/DomForm.vue';
6
+ import { fieldProps } from '../field/fieldProps.js';
7
+ import { inferFormNodeFromValue, normalizeFormNode } from '../form/formDefinition.js';
8
+ import { useField } from '../field/useField.js';
9
+
10
+ defineOptions({
11
+ __doc: {
12
+ name: 'JSON input',
13
+ tag: '<DomJsonInput>',
14
+ description: 'A JSON form input backed by DomCodeInput. Valid JSON emits parsed data; invalid JSON stays local and shows an error.',
15
+ events: [
16
+ { name: 'update:modelValue', payload: 'object | array | string | number | boolean | null', description: 'Fired with parsed JSON when the editor contains valid JSON.' },
17
+ { name: 'valid', payload: 'parsed value', description: 'Fired when JSON parses successfully.' },
18
+ { name: 'invalid', payload: 'Error', description: 'Fired when JSON cannot be parsed.' },
19
+ ],
20
+ },
21
+ });
22
+
23
+ const props = defineProps({
24
+ ...fieldProps,
25
+ modelValue: {
26
+ default: null,
27
+ _edit: { component: 'DomJsonInput', description: 'Any JSON-serializable value.' },
28
+ },
29
+ rows: {
30
+ type: Number,
31
+ default: 10,
32
+ _edit: { description: 'Textarea fallback rows.' },
33
+ },
34
+ editor: {
35
+ type: Boolean,
36
+ default: true,
37
+ _edit: { description: 'Attempt to load the enhanced editor from the CDN.' },
38
+ },
39
+ schema: {
40
+ type: [Object, Array],
41
+ default: null,
42
+ _edit: {
43
+ component: 'DomJsonInput',
44
+ description: 'Optional standardized form definition used to render a visual JSON editor.',
45
+ },
46
+ },
47
+ jsonToggle: {
48
+ type: Boolean,
49
+ default: true,
50
+ _edit: { description: 'Show the visual/raw JSON mode switch when schema is provided.' },
51
+ },
52
+ compact: {
53
+ type: Boolean,
54
+ default: false,
55
+ _edit: { description: 'Reduce vertical spacing for schema-driven editors.' },
56
+ },
57
+ _registerField: {
58
+ type: Boolean,
59
+ default: true,
60
+ },
61
+ });
62
+
63
+ const emit = defineEmits(['update:modelValue', 'valid', 'invalid', 'focus', 'blur']);
64
+ const field = useField(props, emit, { idPrefix: 'dom-json-input', register: props._registerField });
65
+ const text = ref(format(field.value.value));
66
+ const error = ref('');
67
+ const focused = ref(false);
68
+ const mode = ref('fields');
69
+ const schemaNode = computed(() => (
70
+ props.schema
71
+ ? normalizeJsonSchema(props.schema)
72
+ : normalizeFormNode(inferFormNodeFromValue(field.value.value))
73
+ ));
74
+ const hasSchema = computed(() => !!schemaNode.value);
75
+ const schemaFormType = computed(() => (schemaNode.value?.type === 'array' ? 'array' : 'object'));
76
+ const schemaChildren = computed(() => schemaNode.value?.children || []);
77
+ const schemaItems = computed(() => schemaNode.value?.items || null);
78
+ const schemaValue = computed(() => {
79
+ if (schemaFormType.value === 'array') return Array.isArray(field.value.value) ? field.value.value : [];
80
+ return field.value.value && typeof field.value.value === 'object' && !Array.isArray(field.value.value)
81
+ ? field.value.value
82
+ : {};
83
+ });
84
+ const codeFieldAttrs = computed(() => ({
85
+ ...field.fieldAttrs.value,
86
+ invalid: field.invalid.value || !!error.value,
87
+ errors: error.value ? [error.value] : field.errors.value,
88
+ }));
89
+
90
+ function format(value) {
91
+ return JSON.stringify(value ?? null, null, 2);
92
+ }
93
+
94
+ function normalizeJsonSchema(schema) {
95
+ return Array.isArray(schema)
96
+ ? normalizeFormNode({ type: 'DomForm', children: schema })
97
+ : normalizeFormNode(schema);
98
+ }
99
+
100
+ function onInput(value) {
101
+ text.value = value;
102
+ try {
103
+ const parsed = JSON.parse(value);
104
+ error.value = '';
105
+ field.setFieldState({ errors: [], invalid: false });
106
+ field.onInput(parsed);
107
+ emit('valid', parsed);
108
+ } catch (err) {
109
+ error.value = err.message;
110
+ field.setFieldState({ errors: [err.message], invalid: true });
111
+ emit('invalid', err);
112
+ }
113
+ }
114
+
115
+ function onFormInput(value) {
116
+ error.value = '';
117
+ field.setFieldState({ errors: [], invalid: false });
118
+ field.onInput(value);
119
+ emit('valid', value);
120
+ }
121
+
122
+ function onFocus(event) {
123
+ focused.value = true;
124
+ field.onFocus(event);
125
+ }
126
+
127
+ function onBlur(event) {
128
+ focused.value = false;
129
+ field.onBlur(event);
130
+ }
131
+
132
+ watch(field.value, (value) => {
133
+ if (focused.value) return;
134
+ const next = format(value);
135
+ if (next !== text.value) text.value = next;
136
+ });
137
+ </script>
138
+
139
+ <template>
140
+ <DomField v-if="hasSchema" v-bind="field.fieldAttrs.value" :chrome="chrome">
141
+ <input
142
+ v-if="field.htmlName.value && mode === 'fields'"
143
+ type="hidden"
144
+ :name="field.htmlName.value"
145
+ :value="format(field.value.value)"
146
+ />
147
+ <div :class="compact ? 'space-y-1.5' : 'space-y-2'">
148
+ <div v-if="jsonToggle" class="flex justify-end">
149
+ <div class="inline-flex rounded-lg border border-border bg-secondary/60 p-0.5">
150
+ <button
151
+ type="button"
152
+ class="rounded-md px-2.5 py-1 text-xs font-medium transition"
153
+ :class="mode === 'fields' ? 'bg-canvas text-canvas-fg shadow-sm' : 'text-muted-fg hover:text-canvas-fg'"
154
+ :aria-pressed="mode === 'fields'"
155
+ @click="mode = 'fields'"
156
+ >
157
+ Fields
158
+ </button>
159
+ <button
160
+ type="button"
161
+ class="rounded-md px-2.5 py-1 text-xs font-medium transition"
162
+ :class="mode === 'json' ? 'bg-canvas text-canvas-fg shadow-sm' : 'text-muted-fg hover:text-canvas-fg'"
163
+ :aria-pressed="mode === 'json'"
164
+ @click="mode = 'json'"
165
+ >
166
+ JSON
167
+ </button>
168
+ </div>
169
+ </div>
170
+ <DomForm
171
+ v-if="mode === 'fields'"
172
+ :model-value="schemaValue"
173
+ :type="schemaFormType"
174
+ :children="schemaChildren"
175
+ :items="schemaItems"
176
+ :compact="compact"
177
+ tag="div"
178
+ isolated
179
+ @update:model-value="onFormInput"
180
+ @focus="field.onFocus"
181
+ @blur="field.onBlur"
182
+ />
183
+ <div v-else>
184
+ <DomCodeInput
185
+ :model-value="text"
186
+ :id="field.id.value"
187
+ :name="field.htmlName.value"
188
+ :rows="rows"
189
+ :editor="editor"
190
+ :invalid="codeFieldAttrs.invalid"
191
+ :errors="codeFieldAttrs.errors"
192
+ :required="required"
193
+ :disabled="field.disabled.value"
194
+ :read-only="field.readOnly.value"
195
+ chrome="none"
196
+ :_register-field="false"
197
+ lang="json"
198
+ @update:model-value="onInput"
199
+ @focus="onFocus"
200
+ @blur="onBlur"
201
+ />
202
+ </div>
203
+ </div>
204
+ </DomField>
205
+ <div v-else>
206
+ <DomCodeInput
207
+ :model-value="text"
208
+ :id="field.id.value"
209
+ :name="field.htmlName.value"
210
+ :label="codeFieldAttrs.label"
211
+ :description="codeFieldAttrs.description"
212
+ :rows="rows"
213
+ :editor="editor"
214
+ :invalid="codeFieldAttrs.invalid"
215
+ :errors="codeFieldAttrs.errors"
216
+ :required="required"
217
+ :disabled="field.disabled.value"
218
+ :read-only="field.readOnly.value"
219
+ :chrome="chrome"
220
+ :_register-field="false"
221
+ lang="json"
222
+ @update:model-value="onInput"
223
+ @focus="onFocus"
224
+ @blur="onBlur"
225
+ />
226
+ </div>
227
+ </template>
@@ -0,0 +1,206 @@
1
+ <script setup>
2
+ import { computed, ref } from 'vue';
3
+ import DomForm from '../form/DomForm.vue';
4
+ import DomField from '../field/DomField.vue';
5
+ import DomJsonInput from '../json-input/DomJsonInput.vue';
6
+ import { fieldProps } from '../field/fieldProps.js';
7
+ import { inferFormNodeFromValue, normalizeFormNode } from '../form/formDefinition.js';
8
+ import { useField } from '../field/useField.js';
9
+
10
+ defineOptions({
11
+ __doc: {
12
+ name: 'JSON list input',
13
+ tag: '<DomJsonListInput>',
14
+ description: 'A schema-aware editor for arrays of objects. Edit rows through form controls, or toggle to raw JSON for bulk changes.',
15
+ events: [
16
+ { name: 'update:modelValue', payload: 'Array<object>', description: 'Fired when rows are added, removed, reordered, edited, or replaced from valid raw JSON.' },
17
+ ],
18
+ },
19
+ });
20
+
21
+ const props = defineProps({
22
+ ...fieldProps,
23
+ modelValue: {
24
+ type: Array,
25
+ default: () => [],
26
+ _edit: { component: 'DomJsonListInput', props: { compact: true } },
27
+ },
28
+ addLabel: {
29
+ type: String,
30
+ default: '+ Add row',
31
+ _edit: { description: 'Button label for adding a row.' },
32
+ },
33
+ schema: {
34
+ type: [Array, Object],
35
+ default: null,
36
+ _edit: {
37
+ component: 'DomJsonInput',
38
+ props: {
39
+ label: 'Schema',
40
+ description: 'Standardized form definition used by the visual editor. Legacy flat row schema arrays are still supported.',
41
+ rows: 8,
42
+ },
43
+ },
44
+ },
45
+ jsonToggle: {
46
+ type: Boolean,
47
+ default: true,
48
+ _edit: { description: 'Show the visual/raw JSON mode switch.' },
49
+ },
50
+ compact: {
51
+ type: Boolean,
52
+ default: false,
53
+ _edit: { description: 'Reduce vertical spacing for inspectors and narrow tool panels.' },
54
+ },
55
+ });
56
+ const emit = defineEmits(['update:modelValue', 'focus', 'blur']);
57
+ const field = useField(props, emit, { idPrefix: 'dom-json-list-input' });
58
+ const mode = ref('fields');
59
+ const rawError = ref('');
60
+
61
+ const rows = computed(() => (Array.isArray(field.value.value) ? field.value.value : []));
62
+ const normalizedSchema = computed(() => (
63
+ props.schema && !Array.isArray(props.schema) ? normalizeFormNode(props.schema) : null
64
+ ));
65
+ const formItemSchema = computed(() => {
66
+ if (!normalizedSchema.value) return null;
67
+ if (normalizedSchema.value.type === 'array') return normalizedSchema.value.items || { type: 'json' };
68
+ return normalizedSchema.value;
69
+ });
70
+ const fields = computed(() => {
71
+ if (Array.isArray(props.schema) && props.schema.length) return props.schema;
72
+ const keys = [...new Set(rows.value.flatMap((row) => (isObject(row) ? Object.keys(row) : [])))];
73
+ return keys.map((key) => ({
74
+ key,
75
+ label: key.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase()),
76
+ type: key.includes('url') || key.includes('avatar') || key.includes('image') ? 'url' : 'text',
77
+ }));
78
+ });
79
+ const inferredArraySchema = computed(() => inferFormNodeFromValue(rows.value));
80
+ const visualItemSchema = computed(() => (
81
+ formItemSchema.value
82
+ || (Array.isArray(props.schema) && props.schema.length ? legacyFieldsToFormNode(fields.value) : null)
83
+ || inferredArraySchema.value?.items
84
+ || legacyFieldsToFormNode(fields.value)
85
+ ));
86
+
87
+ function isObject(value) {
88
+ return value && typeof value === 'object' && !Array.isArray(value);
89
+ }
90
+
91
+ function update(next) {
92
+ rawError.value = '';
93
+ field.onInput(next);
94
+ }
95
+
96
+ function setRawJson(value) {
97
+ if (!Array.isArray(value)) {
98
+ rawError.value = 'JSON list input expects an array of objects.';
99
+ return;
100
+ }
101
+
102
+ rawError.value = '';
103
+ update(value);
104
+ }
105
+
106
+ function legacyFieldsToFormNode(nextFields) {
107
+ return {
108
+ type: 'DomForm',
109
+ properties: Object.fromEntries((nextFields || [])
110
+ .filter((field) => field?.key)
111
+ .map((field) => [field.key, legacyFieldToFormNode(field)])),
112
+ };
113
+ }
114
+
115
+ function legacyFieldToFormNode(field) {
116
+ const {
117
+ key,
118
+ type = 'string',
119
+ label,
120
+ description,
121
+ placeholder,
122
+ required,
123
+ default: defaultValue,
124
+ options,
125
+ component,
126
+ valueType,
127
+ } = field;
128
+ return {
129
+ type,
130
+ ...(component ? { component } : {}),
131
+ ...(label ? { label } : {}),
132
+ ...(description ? { description } : {}),
133
+ ...(placeholder ? { placeholder } : {}),
134
+ ...(required ? { required } : {}),
135
+ ...(defaultValue !== undefined ? { default: defaultValue } : {}),
136
+ ...(options ? { options } : {}),
137
+ ...(valueType ? { valueType } : {}),
138
+ };
139
+ }
140
+ </script>
141
+
142
+ <template>
143
+ <DomField v-bind="field.fieldAttrs.value" :chrome="chrome">
144
+ <input
145
+ v-if="field.htmlName.value"
146
+ type="hidden"
147
+ :name="field.htmlName.value"
148
+ :value="JSON.stringify(rows)"
149
+ />
150
+ <div :class="compact ? 'space-y-1.5' : 'space-y-2'">
151
+ <div v-if="jsonToggle" class="flex justify-end">
152
+ <div class="inline-flex rounded-lg border border-border bg-secondary/60 p-0.5">
153
+ <button
154
+ type="button"
155
+ class="rounded-md px-2.5 py-1 text-xs font-medium transition"
156
+ :class="mode === 'fields' ? 'bg-canvas text-canvas-fg shadow-sm' : 'text-muted-fg hover:text-canvas-fg'"
157
+ :aria-pressed="mode === 'fields'"
158
+ @click="mode = 'fields'"
159
+ >
160
+ Fields
161
+ </button>
162
+ <button
163
+ type="button"
164
+ class="rounded-md px-2.5 py-1 text-xs font-medium transition"
165
+ :class="mode === 'json' ? 'bg-canvas text-canvas-fg shadow-sm' : 'text-muted-fg hover:text-canvas-fg'"
166
+ :aria-pressed="mode === 'json'"
167
+ @click="mode = 'json'"
168
+ >
169
+ JSON
170
+ </button>
171
+ </div>
172
+ </div>
173
+
174
+ <div v-if="mode === 'json'">
175
+ <DomJsonInput
176
+ :model-value="rows"
177
+ :rows="compact ? 7 : 12"
178
+ :editor="true"
179
+ :chrome="'none'"
180
+ :_register-field="false"
181
+ @update:model-value="setRawJson"
182
+ @focus="field.onFocus"
183
+ @blur="field.onBlur"
184
+ />
185
+ <p v-if="rawError" class="mt-2 rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs font-mono text-destructive">
186
+ {{ rawError }}
187
+ </p>
188
+ </div>
189
+
190
+ <template v-else>
191
+ <DomForm
192
+ :model-value="rows"
193
+ type="array"
194
+ :items="visualItemSchema"
195
+ :add-label="addLabel"
196
+ :compact="compact"
197
+ tag="div"
198
+ isolated
199
+ @update:model-value="update"
200
+ @focus="field.onFocus"
201
+ @blur="field.onBlur"
202
+ />
203
+ </template>
204
+ </div>
205
+ </DomField>
206
+ </template>