@flamingo-stack/openframe-frontend-core 0.0.577 → 0.0.578-snapshot.20260831161155

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 (228) hide show
  1. package/dist/{chunk-XBSW7IF6.cjs → chunk-36TJYCF3.cjs} +27 -27
  2. package/dist/{chunk-XBSW7IF6.cjs.map → chunk-36TJYCF3.cjs.map} +1 -1
  3. package/dist/{chunk-ET3Q4FIZ.cjs → chunk-37XJYOAY.cjs} +17 -17
  4. package/dist/{chunk-ET3Q4FIZ.cjs.map → chunk-37XJYOAY.cjs.map} +1 -1
  5. package/dist/{chunk-4D2BR756.js → chunk-3AX3ZEY3.js} +53 -32
  6. package/dist/chunk-3AX3ZEY3.js.map +1 -0
  7. package/dist/{chunk-JQI726T3.js → chunk-3LMYQGAJ.js} +3 -2
  8. package/dist/chunk-3LMYQGAJ.js.map +1 -0
  9. package/dist/{chunk-XBBCUC32.cjs → chunk-4FJSN2XW.cjs} +15 -15
  10. package/dist/{chunk-XBBCUC32.cjs.map → chunk-4FJSN2XW.cjs.map} +1 -1
  11. package/dist/{chunk-RCLJSENK.cjs → chunk-4HGRXFQP.cjs} +6 -6
  12. package/dist/{chunk-RCLJSENK.cjs.map → chunk-4HGRXFQP.cjs.map} +1 -1
  13. package/dist/{chunk-NQCGDZJ3.cjs → chunk-4LZ7B5H5.cjs} +5 -5
  14. package/dist/{chunk-NQCGDZJ3.cjs.map → chunk-4LZ7B5H5.cjs.map} +1 -1
  15. package/dist/{chunk-IQFXDS5O.js → chunk-4WSKSNXL.js} +10 -10
  16. package/dist/{chunk-NEKT67O5.js → chunk-5Y6TPFW7.js} +6 -6
  17. package/dist/{chunk-XG2WNF4L.cjs → chunk-A6C5HOD7.cjs} +27 -27
  18. package/dist/{chunk-XG2WNF4L.cjs.map → chunk-A6C5HOD7.cjs.map} +1 -1
  19. package/dist/{chunk-AQ5MZVYN.js → chunk-AKPLEWMH.js} +3 -3
  20. package/dist/{chunk-HSOMYCUG.cjs → chunk-BCJQ7CJE.cjs} +13 -13
  21. package/dist/{chunk-HSOMYCUG.cjs.map → chunk-BCJQ7CJE.cjs.map} +1 -1
  22. package/dist/{chunk-RUJKOU5F.js → chunk-BW4WDUSI.js} +3 -3
  23. package/dist/{chunk-NP3XN6ZX.js → chunk-CP5EYMNL.js} +278 -162
  24. package/dist/chunk-CP5EYMNL.js.map +1 -0
  25. package/dist/{chunk-FCVAVOXS.js → chunk-D44S2QLI.js} +3 -3
  26. package/dist/{chunk-U3SDSA5K.js → chunk-DB3TV4CW.js} +7 -7
  27. package/dist/{chunk-KE2TCUOH.cjs → chunk-FEVLXRTC.cjs} +537 -421
  28. package/dist/chunk-FEVLXRTC.cjs.map +1 -0
  29. package/dist/{chunk-ZBJ6XSDO.js → chunk-H4BPHGU5.js} +4 -4
  30. package/dist/{chunk-6FJABKPX.cjs → chunk-H54EPP3P.cjs} +9 -9
  31. package/dist/{chunk-6FJABKPX.cjs.map → chunk-H54EPP3P.cjs.map} +1 -1
  32. package/dist/{chunk-4KR7JWKB.js → chunk-HGA74X35.js} +5 -5
  33. package/dist/{chunk-5DPCIIJ7.cjs → chunk-HHJVIHVR.cjs} +92 -92
  34. package/dist/{chunk-5DPCIIJ7.cjs.map → chunk-HHJVIHVR.cjs.map} +1 -1
  35. package/dist/{chunk-WOHHZZJK.cjs → chunk-IFYQ7DHV.cjs} +62 -62
  36. package/dist/{chunk-WOHHZZJK.cjs.map → chunk-IFYQ7DHV.cjs.map} +1 -1
  37. package/dist/{chunk-QYQXAXVR.cjs → chunk-IKDC3R7V.cjs} +4 -3
  38. package/dist/chunk-IKDC3R7V.cjs.map +1 -0
  39. package/dist/{chunk-W6JA6SFD.js → chunk-JRYS4VMV.js} +45 -45
  40. package/dist/chunk-JRYS4VMV.js.map +1 -0
  41. package/dist/{chunk-2YCUEUIZ.js → chunk-L2CF2QZW.js} +8 -8
  42. package/dist/{chunk-2BFZHTSD.cjs → chunk-LEISUAHF.cjs} +18 -8
  43. package/dist/chunk-LEISUAHF.cjs.map +1 -0
  44. package/dist/{chunk-GDWBJFMG.cjs → chunk-M5GWCMYE.cjs} +42 -42
  45. package/dist/{chunk-GDWBJFMG.cjs.map → chunk-M5GWCMYE.cjs.map} +1 -1
  46. package/dist/{chunk-JCH6HGQD.js → chunk-MCWAZ5XI.js} +2 -2
  47. package/dist/{chunk-PQETCTJ4.js → chunk-MXOSTJAG.js} +4 -3
  48. package/dist/chunk-MXOSTJAG.js.map +1 -0
  49. package/dist/{chunk-MIY5FYX3.cjs → chunk-NNFQLTJ3.cjs} +4 -3
  50. package/dist/chunk-NNFQLTJ3.cjs.map +1 -0
  51. package/dist/{chunk-CFTZAZ7R.cjs → chunk-NPVDKOGD.cjs} +28 -28
  52. package/dist/{chunk-CFTZAZ7R.cjs.map → chunk-NPVDKOGD.cjs.map} +1 -1
  53. package/dist/{chunk-LYOGSVFO.js → chunk-O2FTVZOD.js} +5 -5
  54. package/dist/{chunk-CIUYHBAY.cjs → chunk-OGZAFM6M.cjs} +5 -5
  55. package/dist/{chunk-CIUYHBAY.cjs.map → chunk-OGZAFM6M.cjs.map} +1 -1
  56. package/dist/{chunk-THDGDYJ3.js → chunk-PMENIZXI.js} +7 -7
  57. package/dist/{chunk-TU32TSAK.js → chunk-PWOYEUIQ.js} +7 -7
  58. package/dist/{chunk-RSMUAAPW.js → chunk-QCP2KV7P.js} +5 -5
  59. package/dist/{chunk-RNUH5IHZ.js → chunk-QGJYWLF4.js} +3 -3
  60. package/dist/{chunk-75X6BRID.cjs → chunk-RKGOVTXZ.cjs} +500 -479
  61. package/dist/chunk-RKGOVTXZ.cjs.map +1 -0
  62. package/dist/{chunk-HM2R5MDR.js → chunk-T5HDPHN6.js} +13 -3
  63. package/dist/chunk-T5HDPHN6.js.map +1 -0
  64. package/dist/{chunk-GPWU3PPR.cjs → chunk-VEHZNQ77.cjs} +30 -30
  65. package/dist/{chunk-GPWU3PPR.cjs.map → chunk-VEHZNQ77.cjs.map} +1 -1
  66. package/dist/{chunk-APFGR2KK.cjs → chunk-YW4WBNYV.cjs} +41 -41
  67. package/dist/{chunk-APFGR2KK.cjs.map → chunk-YW4WBNYV.cjs.map} +1 -1
  68. package/dist/{chunk-36NFJULV.cjs → chunk-ZI27ALYX.cjs} +41 -41
  69. package/dist/chunk-ZI27ALYX.cjs.map +1 -0
  70. package/dist/components/case-studies/index.cjs +13 -13
  71. package/dist/components/case-studies/index.js +7 -7
  72. package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
  73. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  74. package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
  75. package/dist/components/chat/hooks/use-unified-chat.d.ts.map +1 -1
  76. package/dist/components/chat/index.cjs +4 -4
  77. package/dist/components/chat/index.js +3 -3
  78. package/dist/components/chat/nav-link-anchor-via-runtime.d.ts.map +1 -1
  79. package/dist/components/chat/types/unified-chat-state.types.d.ts +9 -2
  80. package/dist/components/chat/types/unified-chat-state.types.d.ts.map +1 -1
  81. package/dist/components/chat/utils/chat-nav-resolution.d.ts +9 -3
  82. package/dist/components/chat/utils/chat-nav-resolution.d.ts.map +1 -1
  83. package/dist/components/chat/utils/execute-navigation.d.ts +11 -0
  84. package/dist/components/chat/utils/execute-navigation.d.ts.map +1 -1
  85. package/dist/components/chat/utils/index.d.ts +1 -1
  86. package/dist/components/chat/utils/index.d.ts.map +1 -1
  87. package/dist/components/chat/utils/nav-anchor-props.d.ts +4 -0
  88. package/dist/components/chat/utils/nav-anchor-props.d.ts.map +1 -1
  89. package/dist/components/chat/utils/resolve-fetched-card-href.d.ts +42 -3
  90. package/dist/components/chat/utils/resolve-fetched-card-href.d.ts.map +1 -1
  91. package/dist/components/contact/index.cjs +8 -8
  92. package/dist/components/contact/index.js +7 -7
  93. package/dist/components/docs/index.cjs +11 -11
  94. package/dist/components/docs/index.js +10 -10
  95. package/dist/components/embeds/index.cjs +8 -8
  96. package/dist/components/embeds/index.js +7 -7
  97. package/dist/components/faq/index.cjs +8 -8
  98. package/dist/components/faq/index.js +7 -7
  99. package/dist/components/features/board/board.d.ts.map +1 -1
  100. package/dist/components/features/index.cjs +9 -7
  101. package/dist/components/features/index.cjs.map +1 -1
  102. package/dist/components/features/index.js +8 -6
  103. package/dist/components/features/sortable-list/index.d.ts +2 -1
  104. package/dist/components/features/sortable-list/index.d.ts.map +1 -1
  105. package/dist/components/features/sortable-list/sortable-list.d.ts +8 -1
  106. package/dist/components/features/sortable-list/sortable-list.d.ts.map +1 -1
  107. package/dist/components/features/sortable-list/sortable-move-buttons.d.ts +24 -0
  108. package/dist/components/features/sortable-list/sortable-move-buttons.d.ts.map +1 -0
  109. package/dist/components/features/sortable-list/use-sortable-item.d.ts +12 -4
  110. package/dist/components/features/sortable-list/use-sortable-item.d.ts.map +1 -1
  111. package/dist/components/features/ticket-status-config-list/ticket-status-config-list.d.ts +21 -5
  112. package/dist/components/features/ticket-status-config-list/ticket-status-config-list.d.ts.map +1 -1
  113. package/dist/components/help-center-pages/index.cjs +27 -27
  114. package/dist/components/help-center-pages/index.js +18 -18
  115. package/dist/components/index.cjs +146 -144
  116. package/dist/components/index.cjs.map +1 -1
  117. package/dist/components/index.js +20 -18
  118. package/dist/components/index.js.map +1 -1
  119. package/dist/components/meeting-scheduler/index.cjs +47 -47
  120. package/dist/components/meeting-scheduler/index.js +6 -6
  121. package/dist/components/navigation/index.cjs +10 -10
  122. package/dist/components/navigation/index.js +9 -9
  123. package/dist/components/onboarding-guides/index.cjs +10 -10
  124. package/dist/components/onboarding-guides/index.js +9 -9
  125. package/dist/components/related-content/index.cjs +3 -3
  126. package/dist/components/related-content/index.js +2 -2
  127. package/dist/components/tickets/index.cjs +11 -11
  128. package/dist/components/tickets/index.js +10 -10
  129. package/dist/components/ui/file-manager/index.cjs +4 -4
  130. package/dist/components/ui/file-manager/index.js +1 -1
  131. package/dist/components/ui/index.cjs +7 -7
  132. package/dist/components/ui/index.js +6 -6
  133. package/dist/components/ui/input.d.ts +2 -0
  134. package/dist/components/ui/input.d.ts.map +1 -1
  135. package/dist/components/ui/textarea.d.ts +2 -0
  136. package/dist/components/ui/textarea.d.ts.map +1 -1
  137. package/dist/components/ui/ticket-status-config-row.d.ts +10 -4
  138. package/dist/components/ui/ticket-status-config-row.d.ts.map +1 -1
  139. package/dist/hooks/index.cjs +7 -3
  140. package/dist/hooks/index.cjs.map +1 -1
  141. package/dist/hooks/index.js +6 -2
  142. package/dist/hooks/ui/index.d.ts +1 -0
  143. package/dist/hooks/ui/index.d.ts.map +1 -1
  144. package/dist/hooks/ui/use-drag-and-drop-enabled.d.ts +31 -0
  145. package/dist/hooks/ui/use-drag-and-drop-enabled.d.ts.map +1 -0
  146. package/dist/hooks/ui/use-media-query.d.ts.map +1 -1
  147. package/dist/index.cjs +18 -12
  148. package/dist/index.cjs.map +1 -1
  149. package/dist/index.js +16 -10
  150. package/dist/index.js.map +1 -1
  151. package/dist/schemas/meeting-booking-schema.d.ts +2 -2
  152. package/dist/utils/content-href.d.ts +26 -5
  153. package/dist/utils/content-href.d.ts.map +1 -1
  154. package/dist/utils/index.cjs +1 -1
  155. package/dist/utils/index.cjs.map +1 -1
  156. package/dist/utils/index.d.ts +1 -1
  157. package/dist/utils/index.d.ts.map +1 -1
  158. package/dist/utils/index.js +1 -1
  159. package/dist/utils/index.js.map +1 -1
  160. package/package.json +1 -4
  161. package/src/components/chat/embeddable-chat.tsx +9 -2
  162. package/src/components/chat/entity-cards/.dispatch.md +1 -1
  163. package/src/components/chat/entity-cards/dispatch.tsx +43 -23
  164. package/src/components/chat/hooks/.use-unified-chat.md +5 -1
  165. package/src/components/chat/hooks/use-nats-chat-adapter.ts +4 -7
  166. package/src/components/chat/hooks/use-unified-chat.ts +8 -2
  167. package/src/components/chat/nav-link-anchor-via-runtime.tsx +8 -10
  168. package/src/components/chat/types/.unified-chat-state.types.md +2 -1
  169. package/src/components/chat/types/unified-chat-state.types.ts +9 -2
  170. package/src/components/chat/utils/__tests__/execute-navigation.test.ts +190 -0
  171. package/src/components/chat/utils/__tests__/resolve-fetched-card-href.test.ts +75 -2
  172. package/src/components/chat/utils/chat-nav-resolution.ts +9 -3
  173. package/src/components/chat/utils/execute-navigation.ts +32 -2
  174. package/src/components/chat/utils/index.ts +1 -1
  175. package/src/components/chat/utils/nav-anchor-props.ts +4 -2
  176. package/src/components/chat/utils/resolve-fetched-card-href.ts +50 -2
  177. package/src/components/features/board/.board.md +1 -0
  178. package/src/components/features/board/board.tsx +11 -1
  179. package/src/components/features/sortable-list/.sortable-list.md +23 -12
  180. package/src/components/features/sortable-list/index.ts +2 -1
  181. package/src/components/features/sortable-list/sortable-list.tsx +16 -5
  182. package/src/components/features/sortable-list/sortable-move-buttons.tsx +81 -0
  183. package/src/components/features/sortable-list/use-sortable-item.ts +25 -13
  184. package/src/components/features/ticket-status-config-list/ticket-status-config-list.tsx +55 -62
  185. package/src/components/ui/.ticket-status-config-row.md +11 -9
  186. package/src/components/ui/input.tsx +5 -2
  187. package/src/components/ui/textarea.tsx +5 -2
  188. package/src/components/ui/ticket-status-config-row.tsx +162 -70
  189. package/src/hooks/ui/.index.md +1 -0
  190. package/src/hooks/ui/.use-drag-and-drop-enabled.md +32 -0
  191. package/src/hooks/ui/index.ts +1 -0
  192. package/src/hooks/ui/use-drag-and-drop-enabled.ts +37 -0
  193. package/src/hooks/ui/use-media-query.ts +3 -2
  194. package/src/utils/.content-href.md +1 -1
  195. package/src/utils/__tests__/content-href.test.ts +3 -0
  196. package/src/utils/content-href.ts +30 -3
  197. package/src/utils/index.ts +1 -0
  198. package/dist/chunk-2BFZHTSD.cjs.map +0 -1
  199. package/dist/chunk-36NFJULV.cjs.map +0 -1
  200. package/dist/chunk-4D2BR756.js.map +0 -1
  201. package/dist/chunk-75X6BRID.cjs.map +0 -1
  202. package/dist/chunk-HM2R5MDR.js.map +0 -1
  203. package/dist/chunk-JQI726T3.js.map +0 -1
  204. package/dist/chunk-KE2TCUOH.cjs.map +0 -1
  205. package/dist/chunk-MIY5FYX3.cjs.map +0 -1
  206. package/dist/chunk-NP3XN6ZX.js.map +0 -1
  207. package/dist/chunk-PQETCTJ4.js.map +0 -1
  208. package/dist/chunk-QYQXAXVR.cjs.map +0 -1
  209. package/dist/chunk-W6JA6SFD.js.map +0 -1
  210. package/dist/components/chat/utils/nav-click-handler.d.ts +0 -24
  211. package/dist/components/chat/utils/nav-click-handler.d.ts.map +0 -1
  212. package/src/components/chat/utils/.nav-click-handler.md +0 -43
  213. package/src/components/chat/utils/nav-click-handler.ts +0 -34
  214. /package/dist/{chunk-IQFXDS5O.js.map → chunk-4WSKSNXL.js.map} +0 -0
  215. /package/dist/{chunk-NEKT67O5.js.map → chunk-5Y6TPFW7.js.map} +0 -0
  216. /package/dist/{chunk-AQ5MZVYN.js.map → chunk-AKPLEWMH.js.map} +0 -0
  217. /package/dist/{chunk-RUJKOU5F.js.map → chunk-BW4WDUSI.js.map} +0 -0
  218. /package/dist/{chunk-FCVAVOXS.js.map → chunk-D44S2QLI.js.map} +0 -0
  219. /package/dist/{chunk-U3SDSA5K.js.map → chunk-DB3TV4CW.js.map} +0 -0
  220. /package/dist/{chunk-ZBJ6XSDO.js.map → chunk-H4BPHGU5.js.map} +0 -0
  221. /package/dist/{chunk-4KR7JWKB.js.map → chunk-HGA74X35.js.map} +0 -0
  222. /package/dist/{chunk-2YCUEUIZ.js.map → chunk-L2CF2QZW.js.map} +0 -0
  223. /package/dist/{chunk-JCH6HGQD.js.map → chunk-MCWAZ5XI.js.map} +0 -0
  224. /package/dist/{chunk-LYOGSVFO.js.map → chunk-O2FTVZOD.js.map} +0 -0
  225. /package/dist/{chunk-THDGDYJ3.js.map → chunk-PMENIZXI.js.map} +0 -0
  226. /package/dist/{chunk-TU32TSAK.js.map → chunk-PWOYEUIQ.js.map} +0 -0
  227. /package/dist/{chunk-RSMUAAPW.js.map → chunk-QCP2KV7P.js.map} +0 -0
  228. /package/dist/{chunk-RNUH5IHZ.js.map → chunk-QGJYWLF4.js.map} +0 -0
