diff --git a/craft/src/components/sections/ContentSlider.toHtml.test.ts b/craft/src/components/sections/ContentSlider.toHtml.test.ts index 0fcce45..9dec314 100644 --- a/craft/src/components/sections/ContentSlider.toHtml.test.ts +++ b/craft/src/components/sections/ContentSlider.toHtml.test.ts @@ -182,3 +182,11 @@ describe('ContentSlider.craft.props exposes the animation/visibility rollout', ( expect(style.opacity).toBe('1'); }); }); + +describe('ContentSlider.toHtml palette token fallback (Refs #44)', () => { + test('a slide without bgColor falls back to the primary token', () => { + const { html } = toHtml({ slides: [{ type: 'image' as const, heading: 'One' }] }, ''); + expect(html).toContain('var(--wsb-primary)'); + expect(html).not.toContain('#3b82f6'); + }); +}); diff --git a/craft/src/components/sections/ContentSlider.tsx b/craft/src/components/sections/ContentSlider.tsx index 37bc94f..5778f0b 100644 --- a/craft/src/components/sections/ContentSlider.tsx +++ b/craft/src/components/sections/ContentSlider.tsx @@ -2,6 +2,7 @@ import React, { CSSProperties, useState, useEffect, useRef, useCallback } from ' import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { escapeHtml, escapeAttr, safeUrl, safeImageUrl, scopeId, cssValue } from '../../utils/escape'; +import { tokenRef } from '../../utils/tokens'; interface Slide { type: 'image' | 'content'; @@ -116,7 +117,7 @@ export const ContentSlider: UserComponent = ({ ? { backgroundImage: `url(${slide.imageSrc})`, backgroundSize: 'cover', backgroundPosition: 'center' } : slide.bgColor?.startsWith('linear-gradient') ? { backgroundImage: slide.bgColor } - : { backgroundColor: slide.bgColor || '#3b82f6' }; + : { backgroundColor: slide.bgColor || tokenRef('primary') }; return (
{ + test('feature button background is the primary token', () => { + const features = [{ title: 'F', description: 'D', buttonText: 'More', buttonUrl: '/x' }]; + const { html } = toHtml({ features }, ''); + expect(html).toContain('background:var(--wsb-primary)'); + expect(html).not.toContain('#3b82f6'); + }); +}); diff --git a/craft/src/components/sections/FeaturesGrid.tsx b/craft/src/components/sections/FeaturesGrid.tsx index c8176be..63d117e 100644 --- a/craft/src/components/sections/FeaturesGrid.tsx +++ b/craft/src/components/sections/FeaturesGrid.tsx @@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { escapeHtml, escapeAttr, safeUrl, safeImageUrl } from '../../utils/escape'; +import { tokenRef } from '../../utils/tokens'; interface FeatureItem { title: string; @@ -83,7 +84,7 @@ export const FeaturesGrid: UserComponent = ({ e.preventDefault()} - style={{ display: 'inline-block', marginTop: '16px', padding: '10px 24px', background: '#3b82f6', color: '#fff', borderRadius: '8px', textDecoration: 'none', fontSize: '14px', fontWeight: 600 }} + style={{ display: 'inline-block', marginTop: '16px', padding: '10px 24px', background: tokenRef('primary'), color: '#fff', borderRadius: '8px', textDecoration: 'none', fontSize: '14px', fontWeight: 600 }} > {feat.buttonText} @@ -129,7 +130,7 @@ FeaturesGrid.craft = { ? `${escapeAttr(feat.imageAlt || feat.title || '')}` : `
${escapeHtml(feat.icon)}
`; const button = feat.buttonText - ? `\n ${escapeHtml(feat.buttonText)}` + ? `\n ${escapeHtml(feat.buttonText)}` : ''; return `
${media} diff --git a/craft/src/components/sections/HeroSimple.toHtml.test.ts b/craft/src/components/sections/HeroSimple.toHtml.test.ts index 6362da9..59370e2 100644 --- a/craft/src/components/sections/HeroSimple.toHtml.test.ts +++ b/craft/src/components/sections/HeroSimple.toHtml.test.ts @@ -33,3 +33,18 @@ describe('HeroSimple.toHtml textAlign enum sink (attacker-controlled prop, not e expect(html).toContain('text-align:center'); }); }); + +describe('HeroSimple.toHtml palette token fallback (Refs #44)', () => { + test('buttonBgColor absent/empty -> primary token on the CTA', () => { + for (const buttonBgColor of [undefined, '']) { + const { html } = toHtml({ heading: 'H', buttonText: 'Go', buttonLink: '#', buttonBgColor }, ''); + expect(html).toContain('var(--wsb-primary)'); + expect(html).not.toContain('#3b82f6'); + } + }); + test('explicit buttonBgColor wins', () => { + const { html } = toHtml({ heading: 'H', buttonText: 'Go', buttonLink: '#', buttonBgColor: '#ff0000' }, ''); + expect(html).toContain('#ff0000'); + expect(html).not.toContain('var(--wsb-primary)'); + }); +}); diff --git a/craft/src/components/sections/HeroSimple.tsx b/craft/src/components/sections/HeroSimple.tsx index fc6d12d..6474e41 100644 --- a/craft/src/components/sections/HeroSimple.tsx +++ b/craft/src/components/sections/HeroSimple.tsx @@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { CtaButton, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers'; import { escapeHtml, escapeAttr, safeUrl, cssValue } from '../../utils/escape'; +import { tokenRef } from '../../utils/tokens'; interface HeroProps { heading?: string; @@ -64,7 +65,7 @@ export const HeroSimple: UserComponent = ({ overlayColor = '#000000', overlayOpacity = 0, textColor = '#ffffff', - buttonBgColor = '#3b82f6', + buttonBgColor = tokenRef('primary'), buttonTextColor = '#ffffff', minHeight = '500px', verticalAlign = 'center', @@ -182,7 +183,7 @@ HeroSimple.craft = { overlayColor: '#000000', overlayOpacity: 0, textColor: '#ffffff', - buttonBgColor: '#3b82f6', + buttonBgColor: tokenRef('primary'), buttonTextColor: '#ffffff', minHeight: '500px', verticalAlign: 'center', @@ -240,7 +241,7 @@ HeroSimple.craft = { const ctas = normalizeCtas(props); const buttonsHtml = ctasToHtml(ctas, { - primaryBg: props.buttonBgColor || '#3b82f6', + primaryBg: cssValue(props.buttonBgColor) || tokenRef('primary'), primaryText: props.buttonTextColor || '#fff', outlineText: props.textColor || '#fff', }); diff --git a/craft/src/components/sections/NumberCounter.toHtml.test.ts b/craft/src/components/sections/NumberCounter.toHtml.test.ts index e20a5e3..5a7cec9 100644 --- a/craft/src/components/sections/NumberCounter.toHtml.test.ts +++ b/craft/src/components/sections/NumberCounter.toHtml.test.ts @@ -84,3 +84,13 @@ describe('NumberCounter.craft.props includes the box-model/animation/visibility expect(props).toHaveProperty('hideOnMobile', false); }); }); + +describe('NumberCounter.toHtml palette token fallbacks (Refs #44)', () => { + test('number -> primary token, label -> muted token when colours are absent', () => { + const { html } = toHtml({ counters }, ''); + expect(html).toContain('color:var(--wsb-primary)'); + expect(html).toContain('color:var(--wsb-muted)'); + expect(html).not.toContain('#3b82f6'); + expect(html).not.toContain('#6b7280'); + }); +}); diff --git a/craft/src/components/sections/NumberCounter.tsx b/craft/src/components/sections/NumberCounter.tsx index 81fee8c..132f78a 100644 --- a/craft/src/components/sections/NumberCounter.tsx +++ b/craft/src/components/sections/NumberCounter.tsx @@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { escapeHtml, escapeAttr, scopeId, cssValue } from '../../utils/escape'; +import { tokenRef } from '../../utils/tokens'; interface Counter { number: number; @@ -33,8 +34,8 @@ const defaultCounters: Counter[] = [ export const NumberCounter: UserComponent = ({ counters = defaultCounters, columns = 4, - numberColor = '#3b82f6', - labelColor = '#6b7280', + numberColor = tokenRef('primary'), + labelColor = tokenRef('muted'), numberSize = '48px', style = {}, }) => { @@ -100,8 +101,8 @@ NumberCounter.craft = { props: { counters: defaultCounters, columns: 4, - numberColor: '#3b82f6', - labelColor: '#6b7280', + numberColor: tokenRef('primary'), + labelColor: tokenRef('muted'), numberSize: '48px', style: { backgroundColor: '#ffffff' }, animation: '', @@ -130,8 +131,8 @@ NumberCounter.craft = { const columns = Number(props.columns) || 4; // Sanitized -- raw string-interpolation sinks in the counter/label spans // below. - const numberColor = cssValue(props.numberColor) || '#3b82f6'; - const labelColor = cssValue(props.labelColor) || '#6b7280'; + const numberColor = cssValue(props.numberColor) || tokenRef('primary'); + const labelColor = cssValue(props.labelColor) || tokenRef('muted'); const numberSize = cssValue(props.numberSize) || '48px'; const items = counters.length > 0 ? counters : defaultCounters; diff --git a/craft/src/components/sections/PricingTable.toHtml.test.ts b/craft/src/components/sections/PricingTable.toHtml.test.ts index 52463cb..9f4b6a8 100644 --- a/craft/src/components/sections/PricingTable.toHtml.test.ts +++ b/craft/src/components/sections/PricingTable.toHtml.test.ts @@ -47,7 +47,7 @@ describe('PricingTable.toHtml regular-card color overrides (previously hard-code expect(html).toContain('color:#18181b'); expect(html).toContain('color:#64748b'); expect(html).toContain('color:#4b5563'); - expect(html).toContain('color:#10b981'); + expect(html).toContain('color:var(--wsb-success)'); }); test('unset btnBg falls back to featuredBg (original derivation)', () => { @@ -103,3 +103,18 @@ describe('PricingTable.craft.props includes the box-model/animation/visibility r expect(props).toHaveProperty('hideOnMobile', false); }); }); + +describe('PricingTable.toHtml palette token fallbacks (Refs #44)', () => { + test('featured card -> primary token, tick -> success token when colours are absent', () => { + const { html } = toHtml({ plans }, ''); + expect(html).toContain('background-color:var(--wsb-primary)'); + expect(html).toContain('color:var(--wsb-success)'); + expect(html).not.toContain('#3b82f6'); + expect(html).not.toContain('#10b981'); + }); + test('explicit featuredBg / checkColor win', () => { + const { html } = toHtml({ plans, featuredBg: '#111111', checkColor: '#222222' }, ''); + expect(html).toContain('background-color:#111111'); + expect(html).toContain('color:#222222'); + }); +}); diff --git a/craft/src/components/sections/PricingTable.tsx b/craft/src/components/sections/PricingTable.tsx index 986bc1b..7743c7b 100644 --- a/craft/src/components/sections/PricingTable.tsx +++ b/craft/src/components/sections/PricingTable.tsx @@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { escapeHtml, escapeAttr, safeUrl, cssValue } from '../../utils/escape'; +import { tokenRef } from '../../utils/tokens'; interface PricingPlan { name: string; @@ -75,7 +76,7 @@ const defaultPlans: PricingPlan[] = [ export const PricingTable: UserComponent = ({ plans = defaultPlans, style = {}, - featuredBg = '#3b82f6', + featuredBg = tokenRef('primary'), bulletType = 'check', anchorId, cardBg, @@ -97,7 +98,7 @@ export const PricingTable: UserComponent = ({ const regTextColor = textColor || '#18181b'; const regSubColor = subColor || '#64748b'; const regFeatColor = featColor || '#4b5563'; - const regCheckColor = checkColor || '#10b981'; + const regCheckColor = checkColor || tokenRef('success'); const regBtnBg = btnBg || featuredBg; const regBtnColor = btnColor || '#ffffff'; @@ -236,7 +237,7 @@ PricingTable.craft = { props: { plans: defaultPlans, style: { backgroundColor: '#ffffff' }, - featuredBg: '#3b82f6', + featuredBg: tokenRef('primary'), bulletType: 'check', anchorId: '', cardBg: '', @@ -271,7 +272,7 @@ PricingTable.craft = { const plans = props.plans || defaultPlans; // Sanitized -- featuredBg is a raw string-interpolation sink below (drives // cardBg/btnBg/btnColor, all raw-interpolated into style="..."). - const featuredBg = cssValue(props.featuredBg) || '#3b82f6'; + const featuredBg = cssValue(props.featuredBg) || tokenRef('primary'); // Sanitized -- regular (non-featured) card color overrides, all raw // string-interpolation sinks into style="..." below. Each falls back to // the exact literal the card was previously hard-coded to (or, for the @@ -281,7 +282,7 @@ PricingTable.craft = { const regTextColor = cssValue(props.textColor) || '#18181b'; const regSubColor = cssValue(props.subColor) || '#64748b'; const regFeatColor = cssValue(props.featColor) || '#4b5563'; - const regCheckColor = cssValue(props.checkColor) || '#10b981'; + const regCheckColor = cssValue(props.checkColor) || tokenRef('success'); const regBtnBg = cssValue(props.btnBg) || featuredBg; const regBtnColor = cssValue(props.btnColor) || '#ffffff'; diff --git a/craft/src/components/sections/Tabs.toHtml.test.ts b/craft/src/components/sections/Tabs.toHtml.test.ts index 807f3dc..0abe735 100644 --- a/craft/src/components/sections/Tabs.toHtml.test.ts +++ b/craft/src/components/sections/Tabs.toHtml.test.ts @@ -91,3 +91,11 @@ describe('Tabs.craft.props includes the box-model/animation/visibility rollout p expect(props).toHaveProperty('hideOnMobile', false); }); }); + +describe('Tabs.toHtml palette token fallback (Refs #44)', () => { + test('activeTabBg absent -> primary token', () => { + const { html } = toHtml({ tabs }, ''); + expect(html).toContain('var(--wsb-primary)'); + expect(html).not.toContain('#3b82f6'); + }); +}); diff --git a/craft/src/components/sections/Tabs.tsx b/craft/src/components/sections/Tabs.tsx index 1cce208..9860857 100644 --- a/craft/src/components/sections/Tabs.tsx +++ b/craft/src/components/sections/Tabs.tsx @@ -2,6 +2,7 @@ import React, { CSSProperties, useState } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { escapeHtml, escapeAttr, scopeId, cssValue } from '../../utils/escape'; +import { tokenRef } from '../../utils/tokens'; interface TabItem { label: string; @@ -33,7 +34,7 @@ const defaultTabs: TabItem[] = [ export const Tabs: UserComponent = ({ tabs = defaultTabs, style = {}, - activeTabBg = '#3b82f6', + activeTabBg = tokenRef('primary'), activeTabColor = '#ffffff', inactiveTabBg = '#f1f5f9', inactiveTabColor = '#64748b', @@ -113,7 +114,7 @@ Tabs.craft = { props: { tabs: defaultTabs, style: { backgroundColor: '#ffffff' }, - activeTabBg: '#3b82f6', + activeTabBg: tokenRef('primary'), activeTabColor: '#ffffff', inactiveTabBg: '#f1f5f9', inactiveTabColor: '#64748b', @@ -145,7 +146,7 @@ Tabs.craft = { // attributes AND into an inline