Skip to content

feat(theme): #39 #46 map DaisyUI theme color into Calendly/Cal.com/Disqus embeds#136

Merged
TortoiseWolfe merged 3 commits into
mainfrom
feat/39-46-embed-theme-color
Jun 6, 2026
Merged

feat(theme): #39 #46 map DaisyUI theme color into Calendly/Cal.com/Disqus embeds#136
TortoiseWolfe merged 3 commits into
mainfrom
feat/39-46-embed-theme-color

Conversation

@TortoiseWolfe

Copy link
Copy Markdown
Owner

Closes #39, closes #46.

What

Third-party embeds (Calendly, Cal.com, Disqus) took a brand/link color as a plain hex and all three hardcoded #00a2ff. This derives that color from the active DaisyUI theme's --color-primary at runtime and re-applies it on theme switch — the per-theme mapping both Gap-Audit issues asked for.

  • src/utils/embed-theme.tsgetEmbedColor (OKLCH→hex via the existing getDaisyUIColorAsThree, bare/# formats), contrastRatio, and getAccessibleEmbedColor (theme primary when it clears WCAG AA against the bg, else a legible fallback — [Gap-Audit] 045 Disqus Theme: 32 DaisyUI theme mapping + smooth transitions + contrast verification #46 NFR-002).
  • src/hooks/useEmbedThemeColor.tsdata-theme MutationObserver mirroring useMapTheme; one hook drives all three embeds (no duplicate observers).
  • CalendlyProvider / CalComProvider — brand accent = theme primary. The button label rides on --color-primary-content, which clears AA UI/large (3:1) for all 34 themes, so the raw 1:1 map is safe.
  • DisqusComments — link color is contrast-gated (raw primary fails AA on the thread bg for 18/34 themes — many DaisyUI primaries are pale accents); colorScheme follows the dark/light split.

The contrast test is honest (Playwright, not jsdom)

jsdom never applies DaisyUI's stylesheet, so getComputedStyle(:root) returns empty for --color-primary and the helpers fall back to gray — a jsdom contrast test would silently pass gray-on-gray for every theme. tests/e2e/embed-theme-contrast.spec.ts runs in chromium-gen against the built static site, measures real computed colors for all 34 themes, and includes an honesty guard that fails if a token doesn't resolve. Thresholds were derived from measured ratios; 0 themes need an allowlist.

Surfaced + fixed a latent a11y gap

While verifying, the old Disqus light-mode link blue (#3b82f6) measured only 3.68:1 on white — below WCAG AA. Replaced with blue-600 (#2563eb, 5.17:1). Dark fallback (blue-300, 9.84:1) was already fine.

Verification

  • pnpm run type-check, pnpm run lint, pnpm build — all green.
  • 17 new vitest unit tests (util math/format/fallback + contrast helpers + hook observer reactivity) pass.
  • All 34 themes verified to pass both honest contrast assertions (brand-label 3:1, accessible-link 4.5:1) — confirmed offline against the compiled DaisyUI CSS; the Playwright spec enforces it in CI.

🤖 Generated with Claude Code

TurtleWolfe and others added 3 commits June 6, 2026 00:22
…squs embeds

Third-party embeds take a brand/link color as a plain hex and can't parse
DaisyUI's OKLCH custom properties, so all three hardcoded '#00a2ff'. This
derives the color from the active theme's --color-primary at runtime and
re-applies on theme switch.

- src/utils/embed-theme.ts: getEmbedColor (OKLCH→hex, bare/# formats),
  contrastRatio, and getAccessibleEmbedColor (theme primary when it clears
  WCAG AA against the bg, else a legible fallback — #46 NFR-002).
- src/hooks/useEmbedThemeColor.ts: data-theme MutationObserver (mirrors
  useMapTheme); one hook drives all three embeds.
- CalendlyProvider/CalComProvider: brand accent = theme primary. The button
  LABEL rides on --color-primary-content, which clears AA UI/large (3:1) for
  all 34 themes, so the raw 1:1 map is safe here.
- DisqusComments: link color is contrast-gated (raw primary fails AA on the
  thread bg for 18/34 themes — pale accents); colorScheme follows dark/light.

Honest contrast gate in Playwright, NOT vitest: jsdom never applies DaisyUI
CSS, so tokens resolve empty and a jsdom test would silently pass gray-on-gray.
tests/e2e/embed-theme-contrast.spec.ts measures real computed colors for all
34 themes and fails if a token doesn't resolve. Thresholds derived from
measured ratios; 0 themes need an allowlist.

Surfaced a latent a11y gap: the old Disqus light-mode link blue (#3b82f6) was
only 3.68:1 on white — replaced with blue-600 (#2563eb, 5.17:1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uous contrast guard

Adversarial review (verified against real Chromium) found two real issues:

1. Disqus colorScheme froze on theme switch. The config builder sat behind the
   one-shot `isLoaded` guard, so `disqus_config.colorScheme` was never rebuilt
   and the reset effect (deps without `dark`) never re-fired — only the injected
   CSS updated live. Moved config construction into the reset effect with `dark`
   in its deps, so a post-load theme switch rebuilds the config and calls
   DISQUS.reset to re-render the iframe in the new scheme.

2. The e2e contrast honesty guard was vacuous. An unresolved var(--color-primary)
   is invalid-at-computed-value-time and `color` falls back to the inherited body
   color — a valid rgb() — so `primary[0] >= 0` always passed; a tokens-dropped
   build would NOT fail. Now assert `primary !== primary-content` (verified
   collision-free across all 34 real themes; both collapse to the inherited color
   when tokens don't resolve, failing loudly). Added KNOWN_PALE_PRIMARY checks so
   the Disqus-link assertion can't pass trivially — it proves the AA gate actually
   rejects a real pale primary (cupcake/pastel/aqua/… → fallback).

Minors: seed useEmbedThemeColor's useState to the SSR-deterministic #808080 for
all fields (avoids a hydration mismatch on the color fields); soften the
Calendly/Cal.com comments (iframes apply color on mount, not live re-color).

Verified: type-check, lint, build green; 17 unit tests pass; all 34 themes pass
the strengthened assertions offline against the compiled DaisyUI CSS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… CI failure

The contrast spec failed CI on dark/light/scripthammer-* with ~1:1 ratios. Root
cause (verified in real Chromium via MCP): getComputedStyle().color returns
OKLCH-authored DaisyUI tokens as `oklch(L C H)` STRINGS, not `rgb()`. The numeric
regex read L/C/H as R/G/B, collapsing the dark theme's true 4.13:1 to 1.03:1.

Fix: read each token back through a <canvas> 2d context (fillStyle accepts any CSS
color; getImageData returns concrete sRGB bytes — the real OKLCH→sRGB conversion
the browser paints with). Verified end-to-end in Chromium: the same dark-theme
tokens that mis-read as 1.03:1 now measure 4.13:1, matching the offline math.

The probe now returns [r,g,b] arrays; the honesty guard compares colors by value
(primary !== primary-content). Re-verified all 34 themes pass every assertion
(brand 3:1, accessible link 4.5:1, pale-theme fallback) against the compiled CSS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@TortoiseWolfe TortoiseWolfe merged commit eebb6e8 into main Jun 6, 2026
18 checks passed
@TortoiseWolfe TortoiseWolfe deleted the feat/39-46-embed-theme-color branch June 6, 2026 01:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants