feat(site-builder): tokenise colours hard-coded inside components

Component render/toHtml now use var(--wsb-*) instead of hex so Site Design
palette changes reach FeaturesGrid, Testimonials, Accordion, PricingTable,
CTA/CallToAction, forms, Gallery, Tabs and others. Exemption lists removed.

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:44:39 -07:00
1 parent 8227d768e6
commit 2877bc06fa
47 files changed
+473 -195

No files matched your search

@@ -300,3 +300,12 @@ describe('ContactForm.toHtml palette token fallbacks (Refs #44)', () => {
expect(html).toContain('background-color:#ff0000');
});
});
describe('ContactForm.toHtml design tokens (#44)', () => {
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
const { html } = toHtml({ fields: [{ label: 'N', name: 'n', type: 'text', required: true }] }, '');
const lower = html.toLowerCase();
for (const t of ["bg", "border", "text", "error"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of ["#ef4444", "#374151", "#d1d5db", "#ffffff"]) expect(lower, `old ${hex}`).not.toContain(hex);
});
});
+15 -15
View File
@@ -66,9 +66,9 @@ export const ContactForm: UserComponent<ContactFormProps> = ({
formAction = '#',
successMessage = 'Thank you! We\'ll get back to you soon.',
style = {},
labelColor = '#374151',
inputBg = '#ffffff',
inputBorder = '#d1d5db',
labelColor = tokenRef('text'),
inputBg = tokenRef('bg'),
inputBorder = tokenRef('border'),
}) => {
const {
connectors: { connect, drag },
@@ -109,7 +109,7 @@ export const ContactForm: UserComponent<ContactFormProps> = ({
<div key={i} style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
<label style={{ fontSize: '14px', fontWeight: '500', color: labelColor }}>
{field.label}
{field.required && <span style={{ color: '#ef4444', marginLeft: '2px' }}>*</span>}
{field.required && <span style={{ color: tokenRef('error'), marginLeft: '2px' }}>*</span>}
</label>
{field.type === 'textarea' ? (
<textarea
@@ -148,7 +148,7 @@ export const ContactForm: UserComponent<ContactFormProps> = ({
fontSize: '16px',
fontWeight: '600',
fontFamily: 'Inter, sans-serif',
color: '#ffffff',
color: tokenRef('bg'),
backgroundColor: submitColor,
border: 'none',
borderRadius: '8px',
@@ -173,13 +173,13 @@ ContactForm.craft = {
formAction: '#',
successMessage: 'Thank you! We\'ll get back to you soon.',
style: {
backgroundColor: '#ffffff',
backgroundColor: tokenRef('bg'),
borderRadius: '12px',
border: '1px solid #e5e7eb',
border: `1px solid ${tokenRef('border')}`,
},
labelColor: '#374151',
inputBg: '#ffffff',
inputBorder: '#d1d5db',
labelColor: tokenRef('text'),
inputBg: tokenRef('bg'),
inputBorder: tokenRef('border'),
recipientEmail: '',
thankYouUrl: '',
// Present (not omitted) so FormStylePanel's `nodeProps.X !== undefined`
@@ -213,13 +213,13 @@ ContactForm.craft = {
});
// Sanitized -- raw string-interpolation sinks in inputStyleStr/labelHtml
// below.
const labelColor = cssValue(props.labelColor) || '#374151';
const inputBg = cssValue(props.inputBg) || '#ffffff';
const inputBorder = cssValue(props.inputBorder) || '#d1d5db';
const labelColor = cssValue(props.labelColor) || tokenRef('text');
const inputBg = cssValue(props.inputBg) || tokenRef('bg');
const inputBorder = cssValue(props.inputBorder) || tokenRef('border');
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 ? '<span style="color:#ef4444;margin-left:2px">*</span>' : '';
const reqStar = field.required ? `<span style="color:${tokenRef('error')};margin-left:2px">*</span>` : '';
// Deterministic id: index + slugified name, so repeated fields with the
// same name (or no name) still get unique, stable ids -- no Math.random.
const fieldId = `field-${i}-${slugId(field.name)}`;
@@ -242,7 +242,7 @@ ContactForm.craft = {
fontSize: '16px',
fontWeight: '600',
fontFamily: 'Inter, sans-serif',
color: '#ffffff',
color: tokenRef('bg'),
backgroundColor: cssValue(props.submitColor) || tokenRef('primary'),
border: 'none',
borderRadius: '8px',
@@ -46,3 +46,12 @@ describe('FormButton.toHtml palette token default (Refs #44)', () => {
expect(html).not.toContain('#3b82f6');
});
});
describe('FormButton.toHtml design tokens (#44)', () => {
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
const { html } = toHtml({ ...(FormButton as any).craft.props }, '');
const lower = html.toLowerCase();
for (const t of ["bg", "primary"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of ["#ffffff"]) expect(lower, `old ${hex}`).not.toContain(hex);
});
});
+2 -2
View File
@@ -33,7 +33,7 @@ export const FormButton: UserComponent<FormButtonProps> = ({
style={{
padding: '12px 32px',
backgroundColor: tokenRef('primary'),
color: '#ffffff',
color: tokenRef('bg'),
border: 'none',
borderRadius: '6px',
fontSize: '16px',
@@ -57,7 +57,7 @@ FormButton.craft = {
text: 'Submit',
style: {
backgroundColor: tokenRef('primary'),
color: '#ffffff',
color: tokenRef('bg'),
padding: '12px 32px',
borderRadius: '6px',
fontWeight: '600',
@@ -69,3 +69,12 @@ describe('FormContainer.craft.props includes animation/visibility defaults', ()
});
});
});
describe('FormContainer.toHtml design tokens (#44)', () => {
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
const { html } = toHtml({ ...(FormContainer as any).craft.props }, '');
const lower = html.toLowerCase();
for (const t of ["bg", "border"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of ["#ffffff", "#e4e4e7"]) expect(lower, `old ${hex}`).not.toContain(hex);
});
});
+3 -2
View File
@@ -1,4 +1,5 @@
import React, { CSSProperties } from 'react';
import { tokenRef } from '../../utils/tokens';
import { useNode, Element, UserComponent } from '@craftjs/core';
import { Container } from '../layout/Container';
import { cssPropsToString } from '../../utils/style-helpers';
@@ -60,9 +61,9 @@ FormContainer.craft = {
thankYouUrl: '',
style: {
padding: '24px',
backgroundColor: '#ffffff',
backgroundColor: tokenRef('bg'),
borderRadius: '8px',
border: '1px solid #e4e4e7',
border: `1px solid ${tokenRef('border')}`,
},
animation: '',
animationDelay: '',
@@ -89,3 +89,12 @@ describe('InputField.craft.props includes animation/visibility defaults', () =>
});
});
});
describe('InputField.toHtml design tokens (#44)', () => {
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
const { html } = toHtml({ label: 'L', required: true }, '');
const lower = html.toLowerCase();
for (const t of ["bg", "border", "text", "error"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of ["#18181b", "#ef4444", "#d4d4d8", "#ffffff"]) expect(lower, `old ${hex}`).not.toContain(hex);
});
});
+8 -7
View File
@@ -1,4 +1,5 @@
import React, { CSSProperties } from 'react';
import { tokenRef } from '../../utils/tokens';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { escapeHtml, escapeAttr, scopeId, sanitizeInputType } from '../../utils/escape';
@@ -45,8 +46,8 @@ export const InputField: UserComponent<InputFieldProps> = ({
}}
>
{label && (
<label style={{ fontSize: '14px', fontWeight: '500', color: '#18181b' }}>
{label}{required && <span style={{ color: '#ef4444' }}> *</span>}
<label style={{ fontSize: '14px', fontWeight: '500', color: tokenRef('text') }}>
{label}{required && <span style={{ color: tokenRef('error') }}> *</span>}
</label>
)}
<input
@@ -56,11 +57,11 @@ export const InputField: UserComponent<InputFieldProps> = ({
required={required}
style={{
padding: '10px 12px',
border: '1px solid #d4d4d8',
border: `1px solid ${tokenRef('border')}`,
borderRadius: '6px',
fontSize: '14px',
color: '#18181b',
backgroundColor: '#ffffff',
color: tokenRef('text'),
backgroundColor: tokenRef('bg'),
outline: 'none',
width: '100%',
boxSizing: 'border-box',
@@ -113,7 +114,7 @@ InputField.craft = {
// hash for legacy 2-arg call sites without a node id.
const fieldId = scopeId(nodeId, props.name || 'field', 'field');
const labelHtml = props.label
? `<label for="${escapeAttr(fieldId)}" style="font-size:14px;font-weight:500;color:#18181b">${escapeHtml(props.label)}${props.required ? '<span style="color:#ef4444"> *</span>' : ''}</label>`
? `<label for="${escapeAttr(fieldId)}" style="font-size:14px;font-weight:500;color:${tokenRef('text')}">${escapeHtml(props.label)}${props.required ? `<span style="color:${tokenRef('error')}"> *</span>` : ''}</label>`
: '';
const ariaLabelAttr = !props.label
? ` aria-label="${escapeAttr(props.placeholder || props.name || 'Input field')}"`
@@ -121,7 +122,7 @@ InputField.craft = {
return {
html: `<div${wrapStyle ? ` style="${wrapStyle}"` : ''}>
${labelHtml}
<input id="${escapeAttr(fieldId)}" type="${sanitizeInputType(props.type)}" name="${escapeAttr(props.name || 'field')}" placeholder="${escapeAttr(props.placeholder || '')}"${reqAttr}${ariaLabelAttr} style="padding:10px 12px;border:1px solid #d4d4d8;border-radius:6px;font-size:14px;color:#18181b;background-color:#ffffff;width:100%;box-sizing:border-box" />
<input id="${escapeAttr(fieldId)}" type="${sanitizeInputType(props.type)}" name="${escapeAttr(props.name || 'field')}" placeholder="${escapeAttr(props.placeholder || '')}"${reqAttr}${ariaLabelAttr} style="padding:10px 12px;border:1px solid ${tokenRef('border')};border-radius:6px;font-size:14px;color:${tokenRef('text')};background-color:${tokenRef('bg')};width:100%;box-sizing:border-box" />
</div>`,
};
};
@@ -93,3 +93,12 @@ describe('SubscribeForm.toHtml palette token fallbacks (Refs #44)', () => {
expect(html).toContain('background-color:#ff0000');
});
});
describe('SubscribeForm.toHtml design tokens (#44)', () => {
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
const { html } = toHtml({}, '');
const lower = html.toLowerCase();
for (const t of ["bg", "border", "text"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of ["#d1d5db", "#ffffff"]) expect(lower, `old ${hex}`).not.toContain(hex);
});
});
+6 -6
View File
@@ -82,9 +82,9 @@ export const SubscribeForm: UserComponent<SubscribeFormProps> = ({
padding: '12px 16px',
fontSize: '15px',
fontFamily: 'Inter, sans-serif',
border: '1px solid #d1d5db',
border: `1px solid ${tokenRef('border')}`,
borderRadius: isInline ? '8px 0 0 8px' : '8px',
backgroundColor: '#ffffff',
backgroundColor: tokenRef('bg'),
color: tokenRef('text'),
outline: 'none',
boxSizing: 'border-box',
@@ -97,7 +97,7 @@ export const SubscribeForm: UserComponent<SubscribeFormProps> = ({
fontSize: '15px',
fontWeight: '600',
fontFamily: 'Inter, sans-serif',
color: '#ffffff',
color: tokenRef('bg'),
backgroundColor: buttonColor,
border: 'none',
borderRadius: isInline ? '0 8px 8px 0' : '8px',
@@ -122,7 +122,7 @@ SubscribeForm.craft = {
buttonText: 'Subscribe',
buttonColor: tokenRef('primary'),
layout: 'inline',
style: { backgroundColor: '#f8fafc' },
style: { backgroundColor: tokenRef('bg') },
recipientEmail: '',
thankYouUrl: '',
animation: '',
@@ -171,14 +171,14 @@ 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:${tokenRef('text')};outline:none;box-sizing:border-box`;
const inputStyleStr = `flex:1;padding:12px 16px;font-size:15px;font-family:Inter,sans-serif;border:1px solid ${tokenRef('border')};border-radius:${isInline ? '8px 0 0 8px' : '8px'};background-color:${tokenRef('bg')};color:${tokenRef('text')};outline:none;box-sizing:border-box`;
const btnStyle = cssPropsToString({
padding: '12px 24px',
fontSize: '15px',
fontWeight: '600',
fontFamily: 'Inter, sans-serif',
color: '#ffffff',
color: tokenRef('bg'),
backgroundColor: buttonColor,
border: 'none',
borderRadius: isInline ? '0 8px 8px 0' : '8px',
@@ -80,3 +80,12 @@ describe('TextareaField.craft.props includes animation/visibility defaults', ()
});
});
});
describe('TextareaField.toHtml design tokens (#44)', () => {
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
const { html } = toHtml({ label: 'L', required: true }, '');
const lower = html.toLowerCase();
for (const t of ["bg", "border", "text", "error"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
for (const hex of ["#18181b", "#ef4444", "#d4d4d8", "#ffffff"]) expect(lower, `old ${hex}`).not.toContain(hex);
});
});
+8 -7
View File
@@ -1,4 +1,5 @@
import React, { CSSProperties } from 'react';
import { tokenRef } from '../../utils/tokens';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { escapeHtml, escapeAttr, scopeId } from '../../utils/escape';
@@ -45,8 +46,8 @@ export const TextareaField: UserComponent<TextareaFieldProps> = ({
}}
>
{label && (
<label style={{ fontSize: '14px', fontWeight: '500', color: '#18181b' }}>
{label}{required && <span style={{ color: '#ef4444' }}> *</span>}
<label style={{ fontSize: '14px', fontWeight: '500', color: tokenRef('text') }}>
{label}{required && <span style={{ color: tokenRef('error') }}> *</span>}
</label>
)}
<textarea
@@ -57,11 +58,11 @@ export const TextareaField: UserComponent<TextareaFieldProps> = ({
readOnly
style={{
padding: '10px 12px',
border: '1px solid #d4d4d8',
border: `1px solid ${tokenRef('border')}`,
borderRadius: '6px',
fontSize: '14px',
color: '#18181b',
backgroundColor: '#ffffff',
color: tokenRef('text'),
backgroundColor: tokenRef('bg'),
outline: 'none',
width: '100%',
boxSizing: 'border-box',
@@ -120,7 +121,7 @@ TextareaField.craft = {
// 2-arg call sites without a node id.
const fieldId = scopeId(nodeId, props.name || 'message', 'field');
const labelHtml = props.label
? `<label for="${escapeAttr(fieldId)}" style="font-size:14px;font-weight:500;color:#18181b">${escapeHtml(props.label)}${props.required ? '<span style="color:#ef4444"> *</span>' : ''}</label>`
? `<label for="${escapeAttr(fieldId)}" style="font-size:14px;font-weight:500;color:${tokenRef('text')}">${escapeHtml(props.label)}${props.required ? `<span style="color:${tokenRef('error')}"> *</span>` : ''}</label>`
: '';
const ariaLabelAttr = !props.label
? ` aria-label="${escapeAttr(props.placeholder || props.name || 'Textarea field')}"`
@@ -128,7 +129,7 @@ TextareaField.craft = {
return {
html: `<div${wrapStyle ? ` style="${wrapStyle}"` : ''}>
${labelHtml}
<textarea id="${escapeAttr(fieldId)}" name="${escapeAttr(props.name || 'message')}" placeholder="${escapeAttr(props.placeholder || '')}" rows="${escapeAttr(String(rows))}"${reqAttr}${ariaLabelAttr} style="padding:10px 12px;border:1px solid #d4d4d8;border-radius:6px;font-size:14px;color:#18181b;background-color:#ffffff;width:100%;box-sizing:border-box;resize:vertical;font-family:inherit"></textarea>
<textarea id="${escapeAttr(fieldId)}" name="${escapeAttr(props.name || 'message')}" placeholder="${escapeAttr(props.placeholder || '')}" rows="${escapeAttr(String(rows))}"${reqAttr}${ariaLabelAttr} style="padding:10px 12px;border:1px solid ${tokenRef('border')};border-radius:6px;font-size:14px;color:${tokenRef('text')};background-color:${tokenRef('bg')};width:100%;box-sizing:border-box;resize:vertical;font-family:inherit"></textarea>
</div>`,
};
};