@flamingo-stack/openframe-frontend-core 0.0.579-1952.5335.1 → 0.0.579

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 (236) hide show
  1. package/dist/chat-protocol/decode.d.ts.map +1 -1
  2. package/dist/chat-protocol/events.d.ts +47 -1
  3. package/dist/chat-protocol/events.d.ts.map +1 -1
  4. package/dist/chat-protocol/index.cjs +51 -0
  5. package/dist/chat-protocol/index.cjs.map +1 -1
  6. package/dist/chat-protocol/index.js +48 -1
  7. package/dist/chat-protocol/index.js.map +1 -1
  8. package/dist/chat-protocol/leading-frames.d.ts +9 -3
  9. package/dist/chat-protocol/leading-frames.d.ts.map +1 -1
  10. package/dist/chat-protocol/nats-decoder.d.ts +41 -0
  11. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
  12. package/dist/{chunk-3B42TFEE.cjs → chunk-36TJYCF3.cjs} +27 -27
  13. package/dist/{chunk-3B42TFEE.cjs.map → chunk-36TJYCF3.cjs.map} +1 -1
  14. package/dist/{chunk-YYEVFC3K.cjs → chunk-37XJYOAY.cjs} +17 -17
  15. package/dist/{chunk-YYEVFC3K.cjs.map → chunk-37XJYOAY.cjs.map} +1 -1
  16. package/dist/{chunk-DBP7S75Q.js → chunk-3AX3ZEY3.js} +1481 -1318
  17. package/dist/chunk-3AX3ZEY3.js.map +1 -0
  18. package/dist/{chunk-DGB2FLFH.cjs → chunk-4FJSN2XW.cjs} +10 -10
  19. package/dist/{chunk-DGB2FLFH.cjs.map → chunk-4FJSN2XW.cjs.map} +1 -1
  20. package/dist/{chunk-WHSSI3S7.cjs → chunk-4HGRXFQP.cjs} +3 -3
  21. package/dist/{chunk-WHSSI3S7.cjs.map → chunk-4HGRXFQP.cjs.map} +1 -1
  22. package/dist/{chunk-SY5HCZZO.cjs → chunk-4LZ7B5H5.cjs} +5 -5
  23. package/dist/{chunk-SY5HCZZO.cjs.map → chunk-4LZ7B5H5.cjs.map} +1 -1
  24. package/dist/{chunk-XJ45MHWI.js → chunk-4WSKSNXL.js} +9 -9
  25. package/dist/{chunk-ZCEQLHU3.js → chunk-5Y6TPFW7.js} +5 -5
  26. package/dist/{chunk-EVTRRDIH.cjs → chunk-A6C5HOD7.cjs} +23 -23
  27. package/dist/{chunk-EVTRRDIH.cjs.map → chunk-A6C5HOD7.cjs.map} +1 -1
  28. package/dist/{chunk-RXZWUJEB.js → chunk-AKPLEWMH.js} +2 -2
  29. package/dist/{chunk-7WKFLOC7.cjs → chunk-BCJQ7CJE.cjs} +11 -11
  30. package/dist/{chunk-7WKFLOC7.cjs.map → chunk-BCJQ7CJE.cjs.map} +1 -1
  31. package/dist/{chunk-C4KL7OQI.js → chunk-BW4WDUSI.js} +3 -3
  32. package/dist/{chunk-L66XBQM6.js → chunk-CP5EYMNL.js} +276 -160
  33. package/dist/chunk-CP5EYMNL.js.map +1 -0
  34. package/dist/{chunk-QOPQ6BCA.js → chunk-D44S2QLI.js} +3 -3
  35. package/dist/{chunk-CGVPUA4X.js → chunk-DB3TV4CW.js} +6 -6
  36. package/dist/{chunk-PITZMTGE.cjs → chunk-FEVLXRTC.cjs} +506 -390
  37. package/dist/chunk-FEVLXRTC.cjs.map +1 -0
  38. package/dist/{chunk-TYJITV3H.js → chunk-H4BPHGU5.js} +3 -3
  39. package/dist/{chunk-C7Q2PVHD.cjs → chunk-H54EPP3P.cjs} +7 -7
  40. package/dist/{chunk-C7Q2PVHD.cjs.map → chunk-H54EPP3P.cjs.map} +1 -1
  41. package/dist/{chunk-NWAUHBYH.js → chunk-HGA74X35.js} +4 -4
  42. package/dist/{chunk-4C3MS2G6.cjs → chunk-HHJVIHVR.cjs} +81 -81
  43. package/dist/{chunk-4C3MS2G6.cjs.map → chunk-HHJVIHVR.cjs.map} +1 -1
  44. package/dist/{chunk-3XFMM34I.cjs → chunk-IFYQ7DHV.cjs} +48 -48
  45. package/dist/{chunk-3XFMM34I.cjs.map → chunk-IFYQ7DHV.cjs.map} +1 -1
  46. package/dist/{chunk-QYQXAXVR.cjs → chunk-IKDC3R7V.cjs} +4 -3
  47. package/dist/chunk-IKDC3R7V.cjs.map +1 -0
  48. package/dist/{chunk-W6JA6SFD.js → chunk-JRYS4VMV.js} +45 -45
  49. package/dist/chunk-JRYS4VMV.js.map +1 -0
  50. package/dist/{chunk-AT4WAFJO.js → chunk-L2CF2QZW.js} +7 -7
  51. package/dist/{chunk-2BFZHTSD.cjs → chunk-LEISUAHF.cjs} +18 -8
  52. package/dist/chunk-LEISUAHF.cjs.map +1 -0
  53. package/dist/{chunk-AAHKPPGQ.cjs → chunk-M5GWCMYE.cjs} +42 -42
  54. package/dist/{chunk-AAHKPPGQ.cjs.map → chunk-M5GWCMYE.cjs.map} +1 -1
  55. package/dist/{chunk-PQETCTJ4.js → chunk-MXOSTJAG.js} +4 -3
  56. package/dist/chunk-MXOSTJAG.js.map +1 -0
  57. package/dist/{chunk-653A7FPP.cjs → chunk-NPVDKOGD.cjs} +22 -22
  58. package/dist/{chunk-653A7FPP.cjs.map → chunk-NPVDKOGD.cjs.map} +1 -1
  59. package/dist/{chunk-2ML5QEDH.js → chunk-O2FTVZOD.js} +5 -5
  60. package/dist/{chunk-OCSAB6NT.cjs → chunk-OGZAFM6M.cjs} +5 -5
  61. package/dist/{chunk-OCSAB6NT.cjs.map → chunk-OGZAFM6M.cjs.map} +1 -1
  62. package/dist/{chunk-CSBNQJSI.js → chunk-PMENIZXI.js} +6 -6
  63. package/dist/{chunk-PFTEG6L7.js → chunk-PWOYEUIQ.js} +7 -7
  64. package/dist/{chunk-KXLJT5U5.js → chunk-QCP2KV7P.js} +5 -5
  65. package/dist/{chunk-6QDW4CDG.js → chunk-QGJYWLF4.js} +2 -2
  66. package/dist/{chunk-VLRXLTR4.cjs → chunk-RKGOVTXZ.cjs} +377 -214
  67. package/dist/chunk-RKGOVTXZ.cjs.map +1 -0
  68. package/dist/{chunk-HM2R5MDR.js → chunk-T5HDPHN6.js} +13 -3
  69. package/dist/chunk-T5HDPHN6.js.map +1 -0
  70. package/dist/{chunk-GP2QLZIN.cjs → chunk-YW4WBNYV.cjs} +37 -37
  71. package/dist/{chunk-GP2QLZIN.cjs.map → chunk-YW4WBNYV.cjs.map} +1 -1
  72. package/dist/{chunk-36NFJULV.cjs → chunk-ZI27ALYX.cjs} +41 -41
  73. package/dist/chunk-ZI27ALYX.cjs.map +1 -0
  74. package/dist/components/case-studies/index.cjs +12 -12
  75. package/dist/components/case-studies/index.js +6 -6
  76. package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
  77. package/dist/components/chat/guide-display.d.ts +14 -0
  78. package/dist/components/chat/guide-display.d.ts.map +1 -0
  79. package/dist/components/chat/index.cjs +11 -3
  80. package/dist/components/chat/index.cjs.map +1 -1
  81. package/dist/components/chat/index.d.ts +1 -0
  82. package/dist/components/chat/index.d.ts.map +1 -1
  83. package/dist/components/chat/index.js +10 -2
  84. package/dist/components/chat/stream/chat-stream-reducer.d.ts +9 -0
  85. package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
  86. package/dist/components/chat/stream/delta-batcher.d.ts +3 -3
  87. package/dist/components/chat/stream/delta-batcher.d.ts.map +1 -1
  88. package/dist/components/chat/stream/message-mutations.d.ts.map +1 -1
  89. package/dist/components/chat/types/component.types.d.ts +6 -0
  90. package/dist/components/chat/types/component.types.d.ts.map +1 -1
  91. package/dist/components/chat/types/message.types.d.ts +30 -2
  92. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  93. package/dist/components/chat/types/network.types.d.ts +3 -1
  94. package/dist/components/chat/types/network.types.d.ts.map +1 -1
  95. package/dist/components/chat/types/processing.types.d.ts +4 -1
  96. package/dist/components/chat/types/processing.types.d.ts.map +1 -1
  97. package/dist/components/chat/utils/approval-display.d.ts +36 -8
  98. package/dist/components/chat/utils/approval-display.d.ts.map +1 -1
  99. package/dist/components/chat/utils/extract-incomplete-message-state.d.ts.map +1 -1
  100. package/dist/components/chat/utils/index.d.ts +1 -1
  101. package/dist/components/chat/utils/index.d.ts.map +1 -1
  102. package/dist/components/chat/utils/message-segment-accumulator.d.ts +16 -4
  103. package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
  104. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  105. package/dist/components/chat/utils/resolve-fetched-card-href.d.ts +1 -1
  106. package/dist/components/contact/index.cjs +7 -7
  107. package/dist/components/contact/index.js +6 -6
  108. package/dist/components/docs/index.cjs +10 -10
  109. package/dist/components/docs/index.js +9 -9
  110. package/dist/components/embeds/index.cjs +7 -7
  111. package/dist/components/embeds/index.js +6 -6
  112. package/dist/components/faq/index.cjs +7 -7
  113. package/dist/components/faq/index.js +6 -6
  114. package/dist/components/features/board/board.d.ts.map +1 -1
  115. package/dist/components/features/index.cjs +8 -6
  116. package/dist/components/features/index.cjs.map +1 -1
  117. package/dist/components/features/index.js +7 -5
  118. package/dist/components/features/sortable-list/index.d.ts +2 -1
  119. package/dist/components/features/sortable-list/index.d.ts.map +1 -1
  120. package/dist/components/features/sortable-list/sortable-list.d.ts +8 -1
  121. package/dist/components/features/sortable-list/sortable-list.d.ts.map +1 -1
  122. package/dist/components/features/sortable-list/sortable-move-buttons.d.ts +24 -0
  123. package/dist/components/features/sortable-list/sortable-move-buttons.d.ts.map +1 -0
  124. package/dist/components/features/sortable-list/use-sortable-item.d.ts +12 -4
  125. package/dist/components/features/sortable-list/use-sortable-item.d.ts.map +1 -1
  126. package/dist/components/features/ticket-status-config-list/ticket-status-config-list.d.ts +21 -5
  127. package/dist/components/features/ticket-status-config-list/ticket-status-config-list.d.ts.map +1 -1
  128. package/dist/components/help-center-pages/index.cjs +26 -26
  129. package/dist/components/help-center-pages/index.js +17 -17
  130. package/dist/components/index.cjs +139 -129
  131. package/dist/components/index.cjs.map +1 -1
  132. package/dist/components/index.js +26 -16
  133. package/dist/components/index.js.map +1 -1
  134. package/dist/components/meeting-scheduler/index.cjs +37 -37
  135. package/dist/components/meeting-scheduler/index.js +5 -5
  136. package/dist/components/navigation/index.cjs +9 -9
  137. package/dist/components/navigation/index.js +8 -8
  138. package/dist/components/onboarding-guides/index.cjs +9 -9
  139. package/dist/components/onboarding-guides/index.js +8 -8
  140. package/dist/components/tickets/index.cjs +10 -10
  141. package/dist/components/tickets/index.js +9 -9
  142. package/dist/components/ui/file-manager/index.cjs +4 -4
  143. package/dist/components/ui/file-manager/index.js +1 -1
  144. package/dist/components/ui/index.cjs +14 -6
  145. package/dist/components/ui/index.cjs.map +1 -1
  146. package/dist/components/ui/index.js +13 -5
  147. package/dist/components/ui/input.d.ts +2 -0
  148. package/dist/components/ui/input.d.ts.map +1 -1
  149. package/dist/components/ui/textarea.d.ts +2 -0
  150. package/dist/components/ui/textarea.d.ts.map +1 -1
  151. package/dist/components/ui/ticket-status-config-row.d.ts +10 -4
  152. package/dist/components/ui/ticket-status-config-row.d.ts.map +1 -1
  153. package/dist/hooks/index.cjs +7 -3
  154. package/dist/hooks/index.cjs.map +1 -1
  155. package/dist/hooks/index.js +6 -2
  156. package/dist/hooks/ui/index.d.ts +1 -0
  157. package/dist/hooks/ui/index.d.ts.map +1 -1
  158. package/dist/hooks/ui/use-drag-and-drop-enabled.d.ts +31 -0
  159. package/dist/hooks/ui/use-drag-and-drop-enabled.d.ts.map +1 -0
  160. package/dist/hooks/ui/use-media-query.d.ts.map +1 -1
  161. package/dist/index.cjs +25 -11
  162. package/dist/index.cjs.map +1 -1
  163. package/dist/index.js +23 -9
  164. package/dist/index.js.map +1 -1
  165. package/dist/utils/index.cjs.map +1 -1
  166. package/dist/utils/index.js.map +1 -1
  167. package/package.json +1 -4
  168. package/src/chat-protocol/__tests__/__snapshots__/nats-decoder-golden.test.ts.snap +80 -0
  169. package/src/chat-protocol/__tests__/nats-decoder-golden.test.ts +118 -1
  170. package/src/chat-protocol/decode.ts +4 -3
  171. package/src/chat-protocol/events.ts +51 -0
  172. package/src/chat-protocol/leading-frames.ts +9 -3
  173. package/src/chat-protocol/nats-decoder.ts +110 -1
  174. package/src/components/chat/__tests__/chat-pending-turn.test.tsx +19 -0
  175. package/src/components/chat/chat-message-enhanced.tsx +13 -1
  176. package/src/components/chat/guide-display.tsx +42 -0
  177. package/src/components/chat/index.ts +1 -0
  178. package/src/components/chat/stream/__tests__/chat-stream-reducer.test.ts +90 -0
  179. package/src/components/chat/stream/chat-dialog-store.ts +1 -1
  180. package/src/components/chat/stream/chat-stream-reducer.ts +49 -11
  181. package/src/components/chat/stream/delta-batcher.ts +9 -4
  182. package/src/components/chat/stream/message-mutations.ts +2 -0
  183. package/src/components/chat/types/component.types.ts +9 -0
  184. package/src/components/chat/types/message.types.ts +32 -0
  185. package/src/components/chat/types/network.types.ts +3 -1
  186. package/src/components/chat/types/processing.types.ts +4 -1
  187. package/src/components/chat/utils/__tests__/process-historical-messages-approvals.test.ts +93 -0
  188. package/src/components/chat/utils/__tests__/process-historical-messages-golden.test.ts +78 -1
  189. package/src/components/chat/utils/approval-display.ts +53 -8
  190. package/src/components/chat/utils/extract-incomplete-message-state.ts +1 -0
  191. package/src/components/chat/utils/index.ts +6 -1
  192. package/src/components/chat/utils/message-segment-accumulator.ts +37 -3
  193. package/src/components/chat/utils/process-historical-messages.ts +44 -7
  194. package/src/components/chat/utils/resolve-fetched-card-href.ts +1 -1
  195. package/src/components/features/board/.board.md +1 -0
  196. package/src/components/features/board/board.tsx +11 -1
  197. package/src/components/features/sortable-list/.sortable-list.md +23 -12
  198. package/src/components/features/sortable-list/index.ts +2 -1
  199. package/src/components/features/sortable-list/sortable-list.tsx +16 -5
  200. package/src/components/features/sortable-list/sortable-move-buttons.tsx +81 -0
  201. package/src/components/features/sortable-list/use-sortable-item.ts +25 -13
  202. package/src/components/features/ticket-status-config-list/ticket-status-config-list.tsx +55 -62
  203. package/src/components/ui/.ticket-status-config-row.md +11 -9
  204. package/src/components/ui/input.tsx +5 -2
  205. package/src/components/ui/textarea.tsx +5 -2
  206. package/src/components/ui/ticket-status-config-row.tsx +162 -70
  207. package/src/hooks/ui/.index.md +1 -0
  208. package/src/hooks/ui/.use-drag-and-drop-enabled.md +32 -0
  209. package/src/hooks/ui/index.ts +1 -0
  210. package/src/hooks/ui/use-drag-and-drop-enabled.ts +37 -0
  211. package/src/hooks/ui/use-media-query.ts +3 -2
  212. package/src/stories/ChatMessageEnhanced.stories.tsx +7 -3
  213. package/dist/chunk-2BFZHTSD.cjs.map +0 -1
  214. package/dist/chunk-36NFJULV.cjs.map +0 -1
  215. package/dist/chunk-DBP7S75Q.js.map +0 -1
  216. package/dist/chunk-HM2R5MDR.js.map +0 -1
  217. package/dist/chunk-L66XBQM6.js.map +0 -1
  218. package/dist/chunk-PITZMTGE.cjs.map +0 -1
  219. package/dist/chunk-PQETCTJ4.js.map +0 -1
  220. package/dist/chunk-QYQXAXVR.cjs.map +0 -1
  221. package/dist/chunk-VLRXLTR4.cjs.map +0 -1
  222. package/dist/chunk-W6JA6SFD.js.map +0 -1
  223. /package/dist/{chunk-XJ45MHWI.js.map → chunk-4WSKSNXL.js.map} +0 -0
  224. /package/dist/{chunk-ZCEQLHU3.js.map → chunk-5Y6TPFW7.js.map} +0 -0
  225. /package/dist/{chunk-RXZWUJEB.js.map → chunk-AKPLEWMH.js.map} +0 -0
  226. /package/dist/{chunk-C4KL7OQI.js.map → chunk-BW4WDUSI.js.map} +0 -0
  227. /package/dist/{chunk-QOPQ6BCA.js.map → chunk-D44S2QLI.js.map} +0 -0
  228. /package/dist/{chunk-CGVPUA4X.js.map → chunk-DB3TV4CW.js.map} +0 -0
  229. /package/dist/{chunk-TYJITV3H.js.map → chunk-H4BPHGU5.js.map} +0 -0
  230. /package/dist/{chunk-NWAUHBYH.js.map → chunk-HGA74X35.js.map} +0 -0
  231. /package/dist/{chunk-AT4WAFJO.js.map → chunk-L2CF2QZW.js.map} +0 -0
  232. /package/dist/{chunk-2ML5QEDH.js.map → chunk-O2FTVZOD.js.map} +0 -0
  233. /package/dist/{chunk-CSBNQJSI.js.map → chunk-PMENIZXI.js.map} +0 -0
  234. /package/dist/{chunk-PFTEG6L7.js.map → chunk-PWOYEUIQ.js.map} +0 -0
  235. /package/dist/{chunk-KXLJT5U5.js.map → chunk-QCP2KV7P.js.map} +0 -0
  236. /package/dist/{chunk-6QDW4CDG.js.map → chunk-QGJYWLF4.js.map} +0 -0
