Merge pull request #494 from presenton/refactor/update_ui

refactor: update Ui components in settings & templates
This commit is contained in:
Shiva Raj Badu 2026-04-03 18:44:27 +05:45 committed by GitHub
commit cb7be2db42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 426 additions and 425 deletions

View file

@ -175,7 +175,7 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL
<PopoverContent
className="p-0"
align="start"
style={{ width: "var(--radix-popover-trigger-width)" }}
style={{ width: "300px" }}
>
<Command>
<CommandInput placeholder="Search provider..." />

View file

@ -13,19 +13,20 @@ const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }
<p className='text-xs text-black font-medium border-b mt-[3.15rem] border-[#E1E1E5] pb-3.5'>FILTER BY:</p>
<div className='mt-6 flex-1'>
<p className='text-[#3A3A3A] text-xs font-medium pb-2.5'>Select Mode</p>
<div className='p-1 rounded-[40px] bg-[#ffffff] w-fit border border-[#EDEEEF] flex items-center justify-center mb-[34px] '>
<button className='px-3 py-2 text-xs font-medium text-[#3A3A3A] rounded-[70px]'
<div className='p-0.5 rounded-[40px] bg-[#ffffff] w-fit border border-[#EDEEEF] flex items-center justify-center mb-[34px] '>
<button className='px-3 font-syne h-[26px] text-xs font-medium text-[#3A3A3A] rounded-[70px]'
onClick={() => setMode('presenton')}
style={{
background: mode === 'presenton' ? '#F4F3FF' : 'transparent',
color: mode === 'presenton' ? '#5146E5' : '#3A3A3A'
}}
>Presenton</button>
>Presenton
</button>
<svg xmlns="http://www.w3.org/2000/svg" className='mx-1' width="2" height="17" viewBox="0 0 2 17" fill="none">
<path d="M1 0V16.5" stroke="#EDECEC" strokeWidth="2" />
</svg>
<div className='relative'>
<button className='px-3 py-2 text-xs font-medium rounded-[70px] cursor-not-allowed opacity-60'
<button className='px-3 font-syne h-[26px] text-xs font-medium rounded-[70px] cursor-not-allowed opacity-60'
disabled
style={{
background: 'transparent',
@ -43,15 +44,15 @@ const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }
</div>
<p className='text-[#3A3A3A] text-xs font-medium pb-2.5'>Select Provider</p>
{mode === 'presenton' && <div className='space-y-2.5'>
<button className={` w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'text-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#EDEEEF]'}`} onClick={() => setSelectedProvider('text-provider')}>
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF]'>
<button className={` w-full rounded-[6px] px-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'text-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#EDEEEF]'}`} onClick={() => setSelectedProvider('text-provider')}>
<div className='relative w-[18px] h-[18px] rounded-full overflow-hidden border border-[#EDEEEF]'>
<img src={textProviderIcon} className=' object-cover w-full h-full overflow-hidden' alt='google' />
</div>
<p className='text-[#191919] text-xs font-medium' >Text Provider</p>
</button>
<button className={` w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'image-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#EDEEEF]'}`} onClick={() => setSelectedProvider('image-provider')}>
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF]'>
<button className={` w-full rounded-[6px] px-3 py-4 flex items-center gap-1.5 border ${selectedProvider === 'image-provider' ? 'bg-[#F4F3FF] border-[#D9D6FE]' : 'bg-white border-[#EDEEEF]'}`} onClick={() => setSelectedProvider('image-provider')}>
<div className='relative w-[18px] h-[18px] rounded-full overflow-hidden border border-[#EDEEEF]'>
<img src={imageProviderIcon} className=' object-cover w-full h-full overflow-hidden' alt='google' />
</div>
<p className='text-[#191919] text-xs font-medium' >Image Provider</p>
@ -59,8 +60,8 @@ const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }
</div>}
{
mode === 'nanobanana' && <div>
<button className={` w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border bg-[#F4F3FF] border-[#D9D6FE]`}>
<div className='relative w-6 h-6 rounded-full overflow-hidden border border-[#EDEEEF]'>
<button className={` w-full rounded-[6px] px-3 py-4 flex items-center gap-1.5 border bg-[#F4F3FF] border-[#D9D6FE]`}>
<div className='relative w-[18px] h-[18px] rounded-full overflow-hidden border border-[#EDEEEF]'>
<img src='/providers/openai.png' className=' object-cover w-full h-full overflow-hidden' alt='google' />
</div>

View file

@ -216,8 +216,8 @@ const TextProvider = ({
return (
<div className="space-y-6 bg-[#F9F8F8] p-7 rounded-[12px] ">
{/* API Key Input */}
<div className="mb-4 flex items-center justify-between rounded-[12px] bg-white pt-5 pb-10 px-10">
<div className=" max-w-[290px] pb-[50px]">
<div className="mb-4 flex items-end justify-between rounded-[12px] bg-white pt-5 pb-10 px-10">
<div className=" max-w-[290px] ">
<div className='w-[60px] h-[60px] rounded-[4px] flex items-center justify-center'
style={{ backgroundColor: '#4C55541A' }}
>
@ -232,11 +232,10 @@ const TextProvider = ({
Choosing where text contets come from
</p>
</div>
<div>
<div className='flex flex-col justify-end items-end gap-4'>
<div className={`flex gap-4 justify-end ${selectedProvider === 'codex' ? 'items-end' : 'items-start'}`}>
<div className={`relative ${selectedProvider === 'codex' ? 'w-[240px]' : 'w-[222px]'}`}>
<div className="flex flex-col justify-start ">
<label className="block text-sm font-medium text-gray-700 mb-2">
Select Text Provider
</label>
@ -265,7 +264,7 @@ const TextProvider = ({
<PopoverContent
className="p-0"
align="start"
style={{ width: "var(--radix-popover-trigger-width)" }}
style={{ width: "300px" }}
>
<Command>
<CommandInput placeholder="Search provider..." />
@ -311,8 +310,6 @@ const TextProvider = ({
</PopoverContent>
</Popover>
</div>
</div>
<div className={`relative flex flex-col justify-end ${selectedProvider === 'codex' ? 'items-end w-[262px] max-w-full' : 'items-end w-[222px]'}`}>
<div className="flex flex-col justify-start w-full ">
@ -431,89 +428,86 @@ const TextProvider = ({
"Check models"
)}
</button>
)}
</div>
{/* Model Selection - only show if models are available */}
{selectedProvider !== 'codex' && modelsChecked && availableModels.length > 0 ? (
<div className="w-[222px]">
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
{selectedProvider === 'ollama' ? 'Choose a supported model' : `Select ${modelLabel} Model`}
</label>
<div className="w-full">
<Popover
open={openModelSelect}
onOpenChange={setOpenModelSelect}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openModelSelect}
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
>
<span className="text-sm truncate font-medium text-gray-900">
{currentModel
? availableModels.find(model => model === currentModel) || currentModel
: "Select a model"}
</span>
<ChevronUp className="w-4 h-4 text-gray-500" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
align="start"
style={{ width: "var(--radix-popover-trigger-width)" }}
</div>
{/* Model Selection - only show if models are available */}
{selectedProvider !== 'codex' && modelsChecked && availableModels.length > 0 ? (
<div className="w-[222px]">
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
{selectedProvider === 'ollama' ? 'Choose a supported model' : `Select ${modelLabel} Model`}
</label>
<div className="w-full">
<Popover
open={openModelSelect}
onOpenChange={setOpenModelSelect}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openModelSelect}
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
>
<Command>
<CommandInput placeholder="Search models..." />
<CommandList>
<CommandEmpty>No model found.</CommandEmpty>
<CommandGroup>
{availableModels.map((model, index) => (
<CommandItem
key={index}
value={model}
onSelect={(value) => {
if (currentModelField) {
onInputChange(value, currentModelField);
}
setOpenModelSelect(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
currentModel === model
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex gap-3 items-center">
<div className="flex flex-col space-y-1 flex-1">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-gray-900">
{model}
</span>
</div>
<span className="text-sm truncate font-medium text-gray-900">
{currentModel
? availableModels.find(model => model === currentModel) || currentModel
: "Select a model"}
</span>
<ChevronUp className="w-4 h-4 text-gray-500" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0"
align="start"
style={{ width: "var(--radix-popover-trigger-width)" }}
>
<Command>
<CommandInput placeholder="Search models..." />
<CommandList>
<CommandEmpty>No model found.</CommandEmpty>
<CommandGroup>
{availableModels.map((model, index) => (
<CommandItem
key={index}
value={model}
onSelect={(value) => {
if (currentModelField) {
onInputChange(value, currentModelField);
}
setOpenModelSelect(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
currentModel === model
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex gap-3 items-center">
<div className="flex flex-col space-y-1 flex-1">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-gray-900">
{model}
</span>
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
) : null}
</div>
</div>
) : null}
</div>
</div>
{/* Show message if no models found */}
@ -526,7 +520,7 @@ const TextProvider = ({
)}
{/* Web Grounding Toggle - show at the end, below models dropdown */}
<div className="bg-white flex justify-between items-center p-10 rounded-[12px]">
<div className=' max-w-[290px]'>
@ -536,7 +530,6 @@ const TextProvider = ({
</p>
</div>
<div className="flex items-center gap-4">
<div className="w-[222px]">
<div className="flex items-center mb-4 gap-2.5 ">
<Switch
@ -547,16 +540,9 @@ const TextProvider = ({
Enable Web Grounding
</label>
</div>
</div>
{/* <div className="w-[295px]"></div> */}
</div>
</div>
</div>
)
}

View file

@ -1,76 +1,113 @@
import { Card } from "@/components/ui/card";
export default function LoadingProfile() {
function Shimmer({ className }: { className?: string }) {
return (
<div className="h-screen bg-gradient-to-b font-instrument_sans from-gray-50 to-white flex flex-col overflow-hidden">
{/* Header Skeleton */}
<div className="flex-shrink-0 bg-white border-b border-gray-200 p-4">
<div className="container mx-auto max-w-3xl">
<div className="flex items-center justify-between">
<div className="h-8 w-32 bg-gray-200 animate-pulse rounded-md" />
<div className="flex items-center gap-4">
<div className="h-8 w-8 bg-gray-200 animate-pulse rounded-full" />
<div className="h-8 w-24 bg-gray-200 animate-pulse rounded-md" />
<div
className={`bg-[#E1E1E5] animate-pulse rounded-md ${className ?? ""}`}
aria-hidden
/>
);
}
export default function LoadingSettings() {
return (
<div className="h-screen font-syne flex flex-col overflow-hidden relative">
<div
className="fixed z-0 bottom-[-14.5rem] left-0 w-full h-full pointer-events-none"
style={{
height: "341px",
borderRadius: "1440px",
background:
"radial-gradient(5.92% 104.69% at 50% 100%, rgba(122, 90, 248, 0.00) 0%, rgba(255, 255, 255, 0.00) 100%), radial-gradient(50% 50% at 50% 50%, rgba(122, 90, 248, 0.80) 0%, rgba(122, 90, 248, 0.00) 100%)",
}}
/>
<main className="w-full mx-auto gap-6 overflow-hidden flex">
{/* SettingSideBar structure */}
<div className="w-full max-w-[230px] h-screen px-4 pt-[22px] bg-[#F9FAFB] flex flex-col shrink-0">
<div className="mt-[3.15rem] border-b border-[#E1E1E5] pb-3.5">
<Shimmer className="h-3 w-16" />
</div>
<div className="mt-6 flex-1 min-h-0">
<Shimmer className="h-3 w-24 mb-2.5" />
<div className="p-0.5 rounded-[40px] bg-white w-full max-w-[210px] border border-[#EDEEEF] flex items-center mb-[34px] h-[30px]">
<Shimmer className="h-[26px] flex-1 rounded-[70px] mx-0.5" />
<Shimmer className="h-[26px] flex-1 rounded-[70px] mx-0.5 opacity-70" />
</div>
<Shimmer className="h-3 w-28 mb-2.5" />
<div className="space-y-2.5">
{[0, 1].map((i) => (
<div
key={i}
className="w-full rounded-[6px] px-3 py-4 flex items-center gap-1.5 border border-[#EDEEEF] bg-white"
>
<Shimmer className="h-[18px] w-[18px] rounded-full shrink-0" />
<Shimmer className="h-3 flex-1 max-w-[100px]" />
</div>
))}
</div>
</div>
<div className="border-t border-[#E1E1E5] py-5">
<Shimmer className="h-3 w-12 mb-2.5" />
<div className="w-full rounded-[6px] p-3 py-4 flex items-center gap-1.5 border border-[#EDEEEF] bg-white">
<Shimmer className="h-6 w-6 rounded-full shrink-0" />
<Shimmer className="h-3 w-16" />
</div>
</div>
</div>
</div>
{/* Main Content Skeleton */}
<main className="flex-1 container mx-auto px-4 max-w-3xl overflow-hidden flex flex-col">
<div className="flex-1 overflow-hidden">
{/* LLM Selection Content Skeleton */}
<div className="space-y-6 p-6">
{/* Page Title */}
<div className="space-y-2">
<div className="h-8 w-48 bg-gray-200 animate-pulse rounded-md" />
<div className="h-5 w-72 bg-gray-200 animate-pulse rounded-md" />
{/* Main column — matches SettingPage + TextProvider default */}
<div className="w-full min-w-0 flex flex-col">
<div className="sticky top-0 right-0 z-50 py-[28px] backdrop-blur mb-4">
<div className="flex gap-3 items-center flex-wrap">
<Shimmer className="h-8 w-[132px] rounded-md" />
<Shimmer className="h-[22px] w-[min(320px,55%)] rounded-[50px]" />
</div>
</div>
{/* LLM Provider Cards */}
<div className="space-y-4">
{[...Array(3)].map((_, index) => (
<Card key={index} className="p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="h-10 w-10 bg-gray-200 animate-pulse rounded-md" />
<div className="space-y-1">
<div className="h-5 w-32 bg-gray-200 animate-pulse rounded-md" />
<div className="h-4 w-48 bg-gray-200 animate-pulse rounded-md" />
</div>
</div>
<div className="h-6 w-6 bg-gray-200 animate-pulse rounded-full" />
</div>
{/* Configuration Fields */}
<div className="space-y-4">
{[...Array(2)].map((_, fieldIndex) => (
<div key={fieldIndex} className="space-y-2">
<div className="h-4 w-24 bg-gray-200 animate-pulse rounded-md" />
<div className="h-10 w-full bg-gray-200 animate-pulse rounded-md" />
</div>
))}
</div>
</Card>
))}
</div>
{/* Model Selection */}
<Card className="p-6">
<div className="space-y-4">
<div className="h-5 w-32 bg-gray-200 animate-pulse rounded-md" />
<div className="h-10 w-full bg-gray-200 animate-pulse rounded-md" />
<div className="space-y-6 bg-[#F9F8F8] p-7 rounded-[12px] pr-4 sm:pr-7">
{/* TextProvider top card: white panel, icon + copy left, controls right */}
<div className="mb-4 flex flex-col lg:flex-row lg:items-end lg:justify-between gap-8 rounded-[12px] bg-white pt-5 pb-10 px-6 sm:px-10">
<div className="max-w-[290px] shrink-0">
<Shimmer className="w-[60px] h-[60px] rounded-[4px]" />
<Shimmer className="h-6 w-48 mt-2.5 mb-2" />
<Shimmer className="h-4 w-full max-w-[260px]" />
<Shimmer className="h-4 w-40 mt-1.5" />
</div>
</Card>
<div className="flex flex-col items-stretch lg:items-end gap-4 flex-1 min-w-0">
<div className="flex flex-col sm:flex-row gap-4 sm:justify-end w-full">
<div className="w-full sm:w-[222px]">
<Shimmer className="h-4 w-36 mb-2" />
<Shimmer className="h-12 w-full rounded-lg" />
</div>
<div className="w-full sm:w-[222px]">
<Shimmer className="h-4 w-28 mb-2" />
<Shimmer className="h-12 w-full rounded-lg" />
</div>
</div>
<div className="w-full sm:w-[222px] sm:ml-auto">
<Shimmer className="h-4 w-40 mb-2" />
<Shimmer className="h-12 w-full rounded-lg" />
</div>
</div>
</div>
{/* TextProvider “Advanced” card */}
<div className="bg-white flex flex-col sm:flex-row sm:justify-between sm:items-center gap-6 p-6 sm:p-10 rounded-[12px]">
<div className="max-w-[290px] shrink-0">
<Shimmer className="h-6 w-28 mb-2" />
<Shimmer className="h-4 w-52" />
</div>
<div className="flex items-center gap-2.5 w-full sm:w-[222px] sm:justify-start">
<Shimmer className="h-6 w-11 rounded-full shrink-0" />
<Shimmer className="h-4 flex-1 max-w-[160px]" />
</div>
</div>
</div>
</div>
</main>
{/* Fixed Bottom Button Skeleton */}
<div className="flex-shrink-0 bg-white border-t border-gray-200 p-4">
<div className="container mx-auto max-w-3xl">
<div className="h-12 w-full bg-gray-200 animate-pulse rounded-lg" />
</div>
{/* Fixed save button — matches SettingPage placement */}
<div className="mx-auto fixed bottom-20 right-5 z-40">
<Shimmer className="h-12 w-[200px] sm:w-[240px] rounded-[58px]" />
</div>
</div>
);

View file

@ -11,7 +11,7 @@ const CreateCustomTemplate = () => {
trackEvent(MixpanelEvent.Templates_Build_Template_Clicked);
router.push('/custom-template')
}}
className='w-full rounded-xl border border-[#EDEEEF] cursor-pointer font-syne'>
className='w-full rounded-[22px] border border-[#EDEEEF] cursor-pointer font-syne'>
<div className='relative h-[215px] flex justify-center items-center '>
<img src="/card_bg.svg" alt="" className="absolute top-0 z-[1] left-0 w-full h-full object-cover" />
<div className='w-[36px] h-[36px] relative z-[4] rounded-full bg-[#7A5AF8] flex items-center justify-center'
@ -24,7 +24,7 @@ const CreateCustomTemplate = () => {
</div>
</div>
</div>
<div className='px-5 py-4 bg-white flex items-center gap-4 border-t border-[#EDEEEF]'>
<div className='px-5 py-4 bg-white flex items-center gap-4 overflow-hidden border-t border-[#EDEEEF]'>
<div className='bg-[#7A5AF8] w-[45px] h-[45px] rounded-lg p-2 flex items-center justify-center'>
<Sparkles className='w-6 h-6 text-white' />

View file

@ -2,20 +2,24 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { Card } from "@/components/ui/card";
import { ArrowUpRight, ChevronRight, ExternalLink, Loader2, Plus } from "lucide-react";
import { ArrowUpRight, ChevronRight, Loader2 } from "lucide-react";
import { templates } from "@/app/presentation-templates";
import { TemplateWithData, TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
import {
useCustomTemplateSummaries,
useCustomTemplatePreview,
CustomTemplates,
} from "@/app/hooks/useCustomTemplates";
import { CompiledLayout } from "@/app/hooks/compileLayout";
import CreateCustomTemplate from "./CreateCustomTemplate";
import Link from "next/link";
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
import {
TemplatePreviewStage,
LayoutsBadge,
InbuiltTemplatePreview,
CustomTemplatePreview,
} from "../../../components/TemplatePreviewComponents";
// Component for rendering custom template card with lazy-loaded previews
export const CustomTemplateCard = React.memo(function CustomTemplateCard({ template }: { template: CustomTemplates }) {
const router = useRouter();
const { previewLayouts, loading, totalLayouts } = useCustomTemplatePreview(`${template.id}`);
@ -26,73 +30,29 @@ export const CustomTemplateCard = React.memo(function CustomTemplateCard({ templ
} else {
router.push(`/template-preview?slug=custom-${template.id}`)
}
}
, [router, template.id, template.name]);
}, [router, template.id, template.name]);
return (
<Card
className="cursor-pointer flex flex-col justify-between shadow-none sm:shadow-none relative hover:shadow-lg transition-all duration-200 group overflow-hidden"
className="cursor-pointer flex flex-col shadow-none sm:shadow-none relative hover:shadow-sm transition-all duration-200 group overflow-hidden rounded-[22px] border border-[#E8E9EC] bg-white"
onClick={handleOpen}
>
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<span className="text-xs font-syne absolute top-2 flex gap-1 capitalize items-center left-2 rounded-[100px] px-2.5 py-1 bg-[#3A3A3AF5] text-white font-semibold z-40">
{totalLayouts} {totalLayouts === 1 ? 'Layout' : 'Layouts'}
</span>
<div className="p-5">
{/* Layout previews */}
<div className="grid grid-cols-2 gap-2">
{loading ? (
// Loading placeholders
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
<div
key={`${template.id}-loading-${index}`}
className="relative bg-linear-to-br from-purple-50 to-blue-50 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
>
<Loader2 className="w-4 h-4 text-purple-300 animate-spin" />
</div>
))
) : previewLayouts.length > 0 && (
// Actual layout previews
previewLayouts.slice(0, 4).map((layout: CompiledLayout, index: number) => {
const LayoutComponent = layout.component;
return (
<div
key={`${template.id}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.12] origin-top-left"
style={{ width: "833.33%", height: "833.33%" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
})
)}
</div>
</div>
<div className="flex items-center justify-between p-5 bg-white border-t border-[#EDEEEF] relative z-40 ">
<h3 className="text-sm font-bold w-[191px] text-gray-900">
{template.name}
</h3>
<div className="flex items-center gap-2">
<ArrowUpRight className="w-4 h-4 text-gray-400 group-hover:text-purple-600 transition-colors" />
</div>
<TemplatePreviewStage>
<LayoutsBadge count={totalLayouts} />
<CustomTemplatePreview
previewLayouts={previewLayouts}
loading={loading}
templateId={template.id}
/>
</TemplatePreviewStage>
<div className="relative z-40 flex items-center justify-between border-t border-[#EDEEEF] bg-white px-6 py-5">
<h3 className="max-w-[min(191px,65%)] text-base font-bold text-gray-900">{template.name}</h3>
<ArrowUpRight className="h-4 w-4 shrink-0 text-gray-400 transition-colors group-hover:text-purple-600" />
</div>
</Card>
);
}, (prev, next) => {
// Custom templates may be refetched, producing new object references; compare on fields we render/use.
return (
prev.template.id === next.template.id &&
prev.template.id === next.template.id &&
prev.template.name === next.template.name &&
prev.template.layoutCount === next.template.layoutCount
@ -106,54 +66,24 @@ const InbuiltTemplateCard = React.memo(function InbuiltTemplateCard({
template: TemplateLayoutsWithSettings;
onOpen: (id: string) => void;
}) {
const previewLayouts = useMemo(() => template.layouts.slice(0, 4), [template.layouts]);
const handleOpen = useCallback(() => onOpen(template.id), [onOpen, template.id]);
return (
<Card
key={template.id}
className="cursor-pointer relative sm:shadow-none shadow-none hover:shadow-lg transition-all duration-200 group overflow-hidden"
className="group relative cursor-pointer overflow-hidden rounded-[22px] border border-[#E8E9EC] bg-white shadow-none sm:shadow-none transition-all duration-200 hover:shadow-sm"
onClick={handleOpen}
>
<span className="text-xs font-syne absolute top-2 flex gap-1 capitalize items-center left-2 rounded-[100px] px-2.5 py-1 bg-[#3A3A3AF5] text-white font-semibold z-40">
{template.layouts.length} {template.layouts.length === 1 ? 'Layout' : 'Layouts'}
</span>
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<div className="p-5">
<div className="grid grid-cols-2 gap-2">
{previewLayouts.map((layout: TemplateWithData, index: number) => {
const LayoutComponent = layout.component;
return (
<div
key={`${template.id}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.12] origin-top-left"
style={{ width: "833.33%", height: "833.33%" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
})}
</div>
</div>
<div className="flex items-center justify-between p-5 bg-white border-t border-[#EDEEEF] relative z-40 ">
<div className="w-[191px]">
<h3 className="text-sm font-bold text-gray-900 capitalize">
{template.name}
</h3>
<p className="text-xs text-gray-600 mb-4 line-clamp-2">
{template.description}
</p>
</div>
<div className="flex items-center gap-2">
<ArrowUpRight className="w-4 h-4 text-gray-400 group-hover:text-blue-600 transition-colors" />
<TemplatePreviewStage>
<LayoutsBadge count={template.layouts.length} />
<InbuiltTemplatePreview layouts={template.layouts} templateId={template.id} />
</TemplatePreviewStage>
<div className="relative z-40 flex items-center justify-between gap-4 border-t border-[#EDEEEF] bg-white px-6 py-5">
<div className="min-w-0 flex-1">
<h3 className="text-base font-bold capitalize text-gray-900">{template.name}</h3>
<p className="mt-1 line-clamp-2 text-sm text-gray-500">{template.description}</p>
</div>
<ArrowUpRight className="h-4 w-4 shrink-0 text-gray-400 transition-colors group-hover:text-blue-600" />
</div>
</Card>
);
@ -254,7 +184,7 @@ const LayoutPreview = () => {
{/* Inbuilt Templates Section: non-neo first, then Report (neo) */}
{tab === 'default' && (
<section className="my-12 space-y-12">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{nonNeoInbuilt.map((template) => (
<InbuiltTemplateCard
key={template.id}
@ -268,7 +198,7 @@ const LayoutPreview = () => {
<h4 className="text-base font-semibold text-[#101828] mb-6 font-syne tracking-tight">
Report
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{neoInbuilt.map((template) => (
<InbuiltTemplateCard
key={template.id}
@ -290,7 +220,7 @@ const LayoutPreview = () => {
<span className="ml-3 text-gray-600">Loading custom templates...</span>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 items-center lg:grid-cols-4 gap-6">
<CreateCustomTemplate />
{customTemplateCards}
</div>

View file

@ -0,0 +1,125 @@
"use client";
import React, { memo, useMemo } from "react";
import { Loader2 } from "lucide-react";
import { TemplateWithData } from "@/app/presentation-templates/utils";
import { CompiledLayout } from "@/app/hooks/compileLayout";
export function TemplatePreviewStage({ children }: { children: React.ReactNode }) {
return (
<div className="relative overflow-hidden px-5 pb-5 pt-5 h-[230px]">
<img
src="/card_bg.svg"
alt=""
className="absolute top-0 left-0 w-full h-full object-cover"
/>
{children}
</div>
);
}
export const LayoutsBadge = memo(function LayoutsBadge({ count }: { count: number }) {
return (
<span className="text-xs font-syne absolute top-3.5 left-4 z-40 inline-flex items-center rounded-full bg-[#333333] px-3 py-1 font-semibold text-white">
Layouts-{count}
</span>
);
});
export const ScaledSlidePreview = memo(function ScaledSlidePreview({
children,
id,
index,
isOutline = false,
}: {
children: React.ReactNode;
id: string;
index: number;
isOutline?: boolean;
}) {
const PREVIEW_SCALE = isOutline ? 0.2 : 0.24;
const SLIDE_HEIGHT = 720 * PREVIEW_SCALE;
const SLIDE_WIDTH = 1280;
const SLIDE_NATIVE_HEIGHT = 720;
return (
<div
key={`${id}-preview-${index}`}
className="relative"
style={{ height: `${SLIDE_HEIGHT}px`, overflow: "hidden" }}
>
<div
className={`absolute top-0 ${isOutline ? "left-0" : "left-8"} pointer-events-none`}
style={{
width: SLIDE_WIDTH,
height: SLIDE_NATIVE_HEIGHT,
transformOrigin: "top left",
transform: `scale(${PREVIEW_SCALE})`,
}}
>
{children}
</div>
</div>
);
});
export const InbuiltTemplatePreview = memo(function InbuiltTemplatePreview({
layouts,
templateId,
isOutline = false,
}: {
layouts: TemplateWithData[];
templateId: string;
isOutline?: boolean;
}) {
const previewLayouts = useMemo(() => layouts.slice(0, 2), [layouts]);
return (
<div className="relative z-10 flex flex-col gap-3 overflow-hidden">
{previewLayouts.map((layout, index) => {
const LayoutComponent = layout.component;
return (
<ScaledSlidePreview key={`${templateId}-preview-${index}`} id={templateId} index={index} isOutline={isOutline}>
<LayoutComponent data={layout.sampleData} />
</ScaledSlidePreview>
);
})}
</div>
);
});
export const CustomTemplatePreview = memo(function CustomTemplatePreview({
previewLayouts,
loading,
templateId,
isOutline = false,
}: {
previewLayouts: CompiledLayout[];
loading: boolean;
templateId: string;
isOutline?: boolean;
}) {
return (
<div className="relative z-10 flex flex-col gap-3">
{loading ? (
[...Array(2)].map((_, index) => (
<div
key={`${templateId}-loading-${index}`}
className="relative w-full aspect-video flex items-center justify-center"
>
<Loader2 className="h-4 w-4 animate-spin text-slate-300" />
</div>
))
) : (
previewLayouts.slice(0, 2).map((layout, index) => {
const LayoutComponent = layout.component;
return (
<ScaledSlidePreview key={`${templateId}-preview-${index}`} id={templateId} index={index} isOutline={isOutline}>
<LayoutComponent data={layout.sampleData} />
</ScaledSlidePreview>
);
})
)}
</div>
);
});

View file

@ -1,94 +1,50 @@
"use client";
import React, { memo } from "react";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { CustomTemplates, useCustomTemplatePreview } from "@/app/hooks/useCustomTemplates";
import { Loader2 } from "lucide-react";
import { CompiledLayout } from "@/app/hooks/compileLayout";
import {
TemplatePreviewStage,
LayoutsBadge,
CustomTemplatePreview,
} from "../../components/TemplatePreviewComponents";
// Memoized preview component to prevent re-renders during scroll
export const LayoutPreview = memo(({ layout, templateId, index }: { layout: CompiledLayout, templateId: string, index: number }) => {
const LayoutComponent = layout.component;
return (
<div
key={`${templateId}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
style={{ contain: 'layout style paint', willChange: 'auto' }}
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]"
style={{ transform: 'scale(0.2) translateZ(0)', backfaceVisibility: 'hidden' }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
});
LayoutPreview.displayName = 'LayoutPreview';
export const CustomTemplateCard = memo(({ template, onSelectTemplate, selectedTemplate }: { template: CustomTemplates, onSelectTemplate: (template: string) => void, selectedTemplate: string | null }) => {
const { previewLayouts, loading: customLoading, totalLayouts } = useCustomTemplatePreview(template.id);
export const CustomTemplateCard = memo(function CustomTemplateCard({
template,
onSelectTemplate,
selectedTemplate,
}: {
template: CustomTemplates;
onSelectTemplate: (template: string) => void;
selectedTemplate: string | null;
}) {
const { previewLayouts, loading, totalLayouts } = useCustomTemplatePreview(template.id);
const isSelected = selectedTemplate === template.id;
return (
<Card
className={`${isSelected ? 'border-2 border-blue-500' : ''} font-syne cursor-pointer flex flex-col justify-between relative hover:shadow-lg transition-all duration-200 group overflow-hidden`}
className={cn(
"font-syne cursor-pointer flex flex-col justify-between relative hover:shadow-sm transition-all duration-200 group overflow-hidden rounded-[22px] bg-white border",
isSelected
? " border-blue-500 ring-2 ring-blue-500/25 shadow-sm"
: " border-[#E8E9EC]"
)}
onClick={() => onSelectTemplate(template.id)}
>
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<span className="text-xs font-syne absolute top-2 flex gap-1 capitalize items-center left-2 rounded-[100px] px-2.5 py-1 bg-[#3A3A3AF5] text-white font-semibold z-40">
Layouts- {totalLayouts}
</span>
<div className="p-5">
{/* Layout previews */}
<div className="grid grid-cols-2 gap-2">
{customLoading ? (
// Loading placeholders
[...Array(Math.min(4, template.layoutCount))].map((_, index) => (
<div
key={`${template.id}-loading-${index}`}
className="relative bg-gradient-to-br from-purple-50 to-blue-50 border border-gray-200 overflow-hidden aspect-video rounded flex items-center justify-center"
>
<Loader2 className="w-4 h-4 text-purple-300 animate-spin" />
</div>
))
) : previewLayouts.length > 0 && (
// Actual layout previews
previewLayouts.slice(0, 4).map((layout: CompiledLayout, index: number) => {
const LayoutComponent = layout.component;
return (
<div
key={`${template.id}-preview-${index}`}
className="relative bg-gray-100 border border-gray-200 overflow-hidden aspect-video rounded"
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.12] origin-top-left"
style={{ width: "833.33%", height: "833.33%" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
})
)}
</div>
</div>
<div className="flex items-center justify-between p-5 bg-white border-t border-[#EDEEEF] relative z-40 ">
<h3 className="text-sm font-bold text-gray-900">
<TemplatePreviewStage>
<LayoutsBadge count={totalLayouts} />
<CustomTemplatePreview
previewLayouts={previewLayouts}
loading={loading}
templateId={template.id}
isOutline={true}
/>
</TemplatePreviewStage>
<div className="flex items-center justify-between px-6 py-5 bg-white border-t border-[#EDEEEF] relative z-40">
<h3 className="text-sm font-bold text-gray-900 font-syne">
{template.name}
</h3>
</div>
</Card>
);
});
CustomTemplateCard.displayName = 'CustomTemplateCard';

View file

@ -4,72 +4,49 @@ import React, { useEffect, useMemo, useCallback, memo } from "react";
import { TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils";
import { templates } from "@/app/presentation-templates";
import { Card } from "@/components/ui/card";
import { TemplateWithData } from "@/app/presentation-templates/utils";
import { cn } from "@/lib/utils";
import { CustomTemplates, useCustomTemplateSummaries } from "@/app/hooks/useCustomTemplates";
import { Loader2 } from "lucide-react";
import { CustomTemplateCard } from "./CustomTemplateCard";
import CreateCustomTemplate from "../../(dashboard)/templates/components/CreateCustomTemplate";
import { CustomTemplateCard } from "./CustomTemplateCard";
import {
TemplatePreviewStage,
LayoutsBadge,
InbuiltTemplatePreview,
} from "../../components/TemplatePreviewComponents";
// Memoized layout preview for built-in templates
const BuiltInLayoutPreview = memo(({ layout, templateId, index }: {
layout: TemplateWithData;
templateId: string;
index: number;
}) => {
const LayoutComponent = layout.component;
return (
<div
className="relative bg-gray-100 font-syne border border-gray-200 overflow-hidden aspect-video rounded"
style={{ contain: 'layout style paint' }}
>
<div className="absolute inset-0 bg-transparent z-10" />
<div
className="transform scale-[0.12] origin-top-left"
style={{ width: "833.33%", height: "833.33%" }}
>
<LayoutComponent data={layout.sampleData} />
</div>
</div>
);
});
BuiltInLayoutPreview.displayName = 'BuiltInLayoutPreview';
// Memoized built-in template card
const BuiltInTemplateCard = memo(({ template, isSelected, onSelect }: {
const BuiltInTemplateCard = memo(function BuiltInTemplateCard({
template,
isSelected,
onSelect,
}: {
template: TemplateLayoutsWithSettings;
isSelected: boolean;
onSelect: (template: TemplateLayoutsWithSettings) => void;
}) => {
const previewLayouts = useMemo(() => template.layouts.slice(0, 4), [template.layouts]);
}) {
const handleClick = useCallback(() => onSelect(template), [onSelect, template]);
return (
<Card
className={`${isSelected ? 'border-2 border-blue-500' : ''} cursor-pointer relative hover:shadow-lg transition-all duration-200 group overflow-hidden`}
className={cn(
"cursor-pointer relative hover:shadow-sm transition-all duration-200 group overflow-hidden rounded-[22px] bg-white border",
isSelected
? " border-blue-500 ring-2 ring-blue-500/25 shadow-sm"
: " border-[#E8E9EC]"
)}
onClick={handleClick}
>
<span className="text-xs font-syne absolute top-2 flex gap-1 capitalize items-center left-2 rounded-[100px] px-2.5 py-1 bg-[#3A3A3AF5] text-white font-semibold z-40">
{template.layouts.length} {template.layouts.length === 1 ? 'Layout' : 'Layouts'}
</span>
<img src="/card_bg.svg" alt="" className="absolute top-0 left-0 w-full h-full object-cover" />
<div className="p-5">
<div className="grid grid-cols-2 gap-2">
{previewLayouts.map((layout: TemplateWithData, index: number) => (
<BuiltInLayoutPreview
key={`${template.id}-preview-${index}`}
layout={layout}
templateId={template.id}
index={index}
/>
))}
</div>
</div>
<div className="flex items-center justify-between p-5 bg-white border-t border-[#EDEEEF] relative z-40">
<div>
<TemplatePreviewStage>
<LayoutsBadge count={template.layouts.length} />
<InbuiltTemplatePreview layouts={template.layouts} templateId={template.id} isOutline={true} />
</TemplatePreviewStage>
<div className="flex items-center justify-between px-6 py-5 bg-white border-t border-[#EDEEEF] relative z-40">
<div className="min-w-0 flex-1">
<h3 className="text-sm font-bold text-gray-900 capitalize font-syne">
{template.name}
</h3>
<p className="text-xs text-gray-600 line-clamp-2 font-syne">
<p className="text-xs text-gray-600 line-clamp-2 font-syne">
{template.description}
</p>
</div>
@ -77,17 +54,16 @@ const BuiltInTemplateCard = memo(({ template, isSelected, onSelect }: {
</Card>
);
});
BuiltInTemplateCard.displayName = 'BuiltInTemplateCard';
interface TemplateSelectionProps {
selectedTemplate: (TemplateLayoutsWithSettings | string) | null;
onSelectTemplate: (template: TemplateLayoutsWithSettings | string) => void;
}
const TemplateSelection: React.FC<TemplateSelectionProps> = memo(({
const TemplateSelection: React.FC<TemplateSelectionProps> = memo(function TemplateSelection({
selectedTemplate,
onSelectTemplate
}) => {
onSelectTemplate,
}) {
useEffect(() => {
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
@ -102,50 +78,44 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = memo(({
const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries();
// Stable callback for custom template selection
const handleCustomSelect = useCallback(
(template: TemplateLayoutsWithSettings | string) => onSelectTemplate(template),
[onSelectTemplate]
);
// Stable callback for built-in template selection
const handleBuiltInSelect = useCallback(
(template: TemplateLayoutsWithSettings) => onSelectTemplate(template),
[onSelectTemplate]
);
// Derive the selected custom template id only when selectedTemplate changes
const selectedCustomId = useMemo(
() => (typeof selectedTemplate === 'string' ? selectedTemplate : null),
() => (typeof selectedTemplate === "string" ? selectedTemplate : null),
[selectedTemplate]
);
// Derive the selected built-in template id only when selectedTemplate changes
const selectedBuiltInId = useMemo(
() => (typeof selectedTemplate !== 'string' ? selectedTemplate?.id ?? null : null),
() => (typeof selectedTemplate !== "string" ? selectedTemplate?.id ?? null : null),
[selectedTemplate]
);
// Memoize the custom templates section
const customTemplateCards = useMemo(() => {
if (customLoading) {
return (
<div className="flex items-center justify-center py-12 font-syne">
<Loader2 className="w-8 h-8 animate-spin text-blue-600 font-syne" />
<Loader2 className="w-8 h-8 animate-spin text-blue-600" />
<span className="ml-3 text-gray-600">Loading custom templates...</span>
</div>
);
}
if (customTemplates.length === 0) {
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
<CreateCustomTemplate />
</div>
);
}
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{customTemplates.map((template: CustomTemplates) => (
<CustomTemplateCard
key={template.id}
@ -158,7 +128,6 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = memo(({
);
}, [customLoading, customTemplates, handleCustomSelect, selectedCustomId]);
// Memoize the built-in templates list
const builtInTemplateCards = useMemo(
() =>
templates.map((template: TemplateLayoutsWithSettings) => (
@ -174,23 +143,20 @@ const TemplateSelection: React.FC<TemplateSelectionProps> = memo(({
return (
<div className="space-y-[30px] mb-4">
{/* Custom AI Templates */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-base font-semibold text-gray-900 font-syne">Custom</h3>
</div>
{customTemplateCards}
</div>
{/* In Built Templates */}
<div>
<h3 className="text-base font-semibold text-gray-900 mb-3 font-syne">In Built</h3>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{builtInTemplateCards}
</div>
</div>
</div>
);
});
TemplateSelection.displayName = 'TemplateSelection';
export default TemplateSelection;

View file

@ -73,14 +73,7 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick }: Sor
<V1ContentRender slide={slide} isEditMode={true} />
</div>
</div>
{/* <div className=" slide-box relative z-50 overflow-hidden aspect-video">
<div className="absolute bg-transparent z-50 top-0 left-0 w-full h-full" />
<div className="transform scale-[0.2] flex pointer-events-none justify-center items-center origin-top-left w-[500%] h-[500%]"
>
<ContentRender slide={slide} isEditMode={true} />
</div>
</div> */}
</div>
);
}

View file

@ -403,7 +403,7 @@ export function useCustomTemplatePreview(presentationId: string) {
setTotalLayouts(data.layouts.length);
// Compile first 4 layouts for preview
const compiled: CompiledLayout[] = [];
const layoutsToPreview = data.layouts.slice(0, 4);
const layoutsToPreview = data.layouts.slice(0, 2);
for (const layout of layoutsToPreview) {
try {

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256px" height="256px" viewBox="0 0 256 256" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 76.753906 52.871094 C 77.4375 52.867188 77.4375 52.867188 78.136719 52.863281 C 79.65625 52.859375 81.175781 52.859375 82.695312 52.863281 C 83.792969 52.859375 84.886719 52.859375 85.980469 52.855469 C 88.945312 52.847656 91.910156 52.847656 94.875 52.847656 C 97.355469 52.851562 99.832031 52.847656 102.3125 52.84375 C 108.496094 52.839844 114.679688 52.839844 120.863281 52.84375 C 125.890625 52.84375 130.917969 52.839844 135.941406 52.832031 C 141.789062 52.820312 147.640625 52.816406 153.488281 52.816406 C 156.578125 52.820312 159.667969 52.816406 162.761719 52.8125 C 165.667969 52.804688 168.578125 52.804688 171.488281 52.8125 C 172.550781 52.8125 173.613281 52.8125 174.679688 52.808594 C 183.050781 52.777344 190.167969 53.09375 196.644531 59.050781 C 201.476562 64.164062 203.160156 69.855469 203.128906 76.753906 C 203.132812 77.210938 203.132812 77.667969 203.136719 78.136719 C 203.140625 79.65625 203.140625 81.175781 203.136719 82.695312 C 203.140625 83.792969 203.140625 84.886719 203.144531 85.980469 C 203.152344 88.945312 203.152344 91.910156 203.152344 94.875 C 203.148438 97.355469 203.152344 99.832031 203.15625 102.3125 C 203.160156 108.496094 203.160156 114.679688 203.15625 120.863281 C 203.15625 125.890625 203.160156 130.917969 203.167969 135.941406 C 203.179688 141.789062 203.183594 147.640625 203.183594 153.488281 C 203.179688 156.578125 203.183594 159.667969 203.1875 162.761719 C 203.195312 165.667969 203.195312 168.578125 203.1875 171.488281 C 203.1875 172.550781 203.1875 173.613281 203.191406 174.679688 C 203.222656 183.050781 202.90625 190.167969 196.945312 196.644531 C 191.835938 201.476562 186.144531 203.160156 179.242188 203.128906 C 178.558594 203.132812 178.558594 203.132812 177.859375 203.136719 C 176.335938 203.140625 174.816406 203.140625 173.296875 203.136719 C 172.199219 203.140625 171.105469 203.140625 170.007812 203.144531 C 167.042969 203.152344 164.074219 203.152344 161.109375 203.152344 C 158.628906 203.148438 156.148438 203.152344 153.667969 203.15625 C 147.480469 203.160156 141.296875 203.160156 135.109375 203.15625 C 130.078125 203.15625 125.050781 203.160156 120.019531 203.167969 C 114.167969 203.179688 108.316406 203.183594 102.464844 203.183594 C 99.371094 203.179688 96.28125 203.183594 93.1875 203.1875 C 90.277344 203.195312 87.367188 203.195312 84.457031 203.1875 C 83.390625 203.1875 82.328125 203.1875 81.261719 203.191406 C 70.902344 203.230469 70.902344 203.230469 66.5 201.5 C 66.140625 201.359375 65.777344 201.21875 65.40625 201.078125 C 60.472656 199.027344 56.71875 194.75 54.5 190 C 52.453125 184.570312 52.789062 178.835938 52.804688 173.132812 C 52.804688 172.039062 52.800781 170.945312 52.796875 169.847656 C 52.792969 166.890625 52.792969 163.933594 52.796875 160.972656 C 52.800781 157.867188 52.796875 154.765625 52.796875 151.660156 C 52.792969 146.117188 52.796875 140.570312 52.804688 135.027344 C 52.8125 130.007812 52.8125 124.992188 52.804688 119.972656 C 52.796875 114.132812 52.792969 108.292969 52.796875 102.453125 C 52.800781 99.367188 52.800781 96.28125 52.796875 93.191406 C 52.792969 90.289062 52.792969 87.386719 52.804688 84.484375 C 52.804688 83.425781 52.804688 82.363281 52.800781 81.300781 C 52.777344 72.941406 53.101562 65.824219 59.050781 59.355469 C 64.164062 54.523438 69.855469 52.839844 76.753906 52.871094 Z M 75 75 C 75 109.980469 75 144.960938 75 181 C 109.980469 181 144.960938 181 181 181 C 181 146.019531 181 111.039062 181 75 C 146.019531 75 111.039062 75 75 75 Z M 75 75 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 96 96 C 117.121094 96 138.238281 96 160 96 C 160 117.121094 160 138.238281 160 160 C 138.878906 160 117.761719 160 96 160 C 96 138.878906 96 117.761719 96 96 Z M 96 96 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4 KiB