@flux-ui/components 3.0.0-next.57 → 3.0.0-next.59

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 (306) hide show
  1. package/dist/component/FluxButtonStack.vue.d.ts +1 -1
  2. package/dist/component/FluxCalendar.vue.d.ts +63 -2
  3. package/dist/component/{FluxCalendarEvent.vue.d.ts → FluxCalendarItem.vue.d.ts} +9 -7
  4. package/dist/component/FluxClickablePane.vue.d.ts +1 -1
  5. package/dist/component/FluxDivider.vue.d.ts +1 -1
  6. package/dist/component/FluxFilter.vue.d.ts +1 -1
  7. package/dist/component/FluxFilterBar.vue.d.ts +1 -1
  8. package/dist/component/FluxFormInput.vue.d.ts +2 -2
  9. package/dist/component/FluxFormInputAddition.vue.d.ts +1 -1
  10. package/dist/component/FluxFormTreeViewSelect.vue.d.ts +3 -3
  11. package/dist/component/FluxGallery.vue.d.ts +1 -1
  12. package/dist/component/FluxInfo.vue.d.ts +1 -1
  13. package/dist/component/FluxKanban.vue.d.ts +17 -5
  14. package/dist/component/FluxKanbanColumn.vue.d.ts +13 -2
  15. package/dist/component/{FluxKanbanCard.vue.d.ts → FluxKanbanItem.vue.d.ts} +2 -1
  16. package/dist/component/FluxMenuItem.vue.d.ts +1 -1
  17. package/dist/component/FluxNotice.vue.d.ts +1 -1
  18. package/dist/component/FluxPaneHeader.vue.d.ts +1 -1
  19. package/dist/component/FluxPressable.vue.d.ts +1 -1
  20. package/dist/component/FluxPrompt.vue.d.ts +3 -3
  21. package/dist/component/FluxSplitButton.vue.d.ts +1 -1
  22. package/dist/component/FluxStack.vue.d.ts +1 -1
  23. package/dist/component/FluxTab.vue.d.ts +1 -1
  24. package/dist/component/FluxTabBar.vue.d.ts +4 -1
  25. package/dist/component/FluxTabs.vue.d.ts +4 -1
  26. package/dist/component/FluxTimelineItem.vue.d.ts +1 -1
  27. package/dist/component/calendar/FluxCalendarItemDisplay.vue.d.ts +9 -0
  28. package/dist/component/calendar/FluxCalendarMonthView.vue.d.ts +19 -0
  29. package/dist/component/calendar/FluxCalendarTimeGridView.vue.d.ts +36 -0
  30. package/dist/component/calendar/index.d.ts +3 -0
  31. package/dist/component/index.d.ts +2 -2
  32. package/dist/component/primitive/DialogLayout.vue.d.ts +1 -1
  33. package/dist/component/primitive/SelectBase.vue.d.ts +3 -3
  34. package/dist/composable/index.d.ts +3 -0
  35. package/dist/composable/private/index.d.ts +5 -8
  36. package/dist/composable/private/useAsyncFilterOptions.d.ts +1 -1
  37. package/dist/composable/private/useKanban.d.ts +14 -4
  38. package/dist/composable/private/useKanbanAutoScroll.d.ts +13 -0
  39. package/dist/composable/useCalendarInjection.d.ts +2 -0
  40. package/dist/composable/useKanbanInjection.d.ts +2 -0
  41. package/dist/composable/useTabBarInjection.d.ts +2 -0
  42. package/dist/data/di.d.ts +80 -9
  43. package/dist/data/i18n.d.ts +4 -0
  44. package/dist/index.css +1603 -1204
  45. package/dist/index.js +4567 -2819
  46. package/dist/index.js.map +1 -1
  47. package/package.json +6 -6
  48. package/src/component/FluxAction.vue +1 -1
  49. package/src/component/FluxActionBar.vue +2 -2
  50. package/src/component/FluxActionPane.vue +1 -1
  51. package/src/component/FluxAdaptiveGroup.vue +2 -2
  52. package/src/component/FluxAdaptiveSlot.vue +2 -2
  53. package/src/component/FluxAlert.vue +1 -1
  54. package/src/component/FluxAnimatedColors.vue +1 -1
  55. package/src/component/FluxAspectRatio.vue +1 -1
  56. package/src/component/FluxAvatar.vue +2 -2
  57. package/src/component/FluxBadge.vue +2 -2
  58. package/src/component/FluxBorderShine.vue +1 -1
  59. package/src/component/FluxBoxedIcon.vue +1 -1
  60. package/src/component/FluxButton.vue +2 -2
  61. package/src/component/FluxButtonGroup.vue +1 -1
  62. package/src/component/FluxButtonStack.vue +1 -1
  63. package/src/component/FluxCalendar.vue +588 -179
  64. package/src/component/FluxCalendarItem.vue +79 -0
  65. package/src/component/FluxCheckbox.vue +2 -2
  66. package/src/component/FluxChip.vue +2 -2
  67. package/src/component/FluxClickablePane.vue +2 -2
  68. package/src/component/FluxColorPicker.vue +2 -2
  69. package/src/component/FluxColorSelect.vue +3 -3
  70. package/src/component/FluxColumn.vue +1 -1
  71. package/src/component/FluxCommandPalette.vue +4 -4
  72. package/src/component/FluxCommandPaletteGroup.vue +1 -1
  73. package/src/component/FluxCommandPaletteItem.vue +1 -1
  74. package/src/component/FluxComment.vue +2 -2
  75. package/src/component/FluxConfirm.vue +1 -1
  76. package/src/component/FluxContainer.vue +1 -1
  77. package/src/component/FluxDataTable.vue +1 -2
  78. package/src/component/FluxDatePicker.vue +3 -3
  79. package/src/component/FluxDestructiveButton.vue +1 -1
  80. package/src/component/FluxDisabled.vue +1 -1
  81. package/src/component/FluxDivider.vue +2 -2
  82. package/src/component/FluxDotPattern.vue +1 -1
  83. package/src/component/FluxDropZone.vue +4 -4
  84. package/src/component/FluxExpandable.vue +3 -3
  85. package/src/component/FluxExpandableGroup.vue +2 -2
  86. package/src/component/FluxFader.vue +1 -1
  87. package/src/component/FluxFaderItem.vue +1 -1
  88. package/src/component/FluxFilter.vue +2 -2
  89. package/src/component/FluxFilterBar.vue +5 -6
  90. package/src/component/FluxFilterBase.vue +2 -2
  91. package/src/component/FluxFilterDate.vue +2 -2
  92. package/src/component/FluxFilterDateRange.vue +2 -2
  93. package/src/component/FluxFilterOption.vue +2 -2
  94. package/src/component/FluxFilterOptionAsync.vue +1 -1
  95. package/src/component/FluxFilterOptions.vue +2 -2
  96. package/src/component/FluxFilterOptionsAsync.vue +1 -1
  97. package/src/component/FluxFilterRange.vue +2 -2
  98. package/src/component/FluxFilterWindow.vue +3 -3
  99. package/src/component/FluxFlickeringGrid.vue +1 -1
  100. package/src/component/FluxFlyout.vue +2 -2
  101. package/src/component/FluxFocalPointEditor.vue +3 -3
  102. package/src/component/FluxFocalPointImage.vue +1 -1
  103. package/src/component/FluxForm.vue +1 -1
  104. package/src/component/FluxFormColumn.vue +1 -1
  105. package/src/component/FluxFormDateInput.vue +3 -3
  106. package/src/component/FluxFormDateRangeInput.vue +4 -4
  107. package/src/component/FluxFormDateTimeInput.vue +3 -3
  108. package/src/component/FluxFormField.vue +3 -3
  109. package/src/component/FluxFormFieldAddition.vue +1 -1
  110. package/src/component/FluxFormGrid.vue +1 -1
  111. package/src/component/FluxFormInput.vue +3 -3
  112. package/src/component/FluxFormInputAddition.vue +2 -2
  113. package/src/component/FluxFormInputGroup.vue +2 -2
  114. package/src/component/FluxFormPinInput.vue +3 -3
  115. package/src/component/FluxFormRangeSlider.vue +3 -3
  116. package/src/component/FluxFormRow.vue +1 -1
  117. package/src/component/FluxFormSection.vue +1 -1
  118. package/src/component/FluxFormSelect.vue +4 -4
  119. package/src/component/FluxFormSelectAsync.vue +6 -6
  120. package/src/component/FluxFormSlider.vue +2 -2
  121. package/src/component/FluxFormTextArea.vue +2 -2
  122. package/src/component/FluxFormTimeZonePicker.vue +4 -4
  123. package/src/component/FluxFormTreeViewSelect.vue +9 -12
  124. package/src/component/FluxGallery.vue +3 -3
  125. package/src/component/FluxGalleryItem.vue +1 -1
  126. package/src/component/FluxGrid.vue +1 -1
  127. package/src/component/FluxGridColumn.vue +1 -1
  128. package/src/component/FluxGridPattern.vue +1 -1
  129. package/src/component/FluxIcon.vue +2 -2
  130. package/src/component/FluxInfo.vue +2 -2
  131. package/src/component/FluxItem.vue +1 -1
  132. package/src/component/FluxItemActions.vue +1 -1
  133. package/src/component/FluxItemContent.vue +1 -1
  134. package/src/component/FluxItemMedia.vue +1 -1
  135. package/src/component/FluxItemStack.vue +1 -1
  136. package/src/component/FluxKanban.vue +78 -7
  137. package/src/component/FluxKanbanColumn.vue +225 -32
  138. package/src/component/FluxKanbanItem.vue +162 -0
  139. package/src/component/FluxLayerPane.vue +1 -1
  140. package/src/component/FluxLayerPaneSecondary.vue +1 -1
  141. package/src/component/FluxLegend.vue +1 -1
  142. package/src/component/FluxLink.vue +1 -1
  143. package/src/component/FluxMenu.vue +1 -1
  144. package/src/component/FluxMenuCollapsible.vue +3 -3
  145. package/src/component/FluxMenuGroup.vue +1 -1
  146. package/src/component/FluxMenuItem.vue +2 -2
  147. package/src/component/FluxMenuSubHeader.vue +1 -1
  148. package/src/component/FluxMenuTitle.vue +1 -1
  149. package/src/component/FluxNotice.vue +3 -3
  150. package/src/component/FluxOverflowBar.vue +1 -1
  151. package/src/component/FluxOverlay.vue +3 -3
  152. package/src/component/FluxOverlayProvider.vue +2 -2
  153. package/src/component/FluxPagination.vue +3 -3
  154. package/src/component/FluxPaginationBar.vue +2 -2
  155. package/src/component/FluxPaginationButton.vue +1 -1
  156. package/src/component/FluxPane.vue +1 -1
  157. package/src/component/FluxPaneBody.vue +1 -1
  158. package/src/component/FluxPaneFooter.vue +1 -1
  159. package/src/component/FluxPaneGroup.vue +1 -1
  160. package/src/component/FluxPaneHeader.vue +2 -2
  161. package/src/component/FluxPaneIllustration.vue +1 -1
  162. package/src/component/FluxPaneMedia.vue +1 -1
  163. package/src/component/FluxPercentageBar.vue +1 -1
  164. package/src/component/FluxPersona.vue +1 -1
  165. package/src/component/FluxPlaceholder.vue +1 -1
  166. package/src/component/FluxPressable.vue +1 -1
  167. package/src/component/FluxPrimaryButton.vue +1 -1
  168. package/src/component/FluxPrimaryLinkButton.vue +1 -1
  169. package/src/component/FluxProgressBar.vue +2 -2
  170. package/src/component/FluxPrompt.vue +1 -1
  171. package/src/component/FluxPublishButton.vue +1 -1
  172. package/src/component/FluxQuantitySelector.vue +2 -2
  173. package/src/component/FluxRemove.vue +2 -2
  174. package/src/component/FluxRoot.vue +2 -2
  175. package/src/component/FluxRow.vue +1 -1
  176. package/src/component/FluxSecondaryButton.vue +1 -1
  177. package/src/component/FluxSecondaryLinkButton.vue +1 -1
  178. package/src/component/FluxSegmentedControl.vue +1 -1
  179. package/src/component/FluxSeparator.vue +1 -1
  180. package/src/component/FluxSlideOver.vue +3 -3
  181. package/src/component/FluxSnackbar.vue +2 -2
  182. package/src/component/FluxSnackbarProvider.vue +2 -2
  183. package/src/component/FluxSpacer.vue +1 -1
  184. package/src/component/FluxSpinner.vue +1 -1
  185. package/src/component/FluxSplitButton.vue +2 -2
  186. package/src/component/FluxStack.vue +2 -2
  187. package/src/component/FluxStatistic.vue +2 -2
  188. package/src/component/FluxStepper.vue +1 -1
  189. package/src/component/FluxStepperStep.vue +1 -1
  190. package/src/component/FluxStepperSteps.vue +2 -2
  191. package/src/component/FluxTab.vue +2 -2
  192. package/src/component/FluxTabBar.vue +75 -7
  193. package/src/component/FluxTabBarItem.vue +35 -4
  194. package/src/component/FluxTable.vue +3 -4
  195. package/src/component/FluxTableActions.vue +1 -1
  196. package/src/component/FluxTableBar.vue +3 -3
  197. package/src/component/FluxTableCell.vue +3 -3
  198. package/src/component/FluxTableHeader.vue +3 -3
  199. package/src/component/FluxTableRow.vue +1 -1
  200. package/src/component/FluxTabs.vue +10 -4
  201. package/src/component/FluxTag.vue +2 -2
  202. package/src/component/FluxTicks.vue +1 -1
  203. package/src/component/FluxTimeline.vue +1 -1
  204. package/src/component/FluxTimelineItem.vue +2 -2
  205. package/src/component/FluxToggle.vue +2 -2
  206. package/src/component/FluxToolbar.vue +2 -2
  207. package/src/component/FluxTooltip.vue +1 -1
  208. package/src/component/FluxTooltipProvider.vue +3 -3
  209. package/src/component/FluxTreeView.vue +3 -4
  210. package/src/component/FluxWindow.vue +1 -1
  211. package/src/component/calendar/FluxCalendarItemDisplay.vue +117 -0
  212. package/src/component/calendar/FluxCalendarMonthView.vue +134 -0
  213. package/src/component/calendar/FluxCalendarTimeGridView.vue +550 -0
  214. package/src/component/calendar/index.ts +3 -0
  215. package/src/component/index.ts +2 -2
  216. package/src/component/primitive/CoordinatePicker.vue +2 -2
  217. package/src/component/primitive/CoordinatePickerThumb.vue +2 -2
  218. package/src/component/primitive/DialogLayout.vue +1 -1
  219. package/src/component/primitive/FilterBadge.vue +2 -2
  220. package/src/component/primitive/FilterItem.vue +1 -1
  221. package/src/component/primitive/FilterMenuRenderer.ts +2 -2
  222. package/src/component/primitive/FilterOptionBase.vue +6 -6
  223. package/src/component/primitive/SelectBase.vue +13 -13
  224. package/src/component/primitive/SliderBase.vue +3 -3
  225. package/src/component/primitive/SliderThumb.vue +2 -2
  226. package/src/component/primitive/SliderTrack.vue +1 -1
  227. package/src/component/primitive/TreeNodeRenderer.vue +3 -4
  228. package/src/component/primitive/VNodeRenderer.ts +1 -2
  229. package/src/composable/index.ts +3 -0
  230. package/src/composable/private/index.ts +5 -8
  231. package/src/composable/private/useAsyncFilterOptions.ts +9 -15
  232. package/src/composable/private/useCommandPalette.ts +2 -2
  233. package/src/composable/private/useDateFlyout.ts +1 -1
  234. package/src/composable/private/useDropdownPopup.ts +1 -1
  235. package/src/composable/private/useFilterOption.ts +2 -2
  236. package/src/composable/private/useFormSelect.ts +2 -3
  237. package/src/composable/private/useKanban.ts +528 -25
  238. package/src/composable/private/useKanbanAutoScroll.ts +95 -0
  239. package/src/composable/private/useTranslate.ts +1 -2
  240. package/src/composable/private/useTreeView.ts +1 -1
  241. package/src/composable/useAdaptiveGroupInjection.ts +1 -1
  242. package/src/composable/useBreakpoints.ts +1 -2
  243. package/src/composable/useCalendarInjection.ts +6 -0
  244. package/src/composable/useDisabled.ts +1 -2
  245. package/src/composable/useDisabledInjection.ts +1 -1
  246. package/src/composable/useExpandableGroupInjection.ts +1 -1
  247. package/src/composable/useFilterInjection.ts +1 -1
  248. package/src/composable/useFlyoutInjection.ts +1 -1
  249. package/src/composable/useFormFieldInjection.ts +1 -1
  250. package/src/composable/useKanbanInjection.ts +12 -0
  251. package/src/composable/useTabBarInjection.ts +10 -0
  252. package/src/composable/useTableInjection.ts +1 -1
  253. package/src/composable/useTooltipInjection.ts +1 -1
  254. package/src/css/component/Action.module.scss +1 -1
  255. package/src/css/component/AdaptiveSlot.module.scss +5 -7
  256. package/src/css/component/Avatar.module.scss +1 -1
  257. package/src/css/component/Badge.module.scss +1 -1
  258. package/src/css/component/Button.module.scss +1 -1
  259. package/src/css/component/Calendar.module.scss +274 -44
  260. package/src/css/component/Chip.module.scss +1 -1
  261. package/src/css/component/Color.module.scss +1 -1
  262. package/src/css/component/CommandPalette.module.scss +1 -1
  263. package/src/css/component/DatePicker.module.scss +1 -1
  264. package/src/css/component/Expandable.module.scss +1 -1
  265. package/src/css/component/Form.module.scss +1 -1
  266. package/src/css/component/Gallery.module.scss +1 -1
  267. package/src/css/component/Grid.module.scss +1 -1
  268. package/src/css/component/Item.module.scss +1 -1
  269. package/src/css/component/Kanban.module.scss +189 -0
  270. package/src/css/component/Layout.module.scss +1 -1
  271. package/src/css/component/Menu.module.scss +1 -1
  272. package/src/css/component/Notice.module.scss +1 -1
  273. package/src/css/component/Overlay.module.scss +4 -4
  274. package/src/css/component/Pagination.module.scss +1 -1
  275. package/src/css/component/Pane.module.scss +2 -1
  276. package/src/css/component/Placeholder.module.scss +1 -1
  277. package/src/css/component/Remove.module.scss +1 -1
  278. package/src/css/component/SegmentedControl.module.scss +1 -1
  279. package/src/css/component/Snackbar.module.scss +1 -1
  280. package/src/css/component/Tab.module.scss +138 -34
  281. package/src/css/component/Table.module.scss +1 -1
  282. package/src/css/component/TreeView.module.scss +1 -1
  283. package/src/css/component/TreeViewSelect.module.scss +1 -1
  284. package/src/css/component/base/Button.module.scss +1 -1
  285. package/src/css/component/primitive/Slider.module.scss +1 -1
  286. package/src/css/component/primitive/TreeNode.module.scss +1 -1
  287. package/src/css/variables.scss +2 -2
  288. package/src/data/di.ts +94 -8
  289. package/src/data/filter.ts +4 -4
  290. package/src/data/i18n.ts +4 -0
  291. package/src/data/store.ts +1 -2
  292. package/src/transition/FluxBreakthroughTransition.vue +1 -1
  293. package/src/transition/FluxFadeTransition.vue +1 -1
  294. package/src/transition/FluxOverlayTransition.vue +1 -1
  295. package/src/transition/FluxRouteTransition.vue +1 -1
  296. package/src/transition/FluxSlideOverTransition.vue +1 -1
  297. package/src/transition/FluxTooltipTransition.vue +1 -1
  298. package/src/transition/FluxVerticalWindowTransition.vue +1 -1
  299. package/src/transition/FluxWindowTransition.vue +1 -1
  300. package/src/util/createDialogRenderer.ts +3 -4
  301. package/src/util/createLabelForDateRange.ts +1 -1
  302. package/src/component/FluxCalendarEvent.vue +0 -42
  303. package/src/component/FluxKanbanCard.vue +0 -105
  304. package/src/css/component/FluxKanban.module.scss +0 -7
  305. package/src/css/component/FluxKanbanCard.module.scss +0 -35
  306. package/src/css/component/FluxKanbanColumn.module.scss +0 -49
