From defd94e2c8608a8ad41f6969303470f0ec1029c5 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 16:31:18 -0400 Subject: [PATCH 01/34] feat(trust): add pure isTrustPortalConfigured detection helper Co-Authored-By: Claude Sonnet 4.6 --- .../is-trust-portal-configured.spec.ts | 41 +++++++++++++++++++ .../is-trust-portal-configured.ts | 36 ++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 apps/api/src/trust-portal/is-trust-portal-configured.spec.ts create mode 100644 apps/api/src/trust-portal/is-trust-portal-configured.ts diff --git a/apps/api/src/trust-portal/is-trust-portal-configured.spec.ts b/apps/api/src/trust-portal/is-trust-portal-configured.spec.ts new file mode 100644 index 000000000..9a76fc537 --- /dev/null +++ b/apps/api/src/trust-portal/is-trust-portal-configured.spec.ts @@ -0,0 +1,41 @@ +import { isTrustPortalConfigured } from './is-trust-portal-configured'; + +const DEFAULTS = { + domain: null, + contactEmail: null, + overviewContent: null, + favicon: null, + faqs: null, + frameworkFlags: [false, false, false], + documentCount: 0, + resourceCount: 0, + customLinkCount: 0, +}; + +describe('isTrustPortalConfigured', () => { + it('returns false for a fresh portal on all defaults', () => { + expect(isTrustPortalConfigured(DEFAULTS)).toBe(false); + }); + + it('returns false when faqs is an empty array', () => { + expect(isTrustPortalConfigured({ ...DEFAULTS, faqs: [] })).toBe(false); + }); + + it.each([ + ['domain', { domain: 'trust.acme.com' }], + ['contactEmail', { contactEmail: 'security@acme.com' }], + ['overviewContent', { overviewContent: 'We are secure.' }], + ['favicon', { favicon: 'org/favicon.png' }], + ['faqs', { faqs: [{ question: 'q', answer: 'a', order: 0 }] }], + ['a framework flag', { frameworkFlags: [false, true, false] }], + ['a document', { documentCount: 1 }], + ['a framework cert', { resourceCount: 1 }], + ['a custom link', { customLinkCount: 1 }], + ])('returns true when %s is set', (_label, override) => { + expect(isTrustPortalConfigured({ ...DEFAULTS, ...override })).toBe(true); + }); + + it('ignores non-array faqs values', () => { + expect(isTrustPortalConfigured({ ...DEFAULTS, faqs: 'not-an-array' })).toBe(false); + }); +}); diff --git a/apps/api/src/trust-portal/is-trust-portal-configured.ts b/apps/api/src/trust-portal/is-trust-portal-configured.ts new file mode 100644 index 000000000..375049b82 --- /dev/null +++ b/apps/api/src/trust-portal/is-trust-portal-configured.ts @@ -0,0 +1,36 @@ +export interface TrustPortalConfiguredInput { + domain?: string | null; + contactEmail?: string | null; + overviewContent?: string | null; + favicon?: string | null; + /** Organization.trustPortalFaqs — Json?, expected to be an array when set. */ + faqs?: unknown; + /** Raw Trust framework boolean columns (soc2, soc2type1, … ccpa). */ + frameworkFlags: boolean[]; + documentCount: number; + resourceCount: number; + customLinkCount: number; +} + +/** + * A Trust Portal is "configured" once the org has done anything beyond the + * shared-domain defaults. Used to decide whether to nudge the customer to set + * it up. Computed from RAW values (the settings endpoint substitutes a Context + * Hub default for overviewContent — do not pass the substituted value here). + */ +export function isTrustPortalConfigured(input: TrustPortalConfiguredInput): boolean { + const hasFaqs = Array.isArray(input.faqs) && input.faqs.length > 0; + const hasFramework = input.frameworkFlags.some(Boolean); + + return Boolean( + input.domain || + input.contactEmail || + input.overviewContent || + input.favicon || + hasFaqs || + hasFramework || + input.documentCount > 0 || + input.resourceCount > 0 || + input.customLinkCount > 0, + ); +} From 3d194f8c28270786e58c20d579980d65028cda75 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 16:33:43 -0400 Subject: [PATCH 02/34] refactor(trust): clarify framework-flags contract and test labels --- .../src/trust-portal/is-trust-portal-configured.spec.ts | 6 +++++- apps/api/src/trust-portal/is-trust-portal-configured.ts | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/api/src/trust-portal/is-trust-portal-configured.spec.ts b/apps/api/src/trust-portal/is-trust-portal-configured.spec.ts index 9a76fc537..b0fff76e5 100644 --- a/apps/api/src/trust-portal/is-trust-portal-configured.spec.ts +++ b/apps/api/src/trust-portal/is-trust-portal-configured.spec.ts @@ -29,7 +29,7 @@ describe('isTrustPortalConfigured', () => { ['faqs', { faqs: [{ question: 'q', answer: 'a', order: 0 }] }], ['a framework flag', { frameworkFlags: [false, true, false] }], ['a document', { documentCount: 1 }], - ['a framework cert', { resourceCount: 1 }], + ['a compliance resource (certificate)', { resourceCount: 1 }], ['a custom link', { customLinkCount: 1 }], ])('returns true when %s is set', (_label, override) => { expect(isTrustPortalConfigured({ ...DEFAULTS, ...override })).toBe(true); @@ -38,4 +38,8 @@ describe('isTrustPortalConfigured', () => { it('ignores non-array faqs values', () => { expect(isTrustPortalConfigured({ ...DEFAULTS, faqs: 'not-an-array' })).toBe(false); }); + + it('returns false when frameworkFlags is an empty array', () => { + expect(isTrustPortalConfigured({ ...DEFAULTS, frameworkFlags: [] })).toBe(false); + }); }); diff --git a/apps/api/src/trust-portal/is-trust-portal-configured.ts b/apps/api/src/trust-portal/is-trust-portal-configured.ts index 375049b82..cd5c06ccb 100644 --- a/apps/api/src/trust-portal/is-trust-portal-configured.ts +++ b/apps/api/src/trust-portal/is-trust-portal-configured.ts @@ -5,7 +5,13 @@ export interface TrustPortalConfiguredInput { favicon?: string | null; /** Organization.trustPortalFaqs — Json?, expected to be an array when set. */ faqs?: unknown; - /** Raw Trust framework boolean columns (soc2, soc2type1, … ccpa). */ + /** + * Raw Trust framework "enabled" boolean columns (soc2, soc2type1, soc2type2, + * soc3, iso27001, iso42001, nen7510, gdpr, hipaa, pci_dss, iso9001, pipeda, + * ccpa). Order is irrelevant — any `true` counts as configured. The caller is + * responsible for passing all of them; a dropped column silently weakens the + * signal. Distinct from `resourceCount` (uploaded certificate files). + */ frameworkFlags: boolean[]; documentCount: number; resourceCount: number; From 593f2ddc6d76c4f34fdf993ad8b007e4486a58ad Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 16:38:06 -0400 Subject: [PATCH 03/34] feat(trust): expose derived isConfigured on trust-portal settings --- .../src/trust-portal/trust-portal.service.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/apps/api/src/trust-portal/trust-portal.service.ts b/apps/api/src/trust-portal/trust-portal.service.ts index f15e1a0dc..1cf3232ae 100644 --- a/apps/api/src/trust-portal/trust-portal.service.ts +++ b/apps/api/src/trust-portal/trust-portal.service.ts @@ -36,6 +36,7 @@ import { TrustDocumentUrlResponseDto, UploadTrustDocumentDto, } from './dto/trust-document.dto'; +import { isTrustPortalConfigured } from './is-trust-portal-configured'; interface VercelDomainVerification { type: string; @@ -1533,8 +1534,42 @@ export class TrustPortalService { defaultOverviewContent = missionContext?.answer ?? null; } + const [trustDocumentCount, trustResourceCount, trustCustomLinkCount] = + await Promise.all([ + db.trustDocument.count({ where: { organizationId } }), + db.trustResource.count({ where: { organizationId } }), + db.trustCustomLink.count({ where: { organizationId } }), + ]); + + const isConfigured = isTrustPortalConfigured({ + domain: trust.domain, + contactEmail: trust.contactEmail, + overviewContent: trust.overviewContent, // raw column, not the Context fallback + favicon: trust.favicon, + faqs: org.trustPortalFaqs, + frameworkFlags: [ + trust.soc2, + trust.soc2type1, + trust.soc2type2, + trust.soc3, + trust.iso27001, + trust.iso42001, + trust.nen7510, + trust.gdpr, + trust.hipaa, + trust.pci_dss, + trust.iso9001, + trust.pipeda, + trust.ccpa, + ], + documentCount: trustDocumentCount, + resourceCount: trustResourceCount, + customLinkCount: trustCustomLinkCount, + }); + return { enabled: trust.status === 'published', + isConfigured, friendlyUrl: trust.friendlyUrl, domain: trust.domain ?? '', domainVerified: trust.domainVerified ?? false, From 8ae9c85ed04a256cf88b8288262dfa1682d2fc1f Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 16:40:53 -0400 Subject: [PATCH 04/34] docs(trust): note legacy soc2 flag in isConfigured signal --- apps/api/src/trust-portal/trust-portal.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/trust-portal/trust-portal.service.ts b/apps/api/src/trust-portal/trust-portal.service.ts index 1cf3232ae..e89cb6b6d 100644 --- a/apps/api/src/trust-portal/trust-portal.service.ts +++ b/apps/api/src/trust-portal/trust-portal.service.ts @@ -1548,7 +1548,7 @@ export class TrustPortalService { favicon: trust.favicon, faqs: org.trustPortalFaqs, frameworkFlags: [ - trust.soc2, + trust.soc2, // legacy column; folded into soc2type2 in the response but still a "configured" signal trust.soc2type1, trust.soc2type2, trust.soc3, From 183f0aeeb4d3064154871478eb5b33564eac3c2c Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 16:43:09 -0400 Subject: [PATCH 05/34] feat(trust): add overview nudge host types --- .../(app)/[orgId]/overview/nudges/types.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 apps/app/src/app/(app)/[orgId]/overview/nudges/types.ts diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/types.ts b/apps/app/src/app/(app)/[orgId]/overview/nudges/types.ts new file mode 100644 index 000000000..f3d4c92c3 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/types.ts @@ -0,0 +1,27 @@ +import type { ReactNode } from 'react'; + +/** Server-resolved data the Overview page passes into the nudge host. */ +export interface ServerNudgeData { + trust: { + isTrustNdaEnabled: boolean; + isConfigured: boolean; + }; +} + +/** + * One candidate nudge. The host picks the lowest-`priority` candidate that is + * `ready && eligible && !dismissed` and renders it — at most one at a time. + */ +export interface NudgeState { + id: string; + /** Lower number wins (shown first). */ + priority: number; + /** true → dismissal persists in localStorage; false → session-only. */ + persistDismissal: boolean; + /** false while underlying data is still loading. */ + ready: boolean; + /** Has something to show AND the user is allowed to act on it. */ + eligible: boolean; + /** Renders the nudge UI; called once for the single visible nudge. */ + render: (onDismiss: () => void) => ReactNode; +} From c5591e717a693a9589935296f89d2c79ab8bd506 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 16:43:15 -0400 Subject: [PATCH 06/34] feat(trust): add offboarding nudge for overview host --- .../overview/nudges/OffboardingNudge.tsx | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx new file mode 100644 index 000000000..b786caf2f --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx @@ -0,0 +1,80 @@ +'use client'; + +import { useApiSWR } from '@/hooks/use-api-swr'; +import { Close, WarningAlt } from '@trycompai/design-system/icons'; +import Link from 'next/link'; +import { useParams } from 'next/navigation'; +import type { NudgeState } from './types'; + +interface PendingMember { + memberId: string; + name: string; +} + +interface PendingResponse { + members: PendingMember[]; +} + +export function useOffboardingNudge(): NudgeState { + const { orgId } = useParams<{ orgId: string }>(); + const { data, error } = useApiSWR( + '/v1/offboarding-checklist/pending', + ); + const members = data?.data?.members ?? []; + + return { + id: 'offboarding', + priority: 10, + persistDismissal: false, + ready: data !== undefined || error !== undefined, + eligible: !error && members.length > 0, + render: (onDismiss) => ( + + ), + }; +} + +function OffboardingNudgeView({ + orgId, + members, + onDismiss, +}: { + orgId: string; + members: PendingMember[]; + onDismiss: () => void; +}) { + const link = + members.length === 1 + ? `/${orgId}/people/${members[0].memberId}?tab=offboarding` + : `/${orgId}/people`; + + return ( +
+
+ + + + {members.length} employee{members.length !== 1 ? 's' : ''} + {' '} + require{members.length === 1 ? 's' : ''} offboarding completion + +
+
+ + View details + + +
+
+ ); +} From 88e39144d8a6dbb53e6d577101e64141a636d4a3 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 16:43:22 -0400 Subject: [PATCH 07/34] feat(trust): add trust portal setup nudge --- .../overview/nudges/TrustPortalSetupNudge.tsx | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx new file mode 100644 index 000000000..5aa7fe58a --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { usePermissions } from '@/hooks/use-permissions'; +import { + Alert, + AlertAction, + AlertDescription, + AlertTitle, + Button, +} from '@trycompai/design-system'; +import { Close } from '@trycompai/design-system/icons'; +import Link from 'next/link'; +import type { NudgeState, ServerNudgeData } from './types'; + +export function useTrustPortalSetupNudge({ + orgId, + server, +}: { + orgId: string; + server: ServerNudgeData; +}): NudgeState { + const { hasPermission } = usePermissions(); + const canSetup = hasPermission('trust', 'update'); + const { isTrustNdaEnabled, isConfigured } = server.trust; + + return { + id: 'trust-portal-setup', + priority: 20, + persistDismissal: true, + ready: true, // server data already resolved + eligible: isTrustNdaEnabled && !isConfigured && canSetup, + render: (onDismiss) => ( + + ), + }; +} + +function TrustPortalSetupNudgeView({ + orgId, + onDismiss, +}: { + orgId: string; + onDismiss: () => void; +}) { + return ( + +
+ Showcase your security posture + + Set up your Trust Portal to share your certifications, policies, and + security documents with prospects in one place. + + +
+ + + +
+ ); +} From 390c2634e22f3ef1115255e3cda0f6a8e06155c2 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 16:47:26 -0400 Subject: [PATCH 08/34] fix(trust): use Button render prop and direct Alert slots in setup nudge --- .../overview/nudges/TrustPortalSetupNudge.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx index 5aa7fe58a..2015662f7 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx @@ -44,15 +44,13 @@ function TrustPortalSetupNudgeView({ }) { return ( -
- Showcase your security posture - - Set up your Trust Portal to share your certifications, policies, and - security documents with prospects in one place. - - + Showcase your security posture + + Set up your Trust Portal to share your certifications, policies, and + security documents with prospects in one place. + +
+
-
- - ); -} diff --git a/apps/app/src/app/(app)/[orgId]/overview/components/Overview.tsx b/apps/app/src/app/(app)/[orgId]/overview/components/Overview.tsx index 1cdd08353..ddfe0746f 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/components/Overview.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/components/Overview.tsx @@ -4,7 +4,6 @@ import { FrameworkEditorFramework, Policy, Task } from '@db'; import type { FrameworkInstanceWithControls } from '@/lib/types/framework'; import { ComplianceOverview } from './ComplianceOverview'; import { FrameworksOverview } from './FrameworksOverview'; -import { OffboardingBanner } from './OffboardingBanner'; import { ToDoOverview } from './ToDoOverview'; import { FrameworkInstanceWithComplianceScore } from './types'; @@ -71,7 +70,6 @@ export const Overview = ({ return (
-
}) { const { orgId: organizationId } = await params; - const [scoresRes, frameworksRes, availableRes] = await Promise.all([ + const requestHeaders = await headers(); + const session = await auth.api.getSession({ headers: requestHeaders }); + + const [scoresRes, frameworksRes, availableRes, settingsRes] = await Promise.all([ serverApi.get('/v1/frameworks/scores'), serverApi.get<{ data: FrameworkWithScore[] }>('/v1/frameworks?includeControls=true&includeScores=true'), serverApi.get<{ data: FrameworkEditorFramework[] }>('/v1/frameworks/available'), + serverApi.get<{ isConfigured?: boolean }>('/v1/trust-portal/settings'), ]); const scores = scoresRes.data; const frameworksData = frameworksRes.data?.data ?? []; const allFrameworks = availableRes.data?.data ?? []; + let isTrustNdaEnabled = false; + if (session?.user?.id) { + const flags = await getFeatureFlags(session.user.id, { + groups: { organization: organizationId }, + }); + isTrustNdaEnabled = + flags['is-trust-nda-enabled'] === true || flags['is-trust-nda-enabled'] === 'true'; + } + + // Fail closed: if we can't determine state, don't nudge. + const isTrustConfigured = settingsRes.data?.isConfigured ?? true; + const frameworksWithControls = frameworksData.map( ({ complianceScore: _score, ...fw }: FrameworkWithScore) => fw, ); @@ -55,6 +75,10 @@ export default async function OverviewPage({ params }: { params: Promise<{ orgId return ( <> + } />}> Date: Thu, 28 May 2026 16:57:45 -0400 Subject: [PATCH 12/34] feat(trust): add getting-started block to trust page --- .../TrustPortalGettingStarted.test.tsx | 30 +++++++++++++++++++ .../components/TrustPortalGettingStarted.tsx | 29 ++++++++++++++++++ apps/app/src/app/(app)/[orgId]/trust/page.tsx | 3 ++ 3 files changed, 62 insertions(+) create mode 100644 apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.test.tsx create mode 100644 apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.tsx diff --git a/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.test.tsx b/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.test.tsx new file mode 100644 index 000000000..a0335538e --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.test.tsx @@ -0,0 +1,30 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('next/link', () => ({ + default: ({ href, children }: { href: string; children: React.ReactNode }) => ( + {children} + ), +})); + +vi.mock('@trycompai/design-system', () => ({ + Alert: ({ children }: any) =>
{children}
, + AlertTitle: ({ children }: any) =>
{children}
, + AlertDescription: ({ children }: any) =>
{children}
, +})); + +import { TrustPortalGettingStarted } from './TrustPortalGettingStarted'; + +describe('TrustPortalGettingStarted', () => { + it('renders the live shared portal URL', () => { + render(); + expect(screen.getByText(/trust.inc\/org_123/)).toBeInTheDocument(); + }); + + it('renders the getting-started heading', () => { + render(); + expect( + screen.getByText(/finish setting up your trust portal/i), + ).toBeInTheDocument(); + }); +}); diff --git a/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.tsx b/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.tsx new file mode 100644 index 000000000..9c5008f2b --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.tsx @@ -0,0 +1,29 @@ +import { Alert, AlertDescription, AlertTitle } from '@trycompai/design-system'; +import Link from 'next/link'; + +const STEPS = [ + 'Connect a custom domain so the portal lives on your brand.', + 'Add your compliance certifications (SOC 2, ISO 27001, …).', + 'Upload supporting documents and policies.', + 'Add FAQs and a contact email for prospects.', +]; + +export function TrustPortalGettingStarted({ portalUrl }: { portalUrl: string }) { + return ( + + Finish setting up your Trust Portal + + Your Trust Portal is already live at{' '} + + {portalUrl} + + , but it's still on the defaults. Complete these to make it yours: + +
    + {STEPS.map((step) => ( +
  • {step}
  • + ))} +
+
+ ); +} diff --git a/apps/app/src/app/(app)/[orgId]/trust/page.tsx b/apps/app/src/app/(app)/[orgId]/trust/page.tsx index b980b263c..2dac76362 100644 --- a/apps/app/src/app/(app)/[orgId]/trust/page.tsx +++ b/apps/app/src/app/(app)/[orgId]/trust/page.tsx @@ -3,6 +3,7 @@ import { Button, PageHeader, PageLayout } from '@trycompai/design-system'; import { Launch } from '@trycompai/design-system/icons'; import type { Metadata } from 'next'; import Link from 'next/link'; +import { TrustPortalGettingStarted } from './components/TrustPortalGettingStarted'; import { TrustPortalSwitch } from './portal-settings/components/TrustPortalSwitch'; export default async function TrustPage({ @@ -26,6 +27,7 @@ export default async function TrustPage({ ]); const settings = settingsRes.data as any; + const isTrustConfigured = settings?.isConfigured ?? true; const customLinks = Array.isArray(customLinksRes.data) ? customLinksRes.data : []; @@ -96,6 +98,7 @@ export default async function TrustPage({ /> } > + {!isTrustConfigured && } Date: Thu, 28 May 2026 17:00:47 -0400 Subject: [PATCH 13/34] test(trust): cover getting-started steps, document Alert grid placement --- .../trust/components/TrustPortalGettingStarted.test.tsx | 6 ++++++ .../[orgId]/trust/components/TrustPortalGettingStarted.tsx | 2 ++ 2 files changed, 8 insertions(+) diff --git a/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.test.tsx b/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.test.tsx index a0335538e..4fa37ca30 100644 --- a/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.test.tsx @@ -27,4 +27,10 @@ describe('TrustPortalGettingStarted', () => { screen.getByText(/finish setting up your trust portal/i), ).toBeInTheDocument(); }); + + it('renders the setup steps', () => { + render(); + expect(screen.getByText(/custom domain/i)).toBeInTheDocument(); + expect(screen.getByText(/compliance certifications/i)).toBeInTheDocument(); + }); }); diff --git a/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.tsx b/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.tsx index 9c5008f2b..8876591f5 100644 --- a/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.tsx +++ b/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.tsx @@ -19,6 +19,8 @@ export function TrustPortalGettingStarted({ portalUrl }: { portalUrl: string }) , but it's still on the defaults. Complete these to make it yours: + {/* variant="info" renders an icon, so Alert is a 2-col grid; place the + list in the text column like the title/description slots above. */}
    {STEPS.map((step) => (
  • {step}
  • From ab836e713c140be16ffe5eba4e814c23b2b6bbbd Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 17:08:56 -0400 Subject: [PATCH 14/34] feat(trust): stacked expandable nudge view so waiting notifications are visible --- .../overview/nudges/OverviewNudges.test.tsx | 26 +++++- .../overview/nudges/OverviewNudges.tsx | 86 +++++++++++++++++-- 2 files changed, 103 insertions(+), 9 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.test.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.test.tsx index a7aa72103..37ff7bab7 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.test.tsx @@ -33,6 +33,8 @@ vi.mock('@trycompai/design-system', () => ({ vi.mock('@trycompai/design-system/icons', () => ({ Close: () => x, WarningAlt: () => !, + ChevronDown: () => v, + ChevronUp: () => ^, })); import { OverviewNudges } from './OverviewNudges'; @@ -76,11 +78,33 @@ describe('OverviewNudges', () => { expect(screen.queryByText('Showcase your security posture')).not.toBeInTheDocument(); }); - it('shows offboarding ahead of the trust nudge when both apply', () => { + it('collapses to the top nudge with a stack control when several apply', () => { setOffboarding([{ memberId: 'm1', name: 'Jo' }]); render(); + // Offboarding (priority 10) is shown; trust waits behind the stack. expect(screen.getByText(/offboarding completion/)).toBeInTheDocument(); expect(screen.queryByText('Showcase your security posture')).not.toBeInTheDocument(); + // The user is told more are waiting. + expect(screen.getByText('Show 2 notifications')).toBeInTheDocument(); + }); + + it('expands the stack to reveal every waiting nudge, then collapses', () => { + setOffboarding([{ memberId: 'm1', name: 'Jo' }]); + render(); + + fireEvent.click(screen.getByText('Show 2 notifications')); + expect(screen.getByText(/offboarding completion/)).toBeInTheDocument(); + expect(screen.getByText('Showcase your security posture')).toBeInTheDocument(); + + fireEvent.click(screen.getByText('Show less')); + expect(screen.queryByText('Showcase your security posture')).not.toBeInTheDocument(); + expect(screen.getByText('Show 2 notifications')).toBeInTheDocument(); + }); + + it('shows no stack control when only one nudge applies', () => { + render(); + expect(screen.getByText('Showcase your security posture')).toBeInTheDocument(); + expect(screen.queryByText(/Show \d+ notifications/)).not.toBeInTheDocument(); }); it('dismissing the trust nudge hides it and persists', () => { diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.tsx index f468e6ebc..96fa86499 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.tsx @@ -1,9 +1,10 @@ 'use client'; +import { ChevronDown, ChevronUp } from '@trycompai/design-system/icons'; import { useEffect, useState } from 'react'; import { useOffboardingNudge } from './OffboardingNudge'; import { useTrustPortalSetupNudge } from './TrustPortalSetupNudge'; -import type { ServerNudgeData } from './types'; +import type { NudgeState, ServerNudgeData } from './types'; const dismissKey = (id: string, orgId: string) => `overview-nudge-dismissed:${id}:${orgId}`; @@ -27,6 +28,7 @@ export function OverviewNudges({ .join(','); const [dismissed, setDismissed] = useState>(new Set()); + const [expanded, setExpanded] = useState(false); const [mounted, setMounted] = useState(false); useEffect(() => { @@ -44,16 +46,84 @@ export function OverviewNudges({ const visible = candidates .filter((c) => c.ready && c.eligible && !dismissed.has(c.id)) - .sort((a, b) => a.priority - b.priority)[0]; + .sort((a, b) => a.priority - b.priority); - if (!visible) return null; + if (visible.length === 0) return null; - const handleDismiss = () => { - if (visible.persistDismissal) { - window.localStorage.setItem(dismissKey(visible.id, orgId), '1'); + const dismiss = (nudge: NudgeState) => () => { + if (nudge.persistDismissal) { + window.localStorage.setItem(dismissKey(nudge.id, orgId), '1'); } - setDismissed((prev) => new Set(prev).add(visible.id)); + setDismissed((prev) => new Set(prev).add(nudge.id)); }; - return <>{visible.render(handleDismiss)}; + // Single nudge: render it plainly, no stack chrome. + if (visible.length === 1) { + return <>{visible[0].render(dismiss(visible[0]))}; + } + + // Expanded: every waiting nudge, with a control to collapse back. + if (expanded) { + return ( +
    + {visible.map((nudge) => ( +
    {nudge.render(dismiss(nudge))}
    + ))} + setExpanded(false)} + /> +
    + ); + } + + // Collapsed: the top nudge over a faux "stack" edge, plus an expand control. + const top = visible[0]; + return ( +
    +
    +
    + {top.render(dismiss(top))} +
    + setExpanded(true)} + /> +
    + ); +} + +function StackToggle({ + expanded, + count, + onClick, +}: { + expanded: boolean; + count: number; + onClick: () => void; +}) { + return ( + + ); } From 4d52a405c6b9d2dc123ebeb9d9b428c8649a6612 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 17:11:04 -0400 Subject: [PATCH 15/34] feat(trust): wrap stacked nudges in a NudgeCenter tray with integrated toggle --- .../[orgId]/overview/nudges/NudgeCenter.tsx | 45 +++++++++++ .../overview/nudges/OverviewNudges.tsx | 77 ++++--------------- 2 files changed, 60 insertions(+), 62 deletions(-) create mode 100644 apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx new file mode 100644 index 000000000..c6f2e1d08 --- /dev/null +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { ChevronDown, ChevronUp } from '@trycompai/design-system/icons'; +import type { ReactNode } from 'react'; + +/** + * Wrapper that groups one or more notification nudges into a single tray with + * an integrated expand/collapse footer, so the "show more" control reads as + * part of the unit rather than floating loose on the page. + */ +export function NudgeCenter({ + count, + expanded, + onToggle, + children, +}: { + count: number; + expanded: boolean; + onToggle: () => void; + children: ReactNode; +}) { + return ( +
    +
    {children}
    + +
    + ); +} diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.tsx index 96fa86499..20043bff7 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.tsx @@ -1,7 +1,7 @@ 'use client'; -import { ChevronDown, ChevronUp } from '@trycompai/design-system/icons'; import { useEffect, useState } from 'react'; +import { NudgeCenter } from './NudgeCenter'; import { useOffboardingNudge } from './OffboardingNudge'; import { useTrustPortalSetupNudge } from './TrustPortalSetupNudge'; import type { NudgeState, ServerNudgeData } from './types'; @@ -57,73 +57,26 @@ export function OverviewNudges({ setDismissed((prev) => new Set(prev).add(nudge.id)); }; - // Single nudge: render it plainly, no stack chrome. + // Single nudge: render it plainly, no tray chrome. if (visible.length === 1) { return <>{visible[0].render(dismiss(visible[0]))}; } - // Expanded: every waiting nudge, with a control to collapse back. - if (expanded) { - return ( -
    - {visible.map((nudge) => ( -
    {nudge.render(dismiss(nudge))}
    - ))} - setExpanded(false)} - /> -
    - ); - } + // Only honor `expanded` while there's actually more than one to show, so a + // dismissal that drops the count to 1 (then a new one later) can't surprise- + // expand the tray. + const isExpanded = expanded && visible.length > 1; + const shown = isExpanded ? visible : visible.slice(0, 1); - // Collapsed: the top nudge over a faux "stack" edge, plus an expand control. - const top = visible[0]; - return ( -
    -
    -
    - {top.render(dismiss(top))} -
    - setExpanded(true)} - /> -
    - ); -} - -function StackToggle({ - expanded, - count, - onClick, -}: { - expanded: boolean; - count: number; - onClick: () => void; -}) { return ( - + {shown.map((nudge) => ( +
    {nudge.render(dismiss(nudge))}
    + ))} + ); } From 2ab8c21d8e6ab4e31e498e77e9ac4019c5227954 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 17:13:02 -0400 Subject: [PATCH 16/34] feat(trust): render collapsed nudges as a peeking card stack --- .../[orgId]/overview/nudges/NudgeCenter.tsx | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx index c6f2e1d08..c9c882ad8 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx @@ -3,10 +3,12 @@ import { ChevronDown, ChevronUp } from '@trycompai/design-system/icons'; import type { ReactNode } from 'react'; +const MAX_PEEK_LAYERS = 2; + /** - * Wrapper that groups one or more notification nudges into a single tray with - * an integrated expand/collapse footer, so the "show more" control reads as - * part of the unit rather than floating loose on the page. + * Groups multiple notification nudges. Collapsed, it renders the top nudge on a + * "pile" — a sliver of the cards behind it peeks out — with a toggle to fan the + * whole stack open. Expanded, every nudge is shown in a vertical list. */ export function NudgeCenter({ count, @@ -19,9 +21,34 @@ export function NudgeCenter({ onToggle: () => void; children: ReactNode; }) { + const peekLayers = Math.min(count - 1, MAX_PEEK_LAYERS); + return ( -
    -
    {children}
    +
    + {expanded ? ( +
    {children}
    + ) : ( + // Padding reserves room for the peeking edges that sit below the top card. +
    +
    + {Array.from({ length: peekLayers }).map((_, i) => { + const depth = i + 1; + return ( +
    + ); + })} + {children} +
    +
    + )} diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.test.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.test.tsx index 37ff7bab7..84f50e54c 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.test.tsx @@ -85,26 +85,26 @@ describe('OverviewNudges', () => { expect(screen.getByText(/offboarding completion/)).toBeInTheDocument(); expect(screen.queryByText('Showcase your security posture')).not.toBeInTheDocument(); // The user is told more are waiting. - expect(screen.getByText('Show 2 notifications')).toBeInTheDocument(); + expect(screen.getByText('2 notifications')).toBeInTheDocument(); }); it('expands the stack to reveal every waiting nudge, then collapses', () => { setOffboarding([{ memberId: 'm1', name: 'Jo' }]); render(); - fireEvent.click(screen.getByText('Show 2 notifications')); + fireEvent.click(screen.getByText('2 notifications')); expect(screen.getByText(/offboarding completion/)).toBeInTheDocument(); expect(screen.getByText('Showcase your security posture')).toBeInTheDocument(); fireEvent.click(screen.getByText('Show less')); expect(screen.queryByText('Showcase your security posture')).not.toBeInTheDocument(); - expect(screen.getByText('Show 2 notifications')).toBeInTheDocument(); + expect(screen.getByText('2 notifications')).toBeInTheDocument(); }); it('shows no stack control when only one nudge applies', () => { render(); expect(screen.getByText('Showcase your security posture')).toBeInTheDocument(); - expect(screen.queryByText(/Show \d+ notifications/)).not.toBeInTheDocument(); + expect(screen.queryByText(/\d+ notifications/)).not.toBeInTheDocument(); }); it('dismissing the trust nudge hides it and persists', () => { From 8965dd6771dc65f3312d795ebd838cb9edc5dac5 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 17:26:50 -0400 Subject: [PATCH 19/34] feat(trust): overlay the expand chip on the stack's bottom edge --- .../[orgId]/overview/nudges/NudgeCenter.tsx | 105 ++++++++++-------- 1 file changed, 58 insertions(+), 47 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx index 4d9ce7c00..789c36c64 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx @@ -7,8 +7,9 @@ const MAX_PEEK_LAYERS = 2; /** * Groups multiple notification nudges. Collapsed, it renders the top nudge on a - * "pile" — a sliver of the cards behind it peeks out — with a toggle to fan the - * whole stack open. Expanded, every nudge is shown in a vertical list. + * "pile" — a sliver of the cards behind it peeks out — with a toggle chip + * overlaid on the bottom edge to fan the whole stack open. Expanded, every + * nudge is shown in a vertical list. */ export function NudgeCenter({ count, @@ -23,55 +24,65 @@ export function NudgeCenter({ }) { const peekLayers = Math.min(count - 1, MAX_PEEK_LAYERS); - return ( -
    + const toggle = (positionClass: string) => ( + + ); + + if (expanded) { + return ( +
    +
    {children}
    + {toggle('')} +
    + ); + } + + // Collapsed: top nudge on a pile, with the toggle chip overlaid on the + // bottom-center edge. Padding reserves room for the peeks + the chip overhang. + return ( +
    + {/* `isolate` keeps the stack's own stacking context so the peek layers + sit just behind the top card (not behind an ancestor background). */} +
    + {Array.from({ length: peekLayers }).map((_, i) => { + const depth = i + 1; + return ( +
    + ); + })} +
    {children}
    + {toggle( + 'absolute bottom-0 left-1/2 z-20 -translate-x-1/2 translate-y-1/2', )} - +
    ); } From 36257924466b2e0d341f9174bd80fc48569c0913 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 17:32:51 -0400 Subject: [PATCH 20/34] feat(trust): align nudges to page width, neutral floating expand chip --- .../[orgId]/overview/nudges/NudgeCenter.tsx | 6 +-- .../overview/nudges/OverviewNudges.tsx | 46 +++++++++---------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx index 789c36c64..cfc0fd525 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx @@ -29,7 +29,7 @@ export function NudgeCenter({ type="button" onClick={onToggle} aria-expanded={expanded} - className={`flex items-center gap-1.5 rounded-full border border-border bg-card px-3 py-1 text-xs font-medium text-muted-foreground shadow-sm transition-colors hover:bg-muted hover:text-foreground ${positionClass}`} + className={`flex items-center gap-1.5 rounded-full bg-foreground px-3 py-1 text-xs font-medium text-background shadow-lg transition-colors hover:bg-foreground/90 ${positionClass}`} > {expanded ? ( <> @@ -57,7 +57,7 @@ export function NudgeCenter({ // Collapsed: top nudge on a pile, with the toggle chip overlaid on the // bottom-center edge. Padding reserves room for the peeks + the chip overhang. return ( -
    +
    {/* `isolate` keeps the stack's own stacking context so the peek layers sit just behind the top card (not behind an ancestor background). */}
    @@ -80,7 +80,7 @@ export function NudgeCenter({ })}
    {children}
    {toggle( - 'absolute bottom-0 left-1/2 z-20 -translate-x-1/2 translate-y-1/2', + 'absolute bottom-0 left-1/2 z-20 -translate-x-1/2 translate-y-[65%]', )}
    diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.tsx index 20043bff7..3bd1219ef 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.tsx @@ -42,13 +42,16 @@ export function OverviewNudges({ setDismissed(next); }, [orgId, persistableIds]); - if (!mounted) return null; - const visible = candidates .filter((c) => c.ready && c.eligible && !dismissed.has(c.id)) .sort((a, b) => a.priority - b.priority); - if (visible.length === 0) return null; + // Collapse the tray whenever there's no longer more than one to fan out. + useEffect(() => { + if (visible.length <= 1 && expanded) setExpanded(false); + }, [visible.length, expanded]); + + if (!mounted || visible.length === 0) return null; const dismiss = (nudge: NudgeState) => () => { if (nudge.persistDismissal) { @@ -57,26 +60,21 @@ export function OverviewNudges({ setDismissed((prev) => new Set(prev).add(nudge.id)); }; - // Single nudge: render it plainly, no tray chrome. - if (visible.length === 1) { - return <>{visible[0].render(dismiss(visible[0]))}; - } - - // Only honor `expanded` while there's actually more than one to show, so a - // dismissal that drops the count to 1 (then a new one later) can't surprise- - // expand the tray. - const isExpanded = expanded && visible.length > 1; - const shown = isExpanded ? visible : visible.slice(0, 1); + const body = + visible.length === 1 ? ( + visible[0].render(dismiss(visible[0])) + ) : ( + setExpanded((prev) => !prev)} + > + {(expanded ? visible : visible.slice(0, 1)).map((nudge) => ( +
    {nudge.render(dismiss(nudge))}
    + ))} +
    + ); - return ( - setExpanded((prev) => !prev)} - > - {shown.map((nudge) => ( -
    {nudge.render(dismiss(nudge))}
    - ))} -
    - ); + // Match the page's centered content width so nudges align with everything else. + return
    {body}
    ; } From 463aafda931741055afa79a1c2a68a7e2e814662 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 17:42:56 -0400 Subject: [PATCH 21/34] refactor(trust): use DS Alert variants + semantic tokens, drop hardcoded colors --- .../[orgId]/overview/nudges/NudgeCenter.tsx | 4 +-- .../overview/nudges/OffboardingNudge.tsx | 36 +++++++++---------- .../overview/nudges/TrustPortalSetupNudge.tsx | 2 +- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx index cfc0fd525..3105c1b1c 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx @@ -29,7 +29,7 @@ export function NudgeCenter({ type="button" onClick={onToggle} aria-expanded={expanded} - className={`flex items-center gap-1.5 rounded-full bg-foreground px-3 py-1 text-xs font-medium text-background shadow-lg transition-colors hover:bg-foreground/90 ${positionClass}`} + className={`flex items-center gap-1.5 rounded-full border border-border bg-card px-3 py-1 text-xs font-medium text-foreground shadow-lg transition-colors hover:bg-accent ${positionClass}`} > {expanded ? ( <> @@ -67,7 +67,7 @@ export function NudgeCenter({
    -
    - - - - {members.length} employee{members.length !== 1 ? 's' : ''} - {' '} - require{members.length === 1 ? 's' : ''} offboarding completion - -
    -
    - + + + {`${members.length} employee${members.length !== 1 ? 's' : ''} require${ + members.length === 1 ? 's' : '' + } offboarding completion`} + +
    + +
    + -
    -
    + + ); } diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx index 2015662f7..cb40c4f5f 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx @@ -57,7 +57,7 @@ function TrustPortalSetupNudgeView({ type="button" onClick={onDismiss} aria-label="Dismiss" - className="rounded-md p-1 text-blue-600 transition-colors hover:text-blue-800" + className="rounded-md p-1 opacity-70 transition-opacity hover:opacity-100" > From 4801b7e220d0ae0a449f91e24cc3551c7cec79c0 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 17:47:16 -0400 Subject: [PATCH 22/34] fix(trust): use outline variant for offboarding View details button --- .../src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx index 375a8c895..12245838e 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx @@ -57,7 +57,7 @@ function OffboardingNudgeView({ } offboarding completion`}
    -
    From a1e44ceb183a4580df4ba688f54383188b1a486b Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 17:49:14 -0400 Subject: [PATCH 23/34] fix(trust): amber (warning-tinted) peek cards so the stack is visible --- apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx index 3105c1b1c..9887269f6 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx @@ -67,7 +67,7 @@ export function NudgeCenter({
    Date: Thu, 28 May 2026 17:50:11 -0400 Subject: [PATCH 24/34] style(trust): use warning variant for trust setup nudge to match the amber stack --- .../app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx index cb40c4f5f..ddedbad5f 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/TrustPortalSetupNudge.tsx @@ -43,7 +43,7 @@ function TrustPortalSetupNudgeView({ onDismiss: () => void; }) { return ( - + Showcase your security posture Set up your Trust Portal to share your certifications, policies, and From 0be178211af6d0c34e85c89c627a853132429455 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 17:50:48 -0400 Subject: [PATCH 25/34] style(trust): label the stack toggle '2 notices' --- .../src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx | 2 +- .../(app)/[orgId]/overview/nudges/OverviewNudges.test.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx index 9887269f6..53229ddcc 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/NudgeCenter.tsx @@ -39,7 +39,7 @@ export function NudgeCenter({ ) : ( <> - {count} notifications + {count} notices )} diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.test.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.test.tsx index 84f50e54c..118ab3903 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.test.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/OverviewNudges.test.tsx @@ -85,26 +85,26 @@ describe('OverviewNudges', () => { expect(screen.getByText(/offboarding completion/)).toBeInTheDocument(); expect(screen.queryByText('Showcase your security posture')).not.toBeInTheDocument(); // The user is told more are waiting. - expect(screen.getByText('2 notifications')).toBeInTheDocument(); + expect(screen.getByText('2 notices')).toBeInTheDocument(); }); it('expands the stack to reveal every waiting nudge, then collapses', () => { setOffboarding([{ memberId: 'm1', name: 'Jo' }]); render(); - fireEvent.click(screen.getByText('2 notifications')); + fireEvent.click(screen.getByText('2 notices')); expect(screen.getByText(/offboarding completion/)).toBeInTheDocument(); expect(screen.getByText('Showcase your security posture')).toBeInTheDocument(); fireEvent.click(screen.getByText('Show less')); expect(screen.queryByText('Showcase your security posture')).not.toBeInTheDocument(); - expect(screen.getByText('2 notifications')).toBeInTheDocument(); + expect(screen.getByText('2 notices')).toBeInTheDocument(); }); it('shows no stack control when only one nudge applies', () => { render(); expect(screen.getByText('Showcase your security posture')).toBeInTheDocument(); - expect(screen.queryByText(/\d+ notifications/)).not.toBeInTheDocument(); + expect(screen.queryByText(/\d+ notices/)).not.toBeInTheDocument(); }); it('dismissing the trust nudge hides it and persists', () => { From dcc543e7dd017b2128a06323daac74a399ba3fdc Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 17:52:19 -0400 Subject: [PATCH 26/34] style(trust): use foreground text in nudge alerts for contrast --- .../overview/nudges/OffboardingNudge.tsx | 8 +++++--- .../overview/nudges/TrustPortalSetupNudge.tsx | 10 +++++++--- .../components/TrustPortalGettingStarted.tsx | 18 +++++++++++------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx index 12245838e..b06cbcff9 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx @@ -52,9 +52,11 @@ function OffboardingNudgeView({ return ( - {`${members.length} employee${members.length !== 1 ? 's' : ''} require${ - members.length === 1 ? 's' : '' - } offboarding completion`} + + {`${members.length} employee${members.length !== 1 ? 's' : ''} require${ + members.length === 1 ? 's' : '' + } offboarding completion`} +
    diff --git a/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.tsx b/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.tsx index 8876591f5..fd75b9918 100644 --- a/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.tsx +++ b/apps/app/src/app/(app)/[orgId]/trust/components/TrustPortalGettingStarted.tsx @@ -11,17 +11,21 @@ const STEPS = [ export function TrustPortalGettingStarted({ portalUrl }: { portalUrl: string }) { return ( - Finish setting up your Trust Portal + + Finish setting up your Trust Portal + - Your Trust Portal is already live at{' '} - - {portalUrl} - - , but it's still on the defaults. Complete these to make it yours: + + Your Trust Portal is already live at{' '} + + {portalUrl} + + , but it's still on the defaults. Complete these to make it yours: + {/* variant="info" renders an icon, so Alert is a 2-col grid; place the list in the text column like the title/description slots above. */} -
      +
        {STEPS.map((step) => (
      • {step}
      • ))} From ac08c69ff93c72d927d54e1c78abb62b041c7fa0 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 17:53:12 -0400 Subject: [PATCH 27/34] style(trust): foreground text on View details button for contrast --- .../src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx index b06cbcff9..756278856 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx @@ -60,7 +60,7 @@ function OffboardingNudgeView({
        From 265e4b708e575f18d7c416fe6e21dffab573f870 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Thu, 28 May 2026 17:54:05 -0400 Subject: [PATCH 28/34] style(trust): make View details a primary button to match Set it up --- .../app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx b/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx index 756278856..5eaa6a095 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/nudges/OffboardingNudge.tsx @@ -59,9 +59,7 @@ function OffboardingNudgeView({
        - +
        +
    +