Merge component-tokens
This commit is contained in:
commit
d4400e49ad
65 files changed
+662
-262
No files matched your search
@@ -126,3 +126,12 @@ describe('ButtonLink.toHtml palette token default (Refs #44)', () => {
|
||||
expect(html).not.toContain('#3b82f6');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ButtonLink.toHtml design tokens (#44)', () => {
|
||||
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
|
||||
const { html } = toHtml({ ...(ButtonLink 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);
|
||||
});
|
||||
});
|
||||
@@ -77,7 +77,7 @@ ButtonLink.craft = {
|
||||
hoverColor: '',
|
||||
style: {
|
||||
backgroundColor: tokenRef('primary'),
|
||||
color: '#ffffff',
|
||||
color: tokenRef('bg'),
|
||||
padding: '12px 24px',
|
||||
borderRadius: '8px',
|
||||
fontWeight: '600',
|
||||
|
||||
@@ -32,3 +32,12 @@ describe('Divider.toHtml XSS hardening (thickness/color into style=)', () => {
|
||||
expect(html).not.toMatch(/"\s+onmouseover="/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Divider.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 ["border"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ["#e4e4e7"]) 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';
|
||||
|
||||
@@ -9,7 +10,7 @@ interface DividerProps {
|
||||
}
|
||||
|
||||
export const Divider: UserComponent<DividerProps> = ({
|
||||
color = '#e4e4e7',
|
||||
color = tokenRef('border'),
|
||||
thickness = '1px',
|
||||
style = {},
|
||||
}) => {
|
||||
@@ -39,7 +40,7 @@ export const Divider: UserComponent<DividerProps> = ({
|
||||
Divider.craft = {
|
||||
displayName: 'Divider',
|
||||
props: {
|
||||
color: '#e4e4e7',
|
||||
color: tokenRef('border'),
|
||||
thickness: '1px',
|
||||
style: {},
|
||||
},
|
||||
@@ -55,7 +56,7 @@ Divider.craft = {
|
||||
(Divider as any).toHtml = (props: DividerProps, _childrenHtml: string) => {
|
||||
const styleStr = cssPropsToString({
|
||||
border: 'none',
|
||||
borderTop: `${props.thickness || '1px'} solid ${props.color || '#e4e4e7'}`,
|
||||
borderTop: `${props.thickness || '1px'} solid ${props.color || tokenRef('border')}`,
|
||||
margin: '16px 0',
|
||||
...props.style,
|
||||
});
|
||||
|
||||
@@ -31,3 +31,13 @@ describe('Footer (F4: box-model + animation + visibility props on craft.props)',
|
||||
expect(craftProps).toHaveProperty('hideOnMobile', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Footer.toHtml design tokens (#44)', () => {
|
||||
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
|
||||
const props = { ...((Footer as any).craft.props as object) };
|
||||
const { html } = (((Footer) as any).toHtml)(props, '', 'node-tok');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ['text', 'bg']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ['#18181b', '#a1a1aa']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { CSSProperties, useCallback, useRef, useEffect } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { tokenRef } from '../../utils/tokens';
|
||||
import { escapeHtml } from '../../utils/escape';
|
||||
|
||||
interface FooterProps {
|
||||
@@ -92,8 +93,9 @@ Footer.craft = {
|
||||
props: {
|
||||
text: '© 2026 MySite. All rights reserved.',
|
||||
style: {
|
||||
backgroundColor: '#18181b',
|
||||
color: '#a1a1aa',
|
||||
// Dark band: text token as the band, bg token as its (contrast-safe) text.
|
||||
backgroundColor: tokenRef('text'),
|
||||
color: tokenRef('bg'),
|
||||
fontSize: '14px',
|
||||
padding: '24px 20px',
|
||||
},
|
||||
|
||||
@@ -83,3 +83,12 @@ describe('Logo.toHtml text-logo styling sanitization', () => {
|
||||
expect(html).toContain('MySite');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Logo.toHtml design tokens (#44)', () => {
|
||||
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
|
||||
const { html } = toHtml({ text: "Acme" }, '');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ["text"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ["#1f2937"]) 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 { useSiteDesign } from '../../state/SiteDesignContext';
|
||||
@@ -125,7 +126,7 @@ Logo.craft = {
|
||||
fontWeight: props.fontWeight || '700',
|
||||
fontSize: props.fontSize || '20px',
|
||||
fontFamily: props.fontFamily || 'Inter, sans-serif',
|
||||
color: props.color || '#1f2937',
|
||||
color: props.color || tokenRef('text'),
|
||||
});
|
||||
innerHtml = `<span${spanStyle ? ` style="${spanStyle}"` : ''}>${escapeHtml(props.text || 'MySite')}</span>`;
|
||||
}
|
||||
|
||||
@@ -107,3 +107,13 @@ describe('Menu.toHtml dropdowns (#53)', () => {
|
||||
expect((Menu as any).craft.props.followPages).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Menu.toHtml design tokens (#44)', () => {
|
||||
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
|
||||
const props = { ...((Menu as any).craft.props as object) };
|
||||
const { html } = (((Menu) as any).toHtml)(props, '', 'node-tok');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ['text', 'bg']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ['#3f3f46']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
|
||||
});
|
||||
});
|
||||
@@ -43,10 +43,10 @@ const defaultLinks: MenuLink[] = [
|
||||
export const Menu: UserComponent<MenuProps> = ({
|
||||
links = defaultLinks,
|
||||
alignment = 'right',
|
||||
linkColor = '#3f3f46',
|
||||
linkColor = tokenRef('text'),
|
||||
linkHoverColor = tokenRef('primary'),
|
||||
ctaBgColor = tokenRef('primary'),
|
||||
ctaTextColor = '#ffffff',
|
||||
ctaTextColor = tokenRef('bg'),
|
||||
gap = '24px',
|
||||
orientation = 'horizontal',
|
||||
fontSize = '14px',
|
||||
@@ -139,7 +139,7 @@ export const Menu: UserComponent<MenuProps> = ({
|
||||
|
||||
const SUB_STYLE: CSSProperties = {
|
||||
position: 'absolute', top: '100%', left: 0, zIndex: 1001, minWidth: 180, margin: 0, padding: '8px 0',
|
||||
listStyle: 'none', background: 'var(--wsb-bg, #ffffff)', borderRadius: 6, boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
|
||||
listStyle: 'none', background: tokenRef('bg'), borderRadius: 6, boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
|
||||
};
|
||||
const SUB_STATIC_STYLE: CSSProperties = { margin: 0, padding: '4px 0 0 12px', listStyle: 'none' };
|
||||
|
||||
@@ -151,10 +151,10 @@ Menu.craft = {
|
||||
links: defaultLinks,
|
||||
followPages: false,
|
||||
alignment: 'right',
|
||||
linkColor: '#3f3f46',
|
||||
linkColor: tokenRef('text'),
|
||||
linkHoverColor: tokenRef('primary'),
|
||||
ctaBgColor: tokenRef('primary'),
|
||||
ctaTextColor: '#ffffff',
|
||||
ctaTextColor: tokenRef('bg'),
|
||||
gap: '24px',
|
||||
orientation: 'horizontal',
|
||||
fontSize: '14px',
|
||||
@@ -179,10 +179,10 @@ Menu.craft = {
|
||||
// string-interpolation sinks below (hoverCol goes into a <style> block,
|
||||
// the worst case: </style> breakout -> arbitrary <script>), see
|
||||
// task-cssxss-brief.md.
|
||||
const linkCol = cssValue(props.linkColor) || '#3f3f46';
|
||||
const linkCol = cssValue(props.linkColor) || tokenRef('text');
|
||||
const hoverCol = cssValue(props.linkHoverColor) || tokenRef('primary');
|
||||
const ctaBg = cssValue(props.ctaBgColor) || tokenRef('primary');
|
||||
const ctaText = cssValue(props.ctaTextColor) || '#ffffff';
|
||||
const ctaText = cssValue(props.ctaTextColor) || tokenRef('bg');
|
||||
const gap = cssValue(props.gap) || '24px';
|
||||
const orientation = props.orientation || 'horizontal';
|
||||
const alignment = props.alignment || 'right';
|
||||
|
||||
@@ -185,3 +185,13 @@ describe('Navbar.toHtml dropdowns (#53)', () => {
|
||||
expect((Navbar as any).craft.props.followPages).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Navbar.toHtml design tokens (#44)', () => {
|
||||
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
|
||||
const props = { ...((Navbar as any).craft.props as object) };
|
||||
const { html } = (((Navbar) as any).toHtml)(props, '', 'node-tok');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ['text', 'bg', 'border']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ['#3f3f46', '#e4e4e7']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
|
||||
});
|
||||
});
|
||||
@@ -53,6 +53,11 @@ const PADDING_PRESETS = [
|
||||
{ label: 'Spacious', value: '24px 48px' },
|
||||
];
|
||||
|
||||
function isLightNavBg(bg: string): boolean {
|
||||
const v = (bg || '').trim().toLowerCase();
|
||||
return v === tokenRef('bg') || v === '#ffffff' || v === '#f8fafc' || v === '#f9fafb';
|
||||
}
|
||||
|
||||
/* ---------- Component ---------- */
|
||||
|
||||
export const Navbar: UserComponent<NavbarProps> = ({
|
||||
@@ -65,11 +70,11 @@ export const Navbar: UserComponent<NavbarProps> = ({
|
||||
logoFontSize = '20px',
|
||||
logoColor,
|
||||
links = defaultLinks,
|
||||
backgroundColor = '#ffffff',
|
||||
textColor = '#3f3f46',
|
||||
backgroundColor = tokenRef('bg'),
|
||||
textColor = tokenRef('text'),
|
||||
hoverColor = tokenRef('primary'),
|
||||
ctaColor = tokenRef('primary'),
|
||||
ctaTextColor = '#ffffff',
|
||||
ctaTextColor = tokenRef('bg'),
|
||||
padding = '16px 24px',
|
||||
navAlignment = 'space-between',
|
||||
isSticky = false,
|
||||
@@ -84,8 +89,12 @@ export const Navbar: UserComponent<NavbarProps> = ({
|
||||
}));
|
||||
|
||||
const { design } = useSiteDesign();
|
||||
const resolvedLogoColor = logoColor || (backgroundColor === '#ffffff' || backgroundColor === '#f8fafc' || backgroundColor === '#f9fafb' ? design.textColor : '#ffffff');
|
||||
const resolvedTextColor = textColor || (backgroundColor === '#ffffff' || backgroundColor === '#f8fafc' || backgroundColor === '#f9fafb' ? '#3f3f46' : '#e4e4e7');
|
||||
// Light-background heuristic: the bg token counts as light (it is the page
|
||||
// background); legacy stored hex values keep the old hex test. White/zinc on
|
||||
// an explicitly dark bar is "text on a dark custom colour" -- no token fits.
|
||||
const lightBg = isLightNavBg(backgroundColor);
|
||||
const resolvedLogoColor = logoColor || (lightBg ? design.textColor : '#ffffff');
|
||||
const resolvedTextColor = textColor || (lightBg ? tokenRef('text') : '#e4e4e7');
|
||||
|
||||
const [hoveredLink, setHoveredLink] = useState<number | null>(null);
|
||||
const [openDropdown, setOpenDropdown] = useState<number | null>(null);
|
||||
@@ -225,17 +234,17 @@ Navbar.craft = {
|
||||
logoColor: undefined,
|
||||
links: defaultLinks,
|
||||
followPages: false,
|
||||
backgroundColor: '#ffffff',
|
||||
textColor: '#3f3f46',
|
||||
backgroundColor: tokenRef('bg'),
|
||||
textColor: tokenRef('text'),
|
||||
hoverColor: tokenRef('primary'),
|
||||
ctaColor: tokenRef('primary'),
|
||||
ctaTextColor: '#ffffff',
|
||||
ctaTextColor: tokenRef('bg'),
|
||||
padding: '16px 24px',
|
||||
navAlignment: 'space-between',
|
||||
isSticky: false,
|
||||
showMobileMenu: false,
|
||||
style: {
|
||||
borderBottom: '1px solid #e4e4e7',
|
||||
borderBottom: `1px solid ${tokenRef('border')}`,
|
||||
},
|
||||
hideOnDesktop: false,
|
||||
hideOnTablet: false,
|
||||
@@ -256,11 +265,11 @@ Navbar.craft = {
|
||||
// Sanitized once here -- these are raw string-interpolation sinks below
|
||||
// (hoverCol/bgColor go into a <style> block, the worst case: </style>
|
||||
// breakout -> arbitrary <script>), see task-cssxss-brief.md.
|
||||
const bgColor = cssValue(props.backgroundColor) || '#ffffff';
|
||||
const textCol = cssValue(props.textColor) || '#3f3f46';
|
||||
const bgColor = cssValue(props.backgroundColor) || tokenRef('bg');
|
||||
const textCol = cssValue(props.textColor) || tokenRef('text');
|
||||
const hoverCol = cssValue(props.hoverColor) || tokenRef('primary');
|
||||
const ctaCol = cssValue(props.ctaColor) || tokenRef('primary');
|
||||
const ctaTextCol = cssValue(props.ctaTextColor) || '#ffffff';
|
||||
const ctaTextCol = cssValue(props.ctaTextColor) || tokenRef('bg');
|
||||
const pad = cssValue(props.padding) || '16px 24px';
|
||||
const alignment = props.navAlignment || 'space-between';
|
||||
const sticky = props.isSticky;
|
||||
|
||||
@@ -83,3 +83,12 @@ describe('SearchBar.toHtml palette token fallbacks (Refs #44)', () => {
|
||||
expect(html).not.toContain('#1f2937');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SearchBar.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", "muted", "primary"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ["#d1d5db", "#9ca3af", "#ffffff"]) expect(lower, `old ${hex}`).not.toContain(hex);
|
||||
});
|
||||
});
|
||||
@@ -57,7 +57,7 @@ export const SearchBar: UserComponent<SearchBarProps> = ({
|
||||
left: '14px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
color: '#9ca3af',
|
||||
color: tokenRef('muted'),
|
||||
fontSize: '14px',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
@@ -71,9 +71,9 @@ export const SearchBar: UserComponent<SearchBarProps> = ({
|
||||
padding: '12px 16px 12px 40px',
|
||||
fontSize: '15px',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
border: '1px solid #d1d5db',
|
||||
border: `1px solid ${tokenRef('border')}`,
|
||||
borderRadius: showButton ? '8px 0 0 8px' : '8px',
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: tokenRef('bg'),
|
||||
color: tokenRef('text'),
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
@@ -88,7 +88,7 @@ export const SearchBar: UserComponent<SearchBarProps> = ({
|
||||
fontSize: '15px',
|
||||
fontWeight: '600',
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
color: '#ffffff',
|
||||
color: tokenRef('bg'),
|
||||
backgroundColor: tokenRef('primary'),
|
||||
border: 'none',
|
||||
borderRadius: '0 8px 8px 0',
|
||||
@@ -148,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:${tokenRef('text')};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 ${tokenRef('border')};border-radius:${showButton ? '8px 0 0 8px' : '8px'};background-color:${tokenRef('bg')};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:${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>`
|
||||
? `<button type="submit" style="padding:12px 20px;font-size:15px;font-weight:600;font-family:Inter,sans-serif;color:${tokenRef('bg')};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
|
||||
@@ -165,7 +165,7 @@ SearchBar.craft = {
|
||||
return {
|
||||
html: `<form role="search" action="${actionAttr}" method="GET"${formStyle ? ` style="${formStyle}"` : ''}>
|
||||
<div style="position:relative;flex:1">
|
||||
<i class="fa fa-search" style="position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#9ca3af;font-size:14px;pointer-events:none" aria-hidden="true"></i>
|
||||
<i class="fa fa-search" style="position:absolute;left:14px;top:50%;transform:translateY(-50%);color:${tokenRef('muted')};font-size:14px;pointer-events:none" aria-hidden="true"></i>
|
||||
<input type="search" name="q" placeholder="${escapeAttr(placeholder)}" style="${inputStyleStr}" />
|
||||
</div>
|
||||
${btnHtml}
|
||||
|
||||
@@ -91,3 +91,13 @@ describe('SocialLinks.craft.props includes the box-model/animation/visibility ro
|
||||
expect(props).toHaveProperty('hideOnMobile', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SocialLinks.toHtml design tokens (#44)', () => {
|
||||
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
|
||||
const props = { ...((SocialLinks as any).craft.props as object) };
|
||||
const { html } = (((SocialLinks) as any).toHtml)(props, '', 'node-tok');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ['text', 'bg']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ['#374151']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { tokenRef } from '../../utils/tokens';
|
||||
import { escapeAttr, safeUrl, cssValue } from '../../utils/escape';
|
||||
|
||||
interface SocialLink {
|
||||
@@ -87,8 +88,8 @@ const alignMap: Record<string, string> = {
|
||||
export const SocialLinks: UserComponent<SocialLinksProps> = ({
|
||||
links = defaultLinks,
|
||||
iconSize = '20px',
|
||||
iconColor = '#ffffff',
|
||||
iconBgColor = '#374151',
|
||||
iconColor = tokenRef('bg'),
|
||||
iconBgColor = tokenRef('text'),
|
||||
iconShape = 'circle',
|
||||
gap = '10px',
|
||||
alignment = 'center',
|
||||
@@ -150,8 +151,8 @@ SocialLinks.craft = {
|
||||
props: {
|
||||
links: defaultLinks,
|
||||
iconSize: '20px',
|
||||
iconColor: '#ffffff',
|
||||
iconBgColor: '#374151',
|
||||
iconColor: tokenRef('bg'),
|
||||
iconBgColor: tokenRef('text'),
|
||||
iconShape: 'circle',
|
||||
gap: '10px',
|
||||
alignment: 'center',
|
||||
@@ -175,8 +176,8 @@ SocialLinks.craft = {
|
||||
const links = props.links || defaultLinks;
|
||||
// Sanitized -- raw string-interpolation sinks in aStyle/getShapeStr below.
|
||||
const iconSize = cssValue(props.iconSize) || '20px';
|
||||
const iconColor = cssValue(props.iconColor) || '#ffffff';
|
||||
const iconBgColor = cssValue(props.iconBgColor) || '#374151';
|
||||
const iconColor = cssValue(props.iconColor) || tokenRef('bg');
|
||||
const iconBgColor = cssValue(props.iconBgColor) || tokenRef('text');
|
||||
const iconShape = props.iconShape || 'circle';
|
||||
const gap = props.gap || '10px';
|
||||
const alignment = props.alignment || 'center';
|
||||
|
||||
@@ -86,3 +86,12 @@ describe('StarRating.craft.props includes the box-model/animation/visibility rol
|
||||
expect(props).toHaveProperty('hideOnMobile', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('StarRating.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 ["warning", "border"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ["#f59e0b", "#d1d5db"]) 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 { cssValue, escapeAttr } from '../../utils/escape';
|
||||
@@ -21,8 +22,8 @@ export const StarRating: UserComponent<StarRatingProps> = ({
|
||||
rating = 4.5,
|
||||
maxStars = 5,
|
||||
size = '24px',
|
||||
filledColor = '#f59e0b',
|
||||
emptyColor = '#d1d5db',
|
||||
filledColor = tokenRef('warning'),
|
||||
emptyColor = tokenRef('border'),
|
||||
style = {},
|
||||
}) => {
|
||||
const {
|
||||
@@ -89,8 +90,8 @@ StarRating.craft = {
|
||||
rating: 4.5,
|
||||
maxStars: 5,
|
||||
size: '24px',
|
||||
filledColor: '#f59e0b',
|
||||
emptyColor: '#d1d5db',
|
||||
filledColor: tokenRef('warning'),
|
||||
emptyColor: tokenRef('border'),
|
||||
style: {},
|
||||
animation: '',
|
||||
animationDelay: '',
|
||||
@@ -123,8 +124,8 @@ StarRating.craft = {
|
||||
: 5;
|
||||
// Sanitized -- raw string-interpolation sinks in the star glyphs below.
|
||||
const size = cssValue(props.size) || '24px';
|
||||
const filledColor = cssValue(props.filledColor) || '#f59e0b';
|
||||
const emptyColor = cssValue(props.emptyColor) || '#d1d5db';
|
||||
const filledColor = cssValue(props.filledColor) || tokenRef('warning');
|
||||
const emptyColor = cssValue(props.emptyColor) || tokenRef('border');
|
||||
const wrapperStyle = cssPropsToString({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -108,3 +108,12 @@ describe('TextBlock.toHtml inline rich text (#46)', () => {
|
||||
expect((TextBlock as any).craft.props.html).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TextBlock.toHtml design tokens (#44)', () => {
|
||||
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
|
||||
const { html } = toHtml({ ...(TextBlock as any).craft.props }, '');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ["text"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ["#3f3f46"]) 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 } from '../../utils/escape';
|
||||
@@ -65,7 +66,7 @@ TextBlock.craft = {
|
||||
style: {
|
||||
fontSize: '16px',
|
||||
lineHeight: '1.6',
|
||||
color: '#3f3f46',
|
||||
color: tokenRef('text'),
|
||||
letterSpacing: '',
|
||||
textTransform: '' as CSSProperties['textTransform'],
|
||||
fontStyle: '' as CSSProperties['fontStyle'],
|
||||
|
||||
@@ -46,3 +46,13 @@ describe('StickyCallBar.toHtml', () => {
|
||||
expect(body).toContain('href="tel:+15551234567"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('StickyCallBar.toHtml design tokens (#44)', () => {
|
||||
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
|
||||
const props = { ...((StickyCallBar as any).craft.props as object) };
|
||||
const { html } = (((StickyCallBar) as any).toHtml)(props, '', 'node-tok');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ['bg']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of [] as string[]) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
|
||||
});
|
||||
});
|
||||
@@ -42,7 +42,7 @@ export const StickyCallBar: UserComponent<StickyCallBarProps> = ({
|
||||
directionsUrl = 'https://maps.google.com/?q=Your+Business',
|
||||
directionsLabel = 'Directions',
|
||||
bgColor = tokenRef('primary'),
|
||||
textColor = '#ffffff',
|
||||
textColor = tokenRef('bg'),
|
||||
style = {},
|
||||
}) => {
|
||||
const { connectors: { connect, drag }, selected } = useNode((node) => ({ selected: node.events.selected }));
|
||||
@@ -66,7 +66,7 @@ StickyCallBar.craft = {
|
||||
directionsUrl: 'https://maps.google.com/?q=Your+Business',
|
||||
directionsLabel: 'Directions',
|
||||
bgColor: tokenRef('primary'),
|
||||
textColor: '#ffffff',
|
||||
textColor: tokenRef('bg'),
|
||||
position: 'bottom',
|
||||
style: {},
|
||||
animation: '',
|
||||
@@ -83,7 +83,7 @@ StickyCallBar.craft = {
|
||||
// end of the page to compensate (documented, not automatic).
|
||||
(StickyCallBar as any).toHtml = (props: StickyCallBarProps, _childrenHtml: string) => {
|
||||
const bg = cssValue(props.bgColor) || tokenRef('primary');
|
||||
const fg = cssValue(props.textColor) || '#ffffff';
|
||||
const fg = cssValue(props.textColor) || tokenRef('bg');
|
||||
const tel = telHref(props.phone);
|
||||
const dir = httpUrl(props.directionsUrl);
|
||||
const links: string[] = [];
|
||||
|
||||
@@ -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>`,
|
||||
};
|
||||
};
|
||||
@@ -72,3 +72,13 @@ describe('BackgroundSection.toHtml bgPosition focal point (#55)', () => {
|
||||
expect(html).not.toContain('<script>alert(1)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BackgroundSection.toHtml design tokens (#44)', () => {
|
||||
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
|
||||
const props = { ...((BackgroundSection as any).craft.props as object) };
|
||||
const { html } = (((BackgroundSection) as any).toHtml)(props, '', 'node-tok');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ['text']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ['#1e293b']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react';
|
||||
import { useNode, Element, UserComponent } from '@craftjs/core';
|
||||
import { Container } from './Container';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { tokenRef } from '../../utils/tokens';
|
||||
import { escapeAttr, cssValue } from '../../utils/escape';
|
||||
|
||||
interface BackgroundSectionProps {
|
||||
@@ -20,7 +21,7 @@ interface BackgroundSectionProps {
|
||||
export const BackgroundSection: UserComponent<BackgroundSectionProps> = ({
|
||||
bgImage = '',
|
||||
bgPosition = '',
|
||||
bgColor = '#1e293b',
|
||||
bgColor = tokenRef('text'),
|
||||
overlayColor = '#000000',
|
||||
overlayOpacity = 0.4,
|
||||
innerMaxWidth = '1200px',
|
||||
@@ -79,7 +80,7 @@ BackgroundSection.craft = {
|
||||
props: {
|
||||
bgImage: '',
|
||||
bgPosition: '',
|
||||
bgColor: '#1e293b',
|
||||
bgColor: tokenRef('text'),
|
||||
overlayColor: '#000000',
|
||||
overlayOpacity: 0.4,
|
||||
innerMaxWidth: '1200px',
|
||||
@@ -100,7 +101,7 @@ BackgroundSection.craft = {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
minHeight: '200px',
|
||||
backgroundColor: props.bgColor || '#1e293b',
|
||||
backgroundColor: props.bgColor || tokenRef('text'),
|
||||
backgroundImage: props.bgImage ? `url(${props.bgImage})` : undefined,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: cssValue(props.bgPosition) || 'center',
|
||||
|
||||
@@ -36,3 +36,12 @@ describe('Accordion.craft.props includes the box-model/animation/visibility roll
|
||||
expect(props).toHaveProperty('hideOnMobile', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accordion.toHtml design tokens (#44)', () => {
|
||||
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
|
||||
const { html } = toHtml({ items: [{ title: 'A', content: 'B' }] }, '');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ["bg", "border", "text", "muted"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ["#f8fafc", "#e2e8f0", "#18181b", "#4b5563"]) expect(lower, `old ${hex}`).not.toContain(hex);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { CSSProperties, useState } from 'react';
|
||||
import { tokenRef } from '../../utils/tokens';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { escapeHtml, escapeAttr, cssValue } from '../../utils/escape';
|
||||
@@ -33,10 +34,10 @@ const defaultItems: AccordionItem[] = [
|
||||
export const Accordion: UserComponent<AccordionProps> = ({
|
||||
items = defaultItems,
|
||||
style = {},
|
||||
headerBg = '#f8fafc',
|
||||
headerColor = '#18181b',
|
||||
contentBg = '#ffffff',
|
||||
borderColor = '#e2e8f0',
|
||||
headerBg = tokenRef('bg'),
|
||||
headerColor = tokenRef('text'),
|
||||
contentBg = tokenRef('bg'),
|
||||
borderColor = tokenRef('border'),
|
||||
anchorId,
|
||||
}) => {
|
||||
const {
|
||||
@@ -67,7 +68,7 @@ export const Accordion: UserComponent<AccordionProps> = ({
|
||||
id={anchorId || undefined}
|
||||
style={{
|
||||
padding: '60px 20px',
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: tokenRef('bg'),
|
||||
outline: selected ? '2px solid #3b82f6' : 'none',
|
||||
...style,
|
||||
}}
|
||||
@@ -110,7 +111,7 @@ export const Accordion: UserComponent<AccordionProps> = ({
|
||||
style={{
|
||||
padding: '16px 20px',
|
||||
backgroundColor: contentBg,
|
||||
color: '#4b5563',
|
||||
color: tokenRef('muted'),
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.6',
|
||||
borderTop: `1px solid ${borderColor}`,
|
||||
@@ -134,11 +135,11 @@ Accordion.craft = {
|
||||
displayName: 'Accordion',
|
||||
props: {
|
||||
items: defaultItems,
|
||||
style: { backgroundColor: '#ffffff' },
|
||||
headerBg: '#f8fafc',
|
||||
headerColor: '#18181b',
|
||||
contentBg: '#ffffff',
|
||||
borderColor: '#e2e8f0',
|
||||
style: { backgroundColor: tokenRef('bg') },
|
||||
headerBg: tokenRef('bg'),
|
||||
headerColor: tokenRef('text'),
|
||||
contentBg: tokenRef('bg'),
|
||||
borderColor: tokenRef('border'),
|
||||
anchorId: '',
|
||||
animation: '',
|
||||
animationDelay: '',
|
||||
@@ -163,10 +164,10 @@ Accordion.craft = {
|
||||
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||
// Sanitized -- raw string-interpolation sinks in the <details>/<summary>
|
||||
// style attributes below.
|
||||
const headerBg = cssValue(props.headerBg) || '#f8fafc';
|
||||
const headerColor = cssValue(props.headerColor) || '#18181b';
|
||||
const contentBg = cssValue(props.contentBg) || '#ffffff';
|
||||
const borderColor = cssValue(props.borderColor) || '#e2e8f0';
|
||||
const headerBg = cssValue(props.headerBg) || tokenRef('bg');
|
||||
const headerColor = cssValue(props.headerColor) || tokenRef('text');
|
||||
const contentBg = cssValue(props.contentBg) || tokenRef('bg');
|
||||
const borderColor = cssValue(props.borderColor) || tokenRef('border');
|
||||
const items = props.items || defaultItems;
|
||||
|
||||
const panels = items.map((item, i) => {
|
||||
@@ -178,7 +179,7 @@ Accordion.craft = {
|
||||
<summary style="padding:16px 20px;background-color:${headerBg};color:${headerColor};cursor:pointer;font-weight:600;font-size:16px;list-style:none;display:flex;justify-content:space-between;align-items:center">
|
||||
${escapeHtml(item.title)}
|
||||
</summary>
|
||||
<div style="padding:16px 20px;background-color:${contentBg};color:#4b5563;font-size:14px;line-height:1.6;border-top:1px solid ${borderColor}">
|
||||
<div style="padding:16px 20px;background-color:${contentBg};color:${tokenRef('muted')};font-size:14px;line-height:1.6;border-top:1px solid ${borderColor}">
|
||||
${escapeHtml(item.content)}
|
||||
</div>
|
||||
</details>`;
|
||||
|
||||
@@ -35,3 +35,12 @@ describe('CTASection.craft.props includes the box-model/animation/visibility rol
|
||||
expect(props).toHaveProperty('hideOnMobile', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CTASection.toHtml design tokens (#44)', () => {
|
||||
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
|
||||
const { html } = toHtml({ ...(CTASection as any).craft.props }, '');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ["bg", "text", "primary", "secondary"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ["#18181b", "#2563eb", "#7c3aed"]) expect(lower, `old ${hex}`).not.toContain(hex);
|
||||
});
|
||||
});
|
||||
@@ -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 } from '../../utils/escape';
|
||||
import { tokenRef } from '../../utils/tokens';
|
||||
|
||||
interface CTASectionProps {
|
||||
heading?: string;
|
||||
@@ -21,7 +22,10 @@ interface CTASectionProps {
|
||||
hideOnMobile?: boolean;
|
||||
}
|
||||
|
||||
const defaultGradient = 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)';
|
||||
const defaultGradient = `linear-gradient(135deg, ${tokenRef('primary')} 0%, ${tokenRef('secondary')} 100%)`;
|
||||
// Fixed white: the default background is a primary->secondary gradient / image, so heading
|
||||
// and outline-button text stay light regardless of the palette (no token means "on gradient").
|
||||
const WHITE_ON_GRADIENT = '#ffffff';
|
||||
|
||||
export const CTASection: UserComponent<CTASectionProps> = ({
|
||||
heading = 'Ready to Get Started?',
|
||||
@@ -41,7 +45,7 @@ export const CTASection: UserComponent<CTASectionProps> = ({
|
||||
}));
|
||||
|
||||
const effectiveCtas = normalizeCtas({ ctas, buttonText, buttonHref });
|
||||
const ctaDefaults = { primaryBg: '#ffffff', primaryText: '#18181b', outlineText: '#ffffff' };
|
||||
const ctaDefaults = { primaryBg: tokenRef('bg'), primaryText: tokenRef('text'), outlineText: WHITE_ON_GRADIENT };
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -56,7 +60,7 @@ export const CTASection: UserComponent<CTASectionProps> = ({
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: '700px', margin: '0 auto' }}>
|
||||
<h2 style={{ fontSize: '36px', fontWeight: '700', color: '#ffffff', marginBottom: '12px' }}>
|
||||
<h2 style={{ fontSize: '36px', fontWeight: '700', color: WHITE_ON_GRADIENT, marginBottom: '12px' }}>
|
||||
{heading}
|
||||
</h2>
|
||||
<p style={{ fontSize: '18px', color: 'rgba(255,255,255,0.85)', marginBottom: '28px', lineHeight: '1.6' }}>
|
||||
@@ -111,12 +115,12 @@ CTASection.craft = {
|
||||
...props.style,
|
||||
});
|
||||
const ctas = normalizeCtas(props);
|
||||
const buttonsHtml = ctasToHtml(ctas, { primaryBg: '#ffffff', primaryText: '#18181b', outlineText: '#ffffff' });
|
||||
const buttonsHtml = ctasToHtml(ctas, { primaryBg: tokenRef('bg'), primaryText: tokenRef('text'), outlineText: WHITE_ON_GRADIENT });
|
||||
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||
return {
|
||||
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
||||
<div style="max-width:700px;margin:0 auto">
|
||||
<h2 style="font-size:36px;font-weight:700;color:#ffffff;margin-bottom:12px">${escapeHtml(props.heading || '')}</h2>
|
||||
<h2 style="font-size:36px;font-weight:700;color:${WHITE_ON_GRADIENT};margin-bottom:12px">${escapeHtml(props.heading || '')}</h2>
|
||||
<p style="font-size:18px;color:rgba(255,255,255,0.85);margin-bottom:28px;line-height:1.6">${escapeHtml(props.description || '')}</p>
|
||||
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">${buttonsHtml}</div>
|
||||
</div>
|
||||
|
||||
@@ -35,3 +35,12 @@ describe('CallToAction.craft.props includes the box-model/animation/visibility r
|
||||
expect(props).toHaveProperty('hideOnMobile', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CallToAction.toHtml design tokens (#44)', () => {
|
||||
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
|
||||
const { html } = toHtml({ ...(CallToAction as any).craft.props }, '');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ["bg", "text", "primary", "secondary"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ["#18181b", "#2563eb", "#7c3aed"]) expect(lower, `old ${hex}`).not.toContain(hex);
|
||||
});
|
||||
});
|
||||
@@ -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, cssValue } from '../../utils/escape';
|
||||
import { tokenRef } from '../../utils/tokens';
|
||||
|
||||
interface CallToActionProps {
|
||||
heading?: string;
|
||||
@@ -28,7 +29,16 @@ interface CallToActionProps {
|
||||
hideOnMobile?: boolean;
|
||||
}
|
||||
|
||||
const defaultGradient = 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)';
|
||||
const defaultGradient = `linear-gradient(135deg, ${tokenRef('primary')} 0%, ${tokenRef('secondary')} 100%)`;
|
||||
// Fixed white: default background is a gradient / image + dark overlay, so text stays light
|
||||
// whatever the palette. Everything else (button, gradient) follows the design tokens.
|
||||
const WHITE_ON_GRADIENT = '#ffffff';
|
||||
|
||||
/** A light button (token bg, or legacy white / slate-50) takes the text token; any other fill takes the bg token. */
|
||||
function isLightButton(c: string): boolean {
|
||||
const v = c.trim().toLowerCase();
|
||||
return v === tokenRef('bg') || v === '#ffffff' || v === '#f8fafc';
|
||||
}
|
||||
|
||||
export const CallToAction: UserComponent<CallToActionProps> = ({
|
||||
heading = 'Ready to Get Started?',
|
||||
@@ -42,8 +52,8 @@ export const CallToAction: UserComponent<CallToActionProps> = ({
|
||||
bgValue = defaultGradient,
|
||||
overlayColor = '#000000',
|
||||
overlayOpacity = 0,
|
||||
textColor = '#ffffff',
|
||||
buttonColor = '#ffffff',
|
||||
textColor = WHITE_ON_GRADIENT,
|
||||
buttonColor = tokenRef('bg'),
|
||||
anchorId,
|
||||
style = {},
|
||||
}) => {
|
||||
@@ -65,8 +75,7 @@ export const CallToAction: UserComponent<CallToActionProps> = ({
|
||||
bgStyle.backgroundPosition = 'center';
|
||||
}
|
||||
|
||||
const isButtonDark = buttonColor === '#ffffff' || buttonColor === '#f8fafc';
|
||||
const buttonTextColor = isButtonDark ? '#18181b' : '#ffffff';
|
||||
const buttonTextColor = isLightButton(buttonColor) ? tokenRef('text') : tokenRef('bg');
|
||||
|
||||
const effectiveCtas = normalizeCtas({ ctas, buttonText, buttonHref, secondaryButtonText, secondaryButtonHref });
|
||||
const ctaDefaults = { primaryBg: buttonColor, primaryText: buttonTextColor, outlineText: textColor };
|
||||
@@ -133,8 +142,8 @@ CallToAction.craft = {
|
||||
bgValue: defaultGradient,
|
||||
overlayColor: '#000000',
|
||||
overlayOpacity: 0,
|
||||
textColor: '#ffffff',
|
||||
buttonColor: '#ffffff',
|
||||
textColor: WHITE_ON_GRADIENT,
|
||||
buttonColor: tokenRef('bg'),
|
||||
style: {},
|
||||
animation: '',
|
||||
animationDelay: '',
|
||||
@@ -156,10 +165,9 @@ CallToAction.craft = {
|
||||
const bgValue = props.bgValue || defaultGradient;
|
||||
// Sanitized -- raw string-interpolation sink in the heading/description
|
||||
// style attributes below.
|
||||
const textColor = cssValue(props.textColor) || '#ffffff';
|
||||
const buttonColor = props.buttonColor || '#ffffff';
|
||||
const isButtonDark = buttonColor === '#ffffff' || buttonColor === '#f8fafc';
|
||||
const buttonTextColor = isButtonDark ? '#18181b' : '#ffffff';
|
||||
const textColor = cssValue(props.textColor) || WHITE_ON_GRADIENT;
|
||||
const buttonColor = props.buttonColor || tokenRef('bg');
|
||||
const buttonTextColor = isLightButton(buttonColor) ? tokenRef('text') : tokenRef('bg');
|
||||
|
||||
const sectionCss: CSSProperties = {
|
||||
position: 'relative',
|
||||
|
||||
@@ -190,3 +190,13 @@ describe('ContentSlider.toHtml palette token fallback (Refs #44)', () => {
|
||||
expect(html).not.toContain('#3b82f6');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ContentSlider.toHtml design tokens (#44)', () => {
|
||||
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
|
||||
const props = { ...((ContentSlider as any).craft.props as object) };
|
||||
const { html } = (((ContentSlider) as any).toHtml)(props, '', 'node-tok');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ['primary', 'secondary', 'success', 'warning', 'error', 'text', 'bg']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ['#3b82f6', '#8b5cf6', '#10b981', '#059669', '#f59e0b', '#ef4444', '#18181b']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
|
||||
});
|
||||
});
|
||||
@@ -29,6 +29,9 @@ interface ContentSliderProps {
|
||||
hideOnMobile?: boolean;
|
||||
}
|
||||
|
||||
// White heading/subtitle/dots stay literal: they sit on a slide gradient or
|
||||
// image, where no token means "text on imagery" (same rule as CTASection's
|
||||
// WHITE_ON_GRADIENT); the translucent-white arrow discs are scrims.
|
||||
const defaultSlides: Slide[] = [
|
||||
{
|
||||
type: 'image',
|
||||
@@ -37,7 +40,7 @@ const defaultSlides: Slide[] = [
|
||||
text: 'Welcome to our showcase',
|
||||
buttonText: 'Learn More',
|
||||
buttonHref: '#',
|
||||
bgColor: 'linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%)',
|
||||
bgColor: `linear-gradient(135deg, ${tokenRef('primary')} 0%, ${tokenRef('secondary')} 100%)`,
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
@@ -46,7 +49,7 @@ const defaultSlides: Slide[] = [
|
||||
text: 'Discover something amazing',
|
||||
buttonText: 'Get Started',
|
||||
buttonHref: '#',
|
||||
bgColor: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
bgColor: `linear-gradient(135deg, ${tokenRef('success')} 0%, ${tokenRef('accent')} 100%)`,
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
@@ -55,7 +58,7 @@ const defaultSlides: Slide[] = [
|
||||
text: 'Build your future today',
|
||||
buttonText: 'Contact Us',
|
||||
buttonHref: '#',
|
||||
bgColor: 'linear-gradient(135deg, #f59e0b 0%, #ef4444 100%)',
|
||||
bgColor: `linear-gradient(135deg, ${tokenRef('warning')} 0%, ${tokenRef('error')} 100%)`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -102,7 +105,7 @@ export const ContentSlider: UserComponent<ContentSliderProps> = ({
|
||||
borderRadius: '50%',
|
||||
border: 'none',
|
||||
background: 'rgba(255,255,255,0.9)',
|
||||
color: '#18181b',
|
||||
color: tokenRef('text'),
|
||||
fontSize: '16px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
@@ -156,8 +159,8 @@ export const ContentSlider: UserComponent<ContentSliderProps> = ({
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '12px 28px',
|
||||
background: '#ffffff',
|
||||
color: '#18181b',
|
||||
background: tokenRef('bg'),
|
||||
color: tokenRef('text'),
|
||||
textDecoration: 'none',
|
||||
borderRadius: '8px',
|
||||
fontWeight: '600',
|
||||
@@ -313,7 +316,7 @@ ContentSlider.craft = {
|
||||
contentParts.push(`<p style="font-size:18px;color:rgba(255,255,255,0.9);margin-bottom:20px;font-family:Inter,sans-serif;text-shadow:0 1px 4px rgba(0,0,0,0.3)">${escapeHtml(slide.text)}</p>`);
|
||||
}
|
||||
if (slide.buttonText) {
|
||||
contentParts.push(`<a href="${escapeAttr(safeUrl(slide.buttonHref || '#'))}" style="display:inline-block;padding:12px 28px;background:#ffffff;color:#18181b;text-decoration:none;border-radius:8px;font-weight:600;font-size:15px;font-family:Inter,sans-serif">${escapeHtml(slide.buttonText)}</a>`);
|
||||
contentParts.push(`<a href="${escapeAttr(safeUrl(slide.buttonHref || '#'))}" style="display:inline-block;padding:12px 28px;background:${tokenRef('bg')};color:${tokenRef('text')};text-decoration:none;border-radius:8px;font-weight:600;font-size:15px;font-family:Inter,sans-serif">${escapeHtml(slide.buttonText)}</a>`);
|
||||
}
|
||||
|
||||
const innerHtml = contentParts.length > 0
|
||||
@@ -326,8 +329,8 @@ ContentSlider.craft = {
|
||||
}).join('\n ');
|
||||
|
||||
const arrowsHtml = showArrows && items.length > 1
|
||||
? `<button onclick="${uid}_prev()" aria-label="Previous slide" style="position:absolute;top:50%;left:16px;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.9);color:#18181b;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.15)"><i class="fa fa-chevron-left" aria-hidden="true"></i></button>
|
||||
<button onclick="${uid}_next()" aria-label="Next slide" style="position:absolute;top:50%;right:16px;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.9);color:#18181b;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.15)"><i class="fa fa-chevron-right" aria-hidden="true"></i></button>`
|
||||
? `<button onclick="${uid}_prev()" aria-label="Previous slide" style="position:absolute;top:50%;left:16px;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.9);color:${tokenRef('text')};font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.15)"><i class="fa fa-chevron-left" aria-hidden="true"></i></button>
|
||||
<button onclick="${uid}_next()" aria-label="Next slide" style="position:absolute;top:50%;right:16px;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.9);color:${tokenRef('text')};font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.15)"><i class="fa fa-chevron-right" aria-hidden="true"></i></button>`
|
||||
: '';
|
||||
|
||||
const dotsHtml = showDots && items.length > 1
|
||||
|
||||
@@ -73,3 +73,13 @@ describe('Countdown.craft.props includes the box-model/animation/visibility roll
|
||||
expect(props).toHaveProperty('hideOnMobile', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Countdown.toHtml design tokens (#44)', () => {
|
||||
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
|
||||
const props = { ...((Countdown as any).craft.props as object) };
|
||||
const { html } = (((Countdown) as any).toHtml)(props, '', 'node-tok');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ['text', 'bg']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ['#18181b', 'rgba(255,255,255,0.7)']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,13 @@
|
||||
import React, { CSSProperties, useEffect, useState } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { tokenRef } from '../../utils/tokens';
|
||||
import { escapeHtml, escapeAttr, scopeId, cssValue } from '../../utils/escape';
|
||||
|
||||
// Label text on the (text-token) band: the bg token at 70% so it stays
|
||||
// contrast-safe under any palette.
|
||||
const LABEL_ON_BAND = `color-mix(in srgb, ${tokenRef('bg')} 70%, transparent)`;
|
||||
|
||||
interface CountdownProps {
|
||||
targetDate?: string;
|
||||
heading?: string;
|
||||
@@ -48,9 +53,9 @@ export const Countdown: UserComponent<CountdownProps> = ({
|
||||
targetDate = DEFAULT_TARGET,
|
||||
heading = 'Coming Soon',
|
||||
style = {},
|
||||
digitColor = '#ffffff',
|
||||
labelColor = 'rgba(255,255,255,0.7)',
|
||||
bgColor = '#18181b',
|
||||
digitColor = tokenRef('bg'),
|
||||
labelColor = LABEL_ON_BAND,
|
||||
bgColor = tokenRef('text'),
|
||||
anchorId,
|
||||
}) => {
|
||||
const {
|
||||
@@ -140,9 +145,9 @@ Countdown.craft = {
|
||||
targetDate: DEFAULT_TARGET,
|
||||
heading: 'Coming Soon',
|
||||
style: {},
|
||||
digitColor: '#ffffff',
|
||||
labelColor: 'rgba(255,255,255,0.7)',
|
||||
bgColor: '#18181b',
|
||||
digitColor: tokenRef('bg'),
|
||||
labelColor: LABEL_ON_BAND,
|
||||
bgColor: tokenRef('text'),
|
||||
anchorId: '',
|
||||
animation: '',
|
||||
animationDelay: '',
|
||||
@@ -164,12 +169,12 @@ Countdown.craft = {
|
||||
targetDate = DEFAULT_TARGET,
|
||||
heading = 'Coming Soon',
|
||||
style = {},
|
||||
bgColor = '#18181b',
|
||||
bgColor = tokenRef('text'),
|
||||
} = props;
|
||||
// Sanitized -- raw string-interpolation sinks in the heading/digit/label
|
||||
// style attributes below.
|
||||
const digitColor = cssValue(props.digitColor) || '#ffffff';
|
||||
const labelColor = cssValue(props.labelColor) || 'rgba(255,255,255,0.7)';
|
||||
const digitColor = cssValue(props.digitColor) || tokenRef('bg');
|
||||
const labelColor = cssValue(props.labelColor) || LABEL_ON_BAND;
|
||||
|
||||
const sectionStyle = cssPropsToString({
|
||||
padding: '60px 20px',
|
||||
|
||||
@@ -41,3 +41,12 @@ describe('FeaturesGrid.toHtml palette token fallback (Refs #44)', () => {
|
||||
expect(html).not.toContain('#3b82f6');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FeaturesGrid.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", "muted"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ["#f8fafc", "#e2e8f0", "#18181b", "#64748b", "#fff"]) expect(lower, `old ${hex}`).not.toContain(hex);
|
||||
});
|
||||
});
|
||||
@@ -52,7 +52,7 @@ export const FeaturesGrid: UserComponent<FeaturesGridProps> = ({
|
||||
id={anchorId || undefined}
|
||||
style={{
|
||||
padding: '80px 20px',
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: tokenRef('bg'),
|
||||
outline: selected ? '2px solid #3b82f6' : 'none',
|
||||
...style,
|
||||
}}
|
||||
@@ -65,8 +65,8 @@ export const FeaturesGrid: UserComponent<FeaturesGridProps> = ({
|
||||
textAlign: 'center',
|
||||
padding: '32px 24px',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#f8fafc',
|
||||
border: '1px solid #e2e8f0',
|
||||
backgroundColor: tokenRef('bg'),
|
||||
border: `1px solid ${tokenRef('border')}`,
|
||||
}}
|
||||
>
|
||||
{feat.image ? (
|
||||
@@ -78,13 +78,13 @@ export const FeaturesGrid: UserComponent<FeaturesGridProps> = ({
|
||||
) : (
|
||||
<div style={{ fontSize: '36px', marginBottom: '16px' }}>{feat.icon}</div>
|
||||
)}
|
||||
<h3 style={{ fontSize: '20px', fontWeight: '600', color: '#18181b', marginBottom: '8px' }}>{feat.title}</h3>
|
||||
<p style={{ fontSize: '14px', color: '#64748b', lineHeight: '1.6' }}>{feat.description}</p>
|
||||
<h3 style={{ fontSize: '20px', fontWeight: '600', color: tokenRef('text'), marginBottom: '8px' }}>{feat.title}</h3>
|
||||
<p style={{ fontSize: '14px', color: tokenRef('muted'), lineHeight: '1.6' }}>{feat.description}</p>
|
||||
{feat.buttonText ? (
|
||||
<a
|
||||
href={feat.buttonUrl || '#'}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
style={{ display: 'inline-block', marginTop: '16px', padding: '10px 24px', background: tokenRef('primary'), color: '#fff', borderRadius: '8px', textDecoration: 'none', fontSize: '14px', fontWeight: 600 }}
|
||||
style={{ display: 'inline-block', marginTop: '16px', padding: '10px 24px', background: tokenRef('primary'), color: tokenRef('bg'), borderRadius: '8px', textDecoration: 'none', fontSize: '14px', fontWeight: 600 }}
|
||||
>
|
||||
{feat.buttonText}
|
||||
</a>
|
||||
@@ -102,7 +102,7 @@ FeaturesGrid.craft = {
|
||||
displayName: 'Features Grid',
|
||||
props: {
|
||||
features: defaultFeatures,
|
||||
style: { backgroundColor: '#ffffff' },
|
||||
style: { backgroundColor: tokenRef('bg') },
|
||||
anchorId: '',
|
||||
animation: '',
|
||||
animationDelay: '',
|
||||
@@ -130,12 +130,12 @@ 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:${tokenRef('primary')};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:${tokenRef('bg')};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">
|
||||
return `<div style="text-align:center;padding:32px 24px;border-radius:12px;background-color:${tokenRef('bg')};border:1px solid ${tokenRef('border')}">
|
||||
${media}
|
||||
<h3 style="font-size:20px;font-weight:600;color:#18181b;margin-bottom:8px">${escapeHtml(feat.title)}</h3>
|
||||
<p style="font-size:14px;color:#64748b;line-height:1.6">${escapeHtml(feat.description)}</p>${button}
|
||||
<h3 style="font-size:20px;font-weight:600;color:${tokenRef('text')};margin-bottom:8px">${escapeHtml(feat.title)}</h3>
|
||||
<p style="font-size:14px;color:${tokenRef('muted')};line-height:1.6">${escapeHtml(feat.description)}</p>${button}
|
||||
</div>`;
|
||||
}).join('\n ');
|
||||
|
||||
|
||||
@@ -216,3 +216,12 @@ describe('Gallery.toHtml lightbox focus management (M-2)', () => {
|
||||
expect(closeFnMatch![0]).toMatch(/\.focus\(\)/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Gallery.toHtml design tokens (#44)', () => {
|
||||
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
|
||||
const { html } = toHtml({ images: [{ src: 'a.png', alt: 'a', caption: 'c' }] }, '');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ["bg"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ["#f1f5f9"]) 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, safeImageUrl, scopeId, cssValue } from '../../utils/escape';
|
||||
@@ -56,7 +57,7 @@ export const Gallery: UserComponent<GalleryProps> = ({
|
||||
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
|
||||
style={{
|
||||
padding: '60px 20px',
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: tokenRef('bg'),
|
||||
outline: selected ? '2px solid #3b82f6' : 'none',
|
||||
...style,
|
||||
}}
|
||||
@@ -81,7 +82,7 @@ export const Gallery: UserComponent<GalleryProps> = ({
|
||||
objectFit: 'cover',
|
||||
display: 'block',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#f1f5f9',
|
||||
backgroundColor: tokenRef('bg'),
|
||||
}}
|
||||
/>
|
||||
{img.caption && (
|
||||
@@ -118,7 +119,7 @@ Gallery.craft = {
|
||||
columns: 3,
|
||||
gap: '16px',
|
||||
style: {
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: tokenRef('bg'),
|
||||
marginTop: '', marginRight: '', marginBottom: '', marginLeft: '',
|
||||
paddingTop: '', paddingRight: '', paddingBottom: '', paddingLeft: '',
|
||||
border: 'none',
|
||||
@@ -174,7 +175,7 @@ Gallery.craft = {
|
||||
const lbAttr = lightbox ? ` data-lb-src="${escapeAttr(safeImageUrl(img.src || ''))}" role="button" tabindex="0"` : '';
|
||||
const itemStyle = lightbox ? 'cursor:pointer;position:relative;overflow:hidden;border-radius:8px' : 'position:relative;overflow:hidden;border-radius:8px';
|
||||
return `<div${lbAttr} style="${itemStyle}">
|
||||
<img src="${escapeAttr(safeImageUrl(img.src || ''))}" alt="${escapeAttr(img.alt)}" style="width:100%;height:200px;object-fit:cover;display:block;border-radius:8px;background-color:#f1f5f9" />
|
||||
<img src="${escapeAttr(safeImageUrl(img.src || ''))}" alt="${escapeAttr(img.alt)}" style="width:100%;height:200px;object-fit:cover;display:block;border-radius:8px;background-color:${tokenRef('bg')}" />
|
||||
${caption}
|
||||
</div>`;
|
||||
}).join('\n ');
|
||||
|
||||
@@ -65,3 +65,24 @@ describe('HeroSimple.toHtml bgPosition focal point (#55)', () => {
|
||||
expect(html).not.toContain('<script>alert(1)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('HeroSimple.toHtml design tokens (#44)', () => {
|
||||
test('default craft props export var(--wsb-*) and none of the old design hex', () => {
|
||||
const props = { ...((HeroSimple as any).craft.props as object) };
|
||||
const { html } = (((HeroSimple) as any).toHtml)(props, '', 'node-tok');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ['text', 'bg']) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ['#1e293b', '#667eea', '#764ba2']) expect(lower, `old ${hex}`).not.toContain(hex.toLowerCase());
|
||||
});
|
||||
});
|
||||
|
||||
describe('HeroSimple.toHtml gradient defaults are tokens (#44)', () => {
|
||||
test('bgType gradient with the default stops uses primary -> secondary, not #667eea/#764ba2', () => {
|
||||
const props = { ...((HeroSimple as any).craft.props as object), bgType: 'gradient' };
|
||||
const { html } = toHtml(props, '', 'node-g');
|
||||
expect(html).toContain('var(--wsb-primary)');
|
||||
expect(html).toContain('var(--wsb-secondary)');
|
||||
expect(html.toLowerCase()).not.toContain('#667eea');
|
||||
expect(html.toLowerCase()).not.toContain('#764ba2');
|
||||
});
|
||||
});
|
||||
@@ -40,12 +40,12 @@ interface HeroProps {
|
||||
function buildBackground(props: HeroProps): string {
|
||||
switch (props.bgType) {
|
||||
case 'gradient':
|
||||
return `linear-gradient(${props.bgGradientAngle || 135}deg, ${props.bgGradientFrom || '#667eea'}, ${props.bgGradientTo || '#764ba2'})`;
|
||||
return `linear-gradient(${props.bgGradientAngle || 135}deg, ${props.bgGradientFrom || tokenRef('primary')}, ${props.bgGradientTo || tokenRef('secondary')})`;
|
||||
case 'image':
|
||||
return props.bgImage ? `url('${props.bgImage}') center/cover no-repeat` : '#1e293b';
|
||||
return props.bgImage ? `url('${props.bgImage}') center/cover no-repeat` : tokenRef('text');
|
||||
case 'color':
|
||||
default:
|
||||
return props.bgColor || '#1e293b';
|
||||
return props.bgColor || tokenRef('text');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,18 +58,18 @@ export const HeroSimple: UserComponent<HeroProps> = ({
|
||||
secondaryButtonText,
|
||||
secondaryButtonHref,
|
||||
bgType = 'color',
|
||||
bgColor = '#1e293b',
|
||||
bgGradientFrom = '#667eea',
|
||||
bgGradientTo = '#764ba2',
|
||||
bgColor = tokenRef('text'),
|
||||
bgGradientFrom = tokenRef('primary'),
|
||||
bgGradientTo = tokenRef('secondary'),
|
||||
bgGradientAngle = 135,
|
||||
bgImage = '',
|
||||
bgPosition = '',
|
||||
bgVideo = '',
|
||||
overlayColor = '#000000',
|
||||
overlayOpacity = 0,
|
||||
textColor = '#ffffff',
|
||||
textColor = tokenRef('bg'),
|
||||
buttonBgColor = tokenRef('primary'),
|
||||
buttonTextColor = '#ffffff',
|
||||
buttonTextColor = tokenRef('bg'),
|
||||
minHeight = '500px',
|
||||
verticalAlign = 'center',
|
||||
textAlign = 'center',
|
||||
@@ -177,18 +177,18 @@ HeroSimple.craft = {
|
||||
{ text: 'Get Started', href: '#', variant: 'primary' },
|
||||
] as CtaButton[],
|
||||
bgType: 'color',
|
||||
bgColor: '#1e293b',
|
||||
bgGradientFrom: '#667eea',
|
||||
bgGradientTo: '#764ba2',
|
||||
bgColor: tokenRef('text'),
|
||||
bgGradientFrom: tokenRef('primary'),
|
||||
bgGradientTo: tokenRef('secondary'),
|
||||
bgGradientAngle: 135,
|
||||
bgImage: '',
|
||||
bgPosition: '',
|
||||
bgVideo: '',
|
||||
overlayColor: '#000000',
|
||||
overlayOpacity: 0,
|
||||
textColor: '#ffffff',
|
||||
textColor: tokenRef('bg'),
|
||||
buttonBgColor: tokenRef('primary'),
|
||||
buttonTextColor: '#ffffff',
|
||||
buttonTextColor: tokenRef('bg'),
|
||||
minHeight: '500px',
|
||||
verticalAlign: 'center',
|
||||
textAlign: 'center',
|
||||
@@ -246,12 +246,12 @@ HeroSimple.craft = {
|
||||
const ctas = normalizeCtas(props);
|
||||
const buttonsHtml = ctasToHtml(ctas, {
|
||||
primaryBg: cssValue(props.buttonBgColor) || tokenRef('primary'),
|
||||
primaryText: props.buttonTextColor || '#fff',
|
||||
outlineText: props.textColor || '#fff',
|
||||
primaryText: props.buttonTextColor || tokenRef('bg'),
|
||||
outlineText: props.textColor || tokenRef('bg'),
|
||||
});
|
||||
|
||||
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||
const heroTextColor = cssValue(props.textColor) || '#fff';
|
||||
const heroTextColor = cssValue(props.textColor) || tokenRef('bg');
|
||||
return {
|
||||
html: `<section${idAttr} style="${sectionStyle}">
|
||||
${videoHtml}${overlayHtml}
|
||||
|
||||
@@ -94,3 +94,12 @@ describe('NumberCounter.toHtml palette token fallbacks (Refs #44)', () => {
|
||||
expect(html).not.toContain('#6b7280');
|
||||
});
|
||||
});
|
||||
|
||||
describe('NumberCounter.toHtml design tokens (#44)', () => {
|
||||
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
|
||||
const { html } = toHtml({ counters: [{ number: 1, suffix: '+', label: 'L' }] }, '');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ["bg"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ["#ffffff"]) expect(lower, `old ${hex}`).not.toContain(hex);
|
||||
});
|
||||
});
|
||||
@@ -53,7 +53,7 @@ export const NumberCounter: UserComponent<NumberCounterProps> = ({
|
||||
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
|
||||
style={{
|
||||
padding: '60px 20px',
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: tokenRef('bg'),
|
||||
outline: selected ? '2px solid #3b82f6' : 'none',
|
||||
...style,
|
||||
}}
|
||||
@@ -104,7 +104,7 @@ NumberCounter.craft = {
|
||||
numberColor: tokenRef('primary'),
|
||||
labelColor: tokenRef('muted'),
|
||||
numberSize: '48px',
|
||||
style: { backgroundColor: '#ffffff' },
|
||||
style: { backgroundColor: tokenRef('bg') },
|
||||
animation: '',
|
||||
animationDelay: '',
|
||||
hideOnDesktop: false,
|
||||
@@ -146,7 +146,7 @@ NumberCounter.craft = {
|
||||
|
||||
const sectionStyle = cssPropsToString({
|
||||
padding: '60px 20px',
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: tokenRef('bg'),
|
||||
...style,
|
||||
});
|
||||
|
||||
|
||||
@@ -40,13 +40,12 @@ describe('PricingTable.toHtml regular-card color overrides (previously hard-code
|
||||
expect(html).toContain('color:#eeeeee');
|
||||
});
|
||||
|
||||
test('unset overrides fall back to the exact prior literals (backward compatible)', () => {
|
||||
test('unset overrides fall back to the design tokens', () => {
|
||||
const { html } = toHtml({ plans }, '');
|
||||
// Regular (non-featured) card literals unchanged from before these props existed.
|
||||
expect(html).toContain('background-color:#ffffff');
|
||||
expect(html).toContain('color:#18181b');
|
||||
expect(html).toContain('color:#64748b');
|
||||
expect(html).toContain('color:#4b5563');
|
||||
// Regular (non-featured) card fallbacks are now design tokens (#44), not hex literals.
|
||||
expect(html).toContain('background-color:var(--wsb-bg)');
|
||||
expect(html).toContain('color:var(--wsb-text)');
|
||||
expect(html).toContain('color:var(--wsb-muted)');
|
||||
expect(html).toContain('color:var(--wsb-success)');
|
||||
});
|
||||
|
||||
@@ -118,3 +117,12 @@ describe('PricingTable.toHtml palette token fallbacks (Refs #44)', () => {
|
||||
expect(html).toContain('color:#222222');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PricingTable.toHtml design tokens (#44)', () => {
|
||||
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
|
||||
const { html } = toHtml({ plans: [{ name: 'A', price: '$1', period: '/m', features: ['f'], buttonText: 'B', buttonHref: '#', isFeatured: false }, { name: 'P', price: '$2', period: '/m', features: ['f'], buttonText: 'B', buttonHref: '#', isFeatured: true }] }, '');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ["bg", "border", "text", "muted", "success", "warning", "primary"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ["#e2e8f0", "#18181b", "#64748b", "#4b5563", "#bbf7d0", "#facc15", "#ffffff"]) expect(lower, `old ${hex}`).not.toContain(hex);
|
||||
});
|
||||
});
|
||||
@@ -94,13 +94,13 @@ export const PricingTable: UserComponent<PricingTableProps> = ({
|
||||
selected: node.events.selected,
|
||||
}));
|
||||
|
||||
const regCardBg = cardBg || '#ffffff';
|
||||
const regTextColor = textColor || '#18181b';
|
||||
const regSubColor = subColor || '#64748b';
|
||||
const regFeatColor = featColor || '#4b5563';
|
||||
const regCardBg = cardBg || tokenRef('bg');
|
||||
const regTextColor = textColor || tokenRef('text');
|
||||
const regSubColor = subColor || tokenRef('muted');
|
||||
const regFeatColor = featColor || tokenRef('muted');
|
||||
const regCheckColor = checkColor || tokenRef('success');
|
||||
const regBtnBg = btnBg || featuredBg;
|
||||
const regBtnColor = btnColor || '#ffffff';
|
||||
const regBtnColor = btnColor || tokenRef('bg');
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -108,7 +108,7 @@ export const PricingTable: UserComponent<PricingTableProps> = ({
|
||||
id={anchorId || undefined}
|
||||
style={{
|
||||
padding: '80px 20px',
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: tokenRef('bg'),
|
||||
outline: selected ? '2px solid #3b82f6' : 'none',
|
||||
...style,
|
||||
}}
|
||||
@@ -129,7 +129,7 @@ export const PricingTable: UserComponent<PricingTableProps> = ({
|
||||
flex: '1 1 280px',
|
||||
maxWidth: '360px',
|
||||
backgroundColor: plan.isFeatured ? featuredBg : regCardBg,
|
||||
border: plan.isFeatured ? 'none' : '1px solid #e2e8f0',
|
||||
border: plan.isFeatured ? 'none' : `1px solid ${tokenRef('border')}`,
|
||||
borderRadius: '16px',
|
||||
padding: '40px 32px',
|
||||
display: 'flex',
|
||||
@@ -145,8 +145,8 @@ export const PricingTable: UserComponent<PricingTableProps> = ({
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '-12px',
|
||||
backgroundColor: '#facc15',
|
||||
color: '#18181b',
|
||||
backgroundColor: tokenRef('warning'),
|
||||
color: tokenRef('text'),
|
||||
padding: '4px 16px',
|
||||
borderRadius: '9999px',
|
||||
fontSize: '12px',
|
||||
@@ -160,7 +160,7 @@ export const PricingTable: UserComponent<PricingTableProps> = ({
|
||||
<h3 style={{
|
||||
fontSize: '20px',
|
||||
fontWeight: '600',
|
||||
color: plan.isFeatured ? '#ffffff' : regTextColor,
|
||||
color: plan.isFeatured ? tokenRef('bg') : regTextColor,
|
||||
marginBottom: '8px',
|
||||
marginTop: plan.isFeatured ? '8px' : '0',
|
||||
}}>
|
||||
@@ -170,7 +170,7 @@ export const PricingTable: UserComponent<PricingTableProps> = ({
|
||||
<span style={{
|
||||
fontSize: '48px',
|
||||
fontWeight: '700',
|
||||
color: plan.isFeatured ? '#ffffff' : regTextColor,
|
||||
color: plan.isFeatured ? tokenRef('bg') : regTextColor,
|
||||
lineHeight: '1',
|
||||
}}>
|
||||
{plan.price}
|
||||
@@ -199,7 +199,7 @@ export const PricingTable: UserComponent<PricingTableProps> = ({
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
}}>
|
||||
<span style={{ color: plan.isFeatured ? '#bbf7d0' : regCheckColor, fontWeight: '700' }}>{bulletChars[bulletType] || '✓'}</span>
|
||||
<span style={{ color: plan.isFeatured ? tokenRef('success') : regCheckColor, fontWeight: '700' }}>{bulletChars[bulletType] || '✓'}</span>
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
@@ -211,7 +211,7 @@ export const PricingTable: UserComponent<PricingTableProps> = ({
|
||||
marginTop: 'auto',
|
||||
display: 'inline-block',
|
||||
padding: '14px 32px',
|
||||
backgroundColor: plan.isFeatured ? '#ffffff' : regBtnBg,
|
||||
backgroundColor: plan.isFeatured ? tokenRef('bg') : regBtnBg,
|
||||
color: plan.isFeatured ? featuredBg : regBtnColor,
|
||||
textDecoration: 'none',
|
||||
borderRadius: '8px',
|
||||
@@ -236,7 +236,7 @@ PricingTable.craft = {
|
||||
displayName: 'Pricing Table',
|
||||
props: {
|
||||
plans: defaultPlans,
|
||||
style: { backgroundColor: '#ffffff' },
|
||||
style: { backgroundColor: tokenRef('bg') },
|
||||
featuredBg: tokenRef('primary'),
|
||||
bulletType: 'check',
|
||||
anchorId: '',
|
||||
@@ -278,22 +278,22 @@ PricingTable.craft = {
|
||||
// the exact literal the card was previously hard-coded to (or, for the
|
||||
// button, to featuredBg) when unset, so unmodified pricing tables render
|
||||
// identically to before these props existed.
|
||||
const regCardBg = cssValue(props.cardBg) || '#ffffff';
|
||||
const regTextColor = cssValue(props.textColor) || '#18181b';
|
||||
const regSubColor = cssValue(props.subColor) || '#64748b';
|
||||
const regFeatColor = cssValue(props.featColor) || '#4b5563';
|
||||
const regCardBg = cssValue(props.cardBg) || tokenRef('bg');
|
||||
const regTextColor = cssValue(props.textColor) || tokenRef('text');
|
||||
const regSubColor = cssValue(props.subColor) || tokenRef('muted');
|
||||
const regFeatColor = cssValue(props.featColor) || tokenRef('muted');
|
||||
const regCheckColor = cssValue(props.checkColor) || tokenRef('success');
|
||||
const regBtnBg = cssValue(props.btnBg) || featuredBg;
|
||||
const regBtnColor = cssValue(props.btnColor) || '#ffffff';
|
||||
const regBtnColor = cssValue(props.btnColor) || tokenRef('bg');
|
||||
|
||||
const cards = plans.map((plan) => {
|
||||
const cardBg = plan.isFeatured ? featuredBg : regCardBg;
|
||||
const cardBorder = plan.isFeatured ? 'border:none;' : 'border:1px solid #e2e8f0;';
|
||||
const textColor = plan.isFeatured ? '#ffffff' : regTextColor;
|
||||
const cardBorder = plan.isFeatured ? 'border:none;' : `border:1px solid ${tokenRef('border')};`;
|
||||
const textColor = plan.isFeatured ? tokenRef('bg') : regTextColor;
|
||||
const subColor = plan.isFeatured ? 'rgba(255,255,255,0.8)' : regSubColor;
|
||||
const featColor = plan.isFeatured ? 'rgba(255,255,255,0.9)' : regFeatColor;
|
||||
const checkColor = plan.isFeatured ? '#bbf7d0' : regCheckColor;
|
||||
const btnBg = plan.isFeatured ? '#ffffff' : regBtnBg;
|
||||
const checkColor = plan.isFeatured ? tokenRef('success') : regCheckColor;
|
||||
const btnBg = plan.isFeatured ? tokenRef('bg') : regBtnBg;
|
||||
const btnColor = plan.isFeatured ? featuredBg : regBtnColor;
|
||||
const scale = plan.isFeatured ? 'transform:scale(1.05);' : '';
|
||||
const shadow = plan.isFeatured ? 'box-shadow:0 20px 60px rgba(59,130,246,0.3);' : 'box-shadow:0 1px 3px rgba(0,0,0,0.06);';
|
||||
@@ -303,7 +303,7 @@ PricingTable.craft = {
|
||||
).join('\n ');
|
||||
|
||||
const badge = plan.isFeatured
|
||||
? `<div style="position:absolute;top:-12px;background-color:#facc15;color:#18181b;padding:4px 16px;border-radius:9999px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:0.5px">Most Popular</div>`
|
||||
? `<div style="position:absolute;top:-12px;background-color:${tokenRef('warning')};color:${tokenRef('text')};padding:4px 16px;border-radius:9999px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:0.5px">Most Popular</div>`
|
||||
: '';
|
||||
|
||||
return `<div style="flex:1 1 280px;max-width:360px;background-color:${cardBg};${cardBorder}border-radius:16px;padding:40px 32px;display:flex;flex-direction:column;align-items:center;text-align:center;position:relative;${scale}${shadow}">
|
||||
|
||||
@@ -99,3 +99,12 @@ describe('Tabs.toHtml palette token fallback (Refs #44)', () => {
|
||||
expect(html).not.toContain('#3b82f6');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tabs.toHtml design tokens (#44)', () => {
|
||||
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
|
||||
const { html } = toHtml({ tabs: [{ title: 'A', content: 'B' }, { title: 'C', content: 'D' }] }, '');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ["bg", "border", "muted"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ["#e2e8f0", "#4b5563", "#64748b", "#f1f5f9"]) expect(lower, `old ${hex}`).not.toContain(hex);
|
||||
});
|
||||
});
|
||||
@@ -35,10 +35,10 @@ export const Tabs: UserComponent<TabsProps> = ({
|
||||
tabs = defaultTabs,
|
||||
style = {},
|
||||
activeTabBg = tokenRef('primary'),
|
||||
activeTabColor = '#ffffff',
|
||||
inactiveTabBg = '#f1f5f9',
|
||||
inactiveTabColor = '#64748b',
|
||||
contentBg = '#ffffff',
|
||||
activeTabColor = tokenRef('bg'),
|
||||
inactiveTabBg = tokenRef('bg'),
|
||||
inactiveTabColor = tokenRef('muted'),
|
||||
contentBg = tokenRef('bg'),
|
||||
anchorId,
|
||||
}) => {
|
||||
const {
|
||||
@@ -56,14 +56,14 @@ export const Tabs: UserComponent<TabsProps> = ({
|
||||
id={anchorId || undefined}
|
||||
style={{
|
||||
padding: '60px 20px',
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: tokenRef('bg'),
|
||||
outline: selected ? '2px solid #3b82f6' : 'none',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: '800px', margin: '0 auto' }}>
|
||||
{/* Tab buttons */}
|
||||
<div style={{ display: 'flex', gap: '2px', borderBottom: '2px solid #e2e8f0' }}>
|
||||
<div style={{ display: 'flex', gap: '2px', borderBottom: `2px solid ${tokenRef('border')}` }}>
|
||||
{tabs.map((tab, i) => (
|
||||
<button
|
||||
key={i}
|
||||
@@ -90,13 +90,13 @@ export const Tabs: UserComponent<TabsProps> = ({
|
||||
style={{
|
||||
padding: '24px',
|
||||
backgroundColor: contentBg,
|
||||
border: '1px solid #e2e8f0',
|
||||
border: `1px solid ${tokenRef('border')}`,
|
||||
borderTop: 'none',
|
||||
borderBottomLeftRadius: '8px',
|
||||
borderBottomRightRadius: '8px',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.7',
|
||||
color: '#4b5563',
|
||||
color: tokenRef('muted'),
|
||||
minHeight: '100px',
|
||||
}}
|
||||
>
|
||||
@@ -113,12 +113,12 @@ Tabs.craft = {
|
||||
displayName: 'Tabs',
|
||||
props: {
|
||||
tabs: defaultTabs,
|
||||
style: { backgroundColor: '#ffffff' },
|
||||
style: { backgroundColor: tokenRef('bg') },
|
||||
activeTabBg: tokenRef('primary'),
|
||||
activeTabColor: '#ffffff',
|
||||
inactiveTabBg: '#f1f5f9',
|
||||
inactiveTabColor: '#64748b',
|
||||
contentBg: '#ffffff',
|
||||
activeTabColor: tokenRef('bg'),
|
||||
inactiveTabBg: tokenRef('bg'),
|
||||
inactiveTabColor: tokenRef('muted'),
|
||||
contentBg: tokenRef('bg'),
|
||||
anchorId: '',
|
||||
animation: '',
|
||||
animationDelay: '',
|
||||
@@ -147,10 +147,10 @@ Tabs.craft = {
|
||||
// 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) || tokenRef('primary');
|
||||
const activeTabColor = cssValue(props.activeTabColor) || '#ffffff';
|
||||
const inactiveTabBg = cssValue(props.inactiveTabBg) || '#f1f5f9';
|
||||
const inactiveTabColor = cssValue(props.inactiveTabColor) || '#64748b';
|
||||
const contentBg = cssValue(props.contentBg) || '#ffffff';
|
||||
const activeTabColor = cssValue(props.activeTabColor) || tokenRef('bg');
|
||||
const inactiveTabBg = cssValue(props.inactiveTabBg) || tokenRef('bg');
|
||||
const inactiveTabColor = cssValue(props.inactiveTabColor) || tokenRef('muted');
|
||||
const contentBg = cssValue(props.contentBg) || tokenRef('bg');
|
||||
|
||||
// tabId scopes the functional wiring (onclick/getElementById) as well as
|
||||
// the ARIA tab<->panel linking ids. It must be BOTH deterministic (so
|
||||
@@ -171,7 +171,7 @@ Tabs.craft = {
|
||||
}).join('\n ');
|
||||
|
||||
const tabPanels = tabs.map((tab, i) => {
|
||||
return `<div id="${tabId}_panel_${i}" role="tabpanel" aria-labelledby="${tabId}_btn_${i}" tabindex="0" style="padding:24px;background-color:${contentBg};border:1px solid #e2e8f0;border-top:none;border-bottom-left-radius:8px;border-bottom-right-radius:8px;font-size:14px;line-height:1.7;color:#4b5563;min-height:100px;${i !== 0 ? 'display:none' : ''}">${escapeHtml(tab.content)}</div>`;
|
||||
return `<div id="${tabId}_panel_${i}" role="tabpanel" aria-labelledby="${tabId}_btn_${i}" tabindex="0" style="padding:24px;background-color:${contentBg};border:1px solid ${tokenRef('border')};border-top:none;border-bottom-left-radius:8px;border-bottom-right-radius:8px;font-size:14px;line-height:1.7;color:${tokenRef('muted')};min-height:100px;${i !== 0 ? 'display:none' : ''}">${escapeHtml(tab.content)}</div>`;
|
||||
}).join('\n ');
|
||||
|
||||
const switchScript = `<script>
|
||||
@@ -211,7 +211,7 @@ function ${tabId}_switch(idx){
|
||||
return {
|
||||
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
||||
<div style="max-width:800px;margin:0 auto">
|
||||
<div role="tablist" style="display:flex;gap:2px;border-bottom:2px solid #e2e8f0">
|
||||
<div role="tablist" style="display:flex;gap:2px;border-bottom:2px solid ${tokenRef('border')}">
|
||||
${tabButtons}
|
||||
</div>
|
||||
${tabPanels}
|
||||
|
||||
@@ -82,3 +82,12 @@ describe('Testimonials.craft.props includes the box-model/animation/visibility r
|
||||
expect(props).toHaveProperty('hideOnMobile', false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Testimonials.toHtml design tokens (#44)', () => {
|
||||
test('default export references var(--wsb-*) and no longer carries the old hard-coded hex', () => {
|
||||
const { html } = toHtml({ testimonials: [{ quote: "Q", name: "N", title: "T", rating: 5 }], layout: "single" }, '');
|
||||
const lower = html.toLowerCase();
|
||||
for (const t of ["bg", "border", "text", "muted"]) expect(html, `token ${t}`).toContain(`var(--wsb-${t})`);
|
||||
for (const hex of ["#f8fafc", "#e2e8f0", "#374151", "#18181b", "#64748b"]) 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, cssValue } from '../../utils/escape';
|
||||
@@ -63,8 +64,8 @@ export const Testimonials: UserComponent<TestimonialsProps> = ({
|
||||
layout = 'grid',
|
||||
columns = 3,
|
||||
style = {},
|
||||
cardBg = '#f8fafc',
|
||||
starColor = '#f59e0b',
|
||||
cardBg = tokenRef('bg'),
|
||||
starColor = tokenRef('warning'),
|
||||
anchorId,
|
||||
}) => {
|
||||
const {
|
||||
@@ -79,17 +80,17 @@ export const Testimonials: UserComponent<TestimonialsProps> = ({
|
||||
borderRadius: '12px',
|
||||
padding: '32px 24px',
|
||||
textAlign: 'center',
|
||||
border: '1px solid #e2e8f0',
|
||||
border: `1px solid ${tokenRef('border')}`,
|
||||
};
|
||||
|
||||
const renderCard = (t: Testimonial, i: number) => (
|
||||
<div key={i} style={cardStyle}>
|
||||
{renderStars(t.rating, starColor)}
|
||||
<p style={{ fontSize: '15px', color: '#374151', lineHeight: '1.7', marginBottom: '16px', fontStyle: 'italic', fontFamily: 'Inter, sans-serif' }}>
|
||||
<p style={{ fontSize: '15px', color: tokenRef('text'), lineHeight: '1.7', marginBottom: '16px', fontStyle: 'italic', fontFamily: 'Inter, sans-serif' }}>
|
||||
“{t.quote}”
|
||||
</p>
|
||||
<div style={{ fontWeight: '600', fontSize: '14px', color: '#18181b', fontFamily: 'Inter, sans-serif' }}>{t.name}</div>
|
||||
<div style={{ fontSize: '13px', color: '#64748b', fontFamily: 'Inter, sans-serif' }}>{t.title}</div>
|
||||
<div style={{ fontWeight: '600', fontSize: '14px', color: tokenRef('text'), fontFamily: 'Inter, sans-serif' }}>{t.name}</div>
|
||||
<div style={{ fontSize: '13px', color: tokenRef('muted'), fontFamily: 'Inter, sans-serif' }}>{t.title}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -101,7 +102,7 @@ export const Testimonials: UserComponent<TestimonialsProps> = ({
|
||||
id={anchorId || undefined}
|
||||
style={{
|
||||
padding: '80px 20px',
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: tokenRef('bg'),
|
||||
outline: selected ? '2px solid #3b82f6' : 'none',
|
||||
...style,
|
||||
}}
|
||||
@@ -132,9 +133,9 @@ Testimonials.craft = {
|
||||
testimonials: defaultTestimonials,
|
||||
layout: 'grid',
|
||||
columns: 3,
|
||||
style: { backgroundColor: '#ffffff' },
|
||||
cardBg: '#f8fafc',
|
||||
starColor: '#f59e0b',
|
||||
style: { backgroundColor: tokenRef('bg') },
|
||||
cardBg: tokenRef('bg'),
|
||||
starColor: tokenRef('warning'),
|
||||
anchorId: '',
|
||||
animation: '',
|
||||
animationDelay: '',
|
||||
@@ -163,25 +164,25 @@ Testimonials.craft = {
|
||||
const columns = Number(props.columns) || 3;
|
||||
// Sanitized -- raw string-interpolation sinks below (cardCss / starsHtml
|
||||
// style attributes).
|
||||
const cardBg = cssValue(props.cardBg) || '#f8fafc';
|
||||
const starColor = cssValue(props.starColor) || '#f59e0b';
|
||||
const cardBg = cssValue(props.cardBg) || tokenRef('bg');
|
||||
const starColor = cssValue(props.starColor) || tokenRef('warning');
|
||||
|
||||
const items = testimonials.length > 0 ? testimonials : defaultTestimonials;
|
||||
|
||||
const sectionStyle = cssPropsToString({
|
||||
padding: '80px 20px',
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundColor: tokenRef('bg'),
|
||||
...style,
|
||||
});
|
||||
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||
|
||||
const cardCss = `background-color:${cardBg};border-radius:12px;padding:32px 24px;text-align:center;border:1px solid #e2e8f0`;
|
||||
const cardCss = `background-color:${cardBg};border-radius:12px;padding:32px 24px;text-align:center;border:1px solid ${tokenRef('border')}`;
|
||||
|
||||
const cardHtml = (t: Testimonial): string => `<div style="${cardCss}">
|
||||
${starsHtml(t.rating, starColor)}
|
||||
<p style="font-size:15px;color:#374151;line-height:1.7;margin-bottom:16px;font-style:italic;font-family:Inter,sans-serif">“${escapeHtml(t.quote)}”</p>
|
||||
<div style="font-weight:600;font-size:14px;color:#18181b;font-family:Inter,sans-serif">${escapeHtml(t.name)}</div>
|
||||
<div style="font-size:13px;color:#64748b;font-family:Inter,sans-serif">${escapeHtml(t.title)}</div>
|
||||
<p style="font-size:15px;color:${tokenRef('text')};line-height:1.7;margin-bottom:16px;font-style:italic;font-family:Inter,sans-serif">“${escapeHtml(t.quote)}”</p>
|
||||
<div style="font-weight:600;font-size:14px;color:${tokenRef('text')};font-family:Inter,sans-serif">${escapeHtml(t.name)}</div>
|
||||
<div style="font-size:13px;color:${tokenRef('muted')};font-family:Inter,sans-serif">${escapeHtml(t.title)}</div>
|
||||
</div>`;
|
||||
|
||||
if (layout === 'single') {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CSSProperties } from 'react';
|
||||
import { escapeHtml, escapeAttr, safeUrl, cssValue } from '../../utils/escape';
|
||||
import { tokenRef } from '../../utils/tokens';
|
||||
|
||||
export type CtaVariant = 'primary' | 'outline' | 'ghost';
|
||||
|
||||
@@ -72,9 +73,9 @@ export function ctaCssString(cta: CtaButton, defaults: CtaStyleDefaults): string
|
||||
// Callers pass user-controlled design-token colors (e.g. HeroSimple's
|
||||
// buttonBgColor/buttonTextColor/textColor) through CtaStyleDefaults, so
|
||||
// sanitize once here rather than at every call site.
|
||||
const outlineText = cssValue(defaults.outlineText) || '#000000';
|
||||
const primaryBg = cssValue(defaults.primaryBg) || '#000000';
|
||||
const primaryText = cssValue(defaults.primaryText) || '#ffffff';
|
||||
const outlineText = cssValue(defaults.outlineText) || tokenRef('text');
|
||||
const primaryBg = cssValue(defaults.primaryBg) || tokenRef('primary');
|
||||
const primaryText = cssValue(defaults.primaryText) || tokenRef('bg');
|
||||
switch (variant) {
|
||||
case 'outline':
|
||||
return `display:inline-block;padding:14px 36px;background-color:transparent;color:${outlineText};text-decoration:none;border-radius:8px;font-weight:600;font-size:16px;border:2px solid ${outlineText}`;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from 'react';
|
||||
import { describe, test, expect, afterEach } from 'vitest';
|
||||
import { renderEditorHarness, EditorHarness } from '../../test-utils/editorHarness';
|
||||
import { SiteDesignProvider, useSiteDesign, SiteDesignContextValue, DEFAULT_SITE_DESIGN } from '../../state/SiteDesignContext';
|
||||
import { CanvasTokenStyle } from '../../editor/CanvasTokenStyle';
|
||||
import { buildNodeTree } from '../../utils/craft-tree';
|
||||
import { templateComponentToTreeNode } from '../../templates/apply-template';
|
||||
import { exportBodyHtml, exportToHtml } from '../../utils/html-export';
|
||||
|
||||
/**
|
||||
* #44 component-tokens, real-editor harness: a page of FeaturesGrid + PricingTable +
|
||||
* Testimonials (default props). Their colours must be token references, the canvas
|
||||
* token <style> must follow a Site Design change, and the export must reference tokens
|
||||
* (and define the changed primary once, in :root) rather than carry the old hex.
|
||||
*/
|
||||
let h: EditorHarness | null = null;
|
||||
afterEach(() => {
|
||||
h?.unmount();
|
||||
h = null;
|
||||
});
|
||||
|
||||
const DesignProbe: React.FC<{ out: { current: SiteDesignContextValue | null } }> = ({ out }) => {
|
||||
out.current = useSiteDesign();
|
||||
return null;
|
||||
};
|
||||
|
||||
const OLD_HEX = ['#f8fafc', '#e2e8f0', '#18181b', '#64748b', '#4b5563', '#374151', '#bbf7d0', '#facc15', '#f59e0b'];
|
||||
|
||||
describe('component colours follow Site Design (real editor harness)', () => {
|
||||
test('FeaturesGrid + PricingTable + Testimonials: canvas vars follow primary; export references tokens', () => {
|
||||
h = renderEditorHarness();
|
||||
const ctx: { current: SiteDesignContextValue | null } = { current: null };
|
||||
h.mountChild(
|
||||
<SiteDesignProvider>
|
||||
<CanvasTokenStyle />
|
||||
<DesignProbe out={ctx} />
|
||||
</SiteDesignProvider>,
|
||||
);
|
||||
h.act(() => {
|
||||
for (const type of ['FeaturesGrid', 'PricingTable', 'Testimonials']) {
|
||||
h!.actions.addNodeTree(
|
||||
buildNodeTree(h!.query, templateComponentToTreeNode({ type, props: {} } as any)),
|
||||
'ROOT',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Canvas: the editor-rendered DOM carries var() refs (jsdom keeps them verbatim).
|
||||
const clone = h.container.cloneNode(true) as HTMLElement;
|
||||
clone.querySelectorAll('style').forEach((el) => el.remove()); // token definitions legitimately hold hex
|
||||
const dom = clone.innerHTML;
|
||||
expect(dom).toContain('var(--wsb-border)');
|
||||
expect(dom).toContain('var(--wsb-muted)');
|
||||
expect(dom).toContain('var(--wsb-primary)');
|
||||
for (const hex of OLD_HEX) expect(dom.toLowerCase(), `canvas ${hex}`).not.toContain(hex);
|
||||
|
||||
// Canvas custom properties follow Site Design.
|
||||
const canvasVars = () => document.querySelector('style[data-wsb-canvas-tokens]')!.textContent ?? '';
|
||||
expect(canvasVars()).toContain('--wsb-primary:#3b82f6');
|
||||
h.act(() => ctx.current!.updateDesign({ primaryColor: '#c0ffee', mutedTextColor: '#123456' }));
|
||||
expect(canvasVars()).toContain('--wsb-primary:#c0ffee');
|
||||
expect(canvasVars()).toContain('--wsb-muted:#123456');
|
||||
|
||||
// Export: tokens, no old design hex in the body.
|
||||
const ser = h.getSerialized();
|
||||
const { html } = exportBodyHtml(ser);
|
||||
expect(html).toContain('var(--wsb-primary)');
|
||||
expect(html).toContain('var(--wsb-warning)');
|
||||
for (const hex of OLD_HEX) expect(html.toLowerCase(), `export ${hex}`).not.toContain(hex);
|
||||
|
||||
// Restyling Site Design changes only :root, never the body markup.
|
||||
const a = exportToHtml(ser, { design: { ...DEFAULT_SITE_DESIGN } }).html;
|
||||
const b = exportToHtml(ser, { design: { ...DEFAULT_SITE_DESIGN, primaryColor: '#c0ffee', mutedTextColor: '#123456' } }).html;
|
||||
expect(b).toContain('--wsb-muted:#123456');
|
||||
expect(b.slice(b.indexOf('<body'))).toBe(a.slice(a.indexOf('<body')));
|
||||
});
|
||||
});
|
||||
@@ -14,10 +14,6 @@ const FAMILIES: Array<[string, SectionDef[]]> = [
|
||||
['about', ABOUT_SECTIONS],
|
||||
['cta', CTA_SECTIONS],
|
||||
];
|
||||
/** FeaturesGrid hardcodes card colours in its own render/toHtml (no colour props), so the
|
||||
* skeleton's `services-grid` cannot be token-clean; `services-icon-cards` is the token-clean
|
||||
* equivalent. Only the exported-HTML hex check is skipped (the tree itself is still checked). */
|
||||
const HEX_EXEMPT_EXPORT = new Set(['services-grid']);
|
||||
const ALL = FAMILIES.flatMap(([, defs]) => defs);
|
||||
|
||||
function walk(node: TemplateComponent, fn: (n: TemplateComponent) => void) {
|
||||
@@ -78,7 +74,7 @@ describe('A-content section families', () => {
|
||||
const out = exportBodyHtml(h.getSerialized()).html;
|
||||
expect(out).not.toContain('Export failed');
|
||||
expect(out.length).toBeGreaterThan(50);
|
||||
if (!HEX_EXEMPT_EXPORT.has(def.id)) expect(strayHex(out), 'stray hex').toEqual([]);
|
||||
expect(strayHex(out), 'stray hex').toEqual([]);
|
||||
for (const img of out.match(/<img\b[^>]*>/g) || []) expect(img, img).toMatch(/\balt="[^"]+"/);
|
||||
expect(out).toContain('var(--wsb-');
|
||||
for (const href of [...out.matchAll(/\shref="([^"]*)"/g)].map((m) => m[1])) {
|
||||
|
||||
@@ -18,12 +18,7 @@ const TOP_LEVEL = new Set(['Section', 'HeroSimple', 'CTASection', 'CallToAction'
|
||||
export function stripAllowedHex(html: string): string {
|
||||
return html
|
||||
.replace(/linear-gradient\([^)]*\)/g, '')
|
||||
.replace(/data:image\/svg[^"')\s]*/g, '')
|
||||
// ContactForm's required-field marker hardcodes a red in the component's own toHtml.
|
||||
.replace(/<span style="color:#ef4444;margin-left:2px">\*<\/span>/g, '')
|
||||
// ...and its submit button text (#ffffff, no prop) and Gallery's <img> placeholder background (#f1f5f9, no prop).
|
||||
.replace(/(<button type="submit"[^>]*?)color:#ffffff;/g, '$1')
|
||||
.replace(/(<img [^>]*?);background-color:#f1f5f9/g, '$1');
|
||||
.replace(/data:image\/svg[^"')\s]*/g, '');
|
||||
}
|
||||
|
||||
function walk(t: TemplateComponent, fn: (n: TemplateComponent) => void) {
|
||||
|
||||
@@ -21,15 +21,14 @@ const FONT_KEYS = ['headingFont', 'bodyFont'] as const;
|
||||
|
||||
/**
|
||||
* Design hexes that still appear in the exported first-page body because a
|
||||
* COMPONENT hard-codes them (not the template): CTASection's heading/button
|
||||
* (#ffffff, #18181b -- it has no colour props), FeaturesGrid's card
|
||||
* background (#ffffff) and title (#18181b). Pinned as exact counts so a
|
||||
* template that re-introduces a literal still fails; removing them needs a
|
||||
* component change (out of scope for the template pass, tracked in the report).
|
||||
* COMPONENT hard-codes them (not the template). After component-tokens (#44) the only
|
||||
* survivors are the deliberate fixed whites on gradient/image backgrounds (CTASection
|
||||
* heading, CallToAction text). Pinned as exact counts so a template that re-introduces a
|
||||
* literal still fails.
|
||||
*/
|
||||
const COMPONENT_DEFAULT_HEX: Record<string, Record<string, number>> = {
|
||||
medical: { '#ffffff': 5 },
|
||||
portfolio: { '#18181b': 4, '#ffffff': 2 },
|
||||
medical: { '#ffffff': 1 },
|
||||
portfolio: { '#ffffff': 1 },
|
||||
photography: {},
|
||||
'content-creator': {},
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user