@flamingo-stack/openframe-frontend-core 0.0.716 → 0.0.718

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 (113) hide show
  1. package/dist/chat-protocol/index.cjs +23 -14
  2. package/dist/chat-protocol/index.js +23 -14
  3. package/dist/chat-protocol/nats-decoder.d.ts.map +1 -1
  4. package/dist/chat-protocol/source-metadata.d.ts +11 -2
  5. package/dist/chat-protocol/source-metadata.d.ts.map +1 -1
  6. package/dist/{chunk-DPX6244M.js → chunk-45F7PXUI.js} +3 -3
  7. package/dist/{chunk-KYFNFRSV.js → chunk-67QBMN4R.js} +2 -2
  8. package/dist/{chunk-3RKBJT6K.cjs → chunk-6A43ZDWV.cjs} +2 -2
  9. package/dist/chunk-A6P6DHEZ.cjs +130 -0
  10. package/dist/{chunk-KNOZVVCU.cjs → chunk-BOTM373R.cjs} +9 -9
  11. package/dist/{chunk-DHMO5GI4.cjs → chunk-BWIN42ZA.cjs} +21 -21
  12. package/dist/{chunk-YTMV4OBY.js → chunk-DCD5WZUF.js} +4 -4
  13. package/dist/{chunk-V36WGIYR.js → chunk-DEZDMTBM.js} +5 -5
  14. package/dist/{chunk-YO5JB3WE.cjs → chunk-DXCBOZVT.cjs} +35 -35
  15. package/dist/{chunk-ZX7SCPLO.cjs → chunk-E7YCPY7K.cjs} +3 -3
  16. package/dist/{chunk-I5F2OTFE.cjs → chunk-EZC2F5OT.cjs} +33 -33
  17. package/dist/{chunk-DLEO53NX.cjs → chunk-GN4A7PMB.cjs} +69 -69
  18. package/dist/{chunk-6T3BTD6Q.js → chunk-IWCPXJEE.js} +201 -327
  19. package/dist/{chunk-ROBQ5TR7.js → chunk-JH2SIWPQ.js} +28 -17
  20. package/dist/{chunk-3AJIQPFG.js → chunk-JIWMBIVZ.js} +6 -6
  21. package/dist/{chunk-DDWFCWOE.js → chunk-JPD4NXH6.js} +1 -1
  22. package/dist/{chunk-JVCAK4W3.cjs → chunk-LE5WKGJV.cjs} +3 -3
  23. package/dist/{chunk-NPAQSMRY.js → chunk-LOYPRJWA.js} +1053 -357
  24. package/dist/{chunk-5IJXM3RU.cjs → chunk-MIQLUUVF.cjs} +10 -10
  25. package/dist/{chunk-MXPQMQI3.js → chunk-MM7MCIXL.js} +6 -6
  26. package/dist/{chunk-C5AGSZBT.js → chunk-N2PF5CFX.js} +4837 -5425
  27. package/dist/{chunk-STCJYO2Z.cjs → chunk-NDZ5ZXOQ.cjs} +11 -11
  28. package/dist/{chunk-H5R7U2D2.js → chunk-NZ5BHDFZ.js} +1 -1
  29. package/dist/{chunk-JCA5MBVY.js → chunk-PQ6HBLZ7.js} +4 -4
  30. package/dist/{chunk-EOKSKJSS.cjs → chunk-QBCDSTLE.cjs} +13 -13
  31. package/dist/{chunk-NQWJAOHO.js → chunk-QXZCLQNV.js} +1 -1
  32. package/dist/{chunk-2V7D5GJX.cjs → chunk-S2FUFMLW.cjs} +25 -25
  33. package/dist/{chunk-64F6OEEV.js → chunk-SGFTTJKO.js} +1 -1
  34. package/dist/{chunk-P7IL4CX3.js → chunk-TBCZCRQ5.js} +6 -6
  35. package/dist/{chunk-HETQETPV.cjs → chunk-UWQWMGJ7.cjs} +28 -17
  36. package/dist/{chunk-X2MNNV5H.cjs → chunk-WALCXC7J.cjs} +3 -3
  37. package/dist/{chunk-YBS5QGON.js → chunk-XGHGGBM3.js} +2 -2
  38. package/dist/{chunk-NKILEG3Y.cjs → chunk-YC5COB5V.cjs} +45 -45
  39. package/dist/{chunk-BVZ7W4HW.js → chunk-YEMCCYIF.js} +4 -4
  40. package/dist/chunk-YYYVS7CX.js +130 -0
  41. package/dist/{chunk-S2VWQX6T.cjs → chunk-Z3CVSPO3.cjs} +842 -1430
  42. package/dist/{chunk-PPXR22YC.cjs → chunk-Z5UWW3Z2.cjs} +707 -11
  43. package/dist/{chunk-JO5TLJPM.cjs → chunk-ZXCZYMN4.cjs} +37 -163
  44. package/dist/{cloud-tenants-2J7CPTKT.cjs → cloud-tenants-PFDF7O5U.cjs} +6 -5
  45. package/dist/{cloud-tenants-YOY2WLGC.js → cloud-tenants-TAKOWXLD.js} +5 -4
  46. package/dist/components/case-studies/index.cjs +17 -16
  47. package/dist/components/case-studies/index.js +6 -5
  48. package/dist/components/chat/index.cjs +2 -2
  49. package/dist/components/chat/index.js +1 -1
  50. package/dist/components/chat/types/message.types.d.ts +20 -16
  51. package/dist/components/chat/types/message.types.d.ts.map +1 -1
  52. package/dist/components/chat/types/unified-chat-state.types.d.ts +1 -1
  53. package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
  54. package/dist/components/contact/index.cjs +7 -6
  55. package/dist/components/contact/index.js +6 -5
  56. package/dist/components/docs/index.cjs +10 -9
  57. package/dist/components/docs/index.js +9 -8
  58. package/dist/components/embeds/index.cjs +7 -6
  59. package/dist/components/embeds/index.js +6 -5
  60. package/dist/components/faq/index.cjs +7 -6
  61. package/dist/components/faq/index.js +6 -5
  62. package/dist/components/features/index.cjs +10 -5
  63. package/dist/components/features/index.js +9 -4
  64. package/dist/components/features/remote-session/index.d.ts +2 -0
  65. package/dist/components/features/remote-session/index.d.ts.map +1 -1
  66. package/dist/components/features/remote-session/remote-session-keep-modals.d.ts +3 -3
  67. package/dist/components/features/remote-session/remote-session-keep-modals.d.ts.map +1 -1
  68. package/dist/components/features/remote-session/remote-session-ticket-modals.d.ts +29 -0
  69. package/dist/components/features/remote-session/remote-session-ticket-modals.d.ts.map +1 -0
  70. package/dist/components/help-center-pages/index.cjs +71 -70
  71. package/dist/components/help-center-pages/index.js +17 -16
  72. package/dist/components/index.cjs +44 -39
  73. package/dist/components/index.js +22 -17
  74. package/dist/components/meeting-scheduler/index.cjs +33 -32
  75. package/dist/components/meeting-scheduler/index.js +5 -4
  76. package/dist/components/navigation/index.cjs +9 -8
  77. package/dist/components/navigation/index.js +8 -7
  78. package/dist/components/onboarding-guides/index.cjs +9 -8
  79. package/dist/components/onboarding-guides/index.js +8 -7
  80. package/dist/components/product-demo/index.cjs +7 -7
  81. package/dist/components/product-demo/index.js +7 -7
  82. package/dist/components/tickets/index.cjs +10 -9
  83. package/dist/components/tickets/index.js +9 -8
  84. package/dist/components/ui/autocomplete.d.ts +6 -0
  85. package/dist/components/ui/autocomplete.d.ts.map +1 -1
  86. package/dist/components/ui/index.cjs +8 -7
  87. package/dist/components/ui/index.js +7 -6
  88. package/dist/{devices-5NADSKDZ.cjs → devices-KRCGZMGA.cjs} +7 -6
  89. package/dist/{devices-EQXXILXW.js → devices-LHEX7O5T.js} +5 -4
  90. package/dist/hooks/index.cjs +6 -4
  91. package/dist/hooks/index.js +5 -3
  92. package/dist/index.cjs +17 -11
  93. package/dist/index.js +18 -12
  94. package/dist/{knowledge-LHQYZJE7.cjs → knowledge-KLVPVEYZ.cjs} +7 -6
  95. package/dist/{knowledge-YCQITBJB.js → knowledge-RZSFNR33.js} +5 -4
  96. package/dist/{logs-LZNXGOSW.js → logs-E5DOV5JO.js} +5 -4
  97. package/dist/{logs-DUVKSQU3.cjs → logs-OX3XTVL2.cjs} +14 -13
  98. package/dist/{policies-5H74FS4S.cjs → policies-FOSCIZGK.cjs} +10 -9
  99. package/dist/{policies-VYWOJ7IF.js → policies-MC2S4S5G.js} +5 -4
  100. package/dist/{remote-session-R6VAFESR.cjs → remote-session-DRFE5XZE.cjs} +4 -3
  101. package/dist/{remote-session-GSGZOFDX.js → remote-session-JQ2BPIE3.js} +2 -1
  102. package/dist/{software-update-MSDY3OLX.js → software-update-D4OEIH65.js} +7 -5
  103. package/dist/{software-update-VRRX5U4W.cjs → software-update-HOBSTWVO.cjs} +12 -10
  104. package/package.json +1 -1
  105. package/src/chat-protocol/nats-decoder.ts +10 -8
  106. package/src/chat-protocol/source-metadata.ts +25 -2
  107. package/src/components/chat/types/message.types.ts +21 -15
  108. package/src/components/chat/types/unified-chat-state.types.ts +1 -1
  109. package/src/components/chat/utils/process-historical-messages.ts +12 -4
  110. package/src/components/features/remote-session/index.ts +2 -0
  111. package/src/components/features/remote-session/remote-session-keep-modals.tsx +4 -4
  112. package/src/components/features/remote-session/remote-session-ticket-modals.tsx +140 -0
  113. package/src/components/ui/autocomplete.tsx +18 -4