@@ -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 |
@@ -0,0 +1,32 @@
1
+ <!-- source-hash: manual -->
2
+ The one rule for whether drag-and-drop is initialized anywhere in the UI. `SortableList` and
3
+ `Board` consume it themselves, so hosts get correct touch behavior without wiring anything.
4
+
5
+ ## Key Components
6
+
7
+ | Export | Type | Description |
8
+ |---|---|---|
9
+ | `useDragAndDropEnabled` | Hook | `true` only when `(pointer: fine) and (hover: hover) and (min-width: 800px)` matches |
10
+ | `DRAG_AND_DROP_MEDIA_QUERY` | Const | The query itself, composed from `breakpoints.md` — no new breakpoint |
11
+
12
+ ## Why pointer type AND breakpoint
13
+
14
+ - A breakpoint alone fails on tablets: an iPad in landscape is 1024-1366px wide and would pass any
15
+ sane `min-width` — exactly the case being excluded, because the HTML5 drag-and-drop API Pragmatic
16
+ builds on does not fire under touch.
17
+ - `pointer: coarse` alone fails the other way: a touchscreen laptop with a trackpad reports
18
+ `pointer: fine` and should keep DnD, but in a narrow window its lists collapse to the mobile
19
+ layout where a drag handle is out of place.
20
+ - No `'ontouchstart' in window`, no UA sniffing: the first is true on touch laptops, the second is
21
+ neither accurate nor reactive. A media query re-evaluates live when a mouse is attached or the
22
+ window resizes.
23
+
24
+ ## SSR
25
+
26
+ Returns `false` during SSR and the first client render (`useMediaQuery` is still `undefined`):
27
+ DnD off means no dead drag handle in server HTML, and the layout effect settles the real answer
28
+ before first paint, so a desktop user never sees the touch fallback flash.
29
+
30
+ ## Source
31
+
32
+ [`use-drag-and-drop-enabled.ts`](https://github.com/flamingo-stack/openframe-oss-lib/blob/main/use-drag-and-drop-enabled.ts)
@@ -2,6 +2,7 @@
2
2
  export * from './use-auto-limit-tags';
3
3
  export * from './use-debounce';
4
4
  export * from './use-deferred-error';
5
+ export * from './use-drag-and-drop-enabled';
5
6
  export * from './use-focus-trap';
6
7
  export * from './use-header-height';
7
8
  export * from './use-horizontal-scrollbar';
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+
3
+ import { breakpoints, useMediaQuery } from './use-media-query';
4
+
5
+ /**
6
+ * The one rule for whether drag-and-drop is initialized at all.
7
+ *
8
+ * Pointer type AND breakpoint, never either alone:
9
+ * - A breakpoint alone fails on tablets: an iPad in landscape is 1024-1366px
10
+ * wide and would pass any sane `min-width` — exactly the case being excluded,
11
+ * because the HTML5 drag-and-drop API Pragmatic builds on does not fire under
12
+ * touch.
13
+ * - `pointer: coarse` alone fails the other way: a touchscreen laptop with a
14
+ * trackpad reports `pointer: fine` and should keep DnD, but in a narrow
15
+ * window its lists collapse to the mobile layout where a drag handle is out
16
+ * of place.
17
+ * - No `'ontouchstart' in window`, no UA sniffing: the first is true on touch
18
+ * laptops, the second is neither accurate nor reactive. A media query
19
+ * re-evaluates live when a mouse is attached or the window resizes.
20
+ *
21
+ * `breakpoints.md` (800px) is the existing tablet step — no new breakpoint.
22
+ */
23
+ export const DRAG_AND_DROP_MEDIA_QUERY = `(pointer: fine) and (hover: hover) and ${breakpoints.md}`;
24
+
25
+ /**
26
+ * Whether drag-and-drop should be available on this device/viewport.
27
+ *
28
+ * `false` on touch/narrow screens — there the reordering surfaces render
29
+ * explicit controls (`SortableMoveButtons`) instead of a drag handle, and no
30
+ * DnD is initialized at all. Also `false` during SSR and the first client
31
+ * render (`useMediaQuery` is still `undefined`): defaulting to "off" renders
32
+ * no dead drag handle, and the underlying layout effect settles the real
33
+ * answer before first paint, so a desktop user never sees the fallback flash.
34
+ */
35
+ export function useDragAndDropEnabled(): boolean {
36
+ return useMediaQuery(DRAG_AND_DROP_MEDIA_QUERY) ?? false;
37
+ }
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { useLayoutEffect, useState } from 'react';
3
+ import { useState } from 'react';
4
+ import { useIsomorphicLayoutEffect } from './use-isomorphic-layout-effect';
4
5
 
5
6
  /**
6
7
  * Hook to check if a media query matches
@@ -10,7 +11,7 @@ import { useLayoutEffect, useState } from 'react';
10
11
  export function useMediaQuery(query: string): boolean | undefined {
11
12
  const [matches, setMatches] = useState<boolean | undefined>(undefined);
12
13
 
13
- useLayoutEffect(() => {
14
+ useIsomorphicLayoutEffect(() => {
14
15
  const matchMedia = window.matchMedia(query);
15
16
 
16
17
  const handleChange = () => {
@@ -15,7 +15,7 @@ Unified content-href authority for all embeddable surfaces — resolves content
15
15
 
16
16
  ### Functions
17
17
 
18
- - **`makeComposeContentUrl(opts)`** — Factory that builds the embedder's `composeContentUrl`. Resolution order: explicit `overrides` → hosted in-app route → verbatim `externalUrl` → hub-origin fallback.
18
+ - **`makeComposeContentUrl(opts)`** — Factory that builds the embedder's `composeContentUrl`. Resolution order: explicit `overrides` → hosted in-app route → verbatim `externalUrl` → hub-origin fallback. Only the `overrides` branch returns `hostOverride: true`; fetch-mode chat cards rank that above the fetched row's own url (`pickFetchedCardHref`), and everything else below it.
19
19
  - **`buildDefaultHref(basePath, slug)`** — Fallback href when no `composeContentUrl` is wired (single-platform embedders).
20
20
  - **`resolveContentHref(composeContentUrl, args)`** — Ternary helper used by catalog/detail views: delegates to `composeContentUrl` when wired, otherwise calls `buildDefaultHref`.
21
21
 
@@ -188,6 +188,9 @@ describe('makeComposeContentUrl (unified single-object seam)', () => {
188
188
  expect(c({ type: 'product_release', identifier: 'v2', externalUrl: `${HUB}/releases/v2` })).toEqual({
189
189
  href: '/custom/v2',
190
190
  targetPlatform: 'openframe',
191
+ // Only this branch is flagged — fetch-mode cards rank it above the
192
+ // fetched row's own url (`pickFetchedCardHref`).
193
+ hostOverride: true,
191
194
  });
192
195
  });
193
196
 
@@ -112,10 +112,34 @@ export interface ContentHrefOptions {
112
112
  overrides?: Record<string, (identifier: string) => { href: string; targetPlatform: string | null }>;
113
113
  }
114
114
 
115
+ /** What the seam resolves a content reference to. */
116
+ export interface ComposedContentUrl {
117
+ href: string;
118
+ targetPlatform: string | null;
119
+ /**
120
+ * Set ONLY when the href came from the host's EXPLICIT per-type `overrides`
121
+ * entry — i.e. the host stated where this type lives in ITS app, rather than
122
+ * the composer falling through to a hosted-suffix or `<origin>/<type>/<id>`
123
+ * synthesis.
124
+ *
125
+ * The distinction is load-bearing for fetch-mode chat cards whose fetched row
126
+ * already carries a destination (`refHydratedEntry`'s `item.url`, minted by
127
+ * the CONTENT HOST for its OWN surface). That url must beat a synthesized
128
+ * guess, but must NOT beat a host that has explicitly re-homed the type:
129
+ * a HubSpot ticket's `item.url` is the hub's `/tickets?ticket=…`, which in an
130
+ * embedder that serves the same ticket at `/help-center/tickets` silently
131
+ * resolves against the embedder's own unrelated `/tickets` route.
132
+ *
133
+ * Optional and absent by default, so a hand-written composer (the hub's
134
+ * cross-platform one) keeps the pre-existing "row url wins" behavior.
135
+ */
136
+ hostOverride?: boolean;
137
+ }
138
+
115
139
  /** The unified `composeContentUrl` seam shape on `ChatRuntime`. ALWAYS returns
116
140
  * a tuple (never null) — the seam type is non-nullable and callers read
117
141
  * `.href` unconditionally. */
118
- export type ComposeContentUrl = (input: ComposeContentUrlInput) => { href: string; targetPlatform: string | null };
142
+ export type ComposeContentUrl = (input: ComposeContentUrlInput) => ComposedContentUrl;
119
143
 
120
144
  /**
121
145
  * Owning platform of a content row, from the hydrated junction the list APIs
@@ -162,7 +186,8 @@ function lastPathSegment(url: string): string | null {
162
186
  * Build the embedder's `composeContentUrl` for the unified seam.
163
187
  *
164
188
  * Resolution order (the merged rule used by BOTH page views and chat cards):
165
- * 1. `overrides[type]` — explicit per-type href.
189
+ * 1. `overrides[type]` — explicit per-type href, returned with
190
+ * `hostOverride: true` (the only branch that sets it).
166
191
  * 2. `hostedTypes.has(type)` → relative `/<suffix>/<slug>` (in-app, soft-nav).
167
192
  * Chat rows carry the hub URL not the slug, so the slug is recovered from
168
193
  * `externalUrl`; page views pass the slug as `identifier`.
@@ -184,7 +209,9 @@ export function makeComposeContentUrl(opts: ContentHrefOptions): ComposeContentU
184
209
  const type = canonicalContentRefType(rawType);
185
210
  const override =
186
211
  opts.overrides && Object.prototype.hasOwnProperty.call(opts.overrides, type) ? opts.overrides[type] : undefined;
187
- if (override) return override(identifier);
212
+ // Flagged as an explicit host decision — see `ComposedContentUrl.hostOverride`
213
+ // for why a fetch-mode card has to tell this apart from the branches below.
214
+ if (override) return { ...override(identifier), hostOverride: true };
188
215
 
189
216
  const seg = (Object.prototype.hasOwnProperty.call(suffixes, type) ? suffixes[type] : undefined) ?? type;
190
217
 
@@ -340,6 +340,7 @@ export {
340
340
  buildDefaultHref,
341
341
  type ContentHrefOptions,
342
342
  type ComposeContentUrl,
343
+ type ComposedContentUrl,
343
344
  } from './content-href';
344
345
 
345
346
  // Invisible bot-protection signals (honeypot + timing) — pure + server-safe so