@flamingo-stack/openframe-frontend-core 0.0.562 → 0.0.563-snapshot.20260824140722

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 (110) hide show
  1. package/dist/{chunk-4PCVRVRP.cjs → chunk-6OPEULTD.cjs} +9 -9
  2. package/dist/{chunk-4PCVRVRP.cjs.map → chunk-6OPEULTD.cjs.map} +1 -1
  3. package/dist/{chunk-T2WSZ2W4.js → chunk-7D62XVHH.js} +6 -6
  4. package/dist/{chunk-ACIVGQA4.js → chunk-ASULU3T2.js} +2 -2
  5. package/dist/{chunk-3QP6VIXF.cjs → chunk-AZPUAPXW.cjs} +469 -471
  6. package/dist/chunk-AZPUAPXW.cjs.map +1 -0
  7. package/dist/{chunk-JVXF5DIA.js → chunk-BK7V6CH5.js} +831 -833
  8. package/dist/chunk-BK7V6CH5.js.map +1 -0
  9. package/dist/{chunk-P4G3I3OE.js → chunk-CP7TMQBH.js} +2 -2
  10. package/dist/{chunk-FFQ7IHZZ.js → chunk-CQNK6RS6.js} +2 -2
  11. package/dist/{chunk-ICAWPAGF.js → chunk-DPMOEYEM.js} +2 -2
  12. package/dist/{chunk-PRXMZB6K.cjs → chunk-E6PW7GRQ.cjs} +62 -62
  13. package/dist/{chunk-PRXMZB6K.cjs.map → chunk-E6PW7GRQ.cjs.map} +1 -1
  14. package/dist/{chunk-TGREMQ5Z.cjs → chunk-FG4BSJ4A.cjs} +26 -26
  15. package/dist/{chunk-TGREMQ5Z.cjs.map → chunk-FG4BSJ4A.cjs.map} +1 -1
  16. package/dist/{chunk-DHB2ODZC.js → chunk-G2VDS3M5.js} +2 -2
  17. package/dist/{chunk-WEMZGECB.cjs → chunk-GEC2NHQH.cjs} +20 -20
  18. package/dist/{chunk-WEMZGECB.cjs.map → chunk-GEC2NHQH.cjs.map} +1 -1
  19. package/dist/{chunk-BHHJTOKI.cjs → chunk-J7R3VZL5.cjs} +14 -14
  20. package/dist/{chunk-BHHJTOKI.cjs.map → chunk-J7R3VZL5.cjs.map} +1 -1
  21. package/dist/{chunk-NIQGA3YT.js → chunk-NQ6UF4LP.js} +4 -4
  22. package/dist/{chunk-PL6FTS27.js → chunk-NUKQWLOG.js} +5 -5
  23. package/dist/{chunk-UNR2ROYX.js → chunk-O3SEC45L.js} +4 -4
  24. package/dist/{chunk-E5L6FGRF.cjs → chunk-OYFXHET4.cjs} +7 -7
  25. package/dist/{chunk-E5L6FGRF.cjs.map → chunk-OYFXHET4.cjs.map} +1 -1
  26. package/dist/{chunk-W6RG3S5S.cjs → chunk-PKCZL56Y.cjs} +4 -4
  27. package/dist/{chunk-W6RG3S5S.cjs.map → chunk-PKCZL56Y.cjs.map} +1 -1
  28. package/dist/{chunk-D6I24QAR.cjs → chunk-PT4GS4CE.cjs} +3 -3
  29. package/dist/{chunk-D6I24QAR.cjs.map → chunk-PT4GS4CE.cjs.map} +1 -1
  30. package/dist/{chunk-35U4SIQA.js → chunk-T3P56JKN.js} +2 -2
  31. package/dist/{chunk-5HD3SSC2.js → chunk-TDYF6Q2D.js} +2 -2
  32. package/dist/{chunk-AYJAWSDY.cjs → chunk-U5EVZLL5.cjs} +5 -5
  33. package/dist/{chunk-AYJAWSDY.cjs.map → chunk-U5EVZLL5.cjs.map} +1 -1
  34. package/dist/{chunk-JAJKPAV3.cjs → chunk-VVDLVCPW.cjs} +39 -39
  35. package/dist/{chunk-JAJKPAV3.cjs.map → chunk-VVDLVCPW.cjs.map} +1 -1
  36. package/dist/{chunk-WCHOWGKI.cjs → chunk-WGDZW237.cjs} +86 -86
  37. package/dist/{chunk-WCHOWGKI.cjs.map → chunk-WGDZW237.cjs.map} +1 -1
  38. package/dist/{chunk-6U2VJFKW.js → chunk-X4JKXYY5.js} +2 -2
  39. package/dist/{chunk-VYHMCNZT.cjs → chunk-XFI5PCER.cjs} +31 -31
  40. package/dist/{chunk-VYHMCNZT.cjs.map → chunk-XFI5PCER.cjs.map} +1 -1
  41. package/dist/{chunk-RWGX2HIB.cjs → chunk-YFLSOYX4.cjs} +11 -11
  42. package/dist/{chunk-RWGX2HIB.cjs.map → chunk-YFLSOYX4.cjs.map} +1 -1
  43. package/dist/{chunk-3QYCR7I6.js → chunk-Z4VU6MJ4.js} +2 -2
  44. package/dist/components/case-studies/index.cjs +8 -8
  45. package/dist/components/case-studies/index.js +2 -2
  46. package/dist/components/chat/index.cjs +2 -2
  47. package/dist/components/chat/index.js +1 -1
  48. package/dist/components/contact/index.cjs +3 -3
  49. package/dist/components/contact/index.js +2 -2
  50. package/dist/components/docs/index.cjs +5 -5
  51. package/dist/components/docs/index.js +4 -4
  52. package/dist/components/embeds/index.cjs +3 -3
  53. package/dist/components/embeds/index.js +2 -2
  54. package/dist/components/faq/index.cjs +3 -3
  55. package/dist/components/faq/index.js +2 -2
  56. package/dist/components/features/index.cjs +2 -2
  57. package/dist/components/features/index.js +1 -1
  58. package/dist/components/form.d.ts +1 -1
  59. package/dist/components/form.d.ts.map +1 -1
  60. package/dist/components/help-center-pages/index.cjs +22 -22
  61. package/dist/components/help-center-pages/index.js +13 -13
  62. package/dist/components/index.cjs +131 -131
  63. package/dist/components/index.js +11 -11
  64. package/dist/components/meeting-scheduler/index.cjs +30 -30
  65. package/dist/components/meeting-scheduler/index.js +3 -3
  66. package/dist/components/navigation/index.cjs +2 -2
  67. package/dist/components/navigation/index.js +1 -1
  68. package/dist/components/onboarding-guides/hooks/use-onboarding-guides.d.ts +3 -3
  69. package/dist/components/onboarding-guides/hooks/use-onboarding-guides.d.ts.map +1 -1
  70. package/dist/components/onboarding-guides/index.cjs +5 -5
  71. package/dist/components/onboarding-guides/index.js +4 -4
  72. package/dist/components/related-content/index.cjs +3 -3
  73. package/dist/components/related-content/index.js +2 -2
  74. package/dist/components/tickets/index.cjs +6 -6
  75. package/dist/components/tickets/index.js +5 -5
  76. package/dist/components/ui/dashboard-info-card.d.ts +1 -1
  77. package/dist/components/ui/index.cjs +2 -2
  78. package/dist/components/ui/index.js +1 -1
  79. package/dist/components/ui/ticket-info-section.d.ts +10 -6
  80. package/dist/components/ui/ticket-info-section.d.ts.map +1 -1
  81. package/dist/components/ui/ticket-status-tag.d.ts +8 -1
  82. package/dist/components/ui/ticket-status-tag.d.ts.map +1 -1
  83. package/dist/hooks/use-meeting-booking.d.ts +1 -1
  84. package/dist/index.cjs +2 -2
  85. package/dist/index.js +1 -1
  86. package/package.json +1 -1
  87. package/src/components/features/board/types.ts +1 -1
  88. package/src/components/ui/dashboard-info-card.tsx +1 -1
  89. package/src/components/ui/ticket-info-section.tsx +90 -87
  90. package/src/components/ui/ticket-status-tag.tsx +74 -52
  91. package/src/stories/.Board.stories.md +1 -1
  92. package/src/stories/Board.stories.tsx +4 -4
  93. package/src/stories/DashboardInfoCard.stories.tsx +1 -1
  94. package/src/stories/InfoSection.stories.tsx +1 -1
  95. package/src/stories/TicketEventMessage.stories.tsx +1 -1
  96. package/dist/chunk-3QP6VIXF.cjs.map +0 -1
  97. package/dist/chunk-JVXF5DIA.js.map +0 -1
  98. /package/dist/{chunk-T2WSZ2W4.js.map → chunk-7D62XVHH.js.map} +0 -0
  99. /package/dist/{chunk-ACIVGQA4.js.map → chunk-ASULU3T2.js.map} +0 -0
  100. /package/dist/{chunk-P4G3I3OE.js.map → chunk-CP7TMQBH.js.map} +0 -0
  101. /package/dist/{chunk-FFQ7IHZZ.js.map → chunk-CQNK6RS6.js.map} +0 -0
  102. /package/dist/{chunk-ICAWPAGF.js.map → chunk-DPMOEYEM.js.map} +0 -0
  103. /package/dist/{chunk-DHB2ODZC.js.map → chunk-G2VDS3M5.js.map} +0 -0
  104. /package/dist/{chunk-NIQGA3YT.js.map → chunk-NQ6UF4LP.js.map} +0 -0
  105. /package/dist/{chunk-PL6FTS27.js.map → chunk-NUKQWLOG.js.map} +0 -0
  106. /package/dist/{chunk-UNR2ROYX.js.map → chunk-O3SEC45L.js.map} +0 -0
  107. /package/dist/{chunk-35U4SIQA.js.map → chunk-T3P56JKN.js.map} +0 -0
  108. /package/dist/{chunk-5HD3SSC2.js.map → chunk-TDYF6Q2D.js.map} +0 -0
  109. /package/dist/{chunk-6U2VJFKW.js.map → chunk-X4JKXYY5.js.map} +0 -0
  110. /package/dist/{chunk-3QYCR7I6.js.map → chunk-Z4VU6MJ4.js.map} +0 -0
