feat(theme): #39 #46 map DaisyUI theme color into Calendly/Cal.com/Disqus embeds#136
Merged
Merged
Conversation
…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>
This was referenced Jun 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-primaryat runtime and re-applies it on theme switch — the per-theme mapping both Gap-Audit issues asked for.src/utils/embed-theme.ts—getEmbedColor(OKLCH→hex via the existinggetDaisyUIColorAsThree, bare/#formats),contrastRatio, andgetAccessibleEmbedColor(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.ts—data-themeMutationObserver mirroringuseMapTheme; one hook drives all three embeds (no duplicate observers).--color-primary-content, which clears AA UI/large (3:1) for all 34 themes, so the raw 1:1 map is safe.colorSchemefollows 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-primaryand 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.tsruns inchromium-genagainst 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.🤖 Generated with Claude Code