diff --git a/craft/src/components/basic/ButtonLink.toHtml.test.ts b/craft/src/components/basic/ButtonLink.toHtml.test.ts index cc90303..2b40c18 100644 --- a/craft/src/components/basic/ButtonLink.toHtml.test.ts +++ b/craft/src/components/basic/ButtonLink.toHtml.test.ts @@ -118,3 +118,11 @@ describe('ButtonLink.craft.props exposes target + hover + box-model + animation/ expect(style.opacity).toBe('1'); }); }); + +describe('ButtonLink.toHtml palette token default (Refs #44)', () => { + test('a new button (craft default props) paints the primary token', () => { + const { html } = toHtml({ ...(ButtonLink as any).craft.props }, ''); + expect(html).toContain('background-color:var(--wsb-primary)'); + expect(html).not.toContain('#3b82f6'); + }); +}); diff --git a/craft/src/components/basic/ButtonLink.tsx b/craft/src/components/basic/ButtonLink.tsx index a43a6cd..53036d7 100644 --- a/craft/src/components/basic/ButtonLink.tsx +++ b/craft/src/components/basic/ButtonLink.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, scopeId } from '../../utils/escape'; +import { tokenRef } from '../../utils/tokens'; interface ButtonLinkProps { text?: string; @@ -75,7 +76,7 @@ ButtonLink.craft = { hoverBg: '', hoverColor: '', style: { - backgroundColor: '#3b82f6', + backgroundColor: tokenRef('primary'), color: '#ffffff', padding: '12px 24px', borderRadius: '8px', diff --git a/craft/src/components/basic/Icon.toHtml.test.ts b/craft/src/components/basic/Icon.toHtml.test.ts index 38b134d..017d801 100644 --- a/craft/src/components/basic/Icon.toHtml.test.ts +++ b/craft/src/components/basic/Icon.toHtml.test.ts @@ -70,3 +70,15 @@ describe('Icon.craft.props includes the box-model/animation/visibility rollout p expect(props).toHaveProperty('hideOnMobile', false); }); }); + +describe('Icon.toHtml palette token fallback (Refs #44)', () => { + test('colour absent -> primary token', () => { + const { html } = toHtml({ icon: 'fa-star', size: '32px' }, ''); + expect(html).toContain('color:var(--wsb-primary)'); + expect(html).not.toContain('#3b82f6'); + }); + test('an explicit colour still wins', () => { + const { html } = toHtml({ icon: 'fa-star', size: '32px', color: '#ff0000' }, ''); + expect(html).toContain('color:#ff0000'); + }); +}); diff --git a/craft/src/components/basic/Icon.tsx b/craft/src/components/basic/Icon.tsx index ce11185..ec0d1aa 100644 --- a/craft/src/components/basic/Icon.tsx +++ b/craft/src/components/basic/Icon.tsx @@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { escapeAttr, safeUrl } from '../../utils/escape'; +import { tokenRef } from '../../utils/tokens'; interface IconProps { icon?: string; @@ -29,7 +30,7 @@ function getBgBorderRadius(shape: string): string { export const Icon: UserComponent = ({ icon = 'fa-star', size = '32px', - color = '#3b82f6', + color = tokenRef('primary'), bgColor = 'transparent', bgShape = 'none', bgSize = '56px', @@ -95,7 +96,7 @@ Icon.craft = { props: { icon: 'fa-star', size: '32px', - color: '#3b82f6', + color: tokenRef('primary'), bgColor: 'transparent', bgShape: 'none', bgSize: '56px', @@ -120,7 +121,7 @@ Icon.craft = { const { icon = 'fa-star', size = '32px', - color = '#3b82f6', + color = tokenRef('primary'), bgColor = 'transparent', bgShape = 'none', bgSize = '56px', diff --git a/craft/src/components/basic/SearchBar.toHtml.test.ts b/craft/src/components/basic/SearchBar.toHtml.test.ts index b379217..52fa9fe 100644 --- a/craft/src/components/basic/SearchBar.toHtml.test.ts +++ b/craft/src/components/basic/SearchBar.toHtml.test.ts @@ -73,3 +73,13 @@ describe('SearchBar.craft.props includes animation/visibility defaults', () => { }); }); }); + +describe('SearchBar.toHtml palette token fallbacks (Refs #44)', () => { + test('button background is the primary token and input text is the text token', () => { + const { html } = toHtml({}, ''); + expect(html).toContain('background-color:var(--wsb-primary)'); + expect(html).toContain('color:var(--wsb-text)'); + expect(html).not.toContain('#3b82f6'); + expect(html).not.toContain('#1f2937'); + }); +}); diff --git a/craft/src/components/basic/SearchBar.tsx b/craft/src/components/basic/SearchBar.tsx index d072b66..16db6ff 100644 --- a/craft/src/components/basic/SearchBar.tsx +++ b/craft/src/components/basic/SearchBar.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 } from '../../utils/escape'; +import { tokenRef } from '../../utils/tokens'; interface SearchBarProps { placeholder?: string; @@ -73,7 +74,7 @@ export const SearchBar: UserComponent = ({ border: '1px solid #d1d5db', borderRadius: showButton ? '8px 0 0 8px' : '8px', backgroundColor: '#ffffff', - color: '#1f2937', + color: tokenRef('text'), outline: 'none', boxSizing: 'border-box', }} @@ -88,7 +89,7 @@ export const SearchBar: UserComponent = ({ fontWeight: '600', fontFamily: 'Inter, sans-serif', color: '#ffffff', - backgroundColor: '#3b82f6', + backgroundColor: tokenRef('primary'), border: 'none', borderRadius: '0 8px 8px 0', cursor: 'pointer', @@ -147,10 +148,10 @@ SearchBar.craft = { ...style, }); - const inputStyleStr = `width:100%;padding:12px 16px 12px 40px;font-size:15px;font-family:Inter,sans-serif;border:1px solid #d1d5db;border-radius:${showButton ? '8px 0 0 8px' : '8px'};background-color:#ffffff;color:#1f2937;outline:none;box-sizing:border-box`; + const inputStyleStr = `width:100%;padding:12px 16px 12px 40px;font-size:15px;font-family:Inter,sans-serif;border:1px solid #d1d5db;border-radius:${showButton ? '8px 0 0 8px' : '8px'};background-color:#ffffff;color:${tokenRef('text')};outline:none;box-sizing:border-box`; const btnHtml = showButton - ? `` + ? `` : ''; // F2: previously a purely decorative
-- no action/method/input diff --git a/craft/src/components/forms/ContactForm.toHtml.test.ts b/craft/src/components/forms/ContactForm.toHtml.test.ts index d32201b..2dd75ad 100644 --- a/craft/src/components/forms/ContactForm.toHtml.test.ts +++ b/craft/src/components/forms/ContactForm.toHtml.test.ts @@ -286,3 +286,17 @@ describe('ContactForm.toHtml destination marker', () => { expect(out.html).toContain('name="_gotcha"'); }); }); + +describe('ContactForm.toHtml palette token fallbacks (Refs #44)', () => { + test('submitColor empty -> primary token; input text -> text token', () => { + const { html } = toHtml({ recipientEmail: 'a@b.com', fields: [{ type: 'text', label: 'Name', name: 'name' }], submitColor: '' }, ''); + expect(html).toContain('background-color:var(--wsb-primary)'); + expect(html).toContain('color:var(--wsb-text)'); + expect(html).not.toContain('#3b82f6'); + expect(html).not.toContain('#1f2937'); + }); + test('explicit submitColor wins', () => { + const { html } = toHtml({ recipientEmail: 'a@b.com', fields: [], submitColor: '#ff0000' }, ''); + expect(html).toContain('background-color:#ff0000'); + }); +}); diff --git a/craft/src/components/forms/ContactForm.tsx b/craft/src/components/forms/ContactForm.tsx index f525efa..a19acb4 100644 --- a/craft/src/components/forms/ContactForm.tsx +++ b/craft/src/components/forms/ContactForm.tsx @@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { relayFormWiring } from '../../utils/form-relay-wiring'; import { escapeHtml, escapeAttr, slugId, cssValue, sanitizeInputType } from '../../utils/escape'; +import { tokenRef } from '../../utils/tokens'; // The allowlist enforced at export time lives in `sanitizeInputType` // (utils/escape.ts) -- this union is a superset (it also covers 'textarea' @@ -61,7 +62,7 @@ const defaultFields: ContactFormField[] = [ export const ContactForm: UserComponent = ({ fields = defaultFields, submitText = 'Send Message', - submitColor = '#3b82f6', + submitColor = tokenRef('primary'), formAction = '#', successMessage = 'Thank you! We\'ll get back to you soon.', style = {}, @@ -84,7 +85,7 @@ export const ContactForm: UserComponent = ({ border: `1px solid ${inputBorder}`, borderRadius: '6px', backgroundColor: inputBg, - color: '#1f2937', + color: tokenRef('text'), boxSizing: 'border-box', outline: 'none', }; @@ -168,7 +169,7 @@ ContactForm.craft = { props: { fields: defaultFields, submitText: 'Send Message', - submitColor: '#3b82f6', + submitColor: tokenRef('primary'), formAction: '#', successMessage: 'Thank you! We\'ll get back to you soon.', style: { @@ -215,7 +216,7 @@ ContactForm.craft = { const labelColor = cssValue(props.labelColor) || '#374151'; const inputBg = cssValue(props.inputBg) || '#ffffff'; const inputBorder = cssValue(props.inputBorder) || '#d1d5db'; - const inputStyleStr = `width:100%;padding:10px 14px;font-size:14px;font-family:Inter,sans-serif;border:1px solid ${inputBorder};border-radius:6px;background-color:${inputBg};color:#1f2937;box-sizing:border-box;outline:none`; + const inputStyleStr = `width:100%;padding:10px 14px;font-size:14px;font-family:Inter,sans-serif;border:1px solid ${inputBorder};border-radius:6px;background-color:${inputBg};color:${tokenRef('text')};box-sizing:border-box;outline:none`; const fieldsHtml = (props.fields || defaultFields).map((field, i) => { const reqStar = field.required ? '*' : ''; @@ -242,7 +243,7 @@ ContactForm.craft = { fontWeight: '600', fontFamily: 'Inter, sans-serif', color: '#ffffff', - backgroundColor: props.submitColor || '#3b82f6', + backgroundColor: cssValue(props.submitColor) || tokenRef('primary'), border: 'none', borderRadius: '8px', cursor: 'pointer', diff --git a/craft/src/components/forms/FormButton.toHtml.test.ts b/craft/src/components/forms/FormButton.toHtml.test.ts index 68deb13..421b80e 100644 --- a/craft/src/components/forms/FormButton.toHtml.test.ts +++ b/craft/src/components/forms/FormButton.toHtml.test.ts @@ -38,3 +38,11 @@ describe('FormButton.craft.props includes animation/visibility defaults', () => }); }); }); + +describe('FormButton.toHtml palette token default (Refs #44)', () => { + test('a new form button (craft default props) paints the primary token', () => { + const { html } = toHtml({ ...(FormButton as any).craft.props }, ''); + expect(html).toContain('background-color:var(--wsb-primary)'); + expect(html).not.toContain('#3b82f6'); + }); +}); diff --git a/craft/src/components/forms/FormButton.tsx b/craft/src/components/forms/FormButton.tsx index d76491b..41444fd 100644 --- a/craft/src/components/forms/FormButton.tsx +++ b/craft/src/components/forms/FormButton.tsx @@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { escapeHtml } from '../../utils/escape'; +import { tokenRef } from '../../utils/tokens'; interface FormButtonProps { text?: string; @@ -31,7 +32,7 @@ export const FormButton: UserComponent = ({ onClick={(e) => e.preventDefault()} style={{ padding: '12px 32px', - backgroundColor: '#3b82f6', + backgroundColor: tokenRef('primary'), color: '#ffffff', border: 'none', borderRadius: '6px', @@ -55,7 +56,7 @@ FormButton.craft = { props: { text: 'Submit', style: { - backgroundColor: '#3b82f6', + backgroundColor: tokenRef('primary'), color: '#ffffff', padding: '12px 32px', borderRadius: '6px', diff --git a/craft/src/components/forms/SubscribeForm.toHtml.test.ts b/craft/src/components/forms/SubscribeForm.toHtml.test.ts index 791f968..0c9e73e 100644 --- a/craft/src/components/forms/SubscribeForm.toHtml.test.ts +++ b/craft/src/components/forms/SubscribeForm.toHtml.test.ts @@ -79,3 +79,17 @@ describe('SubscribeForm.craft.props includes animation/visibility defaults', () }); }); }); + +describe('SubscribeForm.toHtml palette token fallbacks (Refs #44)', () => { + test('buttonColor absent -> primary token; heading/input text -> text token', () => { + const { html } = toHtml({ heading: 'Join us' }, ''); + expect(html).toContain('background-color:var(--wsb-primary)'); + expect(html).toContain('color:var(--wsb-text)'); + expect(html).not.toContain('#3b82f6'); + expect(html).not.toContain('#1f2937'); + }); + test('explicit buttonColor wins', () => { + const { html } = toHtml({ buttonColor: '#ff0000' }, ''); + expect(html).toContain('background-color:#ff0000'); + }); +}); diff --git a/craft/src/components/forms/SubscribeForm.tsx b/craft/src/components/forms/SubscribeForm.tsx index d8019fb..b2d730e 100644 --- a/craft/src/components/forms/SubscribeForm.tsx +++ b/craft/src/components/forms/SubscribeForm.tsx @@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { relayFormWiring } from '../../utils/form-relay-wiring'; import { escapeHtml, escapeAttr } from '../../utils/escape'; +import { tokenRef } from '../../utils/tokens'; interface SubscribeFormProps { heading?: string; @@ -28,7 +29,7 @@ export const SubscribeForm: UserComponent = ({ heading = 'Subscribe to our newsletter', placeholder = 'Enter your email', buttonText = 'Subscribe', - buttonColor = '#3b82f6', + buttonColor = tokenRef('primary'), layout = 'inline', style = {}, }) => { @@ -55,7 +56,7 @@ export const SubscribeForm: UserComponent = ({

