@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,347 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {object} BreadcrumbItem
|
|
4
|
+
* @property {string|number} [key] Stable item key.
|
|
5
|
+
* @property {string} label Human-readable location label.
|
|
6
|
+
* @property {string|Record<string, unknown>} [to] Vue Router destination.
|
|
7
|
+
* @property {string} [href] Native link destination.
|
|
8
|
+
* @property {boolean} [current] Mark this item as the current location.
|
|
9
|
+
* @property {boolean} [disabled] Render the item without navigation.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} BreadcrumbNavigatePayload
|
|
14
|
+
* @property {BreadcrumbItem} item Selected breadcrumb item.
|
|
15
|
+
* @property {number} index Original item index.
|
|
16
|
+
* @property {boolean} current Whether the item is the current location.
|
|
17
|
+
* @property {MouseEvent} event Native click event.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {object} BreadcrumbSlotPayload
|
|
22
|
+
* @property {BreadcrumbItem} item Breadcrumb item.
|
|
23
|
+
* @property {number} index Original item index.
|
|
24
|
+
* @property {boolean} current Whether the item is the current location.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export const breadcrumbsProps = {
|
|
28
|
+
items: {
|
|
29
|
+
type: Array,
|
|
30
|
+
default: () => [],
|
|
31
|
+
_edit: { typeRef: 'Array<BreadcrumbItem>', description: 'Ordered application-owned breadcrumb records.' },
|
|
32
|
+
},
|
|
33
|
+
maxItems: {
|
|
34
|
+
type: Number,
|
|
35
|
+
default: 0,
|
|
36
|
+
_edit: { description: 'Maximum visible entries before the middle path collapses. Use 0 to show every item.' },
|
|
37
|
+
},
|
|
38
|
+
currentAsLink: {
|
|
39
|
+
type: Boolean,
|
|
40
|
+
default: false,
|
|
41
|
+
_edit: { description: 'Keep the current location interactive when it has a destination.' },
|
|
42
|
+
},
|
|
43
|
+
separator: {
|
|
44
|
+
type: String,
|
|
45
|
+
default: 'chevron',
|
|
46
|
+
_edit: { options: ['chevron', 'slash'], description: 'Built-in separator treatment.' },
|
|
47
|
+
},
|
|
48
|
+
size: {
|
|
49
|
+
type: String,
|
|
50
|
+
default: 'md',
|
|
51
|
+
_edit: { options: ['sm', 'md'], description: 'Text and spacing density.' },
|
|
52
|
+
},
|
|
53
|
+
ariaLabel: {
|
|
54
|
+
type: String,
|
|
55
|
+
default: 'Breadcrumb',
|
|
56
|
+
_edit: { description: 'Accessible label for the navigation landmark.' },
|
|
57
|
+
},
|
|
58
|
+
collapseLabel: {
|
|
59
|
+
type: String,
|
|
60
|
+
default: 'Show hidden breadcrumbs',
|
|
61
|
+
_edit: { description: 'Accessible label for the collapsed-path control.' },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<script setup>
|
|
67
|
+
import { computed, ref, watch } from 'vue';
|
|
68
|
+
import { RouterLink } from 'vue-router';
|
|
69
|
+
|
|
70
|
+
defineOptions({
|
|
71
|
+
__doc: {
|
|
72
|
+
name: 'Breadcrumbs',
|
|
73
|
+
tag: '<DomBreadcrumbs>',
|
|
74
|
+
description: 'Accessible route or resource-path navigation with application-owned destinations and optional middle-path collapsing.',
|
|
75
|
+
icon: 'M4 7h5l3 5-3 5H4m8-10h5l3 5-3 5h-5',
|
|
76
|
+
events: [
|
|
77
|
+
{ name: 'navigate', payload: 'BreadcrumbNavigatePayload', typeRef: 'BreadcrumbNavigatePayload', description: 'Fired whenever an interactive breadcrumb is selected. Link navigation continues normally.' },
|
|
78
|
+
{ name: 'expand', payload: 'BreadcrumbItem[]', description: 'Fired after a collapsed middle path is revealed.' },
|
|
79
|
+
],
|
|
80
|
+
slots: [
|
|
81
|
+
{ name: 'item', payload: 'BreadcrumbSlotPayload', typeRef: 'BreadcrumbSlotPayload', description: 'Replace an item label inside its semantic link, button, or current-location element.' },
|
|
82
|
+
{ name: 'separator', payload: '{ previous, next, index }', description: 'Replace the separator before each item after the first.' },
|
|
83
|
+
{ name: 'collapsed', payload: '{ items, count, expand }', description: 'Replace the control used to reveal a collapsed middle path.' },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const props = defineProps(breadcrumbsProps);
|
|
89
|
+
const emit = defineEmits(['navigate', 'expand']);
|
|
90
|
+
|
|
91
|
+
const expanded = ref(false);
|
|
92
|
+
const normalizedItems = computed(getNormalizedItems);
|
|
93
|
+
const currentIndex = computed(getCurrentIndex);
|
|
94
|
+
const visibleEntries = computed(getVisibleEntries);
|
|
95
|
+
const listClass = computed(getListClass);
|
|
96
|
+
|
|
97
|
+
watch(() => props.items, collapseChangedPath);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Collapses a newly supplied path so its middle can be expanded deliberately.
|
|
101
|
+
*
|
|
102
|
+
* @returns {void}
|
|
103
|
+
*/
|
|
104
|
+
function collapseChangedPath() {
|
|
105
|
+
expanded.value = false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Normalizes supplied items and their stable rendering keys.
|
|
110
|
+
*
|
|
111
|
+
* @returns {Array<BreadcrumbItem & { key: string|number }>} Safe breadcrumb records.
|
|
112
|
+
*/
|
|
113
|
+
function getNormalizedItems() {
|
|
114
|
+
return (Array.isArray(props.items) ? props.items : [])
|
|
115
|
+
.filter(hasBreadcrumbLabel)
|
|
116
|
+
.map(normalizeBreadcrumb);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Reports whether a candidate has a visible label.
|
|
121
|
+
*
|
|
122
|
+
* @param {BreadcrumbItem} item Candidate breadcrumb.
|
|
123
|
+
* @returns {boolean} True when the item can be rendered.
|
|
124
|
+
*/
|
|
125
|
+
function hasBreadcrumbLabel(item) {
|
|
126
|
+
return Boolean(item && String(item.label || '').trim());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Adds a stable fallback key to one breadcrumb.
|
|
131
|
+
*
|
|
132
|
+
* @param {BreadcrumbItem} item Source breadcrumb.
|
|
133
|
+
* @param {number} index Original item index.
|
|
134
|
+
* @returns {BreadcrumbItem & { key: string|number }} Normalized breadcrumb.
|
|
135
|
+
*/
|
|
136
|
+
function normalizeBreadcrumb(item, index) {
|
|
137
|
+
return {
|
|
138
|
+
...item,
|
|
139
|
+
key: item.key ?? item.href ?? `breadcrumb-${index}`,
|
|
140
|
+
label: String(item.label),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Finds the explicitly current location or defaults to the final item.
|
|
146
|
+
*
|
|
147
|
+
* @returns {number} Current breadcrumb index or -1.
|
|
148
|
+
*/
|
|
149
|
+
function getCurrentIndex() {
|
|
150
|
+
for (let index = normalizedItems.value.length - 1; index >= 0; index -= 1) {
|
|
151
|
+
if (normalizedItems.value[index].current) return index;
|
|
152
|
+
}
|
|
153
|
+
return normalizedItems.value.length - 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Builds complete or middle-collapsed entries for the rendered list.
|
|
158
|
+
*
|
|
159
|
+
* @returns {Array<Record<string, unknown>>} Visible item and collapse-control entries.
|
|
160
|
+
*/
|
|
161
|
+
function getVisibleEntries() {
|
|
162
|
+
const items = normalizedItems.value;
|
|
163
|
+
const limit = Math.max(0, Math.floor(Number(props.maxItems) || 0));
|
|
164
|
+
if (expanded.value || limit < 3 || items.length <= limit) return items.map(createItemEntry);
|
|
165
|
+
|
|
166
|
+
const itemSlots = limit - 1;
|
|
167
|
+
const leadingCount = Math.ceil(itemSlots / 2);
|
|
168
|
+
const trailingCount = Math.floor(itemSlots / 2);
|
|
169
|
+
const hiddenStart = leadingCount;
|
|
170
|
+
const hiddenEnd = items.length - trailingCount;
|
|
171
|
+
const leading = items.slice(0, leadingCount).map(createItemEntry);
|
|
172
|
+
const hiddenItems = items.slice(hiddenStart, hiddenEnd);
|
|
173
|
+
const trailing = items.slice(hiddenEnd).map(createTrailingEntry(hiddenEnd));
|
|
174
|
+
|
|
175
|
+
return [
|
|
176
|
+
...leading,
|
|
177
|
+
{ key: '__collapsed__', collapsed: true, items: hiddenItems },
|
|
178
|
+
...trailing,
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Creates a visible entry using its current array index.
|
|
184
|
+
*
|
|
185
|
+
* @param {BreadcrumbItem} item Breadcrumb item.
|
|
186
|
+
* @param {number} index Original item index.
|
|
187
|
+
* @returns {Record<string, unknown>} Visible item entry.
|
|
188
|
+
*/
|
|
189
|
+
function createItemEntry(item, index) {
|
|
190
|
+
return { key: item.key, item, index, collapsed: false };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Creates a mapper that restores indices for the trailing path segment.
|
|
195
|
+
*
|
|
196
|
+
* @param {number} offset First trailing item index.
|
|
197
|
+
* @returns {(item: BreadcrumbItem, index: number) => Record<string, unknown>} Trailing entry mapper.
|
|
198
|
+
*/
|
|
199
|
+
function createTrailingEntry(offset) {
|
|
200
|
+
/**
|
|
201
|
+
* Restores one trailing breadcrumb's original path index.
|
|
202
|
+
*
|
|
203
|
+
* @param {BreadcrumbItem} item Breadcrumb item.
|
|
204
|
+
* @param {number} index Trailing segment index.
|
|
205
|
+
* @returns {Record<string, unknown>} Visible item entry.
|
|
206
|
+
*/
|
|
207
|
+
return function mapTrailingEntry(item, index) {
|
|
208
|
+
return createItemEntry(item, offset + index);
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Builds size-aware list classes.
|
|
214
|
+
*
|
|
215
|
+
* @returns {string} Ordered-list classes.
|
|
216
|
+
*/
|
|
217
|
+
function getListClass() {
|
|
218
|
+
return props.size === 'sm' ? 'gap-1 text-xs' : 'gap-1.5 text-sm';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Reports whether a visible item is the current location.
|
|
223
|
+
*
|
|
224
|
+
* @param {Record<string, unknown>} entry Visible item entry.
|
|
225
|
+
* @returns {boolean} True for the current location.
|
|
226
|
+
*/
|
|
227
|
+
function isCurrent(entry) {
|
|
228
|
+
return !entry.collapsed && entry.index === currentIndex.value;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Reports whether a current item should render without navigation.
|
|
233
|
+
*
|
|
234
|
+
* @param {Record<string, unknown>} entry Visible item entry.
|
|
235
|
+
* @returns {boolean} True when the item is plain current-location text.
|
|
236
|
+
*/
|
|
237
|
+
function isPlainCurrent(entry) {
|
|
238
|
+
return isCurrent(entry) && !props.currentAsLink;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Reveals the hidden middle path and reports the expanded items.
|
|
243
|
+
*
|
|
244
|
+
* @param {Array<BreadcrumbItem>} items Hidden breadcrumb items.
|
|
245
|
+
* @returns {void}
|
|
246
|
+
*/
|
|
247
|
+
function expandPath(items) {
|
|
248
|
+
expanded.value = true;
|
|
249
|
+
emit('expand', items);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Emits application navigation metadata without blocking native routing.
|
|
254
|
+
*
|
|
255
|
+
* @param {Record<string, unknown>} entry Visible item entry.
|
|
256
|
+
* @param {MouseEvent} event Native click event.
|
|
257
|
+
* @returns {void}
|
|
258
|
+
*/
|
|
259
|
+
function emitNavigate(entry, event) {
|
|
260
|
+
if (entry.item.disabled) {
|
|
261
|
+
event.preventDefault();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
emit('navigate', {
|
|
265
|
+
item: entry.item,
|
|
266
|
+
index: entry.index,
|
|
267
|
+
current: isCurrent(entry),
|
|
268
|
+
event,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Builds the public item slot payload.
|
|
274
|
+
*
|
|
275
|
+
* @param {Record<string, unknown>} entry Visible item entry.
|
|
276
|
+
* @returns {BreadcrumbSlotPayload} Item slot payload.
|
|
277
|
+
*/
|
|
278
|
+
function itemSlotPayload(entry) {
|
|
279
|
+
return { item: entry.item, index: entry.index, current: isCurrent(entry) };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Returns the separator's accessible-hidden glyph.
|
|
284
|
+
*
|
|
285
|
+
* @returns {string} Separator glyph.
|
|
286
|
+
*/
|
|
287
|
+
function separatorGlyph() {
|
|
288
|
+
return props.separator === 'slash' ? '/' : '›';
|
|
289
|
+
}
|
|
290
|
+
</script>
|
|
291
|
+
|
|
292
|
+
<template>
|
|
293
|
+
<nav :aria-label="ariaLabel" class="min-w-0 text-canvas-fg">
|
|
294
|
+
<ol class="flex min-w-0 flex-wrap items-center" :class="listClass">
|
|
295
|
+
<li v-for="(entry, visibleIndex) in visibleEntries" :key="entry.key" class="flex min-w-0 items-center gap-1.5">
|
|
296
|
+
<slot v-if="visibleIndex > 0" name="separator" :previous="visibleEntries[visibleIndex - 1]?.item" :next="entry.item" :index="visibleIndex">
|
|
297
|
+
<span class="select-none text-muted-fg/70" aria-hidden="true">{{ separatorGlyph() }}</span>
|
|
298
|
+
</slot>
|
|
299
|
+
|
|
300
|
+
<slot v-if="entry.collapsed" name="collapsed" :items="entry.items" :count="entry.items.length" :expand="() => expandPath(entry.items)">
|
|
301
|
+
<button
|
|
302
|
+
type="button"
|
|
303
|
+
class="rounded-md px-1.5 py-1 font-semibold text-muted-fg transition hover:bg-secondary hover:text-canvas-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
304
|
+
:aria-label="`${collapseLabel}: ${entry.items.length} hidden`"
|
|
305
|
+
@click="expandPath(entry.items)"
|
|
306
|
+
>
|
|
307
|
+
<span aria-hidden="true">…</span>
|
|
308
|
+
</button>
|
|
309
|
+
</slot>
|
|
310
|
+
|
|
311
|
+
<span v-else-if="isPlainCurrent(entry) || entry.item.disabled" class="truncate px-1 py-1 font-semibold" :class="entry.item.disabled ? 'text-muted-fg/60' : 'text-canvas-fg'" :aria-current="isCurrent(entry) ? 'page' : undefined">
|
|
312
|
+
<slot name="item" v-bind="itemSlotPayload(entry)">{{ entry.item.label }}</slot>
|
|
313
|
+
</span>
|
|
314
|
+
|
|
315
|
+
<RouterLink
|
|
316
|
+
v-else-if="entry.item.to"
|
|
317
|
+
:to="entry.item.to"
|
|
318
|
+
class="truncate rounded-md px-1 py-1 font-medium text-muted-fg transition hover:bg-secondary hover:text-canvas-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
319
|
+
:aria-current="isCurrent(entry) ? 'page' : undefined"
|
|
320
|
+
@click="emitNavigate(entry, $event)"
|
|
321
|
+
>
|
|
322
|
+
<slot name="item" v-bind="itemSlotPayload(entry)">{{ entry.item.label }}</slot>
|
|
323
|
+
</RouterLink>
|
|
324
|
+
|
|
325
|
+
<a
|
|
326
|
+
v-else-if="entry.item.href"
|
|
327
|
+
:href="entry.item.href"
|
|
328
|
+
class="truncate rounded-md px-1 py-1 font-medium text-muted-fg transition hover:bg-secondary hover:text-canvas-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
329
|
+
:aria-current="isCurrent(entry) ? 'page' : undefined"
|
|
330
|
+
@click="emitNavigate(entry, $event)"
|
|
331
|
+
>
|
|
332
|
+
<slot name="item" v-bind="itemSlotPayload(entry)">{{ entry.item.label }}</slot>
|
|
333
|
+
</a>
|
|
334
|
+
|
|
335
|
+
<button
|
|
336
|
+
v-else
|
|
337
|
+
type="button"
|
|
338
|
+
class="truncate rounded-md px-1 py-1 font-medium text-muted-fg transition hover:bg-secondary hover:text-canvas-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
339
|
+
:aria-current="isCurrent(entry) ? 'page' : undefined"
|
|
340
|
+
@click="emitNavigate(entry, $event)"
|
|
341
|
+
>
|
|
342
|
+
<slot name="item" v-bind="itemSlotPayload(entry)">{{ entry.item.label }}</slot>
|
|
343
|
+
</button>
|
|
344
|
+
</li>
|
|
345
|
+
</ol>
|
|
346
|
+
</nav>
|
|
347
|
+
</template>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, useAttrs } from 'vue';
|
|
3
|
+
|
|
4
|
+
defineOptions({
|
|
5
|
+
inheritAttrs: false,
|
|
6
|
+
__doc: {
|
|
7
|
+
name: 'Button',
|
|
8
|
+
tag: '<DomButton>',
|
|
9
|
+
description: 'The atomic call to action — variants, sizes, loading state, and any underlying element.',
|
|
10
|
+
slots: [
|
|
11
|
+
{ name: '(default)', description: 'Button label and/or icons.' },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const props = defineProps({
|
|
17
|
+
as: {
|
|
18
|
+
type: String,
|
|
19
|
+
default: 'button',
|
|
20
|
+
_edit: { description: "The HTML element or component to render. Use 'router-link' or 'a' to render a link." },
|
|
21
|
+
},
|
|
22
|
+
variant: {
|
|
23
|
+
type: String,
|
|
24
|
+
default: 'primary',
|
|
25
|
+
_edit: {
|
|
26
|
+
options: ['primary', 'secondary', 'ghost', 'danger'],
|
|
27
|
+
description: 'Visual style — pick the role this button plays in the layout.',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
size: {
|
|
31
|
+
type: String,
|
|
32
|
+
default: 'md',
|
|
33
|
+
_edit: {
|
|
34
|
+
options: ['xs', 'sm', 'md', 'lg', 'xl'],
|
|
35
|
+
description: 'Five balanced sizes.',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
loading: {
|
|
39
|
+
type: Boolean,
|
|
40
|
+
default: false,
|
|
41
|
+
_edit: { description: 'Show a spinner and block interaction.' },
|
|
42
|
+
},
|
|
43
|
+
disabled: {
|
|
44
|
+
type: Boolean,
|
|
45
|
+
default: false,
|
|
46
|
+
_edit: { description: 'Disable the button — non-interactive, lowered opacity.' },
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const attrs = useAttrs();
|
|
50
|
+
const interaction = 'select-none active:bg-[linear-gradient(rgb(0_0_0/6%)_0_0)] active:scale-[0.97] active:shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)] motion-reduce:active:scale-100';
|
|
51
|
+
const variants = {
|
|
52
|
+
primary: 'skin-action hover:opacity-90',
|
|
53
|
+
secondary: 'skin-control hover:brightness-[0.98]',
|
|
54
|
+
ghost: 'text-canvas-fg hover:bg-secondary hover:text-secondary-fg',
|
|
55
|
+
danger: 'skin-control text-destructive hover:brightness-[0.98]',
|
|
56
|
+
};
|
|
57
|
+
const sizes = {
|
|
58
|
+
xs: 'h-7 px-2.5 text-xs',
|
|
59
|
+
sm: 'h-8 px-3 text-xs',
|
|
60
|
+
md: 'h-10 px-4 text-sm',
|
|
61
|
+
lg: 'h-12 px-6 text-base',
|
|
62
|
+
xl: 'h-14 px-8 text-lg',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const classes = computed(() => [
|
|
66
|
+
interaction,
|
|
67
|
+
'inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-full font-medium tracking-tight transition',
|
|
68
|
+
'focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2 focus-visible:ring-offset-canvas',
|
|
69
|
+
variants[props.variant] || variants.primary,
|
|
70
|
+
sizes[props.size] || sizes.md,
|
|
71
|
+
(props.disabled || props.loading) && 'opacity-50 pointer-events-none',
|
|
72
|
+
]);
|
|
73
|
+
const disabledState = computed(() => props.disabled || props.loading);
|
|
74
|
+
const isNativeButton = computed(() => props.as === 'button');
|
|
75
|
+
const componentAttrs = computed(() => {
|
|
76
|
+
const { disabled, tabindex, onClick, ...rest } = attrs;
|
|
77
|
+
const dataDisabled = disabledState.value ? '' : rest['data-disabled'];
|
|
78
|
+
if (isNativeButton.value) {
|
|
79
|
+
return {
|
|
80
|
+
...rest,
|
|
81
|
+
'data-disabled': dataDisabled,
|
|
82
|
+
disabled: disabledState.value || undefined,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
...rest,
|
|
87
|
+
'data-disabled': dataDisabled,
|
|
88
|
+
'aria-disabled': disabledState.value ? 'true' : undefined,
|
|
89
|
+
tabindex: disabledState.value ? -1 : tabindex,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Invokes one or more click handlers forwarded through component attributes.
|
|
95
|
+
*
|
|
96
|
+
* @param {Function | Function[] | undefined} handler Forwarded click handler or handler collection.
|
|
97
|
+
* @param {MouseEvent} event Native click event.
|
|
98
|
+
* @returns {void}
|
|
99
|
+
*/
|
|
100
|
+
function callClickHandler(handler, event) {
|
|
101
|
+
if (Array.isArray(handler)) {
|
|
102
|
+
handler.forEach((item) => callClickHandler(item, event));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (typeof handler === 'function') handler(event);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Blocks disabled interactions or forwards an enabled click to the consumer.
|
|
110
|
+
*
|
|
111
|
+
* @param {MouseEvent} event Native click event.
|
|
112
|
+
* @returns {void}
|
|
113
|
+
*/
|
|
114
|
+
function onClick(event) {
|
|
115
|
+
if (disabledState.value) {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
event.stopPropagation();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
callClickHandler(attrs.onClick, event);
|
|
121
|
+
}
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<template>
|
|
125
|
+
<component :is="as" v-bind="componentAttrs" :class="classes" @click="onClick">
|
|
126
|
+
<svg v-if="loading" class="size-4 animate-spin" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
127
|
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" stroke-opacity="0.25" />
|
|
128
|
+
<path d="M22 12a10 10 0 0 1-10 10" stroke="currentColor" stroke-width="3" stroke-linecap="round" />
|
|
129
|
+
</svg>
|
|
130
|
+
<slot />
|
|
131
|
+
</component>
|
|
132
|
+
</template>
|