Add settings management with package manager and SVG optimization options

This commit is contained in:
pheralb
2025-09-21 19:15:11 +01:00
parent 0c78255847
commit 3e507cf7c8
10 changed files with 227 additions and 44 deletions
+2
View File
@@ -13,6 +13,7 @@
import SvglVersion from "@/components/svglVersion.svelte"; import SvglVersion from "@/components/svglVersion.svelte";
import SendIcon from "@/components/ui/moving-icons/send-icon.svelte"; import SendIcon from "@/components/ui/moving-icons/send-icon.svelte";
import SidebarMobileMenu from "@/components/layout/sidebarMobileMenu.svelte"; import SidebarMobileMenu from "@/components/layout/sidebarMobileMenu.svelte";
import SettingsMenu from "../settings/settingsMenu.svelte";
interface HeaderProps { interface HeaderProps {
githubStars?: number; githubStars?: number;
@@ -55,6 +56,7 @@
"hover:bg-neutral-200 dark:hover:bg-neutral-800", "hover:bg-neutral-200 dark:hover:bg-neutral-800",
)} )}
/> />
<SettingsMenu />
</div> </div>
<div class="hidden h-5 items-center space-x-2 md:flex"> <div class="hidden h-5 items-center space-x-2 md:flex">
<Separator orientation="vertical" /> <Separator orientation="vertical" />
@@ -0,0 +1,19 @@
<script lang="ts">
import { Switch } from "@/components/ui/switch";
import { settingsStore } from "@/stores/settings.store";
let optimize = $derived($settingsStore.optimizeSvgs);
const handleOptimizeChange = (checked: boolean) => {
settingsStore.setOptimizeSvgs(checked);
};
</script>
<div class="flex items-center gap-3">
<Switch
id="optimize"
checked={optimize}
onCheckedChange={handleOptimizeChange}
/>
<label for="optimize">Optimize SVGs</label>
</div>
@@ -1,16 +1,18 @@
<script lang="ts"> <script lang="ts">
import type { Component } from "svelte";
import * as Select from "@/components/ui/select"; import * as Select from "@/components/ui/select";
import { pkgManager, type PackageManager } from "@/stores/pkgManager.store"; import { buttonVariants } from "@/components/ui/button";
import { settingsStore, type PackageManager } from "@/stores/settings.store";
import Npm from "@/components/logos/npm.svelte"; import Npm from "@/components/logos/npm.svelte";
import Pnpm from "@/components/logos/pnpm.svelte"; import Pnpm from "@/components/logos/pnpm.svelte";
import Yarn from "@/components/logos/yarn.svelte"; import Yarn from "@/components/logos/yarn.svelte";
import Bun from "@/components/logos/bun.svelte"; import Bun from "@/components/logos/bun.svelte";
import { buttonVariants } from "./ui/button";
let pkg = $derived($pkgManager); let pkg = $derived($settingsStore.packageManager);
const managers = { const managers: Record<PackageManager, { label: string; Icon: Component }> = {
npm: { label: "npm", Icon: Npm }, npm: { label: "npm", Icon: Npm },
pnpm: { label: "pnpm", Icon: Pnpm }, pnpm: { label: "pnpm", Icon: Pnpm },
yarn: { label: "yarn", Icon: Yarn }, yarn: { label: "yarn", Icon: Yarn },
@@ -19,18 +21,22 @@
</script> </script>
<Select.Root type="single" bind:value={pkg}> <Select.Root type="single" bind:value={pkg}>
<Select.Trigger class={buttonVariants({ variant: "outline", size: "sm" })}> <Select.Trigger
class={buttonVariants({ variant: "outline", class: "justify-between" })}
>
{#if managers[pkg]} {#if managers[pkg]}
{@const { Icon, label } = managers[pkg]} {@const { Icon, label } = managers[pkg]}
<Icon size={14} /> <div class="flex items-center space-x-2.5">
<Icon size={16} />
<span>{label}</span> <span>{label}</span>
</div>
{/if} {/if}
</Select.Trigger> </Select.Trigger>
<Select.Content sideOffset={1.5}> <Select.Content sideOffset={1.5}>
{#each Object.entries(managers) as [value, { Icon, label }] (value)} {#each Object.entries(managers) as [value, { Icon, label }] (value)}
<Select.Item <Select.Item
{value} {value}
onclick={() => pkgManager.set(value as PackageManager)} onclick={() => settingsStore.setPackageManager(value as PackageManager)}
> >
<Icon size={16} /> <Icon size={16} />
<span>{label}</span> <span>{label}</span>
@@ -0,0 +1,19 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface SettingsCardProps {
title: string;
description: string;
children: Snippet;
}
let { title, description, children }: SettingsCardProps = $props();
</script>
<div class="flex flex-col">
<h3 class="mb-0.5 font-medium">{title}</h3>
<p class="mb-3 text-sm text-neutral-600 dark:text-neutral-400">
{description}
</p>
{@render children?.()}
</div>
@@ -0,0 +1,60 @@
<script lang="ts">
import { cn } from "@/utils/cn";
import { toast } from "svelte-sonner";
import { settingsStore } from "@/stores/settings.store";
import SettingsCard from "@/components/settings/settingsCard.svelte";
import SettingsIcon from "@lucide/svelte/icons/settings";
import * as Dialog from "@/components/ui/dialog";
import { Button, buttonVariants } from "@/components/ui/button";
import Separator from "@/components/ui/separator/separator.svelte";
import OptimizeSvgs from "@/components/settings/options/optimizeSvgs.svelte";
import SelectPkgManager from "@/components/settings/options/selectPkgManager.svelte";
const handleResetSettings = () => {
settingsStore.reset();
toast.success("Settings have been reset to default");
};
</script>
<Dialog.Root>
<Dialog.Trigger
class={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"hover:bg-neutral-200 dark:hover:bg-neutral-800",
)}
>
<SettingsIcon size={20} strokeWidth={1.5} />
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Settings</Dialog.Title>
<Dialog.Description>Customize your preferences.</Dialog.Description>
</Dialog.Header>
<Separator />
<div class="my-3 flex flex-col space-y-8">
<SettingsCard
title="Package Manager"
description="Select your preferred package manager for all installations commands"
>
<SelectPkgManager />
</SettingsCard>
<SettingsCard
title="Copy SVGs"
description="Use SVGO to optimize your SVGs when you copy source code (including all frameworks)"
>
<OptimizeSvgs />
</SettingsCard>
</div>
<Dialog.Footer>
<Button variant="outline" onclick={handleResetSettings}>
<span>Reset</span>
</Button>
<Dialog.Close class={buttonVariants({ variant: "default" })}>
<span>Save</span>
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
+2 -4
View File
@@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import { buttonVariants } from "@/components/ui/button"; import { buttonVariants } from "@/components/ui/button";
import Shadcn from "@/components/logos/shadcn.svelte"; import Shadcn from "@/components/logos/shadcn.svelte";
import SelectPkgManager from "@/components/selectPkgManager.svelte";
import { pkgManager, type PackageManager } from "@/stores/pkgManager.store"; import { settingsStore, type PackageManager } from "@/stores/settings.store";
import CodeBlock from "@/components/codeBlock.svelte"; import CodeBlock from "@/components/codeBlock.svelte";
import ArrowUpRightIcon from "@lucide/svelte/icons/arrow-up-right"; import ArrowUpRightIcon from "@lucide/svelte/icons/arrow-up-right";
@@ -20,7 +19,7 @@
bun: "bunx shadcn@latest add", bun: "bunx shadcn@latest add",
}; };
let pkg = $derived($pkgManager); let pkg = $derived($settingsStore.packageManager);
let shadcnCommand = $derived(shadcnCommands[pkg]); let shadcnCommand = $derived(shadcnCommands[pkg]);
const svgFormatTitle = svgTitle const svgFormatTitle = svgTitle
.toLowerCase() .toLowerCase()
@@ -40,6 +39,5 @@
class="text-neutral-500 dark:text-neutral-400" class="text-neutral-500 dark:text-neutral-400"
/> />
</a> </a>
<SelectPkgManager />
</div> </div>
<CodeBlock code={`${shadcnCommand} @svgl/${svgFormatTitle}`} Icon={Shadcn} /> <CodeBlock code={`${shadcnCommand} @svgl/${svgFormatTitle}`} Icon={Shadcn} />
+9
View File
@@ -6,6 +6,7 @@
import { clipboard } from "@/utils/clipboard"; import { clipboard } from "@/utils/clipboard";
import { getPrefixFromSvgUrl, prefixSvgIds } from "@/utils/prefixSvgIds"; import { getPrefixFromSvgUrl, prefixSvgIds } from "@/utils/prefixSvgIds";
import { copyToClipboard as figmaCopyToClipboard } from "@/figma/copy-to-clipboard"; import { copyToClipboard as figmaCopyToClipboard } from "@/figma/copy-to-clipboard";
import { settingsStore } from "@/stores/settings.store";
// Icons: // Icons:
import XIcon from "@lucide/svelte/icons/x"; import XIcon from "@lucide/svelte/icons/x";
@@ -59,6 +60,7 @@
// States: // States:
let optionsOpen = $state<boolean>(false); let optionsOpen = $state<boolean>(false);
let isLoading = $state<boolean>(false); let isLoading = $state<boolean>(false);
let optimize = $derived($settingsStore.optimizeSvgs);
const getSvgUrl = () => { const getSvgUrl = () => {
let svgUrlToCopy; let svgUrlToCopy;
@@ -105,6 +107,7 @@
let content = await getSource({ let content = await getSource({
url: svgUrlToCopy, url: svgUrlToCopy,
optimize,
}); });
if (svgUrlToCopy) { if (svgUrlToCopy) {
@@ -150,6 +153,7 @@
const title = svgInfo.title.split(" ").join(""); const title = svgInfo.title.split(" ").join("");
let content = await getSource({ let content = await getSource({
url: svgUrlToCopy, url: svgUrlToCopy,
optimize,
}); });
if (svgUrlToCopy) { if (svgUrlToCopy) {
@@ -186,6 +190,7 @@
let content = await getSource({ let content = await getSource({
url: svgUrlToCopy, url: svgUrlToCopy,
optimize,
}); });
if (svgUrlToCopy) { if (svgUrlToCopy) {
@@ -223,6 +228,7 @@
let content = await getSource({ let content = await getSource({
url: svgUrlToCopy, url: svgUrlToCopy,
optimize,
}); });
if (svgUrlToCopy) { if (svgUrlToCopy) {
@@ -260,6 +266,7 @@
const svgUrlToCopy = getSvgUrl(); const svgUrlToCopy = getSvgUrl();
let content = await getSource({ let content = await getSource({
url: svgUrlToCopy, url: svgUrlToCopy,
optimize,
}); });
if (svgUrlToCopy) { if (svgUrlToCopy) {
@@ -297,6 +304,7 @@
const svgUrlToCopy = getSvgUrl(); const svgUrlToCopy = getSvgUrl();
let content = await getSource({ let content = await getSource({
url: svgUrlToCopy, url: svgUrlToCopy,
optimize,
}); });
if (svgUrlToCopy) { if (svgUrlToCopy) {
@@ -333,6 +341,7 @@
const svgUrlToCopy = getSvgUrl(); const svgUrlToCopy = getSvgUrl();
let content = await getSource({ let content = await getSource({
url: svgUrlToCopy, url: svgUrlToCopy,
optimize,
}); });
if (svgUrlToCopy) { if (svgUrlToCopy) {
-32
View File
@@ -1,32 +0,0 @@
import { writable } from "svelte/store";
import { browser } from "$app/environment";
type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
const localStorageKey = "svgl_package_manager";
const defaultValue: PackageManager = "npm";
function getInitialValue(): PackageManager {
if (browser) {
const stored = localStorage.getItem(localStorageKey);
if (
stored === "npm" ||
stored === "pnpm" ||
stored === "yarn" ||
stored === "bun"
) {
return stored;
}
}
return defaultValue;
}
const pkgManager = writable<PackageManager>(getInitialValue());
pkgManager.subscribe((value) => {
if (browser) {
localStorage.setItem(localStorageKey, value);
}
});
export { pkgManager, type PackageManager };
+100
View File
@@ -0,0 +1,100 @@
import { writable } from "svelte/store";
import { browser } from "$app/environment";
type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
interface Settings {
packageManager: PackageManager;
optimizeSvgs: boolean;
}
const localStorageKey = "svgl_settings";
const defaultSettings: Settings = {
packageManager: "pnpm",
optimizeSvgs: true,
};
function getInitialSettings(): Settings {
if (browser) {
try {
const stored = localStorage.getItem(localStorageKey);
if (stored) {
const parsedSettings = JSON.parse(stored) as Partial<Settings>;
return {
packageManager: isValidPackageManager(parsedSettings.packageManager)
? parsedSettings.packageManager
: defaultSettings.packageManager,
optimizeSvgs:
typeof parsedSettings.optimizeSvgs === "boolean"
? parsedSettings.optimizeSvgs
: defaultSettings.optimizeSvgs,
};
}
} catch (error) {
console.error("Error parsing settings from localStorage:", error);
}
}
return defaultSettings;
}
function isValidPackageManager(value: unknown): value is PackageManager {
return (
value === "npm" || value === "pnpm" || value === "yarn" || value === "bun"
);
}
function createSettingsStore() {
const { subscribe, set, update } = writable<Settings>(getInitialSettings());
return {
subscribe,
// Update package manager
setPackageManager: (packageManager: PackageManager) => {
update((settings) => {
const newSettings = { ...settings, packageManager };
if (browser) {
localStorage.setItem(localStorageKey, JSON.stringify(newSettings));
}
return newSettings;
});
},
// Update optimize SVGs setting
setOptimizeSvgs: (optimizeSvgs: boolean) => {
update((settings) => {
const newSettings = { ...settings, optimizeSvgs };
if (browser) {
localStorage.setItem(localStorageKey, JSON.stringify(newSettings));
}
return newSettings;
});
},
// Update multiple settings at once
updateSettings: (newSettings: Partial<Settings>) => {
update((settings) => {
const updatedSettings = { ...settings, ...newSettings };
if (browser) {
localStorage.setItem(
localStorageKey,
JSON.stringify(updatedSettings),
);
}
return updatedSettings;
});
},
// Reset to default settings
reset: () => {
set(defaultSettings);
if (browser) {
localStorage.setItem(localStorageKey, JSON.stringify(defaultSettings));
}
},
};
}
const settingsStore = createSettingsStore();
export { settingsStore, type PackageManager, type Settings };
+2
View File
@@ -2,11 +2,13 @@ import { optimizeSvg } from "@/utils/optimizeSvg";
interface SourceParams { interface SourceParams {
url: string | undefined; url: string | undefined;
optimize?: boolean;
} }
export const getSource = async (params: SourceParams) => { export const getSource = async (params: SourceParams) => {
const response = await fetch(params.url || ""); const response = await fetch(params.url || "");
const content = await response.text(); const content = await response.text();
if (!params.optimize) return content;
const optimizedContent = optimizeSvg({ svgCode: content }); const optimizedContent = optimizeSvg({ svgCode: content });
return optimizedContent; return optimizedContent;
}; };