@@ -1,117 +1,128 @@
1
- "use client"
1
+ 'use client';
2
2
 
3
- import * as React from "react"
4
- import { Chevron02DownIcon } from "../icons-v2-generated/arrows/chevron-02-down-icon"
5
- import { UserIcon } from "../icons-v2-generated/users/user-icon"
6
- import { cn } from "../../utils/cn"
7
- import { AssigneeDropdown, type TicketAssigneeOption } from "./assignee-dropdown"
8
- import { SquareAvatar } from "./square-avatar"
9
- import { Tag } from "./tag"
10
- import { TicketStatusTag, type TicketStatusTagOption } from "./ticket-status-tag"
11
- import { TicketDetailSection } from "./ticket-detail-section"
12
- import { type TicketAttachment, TicketAttachmentsList } from "./ticket-attachments-list"
13
- import type { TicketNote } from "./ticket-note-card"
14
- import { TicketNotesSection } from "./ticket-notes-section"
15
- import { SimpleMarkdownRenderer } from "./markdown/simple-markdown-renderer"
3
+ import * as React from 'react';
4
+ import { cn } from '../../utils/cn';
5
+ import { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon';
6
+ import { UserIcon } from '../icons-v2-generated/users/user-icon';
7
+ import { AssigneeDropdown, type TicketAssigneeOption } from './assignee-dropdown';
8
+ import { SimpleMarkdownRenderer } from './markdown/simple-markdown-renderer';
9
+ import { SquareAvatar } from './square-avatar';
10
+ import { Tag } from './tag';
11
+ import { type TicketAttachment, TicketAttachmentsList } from './ticket-attachments-list';
12
+ import { TicketDetailSection } from './ticket-detail-section';
13
+ import type { TicketNote } from './ticket-note-card';
14
+ import { TicketNotesSection } from './ticket-notes-section';
15
+ import { TicketStatusTag, type TicketStatusTagOption } from './ticket-status-tag';
16
16
 
17
- export type { TicketAssigneeOption }
17
+ export type { TicketAssigneeOption };
18
18
 
19
19
  export interface TicketInfoSectionProps {
20
20
  /** Organization name and image */
21
21
  organization?: {
22
- name: string
23
- imageSrc?: string
24
- }
22
+ name: string;
23
+ imageSrc?: string;
24
+ };
25
25
  /** User name */
26
- user?: string
26
+ user?: string;
27
27
  /** Device info */
28
28
  device?: {
29
- name: string
30
- icon?: React.ReactNode
31
- onClick?: () => void
32
- }
29
+ name: string;
30
+ icon?: React.ReactNode;
31
+ onClick?: () => void;
32
+ };
33
33
  /** Status tag */
