mirror of
https://github.com/pheralb/svgl.git
synced 2025-12-29 08:01:36 +08:00
57bdd6920f
The optimize parameter was missing from the API request payload. The API endpoint defaulted to optimization enabled because `body.optimize !== false` (svgr/+server.ts:L18) evaluates to `true` when `undefined`, removing gradients even when users disabled optimization.
590 lines
16 KiB
Svelte
590 lines
16 KiB
Svelte
<script lang="ts">
|
|
import type { iSVG } from "@/types/svg";
|
|
|
|
// Utils:
|
|
import { cn } from "@/utils/cn";
|
|
import { clipboard } from "@/utils/clipboard";
|
|
import { getPrefixFromSvgUrl, prefixSvgIds } from "@/utils/prefixSvgIds";
|
|
import { copyToClipboard as figmaCopyToClipboard } from "@/figma/copy-to-clipboard";
|
|
import { settingsStore } from "@/stores/settings.store";
|
|
|
|
// Icons:
|
|
import XIcon from "@lucide/svelte/icons/x";
|
|
import CopyIcon from "@lucide/svelte/icons/copy";
|
|
import LoaderIcon from "@lucide/svelte/icons/loader";
|
|
import ClipboardIcon from "@lucide/svelte/icons/clipboard";
|
|
|
|
// UI Components:
|
|
import { toast } from "svelte-sonner";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import * as Tabs from "@/components/ui/tabs";
|
|
import { Button, buttonVariants } from "@/components/ui/button";
|
|
import * as Popover from "@/components/ui/popover";
|
|
|
|
// CLIs:
|
|
import CopyShadcnCommand from "@/components/svgs/copyShadcnCommand.svelte";
|
|
|
|
// Templates:
|
|
import { getSource } from "@/templates/getSource";
|
|
import { getVueCode } from "@/templates/getVueCode";
|
|
import { getReactCode } from "@/templates/getReactCode";
|
|
import { getAstroCode } from "@/templates/getAstroCode";
|
|
import { getSvelteCode } from "@/templates/getSvelteCode";
|
|
import { getAngularCode } from "@/templates/getAngularCode";
|
|
import { getWebComponent } from "@/templates/getWebComponent";
|
|
|
|
// SVGs:
|
|
import Vue from "@/components/logos/vue.svelte";
|
|
import React from "@/components/logos/react.svelte";
|
|
import Astro from "@/components/logos/astro.svelte";
|
|
import Svelte from "@/components/logos/svelte.svelte";
|
|
import Shadcn from "@/components/logos/shadcn.svelte";
|
|
import Angular from "@/components/logos/angular.svelte";
|
|
import WebComponents from "@/components/logos/webComponents.svelte";
|
|
|
|
// Props:
|
|
interface Props {
|
|
size?: number;
|
|
iconStroke?: number;
|
|
isInFigma?: boolean;
|
|
isWordmarkSvg?: boolean;
|
|
svgInfo: iSVG;
|
|
}
|
|
|
|
let {
|
|
size = 24,
|
|
iconStroke = 2,
|
|
isInFigma = false,
|
|
isWordmarkSvg = false,
|
|
svgInfo,
|
|
}: Props = $props();
|
|
|
|
// States:
|
|
let optionsOpen = $state<boolean>(false);
|
|
let isLoading = $state<boolean>(false);
|
|
let optimize = $derived($settingsStore.optimizeSvgs);
|
|
|
|
const getSvgUrl = () => {
|
|
let svgUrlToCopy;
|
|
const dark = document.documentElement.classList.contains("dark");
|
|
|
|
if (isWordmarkSvg) {
|
|
const svgHasTheme = typeof svgInfo.wordmark !== "string";
|
|
if (!svgHasTheme) {
|
|
svgUrlToCopy =
|
|
typeof svgInfo.wordmark === "string"
|
|
? svgInfo.wordmark
|
|
: "Something went wrong. Couldn't copy the SVG.";
|
|
}
|
|
|
|
svgUrlToCopy =
|
|
typeof svgInfo.wordmark !== "string"
|
|
? dark
|
|
? svgInfo.wordmark?.dark
|
|
: svgInfo.wordmark?.light
|
|
: svgInfo.wordmark;
|
|
} else {
|
|
const svgHasTheme = typeof svgInfo.route !== "string";
|
|
if (!svgHasTheme) {
|
|
svgUrlToCopy =
|
|
typeof svgInfo.route === "string"
|
|
? svgInfo.route
|
|
: "Something went wrong. Couldn't copy the SVG.";
|
|
}
|
|
svgUrlToCopy =
|
|
typeof svgInfo.route !== "string"
|
|
? dark
|
|
? svgInfo.route.dark
|
|
: svgInfo.route.light
|
|
: svgInfo.route;
|
|
}
|
|
|
|
return svgUrlToCopy;
|
|
};
|
|
|
|
// Copy SVG to clipboard:
|
|
const copyToClipboard = async () => {
|
|
const svgUrlToCopy = getSvgUrl();
|
|
optionsOpen = false;
|
|
|
|
let content = await getSource({
|
|
url: svgUrlToCopy,
|
|
optimize,
|
|
});
|
|
|
|
if (svgUrlToCopy) {
|
|
content = prefixSvgIds(content, getPrefixFromSvgUrl(svgUrlToCopy));
|
|
}
|
|
|
|
if (isInFigma) {
|
|
figmaCopyToClipboard(content);
|
|
}
|
|
|
|
await clipboard(content);
|
|
|
|
const category = Array.isArray(svgInfo.category)
|
|
? svgInfo.category.sort().join(" - ")
|
|
: svgInfo.category;
|
|
|
|
if (isInFigma) {
|
|
toast.success("Ready to paste in Figma", {
|
|
description: `${svgInfo.title} - ${category}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (isWordmarkSvg) {
|
|
toast.success("Copied wordmark SVG to clipboard", {
|
|
description: `${svgInfo.title} - ${category}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
toast.success("Copied to clipboard", {
|
|
description: `${svgInfo.title} - ${category}`,
|
|
});
|
|
};
|
|
|
|
// Convert SVG as React component:
|
|
const convertSvgReactComponent = async (tsx: boolean) => {
|
|
const svgUrlToCopy = getSvgUrl();
|
|
optionsOpen = false;
|
|
|
|
isLoading = true;
|
|
|
|
const title = svgInfo.title.split(" ").join("");
|
|
let content = await getSource({
|
|
url: svgUrlToCopy,
|
|
optimize,
|
|
});
|
|
|
|
if (svgUrlToCopy) {
|
|
content = prefixSvgIds(content, getPrefixFromSvgUrl(svgUrlToCopy));
|
|
}
|
|
|
|
const dataComponent = {
|
|
code: content,
|
|
typescript: tsx,
|
|
name: title,
|
|
optimize,
|
|
};
|
|
const { data, error } = await getReactCode(dataComponent);
|
|
|
|
if (error || !data) {
|
|
toast.error("Failed to fetch React component", {
|
|
description: `${error ?? ""}`,
|
|
duration: 5000,
|
|
});
|
|
isLoading = false;
|
|
return;
|
|
}
|
|
|
|
await clipboard(data);
|
|
|
|
toast.success(`Copied as React ${tsx ? "TSX" : "JSX"} component`, {
|
|
description: `${svgInfo.title} - ${svgInfo.category}`,
|
|
});
|
|
|
|
isLoading = false;
|
|
};
|
|
|
|
// Copy SVG as Vue Component:
|
|
const convertSvgVueComponent = async (ts: boolean) => {
|
|
try {
|
|
const svgUrlToCopy = getSvgUrl();
|
|
|
|
optionsOpen = false;
|
|
|
|
let content = await getSource({
|
|
url: svgUrlToCopy,
|
|
optimize,
|
|
});
|
|
|
|
if (svgUrlToCopy) {
|
|
content = prefixSvgIds(content, getPrefixFromSvgUrl(svgUrlToCopy));
|
|
}
|
|
|
|
const copyCode = getVueCode({
|
|
content: content,
|
|
lang: ts ? "ts" : "js",
|
|
});
|
|
|
|
if (copyCode) {
|
|
await clipboard(copyCode);
|
|
}
|
|
|
|
const category = Array.isArray(svgInfo?.category)
|
|
? svgInfo.category.sort().join(" - ")
|
|
: svgInfo.category;
|
|
|
|
toast.success(`Copied as Vue ${ts ? "TS" : "JS"} component`, {
|
|
description: `${svgInfo?.title} - ${category}`,
|
|
});
|
|
} catch (err) {
|
|
console.error(`Error copying Vue component:`, err);
|
|
toast.error(`Failed to copy Vue component`);
|
|
}
|
|
};
|
|
|
|
// Copy SVG as Svelte Component:
|
|
const convertSvgSvelteComponent = async (ts: boolean) => {
|
|
try {
|
|
const svgUrlToCopy = getSvgUrl();
|
|
|
|
optionsOpen = false;
|
|
|
|
let content = await getSource({
|
|
url: svgUrlToCopy,
|
|
optimize,
|
|
});
|
|
|
|
if (svgUrlToCopy) {
|
|
content = prefixSvgIds(content, getPrefixFromSvgUrl(svgUrlToCopy));
|
|
}
|
|
|
|
const copyCode = getSvelteCode({
|
|
content: content,
|
|
lang: ts ? "ts" : "js",
|
|
});
|
|
|
|
if (copyCode) {
|
|
await clipboard(copyCode);
|
|
}
|
|
|
|
const category = Array.isArray(svgInfo?.category)
|
|
? svgInfo.category.sort().join(" - ")
|
|
: svgInfo.category;
|
|
|
|
toast.success(`Copied as Svelte ${ts ? "TS" : "JS"} component`, {
|
|
description: `${svgInfo?.title} - ${category}`,
|
|
});
|
|
} catch (err) {
|
|
console.error(`Error copying Svelte component:`, err);
|
|
toast.error(`Failed to copy Svelte component`);
|
|
}
|
|
};
|
|
|
|
// Copy SVG as Standalone Angular component:
|
|
const convertSvgAngularComponent = async () => {
|
|
isLoading = true;
|
|
optionsOpen = false;
|
|
|
|
const title = svgInfo.title.split(" ").join("");
|
|
const svgUrlToCopy = getSvgUrl();
|
|
let content = await getSource({
|
|
url: svgUrlToCopy,
|
|
optimize,
|
|
});
|
|
|
|
if (svgUrlToCopy) {
|
|
content = prefixSvgIds(content, getPrefixFromSvgUrl(svgUrlToCopy));
|
|
}
|
|
|
|
if (!content) {
|
|
toast.error("Failed to fetch the SVG content", {
|
|
duration: 5000,
|
|
});
|
|
isLoading = false;
|
|
return;
|
|
}
|
|
|
|
const angularComponent = getAngularCode({
|
|
componentName: title,
|
|
svgContent: content,
|
|
});
|
|
|
|
await clipboard(angularComponent);
|
|
|
|
toast.success(`Copied as Standalone Angular component`, {
|
|
description: `${svgInfo.title} - ${svgInfo.category}`,
|
|
});
|
|
|
|
isLoading = false;
|
|
};
|
|
|
|
// Copy SVG as Web Component:
|
|
const convertSvgWebComponent = async () => {
|
|
isLoading = true;
|
|
optionsOpen = false;
|
|
|
|
const title = svgInfo.title.split(" ").join("");
|
|
const svgUrlToCopy = getSvgUrl();
|
|
let content = await getSource({
|
|
url: svgUrlToCopy,
|
|
optimize,
|
|
});
|
|
|
|
if (svgUrlToCopy) {
|
|
content = prefixSvgIds(content, getPrefixFromSvgUrl(svgUrlToCopy));
|
|
}
|
|
|
|
if (!content) {
|
|
toast.error("Failed to fetch the SVG content", {
|
|
duration: 5000,
|
|
});
|
|
isLoading = false;
|
|
return;
|
|
}
|
|
|
|
const webComponentCode = getWebComponent({
|
|
name: title,
|
|
content: content,
|
|
});
|
|
|
|
await clipboard(webComponentCode);
|
|
|
|
toast.success(`Copied as Web Component`, {
|
|
description: `${svgInfo.title} - ${svgInfo.category}`,
|
|
});
|
|
|
|
isLoading = false;
|
|
};
|
|
|
|
// Copy SVG as Astro component:
|
|
const convertSvgAstroComponent = async () => {
|
|
isLoading = true;
|
|
optionsOpen = false;
|
|
|
|
const svgUrlToCopy = getSvgUrl();
|
|
let content = await getSource({
|
|
url: svgUrlToCopy,
|
|
optimize,
|
|
});
|
|
|
|
if (svgUrlToCopy) {
|
|
content = prefixSvgIds(content, getPrefixFromSvgUrl(svgUrlToCopy));
|
|
}
|
|
|
|
if (!content) {
|
|
toast.error("Failed to fetch the SVG content", {
|
|
duration: 5000,
|
|
});
|
|
isLoading = false;
|
|
return;
|
|
}
|
|
|
|
const astroComponentCode = getAstroCode({
|
|
svgContent: content,
|
|
});
|
|
|
|
await clipboard(astroComponentCode);
|
|
|
|
toast.success(`Copied as Astro Component`, {
|
|
description: `${svgInfo.title} - ${svgInfo.category}`,
|
|
});
|
|
|
|
isLoading = false;
|
|
};
|
|
</script>
|
|
|
|
<Popover.Root bind:open={optionsOpen}>
|
|
<Popover.Trigger
|
|
title="Copy SVG element as svg file, React TSX code, or React JSX code"
|
|
class={cn(
|
|
buttonVariants({
|
|
variant: "ghost",
|
|
size: "icon",
|
|
class: "hover:bg-neutral-200",
|
|
}),
|
|
)}
|
|
>
|
|
{#if optionsOpen}
|
|
<XIcon {size} strokeWidth={iconStroke} />
|
|
{:else if isLoading}
|
|
<LoaderIcon {size} strokeWidth={iconStroke} class="animate-spin" />
|
|
{:else}
|
|
<CopyIcon {size} strokeWidth={iconStroke} />
|
|
{/if}
|
|
</Popover.Trigger>
|
|
<Popover.Content class="flex flex-col space-y-2 p-4" sideOffset={2}>
|
|
<Tabs.Root value="source" class="flex w-full flex-col space-y-1">
|
|
<Tabs.List
|
|
class="flex h-auto w-auto flex-col space-y-2 space-x-0 border-0 md:h-9 md:flex-row md:space-y-0 md:space-x-1"
|
|
>
|
|
<div class="flex items-center space-x-1">
|
|
<Tabs.Trigger value="source">Source</Tabs.Trigger>
|
|
<Tabs.Trigger value="shadcn" title="shadcn/ui">
|
|
<Shadcn size={18} />
|
|
</Tabs.Trigger>
|
|
</div>
|
|
<div class="flex items-center space-x-1">
|
|
<Tabs.Trigger value="web-component" title="Web Component">
|
|
<WebComponents size={21} />
|
|
</Tabs.Trigger>
|
|
<Tabs.Trigger value="react" title="React">
|
|
<React size={20} />
|
|
</Tabs.Trigger>
|
|
<Tabs.Trigger value="vue" title="Vue">
|
|
<Vue size={20} />
|
|
</Tabs.Trigger>
|
|
<Tabs.Trigger value="svelte" title="Svelte">
|
|
<Svelte size={20} />
|
|
</Tabs.Trigger>
|
|
<Tabs.Trigger value="angular" title="Angular">
|
|
<Angular size={20} />
|
|
</Tabs.Trigger>
|
|
<Tabs.Trigger
|
|
value="astro"
|
|
title="Astro"
|
|
class="text-black dark:text-white"
|
|
>
|
|
<Astro size={21} />
|
|
</Tabs.Trigger>
|
|
</div>
|
|
</Tabs.List>
|
|
<Separator class="block md:hidden" />
|
|
<!-- Source -->
|
|
<Tabs.Content value="source">
|
|
<section class="flex flex-col space-y-2">
|
|
<Button
|
|
variant="outline"
|
|
class="justify-start"
|
|
title={isWordmarkSvg
|
|
? "Copy wordmark SVG to clipboard"
|
|
: "Copy SVG to clipboard"}
|
|
onclick={() => copyToClipboard()}
|
|
>
|
|
<ClipboardIcon size={16} strokeWidth={2} />
|
|
<span>Copy SVG</span>
|
|
</Button>
|
|
</section>
|
|
</Tabs.Content>
|
|
<!-- CLI -->
|
|
<Tabs.Content value="shadcn">
|
|
<section class="flex flex-col space-y-2">
|
|
<CopyShadcnCommand svgTitle={svgInfo.title} />
|
|
</section>
|
|
</Tabs.Content>
|
|
<!-- React -->
|
|
<Tabs.Content value="react">
|
|
<section class="flex flex-col space-y-2">
|
|
<Button
|
|
variant="outline"
|
|
class="justify-start"
|
|
title="Copy as React component"
|
|
disabled={isLoading}
|
|
onclick={() => convertSvgReactComponent(true)}
|
|
>
|
|
<React size={18} />
|
|
<span>Copy TSX</span>
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
class="justify-start"
|
|
title="Copy as React component"
|
|
disabled={isLoading}
|
|
onclick={() => convertSvgReactComponent(false)}
|
|
>
|
|
<React size={18} />
|
|
<span>Copy JSX</span>
|
|
</Button>
|
|
</section>
|
|
</Tabs.Content>
|
|
<!-- Svelte -->
|
|
<Tabs.Content value="svelte">
|
|
<section class="flex flex-col space-y-2">
|
|
<Button
|
|
variant="outline"
|
|
class="justify-start"
|
|
title="Copy as Svelte component"
|
|
disabled={isLoading}
|
|
onclick={() => convertSvgSvelteComponent(false)}
|
|
>
|
|
<Svelte size={18} />
|
|
<span>Copy JS</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
class="justify-start"
|
|
title="Copy as Svelte component"
|
|
disabled={isLoading}
|
|
onclick={() => convertSvgSvelteComponent(true)}
|
|
>
|
|
<Svelte size={18} />
|
|
<span>Copy TS</span>
|
|
</Button>
|
|
</section>
|
|
</Tabs.Content>
|
|
<!-- Vue -->
|
|
<Tabs.Content value="vue">
|
|
<section class="flex flex-col space-y-2">
|
|
<Button
|
|
variant="outline"
|
|
class="justify-start"
|
|
title="Copy as Vue component"
|
|
disabled={isLoading}
|
|
onclick={() => convertSvgVueComponent(false)}
|
|
>
|
|
<Vue size={18} />
|
|
<span>Copy JS</span>
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
class="justify-start"
|
|
title="Copy as Vue component"
|
|
disabled={isLoading}
|
|
onclick={() => convertSvgVueComponent(true)}
|
|
>
|
|
<Vue size={18} />
|
|
<span>Copy TS</span>
|
|
</Button>
|
|
</section>
|
|
</Tabs.Content>
|
|
<!-- Angular -->
|
|
<Tabs.Content value="angular">
|
|
<section class="flex flex-col space-y-2">
|
|
<Button
|
|
variant="outline"
|
|
class="justify-start"
|
|
title="Copy as Standalone Component"
|
|
disabled={isLoading}
|
|
onclick={() => convertSvgAngularComponent()}
|
|
>
|
|
<Angular size={18} />
|
|
<span>Copy Standalone Component</span>
|
|
</Button>
|
|
</section>
|
|
</Tabs.Content>
|
|
<!-- Web Component -->
|
|
<Tabs.Content value="web-component">
|
|
<section class="flex flex-col space-y-2">
|
|
<Button
|
|
variant="outline"
|
|
class="justify-start"
|
|
title="Copy as Web Component"
|
|
disabled={isLoading}
|
|
onclick={() => convertSvgWebComponent()}
|
|
>
|
|
<WebComponents size={18} />
|
|
<span>Copy Web Component</span>
|
|
</Button>
|
|
</section>
|
|
</Tabs.Content>
|
|
<!-- Astro -->
|
|
<Tabs.Content value="astro">
|
|
<section class="flex flex-col space-y-2">
|
|
<Button
|
|
variant="outline"
|
|
class="justify-start"
|
|
title="Copy as Astro Component"
|
|
disabled={isLoading}
|
|
onclick={() => convertSvgAstroComponent()}
|
|
>
|
|
<Astro size={18} />
|
|
<span>Copy Astro Component</span>
|
|
</Button>
|
|
</section>
|
|
</Tabs.Content>
|
|
</Tabs.Root>
|
|
<div
|
|
class="mt-1 flex w-full items-center text-center text-[12px] text-neutral-600 dark:text-neutral-400"
|
|
>
|
|
<p>
|
|
Please ensure you have permission from the creators before using the
|
|
SVG. Modifications are not permitted.
|
|
</p>
|
|
</div>
|
|
</Popover.Content>
|
|
</Popover.Root>
|