@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,86 @@
|
|
|
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
|
+
import { useTextareaAutosize } from './useTextareaAutosize.js';
|
|
7
|
+
|
|
8
|
+
defineOptions({
|
|
9
|
+
__doc: {
|
|
10
|
+
name: 'Textarea input',
|
|
11
|
+
tag: '<DomTextareaInput>',
|
|
12
|
+
description: 'A multi-line field with shared form state and optional content-driven growth, including an optional maximum row limit.',
|
|
13
|
+
events: [
|
|
14
|
+
{ name: 'update:modelValue', payload: 'string', description: 'Fired when the text changes.' },
|
|
15
|
+
{ name: 'focus', payload: 'FocusEvent', description: 'Fired when the textarea receives focus.' },
|
|
16
|
+
{ name: 'blur', payload: 'FocusEvent', description: 'Fired when the textarea loses focus.' },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
...fieldProps,
|
|
23
|
+
rows: {
|
|
24
|
+
type: Number,
|
|
25
|
+
default: 3,
|
|
26
|
+
_edit: { group: 'Control props', min: 1, description: 'Minimum visible rows, or the fixed row count when autosize is disabled.' },
|
|
27
|
+
},
|
|
28
|
+
autosize: {
|
|
29
|
+
type: Boolean,
|
|
30
|
+
default: false,
|
|
31
|
+
_edit: { group: 'Control props', description: 'Grow and shrink the textarea to match its content.' },
|
|
32
|
+
},
|
|
33
|
+
maxRows: {
|
|
34
|
+
type: Number,
|
|
35
|
+
default: 0,
|
|
36
|
+
_edit: { group: 'Control props', min: 0, description: 'Maximum autosized rows before vertical scrolling begins. Use 0 for no maximum.' },
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const emit = defineEmits(['update:modelValue', 'focus', 'blur']);
|
|
40
|
+
const field = useField(props, emit, { idPrefix: 'dom-multiline-input' });
|
|
41
|
+
const autosize = computed(() => props.autosize);
|
|
42
|
+
const rows = computed(() => props.rows);
|
|
43
|
+
const maxRows = computed(() => props.maxRows);
|
|
44
|
+
const { textarea, resize } = useTextareaAutosize({
|
|
45
|
+
value: field.value,
|
|
46
|
+
autosize,
|
|
47
|
+
rows,
|
|
48
|
+
maxRows,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Writes native textarea input through the shared field contract and updates
|
|
53
|
+
* its content-driven height in the same frame.
|
|
54
|
+
*
|
|
55
|
+
* @param {InputEvent} event Native input event.
|
|
56
|
+
* @returns {void}
|
|
57
|
+
*/
|
|
58
|
+
function onInput(event) {
|
|
59
|
+
field.onInput(event.target.value);
|
|
60
|
+
resize();
|
|
61
|
+
}
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<template>
|
|
65
|
+
<DomField v-bind="field.fieldAttrs.value" :chrome="chrome">
|
|
66
|
+
<textarea
|
|
67
|
+
ref="textarea"
|
|
68
|
+
:id="field.id.value"
|
|
69
|
+
:name="field.htmlName.value"
|
|
70
|
+
:value="field.value.value"
|
|
71
|
+
:placeholder="placeholder"
|
|
72
|
+
:rows="rows"
|
|
73
|
+
:disabled="field.disabled.value || undefined"
|
|
74
|
+
:readonly="field.readOnly.value || undefined"
|
|
75
|
+
:required="required || undefined"
|
|
76
|
+
:aria-invalid="field.invalid.value || undefined"
|
|
77
|
+
:aria-describedby="field.describedBy.value || undefined"
|
|
78
|
+
:aria-errormessage="field.errorId.value || undefined"
|
|
79
|
+
:data-invalid="field.invalid.value ? '' : undefined"
|
|
80
|
+
:class="['skin-input', autosize && 'resize-none overflow-y-hidden']"
|
|
81
|
+
@input="onInput"
|
|
82
|
+
@focus="field.onFocus"
|
|
83
|
+
@blur="field.onBlur"
|
|
84
|
+
></textarea>
|
|
85
|
+
</DomField>
|
|
86
|
+
</template>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { nextTick, onBeforeUnmount, onMounted, ref, toValue, watch } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reads a computed CSS length and falls back when the browser reports a
|
|
5
|
+
* keyword such as `normal`.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} value Computed CSS value.
|
|
8
|
+
* @param {number} fallback Numeric fallback in pixels.
|
|
9
|
+
* @returns {number} Parsed pixel value.
|
|
10
|
+
*/
|
|
11
|
+
function cssPixels(value, fallback = 0) {
|
|
12
|
+
const parsedValue = Number.parseFloat(value);
|
|
13
|
+
return Number.isFinite(parsedValue) ? parsedValue : fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolves a row count into a positive whole number.
|
|
18
|
+
*
|
|
19
|
+
* @param {unknown} value Requested row count.
|
|
20
|
+
* @param {number} fallback Fallback row count.
|
|
21
|
+
* @returns {number} Positive whole-number row count.
|
|
22
|
+
*/
|
|
23
|
+
function rowCount(value, fallback) {
|
|
24
|
+
const numericValue = Number(value);
|
|
25
|
+
if (!Number.isFinite(numericValue) || numericValue <= 0) return fallback;
|
|
26
|
+
return Math.max(1, Math.round(numericValue));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Keeps a textarea's rendered height aligned with its content while
|
|
31
|
+
* preserving a minimum row count and an optional maximum row count.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} options Reactive autosize options.
|
|
34
|
+
* @param {unknown} options.value Reactive textarea value.
|
|
35
|
+
* @param {unknown} options.autosize Whether automatic sizing is enabled.
|
|
36
|
+
* @param {unknown} options.rows Minimum visible row count.
|
|
37
|
+
* @param {unknown} options.maxRows Maximum visible row count, or zero for no maximum.
|
|
38
|
+
* @returns {{ textarea: import('vue').Ref<HTMLTextAreaElement|null>, resize: () => void }} Autosize bindings.
|
|
39
|
+
*/
|
|
40
|
+
export function useTextareaAutosize(options) {
|
|
41
|
+
const textarea = ref(null);
|
|
42
|
+
let resizeObserver = null;
|
|
43
|
+
let observedWidth = 0;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Clears inline sizing when autosize is disabled.
|
|
47
|
+
*
|
|
48
|
+
* @param {HTMLTextAreaElement} element Textarea to reset.
|
|
49
|
+
* @returns {void}
|
|
50
|
+
*/
|
|
51
|
+
function clearSizing(element) {
|
|
52
|
+
element.style.height = '';
|
|
53
|
+
element.style.overflowY = '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Measures the textarea content and applies a row-bounded height.
|
|
58
|
+
*
|
|
59
|
+
* @returns {void}
|
|
60
|
+
*/
|
|
61
|
+
function resize() {
|
|
62
|
+
const element = textarea.value;
|
|
63
|
+
if (!element) return;
|
|
64
|
+
if (!toValue(options.autosize)) {
|
|
65
|
+
clearSizing(element);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (typeof window === 'undefined') return;
|
|
69
|
+
|
|
70
|
+
const styles = window.getComputedStyle(element);
|
|
71
|
+
const fontSize = cssPixels(styles.fontSize, 16);
|
|
72
|
+
const lineHeight = cssPixels(styles.lineHeight, fontSize * 1.5);
|
|
73
|
+
const padding = cssPixels(styles.paddingTop) + cssPixels(styles.paddingBottom);
|
|
74
|
+
const border = cssPixels(styles.borderTopWidth) + cssPixels(styles.borderBottomWidth);
|
|
75
|
+
const minimumRows = rowCount(toValue(options.rows), 1);
|
|
76
|
+
const requestedMaximumRows = Number(toValue(options.maxRows));
|
|
77
|
+
const maximumRows = Number.isFinite(requestedMaximumRows) && requestedMaximumRows > 0
|
|
78
|
+
? Math.max(minimumRows, rowCount(requestedMaximumRows, minimumRows))
|
|
79
|
+
: Number.POSITIVE_INFINITY;
|
|
80
|
+
|
|
81
|
+
element.style.height = '0px';
|
|
82
|
+
const measuredContentHeight = element.scrollHeight;
|
|
83
|
+
const minimumContentHeight = (minimumRows * lineHeight) + padding;
|
|
84
|
+
const maximumContentHeight = Number.isFinite(maximumRows)
|
|
85
|
+
? (maximumRows * lineHeight) + padding
|
|
86
|
+
: Number.POSITIVE_INFINITY;
|
|
87
|
+
const contentHeight = Math.max(
|
|
88
|
+
minimumContentHeight,
|
|
89
|
+
Math.min(measuredContentHeight, maximumContentHeight),
|
|
90
|
+
);
|
|
91
|
+
const renderedHeight = styles.boxSizing === 'border-box'
|
|
92
|
+
? contentHeight + border
|
|
93
|
+
: Math.max(0, contentHeight - padding);
|
|
94
|
+
|
|
95
|
+
element.style.height = `${Math.ceil(renderedHeight)}px`;
|
|
96
|
+
element.style.overflowY = measuredContentHeight > maximumContentHeight + 1 ? 'auto' : 'hidden';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Defers measurement until Vue has written reactive text and prop changes.
|
|
101
|
+
*
|
|
102
|
+
* @returns {Promise<void>} Promise resolved after the scheduled measurement.
|
|
103
|
+
*/
|
|
104
|
+
async function scheduleResize() {
|
|
105
|
+
await nextTick();
|
|
106
|
+
resize();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Re-measures wrapped content only when the textarea width changes.
|
|
111
|
+
*
|
|
112
|
+
* @param {ResizeObserverEntry[]} entries Resize observer entries.
|
|
113
|
+
* @returns {void}
|
|
114
|
+
*/
|
|
115
|
+
function onElementResize(entries) {
|
|
116
|
+
const width = entries[0]?.contentRect?.width || 0;
|
|
117
|
+
if (Math.abs(width - observedWidth) < 0.5) return;
|
|
118
|
+
observedWidth = width;
|
|
119
|
+
resize();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
watch(
|
|
123
|
+
() => [
|
|
124
|
+
toValue(options.value),
|
|
125
|
+
toValue(options.autosize),
|
|
126
|
+
toValue(options.rows),
|
|
127
|
+
toValue(options.maxRows),
|
|
128
|
+
],
|
|
129
|
+
() => void scheduleResize(),
|
|
130
|
+
{ immediate: true, flush: 'post' },
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
onMounted(() => {
|
|
134
|
+
resize();
|
|
135
|
+
if (typeof ResizeObserver === 'undefined' || !textarea.value) return;
|
|
136
|
+
observedWidth = textarea.value.getBoundingClientRect().width;
|
|
137
|
+
resizeObserver = new ResizeObserver(onElementResize);
|
|
138
|
+
resizeObserver.observe(textarea.value);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
onBeforeUnmount(() => resizeObserver?.disconnect());
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
textarea,
|
|
145
|
+
resize,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { fieldProps } from '../field/fieldProps.js';
|
|
3
|
+
|
|
4
|
+
export const popoverEditorDefinitions = {
|
|
5
|
+
position: {
|
|
6
|
+
type: String,
|
|
7
|
+
default: 'bottom-start',
|
|
8
|
+
_edit: {
|
|
9
|
+
component: 'DomPositionInput',
|
|
10
|
+
props: { label: 'Position' },
|
|
11
|
+
description: 'Preferred side and alignment around the trigger.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
offset: {
|
|
15
|
+
type: Number,
|
|
16
|
+
default: 8,
|
|
17
|
+
_edit: { description: 'Gap in pixels between the trigger and the popover.' },
|
|
18
|
+
},
|
|
19
|
+
collisionPadding: {
|
|
20
|
+
type: Number,
|
|
21
|
+
default: 8,
|
|
22
|
+
_edit: { description: 'Viewport padding used when the panel flips or shifts.' },
|
|
23
|
+
},
|
|
24
|
+
floatingMode: {
|
|
25
|
+
type: String,
|
|
26
|
+
default: 'viewport',
|
|
27
|
+
_edit: { options: ['viewport', 'anchor'], description: 'viewport keeps the panel inside the browser; anchor keeps it attached while scrolling.' },
|
|
28
|
+
},
|
|
29
|
+
flip: {
|
|
30
|
+
type: Boolean,
|
|
31
|
+
default: true,
|
|
32
|
+
_edit: { description: 'Allow the popover to flip to the opposite side when it would collide with the viewport.' },
|
|
33
|
+
},
|
|
34
|
+
lockScroll: {
|
|
35
|
+
type: Boolean,
|
|
36
|
+
default: false,
|
|
37
|
+
_edit: { description: 'Lock browser scrolling while the popover is open.' },
|
|
38
|
+
},
|
|
39
|
+
arrow: {
|
|
40
|
+
type: Boolean,
|
|
41
|
+
default: true,
|
|
42
|
+
_edit: { description: 'Show a small arrow pointing back to the trigger.' },
|
|
43
|
+
},
|
|
44
|
+
width: {
|
|
45
|
+
type: String,
|
|
46
|
+
default: 'min-w-[14rem] max-w-[20rem]',
|
|
47
|
+
_edit: { description: 'Tailwind width utility(ies) for the panel.' },
|
|
48
|
+
},
|
|
49
|
+
padding: {
|
|
50
|
+
type: String,
|
|
51
|
+
default: 'p-4',
|
|
52
|
+
_edit: { description: 'Tailwind padding utility for the panel.' },
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const timeSelectorProps = {
|
|
57
|
+
...fieldProps,
|
|
58
|
+
modelValue: {
|
|
59
|
+
type: String,
|
|
60
|
+
default: '',
|
|
61
|
+
_edit: { group: 'Control props', description: 'Selected time as HH:mm, or HH:mm:ss when showSeconds is enabled.' },
|
|
62
|
+
},
|
|
63
|
+
timeFormat: {
|
|
64
|
+
type: String,
|
|
65
|
+
default: '24h',
|
|
66
|
+
validator: (value) => ['24h', '12h'].includes(value),
|
|
67
|
+
_edit: { options: ['24h', '12h'], description: 'How times are displayed in the input and option list.' },
|
|
68
|
+
},
|
|
69
|
+
minuteStep: {
|
|
70
|
+
type: Number,
|
|
71
|
+
default: 15,
|
|
72
|
+
_edit: { group: 'Control props', description: 'Minute interval used to generate selectable options.' },
|
|
73
|
+
},
|
|
74
|
+
min: {
|
|
75
|
+
type: String,
|
|
76
|
+
default: '',
|
|
77
|
+
_edit: { group: 'Control props', description: 'Minimum selectable time as HH:mm or HH:mm:ss.' },
|
|
78
|
+
},
|
|
79
|
+
max: {
|
|
80
|
+
type: String,
|
|
81
|
+
default: '',
|
|
82
|
+
_edit: { group: 'Control props', description: 'Maximum selectable time as HH:mm or HH:mm:ss.' },
|
|
83
|
+
},
|
|
84
|
+
options: {
|
|
85
|
+
type: Array,
|
|
86
|
+
default: () => [],
|
|
87
|
+
_edit: {
|
|
88
|
+
component: 'DomJsonListInput',
|
|
89
|
+
group: 'Control props',
|
|
90
|
+
description: 'Optional custom time options. Use strings such as 09:30 or records with value, label, and disabled.',
|
|
91
|
+
props: {
|
|
92
|
+
compact: true,
|
|
93
|
+
schema: [
|
|
94
|
+
{ key: 'value', label: 'Value', placeholder: '09:30' },
|
|
95
|
+
{ key: 'label', label: 'Label', placeholder: '9:30 AM' },
|
|
96
|
+
{ key: 'disabled', label: 'Disabled', type: 'boolean' },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
showSeconds: {
|
|
102
|
+
type: Boolean,
|
|
103
|
+
default: false,
|
|
104
|
+
_edit: { group: 'Control props', description: 'Store and display seconds in addition to hours and minutes.' },
|
|
105
|
+
},
|
|
106
|
+
nativeInput: {
|
|
107
|
+
type: Boolean,
|
|
108
|
+
default: true,
|
|
109
|
+
_edit: { description: 'Include the native input name for HTML form submission.' },
|
|
110
|
+
},
|
|
111
|
+
registerField: {
|
|
112
|
+
type: Boolean,
|
|
113
|
+
default: true,
|
|
114
|
+
_edit: { description: 'Register with a parent form. Disable when composing this inside another form control.' },
|
|
115
|
+
},
|
|
116
|
+
popoverProps: {
|
|
117
|
+
type: Object,
|
|
118
|
+
default: () => ({}),
|
|
119
|
+
_edit: {
|
|
120
|
+
component: 'DomPropDefinitionInput',
|
|
121
|
+
description: 'Props passed to the inner DomPopover. Time selector defaults position to bottom-end, max width to 18rem, padding to p-2, and arrow to true.',
|
|
122
|
+
props: {
|
|
123
|
+
definitions: popoverEditorDefinitions,
|
|
124
|
+
include: ['position', 'offset', 'collisionPadding', 'floatingMode', 'flip', 'lockScroll', 'arrow', 'width', 'padding'],
|
|
125
|
+
compact: true,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<script setup>
|
|
133
|
+
import { computed, nextTick, ref, useId, watch } from 'vue';
|
|
134
|
+
import DomPopover, { popoverProps } from '../../components/popover/DomPopover.vue';
|
|
135
|
+
import DomField from '../field/DomField.vue';
|
|
136
|
+
import { useField } from '../field/useField.js';
|
|
137
|
+
import {
|
|
138
|
+
buildTimeOptions,
|
|
139
|
+
formatTimeDisplay,
|
|
140
|
+
normalizeTimeValue,
|
|
141
|
+
parseTimeInput,
|
|
142
|
+
timeRangeError,
|
|
143
|
+
} from './timeUtils.js';
|
|
144
|
+
|
|
145
|
+
defineOptions({
|
|
146
|
+
__doc: {
|
|
147
|
+
name: 'Time selector',
|
|
148
|
+
tag: '<DomTimeSelector>',
|
|
149
|
+
description: 'A typed time field with a popover list of selectable times. Stores HH:mm by default and can display 12-hour or 24-hour labels.',
|
|
150
|
+
icon: 'M12 6v6l4 2M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z',
|
|
151
|
+
events: [
|
|
152
|
+
{ name: 'update:modelValue', payload: 'HH:mm', description: 'Fired when a valid time is committed or selected.' },
|
|
153
|
+
{ name: 'change', payload: 'HH:mm', description: 'Fired when a valid time is committed.' },
|
|
154
|
+
{ name: 'invalid-time', payload: 'string', description: 'Fired when typed text cannot be parsed or is outside the allowed range.' },
|
|
155
|
+
{ name: 'focus', payload: 'FocusEvent', description: 'Fired when the text input receives focus.' },
|
|
156
|
+
{ name: 'blur', payload: 'FocusEvent', description: 'Fired when the text input loses focus.' },
|
|
157
|
+
],
|
|
158
|
+
keyboard: [
|
|
159
|
+
{ keys: 'Alt + ArrowDown', action: 'Open the time list.' },
|
|
160
|
+
{ keys: 'Enter', action: 'Commit the typed time when it is valid.' },
|
|
161
|
+
{ keys: 'Esc', action: 'Close the popover.' },
|
|
162
|
+
{ keys: 'Tab', action: 'Move through generated time options when the list is open.' },
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const props = defineProps(timeSelectorProps);
|
|
168
|
+
const emit = defineEmits(['update:modelValue', 'change', 'invalid-time', 'focus', 'blur']);
|
|
169
|
+
const field = useField(props, emit, { idPrefix: 'dom-time-selector', register: props.registerField });
|
|
170
|
+
const popover = ref(null);
|
|
171
|
+
const displayValue = ref(formatDisplay(field.value.value));
|
|
172
|
+
const parseError = ref('');
|
|
173
|
+
const optionRefs = new Map();
|
|
174
|
+
const uniqueId = useId();
|
|
175
|
+
const triggerId = computed(() => `${field.id.value || uniqueId}-time-button`);
|
|
176
|
+
const listId = computed(() => `${field.id.value || uniqueId}-time-list`);
|
|
177
|
+
const placeholderText = computed(() => props.placeholder || (props.timeFormat === '12h' ? '9:30 AM' : props.showSeconds ? '09:30:00' : '09:30'));
|
|
178
|
+
const selectionOptions = computed(() => buildTimeOptions({
|
|
179
|
+
options: props.options,
|
|
180
|
+
minuteStep: props.minuteStep,
|
|
181
|
+
min: props.min,
|
|
182
|
+
max: props.max,
|
|
183
|
+
timeFormat: props.timeFormat,
|
|
184
|
+
showSeconds: props.showSeconds,
|
|
185
|
+
}));
|
|
186
|
+
const selectedTime = computed(() => normalizeTimeValue(field.value.value, { showSeconds: props.showSeconds }));
|
|
187
|
+
const resolvedPopoverProps = computed(() => ({
|
|
188
|
+
...defaultPropsFrom(popoverProps),
|
|
189
|
+
position: 'bottom-end',
|
|
190
|
+
width: 'w-[calc(100vw-1rem)] max-w-[18rem]',
|
|
191
|
+
padding: 'p-2',
|
|
192
|
+
arrow: true,
|
|
193
|
+
...props.popoverProps,
|
|
194
|
+
triggerId: triggerId.value,
|
|
195
|
+
}));
|
|
196
|
+
const combinedErrors = computed(() => [
|
|
197
|
+
...field.errors.value,
|
|
198
|
+
...(parseError.value ? [parseError.value] : []),
|
|
199
|
+
]);
|
|
200
|
+
const invalid = computed(() => Boolean(field.invalid.value || parseError.value));
|
|
201
|
+
const fieldAttrs = computed(() => ({
|
|
202
|
+
...field.fieldAttrs.value,
|
|
203
|
+
invalid: invalid.value,
|
|
204
|
+
errors: combinedErrors.value,
|
|
205
|
+
}));
|
|
206
|
+
const inputAttrs = computed(() => {
|
|
207
|
+
const attrs = {
|
|
208
|
+
...field.inputAttrs.value,
|
|
209
|
+
value: displayValue.value,
|
|
210
|
+
placeholder: placeholderText.value,
|
|
211
|
+
'aria-invalid': invalid.value || undefined,
|
|
212
|
+
'aria-controls': listId.value,
|
|
213
|
+
'data-invalid': invalid.value ? '' : undefined,
|
|
214
|
+
};
|
|
215
|
+
if (!props.nativeInput) delete attrs.name;
|
|
216
|
+
return attrs;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
watch(field.value, (value) => {
|
|
220
|
+
displayValue.value = formatDisplay(value);
|
|
221
|
+
if (normalizeTimeValue(value, { showSeconds: props.showSeconds })) clearParseError();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
watch(() => [props.timeFormat, props.showSeconds], () => {
|
|
225
|
+
displayValue.value = formatDisplay(field.value.value);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
function defaultPropsFrom(definitions) {
|
|
229
|
+
return Object.fromEntries(Object.entries(definitions).map(([key, definition]) => [key, defaultPropValue(definition)]));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function defaultPropValue(definition) {
|
|
233
|
+
if (!definition || !Object.prototype.hasOwnProperty.call(definition, 'default')) {
|
|
234
|
+
return definition?.type === Boolean ? false : undefined;
|
|
235
|
+
}
|
|
236
|
+
if (typeof definition.default === 'function' && definition.type !== Function) return definition.default();
|
|
237
|
+
return definition.default;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function formatDisplay(value) {
|
|
241
|
+
const normalized = normalizeTimeValue(value, { showSeconds: props.showSeconds });
|
|
242
|
+
return normalized ? formatTimeDisplay(normalized, props) : '';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function clearParseError() {
|
|
246
|
+
parseError.value = '';
|
|
247
|
+
field.setFieldState({ errors: [], invalid: false });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function setParseError(message) {
|
|
251
|
+
parseError.value = message;
|
|
252
|
+
field.setFieldState({ errors: message ? [message] : [], invalid: Boolean(message) });
|
|
253
|
+
if (message) emit('invalid-time', message);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function parseDisplay(value) {
|
|
257
|
+
const parsed = parseTimeInput(value, props);
|
|
258
|
+
if (parsed.error) return parsed;
|
|
259
|
+
const rangeError = timeRangeError(parsed.value, props);
|
|
260
|
+
if (rangeError) return { value: '', error: rangeError };
|
|
261
|
+
return parsed;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function commitTime(value) {
|
|
265
|
+
clearParseError();
|
|
266
|
+
field.setValue(value);
|
|
267
|
+
displayValue.value = formatDisplay(value);
|
|
268
|
+
emit('change', value);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function commitTypedValue() {
|
|
272
|
+
const parsed = parseDisplay(displayValue.value);
|
|
273
|
+
if (parsed.error) {
|
|
274
|
+
setParseError(parsed.error);
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
commitTime(parsed.value);
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function onInput(event) {
|
|
282
|
+
displayValue.value = event.target.value;
|
|
283
|
+
if (!displayValue.value.trim()) commitTime('');
|
|
284
|
+
else parseError.value = '';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function onBlur(event) {
|
|
288
|
+
let errorMessage = '';
|
|
289
|
+
if (displayValue.value.trim()) {
|
|
290
|
+
const parsed = parseDisplay(displayValue.value);
|
|
291
|
+
if (parsed.error) {
|
|
292
|
+
errorMessage = parsed.error;
|
|
293
|
+
parseError.value = parsed.error;
|
|
294
|
+
} else {
|
|
295
|
+
commitTime(parsed.value);
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
clearParseError();
|
|
299
|
+
}
|
|
300
|
+
await field.onBlur(event);
|
|
301
|
+
if (errorMessage) setParseError(errorMessage);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function onKeydown(event) {
|
|
305
|
+
if (event.key === 'Enter') {
|
|
306
|
+
event.preventDefault();
|
|
307
|
+
commitTypedValue();
|
|
308
|
+
} else if (event.key === 'ArrowDown' && event.altKey) {
|
|
309
|
+
event.preventDefault();
|
|
310
|
+
popover.value?.open();
|
|
311
|
+
} else if (event.key === 'Escape') {
|
|
312
|
+
popover.value?.close();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function selectTime(option) {
|
|
317
|
+
if (option.disabled) return;
|
|
318
|
+
commitTime(option.value);
|
|
319
|
+
popover.value?.close();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function setOptionRef(value, element) {
|
|
323
|
+
if (element) optionRefs.set(value, element);
|
|
324
|
+
else optionRefs.delete(value);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function focusSelectedOption() {
|
|
328
|
+
await nextTick();
|
|
329
|
+
const selected = selectedTime.value && optionRefs.get(selectedTime.value);
|
|
330
|
+
const first = selectionOptions.value.find((option) => !option.disabled);
|
|
331
|
+
(selected || optionRefs.get(first?.value))?.focus?.();
|
|
332
|
+
}
|
|
333
|
+
</script>
|
|
334
|
+
|
|
335
|
+
<template>
|
|
336
|
+
<DomField v-bind="fieldAttrs" :chrome="chrome">
|
|
337
|
+
<div class="relative w-full">
|
|
338
|
+
<input
|
|
339
|
+
v-bind="inputAttrs"
|
|
340
|
+
type="text"
|
|
341
|
+
inputmode="numeric"
|
|
342
|
+
autocomplete="off"
|
|
343
|
+
class="skin-input h-10 pr-11"
|
|
344
|
+
@input="onInput"
|
|
345
|
+
@focus="field.onFocus"
|
|
346
|
+
@blur="onBlur"
|
|
347
|
+
@keydown="onKeydown"
|
|
348
|
+
/>
|
|
349
|
+
<button
|
|
350
|
+
:id="triggerId"
|
|
351
|
+
type="button"
|
|
352
|
+
class="absolute inset-y-1 right-1 grid size-8 cursor-pointer place-items-center rounded-md text-muted-fg transition hover:bg-secondary hover:text-canvas-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50"
|
|
353
|
+
:disabled="field.disabled.value || field.readOnly.value"
|
|
354
|
+
:aria-label="`Choose time for ${props.label || field.name.value}`"
|
|
355
|
+
@click.stop
|
|
356
|
+
@focus="field.onFocus"
|
|
357
|
+
@blur="field.onBlur"
|
|
358
|
+
>
|
|
359
|
+
<svg viewBox="0 0 24 24" class="size-4" fill="none" aria-hidden="true">
|
|
360
|
+
<path d="M12 6v6l4 2M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
|
|
361
|
+
</svg>
|
|
362
|
+
</button>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
<DomPopover
|
|
366
|
+
ref="popover"
|
|
367
|
+
v-bind="resolvedPopoverProps"
|
|
368
|
+
@open="focusSelectedOption"
|
|
369
|
+
>
|
|
370
|
+
<div :id="listId" class="max-h-72 overflow-auto p-1" role="listbox" :aria-label="props.label || 'Time options'">
|
|
371
|
+
<button
|
|
372
|
+
v-for="option in selectionOptions"
|
|
373
|
+
:key="option.value"
|
|
374
|
+
:ref="(element) => setOptionRef(option.value, element)"
|
|
375
|
+
type="button"
|
|
376
|
+
role="option"
|
|
377
|
+
class="flex w-full items-center justify-between gap-3 rounded-md px-3 py-2 text-left text-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-40"
|
|
378
|
+
:class="option.value === selectedTime ? 'bg-primary text-primary-fg' : 'text-canvas-fg hover:bg-secondary'"
|
|
379
|
+
:aria-selected="option.value === selectedTime"
|
|
380
|
+
:disabled="option.disabled"
|
|
381
|
+
@click="selectTime(option)"
|
|
382
|
+
>
|
|
383
|
+
<span class="font-medium">{{ option.label }}</span>
|
|
384
|
+
<span v-if="option.description" class="text-xs opacity-70">{{ option.description }}</span>
|
|
385
|
+
</button>
|
|
386
|
+
</div>
|
|
387
|
+
</DomPopover>
|
|
388
|
+
</DomField>
|
|
389
|
+
</template>
|