@flamingo-stack/openframe-frontend-core 0.0.565-1861.5100.1 → 0.0.565
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/chat-protocol/index.cjs.map +1 -1
- package/dist/chat-protocol/index.js.map +1 -1
- package/dist/{chunk-TA5P2M7V.cjs → chunk-26LZFWY6.cjs} +732 -812
- package/dist/chunk-26LZFWY6.cjs.map +1 -0
- package/dist/{chunk-KHEOXHF4.cjs → chunk-32QBTN5L.cjs} +5 -5
- package/dist/{chunk-KHEOXHF4.cjs.map → chunk-32QBTN5L.cjs.map} +1 -1
- package/dist/{chunk-GG6CAELN.js → chunk-3FBPPKV2.js} +4 -4
- package/dist/{chunk-MSUSONUK.js → chunk-5JYX2GOP.js} +3233 -3313
- package/dist/chunk-5JYX2GOP.js.map +1 -0
- package/dist/{chunk-EQEMZU45.js → chunk-5K3F5AZX.js} +4 -4
- package/dist/{chunk-YVOMHB73.js → chunk-5MVE77D5.js} +21 -35
- package/dist/chunk-5MVE77D5.js.map +1 -0
- package/dist/{chunk-H4RCJ4IB.js → chunk-6F47STCR.js} +3 -7
- package/dist/{chunk-H4RCJ4IB.js.map → chunk-6F47STCR.js.map} +1 -1
- package/dist/{chunk-XPLZB74T.js → chunk-6RILRC7X.js} +2 -2
- package/dist/{chunk-BKDSNVGH.cjs → chunk-7FH6MZ6M.cjs} +9 -9
- package/dist/{chunk-BKDSNVGH.cjs.map → chunk-7FH6MZ6M.cjs.map} +1 -1
- package/dist/{chunk-ZUVVJ24E.js → chunk-7IR5LKYC.js} +2 -2
- package/dist/{chunk-GG2HJQW3.cjs → chunk-AN7YK35P.cjs} +26 -26
- package/dist/{chunk-GG2HJQW3.cjs.map → chunk-AN7YK35P.cjs.map} +1 -1
- package/dist/{chunk-PZH3CZTJ.cjs → chunk-CCFBL7JU.cjs} +75 -89
- package/dist/chunk-CCFBL7JU.cjs.map +1 -0
- package/dist/{chunk-OEN3GTRL.js → chunk-DEYRHTLD.js} +2 -2
- package/dist/{chunk-MIKGZY7V.js → chunk-GCMASPSW.js} +5 -5
- package/dist/{chunk-ZYC4O76Y.js → chunk-HJ7OVIR7.js} +2 -2
- package/dist/{chunk-BWMVI2KD.cjs → chunk-HVS56KQE.cjs} +7 -7
- package/dist/{chunk-BWMVI2KD.cjs.map → chunk-HVS56KQE.cjs.map} +1 -1
- package/dist/{chunk-GZL5SAUI.cjs → chunk-HWQ5HMJN.cjs} +15 -19
- package/dist/chunk-HWQ5HMJN.cjs.map +1 -0
- package/dist/{chunk-NAYXROCV.cjs → chunk-ISK6LRRY.cjs} +3 -3
- package/dist/{chunk-NAYXROCV.cjs.map → chunk-ISK6LRRY.cjs.map} +1 -1
- package/dist/{chunk-4H7L4OSR.cjs → chunk-LOLVMAT3.cjs} +4 -4
- package/dist/{chunk-4H7L4OSR.cjs.map → chunk-LOLVMAT3.cjs.map} +1 -1
- package/dist/{chunk-XZNKPEWB.js → chunk-MIBKEMZY.js} +2 -2
- package/dist/chunk-MVWSLZG7.js +2 -0
- package/dist/{chunk-QWVON3LG.cjs → chunk-NCJUIT3Q.cjs} +20 -20
- package/dist/{chunk-QWVON3LG.cjs.map → chunk-NCJUIT3Q.cjs.map} +1 -1
- package/dist/{chunk-OTDVQBLI.cjs → chunk-NM5NBMMD.cjs} +31 -31
- package/dist/{chunk-OTDVQBLI.cjs.map → chunk-NM5NBMMD.cjs.map} +1 -1
- package/dist/{chunk-SQ53GVCG.js → chunk-QQFUJ6MS.js} +2 -2
- package/dist/{chunk-PBH2BQYR.cjs → chunk-TFEL2C6I.cjs} +86 -86
- package/dist/{chunk-PBH2BQYR.cjs.map → chunk-TFEL2C6I.cjs.map} +1 -1
- package/dist/{chunk-DLSDYLY3.cjs → chunk-TFRJ7B4X.cjs} +11 -11
- package/dist/{chunk-DLSDYLY3.cjs.map → chunk-TFRJ7B4X.cjs.map} +1 -1
- package/dist/{chunk-Y6IGG6RZ.cjs → chunk-TGT3JHVU.cjs} +39 -39
- package/dist/{chunk-Y6IGG6RZ.cjs.map → chunk-TGT3JHVU.cjs.map} +1 -1
- package/dist/{chunk-XSK7ZQ3C.js → chunk-TXLUKQBI.js} +2 -2
- package/dist/{chunk-LOWWLH2W.js → chunk-UK6SSMUR.js} +2 -2
- package/dist/chunk-ZTM35Q76.cjs +2 -0
- package/dist/{chunk-UGAHDPNU.cjs.map → chunk-ZTM35Q76.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/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/employee-entry-card.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/chat/index.js +1 -1
- package/dist/components/chat/stream/chat-stream-reducer.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +8 -0
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/comment-card.d.ts +2 -18
- package/dist/components/comment-card.d.ts.map +1 -1
- 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 +4 -6
- package/dist/components/embeds/index.cjs.map +1 -1
- package/dist/components/embeds/index.d.ts +0 -2
- package/dist/components/embeds/index.d.ts.map +1 -1
- package/dist/components/embeds/index.js +3 -5
- package/dist/components/faq/index.cjs +3 -3
- package/dist/components/faq/index.js +2 -2
- package/dist/components/features/array-entry-manager.d.ts.map +1 -1
- package/dist/components/features/clickup-tasks-manager.d.ts +1 -6
- package/dist/components/features/clickup-tasks-manager.d.ts.map +1 -1
- package/dist/components/features/index.cjs +2 -2
- package/dist/components/features/index.js +1 -1
- package/dist/components/help-center-pages/index.cjs +22 -22
- package/dist/components/help-center-pages/index.js +13 -13
- package/dist/components/index.cjs +204 -281
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.ts +0 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +570 -647
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +30 -30
- package/dist/components/meeting-scheduler/index.js +3 -3
- 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/shared/delivery/delivery-row.d.ts +1 -5
- package/dist/components/shared/delivery/delivery-row.d.ts.map +1 -1
- package/dist/components/shared/roadmap/roadmap-grid.d.ts +1 -10
- package/dist/components/shared/roadmap/roadmap-grid.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +6 -6
- package/dist/components/tickets/index.js +5 -5
- package/dist/components/ui/card.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +2 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -1
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -3
- package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -1
- package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -1
- package/dist/components/ui/table/index.d.ts +1 -1
- package/dist/components/ui/table/index.d.ts.map +1 -1
- package/dist/components/ui/table/table-row.d.ts.map +1 -1
- package/dist/components/ui/table/table-skeleton.d.ts +2 -15
- package/dist/components/ui/table/table-skeleton.d.ts.map +1 -1
- package/dist/components/ui/table/table.d.ts.map +1 -1
- package/dist/components/ui/table/types.d.ts +0 -2
- package/dist/components/ui/table/types.d.ts.map +1 -1
- package/dist/index.cjs +2 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -5
- package/dist/types/employee.d.ts +2 -25
- package/dist/types/employee.d.ts.map +1 -1
- package/dist/types/index.d.ts +0 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/marketing.d.ts +1 -1
- package/dist/types/marketing.d.ts.map +1 -1
- package/dist/types/profile.d.ts +1 -2
- package/dist/types/profile.d.ts.map +1 -1
- package/dist/types/team.d.ts +1 -15
- package/dist/types/team.d.ts.map +1 -1
- package/dist/types/user.d.ts +1 -3
- package/dist/types/user.d.ts.map +1 -1
- package/dist/utils/embed-url-converters.d.ts +0 -17
- package/dist/utils/embed-url-converters.d.ts.map +1 -1
- package/dist/utils/index.cjs +0 -12
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +1 -12
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/.comment-card.md +5 -4
- package/src/components/chat/__tests__/ticket-event.test.ts +86 -0
- package/src/components/chat/chat-message-enhanced.tsx +3 -1
- package/src/components/chat/entity-cards/employee-entry-card.tsx +0 -6
- package/src/components/chat/stream/chat-stream-reducer.ts +7 -1
- package/src/components/chat/types/message.types.ts +8 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +10 -3
- package/src/components/chat/utils/process-historical-messages.ts +17 -8
- package/src/components/comment-card.tsx +17 -42
- package/src/components/embeds/index.ts +0 -2
- package/src/components/features/array-entry-manager.tsx +1 -2
- package/src/components/features/clickup-tasks-manager.tsx +1 -8
- package/src/components/index.ts +0 -1
- package/src/components/shared/delivery/delivery-row.tsx +1 -17
- package/src/components/shared/roadmap/roadmap-grid.tsx +1 -21
- package/src/components/ui/card.tsx +1 -4
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/markdown/rich/__tests__/shortcodes.test.ts +0 -44
- package/src/components/ui/markdown/rich/embed-overrides.tsx +0 -4
- package/src/components/ui/markdown/rich/shortcodes.ts +0 -12
- package/src/components/ui/table/index.ts +1 -1
- package/src/components/ui/table/table-row.tsx +2 -7
- package/src/components/ui/table/table-skeleton.tsx +3 -20
- package/src/components/ui/table/table.tsx +0 -1
- package/src/components/ui/table/types.ts +0 -2
- package/src/types/employee.ts +2 -27
- package/src/types/index.ts +0 -2
- package/src/types/marketing.ts +0 -1
- package/src/types/profile.ts +1 -3
- package/src/types/team.ts +1 -17
- package/src/types/user.ts +1 -4
- package/src/utils/embed-url-converters.ts +0 -32
- package/dist/chunk-5I3BSQ6D.js +0 -2
- package/dist/chunk-GZL5SAUI.cjs.map +0 -1
- package/dist/chunk-MSUSONUK.js.map +0 -1
- package/dist/chunk-PZH3CZTJ.cjs.map +0 -1
- package/dist/chunk-TA5P2M7V.cjs.map +0 -1
- package/dist/chunk-UGAHDPNU.cjs +0 -2
- package/dist/chunk-YVOMHB73.js.map +0 -1
- package/dist/components/comment-composer.d.ts +0 -42
- package/dist/components/comment-composer.d.ts.map +0 -1
- package/dist/components/embeds/claude-embed.d.ts +0 -31
- package/dist/components/embeds/claude-embed.d.ts.map +0 -1
- package/dist/types/department.d.ts +0 -41
- package/dist/types/department.d.ts.map +0 -1
- package/dist/types/design-doc.d.ts +0 -247
- package/dist/types/design-doc.d.ts.map +0 -1
- package/src/components/comment-composer.tsx +0 -117
- package/src/components/embeds/claude-embed.tsx +0 -84
- package/src/types/department.ts +0 -44
- package/src/types/design-doc.ts +0 -295
- package/src/utils/__tests__/claude-embed-url.test.ts +0 -42
- /package/dist/{chunk-GG6CAELN.js.map → chunk-3FBPPKV2.js.map} +0 -0
- /package/dist/{chunk-EQEMZU45.js.map → chunk-5K3F5AZX.js.map} +0 -0
- /package/dist/{chunk-XPLZB74T.js.map → chunk-6RILRC7X.js.map} +0 -0
- /package/dist/{chunk-ZUVVJ24E.js.map → chunk-7IR5LKYC.js.map} +0 -0
- /package/dist/{chunk-OEN3GTRL.js.map → chunk-DEYRHTLD.js.map} +0 -0
- /package/dist/{chunk-MIKGZY7V.js.map → chunk-GCMASPSW.js.map} +0 -0
- /package/dist/{chunk-ZYC4O76Y.js.map → chunk-HJ7OVIR7.js.map} +0 -0
- /package/dist/{chunk-XZNKPEWB.js.map → chunk-MIBKEMZY.js.map} +0 -0
- /package/dist/{chunk-5I3BSQ6D.js.map → chunk-MVWSLZG7.js.map} +0 -0
- /package/dist/{chunk-SQ53GVCG.js.map → chunk-QQFUJ6MS.js.map} +0 -0
- /package/dist/{chunk-XSK7ZQ3C.js.map → chunk-TXLUKQBI.js.map} +0 -0
- /package/dist/{chunk-LOWWLH2W.js.map → chunk-UK6SSMUR.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
<!-- source-hash: 2dc22952dfcc6e283cfc3b6d4d636ade -->
|
|
2
|
-
Renders a styled comment card that adapts its
|
|
2
|
+
Renders a styled comment card that adapts its layout based on context — showing vendor information when viewing a user profile, or user information when viewing a vendor page — with optional delete functionality based on auth state.
|
|
3
3
|
|
|
4
4
|
## Key Components
|
|
5
5
|
|
|
6
6
|
### `CommentCard` (default export)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
A context-aware card component for displaying user-submitted vendor comments.
|
|
9
9
|
|
|
10
10
|
**Props (`CommentCardProps`):**
|
|
11
11
|
|
|
12
12
|
| Prop | Type | Default | Description |
|
|
13
13
|
|------|------|---------|-------------|
|
|
14
14
|
| `comment` | `object` | — | Comment data including content, author, vendor, and metadata |
|
|
15
|
-
| `
|
|
15
|
+
| `context` | `'profile' \| 'vendor'` | `'profile'` | Controls layout and delete permission logic |
|
|
16
16
|
| `onViewProduct` | `(slug: string) => void` | — | Callback when vendor button is clicked |
|
|
17
17
|
| `onDeleteComment` | `(id: string) => void` | — | Callback when delete is triggered |
|
|
18
18
|
| `showVendorInfo` | `boolean` | `true` | Toggles vendor info display |
|
|
@@ -20,13 +20,14 @@ The comment tile: one card for every comment surface, with the header lead, the
|
|
|
20
20
|
|
|
21
21
|
**Internal logic:**
|
|
22
22
|
|
|
23
|
-
- `canUserDeleteComment()` —
|
|
23
|
+
- `canUserDeleteComment()` — Grants delete access to authenticated users; in `profile` context, respects the `canDelete` prop; in `vendor` context, compares `currentUser.id` against `comment.user.id`
|
|
24
24
|
- `formatActivityTime()` — Delegates to `formatRelativeTime` utility for human-readable timestamps
|
|
25
25
|
|
|
26
26
|
## Usage Example
|
|
27
27
|
|
|
28
28
|
```typescript
|
|
29
29
|
<CommentCard
|
|
30
|
+
context="vendor"
|
|
30
31
|
comment={{
|
|
31
32
|
id: "abc123",
|
|
32
33
|
content: "Solid RMM tool for small MSPs.",
|
|
@@ -134,6 +134,21 @@ describe('chat stream reducer — ticket event', () => {
|
|
|
134
134
|
])
|
|
135
135
|
expect(events(trailingSegments(reducer))).toHaveLength(3)
|
|
136
136
|
})
|
|
137
|
+
|
|
138
|
+
it('stamps a live card with its arrival time, not the bubble timestamp', () => {
|
|
139
|
+
// The chunk carries no event time, so arrival IS the event time for a
|
|
140
|
+
// genuinely live receipt. Without the stamp the card renders the trailing
|
|
141
|
+
// bubble's timestamp — the turn's FIRST row, minutes stale by the time a
|
|
142
|
+
// resolve/reopen lands.
|
|
143
|
+
const reducer = createChatStreamReducer({ transport: 'nats' })
|
|
144
|
+
const before = Date.now()
|
|
145
|
+
feed(reducer, [resolvedChunk(1)])
|
|
146
|
+
const after = Date.now()
|
|
147
|
+
const [card] = events(trailingSegments(reducer))
|
|
148
|
+
expect(card.occurredAt).toBeInstanceOf(Date)
|
|
149
|
+
expect(card.occurredAt!.getTime()).toBeGreaterThanOrEqual(before)
|
|
150
|
+
expect(card.occurredAt!.getTime()).toBeLessThanOrEqual(after)
|
|
151
|
+
})
|
|
137
152
|
})
|
|
138
153
|
|
|
139
154
|
describe('accumulator — hydrate/catch-up overlap', () => {
|
|
@@ -262,4 +277,75 @@ describe('processHistoricalMessages — ticket event', () => {
|
|
|
262
277
|
])
|
|
263
278
|
expect(messages.flatMap((m) => events(m.content))).toHaveLength(0)
|
|
264
279
|
})
|
|
280
|
+
|
|
281
|
+
it('stamps each card with its OWN row time, not the bubble timestamp', () => {
|
|
282
|
+
// The reported bug: resolve/reopen rows grouped into one assistant bubble
|
|
283
|
+
// all rendered the bubble's timestamp — the FIRST row of the turn (the
|
|
284
|
+
// ticket-creation-era greeting), so every lifecycle card read the same
|
|
285
|
+
// stale time.
|
|
286
|
+
const rowAt = (id: string, createdAt: string, data: Record<string, unknown>): HistoricalMessage => ({
|
|
287
|
+
id,
|
|
288
|
+
createdAt,
|
|
289
|
+
owner: { type: 'ASSISTANT' },
|
|
290
|
+
messageData: [data as never],
|
|
291
|
+
})
|
|
292
|
+
const { messages } = processHistoricalMessages([
|
|
293
|
+
rowAt('m1', '2026-08-20T13:54:00Z', { type: 'TEXT', text: 'Hi! How can I help?' }),
|
|
294
|
+
rowAt('m2', '2026-08-20T14:10:00Z', { type: 'TICKET_EVENT', kind: 'RESOLVED', actorType: 'AI' }),
|
|
295
|
+
rowAt('m3', '2026-08-20T14:33:00Z', {
|
|
296
|
+
type: 'TICKET_EVENT',
|
|
297
|
+
kind: 'REOPENED',
|
|
298
|
+
actorType: 'CLIENT',
|
|
299
|
+
targetStatusKind: 'AI_ASSISTANCE',
|
|
300
|
+
}),
|
|
301
|
+
])
|
|
302
|
+
expect(messages).toHaveLength(1)
|
|
303
|
+
// The bubble keeps the first row's time…
|
|
304
|
+
expect(messages[0].timestamp).toEqual(new Date('2026-08-20T13:54:00Z'))
|
|
305
|
+
// …but each card carries the time of its own event.
|
|
306
|
+
const cards = events(messages[0].content)
|
|
307
|
+
expect(cards.map((c) => c.occurredAt)).toEqual([
|
|
308
|
+
new Date('2026-08-20T14:10:00Z'),
|
|
309
|
+
new Date('2026-08-20T14:33:00Z'),
|
|
310
|
+
])
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
describe('accumulator — occurredAt across the hydrate/catch-up overlap', () => {
|
|
315
|
+
it('keeps the hydrated row time when the seq-stamped redelivery carries none', () => {
|
|
316
|
+
const accumulator = new MessageSegmentAccumulator()
|
|
317
|
+
accumulator.addTicketEvent(
|
|
318
|
+
{ kind: 'RESOLVED', actorId: 'fae', actorName: 'Fae', actorType: 'AI' },
|
|
319
|
+
undefined,
|
|
320
|
+
new Date('2026-08-20T14:10:00Z'),
|
|
321
|
+
)
|
|
322
|
+
// Same event redelivered live (JetStream catch-up): seq known, time not.
|
|
323
|
+
const segments = accumulator.addTicketEvent(
|
|
324
|
+
{ kind: 'RESOLVED', actorId: 'fae', actorName: 'Fae', actorType: 'AI' },
|
|
325
|
+
412,
|
|
326
|
+
)
|
|
327
|
+
const rendered = events(segments)
|
|
328
|
+
expect(rendered).toHaveLength(1)
|
|
329
|
+
expect(rendered[0].streamSeq).toBe(412)
|
|
330
|
+
expect(rendered[0].occurredAt).toEqual(new Date('2026-08-20T14:10:00Z'))
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('keeps the hydrated row time over a LATE redelivery arrival stamp', () => {
|
|
334
|
+
// Reconnect catch-up redelivers an hour-old event stamped with its (late)
|
|
335
|
+
// arrival time — the hydrated twin's real row time must win.
|
|
336
|
+
const accumulator = new MessageSegmentAccumulator()
|
|
337
|
+
accumulator.addTicketEvent(
|
|
338
|
+
{ kind: 'RESOLVED', actorId: 'fae', actorName: 'Fae', actorType: 'AI' },
|
|
339
|
+
undefined,
|
|
340
|
+
new Date('2026-08-20T14:10:00Z'),
|
|
341
|
+
)
|
|
342
|
+
const segments = accumulator.addTicketEvent(
|
|
343
|
+
{ kind: 'RESOLVED', actorId: 'fae', actorName: 'Fae', actorType: 'AI' },
|
|
344
|
+
412,
|
|
345
|
+
new Date('2026-08-20T15:10:00Z'),
|
|
346
|
+
)
|
|
347
|
+
const rendered = events(segments)
|
|
348
|
+
expect(rendered).toHaveLength(1)
|
|
349
|
+
expect(rendered[0].occurredAt).toEqual(new Date('2026-08-20T14:10:00Z'))
|
|
350
|
+
})
|
|
265
351
|
})
|
|
@@ -652,7 +652,9 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
652
652
|
} else if (segment.type === 'ticket_escalated') {
|
|
653
653
|
return <TicketEscalatedMessage key={index} data={segment.data} timestamp={timestamp} />
|
|
654
654
|
} else if (segment.type === 'ticket_event') {
|
|
655
|
-
|
|
655
|
+
// The card's own event time; the bubble timestamp is the
|
|
656
|
+
// turn's FIRST row and lags every later lifecycle event.
|
|
657
|
+
return <TicketEventMessage key={index} data={segment.data} timestamp={segment.occurredAt ?? timestamp} />
|
|
656
658
|
} else if (segment.type === 'error') {
|
|
657
659
|
return (
|
|
658
660
|
<ErrorMessageDisplay
|
|
@@ -38,12 +38,6 @@ const STATUS_BADGE_CLASS: Record<string, string> = {
|
|
|
38
38
|
published: 'bg-ods-success-secondary text-ods-success',
|
|
39
39
|
draft: 'bg-ods-warning-secondary text-ods-warning',
|
|
40
40
|
archived: 'bg-ods-border text-ods-text-secondary',
|
|
41
|
-
// Design-doc lifecycle (product-hub) — same ODS pairs, no new palette.
|
|
42
|
-
in_review: 'bg-ods-warning-secondary text-ods-warning',
|
|
43
|
-
approved: 'bg-ods-success-secondary text-ods-success',
|
|
44
|
-
building: 'bg-ods-success-secondary text-ods-success',
|
|
45
|
-
shipped: 'bg-ods-success-secondary text-ods-success',
|
|
46
|
-
abandoned: 'bg-ods-border text-ods-text-secondary',
|
|
47
41
|
}
|
|
48
42
|
|
|
49
43
|
/** Shared badge treatment so an entity's own badges (discipline, level) sit
|
|
@@ -1378,7 +1378,13 @@ export function createChatStreamReducer(
|
|
|
1378
1378
|
reason: event.reason,
|
|
1379
1379
|
targetStatusKind: event.targetStatusKind,
|
|
1380
1380
|
}
|
|
1381
|
-
|
|
1381
|
+
// The chunk carries no event time, so stamp arrival — for a genuinely
|
|
1382
|
+
// live event that IS the event time. Without it the card falls back to
|
|
1383
|
+
// the enclosing bubble's timestamp: the turn's FIRST row, minutes
|
|
1384
|
+
// stale by the time a resolve/reopen lands. A catch-up redelivery of
|
|
1385
|
+
// an already-hydrated event does NOT regress to arrival time —
|
|
1386
|
+
// `addTicketEvent` keeps the first-known `occurredAt` on upsert.
|
|
1387
|
+
const segments = accumulator.addTicketEvent(data, seq, new Date())
|
|
1382
1388
|
applyAccumulated(before, segments)
|
|
1383
1389
|
// The card alone is not enough for a host that also tracks the
|
|
1384
1390
|
// ticket's state: a RESOLVED/REOPENED arriving live must move the
|
|
@@ -359,6 +359,14 @@ export type TicketEventSegment = {
|
|
|
359
359
|
type: 'ticket_event'
|
|
360
360
|
data: TicketEventData
|
|
361
361
|
streamSeq?: number
|
|
362
|
+
/** When THIS event happened (history: the persisted row's `createdAt`;
|
|
363
|
+
* live: chunk arrival). Kept beside `streamSeq`, not in `data`: the
|
|
364
|
+
* upsert's payload-equality fallback must keep matching a redelivered
|
|
365
|
+
* chunk to its hydrated twin, and their times come from different clocks.
|
|
366
|
+
* Without it the card inherits the surrounding assistant bubble's
|
|
367
|
+
* timestamp — the FIRST row of the whole turn, so every lifecycle card
|
|
368
|
+
* reads the same stale time. */
|
|
369
|
+
occurredAt?: Date
|
|
362
370
|
}
|
|
363
371
|
|
|
364
372
|
export type ErrorSegment = {
|
|
@@ -550,11 +550,12 @@ export class MessageSegmentAccumulator {
|
|
|
550
550
|
* by the same actor is payload-identical to the first resolve, and matching
|
|
551
551
|
* the old card meant the final one never rendered.
|
|
552
552
|
*/
|
|
553
|
-
addTicketEvent(data: TicketEventData, streamSeq?: number): MessageSegment[] {
|
|
553
|
+
addTicketEvent(data: TicketEventData, streamSeq?: number, occurredAt?: Date): MessageSegment[] {
|
|
554
554
|
const segment: TicketEventSegment = {
|
|
555
555
|
type: 'ticket_event',
|
|
556
556
|
data,
|
|
557
557
|
...(streamSeq !== undefined ? { streamSeq } : {}),
|
|
558
|
+
...(occurredAt !== undefined ? { occurredAt } : {}),
|
|
558
559
|
}
|
|
559
560
|
let existingIndex =
|
|
560
561
|
streamSeq !== undefined
|
|
@@ -581,7 +582,13 @@ export class MessageSegmentAccumulator {
|
|
|
581
582
|
}
|
|
582
583
|
}
|
|
583
584
|
if (existingIndex !== -1) {
|
|
584
|
-
|
|
585
|
+
// The FIRST-known time wins: the hydrated twin carries the row's real
|
|
586
|
+
// `createdAt`, while a catch-up redelivery only knows its (late) arrival
|
|
587
|
+
// time — replacing wholesale would regress the card to arrival or, with
|
|
588
|
+
// neither side stamped, to the bubble timestamp.
|
|
589
|
+
const existing = this.segments[existingIndex] as TicketEventSegment
|
|
590
|
+
const occurredAt = existing.occurredAt ?? segment.occurredAt
|
|
591
|
+
this.segments[existingIndex] = occurredAt !== undefined ? { ...segment, occurredAt } : segment
|
|
585
592
|
return this.getSegments()
|
|
586
593
|
}
|
|
587
594
|
this.segments.push(segment)
|
|
@@ -734,7 +741,7 @@ export class MessageSegmentAccumulator {
|
|
|
734
741
|
this.addTicketEscalated(segment.data)
|
|
735
742
|
break
|
|
736
743
|
case 'ticket_event':
|
|
737
|
-
this.addTicketEvent(segment.data, segment.streamSeq)
|
|
744
|
+
this.addTicketEvent(segment.data, segment.streamSeq, segment.occurredAt)
|
|
738
745
|
break
|
|
739
746
|
case 'error':
|
|
740
747
|
this.addError(segment.title, segment.details)
|
|
@@ -330,6 +330,7 @@ function applyHistoryEvent(
|
|
|
330
330
|
options: MessageProcessingOptions,
|
|
331
331
|
escalatedApprovals?: EscalatedApprovals,
|
|
332
332
|
offerResolutions?: OfferResolutions,
|
|
333
|
+
rowCreatedAt?: Date,
|
|
333
334
|
): void {
|
|
334
335
|
// batchApprovalsEnabled is owned by the consumer (oss-tenant chat client /
|
|
335
336
|
// openframe-frontend tickets). Defaults to ON so consumers that haven't
|
|
@@ -362,14 +363,21 @@ function applyHistoryEvent(
|
|
|
362
363
|
// same event matches this segment by payload equality (see
|
|
363
364
|
// `addTicketEvent`).
|
|
364
365
|
case 'ticket-event':
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
366
|
+
// `rowCreatedAt` is the persisted row's own time — without it the card
|
|
367
|
+
// renders the enclosing assistant bubble's timestamp, i.e. the FIRST
|
|
368
|
+
// row of the turn, and every lifecycle card reads the same stale time.
|
|
369
|
+
accumulator.addTicketEvent(
|
|
370
|
+
{
|
|
371
|
+
kind: event.kind,
|
|
372
|
+
actorId: event.actorId,
|
|
373
|
+
actorName: event.actorName,
|
|
374
|
+
actorType: event.actorType,
|
|
375
|
+
reason: event.reason,
|
|
376
|
+
targetStatusKind: event.targetStatusKind,
|
|
377
|
+
},
|
|
378
|
+
undefined,
|
|
379
|
+
rowCreatedAt,
|
|
380
|
+
)
|
|
373
381
|
break
|
|
374
382
|
|
|
375
383
|
case 'escalation-offer-resolved':
|
|
@@ -738,6 +746,7 @@ export function processHistoricalMessages(
|
|
|
738
746
|
{ displayApprovalTypes, batchApprovalsEnabled, escalationOfferStates },
|
|
739
747
|
escalatedApprovals,
|
|
740
748
|
offerResolutions,
|
|
749
|
+
new Date(msg.createdAt),
|
|
741
750
|
)
|
|
742
751
|
})
|
|
743
752
|
|
|
@@ -37,23 +37,7 @@ interface CommentCardProps {
|
|
|
37
37
|
onDeleteComment?: (commentId: string) => void
|
|
38
38
|
showVendorInfo?: boolean
|
|
39
39
|
compact?: boolean
|
|
40
|
-
|
|
41
|
-
* Which HEADER this tile leads with — the only thing that ever varied by
|
|
42
|
-
* surface. Two states, because there are two headers: the profile page shows
|
|
43
|
-
* a person's comments, so it leads with the VENDOR each one is about;
|
|
44
|
-
* everywhere else the vendor is the page and the commenter is the news.
|
|
45
|
-
* Every BEHAVIOUR is a prop, so a new surface never needs a new member here.
|
|
46
|
-
*/
|
|
47
|
-
leadWith?: 'vendor' | 'commenter'
|
|
48
|
-
/**
|
|
49
|
-
* Read the body in FULL (thread reading) instead of the fixed-height,
|
|
50
|
-
* three-line list tile. Defaults to the list tile.
|
|
51
|
-
*/
|
|
52
|
-
fullBody?: boolean
|
|
53
|
-
/** Rendered under the body — badges + per-surface actions (design-doc threads). */
|
|
54
|
-
footer?: React.ReactNode
|
|
55
|
-
/** Rendered after the card — nested replies + reply composer (design-doc threads). */
|
|
56
|
-
children?: React.ReactNode
|
|
40
|
+
context: 'profile' | 'vendor'
|
|
57
41
|
}
|
|
58
42
|
|
|
59
43
|
export function CommentCard({
|
|
@@ -62,10 +46,7 @@ export function CommentCard({
|
|
|
62
46
|
onDeleteComment,
|
|
63
47
|
showVendorInfo = true,
|
|
64
48
|
compact = false,
|
|
65
|
-
|
|
66
|
-
fullBody = false,
|
|
67
|
-
footer,
|
|
68
|
-
children
|
|
49
|
+
context = 'profile'
|
|
69
50
|
}: CommentCardProps) {
|
|
70
51
|
const { user: currentUser } = useAuth()
|
|
71
52
|
|
|
@@ -75,28 +56,32 @@ export function CommentCard({
|
|
|
75
56
|
}
|
|
76
57
|
|
|
77
58
|
// Check if current user can delete this comment
|
|
78
|
-
/**
|
|
79
|
-
* Deletability is the CALLER's rule, never a per-surface branch in here: an
|
|
80
|
-
* explicit `comment.canDelete` always wins (design docs compute own-or-DRI
|
|
81
|
-
* with their gate module and hand it in), and the default is the only rule
|
|
82
|
-
* this component can honestly know by itself — your own comment.
|
|
83
|
-
*/
|
|
84
59
|
const canUserDeleteComment = () => {
|
|
60
|
+
// If not authenticated, can't delete
|
|
85
61
|
if (!currentUser) return false
|
|
86
|
-
|
|
87
|
-
|
|
62
|
+
|
|
63
|
+
// For profile context, use the existing canDelete prop (since it's the user's own comments)
|
|
64
|
+
if (context === 'profile') {
|
|
65
|
+
return comment.canDelete !== false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// For vendor context, check if current user is the comment creator
|
|
69
|
+
if (context === 'vendor' && comment.user) {
|
|
70
|
+
return currentUser.id === comment.user.id
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return false
|
|
88
74
|
}
|
|
89
75
|
|
|
90
76
|
const showDeleteButton = canUserDeleteComment()
|
|
91
77
|
|
|
92
78
|
return (
|
|
93
|
-
<>
|
|
94
79
|
<div className="bg-ods-card border border-ods-border rounded-lg p-4 hover:border-ods-accent transition-all group overflow-hidden w-full max-w-full box-border" style={{ maxWidth: '100%', wordBreak: 'break-word' }}>
|
|
95
80
|
{/* Comment Header */}
|
|
96
81
|
<div className="flex flex-col gap-3 mb-3 min-[420px]:flex-row min-[420px]:items-center min-[420px]:justify-between min-[420px]:gap-2 w-full">
|
|
97
82
|
{/* Row 1: Info + Timestamp */}
|
|
98
83
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
99
|
-
{
|
|
84
|
+
{context === 'profile' && comment.vendor ? (
|
|
100
85
|
<>
|
|
101
86
|
{/* Vendor Button - Icon + Name */}
|
|
102
87
|
<VendorDisplayButton
|
|
@@ -109,7 +94,7 @@ export function CommentCard({
|
|
|
109
94
|
{formatActivityTime(comment.createdAt)}
|
|
110
95
|
</span>
|
|
111
96
|
</>
|
|
112
|
-
) : comment.user ? (
|
|
97
|
+
) : context === 'vendor' && comment.user ? (
|
|
113
98
|
<UserSummary
|
|
114
99
|
name={comment.user.name}
|
|
115
100
|
email=""
|
|
@@ -167,12 +152,6 @@ export function CommentCard({
|
|
|
167
152
|
</h4>
|
|
168
153
|
</div>
|
|
169
154
|
)}
|
|
170
|
-
{fullBody ? (
|
|
171
|
-
// Thread bodies are read in full — no fixed height, no clamp.
|
|
172
|
-
<p className="text-h4 text-ods-text-primary whitespace-pre-wrap" style={{ wordBreak: 'break-word', overflowWrap: 'break-word', maxWidth: '100%' }}>
|
|
173
|
-
{comment.content}
|
|
174
|
-
</p>
|
|
175
|
-
) : (
|
|
176
155
|
<div className={compact ? "h-[60px] flex items-center" : "h-[72px] flex items-center"}>
|
|
177
156
|
<p className="text-h4 text-ods-text-primary line-clamp-3" style={{
|
|
178
157
|
wordBreak: 'break-word',
|
|
@@ -187,11 +166,7 @@ export function CommentCard({
|
|
|
187
166
|
{comment.content}
|
|
188
167
|
</p>
|
|
189
168
|
</div>
|
|
190
|
-
)}
|
|
191
|
-
{footer ? <div className="pt-2 flex flex-wrap items-center gap-2">{footer}</div> : null}
|
|
192
169
|
</div>
|
|
193
170
|
</div>
|
|
194
|
-
{children}
|
|
195
|
-
</>
|
|
196
171
|
)
|
|
197
172
|
}
|
|
@@ -11,8 +11,6 @@ export type { GoogleSheetsViewerProps } from './google-sheets-viewer'
|
|
|
11
11
|
|
|
12
12
|
export { FigmaEmbed } from './figma-embed'
|
|
13
13
|
export type { FigmaEmbedProps } from './figma-embed'
|
|
14
|
-
export { ClaudeEmbed } from './claude-embed'
|
|
15
|
-
export type { ClaudeEmbedProps, ClaudeEmbedKind } from './claude-embed'
|
|
16
14
|
|
|
17
15
|
export { OGLinkPreview, OGLinkErrorBoundary } from './og-link-preview'
|
|
18
16
|
export type {
|
|
@@ -159,8 +159,7 @@ export function ArrayEntryManager<T extends { [key: string]: any }>({
|
|
|
159
159
|
</div>
|
|
160
160
|
)}
|
|
161
161
|
|
|
162
|
-
|
|
163
|
-
<div className="flex-1 min-w-0 space-y-2">
|
|
162
|
+
<div className="flex-1 space-y-2">
|
|
164
163
|
{renderLabel && renderLabel(item, index)}
|
|
165
164
|
<Input
|
|
166
165
|
placeholder={placeholder}
|
|
@@ -14,19 +14,13 @@ interface ClickUpTasksManagerProps {
|
|
|
14
14
|
onChange: (tasks: ClickUpTaskLink[]) => void;
|
|
15
15
|
title?: ReactNode; // Support string or ReactNode for badge integration
|
|
16
16
|
className?: string;
|
|
17
|
-
/**
|
|
18
|
-
* Per-row label renderer (passthrough to ArrayEntryManager) — lets a host
|
|
19
|
-
* hydrate the bare task id with the mirror's title/status row.
|
|
20
|
-
*/
|
|
21
|
-
renderLabel?: (item: ClickUpTaskLink, index: number) => ReactNode;
|
|
22
17
|
}
|
|
23
18
|
|
|
24
19
|
export function ClickUpTasksManager({
|
|
25
20
|
tasks,
|
|
26
21
|
onChange,
|
|
27
22
|
title = 'ClickUp Roadmap Tasks', // Default title
|
|
28
|
-
className = ''
|
|
29
|
-
renderLabel
|
|
23
|
+
className = ''
|
|
30
24
|
}: ClickUpTasksManagerProps) {
|
|
31
25
|
return (
|
|
32
26
|
<ArrayEntryManager
|
|
@@ -39,7 +33,6 @@ export function ClickUpTasksManager({
|
|
|
39
33
|
addButtonText="Add Task"
|
|
40
34
|
icon={<Lightbulb className="w-5 h-5 text-ods-text-secondary" />}
|
|
41
35
|
className={className}
|
|
42
|
-
renderLabel={renderLabel}
|
|
43
36
|
/>
|
|
44
37
|
);
|
|
45
38
|
}
|
package/src/components/index.ts
CHANGED
|
@@ -16,7 +16,6 @@ export * from './hover-card'
|
|
|
16
16
|
export * from './categories-cart'
|
|
17
17
|
export * from './category-card'
|
|
18
18
|
export * from './comment-card'
|
|
19
|
-
export * from './comment-composer'
|
|
20
19
|
export * from './content-loading-container'
|
|
21
20
|
export * from './dynamic-skeleton'
|
|
22
21
|
export * from './empty-state'
|
|
@@ -68,17 +68,12 @@ export interface DeliveryRowProps {
|
|
|
68
68
|
* paired with `scroll-mt-24` on the outer element so the row lands
|
|
69
69
|
* BELOW the sticky chrome after the scroll. */
|
|
70
70
|
id?: string
|
|
71
|
-
/** `'_blank'` renders a plain external anchor (new tab, `rel` hardened) —
|
|
72
|
-
* for rows whose href leaves the app (e.g. the raw ClickUp task URL on the
|
|
73
|
-
* design-doc surfaces). Internal deep-links omit it and keep the Link shim. */
|
|
74
|
-
target?: '_blank'
|
|
75
71
|
className?: string
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
export function DeliveryRow({
|
|
79
75
|
item,
|
|
80
76
|
href,
|
|
81
|
-
target,
|
|
82
77
|
caption,
|
|
83
78
|
id,
|
|
84
79
|
className,
|
|
@@ -105,10 +100,7 @@ export function DeliveryRow({
|
|
|
105
100
|
</h3>
|
|
106
101
|
</div>
|
|
107
102
|
<div className="min-h-[20px] flex items-center">
|
|
108
|
-
|
|
109
|
-
client render different strings a moment apart — text-only, so
|
|
110
|
-
suppress instead of forcing a client-only render. */}
|
|
111
|
-
<p className="text-h5 text-ods-text-secondary uppercase tracking-[-0.28px] truncate" suppressHydrationWarning>
|
|
103
|
+
<p className="text-h5 text-ods-text-secondary uppercase tracking-[-0.28px] truncate">
|
|
112
104
|
{subtitle}
|
|
113
105
|
</p>
|
|
114
106
|
</div>
|
|
@@ -163,14 +155,6 @@ export function DeliveryRow({
|
|
|
163
155
|
className,
|
|
164
156
|
)
|
|
165
157
|
|
|
166
|
-
if (href && target === '_blank') {
|
|
167
|
-
return (
|
|
168
|
-
<a href={href} id={id} className={baseClass} target="_blank" rel="noopener noreferrer">
|
|
169
|
-
{inner}
|
|
170
|
-
</a>
|
|
171
|
-
)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
158
|
if (href) {
|
|
175
159
|
// `Link` is the env-aware embed-shim — delegates to `next/link` on
|
|
176
160
|
// a Next.js host (soft RSC nav, back-button restores the previous
|
|
@@ -100,15 +100,6 @@ export interface RoadmapGridProps {
|
|
|
100
100
|
/** Show the desktop left margin (~120px) that aligns the grid with the page
|
|
101
101
|
* hero. Default `true`. Related-content rails pass `false`. */
|
|
102
102
|
showLeftMargin?: boolean;
|
|
103
|
-
/**
|
|
104
|
-
* Columns at `md` and up. Default `2` (the roadmap page).
|
|
105
|
-
*
|
|
106
|
-
* A card in this grid needs real width — its title, status badge, target
|
|
107
|
-
* version and vote row sit on one line each. Two columns inside a NARROW
|
|
108
|
-
* host (a modal rail, a sidebar) halve an already-small container and crush
|
|
109
|
-
* every one of them, so such hosts pass `1`.
|
|
110
|
-
*/
|
|
111
|
-
columns?: 1 | 2;
|
|
112
103
|
/** URL builder for the per-task refresh call after a successful vote. */
|
|
113
104
|
buildRefreshUrl?: (taskId: string) => string;
|
|
114
105
|
/** Voting hook options (vote endpoint + storage key). */
|
|
@@ -131,26 +122,18 @@ export interface RoadmapGridProps {
|
|
|
131
122
|
function RoadmapGridSingle({
|
|
132
123
|
items,
|
|
133
124
|
showLeftMargin,
|
|
134
|
-
columns = 2,
|
|
135
125
|
getVote,
|
|
136
126
|
onVote,
|
|
137
127
|
votingTasks,
|
|
138
128
|
}: {
|
|
139
129
|
items: RoadmapItem[];
|
|
140
130
|
showLeftMargin: boolean;
|
|
141
|
-
columns?: 1 | 2;
|
|
142
131
|
getVote: (taskId: string) => 'up' | 'down' | null;
|
|
143
132
|
onVote: (taskId: string, voteType: 'up' | 'down') => void;
|
|
144
133
|
votingTasks: Set<string>;
|
|
145
134
|
}) {
|
|
146
135
|
return (
|
|
147
|
-
|
|
148
|
-
// to Tailwind's scanner and is never generated.
|
|
149
|
-
<div
|
|
150
|
-
className={`grid grid-cols-1 gap-6 ${columns === 2 ? 'md:grid-cols-2' : ''} ${
|
|
151
|
-
showLeftMargin ? 'md:ml-[120px]' : ''
|
|
152
|
-
}`}
|
|
153
|
-
>
|
|
136
|
+
<div className={`grid grid-cols-1 md:grid-cols-2 gap-6 ${showLeftMargin ? 'md:ml-[120px]' : ''}`}>
|
|
154
137
|
{items.map((item) => (
|
|
155
138
|
// DOM id + sticky-header scroll offset live ON RoadmapCard's own
|
|
156
139
|
// outer element (no wrapper div). Anchor mirrors
|
|
@@ -174,7 +157,6 @@ export function RoadmapGrid({
|
|
|
174
157
|
items,
|
|
175
158
|
onItemUpdate,
|
|
176
159
|
showLeftMargin = true,
|
|
177
|
-
columns = 2,
|
|
178
160
|
buildRefreshUrl = DEFAULT_BUILD_REFRESH_URL,
|
|
179
161
|
votingOptions,
|
|
180
162
|
groupByQuarter = false,
|
|
@@ -273,7 +255,6 @@ export function RoadmapGrid({
|
|
|
273
255
|
<RoadmapGridSingle
|
|
274
256
|
items={items}
|
|
275
257
|
showLeftMargin={showLeftMargin}
|
|
276
|
-
columns={columns}
|
|
277
258
|
getVote={getVote}
|
|
278
259
|
onVote={handleVote}
|
|
279
260
|
votingTasks={votingTasks}
|
|
@@ -324,7 +305,6 @@ export function RoadmapGrid({
|
|
|
324
305
|
<RoadmapGridSingle
|
|
325
306
|
items={itemsByQuarter[quarter]}
|
|
326
307
|
showLeftMargin={showLeftMargin}
|
|
327
|
-
columns={columns}
|
|
328
308
|
getVote={getVote}
|
|
329
309
|
onVote={handleVote}
|
|
330
310
|
votingTasks={votingTasks}
|
|
@@ -11,10 +11,7 @@ const Card = React.forwardRef<
|
|
|
11
11
|
<div
|
|
12
12
|
ref={ref}
|
|
13
13
|
className={cn(
|
|
14
|
-
|
|
15
|
-
// with light text that paints a WHITE outline. Every ODS surface borders
|
|
16
|
-
// on `--color-border`; say so.
|
|
17
|
-
"rounded-lg border border-ods-border bg-card text-card-foreground shadow-sm",
|
|
14
|
+
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
|
18
15
|
className
|
|
19
16
|
)}
|
|
20
17
|
{...props}
|
|
@@ -56,47 +56,3 @@ describe('processShortcodes script stripping', () => {
|
|
|
56
56
|
expect(output).toContain('<script>');
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Design-doc embeds (R11): the spec's `{{figma:FILE_KEY[:NODE_ID]}}` grammar is
|
|
62
|
-
* rewritten onto the canonical `{{figma:URL}}` rule, and Claude artifacts get
|
|
63
|
-
* the SAME treatment — a shortcode → a block the renderer maps to `ClaudeEmbed`.
|
|
64
|
-
* Both are markdown blocks, so they render identically in a spec body, a
|
|
65
|
-
* comment, or a links rail, and no surface hand-assembles its own card.
|
|
66
|
-
*/
|
|
67
|
-
describe('processShortcodes design-doc embeds', () => {
|
|
68
|
-
it('rewrites {{figma:KEY:NODE}} to the canonical design URL and renders the figma embed', () => {
|
|
69
|
-
const output = processShortcodes('{{figma:aB3xK9:12-405}}');
|
|
70
|
-
expect(output).toContain('class="figma-embed"');
|
|
71
|
-
expect(output).toContain('data-figma-url="https://www.figma.com/design/aB3xK9?node-id=12-405"');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('accepts a colon-separated node id and a bare key', () => {
|
|
75
|
-
expect(processShortcodes('{{figma:aB3xK9:12:405}}')).toContain('node-id=12-405');
|
|
76
|
-
expect(processShortcodes('{{figma:aB3xK9}}')).toContain('data-figma-url="https://www.figma.com/design/aB3xK9"');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('leaves the existing {{figma:URL}} grammar untouched', () => {
|
|
80
|
-
const output = processShortcodes('{{figma:https://www.figma.com/proto/aB3xK9/Flows?node-id=1-2}}');
|
|
81
|
-
expect(output).toContain('data-figma-url="https://www.figma.com/proto/aB3xK9/Flows?node-id=1-2"');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('renders a Claude artifact / Claude Design link as an embed block, like figma', () => {
|
|
85
|
-
const artifact = processShortcodes('{{claude-artifact:https://claude.ai/public/artifacts/abc-123}}');
|
|
86
|
-
expect(artifact).toContain('class="claude-embed"');
|
|
87
|
-
expect(artifact).toContain('data-url="https://claude.ai/public/artifacts/abc-123"');
|
|
88
|
-
expect(artifact).toContain('data-kind="artifact"');
|
|
89
|
-
|
|
90
|
-
const design = processShortcodes('{{claude-design:https://claude.ai/design/xyz}}');
|
|
91
|
-
expect(design).toContain('data-kind="design"');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('escapes the url attribute so a crafted link cannot break out of the block', () => {
|
|
95
|
-
const output = processShortcodes('{{claude-artifact:https://claude.ai/a"><script>alert(1)</script>}}');
|
|
96
|
-
expect(output).not.toContain('<script>');
|
|
97
|
-
expect(output).toContain('"');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
});
|