@react-x11/components 0.4.0 → 0.6.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 (248) hide show
  1. package/README.md +143 -8
  2. package/dist/charts/components.d.ts +3 -1
  3. package/dist/charts/components.d.ts.map +1 -1
  4. package/dist/charts/components.js +8 -4
  5. package/dist/charts/components.js.map +1 -1
  6. package/dist/charts/node.d.ts +13 -6
  7. package/dist/charts/node.d.ts.map +1 -1
  8. package/dist/charts/node.js +75 -24
  9. package/dist/charts/node.js.map +1 -1
  10. package/dist/charts/render.d.ts +4 -0
  11. package/dist/charts/render.d.ts.map +1 -1
  12. package/dist/charts/render.js +11 -6
  13. package/dist/charts/render.js.map +1 -1
  14. package/dist/code-editor/node.d.ts +47 -16
  15. package/dist/code-editor/node.d.ts.map +1 -1
  16. package/dist/code-editor/node.js +121 -51
  17. package/dist/code-editor/node.js.map +1 -1
  18. package/dist/flow/draw.d.ts +19 -1
  19. package/dist/flow/draw.d.ts.map +1 -1
  20. package/dist/flow/draw.js +64 -35
  21. package/dist/flow/draw.js.map +1 -1
  22. package/dist/flow/index.d.ts.map +1 -1
  23. package/dist/flow/index.js +24 -8
  24. package/dist/flow/index.js.map +1 -1
  25. package/dist/flow/model.d.ts +4 -2
  26. package/dist/flow/model.d.ts.map +1 -1
  27. package/dist/flow/model.js +4 -2
  28. package/dist/flow/model.js.map +1 -1
  29. package/dist/flow/node.d.ts +40 -9
  30. package/dist/flow/node.d.ts.map +1 -1
  31. package/dist/flow/node.js +142 -53
  32. package/dist/flow/node.js.map +1 -1
  33. package/dist/flow/types.d.ts +46 -21
  34. package/dist/flow/types.d.ts.map +1 -1
  35. package/dist/formula/index.d.ts +4 -2
  36. package/dist/formula/index.d.ts.map +1 -1
  37. package/dist/formula/index.js.map +1 -1
  38. package/dist/formula/node.d.ts +5 -1
  39. package/dist/formula/node.d.ts.map +1 -1
  40. package/dist/formula/node.js +19 -3
  41. package/dist/formula/node.js.map +1 -1
  42. package/dist/html/controls.d.ts +4 -1
  43. package/dist/html/controls.d.ts.map +1 -1
  44. package/dist/html/controls.js.map +1 -1
  45. package/dist/html/css/cascade.d.ts +11 -5
  46. package/dist/html/css/cascade.d.ts.map +1 -1
  47. package/dist/html/css/cascade.js +18 -8
  48. package/dist/html/css/cascade.js.map +1 -1
  49. package/dist/html/css/parse.d.ts +15 -10
  50. package/dist/html/css/parse.d.ts.map +1 -1
  51. package/dist/html/css/parse.js +41 -16
  52. package/dist/html/css/parse.js.map +1 -1
  53. package/dist/html/css/style.d.ts +3 -0
  54. package/dist/html/css/style.d.ts.map +1 -1
  55. package/dist/html/css/style.js +3 -1
  56. package/dist/html/css/style.js.map +1 -1
  57. package/dist/html/css/values.d.ts +14 -1
  58. package/dist/html/css/values.d.ts.map +1 -1
  59. package/dist/html/css/values.js +11 -8
  60. package/dist/html/css/values.js.map +1 -1
  61. package/dist/html/index.d.ts.map +1 -1
  62. package/dist/html/index.js +1 -0
  63. package/dist/html/index.js.map +1 -1
  64. package/dist/html/layout/boxes.d.ts +12 -5
  65. package/dist/html/layout/boxes.d.ts.map +1 -1
  66. package/dist/html/layout/boxes.js +7 -4
  67. package/dist/html/layout/boxes.js.map +1 -1
  68. package/dist/html/layout/inline.d.ts.map +1 -1
  69. package/dist/html/layout/inline.js +22 -1
  70. package/dist/html/layout/inline.js.map +1 -1
  71. package/dist/html/node.d.ts +22 -8
  72. package/dist/html/node.d.ts.map +1 -1
  73. package/dist/html/node.js +97 -15
  74. package/dist/html/node.js.map +1 -1
  75. package/dist/html/paint.d.ts +3 -0
  76. package/dist/html/paint.d.ts.map +1 -1
  77. package/dist/html/paint.js +2 -2
  78. package/dist/html/paint.js.map +1 -1
  79. package/dist/index.d.ts +8 -2
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +21 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/maps/clip.d.ts +43 -0
  84. package/dist/maps/clip.d.ts.map +1 -0
  85. package/dist/maps/clip.js +171 -0
  86. package/dist/maps/clip.js.map +1 -0
  87. package/dist/maps/gzip.d.ts +15 -0
  88. package/dist/maps/gzip.d.ts.map +1 -0
  89. package/dist/maps/gzip.js +56 -0
  90. package/dist/maps/gzip.js.map +1 -0
  91. package/dist/maps/index.d.ts +55 -0
  92. package/dist/maps/index.d.ts.map +1 -0
  93. package/dist/maps/index.js +141 -0
  94. package/dist/maps/index.js.map +1 -0
  95. package/dist/maps/labels.d.ts +123 -0
  96. package/dist/maps/labels.d.ts.map +1 -0
  97. package/dist/maps/labels.js +323 -0
  98. package/dist/maps/labels.js.map +1 -0
  99. package/dist/maps/mvt.d.ts +244 -0
  100. package/dist/maps/mvt.d.ts.map +1 -0
  101. package/dist/maps/mvt.js +820 -0
  102. package/dist/maps/mvt.js.map +1 -0
  103. package/dist/maps/node.d.ts +278 -0
  104. package/dist/maps/node.d.ts.map +1 -0
  105. package/dist/maps/node.js +1459 -0
  106. package/dist/maps/node.js.map +1 -0
  107. package/dist/maps/overlay.d.ts +157 -0
  108. package/dist/maps/overlay.d.ts.map +1 -0
  109. package/dist/maps/overlay.js +472 -0
  110. package/dist/maps/overlay.js.map +1 -0
  111. package/dist/maps/paint.d.ts +226 -0
  112. package/dist/maps/paint.d.ts.map +1 -0
  113. package/dist/maps/paint.js +705 -0
  114. package/dist/maps/paint.js.map +1 -0
  115. package/dist/maps/proj.d.ts +281 -0
  116. package/dist/maps/proj.d.ts.map +1 -0
  117. package/dist/maps/proj.js +451 -0
  118. package/dist/maps/proj.js.map +1 -0
  119. package/dist/maps/sources.d.ts +256 -0
  120. package/dist/maps/sources.d.ts.map +1 -0
  121. package/dist/maps/sources.js +210 -0
  122. package/dist/maps/sources.js.map +1 -0
  123. package/dist/maps/style.d.ts +141 -0
  124. package/dist/maps/style.d.ts.map +1 -0
  125. package/dist/maps/style.js +147 -0
  126. package/dist/maps/style.js.map +1 -0
  127. package/dist/maps/styles.d.ts +82 -0
  128. package/dist/maps/styles.d.ts.map +1 -0
  129. package/dist/maps/styles.js +898 -0
  130. package/dist/maps/styles.js.map +1 -0
  131. package/dist/maps/tiles.d.ts +283 -0
  132. package/dist/maps/tiles.d.ts.map +1 -0
  133. package/dist/maps/tiles.js +469 -0
  134. package/dist/maps/tiles.js.map +1 -0
  135. package/dist/maps/types.d.ts +244 -0
  136. package/dist/maps/types.d.ts.map +1 -0
  137. package/dist/maps/types.js +2 -0
  138. package/dist/maps/types.js.map +1 -0
  139. package/dist/markdown/ast.d.ts +80 -6
  140. package/dist/markdown/ast.d.ts.map +1 -1
  141. package/dist/markdown/ast.js +13 -4
  142. package/dist/markdown/ast.js.map +1 -1
  143. package/dist/markdown/expressions.d.ts +27 -0
  144. package/dist/markdown/expressions.d.ts.map +1 -0
  145. package/dist/markdown/expressions.js +203 -0
  146. package/dist/markdown/expressions.js.map +1 -0
  147. package/dist/markdown/index.d.ts +61 -2
  148. package/dist/markdown/index.d.ts.map +1 -1
  149. package/dist/markdown/index.js +101 -2
  150. package/dist/markdown/index.js.map +1 -1
  151. package/dist/markdown/parse.d.ts +1 -1
  152. package/dist/markdown/parse.d.ts.map +1 -1
  153. package/dist/markdown/parse.js +163 -15
  154. package/dist/markdown/parse.js.map +1 -1
  155. package/dist/markdown/spans.d.ts.map +1 -1
  156. package/dist/markdown/spans.js +4 -0
  157. package/dist/markdown/spans.js.map +1 -1
  158. package/dist/markdown/tags.d.ts +35 -0
  159. package/dist/markdown/tags.d.ts.map +1 -0
  160. package/dist/markdown/tags.js +213 -0
  161. package/dist/markdown/tags.js.map +1 -0
  162. package/dist/reorder/hx.d.ts +18 -0
  163. package/dist/reorder/hx.d.ts.map +1 -0
  164. package/dist/reorder/hx.js +10 -0
  165. package/dist/reorder/hx.js.map +1 -0
  166. package/dist/reorder/index.d.ts +314 -0
  167. package/dist/reorder/index.d.ts.map +1 -0
  168. package/dist/reorder/index.js +1539 -0
  169. package/dist/reorder/index.js.map +1 -0
  170. package/dist/reorder/model.d.ts +107 -0
  171. package/dist/reorder/model.d.ts.map +1 -0
  172. package/dist/reorder/model.js +162 -0
  173. package/dist/reorder/model.js.map +1 -0
  174. package/dist/richtext/node.d.ts +15 -5
  175. package/dist/richtext/node.d.ts.map +1 -1
  176. package/dist/richtext/node.js +40 -11
  177. package/dist/richtext/node.js.map +1 -1
  178. package/dist/richtext/runs.d.ts +35 -13
  179. package/dist/richtext/runs.d.ts.map +1 -1
  180. package/dist/richtext/runs.js +83 -27
  181. package/dist/richtext/runs.js.map +1 -1
  182. package/dist/tabs/index.d.ts +17 -0
  183. package/dist/tabs/index.d.ts.map +1 -1
  184. package/dist/tabs/index.js +849 -186
  185. package/dist/tabs/index.js.map +1 -1
  186. package/dist/terminal/vt/fonts.d.ts +45 -15
  187. package/dist/terminal/vt/fonts.d.ts.map +1 -1
  188. package/dist/terminal/vt/fonts.js +89 -19
  189. package/dist/terminal/vt/fonts.js.map +1 -1
  190. package/dist/terminal/vt/node.d.ts +42 -0
  191. package/dist/terminal/vt/node.d.ts.map +1 -1
  192. package/dist/terminal/vt/node.js +95 -11
  193. package/dist/terminal/vt/node.js.map +1 -1
  194. package/dist/terminal/vt/renderer.d.ts.map +1 -1
  195. package/dist/terminal/vt/renderer.js +13 -3
  196. package/dist/terminal/vt/renderer.js.map +1 -1
  197. package/package.json +14 -3
  198. package/src/charts/components.ts +14 -7
  199. package/src/charts/node.ts +81 -26
  200. package/src/charts/render.ts +16 -6
  201. package/src/code-editor/node.ts +147 -53
  202. package/src/flow/draw.ts +104 -35
  203. package/src/flow/index.ts +33 -13
  204. package/src/flow/model.ts +4 -2
  205. package/src/flow/node.ts +158 -59
  206. package/src/flow/types.ts +46 -21
  207. package/src/formula/index.ts +4 -2
  208. package/src/formula/node.ts +24 -5
  209. package/src/html/controls.ts +4 -1
  210. package/src/html/css/cascade.ts +18 -7
  211. package/src/html/css/parse.ts +46 -21
  212. package/src/html/css/style.ts +6 -1
  213. package/src/html/css/values.ts +25 -9
  214. package/src/html/index.ts +1 -0
  215. package/src/html/layout/boxes.ts +19 -9
  216. package/src/html/layout/inline.ts +23 -1
  217. package/src/html/node.ts +110 -17
  218. package/src/html/paint.ts +17 -2
  219. package/src/index.ts +102 -0
  220. package/src/maps/clip.ts +184 -0
  221. package/src/maps/gzip.ts +66 -0
  222. package/src/maps/index.ts +316 -0
  223. package/src/maps/labels.ts +442 -0
  224. package/src/maps/mvt.ts +857 -0
  225. package/src/maps/node.ts +1819 -0
  226. package/src/maps/overlay.ts +639 -0
  227. package/src/maps/paint.ts +978 -0
  228. package/src/maps/proj.ts +641 -0
  229. package/src/maps/sources.ts +425 -0
  230. package/src/maps/style.ts +309 -0
  231. package/src/maps/styles.ts +1040 -0
  232. package/src/maps/tiles.ts +676 -0
  233. package/src/maps/types.ts +260 -0
  234. package/src/markdown/ast.ts +85 -9
  235. package/src/markdown/expressions.ts +223 -0
  236. package/src/markdown/index.ts +190 -5
  237. package/src/markdown/parse.ts +230 -15
  238. package/src/markdown/spans.ts +4 -0
  239. package/src/markdown/tags.ts +240 -0
  240. package/src/reorder/hx.ts +31 -0
  241. package/src/reorder/index.ts +2223 -0
  242. package/src/reorder/model.ts +232 -0
  243. package/src/richtext/node.ts +49 -14
  244. package/src/richtext/runs.ts +115 -30
  245. package/src/tabs/index.ts +1040 -204
  246. package/src/terminal/vt/fonts.ts +128 -36
  247. package/src/terminal/vt/node.ts +105 -12
  248. package/src/terminal/vt/renderer.ts +13 -3
