@flamingo-stack/openframe-frontend-core 0.0.668 → 0.0.669

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.
@@ -12,7 +12,17 @@
12
12
  * server answers as one flat, grouped, counted list.
13
13
  */
14
14
  import { type ReactNode } from 'react';
15
- import { type TrustCenterAiPractice, type TrustCenterControlsPage, type TrustCenterDocument, type TrustCenterFramework, type TrustCenterSubprocessor } from '../../types/trust-center';
15
+ import { type TrustCenterAiPractice, type TrustCenterControlsPage, type TrustCenterDocument, type TrustCenterFramework, type TrustCenterSectionId, type TrustCenterSubprocessor } from '../../types/trust-center';
16
+ /**
17
+ * The page body's layout, ONE copy for the loaded page and its skeleton, so the
18
+ * skeleton cannot drift from what replaces it: the status row, then the sections
19
+ * beside the section rail.
20
+ */
21
+ export declare const TRUST_STATUS_ROW_CLASS = "flex flex-col gap-[var(--spacing-system-xs)] md:flex-row md:items-center md:justify-between";
22
+ export declare const TRUST_BODY_GRID_CLASS = "grid grid-cols-1 gap-[var(--spacing-system-xl)] lg:grid-cols-[minmax(0,1fr)_12rem]";
23
+ export declare const TRUST_SECTIONS_COLUMN_CLASS = "flex min-w-0 flex-col gap-[var(--spacing-system-xxl)]";
24
+ /** Each section's one-line lead under its heading (the page and its skeleton read the same copy). */
25
+ export declare const TRUST_SECTION_LEADS: Partial<Record<TrustCenterSectionId, string>>;
16
26
  /** One anchored page section: an `h2` + optional lead line + content. */
