commit
ce3db23506
27 files changed
+265
-51
No files matched your search
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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<IconProps> = ({
|
||||
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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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<SearchBarProps> = ({
|
||||
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<SearchBarProps> = ({
|
||||
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
|
||||
? `<button type="submit" style="padding:12px 20px;font-size:15px;font-weight:600;font-family:Inter,sans-serif;color:#ffffff;background-color:#3b82f6;border:none;border-radius:0 8px 8px 0;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:6px"><i class="fa fa-search" style="font-size:13px" aria-hidden="true"></i>${escapeHtml(buttonText)}</button>`
|
||||
? `<button type="submit" style="padding:12px 20px;font-size:15px;font-weight:600;font-family:Inter,sans-serif;color:#ffffff;background-color:${tokenRef('primary')};border:none;border-radius:0 8px 8px 0;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:6px"><i class="fa fa-search" style="font-size:13px" aria-hidden="true"></i>${escapeHtml(buttonText)}</button>`
|
||||
: '';
|
||||
|
||||
// F2: previously a purely decorative <form> -- no action/method/input
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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<ContactFormProps> = ({
|
||||
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<ContactFormProps> = ({
|
||||
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 ? '<span style="color:#ef4444;margin-left:2px">*</span>' : '';
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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<FormButtonProps> = ({
|
||||
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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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<SubscribeFormProps> = ({
|
||||
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<SubscribeFormProps> = ({
|
||||
<h3 style={{
|
||||
fontSize: '22px',
|
||||
fontWeight: '600',
|
||||
color: '#1f2937',
|
||||
color: tokenRef('text'),
|
||||
marginBottom: '20px',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
}}>
|
||||
@@ -84,7 +85,7 @@ export const SubscribeForm: UserComponent<SubscribeFormProps> = ({
|
||||
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
|
||||
? `<h3 style="font-size:22px;font-weight:600;color:#1f2937;margin-bottom:20px;font-family:Inter,sans-serif">${escapeHtml(heading)}</h3>`
|
||||
? `<h3 style="font-size:22px;font-weight:600;color:${tokenRef('text')};margin-bottom:20px;font-family:Inter,sans-serif">${escapeHtml(heading)}</h3>`
|
||||
: '';
|
||||
|
||||
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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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<SectionProps> = ({
|
||||
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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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<ContentSliderProps> = ({
|
||||
? { 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 (
|
||||
<div
|
||||
@@ -297,7 +298,7 @@ ContentSlider.craft = {
|
||||
const hasBgImage = slide.imageSrc;
|
||||
// Sanitized -- slide.bgColor is a per-slide raw string-interpolation
|
||||
// sink (a malicious value could break out of the style="..." attribute).
|
||||
const safeBgColor = cssValue(slide.bgColor) || '#3b82f6';
|
||||
const safeBgColor = cssValue(slide.bgColor) || tokenRef('primary');
|
||||
const bgStyle = hasBgImage
|
||||
? `background-image:url('${escapeAttr(safeImageUrl(slide.imageSrc!))}');background-size:cover;background-position:center`
|
||||
: slide.bgColor?.startsWith('linear-gradient')
|
||||
|
||||
@@ -32,3 +32,12 @@ describe('FeaturesGrid.craft.props includes the box-model/animation/visibility r
|
||||
expect(props).toHaveProperty('hideOnMobile', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FeaturesGrid.toHtml palette token fallback (Refs #44)', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -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<FeaturesGridProps> = ({
|
||||
<a
|
||||
href={feat.buttonUrl || '#'}
|
||||
onClick={(e) => 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}
|
||||
</a>
|
||||
@@ -129,7 +130,7 @@ FeaturesGrid.craft = {
|
||||
? `<img src="${escapeAttr(safeImageUrl(feat.image))}" alt="${escapeAttr(feat.imageAlt || feat.title || '')}" style="max-width:100%;height:auto;margin-bottom:16px;border-radius:8px">`
|
||||
: `<div style="font-size:36px;margin-bottom:16px">${escapeHtml(feat.icon)}</div>`;
|
||||
const button = feat.buttonText
|
||||
? `\n <a href="${escapeAttr(safeUrl(feat.buttonUrl || '#'))}" style="display:inline-block;margin-top:16px;padding:10px 24px;background:#3b82f6;color:#fff;border-radius:8px;text-decoration:none;font-size:14px;font-weight:600">${escapeHtml(feat.buttonText)}</a>`
|
||||
? `\n <a href="${escapeAttr(safeUrl(feat.buttonUrl || '#'))}" style="display:inline-block;margin-top:16px;padding:10px 24px;background:${tokenRef('primary')};color:#fff;border-radius:8px;text-decoration:none;font-size:14px;font-weight:600">${escapeHtml(feat.buttonText)}</a>`
|
||||
: '';
|
||||
return `<div style="text-align:center;padding:32px 24px;border-radius:12px;background-color:#f8fafc;border:1px solid #e2e8f0">
|
||||
${media}
|
||||
|
||||
@@ -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)');
|
||||
});
|
||||
});
|
||||
@@ -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<HeroProps> = ({
|
||||
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',
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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<NumberCounterProps> = ({
|
||||
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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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<PricingTableProps> = ({
|
||||
plans = defaultPlans,
|
||||
style = {},
|
||||
featuredBg = '#3b82f6',
|
||||
featuredBg = tokenRef('primary'),
|
||||
bulletType = 'check',
|
||||
anchorId,
|
||||
cardBg,
|
||||
@@ -97,7 +98,7 @@ export const PricingTable: UserComponent<PricingTableProps> = ({
|
||||
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';
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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<TabsProps> = ({
|
||||
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 <script> as single-quoted JS string
|
||||
// literals (a stray `'` there breaks out of the JS string, not just CSS);
|
||||
// cssValue strips quotes too so it neutralizes both contexts at once.
|
||||
const activeTabBg = cssValue(props.activeTabBg) || '#3b82f6';
|
||||
const activeTabBg = cssValue(props.activeTabBg) || tokenRef('primary');
|
||||
const activeTabColor = cssValue(props.activeTabColor) || '#ffffff';
|
||||
const inactiveTabBg = cssValue(props.inactiveTabBg) || '#f1f5f9';
|
||||
const inactiveTabColor = cssValue(props.inactiveTabColor) || '#64748b';
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { describe, test, expect, afterEach } from 'vitest';
|
||||
import { renderEditorHarness, EditorHarness } from '../test-utils/editorHarness';
|
||||
import { ButtonLink } from './basic/ButtonLink';
|
||||
import { Icon } from './basic/Icon';
|
||||
import { FormButton } from './forms/FormButton';
|
||||
import { SubscribeForm } from './forms/SubscribeForm';
|
||||
import { ContactForm } from './forms/ContactForm';
|
||||
import { HeroSimple } from './sections/HeroSimple';
|
||||
import { PricingTable } from './sections/PricingTable';
|
||||
import { Tabs } from './sections/Tabs';
|
||||
import { NumberCounter } from './sections/NumberCounter';
|
||||
import { Section } from './layout/Section';
|
||||
|
||||
let h: EditorHarness;
|
||||
afterEach(() => {
|
||||
h?.unmount();
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
/** Drop a fresh node onto the real editor canvas and return the serialized
|
||||
* doc, i.e. what is persisted for a NEW block. */
|
||||
function dropAndSerialize(element: React.ReactElement): string {
|
||||
h = renderEditorHarness();
|
||||
h.act(() => {
|
||||
const tree = h.query.parseReactElement(element).toNodeTree();
|
||||
h.actions.addNodeTree(tree, 'ROOT');
|
||||
});
|
||||
return h.getSerialized();
|
||||
}
|
||||
|
||||
describe('new blocks take their colour defaults from the site palette tokens', () => {
|
||||
test.each([
|
||||
['ButtonLink', () => <ButtonLink />, '"backgroundColor":"var(--wsb-primary)"'],
|
||||
['Icon', () => <Icon />, '"color":"var(--wsb-primary)"'],
|
||||
['FormButton', () => <FormButton />, '"backgroundColor":"var(--wsb-primary)"'],
|
||||
['SubscribeForm', () => <SubscribeForm />, '"buttonColor":"var(--wsb-primary)"'],
|
||||
['ContactForm', () => <ContactForm />, '"submitColor":"var(--wsb-primary)"'],
|
||||
['HeroSimple', () => <HeroSimple />, '"buttonBgColor":"var(--wsb-primary)"'],
|
||||
['PricingTable', () => <PricingTable />, '"featuredBg":"var(--wsb-primary)"'],
|
||||
['Tabs', () => <Tabs />, '"activeTabBg":"var(--wsb-primary)"'],
|
||||
['NumberCounter', () => <NumberCounter />, '"numberColor":"var(--wsb-primary)"'],
|
||||
['NumberCounter', () => <NumberCounter />, '"labelColor":"var(--wsb-muted)"'],
|
||||
['Section', () => <Section />, '"backgroundColor":"var(--wsb-bg)"'],
|
||||
['Section', () => <Section />, '"topDividerColor":"var(--wsb-bg)"'],
|
||||
['Section', () => <Section />, '"bottomDividerColor":"var(--wsb-bg)"'],
|
||||
] as [string, () => React.ReactElement, string][])('%s default is a token', (_name, make, expected) => {
|
||||
expect(dropAndSerialize(make())).toContain(expected);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user