@@ -15,14 +15,23 @@ export interface SortableItem {
15
15
  * clicking inside the item keep working, and it carries the keyboard
16
16
  * alternative — so spread it rather than picking it apart.
17
17
  */
18
- dragHandleProps: {
19
- // Callback refs, not `Ref<HTMLElement>`: a ref OBJECT of the wider type
20
- // cannot be attached to a `<button>` or a `<div>`, a callback taking the
21
- // wider type can.
22
- ref: (node: HTMLElement | null) => void;
23
- onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
24
- };
18
+ dragHandleProps: SortableDragHandleProps;
25
19
  isDragging: boolean;
20
+ /**
21
+ * The input-type gate: `false` on touch/narrow viewports (and during SSR).
22
+ * When `false` the item registers no drag at all — render explicit move
23
+ * controls (`SortableMoveButtons`) instead of the drag handle.
24
+ */
25
+ dragAndDropEnabled: boolean;
26
+ }
27
+
28
+ /** What `dragHandleProps` carries — spread it onto the grip, don't pick it apart. */
29
+ export interface SortableDragHandleProps {
30
+ // Callback refs, not `Ref<HTMLElement>`: a ref OBJECT of the wider type
31
+ // cannot be attached to a `<button>` or a `<div>`, a callback taking the
32
+ // wider type can.
33
+ ref: (node: HTMLElement | null) => void;
34
+ onKeyDown: (event: KeyboardEvent<HTMLElement>) => void;
26
35
  }