@@ -1,41 +1,238 @@
1
- import type { FluxKanbanMoveEvent } from '@flux-ui/types';
2
- import { ref } from 'vue';
3
- import type { FluxKanbanDragState, FluxKanbanInjection } from '$flux/data/di';
1
+ import type { FluxKanbanMoveColumnEvent, FluxKanbanMoveEvent } from '@flux-ui/types';
2
+ import { computed, ref, unref, type Ref } from 'vue';
3
+ import type { FluxKanbanColumnDragState, FluxKanbanDragState, FluxKanbanInjection, FluxKanbanKeyboardDirection } from '~flux/components/data';
4
+ import { useKanbanAutoScroll } from './useKanbanAutoScroll';
5
+
6
+ export type UseKanbanOptions = {
7
+ readonly disabled: Ref<boolean>;
8
+ readonly reorderableColumns: Ref<boolean>;
9
+ readonly canMove?: Ref<((event: FluxKanbanMoveEvent) => boolean) | undefined>;
10
+ readonly onMove: (event: FluxKanbanMoveEvent) => void;
11
+ readonly onMoveColumn: (event: FluxKanbanMoveColumnEvent) => void;
12
+ readonly onAnnounce: (message: string) => void;
13
+ };
14
+
15
+ const DRAG_LEAVE_GRACE_MS = 50;
16
+
17
+ const WITHIN_COLUMN_DELTA: Record<'up' | 'down', -1 | 1> = {up: -1, down: 1};
18
+ const ACROSS_COLUMN_DELTA: Record<'left' | 'right', -1 | 1> = {left: -1, right: 1};
4
19
 