17
27
  export declare function TrustSection({ id, title, lead, children, }: {
18
28
  id: string;
@@ -84,6 +94,11 @@ export declare function SubprocessorsSection({ subprocessors }: {
84
94
  export declare function ContactSection({ onContact }: {
85
95
  onContact: () => void;
86
96
  }): import("react").JSX.Element;
87
- /** Mirrors the page: a card row, then the controls grid. */
97
+ /**
98
+ * The page BODY while the trust center loads: built on the loaded body's own
99
+ * layout classes and the real section headings and panels, so nothing moves when
100
+ * the content replaces it. The status row, then Compliance, Controls and
101
+ * Documents (what fills the first screens), beside the section rail.
102
+ */
88
103
  export declare function TrustCenterSkeleton(): import("react").JSX.Element;
89
104
  //# sourceMappingURL=trust-center-sections.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"trust-center-sections.d.ts","sourceRoot":"","sources":["../../../src/components/help-center-pages/trust-center-sections.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAA2B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGhE,OAAO,EAML,KAAK,qBAAqB,EAE1B,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAE7B,MAAM,0BAA0B,CAAC;AA4ClC,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,EAC3B,EAAE,EACF,KAAK,EACL,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,SAAS,CAAC;CACrB,+BAkBA;AAMD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,qBAAqB,EAAE,CAAA;CAAE,+BAkB9E;AAMD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,oBAAoB,EAAE,CAAA;CAAE,+BA0BvF;AAkED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,uBAAuB,CAAA;CAAE,+BAkFtG;AAMD,wBAAgB,gBAAgB,CAAC,EAC/B,SAAS,EACT,WAAW,EACX,SAAS,GACV,EAAE;IACD,SAAS,EAAE,mBAAmB,EAAE,CAAC;IACjC,kGAAkG;IAClG,WAAW,EAAE,CAAC,QAAQ,EAAE,mBAAmB,KAAK,MAAM,GAAG,IAAI,CAAC;IAC9D,oEAAoE;IACpE,SAAS,EAAE,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CACnD,+BA0CA;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,IAAI,EACJ,aAAa,EACb,QAAQ,EACR,OAAO,GACR,EAAE;IACD,IAAI,EAAE,OAAO,CAAC;IACd,6GAA6G;IAC7G,QAAQ,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,+BAgCA;AA8BD,wBAAgB,oBAAoB,CAAC,EAAE,aAAa,EAAE,EAAE;IAAE,aAAa,EAAE,uBAAuB,EAAE,CAAA;CAAE,+BA4BnG;AAMD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,IAAI,CAAA;CAAE,+BA8BtE;AAED,4DAA4D;AAC5D,wBAAgB,mBAAmB,gCAOlC"}
1
+ {"version":3,"file":"trust-center-sections.d.ts","sourceRoot":"","sources":["../../../src/components/help-center-pages/trust-center-sections.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAA2B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGhE,OAAO,EAOL,KAAK,qBAAqB,EAE1B,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAE7B,MAAM,0BAA0B,CAAC;AAiClC;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,gGAC4D,CAAC;AAChG,eAAO,MAAM,qBAAqB,uFACoD,CAAC;AACvF,eAAO,MAAM,2BAA2B,0DAA0D,CAAC;AAEnG,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,EAAE,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAM7E,CAAC;AAWF,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,EAC3B,EAAE,EACF,KAAK,EACL,IAAI,EACJ,QAAQ,GACT,EAAE;IACD,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,SAAS,CAAC;CACrB,+BAkBA;AAMD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,qBAAqB,EAAE,CAAA;CAAE,+BAkB9E;AAMD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,UAAU,EAAE,EAAE;IAAE,UAAU,EAAE,oBAAoB,EAAE,CAAA;CAAE,+BA0BvF;AAkED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,uBAAuB,CAAA;CAAE,+BAkFtG;AAMD,wBAAgB,gBAAgB,CAAC,EAC/B,SAAS,EACT,WAAW,EACX,SAAS,GACV,EAAE;IACD,SAAS,EAAE,mBAAmB,EAAE,CAAC;IACjC,kGAAkG;IAClG,WAAW,EAAE,CAAC,QAAQ,EAAE,mBAAmB,KAAK,MAAM,GAAG,IAAI,CAAC;IAC9D,oEAAoE;IACpE,SAAS,EAAE,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CACnD,+BA0CA;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,IAAI,EACJ,aAAa,EACb,QAAQ,EACR,OAAO,GACR,EAAE;IACD,IAAI,EAAE,OAAO,CAAC;IACd,6GAA6G;IAC7G,QAAQ,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,+BAgCA;AA8BD,wBAAgB,oBAAoB,CAAC,EAAE,aAAa,EAAE,EAAE;IAAE,aAAa,EAAE,uBAAuB,EAAE,CAAA;CAAE,+BA4BnG;AAMD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,IAAI,CAAA;CAAE,+BA8BtE;AAoGD;;;;;GAKG;AACH,wBAAgB,mBAAmB,gCAmClC"}
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",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
+ }
@@ -19,6 +19,7 @@ import { useDebounce } from '../../hooks/ui/use-debounce';
19
19
  import { useSelfFetch } from '../../hooks/use-self-fetch';
20
20
  import {
21
21
  TRUST_CENTER_SEARCH_DEBOUNCE_MS,
22
+ TRUST_CENTER_SECTIONS,
22
23
  normalizeTrustControlQuery,
23
24
  trustCenterControlsUrl,
24
25
  trustFrameworkBadge,
@@ -28,6 +29,7 @@ import {
28
29
  type TrustCenterControlsPage,
29
30
  type TrustCenterDocument,
30
31
  type TrustCenterFramework,
32
+ type TrustCenterSectionId,
31
33
  type TrustCenterSubprocessor,
32
34
  type TrustFrameworkMonitoringEntry,
33
35
  } from '../../types/trust-center';
@@ -43,8 +45,7 @@ import { ShieldCheckIcon } from '../icons-v2-generated/security/shield-check-ico
43
45
  import { CheckIcon } from '../icons-v2-generated/signs-and-symbols/check-icon';
44
46
  import { SECTION_HEADING_CLASS } from '../layout/page-heading';
45
47
  import { ListEmptyState } from '../list-empty-state';
46
- import { CardSkeletonGrid } from '../loading/card-skeleton';
47
- import { TextSkeleton } from '../loading/unified-skeleton';
48
+ import { TextSkeleton, UnifiedSkeleton } from '../loading/unified-skeleton';
48
49
  import { Button } from '../ui/button/button';
49
50
  import { ComplianceLogo } from '../ui/compliance-badge';
50
51
  import { EntityImage } from '../ui/entity-image';
@@ -62,8 +63,27 @@ const FRAMEWORK_ICONS: Record<TrustFrameworkMonitoringEntry['icon'], typeof Shie
62
63
  'file-shield': FileShieldIcon,
63
64
  };
64
65
 
65
- const STACK_CLASSES = 'flex flex-col gap-[var(--spacing-system-l)]';
66
66
  const BODY_TEXT = 'text-h6 text-ods-text-secondary';
67
+
68
+ /**
69
+ * The page body's layout, ONE copy for the loaded page and its skeleton, so the
70
+ * skeleton cannot drift from what replaces it: the status row, then the sections
71
+ * beside the section rail.
72
+ */
73
+ export const TRUST_STATUS_ROW_CLASS =
74
+ 'flex flex-col gap-[var(--spacing-system-xs)] md:flex-row md:items-center md:justify-between';
75
+ export const TRUST_BODY_GRID_CLASS =
76
+ 'grid grid-cols-1 gap-[var(--spacing-system-xl)] lg:grid-cols-[minmax(0,1fr)_12rem]';
77
+ export const TRUST_SECTIONS_COLUMN_CLASS = 'flex min-w-0 flex-col gap-[var(--spacing-system-xxl)]';
78
+
79
+ /** Each section's one-line lead under its heading (the page and its skeleton read the same copy). */
80
+ export const TRUST_SECTION_LEADS: Partial<Record<TrustCenterSectionId, string>> = {
81
+ compliance: 'Frameworks we are audited against, and what is next.',
82
+ controls: 'Security controls currently passing in continuous monitoring (via Vanta).',
83
+ documents: 'Public documents open directly. Gated documents are shared under NDA after one short request.',
84
+ subprocessors: 'Third parties that process customer data on our behalf.',
85
+ ai: 'How customer data is handled by the AI in our products.',
86
+ };
67
87
  /**
68
88
  * The right-hand action / status column: ONE width from tablet up, so every
69
89
  * panel's rows line up; on phones it hugs its content so the title keeps the room.
@@ -540,12 +560,143 @@ export function ContactSection({ onContact }: { onContact: () => void }) {
540
560
  );
541
561
  }
542
562
 
543
- /** Mirrors the page: a card row, then the controls grid. */
544
- export function TrustCenterSkeleton() {
563
+ // ---------------------------------------------------------------------------
564
+ // Skeleton
565
+ // ---------------------------------------------------------------------------
566
+
567
+ /** A section's heading and lead, exactly as `TrustSection` renders them (real text: it does not load). */
568
+ function SkeletonSection({ id, children }: { id: TrustCenterSectionId; children: ReactNode }) {
569
+ const label = TRUST_CENTER_SECTIONS.find(section => section.id === id)?.label ?? id;
570
+ const lead = TRUST_SECTION_LEADS[id];
545
571
  return (
546
- <div className={STACK_CLASSES}>
547
- <CardSkeletonGrid count={2} variant="category" />
548
- <CardSkeletonGrid count={4} variant="category" />
572
+ <section aria-hidden="true" className="flex flex-col gap-[var(--spacing-system-m)]">
573
+ <div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
574
+ <h2 className={SECTION_HEADING_CLASS}>{label}</h2>
575
+ {lead ? <p className={BODY_TEXT}>{lead}</p> : null}
576
+ </div>
577
+ {children}
578
+ </section>
579
+ );
580
+ }
581
+
582
+ /**
583
+ * Rows of the REAL `StackedRowsPanel` (its own row heights and borders) whose
584
+ * cells hold bars: a mark (a 40px compliance logo or a 24px glyph), a value over
585
+ * a label, and the right-hand badge or button in the page's `ACTION_COLUMN`.
586
+ */
587
+ function skeletonRows(count: number, shape: { mark: 'logo' | 'glyph'; action: 'badge' | 'button' }): PanelRow[] {
588
+ return Array.from({ length: count }, (_, index) => ({
589
+ id: `skeleton-${index}`,
590
+ columns: [
591
+ {
592
+ key: 'item',
593
+ content: (
594
+ <div className="flex w-full items-center gap-[var(--spacing-system-s)]">
595
+ <UnifiedSkeleton
596
+ className={shape.mark === 'logo' ? 'size-10 shrink-0 rounded-full' : 'size-6 shrink-0 rounded'}
597
+ />
598
+ <div className="flex min-w-0 flex-1 flex-col gap-[var(--spacing-system-xxs)]">
599
+ <UnifiedSkeleton variant="text" className="h-5 w-1/3 md:h-6" />
600
+ <TextSkeleton.Body className="w-1/2" />
601
+ </div>
602
+ </div>
603
+ ),
604
+ },
605
+ {
606
+ key: 'action',
607
+ width: ACTION_COLUMN,
608
+ align: 'right',
609
+ content:
610
+ shape.action === 'badge' ? (
611
+ <UnifiedSkeleton className="h-4 w-28 rounded" />
612
+ ) : (
613
+ <UnifiedSkeleton className="h-6 w-16 rounded md:h-8" />
614
+ ),
615
+ },
616
+ ],
617
+ }));
618
+ }
619
+
620
+ /** The Controls section while loading: its tab strip, its summary and search, and the list box's own skeleton. */
621
+ function ControlsSectionSkeleton() {
622
+ return (
623
+ <div className="flex flex-col gap-[var(--spacing-system-m)]">
624
+ <div className="flex items-center gap-[var(--spacing-system-xxs)] overflow-hidden">
625
+ {[0, 1, 2, 3].map(index => (
626
+ <div key={index} className="shrink-0 p-[var(--spacing-system-m)]">
627
+ <UnifiedSkeleton variant="text" className="h-5 w-36 md:h-6" />
628
+ </div>
629
+ ))}
630
+ </div>
631
+ <div className="flex flex-col gap-[var(--spacing-system-s)] md:flex-row md:items-center md:justify-between">
632
+ <TextSkeleton.Body className="w-48" />
633
+ <UnifiedSkeleton className="h-11 w-full rounded-[6px] md:h-12 md:w-80" />
634
+ </div>
635
+ <div className={CONTROLS_LIST_HEIGHT}>
636
+ <ControlsListSkeleton />
637
+ </div>
638
+ </div>
639
+ );
640
+ }
641
+
642
+ /** The section rail while loading: `StickySectionNav`'s line and its 40px items. */
643
+ function SectionRailSkeleton() {
644
+ return (
645
+ <div className="sticky" style={{ top: STICKY_HEADER_OFFSET_PX }}>
646
+ <div className="relative">
647
+ <div
648
+ className="absolute -left-0.5 top-0 w-px bg-ods-border"
649
+ style={{ height: TRUST_CENTER_SECTIONS.length * 40 }}
650
+ />
651
+ {TRUST_CENTER_SECTIONS.map(section => (
652
+ <div key={section.id} className="flex h-10 items-center px-3 py-2">
653
+ <TextSkeleton.Body className="w-24" />
654
+ </div>
655
+ ))}
656
+ </div>
549
657
  </div>
550
658
  );
551
659
  }
660
+
661
+ /**
662
+ * The page BODY while the trust center loads: built on the loaded body's own
663
+ * layout classes and the real section headings and panels, so nothing moves when
664
+ * the content replaces it. The status row, then Compliance, Controls and
665
+ * Documents (what fills the first screens), beside the section rail.
666
+ */
667
+ export function TrustCenterSkeleton() {
668
+ return (
669
+ <>
670
+ <div className={TRUST_STATUS_ROW_CLASS} aria-hidden="true">
671
+ <div className="flex items-center gap-1.5">
672
+ <UnifiedSkeleton className="size-3 rounded-full" />
673
+ <TextSkeleton.Body className="w-44" />
674
+ </div>
675
+ {/* `DataAttribution`'s own layout: two lines below `sm`, one row above it. */}
676
+ <div className="flex shrink-0 flex-col gap-[var(--spacing-system-xxs)] sm:flex-row sm:items-center sm:gap-[var(--spacing-system-sf)]">
677
+ <div className="flex items-center gap-[var(--spacing-system-xsf)]">
678
+ <UnifiedSkeleton className="size-4 rounded" />
679
+ <TextSkeleton.Body className="w-36" />
680
+ </div>
681
+ <TextSkeleton.Body className="w-44" />
682
+ </div>
683
+ </div>
684
+ <div className={TRUST_BODY_GRID_CLASS} aria-busy="true" aria-label="Loading the trust center">
685
+ <div className={TRUST_SECTIONS_COLUMN_CLASS}>
686
+ <SkeletonSection id="compliance">
687
+ <StackedRowsPanel rows={skeletonRows(3, { mark: 'logo', action: 'badge' })} />
688
+ </SkeletonSection>
689
+ <SkeletonSection id="controls">
690
+ <ControlsSectionSkeleton />
691
+ </SkeletonSection>
692
+ <SkeletonSection id="documents">
693
+ <StackedRowsPanel rows={skeletonRows(2, { mark: 'glyph', action: 'button' })} />
694
+ </SkeletonSection>
695
+ </div>
696
+ <aside className="hidden lg:block" aria-hidden="true">
697
+ <SectionRailSkeleton />
698
+ </aside>
699
+ </div>
700
+ </>
701
+ );
702
+ }