🚀 Add new /templates utility + add support for web components

This commit is contained in:
pheralb
2025-02-26 16:59:52 +00:00
parent 63d2416274
commit dc22285088
14 changed files with 290 additions and 91 deletions
+126 -26
View File
@@ -6,22 +6,27 @@
import * as Popover from '@/ui/popover';
import * as Tabs from '@/ui/tabs';
import { buttonStyles } from '@/ui/styles';
// Utils:
import { getSvgContent } from '@/utils/getSvgContent';
import { getReactComponentCode } from '@/utils/getReactComponentCode';
import { cn } from '@/utils/cn';
import { clipboard } from '@/utils/clipboard';
import { copyToClipboard as figmaCopyToClipboard } from '@/figma/copy-to-clipboard';
import { buttonStyles } from '@/ui/styles';
import { cn } from '@/utils/cn';
import { componentTemplate } from '@/utils/componentTemplate';
import { generateAngularComponent } from '@/utils/generateAngularComponent';
// Templates:
import { getSource } from '@/templates/getSource';
import { getReactCode } from '@/templates/getReactCode';
import { getVueCode } from '@/templates/getVueCode';
import { getSvelteCode } from '@/templates/getSvelteCode';
import { getAngularCode } from '@/templates/getAngularCode';
import { getWebComponent } from '@/templates/getWebComponent';
//Icons:
import ReactIcon from './icons/reactIcon.svelte';
import VueIcon from './icons/vueIcon.svelte';
import SvelteIcon from './icons/svelteIcon.svelte';
import AngularIcon from './icons/angularIcon.svelte';
import ReactIcon from '@/components/icons/reactIcon.svelte';
import VueIcon from '@/components/icons/vueIcon.svelte';
import SvelteIcon from '@/components/icons/svelteIcon.svelte';
import AngularIcon from '@/components/icons/angularIcon.svelte';
import WebComponentIcon from '@/components/icons/webComponentIcon.svelte';
// Props:
export let iconSize = 24;
@@ -77,7 +82,9 @@
const svgUrlToCopy = getSvgUrl();
optionsOpen = false;
const content = await getSvgContent(svgUrlToCopy);
const content = await getSource({
url: svgUrlToCopy
});
if (isInFigma) {
figmaCopyToClipboard(content);
@@ -116,9 +123,11 @@
isLoading = true;
const title = svgInfo.title.split(' ').join('');
const content = await getSvgContent(svgUrlToCopy);
const content = await getSource({
url: svgUrlToCopy
});
const dataComponent = { code: content, typescript: tsx, name: title };
const { data, error } = await getReactComponentCode(dataComponent);
const { data, error } = await getReactCode(dataComponent);
if (error || !data) {
toast.error('Failed to fetch React component', {
@@ -138,16 +147,21 @@
isLoading = false;
};
// Copy as either Vue or Svelte component:
const copySvgComponent = async (ts: boolean, framework: 'Vue' | 'Svelte') => {
// Copy SVG as Vue Component:
const convertSvgVueComponent = async (ts: boolean) => {
try {
const svgUrlToCopy = getSvgUrl();
optionsOpen = false;
const content = await getSvgContent(svgUrlToCopy);
const content = await getSource({
url: svgUrlToCopy
});
const copyCode = componentTemplate(ts ? 'ts' : '', content, framework);
const copyCode = getVueCode({
content: content,
lang: ts ? 'ts' : 'js'
});
if (copyCode) {
await clipboard(copyCode);
@@ -157,12 +171,45 @@
? svgInfo.category.sort().join(' - ')
: svgInfo.category;
toast.success(`Copied as ${framework} ${ts ? 'TS' : 'JS'} component`, {
toast.success(`Copied as Vue ${ts ? 'TS' : 'JS'} component`, {
description: `${svgInfo?.title} - ${category}`
});
} catch (err) {
console.error(`Error copying ${framework} component:`, err);
toast.error(`Failed to copy ${framework} component`);
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;
const content = await getSource({
url: 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`);
}
};
@@ -173,7 +220,9 @@
const title = svgInfo.title.split(' ').join('');
const svgUrlToCopy = getSvgUrl();
const content = await getSvgContent(svgUrlToCopy);
const content = await getSource({
url: svgUrlToCopy
});
if (!content) {
toast.error('Failed to fetch the SVG content', {
@@ -183,7 +232,11 @@
return;
}
const angularComponent = generateAngularComponent(content, title);
const angularComponent = getAngularCode({
componentName: title,
svgContent: content
});
await clipboard(angularComponent);
toast.success(`Copied as Standalone Angular component`, {
@@ -192,6 +245,39 @@
isLoading = false;
};
// Copy SVG as Standalone Angular component:
const convertSvgWebComponent = async () => {
isLoading = true;
optionsOpen = false;
const title = svgInfo.title.split(' ').join('');
const svgUrlToCopy = getSvgUrl();
const content = await getSource({
url: 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;
};
</script>
<Popover.Root open={optionsOpen} onOpenChange={(isOpen) => (optionsOpen = isOpen)}>
@@ -215,6 +301,7 @@
<Tabs.Trigger value="vue">Vue</Tabs.Trigger>
<Tabs.Trigger value="svelte">Svelte</Tabs.Trigger>
<Tabs.Trigger value="angular">Angular</Tabs.Trigger>
<Tabs.Trigger value="web-component">Web Component</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="source">
<section class="flex flex-col space-y-2">
@@ -256,7 +343,7 @@
class={cn(buttonStyles, 'w-full rounded-md')}
title="Copy as Svelte component"
disabled={isLoading}
on:click={() => copySvgComponent(false, 'Svelte')}
on:click={() => convertSvgSvelteComponent(false)}
>
<SvelteIcon iconSize={18} />
<span>Copy JS</span>
@@ -266,7 +353,7 @@
class={cn(buttonStyles, 'w-full rounded-md')}
title="Copy as Svelte component"
disabled={isLoading}
on:click={() => copySvgComponent(true, 'Svelte')}
on:click={() => convertSvgSvelteComponent(true)}
>
<SvelteIcon iconSize={18} />
<span>Copy TS</span>
@@ -279,7 +366,7 @@
class={cn(buttonStyles, 'w-full rounded-md')}
title="Copy as Vue component"
disabled={isLoading}
on:click={() => copySvgComponent(false, 'Vue')}
on:click={() => convertSvgVueComponent(false)}
>
<VueIcon iconSize={18} />
<span>Copy JS</span>
@@ -288,7 +375,7 @@
class={cn(buttonStyles, 'w-full rounded-md')}
title="Copy as Vue component"
disabled={isLoading}
on:click={() => copySvgComponent(true, 'Vue')}
on:click={() => convertSvgVueComponent(true)}
>
<VueIcon iconSize={18} />
<span>Copy TS</span>
@@ -308,6 +395,19 @@
</button>
</section>
</Tabs.Content>
<Tabs.Content value="web-component">
<section class="flex flex-col space-y-2">
<button
class={cn(buttonStyles, 'w-full rounded-md')}
title="Copy as Web Component"
disabled={isLoading}
on:click={() => convertSvgWebComponent()}
>
<WebComponentIcon iconSize={18} />
<span>Copy Web 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"
+8 -3
View File
@@ -4,7 +4,8 @@
import download from 'downloadjs';
import { toast } from 'svelte-sonner';
import { DownloadIcon } from 'lucide-svelte';
import { getSvgContent } from '@/utils/getSvgContent';
import { getSource } from '@/templates/getSource';
import {
Dialog,
DialogTrigger,
@@ -54,8 +55,12 @@
}) => {
const zip = new JSZip();
const lightSvg = await getSvgContent(lightRoute);
const darkSvg = await getSvgContent(darkRoute);
const lightSvg = await getSource({
url: lightRoute
});
const darkSvg = await getSource({
url: darkRoute
});
if (isWordmark) {
zip.file(`${svgInfo.title}_wordmark_light.svg`, lightSvg);
+5 -2
View File
@@ -6,13 +6,16 @@
import Logo from './icons/logo.svelte';
import { clipboard } from '@/utils/clipboard';
import { getSvgContent } from '@/utils/getSvgContent';
import GithubIcon from './icons/githubIcon.svelte';
import { getSource } from '@/templates/getSource';
const logoUrl = '/library/svgl.svg';
const copyToClipboard = async () => {
const content = await getSvgContent(logoUrl);
const content = await getSource({
url: logoUrl
});
await clipboard(content);
toast.success('Copied to clipboard', {
description: `Svgl - Library`
@@ -0,0 +1,48 @@
<script lang="ts">
export let iconSize: number;
</script>
<svg xmlns="http://www.w3.org/2000/svg" width={iconSize} height={iconSize} viewBox="0 0 200 161">
<defs
><linearGradient
id="a"
x1="48.9"
x2="127.1"
y1="40"
y2="40"
gradientTransform="scale(1.25056 .79964)"
gradientUnits="userSpaceOnUse"
><stop offset="0" stop-color="#2a3b8f" /><stop
offset="1"
stop-color="#29abe2"
/></linearGradient
><linearGradient
id="b"
x1="126.9"
x2="48.7"
y1="124.8"
y2="124.8"
gradientTransform="scale(1.2532 .79796)"
gradientUnits="userSpaceOnUse"
><stop offset="0" stop-color="#b4d44e" /><stop
offset="1"
stop-color="#e7f716"
/></linearGradient
></defs
><g fill="none" fill-rule="evenodd" stroke-width=".3"
><path fill="#166da5" d="m197 80.2-21.4 36-30-36.5 30-35.6z" /><path
fill="#8fdb69"
d="m173.3 122.4-32.6-39L121 116l30.4 44.4z"
/><path fill="#166da5" d="m172.9 37.8-32.2 39L121 44.2l30.5-44z" /><path
fill="url(#a)"
d="M61.1 31.4H141L123.4.7H78.7zm53.7 31.9H159l-15.9-26.8H98.8"
opacity=".9"
transform="translate(-.5 -.9) scale(1.22972)"
/><path
fill="url(#b)"
d="M141.3 100.3H61l17.6 30.5h45zm-26.5-31.9H159l-15.9 26.8H98.8"
opacity=".9"
transform="translate(-.5 -.9) scale(1.22972)"
/><path fill="#010101" d="M96.2 160 49.9 80 96.8.2H46L0 80.1 46.1 160z" /></g
>
</svg>
+4 -2
View File
@@ -3,7 +3,6 @@
// Utils:
import { cn } from '@/utils/cn';
import { getSvgContent } from '@/utils/getSvgContent';
// Icons:
import {
@@ -26,6 +25,7 @@
// Figma
import { onMount } from 'svelte';
import { insertSVG as figmaInsertSVG } from '@/figma/insert-svg';
import { getSource } from '@/templates/getSource';
// Props:
export let svgInfo: iSVG;
@@ -46,7 +46,9 @@
}
const insertSVG = async (url?: string) => {
const content = (await getSvgContent(url)) as string;
const content = (await getSource({
url
})) as string;
figmaInsertSVG(content);
};
+26
View File
@@ -0,0 +1,26 @@
interface AngularComponentParams {
svgContent: string;
componentName: string;
}
export function getAngularCode(params: AngularComponentParams): string {
const updatedSvgContent = params.svgContent.replace(
/<svg([^>]*)>/,
`<svg$1 [attr.width]="size.width" [attr.height]="size.height">`
);
return `
import { Component, Input } from '@angular/core';
@Component({
selector: 'svg-${params.componentName}',
standalone: true,
template: \`
${updatedSvgContent.trim()}
\`,
})
export class ${params.componentName}Component {
@Input({ required: true }) size: { width: number; height: number };
}
`;
}
@@ -1,11 +1,11 @@
interface iComponentCode {
interface ReactComponentParams {
code: string;
name: string;
typescript: boolean;
}
export const getReactComponentCode = async (
params: iComponentCode
export const getReactCode = async (
params: ReactComponentParams
): Promise<{ data?: string; error?: string }> => {
try {
const getCode = await fetch('/api/svgs/svgr', {
@@ -18,6 +18,6 @@ export const getReactComponentCode = async (
const data = await getCode.json();
return data;
} catch (error) {
return { error: 'An error has ocurred. Try again.' };
return { error: `⚠️ getReactCode: An error has ocurred - ${error}` };
}
};
+9
View File
@@ -0,0 +1,9 @@
interface SourceParams {
url: string | undefined;
}
export const getSource = async (params: SourceParams) => {
const response = await fetch(params.url || '');
const content = await response.text();
return content;
};
+15
View File
@@ -0,0 +1,15 @@
import { parseSvgContent } from '@/utils/parseSvgContent';
interface SvelteComponentParams {
lang: string;
content: string;
}
export const getSvelteCode = (params: SvelteComponentParams) => {
const { templateContent, componentStyle } = parseSvgContent(params.content, 'Svelte');
return `<script${params.lang ? ` lang="${params.lang}"` : ''}></script>
${templateContent}
${componentStyle}
`;
};
+17
View File
@@ -0,0 +1,17 @@
import { parseSvgContent } from '@/utils/parseSvgContent';
interface VueComponentParams {
lang: string;
content: string;
}
export const getVueCode = (params: VueComponentParams) => {
const { templateContent, componentStyle } = parseSvgContent(params.content, 'Vue');
return `<script setup${params.lang ? ` lang="${params.lang}"` : ''}></script>
<template>
${templateContent}
</template>
${componentStyle}
`;
};
+28
View File
@@ -0,0 +1,28 @@
interface WebComponentParams {
name: string;
content: string;
}
export const getWebComponent = (params: WebComponentParams) => {
return `
class Icon${params.name} extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.shadowRoot.innerHTML = /* html */ \`
<style>
svg {
width: var(--size, 128px);
color: var(--color, currentColor);
}
</style>
${params.content}
\`;
}
}
customElements.define("icon-${params.name.toLowerCase()}", Icon${params.name});
`;
};
-21
View File
@@ -1,21 +0,0 @@
import { parseSvgContent } from './parseSvgContent';
export const componentTemplate = (lang: string, content: string, framework: 'Vue' | 'Svelte') => {
const { templateContent, componentStyle } = parseSvgContent(content, framework);
if (framework === 'Vue') {
return `<script setup${lang ? ` lang="${lang}"` : ''}></script>
<template>
${templateContent}
</template>
${componentStyle}
`;
} else {
return `<script${lang ? ` lang="${lang}"` : ''}></script>
${templateContent}
${componentStyle}
`;
}
};
-28
View File
@@ -1,28 +0,0 @@
export function generateAngularComponent(svgContent: string, componentName: string): string {
const updatedSvgContent = svgContent.replace(
/<svg([^>]*)>/,
`<svg$1 [attr.width]="size.width" [attr.height]="size.height">`
);
return `
/**
* -------------------------------------------------------------------------
* This Angular standalone component was generated by svgl.app
* 🧩 A beautiful library with SVG logos
* -------------------------------------------------------------------------
*/
import { Component, Input } from '@angular/core';
@Component({
selector: 'svg-${componentName}',
standalone: true,
template: \`
${updatedSvgContent.trim()}
\`,
})
export class ${componentName}Component {
@Input({ required: true }) size: { width: number; height: number };
}
`;
}
-5
View File
@@ -1,5 +0,0 @@
export const getSvgContent = async (url: string | undefined) => {
const response = await fetch(url || '');
const content = await response.text();
return content;
};