diff --git a/.changeset/footer-copyright-skip-nav.md b/.changeset/footer-copyright-skip-nav.md new file mode 100644 index 0000000..97f4a86 --- /dev/null +++ b/.changeset/footer-copyright-skip-nav.md @@ -0,0 +1,8 @@ +--- +"brand-shell": minor +--- + +Add `copyrightText` customization and skip navigation link. + +- `BrandDetails.copyrightText` — optional field to override the default `© {year} {name}` footer line +- Skip nav link (`Skip to main content`) added as the first child of every `
`, visually hidden until keyboard-focused, inherits the active theme's CSS variables diff --git a/schemas/brand-shell.schema.json b/schemas/brand-shell.schema.json index d6bbc63..f73fb15 100644 --- a/schemas/brand-shell.schema.json +++ b/schemas/brand-shell.schema.json @@ -85,7 +85,8 @@ "items": { "$ref": "#/$defs/CustomSocialLink" } }, "logoSrc": { "type": "string", "minLength": 1 }, - "logoAlt": { "type": "string", "minLength": 1 } + "logoAlt": { "type": "string", "minLength": 1 }, + "copyrightText": { "type": "string", "minLength": 1 } } }, "BrandTheme": { diff --git a/src/Footer.tsx b/src/Footer.tsx index 4b1c3d9..b639d05 100644 --- a/src/Footer.tsx +++ b/src/Footer.tsx @@ -144,7 +144,9 @@ export function Footer({ details, theme, className, renderLink }: FooterProps) { )} -

© {new Date().getFullYear()} {normalizedDetails.name}

+

+ {normalizedDetails.copyrightText ?? `\u00a9 ${new Date().getFullYear()} ${normalizedDetails.name}`} +

); diff --git a/src/Header.tsx b/src/Header.tsx index fcf6398..fceea2b 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -80,6 +80,7 @@ export function Header({ details, theme, className, renderLink }: HeaderProps) { return (
+ Skip to main content
{brandIdentity}
diff --git a/src/core/types.ts b/src/core/types.ts index 0f0043a..da45092 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -75,6 +75,8 @@ export interface BrandDetails { logoSrc?: string; /** Alt text for the logo. Defaults to `name` if omitted. */ logoAlt?: string; + /** Custom copyright line shown in the footer. Defaults to `© {year} {name}` when omitted. */ + copyrightText?: string; } /** diff --git a/src/core/validation.test.ts b/src/core/validation.test.ts index 92bd3e7..08aa8ae 100644 --- a/src/core/validation.test.ts +++ b/src/core/validation.test.ts @@ -139,6 +139,26 @@ describe("validateBrandDetails", () => { expect(result.normalized?.logoSrc).toBe("https://example.com/logo.png"); expect(result.normalized?.logoAlt).toBeUndefined(); }); + + it("accepts copyrightText and passes it through normalized", () => { + const result = validateBrandDetails({ + name: "Acme Corp", + copyrightText: "© 2026 Acme Corp. All rights reserved.", + }); + + expect(result.valid).toBe(true); + expect(result.normalized?.copyrightText).toBe("© 2026 Acme Corp. All rights reserved."); + }); + + it("rejects empty copyrightText", () => { + const result = validateBrandDetails({ + name: "Acme Corp", + copyrightText: " ", + }); + + expect(result.valid).toBe(false); + expect(result.errors).toContain("details.copyrightText must be a non-empty string when provided."); + }); }); describe("validateBrandTheme", () => { diff --git a/src/core/validation.ts b/src/core/validation.ts index d8124d3..b29592c 100644 --- a/src/core/validation.ts +++ b/src/core/validation.ts @@ -68,6 +68,7 @@ export function validateBrandDetails(details: unknown): BrandValidationResult