@flamingo-stack/openframe-frontend-core 0.0.564 → 0.0.565-1861.5100.1
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-PKCZL56Y.cjs → chunk-4H7L4OSR.cjs} +4 -4
- package/dist/{chunk-PKCZL56Y.cjs.map → chunk-4H7L4OSR.cjs.map} +1 -1
- package/dist/chunk-5I3BSQ6D.js +2 -0
- package/dist/{chunk-6OPEULTD.cjs → chunk-BKDSNVGH.cjs} +9 -9
- package/dist/{chunk-6OPEULTD.cjs.map → chunk-BKDSNVGH.cjs.map} +1 -1
- package/dist/{chunk-OYFXHET4.cjs → chunk-BWMVI2KD.cjs} +7 -7
- package/dist/{chunk-OYFXHET4.cjs.map → chunk-BWMVI2KD.cjs.map} +1 -1
- package/dist/{chunk-YFLSOYX4.cjs → chunk-DLSDYLY3.cjs} +11 -11
- package/dist/{chunk-YFLSOYX4.cjs.map → chunk-DLSDYLY3.cjs.map} +1 -1
- package/dist/{chunk-NQ6UF4LP.js → chunk-EQEMZU45.js} +4 -4
- package/dist/{chunk-FG4BSJ4A.cjs → chunk-GG2HJQW3.cjs} +26 -26
- package/dist/{chunk-FG4BSJ4A.cjs.map → chunk-GG2HJQW3.cjs.map} +1 -1
- package/dist/{chunk-O3SEC45L.js → chunk-GG6CAELN.js} +4 -4
- package/dist/{chunk-J7R3VZL5.cjs → chunk-GZL5SAUI.cjs} +19 -15
- package/dist/chunk-GZL5SAUI.cjs.map +1 -0
- package/dist/{chunk-ASULU3T2.js → chunk-H4RCJ4IB.js} +7 -3
- package/dist/{chunk-ASULU3T2.js.map → chunk-H4RCJ4IB.js.map} +1 -1
- package/dist/{chunk-U5EVZLL5.cjs → chunk-KHEOXHF4.cjs} +5 -5
- package/dist/{chunk-U5EVZLL5.cjs.map → chunk-KHEOXHF4.cjs.map} +1 -1
- package/dist/{chunk-Z4VU6MJ4.js → chunk-LOWWLH2W.js} +2 -2
- package/dist/{chunk-NUKQWLOG.js → chunk-MIKGZY7V.js} +5 -5
- package/dist/{chunk-BK7V6CH5.js → chunk-MSUSONUK.js} +3297 -3209
- package/dist/chunk-MSUSONUK.js.map +1 -0
- package/dist/{chunk-PT4GS4CE.cjs → chunk-NAYXROCV.cjs} +3 -3
- package/dist/{chunk-PT4GS4CE.cjs.map → chunk-NAYXROCV.cjs.map} +1 -1
- package/dist/{chunk-G2VDS3M5.js → chunk-OEN3GTRL.js} +2 -2
- package/dist/{chunk-XFI5PCER.cjs → chunk-OTDVQBLI.cjs} +31 -31
- package/dist/{chunk-XFI5PCER.cjs.map → chunk-OTDVQBLI.cjs.map} +1 -1
- package/dist/{chunk-WGDZW237.cjs → chunk-PBH2BQYR.cjs} +86 -86
- package/dist/{chunk-WGDZW237.cjs.map → chunk-PBH2BQYR.cjs.map} +1 -1
- package/dist/{chunk-E6PW7GRQ.cjs → chunk-PZH3CZTJ.cjs} +89 -75
- package/dist/chunk-PZH3CZTJ.cjs.map +1 -0
- package/dist/{chunk-GEC2NHQH.cjs → chunk-QWVON3LG.cjs} +20 -20
- package/dist/{chunk-GEC2NHQH.cjs.map → chunk-QWVON3LG.cjs.map} +1 -1
- package/dist/{chunk-X4JKXYY5.js → chunk-SQ53GVCG.js} +2 -2
- package/dist/{chunk-AZPUAPXW.cjs → chunk-TA5P2M7V.cjs} +796 -708
- package/dist/chunk-TA5P2M7V.cjs.map +1 -0
- package/dist/chunk-UGAHDPNU.cjs +2 -0
- package/dist/{chunk-ZTM35Q76.cjs.map → chunk-UGAHDPNU.cjs.map} +1 -1
- package/dist/{chunk-T3P56JKN.js → chunk-XPLZB74T.js} +2 -2
- package/dist/{chunk-CQNK6RS6.js → chunk-XSK7ZQ3C.js} +2 -2
- package/dist/{chunk-TDYF6Q2D.js → chunk-XZNKPEWB.js} +2 -2
- package/dist/{chunk-VVDLVCPW.cjs → chunk-Y6IGG6RZ.cjs} +39 -39
- package/dist/{chunk-VVDLVCPW.cjs.map → chunk-Y6IGG6RZ.cjs.map} +1 -1
- package/dist/{chunk-7D62XVHH.js → chunk-YVOMHB73.js} +35 -21
- package/dist/chunk-YVOMHB73.js.map +1 -0
- package/dist/{chunk-CP7TMQBH.js → chunk-ZUVVJ24E.js} +2 -2
- package/dist/{chunk-DPMOEYEM.js → chunk-ZYC4O76Y.js} +2 -2
- package/dist/components/case-studies/index.cjs +8 -8
- package/dist/components/case-studies/index.js +2 -2
- 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/comment-card.d.ts +18 -2
- package/dist/components/comment-card.d.ts.map +1 -1
- package/dist/components/comment-composer.d.ts +42 -0
- package/dist/components/comment-composer.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/claude-embed.d.ts +31 -0
- package/dist/components/embeds/claude-embed.d.ts.map +1 -0
- package/dist/components/embeds/index.cjs +6 -4
- package/dist/components/embeds/index.cjs.map +1 -1
- package/dist/components/embeds/index.d.ts +2 -0
- package/dist/components/embeds/index.d.ts.map +1 -1
- package/dist/components/embeds/index.js +5 -3
- 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 +6 -1
- 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 +281 -204
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +647 -570
- 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 +5 -1
- package/dist/components/shared/delivery/delivery-row.d.ts.map +1 -1
- package/dist/components/shared/roadmap/roadmap-grid.d.ts +10 -1
- 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 +4 -2
- 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 +3 -1
- 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 +15 -2
- 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 +2 -0
- package/dist/components/ui/table/types.d.ts.map +1 -1
- package/dist/index.cjs +6 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -1
- package/dist/types/department.d.ts +41 -0
- package/dist/types/department.d.ts.map +1 -0
- package/dist/types/design-doc.d.ts +247 -0
- package/dist/types/design-doc.d.ts.map +1 -0
- package/dist/types/employee.d.ts +25 -2
- package/dist/types/employee.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- 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 +2 -1
- package/dist/types/profile.d.ts.map +1 -1
- package/dist/types/team.d.ts +15 -1
- package/dist/types/team.d.ts.map +1 -1
- package/dist/types/user.d.ts +3 -1
- package/dist/types/user.d.ts.map +1 -1
- package/dist/utils/embed-url-converters.d.ts +17 -0
- package/dist/utils/embed-url-converters.d.ts.map +1 -1
- package/dist/utils/index.cjs +12 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js +12 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/.comment-card.md +4 -5
- package/src/components/chat/entity-cards/employee-entry-card.tsx +6 -0
- package/src/components/comment-card.tsx +42 -17
- package/src/components/comment-composer.tsx +117 -0
- package/src/components/embeds/claude-embed.tsx +84 -0
- package/src/components/embeds/index.ts +2 -0
- package/src/components/features/array-entry-manager.tsx +2 -1
- package/src/components/features/clickup-tasks-manager.tsx +8 -1
- package/src/components/index.ts +1 -0
- package/src/components/shared/delivery/delivery-row.tsx +17 -1
- package/src/components/shared/roadmap/roadmap-grid.tsx +21 -1
- package/src/components/ui/card.tsx +4 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/markdown/rich/__tests__/shortcodes.test.ts +44 -0
- package/src/components/ui/markdown/rich/embed-overrides.tsx +4 -0
- package/src/components/ui/markdown/rich/shortcodes.ts +12 -0
- package/src/components/ui/table/index.ts +1 -1
- package/src/components/ui/table/table-row.tsx +7 -2
- package/src/components/ui/table/table-skeleton.tsx +20 -3
- package/src/components/ui/table/table.tsx +1 -0
- package/src/components/ui/table/types.ts +2 -0
- package/src/types/department.ts +44 -0
- package/src/types/design-doc.ts +295 -0
- package/src/types/employee.ts +27 -2
- package/src/types/index.ts +2 -0
- package/src/types/marketing.ts +1 -0
- package/src/types/profile.ts +3 -1
- package/src/types/team.ts +17 -1
- package/src/types/user.ts +4 -1
- package/src/utils/__tests__/claude-embed-url.test.ts +42 -0
- package/src/utils/embed-url-converters.ts +32 -0
- package/dist/chunk-7D62XVHH.js.map +0 -1
- package/dist/chunk-AZPUAPXW.cjs.map +0 -1
- package/dist/chunk-BK7V6CH5.js.map +0 -1
- package/dist/chunk-E6PW7GRQ.cjs.map +0 -1
- package/dist/chunk-J7R3VZL5.cjs.map +0 -1
- package/dist/chunk-MVWSLZG7.js +0 -2
- package/dist/chunk-ZTM35Q76.cjs +0 -2
- /package/dist/{chunk-MVWSLZG7.js.map → chunk-5I3BSQ6D.js.map} +0 -0
- /package/dist/{chunk-NQ6UF4LP.js.map → chunk-EQEMZU45.js.map} +0 -0
- /package/dist/{chunk-O3SEC45L.js.map → chunk-GG6CAELN.js.map} +0 -0
- /package/dist/{chunk-Z4VU6MJ4.js.map → chunk-LOWWLH2W.js.map} +0 -0
- /package/dist/{chunk-NUKQWLOG.js.map → chunk-MIKGZY7V.js.map} +0 -0
- /package/dist/{chunk-G2VDS3M5.js.map → chunk-OEN3GTRL.js.map} +0 -0
- /package/dist/{chunk-X4JKXYY5.js.map → chunk-SQ53GVCG.js.map} +0 -0
- /package/dist/{chunk-T3P56JKN.js.map → chunk-XPLZB74T.js.map} +0 -0
- /package/dist/{chunk-CQNK6RS6.js.map → chunk-XSK7ZQ3C.js.map} +0 -0
- /package/dist/{chunk-TDYF6Q2D.js.map → chunk-XZNKPEWB.js.map} +0 -0
- /package/dist/{chunk-CP7TMQBH.js.map → chunk-ZUVVJ24E.js.map} +0 -0
- /package/dist/{chunk-DPMOEYEM.js.map → chunk-ZYC4O76Y.js.map} +0 -0
|
@@ -8,7 +8,7 @@ export { TableDescriptionCell } from './table-description-cell'
|
|
|
8
8
|
export { TableEmptyState } from './table-empty-state'
|
|
9
9
|
export { TableHeader } from './table-header'
|
|
10
10
|
export { TableRow } from './table-row'
|
|
11
|
-
export { ROW_HEIGHT_DESKTOP, ROW_HEIGHT_MOBILE, TableCardSkeleton } from './table-skeleton'
|
|
11
|
+
export { COMPACT_ROW_MIN_HEIGHT_PX, ROW_HEIGHT_DESKTOP, ROW_HEIGHT_MOBILE, TableCardSkeleton } from './table-skeleton'
|
|
12
12
|
export { TableTimestampCell } from './table-timestamp-cell'
|
|
13
13
|
export { getHideClasses, isHiddenOnMobile } from './utils'
|
|
14
14
|
|
|
@@ -5,7 +5,7 @@ import React from 'react'
|
|
|
5
5
|
import { cn } from '../../../utils/cn'
|
|
6
6
|
import { Checkbox } from '../checkbox'
|
|
7
7
|
import { TableCell } from './table-cell'
|
|
8
|
-
import { ROW_HEIGHT_DESKTOP } from './table-skeleton'
|
|
8
|
+
import { COMPACT_ROW_MIN_HEIGHT, ROW_HEIGHT_DESKTOP } from './table-skeleton'
|
|
9
9
|
import type { TableRowProps } from './types'
|
|
10
10
|
import { getHideClasses } from './utils'
|
|
11
11
|
|
|
@@ -99,7 +99,12 @@ export function TableRow<T = any>({
|
|
|
99
99
|
<div
|
|
100
100
|
className={cn(
|
|
101
101
|
'relative flex items-center gap-4 px-4',
|
|
102
|
-
compact
|
|
102
|
+
// `compact` rows are content-sized, but every row in a table shows
|
|
103
|
+
// the same cell shapes, so a MIN height makes the body's height a
|
|
104
|
+
// function of the row COUNT alone. That is what lets a host reserve
|
|
105
|
+
// space for a full page and stop the layout jumping between a full
|
|
106
|
+
// page, a short last page and the skeleton.
|
|
107
|
+
compact ? cn('py-2', COMPACT_ROW_MIN_HEIGHT) : cn('py-0', ROW_HEIGHT_DESKTOP),
|
|
103
108
|
isLinkMode && 'pointer-events-none'
|
|
104
109
|
)}
|
|
105
110
|
>
|
|
@@ -7,12 +7,26 @@ import type { TableCardSkeletonProps } from './types'
|
|
|
7
7
|
// INNER row heights: the bordered row card adds 1px top + bottom, so the
|
|
8
8
|
// outer block totals the designed 68px / 80px.
|
|
9
9
|
const ROW_HEIGHT_DESKTOP = 'h-[66px] md:h-[78px]'
|
|
10
|
+
/**
|
|
11
|
+
* A `compact` row's floor. Compact rows are content-sized (`py-2`), but every
|
|
12
|
+
* row in one table renders the same cell shapes, so pinning a minimum makes the
|
|
13
|
+
* body's height a function of the ROW COUNT alone — which is what lets a host
|
|
14
|
+
* reserve space for a full page and stop the layout jumping between a full
|
|
15
|
+
* page, a short last page and the skeleton. The skeleton uses it too, so the
|
|
16
|
+
* loading state is exactly as tall as the rows that replace it.
|
|
17
|
+
*/
|
|
18
|
+
const COMPACT_ROW_MIN_HEIGHT = 'min-h-[56px]'
|
|
19
|
+
/** The same 56, as a number, for hosts reserving `rows x height` of space.
|
|
20
|
+
* A LITERAL class above and a literal number here: an interpolated Tailwind
|
|
21
|
+
* class is invisible to the scanner and is never generated. */
|
|
22
|
+
const COMPACT_ROW_MIN_HEIGHT_PX = 56
|
|
10
23
|
const ROW_HEIGHT_MOBILE = 'h-[66px]'
|
|
11
24
|
|
|
12
25
|
/** @deprecated Use `DataTableSkeleton` from `data-table` instead. */
|
|
13
26
|
export function TableCardSkeleton({
|
|
14
27
|
columns,
|
|
15
28
|
rows = 10,
|
|
29
|
+
compact = false,
|
|
16
30
|
hasActions = false,
|
|
17
31
|
hasChevron = false,
|
|
18
32
|
className,
|
|
@@ -33,9 +47,12 @@ export function TableCardSkeleton({
|
|
|
33
47
|
)}
|
|
34
48
|
>
|
|
35
49
|
{/* Desktop Skeleton */}
|
|
50
|
+
{/* Matches the ROW it stands in for: a compact row is content-sized
|
|
51
|
+
with a floor, a normal row is a fixed height. Using the taller of
|
|
52
|
+
the two here made every compact table jump on its first paint. */}
|
|
36
53
|
<div className={cn(
|
|
37
|
-
'hidden md:flex items-center gap-4 px-4
|
|
38
|
-
ROW_HEIGHT_DESKTOP,
|
|
54
|
+
'hidden md:flex items-center gap-4 px-4',
|
|
55
|
+
compact ? cn('py-2', COMPACT_ROW_MIN_HEIGHT) : cn('py-0', ROW_HEIGHT_DESKTOP),
|
|
39
56
|
rowClassName
|
|
40
57
|
)}>
|
|
41
58
|
{columns.map((column) => (
|
|
@@ -94,4 +111,4 @@ export function TableCardSkeleton({
|
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
/** @deprecated */
|
|
97
|
-
export { ROW_HEIGHT_DESKTOP, ROW_HEIGHT_MOBILE }
|
|
114
|
+
export { ROW_HEIGHT_DESKTOP, ROW_HEIGHT_MOBILE, COMPACT_ROW_MIN_HEIGHT, COMPACT_ROW_MIN_HEIGHT_PX }
|
|
@@ -240,6 +240,8 @@ export interface TableCellProps {
|
|
|
240
240
|
export interface TableCardSkeletonProps {
|
|
241
241
|
columns: TableColumn[]
|
|
242
242
|
rows?: number // Number of skeleton rows to display (default: 10)
|
|
243
|
+
/** Match a `compact` table's row height, so the first paint does not jump. */
|
|
244
|
+
compact?: boolean
|
|
243
245
|
hasActions?: boolean
|
|
244
246
|
hasChevron?: boolean
|
|
245
247
|
className?: string
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Departments — THE single source of truth for the org's department
|
|
3
|
+
* vocabulary.
|
|
4
|
+
*
|
|
5
|
+
* The `departments` table is populated from Google Workspace (the org chart)
|
|
6
|
+
* by the hub's Google sync, which auto-creates a row the first time it sees a
|
|
7
|
+
* new department name. `profiles.department_id` is an FK to it; there is no
|
|
8
|
+
* free-text department column and no enum anywhere in code — every consumer
|
|
9
|
+
* reads the row (or the embedded `DepartmentRef`) and every picker lists the
|
|
10
|
+
* ACTIVE rows ordered by `display_order`.
|
|
11
|
+
*
|
|
12
|
+
* Hub owner module: `lib/data/department-utils.ts`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface Department {
|
|
16
|
+
id: string
|
|
17
|
+
name: string
|
|
18
|
+
slug: string
|
|
19
|
+
description?: string | null
|
|
20
|
+
parent_department_id?: string | null
|
|
21
|
+
display_order: number
|
|
22
|
+
is_active: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The embedded shape a reader gets through the `department:departments(…)` PostgREST embed. */
|
|
26
|
+
export type DepartmentRef = Pick<Department, 'id' | 'name' | 'slug'> & {
|
|
27
|
+
display_order?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `GET /api/admin/departments?counts=…` row — a department plus how many profiles carry it. */
|
|
31
|
+
export interface DepartmentSummary {
|
|
32
|
+
id: string
|
|
33
|
+
name: string
|
|
34
|
+
slug: string
|
|
35
|
+
count: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Population a department count is taken over. */
|
|
39
|
+
/**
|
|
40
|
+
* Populations the department count endpoint supports. The hub's runtime
|
|
41
|
+
* allowlist (`DEPARTMENT_COUNT_POPULATIONS` in lib/data/department-constants.ts)
|
|
42
|
+
* is the value form of this union — keep the two in step.
|
|
43
|
+
*/
|
|
44
|
+
export type DepartmentCountPopulation = 'directory' | 'scored'
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// Design Doc Types (product-hub)
|
|
2
|
+
// Mirrors product-release.ts — entity + child tables + create/update/filter shapes.
|
|
3
|
+
//
|
|
4
|
+
// Participant sections are keyed by the employee DEPARTMENT (FK to the ONE
|
|
5
|
+
// `departments` table — see department.ts); there is no function enum.
|
|
6
|
+
// Completion-gate math and status transitions live in the hub's
|
|
7
|
+
// `lib/utils/design-doc-gate.ts` (isomorphic, shared by server + client).
|
|
8
|
+
|
|
9
|
+
import type { EntityAuthor } from './entity-author'
|
|
10
|
+
import type { DeliveryItem } from './delivery'
|
|
11
|
+
import type { RoadmapItem } from '../components/chat/types/entities/roadmap-item'
|
|
12
|
+
import type { DepartmentRef } from './department'
|
|
13
|
+
import type { EmployeeDirectoryRow } from './employee'
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Value vocabularies — TYPES ONLY.
|
|
17
|
+
//
|
|
18
|
+
// The runtime lists live ONCE, in the hub's `lib/utils/design-doc-gate.ts`,
|
|
19
|
+
// beside the labels and badge colors that make them useful; that module pins
|
|
20
|
+
// each tuple against the union here at compile time, so the two cannot drift.
|
|
21
|
+
// Declaring `as const` arrays here as well would ship a second copy of every
|
|
22
|
+
// vocabulary that nothing reads — the copy a future contributor edits expecting
|
|
23
|
+
// an effect.
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
export type DesignDocStatus = 'draft' | 'in_review' | 'approved' | 'building' | 'shipped' | 'abandoned'
|
|
27
|
+
|
|
28
|
+
export type DesignDocTier = 'S' | 'M' | 'L'
|
|
29
|
+
|
|
30
|
+
export type DesignDocParticipantStatus = 'pending' | 'in_progress' | 'completed' | 'blocked'
|
|
31
|
+
|
|
32
|
+
export type DesignDocCommentType = 'blocking' | 'suggestion' | 'question' | 'note'
|
|
33
|
+
|
|
34
|
+
export type DesignDocCommentStatus = 'open' | 'resolved' | 'declined'
|
|
35
|
+
|
|
36
|
+
export type DesignDocLinkType = 'roadmap_task' | 'figma' | 'claude_artifact' | 'claude_design' | 'brief' | 'other'
|
|
37
|
+
|
|
38
|
+
export type DesignDocMediaType = 'image' | 'video' | 'screenshot' | 'demo'
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Rows (as returned by the hub DAL — hydrated)
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The profile identity the DAL embeds on participants / comment authors — THE
|
|
46
|
+
* shared person projection, not a second declaration of it. `email` rides the
|
|
47
|
+
* detail payload only and is stripped from lists.
|
|
48
|
+
*/
|
|
49
|
+
export type DesignDocPerson = EmployeeDirectoryRow
|
|
50
|
+
|
|
51
|
+
export interface DesignDocClickUpTask {
|
|
52
|
+
id: number
|
|
53
|
+
doc_id: number
|
|
54
|
+
participant_id: number | null
|
|
55
|
+
clickup_task_id: string
|
|
56
|
+
display_order: number | null
|
|
57
|
+
created_at: string
|
|
58
|
+
/** Hydrated from the `clickup_tasks` mirror (never the live API). `undefined` = not in the mirror. */
|
|
59
|
+
task?: DeliveryItem
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DesignDocLink {
|
|
63
|
+
id: number
|
|
64
|
+
doc_id: number
|
|
65
|
+
link_type: DesignDocLinkType
|
|
66
|
+
url: string | null
|
|
67
|
+
clickup_task_id: string | null
|
|
68
|
+
title: string | null
|
|
69
|
+
display_order: number | null
|
|
70
|
+
created_at: string
|
|
71
|
+
/**
|
|
72
|
+
* A `roadmap_task` link, hydrated from the mirror as the ROADMAP item it is —
|
|
73
|
+
* so it renders with the roadmap card, the same as `/roadmap` and the chat.
|
|
74
|
+
* `undefined` = the mirror has not seen that id. (Section tasks are a
|
|
75
|
+
* different entity and carry `DeliveryItem` on `DesignDocClickUpTask`.)
|
|
76
|
+
*/
|
|
77
|
+
roadmap?: RoadmapItem
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface DesignDocMedia {
|
|
81
|
+
id: number
|
|
82
|
+
doc_id: number
|
|
83
|
+
participant_id: number | null
|
|
84
|
+
media_type: DesignDocMediaType
|
|
85
|
+
media_url: string
|
|
86
|
+
title: string | null
|
|
87
|
+
description: string | null
|
|
88
|
+
display_order: number | null
|
|
89
|
+
created_at: string
|
|
90
|
+
created_by: string | null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A comment. ROOT comments (`parent_comment_id === null`) carry `comment_type`
|
|
95
|
+
* + `status` and gate approval when `blocking` + `open`; REPLIES carry neither
|
|
96
|
+
* (single-level threads — a reply to a reply is re-parented to the root).
|
|
97
|
+
*/
|
|
98
|
+
export interface DesignDocComment {
|
|
99
|
+
id: number
|
|
100
|
+
doc_id: number
|
|
101
|
+
participant_id: number | null
|
|
102
|
+
parent_comment_id: number | null
|
|
103
|
+
author_id: string | null
|
|
104
|
+
comment_type: DesignDocCommentType | null
|
|
105
|
+
title: string | null
|
|
106
|
+
body: string
|
|
107
|
+
status: DesignDocCommentStatus | null
|
|
108
|
+
resolved_at: string | null
|
|
109
|
+
created_at: string
|
|
110
|
+
author?: DesignDocPerson | null
|
|
111
|
+
/** Threaded by the DAL; always `[]` on replies. */
|
|
112
|
+
replies: DesignDocComment[]
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface DesignDocParticipant {
|
|
116
|
+
id: number
|
|
117
|
+
doc_id: number
|
|
118
|
+
department_id: string
|
|
119
|
+
department: DepartmentRef
|
|
120
|
+
assignee_id: string | null
|
|
121
|
+
assignee?: DesignDocPerson | null
|
|
122
|
+
status: DesignDocParticipantStatus
|
|
123
|
+
content: string | null
|
|
124
|
+
completed_at: string | null
|
|
125
|
+
display_order: number | null
|
|
126
|
+
created_at: string
|
|
127
|
+
updated_at: string
|
|
128
|
+
tasks: DesignDocClickUpTask[]
|
|
129
|
+
media: DesignDocMedia[]
|
|
130
|
+
/** Section-scoped root comments (threaded). */
|
|
131
|
+
comments: DesignDocComment[]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface DesignDocCompletion {
|
|
135
|
+
total: number
|
|
136
|
+
completed: number
|
|
137
|
+
blocked: number
|
|
138
|
+
openBlocking: number
|
|
139
|
+
isComplete: boolean
|
|
140
|
+
waitingOn: Array<{
|
|
141
|
+
participantId: number
|
|
142
|
+
department: DepartmentRef
|
|
143
|
+
assigneeName: string | null
|
|
144
|
+
status: DesignDocParticipantStatus
|
|
145
|
+
}>
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** R13 — what the signed-in viewer still owes on one doc. */
|
|
149
|
+
export interface DesignDocMyOpenWork {
|
|
150
|
+
/** One person may own several department sections on the same doc. */
|
|
151
|
+
participants: Array<{ id: number; department: DepartmentRef; status: DesignDocParticipantStatus }>
|
|
152
|
+
openTasks: DeliveryItem[]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface DesignDoc {
|
|
156
|
+
id: number
|
|
157
|
+
title: string
|
|
158
|
+
slug: string
|
|
159
|
+
tier: DesignDocTier
|
|
160
|
+
summary: string | null
|
|
161
|
+
content: string | null
|
|
162
|
+
status: DesignDocStatus
|
|
163
|
+
author_id: string | null
|
|
164
|
+
/**
|
|
165
|
+
* EVERYONE who has handed the DRI role away on this doc (appended, never
|
|
166
|
+
* overwritten). The third-party break-glass excludes all of them: a DRI who
|
|
167
|
+
* also holds management could otherwise vacate the seat, act as a "third
|
|
168
|
+
* party" on their own doc and approve it alone — and with a single scalar,
|
|
169
|
+
* simply hand the seat on twice to erase the memory of their own vacating.
|
|
170
|
+
*/
|
|
171
|
+
dri_vacated_by: string[]
|
|
172
|
+
approved_at: string | null
|
|
173
|
+
created_at: string
|
|
174
|
+
updated_at: string
|
|
175
|
+
/** DRI, hydrated via the shared author hydrator. */
|
|
176
|
+
author?: EntityAuthor
|
|
177
|
+
participants: DesignDocParticipant[]
|
|
178
|
+
links: DesignDocLink[]
|
|
179
|
+
/** Doc-level comments only (participant-scoped comments ride on their participant). */
|
|
180
|
+
comments: DesignDocComment[]
|
|
181
|
+
completion: DesignDocCompletion
|
|
182
|
+
my_open_work?: DesignDocMyOpenWork
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Write shapes
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
export interface DesignDocLinkInput {
|
|
190
|
+
link_type: DesignDocLinkType
|
|
191
|
+
url?: string | null
|
|
192
|
+
clickup_task_id?: string | null
|
|
193
|
+
title?: string | null
|
|
194
|
+
display_order?: number
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface DesignDocClickUpTaskInput {
|
|
198
|
+
clickup_task_id: string
|
|
199
|
+
display_order?: number
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface DesignDocMediaInput {
|
|
203
|
+
media_type: DesignDocMediaType
|
|
204
|
+
media_url: string
|
|
205
|
+
title?: string | null
|
|
206
|
+
description?: string | null
|
|
207
|
+
display_order?: number
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface CreateDesignDocParticipantInput {
|
|
211
|
+
/** Defaults to the assignee's profile department when omitted. */
|
|
212
|
+
department_id?: string
|
|
213
|
+
assignee_id: string
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Wizard payload (POST /api/admin/design-docs). */
|
|
217
|
+
export interface CreateDesignDocData {
|
|
218
|
+
title: string
|
|
219
|
+
slug: string
|
|
220
|
+
tier: DesignDocTier
|
|
221
|
+
summary?: string | null
|
|
222
|
+
/** Spec markdown (Why / What / How); the create screen carries the same body editor as edit. */
|
|
223
|
+
content?: string | null
|
|
224
|
+
/** DRI; defaults to the caller. */
|
|
225
|
+
author_id?: string | null
|
|
226
|
+
participants: CreateDesignDocParticipantInput[]
|
|
227
|
+
links: DesignDocLinkInput[]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Doc-level allowlist (PUT). `status` and `participants` are NOT here — status
|
|
232
|
+
* only moves through PATCH (the gate), participants only through their routes.
|
|
233
|
+
*/
|
|
234
|
+
export interface UpdateDesignDocData {
|
|
235
|
+
title?: string
|
|
236
|
+
slug?: string
|
|
237
|
+
tier?: DesignDocTier
|
|
238
|
+
summary?: string | null
|
|
239
|
+
content?: string | null
|
|
240
|
+
author_id?: string | null
|
|
241
|
+
links?: DesignDocLinkInput[]
|
|
242
|
+
/** OCC token — the `updated_at` the editor seeded from; mismatch → 409. */
|
|
243
|
+
expected_updated_at?: string
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface UpdateDesignDocParticipantData {
|
|
247
|
+
content?: string | null
|
|
248
|
+
status?: DesignDocParticipantStatus
|
|
249
|
+
assignee_id?: string | null
|
|
250
|
+
department_id?: string
|
|
251
|
+
clickup_tasks?: DesignDocClickUpTaskInput[]
|
|
252
|
+
media?: DesignDocMediaInput[]
|
|
253
|
+
expected_updated_at?: string
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface AddDesignDocParticipantData {
|
|
257
|
+
/** Defaults to the assignee's profile department when omitted. */
|
|
258
|
+
department_id?: string
|
|
259
|
+
assignee_id: string
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface AddDesignDocCommentData {
|
|
263
|
+
participant_id?: number | null
|
|
264
|
+
/** Set for a reply; the DAL re-parents replies-of-replies to the root. */
|
|
265
|
+
parent_comment_id?: number | null
|
|
266
|
+
/** Required on root comments, ignored on replies. */
|
|
267
|
+
comment_type?: DesignDocCommentType
|
|
268
|
+
title?: string | null
|
|
269
|
+
body: string
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface DesignDocFilters {
|
|
273
|
+
status?: string
|
|
274
|
+
search?: string
|
|
275
|
+
mine?: 'open'
|
|
276
|
+
limit?: number
|
|
277
|
+
offset?: number
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface DesignDocListResponse {
|
|
281
|
+
data: DesignDoc[]
|
|
282
|
+
count: number
|
|
283
|
+
/** Server-computed status facet counts (how-i-work feed convention). */
|
|
284
|
+
facets: { status: Record<string, number> }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface DesignDocStats {
|
|
288
|
+
total: number
|
|
289
|
+
draft: number
|
|
290
|
+
in_review: number
|
|
291
|
+
approved: number
|
|
292
|
+
building: number
|
|
293
|
+
waiting_on_me_docs: number
|
|
294
|
+
waiting_on_me_tasks: number
|
|
295
|
+
}
|
package/src/types/employee.ts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
|
+
import type { DepartmentRef } from './department';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* THE person projection every people picker and person embed speaks.
|
|
5
|
+
*
|
|
6
|
+
* One shape, because it is one wire payload: the employee-directory endpoints
|
|
7
|
+
* return it, `EmployeeSelector` renders it, and the design-doc DAL embeds it as
|
|
8
|
+
* a DRI / section owner / comment author. Everything past `full_name` is
|
|
9
|
+
* optional because the list payloads project fewer columns than the detail ones
|
|
10
|
+
* — a narrower reader `Pick<>`s from here rather than re-declaring the fields.
|
|
11
|
+
*/
|
|
12
|
+
export interface EmployeeDirectoryRow {
|
|
13
|
+
id: string;
|
|
14
|
+
full_name: string | null;
|
|
15
|
+
email?: string | null;
|
|
16
|
+
avatar_url?: string | null;
|
|
17
|
+
job_title?: string | null;
|
|
18
|
+
/** FK to the ONE `departments` table; `department` is the embedded row. */
|
|
19
|
+
department_id?: string | null;
|
|
20
|
+
department?: DepartmentRef | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
1
23
|
export interface Employee {
|
|
2
24
|
id: string;
|
|
3
25
|
full_name: string;
|
|
4
|
-
department
|
|
26
|
+
/** FK to the ONE `departments` table; `department` is the embedded row. */
|
|
27
|
+
department_id: string | null;
|
|
28
|
+
department: DepartmentRef | null;
|
|
5
29
|
avatar_url: string;
|
|
6
30
|
role: string;
|
|
7
31
|
job_title: string;
|
|
@@ -16,7 +40,8 @@ export interface EmployeeResponse {
|
|
|
16
40
|
|
|
17
41
|
export interface EmployeeProfileData {
|
|
18
42
|
full_name: string;
|
|
19
|
-
|
|
43
|
+
department_id: string | null;
|
|
44
|
+
department: DepartmentRef | null;
|
|
20
45
|
avatar_url: string;
|
|
21
46
|
role: string;
|
|
22
47
|
job_title: string;
|
package/src/types/index.ts
CHANGED
|
@@ -28,11 +28,13 @@ export * from './customer-interview-ai.types'
|
|
|
28
28
|
export * from './luma'
|
|
29
29
|
export * from './product-release'
|
|
30
30
|
export * from './delivery'
|
|
31
|
+
export * from './design-doc'
|
|
31
32
|
export * from './vendor'
|
|
32
33
|
export * from './vendor-links'
|
|
33
34
|
export * from './video-processing'
|
|
34
35
|
|
|
35
36
|
// User and profile types
|
|
37
|
+
export * from './department'
|
|
36
38
|
export * from './employee'
|
|
37
39
|
export * from './profile'
|
|
38
40
|
export * from './team'
|
package/src/types/marketing.ts
CHANGED
|
@@ -350,6 +350,7 @@ export type ContentSourceType =
|
|
|
350
350
|
| 'what_i_shipped' // What I Shipped employee check-ins (lives on people-hub)
|
|
351
351
|
| 'how_i_work' // How I Work employee AI-workflow sessions (lives on people-hub)
|
|
352
352
|
| 'faq' // FAQ Q&A pair (single-page /faqs index; deep-link by category anchor)
|
|
353
|
+
| 'design_doc' // Internal design docs (product-hub admin; preview route only, no public page)
|
|
353
354
|
| 'from_scratch';
|
|
354
355
|
|
|
355
356
|
export type URLInjectionPreference = 'none' | 'in_post' | 'as_comment';
|
package/src/types/profile.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Profile system TypeScript interfaces for OpenMSP
|
|
2
2
|
|
|
3
|
+
import type { DepartmentRef } from './department'
|
|
4
|
+
|
|
3
5
|
export interface ProfileResponse {
|
|
4
6
|
id: string;
|
|
5
7
|
name: string;
|
|
@@ -14,7 +16,7 @@ export interface ProfileResponse {
|
|
|
14
16
|
jobTitle: string | null;
|
|
15
17
|
isCommunityMember: boolean;
|
|
16
18
|
lastSignIn: string | null;
|
|
17
|
-
department:
|
|
19
|
+
department: DepartmentRef | null;
|
|
18
20
|
employeeId: string | null;
|
|
19
21
|
}
|
|
20
22
|
|
package/src/types/team.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { DepartmentRef } from './department';
|
|
2
|
+
|
|
1
3
|
export interface TeamMember {
|
|
2
4
|
id: string;
|
|
3
5
|
employee_id: string;
|
|
@@ -9,7 +11,9 @@ export interface TeamMember {
|
|
|
9
11
|
phone?: string;
|
|
10
12
|
role?: string;
|
|
11
13
|
title?: string;
|
|
14
|
+
/** Display name of the department (presentational shape); `department_id` is the FK. */
|
|
12
15
|
department?: string;
|
|
16
|
+
department_id?: string | null;
|
|
13
17
|
team?: string;
|
|
14
18
|
manager_id?: string;
|
|
15
19
|
profile_image_url?: string;
|
|
@@ -32,8 +36,20 @@ export interface TeamMember {
|
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
export interface TeamSection {
|
|
39
|
+
/**
|
|
40
|
+
* The BUCKET key this section was grouped under — the React key, and the one
|
|
41
|
+
* value guaranteed unique across sections. `department_id` is not: a profile
|
|
42
|
+
* whose `department_id` points at a row the embed could not resolve lands in
|
|
43
|
+
* the same "Other" section as the genuinely department-less, and both would
|
|
44
|
+
* render under the same fallback key.
|
|
45
|
+
*/
|
|
46
|
+
key: string;
|
|
35
47
|
title: string;
|
|
48
|
+
/** Department display name. */
|
|
36
49
|
department: string;
|
|
50
|
+
department_id: string | null;
|
|
51
|
+
/** `departments.display_order` — sections sort by it (no name sentinels). */
|
|
52
|
+
display_order: number;
|
|
37
53
|
description: string;
|
|
38
54
|
members: TeamMember[];
|
|
39
55
|
}
|
|
@@ -45,5 +61,5 @@ export interface TeamData {
|
|
|
45
61
|
|
|
46
62
|
export interface GetTeamOptions {
|
|
47
63
|
includeInactive?: boolean;
|
|
48
|
-
|
|
64
|
+
departmentId?: string;
|
|
49
65
|
}
|
package/src/types/user.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Types related to users
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { DepartmentRef } from './department'
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* User profile
|
|
7
9
|
*/
|
|
@@ -14,7 +16,8 @@ export type UserProfile = {
|
|
|
14
16
|
company: string | null
|
|
15
17
|
bio: string | null
|
|
16
18
|
employee_id: string | null
|
|
17
|
-
|
|
19
|
+
department_id: string | null
|
|
20
|
+
department: DepartmentRef | null
|
|
18
21
|
role: 'user' | 'super_admin'
|
|
19
22
|
msp_id: string | null
|
|
20
23
|
created_at: string
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { toClaudeEmbedUrl } from '../embed-url-converters'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Claude counterpart of `toFigmaEmbedUrl`, and the newest url-trust
|
|
6
|
+
* boundary in the markdown pipeline: whatever it returns is put in an iframe
|
|
7
|
+
* `src`, so "not a published claude.ai artifact" must be `null`, always.
|
|
8
|
+
*/
|
|
9
|
+
describe('toClaudeEmbedUrl', () => {
|
|
10
|
+
it('normalises every shape of a PUBLISHED artifact url to its embed route', () => {
|
|
11
|
+
const embed = 'https://claude.ai/public/artifacts/abc-123/embed'
|
|
12
|
+
expect(toClaudeEmbedUrl('https://claude.ai/public/artifacts/abc-123')).toBe(embed)
|
|
13
|
+
expect(toClaudeEmbedUrl('https://claude.ai/public/artifacts/abc-123/')).toBe(embed)
|
|
14
|
+
// The url Anthropic's own "Get embed code" hands the author — the likeliest
|
|
15
|
+
// paste of all, and it must not be treated as un-embeddable.
|
|
16
|
+
expect(toClaudeEmbedUrl('https://claude.ai/public/artifacts/abc-123/embed')).toBe(embed)
|
|
17
|
+
expect(toClaudeEmbedUrl('https://claude.ai/public/artifacts/abc-123/embed/')).toBe(embed)
|
|
18
|
+
// claude.site short links (and their embed form) 308 to the same artifact.
|
|
19
|
+
expect(toClaudeEmbedUrl('https://claude.site/artifacts/abc-123')).toBe(embed)
|
|
20
|
+
expect(toClaudeEmbedUrl('https://claude.site/artifacts/abc-123/embed')).toBe(embed)
|
|
21
|
+
expect(toClaudeEmbedUrl('https://CLAUDE.AI/public/artifacts/abc-123')).toBe(embed)
|
|
22
|
+
// Query and fragment are the artifact's, not ours: the embed route is built
|
|
23
|
+
// from the id alone.
|
|
24
|
+
expect(toClaudeEmbedUrl('https://claude.ai/public/artifacts/abc-123?x=1#y')).toBe(embed)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('refuses everything that is not a published artifact, and never throws', () => {
|
|
28
|
+
// A Claude CODE artifact has no embeddable route at all.
|
|
29
|
+
expect(toClaudeEmbedUrl('https://claude.ai/code/artifact/abc-123')).toBeNull()
|
|
30
|
+
expect(toClaudeEmbedUrl('https://claude.ai/design/abc-123')).toBeNull()
|
|
31
|
+
// Host must BE claude.ai/claude.site — not a prefix, a subdomain or userinfo.
|
|
32
|
+
expect(toClaudeEmbedUrl('https://claude.ai.evil.com/public/artifacts/abc')).toBeNull()
|
|
33
|
+
expect(toClaudeEmbedUrl('https://evil.com/public/artifacts/abc')).toBeNull()
|
|
34
|
+
expect(toClaudeEmbedUrl('https://evil.com/?u=https://claude.ai/public/artifacts/abc')).toBeNull()
|
|
35
|
+
expect(toClaudeEmbedUrl('javascript:alert(1)')).toBeNull()
|
|
36
|
+
expect(toClaudeEmbedUrl('//claude.ai/public/artifacts/abc')).toBeNull()
|
|
37
|
+
expect(toClaudeEmbedUrl('not a url')).toBeNull()
|
|
38
|
+
expect(toClaudeEmbedUrl('')).toBeNull()
|
|
39
|
+
expect(toClaudeEmbedUrl(null)).toBeNull()
|
|
40
|
+
expect(toClaudeEmbedUrl(undefined)).toBeNull()
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -75,3 +75,35 @@ export function toFigmaOriginalUrl(url: string): string {
|
|
|
75
75
|
}
|
|
76
76
|
return url.replace(/\?.*$/, '')
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The EMBED url for a published Claude artifact, or `null` when the link is not
|
|
81
|
+
* one we can frame — the Claude counterpart of `toFigmaEmbedUrl`.
|
|
82
|
+
*
|
|
83
|
+
* claude.ai serves `frame-ancestors 'self'` on the artifact page itself, but a
|
|
84
|
+
* PUBLISHED artifact also has an `/embed` route whose `frame-ancestors` carries
|
|
85
|
+
* the domains its author allow-listed under "Get embed code → Allowed domains".
|
|
86
|
+
* That is Anthropic's supported way to put an artifact on another site, so that
|
|
87
|
+
* is what the iframe points at.
|
|
88
|
+
*
|
|
89
|
+
* Returns `null` for everything else, including a Claude CODE url
|
|
90
|
+
* (`claude.ai/code/artifact/…`), whose Share dialog exposes no Embed settings
|
|
91
|
+
* and whose `/embed` path answers `frame-ancestors 'self'` (verified against
|
|
92
|
+
* real artifacts, 2026-08). A trailing `/embed` is accepted because that is the
|
|
93
|
+
* url "Get embed code" hands the author.
|
|
94
|
+
*/
|
|
95
|
+
export function toClaudeEmbedUrl(url: string | null | undefined): string | null {
|
|
96
|
+
if (!url) return null
|
|
97
|
+
try {
|
|
98
|
+
const u = new URL(url)
|
|
99
|
+
const host = u.hostname.toLowerCase()
|
|
100
|
+
// `claude.site/artifacts/<id>` 308-redirects to the claude.ai public url.
|
|
101
|
+
const publicId =
|
|
102
|
+
(host === 'claude.ai' && /^\/public\/artifacts\/([\w-]+)(?:\/embed)?\/?$/.exec(u.pathname)?.[1]) ||
|
|
103
|
+
(host === 'claude.site' && /^\/artifacts\/([\w-]+)(?:\/embed)?\/?$/.exec(u.pathname)?.[1]) ||
|
|
104
|
+
null
|
|
105
|
+
return publicId ? `https://claude.ai/public/artifacts/${publicId}/embed` : null
|
|
106
|
+
} catch {
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
}
|