@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.
- package/dist/components/help-center-pages/index.cjs +127 -19
- package/dist/components/help-center-pages/index.cjs.map +1 -1
- package/dist/components/help-center-pages/index.d.ts +1 -1
- package/dist/components/help-center-pages/index.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.js +128 -20
- package/dist/components/help-center-pages/index.js.map +1 -1
- package/dist/components/help-center-pages/trust-center-page.d.ts +6 -0
- package/dist/components/help-center-pages/trust-center-page.d.ts.map +1 -1
- package/dist/components/help-center-pages/trust-center-sections.d.ts +17 -2
- package/dist/components/help-center-pages/trust-center-sections.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/help-center-pages/__tests__/trust-center-page.test.tsx +28 -1
- package/src/components/help-center-pages/index.ts +1 -1
- package/src/components/help-center-pages/trust-center-page.tsx +59 -16
- package/src/components/help-center-pages/trust-center-sections.tsx +159 -8
|
@@ -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
|
-
/**
|
|
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,
|
|
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
|
@@ -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:
|
|
206
|
+
lead: TRUST_SECTION_LEADS.ai,
|
|
209
207
|
render: d => <AiSection practices={d.aiPractices} />,
|
|
210
208
|
},
|
|
211
209
|
compliance: {
|
|
212
|
-
lead:
|
|
210
|
+
lead: TRUST_SECTION_LEADS.compliance,
|
|
213
211
|
render: d => <ComplianceSection frameworks={d.frameworks} />,
|
|
214
212
|
},
|
|
215
213
|
controls: {
|
|
216
|
-
lead:
|
|
214
|
+
lead: TRUST_SECTION_LEADS.controls,
|
|
217
215
|
render: d => <ControlsSection endpoint={endpoint} seed={controlsSeed(d)} />,
|
|
218
216
|
},
|
|
219
217
|
documents: {
|
|
220
|
-
lead:
|
|
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:
|
|
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=
|
|
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=
|
|
267
|
-
<div className=
|
|
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
|
-
{
|
|
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 {
|
|
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
|
-
|
|
544
|
-
|
|
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
|
-
<
|
|
547
|
-
<
|
|
548
|
-
|
|
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
|
+
}
|