@@ -12,15 +12,13 @@ export const MESSAGE_TYPE = {
12
12
  TEXT: 'TEXT',
13
13
  THINKING: 'THINKING',
14
14
  ASK: 'ASK',
15
- /** Per-answer source metadata (documents, videos, entity cards).
15
+ /** Per-answer attachments (cited documents, videos, entity cards).
16
16
  *
17
- * TWO names for ONE payload, on purpose. `SOURCES` is the contract the
18
- * backend is moving to; `GUIDE` is the envelope it ships in today, reused
19
- * from Guide Mode v2 so a released frontend keeps working during the v2→v3
20
- * rollout. Both decode through the same path, so dropping `GUIDE` later is a
21
- * deletion here and nothing else. */
17
+ * `ATTACHMENTS` carries `sources` / `videos` / `cards` on the record itself.
18
+ * `GUIDE` is the envelope it replaced — the same arrays under `payload` —
19
+ * and stays only until every backend ships `ATTACHMENTS`. */
20
+ ATTACHMENTS: 'ATTACHMENTS',
22
21
  GUIDE: 'GUIDE',
23
- SOURCES: 'SOURCES',
24
22
  EXECUTING_TOOL: 'EXECUTING_TOOL',
25
23
  EXECUTED_TOOL: 'EXECUTED_TOOL',
26
24
  APPROVAL_REQUEST: 'APPROVAL_REQUEST',
@@ -412,7 +410,7 @@ export interface ThinkingMessageData extends MessageDataBase {
412
410
  *
413
411
  * Lives here, beside `Message`, because BOTH transports produce it now: the SSE
414
412
  * adapter reads it off the per-turn metadata frame, and the NATS path decodes it
415
- * out of a `GUIDE`/`SOURCES` chunk. It used to be declared inside
413
+ * out of an `ATTACHMENTS` chunk. It used to be declared inside
416
414
  * `use-sse-chat-adapter`, which made "a Guide answer's citations" structurally a
417
415
  * property of one transport — `use-sse-chat-adapter` still re-exports the name
418
416
  * so existing imports keep working.
@@ -451,13 +449,20 @@ export interface ChatSource {
451
449
  label?: string;
452
450
  }
453
451
 
454
- /** Persisted source-metadata row. `payload` is the SAME object the live chunk
455
- * carries, so history and realtime share one decoder (`sourceMetadataEvent`)
456
- * and cannot drift apart. Left as an open record here rather than typed
457
- * structurally: it is unvalidated wire data, and the decoder is what turns it
458
- * into `ChatSource[]` / `ChatRef[]`. */
452
+ /** Persisted attachments row (GraphQL `AttachmentsData`). The arrays are the
453
+ * SAME ones the live chunk carries, so history and realtime share one decoder
454
+ * (`attachmentsEvent`) and cannot drift apart. Left untyped: it is unvalidated
455
+ * wire data, and the decoder is what turns it into `ChatSource[]` / `ChatRef[]`. */
456
+ export interface AttachmentsMessageData extends MessageDataBase {
457
+ type: 'ATTACHMENTS';
458
+ sources?: unknown;
459
+ videos?: unknown;
460
+ cards?: unknown;
461
+ }
462
+
463
+ /** The envelope `ATTACHMENTS` replaced: the same arrays under `payload`. */
459
464
  export interface SourceMetadataMessageData extends MessageDataBase {
460
- type: 'GUIDE' | 'SOURCES';
465
+ type: 'GUIDE';
461
466
  payload?: unknown;
462
467
  }
463
468
 
@@ -603,6 +608,7 @@ export type MessageData =
603
608
  | TextMessageData
604
609
  | ThinkingMessageData
605
610
  | AskMessageData
611
+ | AttachmentsMessageData
606
612
  | SourceMetadataMessageData
607
613
  | ExecutingToolMessageData
608
614
  | ExecutedToolMessageData
@@ -654,7 +660,7 @@ export interface ProcessedMessage {
654
660
  * Absent when the source row(s) carried no seq. */
655
661
  streamSeq?: number;
656
662
  /** Documents this answer cited — see `Message.sources`, the same field.
657
- * Replayed from the turn's persisted `GUIDE`/`SOURCES` row, so a reloaded
663
+ * Replayed from the turn's persisted `ATTACHMENTS` row, so a reloaded
658
664
  * answer carries the citations the live one did. */
659
665
  sources?: ChatSource[];
660
666
  /** Entity references this answer's metadata described — see `Message.refs`. */
@@ -239,7 +239,7 @@ export interface UnifiedChatMessage {
239
239
  /**
240
240
  * Documents this answer cited, rendered as chips beneath it. Produced by
241
241
  * BOTH transports now — SSE reads them off the per-turn metadata frame, NATS
242
- * decodes them out of a `GUIDE`/`SOURCES` chunk.
242
+ * decodes them out of an `ATTACHMENTS` chunk.
243
243
  */
244
244
  sources?: ChatSource[];
245
245
  /**
@@ -34,7 +34,12 @@ import {
34
34
  // One normalizer for ask rows, shared with the live decoder — history and the
35
35
  // stream must agree on which options are usable.
36
36
  import { normalizeAskOptions } from '../../../chat-protocol/nats-decoder';
37
- import { mergeSourceMetadata, sourceMetadataEvent, type SourceMetadata } from '../../../chat-protocol/source-metadata';
37
+ import {
38
+ attachmentsEvent,
39
+ mergeSourceMetadata,
40
+ sourceMetadataEvent,
41
+ type SourceMetadata,
42
+ } from '../../../chat-protocol/source-metadata';
38
43
  import { applyApprovalStatusToSegment } from '../stream/message-mutations';
39
44
  import {
40
45
  MESSAGE_TYPE,
@@ -122,11 +127,14 @@ export function decodeHistoricalMessageData(data: MessageData): ChatStreamEvent
122
127
  }
123
128
  return null;
124
129
 
125
- // Persisted source metadata, through the SAME decoder the live chunk uses
126
- // (`sourceMetadataEvent`). That shared parse is the whole reason a reloaded
130
+ // Persisted attachments, through the SAME decoder the live chunk uses
131
+ // (`attachmentsEvent`). That shared parse is the whole reason a reloaded
127
132
  // answer renders identically to the one the reader watched arrive.
133
+ case MESSAGE_TYPE.ATTACHMENTS:
134
+ return attachmentsEvent(data);
135
+
136
+ // The envelope `ATTACHMENTS` replaced (see `MESSAGE_TYPE.GUIDE`).
128
137
  case MESSAGE_TYPE.GUIDE:
129
- case MESSAGE_TYPE.SOURCES:
130
138
  return 'payload' in data ? sourceMetadataEvent(data.payload) : null;
131
139
 
132
140
  // Same completeness gate as the live decoder (`decodeNatsChunk`): a
@@ -34,6 +34,8 @@ export type {
34
34
  KeepRecordingSelection,
35
35
  ReleaseKeepingModalProps,
36
36
  } from './remote-session-keep-modals';
37
+ export { AssignTicketModal, UnassignTicketModal } from './remote-session-ticket-modals';
38
+ export type { AssignTicketModalProps, UnassignTicketModalProps } from './remote-session-ticket-modals';
37
39
  export {
38
40
  RemoteSessionEventList,
39
41
  RemoteSessionTimelineMarkers,
@@ -151,8 +151,8 @@ export interface ReleaseKeepingModalProps {
151
151
  keptOn: string;
152
152
  /** The reason as it reads in the sentence, e.g. "Client dispute". */
153
153
  reason: string;
154
- /** The ticket or reference the Keep was placed for. */
155
- ticket?: string | null;
154
+ /** The tickets assigned to the recording, as they read in the line, e.g. "TKT-4631"; the line is left out without any. */
155
+ tickets?: string[];
156
156
  /** When the recording will be deleted once released, formatted. */
157
157
  expiresOn: string;
158
158
  /** The original expiry date when it has already passed, so the release adds the grace period. */
@@ -169,7 +169,7 @@ export function ReleaseKeepingModal({
169
169
  keptBy,
170
170
  keptOn,
171
171
  reason,
172
- ticket,
172
+ tickets = [],
173
173
  expiresOn,
174
174
  dueOn,
175
175
  isPending = false,
@@ -185,7 +185,7 @@ export function ReleaseKeepingModal({
185
185
  <p>
186
186
  Kept by {keptBy} on {keptOn} for {reason}.
187
187
  </p>
188
- {ticket && <p>Ticket: {ticket}</p>}
188
+ {tickets.length > 0 && <p>Ticket: {tickets.join(', ')}</p>}
189
189
  </div>
190
190
 
191
191
  <KeepNotice icon={<HourglassIcon size={24} color="currentColor" />} title={`It will expire on ${expiresOn}`}>
@@ -0,0 +1,140 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Autocomplete, type AutocompleteOption } from '../../ui/autocomplete';
5
+ import { Button } from '../../ui/button/button';
6
+ import { ModalV2, ModalV2Footer, ModalV2Header, ModalV2Title } from '../../ui/modal-v2';
7
+
8
+ export interface AssignTicketModalProps {
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ /**
12
+ * The tickets that can be assigned: label = title, value = ticket id. Leave
13
+ * out the ones already assigned to the recording.
14
+ */
15
+ options: AutocompleteOption<string>[];
16
+ /** Server-side search: called with the typed text; the caller refreshes `options`. Client-side filtering otherwise. */
17
+ onSearch?: (query: string) => void;
18
+ /** The ticket search is in flight. */
19
+ loading?: boolean;
20
+ /** The assignment is in flight: the button spins and the dialog refuses to close. */
21
+ isPending?: boolean;
22
+ onConfirm: (ticketIds: string[]) => void;
23
+ }
24
+
25
+ /** Assign Ticket: links one or more tickets to a recording. */
26
+ export function AssignTicketModal({
27
+ isOpen,
28
+ onClose,
29
+ options,
30
+ onSearch,
31
+ loading = false,
32
+ isPending = false,
33
+ onConfirm,
34
+ }: AssignTicketModalProps) {
35
+ const [ticketIds, setTicketIds] = useState<string[]>([]);
36
+ // Titles of the picked tickets: a server-side search replaces `options`, and
37
+ // a picked ticket missing from the new results would otherwise show its id.
38
+ const [titles, setTitles] = useState<Record<string, string>>({});
39
+
40
+ // Every opening starts blank. Reset while rendering, not from an effect: the
41
+ // modal stays mounted when it closes, so an effect would paint the opening
42
+ // frame with the previous selection still in the field.
43
+ const [seededOpen, setSeededOpen] = useState(isOpen);
44
+ if (seededOpen !== isOpen) {
45
+ setSeededOpen(isOpen);
46
+ if (isOpen) {
47
+ setTicketIds([]);
48
+ setTitles({});
49
+ }
50
+ }
51
+
52
+ const handleChange = (ids: string[]) => {
53
+ setTicketIds(ids);
54
+ setTitles(prev => {
55
+ const next: Record<string, string> = {};
56
+ for (const id of ids) next[id] = options.find(option => option.value === id)?.label ?? prev[id] ?? id;
57
+ return next;
58
+ });
59
+ };
60
+
61
+ const handleAssign = () => {
62
+ if (ticketIds.length === 0 || isPending) return;
63
+ onConfirm(ticketIds);
64
+ };
65
+
66
+ return (
67
+ <ModalV2 isOpen={isOpen} onClose={isPending ? () => {} : onClose} className="text-left md:max-w-[600px]">
68
+ <ModalV2Header>
69
+ <ModalV2Title>Assign Ticket</ModalV2Title>
70
+ </ModalV2Header>
71
+
72
+ <Autocomplete
73
+ multiple
74
+ wrapTags
75
+ label="Tickets"
76
+ labelVariant="large"
77
+ options={options}
78
+ value={ticketIds}
79
+ onChange={handleChange}
80
+ renderTag={option => titles[option.value] ?? option.label}
81
+ onInputChange={onSearch ? (value, reason) => reason === 'input' && onSearch(value) : undefined}
82
+ disableClientFilter={Boolean(onSearch)}
83
+ loading={loading}
84
+ noOptionsText="No tickets found"
85
+ disabled={isPending}
86
+ />
87
+
88
+ <ModalV2Footer>
89
+ <Button type="button" variant="outline" onClick={onClose} disabled={isPending} className="flex-1 md:hidden">
90
+ Cancel
91
+ </Button>
92
+ <div className="hidden flex-1 md:block" />
93
+ <Button
94
+ type="button"
95
+ variant="accent"
96
+ onClick={handleAssign}
97
+ loading={isPending}
98
+ disabled={ticketIds.length === 0}
99
+ className="flex-1"
100
+ >
101
+ Assign
102
+ </Button>
103
+ </ModalV2Footer>
104
+ </ModalV2>
105
+ );
106
+ }
107
+
108
+ export interface UnassignTicketModalProps {
109
+ isOpen: boolean;
110
+ onClose: () => void;
111
+ /** The unassignment is in flight: the button spins and the dialog refuses to close. */
112
+ isPending?: boolean;
113
+ onConfirm: () => void;
114
+ }
115
+
116
+ /** Unassign Ticket: unlinks a ticket from a recording; the ticket itself is unchanged. */
117
+ export function UnassignTicketModal({ isOpen, onClose, isPending = false, onConfirm }: UnassignTicketModalProps) {
118
+ return (
119
+ <ModalV2 isOpen={isOpen} onClose={isPending ? () => {} : onClose} className="text-left md:max-w-[600px]">
120
+ <ModalV2Header>
121
+ <ModalV2Title>Unassign Ticket</ModalV2Title>
122
+ </ModalV2Header>
123
+
124
+ <p className="w-full text-ods-text-primary text-h4">
125
+ This Ticket will no longer be linked to this remote session. The ticket itself stays as it is, and you can
126
+ assign it again anytime.
127
+ </p>
128
+
129
+ <ModalV2Footer>
130
+ <Button type="button" variant="outline" onClick={onClose} disabled={isPending} className="flex-1 md:hidden">
131
+ Cancel
132
+ </Button>
133
+ <div className="hidden flex-1 md:block" />
134
+ <Button type="button" variant="destructive" onClick={onConfirm} loading={isPending} className="flex-1">
135
+ Unassign
136
+ </Button>
137
+ </ModalV2Footer>
138
+ </ModalV2>
139
+ );
140
+ }
@@ -140,6 +140,12 @@ export interface AutocompleteMultipleProps<T = string> extends AutocompleteBaseP
140
140
  limitTags?: number | 'auto';
141
141
  /** Custom render function for the "+N" overflow chip */
142
142
  getLimitTagsText?: (more: number) => ReactNode;
143
+ /**
144
+ * Show every tag in full, wrapping onto more lines: the field grows instead
145
+ * of truncating labels and collapsing the rest into "+N". `limitTags` is
146
+ * ignored. For short pickers whose picks must stay readable (ticket titles).
147
+ */
148
+ wrapTags?: boolean;
143
149
  }
144
150
 
145
151
  export type AutocompleteProps<T = string> = AutocompleteSingleProps<T> | AutocompleteMultipleProps<T>;
@@ -189,6 +195,7 @@ function AutocompleteInner<T = string>(props: AutocompleteProps<T>, ref: Forward
189
195
  // Multiple-only props
190
196
  const maxItems = multiple ? (props as AutocompleteMultipleProps<T>).maxItems : undefined;
191
197
  const renderTag = multiple ? (props as AutocompleteMultipleProps<T>).renderTag : undefined;
198
+ const wrapTags = multiple && ((props as AutocompleteMultipleProps<T>).wrapTags ?? false);
192
199
  const limitTagsProp = multiple ? ((props as AutocompleteMultipleProps<T>).limitTags ?? 'auto') : 'auto';
193
200
  const getLimitTagsText = multiple
194
201
  ? ((props as AutocompleteMultipleProps<T>).getLimitTagsText ?? ((more: number) => `+${more}`))
@@ -252,7 +259,7 @@ function AutocompleteInner<T = string>(props: AutocompleteProps<T>, ref: Forward
252
259
  inputRef,
253
260
  } = useAutoLimitTags({
254
261
  count: multiple ? selectedOptions.length : 0,
255
- limitTags: multiple ? limitTagsProp : 0,
262
+ limitTags: multiple ? (wrapTags ? selectedOptions.length : limitTagsProp) : 0,
256
263
  placeholder: inputPlaceholder,
257
264
  });
258
265
 
@@ -506,15 +513,22 @@ function AutocompleteInner<T = string>(props: AutocompleteProps<T>, ref: Forward
506
513
  </span>
507
514
  )}
508
515
 
509
- {/* Middle zone: tags + input — single line with overflow */}
510
- <div ref={middleRef} className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden px-2">
516
+ {/* Middle zone: tags + input — single line with overflow, or wrapped lines with `wrapTags` */}
517
+ <div
518
+ ref={middleRef}
519
+ className={cn(
520
+ 'flex min-w-0 flex-1 items-center gap-2 px-2',
521
+ wrapTags ? 'flex-wrap py-[var(--spacing-system-xsf)]' : 'overflow-hidden',
522
+ )}
523
+ >
511
524
  {/* Tags (multiple mode only) */}
512
525
  {multiple &&
513
526
  visibleTags.map(option => (
514
527
  <Tag
515
528
  key={String(option.value)}
516
529
  variant="outline"
517
- labelClassName="max-w-[90px] truncate"
530
+ className={wrapTags ? 'max-w-full' : undefined}
531
+ labelClassName={wrapTags ? 'truncate' : 'max-w-[90px] truncate'}
518
532
  label={renderTag ? renderTag(option) : option.label}
519
533
  onClose={
520
534
  !disabled