feat: enhance branding features with social media integration and customizable icon settings

This commit is contained in:
2026-01-07 18:58:57 +02:00
parent 5b5646ed78
commit 7dbba818db
5 changed files with 429 additions and 98 deletions
+126 -29
View File
@@ -1,10 +1,11 @@
import React, { useRef, useState, useEffect, useCallback, useMemo, memo } from 'react';
import { Stage, Layer, Rect, Line, Text } from 'react-konva';
import { Stage, Layer, Rect, Line, Text, Group, Path } from 'react-konva';
import type Konva from 'konva';
import { useCanvasStore, createCodeElement, createTextElement, createArrowElement } from '../store/canvasStore';
import CodeBlock from './elements/CodeBlock';
import TextBlock from './elements/TextBlock';
import Arrow from './elements/Arrow';
import { SOCIAL_ICON_PATHS, SOCIAL_PLATFORMS_CONFIG } from './elements/SocialIcons';
import type { CodeElement, TextElement, ArrowElement } from '../types';
interface CanvasProps {
@@ -188,6 +189,10 @@ const Canvas: React.FC<CanvasProps> = ({ stageRef }) => {
const padding = branding.padding || 24;
const fontSize = branding.fontSize || 14;
const lineHeight = fontSize * 1.5;
const iconSize = branding.socialIconSize || 20;
const socialLayout = branding.socialLayout || 'horizontal';
const iconGap = 16;
const iconTextGap = 6; // Gap between icon and its text
// Build branding text lines
const lines: string[] = [];
@@ -197,23 +202,31 @@ const Canvas: React.FC<CanvasProps> = ({ stageRef }) => {
if (branding.showWebsite && branding.website) {
lines.push(branding.website);
}
if (branding.showSocial && branding.social) {
const socialParts: string[] = [];
if (branding.social.twitter) socialParts.push(branding.social.twitter);
if (branding.social.linkedin) socialParts.push(branding.social.linkedin);
if (branding.social.instagram) socialParts.push(branding.social.instagram);
if (branding.social.github) socialParts.push(branding.social.github);
if (branding.social.youtube) socialParts.push(branding.social.youtube);
if (branding.social.tiktok) socialParts.push(branding.social.tiktok);
if (socialParts.length > 0) {
lines.push(socialParts.join(' • '));
}
}
if (lines.length === 0) return null;
// Get active social platforms with their values
const activeSocialPlatforms = branding.showSocial && branding.social
? SOCIAL_PLATFORMS_CONFIG.filter(p => branding.social[p.key as keyof typeof branding.social])
.map(p => ({
...p,
value: branding.social[p.key as keyof typeof branding.social] || ''
}))
: [];
const hasTextContent = lines.length > 0;
const hasSocialIcons = activeSocialPlatforms.length > 0;
if (!hasTextContent && !hasSocialIcons) return null;
const text = lines.join('\n');
const textHeight = lines.length * lineHeight;
// Calculate social section dimensions (icon + text for each platform)
const socialItemHeight = Math.max(iconSize, fontSize);
const socialHeight = socialLayout === 'vertical'
? activeSocialPlatforms.length * socialItemHeight + (activeSocialPlatforms.length - 1) * (iconGap - 4)
: socialItemHeight;
// Calculate total content height
const contentHeight = textHeight + (hasTextContent && hasSocialIcons ? 16 : 0) + (hasSocialIcons ? socialHeight : 0);
// Calculate position based on setting
let x = padding;
@@ -233,29 +246,113 @@ const Canvas: React.FC<CanvasProps> = ({ stageRef }) => {
break;
case 'bottom-left':
x = padding;
y = height - padding - textHeight;
y = height - padding - contentHeight;
align = 'left';
break;
case 'bottom-right':
x = width - padding;
y = height - padding - textHeight;
y = height - padding - contentHeight;
align = 'right';
break;
}
// Calculate icon positions
const iconScale = iconSize / 24; // SVG viewBox is 24x24
const socialStartY = y + textHeight + (hasTextContent ? 16 : 0);
return (
<Text
x={align === 'right' ? 0 : x}
y={y}
width={align === 'right' ? x : width - padding}
text={text}
fontSize={fontSize}
fontFamily={branding.fontFamily || 'Inter'}
fill={branding.color || '#ffffff'}
opacity={branding.opacity || 0.8}
align={align}
lineHeight={1.5}
/>
<Group opacity={branding.opacity || 0.8}>
{/* Text content */}
{hasTextContent && (
<Text
x={align === 'right' ? 0 : x}
y={y}
width={align === 'right' ? x : width - padding}
text={lines.join('\n')}
fontSize={fontSize}
fontFamily={branding.fontFamily || 'Inter'}
fill={branding.color || '#ffffff'}
align={align}
lineHeight={1.5}
/>
)}
{/* Social icons with text */}
{hasSocialIcons && activeSocialPlatforms.map((platform, index) => {
const path = SOCIAL_ICON_PATHS[platform.key];
if (!path) return null;
// Calculate position for this social item
let itemX: number;
let itemY: number;
if (socialLayout === 'vertical') {
itemY = socialStartY + index * (socialItemHeight + iconGap - 4);
itemX = align === 'right' ? x : x;
} else {
itemY = socialStartY;
// For horizontal layout, we need to calculate cumulative width
// This is simplified - for perfect alignment we'd need to measure text
const prevItemsWidth = activeSocialPlatforms.slice(0, index).reduce((acc, p) => {
const textWidth = (p.value.length * fontSize * 0.5); // Approximate text width
return acc + iconSize + iconTextGap + textWidth + iconGap;
}, 0);
itemX = align === 'right' ? x - prevItemsWidth : x + prevItemsWidth;
}
// Icon vertical centering within item
const iconY = itemY + (socialItemHeight - iconSize) / 2;
// Text vertical centering
const textY = itemY + (socialItemHeight - fontSize) / 2;
if (align === 'right') {
// For right alignment: text first, then icon
const textWidth = platform.value.length * fontSize * 0.5; // Approximate
return (
<Group key={platform.key}>
<Path
x={itemX - iconSize}
y={iconY}
data={path}
fill={branding.color || '#ffffff'}
scaleX={iconScale}
scaleY={iconScale}
/>
<Text
x={itemX - iconSize - iconTextGap - textWidth}
y={textY}
text={platform.value}
fontSize={fontSize}
fontFamily={branding.fontFamily || 'Inter'}
fill={branding.color || '#ffffff'}
/>
</Group>
);
} else {
// For left alignment: icon first, then text
return (
<Group key={platform.key}>
<Path
x={itemX}
y={iconY}
data={path}
fill={branding.color || '#ffffff'}
scaleX={iconScale}
scaleY={iconScale}
/>
<Text
x={itemX + iconSize + iconTextGap}
y={textY}
text={platform.value}
fontSize={fontSize}
fontFamily={branding.fontFamily || 'Inter'}
fill={branding.color || '#ffffff'}
/>
</Group>
);
}
})}
</Group>
);
}, [background.branding, width, height]);
+102
View File
@@ -0,0 +1,102 @@
import React from 'react';
// SVG path data for social media icons
export const SOCIAL_ICON_PATHS: Record<string, string> = {
twitter: 'M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z',
linkedin: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z',
instagram: 'M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z',
github: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
youtube: 'M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z',
tiktok: 'M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z',
};
// Social media platform order and colors
export const SOCIAL_PLATFORMS_CONFIG = [
{ key: 'twitter', label: 'X', color: '#000000' },
{ key: 'linkedin', label: 'LinkedIn', color: '#0A66C2' },
{ key: 'instagram', label: 'Instagram', color: '#E4405F' },
{ key: 'github', label: 'GitHub', color: '#181717' },
{ key: 'youtube', label: 'YouTube', color: '#FF0000' },
{ key: 'tiktok', label: 'TikTok', color: '#000000' },
] as const;
interface SocialIconProps {
platform: string;
size?: number;
color?: string;
className?: string;
}
export const SocialIcon: React.FC<SocialIconProps> = ({
platform,
size = 24,
color = 'currentColor',
className = ''
}) => {
const path = SOCIAL_ICON_PATHS[platform];
if (!path) return null;
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill={color}
className={className}
style={{ display: 'block' }}
>
<path d={path} />
</svg>
);
};
interface SocialIconsGroupProps {
social: {
twitter?: string;
linkedin?: string;
instagram?: string;
github?: string;
youtube?: string;
tiktok?: string;
};
size?: number;
color?: string;
layout?: 'horizontal' | 'vertical';
gap?: number;
}
export const SocialIconsGroup: React.FC<SocialIconsGroupProps> = ({
social,
size = 20,
color = '#ffffff',
layout = 'horizontal',
gap = 12,
}) => {
const activePlatforms = SOCIAL_PLATFORMS_CONFIG.filter(
(p) => social[p.key as keyof typeof social]
);
if (activePlatforms.length === 0) return null;
return (
<div
style={{
display: 'flex',
flexDirection: layout === 'horizontal' ? 'row' : 'column',
gap: `${gap}px`,
alignItems: 'center',
}}
>
{activePlatforms.map((platform) => (
<SocialIcon
key={platform.key}
platform={platform.key}
size={size}
color={color}
/>
))}
</div>
);
};
export default SocialIcon;
+105 -69
View File
@@ -1,6 +1,8 @@
import React from 'react';
import React, { useEffect } from 'react';
import { useCanvasStore } from '../../store/canvasStore';
import { useBrandingStore } from '../../store/brandingStore';
import { FONT_FAMILIES } from '../../types';
import { SocialIcon } from '../elements/SocialIcons';
const POSITION_OPTIONS = [
{ value: 'top-left', label: 'Top Left' },
@@ -19,40 +21,31 @@ const SOCIAL_PLATFORMS = [
] as const;
const BrandingPanel: React.FC = () => {
const { snap, setBackground } = useCanvasStore();
// Default branding values for backwards compatibility
const defaultBranding = {
enabled: false,
position: 'bottom-right' as const,
name: '',
website: '',
social: {},
showName: true,
showWebsite: true,
showSocial: true,
fontSize: 14,
fontFamily: 'Inter',
color: '#ffffff',
opacity: 0.8,
padding: 24,
};
const branding = snap.background.branding || defaultBranding;
const { setBackground } = useCanvasStore();
const { info, preferences, updateInfo, updateSocial, updatePreferences } = useBrandingStore();
const updateBranding = (updates: Partial<typeof branding>) => {
setBackground({
branding: { ...branding, ...updates },
});
};
const updateSocial = (platform: string, value: string) => {
// Sync branding store to canvas whenever it changes
useEffect(() => {
setBackground({
branding: {
...branding,
social: { ...(branding.social || {}), [platform]: value },
...preferences,
name: info.name,
website: info.website,
social: info.social,
},
});
}, [info, preferences, setBackground]);
const handleUpdatePreferences = (updates: Partial<typeof preferences>) => {
updatePreferences(updates);
};
const handleUpdateInfo = (updates: Partial<typeof info>) => {
updateInfo(updates);
};
const handleUpdateSocial = (platform: string, value: string) => {
updateSocial(platform, value);
};
return (
@@ -63,20 +56,20 @@ const BrandingPanel: React.FC = () => {
Branding Watermark
</label>
<button
onClick={() => updateBranding({ enabled: !branding.enabled })}
onClick={() => handleUpdatePreferences({ enabled: !preferences.enabled })}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${
branding.enabled ? 'bg-blue-600' : 'bg-white/10'
preferences.enabled ? 'bg-blue-600' : 'bg-white/10'
}`}
>
<span
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white transition-transform ${
branding.enabled ? 'translate-x-4' : 'translate-x-1'
preferences.enabled ? 'translate-x-4' : 'translate-x-1'
}`}
/>
</button>
</div>
{branding.enabled && (
{preferences.enabled && (
<div className="space-y-5">
{/* Position */}
<div>
@@ -87,9 +80,9 @@ const BrandingPanel: React.FC = () => {
{POSITION_OPTIONS.map((opt) => (
<button
key={opt.value}
onClick={() => updateBranding({ position: opt.value })}
onClick={() => handleUpdatePreferences({ position: opt.value })}
className={`py-2 px-3 rounded-lg text-xs font-medium transition-all border ${
branding.position === opt.value
preferences.position === opt.value
? 'bg-blue-600/20 text-blue-400 border-blue-500/50'
: 'bg-white/5 text-neutral-400 hover:text-white hover:bg-white/10 border-white/5'
}`}
@@ -107,20 +100,20 @@ const BrandingPanel: React.FC = () => {
Name
</label>
<button
onClick={() => updateBranding({ showName: !branding.showName })}
onClick={() => handleUpdatePreferences({ showName: !preferences.showName })}
className={`text-xs px-2 py-0.5 rounded ${
branding.showName
preferences.showName
? 'bg-blue-600/20 text-blue-400'
: 'bg-white/5 text-neutral-500'
}`}
>
{branding.showName ? 'Show' : 'Hide'}
{preferences.showName ? 'Show' : 'Hide'}
</button>
</div>
<input
type="text"
value={branding.name}
onChange={(e) => updateBranding({ name: e.target.value })}
value={info.name}
onChange={(e) => handleUpdateInfo({ name: e.target.value })}
placeholder="Your Name"
className="w-full bg-white/5 text-white px-3 py-2 rounded-lg text-sm border border-white/5 focus:border-blue-500/50 focus:outline-none"
/>
@@ -133,20 +126,20 @@ const BrandingPanel: React.FC = () => {
Website
</label>
<button
onClick={() => updateBranding({ showWebsite: !branding.showWebsite })}
onClick={() => handleUpdatePreferences({ showWebsite: !preferences.showWebsite })}
className={`text-xs px-2 py-0.5 rounded ${
branding.showWebsite
preferences.showWebsite
? 'bg-blue-600/20 text-blue-400'
: 'bg-white/5 text-neutral-500'
}`}
>
{branding.showWebsite ? 'Show' : 'Hide'}
{preferences.showWebsite ? 'Show' : 'Hide'}
</button>
</div>
<input
type="text"
value={branding.website}
onChange={(e) => updateBranding({ website: e.target.value })}
value={info.website}
onChange={(e) => handleUpdateInfo({ website: e.target.value })}
placeholder="yourwebsite.com"
className="w-full bg-white/5 text-white px-3 py-2 rounded-lg text-sm border border-white/5 focus:border-blue-500/50 focus:outline-none"
/>
@@ -159,32 +152,75 @@ const BrandingPanel: React.FC = () => {
Social Media
</label>
<button
onClick={() => updateBranding({ showSocial: !branding.showSocial })}
onClick={() => handleUpdatePreferences({ showSocial: !preferences.showSocial })}
className={`text-xs px-2 py-0.5 rounded ${
branding.showSocial
preferences.showSocial
? 'bg-blue-600/20 text-blue-400'
: 'bg-white/5 text-neutral-500'
}`}
>
{branding.showSocial ? 'Show' : 'Hide'}
{preferences.showSocial ? 'Show' : 'Hide'}
</button>
</div>
<div className="space-y-2">
{SOCIAL_PLATFORMS.map((platform) => (
<div key={platform.key} className="flex items-center gap-2">
<span className="text-xs text-neutral-500 w-20 shrink-0">
{platform.label}
</span>
<div className="w-5 h-5 shrink-0 flex items-center justify-center text-neutral-400">
<SocialIcon platform={platform.key} size={16} color="currentColor" />
</div>
<input
type="text"
value={branding.social[platform.key as keyof typeof branding.social] || ''}
onChange={(e) => updateSocial(platform.key, e.target.value)}
value={info.social[platform.key as keyof typeof info.social] || ''}
onChange={(e) => handleUpdateSocial(platform.key, e.target.value)}
placeholder={platform.placeholder}
className="flex-1 bg-white/5 text-white px-2 py-1.5 rounded-md text-xs border border-white/5 focus:border-blue-500/50 focus:outline-none"
/>
</div>
))}
</div>
{/* Social Icons Layout */}
<div className="mt-4 space-y-3">
<div>
<label className="block text-xs text-neutral-500 mb-2">Layout</label>
<div className="flex gap-2">
<button
onClick={() => handleUpdatePreferences({ socialLayout: 'horizontal' })}
className={`flex-1 py-2 px-3 rounded-lg text-xs font-medium transition-all border ${
preferences.socialLayout === 'horizontal'
? 'bg-blue-600/20 text-blue-400 border-blue-500/50'
: 'bg-white/5 text-neutral-400 hover:text-white hover:bg-white/10 border-white/5'
}`}
>
Horizontal
</button>
<button
onClick={() => handleUpdatePreferences({ socialLayout: 'vertical' })}
className={`flex-1 py-2 px-3 rounded-lg text-xs font-medium transition-all border ${
preferences.socialLayout === 'vertical'
? 'bg-blue-600/20 text-blue-400 border-blue-500/50'
: 'bg-white/5 text-neutral-400 hover:text-white hover:bg-white/10 border-white/5'
}`}
>
Vertical
</button>
</div>
</div>
<div>
<label className="block text-xs text-neutral-500 mb-2">
Icon Size: {preferences.socialIconSize}px
</label>
<input
type="range"
min="14"
max="32"
value={preferences.socialIconSize}
onChange={(e) => handleUpdatePreferences({ socialIconSize: parseInt(e.target.value) })}
className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
</div>
</div>
</div>
{/* Styling */}
@@ -197,8 +233,8 @@ const BrandingPanel: React.FC = () => {
<div className="mb-4">
<label className="block text-xs text-neutral-500 mb-2">Font</label>
<select
value={branding.fontFamily}
onChange={(e) => updateBranding({ fontFamily: e.target.value })}
value={preferences.fontFamily}
onChange={(e) => handleUpdatePreferences({ fontFamily: e.target.value })}
className="w-full bg-white/5 text-white px-3 py-2 rounded-lg text-sm border border-white/5 focus:border-blue-500/50 focus:outline-none"
>
{FONT_FAMILIES.text.map((font) => (
@@ -212,14 +248,14 @@ const BrandingPanel: React.FC = () => {
{/* Font Size */}
<div className="mb-4">
<label className="block text-xs text-neutral-500 mb-2">
Font Size: {branding.fontSize}px
Font Size: {preferences.fontSize}px
</label>
<input
type="range"
min="10"
max="24"
value={branding.fontSize}
onChange={(e) => updateBranding({ fontSize: parseInt(e.target.value) })}
value={preferences.fontSize}
onChange={(e) => handleUpdatePreferences({ fontSize: parseInt(e.target.value) })}
className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
</div>
@@ -231,15 +267,15 @@ const BrandingPanel: React.FC = () => {
<div className="w-6 h-6 rounded overflow-hidden relative border border-white/10 shrink-0">
<input
type="color"
value={branding.color}
onChange={(e) => updateBranding({ color: e.target.value })}
value={preferences.color}
onChange={(e) => handleUpdatePreferences({ color: e.target.value })}
className="absolute inset-[-4px] w-[200%] h-[200%] cursor-pointer"
/>
</div>
<input
type="text"
value={branding.color}
onChange={(e) => updateBranding({ color: e.target.value })}
value={preferences.color}
onChange={(e) => handleUpdatePreferences({ color: e.target.value })}
className="w-full bg-transparent text-white text-xs focus:outline-none font-mono"
/>
</div>
@@ -248,15 +284,15 @@ const BrandingPanel: React.FC = () => {
{/* Opacity */}
<div className="mb-4">
<label className="block text-xs text-neutral-500 mb-2">
Opacity: {Math.round(branding.opacity * 100)}%
Opacity: {Math.round(preferences.opacity * 100)}%
</label>
<input
type="range"
min="0.1"
max="1"
step="0.05"
value={branding.opacity}
onChange={(e) => updateBranding({ opacity: parseFloat(e.target.value) })}
value={preferences.opacity}
onChange={(e) => handleUpdatePreferences({ opacity: parseFloat(e.target.value) })}
className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
</div>
@@ -264,14 +300,14 @@ const BrandingPanel: React.FC = () => {
{/* Padding */}
<div>
<label className="block text-xs text-neutral-500 mb-2">
Padding: {branding.padding}px
Padding: {preferences.padding}px
</label>
<input
type="range"
min="8"
max="48"
value={branding.padding}
onChange={(e) => updateBranding({ padding: parseInt(e.target.value) })}
value={preferences.padding}
onChange={(e) => handleUpdatePreferences({ padding: parseInt(e.target.value) })}
className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600"
/>
</div>
+94
View File
@@ -0,0 +1,94 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface BrandingInfo {
name: string;
website: string;
social: {
twitter?: string;
linkedin?: string;
instagram?: string;
github?: string;
youtube?: string;
tiktok?: string;
};
}
interface BrandingPreferences {
enabled: boolean;
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
showName: boolean;
showWebsite: boolean;
showSocial: boolean;
fontSize: number;
fontFamily: string;
color: string;
opacity: number;
padding: number;
socialIconSize: number;
socialLayout: 'horizontal' | 'vertical';
}
interface BrandingStore {
info: BrandingInfo;
preferences: BrandingPreferences;
// Actions
updateInfo: (updates: Partial<BrandingInfo>) => void;
updateSocial: (platform: string, value: string) => void;
updatePreferences: (updates: Partial<BrandingPreferences>) => void;
resetToDefaults: () => void;
}
const defaultInfo: BrandingInfo = {
name: '',
website: '',
social: {},
};
const defaultPreferences: BrandingPreferences = {
enabled: false,
position: 'bottom-right',
showName: true,
showWebsite: true,
showSocial: true,
fontSize: 14,
fontFamily: 'Inter',
color: '#ffffff',
opacity: 0.8,
padding: 24,
socialIconSize: 20,
socialLayout: 'horizontal',
};
export const useBrandingStore = create<BrandingStore>()(
persist(
(set) => ({
info: defaultInfo,
preferences: defaultPreferences,
updateInfo: (updates) => set((state) => ({
info: { ...state.info, ...updates },
})),
updateSocial: (platform, value) => set((state) => ({
info: {
...state.info,
social: { ...state.info.social, [platform]: value },
},
})),
updatePreferences: (updates) => set((state) => ({
preferences: { ...state.preferences, ...updates },
})),
resetToDefaults: () => set({
info: defaultInfo,
preferences: defaultPreferences,
}),
}),
{
name: 'yvcode-branding',
}
)
);
+2
View File
@@ -33,6 +33,8 @@ export interface Branding {
color: string;
opacity: number;
padding: number;
socialIconSize?: number;
socialLayout?: 'horizontal' | 'vertical';
}
export interface BrandStrip {