27
36
 
28
37
  /**
@@ -38,7 +47,10 @@ export interface SortableItem {
38
47
  * screen-reader users, who hear the result from the list's live region.
39
48
  */
40
49
  export function useSortableItem(): SortableItem {
41
- const { dragType, disabled, reorder, readItems } = useSortableListContext();
50
+ const { dragType, disabled, dragAndDropEnabled, reorder, readItems } = useSortableListContext();
51
+ // Both stop the drag registration and the keyboard path; only the consumer's
52
+ // `disabled` also stops the move buttons, which are the touch replacement.
53
+ const inert = disabled || !dragAndDropEnabled;
42
54
  // State rather than refs: registration has to wait until the consumer has
43
55
  // actually rendered its grip, and only a re-render tells us it did.
44
56
  const [item, setItem] = useState<HTMLElement | null>(null);
@@ -54,7 +66,7 @@ export function useSortableItem(): SortableItem {
54
66
  }, []);
55
67
 
56
68
  useEffect(() => {
57
- if (!item || !handle || disabled) return undefined;
69
+ if (!item || !handle || inert) return undefined;
58
70
  return combine(
59
71
  // The drag has to start as a "move", or its first frame is drawn with the
60
72
  // copy cursor — see `drag-effect.ts`.
@@ -69,12 +81,12 @@ export function useSortableItem(): SortableItem {
69
81
  onDrop: () => setIsDragging(false),
70
82
  }),
71
83
  );
72
- }, [item, handle, disabled, dragType]);
84
+ }, [item, handle, inert, dragType]);
73
85
 
