@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,129 @@
|
|
|
1
|
+
function treeItemActionSchema() {
|
|
2
|
+
return {
|
|
3
|
+
type: 'DomForm',
|
|
4
|
+
properties: {
|
|
5
|
+
value: {
|
|
6
|
+
type: 'string',
|
|
7
|
+
label: 'Value',
|
|
8
|
+
placeholder: 'open',
|
|
9
|
+
},
|
|
10
|
+
label: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
label: 'Label',
|
|
13
|
+
placeholder: 'Open',
|
|
14
|
+
},
|
|
15
|
+
icon: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
label: 'Icon path',
|
|
18
|
+
placeholder: 'SVG path data',
|
|
19
|
+
},
|
|
20
|
+
disabled: {
|
|
21
|
+
type: 'boolean',
|
|
22
|
+
label: 'Disabled',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function treeItemSchema(depth = 3) {
|
|
29
|
+
return {
|
|
30
|
+
type: 'DomForm',
|
|
31
|
+
properties: {
|
|
32
|
+
id: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
label: 'ID',
|
|
35
|
+
placeholder: 'node-id',
|
|
36
|
+
},
|
|
37
|
+
value: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
label: 'Value',
|
|
40
|
+
placeholder: 'node-value',
|
|
41
|
+
},
|
|
42
|
+
label: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
label: 'Label',
|
|
45
|
+
placeholder: 'Node label',
|
|
46
|
+
},
|
|
47
|
+
name: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
label: 'Name',
|
|
50
|
+
placeholder: 'Fallback label',
|
|
51
|
+
},
|
|
52
|
+
type: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
label: 'Node type',
|
|
55
|
+
placeholder: 'file',
|
|
56
|
+
},
|
|
57
|
+
icon: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
label: 'Icon path',
|
|
60
|
+
placeholder: 'SVG path data',
|
|
61
|
+
},
|
|
62
|
+
rightIcon: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
label: 'Right icon',
|
|
65
|
+
placeholder: 'SVG path data',
|
|
66
|
+
},
|
|
67
|
+
rightIconAction: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
label: 'Right icon action',
|
|
70
|
+
},
|
|
71
|
+
rightIconLabel: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
label: 'Right icon label',
|
|
74
|
+
},
|
|
75
|
+
open: {
|
|
76
|
+
type: 'boolean',
|
|
77
|
+
label: 'Open',
|
|
78
|
+
},
|
|
79
|
+
lazy: {
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
label: 'Lazy',
|
|
82
|
+
},
|
|
83
|
+
loading: {
|
|
84
|
+
type: 'boolean',
|
|
85
|
+
label: 'Loading',
|
|
86
|
+
},
|
|
87
|
+
disabled: {
|
|
88
|
+
type: 'boolean',
|
|
89
|
+
label: 'Disabled',
|
|
90
|
+
},
|
|
91
|
+
draggable: {
|
|
92
|
+
type: 'boolean',
|
|
93
|
+
label: 'Draggable',
|
|
94
|
+
},
|
|
95
|
+
acceptsChildren: {
|
|
96
|
+
type: 'boolean',
|
|
97
|
+
label: 'Accept drops',
|
|
98
|
+
default: true,
|
|
99
|
+
},
|
|
100
|
+
slot: {
|
|
101
|
+
type: 'string',
|
|
102
|
+
label: 'Slot',
|
|
103
|
+
placeholder: 'custom-node',
|
|
104
|
+
},
|
|
105
|
+
actions: {
|
|
106
|
+
type: 'array',
|
|
107
|
+
label: 'Actions',
|
|
108
|
+
items: treeItemActionSchema(),
|
|
109
|
+
},
|
|
110
|
+
children: depth > 0
|
|
111
|
+
? {
|
|
112
|
+
type: 'array',
|
|
113
|
+
label: 'Children',
|
|
114
|
+
items: treeItemSchema(depth - 1),
|
|
115
|
+
}
|
|
116
|
+
: {
|
|
117
|
+
type: 'json',
|
|
118
|
+
label: 'Children',
|
|
119
|
+
default: [],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const treeItemsFormSchema = {
|
|
126
|
+
type: 'array',
|
|
127
|
+
label: 'Tree items',
|
|
128
|
+
items: treeItemSchema(),
|
|
129
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the stable value used to address a tree item.
|
|
3
|
+
*
|
|
4
|
+
* The component supports `id`, `value`, or `label` so small examples can stay
|
|
5
|
+
* terse, but production trees should provide stable ids.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} item
|
|
8
|
+
* @returns {string|number|undefined}
|
|
9
|
+
*/
|
|
10
|
+
export function treeItemValue(item) {
|
|
11
|
+
return item?.id ?? item?.value ?? item?.label;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Return the human-readable label for a tree item.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} item
|
|
18
|
+
* @returns {string|number|undefined}
|
|
19
|
+
*/
|
|
20
|
+
export function treeItemLabel(item) {
|
|
21
|
+
return item?.label ?? item?.name ?? treeItemValue(item);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Return children for a nested tree item.
|
|
26
|
+
*
|
|
27
|
+
* @param {object} item
|
|
28
|
+
* @returns {Array}
|
|
29
|
+
*/
|
|
30
|
+
export function treeItemChildren(item) {
|
|
31
|
+
return Array.isArray(item?.children) ? item.children : [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Walk every item in a nested tree.
|
|
36
|
+
*
|
|
37
|
+
* @param {Array} items
|
|
38
|
+
* @param {(item: object) => void} callback
|
|
39
|
+
*/
|
|
40
|
+
export function visitTreeItems(items, callback) {
|
|
41
|
+
for (const item of items || []) {
|
|
42
|
+
callback(item);
|
|
43
|
+
if (Array.isArray(item?.children)) visitTreeItems(item.children, callback);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build the initial open set from a controlled open array, item.open flags,
|
|
49
|
+
* and any previous local open state.
|
|
50
|
+
*
|
|
51
|
+
* @param {Array} items
|
|
52
|
+
* @param {Array<string|number>} controlled
|
|
53
|
+
* @param {Set<string>} previous
|
|
54
|
+
* @returns {Array<string>}
|
|
55
|
+
*/
|
|
56
|
+
export function seedTreeOpenValues(items, controlled = [], previous = new Set()) {
|
|
57
|
+
if (controlled?.length) return controlled.map(String);
|
|
58
|
+
const values = new Set(previous);
|
|
59
|
+
visitTreeItems(items, (item) => {
|
|
60
|
+
if (item?.open) values.add(String(treeItemValue(item)));
|
|
61
|
+
});
|
|
62
|
+
return [...values];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Does the item display as a branch?
|
|
67
|
+
*
|
|
68
|
+
* Lazy items are treated as expandable before their children have loaded.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} item
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
export function treeItemHasChildren(item) {
|
|
74
|
+
return treeItemChildren(item).length > 0 || item?.lazy;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Can this item accept drops as children?
|
|
79
|
+
*
|
|
80
|
+
* `acceptsChildren` can explicitly opt in or out. Without it, only visible
|
|
81
|
+
* branches accept child drops.
|
|
82
|
+
*
|
|
83
|
+
* @param {object} item
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
export function treeItemCanAcceptChildren(item) {
|
|
87
|
+
if (item?.acceptsChildren !== undefined) return item.acceptsChildren !== false;
|
|
88
|
+
return treeItemHasChildren(item);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Flatten a nested tree into visible row records.
|
|
93
|
+
*
|
|
94
|
+
* This is intentionally pure so a store can derive its visible rows without
|
|
95
|
+
* mounting the TreeView component.
|
|
96
|
+
*
|
|
97
|
+
* @param {Array} items
|
|
98
|
+
* @param {Set<string>} openValues
|
|
99
|
+
* @param {number} depth
|
|
100
|
+
* @param {object|null} parent
|
|
101
|
+
* @param {Array<number>} path
|
|
102
|
+
* @returns {Array<object>}
|
|
103
|
+
*/
|
|
104
|
+
export function flattenTreeItems(items, openValues, depth = 1, parent = null, path = []) {
|
|
105
|
+
const out = [];
|
|
106
|
+
(items || []).forEach((item, index) => {
|
|
107
|
+
const value = treeItemValue(item);
|
|
108
|
+
const node = {
|
|
109
|
+
item,
|
|
110
|
+
value,
|
|
111
|
+
label: treeItemLabel(item),
|
|
112
|
+
depth,
|
|
113
|
+
parent,
|
|
114
|
+
path: [...path, index],
|
|
115
|
+
open: openValues.has(String(value)),
|
|
116
|
+
loading: Boolean(item?.loading),
|
|
117
|
+
expandable: treeItemHasChildren(item),
|
|
118
|
+
};
|
|
119
|
+
out.push(node);
|
|
120
|
+
if (node.open && treeItemChildren(item).length) {
|
|
121
|
+
out.push(...flattenTreeItems(treeItemChildren(item), openValues, depth + 1, node, node.path));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Build an id lookup for nested tree data.
|
|
129
|
+
*
|
|
130
|
+
* This is useful for Pinia stores that keep nested data but need fast lookup
|
|
131
|
+
* by id for selection, stage highlighting, or permissions.
|
|
132
|
+
*
|
|
133
|
+
* @param {Array} items
|
|
134
|
+
* @returns {Map<string, { item: object, path: Array<number>, parent: object|null }>}
|
|
135
|
+
*/
|
|
136
|
+
export function indexTreeItems(items) {
|
|
137
|
+
const index = new Map();
|
|
138
|
+
function visit(children, parent = null, path = []) {
|
|
139
|
+
(children || []).forEach((item, itemIndex) => {
|
|
140
|
+
const nextPath = [...path, itemIndex];
|
|
141
|
+
index.set(String(treeItemValue(item)), { item, path: nextPath, parent });
|
|
142
|
+
visit(treeItemChildren(item), item, nextPath);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
visit(items);
|
|
146
|
+
return index;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Clone nested tree items while preserving item payload properties.
|
|
151
|
+
*
|
|
152
|
+
* @param {Array} items
|
|
153
|
+
* @returns {Array}
|
|
154
|
+
*/
|
|
155
|
+
export function cloneTreeItems(items) {
|
|
156
|
+
return (items || []).map((item) => ({
|
|
157
|
+
...item,
|
|
158
|
+
children: Array.isArray(item.children) ? cloneTreeItems(item.children) : item.children,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Move a nested tree item before, after, or inside another item.
|
|
164
|
+
*
|
|
165
|
+
* The original array is not mutated. Stores can call this from their own drop
|
|
166
|
+
* handlers, then decide whether to commit the returned tree.
|
|
167
|
+
*
|
|
168
|
+
* @param {Array} items
|
|
169
|
+
* @param {string|number} sourceValue
|
|
170
|
+
* @param {string|number} targetValue
|
|
171
|
+
* @param {'before'|'inside'|'after'} position
|
|
172
|
+
* @returns {{ items: Array, item: object } | null}
|
|
173
|
+
*/
|
|
174
|
+
export function moveTreeItem(items, sourceValue, targetValue, position) {
|
|
175
|
+
const sourceIndex = indexTreeItems(items).get(String(sourceValue));
|
|
176
|
+
const targetIndex = indexTreeItems(items).get(String(targetValue));
|
|
177
|
+
if (!sourceIndex || !targetIndex) return null;
|
|
178
|
+
if (isSameOrDescendantPath(targetIndex.path, sourceIndex.path)) return null;
|
|
179
|
+
|
|
180
|
+
const next = cloneTreeItems(items);
|
|
181
|
+
const item = removeTreeItemByValue(next, sourceValue);
|
|
182
|
+
if (!item) return null;
|
|
183
|
+
if (!insertTreeItemByValue(next, targetValue, position, item)) return null;
|
|
184
|
+
return { items: next, item };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Is path inside possibleParentPath, or exactly the same item?
|
|
189
|
+
*
|
|
190
|
+
* @param {Array<number>} path
|
|
191
|
+
* @param {Array<number>} possibleParentPath
|
|
192
|
+
* @returns {boolean}
|
|
193
|
+
*/
|
|
194
|
+
export function isSameOrDescendantPath(path, possibleParentPath) {
|
|
195
|
+
if (path.length < possibleParentPath.length) return false;
|
|
196
|
+
return possibleParentPath.every((part, index) => path[index] === part);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function removeTreeItemByValue(items, value) {
|
|
200
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
201
|
+
if (String(treeItemValue(items[index])) === String(value)) {
|
|
202
|
+
return items.splice(index, 1)[0];
|
|
203
|
+
}
|
|
204
|
+
const children = treeItemChildren(items[index]);
|
|
205
|
+
if (children.length) {
|
|
206
|
+
const removed = removeTreeItemByValue(children, value);
|
|
207
|
+
if (removed) return removed;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function insertTreeItemByValue(items, targetValue, position, item) {
|
|
214
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
215
|
+
if (String(treeItemValue(items[index])) === String(targetValue)) {
|
|
216
|
+
if (position === 'inside') {
|
|
217
|
+
if (!Array.isArray(items[index].children)) items[index].children = [];
|
|
218
|
+
items[index].children.push(item);
|
|
219
|
+
} else {
|
|
220
|
+
items.splice(position === 'before' ? index : index + 1, 0, item);
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
const children = treeItemChildren(items[index]);
|
|
225
|
+
if (children.length && insertTreeItemByValue(children, targetValue, position, item)) return true;
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calculates a fixed-size virtual render window for either axis.
|
|
3
|
+
*
|
|
4
|
+
* The returned end index is exclusive so it can be passed directly to
|
|
5
|
+
* `Array.prototype.slice()`. Callers can translate spacer item counts into
|
|
6
|
+
* pixels for rows, columns, weeks, or any other fixed-size collection.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} options Virtual-window inputs.
|
|
9
|
+
* @param {number} options.itemCount Total number of available items.
|
|
10
|
+
* @param {number} options.itemSize Fixed pixel size of one item.
|
|
11
|
+
* @param {number} options.scrollOffset Current scroll offset on the virtualized axis.
|
|
12
|
+
* @param {number} options.viewportSize Visible viewport size on the virtualized axis.
|
|
13
|
+
* @param {number} [options.overscan] Extra items rendered before and after the visible window.
|
|
14
|
+
* @returns {{
|
|
15
|
+
* startIndex: number,
|
|
16
|
+
* endIndex: number,
|
|
17
|
+
* firstVisibleIndex: number,
|
|
18
|
+
* lastVisibleIndex: number,
|
|
19
|
+
* renderedItems: number,
|
|
20
|
+
* totalItems: number,
|
|
21
|
+
* itemSize: number,
|
|
22
|
+
* overscan: number,
|
|
23
|
+
* viewportItems: number,
|
|
24
|
+
* beforeSpacerSize: number,
|
|
25
|
+
* afterSpacerSize: number,
|
|
26
|
+
* }} Normalized render window and spacer measurements.
|
|
27
|
+
*/
|
|
28
|
+
export function calculateVirtualWindow({
|
|
29
|
+
itemCount,
|
|
30
|
+
itemSize,
|
|
31
|
+
scrollOffset,
|
|
32
|
+
viewportSize,
|
|
33
|
+
overscan = 0,
|
|
34
|
+
}) {
|
|
35
|
+
const totalItems = normalizeNonNegativeInteger(itemCount);
|
|
36
|
+
const safeItemSize = normalizePositiveNumber(itemSize, 1);
|
|
37
|
+
const safeScrollOffset = Math.max(0, normalizeFiniteNumber(scrollOffset, 0));
|
|
38
|
+
const safeViewportSize = Math.max(0, normalizeFiniteNumber(viewportSize, 0));
|
|
39
|
+
const safeOverscan = normalizeNonNegativeInteger(overscan);
|
|
40
|
+
const viewportItems = Math.max(1, Math.ceil(safeViewportSize / safeItemSize));
|
|
41
|
+
const firstVisibleIndex = Math.min(
|
|
42
|
+
Math.max(0, totalItems - 1),
|
|
43
|
+
Math.floor(safeScrollOffset / safeItemSize),
|
|
44
|
+
);
|
|
45
|
+
const lastVisibleIndex = totalItems
|
|
46
|
+
? Math.min(totalItems - 1, firstVisibleIndex + viewportItems - 1)
|
|
47
|
+
: -1;
|
|
48
|
+
const startIndex = totalItems ? Math.max(0, firstVisibleIndex - safeOverscan) : 0;
|
|
49
|
+
const endIndex = totalItems
|
|
50
|
+
? Math.min(totalItems, firstVisibleIndex + viewportItems + safeOverscan)
|
|
51
|
+
: 0;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
startIndex,
|
|
55
|
+
endIndex,
|
|
56
|
+
firstVisibleIndex: totalItems ? firstVisibleIndex : 0,
|
|
57
|
+
lastVisibleIndex,
|
|
58
|
+
renderedItems: Math.max(0, endIndex - startIndex),
|
|
59
|
+
totalItems,
|
|
60
|
+
itemSize: safeItemSize,
|
|
61
|
+
overscan: safeOverscan,
|
|
62
|
+
viewportItems,
|
|
63
|
+
beforeSpacerSize: startIndex * safeItemSize,
|
|
64
|
+
afterSpacerSize: Math.max(0, totalItems - endIndex) * safeItemSize,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Normalizes a value as a finite number.
|
|
70
|
+
*
|
|
71
|
+
* @param {unknown} value Candidate numeric value.
|
|
72
|
+
* @param {number} fallback Value returned when the candidate is not finite.
|
|
73
|
+
* @returns {number} Finite numeric value.
|
|
74
|
+
*/
|
|
75
|
+
function normalizeFiniteNumber(value, fallback) {
|
|
76
|
+
const number = Number(value);
|
|
77
|
+
return Number.isFinite(number) ? number : fallback;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Normalizes a value as a positive finite number.
|
|
82
|
+
*
|
|
83
|
+
* @param {unknown} value Candidate numeric value.
|
|
84
|
+
* @param {number} fallback Value returned when the candidate is not positive.
|
|
85
|
+
* @returns {number} Positive finite numeric value.
|
|
86
|
+
*/
|
|
87
|
+
function normalizePositiveNumber(value, fallback) {
|
|
88
|
+
const number = normalizeFiniteNumber(value, fallback);
|
|
89
|
+
return number > 0 ? number : fallback;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Normalizes a value as a non-negative integer.
|
|
94
|
+
*
|
|
95
|
+
* @param {unknown} value Candidate numeric value.
|
|
96
|
+
* @returns {number} Non-negative integer.
|
|
97
|
+
*/
|
|
98
|
+
function normalizeNonNegativeInteger(value) {
|
|
99
|
+
const number = Math.floor(normalizeFiniteNumber(value, 0));
|
|
100
|
+
return Math.max(0, number);
|
|
101
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a value from an application-owned resource row.
|
|
3
|
+
*
|
|
4
|
+
* Data components use the same string-or-function contract so applications can
|
|
5
|
+
* pass ActiveRecord logical field names directly while retaining an escape hatch
|
|
6
|
+
* for computed presentation values.
|
|
7
|
+
*
|
|
8
|
+
* @param {Record<string, unknown>} row Resource row to inspect.
|
|
9
|
+
* @param {string|Function} field Logical field name or custom resolver.
|
|
10
|
+
* @param {number} index Source row index supplied to custom resolvers.
|
|
11
|
+
* @returns {unknown} Resolved row value.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveResourceValue(row, field, index = 0) {
|
|
14
|
+
if (typeof field === 'function') return field(row, index);
|
|
15
|
+
if (!field || typeof field !== 'string') return undefined;
|
|
16
|
+
return row?.[field];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves a stable resource-row key with an index fallback.
|
|
21
|
+
*
|
|
22
|
+
* @param {Record<string, unknown>} row Resource row to identify.
|
|
23
|
+
* @param {string|Function} rowKey Logical key field or custom resolver.
|
|
24
|
+
* @param {number} index Source row index used when no key is available.
|
|
25
|
+
* @returns {string|number} Stable key or the supplied fallback index.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveResourceKey(row, rowKey = 'id', index = 0) {
|
|
28
|
+
return resolveResourceValue(row, rowKey, index) ?? index;
|
|
29
|
+
}
|