diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index 4b8ad7e..009d840 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 content */} + {hasTextContent && ( + + )} + + {/* 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 ( + + + + + ); + } else { + // For left alignment: icon first, then text + return ( + + + + + ); + } + })} + ); }, [background.branding, width, height]); diff --git a/src/components/elements/SocialIcons.tsx b/src/components/elements/SocialIcons.tsx new file mode 100644 index 0000000..5e1d900 --- /dev/null +++ b/src/components/elements/SocialIcons.tsx @@ -0,0 +1,102 @@ +import React from 'react'; + +// SVG path data for social media icons +export const SOCIAL_ICON_PATHS: Record = { + 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 = ({ + platform, + size = 24, + color = 'currentColor', + className = '' +}) => { + const path = SOCIAL_ICON_PATHS[platform]; + if (!path) return null; + + return ( + + + + ); +}; + +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 = ({ + 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 ( +
+ {activePlatforms.map((platform) => ( + + ))} +
+ ); +}; + +export default SocialIcon; diff --git a/src/components/inspector/BrandingPanel.tsx b/src/components/inspector/BrandingPanel.tsx index a447469..da0abef 100644 --- a/src/components/inspector/BrandingPanel.tsx +++ b/src/components/inspector/BrandingPanel.tsx @@ -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) => { - 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) => { + updatePreferences(updates); + }; + + const handleUpdateInfo = (updates: Partial) => { + updateInfo(updates); + }; + + const handleUpdateSocial = (platform: string, value: string) => { + updateSocial(platform, value); }; return ( @@ -63,20 +56,20 @@ const BrandingPanel: React.FC = () => { Branding Watermark - {branding.enabled && ( + {preferences.enabled && (
{/* Position */}
@@ -87,9 +80,9 @@ const BrandingPanel: React.FC = () => { {POSITION_OPTIONS.map((opt) => (
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
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
{SOCIAL_PLATFORMS.map((platform) => (
- - {platform.label} - +
+ +
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" />
))}
+ + {/* Social Icons Layout */} +
+
+ +
+ + +
+
+ +
+ + handleUpdatePreferences({ socialIconSize: parseInt(e.target.value) })} + className="w-full h-1.5 bg-white/10 rounded-lg appearance-none cursor-pointer accent-blue-600" + /> +
+
{/* Styling */} @@ -197,8 +233,8 @@ const BrandingPanel: React.FC = () => {
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" />
@@ -231,15 +267,15 @@ const BrandingPanel: React.FC = () => {
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" />
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" /> @@ -248,15 +284,15 @@ const BrandingPanel: React.FC = () => { {/* Opacity */}
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" />
@@ -264,14 +300,14 @@ const BrandingPanel: React.FC = () => { {/* Padding */}
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" />
diff --git a/src/store/brandingStore.ts b/src/store/brandingStore.ts new file mode 100644 index 0000000..100d7cf --- /dev/null +++ b/src/store/brandingStore.ts @@ -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) => void; + updateSocial: (platform: string, value: string) => void; + updatePreferences: (updates: Partial) => 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()( + 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', + } + ) +); diff --git a/src/types/index.ts b/src/types/index.ts index e84555d..e362154 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -33,6 +33,8 @@ export interface Branding { color: string; opacity: number; padding: number; + socialIconSize?: number; + socialLayout?: 'horizontal' | 'vertical'; } export interface BrandStrip {