@@ -84,7 +85,7 @@ export const SubscribeForm: UserComponent = ({ border: '1px solid #d1d5db', borderRadius: isInline ? '8px 0 0 8px' : '8px', backgroundColor: '#ffffff', - color: '#1f2937', + color: tokenRef('text'), outline: 'none', boxSizing: 'border-box', }} @@ -119,7 +120,7 @@ SubscribeForm.craft = { heading: 'Subscribe to our newsletter', placeholder: 'Enter your email', buttonText: 'Subscribe', - buttonColor: '#3b82f6', + buttonColor: tokenRef('primary'), layout: 'inline', style: { backgroundColor: '#f8fafc' }, recipientEmail: '', @@ -144,7 +145,7 @@ SubscribeForm.craft = { heading = 'Subscribe to our newsletter', placeholder = 'Enter your email', buttonText = 'Subscribe', - buttonColor = '#3b82f6', + buttonColor = tokenRef('primary'), layout = 'inline', style = {}, } = props; @@ -158,7 +159,7 @@ SubscribeForm.craft = { }); const headingHtml = heading - ? `

${escapeHtml(heading)}

` + ? `

${escapeHtml(heading)}

` : ''; const formStyle = cssPropsToString({ @@ -170,7 +171,7 @@ SubscribeForm.craft = { alignItems: 'stretch', }); - const inputStyleStr = `flex:1;padding:12px 16px;font-size:15px;font-family:Inter,sans-serif;border:1px solid #d1d5db;border-radius:${isInline ? '8px 0 0 8px' : '8px'};background-color:#ffffff;color:#1f2937;outline:none;box-sizing:border-box`; + const inputStyleStr = `flex:1;padding:12px 16px;font-size:15px;font-family:Inter,sans-serif;border:1px solid #d1d5db;border-radius:${isInline ? '8px 0 0 8px' : '8px'};background-color:#ffffff;color:${tokenRef('text')};outline:none;box-sizing:border-box`; const btnStyle = cssPropsToString({ padding: '12px 24px', diff --git a/craft/src/components/layout/Section.toHtml.test.ts b/craft/src/components/layout/Section.toHtml.test.ts index 0d9afa8..168611f 100644 --- a/craft/src/components/layout/Section.toHtml.test.ts +++ b/craft/src/components/layout/Section.toHtml.test.ts @@ -148,3 +148,23 @@ describe('Section.craft.props exposes the vertical-alignment/box-model/animation expect(style.opacity).toBe('1'); }); }); + +describe('Section.toHtml palette token fallbacks (Refs #44)', () => { + test('divider fill falls back to the bg token (valid in an SVG style)', () => { + const { html } = toHtml({ topDivider: 'wave', bottomDivider: 'curve' }, ''); + expect(html.match(/fill:var\(--wsb-bg\)/g)).toHaveLength(2); + expect(html).not.toContain('#ffffff'); + }); + test('a new section (craft default props) paints the bg token and its divider colours', () => { + const p = (Section as any).craft.props; + expect(p.style.backgroundColor).toBe('var(--wsb-bg)'); + expect(p.topDividerColor).toBe('var(--wsb-bg)'); + const { html } = toHtml({ ...p, topDivider: 'wave' }, ''); + expect(html).toContain('background-color:var(--wsb-bg)'); + expect(html).toContain('fill:var(--wsb-bg)'); + }); + test('explicit divider colour wins', () => { + const { html } = toHtml({ topDivider: 'wave', topDividerColor: '#123456' }, ''); + expect(html).toContain('fill:#123456'); + }); +}); diff --git a/craft/src/components/layout/Section.tsx b/craft/src/components/layout/Section.tsx index a425e8f..d57366e 100644 --- a/craft/src/components/layout/Section.tsx +++ b/craft/src/components/layout/Section.tsx @@ -3,6 +3,7 @@ import { useNode, Element, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { Container } from './Container'; import { escapeAttr, cssValue } from '../../utils/escape'; +import { tokenRef } from '../../utils/tokens'; /* ---------- Shape Divider SVG Paths ---------- */ @@ -74,7 +75,7 @@ const ShapeDivider: React.FC<{ style={{ width: '100%', height: '100%', - fill: color || '#ffffff', + fill: color || tokenRef('bg'), transform: isTop ? 'rotate(180deg)' : undefined, display: 'block', }} @@ -92,10 +93,10 @@ export const Section: UserComponent = ({ innerMaxWidth = '1200px', children, topDivider = 'none', - topDividerColor = '#ffffff', + topDividerColor = tokenRef('bg'), topDividerHeight = '50px', bottomDivider = 'none', - bottomDividerColor = '#ffffff', + bottomDividerColor = tokenRef('bg'), bottomDividerHeight = '50px', anchorId, }) => { @@ -158,7 +159,7 @@ Section.craft = { props: { style: { padding: '40px 0', - backgroundColor: '#ffffff', + backgroundColor: tokenRef('bg'), minHeight: '', justifyContent: '', marginTop: '', marginRight: '', marginBottom: '', marginLeft: '', @@ -169,10 +170,10 @@ Section.craft = { }, innerMaxWidth: '1200px', topDivider: 'none', - topDividerColor: '#ffffff', + topDividerColor: tokenRef('bg'), topDividerHeight: '50px', bottomDivider: 'none', - bottomDividerColor: '#ffffff', + bottomDividerColor: tokenRef('bg'), bottomDividerHeight: '50px', anchorId: '', animation: '', @@ -206,7 +207,7 @@ function buildDividerHtml( const isTop = position === 'top'; const h = height || '50px'; // Sanitized -- raw string-interpolation sink in the SVG `fill:${c}` below. - const c = cssValue(color) || '#ffffff'; + const c = cssValue(color) || tokenRef('bg'); const wrapperStyle = cssPropsToString({ position: 'absolute', 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