diff --git a/.agents/plans/happy-teal-tree-platform-signing-page-branding.md b/.agents/plans/happy-teal-tree-platform-signing-page-branding.md new file mode 100644 index 000000000..efe3c75b7 --- /dev/null +++ b/.agents/plans/happy-teal-tree-platform-signing-page-branding.md @@ -0,0 +1,138 @@ +--- +date: 2026-05-06 +title: Platform Signing Page Branding +--- + +## What + +Platform-plan organisations (and their teams) can customise the **non-embed +signing pages** (`/sign/:token`, `/d/:token`, and the sibling +complete/expired/rejected/waiting pages) with: + +- Six brand colour tokens (background, foreground, primary, primary-foreground, + border, ring) plus a border-radius length. +- A free-text custom CSS block (up to 256 KB). + +Settings live on `OrganisationGlobalSettings` and `TeamGlobalSettings`. Teams +inherit from the org via the existing `brandingEnabled === null` mechanism. + +## Why + +- Embed customers already have white-label CSS; Platform customers want the + same coverage on direct signing URLs that they iframe or link to. +- Persisting on org/team (not per envelope) means it's set-and-forget. +- Sanitising **on save** lets us inline the verbatim string at SSR — no + per-render parsing cost, no `; +}; diff --git a/apps/remix/app/root.tsx b/apps/remix/app/root.tsx index 0c6341cad..096b1504e 100644 --- a/apps/remix/app/root.tsx +++ b/apps/remix/app/root.tsx @@ -17,6 +17,7 @@ import { Scripts, ScrollRestoration, useLoaderData, + useMatches, } from 'react-router'; import { PreventFlashOnWrongTheme, ThemeProvider, useTheme } from 'remix-themes'; @@ -110,6 +111,13 @@ export function LayoutContent({ children }: { children: React.ReactNode }) { const [theme] = useTheme(); + // Recipient routes (signing pages) put `documenso-branded` on so the + // inside a string value', () => { + const result = sanitizeBrandingCss('.x { font-family: ""; }'); + + expect(result.css.toLowerCase()).not.toContain(' inside a CSS comment', () => { + const result = sanitizeBrandingCss('.x { color: red; /* */ }'); + + expect(result.css.toLowerCase()).not.toContain(' inside an at-rule params block', () => { + const result = sanitizeBrandingCss( + '@media screen and (foo: bar) { .x { color: red; } }', + ); + + expect(result.css.toLowerCase()).not.toContain(' in a value', () => { + const result = sanitizeBrandingCss('.x { font-family: "foo"; }'); + + expect(result.css.toLowerCase()).not.toContain(' in an attribute selector value', () => { + const result = sanitizeBrandingCss('[data-x=""] { color: red; }'); + + expect(result.css.toLowerCase()).not.toContain(' { + // `"; }'); + + // The output keeps the literal `` end tag's `<` for the same reason it'd escape ``. + expect(result.css).toContain('