@flamingo-stack/openframe-frontend-core 0.0.668 → 0.0.669-snapshot.20260927145656

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 (148) hide show
  1. package/dist/{chunk-B5UP36KY.cjs → chunk-22E6CSIS.cjs} +12 -12
  2. package/dist/{chunk-B5UP36KY.cjs.map → chunk-22E6CSIS.cjs.map} +1 -1
  3. package/dist/{chunk-6KJV6GAJ.js → chunk-5IP7FYUN.js} +4 -4
  4. package/dist/{chunk-GF6WJ4MG.js → chunk-5Q6BE56A.js} +6 -6
  5. package/dist/{chunk-NZD2WTJ2.cjs → chunk-5ZVYPAFQ.cjs} +3 -3
  6. package/dist/{chunk-NZD2WTJ2.cjs.map → chunk-5ZVYPAFQ.cjs.map} +1 -1
  7. package/dist/{chunk-Z3HUP4G7.cjs → chunk-635CAJ36.cjs} +32 -32
  8. package/dist/{chunk-Z3HUP4G7.cjs.map → chunk-635CAJ36.cjs.map} +1 -1
  9. package/dist/{chunk-GO63IT6K.js → chunk-6P335PRA.js} +5 -5
  10. package/dist/{chunk-5V3LERVC.js → chunk-6X66J23J.js} +4 -4
  11. package/dist/{chunk-6W4LUHMY.cjs → chunk-7VZZ5S5V.cjs} +206 -206
  12. package/dist/{chunk-6W4LUHMY.cjs.map → chunk-7VZZ5S5V.cjs.map} +1 -1
  13. package/dist/{chunk-EV2KJ4D2.cjs → chunk-7WK544AC.cjs} +7 -7
  14. package/dist/{chunk-EV2KJ4D2.cjs.map → chunk-7WK544AC.cjs.map} +1 -1
  15. package/dist/{chunk-FOH4MNOZ.js → chunk-C5CTLTKJ.js} +3 -3
  16. package/dist/{chunk-TFVUM6KK.cjs → chunk-CK4BZRVJ.cjs} +10 -10
  17. package/dist/{chunk-TFVUM6KK.cjs.map → chunk-CK4BZRVJ.cjs.map} +1 -1
  18. package/dist/{chunk-Z3GN3MIW.cjs → chunk-D2HYPVYQ.cjs} +81 -81
  19. package/dist/{chunk-Z3GN3MIW.cjs.map → chunk-D2HYPVYQ.cjs.map} +1 -1
  20. package/dist/{chunk-WVCDX7FE.cjs → chunk-DAMA3A5Z.cjs} +15 -15
  21. package/dist/{chunk-WVCDX7FE.cjs.map → chunk-DAMA3A5Z.cjs.map} +1 -1
  22. package/dist/{chunk-P6IJXAEE.js → chunk-DLJGCDHX.js} +2 -1
  23. package/dist/{chunk-P6IJXAEE.js.map → chunk-DLJGCDHX.js.map} +1 -1
  24. package/dist/{chunk-IJPV7OMA.js → chunk-EHB5LETA.js} +591 -466
  25. package/dist/chunk-EHB5LETA.js.map +1 -0
  26. package/dist/{chunk-CFUUVLYQ.cjs → chunk-GXKKWFZR.cjs} +33 -33
  27. package/dist/{chunk-CFUUVLYQ.cjs.map → chunk-GXKKWFZR.cjs.map} +1 -1
  28. package/dist/{chunk-IZ6F7LOE.js → chunk-IHOSMLBC.js} +3 -3
  29. package/dist/{chunk-DK3BHDUZ.js → chunk-INIITSC3.js} +3 -3
  30. package/dist/{chunk-ZHRIFUN3.cjs → chunk-L5TTJXLU.cjs} +28 -28
  31. package/dist/{chunk-ZHRIFUN3.cjs.map → chunk-L5TTJXLU.cjs.map} +1 -1
  32. package/dist/{chunk-7UW2BBFI.cjs → chunk-LGIBMKRM.cjs} +59 -59
  33. package/dist/{chunk-7UW2BBFI.cjs.map → chunk-LGIBMKRM.cjs.map} +1 -1
  34. package/dist/{chunk-IBQT4RTS.js → chunk-O3PPPILJ.js} +3 -3
  35. package/dist/{chunk-CWK4QHVF.js → chunk-OABFQXAI.js} +3 -3
  36. package/dist/{chunk-VEQWH5KH.cjs → chunk-OHLOPLOM.cjs} +30 -30
  37. package/dist/{chunk-VEQWH5KH.cjs.map → chunk-OHLOPLOM.cjs.map} +1 -1
  38. package/dist/{chunk-PHLBRU5P.js → chunk-P7FKI2UW.js} +3 -3
  39. package/dist/{chunk-YH3BUB4W.js → chunk-PTZKMV35.js} +5 -5
  40. package/dist/{chunk-E5HUHKHH.js → chunk-QGHMHW7R.js} +7 -7
  41. package/dist/{chunk-VJ4XBTR5.cjs → chunk-QH6PMRGF.cjs} +22 -22
  42. package/dist/{chunk-VJ4XBTR5.cjs.map → chunk-QH6PMRGF.cjs.map} +1 -1
  43. package/dist/{chunk-37NYF4W2.cjs → chunk-QRUW4UXB.cjs} +2 -1
  44. package/dist/chunk-QRUW4UXB.cjs.map +1 -0
  45. package/dist/{chunk-LJOELK7P.js → chunk-RG7XYHN7.js} +2 -2
  46. package/dist/{chunk-Y7SFGCF4.cjs → chunk-RRJ6EL63.cjs} +20 -20
  47. package/dist/{chunk-Y7SFGCF4.cjs.map → chunk-RRJ6EL63.cjs.map} +1 -1
  48. package/dist/{chunk-FJNX67W2.cjs → chunk-SEW23M5X.cjs} +5 -5
  49. package/dist/{chunk-FJNX67W2.cjs.map → chunk-SEW23M5X.cjs.map} +1 -1
  50. package/dist/{chunk-K4WYWTQV.cjs → chunk-TLMPNPYS.cjs} +6 -6
  51. package/dist/{chunk-K4WYWTQV.cjs.map → chunk-TLMPNPYS.cjs.map} +1 -1
  52. package/dist/{chunk-SHGMDYCR.js → chunk-VEAA5C5X.js} +2 -2
  53. package/dist/{chunk-5CC33IJ7.js → chunk-VXRWR56S.js} +6 -6
  54. package/dist/{chunk-NNYND3I4.js → chunk-XUHLWFEM.js} +3 -3
  55. package/dist/{chunk-QHL4GYYY.cjs → chunk-ZYFDPD73.cjs} +577 -452
  56. package/dist/chunk-ZYFDPD73.cjs.map +1 -0
  57. package/dist/components/case-studies/index.cjs +16 -16
  58. package/dist/components/case-studies/index.js +4 -4
  59. package/dist/components/chat/entity-cards/design-doc-card.d.ts +23 -0
  60. package/dist/components/chat/entity-cards/design-doc-card.d.ts.map +1 -0
  61. package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
  62. package/dist/components/chat/entity-cards/index.d.ts +1 -0
  63. package/dist/components/chat/entity-cards/index.d.ts.map +1 -1
  64. package/dist/components/chat/index.cjs +11 -3
  65. package/dist/components/chat/index.cjs.map +1 -1
  66. package/dist/components/chat/index.js +10 -2
  67. package/dist/components/contact/index.cjs +5 -5
  68. package/dist/components/contact/index.js +4 -4
  69. package/dist/components/docs/index.cjs +8 -8
  70. package/dist/components/docs/index.js +7 -7
  71. package/dist/components/embeds/index.cjs +5 -5
  72. package/dist/components/embeds/index.js +4 -4
  73. package/dist/components/faq/index.cjs +5 -5
  74. package/dist/components/faq/index.js +4 -4
  75. package/dist/components/features/index.cjs +4 -4
  76. package/dist/components/features/index.js +3 -3
  77. package/dist/components/help-center-pages/index.cjs +197 -89
  78. package/dist/components/help-center-pages/index.cjs.map +1 -1
  79. package/dist/components/help-center-pages/index.d.ts +1 -1
  80. package/dist/components/help-center-pages/index.d.ts.map +1 -1
  81. package/dist/components/help-center-pages/index.js +143 -35
  82. package/dist/components/help-center-pages/index.js.map +1 -1
  83. package/dist/components/help-center-pages/trust-center-page.d.ts +6 -0
  84. package/dist/components/help-center-pages/trust-center-page.d.ts.map +1 -1
  85. package/dist/components/help-center-pages/trust-center-sections.d.ts +17 -2
  86. package/dist/components/help-center-pages/trust-center-sections.d.ts.map +1 -1
  87. package/dist/components/index.cjs +62 -54
  88. package/dist/components/index.cjs.map +1 -1
  89. package/dist/components/index.js +23 -15
  90. package/dist/components/index.js.map +1 -1
  91. package/dist/components/meeting-scheduler/index.cjs +40 -40
  92. package/dist/components/meeting-scheduler/index.js +3 -3
  93. package/dist/components/navigation/index.cjs +7 -7
  94. package/dist/components/navigation/index.js +6 -6
  95. package/dist/components/onboarding-guides/index.cjs +7 -7
  96. package/dist/components/onboarding-guides/index.js +6 -6
  97. package/dist/components/related-content/index.cjs +3 -3
  98. package/dist/components/related-content/index.js +2 -2
  99. package/dist/components/tickets/index.cjs +8 -8
  100. package/dist/components/tickets/index.js +7 -7
  101. package/dist/components/ui/index.cjs +12 -4
  102. package/dist/components/ui/index.cjs.map +1 -1
  103. package/dist/components/ui/index.js +11 -3
  104. package/dist/index.cjs +27 -9
  105. package/dist/index.cjs.map +1 -1
  106. package/dist/index.js +25 -7
  107. package/dist/index.js.map +1 -1
  108. package/dist/utils/design-doc-readiness.d.ts +20 -0
  109. package/dist/utils/design-doc-readiness.d.ts.map +1 -0
  110. package/dist/utils/index.cjs +28 -3
  111. package/dist/utils/index.cjs.map +1 -1
  112. package/dist/utils/index.d.ts +1 -0
  113. package/dist/utils/index.d.ts.map +1 -1
  114. package/dist/utils/index.js +24 -4
  115. package/dist/utils/index.js.map +1 -1
  116. package/dist/utils/list-url.d.ts.map +1 -1
  117. package/dist/utils/source-icons.d.ts.map +1 -1
  118. package/package.json +1 -1
  119. package/src/components/chat/entity-cards/design-doc-card.tsx +114 -0
  120. package/src/components/chat/entity-cards/dispatch.tsx +62 -1
  121. package/src/components/chat/entity-cards/index.ts +7 -0
  122. package/src/components/help-center-pages/__tests__/trust-center-page.test.tsx +28 -1
  123. package/src/components/help-center-pages/index.ts +1 -1
  124. package/src/components/help-center-pages/trust-center-page.tsx +59 -16
  125. package/src/components/help-center-pages/trust-center-sections.tsx +159 -8
  126. package/src/utils/design-doc-readiness.ts +34 -0
  127. package/src/utils/index.ts +1 -0
  128. package/src/utils/list-url.ts +1 -0
  129. package/src/utils/source-icons.ts +3 -0
  130. package/dist/chunk-37NYF4W2.cjs.map +0 -1
  131. package/dist/chunk-IJPV7OMA.js.map +0 -1
  132. package/dist/chunk-QHL4GYYY.cjs.map +0 -1
  133. /package/dist/{chunk-6KJV6GAJ.js.map → chunk-5IP7FYUN.js.map} +0 -0
  134. /package/dist/{chunk-GF6WJ4MG.js.map → chunk-5Q6BE56A.js.map} +0 -0
  135. /package/dist/{chunk-GO63IT6K.js.map → chunk-6P335PRA.js.map} +0 -0
  136. /package/dist/{chunk-5V3LERVC.js.map → chunk-6X66J23J.js.map} +0 -0
  137. /package/dist/{chunk-FOH4MNOZ.js.map → chunk-C5CTLTKJ.js.map} +0 -0
  138. /package/dist/{chunk-IZ6F7LOE.js.map → chunk-IHOSMLBC.js.map} +0 -0
  139. /package/dist/{chunk-DK3BHDUZ.js.map → chunk-INIITSC3.js.map} +0 -0
  140. /package/dist/{chunk-IBQT4RTS.js.map → chunk-O3PPPILJ.js.map} +0 -0
  141. /package/dist/{chunk-CWK4QHVF.js.map → chunk-OABFQXAI.js.map} +0 -0
  142. /package/dist/{chunk-PHLBRU5P.js.map → chunk-P7FKI2UW.js.map} +0 -0
  143. /package/dist/{chunk-YH3BUB4W.js.map → chunk-PTZKMV35.js.map} +0 -0
  144. /package/dist/{chunk-E5HUHKHH.js.map → chunk-QGHMHW7R.js.map} +0 -0
  145. /package/dist/{chunk-LJOELK7P.js.map → chunk-RG7XYHN7.js.map} +0 -0
  146. /package/dist/{chunk-SHGMDYCR.js.map → chunk-VEAA5C5X.js.map} +0 -0
  147. /package/dist/{chunk-5CC33IJ7.js.map → chunk-VXRWR56S.js.map} +0 -0
  148. /package/dist/{chunk-NNYND3I4.js.map → chunk-XUHLWFEM.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"list-url.d.ts","sourceRoot":"","sources":["../../src/utils/list-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAaH;;;;iEAIiE;AACjE,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAKtE;AAqFD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,SAAK,GAAG,MAAM,GAAG,IAAI,CAa5F"}
1
+ {"version":3,"file":"list-url.d.ts","sourceRoot":"","sources":["../../src/utils/list-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAaH;;;;iEAIiE;AACjE,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAKtE;AAsFD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,SAAK,GAAG,MAAM,GAAG,IAAI,CAa5F"}
@@ -1 +1 @@
1
- {"version":3,"file":"source-icons.d.ts","sourceRoot":"","sources":["../../src/utils/source-icons.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkEpD,CAAC;AAEF;;6DAE6D;AAC7D,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGxF;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAwEzD,CAAC;AAEF;;kEAEkE;AAClE,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iCAAiC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkEpE,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEjF;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5E"}
1
+ {"version":3,"file":"source-icons.d.ts","sourceRoot":"","sources":["../../src/utils/source-icons.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAmEpD,CAAC;AAEF;;6DAE6D;AAC7D,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAGxF;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAyEzD,CAAC;AAEF;;kEAEkE;AAClE,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iCAAiC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAmEpE,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEjF;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.668",
3
+ "version": "0.0.669-snapshot.20260927145656",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * DesignDocCard (pure presentation) — a design doc as the entity card every surface shows it with: the full card
5
+ * (`default`, the `RoadmapCard` layout without its image slot, since a doc has no image of its own and a constant glyph
6
+ * says nothing: title, DRI and date, readiness (the hub's own wording, `design-doc-readiness`), summary, sign-off caption and the people) for admin surfaces, and the chat's compact card (`DesignDocChatCard` in `dispatch.tsx`, which reads
7
+ * the same readiness and meta). The card writes no click logic beyond its `href` anchor.
8
+ */
9
+
10
+ import type { DesignDoc } from '../../../types/design-doc';
11
+ import { formatDateShort } from '../../../utils/date-formatters';
12
+ import {
13
+ DESIGN_DOC_READINESS_DISPLAY,
14
+ designDocReadiness,
15
+ formatCompletionLabelWithBlockers,
16
+ } from '../../../utils/design-doc-readiness';
17
+ import { AvatarStack, type AvatarStackPerson } from '../../ui/avatar-stack';
18
+ import { StatusBadge } from '../../ui/status-badge';
19
+ import { safeHref } from '../utils/compact-card-classes';
20
+
21
+ /** The people behind a doc: the DRI first, then the implementation owners (display only). */
22
+ export function designDocPeople(doc: Pick<DesignDoc, 'author' | 'feature_leads'>): AvatarStackPerson[] {
23
+ const people: AvatarStackPerson[] = [];
24
+ if (doc.author?.full_name)
25
+ people.push({
26
+ key: `dri-${doc.author.id ?? doc.author.full_name}`,
27
+ name: doc.author.full_name,
28
+ avatarUrl: doc.author.avatar_url,
29
+ });
30
+ for (const p of doc.feature_leads ?? []) {
31
+ if (!p.full_name) continue;
32
+ people.push({ key: `lead-${p.id}`, name: p.full_name, avatarUrl: p.avatar_url });
33
+ }
34
+ return people;
35
+ }
36
+
37
+ /** "DRI Jane Doe · updated Sep 24, 2026" — the card's subtitle. */
38
+ export function designDocMetaLine(doc: Pick<DesignDoc, 'author' | 'updated_at'>): string {
39
+ return [doc.author?.full_name ? `DRI ${doc.author.full_name}` : null, `updated ${formatDateShort(doc.updated_at)}`]
40
+ .filter(Boolean)
41
+ .join(' · ');
42
+ }
43
+
44
+ export interface DesignDocCardProps {
45
+ doc: DesignDoc;
46
+ /** The doc's page. */
47
+ href?: string | null;
48
+ target?: '_blank';
49
+ rel?: 'noopener' | 'noopener noreferrer';
50
+ className?: string;
51
+ }
52
+
53
+ export function DesignDocCardSkeleton() {
54
+ return (
55
+ <div className="flex h-full animate-pulse flex-col gap-[16px] rounded-[6px] border border-ods-border bg-ods-card p-[24px]">
56
+ <div className="flex items-center gap-[16px]">
57
+ <div className="flex flex-1 flex-col gap-2">
58
+ <div className="h-5 w-3/4 rounded bg-ods-bg" />
59
+ <div className="h-4 w-1/3 rounded bg-ods-bg/70" />
60
+ </div>
61
+ </div>
62
+ <div className="h-4 w-full rounded bg-ods-bg/60" />
63
+ <div className="h-4 w-5/6 rounded bg-ods-bg/60" />
64
+ </div>
65
+ );
66
+ }
67
+
68
+ export function DesignDocCard({ doc, href, target, rel, className }: DesignDocCardProps) {
69
+ const readiness = DESIGN_DOC_READINESS_DISPLAY[designDocReadiness(doc.completion)];
70
+ const people = designDocPeople(doc);
71
+ const link = safeHref(href ?? null);
72
+ const body = (
73
+ <>
74
+ <div className="flex w-full items-center gap-[16px]">
75
+ <div className="flex min-w-0 flex-1 flex-col">
76
+ <div className="flex min-h-[48px] items-center">
77
+ <h3 className="line-clamp-2 flex-1 text-ods-text-primary text-h3">{doc.title || 'Untitled design doc'}</h3>
78
+ </div>
79
+ <div className="flex min-h-[20px] items-center">
80
+ <p className="truncate text-ods-text-secondary text-h5">{designDocMetaLine(doc)}</p>
81
+ </div>
82
+ </div>
83
+ <StatusBadge
84
+ text={readiness.label}
85
+ colorScheme={readiness.scheme}
86
+ variant="button"
87
+ singleLine
88
+ className="hidden md:inline-flex"
89
+ />
90
+ </div>
91
+ <div className="md:hidden">
92
+ <StatusBadge text={readiness.label} colorScheme={readiness.scheme} variant="button" singleLine />
93
+ </div>
94
+ <div className="flex min-h-[72px] items-center">
95
+ <p className="line-clamp-3 text-ods-text-secondary text-h4">{doc.summary || ''}</p>
96
+ </div>
97
+ <div className="flex-1" />
98
+ <div className="flex w-full items-center justify-between gap-2">
99
+ <span className="truncate text-ods-text-secondary text-h6">
100
+ {formatCompletionLabelWithBlockers(doc.completion)}
101
+ </span>
102
+ {people.length > 0 ? <AvatarStack size="xs" people={people} className="shrink-0" /> : null}
103
+ </div>
104
+ </>
105
+ );
106
+ const outer = `flex h-full flex-col gap-[16px] rounded-[6px] border border-ods-border bg-ods-card p-[24px] transition-all hover:border-ods-accent ${className ?? ''}`;
107
+ return link ? (
108
+ <a href={link} target={target} rel={rel} className={outer}>
109
+ {body}
110
+ </a>
111
+ ) : (
112
+ <div className={outer}>{body}</div>
113
+ );
114
+ }
@@ -33,6 +33,7 @@ import React, { type ReactNode } from 'react';
33
33
  import { useRequiredChatRuntime } from '../../../contexts/chat-runtime-context';
34
34
  import Image from '../../../embed-shims/next-image';
35
35
  import { useRouter } from '../../../embed-shims/next-navigation';
36
+ import type { DesignDoc } from '../../../types/design-doc';
36
37
  import {
37
38
  TRUST_CENTER_DOCUMENT_TYPE,
38
39
  TRUST_CENTER_PAGE_PATH,
@@ -45,6 +46,11 @@ import {
45
46
  type TrustFrameworkMonitoringEntry,
46
47
  } from '../../../types/trust-center';
47
48
  import { formatDateShort } from '../../../utils/date-formatters';
49
+ import {
50
+ DESIGN_DOC_READINESS_DISPLAY,
51
+ designDocReadiness,
52
+ formatCompletionLabel,
53
+ } from '../../../utils/design-doc-readiness';
48
54
  import { faqItemAnchor } from '../../../utils/faq-anchor';
49
55
  import { formatDateUTC as formatDate } from '../../../utils/format';
50
56
  import { programMetaFormatters, programMetaLine } from '../../../utils/program-instant';
@@ -62,6 +68,7 @@ import { CodeIcon } from '../../icons-v2-generated/coding/code-icon';
62
68
  import { CodeSquareIcon } from '../../icons-v2-generated/coding/code-square-icon';
63
69
  import { CodingBranchIcon } from '../../icons-v2-generated/coding/coding-branch-icon';
64
70
  import { CodingCommitIcon } from '../../icons-v2-generated/coding/coding-commit-icon';
71
+ import { CodingMergeIcon } from '../../icons-v2-generated/coding/coding-merge-icon';
65
72
  import { CodingPullRequestIcon } from '../../icons-v2-generated/coding/coding-pull-request-icon';
66
73
  import { PackageIcon } from '../../icons-v2-generated/coding/package-icon';
67
74
  import { CallIcon } from '../../icons-v2-generated/communication/call-icon';
@@ -115,6 +122,7 @@ import { CaseStudyCardSkeleton } from './case-study-card';
115
122
  import { ChatVideoEntityCard } from './chat-video-entity-card';
116
123
  import { CustomerInterviewCardSkeleton } from './customer-interview-card';
117
124
  import { DeletedDataCard } from './deleted-data-card';
125
+ import { designDocMetaLine } from './design-doc-card';
118
126
  import {
119
127
  parseGithubTitle,
120
128
  formatActivityId,
@@ -1068,6 +1076,46 @@ function CustomerInterviewChatCard({
1068
1076
  );
1069
1077
  }
1070
1078
 
1079
+ /**
1080
+ * Design doc → the doc glyph, its readiness as the pill (the hub's own wording: "Ready to build" / "Not ready",
1081
+ * `design-doc-readiness`) and "DRI · updated · n/m reviews signed off" under the title. The hub's card route attaches the doc's list row as
1082
+ * `item.doc`; a row without it (an older hub) renders the ref's own title and preview.
1083
+ */
1084
+ function DesignDocChatCard({
1085
+ item,
1086
+ chatRef,
1087
+ isNewTab,
1088
+ discuss,
1089
+ }: {
1090
+ item: (ChatCardItem & { doc?: DesignDoc }) | undefined;
1091
+ chatRef: ChatRef;
1092
+ isNewTab: boolean;
1093
+ discuss?: CardDiscussAction;
1094
+ }) {
1095
+ const displayRef = fetchedItemDisplayRef(item, chatRef);
1096
+ const doc = item?.doc;
1097
+ const readiness = doc ? DESIGN_DOC_READINESS_DISPLAY[designDocReadiness(doc.completion)] : null;
1098
+ return (
1099
+ <MingoInfoCard
1100
+ title={doc?.title ?? displayRef.title}
1101
+ description={
1102
+ doc
1103
+ ? [designDocMetaLine(doc), formatCompletionLabel(doc.completion)].join(' · ')
1104
+ : (displayRef.preview ?? undefined)
1105
+ }
1106
+ icon={<FileContentIcon size={24} />}
1107
+ status={
1108
+ readiness
1109
+ ? { label: readiness.label, variant: readiness.scheme === 'success' ? 'success' : 'grey' }
1110
+ : { label: 'Design doc', variant: 'grey' }
1111
+ }
1112
+ anchorProps={buildAnchorProps(displayRef.url, isNewTab)}
1113
+ menuGroups={cardMenuGroups(displayRef.url, discuss)}
1114
+ menuAriaLabel="Design doc actions"
1115
+ />
1116
+ );
1117
+ }
1118
+
1071
1119
  /** Investor update → presentation icon + "Investor update" pill. Title falls
1072
1120
  * back to "Update #N" when the row has no explicit title. */
1073
1121
  function InvestorUpdateChatCard({
@@ -1411,7 +1459,6 @@ interface GlyphCardConfig {
1411
1459
  /** The OpenFrame logo every OpenFrame surface uses (the `openframe` icon name). */
1412
1460
  const OpenFrameGlyph = resolveIcon('openframe');
1413
1461
  const REF_GLYPH_CARD_CONFIGS: Record<string, GlyphCardConfig> = {
1414
- design_doc: { label: 'Design doc', icon: () => <FileContentIcon size={24} /> },
1415
1462
  openframe_tenant: { label: 'OpenFrame tenant', icon: () => <OpenFrameGlyph size={24} /> },
1416
1463
  prospect_call: { label: 'Prospect call', icon: () => <CallIcon size={24} />, media: true },
1417
1464
  // Code intelligence (product-hub internal): the review rules a repository is
@@ -1429,6 +1476,9 @@ const REF_GLYPH_CARD_CONFIGS: Record<string, GlyphCardConfig> = {
1429
1476
  code_symbol: { label: 'Code symbol', icon: () => <CodeSquareIcon size={24} /> },
1430
1477
  code_duplicate: { label: 'Duplicate code', icon: () => <Copy01Icon size={24} /> },
1431
1478
  code_impact: { label: 'Change impact', icon: () => <CodingPullRequestIcon size={24} /> },
1479
+ // A change set: pull requests across repositories declared as one change, with the ClickUp tasks and design
1480
+ // docs its pull requests are attached to. ONE card for it wherever a set is shown (chat, a design doc's page).
1481
+ change_set: { label: 'Change set', icon: () => <CodingMergeIcon size={24} /> },
1432
1482
  };
1433
1483
  function refGlyphRegistryEntries(): Record<string, ChatCardRegistryEntry> {
1434
1484
  return registryEntries(REF_GLYPH_CARD_CONFIGS, (cfg, docType) =>
@@ -1698,6 +1748,17 @@ const CHAT_CARD_REGISTRY: Record<string, ChatCardRegistryEntry> = {
1698
1748
  />
1699
1749
  ),
1700
1750
  },
1751
+ design_doc: {
1752
+ label: 'Design doc',
1753
+ contentRefType: 'design_doc',
1754
+ bareInline: true,
1755
+ noComposedHref: true,
1756
+ fallbackHref: (item: { url?: string | null }) => item?.url ?? null,
1757
+ skeleton: () => <MingoInfoCardSkeleton />,
1758
+ render: (item, chatRef, opts) => (
1759
+ <DesignDocChatCard item={item} chatRef={chatRef} isNewTab={opts.isNewTab} discuss={opts.discuss} />
1760
+ ),
1761
+ },
1701
1762
  customer_interview: {
1702
1763
  label: 'Customer interview',
1703
1764
  contentRefType: 'customer_interview',
@@ -69,6 +69,13 @@ export {
69
69
  type OnboardingGuideCardProps,
70
70
  } from './onboarding-guide-card';
71
71
  export { RoadmapCard, RoadmapCardSkeleton, type RoadmapCardProps, type VoteType } from './roadmap-card';
72
+ export {
73
+ DesignDocCard,
74
+ DesignDocCardSkeleton,
75
+ designDocMetaLine,
76
+ designDocPeople,
77
+ type DesignDocCardProps,
78
+ } from './design-doc-card';
72
79
  export { RoadmapVoteButton, type RoadmapVoteButtonProps } from './roadmap-vote-button';
73
80
  export { TaskTypeIcon, type TaskTypeIconProps } from './task-type-icon';
74
81
  export {
@@ -14,7 +14,8 @@ import {
14
14
  type TrustCenterPublic,
15
15
  } from '../../../types/trust-center';
16
16
  import { TRUST_CENTER_FIXTURE_WINDOW_MS, makeTrustCenterData } from '../__fixtures__/trust-center';
17
- import { TrustCenterPage } from '../trust-center-page';
17
+ import { TrustCenterPage, TrustCenterPageSkeleton } from '../trust-center-page';
18
+ import { TRUST_SECTION_LEADS } from '../trust-center-sections';
18
19
 
19
20
  // The real ContactForm needs the endpoints + chat runtimes; the page only
20
21
  // decides WHAT it is handed, so a stub that echoes its props is the honest
@@ -114,6 +115,32 @@ describe('TrustCenterPage', () => {
114
115
  expect(String(fetchMock.mock.calls[0][0])).toBe('/content/api/trust-center');
115
116
  });
116
117
 
118
+ it('while it loads, the skeleton shows the real section headings and leads the content will fill', async () => {
119
+ let answer: (response: Response) => void = () => {};
120
+ fetchMock.mockReturnValueOnce(new Promise<Response>(resolve => (answer = resolve)));
121
+ render(<TrustCenterPage endpoint="/content/api/trust-center" />);
122
+ const loading = await screen.findByLabelText('Loading the trust center');
123
+ for (const [label, lead] of [
124
+ ['Compliance', TRUST_SECTION_LEADS.compliance],
125
+ ['Controls', TRUST_SECTION_LEADS.controls],
126
+ ['Documents', TRUST_SECTION_LEADS.documents],
127
+ ] as const) {
128
+ expect(within(loading).getByRole('heading', { level: 2, name: label, hidden: true })).toBeInTheDocument();
129
+ expect(within(loading).getByText(lead as string)).toBeInTheDocument();
130
+ }
131
+ act(() => answer(jsonResponse(makeData())));
132
+ expect(await screen.findByText('SOC 2 Type II')).toBeInTheDocument();
133
+ expect(screen.queryByLabelText('Loading the trust center')).toBeNull();
134
+ });
135
+
136
+ it('TrustCenterPageSkeleton wears the page chrome it is given: no title and subtitle means no header at all', () => {
137
+ const { rerender } = render(<TrustCenterPageSkeleton shell={false} title="" subtitle="" />);
138
+ expect(screen.queryByRole('heading', { level: 1 })).toBeNull();
139
+ expect(screen.getByLabelText('Loading the trust center')).toBeInTheDocument();
140
+ rerender(<TrustCenterPageSkeleton shell={false} />);
141
+ expect(screen.getByRole('heading', { level: 1, name: TRUST_CENTER_TITLE })).toBeInTheDocument();
142
+ });
143
+
117
144
  it('is ONE page: every section is an anchored h2, in reading order — AI & data use after Subprocessors, Contact before FAQ', () => {
118
145
  render(<TrustCenterPage initialData={makeData()} />);
119
146
  const headings = screen.getAllByRole('heading', { level: 2 }).map(h => h.textContent);
@@ -21,7 +21,7 @@ export { RoadmapPage, type RoadmapPageProps } from './roadmap-page';
21
21
  export { ProductReleasesListPage, type ProductReleasesListPageProps } from './product-releases-list-page';
22
22
  export { DeliveryPage, type DeliveryPageProps } from './delivery-page';
23
23
  export { OnboardingGuidesCatalogPage, type OnboardingGuidesCatalogPageProps } from './onboarding-guides-catalog-page';
24
- export { TrustCenterPage, type TrustCenterPageProps } from './trust-center-page';
24
+ export { TrustCenterPage, TrustCenterPageSkeleton, type TrustCenterPageProps } from './trust-center-page';
25
25
 
26
26
  // Existing full-pages re-exported for a single Help Center import site.
27
27
  export { FaqDocumentPage, type FaqDocumentPageProps } from '../faq';
@@ -61,6 +61,10 @@ import {
61
61
  DocumentRequestModal,
62
62
  DocumentsSection,
63
63
  SubprocessorsSection,
64
+ TRUST_BODY_GRID_CLASS,
65
+ TRUST_SECTIONS_COLUMN_CLASS,
66
+ TRUST_SECTION_LEADS,
67
+ TRUST_STATUS_ROW_CLASS,
64
68
  TrustCenterSkeleton,
65
69
  TrustSection,
66
70
  } from './trust-center-sections';
@@ -146,7 +150,6 @@ export function TrustCenterPage({
146
150
  title = TRUST_CENTER_TITLE,
147
151
  subtitle = TRUST_CENTER_TAGLINE,
148
152
  }: TrustCenterPageProps) {
149
- const router = useRouter();
150
153
  const { data, isLoading, error, reload } = useSelfFetch<TrustCenterPublic>(endpoint, {
151
154
  initialData,
152
155
  revalidateOnVisibleAfterMs: TRUST_CENTER_CACHE_SECONDS * 1000,
@@ -191,11 +194,6 @@ export function TrustCenterPage({
191
194
  const openRequest = useCallback((documentTitle: string | null) => setRequest({ open: true, documentTitle }), []);
192
195
  const closeRequest = useCallback(() => setRequest(current => ({ ...current, open: false })), []);
193
196
 
194
- const backCfg =
195
- backButton === false
196
- ? undefined
197
- : { label: backButton.label ?? 'Back to home', onClick: () => router.push(backButton.href ?? '/') };
198
-
199
197
  const hasGatedDocuments = data?.documents.some(document => document.access === 'request') ?? false;
200
198
  const actions = hasGatedDocuments
201
199
  ? [{ label: 'Request access', variant: 'accent' as const, onClick: () => openRequest(null) }]
@@ -205,19 +203,19 @@ export function TrustCenterPage({
205
203
  // sections show comes from `visibleSections` — this map only says what each renders.
206
204
  const views: Record<TrustCenterSectionId, TrustSectionView> = {
207
205
  ai: {
208
- lead: 'How customer data is handled by the AI in our products.',
206
+ lead: TRUST_SECTION_LEADS.ai,
209
207
  render: d => <AiSection practices={d.aiPractices} />,
210
208
  },
211
209
  compliance: {
212
- lead: 'Frameworks we are audited against, and what is next.',
210
+ lead: TRUST_SECTION_LEADS.compliance,
213
211
  render: d => <ComplianceSection frameworks={d.frameworks} />,
214
212
  },
215
213
  controls: {
216
- lead: 'Security controls currently passing in continuous monitoring (via Vanta).',
214
+ lead: TRUST_SECTION_LEADS.controls,
217
215
  render: d => <ControlsSection endpoint={endpoint} seed={controlsSeed(d)} />,
218
216
  },
219
217
  documents: {
220
- lead: 'Public documents open directly. Gated documents are shared under NDA after one short request.',
218
+ lead: TRUST_SECTION_LEADS.documents,
221
219
  render: d => (
222
220
  <DocumentsSection
223
221
  documents={d.documents}
@@ -227,7 +225,7 @@ export function TrustCenterPage({
227
225
  ),
228
226
  },
229
227
  subprocessors: {
230
- lead: 'Third parties that process customer data on our behalf.',
228
+ lead: TRUST_SECTION_LEADS.subprocessors,
231
229
  render: d => <SubprocessorsSection subprocessors={d.subprocessors} />,
232
230
  },
233
231
  faq: { render: d => <FaqSection initialFaqs={d.faqs} heading={null} /> },
@@ -243,7 +241,7 @@ export function TrustCenterPage({
243
241
  const status = monitoringStatus(data, hydrated, nowMs);
244
242
  body = (
245
243
  <>
246
- <div className="flex flex-col gap-[var(--spacing-system-xs)] md:flex-row md:items-center md:justify-between">
244
+ <div className={TRUST_STATUS_ROW_CLASS}>
247
245
  <StatusIndicator status={status.status} label={status.label} />
248
246
  {data.connected ? (
249
247
  <DataAttribution
@@ -263,8 +261,8 @@ export function TrustCenterPage({
263
261
  ) : null}
264
262
  </div>
265
263
 
266
- <div className="grid grid-cols-1 gap-[var(--spacing-system-xl)] lg:grid-cols-[minmax(0,1fr)_12rem]">
267
- <div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxl)]">
264
+ <div className={TRUST_BODY_GRID_CLASS}>
265
+ <div className={TRUST_SECTIONS_COLUMN_CLASS}>
268
266
  {sections.map(section => {
269
267
  const view = views[section.id];
270
268
  return (
@@ -296,6 +294,34 @@ export function TrustCenterPage({
296
294
  );
297
295
  }
298
296
 
297
+ return (
298
+ <TrustCenterChrome shell={shell} title={title} subtitle={subtitle} backButton={backButton} actions={actions}>
299
+ {body}
300
+ </TrustCenterChrome>
301
+ );
302
+ }
303
+
304
+ /**
305
+ * The page's chrome, ONE copy for the page and its skeleton: the standalone
306
+ * `PageShell` or the host's padded box (`shell`), and the frozen `PageLayout`
307
+ * header (title, subtitle, back button, actions).
308
+ */
309
+ function TrustCenterChrome({
310
+ shell,
311
+ title,
312
+ subtitle,
313
+ backButton,
314
+ actions,
315
+ children,
316
+ }: Required<Pick<TrustCenterPageProps, 'shell' | 'title' | 'subtitle' | 'backButton'>> & {
317
+ actions?: Array<{ label: string; variant: 'accent'; onClick: () => void }>;
318
+ children: ReactNode;
319
+ }) {
320
+ const router = useRouter();
321
+ const backCfg =
322
+ backButton === false
323
+ ? undefined
324
+ : { label: backButton.label ?? 'Back to home', onClick: () => router.push(backButton.href ?? '/') };
299
325
  const inner = (
300
326
  <PageLayout
301
327
  title={title}
@@ -306,9 +332,26 @@ export function TrustCenterPage({
306
332
  actions={actions}
307
333
  actionsVariant="primary-buttons"
308
334
  >
309
- {body}
335
+ {children}
310
336
  </PageLayout>
311
337
  );
312
-
313
338
  return shell ? <PageShell>{inner}</PageShell> : <div className="page-shell-content">{inner}</div>;
314
339
  }
340
+
341
+ /**
342
+ * The whole page while a host loads the data itself (the hub's admin preview):
343
+ * the SAME chrome and the same body skeleton `TrustCenterPage` shows while it
344
+ * self-fetches. Pass the chrome props the page will get.
345
+ */
346
+ export function TrustCenterPageSkeleton({
347
+ shell = true,
348
+ backButton = false,
349
+ title = TRUST_CENTER_TITLE,
350
+ subtitle = TRUST_CENTER_TAGLINE,
351
+ }: Pick<TrustCenterPageProps, 'shell' | 'backButton' | 'title' | 'subtitle'>) {
352
+ return (
353
+ <TrustCenterChrome shell={shell} title={title} subtitle={subtitle} backButton={backButton}>
354
+ <TrustCenterSkeleton />
355
+ </TrustCenterChrome>
356
+ );
357
+ }