@getdom/studio 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE.md +109 -0
  2. package/README.md +44 -0
  3. package/SKILL.md +83 -0
  4. package/package.json +73 -0
  5. package/public/llms.txt +57 -0
  6. package/public/skills.sh +11 -0
  7. package/src/pages/_layout/componentCatalog.generated.js +4514 -0
  8. package/src/pages/_layout/componentInspector.js +60 -0
  9. package/src/pages/_layout/componentManager.js +192 -0
  10. package/src/pages/_layout/componentRuntimeRecords.js +49 -0
  11. package/src/pages/_layout/inspector/componentRegistry.js +639 -0
  12. package/src/pages/_layout/inspector/serialize.js +134 -0
  13. package/src/pages/_layout/inspector/useInspector.js +293 -0
  14. package/src/pages/components/accordion/DomAccordion.vue +39 -0
  15. package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
  16. package/src/pages/components/button/DomButton.vue +132 -0
  17. package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
  18. package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
  19. package/src/pages/components/dialog/DomDialog.vue +271 -0
  20. package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
  21. package/src/pages/components/dialog/DomDialogStack.vue +289 -0
  22. package/src/pages/components/dialog/useDialogs.js +253 -0
  23. package/src/pages/components/drawer/DomDrawer.vue +159 -0
  24. package/src/pages/components/dropdown/DomDropdown.vue +261 -0
  25. package/src/pages/components/icon-button/DomIconButton.vue +155 -0
  26. package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
  27. package/src/pages/components/menu/DomMenu.vue +359 -0
  28. package/src/pages/components/menu/DomMenuItem.vue +233 -0
  29. package/src/pages/components/menu/DomMenuLabel.vue +19 -0
  30. package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
  31. package/src/pages/components/menu/menuContext.js +11 -0
  32. package/src/pages/components/menu/menuPlacement.js +26 -0
  33. package/src/pages/components/menu/submenu.js +58 -0
  34. package/src/pages/components/pagination/DomPagination.vue +265 -0
  35. package/src/pages/components/playground/DomRenderer.vue +48 -0
  36. package/src/pages/components/popover/DomPopover.vue +244 -0
  37. package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
  38. package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
  39. package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
  40. package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
  41. package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
  42. package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
  43. package/src/pages/components/stepper/DomStepper.vue +386 -0
  44. package/src/pages/components/tabs/DomTabs.vue +149 -0
  45. package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
  46. package/src/pages/components/toast/DomToastItem.vue +52 -0
  47. package/src/pages/components/toast/DomToastStack.vue +768 -0
  48. package/src/pages/components/toast/useToasts.js +237 -0
  49. package/src/pages/components/toolbar/DomToolbar.vue +158 -0
  50. package/src/pages/components/tooltip/DomTooltip.vue +69 -0
  51. package/src/pages/components/tooltip/vDomTooltip.js +106 -0
  52. package/src/pages/components/tree-view/DomTreeView.vue +961 -0
  53. package/src/pages/components/tree-view/treeSchemas.js +129 -0
  54. package/src/pages/components/tree-view/treeUtils.js +228 -0
  55. package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
  56. package/src/pages/data/_shared/resourceRecord.js +29 -0
  57. package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
  58. package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
  59. package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
  60. package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
  61. package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
  62. package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
  63. package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
  64. package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
  65. package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
  66. package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
  67. package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
  68. package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
  69. package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
  70. package/src/pages/data/kanban/DomKanban.vue +1703 -0
  71. package/src/pages/data/kanban/kanbanApi.js +80 -0
  72. package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
  73. package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
  74. package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
  75. package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
  76. package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
  77. package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
  78. package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
  79. package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
  80. package/src/pages/forms/_shared/options.js +61 -0
  81. package/src/pages/forms/_shared/useOptionMenu.js +59 -0
  82. package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
  83. package/src/pages/forms/calendar/DomCalendar.vue +442 -0
  84. package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
  85. package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
  86. package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
  87. package/src/pages/forms/color-input/DomColorInput.vue +118 -0
  88. package/src/pages/forms/combobox/DomCombobox.vue +237 -0
  89. package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
  90. package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
  91. package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
  92. package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
  93. package/src/pages/forms/field/DomField.vue +267 -0
  94. package/src/pages/forms/field/fieldLayout.js +1 -0
  95. package/src/pages/forms/field/fieldProps.js +114 -0
  96. package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
  97. package/src/pages/forms/field/useField.js +343 -0
  98. package/src/pages/forms/field/validators.js +364 -0
  99. package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
  100. package/src/pages/forms/form/DomForm.vue +985 -0
  101. package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
  102. package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
  103. package/src/pages/forms/form/adapters/shared.js +53 -0
  104. package/src/pages/forms/form/adapters/zod.js +235 -0
  105. package/src/pages/forms/form/formApi.js +38 -0
  106. package/src/pages/forms/form/formDefinition.js +392 -0
  107. package/src/pages/forms/form/schemaAdapter.js +54 -0
  108. package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
  109. package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
  110. package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
  111. package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
  112. package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
  113. package/src/pages/forms/listbox/DomListbox.vue +105 -0
  114. package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
  115. package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
  116. package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
  117. package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
  118. package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
  119. package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
  120. package/src/pages/forms/password-strength/passwordStrength.js +92 -0
  121. package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
  122. package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
  123. package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
  124. package/src/pages/forms/select/DomSelect.vue +479 -0
  125. package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
  126. package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
  127. package/src/pages/forms/text-input/DomTextInput.vue +48 -0
  128. package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
  129. package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
  130. package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
  131. package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
  132. package/src/pages/forms/time-selector/timeUtils.js +194 -0
  133. package/src/pages/forms/toggle/DomToggle.vue +158 -0
  134. package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
  135. package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
  136. package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
  137. package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
  138. package/src/pages/lib/css/color.js +245 -0
  139. package/src/pages/lib/headless/accordion.d.ts +7 -0
  140. package/src/pages/lib/headless/accordion.js +246 -0
  141. package/src/pages/lib/headless/autocomplete.d.ts +4 -0
  142. package/src/pages/lib/headless/autocomplete.js +33 -0
  143. package/src/pages/lib/headless/base.d.ts +25 -0
  144. package/src/pages/lib/headless/base.js +127 -0
  145. package/src/pages/lib/headless/checkbox.d.ts +19 -0
  146. package/src/pages/lib/headless/checkbox.js +99 -0
  147. package/src/pages/lib/headless/combobox.d.ts +7 -0
  148. package/src/pages/lib/headless/combobox.js +488 -0
  149. package/src/pages/lib/headless/dialog.d.ts +23 -0
  150. package/src/pages/lib/headless/dialog.js +384 -0
  151. package/src/pages/lib/headless/drawer.d.ts +21 -0
  152. package/src/pages/lib/headless/drawer.js +682 -0
  153. package/src/pages/lib/headless/dropdown.d.ts +19 -0
  154. package/src/pages/lib/headless/dropdown.js +412 -0
  155. package/src/pages/lib/headless/floating-effect.js +118 -0
  156. package/src/pages/lib/headless/floating.d.ts +34 -0
  157. package/src/pages/lib/headless/floating.js +382 -0
  158. package/src/pages/lib/headless/index.d.ts +19 -0
  159. package/src/pages/lib/headless/index.js +19 -0
  160. package/src/pages/lib/headless/listbox.d.ts +19 -0
  161. package/src/pages/lib/headless/listbox.js +126 -0
  162. package/src/pages/lib/headless/menu.d.ts +18 -0
  163. package/src/pages/lib/headless/menu.js +117 -0
  164. package/src/pages/lib/headless/popover-panel.d.ts +18 -0
  165. package/src/pages/lib/headless/popover-panel.js +309 -0
  166. package/src/pages/lib/headless/popover.d.ts +19 -0
  167. package/src/pages/lib/headless/popover.js +391 -0
  168. package/src/pages/lib/headless/radio-group.d.ts +19 -0
  169. package/src/pages/lib/headless/radio-group.js +100 -0
  170. package/src/pages/lib/headless/tabs.d.ts +18 -0
  171. package/src/pages/lib/headless/tabs.js +119 -0
  172. package/src/pages/lib/headless/toast.d.ts +13 -0
  173. package/src/pages/lib/headless/toast.js +65 -0
  174. package/src/pages/lib/headless/toggle.d.ts +19 -0
  175. package/src/pages/lib/headless/toggle.js +162 -0
  176. package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
  177. package/src/pages/lib/headless/tooltip-controller.js +549 -0
  178. package/src/pages/lib/headless/tooltip.d.ts +19 -0
  179. package/src/pages/lib/headless/tooltip.js +153 -0
  180. package/src/pages/lib/headless/transitions.d.ts +5 -0
  181. package/src/pages/lib/headless/transitions.js +89 -0
  182. package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
  183. package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
  184. package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
  185. package/src/pages/lib/vue/codeHighlighter.js +132 -0
  186. package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
  187. package/src/pages/lib/vue/index.d.ts +6806 -0
  188. package/src/pages/lib/vue/index.js +218 -0
  189. package/src/pages/lib/vue/useTheme.js +116 -0
  190. package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
  191. package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
  192. package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
  193. package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
  194. package/src/pages/mobile/shell/DomAppShell.vue +51 -0
  195. package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
  196. package/src/pages/mobile/stack/DomAppStack.vue +711 -0
  197. package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
  198. package/src/pages/visual/alert/DomAlert.vue +363 -0
  199. package/src/pages/visual/avatar/DomAvatar.vue +110 -0
  200. package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
  201. package/src/pages/visual/badge/DomBadge.vue +105 -0
  202. package/src/pages/visual/card/DomCard.vue +65 -0
  203. package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
  204. package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
  205. package/src/pages/visual/progress/DomProgress.vue +212 -0
  206. package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
  207. package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
  208. package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
  209. package/src/pages/visual/spinner/DomSpinner.vue +122 -0
  210. package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
  211. package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
  212. package/src/pages/visual/text-diff/textDiff.js +300 -0
  213. package/src/style.css +446 -0
@@ -0,0 +1,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
+ }