feat(site-builder): tokenise remaining component design colours (Navbar, Menu, Footer, SocialLinks, StickyCallBar, BackgroundSection, ContentSlider, Countdown, HeroSimple)

Refs #44

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Sonnet 5.5 committed 2026-10-07 12:50:47 -07:00
1 parent 2877bc06fa
commit e347a4334d
19 files changed
+190 -68

No files matched your search

@@ -31,3 +31,13 @@ describe('Footer (F4: box-model + animation + visibility props on craft.props)',
expect(craftProps).toHaveProperty('hideOnMobile', false);
});
});
describe('Footer.toHtml design tokens (#44)', () => {
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
const props = { ...((Footer as any).craft.props as object) };
const { html } = (((Footer) as any).toHtml)(props, '', 'node-tok');
const lower = html.toLowerCase();
for (const t of ['text', 'bg']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of ['#18181b', '#a1a1aa']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
});
});
+4 -2
View File
@@ -1,6 +1,7 @@
import React, { CSSProperties, useCallback, useRef, useEffect } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { tokenRef } from '../../utils/tokens';
import { escapeHtml } from '../../utils/escape';
interface FooterProps {
@@ -92,8 +93,9 @@ Footer.craft = {
props: {
text: '© 2026 MySite. All rights reserved.',
style: {
backgroundColor: '#18181b',
color: '#a1a1aa',
// Dark band: text token as the band, bg token as its (contrast-safe) text.
backgroundColor: tokenRef('text'),
color: tokenRef('bg'),
fontSize: '14px',
padding: '24px 20px',
},
@@ -107,3 +107,13 @@ describe('Menu.toHtml dropdowns (#53)', () => {
expect((Menu as any).craft.props.followPages).toBe(false);
});
});
describe('Menu.toHtml design tokens (#44)', () => {
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
const props = { ...((Menu as any).craft.props as object) };
const { html } = (((Menu) as any).toHtml)(props, '', 'node-tok');
const lower = html.toLowerCase();
for (const t of ['text', 'bg']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of ['#3f3f46']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
});
});
+7 -7
View File
@@ -43,10 +43,10 @@ const defaultLinks: MenuLink[] = [
export const Menu: UserComponent<MenuProps> = ({
links = defaultLinks,
alignment = 'right',
linkColor = '#3f3f46',
linkColor = tokenRef('text'),
linkHoverColor = tokenRef('primary'),
ctaBgColor = tokenRef('primary'),
ctaTextColor = '#ffffff',
ctaTextColor = tokenRef('bg'),
gap = '24px',
orientation = 'horizontal',
fontSize = '14px',
@@ -139,7 +139,7 @@ export const Menu: UserComponent<MenuProps> = ({
const SUB_STYLE: CSSProperties = {
position: 'absolute', top: '100%', left: 0, zIndex: 1001, minWidth: 180, margin: 0, padding: '8px 0',
listStyle: 'none', background: 'var(--wsb-bg, #ffffff)', borderRadius: 6, boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
listStyle: 'none', background: tokenRef('bg'), borderRadius: 6, boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
};
const SUB_STATIC_STYLE: CSSProperties = { margin: 0, padding: '4px 0 0 12px', listStyle: 'none' };
@@ -151,10 +151,10 @@ Menu.craft = {
links: defaultLinks,
followPages: false,
alignment: 'right',
linkColor: '#3f3f46',
linkColor: tokenRef('text'),
linkHoverColor: tokenRef('primary'),
ctaBgColor: tokenRef('primary'),
ctaTextColor: '#ffffff',
ctaTextColor: tokenRef('bg'),
gap: '24px',
orientation: 'horizontal',
fontSize: '14px',
@@ -179,10 +179,10 @@ Menu.craft = {
// string-interpolation sinks below (hoverCol goes into a <style> block,
// the worst case: </style> breakout -> arbitrary <script>), see
// task-cssxss-brief.md.
const linkCol = cssValue(props.linkColor) || '#3f3f46';
const linkCol = cssValue(props.linkColor) || tokenRef('text');
const hoverCol = cssValue(props.linkHoverColor) || tokenRef('primary');
const ctaBg = cssValue(props.ctaBgColor) || tokenRef('primary');
const ctaText = cssValue(props.ctaTextColor) || '#ffffff';
const ctaText = cssValue(props.ctaTextColor) || tokenRef('bg');
const gap = cssValue(props.gap) || '24px';
const orientation = props.orientation || 'horizontal';
const alignment = props.alignment || 'right';
@@ -185,3 +185,13 @@ describe('Navbar.toHtml dropdowns (#53)', () => {
expect((Navbar as any).craft.props.followPages).toBe(false);
});
});
describe('Navbar.toHtml design tokens (#44)', () => {
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
const props = { ...((Navbar as any).craft.props as object) };
const { html } = (((Navbar) as any).toHtml)(props, '', 'node-tok');
const lower = html.toLowerCase();
for (const t of ['text', 'bg', 'border']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of ['#3f3f46', '#e4e4e7']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
});
});
+21 -12
View File
@@ -53,6 +53,11 @@ const PADDING_PRESETS = [
{ label: 'Spacious', value: '24px 48px' },
];
function isLightNavBg(bg: string): boolean {
const v = (bg || '').trim().toLowerCase();
return v === tokenRef('bg') || v === '#ffffff' || v === '#f8fafc' || v === '#f9fafb';
}
/* ---------- Component ---------- */
export const Navbar: UserComponent<NavbarProps> = ({
@@ -65,11 +70,11 @@ export const Navbar: UserComponent<NavbarProps> = ({
logoFontSize = '20px',
logoColor,
links = defaultLinks,
backgroundColor = '#ffffff',
textColor = '#3f3f46',
backgroundColor = tokenRef('bg'),
textColor = tokenRef('text'),
hoverColor = tokenRef('primary'),
ctaColor = tokenRef('primary'),
ctaTextColor = '#ffffff',
ctaTextColor = tokenRef('bg'),
padding = '16px 24px',
navAlignment = 'space-between',
isSticky = false,
@@ -84,8 +89,12 @@ export const Navbar: UserComponent<NavbarProps> = ({
}));
const { design } = useSiteDesign();
const resolvedLogoColor = logoColor || (backgroundColor === '#ffffff' || backgroundColor === '#f8fafc' || backgroundColor === '#f9fafb' ? design.textColor : '#ffffff');
const resolvedTextColor = textColor || (backgroundColor === '#ffffff' || backgroundColor === '#f8fafc' || backgroundColor === '#f9fafb' ? '#3f3f46' : '#e4e4e7');
// Light-background heuristic: the bg token counts as light (it is the page
// background); legacy stored hex values keep the old hex test. White/zinc on
// an explicitly dark bar is "text on a dark custom colour" -- no token fits.
const lightBg = isLightNavBg(backgroundColor);
const resolvedLogoColor = logoColor || (lightBg ? design.textColor : '#ffffff');
const resolvedTextColor = textColor || (lightBg ? tokenRef('text') : '#e4e4e7');
const [hoveredLink, setHoveredLink] = useState<number | null>(null);
const [openDropdown, setOpenDropdown] = useState<number | null>(null);
@@ -225,17 +234,17 @@ Navbar.craft = {
logoColor: undefined,
links: defaultLinks,
followPages: false,
backgroundColor: '#ffffff',
textColor: '#3f3f46',
backgroundColor: tokenRef('bg'),
textColor: tokenRef('text'),
hoverColor: tokenRef('primary'),
ctaColor: tokenRef('primary'),
ctaTextColor: '#ffffff',
ctaTextColor: tokenRef('bg'),
padding: '16px 24px',
navAlignment: 'space-between',
isSticky: false,
showMobileMenu: false,
style: {
borderBottom: '1px solid #e4e4e7',
borderBottom: `1px solid ${tokenRef('border')}`,
},
hideOnDesktop: false,
hideOnTablet: false,
@@ -256,11 +265,11 @@ Navbar.craft = {
// Sanitized once here -- these are raw string-interpolation sinks below
// (hoverCol/bgColor go into a <style> block, the worst case: </style>
// breakout -> arbitrary <script>), see task-cssxss-brief.md.
const bgColor = cssValue(props.backgroundColor) || '#ffffff';
const textCol = cssValue(props.textColor) || '#3f3f46';
const bgColor = cssValue(props.backgroundColor) || tokenRef('bg');
const textCol = cssValue(props.textColor) || tokenRef('text');
const hoverCol = cssValue(props.hoverColor) || tokenRef('primary');
const ctaCol = cssValue(props.ctaColor) || tokenRef('primary');
const ctaTextCol = cssValue(props.ctaTextColor) || '#ffffff';
const ctaTextCol = cssValue(props.ctaTextColor) || tokenRef('bg');
const pad = cssValue(props.padding) || '16px 24px';
const alignment = props.navAlignment || 'space-between';
const sticky = props.isSticky;
@@ -91,3 +91,13 @@ describe('SocialLinks.craft.props includes the box-model/animation/visibility ro
expect(props).toHaveProperty('hideOnMobile', false);
});
});
describe('SocialLinks.toHtml design tokens (#44)', () => {
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
const props = { ...((SocialLinks as any).craft.props as object) };
const { html } = (((SocialLinks) as any).toHtml)(props, '', 'node-tok');
const lower = html.toLowerCase();
for (const t of ['text', 'bg']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of ['#374151']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
});
});
+7 -6
View File
@@ -1,6 +1,7 @@
import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { tokenRef } from '../../utils/tokens';
import { escapeAttr, safeUrl, cssValue } from '../../utils/escape';
interface SocialLink {
@@ -87,8 +88,8 @@ const alignMap: Record<string, string> = {
export const SocialLinks: UserComponent<SocialLinksProps> = ({
links = defaultLinks,
iconSize = '20px',
iconColor = '#ffffff',
iconBgColor = '#374151',
iconColor = tokenRef('bg'),
iconBgColor = tokenRef('text'),
iconShape = 'circle',
gap = '10px',
alignment = 'center',
@@ -150,8 +151,8 @@ SocialLinks.craft = {
props: {
links: defaultLinks,
iconSize: '20px',
iconColor: '#ffffff',
iconBgColor: '#374151',
iconColor: tokenRef('bg'),
iconBgColor: tokenRef('text'),
iconShape: 'circle',
gap: '10px',
alignment: 'center',
@@ -175,8 +176,8 @@ SocialLinks.craft = {
const links = props.links || defaultLinks;
// Sanitized -- raw string-interpolation sinks in aStyle/getShapeStr below.
const iconSize = cssValue(props.iconSize) || '20px';
const iconColor = cssValue(props.iconColor) || '#ffffff';
const iconBgColor = cssValue(props.iconBgColor) || '#374151';
const iconColor = cssValue(props.iconColor) || tokenRef('bg');
const iconBgColor = cssValue(props.iconBgColor) || tokenRef('text');
const iconShape = props.iconShape || 'circle';
const gap = props.gap || '10px';
const alignment = props.alignment || 'center';
@@ -46,3 +46,13 @@ describe('StickyCallBar.toHtml', () => {
expect(body).toContain('href="tel:+15551234567"');
});
});
describe('StickyCallBar.toHtml design tokens (#44)', () => {
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
const props = { ...((StickyCallBar as any).craft.props as object) };
const { html } = (((StickyCallBar) as any).toHtml)(props, '', 'node-tok');
const lower = html.toLowerCase();
for (const t of ['bg']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of [] as string[]) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
});
});
@@ -42,7 +42,7 @@ export const StickyCallBar: UserComponent<StickyCallBarProps> = ({
directionsUrl = 'https://maps.google.com/?q=Your+Business',
directionsLabel = 'Directions',
bgColor = tokenRef('primary'),
textColor = '#ffffff',
textColor = tokenRef('bg'),
style = {},
}) => {
const { connectors: { connect, drag }, selected } = useNode((node) => ({ selected: node.events.selected }));
@@ -66,7 +66,7 @@ StickyCallBar.craft = {
directionsUrl: 'https://maps.google.com/?q=Your+Business',
directionsLabel: 'Directions',
bgColor: tokenRef('primary'),
textColor: '#ffffff',
textColor: tokenRef('bg'),
position: 'bottom',
style: {},
animation: '',
@@ -83,7 +83,7 @@ StickyCallBar.craft = {
// end of the page to compensate (documented, not automatic).
(StickyCallBar as any).toHtml = (props: StickyCallBarProps, _childrenHtml: string) => {
const bg = cssValue(props.bgColor) || tokenRef('primary');
const fg = cssValue(props.textColor) || '#ffffff';
const fg = cssValue(props.textColor) || tokenRef('bg');
const tel = telHref(props.phone);
const dir = httpUrl(props.directionsUrl);
const links: string[] = [];
@@ -72,3 +72,13 @@ describe('BackgroundSection.toHtml bgPosition focal point (#55)', () => {
expect(html).not.toContain('<script>alert(1)');
});
});
describe('BackgroundSection.toHtml design tokens (#44)', () => {
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
const props = { ...((BackgroundSection as any).craft.props as object) };
const { html } = (((BackgroundSection) as any).toHtml)(props, '', 'node-tok');
const lower = html.toLowerCase();
for (const t of ['text']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of ['#1e293b']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
});
});
@@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react';
import { useNode, Element, UserComponent } from '@craftjs/core';
import { Container } from './Container';
import { cssPropsToString } from '../../utils/style-helpers';
import { tokenRef } from '../../utils/tokens';
import { escapeAttr, cssValue } from '../../utils/escape';
interface BackgroundSectionProps {
@@ -20,7 +21,7 @@ interface BackgroundSectionProps {
export const BackgroundSection: UserComponent<BackgroundSectionProps> = ({
bgImage = '',
bgPosition = '',
bgColor = '#1e293b',
bgColor = tokenRef('text'),
overlayColor = '#000000',
overlayOpacity = 0.4,
innerMaxWidth = '1200px',
@@ -79,7 +80,7 @@ BackgroundSection.craft = {
props: {
bgImage: '',
bgPosition: '',
bgColor: '#1e293b',
bgColor: tokenRef('text'),
overlayColor: '#000000',
overlayOpacity: 0.4,
innerMaxWidth: '1200px',
@@ -100,7 +101,7 @@ BackgroundSection.craft = {
position: 'relative',
width: '100%',
minHeight: '200px',
backgroundColor: props.bgColor || '#1e293b',
backgroundColor: props.bgColor || tokenRef('text'),
backgroundImage: props.bgImage ? `url(${props.bgImage})` : undefined,
backgroundSize: 'cover',
backgroundPosition: cssValue(props.bgPosition) || 'center',
@@ -190,3 +190,13 @@ describe('ContentSlider.toHtml palette token fallback (Refs #44)', () => {
expect(html).not.toContain('#3b82f6');
});
});
describe('ContentSlider.toHtml design tokens (#44)', () => {
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
const props = { ...((ContentSlider as any).craft.props as object) };
const { html } = (((ContentSlider) as any).toHtml)(props, '', 'node-tok');
const lower = html.toLowerCase();
for (const t of ['primary', 'secondary', 'success', 'warning', 'error', 'text', 'bg']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of ['#3b82f6', '#8b5cf6', '#10b981', '#059669', '#f59e0b', '#ef4444', '#18181b']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
});
});
@@ -29,6 +29,9 @@ interface ContentSliderProps {
hideOnMobile?: boolean;
}
// White heading/subtitle/dots stay literal: they sit on a slide gradient or
// image, where no token means "text on imagery" (same rule as CTASection's
// WHITE_ON_GRADIENT); the translucent-white arrow discs are scrims.
const defaultSlides: Slide[] = [
{
type: 'image',
@@ -37,7 +40,7 @@ const defaultSlides: Slide[] = [
text: 'Welcome to our showcase',
buttonText: 'Learn More',
buttonHref: '#',
bgColor: 'linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%)',
bgColor: `linear-gradient(135deg, ${tokenRef('primary')} 0%, ${tokenRef('secondary')} 100%)`,
},
{
type: 'image',
@@ -46,7 +49,7 @@ const defaultSlides: Slide[] = [
text: 'Discover something amazing',
buttonText: 'Get Started',
buttonHref: '#',
bgColor: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
bgColor: `linear-gradient(135deg, ${tokenRef('success')} 0%, ${tokenRef('accent')} 100%)`,
},
{
type: 'image',
@@ -55,7 +58,7 @@ const defaultSlides: Slide[] = [
text: 'Build your future today',
buttonText: 'Contact Us',
buttonHref: '#',
bgColor: 'linear-gradient(135deg, #f59e0b 0%, #ef4444 100%)',
bgColor: `linear-gradient(135deg, ${tokenRef('warning')} 0%, ${tokenRef('error')} 100%)`,
},
];
@@ -102,7 +105,7 @@ export const ContentSlider: UserComponent<ContentSliderProps> = ({
borderRadius: '50%',
border: 'none',
background: 'rgba(255,255,255,0.9)',
color: '#18181b',
color: tokenRef('text'),
fontSize: '16px',
cursor: 'pointer',
display: 'flex',
@@ -156,8 +159,8 @@ export const ContentSlider: UserComponent<ContentSliderProps> = ({
style={{
display: 'inline-block',
padding: '12px 28px',
background: '#ffffff',
color: '#18181b',
background: tokenRef('bg'),
color: tokenRef('text'),
textDecoration: 'none',
borderRadius: '8px',
fontWeight: '600',
@@ -313,7 +316,7 @@ ContentSlider.craft = {
contentParts.push(`<p style="font-size:18px;color:rgba(255,255,255,0.9);margin-bottom:20px;font-family:Inter,sans-serif;text-shadow:0 1px 4px rgba(0,0,0,0.3)">${escapeHtml(slide.text)}</p>`);
}
if (slide.buttonText) {
contentParts.push(`<a href="${escapeAttr(safeUrl(slide.buttonHref || '#'))}" style="display:inline-block;padding:12px 28px;background:#ffffff;color:#18181b;text-decoration:none;border-radius:8px;font-weight:600;font-size:15px;font-family:Inter,sans-serif">${escapeHtml(slide.buttonText)}</a>`);
contentParts.push(`<a href="${escapeAttr(safeUrl(slide.buttonHref || '#'))}" style="display:inline-block;padding:12px 28px;background:${tokenRef('bg')};color:${tokenRef('text')};text-decoration:none;border-radius:8px;font-weight:600;font-size:15px;font-family:Inter,sans-serif">${escapeHtml(slide.buttonText)}</a>`);
}
const innerHtml = contentParts.length > 0
@@ -326,8 +329,8 @@ ContentSlider.craft = {
}).join('\n ');
const arrowsHtml = showArrows && items.length > 1
? `<button onclick="${uid}_prev()" aria-label="Previous slide" style="position:absolute;top:50%;left:16px;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.9);color:#18181b;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.15)"><i class="fa fa-chevron-left" aria-hidden="true"></i></button>
<button onclick="${uid}_next()" aria-label="Next slide" style="position:absolute;top:50%;right:16px;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.9);color:#18181b;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.15)"><i class="fa fa-chevron-right" aria-hidden="true"></i></button>`
? `<button onclick="${uid}_prev()" aria-label="Previous slide" style="position:absolute;top:50%;left:16px;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.9);color:${tokenRef('text')};font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.15)"><i class="fa fa-chevron-left" aria-hidden="true"></i></button>
<button onclick="${uid}_next()" aria-label="Next slide" style="position:absolute;top:50%;right:16px;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.9);color:${tokenRef('text')};font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.15)"><i class="fa fa-chevron-right" aria-hidden="true"></i></button>`
: '';
const dotsHtml = showDots && items.length > 1
@@ -73,3 +73,13 @@ describe('Countdown.craft.props includes the box-model/animation/visibility roll
expect(props).toHaveProperty('hideOnMobile', false);
});
});
describe('Countdown.toHtml design tokens (#44)', () => {
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
const props = { ...((Countdown as any).craft.props as object) };
const { html } = (((Countdown) as any).toHtml)(props, '', 'node-tok');
const lower = html.toLowerCase();
for (const t of ['text', 'bg']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of ['#18181b', 'rgba(255,255,255,0.7)']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
});
});
+14 -9
View File
@@ -1,8 +1,13 @@
import React, { CSSProperties, useEffect, useState } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { tokenRef } from '../../utils/tokens';
import { escapeHtml, escapeAttr, scopeId, cssValue } from '../../utils/escape';
// Label text on the (text-token) band: the bg token at 70% so it stays
// contrast-safe under any palette.
const LABEL_ON_BAND = `color-mix(in srgb, ${tokenRef('bg')} 70%, transparent)`;
interface CountdownProps {
targetDate?: string;
heading?: string;
@@ -48,9 +53,9 @@ export const Countdown: UserComponent<CountdownProps> = ({
targetDate = DEFAULT_TARGET,
heading = 'Coming Soon',
style = {},
digitColor = '#ffffff',
labelColor = 'rgba(255,255,255,0.7)',
bgColor = '#18181b',
digitColor = tokenRef('bg'),
labelColor = LABEL_ON_BAND,
bgColor = tokenRef('text'),
anchorId,
}) => {
const {
@@ -140,9 +145,9 @@ Countdown.craft = {
targetDate: DEFAULT_TARGET,
heading: 'Coming Soon',
style: {},
digitColor: '#ffffff',
labelColor: 'rgba(255,255,255,0.7)',
bgColor: '#18181b',
digitColor: tokenRef('bg'),
labelColor: LABEL_ON_BAND,
bgColor: tokenRef('text'),
anchorId: '',
animation: '',
animationDelay: '',
@@ -164,12 +169,12 @@ Countdown.craft = {
targetDate = DEFAULT_TARGET,
heading = 'Coming Soon',
style = {},
bgColor = '#18181b',
bgColor = tokenRef('text'),
} = props;
// Sanitized -- raw string-interpolation sinks in the heading/digit/label
// style attributes below.
const digitColor = cssValue(props.digitColor) || '#ffffff';
const labelColor = cssValue(props.labelColor) || 'rgba(255,255,255,0.7)';
const digitColor = cssValue(props.digitColor) || tokenRef('bg');
const labelColor = cssValue(props.labelColor) || LABEL_ON_BAND;
const sectionStyle = cssPropsToString({
padding: '60px 20px',
@@ -65,3 +65,24 @@ describe('HeroSimple.toHtml bgPosition focal point (#55)', () => {
expect(html).not.toContain('<script>alert(1)');
});
});
describe('HeroSimple.toHtml design tokens (#44)', () => {
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
const props = { ...((HeroSimple as any).craft.props as object) };
const { html } = (((HeroSimple) as any).toHtml)(props, '', 'node-tok');
const lower = html.toLowerCase();
for (const t of ['text', 'bg']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of ['#1e293b', '#667eea', '#764ba2']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
});
});
describe('HeroSimple.toHtml gradient defaults are tokens (#44)', () => {
test('bgType gradient with the default stops uses primary -> secondary, not #667eea/#764ba2', () => {
const props = { ...((HeroSimple as any).craft.props as object), bgType: 'gradient' };
const { html } = toHtml(props, '', 'node-g');
expect(html).toContain('var(--wsb-primary)');
expect(html).toContain('var(--wsb-secondary)');
expect(html.toLowerCase()).not.toContain('#667eea');
expect(html.toLowerCase()).not.toContain('#764ba2');
});
});
+16 -16
View File
@@ -40,12 +40,12 @@ interface HeroProps {
function buildBackground(props: HeroProps): string {
switch (props.bgType) {
case 'gradient':
return `linear-gradient(${props.bgGradientAngle || 135}deg, ${props.bgGradientFrom || '#667eea'}, ${props.bgGradientTo || '#764ba2'})`;
return `linear-gradient(${props.bgGradientAngle || 135}deg, ${props.bgGradientFrom || tokenRef('primary')}, ${props.bgGradientTo || tokenRef('secondary')})`;
case 'image':
return props.bgImage ? `url('${props.bgImage}') center/cover no-repeat` : '#1e293b';
return props.bgImage ? `url('${props.bgImage}') center/cover no-repeat` : tokenRef('text');
case 'color':
default:
return props.bgColor || '#1e293b';
return props.bgColor || tokenRef('text');
}
}
@@ -58,18 +58,18 @@ export const HeroSimple: UserComponent<HeroProps> = ({
secondaryButtonText,
secondaryButtonHref,
bgType = 'color',
bgColor = '#1e293b',
bgGradientFrom = '#667eea',
bgGradientTo = '#764ba2',
bgColor = tokenRef('text'),
bgGradientFrom = tokenRef('primary'),
bgGradientTo = tokenRef('secondary'),
bgGradientAngle = 135,
bgImage = '',
bgPosition = '',
bgVideo = '',
overlayColor = '#000000',
overlayOpacity = 0,
textColor = '#ffffff',
textColor = tokenRef('bg'),
buttonBgColor = tokenRef('primary'),
buttonTextColor = '#ffffff',
buttonTextColor = tokenRef('bg'),
minHeight = '500px',
verticalAlign = 'center',
textAlign = 'center',
@@ -177,18 +177,18 @@ HeroSimple.craft = {
{ text: 'Get Started', href: '#', variant: 'primary' },
] as CtaButton[],
bgType: 'color',
bgColor: '#1e293b',
bgGradientFrom: '#667eea',
bgGradientTo: '#764ba2',
bgColor: tokenRef('text'),
bgGradientFrom: tokenRef('primary'),
bgGradientTo: tokenRef('secondary'),
bgGradientAngle: 135,
bgImage: '',
bgPosition: '',
bgVideo: '',
overlayColor: '#000000',
overlayOpacity: 0,
textColor: '#ffffff',
textColor: tokenRef('bg'),
buttonBgColor: tokenRef('primary'),
buttonTextColor: '#ffffff',
buttonTextColor: tokenRef('bg'),
minHeight: '500px',
verticalAlign: 'center',
textAlign: 'center',
@@ -246,12 +246,12 @@ HeroSimple.craft = {
const ctas = normalizeCtas(props);
const buttonsHtml = ctasToHtml(ctas, {
primaryBg: cssValue(props.buttonBgColor) || tokenRef('primary'),
primaryText: props.buttonTextColor || '#fff',
outlineText: props.textColor || '#fff',
primaryText: props.buttonTextColor || tokenRef('bg'),
outlineText: props.textColor || tokenRef('bg'),
});
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
const heroTextColor = cssValue(props.textColor) || '#fff';
const heroTextColor = cssValue(props.textColor) || tokenRef('bg');
return {
html: `<section${idAttr} style="${sectionStyle}">
${videoHtml}${overlayHtml}
@@ -27,7 +27,7 @@ const FONT_KEYS = ['headingFont', 'bodyFont'] as const;
* literal still fails.
*/
const COMPONENT_DEFAULT_HEX: Record<string, Record<string, number>> = {
medical: { '#ffffff': 4 },
medical: { '#ffffff': 1 },
portfolio: { '#ffffff': 1 },
photography: {},
'content-creator': {},