@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.
- package/LICENSE.md +109 -0
- package/README.md +44 -0
- package/SKILL.md +83 -0
- package/package.json +73 -0
- package/public/llms.txt +57 -0
- package/public/skills.sh +11 -0
- package/src/pages/_layout/componentCatalog.generated.js +4514 -0
- package/src/pages/_layout/componentInspector.js +60 -0
- package/src/pages/_layout/componentManager.js +192 -0
- package/src/pages/_layout/componentRuntimeRecords.js +49 -0
- package/src/pages/_layout/inspector/componentRegistry.js +639 -0
- package/src/pages/_layout/inspector/serialize.js +134 -0
- package/src/pages/_layout/inspector/useInspector.js +293 -0
- package/src/pages/components/accordion/DomAccordion.vue +39 -0
- package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
- package/src/pages/components/button/DomButton.vue +132 -0
- package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
- package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
- package/src/pages/components/dialog/DomDialog.vue +271 -0
- package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
- package/src/pages/components/dialog/DomDialogStack.vue +289 -0
- package/src/pages/components/dialog/useDialogs.js +253 -0
- package/src/pages/components/drawer/DomDrawer.vue +159 -0
- package/src/pages/components/dropdown/DomDropdown.vue +261 -0
- package/src/pages/components/icon-button/DomIconButton.vue +155 -0
- package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
- package/src/pages/components/menu/DomMenu.vue +359 -0
- package/src/pages/components/menu/DomMenuItem.vue +233 -0
- package/src/pages/components/menu/DomMenuLabel.vue +19 -0
- package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
- package/src/pages/components/menu/menuContext.js +11 -0
- package/src/pages/components/menu/menuPlacement.js +26 -0
- package/src/pages/components/menu/submenu.js +58 -0
- package/src/pages/components/pagination/DomPagination.vue +265 -0
- package/src/pages/components/playground/DomRenderer.vue +48 -0
- package/src/pages/components/popover/DomPopover.vue +244 -0
- package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
- package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
- package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
- package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
- package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
- package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
- package/src/pages/components/stepper/DomStepper.vue +386 -0
- package/src/pages/components/tabs/DomTabs.vue +149 -0
- package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
- package/src/pages/components/toast/DomToastItem.vue +52 -0
- package/src/pages/components/toast/DomToastStack.vue +768 -0
- package/src/pages/components/toast/useToasts.js +237 -0
- package/src/pages/components/toolbar/DomToolbar.vue +158 -0
- package/src/pages/components/tooltip/DomTooltip.vue +69 -0
- package/src/pages/components/tooltip/vDomTooltip.js +106 -0
- package/src/pages/components/tree-view/DomTreeView.vue +961 -0
- package/src/pages/components/tree-view/treeSchemas.js +129 -0
- package/src/pages/components/tree-view/treeUtils.js +228 -0
- package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
- package/src/pages/data/_shared/resourceRecord.js +29 -0
- package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
- package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
- package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
- package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
- package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
- package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
- package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
- package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
- package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
- package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
- package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
- package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
- package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
- package/src/pages/data/kanban/DomKanban.vue +1703 -0
- package/src/pages/data/kanban/kanbanApi.js +80 -0
- package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
- package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
- package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
- package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
- package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
- package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
- package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
- package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
- package/src/pages/forms/_shared/options.js +61 -0
- package/src/pages/forms/_shared/useOptionMenu.js +59 -0
- package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
- package/src/pages/forms/calendar/DomCalendar.vue +442 -0
- package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
- package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
- package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
- package/src/pages/forms/color-input/DomColorInput.vue +118 -0
- package/src/pages/forms/combobox/DomCombobox.vue +237 -0
- package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
- package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
- package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
- package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
- package/src/pages/forms/field/DomField.vue +267 -0
- package/src/pages/forms/field/fieldLayout.js +1 -0
- package/src/pages/forms/field/fieldProps.js +114 -0
- package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
- package/src/pages/forms/field/useField.js +343 -0
- package/src/pages/forms/field/validators.js +364 -0
- package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
- package/src/pages/forms/form/DomForm.vue +985 -0
- package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
- package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
- package/src/pages/forms/form/adapters/shared.js +53 -0
- package/src/pages/forms/form/adapters/zod.js +235 -0
- package/src/pages/forms/form/formApi.js +38 -0
- package/src/pages/forms/form/formDefinition.js +392 -0
- package/src/pages/forms/form/schemaAdapter.js +54 -0
- package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
- package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
- package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
- package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
- package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
- package/src/pages/forms/listbox/DomListbox.vue +105 -0
- package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
- package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
- package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
- package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
- package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
- package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
- package/src/pages/forms/password-strength/passwordStrength.js +92 -0
- package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
- package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
- package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
- package/src/pages/forms/select/DomSelect.vue +479 -0
- package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
- package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
- package/src/pages/forms/text-input/DomTextInput.vue +48 -0
- package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
- package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
- package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
- package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
- package/src/pages/forms/time-selector/timeUtils.js +194 -0
- package/src/pages/forms/toggle/DomToggle.vue +158 -0
- package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
- package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
- package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
- package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
- package/src/pages/lib/css/color.js +245 -0
- package/src/pages/lib/headless/accordion.d.ts +7 -0
- package/src/pages/lib/headless/accordion.js +246 -0
- package/src/pages/lib/headless/autocomplete.d.ts +4 -0
- package/src/pages/lib/headless/autocomplete.js +33 -0
- package/src/pages/lib/headless/base.d.ts +25 -0
- package/src/pages/lib/headless/base.js +127 -0
- package/src/pages/lib/headless/checkbox.d.ts +19 -0
- package/src/pages/lib/headless/checkbox.js +99 -0
- package/src/pages/lib/headless/combobox.d.ts +7 -0
- package/src/pages/lib/headless/combobox.js +488 -0
- package/src/pages/lib/headless/dialog.d.ts +23 -0
- package/src/pages/lib/headless/dialog.js +384 -0
- package/src/pages/lib/headless/drawer.d.ts +21 -0
- package/src/pages/lib/headless/drawer.js +682 -0
- package/src/pages/lib/headless/dropdown.d.ts +19 -0
- package/src/pages/lib/headless/dropdown.js +412 -0
- package/src/pages/lib/headless/floating-effect.js +118 -0
- package/src/pages/lib/headless/floating.d.ts +34 -0
- package/src/pages/lib/headless/floating.js +382 -0
- package/src/pages/lib/headless/index.d.ts +19 -0
- package/src/pages/lib/headless/index.js +19 -0
- package/src/pages/lib/headless/listbox.d.ts +19 -0
- package/src/pages/lib/headless/listbox.js +126 -0
- package/src/pages/lib/headless/menu.d.ts +18 -0
- package/src/pages/lib/headless/menu.js +117 -0
- package/src/pages/lib/headless/popover-panel.d.ts +18 -0
- package/src/pages/lib/headless/popover-panel.js +309 -0
- package/src/pages/lib/headless/popover.d.ts +19 -0
- package/src/pages/lib/headless/popover.js +391 -0
- package/src/pages/lib/headless/radio-group.d.ts +19 -0
- package/src/pages/lib/headless/radio-group.js +100 -0
- package/src/pages/lib/headless/tabs.d.ts +18 -0
- package/src/pages/lib/headless/tabs.js +119 -0
- package/src/pages/lib/headless/toast.d.ts +13 -0
- package/src/pages/lib/headless/toast.js +65 -0
- package/src/pages/lib/headless/toggle.d.ts +19 -0
- package/src/pages/lib/headless/toggle.js +162 -0
- package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
- package/src/pages/lib/headless/tooltip-controller.js +549 -0
- package/src/pages/lib/headless/tooltip.d.ts +19 -0
- package/src/pages/lib/headless/tooltip.js +153 -0
- package/src/pages/lib/headless/transitions.d.ts +5 -0
- package/src/pages/lib/headless/transitions.js +89 -0
- package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
- package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
- package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
- package/src/pages/lib/vue/codeHighlighter.js +132 -0
- package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
- package/src/pages/lib/vue/index.d.ts +6806 -0
- package/src/pages/lib/vue/index.js +218 -0
- package/src/pages/lib/vue/useTheme.js +116 -0
- package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
- package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
- package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
- package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
- package/src/pages/mobile/shell/DomAppShell.vue +51 -0
- package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
- package/src/pages/mobile/stack/DomAppStack.vue +711 -0
- package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
- package/src/pages/visual/alert/DomAlert.vue +363 -0
- package/src/pages/visual/avatar/DomAvatar.vue +110 -0
- package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
- package/src/pages/visual/badge/DomBadge.vue +105 -0
- package/src/pages/visual/card/DomCard.vue +65 -0
- package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
- package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
- package/src/pages/visual/progress/DomProgress.vue +212 -0
- package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
- package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
- package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
- package/src/pages/visual/spinner/DomSpinner.vue +122 -0
- package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
- package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
- package/src/pages/visual/text-diff/textDiff.js +300 -0
- package/src/style.css +446 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
export function pad(value) {
|
|
2
|
+
return String(value).padStart(2, '0');
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function normalizeMinuteStep(value, fallback = 15) {
|
|
6
|
+
const number = Math.round(Number(value));
|
|
7
|
+
if (!Number.isFinite(number) || number <= 0) return fallback;
|
|
8
|
+
return Math.min(720, Math.max(1, number));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function timeToParts(value) {
|
|
12
|
+
const match = String(value || '').match(/^(\d{2}):(\d{2})(?::(\d{2}))?$/);
|
|
13
|
+
if (!match) return null;
|
|
14
|
+
const hours = Number(match[1]);
|
|
15
|
+
const minutes = Number(match[2]);
|
|
16
|
+
const seconds = Number(match[3] || 0);
|
|
17
|
+
if (hours > 23 || minutes > 59 || seconds > 59) return null;
|
|
18
|
+
return { hours, minutes, seconds };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function partsToTime(parts, { showSeconds = false } = {}) {
|
|
22
|
+
const hours = Number(parts?.hours);
|
|
23
|
+
const minutes = Number(parts?.minutes);
|
|
24
|
+
const seconds = Number(parts?.seconds || 0);
|
|
25
|
+
if (
|
|
26
|
+
!Number.isInteger(hours)
|
|
27
|
+
|| !Number.isInteger(minutes)
|
|
28
|
+
|| !Number.isInteger(seconds)
|
|
29
|
+
|| hours < 0
|
|
30
|
+
|| hours > 23
|
|
31
|
+
|| minutes < 0
|
|
32
|
+
|| minutes > 59
|
|
33
|
+
|| seconds < 0
|
|
34
|
+
|| seconds > 59
|
|
35
|
+
) return '';
|
|
36
|
+
const base = `${pad(hours)}:${pad(minutes)}`;
|
|
37
|
+
return showSeconds ? `${base}:${pad(seconds)}` : base;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function normalizeTimeValue(value, options = {}) {
|
|
41
|
+
const parts = timeToParts(value);
|
|
42
|
+
if (!parts) return '';
|
|
43
|
+
return partsToTime(parts, options);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isTimeValue(value, options = {}) {
|
|
47
|
+
return normalizeTimeValue(value, options) === value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function timeToSeconds(value) {
|
|
51
|
+
const parts = timeToParts(value);
|
|
52
|
+
if (!parts) return NaN;
|
|
53
|
+
return parts.hours * 3600 + parts.minutes * 60 + parts.seconds;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function secondsToTime(totalSeconds, options = {}) {
|
|
57
|
+
const safeSeconds = Math.min(86399, Math.max(0, Math.round(Number(totalSeconds) || 0)));
|
|
58
|
+
const hours = Math.floor(safeSeconds / 3600);
|
|
59
|
+
const minutes = Math.floor((safeSeconds % 3600) / 60);
|
|
60
|
+
const seconds = safeSeconds % 60;
|
|
61
|
+
return partsToTime({ hours, minutes, seconds }, options);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function compareTimeValues(left, right) {
|
|
65
|
+
const leftSeconds = timeToSeconds(left);
|
|
66
|
+
const rightSeconds = timeToSeconds(right);
|
|
67
|
+
if (!Number.isFinite(leftSeconds) || !Number.isFinite(rightSeconds)) return 0;
|
|
68
|
+
return leftSeconds - rightSeconds;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function parseTimeInput(input, options = {}) {
|
|
72
|
+
const showSeconds = Boolean(options.showSeconds);
|
|
73
|
+
const trimmed = String(input || '').trim();
|
|
74
|
+
if (!trimmed) return { value: '', error: '' };
|
|
75
|
+
|
|
76
|
+
const compact = trimmed.match(/^(\d{3,4})(?:\s*([ap]m))?$/i);
|
|
77
|
+
if (compact) {
|
|
78
|
+
const digits = compact[1];
|
|
79
|
+
const suffix = compact[2] || '';
|
|
80
|
+
const hours = Number(digits.slice(0, -2));
|
|
81
|
+
const minutes = Number(digits.slice(-2));
|
|
82
|
+
return normalizeParsedParts({ hours, minutes, seconds: 0, suffix, showSeconds });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const colon = trimmed.match(/^(\d{1,2})(?::(\d{1,2}))?(?::(\d{1,2}))?(?:\s*([ap]m))?$/i);
|
|
86
|
+
if (!colon) {
|
|
87
|
+
return { value: '', error: 'Use a time like 09:30 or 2:30 PM.' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const hours = Number(colon[1]);
|
|
91
|
+
const minutes = colon[2] == null ? 0 : Number(colon[2]);
|
|
92
|
+
const seconds = colon[3] == null ? 0 : Number(colon[3]);
|
|
93
|
+
const suffix = colon[4] || '';
|
|
94
|
+
return normalizeParsedParts({ hours, minutes, seconds, suffix, showSeconds });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function formatTimeDisplay(value, options = {}) {
|
|
98
|
+
const parts = timeToParts(value);
|
|
99
|
+
if (!parts) return '';
|
|
100
|
+
const showSeconds = Boolean(options.showSeconds);
|
|
101
|
+
if (options.timeFormat === '12h') {
|
|
102
|
+
const suffix = parts.hours >= 12 ? 'PM' : 'AM';
|
|
103
|
+
const displayHours = parts.hours % 12 || 12;
|
|
104
|
+
const base = `${displayHours}:${pad(parts.minutes)}`;
|
|
105
|
+
return showSeconds ? `${base}:${pad(parts.seconds)} ${suffix}` : `${base} ${suffix}`;
|
|
106
|
+
}
|
|
107
|
+
const base = `${pad(parts.hours)}:${pad(parts.minutes)}`;
|
|
108
|
+
return showSeconds ? `${base}:${pad(parts.seconds)}` : base;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function timeRangeError(value, options = {}) {
|
|
112
|
+
const normalized = normalizeTimeValue(value, options);
|
|
113
|
+
if (!normalized) return '';
|
|
114
|
+
const min = normalizeTimeValue(options.min, options);
|
|
115
|
+
const max = normalizeTimeValue(options.max, options);
|
|
116
|
+
if (min && compareTimeValues(normalized, min) < 0) {
|
|
117
|
+
return `Time must be at or after ${formatTimeDisplay(min, options)}.`;
|
|
118
|
+
}
|
|
119
|
+
if (max && compareTimeValues(normalized, max) > 0) {
|
|
120
|
+
return `Time must be at or before ${formatTimeDisplay(max, options)}.`;
|
|
121
|
+
}
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function buildTimeOptions(options = {}) {
|
|
126
|
+
const customOptions = Array.isArray(options.options) ? options.options : [];
|
|
127
|
+
if (customOptions.length) {
|
|
128
|
+
return customOptions
|
|
129
|
+
.map((option) => normalizeOption(option, options))
|
|
130
|
+
.filter((option) => option.value);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const showSeconds = Boolean(options.showSeconds);
|
|
134
|
+
const stepSeconds = normalizeMinuteStep(options.minuteStep) * 60;
|
|
135
|
+
const min = normalizeTimeValue(options.min, { showSeconds }) || '00:00';
|
|
136
|
+
const max = normalizeTimeValue(options.max, { showSeconds }) || (showSeconds ? '23:59:59' : '23:59');
|
|
137
|
+
const minSeconds = timeToSeconds(min);
|
|
138
|
+
const maxSeconds = timeToSeconds(max);
|
|
139
|
+
if (!Number.isFinite(minSeconds) || !Number.isFinite(maxSeconds) || minSeconds > maxSeconds) return [];
|
|
140
|
+
|
|
141
|
+
const start = Math.ceil(minSeconds / stepSeconds) * stepSeconds;
|
|
142
|
+
const items = [];
|
|
143
|
+
for (let seconds = start; seconds <= maxSeconds; seconds += stepSeconds) {
|
|
144
|
+
const value = secondsToTime(seconds, { showSeconds });
|
|
145
|
+
items.push({
|
|
146
|
+
value,
|
|
147
|
+
label: formatTimeDisplay(value, options),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return items;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizeParsedParts({ hours, minutes, seconds, suffix, showSeconds }) {
|
|
154
|
+
const normalizedSuffix = String(suffix || '').toLowerCase();
|
|
155
|
+
if (normalizedSuffix) {
|
|
156
|
+
if (hours < 1 || hours > 12) return { value: '', error: 'Use 1-12 with AM or PM.' };
|
|
157
|
+
if (normalizedSuffix === 'am') hours = hours === 12 ? 0 : hours;
|
|
158
|
+
if (normalizedSuffix === 'pm') hours = hours === 12 ? 12 : hours + 12;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
!Number.isInteger(hours)
|
|
163
|
+
|| !Number.isInteger(minutes)
|
|
164
|
+
|| !Number.isInteger(seconds)
|
|
165
|
+
|| hours < 0
|
|
166
|
+
|| hours > 23
|
|
167
|
+
|| minutes < 0
|
|
168
|
+
|| minutes > 59
|
|
169
|
+
|| seconds < 0
|
|
170
|
+
|| seconds > 59
|
|
171
|
+
) {
|
|
172
|
+
return { value: '', error: 'Enter a real time.' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!showSeconds && seconds > 0) {
|
|
176
|
+
return { value: '', error: 'Use hours and minutes only.' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
value: partsToTime({ hours, minutes, seconds }, { showSeconds }),
|
|
181
|
+
error: '',
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeOption(option, options) {
|
|
186
|
+
const record = option && typeof option === 'object' ? option : { value: option };
|
|
187
|
+
const value = normalizeTimeValue(record.value, options) || parseTimeInput(record.value, options).value;
|
|
188
|
+
return {
|
|
189
|
+
...record,
|
|
190
|
+
value,
|
|
191
|
+
label: record.label || formatTimeDisplay(value, options),
|
|
192
|
+
disabled: Boolean(record.disabled),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
3
|
+
import { fieldProps } from '../field/fieldProps.js';
|
|
4
|
+
import { useField } from '../field/useField.js';
|
|
5
|
+
|
|
6
|
+
defineOptions({
|
|
7
|
+
__doc: {
|
|
8
|
+
name: 'Toggle',
|
|
9
|
+
tag: '<DomToggle>',
|
|
10
|
+
description: 'Accessible switch for boolean values. Works standalone with v-model or inside DomForm through useField.',
|
|
11
|
+
events: [
|
|
12
|
+
{ name: 'update:modelValue', payload: '(checked: boolean)', description: 'Emitted when checked changes.' },
|
|
13
|
+
{ name: 'focus', payload: 'FocusEvent', description: 'Fired when the switch receives focus.' },
|
|
14
|
+
{ name: 'blur', payload: 'FocusEvent', description: 'Fired when the switch loses focus.' },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const props = defineProps({
|
|
20
|
+
...fieldProps,
|
|
21
|
+
modelValue: {
|
|
22
|
+
type: Boolean,
|
|
23
|
+
default: false,
|
|
24
|
+
_edit: { group: 'Control props', description: 'On / off state.' },
|
|
25
|
+
},
|
|
26
|
+
size: {
|
|
27
|
+
type: String,
|
|
28
|
+
default: 'md',
|
|
29
|
+
_edit: { group: 'Control props', options: ['sm', 'md', 'lg'], description: 'Switch size. Matches DomButton size names.' },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const emit = defineEmits(['update:modelValue', 'focus', 'blur']);
|
|
33
|
+
|
|
34
|
+
const root = ref(null);
|
|
35
|
+
const field = useField(props, emit, { idPrefix: 'dom-toggle' });
|
|
36
|
+
const labelId = computed(() => (props.label ? `${field.id.value}-label` : ''));
|
|
37
|
+
const sizeConfig = computed(() => ({
|
|
38
|
+
sm: {
|
|
39
|
+
wrapper: 'gap-2',
|
|
40
|
+
label: 'text-xs',
|
|
41
|
+
description: 'text-[0.6875rem] leading-4',
|
|
42
|
+
style: {
|
|
43
|
+
'--dom-toggle-height': '1.25rem',
|
|
44
|
+
'--dom-toggle-width': '2.25rem',
|
|
45
|
+
'--dom-toggle-thumb-size': '1rem',
|
|
46
|
+
'--dom-toggle-thumb-offset': '1rem',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
md: {
|
|
50
|
+
wrapper: 'gap-3',
|
|
51
|
+
label: 'text-sm',
|
|
52
|
+
description: 'text-xs',
|
|
53
|
+
style: {
|
|
54
|
+
'--dom-toggle-height': '1.5rem',
|
|
55
|
+
'--dom-toggle-width': '2.75rem',
|
|
56
|
+
'--dom-toggle-thumb-size': '1.25rem',
|
|
57
|
+
'--dom-toggle-thumb-offset': '1.25rem',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
lg: {
|
|
61
|
+
wrapper: 'gap-3.5',
|
|
62
|
+
label: 'text-base',
|
|
63
|
+
description: 'text-sm',
|
|
64
|
+
style: {
|
|
65
|
+
'--dom-toggle-height': '1.75rem',
|
|
66
|
+
'--dom-toggle-width': '3.25rem',
|
|
67
|
+
'--dom-toggle-thumb-size': '1.5rem',
|
|
68
|
+
'--dom-toggle-thumb-offset': '1.5rem',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
}[props.size] || {
|
|
72
|
+
wrapper: 'gap-3',
|
|
73
|
+
label: 'text-sm',
|
|
74
|
+
description: 'text-xs',
|
|
75
|
+
style: {},
|
|
76
|
+
}));
|
|
77
|
+
const wrapperClasses = computed(() => [
|
|
78
|
+
'inline-flex items-center',
|
|
79
|
+
sizeConfig.value.wrapper,
|
|
80
|
+
]);
|
|
81
|
+
const labelClasses = computed(() => [
|
|
82
|
+
sizeConfig.value.label,
|
|
83
|
+
'text-canvas-fg',
|
|
84
|
+
]);
|
|
85
|
+
const descriptionClasses = computed(() => [
|
|
86
|
+
sizeConfig.value.description,
|
|
87
|
+
'text-muted-fg',
|
|
88
|
+
]);
|
|
89
|
+
const errorClasses = computed(() => [
|
|
90
|
+
sizeConfig.value.description,
|
|
91
|
+
'text-destructive',
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
onMounted(async () => {
|
|
95
|
+
await import('../../lib/headless/toggle.js');
|
|
96
|
+
syncToggleFromValue();
|
|
97
|
+
root.value?.addEventListener('dom:change', (event) => field.onInput(Boolean(event.detail.checked)));
|
|
98
|
+
});
|
|
99
|
+
watch(field.value, syncToggleFromValue);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Keep the underlying headless toggle state aligned with the Vue field value.
|
|
103
|
+
*
|
|
104
|
+
* @returns {void}
|
|
105
|
+
*/
|
|
106
|
+
function syncToggleFromValue() {
|
|
107
|
+
if (root.value && root.value.checked !== Boolean(field.value.value)) {
|
|
108
|
+
root.value.checked = Boolean(field.value.value);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Toggle the switch when the visible text label is clicked.
|
|
114
|
+
*
|
|
115
|
+
* @returns {void}
|
|
116
|
+
*/
|
|
117
|
+
function toggleFromLabel() {
|
|
118
|
+
if (field.disabled.value || field.readOnly.value) return;
|
|
119
|
+
root.value?.focus?.();
|
|
120
|
+
root.value?.toggle?.();
|
|
121
|
+
}
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<template>
|
|
125
|
+
<div v-if="field.visible.value" :class="wrapperClasses">
|
|
126
|
+
<input
|
|
127
|
+
v-if="field.htmlName.value"
|
|
128
|
+
type="hidden"
|
|
129
|
+
:name="field.htmlName.value"
|
|
130
|
+
:value="field.value.value ? 'true' : 'false'"
|
|
131
|
+
/>
|
|
132
|
+
<dom-toggle
|
|
133
|
+
ref="root"
|
|
134
|
+
:id="field.id.value"
|
|
135
|
+
:checked="Boolean(field.value.value) || null"
|
|
136
|
+
:disabled="field.disabled.value || field.readOnly.value || null"
|
|
137
|
+
:aria-label="!labelId ? label || undefined : undefined"
|
|
138
|
+
:aria-labelledby="labelId || undefined"
|
|
139
|
+
:aria-describedby="field.describedBy.value || undefined"
|
|
140
|
+
:aria-invalid="field.invalid.value || undefined"
|
|
141
|
+
:aria-errormessage="field.errorId.value || undefined"
|
|
142
|
+
:data-invalid="field.invalid.value ? '' : undefined"
|
|
143
|
+
:style="sizeConfig.style"
|
|
144
|
+
@focus="field.onFocus"
|
|
145
|
+
@blur="field.onBlur"
|
|
146
|
+
/>
|
|
147
|
+
<span
|
|
148
|
+
v-if="chrome !== 'none' && (label || description || field.errors.value.length)"
|
|
149
|
+
class="grid gap-0.5"
|
|
150
|
+
:class="field.disabled.value ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'"
|
|
151
|
+
@click="toggleFromLabel"
|
|
152
|
+
>
|
|
153
|
+
<span v-if="label" :id="labelId" :class="labelClasses">{{ label }}</span>
|
|
154
|
+
<span v-if="description" :id="field.descriptionId.value" :class="descriptionClasses">{{ description }}</span>
|
|
155
|
+
<span v-if="field.errors.value.length" :id="field.errorId.value" :class="errorClasses">{{ field.errors.value[0].message || field.errors.value[0] }}</span>
|
|
156
|
+
</span>
|
|
157
|
+
</div>
|
|
158
|
+
</template>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import DomField from '../field/DomField.vue';
|
|
4
|
+
import { fieldProps } from '../field/fieldProps.js';
|
|
5
|
+
import { useField } from '../field/useField.js';
|
|
6
|
+
|
|
7
|
+
defineOptions({
|
|
8
|
+
__doc: {
|
|
9
|
+
name: 'Toggle button',
|
|
10
|
+
tag: '<DomToggleButton>',
|
|
11
|
+
description: 'A pressable form button for boolean choices, using aria-pressed and v-model.',
|
|
12
|
+
slots: [{ name: '(default)', description: 'Button label or custom inline content.' }],
|
|
13
|
+
events: [{ name: 'update:modelValue', payload: '(pressed: boolean)', description: 'Emitted when pressed changes.' }],
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const props = defineProps({
|
|
18
|
+
...fieldProps,
|
|
19
|
+
modelValue: {
|
|
20
|
+
type: Boolean,
|
|
21
|
+
default: false,
|
|
22
|
+
_edit: { group: 'Control props', description: 'Pressed state.' },
|
|
23
|
+
},
|
|
24
|
+
pressedLabel: {
|
|
25
|
+
type: String,
|
|
26
|
+
default: '',
|
|
27
|
+
_edit: { group: 'Control props', description: 'Optional button label shown while pressed.' },
|
|
28
|
+
},
|
|
29
|
+
unpressedLabel: {
|
|
30
|
+
type: String,
|
|
31
|
+
default: '',
|
|
32
|
+
_edit: { group: 'Control props', description: 'Optional button label shown while not pressed.' },
|
|
33
|
+
},
|
|
34
|
+
size: {
|
|
35
|
+
type: String,
|
|
36
|
+
default: 'md',
|
|
37
|
+
_edit: { group: 'Control props', options: ['sm', 'md', 'lg'], description: 'Button size.' },
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const emit = defineEmits(['update:modelValue', 'focus', 'blur']);
|
|
41
|
+
const field = useField(props, emit, { idPrefix: 'dom-toggle-button' });
|
|
42
|
+
|
|
43
|
+
const pressed = computed(() => Boolean(field.value.value));
|
|
44
|
+
const displayLabel = computed(() => {
|
|
45
|
+
if (pressed.value && props.pressedLabel) return props.pressedLabel;
|
|
46
|
+
if (!pressed.value && props.unpressedLabel) return props.unpressedLabel;
|
|
47
|
+
if (!props.pressedLabel && !props.unpressedLabel) return props.label;
|
|
48
|
+
return pressed.value ? props.unpressedLabel : props.pressedLabel;
|
|
49
|
+
});
|
|
50
|
+
const sizeClass = computed(() => ({
|
|
51
|
+
sm: 'h-8 px-3 text-xs',
|
|
52
|
+
md: 'h-10 px-4 text-sm',
|
|
53
|
+
lg: 'h-11 px-5 text-base',
|
|
54
|
+
}[props.size] || 'h-10 px-4 text-sm'));
|
|
55
|
+
|
|
56
|
+
function toggle() {
|
|
57
|
+
if (field.disabled.value || field.readOnly.value) return;
|
|
58
|
+
field.onInput(!pressed.value);
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<DomField v-bind="field.fieldAttrs.value" :chrome="chrome">
|
|
64
|
+
<input
|
|
65
|
+
v-if="field.htmlName.value"
|
|
66
|
+
type="hidden"
|
|
67
|
+
:name="field.htmlName.value"
|
|
68
|
+
:value="pressed ? 'true' : 'false'"
|
|
69
|
+
/>
|
|
70
|
+
<button
|
|
71
|
+
:id="field.id.value"
|
|
72
|
+
type="button"
|
|
73
|
+
:aria-pressed="String(pressed)"
|
|
74
|
+
:aria-invalid="field.invalid.value || undefined"
|
|
75
|
+
:aria-describedby="field.describedBy.value || undefined"
|
|
76
|
+
:aria-errormessage="field.errorId.value || undefined"
|
|
77
|
+
:data-invalid="field.invalid.value ? '' : undefined"
|
|
78
|
+
:disabled="field.disabled.value || undefined"
|
|
79
|
+
class="skin-control inline-flex w-fit items-center justify-center gap-2 rounded-xl font-medium outline-none transition hover:brightness-[0.98] focus-visible:ring-2 focus-visible:ring-ring/50 active:translate-y-px disabled:cursor-not-allowed disabled:opacity-50 aria-pressed:border-primary aria-pressed:bg-primary aria-pressed:text-primary-fg data-[invalid]:border-destructive data-[invalid]:ring-destructive/20"
|
|
80
|
+
:class="sizeClass"
|
|
81
|
+
@click="toggle"
|
|
82
|
+
@focus="field.onFocus"
|
|
83
|
+
@blur="field.onBlur"
|
|
84
|
+
>
|
|
85
|
+
<slot :pressed="pressed" :label="displayLabel">{{ displayLabel || 'Toggle' }}</slot>
|
|
86
|
+
</button>
|
|
87
|
+
</DomField>
|
|
88
|
+
</template>
|