5
20
  /**
6
21
  * Internal composable for managing kanban drag-and-drop state.
7
- * Provides card registration, drag tracking, and drop target management.
22
+ * Provides item registration, drag tracking, drop target management,
23
+ * keyboard drag-and-drop, column reordering, drop validation and auto-scroll.
8
24
  */
9
- export function useKanban(onMove: (event: FluxKanbanMoveEvent) => void): FluxKanbanInjection {
25
+ export function useKanban(options: UseKanbanOptions): FluxKanbanInjection {
10
26
  const dragState = ref<FluxKanbanDragState | null>(null);
11
- const cardRegistry = new WeakMap<Element, { readonly cardId: string | number }>();
27
+ const columnDragState = ref<FluxKanbanColumnDragState | null>(null);
28
+ const isOverColumnId = ref<string | number | null>(null);
29
+
30
+ const itemRegistry = new WeakMap<Element, { readonly itemId: string | number }>();
31
+ const itemElementsById = new Map<string | number, Element>();
32
+ const columnRegistry = new WeakMap<Element, { readonly columnId: string | number }>();
33
+ const columnElementsById = new Map<string | number, Element>();
34
+ const columnBodyById = new Map<string | number, Element>();
35
+ const dragEnterCounts = new Map<string | number, number>();
36
+
37
+ let boardElement: Element | null = null;
38
+ let clearTimer: ReturnType<typeof setTimeout> | null = null;
39
+
40
+ const autoScroll = useKanbanAutoScroll({
41
+ getBoardElement: () => boardElement,
42
+ getVerticalTarget: () => {
43
+ const state = unref(dragState);
44
+ return state && state.dropColumnId !== null ? columnBodyById.get(state.dropColumnId) ?? null : null;
45
+ }
46
+ });
47
+
48
+ const grabbedId = computed<string | number | null>(() => {
49
+ const state = unref(dragState);
50
+ return state !== null && state.mode === 'keyboard' ? state.itemId : null;
51
+ });
52
+
53
+ const currentMoveEvent = computed<FluxKanbanMoveEvent | null>(() => tryBuildMoveEvent(unref(dragState)));
54
+
55
+ const isDropAllowed = computed(() => {
56
+ const event = unref(currentMoveEvent);
57
+ return event === null || validateMove(event);
58
+ });
59
+
60
+ function tryBuildMoveEvent(state: FluxKanbanDragState | null): FluxKanbanMoveEvent | null {
61
+ if (!state || state.dropColumnId === null) {
62
+ return null;
63
+ }
64
+
65
+ return {
66
+ itemId: state.itemId,
67
+ fromColumnId: state.fromColumnId,
68
+ toColumnId: state.dropColumnId,
69
+ beforeItemId: state.beforeItemId ?? undefined
70
+ };
71
+ }
72
+
73
+ function validateMove(event: FluxKanbanMoveEvent): boolean {
74
+ const validate = unref(options.canMove);
75
+ return validate ? validate(event) : true;
76
+ }
77
+
78
+ function clearTimerIfAny(): void {
79
+ if (clearTimer !== null) {
80
+ clearTimeout(clearTimer);
81
+ clearTimer = null;
82
+ }
83
+ }
84
+
85
+ function isSelfDrop(state: FluxKanbanDragState): boolean {
86
+ return state.fromColumnId === state.dropColumnId
87
+ && state.beforeItemId === (state.originBeforeItemId ?? null);
88
+ }
89
+
90
+ function getColumnIndex(columnId: string | number): number {
91
+ if (!boardElement) {
92
+ return -1;
93
+ }
94
+
95
+ const columns = Array.from(boardElement.children).filter(child => columnRegistry.has(child));
96
+ return columns.findIndex(elm => columnRegistry.get(elm)?.columnId === columnId);
97
+ }
98
+
99
+ function getColumnByIndex(index: number): { readonly columnId: string | number } | null {
100
+ if (!boardElement) {
101
+ return null;
102
+ }
103
+
104
+ const columns = Array.from(boardElement.children).filter(child => columnRegistry.has(child));
105
+ const elm = columns[index];
106
+
107
+ return elm ? columnRegistry.get(elm)! : null;
108
+ }
109
+
110
+ function getItemsInColumn(columnId: string | number): (string | number)[] {
111
+ const body = columnBodyById.get(columnId);
112
+
113
+ if (!body) {
114
+ return [];
115
+ }
116
+
117
+ return Array.from(body.children)
118
+ .map(child => itemRegistry.get(child)?.itemId)
119
+ .filter((id): id is string | number => id !== undefined);
120
+ }
121
+
122
+ function registerItem(element: Element, itemId: string | number): void {
123
+ itemRegistry.set(element, {itemId});
124
+ itemElementsById.set(itemId, element);
125
+ }
126
+
127
+ function unregisterItem(element: Element): void {
128
+ const info = itemRegistry.get(element);
129
+
130
+ if (info) {
131
+ itemElementsById.delete(info.itemId);
132
+ }
133
+
134
+ itemRegistry.delete(element);
135
+ }
136
+
137
+ function getItemInfo(element: Element): { readonly itemId: string | number } | undefined {
138
+ return itemRegistry.get(element);
139
+ }
140
+
141
+ function registerColumn(element: Element, columnId: string | number): void {
142
+ columnRegistry.set(element, {columnId});
143
+ columnElementsById.set(columnId, element);
144
+ }
12
145
 
13
- function registerCard(element: Element, cardId: string | number): void {
14
- cardRegistry.set(element, {cardId});
146
+ function unregisterColumn(element: Element): void {
147
+ const info = columnRegistry.get(element);
148
+
149
+ if (info) {
150
+ columnElementsById.delete(info.columnId);
151
+ columnBodyById.delete(info.columnId);
152
+ dragEnterCounts.delete(info.columnId);
153
+
154
+ if (isOverColumnId.value === info.columnId) {
155
+ isOverColumnId.value = null;
156
+ }
157
+ }
158
+
159
+ columnRegistry.delete(element);
15
160
  }
16
161
 
17
- function unregisterCard(element: Element): void {
18
- cardRegistry.delete(element);
162
+ function getColumnInfo(element: Element): { readonly columnId: string | number } | undefined {
163
+ return columnRegistry.get(element);
19
164
  }
20
165
 
21
- function getCardInfo(element: Element): { readonly cardId: string | number } | undefined {
22
- return cardRegistry.get(element);
166
+ function setBoardElement(element: Element | null): void {
167
+ boardElement = element;
23
168
  }
24
169
 
25
- function startDrag(cardId: string | number, fromColumnId: string | number): void {
26
- dragState.value = {cardId, fromColumnId, dropColumnId: null, beforeCardId: null};
170
+ function setColumnBodyElement(columnId: string | number, element: Element | null): void {
171
+ if (element) {
172
+ columnBodyById.set(columnId, element);
173
+ } else {
174
+ columnBodyById.delete(columnId);
175
+ }
176
+ }
177
+
178
+ function enterColumn(columnId: string | number): void {
179
+ if (unref(options.disabled)) {
180
+ return;
181
+ }
182
+
183
+ const count = (dragEnterCounts.get(columnId) ?? 0) + 1;
184
+ dragEnterCounts.set(columnId, count);
185
+ isOverColumnId.value = columnId;
186
+ }
187
+
188
+ function leaveColumn(columnId: string | number): void {
189
+ const next = (dragEnterCounts.get(columnId) ?? 0) - 1;
190
+
191
+ if (next > 0) {
192
+ dragEnterCounts.set(columnId, next);
193
+ return;
194
+ }
195
+
196
+ dragEnterCounts.delete(columnId);
197
+
198
+ if (isOverColumnId.value === columnId) {
199
+ isOverColumnId.value = null;
200
+ }
201
+
202
+ clearDropTarget();
203
+ }
204
+
205
+ function startDrag(itemId: string | number, fromColumnId: string | number): void {
206
+ if (unref(options.disabled)) {
207
+ return;
208
+ }
209
+
210
+ clearTimerIfAny();
211
+ dragState.value = {
212
+ mode: 'pointer',
213
+ itemId,
214
+ fromColumnId,
215
+ dropColumnId: null,
216
+ beforeItemId: null,
217
+ originBeforeItemId: findCurrentBeforeItemId(itemId, fromColumnId)
218
+ };
27
219
  }
28
220
 
29
221
  function endDrag(): void {
222
+ clearTimerIfAny();
223
+ autoScroll.stop();
30
224
  dragState.value = null;
225
+ isOverColumnId.value = null;
226
+ dragEnterCounts.clear();
31
227
  }
32
228
 
33
- function updateDropTarget(columnId: string | number, beforeCardId: string | number | null): void {
229
+ function updateDropTarget(columnId: string | number, beforeItemId: string | number | null): void {
34
230
  if (!dragState.value) {
35
231
  return;
36
232
  }
37
233
 
38
- dragState.value = {...dragState.value, dropColumnId: columnId, beforeCardId};
234
+ clearTimerIfAny();
235
+ dragState.value = {...dragState.value, dropColumnId: columnId, beforeItemId};
39
236
  }
40
237
 
41
238
  function clearDropTarget(): void {
@@ -43,26 +240,332 @@ export function useKanban(onMove: (event: FluxKanbanMoveEvent) => void): FluxKan
43
240
  return;
44
241
  }
45
242
 
46
- dragState.value = {...dragState.value, dropColumnId: null, beforeCardId: null};
243
+ clearTimerIfAny();
244
+ clearTimer = setTimeout(() => {
245
+ clearTimer = null;
246
+
247
+ if (!dragState.value) {
248
+ return;
249
+ }
250
+
251
+ dragState.value = {...dragState.value, dropColumnId: null, beforeItemId: null};
252
+ }, DRAG_LEAVE_GRACE_MS);
47
253
  }
48
254
 
49
255
  function commitDrop(): void {
50
256
  const state = dragState.value;
257
+ const event = tryBuildMoveEvent(state);
258
+ endDrag();
51
259
 
52
- if (!state || state.dropColumnId === null) {
53
- dragState.value = null;
260
+ if (!event || !state || isSelfDrop(state)) {
54
261
  return;
55
262
  }
56
263
 
57
- onMove({
58
- cardId: state.cardId,
59
- fromColumnId: state.fromColumnId,
60
- toColumnId: state.dropColumnId,
61
- beforeCardId: state.beforeCardId ?? undefined
264
+ if (!validateMove(event)) {
265
+ options.onAnnounce('Drop not allowed.');
266
+ return;
267
+ }
268
+
269
+ options.onMove(event);
270
+ options.onAnnounce(`Item moved to ${String(event.toColumnId)}.`);
271
+ }
272
+
273
+ /* region keyboard */
274
+
275
+ function findCurrentColumnId(itemId: string | number): string | number | null {
276
+ for (const columnId of columnElementsById.keys()) {
277
+ if (getItemsInColumn(columnId).includes(itemId)) {
278
+ return columnId;
279
+ }
280
+ }
281
+
282
+ return null;
283
+ }
284
+
285
+ function findCurrentBeforeItemId(itemId: string | number, columnId: string | number): string | number | null {
286
+ const items = getItemsInColumn(columnId);
287
+ const idx = items.indexOf(itemId);
288
+
289
+ if (idx === -1) {
290
+ return null;
291
+ }
292
+
293
+ return items[idx + 1] ?? null;
294
+ }
295
+
296
+ function grabItem(itemId: string | number, fromColumnId: string | number): void {
297
+ if (unref(options.disabled)) {
298
+ return;
299
+ }
300
+
301
+ clearTimerIfAny();
302
+
303
+ dragState.value = {
304
+ mode: 'keyboard',
305
+ itemId,
306
+ fromColumnId,
307
+ dropColumnId: null,
308
+ beforeItemId: null,
309
+ originBeforeItemId: findCurrentBeforeItemId(itemId, fromColumnId)
310
+ };
311
+ options.onAnnounce('Item grabbed. Use arrow keys to move, Enter to drop, Escape to cancel.');
312
+ }
313
+
314
+ function moveKeyboard(direction: FluxKanbanKeyboardDirection): void {
315
+ const state = dragState.value;
316
+
317
+ if (!state || state.mode !== 'keyboard') {
318
+ return;
319
+ }
320
+
321
+ const itemId = state.itemId;
322
+ const currentColumnId = findCurrentColumnId(itemId);
323
+
324
+ if (currentColumnId === null) {
325
+ return;
326
+ }
327
+
328
+ const target = direction === 'up' || direction === 'down'
329
+ ? computeWithinColumnTarget(itemId, currentColumnId, direction)
330
+ : computeAcrossColumnTarget(currentColumnId, direction);
331
+
332
+ if (!target) {
333
+ return;
334
+ }
335
+
336
+ const event: FluxKanbanMoveEvent = {
337
+ itemId,
338
+ fromColumnId: currentColumnId,
339
+ toColumnId: target.columnId,
340
+ beforeItemId: target.beforeItemId ?? undefined
341
+ };
342
+
343
+ if (!validateMove(event)) {
344
+ options.onAnnounce('Move not allowed.');
345
+ return;
346
+ }
347
+
348
+ options.onMove(event);
349
+ options.onAnnounce(target.announcement);
350
+ restoreItemFocus(itemId);
351
+ }
352
+
353
+ function restoreItemFocus(itemId: string | number): void {
354
+ requestAnimationFrame(() => {
355
+ const elm = itemElementsById.get(itemId);
356
+
357
+ if (elm instanceof HTMLElement) {
358
+ elm.focus();
359
+ }
62
360
  });
361
+ }
362
+
363
+ function computeWithinColumnTarget(
364
+ itemId: string | number,
365
+ columnId: string | number,
366
+ direction: 'up' | 'down'
367
+ ): { columnId: string | number; beforeItemId: string | number | null; announcement: string } | null {
368
+ const items = getItemsInColumn(columnId);
369
+ const idx = items.indexOf(itemId);
370
+
371
+ if (idx === -1) {
372
+ return null;
373
+ }
63
374
 
375
+ const targetIdx = idx + WITHIN_COLUMN_DELTA[direction];
376
+
377
+ if (targetIdx < 0 || targetIdx >= items.length) {
378
+ return null;
379
+ }
380
+
381
+ // For "down" we want the item to land after items[targetIdx], so beforeItemId is items[targetIdx + 1]; for "up" it's items[targetIdx].
382
+ const beforeItemId = direction === 'up' ? items[targetIdx] : items[targetIdx + 1] ?? null;
383
+
384
+ return {
385
+ columnId,
386
+ beforeItemId,
387
+ announcement: `Position ${targetIdx + 1} of ${items.length}.`
388
+ };
389
+ }
390
+
391
+ function computeAcrossColumnTarget(
392
+ currentColumnId: string | number,
393
+ direction: 'left' | 'right'
394
+ ): { columnId: string | number; beforeItemId: string | number | null; announcement: string } | null {
395
+ const currentIdx = getColumnIndex(currentColumnId);
396
+
397
+ if (currentIdx === -1) {
398
+ return null;
399
+ }
400
+
401
+ const nextColumn = getColumnByIndex(currentIdx + ACROSS_COLUMN_DELTA[direction]);
402
+
403
+ if (!nextColumn) {
404
+ return null;
405
+ }
406
+
407
+ const targetItems = getItemsInColumn(nextColumn.columnId);
408
+
409
+ return {
410
+ columnId: nextColumn.columnId,
411
+ beforeItemId: targetItems[0] ?? null,
412
+ announcement: `Moved to column ${String(nextColumn.columnId)}.`
413
+ };
414
+ }
415
+
416
+ function commitKeyboardDrop(): void {
417
+ const state = dragState.value;
418
+
419
+ if (!state || state.mode !== 'keyboard') {
420
+ return;
421
+ }
422
+
423
+ const currentColumnId = findCurrentColumnId(state.itemId);
64
424
  dragState.value = null;
425
+
426
+ if (currentColumnId !== null) {
427
+ options.onAnnounce(`Item dropped in ${String(currentColumnId)}.`);
428
+ }
429
+ }
430
+
431
+ function cancelKeyboardDrop(): void {
432
+ const state = dragState.value;
433
+
434
+ if (!state || state.mode !== 'keyboard') {
435
+ return;
436
+ }
437
+
438
+ const itemId = state.itemId;
439
+ const currentColumnId = findCurrentColumnId(itemId);
440
+ const currentBeforeItemId = currentColumnId !== null
441
+ ? findCurrentBeforeItemId(itemId, currentColumnId)
442
+ : null;
443
+
444
+ const isAtOrigin = currentColumnId === state.fromColumnId
445
+ && currentBeforeItemId === (state.originBeforeItemId ?? null);
446
+
447
+ if (!isAtOrigin && currentColumnId !== null) {
448
+ options.onMove({
449
+ itemId,
450
+ fromColumnId: currentColumnId,
451
+ toColumnId: state.fromColumnId,
452
+ beforeItemId: state.originBeforeItemId ?? undefined
453
+ });
454
+ }
455
+
456
+ dragState.value = null;
457
+ options.onAnnounce('Drop cancelled.');
458
+ }
459
+
460
+ function isItemGrabbed(itemId: string | number): boolean {
461
+ const state = unref(dragState);
462
+ return state !== null && state.mode === 'keyboard' && state.itemId === itemId;
463
+ }
464
+
465
+ /* endregion */
466
+
467
+ /* region columns */
468
+
469
+ function startColumnDrag(columnId: string | number): void {
470
+ if (!unref(options.reorderableColumns) || unref(options.disabled)) {
471
+ return;
472
+ }
473
+
474
+ columnDragState.value = {columnId, dropBeforeColumnId: null};
475
+ }
476
+
477
+ function endColumnDrag(): void {
478
+ autoScroll.stop();
479
+ columnDragState.value = null;
480
+ }
481
+
482
+ function updateColumnDropTarget(beforeColumnId: string | number | null): void {
483
+ if (!columnDragState.value) {
484
+ return;
485
+ }
486
+
487
+ columnDragState.value = {...columnDragState.value, dropBeforeColumnId: beforeColumnId};
488
+ }
489
+
490
+ function commitColumnDrop(): void {
491
+ const state = columnDragState.value;
492
+ autoScroll.stop();
493
+
494
+ if (!state) {
495
+ return;
496
+ }
497
+
498
+ if (state.dropBeforeColumnId === state.columnId) {
499
+ columnDragState.value = null;
500
+ return;
501
+ }
502
+
503
+ const fromIdx = getColumnIndex(state.columnId);
504
+ const beforeIdx = state.dropBeforeColumnId === null
505
+ ? -1
506
+ : getColumnIndex(state.dropBeforeColumnId);
507
+
508
+ if (fromIdx !== -1 && beforeIdx === fromIdx + 1) {
509
+ columnDragState.value = null;
510
+ return;
511
+ }
512
+
513
+ options.onMoveColumn({
514
+ columnId: state.columnId,
515
+ beforeColumnId: state.dropBeforeColumnId ?? undefined
516
+ });
517
+ columnDragState.value = null;
518
+ }
519
+
520
+ /* endregion */
521
+
522
+ function cancelAll(): void {
523
+ endDrag();
524
+ endColumnDrag();
525
+ cancelKeyboardDrop();
526
+ }
527
+
528
+ function onPointerMove(clientX: number, clientY: number): void {
529
+ if (!unref(dragState) && !unref(columnDragState)) {
530
+ return;
531
+ }
532
+
533
+ autoScroll.onPointerMove(clientX, clientY);
65
534
  }
66
535
 
67
- return {dragState, registerCard, unregisterCard, getCardInfo, startDrag, endDrag, updateDropTarget, clearDropTarget, commitDrop};
536
+ return {
537
+ disabled: options.disabled,
538
+ reorderableColumns: options.reorderableColumns,
539
+ dragState,
540
+ columnDragState,
541
+ grabbedId,
542
+ isOverColumnId,
543
+ isDropAllowed,
544
+ registerItem,
545
+ unregisterItem,
546
+ getItemInfo,
547
+ registerColumn,
548
+ unregisterColumn,
549
+ getColumnInfo,
550
+ setBoardElement,
551
+ setColumnBodyElement,
552
+ enterColumn,
553
+ leaveColumn,
554
+ startDrag,
555
+ endDrag,
556
+ updateDropTarget,
557
+ clearDropTarget,
558
+ commitDrop,
559
+ grabItem,
560
+ moveKeyboard,
561
+ commitKeyboardDrop,
562
+ cancelKeyboardDrop,
563
+ isItemGrabbed,
564
+ startColumnDrag,
565
+ endColumnDrag,
566
+ updateColumnDropTarget,
567
+ commitColumnDrop,
568
+ cancelAll,
569
+ onPointerMove
570
+ };
68
571
  }
@@ -0,0 +1,95 @@
1
+ const AUTOSCROLL_ZONE = 40;
2
+ const AUTOSCROLL_MAX_SPEED = 12;
3
+
4
+ export type UseKanbanAutoScrollOptions = {
5
+ getBoardElement(): Element | null;
6
+ getVerticalTarget(): Element | null;
7
+ };
8
+
9
+ export type UseKanbanAutoScrollReturn = {
10
+ onPointerMove(clientX: number, clientY: number): void;
11
+ stop(): void;
12
+ };
13
+
14
+ /**
15
+ * Drives horizontal (board) and vertical (column body) auto-scroll while a drag
16
+ * is in progress. Reads the active scroll containers via the option callbacks.
17
+ */
18
+ export function useKanbanAutoScroll(options: UseKanbanAutoScrollOptions): UseKanbanAutoScrollReturn {
19
+ let frame: number | null = null;
20
+ let deltaX = 0;
21
+ let deltaY = 0;
22
+ let verticalTarget: Element | null = null;
23
+
24
+ function onPointerMove(clientX: number, clientY: number): void {
25
+ const board = options.getBoardElement();
26
+ const target = options.getVerticalTarget();
27
+
28
+ deltaX = board ? computeScrollDelta(board.getBoundingClientRect(), clientX, 'horizontal') : 0;
29
+ deltaY = target ? computeScrollDelta(target.getBoundingClientRect(), clientY, 'vertical') : 0;
30
+ verticalTarget = target;
31
+
32
+ if (deltaX !== 0 || deltaY !== 0) {
33
+ start();
34
+ } else {
35
+ stop();
36
+ }
37
+ }
38
+
39
+ function start(): void {
40
+ if (frame !== null) {
41
+ return;
42
+ }
43
+
44
+ const tick = (): void => {
45
+ const board = options.getBoardElement();
46
+
47
+ if (deltaX !== 0 && board) {
48
+ board.scrollLeft += deltaX;
49
+ }
50
+
51
+ if (deltaY !== 0 && verticalTarget) {
52
+ verticalTarget.scrollTop += deltaY;
53
+ }
54
+
55
+ if (deltaX === 0 && deltaY === 0) {
56
+ frame = null;
57
+ return;
58
+ }
59
+
60
+ frame = requestAnimationFrame(tick);
61
+ };
62
+
63
+ frame = requestAnimationFrame(tick);
64
+ }
65
+
66
+ function stop(): void {
67
+ if (frame !== null) {
68
+ cancelAnimationFrame(frame);
69
+ frame = null;
70
+ }
71
+
72
+ deltaX = 0;
73
+ deltaY = 0;
74
+ verticalTarget = null;
75
+ }
76
+
77
+ return {onPointerMove, stop};
78
+ }
79
+
80
+ function computeScrollDelta(rect: DOMRect, position: number, axis: 'horizontal' | 'vertical'): number {
81
+ const start = axis === 'horizontal' ? rect.left : rect.top;
82
+ const end = axis === 'horizontal' ? rect.right : rect.bottom;
83
+
84
+ if (position < start + AUTOSCROLL_ZONE) {
85
+ const distance = Math.max(0, position - start);
86
+ return -Math.round(((AUTOSCROLL_ZONE - distance) / AUTOSCROLL_ZONE) * AUTOSCROLL_MAX_SPEED);
87
+ }
88
+
89
+ if (position > end - AUTOSCROLL_ZONE) {
90
+ const distance = Math.max(0, end - position);
91
+ return Math.round(((AUTOSCROLL_ZONE - distance) / AUTOSCROLL_ZONE) * AUTOSCROLL_MAX_SPEED);
92
+ }
93
+
94
+ return 0;
95
+ }
@@ -1,6 +1,5 @@
1
1
  import { getCurrentInstance } from 'vue';
2
- import type { FluxTranslate, FluxTranslation } from '$flux/data';
3
- import { english } from '$flux/data';
2
+ import { english, type FluxTranslate, type FluxTranslation } from '~flux/components/data';
4
3
 
5
4
  const fallback: FluxTranslate = (key, params) => {
6
5
  if (!(key in english)) {
@@ -1,5 +1,5 @@
1
1
  import type { FluxColor, FluxIconName } from '@flux-ui/types';
2
- import { type ComputedRef, nextTick, ref, type Ref, unref, watch } from 'vue';
2
+ import { nextTick, ref, unref, watch, type ComputedRef, type Ref } from 'vue';
3
3
 
4
4
  export type TreeBaseOption = {
5
5
  readonly id: string | number;