74
86
  const onKeyDown = useCallback(
75
87
  (event: KeyboardEvent<HTMLElement>) => {
76
88
  const step = event.key === 'ArrowUp' ? -1 : event.key === 'ArrowDown' ? 1 : 0;
77
- if (!step || disabled) return;
89
+ if (!step || inert) return;
78
90
  const items = readItems();
79
91
  const from = items.indexOf(event.currentTarget.closest(`[${SORTABLE_ITEM_ATTRIBUTE}]`) as HTMLElement);
80
92
  const to = from + step;
@@ -85,8 +97,8 @@ export function useSortableItem(): SortableItem {
85
97
  // run of presses keeps moving the same item.
86
98
  requestAnimationFrame(() => handle?.focus());
87
99
  },
88
- [disabled, readItems, reorder, handle],
100
+ [inert, readItems, reorder, handle],
89
101
  );
90
102
 
91
- return { itemRef, dragHandleProps: { ref: setHandle, onKeyDown }, isDragging };
103
+ return { itemRef, dragHandleProps: { ref: setHandle, onKeyDown }, isDragging, dragAndDropEnabled };
92
104
  }
@@ -1,94 +1,87 @@
1
1
  'use client';
2
2
 
3
- import {
4
- DndContext,
5
- type DragEndEvent,
6
- type DraggableAttributes,
7
- type DraggableSyntheticListeners,
8
- KeyboardSensor,
9
- PointerSensor,
10
- closestCenter,
11
- useSensor,
12
- useSensors,
13
- } from '@dnd-kit/core';
14
- import {
15
- SortableContext,
16
- sortableKeyboardCoordinates,
17
- useSortable,
18
- verticalListSortingStrategy,
19
- } from '@dnd-kit/sortable';
20
- import { CSS } from '@dnd-kit/utilities';
21
- import { type CSSProperties, type ReactNode, useMemo } from 'react';
3
+ import type { ReactNode } from 'react';
22
4
  import { cn } from '../../../utils/cn';
5
+ import { SortableList } from '../sortable-list/sortable-list';
6
+ import { SortableMoveButtons } from '../sortable-list/sortable-move-buttons';
7
+ import { type SortableDragHandleProps, useSortableItem } from '../sortable-list/use-sortable-item';
23
8
 
24
9
  export interface SortableRowRenderArgs {
25
- dragHandleProps: DraggableSyntheticListeners;
26
- dragHandleAttributes: DraggableAttributes;
10
+ /** Spread onto the grip whole — it carries the ref and the keyboard path. */
11
+ dragHandleProps: SortableDragHandleProps;
27
12
  isDragging: boolean;
13
+ /**
14
+ * The touch replacement for the grip: an up/down `SortableMoveButtons` pair,
15
+ * non-null exactly when drag is unavailable (touch/narrow viewports). Render
16
+ * it where the row's reorder control belongs — and no grip beside it.
17
+ */
18
+ moveButtons: ReactNode | null;
28
19
  }
