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:
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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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>`,
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user