34
- status?: string
34
+ status?: string;
35
35
  /** Display label for the status tag (e.g. a custom status name). */
36
- statusLabel?: string
36
+ statusLabel?: string;
37
37
  /** Hex color for the status tag (e.g. a custom status color). */
38
- statusColor?: string
38
+ statusColor?: string;
39
39
  /** When provided, the status tag becomes an inline changer with these options. */
40
- statusOptions?: TicketStatusTagOption[]
40
+ statusOptions?: TicketStatusTagOption[];
41
41
  /** Called with the chosen status id from the inline status dropdown. */
42
- onStatusSelect?: (id: string) => void
42
+ onStatusSelect?: (id: string) => void;
43
43
  /** Disables the inline status dropdown while a change is in flight. */
44
- isStatusPending?: boolean
44
+ isStatusPending?: boolean;
45
+ /** Locks the status: renders the plain tag instead of the dropdown (e.g. a pending approval blocks the transition). */
46
+ isStatusDisabled?: boolean;
47
+ /** Reason shown in a tooltip over the status tag when `isStatusDisabled`. */
48
+ statusDisabledReason?: string;
45
49
  /** Expand button click handler */
46
- onExpand?: () => void
50
+ onExpand?: () => void;
47
51
  /** Whether the section is expanded */
48
- expanded?: boolean
52
+ expanded?: boolean;
49
53
  /** Additional className */
50
- className?: string
54
+ className?: string;
51
55
 
52
56
  // --- Expanded view props ---
53
57
 
54
58
  /** Assigned person info with inline dropdown */