29
20
 
30
21
  export interface TicketStatusConfigListProps<T extends { id: string }> {
31
22
  items: T[];
32
23
  onReorder: (oldIndex: number, newIndex: number) => void;
33
24
  renderRow: (item: T, args: SortableRowRenderArgs) => ReactNode;
25
+ /** Names a row for the live region and the move buttons, e.g. its status name. */
26
+ getItemLabel?: (index: number) => string | undefined;
34
27
  className?: string;
35
28
  }
36
29
 
30
+ /**
31
+ * A reorderable list of config rows (ticket statuses, AI quick actions), built
32
+ * on {@link SortableList} — Pragmatic drag and drop on desktop, an explicit
33
+ * up/down pair on touch, where the HTML5 drag events never fire.
34
+ *
35
+ * `id` keys React rows only; the reorder itself is DOM-order based, so
36
+ * duplicates in `items` cannot collide.
37
+ */
37
38
  export function TicketStatusConfigList<T extends { id: string }>({
38
39
  items,
39
40
  onReorder,
40
41
  renderRow,
42
+ getItemLabel,
41
43
  className,
42
44
  }: TicketStatusConfigListProps<T>) {
43
- const sensors = useSensors(
44
- useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
45
- useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
46
- );
47
-
48
- const handleDragEnd = (event: DragEndEvent) => {
49
- const { active, over } = event;
50
- if (!over || active.id === over.id) return;
51
- const oldIndex = items.findIndex(i => i.id === active.id);
52
- const newIndex = items.findIndex(i => i.id === over.id);
53
- if (oldIndex === -1 || newIndex === -1) return;
54
- onReorder(oldIndex, newIndex);
55
- };
56
-
57
- const sortableIds = useMemo(() => items.map(i => i.id), [items]);
58
-
59
45
  return (
60
- <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
61
- <SortableContext items={sortableIds} strategy={verticalListSortingStrategy}>
62
- <div className={cn('flex w-full flex-col gap-[var(--spacing-system-xs)]', className)}>
63
- {items.map(item => (
64
- <SortableRow key={item.id} id={item.id}>
65
- {args => renderRow(item, args)}
66
- </SortableRow>
67
- ))}
68
- </div>
69
- </SortableContext>
70
- </DndContext>
46
+ <SortableList
47
+ onReorder={onReorder}
48
+ getItemLabel={getItemLabel}
49
+ className={cn('flex w-full flex-col gap-[var(--spacing-system-xs)]', className)}
50
+ >
51
+ {items.map((item, index) => (
52
+ <SortableRow key={item.id} index={index} count={items.length} label={getItemLabel?.(index)}>
53
+ {args => renderRow(item, args)}
54
+ </SortableRow>
55
+ ))}
56
+ </SortableList>
71
57
  );
72
58
  }
73
59
 
