@flamingo-stack/openframe-frontend-core 0.0.429 → 0.0.430
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.
- package/dist/{chunk-GMR5AA3C.js → chunk-2ELINEWQ.js} +2 -2
- package/dist/{chunk-YGITDP6O.cjs → chunk-374KFDET.cjs} +31 -31
- package/dist/{chunk-YGITDP6O.cjs.map → chunk-374KFDET.cjs.map} +1 -1
- package/dist/{chunk-4VODJPTB.js → chunk-3AWPRVFM.js} +2 -2
- package/dist/{chunk-RTOVYU4D.js → chunk-3OKVH6FW.js} +4 -4
- package/dist/{chunk-2WJ25MTA.js → chunk-3XWBE3SM.js} +4 -4
- package/dist/{chunk-TJ4HJ3KX.js → chunk-67LZDLIO.js} +2 -2
- package/dist/{chunk-7PA4B6SD.js → chunk-A6JKKKIF.js} +4 -4
- package/dist/{chunk-KHKB5RY2.js → chunk-AY5TBDWZ.js} +2 -2
- package/dist/{chunk-CRGH6TV6.cjs → chunk-CJOFXDTK.cjs} +26 -26
- package/dist/{chunk-CRGH6TV6.cjs.map → chunk-CJOFXDTK.cjs.map} +1 -1
- package/dist/{chunk-B3Y47C2U.cjs → chunk-CTY37UUO.cjs} +59 -59
- package/dist/{chunk-B3Y47C2U.cjs.map → chunk-CTY37UUO.cjs.map} +1 -1
- package/dist/{chunk-OLGFIDZY.cjs → chunk-D4K53CES.cjs} +26 -26
- package/dist/{chunk-OLGFIDZY.cjs.map → chunk-D4K53CES.cjs.map} +1 -1
- package/dist/{chunk-WXKFFVGC.cjs → chunk-FL4IOCYH.cjs} +5 -5
- package/dist/{chunk-WXKFFVGC.cjs.map → chunk-FL4IOCYH.cjs.map} +1 -1
- package/dist/{chunk-7MJTM6QT.cjs → chunk-FYMD7CCA.cjs} +7 -7
- package/dist/{chunk-7MJTM6QT.cjs.map → chunk-FYMD7CCA.cjs.map} +1 -1
- package/dist/{chunk-Y2TW664W.js → chunk-IMOJWQDR.js} +4 -4
- package/dist/{chunk-2LJA3ABH.cjs → chunk-KDVGC6EG.cjs} +37 -37
- package/dist/{chunk-2LJA3ABH.cjs.map → chunk-KDVGC6EG.cjs.map} +1 -1
- package/dist/{chunk-NYISC4Z3.js → chunk-NZBQGODM.js} +2 -2
- package/dist/{chunk-XDBD6BJN.cjs → chunk-PP5KNHMK.cjs} +318 -215
- package/dist/chunk-PP5KNHMK.cjs.map +1 -0
- package/dist/{chunk-E4PKZNR7.cjs → chunk-Q7SYPXUX.cjs} +11 -11
- package/dist/{chunk-E4PKZNR7.cjs.map → chunk-Q7SYPXUX.cjs.map} +1 -1
- package/dist/{chunk-MAYEJ3NY.js → chunk-S7UXICHB.js} +2 -2
- package/dist/{chunk-B3FBMRHQ.js → chunk-TDITU4KW.js} +2 -2
- package/dist/{chunk-62L7S7HQ.js → chunk-TYANQCZA.js} +269 -166
- package/dist/chunk-TYANQCZA.js.map +1 -0
- package/dist/{chunk-BN3V465H.cjs → chunk-WQN4B6CM.cjs} +15 -15
- package/dist/{chunk-BN3V465H.cjs.map → chunk-WQN4B6CM.cjs.map} +1 -1
- package/dist/{chunk-CCL6JEMS.cjs → chunk-WQR7H56P.cjs} +62 -62
- package/dist/{chunk-CCL6JEMS.cjs.map → chunk-WQR7H56P.cjs.map} +1 -1
- package/dist/{chunk-GGJUXN6Z.cjs → chunk-ZUP2ZKS6.cjs} +9 -9
- package/dist/{chunk-GGJUXN6Z.cjs.map → chunk-ZUP2ZKS6.cjs.map} +1 -1
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- package/dist/components/chat/hooks/index.d.ts +1 -0
- package/dist/components/chat/hooks/index.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts +10 -1
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-scripted-stream.d.ts +3 -0
- package/dist/components/chat/hooks/use-scripted-stream.d.ts.map +1 -0
- package/dist/components/chat/index.cjs +12 -2
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.js +11 -1
- package/dist/components/chat/utils/index.d.ts +1 -0
- package/dist/components/chat/utils/index.d.ts.map +1 -1
- package/dist/components/chat/utils/scripted-stream.d.ts +38 -0
- package/dist/components/chat/utils/scripted-stream.d.ts.map +1 -0
- package/dist/components/contact/index.cjs +3 -3
- package/dist/components/contact/index.js +2 -2
- package/dist/components/docs/index.cjs +5 -5
- package/dist/components/docs/index.js +4 -4
- package/dist/components/embeds/index.cjs +3 -3
- package/dist/components/embeds/index.js +2 -2
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/board/index.d.ts +2 -2
- package/dist/components/features/board/index.d.ts.map +1 -1
- package/dist/components/features/board/ticket-card.d.ts +22 -2
- package/dist/components/features/board/ticket-card.d.ts.map +1 -1
- package/dist/components/features/index.cjs +6 -2
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +5 -1
- package/dist/components/help-center-pages/index.cjs +20 -20
- package/dist/components/help-center-pages/index.js +11 -11
- package/dist/components/index.cjs +142 -128
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +23 -9
- package/dist/components/index.js.map +1 -1
- package/dist/components/navigation/index.cjs +2 -2
- package/dist/components/navigation/index.js +1 -1
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/onboarding-guides/index.js +4 -4
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +5 -5
- package/dist/components/tickets/index.js +4 -4
- package/dist/components/ui/index.cjs +12 -2
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +11 -1
- package/dist/index.cjs +16 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +15 -1
- package/package.json +1 -1
- package/src/components/chat/hooks/index.ts +1 -0
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +23 -0
- package/src/components/chat/hooks/use-scripted-stream.ts +69 -0
- package/src/components/chat/utils/index.ts +2 -0
- package/src/components/chat/utils/scripted-stream.ts +111 -0
- package/src/components/features/board/index.ts +2 -2
- package/src/components/features/board/ticket-card.tsx +108 -64
- package/dist/chunk-62L7S7HQ.js.map +0 -1
- package/dist/chunk-XDBD6BJN.cjs.map +0 -1
- /package/dist/{chunk-GMR5AA3C.js.map → chunk-2ELINEWQ.js.map} +0 -0
- /package/dist/{chunk-4VODJPTB.js.map → chunk-3AWPRVFM.js.map} +0 -0
- /package/dist/{chunk-RTOVYU4D.js.map → chunk-3OKVH6FW.js.map} +0 -0
- /package/dist/{chunk-2WJ25MTA.js.map → chunk-3XWBE3SM.js.map} +0 -0
- /package/dist/{chunk-TJ4HJ3KX.js.map → chunk-67LZDLIO.js.map} +0 -0
- /package/dist/{chunk-7PA4B6SD.js.map → chunk-A6JKKKIF.js.map} +0 -0
- /package/dist/{chunk-KHKB5RY2.js.map → chunk-AY5TBDWZ.js.map} +0 -0
- /package/dist/{chunk-Y2TW664W.js.map → chunk-IMOJWQDR.js.map} +0 -0
- /package/dist/{chunk-NYISC4Z3.js.map → chunk-NZBQGODM.js.map} +0 -0
- /package/dist/{chunk-MAYEJ3NY.js.map → chunk-S7UXICHB.js.map} +0 -0
- /package/dist/{chunk-B3FBMRHQ.js.map → chunk-TDITU4KW.js.map} +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Playback clock for {@link buildStreamFrames}: advance through pre-built
|
|
5
|
+
* scripted frames like a live chat stream — each frame holds for its own
|
|
6
|
+
* `delayMs`, then the next reveals, parking on the last. Surface-agnostic: the
|
|
7
|
+
* caller maps `frame.messages` to its own chat shape and reads `frame.phase` /
|
|
8
|
+
* `frame.typing`.
|
|
9
|
+
*
|
|
10
|
+
* `resetKey` restarts playback from the top when a NEW conversation is selected
|
|
11
|
+
* (identity change) — NOT when the SAME conversation grows (e.g. an Approve
|
|
12
|
+
* appends its continuation), which must resume from where it paused. Because the
|
|
13
|
+
* frame list's shared prefix is deterministic, keeping the index across a grow
|
|
14
|
+
* resumes seamlessly into the appended frames.
|
|
15
|
+
*
|
|
16
|
+
* `prefers-reduced-motion` (or `enabled === false`) jumps straight to the final
|
|
17
|
+
* frame (everything revealed, idle).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { useEffect, useState } from 'react'
|
|
21
|
+
import type { StreamFrame } from '../utils/scripted-stream'
|
|
22
|
+
|
|
23
|
+
const EMPTY_FRAME: StreamFrame = { messages: [], phase: 'idle', typing: false, delayMs: 0 }
|
|
24
|
+
|
|
25
|
+
function prefersReducedMotion(): boolean {
|
|
26
|
+
return typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useScriptedStream(frames: StreamFrame[], resetKey: unknown, enabled = true): StreamFrame {
|
|
30
|
+
const [index, setIndex] = useState(0)
|
|
31
|
+
const [reducedMotion, setReducedMotion] = useState(false)
|
|
32
|
+
|
|
33
|
+
// Restart from the top on a new conversation, synchronously during render
|
|
34
|
+
// (React's "adjust state on prop change" pattern) so the previous stream's
|
|
35
|
+
// last frame never flashes before the reset effect runs.
|
|
36
|
+
const [prevKey, setPrevKey] = useState(resetKey)
|
|
37
|
+
if (resetKey !== prevKey) {
|
|
38
|
+
setPrevKey(resetKey)
|
|
39
|
+
setIndex(0)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setReducedMotion(prefersReducedMotion())
|
|
44
|
+
const mq = window.matchMedia?.('(prefers-reduced-motion: reduce)')
|
|
45
|
+
if (!mq) return
|
|
46
|
+
const onChange = () => setReducedMotion(mq.matches)
|
|
47
|
+
mq.addEventListener('change', onChange)
|
|
48
|
+
return () => mq.removeEventListener('change', onChange)
|
|
49
|
+
}, [])
|
|
50
|
+
|
|
51
|
+
const playing = enabled && !reducedMotion && frames.length > 0
|
|
52
|
+
const lastIndex = Math.max(0, frames.length - 1)
|
|
53
|
+
|
|
54
|
+
// Advance one frame after the current frame's hold. When `frames` grows (an
|
|
55
|
+
// Approve appends more), this effect re-runs and, if we were parked on the old
|
|
56
|
+
// last frame, schedules the next — resuming the stream. `frames[index]` is
|
|
57
|
+
// clamped because a shrink (a shorter new conversation before the reset lands)
|
|
58
|
+
// could momentarily leave `index` past the end.
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!playing || index >= lastIndex) return
|
|
61
|
+
const current = frames[Math.min(index, lastIndex)] ?? EMPTY_FRAME
|
|
62
|
+
const t = setTimeout(() => setIndex((i) => Math.min(i + 1, lastIndex)), current.delayMs)
|
|
63
|
+
return () => clearTimeout(t)
|
|
64
|
+
}, [index, frames, playing, lastIndex])
|
|
65
|
+
|
|
66
|
+
if (frames.length === 0) return EMPTY_FRAME
|
|
67
|
+
if (!playing) return frames[lastIndex]
|
|
68
|
+
return frames[Math.min(index, lastIndex)] ?? EMPTY_FRAME
|
|
69
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scripted-conversation replay — turn a stored `HistoricalMessage[]` into ordered
|
|
3
|
+
* "mock SSE" playback frames that animate a chat surface exactly like the live
|
|
4
|
+
* NATS/SSE stream (a `thinking` pause → assistant text token-by-token → tool /
|
|
5
|
+
* approval segments popping in → `idle`), just replayed from a script instead of
|
|
6
|
+
* a network stream.
|
|
7
|
+
*
|
|
8
|
+
* Lives in the lib (not a host) so any consumer — marketing hero demos, docs,
|
|
9
|
+
* onboarding tours — can drive a `previewMode` `EmbeddableChat` or a
|
|
10
|
+
* `ChatMessageList` from a canned conversation without re-implementing the
|
|
11
|
+
* streaming cadence. Pair with {@link useScriptedStream} (the playback clock).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { HistoricalMessage, MessageData, StreamingPhase } from '../types'
|
|
15
|
+
|
|
16
|
+
/** Normalize the single-or-array `messageData` to an array (empty when absent). */
|
|
17
|
+
export function normalizeHistoricalMessageData(md: HistoricalMessage['messageData']): MessageData[] {
|
|
18
|
+
return Array.isArray(md) ? md : md ? [md] : []
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A message the user authored carries an `owner`; assistant turns have none —
|
|
22
|
+
* drives whether a turn streams (assistant) or lands at once (user). */
|
|
23
|
+
export function isHistoricalUserTurn(m: HistoricalMessage): boolean {
|
|
24
|
+
return !!m.owner
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const isTextSegment = (d: MessageData): d is MessageData & { type: 'TEXT'; text: string } =>
|
|
28
|
+
(d as { type?: string })?.type === 'TEXT' && typeof (d as { text?: unknown }).text === 'string'
|
|
29
|
+
|
|
30
|
+
/** One rendered step of the mocked stream. */
|
|
31
|
+
export interface StreamFrame {
|
|
32
|
+
/** The partial message list to show at this step. */
|
|
33
|
+
messages: HistoricalMessage[]
|
|
34
|
+
phase: StreamingPhase
|
|
35
|
+
/** The chat "typing" indicator (up during the `thinking` pause). */
|
|
36
|
+
typing: boolean
|
|
37
|
+
/** Hold time (ms) before advancing to the next frame. */
|
|
38
|
+
delayMs: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Playback cadence. Assistant turns "think" then stream token-by-token; user
|
|
42
|
+
// turns land at once. Tuned to read like a real chat without dragging.
|
|
43
|
+
const THINK_MS = 550
|
|
44
|
+
const TOKEN_MS = 55 // per word revealed while streaming text
|
|
45
|
+
const USER_MS = 480 // dwell on a user turn before the reply starts thinking
|
|
46
|
+
const SEGMENT_MS = 340 // a tool / approval card pops in
|
|
47
|
+
const POST_MS = 240 // dwell after a completed assistant turn
|
|
48
|
+
|
|
49
|
+
/** Split text into tokens that KEEP surrounding whitespace, so re-joining a
|
|
50
|
+
* prefix reproduces the original spacing exactly (`"a b"` → `["a ", "b"]`). */
|
|
51
|
+
function tokenize(text: string): string[] {
|
|
52
|
+
return text.match(/\s*\S+\s*/g) ?? (text ? [text] : [])
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Expand a message list into ordered playback frames reproducing the live
|
|
57
|
+
* stream: each user turn lands whole; each assistant turn first shows a
|
|
58
|
+
* `thinking` indicator, then streams its text token-by-token, then pops any
|
|
59
|
+
* tool / approval segments. Frame COUNT for a given prefix is deterministic, so
|
|
60
|
+
* a host that appends a continuation (e.g. after an Approve) resumes seamlessly
|
|
61
|
+
* from where it paused — the shared prefix's frames are unchanged.
|
|
62
|
+
*/
|
|
63
|
+
export function buildStreamFrames(messages: HistoricalMessage[]): StreamFrame[] {
|
|
64
|
+
const frames: StreamFrame[] = []
|
|
65
|
+
const revealed: HistoricalMessage[] = []
|
|
66
|
+
const push = (msgs: HistoricalMessage[], phase: StreamingPhase, typing: boolean, delayMs: number) =>
|
|
67
|
+
frames.push({ messages: msgs.slice(), phase, typing, delayMs })
|
|
68
|
+
|
|
69
|
+
messages.forEach((msg) => {
|
|
70
|
+
if (isHistoricalUserTurn(msg)) {
|
|
71
|
+
revealed.push(msg)
|
|
72
|
+
push(revealed, 'idle', false, USER_MS)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Assistant turn: a `thinking` pause, then stream this turn's segments in
|
|
77
|
+
// order. The pause is emitted ONLY when there is already content to show
|
|
78
|
+
// (`revealed.length > 0`) — an empty first frame would hand the surface a
|
|
79
|
+
// zero-message list, flashing its empty state before the opening line.
|
|
80
|
+
if (revealed.length > 0) push(revealed, 'thinking', true, THINK_MS)
|
|
81
|
+
|
|
82
|
+
const segs = normalizeHistoricalMessageData(msg.messageData)
|
|
83
|
+
const built: MessageData[] = []
|
|
84
|
+
const emit = () => push([...revealed, { ...msg, messageData: [...built] }], 'streaming', false, 0)
|
|
85
|
+
|
|
86
|
+
segs.forEach((seg) => {
|
|
87
|
+
if (isTextSegment(seg)) {
|
|
88
|
+
const tokens = tokenize(seg.text)
|
|
89
|
+
let acc = ''
|
|
90
|
+
built.push({ ...seg, text: '' })
|
|
91
|
+
tokens.forEach((tok) => {
|
|
92
|
+
acc += tok
|
|
93
|
+
built[built.length - 1] = { ...seg, text: acc }
|
|
94
|
+
push([...revealed, { ...msg, messageData: [...built] }], 'streaming', false, TOKEN_MS)
|
|
95
|
+
})
|
|
96
|
+
built[built.length - 1] = { ...seg }
|
|
97
|
+
} else {
|
|
98
|
+
// Tool / approval segment: pops in fully.
|
|
99
|
+
built.push(seg)
|
|
100
|
+
emit()
|
|
101
|
+
frames[frames.length - 1].delayMs = SEGMENT_MS
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
revealed.push(msg)
|
|
106
|
+
push(revealed, 'idle', false, POST_MS)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
if (frames.length === 0) push(revealed, 'idle', false, 0)
|
|
110
|
+
return frames
|
|
111
|
+
}
|
|
@@ -6,8 +6,8 @@ export { BoardColumn } from './board-column'
|
|
|
6
6
|
export type { BoardColumnProps } from './board-column'
|
|
7
7
|
export { BoardColumnHeader } from './board-column-header'
|
|
8
8
|
export type { BoardColumnHeaderProps } from './board-column-header'
|
|
9
|
-
export { TicketCard } from './ticket-card'
|
|
10
|
-
export type { TicketCardProps } from './ticket-card'
|
|
9
|
+
export { TicketCard, TicketCardView, TicketCardBody } from './ticket-card'
|
|
10
|
+
export type { TicketCardProps, TicketCardViewProps, TicketCardBodyProps } from './ticket-card'
|
|
11
11
|
export { BoardTicketApproval } from './board-ticket-approval'
|
|
12
12
|
export type { BoardTicketApprovalProps } from './board-ticket-approval'
|
|
13
13
|
export { TicketCardSkeleton } from './ticket-card-skeleton'
|
|
@@ -4,7 +4,7 @@ import { useSortable } from '@dnd-kit/sortable'
|
|
|
4
4
|
import { CSS } from '@dnd-kit/utilities'
|
|
5
5
|
import Link from '../../../embed-shims/next-link'
|
|
6
6
|
import * as React from 'react'
|
|
7
|
-
import { LaptopIcon, Flag02Icon, MessagesIcon
|
|
7
|
+
import { LaptopIcon, Flag02Icon, MessagesIcon } from '../../icons-v2-generated'
|
|
8
8
|
import { SquareAvatar } from '../../ui/square-avatar'
|
|
9
9
|
import { Tag } from '../../ui/tag'
|
|
10
10
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/tooltip'
|
|
@@ -24,64 +24,36 @@ const PRIORITY_COLOR_CLASS: Record<BoardPriority, string> = {
|
|
|
24
24
|
const MAX_VISIBLE_TAGS = 2
|
|
25
25
|
const MAX_VISIBLE_ASSIGNEES = 3
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
/** Shared card shell (border / padding / bg). Same footprint for the draggable
|
|
28
|
+
* board card and the static {@link TicketCardView}. */
|
|
29
|
+
const TICKET_CARD_SHELL =
|
|
30
|
+
'relative flex flex-col gap-[var(--spacing-system-sf)] rounded-md border border-ods-border bg-ods-bg p-[var(--spacing-system-sf)] select-none text-left'
|
|
31
|
+
|
|
32
|
+
// =============================================================================
|
|
33
|
+
// Presentational body — the ONE ticket-card design, no drag/link/board context.
|
|
34
|
+
// Reused by the draggable `TicketCard` AND the static `TicketCardView`.
|
|
35
|
+
// =============================================================================
|
|
36
|
+
|
|
37
|
+
export interface TicketCardBodyProps {
|
|
28
38
|
ticket: BoardTicket
|
|
29
|
-
columnId: string
|
|
30
39
|
columnColor?: string
|
|
31
|
-
href?: string
|
|
32
|
-
isOverlay?: boolean
|
|
33
|
-
dragDisabled?: boolean
|
|
34
|
-
approved?: boolean
|
|
35
40
|
renderAssignSlot?: (ticket: BoardTicket) => React.ReactNode
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
/** Approval callbacks receive the request id directly (the draggable
|
|
42
|
+
* `TicketCard` adapts its own `(ticketId, requestId)` signature onto these). */
|
|
43
|
+
onApprove?: (requestId?: string) => void | Promise<void>
|
|
44
|
+
onReject?: (requestId?: string) => void | Promise<void>
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
columnColor,
|
|
44
|
-
href,
|
|
45
|
-
isOverlay = false,
|
|
46
|
-
dragDisabled,
|
|
47
|
-
approved = false,
|
|
48
|
-
renderAssignSlot,
|
|
49
|
-
onApprove,
|
|
50
|
-
onReject,
|
|
51
|
-
}: TicketCardProps) {
|
|
52
|
-
const sortableData = React.useMemo(
|
|
53
|
-
() => ({ columnId, type: 'ticket' as const }),
|
|
54
|
-
[columnId],
|
|
55
|
-
)
|
|
56
|
-
const sortable = useSortable({
|
|
57
|
-
id: ticket.id,
|
|
58
|
-
data: sortableData,
|
|
59
|
-
disabled: dragDisabled,
|
|
60
|
-
})
|
|
61
|
-
|
|
47
|
+
/** The card's inner content: title + device/org, priority + assignees, tags,
|
|
48
|
+
* timestamp, "New Message", and the approval row. Pure — driven only by props. */
|
|
49
|
+
export function TicketCardBody({ ticket, columnColor, renderAssignSlot, onApprove, onReject }: TicketCardBodyProps) {
|
|
62
50
|
const showNewMessage = !!ticket.hasNewMessage && !!columnColor
|
|
63
51
|
const newMessageTextColor = columnColor ? getReadableTextColor(columnColor) : undefined
|
|
64
52
|
|
|
65
|
-
const style: React.CSSProperties = isOverlay
|
|
66
|
-
? {}
|
|
67
|
-
: {
|
|
68
|
-
transform: CSS.Transform.toString(sortable.transform),
|
|
69
|
-
transition: sortable.transition,
|
|
70
|
-
}
|
|
71
|
-
if (showNewMessage) style.borderColor = columnColor
|
|
72
|
-
|
|
73
53
|
const showDeviceRow = !!(ticket.deviceHostnames?.length || ticket.organizationName)
|
|
74
|
-
const deviceText = [
|
|
75
|
-
ticket.deviceHostnames?.join(', '),
|
|
76
|
-
ticket.organizationName,
|
|
77
|
-
].filter(Boolean).join(', ')
|
|
78
|
-
|
|
79
|
-
const handleClick = (e: React.MouseEvent) => {
|
|
80
|
-
if (sortable.isDragging) e.preventDefault()
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const hasRightSection = !!(approved || ticket.priority || ticket.assignees?.length || renderAssignSlot)
|
|
54
|
+
const deviceText = [ticket.deviceHostnames?.join(', '), ticket.organizationName].filter(Boolean).join(', ')
|
|
84
55
|
|
|
56
|
+
const hasRightSection = !!(ticket.priority || ticket.assignees?.length || renderAssignSlot)
|
|
85
57
|
const rightSection = hasRightSection ? (
|
|
86
58
|
<div className="pointer-events-auto flex shrink-0 items-center gap-[var(--spacing-system-xsf)]">
|
|
87
59
|
{ticket.priority && (
|
|
@@ -90,14 +62,6 @@ export function TicketCard({
|
|
|
90
62
|
aria-label={`Priority: ${ticket.priority}`}
|
|
91
63
|
/>
|
|
92
64
|
)}
|
|
93
|
-
{approved && (
|
|
94
|
-
<CheckCircleIcon
|
|
95
|
-
size={20}
|
|
96
|
-
color="var(--ods-attention-green-success)"
|
|
97
|
-
className="shrink-0"
|
|
98
|
-
aria-label="Approved"
|
|
99
|
-
/>
|
|
100
|
-
)}
|
|
101
65
|
{renderAssignSlot ? (
|
|
102
66
|
renderAssignSlot(ticket)
|
|
103
67
|
) : ticket.assignees?.length ? (
|
|
@@ -125,7 +89,7 @@ export function TicketCard({
|
|
|
125
89
|
const timestampLabel = ticket.createdAt ? formatTicketRelativeTime(ticket.createdAt) : null
|
|
126
90
|
const tooltipLabel = ticket.createdAt ? formatTicketFullTimestamp(ticket.createdAt) : null
|
|
127
91
|
|
|
128
|
-
|
|
92
|
+
return (
|
|
129
93
|
<>
|
|
130
94
|
<div className="flex items-start gap-[var(--spacing-system-sf)]">
|
|
131
95
|
<div className="flex min-w-0 flex-1 flex-col gap-[var(--spacing-system-zero)]" title={ticket.title}>
|
|
@@ -159,17 +123,97 @@ export function TicketCard({
|
|
|
159
123
|
/>
|
|
160
124
|
)}
|
|
161
125
|
{ticket.pendingApproval && (
|
|
162
|
-
<BoardTicketApproval
|
|
163
|
-
pendingApproval={ticket.pendingApproval}
|
|
164
|
-
onApprove={onApprove ? requestId => onApprove(ticket.id, requestId) : undefined}
|
|
165
|
-
onReject={onReject ? requestId => onReject(ticket.id, requestId) : undefined}
|
|
166
|
-
/>
|
|
126
|
+
<BoardTicketApproval pendingApproval={ticket.pendingApproval} onApprove={onApprove} onReject={onReject} />
|
|
167
127
|
)}
|
|
168
128
|
</>
|
|
169
129
|
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// =============================================================================
|
|
133
|
+
// Static card — the real board card design, rendered from props with NO
|
|
134
|
+
// drag-and-drop / board context (for embeds, marketing heroes, previews).
|
|
135
|
+
// =============================================================================
|
|
136
|
+
|
|
137
|
+
export interface TicketCardViewProps extends TicketCardBodyProps {
|
|
138
|
+
className?: string
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The exact board `TicketCard` visual, standalone — same shell + `TicketCardBody`
|
|
143
|
+
* as the draggable card, but without the `useSortable` dnd-kit dependency, so it
|
|
144
|
+
* renders anywhere from static props. Use this outside a `<Board>`.
|
|
145
|
+
*/
|
|
146
|
+
export function TicketCardView({ className, ...bodyProps }: TicketCardViewProps) {
|
|
147
|
+
const { ticket, columnColor } = bodyProps
|
|
148
|
+
const showNewMessage = !!ticket.hasNewMessage && !!columnColor
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
className={cn(TICKET_CARD_SHELL, className)}
|
|
152
|
+
style={showNewMessage ? { borderColor: columnColor } : undefined}
|
|
153
|
+
>
|
|
154
|
+
<div className="relative z-10 flex flex-col gap-[var(--spacing-system-sf)]">
|
|
155
|
+
<TicketCardBody {...bodyProps} />
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// =============================================================================
|
|
162
|
+
// Draggable board card
|
|
163
|
+
// =============================================================================
|
|
164
|
+
|
|
165
|
+
export interface TicketCardProps {
|
|
166
|
+
ticket: BoardTicket
|
|
167
|
+
columnId: string
|
|
168
|
+
columnColor?: string
|
|
169
|
+
href?: string
|
|
170
|
+
isOverlay?: boolean
|
|
171
|
+
dragDisabled?: boolean
|
|
172
|
+
renderAssignSlot?: (ticket: BoardTicket) => React.ReactNode
|
|
173
|
+
onApprove?: (ticketId: string, requestId?: string) => void | Promise<void>
|
|
174
|
+
onReject?: (ticketId: string, requestId?: string) => void | Promise<void>
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function TicketCard({
|
|
178
|
+
ticket,
|
|
179
|
+
columnId,
|
|
180
|
+
columnColor,
|
|
181
|
+
href,
|
|
182
|
+
isOverlay = false,
|
|
183
|
+
dragDisabled,
|
|
184
|
+
renderAssignSlot,
|
|
185
|
+
onApprove,
|
|
186
|
+
onReject,
|
|
187
|
+
}: TicketCardProps) {
|
|
188
|
+
const sortableData = React.useMemo(() => ({ columnId, type: 'ticket' as const }), [columnId])
|
|
189
|
+
const sortable = useSortable({ id: ticket.id, data: sortableData, disabled: dragDisabled })
|
|
190
|
+
|
|
191
|
+
const showNewMessage = !!ticket.hasNewMessage && !!columnColor
|
|
192
|
+
|
|
193
|
+
const style: React.CSSProperties = isOverlay
|
|
194
|
+
? {}
|
|
195
|
+
: {
|
|
196
|
+
transform: CSS.Transform.toString(sortable.transform),
|
|
197
|
+
transition: sortable.transition,
|
|
198
|
+
}
|
|
199
|
+
if (showNewMessage) style.borderColor = columnColor
|
|
200
|
+
|
|
201
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
202
|
+
if (sortable.isDragging) e.preventDefault()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const body = (
|
|
206
|
+
<TicketCardBody
|
|
207
|
+
ticket={ticket}
|
|
208
|
+
columnColor={columnColor}
|
|
209
|
+
renderAssignSlot={renderAssignSlot}
|
|
210
|
+
onApprove={onApprove ? requestId => onApprove(ticket.id, requestId) : undefined}
|
|
211
|
+
onReject={onReject ? requestId => onReject(ticket.id, requestId) : undefined}
|
|
212
|
+
/>
|
|
213
|
+
)
|
|
170
214
|
|
|
171
215
|
const cardClasses = cn(
|
|
172
|
-
|
|
216
|
+
TICKET_CARD_SHELL,
|
|
173
217
|
!dragDisabled && 'cursor-pointer',
|
|
174
218
|
sortable.isDragging && !isOverlay && 'opacity-40',
|
|
175
219
|
isOverlay && 'rotate-1 shadow-card-hover',
|