55
59
  assigned?: {
56
60
  currentAssignee?: {
57
- id: string
58
- name: string
59
- avatarSrc?: string
61
+ id: string;
62
+ name: string;
63
+ avatarSrc?: string;
60
64
  /** Deleted account (DELETED / SELF_DELETED) — renders the red user-x placeholder + red name. */
61
- deleted?: boolean
62
- }
63
- options: TicketAssigneeOption[]
64
- isLoading?: boolean
65
- isPending?: boolean
66
- onAssign: (userId: string | null) => void
67
- }
65
+ deleted?: boolean;
66
+ };
67
+ options: TicketAssigneeOption[];
68
+ isLoading?: boolean;
69
+ isPending?: boolean;
70
+ onAssign: (userId: string | null) => void;
71
+ };
68
72
  /** Created date string */
69
- createdAt?: string
73
+ createdAt?: string;
70
74
  /** Ticket description — markdown/HTML content */
71
- description?: string
75
+ description?: string;
72
76
  /** File attachments (view-only with download) */
73
- attachments?: TicketAttachment[]
77
+ attachments?: TicketAttachment[];
74
78
  /** Tag labels */
75
- tags?: string[]
79
+ tags?: string[];
76
80
  /** Notes */
77
- notes?: TicketNote[]
78
- onAddNote?: (text: string) => void
79
- onEditNote?: (id: string, text: string) => void
80
- onDeleteNote?: (id: string) => void
81
+ notes?: TicketNote[];
82
+ onAddNote?: (text: string) => void;
83
+ onEditNote?: (id: string, text: string) => void;
84
+ onDeleteNote?: (id: string) => void;
81
85
  /** Disables the note input while a note is being added */
82
- isAddingNote?: boolean
86
+ isAddingNote?: boolean;
83
87
  }
84
88
 