74
- function SortableRow({ id, children }: { id: string; children: (args: SortableRowRenderArgs) => ReactNode }) {
75
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
76
-
77
- const style: CSSProperties = {
78
- transform: CSS.Transform.toString(transform),
79
- transition,
80
- // Raise the actively dragged row above its siblings — without this the
81
- // stacking follows DOM order, so a row dragged DOWN slides UNDER the rows
82
- // below it while a row dragged up correctly renders on top.
83
- zIndex: isDragging ? 1 : undefined,
84
- };
60
+ function SortableRow({
61
+ index,
62
+ count,
63
+ label,
64
+ children,
65
+ }: {
66
+ index: number;
67
+ count: number;
68
+ label?: string;
69
+ children: (args: SortableRowRenderArgs) => ReactNode;
70
+ }) {
71
+ const { itemRef, dragHandleProps, isDragging, dragAndDropEnabled } = useSortableItem();
85
72
 
86
73
  return (
87
- <div ref={setNodeRef} style={style}>
74
+ <div
75
+ ref={itemRef}
76
+ // Raise the actively dragged row above its siblings — without this the
77
+ // stacking follows DOM order, so a row dragged DOWN slides UNDER the rows
78
+ // below it while a row dragged up correctly renders on top.
79
+ className={isDragging ? 'relative z-10' : undefined}
80
+ >
88
81
  {children({
89
- dragHandleProps: listeners,
90
- dragHandleAttributes: attributes,
82
+ dragHandleProps,
91
83
  isDragging,
84
+ moveButtons: dragAndDropEnabled ? null : <SortableMoveButtons index={index} count={count} label={label} />,
92
85
  })}
93
86
  </div>
94
87
  );
@@ -1,14 +1,15 @@
1
- <!-- source-hash: af0402d9923c0e0c41315794a7eff4a9 -->
2
- A client-side React component that renders a single row in a ticket status configuration UI, supporting both system-defined and custom ticket statuses with drag-and-drop reordering, color picking, and delete controls.
1
+ <!-- source-hash: manual -->
2
+ A client-side React component that renders a single row in a ticket status configuration UI, supporting both system-defined and custom ticket statuses with reordering, color picking, and delete controls.
3
3
 
4
4
  ## Key Components
5
5
 
6
6
  ### `TicketStatusConfigRow`
7
7
  The primary exported component. Renders a row containing:
8
- - **Drag handle** — interactive for custom rows, decorative/disabled for system rows
9
- - **Name input** — editable for custom, read-only for system rows
10
- - **Color controls** — `ColorPresetSelect` and `ColorPickerInput` shown only for custom rows with color support
11
- - **Live preview** — `TicketStatusTag` reflecting current name and color
8
+ - **Drag handle** — interactive for custom rows, decorative/disabled for system rows. The whole rail is rendered only while `useDragAndDropEnabled()` holds; on touch/narrow viewports it disappears and the `moveButtons` node (an up/down `SortableMoveButtons` pair handed in by `TicketStatusConfigList`) renders beside the delete button instead
9
+ - **Name input** — editable for custom, read-only for system rows; carries a visible "Status Name" label (the rail, chip and controls offset below the label line to align with the field)
10
+ - **Color controls** — `ColorPresetSelect` (labelled "Color") and `ColorPickerInput` shown only for custom rows with color support
11
+ - **Live preview** — `TicketStatusTag` reflecting current name and color. Inline right on desktop (lg+); below the fields on tablet (own row, right-aligned) and mobile (beside the color select). System rows keep it beside the name everywhere
12
+ - **Transparent background** — only the border outlines the row; the dragged row gets an opaque `bg-ods-bg` so it can travel over siblings
12
13
  - **Action button** — info tooltip icon for system rows; delete button (with optional disabled state tooltip) for custom rows
13
14
 
14
15
  ### `WithLeftTooltip` (internal)
@@ -22,7 +23,8 @@ The full props interface, key fields:
22
23
  | `variant` | `'system'` or `'custom'` — controls editability and available actions |
23
24
  | `statusKey` | Canonical key forwarded to `TicketStatusTag` |
24
25
  | `onColorChange` | Callback receiving `{ color, preset? }` |
25
- | `dragHandleProps` / `dragHandleAttributes` | From `@dnd-kit/core` for drag-and-drop |
26
+ | `dragHandleProps` | `SortableDragHandleProps` from `useSortableItem` (spread whole onto the grip) |
27
+ | `moveButtons` | Touch fallback node (`SortableRowRenderArgs.moveButtons`), rendered beside delete |
26
28
  | `deleteDisabledReason` | Shown as tooltip when delete is disabled |
27
29
  | `isDragging` | Applies visual feedback during drag |
28
30
 
@@ -46,9 +48,9 @@ import { TicketStatusConfigRow } from './ticket-status-config-row'
46
48
  onDelete={handleDelete}
47
49
  deleteDisabled={isOnlyStatus}
48
50
  deleteDisabledReason="At least one status is required"
49
- dragHandleProps={listeners}
50
- dragHandleAttributes={attributes}
51
+ dragHandleProps={dragHandleProps}
51
52
  isDragging={isDragging}
53
+ moveButtons={moveButtons}
52
54
  />
53
55
 
54
56
  // System row (read-only, no delete)
@@ -14,6 +14,8 @@ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
14
14
  endAdornment?: ReactNode;
15
15
  /** Label text displayed above the input */
16
16
  label?: string;
17
+ /** Label scale forwarded to FieldWrapper ('large' = text-h4 for designs with body-scale field titles) */
18
+ labelVariant?: 'default' | 'large';
17
19
  /** Status message displayed below the input */
18
20
  error?: string;
19
21
  /** Color variant for the message: "error" (red), "warning" (yellow), "success" (green) or "muted" (grey).
@@ -37,6 +39,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
37
39
  startAdornment,
38
40
  endAdornment,
39
41
  label,
42
+ labelVariant,
40
43
  error,
41
44
  errorVariant = 'error',
42
45
  loading = false,
@@ -75,7 +78,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
75
78
  />
76
79
  );
77
80
  return label ? (
78
- <FieldWrapper label={label} error={error} errorVariant={errorVariant}>
81
+ <FieldWrapper label={label} labelVariant={labelVariant} error={error} errorVariant={errorVariant}>
79
82
  {rangeInput}
80
83
  </FieldWrapper>
81
84
  ) : (
@@ -149,7 +152,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
149
152
  );
150
153
 
151
154
  return (
152
- <FieldWrapper label={label} error={error} errorVariant={errorVariant}>
155
+ <FieldWrapper label={label} labelVariant={labelVariant} error={error} errorVariant={errorVariant}>
153
156
  {content}
154
157
  </FieldWrapper>
155
158
  );
@@ -10,6 +10,8 @@ export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElemen
10
10
  invalid?: boolean;
11
11
  /** Label text displayed above the textarea */
12
12
  label?: string;
13
+ /** Label scale forwarded to FieldWrapper ('large' = text-h4 for designs with body-scale field titles) */
14
+ labelVariant?: 'default' | 'large';
13
15
  /** Error message displayed below the textarea */
14
16
  error?: string;
15
17
  /** Element rendered at the right edge of the field (e.g. send icon). */
@@ -41,6 +43,7 @@ const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
41
43
  className,
42
44
  invalid = false,
43
45
  label,
46
+ labelVariant,
44
47
  error,
45
48
  endIcon,
46
49
  endIconAsButton = false,
@@ -66,7 +69,7 @@ const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
66
69
  // border/bg/hover all live on the textarea itself.
67
70
  if (!hasAdornment) {
68
71
  return (
69
- <FieldWrapper label={label} error={error}>
72
+ <FieldWrapper label={label} labelVariant={labelVariant} error={error}>
70
73
  <textarea
71
74
  className={cn(
72
75
  'flex min-h-[96px] w-full rounded-[6px] border p-3',
@@ -214,7 +217,7 @@ const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
214
217
  );
215
218
 
216
219
  return (
217
- <FieldWrapper label={label} error={error}>
220
+ <FieldWrapper label={label} labelVariant={labelVariant} error={error}>
218
221
  {content}
219
222
  </FieldWrapper>
220
223
  );
@@ -1,13 +1,16 @@
1
1
  'use client';
2
2
 
3
- import type { DraggableAttributes, DraggableSyntheticListeners } from '@dnd-kit/core';
3
+ import { type ReactNode, useId } from 'react';
4
+ import { useDragAndDropEnabled } from '../../hooks/ui/use-drag-and-drop-enabled';
4
5
  import { cn } from '../../utils/cn';
6
+ import type { SortableDragHandleProps } from '../features/sortable-list/use-sortable-item';
5
7
  import { DraggerIcon } from '../icons-v2-generated/interface/dragger-icon';
6
8
  import { TrashIcon } from '../icons-v2-generated/interface/trash-icon';
7
9
  import { InfoCircleIcon } from '../icons-v2-generated/signs-and-symbols/info-circle-icon';
8
10
  import { Button } from './button';
9
11
  import { ColorPresetSelect, ColorPickerInput } from './color-preset-select';
10
12
  import { Input } from './input';
13
+ import { Label } from './label';
11
14
  import type { TagProps } from './tag';
12
15
  import { TicketStatusTag } from './ticket-status-tag';
13
16
  import { TouchFriendlyTooltip } from './touch-friendly-tooltip';
@@ -31,9 +34,14 @@ export interface TicketStatusConfigRowProps {
31
34
  onDelete?: () => void;
32
35
  deleteDisabled?: boolean;
33
36
  deleteDisabledReason?: string;
34
- dragHandleProps?: DraggableSyntheticListeners;
35
- dragHandleAttributes?: DraggableAttributes;
37
+ /** From `useSortableItem` (via `TicketStatusConfigList`'s render args). */
38
+ dragHandleProps?: SortableDragHandleProps;
36
39
  isDragging?: boolean;
40
+ /**
41
+ * The touch replacement for the drag handle (`SortableRowRenderArgs.moveButtons`).
42
+ * Rendered beside the delete button; the drag rail is hidden on touch anyway.
43
+ */
44
+ moveButtons?: ReactNode;
37
45
  }
38
46
 
39
47
  export function TicketStatusConfigRow({
@@ -50,9 +58,13 @@ export function TicketStatusConfigRow({
50
58
  deleteDisabled,
51
59
  deleteDisabledReason,
52
60
  dragHandleProps,
53
- dragHandleAttributes,
54
61
  isDragging,
62
+ moveButtons,
55
63
  }: TicketStatusConfigRowProps) {
64
+ // Standalone system rows render outside any SortableList, so the row answers
65
+ // "is there a drag rail at all?" itself — same media gate the list uses.
66
+ const dragAndDropEnabled = useDragAndDropEnabled();
67
+ const nameInputId = useId();
56
68
  const isSystem = variant === 'system';
57
69
  const isCustomColor = !isSystem && presetKey === undefined;
58
70
  const previewColor = isSystem ? undefined : color;
@@ -60,101 +72,181 @@ export function TicketStatusConfigRow({
60
72
  // Custom rows render as filled pills (no border); system rows take the
61
73
  // page-configured variant (outline/primary). text-h5 already uppercases.
62
74
  const previewVariant: SystemTagVariant | undefined = systemTagVariant ?? (isSystem ? undefined : 'primary');
63
- const colClass = 'min-w-0 grow basis-[calc(50%-6px)] md:basis-0';
75
+
76
+ // The fields carry a visible label, so everything that sits beside the
77
+ // 44/48px field row (rail, chip, controls) drops below the label line by
78
+ // exactly its height: the h4 line + the label's mb-1.
79
+ const fieldRowOffset = 'mt-[calc(var(--font-line-space-h4-body)+0.25rem)]';
80
+
81
+ const chip = (
82
+ <TicketStatusTag
83
+ status={statusKey}
84
+ label={name || ' '}
85
+ color={previewColor}
86
+ variant={previewVariant}
87
+ showIcon={isSystem}
88
+ className="max-w-full"
89
+ />
90
+ );
64
91
 
65
92
  return (
66
93
  <div
67
94
  className={cn(
68
- 'flex w-full items-start gap-3 rounded-md border border-ods-border bg-ods-card md:gap-[var(--spacing-system-m)]',
95
+ // Transparent background — only the border outlines the row.
96
+ 'flex w-full items-start gap-3 rounded-md border border-ods-border md:gap-[var(--spacing-system-m)]',
69
97
  'p-[var(--spacing-system-m)]',
70
- isDragging && 'opacity-70 shadow-lg',
98
+ // The dragged row travels over its siblings, so it needs an opaque back.
99
+ isDragging && 'bg-ods-bg opacity-70 shadow-lg',
71
100
  )}
72
101
  >
73
- <div className="flex h-11 w-8 shrink-0 items-center justify-center text-ods-text-secondary md:h-12">
74
- {isSystem ? (
75
- <DraggerIcon size={24} aria-hidden className="opacity-40" />
76
- ) : (
77
- <button
78
- type="button"
79
- aria-label="Drag to reorder"
80
- className="flex size-6 cursor-grab items-center justify-center rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ods-focus active:cursor-grabbing"
81
- {...dragHandleAttributes}
82
- {...dragHandleProps}
83
- >
84
- <DraggerIcon size={24} />
85
- </button>
86
- )}
87
- </div>
102
+ {dragAndDropEnabled && (
103
+ <div
104
+ className={cn(
105
+ 'flex h-11 w-8 shrink-0 items-center justify-center text-ods-text-secondary md:h-12',
106
+ fieldRowOffset,
107
+ )}
108
+ >
109
+ {isSystem ? (
110
+ <DraggerIcon size={24} aria-hidden className="opacity-40" />
111
+ ) : (
112
+ <button
113
+ type="button"
114
+ aria-label="Drag to reorder"
115
+ className="flex size-6 cursor-grab items-center justify-center rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ods-focus active:cursor-grabbing"
116
+ {...dragHandleProps}
117
+ >
118
+ <DraggerIcon size={24} />
119
+ </button>
120
+ )}
121
+ </div>
122
+ )}
88
123
 
89
- <div className="flex min-w-0 flex-1 flex-wrap items-start gap-x-3 gap-y-4 md:flex-nowrap md:gap-[var(--spacing-system-m)]">
90
- <div className={cn('flex flex-col gap-[var(--spacing-system-xxs)]', colClass)}>
124
+ {/* Every row spans the same equal column slots — two on tablet
125
+ (name + color for custom, name + chip for system), four on desktop
126
+ (name, color, a reserved slot, chip zone) with invisible spacers
127
+ standing in for the cells a row doesn't use — so the name field is one
128
+ width on every row and the columns align across the whole list.
129
+ The trailing controls are a WRAP ITEM (ordered per breakpoint), not a
130
+ full-height sibling column, so the lower rows span the full card width
131
+ and the chip pins to the card's right edge on tablet/mobile. */}
132
+ <div
133
+ className={cn(
134
+ 'min-w-0 flex-1 items-start gap-x-3 gap-y-[var(--spacing-system-m)] md:gap-x-[var(--spacing-system-m)]',
135
+ isSystem
136
+ ? 'flex flex-wrap'
137
+ : // Mobile custom rows are a strict two-column grid, per the mock:
138
+ // fields (name / color / hex, all one width) on the left, the
139
+ // controls and the chip stacked in the right column. From md up
140
+ // the same children flow as a wrapping flex row.
141
+ 'grid grid-cols-[minmax(0,1fr)_auto] md:flex md:flex-wrap',
142
+ )}
143
+ >
144
+ {/* System rows keep the chip beside the name on every width, so the name
145
+ shares its row. */}
146
+ <div className="order-1 flex min-w-0 grow basis-0 flex-col gap-[var(--spacing-system-xxs)]">
147
+ <Label variant="large" htmlFor={nameInputId}>
148
+ Status Name
149
+ </Label>
91
150
  <Input
151
+ id={nameInputId}
92
152
  value={name}
93
153
  onChange={e => onNameChange?.(e.target.value)}
94
154
  disabled={isSystem}
95
155
  readOnly={isSystem}
96
- aria-label="Status name"
97
156
  maxLength={50}
98
157
  />
99
158
  </div>
100
159
 
101
160
  {showColorPicker ? (
102
- <div className={cn('flex', colClass)}>
161
+ <div className="order-3 flex min-w-0 grow basis-full flex-col gap-[var(--spacing-system-xxs)] md:order-2 md:basis-0">
162
+ <Label variant="large">Color</Label>
103
163
  <ColorPresetSelect value={color} presetKey={presetKey} onChange={onColorChange} />
164
+ {/* Custom hex: a second field row inside the Color column, directly
165
+ under the select (per design). */}
166
+ {isCustomColor && (
167
+ <div className="mt-[var(--spacing-system-xxs)] flex min-w-0">
168
+ <ColorPickerInput value={color} onChange={next => onColorChange({ color: next, preset: undefined })} />
169
+ </div>
170
+ )}
104
171
  </div>
105
172
  ) : (
106
- <div aria-hidden className={cn('hidden md:block', colClass)} />
173
+ <div aria-hidden className="hidden min-w-0 grow basis-0 lg:order-2 lg:block" />
107
174
  )}
108
175
 
109
- {showColorPicker && isCustomColor ? (
110
- <div className={cn('flex', colClass)}>
111
- <ColorPickerInput value={color} onChange={next => onColorChange({ color: next, preset: undefined })} />
176
+ {/* Reserved desktop column slot (the mock's "No activity Indicator" zone). */}
177
+ <div aria-hidden className="hidden min-w-0 grow basis-0 lg:order-3 lg:block" />
178
+
179
+ {isSystem ? (
180
+ <div
181
+ className={cn(
182
+ 'order-2 flex h-11 min-w-0 grow basis-0 items-center justify-end md:h-12 lg:order-4',
183
+ fieldRowOffset,
184
+ )}
185
+ >
186
+ {chip}
112
187
  </div>
113
188
  ) : (
114
- <div aria-hidden className={cn('hidden md:block', colClass)} />
189
+ <>
190
+ {/* Desktop (lg+): the chip rides the first row, in its own column
191
+ before the controls. */}
192
+ <div
193
+ className={cn(
194
+ 'hidden h-12 min-w-0 grow basis-0 items-center justify-end lg:order-4 lg:flex',
195
+ fieldRowOffset,
196
+ )}
197
+ >
198
+ {chip}
199
+ </div>
200
+ {/* Tablet: the chip takes a full row of its own, pinned to the
201
+ card's right edge (the controls above no longer narrow it). */}
202
+ <div className="hidden min-w-0 basis-full items-center justify-end md:order-4 md:flex lg:hidden">
203
+ {chip}
204
+ </div>
205
+ {/* Mobile: the chip fills the second grid column beside the color
206
+ field, left-aligned with the controls above it. */}
207
+ <div className={cn('order-4 flex h-11 min-w-0 items-center md:hidden', fieldRowOffset)}>{chip}</div>
208
+ </>
115
209
  )}
116
210
 
117
- <div className={cn('flex h-11 items-center justify-start md:h-12 md:justify-end', colClass)}>
118
- <TicketStatusTag
119
- status={statusKey}
120
- label={name || ' '}
121
- color={previewColor}
122
- variant={previewVariant}
123
- showIcon={isSystem}
124
- className="max-w-full"
125
- />
211
+ <div
212
+ className={cn(
213
+ 'flex h-11 shrink-0 items-center justify-end gap-[var(--spacing-system-s)] md:h-12 lg:order-5',
214
+ isSystem ? 'order-3' : 'order-2 md:order-3',
215
+ fieldRowOffset,
216
+ )}
217
+ >
218
+ {!isSystem && moveButtons}
219
+ {isSystem ? (
220
+ <div className="flex w-11 justify-center md:w-12">
221
+ <TouchFriendlyTooltip content={systemTooltip}>
222
+ <button
223
+ type="button"
224
+ aria-label={systemTooltip ?? 'System status'}
225
+ className="flex size-6 items-center justify-center text-ods-text-secondary outline-none focus-visible:ring-2 focus-visible:ring-ods-focus"
226
+ >
227
+ <InfoCircleIcon size={24} />
228
+ </button>
229
+ </TouchFriendlyTooltip>
230
+ </div>
231
+ ) : (
232
+ <TouchFriendlyTooltip content={deleteDisabled ? deleteDisabledReason : undefined}>
233
+ <span className="inline-flex">
234
+ <Button
235
+ type="button"
236
+ variant="outline"
237
+ size="icon"
238
+ aria-label="Delete status"
239
+ disabled={deleteDisabled}
240
+ onClick={deleteDisabled ? undefined : onDelete}
241
+ className={cn('size-11 md:size-12', deleteDisabled && 'pointer-events-none')}
242
+ >
243
+ <TrashIcon className="text-ods-error" />
244
+ </Button>
245
+ </span>
246
+ </TouchFriendlyTooltip>
247
+ )}
126
248
  </div>
127
249
  </div>
128
-
129
- <div className="flex h-11 w-12 shrink-0 items-center justify-center md:h-12">
130
- {isSystem ? (
131
- <TouchFriendlyTooltip content={systemTooltip}>
132
- <button
133
- type="button"
134
- aria-label={systemTooltip ?? 'System status'}
135
- className="flex size-6 items-center justify-center text-ods-text-secondary outline-none focus-visible:ring-2 focus-visible:ring-ods-focus"
136
- >
137
- <InfoCircleIcon size={24} />
138
- </button>
139
- </TouchFriendlyTooltip>
140
- ) : (
141
- <TouchFriendlyTooltip content={deleteDisabled ? deleteDisabledReason : undefined}>
142
- <span className="inline-flex">
143
- <Button
144
- type="button"
145
- variant="outline"
146
- size="icon"
147
- aria-label="Delete status"
148
- disabled={deleteDisabled}
149
- onClick={deleteDisabled ? undefined : onDelete}
150
- className={cn('size-11 md:size-12', deleteDisabled && 'pointer-events-none')}
151
- >
152
- <TrashIcon className="text-ods-error" />
153
- </Button>
154
- </span>
155
- </TouchFriendlyTooltip>
156
- )}
157
- </div>
158
250
  </div>
159
251
  );
160
252
  }
@@ -8,6 +8,7 @@ Barrel export file for the UI hooks library, re-exporting all custom React hooks
8
8
  | `useAutoLimitTags` | Limits visible tags based on container width |
9
9
  | `useDebounce` | Delays value updates by a specified duration |
10
10
  | `useDeferredError` | Defers error state for async workflows |
11
+ | `useDragAndDropEnabled` | Whether DnD is initialized at all — fine pointer + hover + 800px, else touch fallbacks |
11
12
  | `useFocusTrap` | Traps keyboard focus within a DOM subtree |
12
13
  | `useHeaderHeight` | Tracks the application header's pixel height |
13
14
  | `useHorizontalScrollbar` | Detects and manages horizontal scrollbar presence |