@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.
Files changed (189) hide show
  1. package/dist/{chunk-PKCZL56Y.cjs → chunk-4H7L4OSR.cjs} +4 -4
  2. package/dist/{chunk-PKCZL56Y.cjs.map → chunk-4H7L4OSR.cjs.map} +1 -1
  3. package/dist/chunk-5I3BSQ6D.js +2 -0
  4. package/dist/{chunk-6OPEULTD.cjs → chunk-BKDSNVGH.cjs} +9 -9
  5. package/dist/{chunk-6OPEULTD.cjs.map → chunk-BKDSNVGH.cjs.map} +1 -1
  6. package/dist/{chunk-OYFXHET4.cjs → chunk-BWMVI2KD.cjs} +7 -7
  7. package/dist/{chunk-OYFXHET4.cjs.map → chunk-BWMVI2KD.cjs.map} +1 -1
  8. package/dist/{chunk-YFLSOYX4.cjs → chunk-DLSDYLY3.cjs} +11 -11
  9. package/dist/{chunk-YFLSOYX4.cjs.map → chunk-DLSDYLY3.cjs.map} +1 -1
  10. package/dist/{chunk-NQ6UF4LP.js → chunk-EQEMZU45.js} +4 -4
  11. package/dist/{chunk-FG4BSJ4A.cjs → chunk-GG2HJQW3.cjs} +26 -26
  12. package/dist/{chunk-FG4BSJ4A.cjs.map → chunk-GG2HJQW3.cjs.map} +1 -1
  13. package/dist/{chunk-O3SEC45L.js → chunk-GG6CAELN.js} +4 -4
  14. package/dist/{chunk-J7R3VZL5.cjs → chunk-GZL5SAUI.cjs} +19 -15
  15. package/dist/chunk-GZL5SAUI.cjs.map +1 -0
  16. package/dist/{chunk-ASULU3T2.js → chunk-H4RCJ4IB.js} +7 -3
  17. package/dist/{chunk-ASULU3T2.js.map → chunk-H4RCJ4IB.js.map} +1 -1
  18. package/dist/{chunk-U5EVZLL5.cjs → chunk-KHEOXHF4.cjs} +5 -5
  19. package/dist/{chunk-U5EVZLL5.cjs.map → chunk-KHEOXHF4.cjs.map} +1 -1
  20. package/dist/{chunk-Z4VU6MJ4.js → chunk-LOWWLH2W.js} +2 -2
  21. package/dist/{chunk-NUKQWLOG.js → chunk-MIKGZY7V.js} +5 -5
  22. package/dist/{chunk-BK7V6CH5.js → chunk-MSUSONUK.js} +3297 -3209
  23. package/dist/chunk-MSUSONUK.js.map +1 -0
  24. package/dist/{chunk-PT4GS4CE.cjs → chunk-NAYXROCV.cjs} +3 -3
  25. package/dist/{chunk-PT4GS4CE.cjs.map → chunk-NAYXROCV.cjs.map} +1 -1
  26. package/dist/{chunk-G2VDS3M5.js → chunk-OEN3GTRL.js} +2 -2
  27. package/dist/{chunk-XFI5PCER.cjs → chunk-OTDVQBLI.cjs} +31 -31
  28. package/dist/{chunk-XFI5PCER.cjs.map → chunk-OTDVQBLI.cjs.map} +1 -1
  29. package/dist/{chunk-WGDZW237.cjs → chunk-PBH2BQYR.cjs} +86 -86
  30. package/dist/{chunk-WGDZW237.cjs.map → chunk-PBH2BQYR.cjs.map} +1 -1
  31. package/dist/{chunk-E6PW7GRQ.cjs → chunk-PZH3CZTJ.cjs} +89 -75
  32. package/dist/chunk-PZH3CZTJ.cjs.map +1 -0
  33. package/dist/{chunk-GEC2NHQH.cjs → chunk-QWVON3LG.cjs} +20 -20
  34. package/dist/{chunk-GEC2NHQH.cjs.map → chunk-QWVON3LG.cjs.map} +1 -1
  35. package/dist/{chunk-X4JKXYY5.js → chunk-SQ53GVCG.js} +2 -2
  36. package/dist/{chunk-AZPUAPXW.cjs → chunk-TA5P2M7V.cjs} +796 -708
  37. package/dist/chunk-TA5P2M7V.cjs.map +1 -0
  38. package/dist/chunk-UGAHDPNU.cjs +2 -0
  39. package/dist/{chunk-ZTM35Q76.cjs.map → chunk-UGAHDPNU.cjs.map} +1 -1
  40. package/dist/{chunk-T3P56JKN.js → chunk-XPLZB74T.js} +2 -2
  41. package/dist/{chunk-CQNK6RS6.js → chunk-XSK7ZQ3C.js} +2 -2
  42. package/dist/{chunk-TDYF6Q2D.js → chunk-XZNKPEWB.js} +2 -2
  43. package/dist/{chunk-VVDLVCPW.cjs → chunk-Y6IGG6RZ.cjs} +39 -39
  44. package/dist/{chunk-VVDLVCPW.cjs.map → chunk-Y6IGG6RZ.cjs.map} +1 -1
  45. package/dist/{chunk-7D62XVHH.js → chunk-YVOMHB73.js} +35 -21
  46. package/dist/chunk-YVOMHB73.js.map +1 -0
  47. package/dist/{chunk-CP7TMQBH.js → chunk-ZUVVJ24E.js} +2 -2
  48. package/dist/{chunk-DPMOEYEM.js → chunk-ZYC4O76Y.js} +2 -2
  49. package/dist/components/case-studies/index.cjs +8 -8
  50. package/dist/components/case-studies/index.js +2 -2
  51. package/dist/components/chat/entity-cards/employee-entry-card.d.ts.map +1 -1
  52. package/dist/components/chat/index.cjs +2 -2
  53. package/dist/components/chat/index.js +1 -1
  54. package/dist/components/comment-card.d.ts +18 -2
  55. package/dist/components/comment-card.d.ts.map +1 -1
  56. package/dist/components/comment-composer.d.ts +42 -0
  57. package/dist/components/comment-composer.d.ts.map +1 -0
  58. package/dist/components/contact/index.cjs +3 -3
  59. package/dist/components/contact/index.js +2 -2
  60. package/dist/components/docs/index.cjs +5 -5
  61. package/dist/components/docs/index.js +4 -4
  62. package/dist/components/embeds/claude-embed.d.ts +31 -0
  63. package/dist/components/embeds/claude-embed.d.ts.map +1 -0
  64. package/dist/components/embeds/index.cjs +6 -4
  65. package/dist/components/embeds/index.cjs.map +1 -1
  66. package/dist/components/embeds/index.d.ts +2 -0
  67. package/dist/components/embeds/index.d.ts.map +1 -1
  68. package/dist/components/embeds/index.js +5 -3
  69. package/dist/components/faq/index.cjs +3 -3
  70. package/dist/components/faq/index.js +2 -2
  71. package/dist/components/features/array-entry-manager.d.ts.map +1 -1
  72. package/dist/components/features/clickup-tasks-manager.d.ts +6 -1
  73. package/dist/components/features/clickup-tasks-manager.d.ts.map +1 -1
  74. package/dist/components/features/index.cjs +2 -2
  75. package/dist/components/features/index.js +1 -1
  76. package/dist/components/help-center-pages/index.cjs +22 -22
  77. package/dist/components/help-center-pages/index.js +13 -13
  78. package/dist/components/index.cjs +281 -204
  79. package/dist/components/index.cjs.map +1 -1
  80. package/dist/components/index.d.ts +1 -0
  81. package/dist/components/index.d.ts.map +1 -1
  82. package/dist/components/index.js +647 -570
  83. package/dist/components/index.js.map +1 -1
  84. package/dist/components/meeting-scheduler/index.cjs +30 -30
  85. package/dist/components/meeting-scheduler/index.js +3 -3
  86. package/dist/components/navigation/index.cjs +2 -2
  87. package/dist/components/navigation/index.js +1 -1
  88. package/dist/components/onboarding-guides/index.cjs +5 -5
  89. package/dist/components/onboarding-guides/index.js +4 -4
  90. package/dist/components/related-content/index.cjs +3 -3
  91. package/dist/components/related-content/index.js +2 -2
  92. package/dist/components/shared/delivery/delivery-row.d.ts +5 -1
  93. package/dist/components/shared/delivery/delivery-row.d.ts.map +1 -1
  94. package/dist/components/shared/roadmap/roadmap-grid.d.ts +10 -1
  95. package/dist/components/shared/roadmap/roadmap-grid.d.ts.map +1 -1
  96. package/dist/components/tickets/index.cjs +6 -6
  97. package/dist/components/tickets/index.js +5 -5
  98. package/dist/components/ui/card.d.ts.map +1 -1
  99. package/dist/components/ui/index.cjs +4 -2
  100. package/dist/components/ui/index.cjs.map +1 -1
  101. package/dist/components/ui/index.d.ts +1 -1
  102. package/dist/components/ui/index.d.ts.map +1 -1
  103. package/dist/components/ui/index.js +3 -1
  104. package/dist/components/ui/markdown/rich/embed-overrides.d.ts.map +1 -1
  105. package/dist/components/ui/markdown/rich/shortcodes.d.ts.map +1 -1
  106. package/dist/components/ui/table/index.d.ts +1 -1
  107. package/dist/components/ui/table/index.d.ts.map +1 -1
  108. package/dist/components/ui/table/table-row.d.ts.map +1 -1
  109. package/dist/components/ui/table/table-skeleton.d.ts +15 -2
  110. package/dist/components/ui/table/table-skeleton.d.ts.map +1 -1
  111. package/dist/components/ui/table/table.d.ts.map +1 -1
  112. package/dist/components/ui/table/types.d.ts +2 -0
  113. package/dist/components/ui/table/types.d.ts.map +1 -1
  114. package/dist/index.cjs +6 -2
  115. package/dist/index.cjs.map +1 -1
  116. package/dist/index.js +5 -1
  117. package/dist/types/department.d.ts +41 -0
  118. package/dist/types/department.d.ts.map +1 -0
  119. package/dist/types/design-doc.d.ts +247 -0
  120. package/dist/types/design-doc.d.ts.map +1 -0
  121. package/dist/types/employee.d.ts +25 -2
  122. package/dist/types/employee.d.ts.map +1 -1
  123. package/dist/types/index.d.ts +2 -0
  124. package/dist/types/index.d.ts.map +1 -1
  125. package/dist/types/marketing.d.ts +1 -1
  126. package/dist/types/marketing.d.ts.map +1 -1
  127. package/dist/types/profile.d.ts +2 -1
  128. package/dist/types/profile.d.ts.map +1 -1
  129. package/dist/types/team.d.ts +15 -1
  130. package/dist/types/team.d.ts.map +1 -1
  131. package/dist/types/user.d.ts +3 -1
  132. package/dist/types/user.d.ts.map +1 -1
  133. package/dist/utils/embed-url-converters.d.ts +17 -0
  134. package/dist/utils/embed-url-converters.d.ts.map +1 -1
  135. package/dist/utils/index.cjs +12 -0
  136. package/dist/utils/index.cjs.map +1 -1
  137. package/dist/utils/index.js +12 -1
  138. package/dist/utils/index.js.map +1 -1
  139. package/package.json +1 -1
  140. package/src/components/.comment-card.md +4 -5
  141. package/src/components/chat/entity-cards/employee-entry-card.tsx +6 -0
  142. package/src/components/comment-card.tsx +42 -17
  143. package/src/components/comment-composer.tsx +117 -0
  144. package/src/components/embeds/claude-embed.tsx +84 -0
  145. package/src/components/embeds/index.ts +2 -0
  146. package/src/components/features/array-entry-manager.tsx +2 -1
  147. package/src/components/features/clickup-tasks-manager.tsx +8 -1
  148. package/src/components/index.ts +1 -0
  149. package/src/components/shared/delivery/delivery-row.tsx +17 -1
  150. package/src/components/shared/roadmap/roadmap-grid.tsx +21 -1
  151. package/src/components/ui/card.tsx +4 -1
  152. package/src/components/ui/index.ts +1 -0
  153. package/src/components/ui/markdown/rich/__tests__/shortcodes.test.ts +44 -0
  154. package/src/components/ui/markdown/rich/embed-overrides.tsx +4 -0
  155. package/src/components/ui/markdown/rich/shortcodes.ts +12 -0
  156. package/src/components/ui/table/index.ts +1 -1
  157. package/src/components/ui/table/table-row.tsx +7 -2
  158. package/src/components/ui/table/table-skeleton.tsx +20 -3
  159. package/src/components/ui/table/table.tsx +1 -0
  160. package/src/components/ui/table/types.ts +2 -0
  161. package/src/types/department.ts +44 -0
  162. package/src/types/design-doc.ts +295 -0
  163. package/src/types/employee.ts +27 -2
  164. package/src/types/index.ts +2 -0
  165. package/src/types/marketing.ts +1 -0
  166. package/src/types/profile.ts +3 -1
  167. package/src/types/team.ts +17 -1
  168. package/src/types/user.ts +4 -1
  169. package/src/utils/__tests__/claude-embed-url.test.ts +42 -0
  170. package/src/utils/embed-url-converters.ts +32 -0
  171. package/dist/chunk-7D62XVHH.js.map +0 -1
  172. package/dist/chunk-AZPUAPXW.cjs.map +0 -1
  173. package/dist/chunk-BK7V6CH5.js.map +0 -1
  174. package/dist/chunk-E6PW7GRQ.cjs.map +0 -1
  175. package/dist/chunk-J7R3VZL5.cjs.map +0 -1
  176. package/dist/chunk-MVWSLZG7.js +0 -2
  177. package/dist/chunk-ZTM35Q76.cjs +0 -2
  178. /package/dist/{chunk-MVWSLZG7.js.map → chunk-5I3BSQ6D.js.map} +0 -0
  179. /package/dist/{chunk-NQ6UF4LP.js.map → chunk-EQEMZU45.js.map} +0 -0
  180. /package/dist/{chunk-O3SEC45L.js.map → chunk-GG6CAELN.js.map} +0 -0
  181. /package/dist/{chunk-Z4VU6MJ4.js.map → chunk-LOWWLH2W.js.map} +0 -0
  182. /package/dist/{chunk-NUKQWLOG.js.map → chunk-MIKGZY7V.js.map} +0 -0
  183. /package/dist/{chunk-G2VDS3M5.js.map → chunk-OEN3GTRL.js.map} +0 -0
  184. /package/dist/{chunk-X4JKXYY5.js.map → chunk-SQ53GVCG.js.map} +0 -0
  185. /package/dist/{chunk-T3P56JKN.js.map → chunk-XPLZB74T.js.map} +0 -0
  186. /package/dist/{chunk-CQNK6RS6.js.map → chunk-XSK7ZQ3C.js.map} +0 -0
  187. /package/dist/{chunk-TDYF6Q2D.js.map → chunk-XZNKPEWB.js.map} +0 -0
  188. /package/dist/{chunk-CP7TMQBH.js.map → chunk-ZUVVJ24E.js.map} +0 -0
  189. /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 ? 'py-2' : cn('py-0', ROW_HEIGHT_DESKTOP),
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 py-0',
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 }
@@ -250,6 +250,7 @@ export function Table<T = any>({
250
250
  <TableCardSkeleton
251
251
  columns={columns}
252
252
  rows={skeletonRows}
253
+ compact={compact}
253
254
  hasActions={Boolean(rowActions) && rowActions!.length > 0}
254
255
  hasChevron={Boolean(rowHref)}
255
256
  />
@@ -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
+ }
@@ -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: string;
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
- department: string;
43
+ department_id: string | null;
44
+ department: DepartmentRef | null;
20
45
  avatar_url: string;
21
46
  role: string;
22
47
  job_title: string;
@@ -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'
@@ -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';
@@ -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: string | null;
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
- department?: string;
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
- department: string | null
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
+ }