85
- function InfoCell({ value, label, icon, onClick }: {
86
- value: string
87
- label: string
88
- icon?: React.ReactNode
89
- onClick?: () => void
89
+ function InfoCell({
90
+ value,
91
+ label,
92
+ icon,
93
+ onClick,
94
+ }: {
95
+ value: string;
96
+ label: string;
97
+ icon?: React.ReactNode;
98
+ onClick?: () => void;
90
99
  }) {
91
100
  return (
92
101
  <div className="flex-1 min-w-0 overflow-hidden">
93
102
  <div className="flex flex-col justify-center">
94
103
  <div className="flex items-center gap-1 w-full min-w-0">
95
104
  {icon && (
96
- <span className="shrink-0 size-4 flex items-center justify-center text-ods-text-secondary">
97
- {icon}
98
- </span>
105
+ <span className="shrink-0 size-4 flex items-center justify-center text-ods-text-secondary">{icon}</span>
99
106
  )}
100
107
  {onClick ? (
101
108
  <button
102
109
  type="button"
103
110
  onClick={onClick}
104
- className="text-h4 text-ods-text-primary truncate hover:text-ods-accent transition-colors cursor-pointer text-left" title={value}>
111
+ className="text-h4 text-ods-text-primary truncate hover:text-ods-accent transition-colors cursor-pointer text-left"
112
+ title={value}
113
+ >
105
114
  {value}
106
115
  </button>
107
116
  ) : (
108
- <span className="text-h4 text-ods-text-primary truncate" title={value}>{value}</span>
117
+ <span className="text-h4 text-ods-text-primary truncate" title={value}>
118
+ {value}
119
+ </span>
109
120
  )}
110
121
  </div>
111
122
  <span className="text-h6 text-ods-text-secondary truncate">{label}</span>
112
123
  </div>
113
124
  </div>
114
- )
125
+ );
115
126
  }
116
127
 
117
128
  export function TicketInfoSection({
@@ -123,6 +134,8 @@ export function TicketInfoSection({
123
134
  statusOptions,
124
135
  onStatusSelect,
125
136
  isStatusPending,
137
+ isStatusDisabled,
138
+ statusDisabledReason,
126
139
  onExpand,
127
140
  expanded = false,
128
141
  className,
@@ -138,12 +151,7 @@ export function TicketInfoSection({
138
151
  isAddingNote,
139
152
  }: TicketInfoSectionProps) {
140
153
  return (
141
- <div
142
- className={cn(
143
- "rounded-[6px] border border-ods-border overflow-hidden",
144
- className,
145
- )}
146
- >
154
+ <div className={cn('rounded-[6px] border border-ods-border overflow-hidden', className)}>
147
155
  {/* Header row */}
148
156
  <div className="grid grid-cols-2 lg:grid-cols-[1fr_1fr_1fr_auto] gap-4 px-4 py-3 bg-ods-card border-b border-ods-border items-center">
149
157
  {/* Organization with image */}
@@ -151,11 +159,11 @@ export function TicketInfoSection({
151
159
  <SquareAvatar
152
160
  src={organization?.imageSrc}
153
161
  alt={organization?.name}
154
- fallback={organization?.name || "Org"}
162
+ fallback={organization?.name || 'Org'}
155
163
  size="md"
156
164
  className="shrink-0"
157
165
  />
158
- <InfoCell value={organization?.name || "Unassigned"} label="Organization" />
166
+ <InfoCell value={organization?.name || 'Unassigned'} label="Organization" />
159
167
  </div>
160
168
 
161
169
  {/* Assigned */}
@@ -180,24 +188,21 @@ export function TicketInfoSection({
180
188
  </div>
181
189
 
182
190
  {/* Device */}
183
- <InfoCell
184
- value={device?.name || "Unassigned"}
185
- label="Device"
186
- icon={device?.icon}
187
- onClick={device?.onClick}
188
- />
191
+ <InfoCell value={device?.name || 'Unassigned'} label="Device" icon={device?.icon} onClick={device?.onClick} />
189
192
 
190
193
  {/* Status tag + expand button */}
191
194
  <div className="flex items-center gap-4 min-w-0">
192
195
  {(status || statusLabel) && (
193
196
  <div className="min-w-0">
194
197
  <TicketStatusTag
195
- status={status ?? ""}
198
+ status={status ?? ''}
196
199
  label={statusLabel}
197
200
  color={statusColor}
198
201
  options={statusOptions}
199
202
  onSelect={onStatusSelect}
200
203
  isPending={isStatusPending}
204
+ disabled={isStatusDisabled}
205
+ disabledReason={statusDisabledReason}
201
206
  />
202
207
  </div>
203
208
  )}
@@ -206,16 +211,16 @@ export function TicketInfoSection({
206
211
  type="button"
207
212
  onClick={onExpand}
208
213
  className={cn(
209
- "shrink-0 flex items-center justify-center p-3 rounded-[6px]",
210
- "bg-ods-card border border-ods-border",
211
- "hover:bg-ods-bg-hover transition-colors duration-150",
214
+ 'shrink-0 flex items-center justify-center p-3 rounded-[6px]',
215
+ 'bg-ods-card border border-ods-border',
216
+ 'hover:bg-ods-bg-hover transition-colors duration-150',
212
217
  )}
213
- aria-label={expanded ? "Collapse" : "Expand"}
218
+ aria-label={expanded ? 'Collapse' : 'Expand'}
214
219
  >
215
220
  <Chevron02DownIcon
216
221
  className={cn(
217
- "size-6 text-ods-text-primary transition-transform duration-200",
218
- expanded && "rotate-180",
222
+ 'size-6 text-ods-text-primary transition-transform duration-200',
223
+ expanded && 'rotate-180',
219
224
  )}
220
225
  />
221
226
  </button>
@@ -236,9 +241,7 @@ export function TicketInfoSection({
236
241
  {/* Content area */}
237
242
  <div className="flex flex-col gap-4 p-4 bg-ods-bg border-b border-ods-border">
238
243
  {/* Description */}
239
- {description && (
240
- <SimpleMarkdownRenderer content={description} />
241
- )}
244
+ {description && <SimpleMarkdownRenderer content={description} />}
242
245
 
243
246
  {/* Attachments */}
244
247
  {attachments && attachments.length > 0 && (
@@ -274,5 +277,5 @@ export function TicketInfoSection({
274
277
  </>
275
278
  )}
276
279
  </div>
277
- )
280
+ );
278
281
  }
@@ -1,25 +1,26 @@
1
- 'use client'
1
+ 'use client';
2
2
 
3
- import React from 'react'
4
- import { ActionsMenuDropdown } from './actions-menu'
5
- import { ColorSwatch } from './color-swatch'
6
- import { Tag, type TagProps, tagVariants } from './tag'
7
- import { CheckCircleIcon, Chevron02DownIcon } from '../icons-v2-generated'
8
- import { cn } from '../../utils/cn'
9
- import { deriveActiveColor, deriveHoverColor, getReadableTextColor } from '../../utils/ods-color-utils'
3
+ import React from 'react';
4
+ import { cn } from '../../utils/cn';
5
+ import { deriveActiveColor, deriveHoverColor, getReadableTextColor } from '../../utils/ods-color-utils';
6
+ import { CheckCircleIcon, Chevron02DownIcon } from '../icons-v2-generated';
7
+ import { ActionsMenuDropdown } from './actions-menu';
8
+ import { ColorSwatch } from './color-swatch';
9
+ import { Tag, type TagProps, tagVariants } from './tag';
10
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip';
10
11
 
11
12
  /**
12
13
  * Canonical ticket status values.
13
14
  * This is the single source of truth for ticket statuses across all frontends.
14
15
  */
15
- export type TicketStatus = 'ACTIVE' | 'AI_ASSISTANCE' | 'TECH_REQUIRED' | 'ON_HOLD' | 'RESOLVED' | 'ARCHIVED'
16
+ export type TicketStatus = 'ACTIVE' | 'AI_ASSISTANCE' | 'TECH_REQUIRED' | 'ON_HOLD' | 'RESOLVED' | 'ARCHIVED';
16
17
 
17
- type TagVariant = NonNullable<TagProps['variant']>
18
+ type TagVariant = NonNullable<TagProps['variant']>;
18
19
 
19
20
  interface TicketStatusConfig {
20
- label: string
21
- variant: TagVariant
22
- icon?: React.ReactNode
21
+ label: string;
22
+ variant: TagVariant;
23
+ icon?: React.ReactNode;
23
24
  }
24
25
 
25
26
  const STATUS_CONFIG: Record<TicketStatus, TicketStatusConfig> = {
@@ -28,7 +29,7 @@ const STATUS_CONFIG: Record<TicketStatus, TicketStatusConfig> = {
28
29
  variant: 'success',
29
30
  },
30
31
  AI_ASSISTANCE: {
31
- label: 'AI Assistance',
32
+ label: 'AI Handling',
32
33
  variant: 'outline',
33
34
  },
34
35
  TECH_REQUIRED: {
@@ -48,7 +49,7 @@ const STATUS_CONFIG: Record<TicketStatus, TicketStatusConfig> = {
48
49
  label: 'Archived',
49
50
  variant: 'grey',
50
51
  },
51
- }
52
+ };
52
53
 
53
54
  /**
54
55
  * Maps known aliases to canonical TicketStatus values.
@@ -81,14 +82,14 @@ const STATUS_ALIASES: Record<string, TicketStatus> = {
81
82
  resolved: 'RESOLVED',
82
83
  archived: 'ARCHIVED',
83
84
  action_required: 'TECH_REQUIRED',
84
- }
85
+ };
85
86
 
86
87
  /**
87
88
  * Resolves any status string to a canonical TicketStatus.
88
89
  * Returns null for unknown values.
89
90
  */
90
91
  export function resolveTicketStatus(status: string): TicketStatus | null {
91
- return STATUS_ALIASES[status] ?? null
92
+ return STATUS_ALIASES[status] ?? null;
92
93
  }
93
94
 
94
95
  /**
@@ -96,11 +97,11 @@ export function resolveTicketStatus(status: string): TicketStatus | null {
96
97
  * Accepts any known status format (backend, chat, dialog).
97
98
  */
98
99
  export function getTicketStatusConfig(status: string): TicketStatusConfig {
99
- const canonical = resolveTicketStatus(status)
100
+ const canonical = resolveTicketStatus(status);
100
101
  if (!canonical) {
101
- return { label: status.replace(/_/g, ' '), variant: 'outline' }
102
+ return { label: status.replace(/_/g, ' '), variant: 'outline' };
102
103
  }
103
- return STATUS_CONFIG[canonical]
104
+ return STATUS_CONFIG[canonical];
104
105
  }
105
106
 
106
107
  /**
@@ -108,8 +109,8 @@ export function getTicketStatusConfig(status: string): TicketStatusConfig {
108
109
  * Drop-in replacement for the getTicketStatusTag utility in openframe-frontend.
109
110
  */
110
111
  export function getTicketStatusTag(status: string): { label: string; variant: TagVariant; icon?: React.ReactNode } {
111
- const config = getTicketStatusConfig(status)
112
- return { label: config.label, variant: config.variant, icon: config.icon }
112
+ const config = getTicketStatusConfig(status);
113
+ return { label: config.label, variant: config.variant, icon: config.icon };
113
114
  }
114
115
 
115
116
  // ---------------------------------------------------------------------------
@@ -117,19 +118,19 @@ export function getTicketStatusTag(status: string): { label: string; variant: Ta
117
118
  // ---------------------------------------------------------------------------
118
119
 
119
120
  /** Backend status-definition kinds. */
120
- export type TicketStatusKind = 'AI_ASSISTANCE' | 'TECH_REQUIRED' | 'RESOLVED' | 'ARCHIVED' | 'CUSTOM'
121
+ export type TicketStatusKind = 'AI_ASSISTANCE' | 'TECH_REQUIRED' | 'RESOLVED' | 'ARCHIVED' | 'CUSTOM';
121
122
 
122
123
  const KIND_TO_CANONICAL: Record<Exclude<TicketStatusKind, 'CUSTOM'>, TicketStatus> = {
123
124
  AI_ASSISTANCE: 'AI_ASSISTANCE',
124
125
  TECH_REQUIRED: 'TECH_REQUIRED',
125
126
  RESOLVED: 'RESOLVED',
126
127
  ARCHIVED: 'ARCHIVED',
127
- }
128
+ };
128
129
 
129
130
  /** Canonical TicketStatus for a backend kind, or undefined for CUSTOM/unknown. */
130
131
  export function kindToCanonicalStatus(kind: string | null | undefined): TicketStatus | undefined {
131
- if (kind == null || kind === 'CUSTOM') return undefined
132
- return (KIND_TO_CANONICAL as Record<string, TicketStatus | undefined>)[kind]
132
+ if (kind == null || kind === 'CUSTOM') return undefined;
133
+ return (KIND_TO_CANONICAL as Record<string, TicketStatus | undefined>)[kind];
133
134
  }
134
135
 
135
136
  /**
@@ -138,18 +139,18 @@ export function kindToCanonicalStatus(kind: string | null | undefined): TicketSt
138
139
  * TECH_REQUIRED and custom statuses use their color.
139
140
  */
140
141
  export function usesCanonicalStatusStyle(kind: string | null | undefined): boolean {
141
- return kind != null && kind !== 'CUSTOM' && kind !== 'TECH_REQUIRED'
142
+ return kind != null && kind !== 'CUSTOM' && kind !== 'TECH_REQUIRED';
142
143
  }
143
144
 
144
145
  export interface TicketStatusInput {
145
146
  /** Legacy enum status (may be empty/null under the lifecycle feature). */
146
- status?: string | null
147
+ status?: string | null;
147
148
  /** Backend status kind (AI_ASSISTANCE, TECH_REQUIRED, RESOLVED, ARCHIVED, CUSTOM). */
148
- statusKind?: string | null
149
+ statusKind?: string | null;
149
150
  /** Custom status display name. */
150
- statusName?: string | null
151
+ statusName?: string | null;
151
152
  /** Custom status hex color. */
152
- statusColor?: string | null
153
+ statusColor?: string | null;
153
154
  }
154
155
 
155
156
  /**
@@ -159,12 +160,12 @@ export interface TicketStatusInput {
159
160
  * backend color, labelled by the custom status name.
160
161
  */
161
162
  export function resolveStatusTagProps(input: TicketStatusInput): { status: string; label?: string; color?: string } {
162
- const canonical = usesCanonicalStatusStyle(input.statusKind) ? kindToCanonicalStatus(input.statusKind) : undefined
163
+ const canonical = usesCanonicalStatusStyle(input.statusKind) ? kindToCanonicalStatus(input.statusKind) : undefined;
163
164
  return {
164
165
  status: canonical ?? input.status ?? '',
165
166
  label: input.statusName ?? undefined,
166
167
  color: canonical ? undefined : (input.statusColor ?? undefined),
167
- }
168
+ };
168
169
  }
169
170
 
170
171
  // ---------------------------------------------------------------------------
@@ -173,36 +174,43 @@ export function resolveStatusTagProps(input: TicketStatusInput): { status: strin
173
174
 
174
175
  /** A selectable status option for the inline status-change dropdown. */
175
176
  export interface TicketStatusTagOption {
176
- id: string
177
- name: string
178
- color: string
177
+ id: string;
178
+ name: string;
179
+ color: string;
179
180
  }
180
181
 
181
182
  export interface TicketStatusTagProps {
182
183
  /** Ticket status in any known format (ACTIVE, active, ACTION_REQUIRED, etc.) */
183
- status: string
184
+ status: string;
184
185
  /** Override the default label */
185
- label?: string
186
+ label?: string;
186
187
  /**
187
188
  * Hex color override. Replaces the variant's fill; text
188
189
  * color is computed for contrast. The status's preset icon is preserved.
189
190
  */
190
- color?: string
191
+ color?: string;
191
192
  /** Override the status's default Tag variant (e.g. force 'outline' or 'primary'). */
192
- variant?: TagVariant
193
+ variant?: TagVariant;
193
194
  /** Additional className for the Tag */
194
- className?: string
195
+ className?: string;
195
196
  /** Show the status-specific icon (e.g. checkmark for resolved) */
196
- showIcon?: boolean
197
+ showIcon?: boolean;
197
198
  /**
198
199
  * Make the tag an inline status changer. When provided (with `onSelect`), the
199
200
  * tag renders with a chevron and opens a dropdown of these options on click.
200
201
  */
201
- options?: TicketStatusTagOption[]
202
+ options?: TicketStatusTagOption[];
202
203
  /** Called with the chosen option id. Required for the inline dropdown. */
203
- onSelect?: (id: string) => void
204
+ onSelect?: (id: string) => void;
204
205
  /** Disables the dropdown while a change is in flight. */
205
- isPending?: boolean
206
+ isPending?: boolean;
207
+ /**
208
+ * Lock the status: renders the plain non-interactive tag even when `options`
209
+ * are provided (e.g. a pending approval request blocks the transition).
210
+ */
211
+ disabled?: boolean;
212
+ /** Reason shown in a tooltip over the tag when `disabled`. */
213
+ disabledReason?: string;
206
214
  }
207
215
 
208
216
  /**
@@ -220,8 +228,10 @@ export function TicketStatusTag({
220
228
  options,
221
229
  onSelect,
222
230
  isPending = false,
231
+ disabled = false,
232
+ disabledReason,
223
233
  }: TicketStatusTagProps) {
224
- const config = getTicketStatusConfig(status)
234
+ const config = getTicketStatusConfig(status);
225
235
  // Custom color: drive bg via CSS vars so the derived hover/active fills can win
226
236
  // on :hover/:active (an inline backgroundColor would block class-based hover).
227
237
  const customStyle = color
@@ -231,10 +241,10 @@ export function TicketStatusTag({
231
241
  '--tag-bg-active': deriveActiveColor(color),
232
242
  color: getReadableTextColor(color),
233
243
  } as React.CSSProperties)
234
- : undefined
244
+ : undefined;
235
245
  const customColorClasses = color
236
246
  ? 'bg-[var(--tag-bg)] hover:bg-[var(--tag-bg-hover)] active:bg-[var(--tag-bg-active)]'
237
- : undefined
247
+ : undefined;
238
248
 
239
249
  const tag = (
240
250
  <Tag
@@ -244,10 +254,22 @@ export function TicketStatusTag({
244
254
  className={cn('shrink-0 w-fit', customColorClasses, className)}
245
255
  style={customStyle}
246
256
  />
247
- )
257
+ );
248
258
 
249
- if (!options || options.length === 0 || !onSelect) {
250
- return tag
259
+ if (disabled || !options || options.length === 0 || !onSelect) {
260
+ if (disabled && disabledReason) {
261
+ return (
262
+ <TooltipProvider delayDuration={0}>
263
+ <Tooltip>
264
+ <TooltipTrigger asChild>
265
+ <span className="w-fit shrink-0 cursor-not-allowed">{tag}</span>
266
+ </TooltipTrigger>
267
+ <TooltipContent>{disabledReason}</TooltipContent>
268
+ </Tooltip>
269
+ </TooltipProvider>
270
+ );
271
+ }
272
+ return tag;
251
273
  }
252
274
 
253
275
  return (
@@ -293,5 +315,5 @@ export function TicketStatusTag({
293
315
  </button>
294
316
  }
295
317
  />
296
- )
318
+ );
297
319
  }
@@ -16,7 +16,7 @@ Storybook stories for the `Board` component, covering all major interaction stat
16
16
  | Story | Description |
17
17
  |---|---|
18
18
  | `Default` | Five canonical-status columns with full DnD wired up |
19
- | `SystemAndCustom` | Mixed system + custom columns; AI-Assistance refuses drops, Resolved tickets are pinned |
19
+ | `SystemAndCustom` | Mixed system + custom columns; AI Handling refuses drops, Resolved tickets are pinned |
20
20
  | `CustomColumns` | Fully custom columns decoupled from `TicketStatus` (Backlog, In Review, Blocked, Done) |
21
21
  | `Collapsible` | Column collapse/expand with `localStorage` persistence via `collapseStorageKey` |
22
22
  | `Pagination` | One column loads 10 of 50 tickets; `onLoadMore` appends pages after a 600ms simulated delay |
@@ -112,11 +112,11 @@ export const Default: Story = {
112
112
 
113
113
  /**
114
114
  * Mixed system + custom columns matching the OpenFrame Tickets layout:
115
- * AI-Assistance and Tech Required are system columns (default surface, joined
115
+ * AI Handling and Tech Required are system columns (default surface, joined
116
116
  * with no gap), middle columns are custom (tinted by color), Resolved is a
117
117
  * system column whose `RESOLVED` status gives the green-checkmark tag.
118
118
  *
119
- * DnD restrictions: AI-Assistance refuses drops (tickets enter via the AI
119
+ * DnD restrictions: AI Handling refuses drops (tickets enter via the AI
120
120
  * pipeline, not by dragging), but tickets in it can still be dragged out.
121
121
  * Resolved tickets are pinned (`dragDisabled`).
122
122
  */
@@ -124,8 +124,8 @@ export const SystemAndCustom: Story = {
124
124
  render: function Render() {
125
125
  const [columns, setColumns] = React.useState<BoardColumnDef[]>([
126
126
  {
127
- id: 'ai-assistance',
128
- label: 'AI-Assistance',
127
+ id: 'ai-handling',
128
+ label: 'AI Handling',
129
129
  color: '#f357bb',
130
130
  system: true,
131
131
  dropDisabled: true,
@@ -356,7 +356,7 @@ export const ProgressSizeComparison: Story = {
356
356
  export const WithTitleTag: Story = {
357
357
  args: {
358
358
  value: 75,
359
- titleTag: <Tag variant="outline" label="AI-Assistance" />,
359
+ titleTag: <Tag variant="outline" label="AI Handling" />,
360
360
  },
361
361
  parameters: {
362
362
  docs: {
@@ -35,7 +35,7 @@ const assigneeOptions = [
35
35
  ];
36
36
 
37
37
  const statusOptions = [
38
- { id: 's1', name: 'AI Assistance', color: '#3a3a3a' },
38
+ { id: 's1', name: 'AI Handling', color: '#3a3a3a' },
39
39
  { id: 's2', name: 'Tech Required', color: '#f59e0b' },
40
40
  { id: 's3', name: 'Resolved', color: '#5ea62e' },
41
41
  ];
@@ -71,7 +71,7 @@ export const ReopenedWithReason: Story = {
71
71
  },
72
72
  };
73
73
 
74
- /** Reopened into AI Assistance (targetStatusKind decides) — Figma type=reopened-fae. */
74
+ /** Reopened into AI Handling (targetStatusKind decides) — Figma type=reopened-fae. */
75
75
  export const ReopenedFaeContinues: Story = {
76
76
  args: {
77
77
  data: { kind: "REOPENED", actorId: "u-7", actorName: "John Smith", actorType: "CLIENT", targetStatusKind: "AI_ASSISTANCE" },