@@ -0,0 +1,1539 @@
1
+ // <ReorderList> — a list the user reorders by dragging its items, or by
2
+ // lifting one from the keyboard and walking it with the arrows: a todo list,
3
+ // a playlist, a settings dialog's columns, a kanban board when several lists
4
+ // share a `group`.
5
+ //
6
+ // This is the **sortable layer over react-x11's own drag and drop**, the
7
+ // position `@dnd-kit/sortable` holds over `@dnd-kit/core` — except that the
8
+ // engine is core's (docs/drag-and-drop.md there): the 4px threshold, click
9
+ // suppression, `:dragging`/`:drag-over`, the in-app payload arriving by
10
+ // reference on `e.items`, edge auto-scroll of any scroll container, the
11
+ // `<popup dragPreview>` a preview is drawn in, and the promotion of the same
12
+ // drag to XDND — or, on the cocoa backend, to NSDragging — when the pointer
13
+ // leaves the app. None of that is here. What is here is what a sortable
14
+ // preset is: the insertion arithmetic (`./model.ts`), the indicator, the
15
+ // keyboard model, and an event vocabulary that speaks list-and-index rather
16
+ // than node-and-pointer.
17
+ //
18
+ // `docs/prd-reorder.md` is the design record — the survey of dnd-kit,
19
+ // hello-pangea, pragmatic-drag-and-drop, React Aria and Framer's Reorder,
20
+ // and what each settled. The decisions a reader of this file would not
21
+ // guess, each with its paragraph there:
22
+ //
23
+ // - **The list is the only drop target; items are not.** One `dropAccept`
24
+ // on the root, one `onDragOver` that reads every item's rectangle and
25
+ // finds the closest edge, and the two items either side of the slot are
26
+ // told to draw or clear their indicator through a per-item setter — so a
27
+ // pointer motion re-renders at most two items and never the list.
28
+ // - **Membership is in the payload's type name.** Lists in a group accept
29
+ // `application/x-react-x11-reorder;scope=group:<name>`; a list on its own
30
+ // accepts `…scope=list:<its uid>`. That keeps "who takes this drop" a
31
+ // declarative fact core answers from `dropAccept` data with no React in
32
+ // the loop — and it has to be, because `e.accept()` in `onDragOver` can
33
+ // only override a node whose `dropAccept` already matched. `canDrop` is
34
+ // the *dynamic* half, and it works the other way round: it can only
35
+ // refuse (`ev.reject()`), which is a decision core lets a handler make.
36
+ // - **A move between lists is two local handlers**, `onInsert` on the list
37
+ // it landed in and `onRemove` on the list it left — React Aria's model,
38
+ // and the order core already fires them in (`onDrop`, then `onDragEnd`
39
+ // "always last"). The target writes where the item landed onto the live
40
+ // payload object, which is how the source can say `to`.
41
+ // - **The hover channel is module state, and it has to be.** `onDragOver`
42
+ // hands the target a `DragEvent`, which carries no `items` — only the
43
+ // drop does — so a target physically cannot write on the payload while
44
+ // the pointer is merely over it. `activeDrag` below is the one-gesture
45
+ // scratchpad the target writes and the source reads in its own `onDrag`,
46
+ // which is what makes `onDragUpdate` able to say *which list* the pointer
47
+ // is over. It is one object for one gesture, never read at import time.
48
+ // - **The indicator, not the slide.** Neighbours do not glide out of the
49
+ // way: this renderer has no transform, and a line at the closest edge
50
+ // answers the same question for a rectangle per item. The one thing that
51
+ // does move is the drop: `dropAnimation` flies a copy of the item from
52
+ // where the pointer let go to where the item landed. That copy is a box
53
+ // **inside the item**, not the ghost popup carried on — a popup outlives
54
+ // the gesture as a real window over the list, and the press that follows
55
+ // a drop by less than the animation lands on it instead of on the list.
56
+ // An absolutely positioned box with `pointerEvents: 'none'` cannot take
57
+ // an input the list should have had.
58
+ // - **The ghost popup is `transparent`.** It is a real window, and the card
59
+ // inside it is rounded, so on an opaque popup the four corners the radius
60
+ // gives up show the window's own ground — white on a light theme, near
61
+ // black on a dark one. Where no compositor runs the window fills itself
62
+ // square, which is what an opaque one looked like anyway.
63
+ // - **A drag's events bubble, so an item must check the source is its own.**
64
+ // `DragStart`, `onDrag` and `onDragEnd` dispatch capture → target →
65
+ // bubble like every other event, which means a list item that *contains*
66
+ // the dragged one — a board's column, holding the list the card is in —
67
+ // sees them all, and `useDragSource` inside it dutifully sets a position
68
+ // and would draw a second ghost. Worse, its handlers would claim the
69
+ // gesture: a second `dragStarted` on the outer list, and `activeDrag`
70
+ // overwritten with the column's payload, so the hover channel described
71
+ // the wrong item. Every source callback here therefore begins by asking
72
+ // whether the event's target is this item's own drag source (itself, or
73
+ // its handle) and returns if not. `preventDefault` is not the tool for
74
+ // that: on this event it cancels the whole drag.
75
+ // - **An in-window ghost lifts the whole path, not just its item.**
76
+ // `zIndex` sorts a node among its *siblings* — `paintOrder()` is per
77
+ // node, and there is no stacking context to escape — so a ghost lifted
78
+ // inside its item paints under the next item, its list under the next
79
+ // list, its column under the next column. Every `<ReorderList>` and
80
+ // `<ReorderItem>` therefore asks, for the length of the gesture, whether
81
+ // the drag is inside it (`node.contains(activeDrag.source)`) and lifts
82
+ // itself if so: a board's card, its column and the board all come
83
+ // forward together. They learn about it by subscribing, because a list
84
+ // two levels up has nothing to re-render it — its children are the
85
+ // application's own elements, unchanged.
86
+ // What that cannot reach is content outside the outermost list; a popup
87
+ // ghost has no such limit, which is why `'auto'` is one. (It was the
88
+ // in-window box on the cocoa backend for two releases, while a preview
89
+ // window there stopped the drop reaching the list — react-x11#488, fixed
90
+ // in 2.8.2 by giving the preview `ignoresMouseEvents` so the pointer
91
+ // passes through it.)
92
+ // - **Every item (or its handle) is a tab stop.** An item is arbitrary
93
+ // content, often with controls of its own; the Tree/Table model of one
94
+ // tab stop with a cursor would leave a button inside a card unreachable.
95
+ // - **A press on a control inside an item is not a drag.** Core arms a drag
96
+ // from the nearest draggable *ancestor* of whatever was pressed, so a
97
+ // press on a `<textinput>` in a card would drag the card after 4px. The
98
+ // item records what the press landed on and cancels the drag at the
99
+ // threshold — `onDragStart`'s `preventDefault()`, which core documents as
100
+ // "the gesture continues as ordinary mouse events". `dragFromInteractive`
101
+ // turns it off; a `<ReorderHandle>` makes the question moot.
102
+ //
103
+ // It is pure composition of `<box>`, `<popup>` and core's `<Icon>`, so
104
+ // there is no `registerElement` here and no side effect at import time.
105
+ import React, { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
106
+ import { Icon, announce, useDragSource, useSystemAppearance, useTheme, } from 'react-x11';
107
+ import { flattenStyle } from 'react-x11/style';
108
+ import { XK_DOWN, XK_END, XK_ESCAPE, XK_HOME, XK_LEFT, XK_RETURN, XK_RIGHT, XK_UP, } from 'react-x11/keysyms';
109
+ import { hx } from './hx.js';
110
+ // Shared with <Tree> and <Table> — internal, deliberately not a shared
111
+ // *module*; the header of src/internal/timers.ts says why. The drop flight
112
+ // needs both: a tick after the layout that placed the item, and a clock.
113
+ import { afterLayout, cancelAfterLayout, cancelLater, later, } from '../internal/timers.js';
114
+ import { closestSlot, insertManyAtSlot, moveManyToSlot, slotMark, } from './model.js';
115
+ export { arrayMove, closestSlot, insertAtSlot, insertManyAtSlot, isNoopSlot, moveManyToSlot, moveToSlot, slotMark, } from './model.js';
116
+ const h = React.createElement;
117
+ /**
118
+ * The private payload type every item offers. The scope suffix is what
119
+ * `dropAccept` matches on — see the header. Exported because it is the one
120
+ * string only this component writes (the tree-shaking guard keys on it),
121
+ * and because an app's own dropzone may want to refuse or take a list item
122
+ * by name.
123
+ */
124
+ export const REORDER_TYPE = 'application/x-react-x11-reorder';
125
+ function scopedType(scope) {
126
+ return `${REORDER_TYPE};scope=${scope}`;
127
+ }
128
+ /** The indicator's thickness. Centred on the edge it marks. */
129
+ const INDICATOR = 2;
130
+ /** The default handle: two columns of core's `moreVertical` dots. */
131
+ const GRIP_DOT = 10;
132
+ /**
133
+ * How much of an item, at each end, still reads as an edge once `combine`
134
+ * is on — so the middle half of an item is where a merge lands. Small
135
+ * enough that reordering past a combining list is still comfortable.
136
+ */
137
+ const COMBINE_BAND = 0.25;
138
+ /** How long the ghost takes to fly to where the item landed. */
139
+ const DROP_MS = 180;
140
+ /** One step of that flight. Not a frame clock: this is a handful of ticks
141
+ * moving one override-redirect window, and core's own animation machinery
142
+ * drives style properties rather than a popup's geometry. */
143
+ const FLIGHT_STEP_MS = 16;
144
+ /** What the focus target tells a screen reader it can do. */
145
+ const HINT = 'Draggable. Press Space to lift, the arrow keys to move, Space to drop.';
146
+ /** Every announcement, in one place, because none of it is localised yet. */
147
+ const SAY = {
148
+ lifted: (label, at, of, many) => `Lifted ${label}${many > 1 ? ` and ${many - 1} more` : ''}, ` +
149
+ `position ${at} of ${of}. ` +
150
+ 'Use the arrow keys to move, Space to drop, Escape to cancel.',
151
+ pickedUp: (label, at, of, many) => `Dragging ${label}${many > 1 ? ` and ${many - 1} more` : ''}, ` +
152
+ `position ${at} of ${of}.`,
153
+ moved: (label, at, of) => `${label} moved to position ${at} of ${of}.`,
154
+ dropped: (label, at, of) => `${label} dropped at position ${at} of ${of}.`,
155
+ droppedInto: (label, list) => `${label} dropped${list ? ` in ${list}` : ' in another list'}.`,
156
+ combined: (label, into) => `${label} dropped onto ${into}.`,
157
+ cancelled: (label, at) => `Move cancelled. ${label} returned to position ${at}.`,
158
+ returned: (label) => `${label} returned to where it was.`,
159
+ };
160
+ /**
161
+ * The gesture's scratchpad — see the header. Written by whichever list the
162
+ * pointer is over, read by the source's own `onDrag`, and null between
163
+ * gestures. One drag at a time is a property of the pointer, not an
164
+ * assumption this makes.
165
+ *
166
+ * `source` and `inline` are here for the stacking: an in-window ghost has to
167
+ * be painted over everything between it and the drop, and `zIndex` only
168
+ * sorts siblings, so every list and item that *contains* the drag lifts
169
+ * itself. They ask this.
170
+ */
171
+ let activeDrag = null;
172
+ /**
173
+ * Who to tell when a drag starts or ends.
174
+ *
175
+ * A list two levels above the one being dragged in has nothing to re-render
176
+ * it — its children are the application's elements, unchanged — so it
177
+ * subscribes instead. Twice a gesture, for as many entries as there are
178
+ * mounted lists and items, each answering one `contains`.
179
+ */
180
+ const watchers = new Set();
181
+ function announceDragChanged() {
182
+ for (const watcher of [...watchers])
183
+ watcher();
184
+ }
185
+ /**
186
+ * Whether the drag in flight is inside `ref`, and drawing a ghost this node
187
+ * has to be painted above. What lifts the whole path from the dragged item
188
+ * up to the outermost list — a board's card, its column, and the board.
189
+ */
190
+ function useCarryingDrag(ref) {
191
+ const [carrying, setCarrying] = useState(false);
192
+ useEffect(() => {
193
+ const update = () => {
194
+ const drag = activeDrag;
195
+ const node = ref.current;
196
+ const holds = Boolean(drag?.inline && drag.source && node && node.contains(drag.source));
197
+ setCarrying(holds);
198
+ };
199
+ watchers.add(update);
200
+ update();
201
+ return () => {
202
+ watchers.delete(update);
203
+ };
204
+ }, [ref]);
205
+ return carrying;
206
+ }
207
+ const ListContext = React.createContext(null);
208
+ function useList(part) {
209
+ const list = useContext(ListContext);
210
+ if (!list) {
211
+ throw new Error(`@react-x11/components: <${part}> has to be inside a <ReorderList>.`);
212
+ }
213
+ return list;
214
+ }
215
+ const ItemContext = React.createContext(null);
216
+ /**
217
+ * The state of the `<ReorderItem>` a component is rendered inside — what
218
+ * function `children` are handed, for a component deeper in the item. Reads
219
+ * `preview` to tell the ghost's copy from the item in the list.
220
+ */
221
+ export function useReorderItem() {
222
+ const item = useContext(ItemContext);
223
+ if (!item) {
224
+ throw new Error('@react-x11/components: useReorderItem() has to be called inside a <ReorderItem>.');
225
+ }
226
+ return item.state;
227
+ }
228
+ const NO_DRAG = {};
229
+ const noop = () => { };
230
+ const detachNoop = () => noop;
231
+ /** What the preview's copy of the children is handed: the state with
232
+ * `preview` set, and a handle rendered in the ghost draws its grip and does
233
+ * nothing else — it is a picture of one. */
234
+ function inertItem(state, styles) {
235
+ return {
236
+ state,
237
+ disabled: true,
238
+ dragProps: NO_DRAG,
239
+ onKeyDown: noop,
240
+ onBlur: noop,
241
+ styles,
242
+ attachHandle: detachNoop,
243
+ };
244
+ }
245
+ /**
246
+ * Where a list's ghost is drawn, resolved.
247
+ *
248
+ * `'auto'` is the popup on both backends. It was the in-window box on cocoa
249
+ * for two releases, while a `<popup dragPreview>` there stopped the drop
250
+ * reaching the list beneath it: first because it registered as a dragging
251
+ * destination (react-x11#488, fixed in 2.8.1), and then because the window
252
+ * server finds the window under the pointer whether or not it has a
253
+ * destination, so a preview with none left the drag with none (#492, fixed
254
+ * in 2.8.2 by creating the preview with `ignoresMouseEvents`, so the hit
255
+ * passes through it). That needs `@windowkit/appkit` >= 0.6.0, which
256
+ * react-x11 2.8.2 asks for — an older bridge ignores the option silently.
257
+ */
258
+ function ghostMode(preview) {
259
+ return preview === 'auto' ? 'popup' : preview;
260
+ }
261
+ function scaleOf(node) {
262
+ const scale = node?.scale;
263
+ return scale && scale > 0 ? scale : 1;
264
+ }
265
+ /**
266
+ * The elements and roles a press belongs to rather than to the item around
267
+ * them. Roles rather than kinds wherever core's own widgets have one, so an
268
+ * app's `<Button>`, `<Slider>` or `<Switch>` is covered without this
269
+ * knowing what they are made of; kinds for the text elements, which are
270
+ * elements rather than compositions.
271
+ */
272
+ const INTERACTIVE_KINDS = new Set(['textinput', 'textarea', 'codeeditor']);
273
+ const INTERACTIVE_ROLES = new Set([
274
+ 'button',
275
+ 'checkbox',
276
+ 'combobox',
277
+ 'link',
278
+ 'menuitem',
279
+ 'menuitemcheckbox',
280
+ 'menuitemradio',
281
+ 'option',
282
+ 'radio',
283
+ 'scrollbar',
284
+ 'searchbox',
285
+ 'slider',
286
+ 'spinbutton',
287
+ 'switch',
288
+ 'tab',
289
+ 'textbox',
290
+ ]);
291
+ /** Was this press on a control, somewhere between the hit node and the item
292
+ * around it? */
293
+ function pressedControl(target, item) {
294
+ for (let n = target; n; n = n.parent) {
295
+ if (INTERACTIVE_KINDS.has(n.kind))
296
+ return true;
297
+ const role = n.props?.role;
298
+ if (typeof role === 'string' && INTERACTIVE_ROLES.has(role))
299
+ return true;
300
+ if (n === item)
301
+ break;
302
+ }
303
+ return false;
304
+ }
305
+ /** Every piece of text under a node, joined — what an item is called when
306
+ * it has no `aria-label`. `textContent()` answers for one node; a card is
307
+ * several. */
308
+ function labelOf(node) {
309
+ if (!node)
310
+ return 'item';
311
+ const parts = [];
312
+ const walk = (n) => {
313
+ const text = n.textContent();
314
+ if (text)
315
+ parts.push(text);
316
+ for (const child of n.children)
317
+ walk(child);
318
+ };
319
+ walk(node);
320
+ const label = parts.join(' ').trim();
321
+ return label || 'item';
322
+ }
323
+ /**
324
+ * The list's `dropAccept`: its own scope, plus whatever the app's `accept`
325
+ * says. An array gains the scope and a predicate is or-ed with it, so
326
+ * core's own group matching (`'files'`, `'text'`, `'uris'`) keeps applying
327
+ * to the foreign half — nothing about accept vocabulary is re-implemented
328
+ * here.
329
+ */
330
+ function widenAccept(accept, type) {
331
+ if (accept === undefined)
332
+ return [type];
333
+ if (typeof accept === 'function') {
334
+ return (types) => types.includes(type) || Boolean(accept(types));
335
+ }
336
+ return [...(Array.isArray(accept) ? accept : [accept]), type];
337
+ }
338
+ /** Whether the app's `accept` — on its own, without the list's scope —
339
+ * takes what is on offer. `ev.has` is core's alias-aware test, which is
340
+ * what keeps the semantic groups core's to define. */
341
+ function foreignAccepted(accept, ev) {
342
+ if (accept === undefined)
343
+ return false;
344
+ if (typeof accept === 'function')
345
+ return Boolean(accept(ev.types));
346
+ return (Array.isArray(accept) ? accept : [accept]).some((want) => ev.has(want));
347
+ }
348
+ function refuse() {
349
+ return false;
350
+ }
351
+ function sameOver(a, b) {
352
+ if (a === b)
353
+ return true;
354
+ if (!a || !b)
355
+ return false;
356
+ return (a.listUid === b.listUid && a.index === b.index && a.combine === b.combine);
357
+ }
358
+ /** Ease-out cubic — the curve core's own transitions use. */
359
+ function ease(t) {
360
+ return 1 - (1 - t) ** 3;
361
+ }
362
+ // --- the list ---------------------------------------------------------------
363
+ /**
364
+ * `<ReorderList onReorder>` around `<ReorderItem id>`s.
365
+ *
366
+ * <ReorderList onReorder={(e) => setTodos((l) => arrayMove(l, e.from, e.to))}>
367
+ * {todos.map((t) => (
368
+ * <ReorderItem key={t.id} id={t.id}><text>{t.title}</text></ReorderItem>
369
+ * ))}
370
+ * </ReorderList>
371
+ *
372
+ * The order is read from the tree the list rendered, so it is written once —
373
+ * there is no `items` array to keep beside the children.
374
+ */
375
+ export function ReorderList(props) {
376
+ const { id, group, orientation = 'vertical', disabled = false, selected, onReorder, onInsert, onRemove, onCombine, combine = false, canDrop, accept, onDrop, onDragStart, onDragUpdate, onDragEnd, preview = 'auto', renderPreview, previewSize, dropAnimation = true, styles, style, 'data-testname': testname, children, } = props;
377
+ const uid = React.useId();
378
+ const type = scopedType(group !== undefined ? `group:${group}` : `list:${uid}`);
379
+ const { reducedMotion } = useSystemAppearance();
380
+ const rootRef = useRef(null);
381
+ const entries = useRef(new Map());
382
+ /** The item showing the indicator, and how. */
383
+ const marked = useRef(null);
384
+ /** The item of *this* list the pointer is dragging, if any. */
385
+ const dragging = useRef(null);
386
+ /** What the last `onDragUpdate` said, so one is not sent per motion. */
387
+ const reportedOver = useRef(null);
388
+ /** Whether the drag in flight is inside this list and drawing an
389
+ * in-window ghost — which is what its own stacking depends on. */
390
+ const carrying = useCarryingDrag(rootRef);
391
+ const [lifted, setLifted] = useState(null);
392
+ const liftedRef = useRef(lifted);
393
+ liftedRef.current = lifted;
394
+ // The handlers by ref, so the context value — and with it every item —
395
+ // does not re-render because an app wrote an inline arrow.
396
+ const handlers = useRef({
397
+ onReorder,
398
+ onInsert,
399
+ onRemove,
400
+ onCombine,
401
+ onDrop,
402
+ onDragStart,
403
+ onDragUpdate,
404
+ onDragEnd,
405
+ canDrop,
406
+ accept,
407
+ selected,
408
+ combine,
409
+ });
410
+ handlers.current = {
411
+ onReorder,
412
+ onInsert,
413
+ onRemove,
414
+ onCombine,
415
+ onDrop,
416
+ onDragStart,
417
+ onDragUpdate,
418
+ onDragEnd,
419
+ canDrop,
420
+ accept,
421
+ selected,
422
+ combine,
423
+ };
424
+ /**
425
+ * The ids in tree order — the order the app rendered the items in. A
426
+ * walk of the root's retained children, stopping at each item, so a
427
+ * wrapper around an item costs nothing and an item inside an item is not
428
+ * a thing.
429
+ */
430
+ const order = useCallback(() => {
431
+ const root = rootRef.current;
432
+ if (!root)
433
+ return [];
434
+ const byNode = new Map();
435
+ for (const entry of entries.current.values()) {
436
+ if (entry.node.current)
437
+ byNode.set(entry.node.current, entry.id);
438
+ }
439
+ const out = [];
440
+ const walk = (n) => {
441
+ const found = byNode.get(n);
442
+ if (found !== undefined) {
443
+ out.push(found);
444
+ return;
445
+ }
446
+ for (const child of n.children)
447
+ walk(child);
448
+ };
449
+ for (const child of root.children)
450
+ walk(child);
451
+ return out;
452
+ }, []);
453
+ /** What travels when this item is picked up: the selection, in the list's
454
+ * own order, when the item is in it — and just the item otherwise. */
455
+ const movingIds = useCallback((itemId) => {
456
+ const set = handlers.current.selected;
457
+ if (!set?.length || !set.includes(itemId))
458
+ return [itemId];
459
+ const chosen = new Set(set);
460
+ const ids = order().filter((x) => chosen.has(x));
461
+ return ids.length ? ids : [itemId];
462
+ }, [order]);
463
+ /** The items' boxes in logical pixels — `abs` is device, and the pointer
464
+ * in a drag event is not. One division, here. */
465
+ const rectsOf = (ids) => {
466
+ const scale = scaleOf(rootRef.current);
467
+ return ids.map((itemId) => {
468
+ const node = entries.current.get(itemId)?.node.current;
469
+ const { x, y, width, height } = node?.abs ?? {
470
+ x: 0,
471
+ y: 0,
472
+ width: 0,
473
+ height: 0,
474
+ };
475
+ return {
476
+ x: x / scale,
477
+ y: y / scale,
478
+ width: width / scale,
479
+ height: height / scale,
480
+ };
481
+ });
482
+ };
483
+ /** Move the indicator: clear the item that had it, set the one that does. */
484
+ const mark = (next) => {
485
+ const prev = marked.current;
486
+ const same = prev !== null &&
487
+ next !== null &&
488
+ prev.id === next.id &&
489
+ prev.edge === next.edge &&
490
+ prev.combine === next.combine &&
491
+ prev.offset === next.offset;
492
+ if (same)
493
+ return;
494
+ if (prev)
495
+ entries.current.get(prev.id)?.setMark(null);
496
+ if (next) {
497
+ entries.current.get(next.id)?.setMark({
498
+ edge: next.edge,
499
+ combine: next.combine,
500
+ offset: next.offset,
501
+ });
502
+ }
503
+ marked.current = next;
504
+ };
505
+ /** Where a drag event's pointer would land. */
506
+ const slotFor = (ev, moving) => {
507
+ const ids = order();
508
+ const hit = closestSlot(rectsOf(ids), { x: ev.x, y: ev.y }, orientation, rootRef.current?.direction ?? 'ltr', handlers.current.combine ? COMBINE_BAND : 0);
509
+ if (!hit)
510
+ return { ids, slot: 0, at: null };
511
+ const over = ids[hit.index];
512
+ // an item cannot merge into itself, nor into anything travelling with it
513
+ if (hit.combine && !moving.includes(over)) {
514
+ return {
515
+ ids,
516
+ slot: hit.slot,
517
+ at: { id: over, edge: hit.edge, combine: true, offset: 0 },
518
+ };
519
+ }
520
+ // The mark comes from the **slot**, not from the item the slot was read
521
+ // off: the lower half of one item and the upper half of the next are the
522
+ // same gap, and a mark keyed to the item would draw that one insertion
523
+ // point in two places and flip between them mid-gap. See `slotMark`.
524
+ const line = slotMark(ids.length, hit.slot);
525
+ if (!line)
526
+ return { ids, slot: hit.slot, at: null };
527
+ // half the gap it is marking, so the line sits in the space rather than
528
+ // against the item below it. The ends have no gap: they stay on the edge.
529
+ const rects = rectsOf(ids);
530
+ const before = line.edge === 'before' ? line.index - 1 : -1;
531
+ let offset = 0;
532
+ if (before >= 0) {
533
+ const a = rects[before];
534
+ const b = rects[line.index];
535
+ const rtl = (rootRef.current?.direction ?? 'ltr') === 'rtl';
536
+ const space = orientation === 'vertical'
537
+ ? b.y - (a.y + a.height)
538
+ : rtl
539
+ ? a.x - (b.x + b.width)
540
+ : b.x - (a.x + a.width);
541
+ offset = -Math.max(space, 0) / 2;
542
+ }
543
+ return {
544
+ ids,
545
+ slot: hit.slot,
546
+ at: { id: ids[line.index], edge: line.edge, combine: false, offset },
547
+ };
548
+ };
549
+ /** What the payload of a drag is, when it is one of ours. */
550
+ const payloadOf = (ev) => {
551
+ const items = ev.items;
552
+ const found = items?.[type];
553
+ return found && typeof found === 'object' ? found : null;
554
+ };
555
+ /** The live payload while the pointer is merely *over* us: a `DragEvent`
556
+ * carries no `items`, so the scratchpad is the only way to know whose
557
+ * item this is — see the header. */
558
+ const hoveringPayload = (ev) => {
559
+ if (!ev.types.includes(type))
560
+ return null;
561
+ return payloadOf(ev) ?? activeDrag?.payload ?? null;
562
+ };
563
+ const allowed = (ev, payload, slot, combining) => {
564
+ const ask = handlers.current.canDrop;
565
+ if (!ask)
566
+ return true;
567
+ return Boolean(ask({
568
+ id: payload?.id,
569
+ ids: payload?.ids ?? [],
570
+ source: payload ? { list: payload.list, index: payload.index } : null,
571
+ index: slot,
572
+ combine: combining,
573
+ event: ev,
574
+ }));
575
+ };
576
+ const onDragOver = (ev) => {
577
+ // `onDragOver` reaches every node on the path under the pointer, matched
578
+ // or not — so a list has to ask the question `dropAccept` already
579
+ // answered before it promises a slot to a drag it will never be given
580
+ const payload = hoveringPayload(ev);
581
+ if (!payload && !foreignAccepted(handlers.current.accept, ev)) {
582
+ mark(null);
583
+ return;
584
+ }
585
+ const moving = payload?.listUid === uid ? payload.ids : [];
586
+ const { ids, slot, at } = slotFor(ev, moving);
587
+ const combining = at?.combine ? at.id : null;
588
+ if (!allowed(ev, payload, slot, combining)) {
589
+ mark(null);
590
+ if (activeDrag?.over?.listUid === uid)
591
+ activeDrag.over = null;
592
+ // core keeps the node its `dropAccept` matched; what a handler may do
593
+ // is refuse this position, which is what turns the ghost's `accepted`
594
+ // off and what `canDrop` means
595
+ ev.reject();
596
+ return;
597
+ }
598
+ // one of this list's own items, over a gap that is already its own:
599
+ // nothing would move, so nothing is promised
600
+ const move = !combining && moving.length > 0
601
+ ? moveManyToSlot(ids, moving, slot, payload.id)
602
+ : null;
603
+ if (!combining && moving.length > 0 && move === null) {
604
+ mark(null);
605
+ if (activeDrag?.over?.listUid === uid)
606
+ activeDrag.over = null;
607
+ return;
608
+ }
609
+ mark(at);
610
+ if (activeDrag) {
611
+ activeDrag.over = {
612
+ listUid: uid,
613
+ list: id,
614
+ // where it would land, not the gap it is over — a same-list move
615
+ // closes the hole it left behind, and a newcomer lands at the gap
616
+ index: move ? move.to : slot,
617
+ combine: combining,
618
+ };
619
+ }
620
+ };
621
+ const onDragLeave = () => {
622
+ mark(null);
623
+ if (activeDrag?.over?.listUid === uid)
624
+ activeDrag.over = null;
625
+ };
626
+ const onDropHere = (ev) => {
627
+ mark(null);
628
+ const payload = payloadOf(ev);
629
+ const moving = payload?.listUid === uid ? payload.ids : [];
630
+ const { ids, slot, at } = slotFor(ev, moving);
631
+ const combining = at?.combine ? at.id : null;
632
+ if (!allowed(ev, payload, slot, combining)) {
633
+ ev.reject();
634
+ return;
635
+ }
636
+ if (payload) {
637
+ // a copy leaves the source's item where it is; anything else moves it
638
+ const action = ev.action === 'copy' ? 'copy' : 'move';
639
+ payload.to = { listUid: uid, list: id, index: slot, combine: combining };
640
+ if (combining !== null) {
641
+ ev.accept(action);
642
+ handlers.current.onCombine?.({
643
+ id: payload.id,
644
+ ids: payload.ids,
645
+ into: combining,
646
+ index: ids.indexOf(combining),
647
+ source: { list: payload.list, index: payload.index },
648
+ event: ev,
649
+ });
650
+ return;
651
+ }
652
+ if (payload.listUid === uid) {
653
+ // a list does not duplicate into itself: a same-list drop reorders
654
+ const move = moveManyToSlot(ids, payload.ids, slot, payload.id);
655
+ payload.to.index = move ? move.to : payload.index;
656
+ ev.accept('move');
657
+ if (move) {
658
+ handlers.current.onReorder?.({
659
+ items: move.items,
660
+ id: payload.id,
661
+ ids: move.ids,
662
+ from: move.from,
663
+ to: move.to,
664
+ });
665
+ }
666
+ return;
667
+ }
668
+ ev.accept(action);
669
+ handlers.current.onInsert?.({
670
+ items: insertManyAtSlot(ids, payload.ids, slot),
671
+ id: payload.id,
672
+ ids: payload.ids,
673
+ index: slot,
674
+ action,
675
+ source: { list: payload.list, index: payload.index },
676
+ event: ev,
677
+ });
678
+ return;
679
+ }
680
+ // Not a live list item: files, text, another application's drag — or
681
+ // the scope type from another process, which is JSON by then and not
682
+ // ours to act on. The app's `accept` alone decides.
683
+ if (!foreignAccepted(handlers.current.accept, ev)) {
684
+ ev.reject();
685
+ return;
686
+ }
687
+ handlers.current.onDrop?.({ index: slot, event: ev });
688
+ };
689
+ // --- the keyboard's half, lent to the items ------------------------------
690
+ const labelFor = (itemId) => entries.current.get(itemId)?.label() ?? 'item';
691
+ const lift = useCallback((itemId) => {
692
+ const ids = order();
693
+ const moving = movingIds(itemId);
694
+ setLifted({ id: itemId, ids: moving, origin: ids });
695
+ const at = ids.indexOf(itemId) + 1;
696
+ handlers.current.onDragStart?.({
697
+ id: itemId,
698
+ ids: moving,
699
+ index: at - 1,
700
+ input: 'keyboard',
701
+ });
702
+ announce(SAY.lifted(labelFor(itemId), at, ids.length, moving.length));
703
+ }, [order, movingIds]);
704
+ const drop = useCallback(() => {
705
+ const current = liftedRef.current;
706
+ if (!current)
707
+ return;
708
+ setLifted(null);
709
+ const ids = order();
710
+ const at = ids.indexOf(current.id);
711
+ handlers.current.onDragEnd?.({
712
+ id: current.id,
713
+ ids: current.ids,
714
+ from: current.origin.indexOf(current.id),
715
+ to: { list: id, index: at },
716
+ combine: null,
717
+ reason: 'drop',
718
+ input: 'keyboard',
719
+ });
720
+ announce(SAY.dropped(labelFor(current.id), at + 1, ids.length));
721
+ }, [id, order]);
722
+ const cancel = useCallback(() => {
723
+ const current = liftedRef.current;
724
+ if (!current)
725
+ return;
726
+ setLifted(null);
727
+ const ids = order();
728
+ const from = ids.indexOf(current.id);
729
+ const to = current.origin.indexOf(current.id);
730
+ if (from >= 0 && to >= 0 && from !== to) {
731
+ const back = moveManyToSlot(ids, current.ids, to + (to > from ? 1 : 0), current.id);
732
+ if (back) {
733
+ handlers.current.onReorder?.({
734
+ items: back.items,
735
+ id: current.id,
736
+ ids: back.ids,
737
+ from: back.from,
738
+ to: back.to,
739
+ });
740
+ }
741
+ }
742
+ handlers.current.onDragEnd?.({
743
+ id: current.id,
744
+ ids: current.ids,
745
+ from,
746
+ to: null,
747
+ combine: null,
748
+ reason: 'cancel',
749
+ input: 'keyboard',
750
+ });
751
+ announce(SAY.cancelled(labelFor(current.id), (to >= 0 ? to : from) + 1));
752
+ }, [order]);
753
+ const step = useCallback((itemId, where) => {
754
+ const current = liftedRef.current;
755
+ const ids = order();
756
+ const from = ids.indexOf(itemId);
757
+ if (from < 0)
758
+ return;
759
+ const moving = current?.ids ?? [itemId];
760
+ const set = new Set(moving);
761
+ const held = ids
762
+ .map((x, i) => (set.has(x) ? i : -1))
763
+ .filter((i) => i >= 0);
764
+ // A step moves the whole run **past its next neighbour**, which for one
765
+ // item is the ordinary "swap with the one below". `from + 1` would be
766
+ // inside the run itself for a multi-drag, and land it where it already
767
+ // is — the shape of that bug is a selection that will not move.
768
+ let slot;
769
+ if (where === 'home')
770
+ slot = 0;
771
+ else if (where === 'end')
772
+ slot = ids.length;
773
+ else if (where === 1) {
774
+ const last = held.at(-1) ?? from;
775
+ const next = ids.findIndex((x, i) => i > last && !set.has(x));
776
+ if (next < 0)
777
+ return;
778
+ slot = next + 1;
779
+ }
780
+ else {
781
+ const first = held[0] ?? from;
782
+ let prev = -1;
783
+ for (let i = first - 1; i >= 0; i--) {
784
+ if (!set.has(ids[i])) {
785
+ prev = i;
786
+ break;
787
+ }
788
+ }
789
+ if (prev < 0)
790
+ return;
791
+ slot = prev;
792
+ }
793
+ const move = moveManyToSlot(ids, moving, slot, itemId);
794
+ if (!move)
795
+ return;
796
+ handlers.current.onReorder?.({
797
+ items: move.items,
798
+ id: itemId,
799
+ ids: move.ids,
800
+ from: move.from,
801
+ to: move.to,
802
+ });
803
+ handlers.current.onDragUpdate?.({
804
+ id: itemId,
805
+ ids: move.ids,
806
+ from,
807
+ over: { list: id, index: move.to },
808
+ combine: null,
809
+ input: 'keyboard',
810
+ });
811
+ announce(SAY.moved(labelFor(itemId), move.to + 1, ids.length));
812
+ }, [id, order]);
813
+ const focusNeighbour = useCallback((itemId, delta) => {
814
+ const ids = order();
815
+ const next = ids[ids.indexOf(itemId) + delta];
816
+ if (next !== undefined)
817
+ entries.current.get(next)?.focus();
818
+ }, [order]);
819
+ const register = useCallback((entry) => {
820
+ entries.current.set(entry.id, entry);
821
+ return () => {
822
+ if (entries.current.get(entry.id) === entry) {
823
+ entries.current.delete(entry.id);
824
+ }
825
+ if (marked.current?.id === entry.id)
826
+ marked.current = null;
827
+ };
828
+ }, []);
829
+ const dragStarted = useCallback((itemId) => {
830
+ dragging.current = itemId;
831
+ // a pointer drag and a keyboard lift are one gesture's worth of state
832
+ if (liftedRef.current)
833
+ setLifted(null);
834
+ reportedOver.current = null;
835
+ const ids = order();
836
+ const moving = movingIds(itemId);
837
+ const index = ids.indexOf(itemId);
838
+ handlers.current.onDragStart?.({
839
+ id: itemId,
840
+ ids: moving,
841
+ index,
842
+ input: 'pointer',
843
+ });
844
+ announce(SAY.pickedUp(labelFor(itemId), index + 1, ids.length, moving.length));
845
+ return { index, ids: moving };
846
+ }, [order, movingIds]);
847
+ /** One pointer position, on the source's side: core tells the target
848
+ * first, so the scratchpad is already up to date here. */
849
+ const dragMoved = useCallback((itemId) => {
850
+ const over = activeDrag?.over ?? null;
851
+ if (sameOver(over, reportedOver.current))
852
+ return;
853
+ reportedOver.current = over;
854
+ const payload = activeDrag?.payload;
855
+ handlers.current.onDragUpdate?.({
856
+ id: itemId,
857
+ ids: payload?.ids ?? [itemId],
858
+ from: payload?.index ?? -1,
859
+ over: over ? { list: over.list, index: over.index } : null,
860
+ combine: over?.combine ?? null,
861
+ input: 'pointer',
862
+ });
863
+ }, []);
864
+ const dragEnded = useCallback((itemId, ev, payload) => {
865
+ dragging.current = null;
866
+ reportedOver.current = null;
867
+ mark(null);
868
+ const to = payload?.to;
869
+ const moved = ev.dropped && ev.action === 'move';
870
+ const ids = order();
871
+ // a move within this list has already been reported as a reorder
872
+ const elsewhere = moved && !(to && to.listUid === uid);
873
+ if (elsewhere) {
874
+ handlers.current.onRemove?.({
875
+ items: ids.filter((x) => !(payload?.ids ?? [itemId]).includes(x)),
876
+ id: itemId,
877
+ ids: payload?.ids ?? [itemId],
878
+ index: ids.indexOf(itemId),
879
+ to: to ? { list: to.list, index: to.index } : null,
880
+ event: ev,
881
+ });
882
+ }
883
+ handlers.current.onDragEnd?.({
884
+ id: itemId,
885
+ ids: payload?.ids ?? [itemId],
886
+ from: payload?.index ?? -1,
887
+ to: ev.dropped && to ? { list: to.list, index: to.index } : null,
888
+ combine: to?.combine ?? null,
889
+ reason: ev.dropped ? 'drop' : 'cancel',
890
+ input: 'pointer',
891
+ });
892
+ const label = labelFor(itemId);
893
+ if (!ev.dropped)
894
+ announce(SAY.returned(label));
895
+ else if (to?.combine != null) {
896
+ announce(SAY.combined(label, labelFor(to.combine)));
897
+ }
898
+ else if (to && to.listUid === uid) {
899
+ announce(SAY.dropped(label, to.index + 1, ids.length));
900
+ }
901
+ else if (to)
902
+ announce(SAY.droppedInto(label, to.list));
903
+ }, [uid, order]);
904
+ const shared = useMemo(() => ({
905
+ uid,
906
+ id,
907
+ type,
908
+ orientation,
909
+ disabled,
910
+ preview: preview === true ? 'auto' : preview,
911
+ renderPreview,
912
+ previewSize,
913
+ dropMs: reducedMotion || dropAnimation === false
914
+ ? 0
915
+ : dropAnimation === true
916
+ ? DROP_MS
917
+ : dropAnimation,
918
+ styles,
919
+ liftedId: lifted?.id ?? null,
920
+ register,
921
+ order,
922
+ movingIds,
923
+ lift,
924
+ drop,
925
+ cancel,
926
+ step,
927
+ focusNeighbour,
928
+ dragStarted,
929
+ dragMoved,
930
+ dragEnded,
931
+ }), [
932
+ uid,
933
+ id,
934
+ type,
935
+ orientation,
936
+ disabled,
937
+ preview,
938
+ renderPreview,
939
+ previewSize,
940
+ dropAnimation,
941
+ reducedMotion,
942
+ styles,
943
+ lifted,
944
+ register,
945
+ order,
946
+ movingIds,
947
+ lift,
948
+ drop,
949
+ cancel,
950
+ step,
951
+ focusNeighbour,
952
+ dragStarted,
953
+ dragMoved,
954
+ dragEnded,
955
+ ]);
956
+ const dropAccept = useMemo(() => (disabled ? refuse : widenAccept(accept, type)), [accept, type, disabled]);
957
+ return hx('box', {
958
+ ref: rootRef,
959
+ role: 'list',
960
+ 'aria-orientation': orientation,
961
+ 'data-testname': testname,
962
+ dropAccept,
963
+ onDragOver: disabled ? undefined : onDragOver,
964
+ onDragLeave: disabled ? undefined : onDragLeave,
965
+ onDrop: disabled ? undefined : onDropHere,
966
+ style: [
967
+ { flexDirection: orientation === 'horizontal' ? 'row' : 'column' },
968
+ style,
969
+ // An in-window ghost is a box inside this list, and `zIndex` only
970
+ // sorts among siblings — so every list that *contains* the drag comes
971
+ // forward among its own, or the ghost paints under whatever is next.
972
+ // See the header.
973
+ carrying ? { zIndex: 1 } : null,
974
+ ],
975
+ }, h(ListContext.Provider, { value: shared }, children));
976
+ }
977
+ // --- the item ---------------------------------------------------------------
978
+ /** The item's own look, under its `style`. */
979
+ function itemStyle(state, hasHandle) {
980
+ return { cursor: state.disabled || hasHandle ? undefined : 'grab' };
981
+ }
982
+ /** An item that contains the drag has to paint over its neighbours, or the
983
+ * ghost following the pointer slides under the next row — or, for the item
984
+ * that is a whole board column, under the next column. */
985
+ function liftStyle(lifted) {
986
+ return lifted ? { zIndex: 3 } : null;
987
+ }
988
+ /**
989
+ * What a lifted or dragging item wears — the wash the palette already has
990
+ * for "this one", the way a hovered row does. It sits *over* the item's
991
+ * `style`, the way a `:hover` block would: it is state, not look, and an
992
+ * app that painted its rows `$surface` still gets to see which one it is
993
+ * holding. `styles.item(state)` merges over this, so it stays the seam.
994
+ */
995
+ function washStyle(state, theme) {
996
+ return state.lifted || state.dragging
997
+ ? { backgroundColor: theme.surfaceHover }
998
+ : null;
999
+ }
1000
+ /** The line at an edge: across the item and centred on the edge, inside the
1001
+ * item so a scrolling list carries it, logical edges so RTL costs nothing.
1002
+ * A combining item is outlined instead — the drop lands *on* it, and a line
1003
+ * beside it would say the opposite. */
1004
+ function indicatorStyle(mark, orientation, theme) {
1005
+ const base = {
1006
+ position: 'absolute',
1007
+ zIndex: 1,
1008
+ pointerEvents: 'none',
1009
+ };
1010
+ if (mark.combine) {
1011
+ return {
1012
+ ...base,
1013
+ top: 0,
1014
+ bottom: 0,
1015
+ left: 0,
1016
+ right: 0,
1017
+ borderWidth: INDICATOR,
1018
+ borderColor: theme.accent,
1019
+ borderRadius: theme.radius,
1020
+ };
1021
+ }
1022
+ const line = { ...base, backgroundColor: theme.accent };
1023
+ // `offset` walks the line back into the middle of the gap; at the ends,
1024
+ // where there is no gap, it is zero and the line is centred on the edge
1025
+ const at = -INDICATOR / 2 + mark.offset;
1026
+ if (orientation === 'vertical') {
1027
+ return {
1028
+ ...line,
1029
+ left: 0,
1030
+ right: 0,
1031
+ height: INDICATOR,
1032
+ ...(mark.edge === 'before' ? { top: at } : { bottom: at }),
1033
+ };
1034
+ }
1035
+ return {
1036
+ ...line,
1037
+ top: 0,
1038
+ bottom: 0,
1039
+ width: INDICATOR,
1040
+ ...(mark.edge === 'before' ? { start: at } : { end: at }),
1041
+ };
1042
+ }
1043
+ /**
1044
+ * What the default ghost is drawn on, *under* the item's own style.
1045
+ *
1046
+ * The ghost is the item, so it should look like the item: same padding, same
1047
+ * corners, same ground. An earlier cut drew a card of its own over the top —
1048
+ * a surface, a hairline and the theme's radius — and it was wrong in a way
1049
+ * that is easy to miss until an item has a look of its own. A chip lost its
1050
+ * rounded corners, and the border it gained ate two pixels of content box,
1051
+ * which is enough to wrap a word: "later" came out as "late" and "r".
1052
+ *
1053
+ * So the only thing here is a ground, and only when the item does not bring
1054
+ * one — a ghost with nothing behind it is a floating label, which is not
1055
+ * what anybody meant. `styles.preview` and `renderPreview` are where a
1056
+ * lifted-card look belongs, for an app that wants one.
1057
+ */
1058
+ function previewStyle(theme, hasGround) {
1059
+ return hasGround
1060
+ ? { flexGrow: 1 }
1061
+ : { flexGrow: 1, backgroundColor: theme.surface };
1062
+ }
1063
+ /** The badge a multi-drag's ghost wears: how many are travelling. */
1064
+ function badgeStyle(theme) {
1065
+ return {
1066
+ position: 'absolute',
1067
+ top: -8,
1068
+ end: -8,
1069
+ minWidth: 18,
1070
+ paddingLeft: 5,
1071
+ paddingRight: 5,
1072
+ alignItems: 'center',
1073
+ justifyContent: 'center',
1074
+ borderRadius: 9,
1075
+ backgroundColor: theme.accent,
1076
+ };
1077
+ }
1078
+ /**
1079
+ * `<ReorderItem id>` — one item. Draggable by any part of itself, unless a
1080
+ * `<ReorderHandle>` inside it takes that over; a tab stop, with the keyboard
1081
+ * model on it, for the same "unless".
1082
+ */
1083
+ export function ReorderItem(props) {
1084
+ const { id, disabled: ownDisabled = false, dragData, dragActions, dragFromInteractive = false, 'aria-label': ariaLabel, style, 'data-testname': testname, children, } = props;
1085
+ const list = useList('ReorderItem');
1086
+ const theme = useTheme();
1087
+ const nodeRef = useRef(null);
1088
+ const handleRef = useRef(null);
1089
+ const [mark, setMark] = useState(null);
1090
+ /** Whether the drag in flight is inside this item — this one being
1091
+ * dragged, or a whole column of a board whose card is. */
1092
+ const carrying = useCarryingDrag(nodeRef);
1093
+ const [hasHandle, setHasHandle] = useState(false);
1094
+ const disabled = ownDisabled || list.disabled;
1095
+ const lifted = list.liftedId === id;
1096
+ const liftedRef = useRef(lifted);
1097
+ liftedRef.current = lifted;
1098
+ /** Whether *this* item is the one being dragged, rather than one that
1099
+ * merely contains it — see the header. `useDragSource`'s own
1100
+ * `isDragging` cannot tell, because the events bubble. */
1101
+ const [holdingOwn, setHoldingOwn] = useState(false);
1102
+ /** The live payload, from the press that started a drag to its end. */
1103
+ const payload = useRef(null);
1104
+ /** Where in the item the press landed, so the ghost appears under it
1105
+ * rather than jumping to the cursor; and how big the item was. */
1106
+ const grab = useRef({ x: 0, y: 0, width: 0, height: 0 });
1107
+ /** What the press landed on, for the interactive-element question, and
1108
+ * where the window is on screen, for the drop flight. */
1109
+ const pressed = useRef(null);
1110
+ const windowOrigin = useRef({ x: 0, y: 0 });
1111
+ /** Where the ghost was last seen, in screen coordinates, and where the
1112
+ * landing copy is now — an offset from the item's own origin, easing to
1113
+ * nothing. */
1114
+ const lastGhost = useRef(null);
1115
+ const [flight, setFlight] = useState(null);
1116
+ const ticks = useRef({
1117
+ layout: null,
1118
+ timer: null,
1119
+ });
1120
+ const label = useCallback(() => ariaLabel ?? labelOf(nodeRef.current), [ariaLabel]);
1121
+ useEffect(() => list.register({
1122
+ id,
1123
+ node: nodeRef,
1124
+ setMark,
1125
+ focus: () => {
1126
+ (handleRef.current?.current ?? nodeRef.current)?.focus();
1127
+ },
1128
+ label,
1129
+ }), [id, list.register, label]);
1130
+ /** Nothing may outlive the item: a flight tick that fires after unmount
1131
+ * would set state on a component that is gone. */
1132
+ useEffect(() => () => {
1133
+ cancelAfterLayout(ticks.current.layout);
1134
+ cancelLater(ticks.current.timer);
1135
+ }, []);
1136
+ /**
1137
+ * Should the drop be animated at all?
1138
+ *
1139
+ * The flight ends at where the item *is*, which is the right picture for
1140
+ * exactly two endings: a move inside this list, where it is somewhere new,
1141
+ * and a drag that landed nowhere, where flying back to an unchanged
1142
+ * position is precisely "it returned". Every other ending leaves the item
1143
+ * where it started while the drop *did* something — a merge into another
1144
+ * item, a copy taken by another list — and flying home there says the
1145
+ * opposite of what happened: it reads as a rejection. Those endings let
1146
+ * the ghost go where the pointer left it.
1147
+ */
1148
+ const flies = (ev, carried) => {
1149
+ if (!ev.dropped || ev.action === null)
1150
+ return true; // it went nowhere
1151
+ const to = carried?.to;
1152
+ return Boolean(to && to.listUid === list.uid && to.combine === null);
1153
+ };
1154
+ /**
1155
+ * Fly a copy of the item from where the pointer let go to where the item
1156
+ * landed, then let it go.
1157
+ *
1158
+ * Two things make this the shape it is. The landing place is only real
1159
+ * once the layout that moved the item has run — hence the tick — and the
1160
+ * copy travels in the *item's own* coordinates, as an offset easing to
1161
+ * zero, so it is a box inside the list rather than a window over it (see
1162
+ * the header). The screen point the pointer let go at is turned into that
1163
+ * offset here, which is the only thing the window's origin is needed for.
1164
+ */
1165
+ const flyHome = useCallback((from, ms) => {
1166
+ ticks.current.layout = afterLayout(() => {
1167
+ ticks.current.layout = null;
1168
+ const node = nodeRef.current;
1169
+ if (!node) {
1170
+ setFlight(null);
1171
+ return;
1172
+ }
1173
+ const scale = scaleOf(node);
1174
+ const dx = from.x - (windowOrigin.current.x + node.abs.x / scale);
1175
+ const dy = from.y - (windowOrigin.current.y + node.abs.y / scale);
1176
+ if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
1177
+ setFlight(null);
1178
+ return;
1179
+ }
1180
+ const started = Date.now();
1181
+ const stepOn = () => {
1182
+ const t = Math.min((Date.now() - started) / ms, 1);
1183
+ if (t >= 1) {
1184
+ ticks.current.timer = null;
1185
+ setFlight(null);
1186
+ return;
1187
+ }
1188
+ const left = 1 - ease(t);
1189
+ setFlight({ x: dx * left, y: dy * left });
1190
+ ticks.current.timer = later(stepOn, FLIGHT_STEP_MS);
1191
+ };
1192
+ stepOn();
1193
+ });
1194
+ }, []);
1195
+ /** Is this event about this item's own drag source — the item box, or the
1196
+ * handle the drag props were spread on — rather than a descendant's? */
1197
+ const ownSource = (ev) => ev.target === nodeRef.current ||
1198
+ ev.target === (handleRef.current?.current ?? null);
1199
+ const { dragProps, isDragging, position } = useDragSource({
1200
+ data: { [list.type]: () => payload.current, ...dragData },
1201
+ actions: dragActions ?? ['move'],
1202
+ onDragStart: (ev) => {
1203
+ if (!ownSource(ev))
1204
+ return;
1205
+ // A press on a control inside the item belongs to the control, not to
1206
+ // the item around it — core arms from the nearest draggable ancestor,
1207
+ // so this is the layer's own answer. Cancelling here leaves the
1208
+ // gesture as ordinary mouse events, which is what the control wanted.
1209
+ if (!dragFromInteractive &&
1210
+ !hasHandle &&
1211
+ pressedControl(pressed.current, nodeRef.current)) {
1212
+ ev.preventDefault();
1213
+ return;
1214
+ }
1215
+ const node = nodeRef.current;
1216
+ const scale = scaleOf(node);
1217
+ if (node) {
1218
+ const { x, y, width, height } = node.abs;
1219
+ grab.current = {
1220
+ x: ev.x - x / scale,
1221
+ y: ev.y - y / scale,
1222
+ width: width / scale,
1223
+ height: height / scale,
1224
+ };
1225
+ }
1226
+ // the window's own origin on screen, which the ghost's flight home
1227
+ // needs and no ref reports: the same point in both spaces, once.
1228
+ windowOrigin.current = { x: ev.screenX - ev.x, y: ev.screenY - ev.y };
1229
+ const { index, ids } = list.dragStarted(id);
1230
+ payload.current = {
1231
+ listUid: list.uid,
1232
+ list: list.id,
1233
+ id,
1234
+ ids,
1235
+ index,
1236
+ };
1237
+ activeDrag = {
1238
+ payload: payload.current,
1239
+ over: null,
1240
+ source: node,
1241
+ inline: ghostMode(list.preview) === 'inline',
1242
+ };
1243
+ setHoldingOwn(true);
1244
+ announceDragChanged();
1245
+ },
1246
+ onDrag: (ev) => {
1247
+ if (ownSource(ev))
1248
+ list.dragMoved(id);
1249
+ },
1250
+ onDragEnd: (ev) => {
1251
+ if (!ownSource(ev))
1252
+ return;
1253
+ setHoldingOwn(false);
1254
+ const carried = payload.current;
1255
+ payload.current = null;
1256
+ activeDrag = null;
1257
+ announceDragChanged();
1258
+ const from = lastGhost.current;
1259
+ lastGhost.current = null;
1260
+ list.dragEnded(id, ev, carried);
1261
+ if (from &&
1262
+ list.dropMs > 0 &&
1263
+ list.preview !== false &&
1264
+ flies(ev, carried)) {
1265
+ flyHome(from, list.dropMs);
1266
+ }
1267
+ },
1268
+ });
1269
+ // --- the keyboard --------------------------------------------------------
1270
+ const onKeyDown = (ev) => {
1271
+ if (disabled)
1272
+ return;
1273
+ const key = ev.keysym;
1274
+ const rtl = nodeRef.current?.direction === 'rtl';
1275
+ const [prev, next] = list.orientation === 'vertical'
1276
+ ? [XK_UP, XK_DOWN]
1277
+ : rtl
1278
+ ? [XK_RIGHT, XK_LEFT]
1279
+ : [XK_LEFT, XK_RIGHT];
1280
+ const isLifted = liftedRef.current;
1281
+ if (key === XK_RETURN || ev.codepoint === 32) {
1282
+ if (isLifted)
1283
+ list.drop();
1284
+ else
1285
+ list.lift(id);
1286
+ ev.preventDefault();
1287
+ return;
1288
+ }
1289
+ if (key === XK_ESCAPE) {
1290
+ if (!isLifted)
1291
+ return;
1292
+ list.cancel();
1293
+ ev.preventDefault();
1294
+ return;
1295
+ }
1296
+ const delta = key === prev ? -1 : key === next ? 1 : 0;
1297
+ if (delta !== 0) {
1298
+ if (isLifted)
1299
+ list.step(id, delta);
1300
+ else
1301
+ list.focusNeighbour(id, delta);
1302
+ ev.preventDefault();
1303
+ return;
1304
+ }
1305
+ if (isLifted && (key === XK_HOME || key === XK_END)) {
1306
+ list.step(id, key === XK_HOME ? 'home' : 'end');
1307
+ ev.preventDefault();
1308
+ }
1309
+ };
1310
+ /** Focus leaving a lifted item drops it where it is — a lifted item nobody
1311
+ * is holding is a list that will not sit still. */
1312
+ const onBlur = () => {
1313
+ if (liftedRef.current)
1314
+ list.drop();
1315
+ };
1316
+ const attachHandle = useCallback((ref) => {
1317
+ handleRef.current = ref;
1318
+ setHasHandle(true);
1319
+ return () => {
1320
+ if (handleRef.current === ref)
1321
+ handleRef.current = null;
1322
+ setHasHandle(false);
1323
+ };
1324
+ }, []);
1325
+ const drag = disabled ? NO_DRAG : dragProps;
1326
+ /** The hook reports a drag anywhere beneath this item; only ours counts. */
1327
+ const dragging = isDragging && holdingOwn;
1328
+ const moving = dragging || lifted ? list.movingIds(id) : [id];
1329
+ const state = {
1330
+ id,
1331
+ dragging,
1332
+ lifted,
1333
+ disabled,
1334
+ edge: mark?.edge ?? null,
1335
+ combining: Boolean(mark?.combine),
1336
+ accepted: (holdingOwn && position?.accepted) ?? false,
1337
+ preview: false,
1338
+ ids: moving,
1339
+ };
1340
+ // A new object per render, deliberately: `dragProps` is new per render
1341
+ // too, and what it keeps current is one handle box and whoever called
1342
+ // `useReorderItem()` — both of which want the state of *this* render.
1343
+ const itemShared = {
1344
+ state,
1345
+ disabled,
1346
+ dragProps: drag,
1347
+ onKeyDown,
1348
+ onBlur,
1349
+ styles: list.styles,
1350
+ attachHandle,
1351
+ };
1352
+ const previewState = { ...state, preview: true };
1353
+ const content = typeof children === 'function' ? children(state) : children;
1354
+ const size = typeof list.previewSize === 'function'
1355
+ ? list.previewSize(previewState)
1356
+ : (list.previewSize ?? grab.current);
1357
+ const ghostAt = holdingOwn &&
1358
+ position && {
1359
+ x: position.x - grab.current.x,
1360
+ y: position.y - grab.current.y,
1361
+ };
1362
+ if (dragging && ghostAt)
1363
+ lastGhost.current = ghostAt;
1364
+ // Where the ghost is drawn. A popup can leave the window and draws over
1365
+ // other applications; an in-window copy cannot, and is the opt-in.
1366
+ const mode = ghostMode(list.preview);
1367
+ /** The item's own origin on screen, which turns a pointer position into an
1368
+ * offset inside the item — what an inline ghost is placed by, and what the
1369
+ * drop flight eases to zero. */
1370
+ const originOnScreen = () => {
1371
+ const node = nodeRef.current;
1372
+ if (!node)
1373
+ return null;
1374
+ const scale = scaleOf(node);
1375
+ return {
1376
+ x: windowOrigin.current.x + node.abs.x / scale,
1377
+ y: windowOrigin.current.y + node.abs.y / scale,
1378
+ };
1379
+ };
1380
+ const origin = mode === 'inline' && ghostAt ? originOnScreen() : null;
1381
+ /** The offset the in-window copy is drawn at: the pointer's while the item
1382
+ * is held, and the flight's easing to zero once it has landed. One box
1383
+ * either way. */
1384
+ const inlineAt = flight ??
1385
+ (origin && ghostAt
1386
+ ? { x: ghostAt.x - origin.x, y: ghostAt.y - origin.y }
1387
+ : null);
1388
+ const showPopup = Boolean(ghostAt) && mode === 'popup';
1389
+ /** The look: the defaults, then the app's `style`. */
1390
+ const ownStyle = [itemStyle(state, hasHandle), style].filter((s) => Boolean(s));
1391
+ /** Whether the item paints its own ground, which decides whether the ghost
1392
+ * needs one of its own. */
1393
+ // `flattenStyle` takes one style or a flat list; ours may hold a nested
1394
+ // one, since `style` itself accepts an array.
1395
+ const hasGround = flattenStyle(ownStyle.flat()).backgroundColor !== undefined;
1396
+ /** The state, over the look: the lift, the wash, then the seam. */
1397
+ const stateStyle = [
1398
+ liftStyle(carrying),
1399
+ washStyle(state, theme),
1400
+ list.styles?.item?.(state) || null,
1401
+ ].filter((s) => Boolean(s));
1402
+ /** The ghost's content, drawn twice: once in the popup that follows the
1403
+ * pointer, once in the copy that flies home after the drop. */
1404
+ const ghostBody = () => h(ItemContext.Provider, { value: inertItem(previewState, list.styles) }, list.renderPreview
1405
+ ? list.renderPreview(previewState)
1406
+ : hx('box', {
1407
+ style: [
1408
+ // the ground goes *under* the item's own style, so a chip
1409
+ // keeps its corners and its width — see `previewStyle`
1410
+ previewStyle(theme, hasGround),
1411
+ ...ownStyle,
1412
+ // the seam sees the ghost too, with `preview` set
1413
+ list.styles?.item?.(previewState) || null,
1414
+ list.styles?.preview,
1415
+ ],
1416
+ }, typeof children === 'function' ? children(previewState) : children));
1417
+ return hx('box', {
1418
+ ref: nodeRef,
1419
+ role: 'listitem',
1420
+ 'aria-label': ariaLabel,
1421
+ 'aria-description': hasHandle || disabled ? undefined : HINT,
1422
+ 'data-testname': testname,
1423
+ // Every press is remembered, whether or not it becomes a drag: at the
1424
+ // threshold there is no event saying where the gesture began. Capture
1425
+ // phase, so a child that stops propagation cannot hide it.
1426
+ onMouseDownCapture: (ev) => {
1427
+ pressed.current = ev.target;
1428
+ },
1429
+ // without a handle the item is the drag source and the tab stop
1430
+ ...(hasHandle ? {} : drag),
1431
+ ...(hasHandle
1432
+ ? {}
1433
+ : {
1434
+ focusable: !disabled,
1435
+ onKeyDown: disabled ? undefined : onKeyDown,
1436
+ onBlur,
1437
+ }),
1438
+ style: [...ownStyle, ...stateStyle],
1439
+ }, h(ItemContext.Provider, { key: 'content', value: itemShared }, content), mark !== null &&
1440
+ hx('box', {
1441
+ key: 'indicator',
1442
+ 'data-testname': testname ? `${testname}-indicator` : undefined,
1443
+ style: [
1444
+ indicatorStyle(mark, list.orientation, theme),
1445
+ list.styles?.indicator,
1446
+ ],
1447
+ }), inlineAt !== null &&
1448
+ hx('box', {
1449
+ key: 'ghost',
1450
+ // the same box before and after the release: the preview while the
1451
+ // pointer holds it, the flight once it has landed
1452
+ 'data-testname': testname
1453
+ ? `${testname}-${flight ? 'flight' : 'preview'}`
1454
+ : undefined,
1455
+ style: {
1456
+ position: 'absolute',
1457
+ left: inlineAt.x,
1458
+ top: inlineAt.y,
1459
+ width: Math.max(1, Math.ceil(size.width)),
1460
+ height: Math.max(1, Math.ceil(size.height)),
1461
+ zIndex: 2,
1462
+ // the copy is a picture of the drop, never a target for the
1463
+ // press that follows it
1464
+ pointerEvents: 'none',
1465
+ },
1466
+ }, ghostBody()), showPopup &&
1467
+ ghostAt &&
1468
+ hx('popup', {
1469
+ key: 'preview',
1470
+ dragPreview: true,
1471
+ // a real window, and the card in it is rounded: see the header
1472
+ transparent: true,
1473
+ theme,
1474
+ x: Math.round(ghostAt.x),
1475
+ y: Math.round(ghostAt.y),
1476
+ width: Math.max(1, Math.ceil(size.width)),
1477
+ height: Math.max(1, Math.ceil(size.height)),
1478
+ 'data-testname': testname ? `${testname}-preview` : undefined,
1479
+ }, ghostBody(), moving.length > 1 &&
1480
+ hx('box', {
1481
+ key: 'badge',
1482
+ 'data-testname': testname ? `${testname}-count` : undefined,
1483
+ style: badgeStyle(theme),
1484
+ }, hx('text', { style: { fontSize: 11, color: theme.accentText } }, String(moving.length)))));
1485
+ }
1486
+ // --- the handle -------------------------------------------------------------
1487
+ /**
1488
+ * `<ReorderHandle>` — the one part of an item that drags it. Rendering one
1489
+ * inside a `<ReorderItem>` is the whole opt-in: the item stops being the
1490
+ * press target and the tab stop, and this is both.
1491
+ */
1492
+ export function ReorderHandle(props) {
1493
+ const { 'aria-label': ariaLabel = 'Drag to reorder', style, 'data-testname': testname, children, } = props;
1494
+ const item = useContext(ItemContext);
1495
+ if (!item) {
1496
+ throw new Error('@react-x11/components: <ReorderHandle> has to be inside a <ReorderItem>.');
1497
+ }
1498
+ const ref = useRef(null);
1499
+ const { attachHandle } = item;
1500
+ useLayoutEffect(() => attachHandle(ref), [attachHandle]);
1501
+ return hx('box', {
1502
+ ref,
1503
+ role: 'button',
1504
+ 'aria-label': ariaLabel,
1505
+ 'aria-description': item.disabled ? undefined : HINT,
1506
+ 'data-testname': testname,
1507
+ focusable: !item.disabled,
1508
+ ...item.dragProps,
1509
+ onKeyDown: item.disabled ? undefined : item.onKeyDown,
1510
+ onBlur: item.onBlur,
1511
+ style: [
1512
+ {
1513
+ flexDirection: 'row',
1514
+ alignItems: 'center',
1515
+ alignSelf: 'center',
1516
+ padding: 2,
1517
+ borderRadius: 3,
1518
+ cursor: item.disabled ? undefined : 'grab',
1519
+ },
1520
+ !item.disabled && { ':hover': { backgroundColor: '$surfaceHover' } },
1521
+ item.styles?.handle,
1522
+ style,
1523
+ ],
1524
+ }, children ??
1525
+ // two columns of core's three dots: the grip every list draws, made of
1526
+ // the affordance set rather than drawn here
1527
+ hx('box', { key: 'grip', style: { flexDirection: 'row' } }, h(Icon, {
1528
+ key: 'a',
1529
+ name: 'moreVertical',
1530
+ size: GRIP_DOT,
1531
+ color: '$textMuted',
1532
+ }), h(Icon, {
1533
+ key: 'b',
1534
+ name: 'moreVertical',
1535
+ size: GRIP_DOT,
1536
+ color: '$textMuted',
1537
+ })));
1538
+ }
1539
+ //# sourceMappingURL=index.js.map