feat(site-builder): tokenise remaining component design colours (Navbar, Menu, Footer, SocialLinks, StickyCallBar, BackgroundSection, ContentSlider, Countdown, HeroSimple)
Refs #44 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2877bc06fa
commit
e347a4334d
19 files changed
+190
-68
No files matched your search
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -27,7 +27,7 @@ const FONT_KEYS = ['headingFont', 'bodyFont'] as const;
|
||||
* literal still fails.
|
||||
*/
|
||||
const COMPONENT_DEFAULT_HEX: Record<string, Record<string, number>> = {
|
||||
medical: { '#ffffff': 4 },
|
||||
medical: { '#ffffff': 1 },
|
||||
portfolio: { '#ffffff': 1 },
|
||||
photography: {},
|
||||
'content-creator': {},
|
||||
|
||||
Reference in new issue
Block a user