@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,340 @@
1
+ <script setup>
2
+ import { computed, ref } from 'vue';
3
+ import DomField from '../field/DomField.vue';
4
+ import DomTooltip from '../../components/tooltip/DomTooltip.vue';
5
+ import { fieldProps } from '../field/fieldProps.js';
6
+ import { useField } from '../field/useField.js';
7
+
8
+ defineOptions({
9
+ __doc: {
10
+ name: 'Toggle button group',
11
+ tag: '<DomToggleButtonGroup>',
12
+ description: 'A single or multiple selection button group for compact form choices.',
13
+ slots: [{ name: 'option', payload: '{ option, index, selected }', description: 'Custom option markup.' }],
14
+ events: [{ name: 'update:modelValue', payload: '(value: string | string[])', description: 'Emitted when selection changes.' }],
15
+ },
16
+ });
17
+
18
+ const props = defineProps({
19
+ ...fieldProps,
20
+ modelValue: {
21
+ type: [String, Number, Array],
22
+ default: '',
23
+ _edit: { description: 'Selected value for single mode, or selected values for multiple mode.' },
24
+ },
25
+ options: {
26
+ type: Array,
27
+ required: true,
28
+ _edit: {
29
+ component: 'DomJsonListInput',
30
+ description: 'Toggle button options.',
31
+ props: {
32
+ compact: true,
33
+ addLabel: '+ Add option',
34
+ schema: [
35
+ { key: 'label', label: 'Label', placeholder: 'Option label', default: (index) => `Option ${index + 1}` },
36
+ { key: 'value', label: 'Value', placeholder: 'option-value', default: (index) => `option-${index + 1}` },
37
+ { key: 'tooltip', label: 'Tooltip', placeholder: 'Choose this option' },
38
+ ],
39
+ },
40
+ },
41
+ },
42
+ type: {
43
+ type: String,
44
+ default: 'single',
45
+ _edit: { options: ['single', 'multiple'], description: 'Allow one selection or many.' },
46
+ },
47
+ orientation: {
48
+ type: String,
49
+ default: 'horizontal',
50
+ _edit: { options: ['horizontal', 'vertical'], description: 'Visual direction and arrow key behaviour.' },
51
+ },
52
+ size: {
53
+ type: String,
54
+ default: 'md',
55
+ _edit: { options: ['sm', 'md', 'lg', 'icon-sm', 'icon-md', 'icon-lg'], description: 'Button size.' },
56
+ },
57
+ variant: {
58
+ type: String,
59
+ default: 'default',
60
+ validator: (value) => ['default', 'switch'].includes(value),
61
+ _edit: { options: ['default', 'switch'], description: 'Visual treatment. Use switch for raised icon-only segmented controls.' },
62
+ },
63
+ });
64
+ const emit = defineEmits(['update:modelValue', 'focus', 'blur']);
65
+ const field = useField(props, emit, { idPrefix: 'dom-toggle-button-group' });
66
+ const buttons = ref([]);
67
+
68
+ const isMultiple = computed(() => props.type === 'multiple');
69
+ const selectedValues = computed(getSelectedValues);
70
+ const sizeClass = computed(getSizeClass);
71
+ const groupClass = computed(getGroupClass);
72
+
73
+ /**
74
+ * Resolves the selected values as stable strings for aria-pressed checks.
75
+ *
76
+ * @returns {Array<string>} Selected option values.
77
+ */
78
+ function getSelectedValues() {
79
+ if (isMultiple.value) return Array.isArray(field.value.value) ? field.value.value.map(String) : [];
80
+ return field.value.value == null || field.value.value === '' ? [] : [String(field.value.value)];
81
+ }
82
+
83
+ /**
84
+ * Resolves the size utility classes for each button.
85
+ *
86
+ * @returns {string} Button size class list.
87
+ */
88
+ function getSizeClass() {
89
+ return {
90
+ sm: 'min-h-8 px-3 py-1.5 text-xs',
91
+ md: 'min-h-10 px-4 py-2 text-sm',
92
+ lg: 'min-h-11 px-5 py-2.5 text-base',
93
+ 'icon-sm': 'size-8 p-0 text-xs',
94
+ 'icon-md': 'size-10 p-0 text-sm',
95
+ 'icon-lg': 'size-12 p-0 text-sm',
96
+ }[props.size] || 'min-h-10 px-4 py-2 text-sm';
97
+ }
98
+
99
+ /**
100
+ * Builds the root group classes for the selected visual variant.
101
+ *
102
+ * @returns {Array<string>} Vue class list.
103
+ */
104
+ function getGroupClass() {
105
+ const directionClass = props.orientation === 'vertical' ? 'flex-col' : 'flex-row';
106
+ if (props.variant === 'switch') {
107
+ return [
108
+ 'inline-flex w-fit items-center overflow-hidden rounded-xl bg-secondary/80 p-0.5 shadow-inner ring-1 ring-border/30 data-[invalid]:ring-destructive/30',
109
+ directionClass,
110
+ 'gap-0.5',
111
+ ];
112
+ }
113
+
114
+ return [
115
+ 'skin-control inline-flex w-fit overflow-hidden rounded-xl p-0 data-[invalid]:border-destructive',
116
+ directionClass,
117
+ ];
118
+ }
119
+
120
+ /**
121
+ * Resolves the visible label for an option.
122
+ *
123
+ * @param {unknown} option Toggle option.
124
+ * @returns {unknown} Label content.
125
+ */
126
+ function labelOf(option) {
127
+ return option && typeof option === 'object' ? (option.label ?? option.value) : option;
128
+ }
129
+
130
+ /**
131
+ * Resolves the emitted value for an option.
132
+ *
133
+ * @param {unknown} option Toggle option.
134
+ * @returns {string} Stable option value.
135
+ */
136
+ function valueOf(option) {
137
+ return String(option && typeof option === 'object' ? (option.value ?? option.label) : option);
138
+ }
139
+
140
+ /**
141
+ * Checks whether an option is selected.
142
+ *
143
+ * @param {unknown} option Toggle option.
144
+ * @returns {boolean} True when the option is pressed.
145
+ */
146
+ function selected(option) {
147
+ return selectedValues.value.includes(valueOf(option));
148
+ }
149
+
150
+ /**
151
+ * Resolves optional tooltip text for an option.
152
+ *
153
+ * @param {unknown} option Toggle option.
154
+ * @returns {string} Tooltip text.
155
+ */
156
+ function tooltipOf(option) {
157
+ return option && typeof option === 'object' ? option.tooltip : '';
158
+ }
159
+
160
+ /**
161
+ * Resolves an accessible label for icon-only options.
162
+ *
163
+ * @param {unknown} option Toggle option.
164
+ * @returns {string|null} Accessible label or null.
165
+ */
166
+ function ariaLabelOf(option) {
167
+ return option && typeof option === 'object' ? option.ariaLabel || option.tooltip || null : null;
168
+ }
169
+
170
+ /**
171
+ * Builds joining classes for adjacent buttons.
172
+ *
173
+ * @param {number} index Option index.
174
+ * @returns {Array<string>} Classes for the option position.
175
+ */
176
+ function joinClass(index) {
177
+ if (props.variant === 'switch') return ['rounded-lg'];
178
+
179
+ const last = index === props.options.length - 1;
180
+ if (props.orientation === 'vertical') {
181
+ return [
182
+ index > 0 ? 'border-t border-border/80' : '',
183
+ index === 0 ? 'rounded-t-xl' : '',
184
+ last ? 'rounded-b-xl' : '',
185
+ ];
186
+ }
187
+ return [
188
+ index > 0 ? 'border-l border-border/80' : '',
189
+ index === 0 ? 'rounded-l-xl' : '',
190
+ last ? 'rounded-r-xl' : '',
191
+ ];
192
+ }
193
+
194
+ /**
195
+ * Builds the button classes for an option.
196
+ *
197
+ * @param {number} index Option index.
198
+ * @returns {Array<string>} Vue class list.
199
+ */
200
+ function buttonClass(index) {
201
+ if (props.variant === 'switch') {
202
+ return [
203
+ 'relative inline-flex items-center justify-center gap-2 font-medium text-muted-fg outline-none transition hover:bg-canvas/35 hover:text-canvas-fg focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring/50 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 aria-pressed:z-10 aria-pressed:bg-canvas aria-pressed:text-canvas-fg aria-pressed:shadow-md aria-pressed:ring-1 aria-pressed:ring-border/70',
204
+ sizeClass.value,
205
+ joinClass(index),
206
+ ];
207
+ }
208
+
209
+ return [
210
+ 'relative inline-flex items-center justify-center gap-2 font-medium text-muted-fg outline-none transition hover:bg-secondary hover:text-canvas-fg focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring/50 active:translate-y-px disabled:cursor-not-allowed disabled:opacity-50 aria-pressed:z-10 aria-pressed:bg-canvas aria-pressed:text-canvas-fg aria-pressed:shadow-sm aria-pressed:ring-1 aria-pressed:ring-ring/30',
211
+ sizeClass.value,
212
+ joinClass(index),
213
+ ];
214
+ }
215
+
216
+ /**
217
+ * Stores a button reference for keyboard roving focus.
218
+ *
219
+ * @param {HTMLButtonElement|null} element Button element.
220
+ * @param {number} index Option index.
221
+ */
222
+ function setButtonRef(element, index) {
223
+ if (element) buttons.value[index] = element;
224
+ }
225
+
226
+ /**
227
+ * Selects or toggles an option depending on group type.
228
+ *
229
+ * @param {unknown} option Toggle option.
230
+ */
231
+ function selectOption(option) {
232
+ if (field.disabled.value || field.readOnly.value || option?.disabled) return;
233
+ const value = valueOf(option);
234
+ if (isMultiple.value) {
235
+ const next = selected(option)
236
+ ? selectedValues.value.filter((item) => item !== value)
237
+ : [...selectedValues.value, value];
238
+ field.onInput(next);
239
+ return;
240
+ }
241
+ field.onInput(value);
242
+ }
243
+
244
+ /**
245
+ * Moves focus between enabled buttons.
246
+ *
247
+ * @param {number} index Current option index.
248
+ * @param {number} direction Relative movement direction.
249
+ */
250
+ function moveFocus(index, direction) {
251
+ const enabled = buttons.value.filter((button) => button && !button.disabled);
252
+ if (!enabled.length) return;
253
+ const current = buttons.value[index];
254
+ const currentIndex = Math.max(0, enabled.indexOf(current));
255
+ enabled[(currentIndex + direction + enabled.length) % enabled.length]?.focus();
256
+ }
257
+
258
+ /**
259
+ * Handles keyboard selection and roving focus for the group.
260
+ *
261
+ * @param {KeyboardEvent} event Native keyboard event.
262
+ * @param {number} index Current option index.
263
+ * @param {unknown} option Toggle option.
264
+ */
265
+ function onKeydown(event, index, option) {
266
+ const nextKey = props.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
267
+ const prevKey = props.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
268
+ if (event.key === nextKey) {
269
+ event.preventDefault();
270
+ moveFocus(index, 1);
271
+ } else if (event.key === prevKey) {
272
+ event.preventDefault();
273
+ moveFocus(index, -1);
274
+ } else if (event.key === 'Home') {
275
+ event.preventDefault();
276
+ buttons.value.find((button) => button && !button.disabled)?.focus();
277
+ } else if (event.key === 'End') {
278
+ event.preventDefault();
279
+ [...buttons.value].reverse().find((button) => button && !button.disabled)?.focus();
280
+ } else if (event.key === ' ' || event.key === 'Enter') {
281
+ event.preventDefault();
282
+ selectOption(option);
283
+ }
284
+ }
285
+ </script>
286
+
287
+ <template>
288
+ <DomField v-bind="field.fieldAttrs.value" :chrome="chrome">
289
+ <input
290
+ v-if="field.htmlName.value"
291
+ type="hidden"
292
+ :name="field.htmlName.value"
293
+ :value="isMultiple ? JSON.stringify(selectedValues) : (selectedValues[0] || '')"
294
+ />
295
+ <div
296
+ :id="field.id.value"
297
+ role="group"
298
+ :aria-label="label || undefined"
299
+ :aria-describedby="field.describedBy.value || undefined"
300
+ :aria-invalid="field.invalid.value || undefined"
301
+ :aria-errormessage="field.errorId.value || undefined"
302
+ :data-invalid="field.invalid.value ? '' : undefined"
303
+ :class="groupClass"
304
+ >
305
+ <template v-for="(option, index) in options" :key="valueOf(option) || index">
306
+ <DomTooltip v-if="tooltipOf(option)" :text="tooltipOf(option)" placement="top">
307
+ <button
308
+ :ref="(element) => setButtonRef(element, index)"
309
+ type="button"
310
+ :aria-label="ariaLabelOf(option) || undefined"
311
+ :aria-pressed="String(selected(option))"
312
+ :disabled="field.disabled.value || option?.disabled || undefined"
313
+ :class="buttonClass(index)"
314
+ @click="selectOption(option)"
315
+ @keydown="onKeydown($event, index, option)"
316
+ @focus="field.onFocus"
317
+ @blur="field.onBlur"
318
+ >
319
+ <slot name="option" :option="option" :index="index" :selected="selected(option)">{{ labelOf(option) }}</slot>
320
+ </button>
321
+ </DomTooltip>
322
+ <button
323
+ v-else
324
+ :ref="(element) => setButtonRef(element, index)"
325
+ type="button"
326
+ :aria-label="ariaLabelOf(option) || undefined"
327
+ :aria-pressed="String(selected(option))"
328
+ :disabled="field.disabled.value || option?.disabled || undefined"
329
+ :class="buttonClass(index)"
330
+ @click="selectOption(option)"
331
+ @keydown="onKeydown($event, index, option)"
332
+ @focus="field.onFocus"
333
+ @blur="field.onBlur"
334
+ >
335
+ <slot name="option" :option="option" :index="index" :selected="selected(option)">{{ labelOf(option) }}</slot>
336
+ </button>
337
+ </template>
338
+ </div>
339
+ </DomField>
340
+ </template>
@@ -0,0 +1,55 @@
1
+ <script setup>
2
+ import { computed } from 'vue';
3
+ import DomTextInput from '../text-input/DomTextInput.vue';
4
+ import { fieldProps } from '../field/fieldProps.js';
5
+ import { urlValidator } from '../field/validators.js';
6
+
7
+ defineOptions({
8
+ __doc: {
9
+ name: 'URL input',
10
+ tag: '<DomUrlInput>',
11
+ description: 'A convention text field for web addresses with URL type, autocomplete, and a URL validator.',
12
+ icon: 'M10.25 13.75 13.75 10.25M8.9 10.9 7.8 12a3 3 0 0 0 4.2 4.2l1.1-1.1M15.1 13.1l1.1-1.1A3 3 0 0 0 12 7.8l-1.1 1.1',
13
+ events: [
14
+ { name: 'update:modelValue', payload: 'string', description: 'Fired when the URL changes.' },
15
+ { name: 'focus', payload: 'FocusEvent', description: 'Fired when the input receives focus.' },
16
+ { name: 'blur', payload: 'FocusEvent', description: 'Fired when the input loses focus.' },
17
+ ],
18
+ },
19
+ });
20
+
21
+ const props = defineProps({
22
+ ...fieldProps,
23
+ label: {
24
+ type: String,
25
+ default: 'URL',
26
+ _edit: { description: 'Visible field label.' },
27
+ },
28
+ placeholder: {
29
+ type: String,
30
+ default: 'https://example.com',
31
+ _edit: { description: 'Placeholder text shown before entry.' },
32
+ },
33
+ autocomplete: {
34
+ type: String,
35
+ default: 'url',
36
+ _edit: { description: 'Native autocomplete hint.' },
37
+ },
38
+ });
39
+ const emit = defineEmits(['update:modelValue', 'focus', 'blur']);
40
+
41
+ const inputProps = computed(() => ({
42
+ ...props,
43
+ type: 'url',
44
+ validators: [urlValidator, ...(props.validators || [])],
45
+ }));
46
+ </script>
47
+
48
+ <template>
49
+ <DomTextInput
50
+ v-bind="inputProps"
51
+ @update:model-value="emit('update:modelValue', $event)"
52
+ @focus="emit('focus', $event)"
53
+ @blur="emit('blur', $event)"
54
+ />
55
+ </template>
@@ -0,0 +1,204 @@
1
+ <script>
2
+ import { fieldProps } from '../field/fieldProps.js';
3
+ import { createDefaultDocumentContent } from '../../data/document-editor/DomDocumentEditor.vue';
4
+
5
+ /**
6
+ * Returns the default editable document for the shorthand WYSIWYG field.
7
+ *
8
+ * @returns {Record<string, unknown>} Default Tiptap document content.
9
+ */
10
+ function defaultWysiwygContent() {
11
+ return createDefaultDocumentContent();
12
+ }
13
+
14
+ /**
15
+ * Returns the built-in tools shown by the shorthand WYSIWYG field.
16
+ *
17
+ * @returns {Array<string>} Built-in tool ids.
18
+ */
19
+ function defaultWysiwygTools() {
20
+ return [
21
+ 'undo',
22
+ 'redo',
23
+ 'paragraph',
24
+ 'heading1',
25
+ 'heading2',
26
+ 'heading3',
27
+ 'bold',
28
+ 'italic',
29
+ 'underline',
30
+ 'bulletList',
31
+ 'orderedList',
32
+ 'blockquote',
33
+ 'codeBlock',
34
+ 'link',
35
+ 'table',
36
+ ];
37
+ }
38
+
39
+ /**
40
+ * Returns an empty custom tool list.
41
+ *
42
+ * @returns {Array<Record<string, unknown>>} Custom toolbar tools.
43
+ */
44
+ function defaultCustomTools() {
45
+ return [];
46
+ }
47
+
48
+ export const wysiwygProps = {
49
+ ...fieldProps,
50
+ modelValue: {
51
+ type: [Object, String],
52
+ default: defaultWysiwygContent,
53
+ _edit: { component: 'DomJsonInput', description: 'Controlled editor content. JSON is preferred; HTML strings are accepted for simple imports.' },
54
+ },
55
+ output: {
56
+ type: String,
57
+ default: 'json',
58
+ _edit: { options: ['json', 'html'], description: 'Value emitted through update:modelValue.' },
59
+ },
60
+ editable: {
61
+ type: Boolean,
62
+ default: true,
63
+ _edit: { description: 'Allow editing and toolbar commands.' },
64
+ },
65
+ title: {
66
+ type: String,
67
+ default: '',
68
+ _edit: { description: 'Accessible editor title. Defaults to the field label.' },
69
+ },
70
+ status: {
71
+ type: String,
72
+ default: '',
73
+ _edit: { description: 'Optional save or validation status label.' },
74
+ },
75
+ tools: {
76
+ type: Array,
77
+ default: defaultWysiwygTools,
78
+ _edit: { component: 'DomJsonListInput', description: 'Built-in toolbar tool ids to show.' },
79
+ },
80
+ customTools: {
81
+ type: Array,
82
+ default: defaultCustomTools,
83
+ _edit: { component: 'DomJsonListInput', description: 'Additional toolbar actions emitted through tool-action.' },
84
+ },
85
+ showToolbar: {
86
+ type: Boolean,
87
+ default: true,
88
+ _edit: { description: 'Show the WYSIWYG toolbar.' },
89
+ },
90
+ showSlashMenu: {
91
+ type: Boolean,
92
+ default: true,
93
+ _edit: { description: 'Open the block command menu when the user types / at the start of a block.' },
94
+ },
95
+ minHeight: {
96
+ type: String,
97
+ default: '16rem',
98
+ _edit: { description: 'Minimum height for the editable field.' },
99
+ },
100
+ };
101
+ </script>
102
+
103
+ <script setup>
104
+ import { computed, ref, useAttrs } from 'vue';
105
+ import DomDocumentEditor from '../../data/document-editor/DomDocumentEditor.vue';
106
+
107
+ defineOptions({
108
+ inheritAttrs: false,
109
+ __doc: {
110
+ name: 'WYSIWYG field',
111
+ tag: '<DomWysiwyg>',
112
+ description: 'A textarea-shaped rich text field powered by DomDocumentEditor, with headings, language-aware highlighted code blocks, tables, simple v-model wiring, and no collaboration or AI review rail by default.',
113
+ events: [
114
+ { name: 'update:modelValue', payload: 'Record<string, unknown> | string', description: 'Fired with JSON or HTML content depending on the output prop.' },
115
+ { name: 'change', payload: 'DocumentEditorChangePayload', description: 'Fired with JSON, HTML, and plain text whenever the editor changes.' },
116
+ { name: 'selection-change', payload: 'DocumentEditorSelectionPayload', description: 'Fired when the editor selection changes.' },
117
+ { name: 'tool-action', payload: '({ tool, editor })', description: 'Fired when a custom toolbar tool is activated.' },
118
+ { name: 'ready', payload: '({ editor })', description: 'Fired after the browser-only editor runtime mounts.' },
119
+ { name: 'runtime-error', payload: 'Error', description: 'Fired when the editor runtime cannot mount.' },
120
+ { name: 'focus', payload: 'FocusEvent', description: 'Fired when the editable document receives focus.' },
121
+ { name: 'blur', payload: 'FocusEvent', description: 'Fired when the editable document loses focus.' },
122
+ ],
123
+ },
124
+ });
125
+
126
+ const props = defineProps(wysiwygProps);
127
+ const emit = defineEmits([
128
+ 'update:modelValue',
129
+ 'change',
130
+ 'selection-change',
131
+ 'tool-action',
132
+ 'ready',
133
+ 'runtime-error',
134
+ 'focus',
135
+ 'blur',
136
+ ]);
137
+ const attrs = useAttrs();
138
+ const editorRef = ref(null);
139
+ const editorTitle = computed(() => props.title || props.label || 'Rich text field');
140
+
141
+ /**
142
+ * Moves focus into the wrapped editor instance.
143
+ *
144
+ * @returns {void}
145
+ */
146
+ function focusEditor() {
147
+ editorRef.value?.focusEditor?.();
148
+ }
149
+
150
+ /**
151
+ * Returns the latest serializable editor payload from the wrapped editor.
152
+ *
153
+ * @returns {{ json: Record<string, unknown>|null, html: string, text: string }|undefined} Current editor payload.
154
+ */
155
+ function savePayload() {
156
+ return editorRef.value?.savePayload?.();
157
+ }
158
+
159
+ defineExpose({
160
+ editorRef,
161
+ focusEditor,
162
+ savePayload,
163
+ });
164
+ </script>
165
+
166
+ <template>
167
+ <DomDocumentEditor
168
+ ref="editorRef"
169
+ v-bind="attrs"
170
+ :model-value="modelValue"
171
+ :output="output"
172
+ :editable="editable"
173
+ :chrome="chrome"
174
+ :id="id"
175
+ :name="name"
176
+ :label="label"
177
+ :description="description"
178
+ :required="required"
179
+ :disabled="disabled"
180
+ :read-only="readOnly"
181
+ :invalid="invalid"
182
+ :errors="errors"
183
+ :visible="visible"
184
+ :validators="validators"
185
+ :validate-on-blur="validateOnBlur"
186
+ :placeholder="placeholder || 'Write something useful...'"
187
+ :title="editorTitle"
188
+ :status="status"
189
+ :tools="tools"
190
+ :custom-tools="customTools"
191
+ :show-toolbar="showToolbar"
192
+ :show-slash-menu="showSlashMenu"
193
+ :show-suggestion-rail="false"
194
+ :min-height="minHeight"
195
+ @update:model-value="emit('update:modelValue', $event)"
196
+ @change="emit('change', $event)"
197
+ @selection-change="emit('selection-change', $event)"
198
+ @tool-action="emit('tool-action', $event)"
199
+ @ready="emit('ready', $event)"
200
+ @runtime-error="emit('runtime-error', $event)"
201
+ @focus="emit('focus', $event)"
202
+ @blur="emit('blur', $event)"
203
+ />
204
+ </template>