From 7b3baa815c07ec705fe70514afbaa7353b6f8c2e Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sat, 31 Oct 2026 11:59:46 -0700 Subject: [PATCH] feat(site-builder): Font Awesome 7.3.1 icons (per-network brands) in editor and exports Exporters emit fa-solid/fa-brands/fa-regular classes through utils/fa-icon.ts; SocialLinks gets X/TikTok/Bluesky/Threads/Discord/Mastodon/Reddit/Telegram; the editor loads all.min.css + v4-shims (SRI) so editor chrome FA4 names work. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_0185ukXgKV8v9BUrQWXSUW4P --- craft/index.html | 4 +- .../src/components/basic/Icon.toHtml.test.ts | 2 +- craft/src/components/basic/Icon.tsx | 5 +- .../components/basic/SearchBar.toHtml.test.ts | 2 +- craft/src/components/basic/SearchBar.tsx | 8 +-- .../basic/SocialLinks.toHtml.test.ts | 2 +- craft/src/components/basic/SocialLinks.tsx | 44 +++++++++------ .../basic/StarRating.toHtml.test.ts | 6 +- craft/src/components/basic/StarRating.tsx | 14 ++--- craft/src/components/media/VideoBlock.tsx | 4 +- .../sections/ContentSlider.toHtml.test.ts | 4 +- .../src/components/sections/ContentSlider.tsx | 8 +-- .../sections/Testimonials.toHtml.test.ts | 2 +- .../src/components/sections/Testimonials.tsx | 4 +- .../panels/right/styles/SocialStylePanel.tsx | 2 +- craft/src/utils/fa-icon.test.ts | 39 +++++++++++++ craft/src/utils/fa-icon.ts | 55 +++++++++++++++++++ 17 files changed, 157 insertions(+), 48 deletions(-) create mode 100644 craft/src/utils/fa-icon.test.ts create mode 100644 craft/src/utils/fa-icon.ts diff --git a/craft/index.html b/craft/index.html index d57a881..d6becef 100644 --- a/craft/index.html +++ b/craft/index.html @@ -7,7 +7,9 @@ - + + +
diff --git a/craft/src/components/basic/Icon.toHtml.test.ts b/craft/src/components/basic/Icon.toHtml.test.ts index 017d801..fc23c07 100644 --- a/craft/src/components/basic/Icon.toHtml.test.ts +++ b/craft/src/components/basic/Icon.toHtml.test.ts @@ -6,7 +6,7 @@ const toHtml = (Icon as any).toHtml; describe('Icon.toHtml normal rendering', () => { test('renders icon class, size/color style, and link href', () => { const { html } = toHtml({ icon: 'fa-star', size: '32px', color: '#3b82f6', link: 'https://example.com' }, ''); - expect(html).toContain('class="fa fa-star"'); + expect(html).toContain('class="fa-solid fa-star"'); expect(html).toContain('font-size:32px'); expect(html).toContain('color:#3b82f6'); expect(html).toContain('href="https://example.com"'); diff --git a/craft/src/components/basic/Icon.tsx b/craft/src/components/basic/Icon.tsx index ec0d1aa..dfeed5b 100644 --- a/craft/src/components/basic/Icon.tsx +++ b/craft/src/components/basic/Icon.tsx @@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { escapeAttr, safeUrl } from '../../utils/escape'; import { tokenRef } from '../../utils/tokens'; +import { faClass } from '../../utils/fa-icon'; interface IconProps { icon?: string; @@ -46,7 +47,7 @@ export const Icon: UserComponent = ({ const iconEl = ( ); @@ -130,7 +131,7 @@ Icon.craft = { } = props; const iconStyle = cssPropsToString({ fontSize: size, color, lineHeight: '1' }); - let iconHtml = ``; + let iconHtml = ``; const hasBg = bgShape !== 'none' && bgColor !== 'transparent'; if (hasBg) { diff --git a/craft/src/components/basic/SearchBar.toHtml.test.ts b/craft/src/components/basic/SearchBar.toHtml.test.ts index 9632081..103700b 100644 --- a/craft/src/components/basic/SearchBar.toHtml.test.ts +++ b/craft/src/components/basic/SearchBar.toHtml.test.ts @@ -6,7 +6,7 @@ const toHtml = (SearchBar as any).toHtml; describe('SearchBar.toHtml decorative icons (F2.5)', () => { test('the input-adjacent search icon is aria-hidden', () => { const { html } = toHtml({}, ''); - const icons = html.match(/]*>/g) || []; + const icons = html.match(/]*>/g) || []; expect(icons.length).toBeGreaterThan(0); icons.forEach((tag: string) => expect(tag).toContain('aria-hidden="true"')); }); diff --git a/craft/src/components/basic/SearchBar.tsx b/craft/src/components/basic/SearchBar.tsx index 4e2135d..6529b23 100644 --- a/craft/src/components/basic/SearchBar.tsx +++ b/craft/src/components/basic/SearchBar.tsx @@ -51,7 +51,7 @@ export const SearchBar: UserComponent = ({ >
= ({ gap: '6px', }} > - + {buttonText} )} @@ -151,7 +151,7 @@ SearchBar.craft = { 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 - ? `` + ? `` : ''; // F2: previously a purely decorative
-- no action/method/input @@ -165,7 +165,7 @@ SearchBar.craft = { return { html: `
- +
${btnHtml} diff --git a/craft/src/components/basic/SocialLinks.toHtml.test.ts b/craft/src/components/basic/SocialLinks.toHtml.test.ts index c5c42c7..640c737 100644 --- a/craft/src/components/basic/SocialLinks.toHtml.test.ts +++ b/craft/src/components/basic/SocialLinks.toHtml.test.ts @@ -11,7 +11,7 @@ describe('SocialLinks.toHtml accessibility (F2.5)', () => { test('the icon glyph itself is aria-hidden', () => { const { html } = toHtml({ links: [{ platform: 'twitter', url: '#' }] }, ''); - expect(html).toMatch(/]*aria-hidden="true"/); + expect(html).toMatch(/]*aria-hidden="true"/); }); }); diff --git a/craft/src/components/basic/SocialLinks.tsx b/craft/src/components/basic/SocialLinks.tsx index 7051348..266342d 100644 --- a/craft/src/components/basic/SocialLinks.tsx +++ b/craft/src/components/basic/SocialLinks.tsx @@ -26,18 +26,24 @@ interface SocialLinksProps { } const platformIcons: Record = { - facebook: 'fa-facebook', - twitter: 'fa-twitter', - instagram: 'fa-instagram', - linkedin: 'fa-linkedin', - youtube: 'fa-youtube', - github: 'fa-github', - tiktok: 'fa-music', - pinterest: 'fa-pinterest', - snapchat: 'fa-snapchat', - whatsapp: 'fa-whatsapp', - spotify: 'fa-spotify', - twitch: 'fa-twitch', + facebook: 'fa-brands fa-facebook', + twitter: 'fa-brands fa-x-twitter', + instagram: 'fa-brands fa-instagram', + linkedin: 'fa-brands fa-linkedin', + youtube: 'fa-brands fa-youtube', + github: 'fa-brands fa-github', + tiktok: 'fa-brands fa-tiktok', + pinterest: 'fa-brands fa-pinterest', + snapchat: 'fa-brands fa-snapchat', + whatsapp: 'fa-brands fa-whatsapp', + spotify: 'fa-brands fa-spotify', + bluesky: 'fa-brands fa-bluesky', + threads: 'fa-brands fa-threads', + discord: 'fa-brands fa-discord', + mastodon: 'fa-brands fa-mastodon', + reddit: 'fa-brands fa-reddit', + telegram: 'fa-brands fa-telegram', + twitch: 'fa-brands fa-twitch', }; const platformLabels: Record = { @@ -53,6 +59,12 @@ const platformLabels: Record = { whatsapp: 'WhatsApp', spotify: 'Spotify', twitch: 'Twitch', + bluesky: 'Bluesky', + threads: 'Threads', + discord: 'Discord', + mastodon: 'Mastodon', + reddit: 'Reddit', + telegram: 'Telegram', }; const defaultLinks: SocialLink[] = [ @@ -116,7 +128,7 @@ export const SocialLinks: UserComponent = ({ }} > {links.map((link, i) => { - const iconClass = platformIcons[link.platform] || 'fa-link'; + const iconClass = platformIcons[link.platform] || 'fa-solid fa-link'; const shapeStyle = getShapeStyle(iconShape, iconSize); const hasBg = iconShape !== 'none'; @@ -136,7 +148,7 @@ export const SocialLinks: UserComponent = ({ ...shapeStyle, }} > - + ); })} @@ -210,7 +222,7 @@ SocialLinks.craft = { }; const linksHtml = links.map((link) => { - const iconClass = platformIcons[link.platform] || 'fa-link'; + const iconClass = platformIcons[link.platform] || 'fa-solid fa-link'; const title = platformLabels[link.platform] || link.platform; let aStyle = `text-decoration:none;color:${iconColor};background-color:${hasBg ? iconBgColor : 'transparent'}`; if (hasBg) { @@ -220,7 +232,7 @@ SocialLinks.craft = { // aria-hidden and the accessible name lives on the link (aria-label, // mirroring the existing `title` tooltip since title support in // screen readers is inconsistent). - return ``; + return ``; }).join('\n '); return { diff --git a/craft/src/components/basic/StarRating.toHtml.test.ts b/craft/src/components/basic/StarRating.toHtml.test.ts index d335b69..6964053 100644 --- a/craft/src/components/basic/StarRating.toHtml.test.ts +++ b/craft/src/components/basic/StarRating.toHtml.test.ts @@ -11,7 +11,7 @@ describe('StarRating.toHtml accessibility (F2.2)', () => { test('individual star glyphs are aria-hidden', () => { const { html } = toHtml({ rating: 3, maxStars: 5 }, ''); - const glyphs = html.match(/]*>/g) || []; + const glyphs = html.match(/]*>/g) || []; expect(glyphs.length).toBeGreaterThan(0); glyphs.forEach((tag: string) => expect(tag).toContain('aria-hidden="true"')); }); @@ -58,7 +58,7 @@ describe('StarRating.toHtml XSS hardening (rating/maxStars into aria-label, F2.2 test('a non-numeric maxStars does not blow up the star loop (no NaN glyph count, no huge output)', () => { const malicious = '5" onmouseover="alert(1)'; const { html } = toHtml({ rating: 3, maxStars: malicious as any }, ''); - const glyphs = html.match(/ { const { html } = toHtml({ rating: 3, maxStars: 1e9 as any }, ''); - const glyphs = html.match(/ = ({ stars.push( ); @@ -48,9 +48,9 @@ export const StarRating: UserComponent = ({ // Half star stars.push( - + - + ); @@ -59,7 +59,7 @@ export const StarRating: UserComponent = ({ stars.push( ); @@ -136,11 +136,11 @@ StarRating.craft = { let starsHtml = ''; for (let i = 1; i <= maxStars; i++) { if (i <= Math.floor(rating)) { - starsHtml += ``; + starsHtml += ``; } else if (i === Math.ceil(rating) && rating % 1 !== 0) { - starsHtml += ``; + starsHtml += ``; } else { - starsHtml += ``; + starsHtml += ``; } } diff --git a/craft/src/components/media/VideoBlock.tsx b/craft/src/components/media/VideoBlock.tsx index d165e2b..a1431ae 100644 --- a/craft/src/components/media/VideoBlock.tsx +++ b/craft/src/components/media/VideoBlock.tsx @@ -113,7 +113,7 @@ const VIDEO_PLACEHOLDER = ( textAlign: 'center' as const, }} > - + Add a video URL in settings
); @@ -217,7 +217,7 @@ export const VideoBlock: UserComponent = ({ zIndex: 0, }} > - + )} {/* Overlay */} diff --git a/craft/src/components/sections/ContentSlider.toHtml.test.ts b/craft/src/components/sections/ContentSlider.toHtml.test.ts index e2d662b..9eb7ca6 100644 --- a/craft/src/components/sections/ContentSlider.toHtml.test.ts +++ b/craft/src/components/sections/ContentSlider.toHtml.test.ts @@ -30,8 +30,8 @@ describe('ContentSlider.toHtml accessibility (F1.1)', () => { test('decorative chevron icons in arrows are aria-hidden', () => { const { html } = toHtml({ slides, showArrows: true }, ''); - expect(html).toMatch(/