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
);
@@ -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(/<\/i>/);
- expect(html).toMatch(/<\/i>/);
+ expect(html).toMatch(/<\/i>/);
+ expect(html).toMatch(/<\/i>/);
});
});
diff --git a/craft/src/components/sections/ContentSlider.tsx b/craft/src/components/sections/ContentSlider.tsx
index b739eb0..a1a7e7a 100644
--- a/craft/src/components/sections/ContentSlider.tsx
+++ b/craft/src/components/sections/ContentSlider.tsx
@@ -194,10 +194,10 @@ export const ContentSlider: UserComponent = ({
{showArrows && items.length > 1 && (
<>
>
)}
@@ -329,8 +329,8 @@ ContentSlider.craft = {
}).join('\n ');
const arrowsHtml = showArrows && items.length > 1
- ? `
- `
+ ? `
+ `
: '';
const dotsHtml = showDots && items.length > 1
diff --git a/craft/src/components/sections/Testimonials.toHtml.test.ts b/craft/src/components/sections/Testimonials.toHtml.test.ts
index 1466d30..f966622 100644
--- a/craft/src/components/sections/Testimonials.toHtml.test.ts
+++ b/craft/src/components/sections/Testimonials.toHtml.test.ts
@@ -41,7 +41,7 @@ describe('Testimonials.toHtml single-layout export parity', () => {
describe('Testimonials.toHtml decorative star icons (F2.5)', () => {
test('star glyphs are aria-hidden', () => {
const { html } = toHtml({ testimonials, layout: 'grid' }, '');
- const stars = html.match(/]*>/g) || [];
+ const stars = html.match(/]*>/g) || [];
expect(stars.length).toBeGreaterThan(0);
stars.forEach((tag: string) => expect(tag).toContain('aria-hidden="true"'));
});
diff --git a/craft/src/components/sections/Testimonials.tsx b/craft/src/components/sections/Testimonials.tsx
index 5ef7120..af6ef43 100644
--- a/craft/src/components/sections/Testimonials.tsx
+++ b/craft/src/components/sections/Testimonials.tsx
@@ -52,7 +52,7 @@ function renderStars(count: number, color: string): React.ReactNode {
{[1, 2, 3, 4, 5].map((i) => (
))}
@@ -68,7 +68,7 @@ function starsHtml(count: number, color: string): string {
// non-numeric/garbage value).
const safeCount = Number(count) || 0;
const stars = [1, 2, 3, 4, 5].map((i) =>
- ``
+ ``
).join('');
return `${stars}
`;
}
diff --git a/craft/src/panels/right/styles/SocialStylePanel.tsx b/craft/src/panels/right/styles/SocialStylePanel.tsx
index 3714d11..2cdd5a8 100644
--- a/craft/src/panels/right/styles/SocialStylePanel.tsx
+++ b/craft/src/panels/right/styles/SocialStylePanel.tsx
@@ -108,7 +108,7 @@ export const SocialStylePanel: React.FC = ({ selectedId, nodePr
style={{ width: '100%', padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer' }}
>
- {['facebook', 'twitter', 'instagram', 'linkedin', 'youtube', 'github', 'tiktok', 'pinterest', 'snapchat', 'whatsapp', 'spotify', 'twitch'].map((p) => (
+ {['facebook', 'twitter', 'instagram', 'linkedin', 'youtube', 'github', 'tiktok', 'pinterest', 'snapchat', 'whatsapp', 'spotify', 'twitch', 'bluesky', 'threads', 'discord', 'mastodon', 'reddit', 'telegram'].map((p) => (
))}
diff --git a/craft/src/utils/fa-icon.test.ts b/craft/src/utils/fa-icon.test.ts
new file mode 100644
index 0000000..e533c9c
--- /dev/null
+++ b/craft/src/utils/fa-icon.test.ts
@@ -0,0 +1,39 @@
+import { describe, test, expect } from 'vitest';
+import { faClass } from './fa-icon';
+import { SocialLinks } from '../components/basic/SocialLinks';
+
+describe('faClass (FA4 name -> FA7 class string)', () => {
+ test('solid default, brand and regular mappings', () => {
+ expect(faClass('fa-star')).toBe('fa-solid fa-star');
+ expect(faClass('fa-facebook')).toBe('fa-brands fa-facebook');
+ expect(faClass('fa-star-o')).toBe('fa-regular fa-star');
+ expect(faClass('fa-picture-o')).toBe('fa-regular fa-image');
+ expect(faClass('fa-star-half-o')).toBe('fa-regular fa-star-half-stroke');
+ });
+ test('legacy "fa fa-x" and already-styled values are normalised, extra tokens kept', () => {
+ expect(faClass('fa fa-github')).toBe('fa-brands fa-github');
+ expect(faClass('fa-brands fa-x-twitter')).toBe('fa-brands fa-x-twitter');
+ expect(faClass('fa-star fa-spin')).toBe('fa-solid fa-star fa-spin');
+ });
+ test('empty falls back to the star', () => {
+ expect(faClass('')).toBe('fa-solid fa-star');
+ expect(faClass(undefined)).toBe('fa-solid fa-star');
+ });
+});
+
+describe('SocialLinks exports per-network FA7 brand icons', () => {
+ const toHtml = (SocialLinks as any).toHtml;
+ const cases: Array<[string, string]> = [
+ ['twitter', 'fa-x-twitter'], ['tiktok', 'fa-tiktok'], ['bluesky', 'fa-bluesky'], ['threads', 'fa-threads'],
+ ['discord', 'fa-discord'], ['mastodon', 'fa-mastodon'], ['facebook', 'fa-facebook'], ['spotify', 'fa-spotify'],
+ ['twitch', 'fa-twitch'], ['youtube', 'fa-youtube'], ['reddit', 'fa-reddit'], ['telegram', 'fa-telegram'],
+ ];
+ test.each(cases)('%s -> fa-brands %s', (platform, icon) => {
+ const { html } = toHtml({ links: [{ platform, url: '#' }] }, '');
+ expect(html).toContain(` {
+ expect(toHtml({ links: [{ platform: 'nope', url: '#' }] }, '').html).toContain('` minus `-o`. */
+const REGULAR_RENAMES: Record = {
+ 'picture-o': 'image',
+ 'star-half-o': 'star-half-stroke',
+ 'trash-o': 'trash-can',
+ 'file-o': 'file',
+ 'folder-o': 'folder',
+ 'envelope-o': 'envelope',
+};
+
+const STYLE_TOKENS = new Set([
+ 'fa-solid', 'fa-regular', 'fa-brands', 'fa-light', 'fa-thin', 'fa-duotone', 'fa-classic', 'fas', 'far', 'fab',
+]);
+
+/**
+ * Class string for an icon value: a bare/legacy name (`fa-star`, `fa-star-o`,
+ * `fa-facebook`, `fa fa-star`) or an already-styled one (`fa-brands fa-x-twitter`).
+ * Returns e.g. `fa-solid fa-star`. Extra tokens (fa-spin, fa-2x) pass through.
+ */
+export function faClass(icon: string | undefined, fallback = 'fa-star'): string {
+ const tokens = String(icon ?? '').trim().split(/\s+/).filter((t) => t && t !== 'fa');
+ if (tokens.length === 0) tokens.push(fallback);
+ if (tokens.some((t) => STYLE_TOKENS.has(t))) return tokens.join(' ');
+ const idx = tokens.findIndex((t) => /^fa-/.test(t));
+ if (idx < 0) return ['fa-solid', ...tokens].join(' ');
+ const name = tokens[idx].slice(3);
+ const rest = tokens.filter((_, i) => i !== idx);
+ let style = 'fa-solid';
+ let out = name;
+ if (BRANDS.has(name)) {
+ style = 'fa-brands';
+ } else if (name in REGULAR_RENAMES) {
+ style = 'fa-regular';
+ out = REGULAR_RENAMES[name];
+ } else if (/-o$/.test(name)) {
+ style = 'fa-regular';
+ out = name.slice(0, -2);
+ }
+ return [style, `fa-${out}`, ...rest].join(' ');
+}