├── .claude └── CLAUDE.md ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .rules ├── .zed └── settings.json ├── LICENSE ├── README.md ├── apps └── web │ ├── .env.example │ ├── .gitignore │ ├── components.json │ ├── content-collections.ts │ ├── content │ └── blog │ │ ├── advanced-svg-techniques │ │ ├── de.mdx │ │ ├── en.mdx │ │ ├── fr.mdx │ │ ├── ko.mdx │ │ └── zh.mdx │ │ ├── getting-started-with-svg-optimization │ │ ├── de.mdx │ │ ├── en.mdx │ │ ├── fr.mdx │ │ ├── ko.mdx │ │ └── zh.mdx │ │ ├── optimize-settings-panel-guide │ │ ├── de.mdx │ │ ├── en.mdx │ │ ├── fr.mdx │ │ ├── ko.mdx │ │ └── zh.mdx │ │ ├── popular-svg-icon-libraries │ │ ├── de.mdx │ │ ├── en.mdx │ │ ├── fr.mdx │ │ ├── ko.mdx │ │ └── zh.mdx │ │ ├── react-svg-best-practices │ │ ├── de.mdx │ │ ├── en.mdx │ │ ├── fr.mdx │ │ ├── ko.mdx │ │ └── zh.mdx │ │ ├── svg-vs-png-when-to-use-each │ │ ├── de.mdx │ │ ├── en.mdx │ │ ├── fr.mdx │ │ ├── ko.mdx │ │ └── zh.mdx │ │ ├── svg-vs-raster-images │ │ ├── de.mdx │ │ ├── en.mdx │ │ ├── fr.mdx │ │ ├── ko.mdx │ │ └── zh.mdx │ │ ├── why-client-side-processing-matters │ │ ├── de.mdx │ │ ├── en.mdx │ │ ├── fr.mdx │ │ ├── ko.mdx │ │ └── zh.mdx │ │ └── why-compress-svg │ │ ├── de.mdx │ │ ├── en.mdx │ │ ├── fr.mdx │ │ ├── ko.mdx │ │ └── zh.mdx │ ├── intlayer.config.ts │ ├── package.json │ ├── public │ ├── apple-touch-icon-180x180.png │ ├── favicon.ico │ ├── favicon.svg │ ├── icon.svg │ ├── maskable-icon-512x512.png │ ├── pwa-192x192.png │ ├── pwa-512x512.png │ ├── pwa-64x64.png │ ├── robots.txt │ ├── site.webmanifest │ └── sitemap.xml │ ├── src │ ├── components │ │ ├── blocks │ │ │ └── diff-viewer │ │ │ │ └── diff-viewer.tsx │ │ ├── blur-fade │ │ │ ├── blur-fade.tsx │ │ │ └── fade.module.css │ │ ├── code-diff-viewer.tsx │ │ ├── code-viewer.tsx │ │ ├── config-panel.tsx │ │ ├── error-boundary.tsx │ │ ├── export-panel.tsx │ │ ├── header.tsx │ │ ├── history-button.tsx │ │ ├── history-panel.tsx │ │ ├── intlayer │ │ │ ├── locale-switcher.content.ts │ │ │ ├── locale-switcher.tsx │ │ │ └── localized-link.tsx │ │ ├── lazy │ │ │ ├── code-diff-viewer-lazy.tsx │ │ │ ├── code-viewer-lazy.tsx │ │ │ ├── config-panel-lazy.tsx │ │ │ └── react-tab-content-lazy.tsx │ │ ├── loader.tsx │ │ ├── logo.tsx │ │ ├── mdx-code-block.tsx │ │ ├── mdx-components.tsx │ │ ├── mdx-wrapper.tsx │ │ ├── og │ │ │ ├── base-template.tsx │ │ │ └── images │ │ │ │ ├── box.tsx │ │ │ │ ├── logo.tsx │ │ │ │ └── tip.tsx │ │ ├── optimize │ │ │ ├── code-tab-content.tsx │ │ │ ├── compact-upload-button.tsx │ │ │ ├── data-uri-tab-content.tsx │ │ │ ├── index.ts │ │ │ ├── optimize-header.tsx │ │ │ ├── optimize-layout.tsx │ │ │ ├── optimize-tabs.tsx │ │ │ └── react-tab-content.tsx │ │ ├── pwa │ │ │ ├── index.ts │ │ │ ├── install-prompt.tsx │ │ │ ├── online-status-indicator.tsx │ │ │ └── pwa-update-prompt.tsx │ │ ├── recent-svgs.tsx │ │ ├── svg-preview.tsx │ │ ├── svg-size-adjuster.tsx │ │ ├── svg-thumbnail.tsx │ │ ├── theme-provider.tsx │ │ ├── ui │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── code-actions-toolbar.tsx │ │ │ ├── collapsible-card.tsx │ │ │ ├── copy-button.tsx │ │ │ ├── diff │ │ │ │ ├── index.tsx │ │ │ │ ├── theme.css │ │ │ │ └── utils │ │ │ │ │ ├── guess-lang.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── parse.ts │ │ │ ├── dropdown-menu.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── sonner.tsx │ │ │ ├── switch.tsx │ │ │ └── tabs.tsx │ │ └── upload-box.tsx │ ├── contents │ │ ├── about.content.ts │ │ ├── blog.content.ts │ │ ├── header.content.ts │ │ ├── home.content.ts │ │ ├── optimize.content.ts │ │ ├── plugins.content.ts │ │ └── seo.content.ts │ ├── hooks │ │ ├── index.ts │ │ ├── use-auto-compress.ts │ │ ├── use-auto-tab-switch.ts │ │ ├── use-code-generation.ts │ │ ├── use-drag-and-drop.ts │ │ ├── use-i18n-html-attrs.ts │ │ ├── use-local-storage.ts │ │ ├── use-localize-navigate.ts │ │ ├── use-optimize-page.ts │ │ ├── use-paste-handler.ts │ │ ├── use-prettified-svg.ts │ │ ├── use-svg-history.ts │ │ └── use-svg-pan-zoom.ts │ ├── lib │ │ ├── animation-utils.ts │ │ ├── blog.ts │ │ ├── clamp.ts │ │ ├── constants.ts │ │ ├── constants │ │ │ └── history.ts │ │ ├── data-uri-utils.ts │ │ ├── file-utils.ts │ │ ├── prettify-code.ts │ │ ├── pwa-utils.ts │ │ ├── seo.ts │ │ ├── svg-history-storage.ts │ │ ├── svg-to-code.ts │ │ ├── svg-transform.ts │ │ ├── svg-utils.ts │ │ ├── svgo-config.ts │ │ ├── svgo-plugins.ts │ │ ├── svgo.ts │ │ ├── utils.ts │ │ └── worker-utils │ │ │ ├── cache.ts │ │ │ ├── code-generator-worker-client.ts │ │ │ ├── prettier-worker-client.ts │ │ │ ├── svgo-worker-client.ts │ │ │ └── worker-manager.ts │ ├── routeTree.gen.ts │ ├── router.tsx │ ├── routes │ │ ├── __root.tsx │ │ ├── og.tsx │ │ └── {-$locale} │ │ │ ├── about.tsx │ │ │ ├── blog │ │ │ ├── $slug.tsx │ │ │ ├── index.tsx │ │ │ └── route.tsx │ │ │ ├── index.tsx │ │ │ ├── optimize.tsx │ │ │ └── route.tsx │ ├── store │ │ ├── svg-store.ts │ │ └── ui-store.ts │ ├── styles.css │ ├── types │ │ ├── history.ts │ │ └── pwa.d.ts │ └── workers │ │ ├── code-generator.worker.ts │ │ ├── prettier.worker.ts │ │ └── svgo.worker.ts │ ├── tsconfig.json │ ├── vercel.json │ └── vite.config.ts ├── biome.json ├── bts.jsonc ├── docs └── images │ ├── .gitkeep │ ├── home.webp │ ├── logo.png │ ├── optimize-code.webp │ └── optimize.webp ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.base.json └── tsconfig.json /.claude/CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | Tiny SVG is a web-based SVG optimizer and code generator built with TanStack Start, React 19, and TypeScript. It optimizes SVGs using SVGO and generates framework-specific code (React, Vue, Svelte, React Native, Flutter). 8 | 9 | **Tech Stack:** 10 | - Framework: TanStack Start (SSR with file-based routing) 11 | - UI: React 19, Tailwind CSS 4, Radix UI, shadcn/ui 12 | - State: Zustand 13 | - i18n: Intlayer (EN, ZH, KO, DE) 14 | - Build: Vite 7, pnpm workspaces 15 | - Linting: Biome + Ultracite 16 | 17 | ## Common Commands 18 | 19 | ```bash 20 | # Development 21 | pnpm dev # Start all workspace apps 22 | pnpm dev:web # Start only web app (port 3001) 23 | 24 | # Build 25 | pnpm build # Build all packages 26 | pnpm --filter web build # Build web app only 27 | 28 | # Code Quality 29 | pnpm check # Run Biome linter/formatter (auto-fix) 30 | pnpm check-types # TypeScript type checking 31 | 32 | # Internationalization 33 | pnpm exec intlayer build # Build i18n dictionaries (run from apps/web) 34 | ``` 35 | 36 | ## Architecture 37 | 38 | ``` 39 | tiny-svg/ 40 | ├── apps/web/ # Main web application 41 | │ ├── src/ 42 | │ │ ├── components/ # React components (UI, optimize, lazy-loaded) 43 | │ │ ├── contents/ # i18n definitions (*.content.ts) 44 | │ │ ├── hooks/ # Custom React hooks 45 | │ │ ├── lib/ # Utilities (SVGO config, code generators) 46 | │ │ ├── routes/ # TanStack Start file-based routing 47 | │ │ │ └── {-$locale}/ # Locale-prefixed routes 48 | │ │ ├── store/ # Zustand stores (svg-store, ui-store) 49 | │ │ └── workers/ # Web Workers for heavy tasks 50 | │ │ ├── svgo.worker.ts # SVG optimization 51 | │ │ ├── prettier.worker.ts # Code formatting 52 | │ │ └── code-generator.worker.ts # Framework code generation 53 | │ └── intlayer.config.ts # i18n config 54 | └── package.json # Root workspace config 55 | ``` 56 | 57 | ## Key Patterns 58 | 59 | ### Internationalization 60 | - Define translations in `*.content.ts` files using `t()` function 61 | - Access translations with `useIntlayer('contentName')` hook 62 | - Routes use `{-$locale}` pattern for locale-based routing 63 | 64 | ### Web Workers 65 | Heavy operations (SVGO, Prettier, code generation) run in Web Workers to avoid blocking the main thread. Worker utilities are in `apps/web/src/lib/`. 66 | 67 | ### State Management 68 | - `svg-store.ts`: SVG content, optimization settings, transformations 69 | - `ui-store.ts`: UI state (theme, panels, preferences) 70 | 71 | ## Code Style 72 | 73 | This project uses Ultracite with Biome for strict linting. Key rules: 74 | - Use `import type` for type imports 75 | - Use `export type` for type exports 76 | - No TypeScript enums (use `as const` objects) 77 | - No `any` type 78 | - No non-null assertions (`!`) 79 | - Use `for...of` instead of `Array.forEach` 80 | - Use arrow functions over function expressions 81 | - Always include `type` attribute on buttons 82 | - Include `title` element for SVG accessibility 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | .pnp 4 | .pnp.js 5 | 6 | # Build outputs 7 | dist 8 | build 9 | *.tsbuildinfo 10 | 11 | # Environment variables 12 | .env 13 | .env*.local 14 | 15 | # IDEs and editors 16 | .vscode/* 17 | !.vscode/settings.json 18 | !.vscode/tasks.json 19 | !.vscode/launch.json 20 | !.vscode/extensions.json 21 | .idea 22 | *.swp 23 | *.swo 24 | *~ 25 | .DS_Store 26 | 27 | # Logs 28 | logs 29 | *.log 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | lerna-debug.log* 34 | .pnpm-debug.log* 35 | 36 | # Turbo 37 | .turbo 38 | 39 | # Better-T-Stack 40 | .alchemy 41 | 42 | # Testing 43 | coverage 44 | .nyc_output 45 | 46 | # Misc 47 | *.tgz 48 | .cache 49 | tmp 50 | temp 51 | 52 | # Content collections generated 53 | .content-collections 54 | 55 | # i18n 56 | .intlayer 57 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Exit on any error 3 | set -e 4 | 5 | # Check if there are any staged files 6 | if [ -z "$(git diff --cached --name-only)" ]; then 7 | echo "No staged files to format" 8 | exit 0 9 | fi 10 | 11 | # Store the hash of staged changes to detect modifications 12 | STAGED_HASH=$(git diff --cached | sha256sum | cut -d' ' -f1) 13 | 14 | # Save list of staged files (handling all file states) 15 | STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR) 16 | PARTIALLY_STAGED=$(git diff --name-only) 17 | 18 | # Stash unstaged changes to preserve working directory 19 | # --keep-index keeps staged changes in working tree 20 | git stash push --quiet --keep-index --message "pre-commit-stash" || true 21 | STASHED=$? 22 | 23 | # Run formatter on the staged files 24 | pnpm dlx ultracite fix 25 | FORMAT_EXIT_CODE=$? 26 | 27 | # Restore working directory state 28 | if [ $STASHED -eq 0 ]; then 29 | # Re-stage the formatted files 30 | if [ -n "$STAGED_FILES" ]; then 31 | echo "$STAGED_FILES" | while IFS= read -r file; do 32 | if [ -f "$file" ]; then 33 | git add "$file" 34 | fi 35 | done 36 | fi 37 | 38 | # Restore unstaged changes 39 | git stash pop --quiet || true 40 | 41 | # Restore partial staging if files were partially staged 42 | if [ -n "$PARTIALLY_STAGED" ]; then 43 | for file in $PARTIALLY_STAGED; do 44 | if [ -f "$file" ] && echo "$STAGED_FILES" | grep -q "^$file$"; then 45 | # File was partially staged - need to unstage the unstaged parts 46 | git restore --staged "$file" 2>/dev/null || true 47 | git add -p "$file" < /dev/null 2>/dev/null || git add "$file" 48 | fi 49 | done 50 | fi 51 | else 52 | # No stash was created, just re-add the formatted files 53 | if [ -n "$STAGED_FILES" ]; then 54 | echo "$STAGED_FILES" | while IFS= read -r file; do 55 | if [ -f "$file" ]; then 56 | git add "$file" 57 | fi 58 | done 59 | fi 60 | fi 61 | 62 | # Check if staged files actually changed 63 | NEW_STAGED_HASH=$(git diff --cached | sha256sum | cut -d' ' -f1) 64 | if [ "$STAGED_HASH" != "$NEW_STAGED_HASH" ]; then 65 | echo "✨ Files formatted by Ultracite" 66 | fi 67 | 68 | exit $FORMAT_EXIT_CODE 69 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=@takumi-rs/core-* -------------------------------------------------------------------------------- /.zed/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatter": "language_server", 3 | "format_on_save": "on", 4 | "languages": { 5 | "CSS": { 6 | "language_servers": [ 7 | "tailwindcss-language-server" 8 | ] 9 | }, 10 | "JavaScript": { 11 | "formatter": { 12 | "language_server": { 13 | "name": "biome" 14 | } 15 | }, 16 | "code_actions_on_format": { 17 | "source.fixAll.biome": true, 18 | "source.organizeImports.biome": true 19 | } 20 | }, 21 | "TypeScript": { 22 | "formatter": { 23 | "language_server": { 24 | "name": "biome" 25 | } 26 | }, 27 | "code_actions_on_format": { 28 | "source.fixAll.biome": true, 29 | "source.organizeImports.biome": true 30 | } 31 | }, 32 | "TSX": { 33 | "formatter": { 34 | "language_server": { 35 | "name": "biome" 36 | } 37 | }, 38 | "code_actions_on_format": { 39 | "source.fixAll.biome": true, 40 | "source.organizeImports.biome": true 41 | } 42 | } 43 | }, 44 | "lsp": { 45 | "typescript-language-server": { 46 | "settings": { 47 | "typescript": { 48 | "preferences": { 49 | "includePackageJsonAutoImports": "on" 50 | } 51 | } 52 | } 53 | }, 54 | "tailwindcss-language-server": { 55 | "settings": { 56 | "classFunctions": [ 57 | "cva", 58 | "cx", 59 | "cn", 60 | "tw" 61 | ], 62 | "experimental": { 63 | "classRegex": [ 64 | "[cls|className]\\s\\:\\=\\s\"([^\"]*)" 65 | ] 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Tiny SVG Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/web/.env.example: -------------------------------------------------------------------------------- 1 | CORS_ORIGIN= 2 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.* 5 | .yarn/* 6 | !.yarn/patches 7 | !.yarn/plugins 8 | !.yarn/releases 9 | !.yarn/versions 10 | 11 | # Testing 12 | /coverage 13 | 14 | # Build outputs 15 | /.next/ 16 | /out/ 17 | /build/ 18 | /dist/ 19 | .vinxi 20 | .output 21 | .react-router/ 22 | .tanstack/ 23 | .nitro/ 24 | 25 | # Deployment 26 | .vercel 27 | .netlify 28 | 29 | # Environment & local files 30 | .env* 31 | !.env.example 32 | .DS_Store 33 | *.pem 34 | *.local 35 | 36 | # Logs 37 | npm-debug.log* 38 | yarn-debug.log* 39 | yarn-error.log* 40 | .pnpm-debug.log* 41 | *.log* 42 | 43 | # TypeScript 44 | *.tsbuildinfo 45 | next-env.d.ts 46 | 47 | # Intlayer 48 | .intlayer 49 | 50 | # IDE 51 | .vscode/* 52 | !.vscode/extensions.json 53 | .idea 54 | 55 | # Other 56 | dev-dist 57 | .open-next 58 | -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/content-collections.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection, defineConfig } from "@content-collections/core"; 2 | import { compileMDX } from "@content-collections/mdx"; 3 | import { 4 | transformerMetaHighlight, 5 | transformerMetaWordHighlight, 6 | transformerNotationDiff, 7 | } from "@shikijs/transformers"; 8 | import rehypeAutolinkHeadings from "rehype-autolink-headings"; 9 | import rehypePrettyCode from "rehype-pretty-code"; 10 | import rehypeSlug from "rehype-slug"; 11 | import remarkGfm from "remark-gfm"; 12 | import { z } from "zod"; 13 | 14 | const posts = defineCollection({ 15 | name: "posts", 16 | directory: "content/blog", 17 | include: "**/*.{md,mdx}", 18 | schema: z.object({ 19 | title: z.string(), 20 | desc: z.string(), 21 | cover: z.url().optional(), 22 | datetime: z.string().optional(), 23 | }), 24 | transform: async (doc, context) => { 25 | const createdAt = doc.datetime ? new Date(doc.datetime) : new Date(); 26 | const locale = doc._meta.fileName.split(".")[0] || "en"; 27 | const slug = doc._meta.directory.split("/").pop(); 28 | 29 | // Format date based on locale 30 | const localeMap: Record = { 31 | en: "en-US", 32 | zh: "zh-CN", 33 | ko: "ko-KR", 34 | de: "de-DE", 35 | }; 36 | 37 | const formattedDate = createdAt.toLocaleDateString( 38 | localeMap[locale] || "en-US", 39 | { 40 | year: "numeric", 41 | month: "long", 42 | day: "numeric", 43 | } 44 | ); 45 | 46 | const mdx = await compileMDX(context, doc, { 47 | remarkPlugins: [remarkGfm], 48 | rehypePlugins: [ 49 | rehypeSlug, 50 | [ 51 | rehypePrettyCode, 52 | { 53 | theme: "material-theme-palenight", 54 | transformers: [ 55 | transformerMetaHighlight(), 56 | transformerMetaWordHighlight(), 57 | transformerNotationDiff({ 58 | matchAlgorithm: "v3", 59 | }), 60 | ], 61 | onVisitLine(node: any) { 62 | // Prevent lines from collapsing in `display: grid` mode, and allow empty 63 | // lines to be copy/pasted 64 | if (node.children.length === 0) { 65 | node.children = [{ type: "text", value: " " }]; 66 | } 67 | }, 68 | onVisitHighlightedLine(node: any) { 69 | node.properties.className.push("line--highlighted"); 70 | }, 71 | onVisitHighlightedWord(node: any) { 72 | node.properties.className = ["word--highlighted"]; 73 | }, 74 | }, 75 | ], 76 | [ 77 | rehypeAutolinkHeadings, 78 | { 79 | properties: { 80 | className: ["subheading-anchor"], 81 | ariaLabel: "Link to section", 82 | }, 83 | }, 84 | ], 85 | ], 86 | }); 87 | 88 | return { 89 | ...doc, 90 | createdAt, 91 | formattedDate, 92 | locale, 93 | slug, 94 | mdx, 95 | _meta: { 96 | ...doc._meta, 97 | createdAt, 98 | formattedDate, 99 | locale, 100 | slug, 101 | }, 102 | }; 103 | }, 104 | }); 105 | 106 | export default defineConfig({ 107 | collections: [posts], 108 | }); 109 | -------------------------------------------------------------------------------- /apps/web/content/blog/advanced-svg-techniques/ko.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 웹 개발자를 위한 고급 SVG 기술 3 | desc: 애니메이션, 필터 및 성능 최적화를 포함한 고급 SVG 기술 탐색 4 | cover: https://images.unsplash.com/photo-1587620962725-abab7fe55159?w=800&h=400&fit=crop 5 | datetime: 2025-01-05 6 | --- 7 | 8 | # 웹 개발자를 위한 고급 SVG 기술 9 | 10 | SVG의 기본을 마스터했다면, 이제 그래픽을 한 단계 더 발전시킬 수 있는 고급 기술을 탐색할 시간입니다. 11 | 12 | ## 성능 향상을 위한 SVG 스프라이트 13 | 14 | 여러 SVG 파일을 로드하는 대신 스프라이트로 결합하세요: 15 | 16 | ```xml 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ``` 31 | 32 | ## SVG에 CSS 애니메이션 적용 33 | 34 | CSS를 사용하여 SVG 속성에 애니메이션 적용: 35 | 36 | ```css 37 | .animated-path { 38 | stroke-dasharray: 1000; 39 | stroke-dashoffset: 1000; 40 | animation: draw 2s forwards; 41 | } 42 | 43 | @keyframes draw { 44 | to { 45 | stroke-dashoffset: 0; 46 | } 47 | } 48 | ``` 49 | 50 | ## 시각 효과를 위한 SVG 필터 51 | 52 | SVG 필터로 멋진 효과 만들기: 53 | 54 | ```xml 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ``` 68 | 69 | ## viewBox로 반응형 SVG 만들기 70 | 71 | 종횡비를 유지하면서 SVG를 반응형으로 만들기: 72 | 73 | ```xml 74 | 75 | 76 | 77 | ``` 78 | 79 | ## 인라인 SVG vs 외부 파일 80 | 81 | ### 인라인 SVG의 장점 82 | 83 | - HTTP 요청 없음 84 | - CSS로 스타일 지정 가능 85 | - JavaScript로 애니메이션 가능 86 | - 중요한 그래픽에 더 적합 87 | 88 | ### 외부 SVG의 장점 89 | 90 | - 브라우저 캐싱 91 | - 페이지 간 재사용 가능 92 | - 더 깨끗한 HTML 93 | - 중요하지 않은 그래픽에 더 적합 94 | 95 | ## 성능 모범 사례 96 | 97 | 1. **경로 복잡도 최소화** - 포인트가 적을수록 렌더링이 빠름 98 | 2. **반복 요소에 `` 사용** - DOM 크기 감소 99 | 3. **SVGO로 최적화** - 불필요한 데이터 제거 100 | 4. **지연 로딩 고려** - 스크롤 아래의 SVG용 101 | 5. **CSS 변환 사용** - 하드웨어 가속 102 | 103 | ## 접근성 고려사항 104 | 105 | SVG를 접근 가능하게 만들기: 106 | 107 | ```xml 108 | 109 | 회사 로고 110 | 흰색 텍스트가 있는 파란색 원 111 | 112 | 113 | ``` 114 | 115 | ## JavaScript 통합 116 | 117 | JavaScript로 SVG 조작: 118 | 119 | ```javascript 120 | const circle = document.querySelector('circle'); 121 | circle.setAttribute('r', 50); 122 | circle.style.fill = 'red'; 123 | 124 | // GSAP 또는 다른 라이브러리로 애니메이션 125 | gsap.to(circle, { 126 | attr: { r: 100 }, 127 | duration: 1 128 | }); 129 | ``` 130 | 131 | ## 결론 132 | 133 | SVG는 무한한 가능성을 가진 강력한 형식입니다. 이러한 고급 기술을 마스터함으로써 웹을 위한 성능이 뛰어나고 접근 가능하며 시각적으로 멋진 그래픽을 만들 수 있습니다. 134 | 135 | 계속 실험하고 Tiny SVG로 SVG 파일을 최적화하는 것을 잊지 마세요! 136 | -------------------------------------------------------------------------------- /apps/web/content/blog/advanced-svg-techniques/zh.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Web 开发者的高级 SVG 技术 3 | desc: 探索高级 SVG 技术,包括动画、滤镜和性能优化 4 | cover: https://images.unsplash.com/photo-1587620962725-abab7fe55159?w=800&h=400&fit=crop 5 | datetime: 2025-01-05 6 | --- 7 | 8 | # Web 开发者的高级 SVG 技术 9 | 10 | 掌握了 SVG 基础知识后,是时候探索可以将图形提升到新高度的高级技术了。 11 | 12 | ## 使用 SVG Sprites 提升性能 13 | 14 | 与其加载多个 SVG 文件,不如将它们合并到一个雪碧图中: 15 | 16 | ```xml 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ``` 31 | 32 | ## 使用 CSS 为 SVG 添加动画 33 | 34 | 使用 CSS 对 SVG 属性进行动画处理: 35 | 36 | ```css 37 | .animated-path { 38 | stroke-dasharray: 1000; 39 | stroke-dashoffset: 1000; 40 | animation: draw 2s forwards; 41 | } 42 | 43 | @keyframes draw { 44 | to { 45 | stroke-dashoffset: 0; 46 | } 47 | } 48 | ``` 49 | 50 | ## 使用 SVG 滤镜创建视觉效果 51 | 52 | 使用 SVG 滤镜创建令人惊艳的效果: 53 | 54 | ```xml 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ``` 68 | 69 | ## 使用 viewBox 实现响应式 SVG 70 | 71 | 在保持纵横比的同时使 SVG 具有响应性: 72 | 73 | ```xml 74 | 75 | 76 | 77 | ``` 78 | 79 | ## 内联 SVG 与外部文件 80 | 81 | ### 内联 SVG 的优势 82 | 83 | - 无需 HTTP 请求 84 | - 可以使用 CSS 样式化 85 | - 可以使用 JavaScript 动画 86 | - 更适合关键图形 87 | 88 | ### 外部 SVG 的优势 89 | 90 | - 浏览器缓存 91 | - 可跨页面重用 92 | - HTML 更简洁 93 | - 更适合非关键图形 94 | 95 | ## 性能最佳实践 96 | 97 | 1. **最小化路径复杂度** - 更少的点 = 更快的渲染 98 | 2. **对重复元素使用 ``** - 减少 DOM 大小 99 | 3. **使用 SVGO 优化** - 删除不必要的数据 100 | 4. **考虑懒加载** - 用于首屏之下的 SVG 101 | 5. **使用 CSS 变换** - 硬件加速 102 | 103 | ## 可访问性注意事项 104 | 105 | 使您的 SVG 具有可访问性: 106 | 107 | ```xml 108 | 109 | 公司徽标 110 | 带有白色文本的蓝色圆圈 111 | 112 | 113 | ``` 114 | 115 | ## JavaScript 集成 116 | 117 | 使用 JavaScript 操作 SVG: 118 | 119 | ```javascript 120 | const circle = document.querySelector('circle'); 121 | circle.setAttribute('r', 50); 122 | circle.style.fill = 'red'; 123 | 124 | // 使用 GSAP 或其他库进行动画 125 | gsap.to(circle, { 126 | attr: { r: 100 }, 127 | duration: 1 128 | }); 129 | ``` 130 | 131 | ## 结论 132 | 133 | SVG 是一种功能强大的格式,拥有无限可能。通过掌握这些高级技术,您可以为 Web 创建高性能、可访问且视觉效果惊艳的图形。 134 | 135 | 继续实验,别忘了使用 Tiny SVG 优化您的 SVG 文件! 136 | -------------------------------------------------------------------------------- /apps/web/content/blog/getting-started-with-svg-optimization/de.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Erste Schritte mit SVG-Optimierung 3 | desc: Erfahren Sie, wie Sie Ihre SVG-Dateien für bessere Leistung und kleinere Dateigrößen optimieren 4 | cover: https://images.unsplash.com/photo-1558403194-611308249627?w=800&h=400&fit=crop 5 | datetime: 2025-01-15 6 | --- 7 | 8 | # Erste Schritte mit SVG-Optimierung 9 | 10 | SVG-Dateien (Scalable Vector Graphics) sind leistungsstarke Assets für die moderne Webentwicklung, enthalten jedoch oft unnötige Daten, die Ihre Website verlangsamen können. In diesem Leitfaden werden wir untersuchen, wie Sie SVG-Dateien effektiv optimieren können. 11 | 12 | ## Warum SVG-Dateien optimieren? 13 | 14 | SVG-Dateien, die aus Design-Tools wie Figma, Sketch oder Adobe Illustrator exportiert wurden, enthalten oft: 15 | 16 | - Versteckte Metadaten und Kommentare 17 | - Ungenutzte Definitionen und Gruppen 18 | - Redundante Attribute 19 | - Ineffiziente Pfaddaten 20 | - Standardwerte, die weggelassen werden können 21 | 22 | Durch die Optimierung dieser Dateien können Sie: 23 | 24 | 1. **Dateigröße reduzieren** um 30-70% 25 | 2. **Seitenladezeiten verbessern** 26 | 3. **Rendering-Leistung steigern** 27 | 4. **Ihren SVG-Code wartbarer machen** 28 | 29 | ## Grundlegende Optimierungstechniken 30 | 31 | ### Unnötige Metadaten entfernen 32 | 33 | Die meisten Design-Tools fügen Metadaten hinzu, die Browser nicht benötigen: 34 | 35 | ```xml 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ``` 47 | 48 | ### Pfade vereinfachen 49 | 50 | Pfaddaten können oft ohne visuelle Änderungen vereinfacht werden: 51 | 52 | ```xml 53 | 54 | 55 | 56 | 57 | 58 | ``` 59 | 60 | ### Standardwerte entfernen 61 | 62 | Viele Attribute haben Standardwerte, die nicht angegeben werden müssen: 63 | 64 | ```xml 65 | 66 | 67 | 68 | 69 | 70 | ``` 71 | 72 | ## Tiny SVG verwenden 73 | 74 | Unser Tool macht die Optimierung mühelos: 75 | 76 | 1. **Hochladen oder einfügen** Sie Ihre SVG-Datei 77 | 2. **Vorschau** von Vorher und Nachher 78 | 3. **Herunterladen** der optimierten Version 79 | 80 | Die gesamte Verarbeitung erfolgt in Ihrem Browser - Ihre Dateien verlassen niemals Ihr Gerät! 81 | 82 | ## Fazit 83 | 84 | SVG-Optimierung ist ein wesentlicher Schritt in der modernen Webentwicklung. Mit den richtigen Tools können Sie Dateigrößen erheblich reduzieren und dabei perfekte visuelle Qualität beibehalten. 85 | 86 | Beginnen Sie noch heute mit der Optimierung Ihrer SVG-Dateien und sehen Sie den Unterschied! 87 | -------------------------------------------------------------------------------- /apps/web/content/blog/getting-started-with-svg-optimization/en.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started with SVG Optimization 3 | desc: Learn how to optimize your SVG files for better performance and smaller file sizes 4 | cover: https://images.unsplash.com/photo-1558403194-611308249627?w=800&h=400&fit=crop 5 | datetime: 2025-01-15 6 | --- 7 | 8 | # Getting Started with SVG Optimization 9 | 10 | SVG (Scalable Vector Graphics) files are powerful assets for modern web development, but they often contain unnecessary data that can slow down your website. In this guide, we'll explore how to optimize SVG files effectively. 11 | 12 | ## Why Optimize SVG Files? 13 | 14 | SVG files exported from design tools like Figma, Sketch, or Adobe Illustrator often contain: 15 | 16 | - Hidden metadata and comments 17 | - Unused definitions and groups 18 | - Redundant attributes 19 | - Inefficient path data 20 | - Default values that can be omitted 21 | 22 | By optimizing these files, you can: 23 | 24 | 1. **Reduce file size** by 30-70% 25 | 2. **Improve page load times** 26 | 3. **Enhance rendering performance** 27 | 4. **Make your SVG code more maintainable** 28 | 29 | ## Basic Optimization Techniques 30 | 31 | ### Remove Unnecessary Metadata 32 | 33 | Most design tools add metadata that browsers don't need: 34 | 35 | ```xml 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ``` 47 | 48 | ### Simplify Paths 49 | 50 | Path data can often be simplified without visual changes: 51 | 52 | ```xml 53 | 54 | 55 | 56 | 57 | 58 | ``` 59 | 60 | ### Remove Default Values 61 | 62 | Many attributes have default values that don't need to be specified: 63 | 64 | ```xml 65 | 66 | 67 | 68 | 69 | 70 | ``` 71 | 72 | ## Using Tiny SVG 73 | 74 | Our tool makes optimization effortless: 75 | 76 | 1. **Upload or paste** your SVG file 77 | 2. **Preview** the before and after 78 | 3. **Download** the optimized version 79 | 80 | All processing happens in your browser - your files never leave your device! 81 | 82 | ## Conclusion 83 | 84 | SVG optimization is an essential step in modern web development. With the right tools, you can significantly reduce file sizes while maintaining perfect visual quality. 85 | 86 | Start optimizing your SVG files today and see the difference! 87 | -------------------------------------------------------------------------------- /apps/web/content/blog/getting-started-with-svg-optimization/fr.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Commencer avec l'optimisation SVG 3 | desc: Apprenez à optimiser vos fichiers SVG pour de meilleures performances et des tailles de fichiers plus petites 4 | cover: https://images.unsplash.com/photo-1558403194-611308249627?w=800&h=400&fit=crop 5 | datetime: 2025-01-15 6 | --- 7 | 8 | # Commencer avec l'optimisation SVG 9 | 10 | Les fichiers SVG (Scalable Vector Graphics) sont des ressources puissantes pour le développement web moderne, mais ils contiennent souvent des données inutiles qui peuvent ralentir votre site web. Dans ce guide, nous explorerons comment optimiser efficacement les fichiers SVG. 11 | 12 | ## Pourquoi optimiser les fichiers SVG ? 13 | 14 | Les fichiers SVG exportés depuis les outils de conception comme Figma, Sketch ou Adobe Illustrator contiennent souvent : 15 | 16 | - Métadonnées et commentaires masqués 17 | - Définitions et groupes inutilisés 18 | - Attributs redondants 19 | - Données de chemin inefficaces 20 | - Valeurs par défaut qui peuvent être omises 21 | 22 | En optimisant ces fichiers, vous pouvez : 23 | 24 | 1. **Réduire la taille du fichier** de 30-70% 25 | 2. **Améliorer les temps de chargement de page** 26 | 3. **Améliorer les performances de rendu** 27 | 4. **Rendre votre code SVG plus maintenable** 28 | 29 | ## Techniques d'optimisation de base 30 | 31 | ### Supprimer les métadonnées inutiles 32 | 33 | La plupart des outils de conception ajoutent des métadonnées dont les navigateurs n'ont pas besoin : 34 | 35 | ```xml 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ``` 47 | 48 | ### Simplifier les chemins 49 | 50 | Les données de chemin peuvent souvent être simplifiées sans changements visuels : 51 | 52 | ```xml 53 | 54 | 55 | 56 | 57 | 58 | ``` 59 | 60 | ### Supprimer les valeurs par défaut 61 | 62 | De nombreux attributs ont des valeurs par défaut qui n'ont pas besoin d'être spécifiées : 63 | 64 | ```xml 65 | 66 | 67 | 68 | 69 | 70 | ``` 71 | 72 | ## Utiliser Tiny SVG 73 | 74 | Notre outil rend l'optimisation sans effort : 75 | 76 | 1. **Téléchargez ou collez** votre fichier SVG 77 | 2. **Prévisualisez** avant et après 78 | 3. **Téléchargez** la version optimisée 79 | 80 | Tout le traitement se fait dans votre navigateur - vos fichiers ne quittent jamais votre appareil ! 81 | 82 | ## Conclusion 83 | 84 | L'optimisation SVG est une étape essentielle dans le développement web moderne. Avec les bons outils, vous pouvez réduire considérablement les tailles de fichiers tout en maintenant une qualité visuelle parfaite. 85 | 86 | Commencez à optimiser vos fichiers SVG dès aujourd'hui et voyez la différence ! -------------------------------------------------------------------------------- /apps/web/content/blog/getting-started-with-svg-optimization/ko.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: SVG 최적화 시작하기 3 | desc: 더 나은 성능과 더 작은 파일 크기를 위해 SVG 파일을 최적화하는 방법 배우기 4 | cover: https://images.unsplash.com/photo-1558403194-611308249627?w=800&h=400&fit=crop 5 | datetime: 2025-01-15 6 | --- 7 | 8 | # SVG 최적화 시작하기 9 | 10 | SVG(Scalable Vector Graphics) 파일은 현대 웹 개발에 강력한 자산이지만, 웹사이트 속도를 느리게 할 수 있는 불필요한 데이터가 포함되어 있는 경우가 많습니다. 이 가이드에서는 SVG 파일을 효과적으로 최적화하는 방법을 살펴보겠습니다. 11 | 12 | ## SVG 파일을 최적화하는 이유는? 13 | 14 | Figma, Sketch 또는 Adobe Illustrator와 같은 디자인 도구에서 내보낸 SVG 파일에는 다음이 포함되는 경우가 많습니다: 15 | 16 | - 숨겨진 메타데이터 및 주석 17 | - 사용하지 않는 정의 및 그룹 18 | - 중복 속성 19 | - 비효율적인 경로 데이터 20 | - 생략할 수 있는 기본값 21 | 22 | 이러한 파일을 최적화하면 다음을 수행할 수 있습니다: 23 | 24 | 1. **파일 크기 감소** 30-70% 25 | 2. **페이지 로드 시간 개선** 26 | 3. **렌더링 성능 향상** 27 | 4. **SVG 코드 유지 관리 용이** 28 | 29 | ## 기본 최적화 기술 30 | 31 | ### 불필요한 메타데이터 제거 32 | 33 | 대부분의 디자인 도구는 브라우저에 필요하지 않은 메타데이터를 추가합니다: 34 | 35 | ```xml 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ``` 47 | 48 | ### 경로 단순화 49 | 50 | 경로 데이터는 시각적 변화 없이 단순화할 수 있는 경우가 많습니다: 51 | 52 | ```xml 53 | 54 | 55 | 56 | 57 | 58 | ``` 59 | 60 | ### 기본값 제거 61 | 62 | 많은 속성에는 지정할 필요가 없는 기본값이 있습니다: 63 | 64 | ```xml 65 | 66 | 67 | 68 | 69 | 70 | ``` 71 | 72 | ## Tiny SVG 사용하기 73 | 74 | 우리 도구는 최적화를 쉽게 만듭니다: 75 | 76 | 1. SVG 파일 **업로드 또는 붙여넣기** 77 | 2. 이전과 이후를 **미리보기** 78 | 3. 최적화된 버전 **다운로드** 79 | 80 | 모든 처리는 브라우저에서 이루어집니다 - 파일이 기기를 떠나지 않습니다! 81 | 82 | ## 결론 83 | 84 | SVG 최적화는 현대 웹 개발의 필수 단계입니다. 올바른 도구를 사용하면 완벽한 시각적 품질을 유지하면서 파일 크기를 크게 줄일 수 있습니다. 85 | 86 | 오늘부터 SVG 파일 최적화를 시작하고 그 차이를 확인하세요! 87 | -------------------------------------------------------------------------------- /apps/web/content/blog/getting-started-with-svg-optimization/zh.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: SVG 优化入门 3 | desc: 了解如何优化 SVG 文件以获得更好的性能和更小的文件体积 4 | cover: https://images.unsplash.com/photo-1558403194-611308249627?w=800&h=400&fit=crop 5 | datetime: 2025-01-15 6 | --- 7 | 8 | # SVG 优化入门 9 | 10 | SVG(可缩放矢量图形)文件是现代 Web 开发的强大资源,但它们通常包含不必要的数据,可能会拖慢您的网站。在本指南中,我们将探索如何有效地优化 SVG 文件。 11 | 12 | ## 为什么要优化 SVG 文件? 13 | 14 | 从 Figma、Sketch 或 Adobe Illustrator 等设计工具导出的 SVG 文件通常包含: 15 | 16 | - 隐藏的元数据和注释 17 | - 未使用的定义和组 18 | - 冗余属性 19 | - 低效的路径数据 20 | - 可以省略的默认值 21 | 22 | 通过优化这些文件,您可以: 23 | 24 | 1. **减少文件体积** 30-70% 25 | 2. **改善页面加载时间** 26 | 3. **增强渲染性能** 27 | 4. **使 SVG 代码更易于维护** 28 | 29 | ## 基本优化技术 30 | 31 | ### 删除不必要的元数据 32 | 33 | 大多数设计工具会添加浏览器不需要的元数据: 34 | 35 | ```xml 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ``` 47 | 48 | ### 简化路径 49 | 50 | 路径数据通常可以在不改变视觉效果的情况下进行简化: 51 | 52 | ```xml 53 | 54 | 55 | 56 | 57 | 58 | ``` 59 | 60 | ### 删除默认值 61 | 62 | 许多属性具有不需要指定的默认值: 63 | 64 | ```xml 65 | 66 | 67 | 68 | 69 | 70 | ``` 71 | 72 | ## 使用 Tiny SVG 73 | 74 | 我们的工具让优化变得轻而易举: 75 | 76 | 1. **上传或粘贴** 您的 SVG 文件 77 | 2. **预览** 优化前后的效果 78 | 3. **下载** 优化后的版本 79 | 80 | 所有处理都在您的浏览器中进行 - 您的文件永远不会离开您的设备! 81 | 82 | ## 结论 83 | 84 | SVG 优化是现代 Web 开发中的关键步骤。使用合适的工具,您可以在保持完美视觉质量的同时显著减少文件体积。 85 | 86 | 今天就开始优化您的 SVG 文件,看看效果如何! 87 | -------------------------------------------------------------------------------- /apps/web/content/blog/popular-svg-icon-libraries/de.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Die 12 besten Open-Source-SVG-Iconbibliotheken für kommerzielle Nutzung 2025" 3 | desc: Eine kuratierte Liste der besten kostenlosen Open-Source-SVG-Iconbibliotheken und Websites für kommerzielle Projekte mit Beispielen und Vergleichen 4 | cover: https://images.unsplash.com/photo-1618761714954-0b8cd0026356?w=800&h=400&fit=crop 5 | datetime: 2025-11-01 6 | --- 7 | 8 | # Die 12 besten Open-Source-SVG-Iconbibliotheken für 2025 9 | 10 | Hochwertige, kostenlose SVG-Icons für kommerzielle Projekte zu finden, kann herausfordernd sein. In diesem umfassenden Leitfaden erkunden wir die besten Open-Source-SVG-Iconbibliotheken mit permissiven Lizenzen für persönliche und kommerzielle Nutzung. 11 | 12 | ## Kriterien für großartige Iconbibliotheken 13 | 14 | ### Wesentliche Funktionen 15 | - ✅ **Kostenlos für kommerzielle Nutzung** - Permissive Lizenz (MIT, Apache, CC0) 16 | - ✅ **SVG-Format** - Skalierbar, bearbeitbar, optimiert 17 | - ✅ **Konsistenter Stil** - Einheitliche visuelle Sprache 18 | - ✅ **Regelmäßige Updates** - Aktive Wartung und neue Icons 19 | - ✅ **Mehrere Größen/Varianten** - Outline, Solid, verschiedene Gewichte 20 | 21 | ## 12 beste Open-Source-Iconbibliotheken 22 | 23 | ### 1. Heroicons 24 | **Ersteller**: Tailwind Labs 25 | **Gesamtanzahl Icons**: 450+ 26 | **Lizenz**: MIT 27 | **Website**: https://heroicons.com 28 | 29 | **Installation**: `npm install @heroicons/react` 30 | **Am besten für**: Tailwind CSS-Projekte 31 | 32 | ### 2. Lucide Icons 33 | **Gesamtanzahl**: 1,560+ 34 | **Lizenz**: ISC 35 | **Am besten für**: Sauberes, minimales Design 36 | 37 | ### 3. Bootstrap Icons 38 | **Gesamtanzahl**: 2,000+ 39 | **Lizenz**: MIT 40 | **Am besten für**: Umfassende Anforderungen 41 | 42 | ### 4. Iconoir 43 | **Gesamtanzahl**: 1,500+ 44 | **Lizenz**: MIT 45 | **Am besten für**: Abgerundeter Stil 46 | 47 | ### 5. Tabler Icons 48 | **Gesamtanzahl**: 5,200+ 49 | **Lizenz**: MIT 50 | **Am besten für**: Dashboards 51 | 52 | ### 6. Feather Icons 53 | **Gesamtanzahl**: 287 54 | **Lizenz**: MIT 55 | **Am besten für**: Minimal 56 | 57 | ### 7. Phosphor Icons 58 | **Gesamtanzahl**: 9,072 (1,512 × 6 Gewichte) 59 | **Lizenz**: MIT 60 | **Am besten für**: Visuelle Hierarchie 61 | 62 | ### 8. Font Awesome 63 | **Gesamtanzahl**: 2,000+ (kostenlos) 64 | **Lizenz**: CC BY 4.0 65 | **Am besten für**: Marken-Icons 66 | 67 | ### 9. Material Symbols 68 | **Gesamtanzahl**: 3,000+ 69 | **Lizenz**: Apache 2.0 70 | **Am besten für**: Material Design 71 | 72 | ### 10. Ionicons 73 | **Gesamtanzahl**: 1,300+ 74 | **Lizenz**: MIT 75 | **Am besten für**: Mobile Apps 76 | 77 | ### 11. SVG Repo 78 | **Gesamtanzahl**: 500,000+ 79 | **Am besten für**: Entdeckung 80 | 81 | ### 12. Hugeicons 82 | **Gesamtanzahl**: 4,000+ 83 | **Am besten für**: Professionelle Projekte 84 | 85 | ## Vergleichstabelle 86 | 87 | | Bibliothek | Icons | Stile | Lizenz | React | Am besten für | 88 | |-----------|-------|-------|--------|-------|---------------| 89 | | Heroicons | 450+ | 3 | MIT | ✅ | Tailwind | 90 | | Lucide | 1,560+ | 1 | ISC | ✅ | Minimal | 91 | | Bootstrap Icons | 2,000+ | 1 | MIT | ✅ | Umfassend | 92 | | Tabler Icons | 5,200+ | 1 | MIT | ✅ | Dashboards | 93 | | Phosphor | 9,072 | 6 | MIT | ✅ | Hierarchie | 94 | 95 | ## Fazit 96 | 97 | **🏆 Bestes Gesamt**: **Lucide Icons** 98 | **🎨 Bestes Design-System**: **Phosphor Icons** 99 | **📊 Beste Dashboards**: **Tabler Icons** 100 | **⚡ Bestes Tailwind**: **Heroicons** 101 | **🔍 Beste Entdeckung**: **SVG Repo** 102 | 103 | Beginnen Sie mit einer Bibliothek, die zu Ihrem Designstil passt! 104 | -------------------------------------------------------------------------------- /apps/web/content/blog/popular-svg-icon-libraries/ko.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "2025년 상업용 최고의 오픈소스 SVG 아이콘 라이브러리 12선" 3 | desc: 상업 프로젝트에 사용할 수 있는 최고의 무료 오픈소스 SVG 아이콘 라이브러리 및 웹사이트 목록, 예제 및 비교 포함 4 | cover: https://images.unsplash.com/photo-1618761714954-0b8cd0026356?w=800&h=400&fit=crop 5 | datetime: 2025-11-01 6 | --- 7 | 8 | # 2025년 상업용 최고의 오픈소스 SVG 아이콘 라이브러리 12선 9 | 10 | 상업 프로젝트를 위한 고품질 무료 SVG 아이콘을 찾는 것은 어려울 수 있습니다. 이 종합 가이드에서는 개인 및 상업적 사용을 위한 관대한 라이선스가 있는 무료 아이콘을 제공하는 최고의 오픈소스 SVG 아이콘 라이브러리와 웹사이트를 살펴봅니다. 11 | 12 | ## 훌륭한 아이콘 라이브러리의 기준 13 | 14 | ### 필수 기능 15 | - ✅ **상업적 사용 무료** - 관대한 라이선스(MIT, Apache, CC0) 16 | - ✅ **SVG 형식** - 확장 가능, 편집 가능, 최적화됨 17 | - ✅ **일관된 스타일** - 통일된 시각적 언어 18 | - ✅ **정기 업데이트** - 활발한 유지보수 및 새로운 아이콘 19 | - ✅ **다양한 크기/변형** - 윤곽선, 채워진, 다양한 두께 20 | 21 | ## 12개 최고의 오픈소스 아이콘 라이브러리 22 | 23 | ### 1. Heroicons 24 | **제작자**: Tailwind Labs 25 | **총 아이콘 수**: 450+ 26 | **라이선스**: MIT 27 | **웹사이트**: https://heroicons.com 28 | 29 | **설치**: `npm install @heroicons/react` 30 | **최적**: Tailwind CSS 프로젝트, 현대적인 웹 앱 31 | 32 | ### 2. Lucide Icons 33 | **총 아이콘 수**: 1,560+ 34 | **라이선스**: ISC 35 | **최적**: 깔끔하고 미니멀한 디자인 36 | 37 | ### 3. Bootstrap Icons 38 | **총 아이콘 수**: 2,000+ 39 | **라이선스**: MIT 40 | **최적**: 포괄적인 아이콘 요구사항 41 | 42 | ### 4. Iconoir 43 | **총 아이콘 수**: 1,500+ 44 | **라이선스**: MIT 45 | **최적**: 둥근 스타일, React Native 앱 46 | 47 | ### 5. Tabler Icons 48 | **총 아이콘 수**: 5,200+ 49 | **라이선스**: MIT 50 | **최적**: 대규모 애플리케이션, 대시보드 51 | 52 | ### 6. Feather Icons 53 | **총 아이콘 수**: 287 54 | **라이선스**: MIT 55 | **최적**: 미니멀 디자인 56 | 57 | ### 7. Phosphor Icons 58 | **총 아이콘 수**: 9,072 (1,512 × 6 두께) 59 | **라이선스**: MIT 60 | **최적**: 시각적 계층 구조 61 | 62 | ### 8. Font Awesome 63 | **총 아이콘 수**: 2,000+ (무료) 64 | **라이선스**: CC BY 4.0 65 | **최적**: 브랜드 아이콘 66 | 67 | ### 9. Material Symbols 68 | **총 아이콘 수**: 3,000+ 69 | **라이선스**: Apache 2.0 70 | **최적**: Material Design 프로젝트 71 | 72 | ### 10. Ionicons 73 | **총 아이콘 수**: 1,300+ 74 | **라이선스**: MIT 75 | **최적**: 모바일 앱 76 | 77 | ### 11. SVG Repo 78 | **총 아이콘 수**: 500,000+ 79 | **최적**: 아이콘 발견 80 | 81 | ### 12. Hugeicons 82 | **총 아이콘 수**: 4,000+ 83 | **최적**: 전문 프로젝트 84 | 85 | ## 비교표 86 | 87 | | 라이브러리 | 아이콘 수 | 스타일 | 라이선스 | React | 최적 용도 | 88 | |-----------|---------|-------|---------|-------|----------| 89 | | Heroicons | 450+ | 3 | MIT | ✅ | Tailwind | 90 | | Lucide | 1,560+ | 1 | ISC | ✅ | 미니멀 | 91 | | Bootstrap Icons | 2,000+ | 1 | MIT | ✅ | 포괄적 | 92 | | Tabler Icons | 5,200+ | 1 | MIT | ✅ | 대시보드 | 93 | | Phosphor | 9,072 | 6 | MIT | ✅ | 계층 구조 | 94 | 95 | ## 결론 96 | 97 | **🏆 최고 종합**: **Lucide Icons** 98 | **🎨 최고 디자인 시스템**: **Phosphor Icons** 99 | **📊 최고 대시보드**: **Tabler Icons** 100 | **⚡ 최고 Tailwind**: **Heroicons** 101 | **🔍 최고 발견**: **SVG Repo** 102 | 103 | 디자인 스타일과 프로젝트 요구 사항에 맞는 라이브러리 하나로 시작하세요! 104 | -------------------------------------------------------------------------------- /apps/web/content/blog/react-svg-best-practices/de.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "React SVG Best Practices: Vollständiger Leitfaden" 3 | desc: Umfassender Leitfaden zur Verwendung von SVG in React - Vergleich aller Methoden 4 | cover: https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&h=400&fit=crop 5 | datetime: 2025-10-20 6 | --- 7 | 8 | # React SVG Best Practices: Vollständiger Leitfaden 9 | 10 | ## 5 Methoden zur Verwendung von SVG in React 11 | 12 | ### Methode 1: ``-Tag 13 | 14 | ```tsx 15 | import logoUrl from './logo.svg' 16 | function Logo() { 17 | return Logo 18 | } 19 | ``` 20 | 21 | **Vorteile**: ✅ Einfachste Methode ✅ Browser-Caching 22 | **Nachteile**: ❌ Kein CSS-Styling ❌ Keine JavaScript-Kontrolle 23 | **Am besten für**: Statische Logos, große Illustrationen 24 | 25 | ### Methode 2: Inline-SVG 26 | 27 | ```tsx 28 | function HomeIcon() { 29 | return ( 30 | 31 | 32 | 33 | ) 34 | } 35 | ``` 36 | 37 | **Vorteile**: ✅ Volle CSS-Kontrolle ✅ JavaScript-Manipulation 38 | **Nachteile**: ❌ Bundle-Größe erhöht 39 | **Am besten für**: Kleine Icons, Animationen 40 | 41 | ### Methode 3: SVGR 42 | 43 | ```typescript 44 | // vite.config.ts 45 | import svgr from 'vite-plugin-svgr' 46 | 47 | export default defineConfig({ 48 | plugins: [react(), svgr()], 49 | }) 50 | ``` 51 | 52 | ```tsx 53 | import { ReactComponent as Logo } from './logo.svg' 54 | 55 | ``` 56 | 57 | **Vorteile**: ✅ Dateitrennung + volle Kontrolle ✅ TypeScript-Unterstützung 58 | **Am besten für**: Wiederverwendbare Komponenten, Design-Systeme 59 | 60 | ### Methode 4: Icon-Bibliotheken 61 | 62 | ```tsx 63 | import { HomeIcon } from '@heroicons/react/24/outline' 64 | import { Heart } from 'lucide-react' 65 | 66 | 67 | 68 | ``` 69 | 70 | **Vorteile**: ✅ Null-Konfiguration ✅ Konsistentes Design 71 | **Am besten für**: Schnelle Entwicklung, Standard-UI-Icons 72 | 73 | ### Methode 5: Dynamisches Laden 74 | 75 | ```tsx 76 | function DynamicSVG({ name }) { 77 | const [svg, setSvg] = useState('') 78 | useEffect(() => { 79 | import(`./icons/${name}.svg?raw`).then(m => setSvg(m.default)) 80 | }, [name]) 81 | return
82 | } 83 | ``` 84 | 85 | **Am besten für**: CMS-Inhalte, Plugin-Systeme 86 | 87 | ## Vergleichstabelle 88 | 89 | | Methode | Bundle | Styling | TypeScript | Am besten für | 90 | |---------|--------|---------|-----------|---------------| 91 | | `` | ✅ Keins | ❌ Nein | ⚠️ Begrenzt | Statisch | 92 | | Inline | ❌ Erhöht | ✅ Voll | ✅ Ja | Klein | 93 | | SVGR | ❌ Erhöht | ✅ Voll | ✅ Voll | Komponenten | 94 | | Bibliothek | ⚠️ Pro Icon | ✅ Gut | ✅ Voll | Schnell | 95 | 96 | ## Leistungsoptimierung 97 | 98 | ```tsx 99 | // 1. Lazy Loading 100 | const Icons = lazy(() => import('./icons')) 101 | 102 | // 2. Tree Shaking 103 | import { HomeIcon } from '@heroicons/react/24/outline' // ✅ 104 | 105 | // 3. SVG-Optimierung 106 | npx svgo icon.svg -o icon.optimized.svg 107 | ``` 108 | 109 | ## Endgültige Empfehlung 110 | 111 | 1. **Icon-Bibliothek** (Heroicons/Lucide) - Standard-UI-Icons 112 | 2. **SVGR** - Benutzerdefinierte Marken-Icons 113 | 3. **``-Tag** - Große dekorative SVGs 114 | 115 | Diese Kombination bietet die beste Balance aus Entwicklererfahrung, Leistung und Flexibilität! 116 | -------------------------------------------------------------------------------- /apps/web/content/blog/react-svg-best-practices/ko.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "React SVG 모범 사례: 모든 구현 방법 완전 가이드" 3 | desc: React에서 SVG 사용에 대한 종합 가이드 - img 태그, 인라인 SVG, SVGR, 아이콘 라이브러리 및 동적 가져오기를 포함한 모든 방법 비교 4 | cover: https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=800&h=400&fit=crop 5 | datetime: 2025-10-20 6 | --- 7 | 8 | # React SVG 모범 사례: 완전 가이드 9 | 10 | ## React에서 SVG 사용하는 5가지 방법 11 | 12 | ### 방법 1: `` 태그 13 | 14 | ```tsx 15 | import logoUrl from './logo.svg' 16 | function Logo() { 17 | return 로고 18 | } 19 | ``` 20 | 21 | **장점**: ✅ 가장 간단 ✅ 브라우저 캐싱 ✅ 지연 로딩 22 | **단점**: ❌ CSS 스타일 불가 ❌ JavaScript 제어 불가 23 | **적합**: 정적 로고, 대형 일러스트 24 | 25 | ### 방법 2: 인라인 SVG 26 | 27 | ```tsx 28 | function HomeIcon() { 29 | return ( 30 | 31 | 32 | 33 | ) 34 | } 35 | ``` 36 | 37 | **장점**: ✅ 완전한 CSS 제어 ✅ JavaScript 조작 ✅ 테마 지원 38 | **단점**: ❌ 번들 크기 증가 ❌ 코드 장황함 39 | **적합**: 작은 아이콘, 애니메이션 SVG 40 | 41 | ### 방법 3: SVGR 42 | 43 | ```typescript 44 | // vite.config.ts 45 | import svgr from 'vite-plugin-svgr' 46 | 47 | export default defineConfig({ 48 | plugins: [react(), svgr()], 49 | }) 50 | ``` 51 | 52 | ```tsx 53 | import { ReactComponent as Logo } from './logo.svg' 54 | 55 | ``` 56 | 57 | **장점**: ✅ 파일 분리 + 완전한 제어 ✅ TypeScript 지원 58 | **단점**: ❌ 빌드 구성 필요 59 | **적합**: 재사용 가능한 컴포넌트, 디자인 시스템 60 | 61 | ### 방법 4: 아이콘 라이브러리 62 | 63 | ```tsx 64 | import { HomeIcon } from '@heroicons/react/24/outline' 65 | import { Heart } from 'lucide-react' 66 | 67 | 68 | 69 | ``` 70 | 71 | **장점**: ✅ 제로 구성 ✅ 일관된 디자인 ✅ Tree shaking 72 | **단점**: ❌ 외부 의존성 73 | **적합**: 빠른 개발, 표준 UI 아이콘 74 | 75 | ### 방법 5: 동적 로딩 76 | 77 | ```tsx 78 | function DynamicSVG({ name }) { 79 | const [svg, setSvg] = useState('') 80 | useEffect(() => { 81 | import(`./icons/${name}.svg?raw`).then(m => setSvg(m.default)) 82 | }, [name]) 83 | return
84 | } 85 | ``` 86 | 87 | **적합**: CMS 콘텐츠, 플러그인 시스템 88 | 89 | ## 비교표 90 | 91 | | 방법 | 번들 크기 | 스타일링 | TypeScript | 최적 용도 | 92 | |------|----------|---------|-----------|----------| 93 | | `` | ✅ 없음 | ❌ 없음 | ⚠️ 제한적 | 정적 로고 | 94 | | 인라인 | ❌ 증가 | ✅ 완전 | ✅ 예 | 작은 아이콘 | 95 | | SVGR | ❌ 증가 | ✅ 완전 | ✅ 완전 | 컴포넌트 | 96 | | 라이브러리 | ⚠️ 아이콘당 | ✅ 좋음 | ✅ 완전 | 빠른 개발 | 97 | 98 | ## 성능 최적화 99 | 100 | ```tsx 101 | // 1. 지연 로딩 102 | const Icons = lazy(() => import('./icons')) 103 | 104 | // 2. Tree shaking 105 | import { HomeIcon } from '@heroicons/react/24/outline' // ✅ 106 | 107 | // 3. SVG 최적화 108 | npx svgo icon.svg -o icon.optimized.svg 109 | ``` 110 | 111 | ## 최종 권장사항 112 | 113 | 1. **아이콘 라이브러리** (Heroicons/Lucide) - 표준 UI 아이콘 114 | 2. **SVGR** - 사용자 정의 브랜드 아이콘 115 | 3. **`` 태그** - 대형 장식용 SVG 116 | 117 | 이 조합은 개발 경험, 성능 및 유연성의 최상의 균형을 제공합니다! 118 | -------------------------------------------------------------------------------- /apps/web/content/blog/svg-vs-png-when-to-use-each/de.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: SVG vs PNG - Wann welches Format verwenden 3 | desc: Die Unterschiede zwischen SVG- und PNG-Formaten verstehen und das richtige für Ihr Projekt wählen 4 | cover: https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=400&fit=crop 5 | datetime: 2025-01-10 6 | --- 7 | 8 | # SVG vs PNG: Wann welches Format verwenden 9 | 10 | Die Wahl zwischen SVG und PNG kann die Leistung und visuelle Qualität Ihrer Website erheblich beeinflussen. Lassen Sie uns untersuchen, wann welches Format zu verwenden ist. 11 | 12 | ## Was ist SVG? 13 | 14 | SVG (Scalable Vector Graphics) ist ein vektorbasiertes Bildformat, das auf XML basiert. Es verwendet mathematische Formeln, um Formen, Pfade und Farben zu definieren. 15 | 16 | ### Vorteile von SVG 17 | 18 | - **Unendlich skalierbar** - sieht bei jeder Größe perfekt aus 19 | - **Kleine Dateigröße** - besonders bei einfachen Grafiken 20 | - **Editierbar** - kann mit Code modifiziert werden 21 | - **SEO-freundlich** - durchsuchbar und indexierbar 22 | - **Barrierefrei** - kann Textalternativen enthalten 23 | 24 | ### Beste Anwendungsfälle für SVG 25 | 26 | - Logos und Icons 27 | - Illustrationen und Diagramme 28 | - UI-Elemente 29 | - Charts und Grafiken 30 | - Einfache Animationen 31 | 32 | ## Was ist PNG? 33 | 34 | PNG (Portable Network Graphics) ist ein Rasterbild-Format, das Bilder als Pixelraster speichert. 35 | 36 | ### Vorteile von PNG 37 | 38 | - **Unterstützt Transparenz** - Alpha-Kanal 39 | - **Breite Kompatibilität** - funktioniert überall 40 | - **Besser für komplexe Bilder** - Fotos, Verläufe 41 | - **Verlustfreie Kompression** - kein Qualitätsverlust 42 | 43 | ### Beste Anwendungsfälle für PNG 44 | 45 | - Fotografien 46 | - Komplexe Grafiken mit vielen Farben 47 | - Screenshots 48 | - Bilder mit Text, der scharf sein muss 49 | - Wenn Sie Transparenz mit Fotos benötigen 50 | 51 | ## Leistungsvergleich 52 | 53 | | Merkmal | SVG | PNG | 54 | |---------|-----|-----| 55 | | Dateigröße (einfaches Logo) | 2-5 KB | 10-50 KB | 56 | | Skalierbarkeit | Perfekt bei jeder Größe | Pixelig beim Skalieren | 57 | | Farben | Begrenzt für einfache Grafiken | Millionen von Farben | 58 | | Animation | CSS/JS-Animationen | Benötigt GIF oder Video | 59 | | Bearbeitung | Einfach mit Code | Benötigt Bildeditor | 60 | 61 | ## Die richtige Wahl treffen 62 | 63 | Wählen Sie **SVG**, wenn: 64 | - Die Grafik relativ einfach ist 65 | - Sie perfekte Skalierung benötigen 66 | - Sie es animieren oder damit interagieren möchten 67 | - Dateigröße wichtig ist 68 | 69 | Wählen Sie **PNG**, wenn: 70 | - Das Bild ein Foto ist 71 | - Die Grafik komplexe Verläufe oder Effekte hat 72 | - Sie pixelperfektes Rendering benötigen 73 | - SVG-Unterstützung ein Problem ist 74 | 75 | ## Hybridansatz 76 | 77 | Moderne Websites verwenden oft beide: 78 | 79 | - SVG für UI-Elemente, Logos und Icons 80 | - PNG für Fotos und komplexe Grafiken 81 | - WebP als Fallback für bessere Kompression 82 | 83 | ## Fazit 84 | 85 | Beide Formate haben ihren Platz in der Webentwicklung. Ihre Stärken zu verstehen, hilft Ihnen, fundierte Entscheidungen zu treffen, die sowohl Leistung als auch visuelle Qualität verbessern. 86 | 87 | Verwenden Sie Tiny SVG, um Ihre SVG-Dateien zu optimieren und Ihre Website schnell zu halten! 88 | -------------------------------------------------------------------------------- /apps/web/content/blog/svg-vs-png-when-to-use-each/en.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: SVG vs PNG - When to Use Each Format 3 | desc: Understanding the differences between SVG and PNG formats and choosing the right one for your project 4 | cover: https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=400&fit=crop 5 | datetime: 2025-01-10 6 | --- 7 | 8 | # SVG vs PNG: When to Use Each Format 9 | 10 | Choosing between SVG and PNG can significantly impact your website's performance and visual quality. Let's explore when to use each format. 11 | 12 | ## What is SVG? 13 | 14 | SVG (Scalable Vector Graphics) is a vector image format based on XML. It uses mathematical formulas to define shapes, paths, and colors. 15 | 16 | ### Advantages of SVG 17 | 18 | - **Infinitely scalable** - looks perfect at any size 19 | - **Small file size** - especially for simple graphics 20 | - **Editable** - can be modified with code 21 | - **SEO-friendly** - searchable and indexable 22 | - **Accessible** - can include text alternatives 23 | 24 | ### Best Use Cases for SVG 25 | 26 | - Logos and icons 27 | - Illustrations and diagrams 28 | - UI elements 29 | - Charts and graphs 30 | - Simple animations 31 | 32 | ## What is PNG? 33 | 34 | PNG (Portable Network Graphics) is a raster image format that stores images as a grid of pixels. 35 | 36 | ### Advantages of PNG 37 | 38 | - **Supports transparency** - alpha channel 39 | - **Wide compatibility** - works everywhere 40 | - **Better for complex images** - photographs, gradients 41 | - **Lossless compression** - no quality loss 42 | 43 | ### Best Use Cases for PNG 44 | 45 | - Photographs 46 | - Complex graphics with many colors 47 | - Screenshots 48 | - Images with text that needs to be crisp 49 | - When you need transparency with photos 50 | 51 | ## Performance Comparison 52 | 53 | | Feature | SVG | PNG | 54 | |---------|-----|-----| 55 | | File Size (simple logo) | 2-5 KB | 10-50 KB | 56 | | Scalability | Perfect at any size | Pixelated when scaled | 57 | | Colors | Limited for simple graphics | Millions of colors | 58 | | Animation | CSS/JS animations | Requires GIF or video | 59 | | Editing | Easy with code | Requires image editor | 60 | 61 | ## Making the Right Choice 62 | 63 | Choose **SVG** when: 64 | - The graphic is relatively simple 65 | - You need it to scale perfectly 66 | - You want to animate or interact with it 67 | - File size matters 68 | 69 | Choose **PNG** when: 70 | - The image is a photograph 71 | - The graphic has complex gradients or effects 72 | - You need pixel-perfect rendering 73 | - SVG support is a concern 74 | 75 | ## Hybrid Approach 76 | 77 | Modern websites often use both: 78 | 79 | - SVG for UI elements, logos, and icons 80 | - PNG for photos and complex graphics 81 | - WebP as a fallback for better compression 82 | 83 | ## Conclusion 84 | 85 | Both formats have their place in web development. Understanding their strengths helps you make informed decisions that improve both performance and visual quality. 86 | 87 | Use Tiny SVG to optimize your SVG files and keep your website fast! 88 | -------------------------------------------------------------------------------- /apps/web/content/blog/svg-vs-png-when-to-use-each/fr.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: SVG vs PNG - Quand utiliser chaque format 3 | desc: Comprendre les différences entre les formats SVG et PNG et choisir le bon pour votre projet 4 | cover: https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=400&fit=crop 5 | datetime: 2025-01-10 6 | --- 7 | 8 | # SVG vs PNG : Quand utiliser chaque format 9 | 10 | Le choix entre SVG et PNG peut avoir un impact significatif sur les performances et la qualité visuelle de votre site web. Explorons quand utiliser chaque format. 11 | 12 | ## Qu'est-ce que le SVG ? 13 | 14 | Le SVG (Scalable Vector Graphics) est un format d'image vectoriel basé sur XML. Il utilise des formules mathématiques pour définir les formes, les chemins et les couleurs. 15 | 16 | ### Avantages du SVG 17 | 18 | - **Infiniment scalable** - parfait à toute taille 19 | - **Petite taille de fichier** - surtout pour les graphiques simples 20 | - **Modifiable** - peut être modifié avec du code 21 | - **Optimisé pour le SEO** - consultable et indexable 22 | - **Accessible** - peut inclure des alternatives textuelles 23 | 24 | ### Meilleurs cas d'usage du SVG 25 | 26 | - Logos et icônes 27 | - Illustrations et diagrammes 28 | - Éléments d'interface 29 | - Graphiques et diagrammes 30 | - Animations simples 31 | 32 | ## Qu'est-ce que le PNG ? 33 | 34 | Le PNG (Portable Network Graphics) est un format d'image matriciel qui stocke les images sous forme de grille de pixels. 35 | 36 | ### Avantages du PNG 37 | 38 | - **Supporte la transparence** - canal alpha 39 | - **Large compatibilité** - fonctionne partout 40 | - **Meilleur pour les images complexes** - photographies, dégradés 41 | - **Compression sans perte** - aucune perte de qualité 42 | 43 | ### Meilleurs cas d'usage du PNG 44 | 45 | - Photographies 46 | - Graphiques complexes avec beaucoup de couleurs 47 | - Captures d'écran 48 | - Images avec du texte qui doit être net 49 | - Quand vous avez besoin de transparence avec des photos 50 | 51 | ## Comparaison des performances 52 | 53 | | Caractéristique | SVG | PNG | 54 | |----------------|-----|-----| 55 | | Taille du fichier (logo simple) | 2-5 Ko | 10-50 Ko | 56 | | Scalabilité | Parfait à toute taille | Pixelisé lorsqu'il est redimensionné | 57 | | Couleurs | Limité pour les graphiques simples | Millions de couleurs | 58 | | Animation | Animations CSS/JS | Nécessite GIF ou vidéo | 59 | | Édition | Facile avec du code | Nécessite un éditeur d'images | 60 | 61 | ## Faire le bon choix 62 | 63 | Choisissez **SVG** quand : 64 | - Le graphique est relativement simple 65 | - Vous avez besoin qu'il s'adapte parfaitement 66 | - Vous voulez l'animer ou interagir avec 67 | - La taille du fichier est importante 68 | 69 | Choisissez **PNG** quand : 70 | - L'image est une photographie 71 | - Le graphique a des dégradés complexes ou des effets 72 | - Vous avez besoin d'un rendu pixel-parfait 73 | - Le support SVG est un problème 74 | 75 | ## Approche hybride 76 | 77 | Les sites web modernes utilisent souvent les deux : 78 | 79 | - SVG pour les éléments d'interface, logos et icônes 80 | - PNG pour les photos et graphiques complexes 81 | - WebP comme alternative pour une meilleure compression 82 | 83 | ## Conclusion 84 | 85 | Les deux formats ont leur place dans le développement web. Comprendre leurs forces vous aide à prendre des décisions éclairées qui améliorent à la fois les performances et la qualité visuelle. 86 | 87 | Utilisez Tiny SVG pour optimiser vos fichiers SVG et garder votre site web rapide ! -------------------------------------------------------------------------------- /apps/web/content/blog/svg-vs-png-when-to-use-each/ko.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: SVG vs PNG - 각 형식을 언제 사용할지 3 | desc: SVG와 PNG 형식의 차이점을 이해하고 프로젝트에 적합한 형식 선택하기 4 | cover: https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=400&fit=crop 5 | datetime: 2025-01-10 6 | --- 7 | 8 | # SVG vs PNG: 각 형식을 언제 사용할지 9 | 10 | SVG와 PNG 중 선택하는 것은 웹사이트의 성능과 시각적 품질에 큰 영향을 미칠 수 있습니다. 각 형식을 언제 사용해야 하는지 살펴보겠습니다. 11 | 12 | ## SVG란 무엇인가? 13 | 14 | SVG(Scalable Vector Graphics)는 XML 기반의 벡터 이미지 형식입니다. 수학 공식을 사용하여 도형, 경로 및 색상을 정의합니다. 15 | 16 | ### SVG의 장점 17 | 18 | - **무한 확장 가능** - 모든 크기에서 완벽하게 보임 19 | - **작은 파일 크기** - 특히 간단한 그래픽의 경우 20 | - **편집 가능** - 코드로 수정 가능 21 | - **SEO 친화적** - 검색 및 색인 가능 22 | - **접근성** - 텍스트 대안 포함 가능 23 | 24 | ### SVG의 최적 사용 사례 25 | 26 | - 로고 및 아이콘 27 | - 일러스트레이션 및 다이어그램 28 | - UI 요소 29 | - 차트 및 그래프 30 | - 간단한 애니메이션 31 | 32 | ## PNG란 무엇인가? 33 | 34 | PNG(Portable Network Graphics)는 이미지를 픽셀 그리드로 저장하는 래스터 이미지 형식입니다. 35 | 36 | ### PNG의 장점 37 | 38 | - **투명도 지원** - 알파 채널 39 | - **광범위한 호환성** - 어디서나 작동 40 | - **복잡한 이미지에 더 좋음** - 사진, 그라디언트 41 | - **무손실 압축** - 품질 손실 없음 42 | 43 | ### PNG의 최적 사용 사례 44 | 45 | - 사진 46 | - 많은 색상이 포함된 복잡한 그래픽 47 | - 스크린샷 48 | - 선명한 텍스트가 필요한 이미지 49 | - 사진에 투명도가 필요할 때 50 | 51 | ## 성능 비교 52 | 53 | | 기능 | SVG | PNG | 54 | |---------|-----|-----| 55 | | 파일 크기 (간단한 로고) | 2-5 KB | 10-50 KB | 56 | | 확장성 | 모든 크기에서 완벽 | 확대 시 픽셀화됨 | 57 | | 색상 | 간단한 그래픽에는 제한적 | 수백만 가지 색상 | 58 | | 애니메이션 | CSS/JS 애니메이션 | GIF 또는 비디오 필요 | 59 | | 편집 | 코드로 쉽게 편집 | 이미지 편집기 필요 | 60 | 61 | ## 올바른 선택하기 62 | 63 | 다음의 경우 **SVG**를 선택하세요: 64 | - 그래픽이 비교적 간단한 경우 65 | - 완벽한 확장이 필요한 경우 66 | - 애니메이션이나 상호 작용이 필요한 경우 67 | - 파일 크기가 중요한 경우 68 | 69 | 다음의 경우 **PNG**를 선택하세요: 70 | - 이미지가 사진인 경우 71 | - 그래픽에 복잡한 그라디언트나 효과가 있는 경우 72 | - 픽셀 완벽한 렌더링이 필요한 경우 73 | - SVG 지원이 문제가 되는 경우 74 | 75 | ## 하이브리드 접근 방식 76 | 77 | 현대 웹사이트는 종종 둘 다 사용합니다: 78 | 79 | - UI 요소, 로고 및 아이콘에는 SVG 80 | - 사진 및 복잡한 그래픽에는 PNG 81 | - 더 나은 압축을 위한 대체로 WebP 82 | 83 | ## 결론 84 | 85 | 두 형식 모두 웹 개발에서 제자리를 가지고 있습니다. 그들의 강점을 이해하면 성능과 시각적 품질을 모두 향상시키는 정보에 입각한 결정을 내리는 데 도움이 됩니다. 86 | 87 | Tiny SVG를 사용하여 SVG 파일을 최적화하고 웹사이트를 빠르게 유지하세요! 88 | -------------------------------------------------------------------------------- /apps/web/content/blog/svg-vs-png-when-to-use-each/zh.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: SVG vs PNG - 何时使用哪种格式 3 | desc: 了解 SVG 和 PNG 格式之间的区别,并为您的项目选择合适的格式 4 | cover: https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=400&fit=crop 5 | datetime: 2025-01-10 6 | --- 7 | 8 | # SVG vs PNG:何时使用哪种格式 9 | 10 | 在 SVG 和 PNG 之间进行选择会显著影响网站的性能和视觉质量。让我们探讨何时使用每种格式。 11 | 12 | ## 什么是 SVG? 13 | 14 | SVG(可缩放矢量图形)是一种基于 XML 的矢量图像格式。它使用数学公式来定义形状、路径和颜色。 15 | 16 | ### SVG 的优势 17 | 18 | - **无限可缩放** - 在任何尺寸下都完美显示 19 | - **文件体积小** - 特别是对于简单图形 20 | - **可编辑** - 可以用代码修改 21 | - **SEO 友好** - 可搜索和可索引 22 | - **可访问** - 可以包含文本替代 23 | 24 | ### SVG 的最佳使用场景 25 | 26 | - 徽标和图标 27 | - 插图和图表 28 | - UI 元素 29 | - 图表和图形 30 | - 简单动画 31 | 32 | ## 什么是 PNG? 33 | 34 | PNG(便携式网络图形)是一种将图像存储为像素网格的光栅图像格式。 35 | 36 | ### PNG 的优势 37 | 38 | - **支持透明度** - Alpha 通道 39 | - **广泛兼容** - 随处可用 40 | - **更适合复杂图像** - 照片、渐变 41 | - **无损压缩** - 无质量损失 42 | 43 | ### PNG 的最佳使用场景 44 | 45 | - 照片 46 | - 具有多种颜色的复杂图形 47 | - 屏幕截图 48 | - 需要清晰文本的图像 49 | - 需要照片透明度时 50 | 51 | ## 性能比较 52 | 53 | | 特性 | SVG | PNG | 54 | |---------|-----|-----| 55 | | 文件大小(简单徽标) | 2-5 KB | 10-50 KB | 56 | | 可缩放性 | 任何尺寸都完美 | 缩放时像素化 | 57 | | 颜色 | 简单图形颜色有限 | 数百万种颜色 | 58 | | 动画 | CSS/JS 动画 | 需要 GIF 或视频 | 59 | | 编辑 | 使用代码轻松编辑 | 需要图像编辑器 | 60 | 61 | ## 做出正确的选择 62 | 63 | 选择 **SVG** 当: 64 | - 图形相对简单 65 | - 需要完美缩放 66 | - 想要动画或交互 67 | - 文件大小很重要 68 | 69 | 选择 **PNG** 当: 70 | - 图像是照片 71 | - 图形具有复杂的渐变或效果 72 | - 需要像素完美的渲染 73 | - SVG 支持是一个问题 74 | 75 | ## 混合方法 76 | 77 | 现代网站通常两者都使用: 78 | 79 | - SVG 用于 UI 元素、徽标和图标 80 | - PNG 用于照片和复杂图形 81 | - WebP 作为后备以获得更好的压缩 82 | 83 | ## 结论 84 | 85 | 这两种格式在 Web 开发中都有各自的位置。了解它们的优势有助于您做出明智的决策,从而改善性能和视觉质量。 86 | 87 | 使用 Tiny SVG 优化您的 SVG 文件,保持网站快速运行! 88 | -------------------------------------------------------------------------------- /apps/web/content/blog/why-client-side-processing-matters/ko.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 클라이언트 측 처리가 개인정보 보호에 중요한 이유 3 | desc: 클라이언트 측 SVG 최적화의 중요성과 데이터를 보호하는 방법 이해하기 4 | cover: https://images.unsplash.com/photo-1563986768609-322da13575f3?w=800&h=400&fit=crop 5 | datetime: 2025-01-01 6 | --- 7 | 8 | # 클라이언트 측 처리가 개인정보 보호에 중요한 이유 9 | 10 | 데이터 프라이버시가 가장 중요한 시대에, 온라인에서 파일을 처리하는 방식은 그 어느 때보다 중요합니다. 클라이언트 측 처리가 웹 애플리케이션의 미래인 이유를 살펴보겠습니다. 11 | 12 | ## 전통적인 접근 방식 13 | 14 | 대부분의 온라인 도구는 다음 패턴을 따릅니다: 15 | 16 | 1. 사용자가 서버에 파일 업로드 17 | 2. 서버가 파일 처리 18 | 3. 서버가 결과 다시 전송 19 | 4. 서버가 파일을 저장할 수 있음 20 | 21 | **문제**: 파일이 기기를 떠나 다른 사람의 컴퓨터에 존재합니다. 22 | 23 | ## 클라이언트 측 접근 방식 24 | 25 | 클라이언트 측 처리를 사용하면: 26 | 27 | 1. 파일이 브라우저에 남아있음 28 | 2. 로컬에서 처리 발생 29 | 3. 네트워크 전송 불필요 30 | 4. 서버 저장소 없음 31 | 32 | **이점**: 파일이 절대 기기를 떠나지 않습니다. 33 | 34 | ## 이것이 중요한 이유 35 | 36 | ### 1. 완전한 개인정보 보호 37 | 38 | SVG 파일에는 다음이 포함될 수 있습니다: 39 | - 독점 디자인 40 | - 미출시 제품 41 | - NDA 하의 클라이언트 작업 42 | - 민감한 정보 43 | 44 | 클라이언트 측 처리를 사용하면 이러한 파일이 100% 비공개로 유지됩니다. 45 | 46 | ### 2. 더 나은 보안 47 | 48 | 업로드가 없다는 것은: 49 | - 전송 중 중간자 공격 없음 50 | - 파일을 노출하는 서버 침해 없음 51 | - 우발적인 데이터 유출 없음 52 | - 서비스 약관 문제 없음 53 | 54 | ### 3. 더 빠른 처리 55 | 56 | 클라이언트 측 처리는 종종 더 빠릅니다: 57 | - 업로드/다운로드 시간 없음 58 | - 서버 대기열 없음 59 | - 네트워크 지연 없음 60 | - 하드웨어에서 직접 처리 61 | 62 | ### 4. 오프라인 작동 63 | 64 | 앱이 로드되면 다음을 수행할 수 있습니다: 65 | - 인터넷 없이 파일 처리 66 | - 비행기나 기차에서 작업 67 | - 연결 문제 방지 68 | - 어디서나 생산성 유지 69 | 70 | ### 5. 무제한 사용 71 | 72 | 서버 기반 도구는 종종 다음을 제한합니다: 73 | - 파일 수 74 | - 파일 크기 75 | - 처리 빈도 76 | - 유료 구독 뒤의 기능 77 | 78 | 클라이언트 측 도구에는 이러한 제한이 없습니다. 79 | 80 | ## Tiny SVG가 이를 구현하는 방법 81 | 82 | 우리의 접근 방식: 83 | 84 | ```javascript 85 | // 모든 것이 브라우저에서 일어남 86 | const worker = new Worker('svgo.worker.js'); 87 | 88 | worker.postMessage({ svg: yourSVGContent }); 89 | 90 | worker.onmessage = (e) => { 91 | const optimizedSVG = e.data; 92 | // 브라우저를 절대 떠나지 않습니다! 93 | }; 94 | ``` 95 | 96 | ### 성능을 위한 Web Workers 97 | 98 | 우리는 Web Workers를 사용하여: 99 | - UI 반응성 유지 100 | - 대용량 파일을 효율적으로 처리 101 | - 백그라운드 스레드에서 최적화 실행 102 | - 사용자 상호 작용 차단 방지 103 | 104 | ### 로컬 스토리지만 사용 105 | 106 | 귀하의 환경 설정은 다음을 사용하여 저장됩니다: 107 | - 브라우저의 localStorage 108 | - 쿠키 없음 109 | - 추적 없음 110 | - 외부 데이터베이스 없음 111 | 112 | ## 웹 앱의 미래 113 | 114 | 현대 브라우저는 매우 강력합니다. 다음을 수행할 수 있습니다: 115 | - 이미지 및 비디오 처리 116 | - 복잡한 계산 실행 117 | - 대규모 데이터 세트 처리 118 | - AI/ML 추론 수행 119 | 120 | 클라이언트 측 처리는 이러한 능력을 활용하면서 개인정보를 존중합니다. 121 | 122 | ## 트레이드오프 123 | 124 | 클라이언트 측 처리가 항상 완벽한 것은 아닙니다: 125 | 126 | **제한 사항**: 127 | - 최신 브라우저 필요 128 | - 기기 리소스 사용 129 | - 브라우저 기능에 의해 제한됨 130 | - 기기 간 동기화 없음 (명시적 설정 없이) 131 | 132 | **서버 측이 의미가 있을 때**: 133 | - 협업 기능 필요 134 | - 브라우저에 너무 집약적인 처리 135 | - 기기 간 동기화 필요 136 | - 중앙 집중식 데이터 관리 필요 137 | 138 | ## 결론 139 | 140 | SVG 최적화와 같은 도구의 경우, 클라이언트 측 처리는 완벽한 균형을 제공합니다: 141 | - 개인정보 보호 142 | - 보안 143 | - 성능 144 | - 편의성 145 | 146 | 귀하의 파일은 귀하의 것입니다. 기기에 남아 있어야 합니다. 147 | 148 | 오늘 Tiny SVG를 시도하고 진정한 클라이언트 측 처리의 이점을 경험하세요! 149 | -------------------------------------------------------------------------------- /apps/web/content/blog/why-client-side-processing-matters/zh.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 为什么客户端处理对隐私很重要 3 | desc: 了解客户端 SVG 优化的重要性以及它如何保护您的数据 4 | cover: https://images.unsplash.com/photo-1563986768609-322da13575f3?w=800&h=400&fit=crop 5 | datetime: 2025-01-01 6 | --- 7 | 8 | # 为什么客户端处理对隐私很重要 9 | 10 | 在数据隐私至关重要的时代,我们在线处理文件的方式比以往任何时候都更重要。让我们探讨为什么客户端处理是 Web 应用程序的未来。 11 | 12 | ## 传统方法 13 | 14 | 大多数在线工具遵循这种模式: 15 | 16 | 1. 用户将文件上传到服务器 17 | 2. 服务器处理文件 18 | 3. 服务器发送回结果 19 | 4. 服务器可能存储您的文件 20 | 21 | **问题**:您的文件离开了您的设备,存在于别人的计算机上。 22 | 23 | ## 客户端方法 24 | 25 | 使用客户端处理: 26 | 27 | 1. 文件留在您的浏览器中 28 | 2. 在本地进行处理 29 | 3. 无需网络传输 30 | 4. 无服务器存储 31 | 32 | **好处**:您的文件永远不会离开您的设备。 33 | 34 | ## 为什么这很重要 35 | 36 | ### 1. 完全隐私 37 | 38 | 您的 SVG 文件可能包含: 39 | - 专有设计 40 | - 未发布的产品 41 | - NDA 下的客户工作 42 | - 敏感信息 43 | 44 | 使用客户端处理,这些文件保持 100% 私密。 45 | 46 | ### 2. 更好的安全性 47 | 48 | 无上传意味着: 49 | - 传输过程中无中间人攻击 50 | - 无服务器泄露您的文件 51 | - 无意外数据泄漏 52 | - 无服务条款问题 53 | 54 | ### 3. 更快的处理 55 | 56 | 客户端处理通常更快,因为: 57 | - 无上传/下载时间 58 | - 无服务器队列 59 | - 无网络延迟 60 | - 直接在您的硬件上处理 61 | 62 | ### 4. 离线工作 63 | 64 | 应用加载后,您可以: 65 | - 无需互联网即可处理文件 66 | - 在飞机或火车上工作 67 | - 避免连接问题 68 | - 随时随地保持生产力 69 | 70 | ### 5. 无限使用 71 | 72 | 基于服务器的工具通常限制: 73 | - 文件数量 74 | - 文件大小 75 | - 处理频率 76 | - 付费墙后的功能 77 | 78 | 客户端工具没有这些限制。 79 | 80 | ## Tiny SVG 如何实现这一点 81 | 82 | 我们的方法: 83 | 84 | ```javascript 85 | // 一切都在您的浏览器中进行 86 | const worker = new Worker('svgo.worker.js'); 87 | 88 | worker.postMessage({ svg: yourSVGContent }); 89 | 90 | worker.onmessage = (e) => { 91 | const optimizedSVG = e.data; 92 | // 永远不会离开您的浏览器! 93 | }; 94 | ``` 95 | 96 | ### 使用 Web Workers 提高性能 97 | 98 | 我们使用 Web Workers 来: 99 | - 保持 UI 响应 100 | - 高效处理大文件 101 | - 在后台线程中运行优化 102 | - 避免阻塞用户交互 103 | 104 | ### 仅本地存储 105 | 106 | 您的偏好设置使用以下方式存储: 107 | - 浏览器的 localStorage 108 | - 无 cookies 109 | - 无跟踪 110 | - 无外部数据库 111 | 112 | ## Web 应用程序的未来 113 | 114 | 现代浏览器功能非常强大。它们可以: 115 | - 处理图像和视频 116 | - 运行复杂计算 117 | - 处理大型数据集 118 | - 执行 AI/ML 推理 119 | 120 | 客户端处理利用这种能力,同时尊重您的隐私。 121 | 122 | ## 权衡 123 | 124 | 客户端处理并不总是完美的: 125 | 126 | **限制**: 127 | - 需要现代浏览器 128 | - 使用设备资源 129 | - 受浏览器功能限制 130 | - 无跨设备同步(没有明确设置) 131 | 132 | **何时服务器端有意义**: 133 | - 需要协作功能 134 | - 处理对浏览器来说太密集 135 | - 需要跨设备同步 136 | - 需要集中数据管理 137 | 138 | ## 结论 139 | 140 | 对于像 SVG 优化这样的工具,客户端处理提供了完美的平衡: 141 | - 隐私 142 | - 安全 143 | - 性能 144 | - 便利 145 | 146 | 您的文件是您的。它们应该留在您的设备上。 147 | 148 | 今天就试试 Tiny SVG,体验真正的客户端处理的好处! 149 | -------------------------------------------------------------------------------- /apps/web/intlayer.config.ts: -------------------------------------------------------------------------------- 1 | import { type IntlayerConfig, Locales } from "intlayer"; 2 | 3 | const config: IntlayerConfig = { 4 | internationalization: { 5 | locales: [ 6 | Locales.ENGLISH, 7 | Locales.CHINESE, 8 | Locales.KOREAN, 9 | Locales.GERMAN, 10 | Locales.FRENCH, 11 | ], 12 | defaultLocale: Locales.ENGLISH, 13 | }, 14 | editor: { 15 | enabled: process.env.NODE_ENV === "development", 16 | }, 17 | }; 18 | 19 | export default config; 20 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "build": "vite build", 7 | "serve": "vite preview", 8 | "dev": "vite dev --port=3001", 9 | "deploy": "vercel --prod", 10 | "blog:build": "content-collections build", 11 | "check-types": "tsc --noEmit" 12 | }, 13 | "dependencies": { 14 | "@egoist/tailwindcss-icons": "^1.9.0", 15 | "@mdx-js/mdx": "^3.1.1", 16 | "@mdx-js/react": "^3.1.1", 17 | "@radix-ui/react-collapsible": "^1.1.12", 18 | "@radix-ui/react-dialog": "^1.1.15", 19 | "@radix-ui/react-select": "^2.2.6", 20 | "@radix-ui/react-separator": "^1.1.7", 21 | "@radix-ui/react-slot": "^1.2.4", 22 | "@radix-ui/react-switch": "^1.2.6", 23 | "@radix-ui/react-tabs": "^1.1.13", 24 | "@tailwindcss/vite": "^4.1.8", 25 | "@takumi-rs/core": "^0.53.1", 26 | "@takumi-rs/image-response": "^0.53.1", 27 | "@tanstack/react-form": "^1.23.5", 28 | "@tanstack/react-query": "^5.85.5", 29 | "@tanstack/react-router": "^1.139.1", 30 | "@tanstack/react-router-with-query": "^1.130.17", 31 | "@tanstack/react-start": "^1.139.1", 32 | "@tanstack/router-plugin": "^1.139.1", 33 | "class-variance-authority": "^0.7.1", 34 | "clsx": "^2.1.1", 35 | "diff": "^8.0.2", 36 | "diff-match-patch": "^1.0.5", 37 | "es-toolkit": "^1.41.0", 38 | "gitdiff-parser": "^0.3.1", 39 | "html2canvas": "^1.4.1", 40 | "intlayer": "^6.1.6", 41 | "jspdf": "^2.5.2", 42 | "localforage": "^1.10.0", 43 | "lucide-react": "^0.525.0", 44 | "nitro": "3.0.1-alpha.0", 45 | "prettier": "^3.6.2", 46 | "radix-ui": "^1.4.2", 47 | "react": "19.1.0", 48 | "react-dom": "19.1.0", 49 | "react-dropzone": "^14.3.8", 50 | "react-intlayer": "^6.1.6", 51 | "refractor": "^5.0.0", 52 | "shiki": "^3.13.0", 53 | "sonner": "^2.0.3", 54 | "svgo": "^4.0.0", 55 | "tailwind-merge": "^3.3.1", 56 | "tailwindcss": "^4.1.3", 57 | "tw-animate-css": "^1.2.5", 58 | "use-file-picker": "^2.1.4", 59 | "vite-tsconfig-paths": "^5.1.4", 60 | "zod": "^4.1.11", 61 | "zustand": "^5.0.8" 62 | }, 63 | "devDependencies": { 64 | "@content-collections/cli": "^0.1.7", 65 | "@content-collections/core": "^0.11.1", 66 | "@content-collections/mdx": "^0.2.2", 67 | "@content-collections/vite": "^0.2.7", 68 | "@iconify-json/hugeicons": "^1.2.17", 69 | "@shikijs/transformers": "^3.14.0", 70 | "@tanstack/react-query-devtools": "^5.85.5", 71 | "@tanstack/react-router-devtools": "^1.132.31", 72 | "@testing-library/dom": "^10.4.0", 73 | "@testing-library/react": "^16.2.0", 74 | "@types/diff": "^8.0.0", 75 | "@types/react": "~19.1.10", 76 | "@types/react-dom": "^19.0.4", 77 | "@vite-pwa/assets-generator": "^1.0.2", 78 | "@vitejs/plugin-react": "^5.0.4", 79 | "babel-plugin-react-compiler": "^1.0.0", 80 | "intlayer-cli": "^6.1.6", 81 | "jsdom": "^26.0.0", 82 | "rehype-autolink-headings": "^7.1.0", 83 | "rehype-pretty-code": "^0.14.1", 84 | "rehype-slug": "^6.0.0", 85 | "remark-gfm": "^4.0.1", 86 | "typescript": "^5.7.2", 87 | "vite": "^7.0.2", 88 | "vite-intlayer": "^6.1.6", 89 | "vite-plugin-pwa": "^1.1.0", 90 | "web-vitals": "^5.0.3", 91 | "workbox-window": "^7.3.0" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehai/tiny-svg/799a72af3978d4150d7b83f9423837e539cf5f03/apps/web/public/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehai/tiny-svg/799a72af3978d4150d7b83f9423837e539cf5f03/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /apps/web/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /apps/web/public/maskable-icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehai/tiny-svg/799a72af3978d4150d7b83f9423837e539cf5f03/apps/web/public/maskable-icon-512x512.png -------------------------------------------------------------------------------- /apps/web/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehai/tiny-svg/799a72af3978d4150d7b83f9423837e539cf5f03/apps/web/public/pwa-192x192.png -------------------------------------------------------------------------------- /apps/web/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehai/tiny-svg/799a72af3978d4150d7b83f9423837e539cf5f03/apps/web/public/pwa-512x512.png -------------------------------------------------------------------------------- /apps/web/public/pwa-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehai/tiny-svg/799a72af3978d4150d7b83f9423837e539cf5f03/apps/web/public/pwa-64x64.png -------------------------------------------------------------------------------- /apps/web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Allow: / 4 | 5 | # Disallow admin routes (if any in future) 6 | # Disallow: /admin/ 7 | 8 | # Sitemaps 9 | Sitemap: https://tiny-svg.actnow.dev/sitemap.xml 10 | 11 | # Crawl delay (optional, adjust if needed) 12 | # Crawl-delay: 1 13 | -------------------------------------------------------------------------------- /apps/web/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tiny SVG - SVG Optimizer & Converter", 3 | "short_name": "Tiny SVG", 4 | "description": "Free online SVG optimizer and converter. Compress SVG files up to 70%, convert to React, Vue, and Svelte components. Works offline!", 5 | "start_url": "/", 6 | "scope": "/", 7 | "display": "standalone", 8 | "orientation": "any", 9 | "background_color": "#ffffff", 10 | "theme_color": "#3b82f6", 11 | "categories": ["productivity", "utilities", "development"], 12 | "icons": [ 13 | { 14 | "src": "/pwa-64x64.png", 15 | "sizes": "64x64", 16 | "type": "image/png" 17 | }, 18 | { 19 | "src": "/pwa-192x192.png", 20 | "sizes": "192x192", 21 | "type": "image/png" 22 | }, 23 | { 24 | "src": "/pwa-512x512.png", 25 | "sizes": "512x512", 26 | "type": "image/png" 27 | }, 28 | { 29 | "src": "/maskable-icon-512x512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ], 35 | "shortcuts": [ 36 | { 37 | "name": "Optimize SVG", 38 | "short_name": "Optimize", 39 | "description": "Open SVG optimizer", 40 | "url": "/optimize", 41 | "icons": [ 42 | { 43 | "src": "/pwa-192x192.png", 44 | "sizes": "192x192", 45 | "type": "image/png" 46 | } 47 | ] 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /apps/web/public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | https://tiny-svg.actnow.dev/ 11 | 2025-01-15 12 | weekly 13 | 1.0 14 | 15 | 16 | 17 | 18 | https://tiny-svg.actnow.dev/optimize 19 | 2025-01-15 20 | monthly 21 | 0.9 22 | 23 | 24 | 25 | 26 | https://tiny-svg.actnow.dev/blog 27 | 2025-01-15 28 | weekly 29 | 0.8 30 | 31 | 32 | 33 | 34 | https://tiny-svg.actnow.dev/blog/getting-started-with-svg-optimization 35 | 2025-01-15 36 | monthly 37 | 0.7 38 | 39 | 40 | 41 | https://tiny-svg.actnow.dev/blog/svg-vs-png-when-to-use-each 42 | 2025-01-15 43 | monthly 44 | 0.7 45 | 46 | 47 | 48 | https://tiny-svg.actnow.dev/blog/advanced-svg-techniques 49 | 2025-01-15 50 | monthly 51 | 0.7 52 | 53 | 54 | 55 | https://tiny-svg.actnow.dev/blog/why-client-side-processing-matters 56 | 2025-01-15 57 | monthly 58 | 0.7 59 | 60 | 61 | 62 | 63 | https://tiny-svg.actnow.dev/about 64 | 2025-01-15 65 | monthly 66 | 0.6 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /apps/web/src/components/blocks/diff-viewer/diff-viewer.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CollapsibleCard, 3 | CollapsibleCardContent, 4 | CollapsibleCardHeader, 5 | CollapsibleCardTitle, 6 | } from "@/components/ui/collapsible-card"; 7 | import { Diff, Hunk } from "@/components/ui/diff"; 8 | 9 | import { type ParseOptions, parseDiff } from "@/components/ui/diff/utils/parse"; 10 | 11 | export function DiffViewer({ 12 | patch, 13 | options = {}, 14 | }: { 15 | patch: string; 16 | options?: Partial; 17 | }) { 18 | const [file] = parseDiff(patch, options); 19 | 20 | if (!file) { 21 | return ( 22 |
23 | No diff to display 24 |
25 | ); 26 | } 27 | 28 | return ( 29 | 36 | 37 | 38 | {file.newPath} 39 | 40 | 41 | 42 | 43 | {file.hunks.map((hunk) => ( 44 | 45 | ))} 46 | 47 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /apps/web/src/components/blur-fade/blur-fade.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import styles from "./fade.module.css"; 5 | 6 | export function Fade({ 7 | stop, 8 | blur, 9 | side = "top", 10 | className, 11 | background, 12 | style, 13 | ref, 14 | debug, 15 | }: { 16 | stop?: string; 17 | blur?: string; 18 | side: "top" | "bottom" | "left" | "right"; 19 | className?: string; 20 | background: string; 21 | debug?: boolean; 22 | style?: React.CSSProperties; 23 | ref?: React.Ref; 24 | }) { 25 | return ( 26 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /apps/web/src/components/blur-fade/fade.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | --blur: 4px; 3 | --stop: 25%; 4 | position: absolute; 5 | pointer-events: none; 6 | user-select: none; 7 | backdrop-filter: blur(var(--blur)); 8 | 9 | &[data-side="top"] { 10 | background: linear-gradient(to top, transparent, var(--background)); 11 | mask-image: linear-gradient( 12 | to bottom, 13 | var(--background) var(--stop), 14 | transparent 15 | ); 16 | } 17 | 18 | &[data-side="left"] { 19 | background: linear-gradient(to left, transparent, var(--background)); 20 | mask-image: linear-gradient( 21 | to right, 22 | var(--background) var(--stop), 23 | transparent 24 | ); 25 | } 26 | 27 | &[data-side="right"] { 28 | background: linear-gradient(to right, transparent, var(--background)); 29 | mask-image: linear-gradient( 30 | to left, 31 | var(--background) var(--stop), 32 | transparent 33 | ); 34 | } 35 | 36 | &[data-side="bottom"] { 37 | background: linear-gradient(to bottom, transparent, var(--background)); 38 | mask-image: linear-gradient( 39 | to top, 40 | var(--background) var(--stop), 41 | transparent 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/web/src/components/code-diff-viewer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Diff, Hunk } from "@/components/ui/diff"; 3 | import type { ParseOptions } from "@/components/ui/diff/utils/parse"; 4 | import { parseDiff } from "@/components/ui/diff/utils/parse"; 5 | 6 | type CodeDiffViewerProps = { 7 | original: string; 8 | modified: string; 9 | language?: string; 10 | }; 11 | 12 | // Debounce delay for content updates (in milliseconds) 13 | const DEBOUNCE_DELAY = 150; 14 | 15 | export function CodeDiffViewer({ 16 | original, 17 | modified, 18 | language = "html", 19 | }: CodeDiffViewerProps) { 20 | const [debouncedContent, setDebouncedContent] = useState({ 21 | original, 22 | modified, 23 | }); 24 | 25 | // Debounce content updates 26 | useEffect(() => { 27 | const updateTimer = setTimeout(() => { 28 | setDebouncedContent({ original, modified }); 29 | }, DEBOUNCE_DELAY); 30 | 31 | return () => { 32 | clearTimeout(updateTimer); 33 | }; 34 | }, [original, modified]); 35 | 36 | // Generate unified diff format 37 | const patch = generateUnifiedDiff( 38 | debouncedContent.original, 39 | debouncedContent.modified 40 | ); 41 | 42 | const parseOptions: Partial = { 43 | mergeModifiedLines: true, 44 | maxChangeRatio: 0.45, 45 | maxDiffDistance: 30, 46 | inlineMaxCharEdits: 2, 47 | }; 48 | 49 | const [file] = parseDiff(patch, parseOptions); 50 | 51 | if (!file) { 52 | return ( 53 |
54 | No diff to display 55 |
56 | ); 57 | } 58 | 59 | return ( 60 |
61 | 67 | {file.hunks.map((hunk) => ( 68 | 69 | ))} 70 | 71 |
72 | ); 73 | } 74 | 75 | // Helper function to generate unified diff format from two strings 76 | function generateUnifiedDiff(original: string, modified: string): string { 77 | const originalLines = original.split("\n"); 78 | const modifiedLines = modified.split("\n"); 79 | 80 | // Simple line-by-line diff 81 | const diffLines: string[] = []; 82 | diffLines.push("diff --git a/file b/file"); 83 | diffLines.push("index 0000000..0000000 100644"); 84 | diffLines.push("--- a/file"); 85 | diffLines.push("+++ b/file"); 86 | diffLines.push(`@@ -1,${originalLines.length} +1,${modifiedLines.length} @@`); 87 | 88 | // Track which lines we've processed 89 | let i = 0; 90 | let j = 0; 91 | 92 | while (i < originalLines.length || j < modifiedLines.length) { 93 | if (i < originalLines.length && j < modifiedLines.length) { 94 | if (originalLines[i] === modifiedLines[j]) { 95 | diffLines.push(` ${originalLines[i]}`); 96 | i++; 97 | j++; 98 | } else { 99 | // Lines differ - mark as deletion and addition 100 | diffLines.push(`-${originalLines[i]}`); 101 | i++; 102 | if (j < modifiedLines.length) { 103 | diffLines.push(`+${modifiedLines[j]}`); 104 | j++; 105 | } 106 | } 107 | } else if (i < originalLines.length) { 108 | // Remaining lines from original (deletions) 109 | diffLines.push(`-${originalLines[i]}`); 110 | i++; 111 | } else { 112 | // Remaining lines from modified (additions) 113 | diffLines.push(`+${modifiedLines[j]}`); 114 | j++; 115 | } 116 | } 117 | 118 | return diffLines.join("\n"); 119 | } 120 | -------------------------------------------------------------------------------- /apps/web/src/components/error-boundary.tsx: -------------------------------------------------------------------------------- 1 | import { Component, type ReactNode } from "react"; 2 | 3 | interface ErrorBoundaryState { 4 | hasError: boolean; 5 | error?: Error; 6 | } 7 | 8 | interface ErrorBoundaryProps { 9 | children: ReactNode; 10 | fallback?: (error: Error) => ReactNode; 11 | } 12 | 13 | export class ErrorBoundary extends Component< 14 | ErrorBoundaryProps, 15 | ErrorBoundaryState 16 | > { 17 | constructor(props: ErrorBoundaryProps) { 18 | super(props); 19 | this.state = { hasError: false }; 20 | } 21 | 22 | static getDerivedStateFromError(error: Error): ErrorBoundaryState { 23 | return { hasError: true, error }; 24 | } 25 | 26 | componentDidCatch(error: Error, errorInfo: any) { 27 | console.error("ErrorBoundary caught an error:", error, errorInfo); 28 | } 29 | 30 | render() { 31 | if (this.state.hasError) { 32 | if (this.props.fallback && this.state.error) { 33 | return this.props.fallback(this.state.error); 34 | } 35 | 36 | return ( 37 |
38 |
39 |

40 | Something went wrong! 41 |

42 |

43 | {this.state.error?.message || "An unexpected error occurred"} 44 |

45 | 54 |
55 |
56 | ); 57 | } 58 | 59 | return this.props.children; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /apps/web/src/components/history-button.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | type HistoryButtonProps = { 6 | onClick: () => void; 7 | count: number; 8 | label?: ReactNode; 9 | className?: string; 10 | }; 11 | 12 | export function HistoryButton({ 13 | onClick, 14 | count, 15 | label = "History", 16 | className, 17 | }: HistoryButtonProps) { 18 | if (count === 0) { 19 | return null; 20 | } 21 | 22 | return ( 23 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /apps/web/src/components/intlayer/locale-switcher.content.ts: -------------------------------------------------------------------------------- 1 | import { type Dictionary, t } from "intlayer"; 2 | 3 | const localeSwitcherContent = { 4 | content: { 5 | languageListLabel: t({ 6 | en: "Language list", 7 | zh: "语言列表", 8 | ko: "언어 목록", 9 | de: "Sprachliste", 10 | }), 11 | localeSwitcherLabel: t({ 12 | en: "Select language", 13 | zh: "选择语言", 14 | ko: "언어 선택", 15 | de: "Sprache wählen", 16 | }), 17 | }, 18 | key: "locale-switcher", 19 | } satisfies Dictionary; 20 | 21 | export default localeSwitcherContent; 22 | -------------------------------------------------------------------------------- /apps/web/src/components/intlayer/locale-switcher.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation } from "@tanstack/react-router"; 2 | import { getLocaleName, getPathWithoutLocale, Locales } from "intlayer"; 3 | import type { FC } from "react"; 4 | import { setLocaleCookie, useLocale } from "react-intlayer"; 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuItem, 9 | DropdownMenuTrigger, 10 | } from "@/components/ui/dropdown-menu"; 11 | 12 | const localeFlags: Partial> = { 13 | [Locales.ENGLISH]: "🇺🇸", 14 | [Locales.CHINESE]: "🇨🇳", 15 | [Locales.KOREAN]: "🇰🇷", 16 | [Locales.GERMAN]: "🇩🇪", 17 | [Locales.FRENCH]: "🇫🇷", 18 | }; 19 | 20 | const TRAILING_SLASH_REGEX = /\/$/; 21 | 22 | export const LocaleSwitcher: FC = () => { 23 | const { availableLocales, locale } = useLocale(); 24 | const { pathname } = useLocation(); 25 | 26 | const currentFlag = localeFlags[locale as Locales] || "🌐"; 27 | const currentLabel = getLocaleName(locale); 28 | 29 | const handleLocaleChange = (newLocale: Locales) => { 30 | // Set cookie for persistence 31 | setLocaleCookie(newLocale); 32 | 33 | // Get path without locale prefix 34 | const pathWithoutLocale = getPathWithoutLocale(pathname); 35 | 36 | // Remove trailing slash if present 37 | const cleanPath = 38 | pathWithoutLocale === "/" 39 | ? "" 40 | : pathWithoutLocale.replace(TRAILING_SLASH_REGEX, ""); 41 | 42 | // Construct new URL with locale prefix 43 | const newPath = `/${newLocale}${cleanPath}`; 44 | 45 | // Use setTimeout to ensure cookie is set before navigation 46 | // This prevents SSR hydration mismatch causing language flash 47 | setTimeout(() => { 48 | window.location.href = newPath; 49 | }, 0); 50 | }; 51 | 52 | return ( 53 | 54 | 55 | 64 | 65 | 66 | {availableLocales.map((localeEl) => ( 67 | handleLocaleChange(localeEl as Locales)} 71 | > 72 | 73 | {localeFlags[localeEl as Locales] || "🌐"} 74 | 75 | {getLocaleName(localeEl)} 76 | {locale === localeEl && ( 77 | 78 | )} 79 | 80 | ))} 81 | 82 | 83 | ); 84 | }; 85 | -------------------------------------------------------------------------------- /apps/web/src/components/intlayer/localized-link.tsx: -------------------------------------------------------------------------------- 1 | import { Link, type LinkComponentProps } from "@tanstack/react-router"; 2 | import type { FC } from "react"; 3 | import { useLocale } from "react-intlayer"; 4 | 5 | export const LOCALE_ROUTE = "{-$locale}" as const; 6 | 7 | // Main utility 8 | export type RemoveLocaleParam = T extends string 9 | ? RemoveLocaleFromString 10 | : T; 11 | 12 | export type To = RemoveLocaleParam; 13 | 14 | type CollapseDoubleSlashes = 15 | S extends `${infer H}//${infer T}` ? CollapseDoubleSlashes<`${H}/${T}`> : S; 16 | 17 | type LocalizedLinkProps = { 18 | to?: To; 19 | viewTransition?: LinkComponentProps["viewTransition"]; 20 | } & Omit; 21 | 22 | // Helpers 23 | type RemoveAll< 24 | S extends string, 25 | Sub extends string, 26 | > = S extends `${infer H}${Sub}${infer T}` ? RemoveAll<`${H}${T}`, Sub> : S; 27 | 28 | type RemoveLocaleFromString = CollapseDoubleSlashes< 29 | RemoveAll 30 | >; 31 | 32 | export const LocalizedLink: FC = (props) => { 33 | const { locale } = useLocale(); 34 | const { viewTransition, ...restProps } = props; 35 | 36 | return ( 37 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /apps/web/src/components/lazy/code-diff-viewer-lazy.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Lazy-loaded wrapper for CodeDiffViewer 3 | * Monaco Editor is heavy (~500KB), so we load it on-demand 4 | */ 5 | 6 | import { lazy, Suspense } from "react"; 7 | 8 | const CodeDiffViewerComponent = lazy(() => 9 | import("@/components/code-diff-viewer").then((mod) => ({ 10 | default: mod.CodeDiffViewer, 11 | })) 12 | ); 13 | 14 | type CodeDiffViewerProps = { 15 | original: string; 16 | modified: string; 17 | language?: string; 18 | }; 19 | 20 | export function CodeDiffViewerLazy(props: CodeDiffViewerProps) { 21 | return ( 22 | 25 |
Loading editor...
26 |
27 | } 28 | > 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /apps/web/src/components/lazy/code-viewer-lazy.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Lazy-loaded wrapper for CodeViewer 3 | * Shiki highlighter is loaded on-demand for better performance 4 | */ 5 | 6 | import { lazy, Suspense } from "react"; 7 | 8 | const CodeViewerComponent = lazy(() => 9 | import("@/components/code-viewer").then((mod) => ({ 10 | default: mod.CodeViewer, 11 | })) 12 | ); 13 | 14 | type SupportedLanguage = "javascript" | "typescript" | "html" | "dart"; 15 | 16 | type CodeViewerProps = { 17 | code: string; 18 | language: SupportedLanguage; 19 | fileName: string; 20 | }; 21 | 22 | export function CodeViewerLazy(props: CodeViewerProps) { 23 | return ( 24 | 27 |
Loading editor...
28 |
29 | } 30 | > 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /apps/web/src/components/lazy/config-panel-lazy.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Lazy-loaded wrapper for ConfigPanel 3 | * Large component with many dependencies 4 | */ 5 | 6 | import { lazy, Suspense } from "react"; 7 | 8 | const ConfigPanelComponent = lazy(() => 9 | import("@/components/config-panel").then((mod) => ({ 10 | default: mod.ConfigPanel, 11 | })) 12 | ); 13 | 14 | type ConfigPanelProps = { 15 | className?: string; 16 | isCollapsed: boolean; 17 | onToggleCollapse: () => void; 18 | }; 19 | 20 | export function ConfigPanelLazy(props: ConfigPanelProps) { 21 | return ( 22 | 25 |
Loading config...
26 |
27 | } 28 | > 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /apps/web/src/components/lazy/react-tab-content-lazy.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Lazy-loaded wrapper for ReactTabContent 3 | * Code viewer and syntax highlighter loaded on-demand for better performance 4 | */ 5 | 6 | import { lazy, Suspense } from "react"; 7 | 8 | const ReactTabContentComponent = lazy(() => 9 | import("@/components/optimize/react-tab-content").then((mod) => ({ 10 | default: mod.ReactTabContent, 11 | })) 12 | ); 13 | 14 | type ReactTabContentProps = { 15 | generatedCodes: Map; 16 | componentName: string; 17 | }; 18 | 19 | export function ReactTabContentLazy(props: ReactTabContentProps) { 20 | return ( 21 | 24 |
25 | Loading React code viewer... 26 |
27 | 28 | } 29 | > 30 | 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/src/components/loader.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from "lucide-react"; 2 | 3 | export default function Loader() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/src/components/logo.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | interface LogoProps { 4 | className?: string; 5 | } 6 | 7 | /** 8 | * Tiny SVG Logo Icon 9 | * Automatically adapts to light/dark theme via currentColor 10 | */ 11 | export function Logo({ className }: LogoProps) { 12 | return ( 13 | 22 | tiny svg 23 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/web/src/components/mdx-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { useMDXComponent } from "@content-collections/mdx/react"; 2 | import { mdxComponents } from "./mdx-components"; 3 | 4 | interface MdxProps { 5 | code: string; 6 | } 7 | 8 | export function MDX({ code }: MdxProps) { 9 | const Component = useMDXComponent(code); 10 | 11 | return ( 12 |
13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/src/components/og/base-template.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import Box from "./images/box"; 3 | import { Logo } from "./images/logo"; 4 | import Tip from "./images/tip"; 5 | 6 | export default function BaseTemplate({ 7 | title, 8 | description, 9 | site, 10 | }: { 11 | title: ReactNode; 12 | description: ReactNode; 13 | site?: ReactNode; 14 | }) { 15 | return ( 16 |
17 |
18 |
19 |
20 |
{site}
21 |
{title}
22 |

{description}

23 |
24 |
25 | 26 |
27 |
28 | 33 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /apps/web/src/components/og/images/logo.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export function Logo(props: SVGProps) { 4 | return ( 5 | 12 | logo 13 | 17 | 21 | 22 | ); 23 | } 24 | 25 | export default Logo; 26 | -------------------------------------------------------------------------------- /apps/web/src/components/optimize/code-tab-content.tsx: -------------------------------------------------------------------------------- 1 | import { CodeViewerLazy } from "@/components/lazy/code-viewer-lazy"; 2 | 3 | type SupportedLanguage = "javascript" | "typescript" | "html" | "dart"; 4 | 5 | type CodeTabContentProps = { 6 | activeTab: string; 7 | generatedCodes: Map; 8 | componentName: string; 9 | }; 10 | 11 | const codeTabConfig: Record< 12 | string, 13 | { ext: string; language: SupportedLanguage } 14 | > = { 15 | "react-jsx": { ext: "jsx", language: "javascript" }, 16 | "react-tsx": { ext: "tsx", language: "typescript" }, 17 | vue: { ext: "vue", language: "html" }, 18 | svelte: { ext: "svelte", language: "html" }, 19 | "react-native": { ext: "jsx", language: "javascript" }, 20 | flutter: { ext: "dart", language: "dart" }, 21 | }; 22 | 23 | export function CodeTabContent({ 24 | activeTab, 25 | generatedCodes, 26 | componentName, 27 | }: CodeTabContentProps) { 28 | const config = codeTabConfig[activeTab as keyof typeof codeTabConfig]; 29 | if (!config) { 30 | return null; 31 | } 32 | 33 | const code = generatedCodes.get(activeTab); 34 | if (!code) { 35 | return ( 36 |
37 | Generating code... 38 |
39 | ); 40 | } 41 | 42 | return ( 43 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /apps/web/src/components/optimize/compact-upload-button.tsx: -------------------------------------------------------------------------------- 1 | import { useIntlayer } from "react-intlayer"; 2 | import { useFilePicker } from "use-file-picker"; 3 | import { Button } from "@/components/ui/button"; 4 | import { isSvgFile } from "@/lib/file-utils"; 5 | 6 | type CompactUploadButtonProps = { 7 | onUpload: (file: File) => void; 8 | className?: string; 9 | }; 10 | 11 | export function CompactUploadButton({ 12 | onUpload, 13 | className, 14 | }: CompactUploadButtonProps) { 15 | const { header } = useIntlayer("optimize"); 16 | 17 | const { openFilePicker, loading } = useFilePicker({ 18 | accept: ".svg", 19 | multiple: false, 20 | onFilesSelected: (data: { 21 | plainFiles?: File[]; 22 | filesContent?: unknown[]; 23 | errors?: unknown[]; 24 | }) => { 25 | if (data.plainFiles && data.plainFiles.length > 0) { 26 | const file = data.plainFiles[0]; 27 | if (file && isSvgFile(file)) { 28 | onUpload(file); 29 | } 30 | } 31 | }, 32 | }); 33 | 34 | return ( 35 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /apps/web/src/components/optimize/index.ts: -------------------------------------------------------------------------------- 1 | export { CodeTabContent } from "./code-tab-content"; 2 | export { CompactUploadButton } from "./compact-upload-button"; 3 | export { OptimizeHeader } from "./optimize-header"; 4 | -------------------------------------------------------------------------------- /apps/web/src/components/optimize/optimize-header.tsx: -------------------------------------------------------------------------------- 1 | import { useIntlayer } from "react-intlayer"; 2 | import { HistoryButton } from "@/components/history-button"; 3 | import { Button } from "@/components/ui/button"; 4 | import { Separator } from "@/components/ui/separator"; 5 | import { formatBytes } from "@/lib/svgo-config"; 6 | import { CompactUploadButton } from "./compact-upload-button"; 7 | 8 | type OptimizeHeaderProps = { 9 | fileName: string; 10 | originalSize: number; 11 | compressedSize: number; 12 | compressionRate: number; 13 | compressedSvg: string; 14 | historyCount: number; 15 | onCopy: () => void; 16 | onDownload: () => void; 17 | onFileUpload: (file: File) => void; 18 | onToggleHistoryPanel: () => void; 19 | isSettingsCollapsed?: boolean; 20 | onToggleSettings?: () => void; 21 | }; 22 | 23 | export function OptimizeHeader({ 24 | fileName, 25 | originalSize, 26 | compressedSize, 27 | compressionRate, 28 | compressedSvg, 29 | historyCount, 30 | onCopy, 31 | onDownload, 32 | onFileUpload, 33 | onToggleHistoryPanel, 34 | isSettingsCollapsed, 35 | onToggleSettings, 36 | }: OptimizeHeaderProps) { 37 | const { header } = useIntlayer("optimize"); 38 | 39 | return ( 40 |
41 |
42 |
43 | 49 | 50 |
51 |

52 | {header.title} 53 |

54 |

55 | {fileName} 56 |

57 |
58 |
59 |
60 |
61 |
62 | {formatBytes(originalSize)} → {formatBytes(compressedSize)} 63 |
64 | {compressionRate > 0 && ( 65 |
66 | -{compressionRate.toFixed(1)}% 67 |
68 | )} 69 |
70 | 79 | 83 | {isSettingsCollapsed && onToggleSettings && ( 84 | <> 85 | 86 | 95 | 96 | )} 97 |
98 |
99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /apps/web/src/components/pwa/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PWA Components Index 3 | * Centralized exports for all PWA-related components 4 | */ 5 | 6 | export { InstallButton, InstallPrompt } from "./install-prompt"; 7 | export { 8 | OnlineStatusBadge, 9 | OnlineStatusIndicator, 10 | } from "./online-status-indicator"; 11 | export { PWAUpdatePrompt } from "./pwa-update-prompt"; 12 | -------------------------------------------------------------------------------- /apps/web/src/components/pwa/install-prompt.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * PWA Install Prompt Component 3 | * Allows users to install the app to their device 4 | */ 5 | 6 | import { Download, X } from "lucide-react"; 7 | import { useState } from "react"; 8 | 9 | import { useInstallPrompt } from "@/lib/pwa-utils"; 10 | import { cn } from "@/lib/utils"; 11 | 12 | import { Button } from "../ui/button"; 13 | 14 | export function InstallPrompt() { 15 | const { showInstallButton, promptInstall, dismissInstall } = 16 | useInstallPrompt(); 17 | const [isVisible, setIsVisible] = useState(true); 18 | 19 | if (!(showInstallButton && isVisible)) { 20 | return null; 21 | } 22 | 23 | const handleInstall = async () => { 24 | const outcome = await promptInstall(); 25 | if (outcome === "accepted") { 26 | setIsVisible(false); 27 | } 28 | }; 29 | 30 | const handleDismiss = () => { 31 | dismissInstall(); 32 | setIsVisible(false); 33 | }; 34 | 35 | return ( 36 |
37 |
38 |
39 |
40 |
41 |
42 | 43 |
44 |
45 |

Install Tiny SVG

46 |

47 | Quick access from your home screen 48 |

49 |
50 |
51 |
52 | 55 | 58 |
59 |
60 | 68 |
69 |
70 |
71 | ); 72 | } 73 | 74 | /** 75 | * Inline install button for header/toolbar 76 | */ 77 | export function InstallButton({ className }: { className?: string }) { 78 | const { showInstallButton, promptInstall } = useInstallPrompt(); 79 | 80 | if (!showInstallButton) { 81 | return null; 82 | } 83 | 84 | return ( 85 | 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /apps/web/src/components/pwa/online-status-indicator.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Online Status Indicator Component 3 | * Shows connection status and notifies users when offline 4 | */ 5 | 6 | import { Cloud, CloudOff, Wifi, WifiOff } from "lucide-react"; 7 | import { useEffect, useRef } from "react"; 8 | import { toast } from "sonner"; 9 | 10 | import { useOnlineStatus } from "@/lib/pwa-utils"; 11 | import { cn } from "@/lib/utils"; 12 | 13 | export function OnlineStatusIndicator() { 14 | const isOnline = useOnlineStatus(); 15 | const previousStatusRef = useRef(true); 16 | 17 | useEffect(() => { 18 | // Only show toast after initial render and when status actually changes 19 | if (previousStatusRef.current !== isOnline) { 20 | if (isOnline) { 21 | toast.success("Back Online", { 22 | description: "Your internet connection has been restored.", 23 | icon: , 24 | duration: 3000, 25 | }); 26 | } else { 27 | toast.warning("Working Offline", { 28 | description: 29 | "No internet connection. SVG optimization still works offline!", 30 | icon: , 31 | duration: 5000, 32 | }); 33 | } 34 | } 35 | previousStatusRef.current = isOnline; 36 | }, [isOnline]); 37 | 38 | return ( 39 |
48 | {isOnline ? ( 49 | 50 | ) : ( 51 | 52 | )} 53 | 54 | {isOnline ? "Online" : "Offline"} 55 | 56 |
57 | ); 58 | } 59 | 60 | /** 61 | * Simplified version for header/footer 62 | */ 63 | export function OnlineStatusBadge() { 64 | const isOnline = useOnlineStatus(); 65 | 66 | if (isOnline) { 67 | return null; // Don't show anything when online 68 | } 69 | 70 | return ( 71 |
75 | 76 | Offline 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /apps/web/src/components/pwa/pwa-update-prompt.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * PWA Update Prompt Component 3 | * Shows a notification when a new version of the app is available 4 | */ 5 | 6 | import { X } from "lucide-react"; 7 | import { useEffect } from "react"; 8 | import { toast } from "sonner"; 9 | 10 | import { usePWA } from "@/lib/pwa-utils"; 11 | 12 | import { Button } from "../ui/button"; 13 | 14 | export function PWAUpdatePrompt() { 15 | const { needRefresh, updateServiceWorker, closePrompt } = usePWA(); 16 | 17 | useEffect(() => { 18 | if (needRefresh) { 19 | // Show a toast notification for updates 20 | toast( 21 |
22 |
23 |

Update Available

24 |

25 | A new version of Tiny SVG is ready to install 26 |

27 |
28 |
29 | 38 | 48 |
49 |
, 50 | { 51 | duration: Number.POSITIVE_INFINITY, // Keep toast visible until user acts 52 | closeButton: false, 53 | } 54 | ); 55 | } 56 | }, [needRefresh, updateServiceWorker, closePrompt]); 57 | 58 | return null; // This component renders via toast 59 | } 60 | -------------------------------------------------------------------------------- /apps/web/src/components/recent-svgs.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "@tanstack/react-router"; 2 | import { useCallback } from "react"; 3 | import { getCardRotation, getStaggerDelay } from "@/lib/animation-utils"; 4 | import { HISTORY_CONSTANTS } from "@/lib/constants/history"; 5 | import { cn } from "@/lib/utils"; 6 | import { useSvgStore } from "@/store/svg-store"; 7 | import type { HistoryEntry } from "@/types/history"; 8 | import { SvgThumbnail } from "./svg-thumbnail"; 9 | 10 | type RecentSvgsProps = { 11 | entries: HistoryEntry[]; 12 | className?: string; 13 | }; 14 | 15 | export function RecentSvgs({ entries, className }: RecentSvgsProps) { 16 | const router = useRouter(); 17 | const { setHistoryEntry } = useSvgStore(); 18 | 19 | const handleSelectEntry = useCallback( 20 | (entry: HistoryEntry) => { 21 | setHistoryEntry({ 22 | compressedSvg: entry.compressedSvg, 23 | fileName: entry.fileName, 24 | originalSvg: entry.originalSvg, 25 | }); 26 | router.navigate({ to: "/{-$locale}/optimize" }); 27 | }, 28 | [router, setHistoryEntry] 29 | ); 30 | 31 | if (entries.length === 0) { 32 | return null; 33 | } 34 | 35 | return ( 36 |
37 | {entries.map((entry, index) => ( 38 | 63 | ))} 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /apps/web/src/components/svg-thumbnail.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | type SvgThumbnailProps = { 4 | svg: string; 5 | variant?: "fill" | "contain"; 6 | className?: string; 7 | ariaLabel?: string; 8 | }; 9 | 10 | export function SvgThumbnail({ 11 | svg, 12 | variant = "contain", 13 | className, 14 | ariaLabel = "SVG thumbnail preview", 15 | }: SvgThumbnailProps) { 16 | const wrapperClass = 17 | variant === "fill" ? "svg-thumbnail-fill" : "svg-thumbnail-container"; 18 | 19 | return ( 20 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import type * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const badgeVariants = cva( 8 | "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border px-2 py-0.5 font-medium text-xs transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", 14 | secondary: 15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", 16 | destructive: 17 | "border-transparent bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", 18 | outline: 19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | } 26 | ); 27 | 28 | function Badge({ 29 | className, 30 | variant, 31 | asChild = false, 32 | ...props 33 | }: React.ComponentProps<"span"> & 34 | VariantProps & { asChild?: boolean }) { 35 | const Comp = asChild ? Slot : "span"; 36 | 37 | return ( 38 | 43 | ); 44 | } 45 | 46 | export { Badge, badgeVariants }; 47 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from "class-variance-authority"; 2 | import { Slot as SlotPrimitive } from "radix-ui"; 3 | import type * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ); 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean; 47 | }) { 48 | const Comp = asChild ? SlotPrimitive.Slot : "button"; 49 | 50 | return ( 51 | 56 | ); 57 | } 58 | 59 | export { Button, buttonVariants }; 60 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ); 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ); 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ); 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ); 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ); 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ); 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ); 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | }; 93 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { CheckIcon } from "lucide-react"; 2 | import { Checkbox as CheckboxPrimitive } from "radix-ui"; 3 | import type * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | function Checkbox({ 8 | className, 9 | ...props 10 | }: React.ComponentProps) { 11 | return ( 12 | 20 | 24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | export { Checkbox }; 31 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/code-actions-toolbar.tsx: -------------------------------------------------------------------------------- 1 | import { toast } from "sonner"; 2 | import { Button } from "@/components/ui/button"; 3 | import { copyToClipboard, downloadFile } from "@/lib/file-utils"; 4 | import type { SupportedLanguage } from "@/lib/worker-utils/prettier-worker-client"; 5 | import { prettierWorkerClient } from "@/lib/worker-utils/prettier-worker-client"; 6 | 7 | type CodeActionsToolbarProps = { 8 | code: string; 9 | fileName: string; 10 | language?: SupportedLanguage; 11 | onCodeChange?: (code: string) => void; 12 | showPrettify?: boolean; 13 | showCopy?: boolean; 14 | showDownload?: boolean; 15 | isPrettified?: boolean; 16 | onPrettifyStateChange?: (isPrettified: boolean) => void; 17 | }; 18 | 19 | export function CodeActionsToolbar({ 20 | code, 21 | fileName, 22 | language, 23 | onCodeChange, 24 | showPrettify = true, 25 | showCopy = true, 26 | showDownload = true, 27 | isPrettified = false, 28 | onPrettifyStateChange, 29 | }: CodeActionsToolbarProps) { 30 | const handlePrettify = async () => { 31 | if (!language) { 32 | toast.error("No language specified for formatting"); 33 | return; 34 | } 35 | 36 | try { 37 | const prettified = await prettierWorkerClient.format(code, language); 38 | onCodeChange?.(prettified); 39 | onPrettifyStateChange?.(true); 40 | toast.success("Code formatted successfully!"); 41 | } catch { 42 | toast.error("Failed to format code"); 43 | } 44 | }; 45 | 46 | const handleCopy = async () => { 47 | try { 48 | await copyToClipboard(code); 49 | toast.success("Code copied to clipboard!"); 50 | } catch { 51 | toast.error("Failed to copy code"); 52 | } 53 | }; 54 | 55 | const handleDownload = () => { 56 | try { 57 | downloadFile(code, fileName); 58 | toast.success(`Downloaded ${fileName}`); 59 | } catch { 60 | toast.error("Failed to download file"); 61 | } 62 | }; 63 | 64 | return ( 65 |
66 | {showPrettify && language && ( 67 | 77 | )} 78 | {showCopy && ( 79 | 83 | )} 84 | {showDownload && ( 85 | 94 | )} 95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/copy-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Check, Copy } from "lucide-react"; 4 | import type React from "react"; 5 | import { useState } from "react"; 6 | import { Button } from "@/components/ui/button"; 7 | import { cn } from "@/lib/utils"; 8 | 9 | const COPY_FEEDBACK_TIMEOUT_MS = 2000; 10 | 11 | export const CopyButton = ({ 12 | value, 13 | className, 14 | ...props 15 | }: { 16 | value: string; 17 | className?: string; 18 | } & React.ComponentProps<"button">) => { 19 | const [copied, setCopied] = useState(false); 20 | 21 | const handleCopy = (e: React.MouseEvent) => { 22 | if (!value) { 23 | return; 24 | } 25 | e.stopPropagation(); 26 | navigator.clipboard.writeText(value); 27 | setCopied(true); 28 | setTimeout(() => { 29 | setCopied(false); 30 | }, COPY_FEEDBACK_TIMEOUT_MS); 31 | }; 32 | 33 | return ( 34 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/diff/utils/guess-lang.ts: -------------------------------------------------------------------------------- 1 | const extToLang: Record = { 2 | // JavaScript/TypeScript 3 | js: "javascript", 4 | jsx: "jsx", 5 | ts: "typescript", 6 | tsx: "tsx", 7 | mjs: "javascript", 8 | cjs: "javascript", 9 | 10 | // Web 11 | html: "markup", 12 | htm: "markup", 13 | xml: "markup", 14 | svg: "markup", 15 | css: "css", 16 | scss: "scss", 17 | sass: "sass", 18 | less: "less", 19 | stylus: "stylus", 20 | 21 | // Python 22 | py: "python", 23 | pyw: "python", 24 | pyi: "python", 25 | 26 | // Java/JVM 27 | java: "java", 28 | kt: "kotlin", 29 | kts: "kotlin", 30 | scala: "scala", 31 | groovy: "groovy", 32 | 33 | // C/C++ 34 | c: "c", 35 | cpp: "cpp", 36 | cc: "cpp", 37 | cxx: "cpp", 38 | h: "cpp", 39 | hpp: "cpp", 40 | hh: "cpp", 41 | hxx: "cpp", 42 | 43 | // C#/.NET 44 | cs: "csharp", 45 | vb: "vbnet", 46 | fs: "fsharp", 47 | 48 | // Rust 49 | rs: "rust", 50 | 51 | // Go 52 | go: "go", 53 | 54 | // Ruby 55 | rb: "ruby", 56 | rake: "ruby", 57 | 58 | // PHP 59 | php: "php", 60 | phtml: "php", 61 | 62 | // Shell 63 | sh: "bash", 64 | bash: "bash", 65 | zsh: "bash", 66 | fish: "bash", 67 | 68 | // Data formats 69 | json: "json", 70 | json5: "json5", 71 | yml: "yaml", 72 | yaml: "yaml", 73 | toml: "toml", 74 | ini: "ini", 75 | csv: "csv", 76 | 77 | // Markdown/Docs 78 | md: "markdown", 79 | markdown: "markdown", 80 | tex: "latex", 81 | 82 | // Swift/Objective-C 83 | swift: "swift", 84 | m: "objectivec", 85 | mm: "objectivec", 86 | 87 | // SQL 88 | sql: "sql", 89 | 90 | // Other languages 91 | r: "r", 92 | lua: "lua", 93 | perl: "perl", 94 | pl: "perl", 95 | dart: "dart", 96 | elm: "elm", 97 | ex: "elixir", 98 | exs: "elixir", 99 | erl: "erlang", 100 | clj: "clojure", 101 | cljs: "clojure", 102 | lisp: "lisp", 103 | hs: "haskell", 104 | ml: "ocaml", 105 | 106 | // Config files 107 | dockerfile: "docker", 108 | gitignore: "ignore", 109 | 110 | // Other 111 | graphql: "graphql", 112 | proto: "protobuf", 113 | wasm: "wasm", 114 | vim: "vim", 115 | zig: "zig", 116 | mermaid: "mermaid", 117 | }; 118 | 119 | export const guessLang = (filename?: string): string => { 120 | const ext = filename?.split(".").pop()?.toLowerCase() ?? ""; 121 | return extToLang[ext] ?? "tsx"; 122 | }; 123 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/diff/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./guess-lang"; 2 | export * from "./parse"; 3 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ); 19 | } 20 | 21 | export { Input }; 22 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import { Label as LabelPrimitive } from "radix-ui"; 2 | import type * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | function Label({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ); 20 | } 21 | 22 | export { Label }; 23 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 2 | import * as React from "react"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Separator = React.forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >( 9 | ( 10 | { className, orientation = "horizontal", decorative = true, ...props }, 11 | ref 12 | ) => ( 13 | 24 | ) 25 | ); 26 | Separator.displayName = SeparatorPrimitive.Root.displayName; 27 | 28 | export { Separator }; 29 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ); 11 | } 12 | 13 | export { Skeleton }; 14 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Toaster as Sonner, type ToasterProps } from "sonner"; 4 | import { useTheme } from "@/components/theme-provider"; 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme(); 8 | 9 | return ( 10 | 22 | ); 23 | }; 24 | 25 | export { Toaster }; 26 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as SwitchPrimitive from "@radix-ui/react-switch"; 2 | import { forwardRef } from "react"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Switch = forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, ...props }, ref) => ( 9 | 17 | 22 | 23 | )); 24 | Switch.displayName = SwitchPrimitive.Root.displayName; 25 | 26 | export { Switch }; 27 | -------------------------------------------------------------------------------- /apps/web/src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 2 | import { forwardRef } from "react"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Tabs = TabsPrimitive.Root; 6 | 7 | const TabsList = forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | )); 20 | TabsList.displayName = TabsPrimitive.List.displayName; 21 | 22 | const TabsTrigger = forwardRef< 23 | React.ElementRef, 24 | React.ComponentPropsWithoutRef 25 | >(({ className, ...props }, ref) => ( 26 | 34 | )); 35 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 36 | 37 | const TabsContent = forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({ className, ...props }, ref) => ( 41 | 49 | )); 50 | TabsContent.displayName = TabsPrimitive.Content.displayName; 51 | 52 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 53 | -------------------------------------------------------------------------------- /apps/web/src/contents/blog.content.ts: -------------------------------------------------------------------------------- 1 | import { type Dictionary, t } from "intlayer"; 2 | 3 | const blogContent = { 4 | key: "blog", 5 | content: { 6 | backToAllPosts: t({ 7 | en: "Back to all posts", 8 | zh: "返回所有文章", 9 | ko: "모든 게시물로 돌아가기", 10 | de: "Zurück zu allen Beiträgen", 11 | fr: "Retour à tous les articles", 12 | }), 13 | readMore: t({ 14 | en: "Read more", 15 | zh: "阅读更多", 16 | ko: "더 읽기", 17 | de: "Mehr lesen", 18 | fr: "Lire la suite", 19 | }), 20 | copy: t({ 21 | en: "Copy", 22 | zh: "复制", 23 | ko: "복사", 24 | de: "Kopieren", 25 | fr: "Copier", 26 | }), 27 | copied: t({ 28 | en: "Copied!", 29 | zh: "已复制!", 30 | ko: "복사됨!", 31 | de: "Kopiert!", 32 | fr: "Copié !", 33 | }), 34 | }, 35 | } satisfies Dictionary; 36 | 37 | export default blogContent; 38 | -------------------------------------------------------------------------------- /apps/web/src/contents/header.content.ts: -------------------------------------------------------------------------------- 1 | import { type Dictionary, t } from "intlayer"; 2 | 3 | const headerContent = { 4 | key: "header", 5 | content: { 6 | nav: { 7 | home: t({ 8 | en: "Home", 9 | zh: "首页", 10 | ko: "홈", 11 | de: "Startseite", 12 | fr: "Accueil", 13 | }), 14 | optimize: t({ 15 | en: "Optimize", 16 | zh: "优化", 17 | ko: "최적화", 18 | de: "Optimieren", 19 | fr: "Optimiser", 20 | }), 21 | blog: t({ 22 | en: "Blog", 23 | zh: "博客", 24 | ko: "블로그", 25 | de: "Blog", 26 | fr: "Blog", 27 | }), 28 | about: t({ 29 | en: "About", 30 | zh: "关于", 31 | ko: "정보", 32 | de: "Über", 33 | fr: "À propos", 34 | }), 35 | }, 36 | }, 37 | } satisfies Dictionary; 38 | 39 | export default headerContent; 40 | -------------------------------------------------------------------------------- /apps/web/src/contents/seo.content.ts: -------------------------------------------------------------------------------- 1 | import { type Dictionary, t } from "intlayer"; 2 | 3 | const seoContent = { 4 | key: "seo", 5 | content: { 6 | title: t({ 7 | en: "Tiny SVG - Optimize SVGs & Convert to React, Vue, Svelte Components", 8 | zh: "Tiny SVG - 优化 SVG 并转换为 React、Vue、Svelte 组件", 9 | ko: "Tiny SVG - SVG 최적화 및 React, Vue, Svelte 컴포넌트로 변환", 10 | de: "Tiny SVG - SVGs optimieren & in React, Vue, Svelte Komponenten konvertieren", 11 | fr: "Tiny SVG - Optimisez vos SVG et convertissez-les en composants React, Vue, Svelte", 12 | }), 13 | description: t({ 14 | en: "Free online SVG optimizer and converter. Compress SVG files up to 70%, convert to React, Vue, and Svelte components. All processing happens in your browser - secure and fast!", 15 | zh: "免费在线 SVG 优化和转换器。压缩 SVG 文件高达 70%,转换为 React、Vue 和 Svelte 组件。所有处理都在您的浏览器中进行 - 安全快速!", 16 | ko: "무료 온라인 SVG 최적화 및 변환 도구. SVG 파일을 최대 70% 압축하고 React, Vue, Svelte 컴포넌트로 변환. 모든 처리는 브라우저에서 이루어집니다 - 안전하고 빠름!", 17 | de: "Kostenloser Online-SVG-Optimierer und -Konverter. Komprimieren Sie SVG-Dateien um bis zu 70%, konvertieren Sie sie in React-, Vue- und Svelte-Komponenten. Die gesamte Verarbeitung erfolgt in Ihrem Browser - sicher und schnell!", 18 | fr: "Optimiseur et convertisseur SVG en ligne gratuit. Compressez vos fichiers SVG jusqu'à 70%, convertissez-les en composants React, Vue et Svelte. Tout le traitement se fait dans votre navigateur - sécurisé et rapide !", 19 | }), 20 | keywords: t({ 21 | en: "SVG optimizer, SVG compressor, SVG to React, SVG to Vue, SVG to Svelte, minify SVG, optimize SVG online, SVG converter, web performance", 22 | zh: "SVG 优化器, SVG 压缩器, SVG 转 React, SVG 转 Vue, SVG 转 Svelte, 压缩 SVG, 在线优化 SVG, SVG 转换器, 网页性能", 23 | ko: "SVG 최적화, SVG 압축, SVG to React, SVG to Vue, SVG to Svelte, SVG 압축, 온라인 SVG 최적화, SVG 변환기, 웹 성능", 24 | de: "SVG-Optimierer, SVG-Kompressor, SVG zu React, SVG zu Vue, SVG zu Svelte, SVG minimieren, SVG online optimieren, SVG-Konverter, Web-Performance", 25 | fr: "Optimiseur SVG, compresseur SVG, SVG vers React, SVG vers Vue, SVG vers Svelte, minifier SVG, optimiser SVG en ligne, convertisseur SVG, performance web", 26 | }), 27 | ogTitle: t({ 28 | en: "Tiny SVG - Optimize SVGs & Convert to React, Vue, Svelte", 29 | zh: "Tiny SVG - 优化 SVG 并转换为 React、Vue、Svelte", 30 | ko: "Tiny SVG - SVG 최적화 및 React, Vue, Svelte로 변환", 31 | de: "Tiny SVG - SVGs optimieren & in React, Vue, Svelte konvertieren", 32 | fr: "Tiny SVG - Optimisez vos SVG et convertissez-les en React, Vue, Svelte", 33 | }), 34 | ogDescription: t({ 35 | en: "Free online SVG optimizer and converter. Compress SVG files up to 70%, convert to React, Vue, and Svelte components.", 36 | zh: "免费在线 SVG 优化和转换器。压缩 SVG 文件高达 70%,转换为 React、Vue 和 Svelte 组件。", 37 | ko: "무료 온라인 SVG 최적화 및 변환 도구. SVG 파일을 최대 70% 압축하고 React, Vue, Svelte 컴포넌트로 변환.", 38 | de: "Kostenloser Online-SVG-Optimierer und -Konverter. Komprimieren Sie SVG-Dateien um bis zu 70%, konvertieren Sie sie in React-, Vue- und Svelte-Komponenten.", 39 | fr: "Optimiseur et convertisseur SVG en ligne gratuit. Compressez vos fichiers SVG jusqu'à 70%, convertissez-les en composants React, Vue et Svelte.", 40 | }), 41 | language: t({ 42 | en: "English", 43 | zh: "中文", 44 | ko: "한국어", 45 | de: "Deutsch", 46 | fr: "Français", 47 | }), 48 | author: t({ 49 | en: "Tiny SVG", 50 | zh: "Tiny SVG", 51 | ko: "Tiny SVG", 52 | de: "Tiny SVG", 53 | fr: "Tiny SVG", 54 | }), 55 | }, 56 | } satisfies Dictionary; 57 | 58 | export default seoContent; 59 | -------------------------------------------------------------------------------- /apps/web/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useAutoCompress } from "./use-auto-compress"; 2 | export { useAutoTabSwitch } from "./use-auto-tab-switch"; 3 | export { useCodeGeneration } from "./use-code-generation"; 4 | export { useDragAndDrop } from "./use-drag-and-drop"; 5 | export { useLocalStorage } from "./use-local-storage"; 6 | export { usePasteHandler } from "./use-paste-handler"; 7 | export { usePrettifiedSvg } from "./use-prettified-svg"; 8 | export { useSvgPanZoom } from "./use-svg-pan-zoom"; 9 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-auto-compress.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import { toast } from "sonner"; 3 | import { buildSvgoConfig } from "@/lib/svgo-config"; 4 | import type { SvgoGlobalSettings, SvgoPluginConfig } from "@/lib/svgo-plugins"; 5 | import { svgoWorkerClient } from "@/lib/worker-utils/svgo-worker-client"; 6 | 7 | export function useAutoCompress( 8 | originalSvg: string, 9 | plugins: SvgoPluginConfig[], 10 | globalSettings: SvgoGlobalSettings, 11 | setCompressedSvg: (svg: string) => void 12 | ) { 13 | const [isCompressing, setIsCompressing] = useState(false); 14 | 15 | const handleCompress = useCallback(async () => { 16 | if (!originalSvg) { 17 | return; 18 | } 19 | 20 | setIsCompressing(true); 21 | 22 | try { 23 | const config = buildSvgoConfig(plugins, globalSettings); 24 | const result = await svgoWorkerClient.compress(originalSvg, config); 25 | setCompressedSvg(result); 26 | } catch (_error) { 27 | toast.error("Failed to optimize SVG"); 28 | } finally { 29 | setIsCompressing(false); 30 | } 31 | }, [originalSvg, plugins, globalSettings, setCompressedSvg]); 32 | 33 | useEffect(() => { 34 | if (originalSvg) { 35 | handleCompress(); 36 | } 37 | }, [originalSvg, handleCompress]); 38 | 39 | return { isCompressing }; 40 | } 41 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-auto-tab-switch.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useAutoTabSwitch( 4 | compressedSvg: string, 5 | setActiveTab: (tab: string) => void 6 | ) { 7 | const [hasAutoSwitchedTab, setHasAutoSwitchedTab] = useState(false); 8 | 9 | useEffect(() => { 10 | if (compressedSvg && !hasAutoSwitchedTab) { 11 | setActiveTab("optimized"); 12 | setHasAutoSwitchedTab(true); 13 | } 14 | }, [compressedSvg, hasAutoSwitchedTab, setActiveTab]); 15 | 16 | return [hasAutoSwitchedTab, setHasAutoSwitchedTab] as const; 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-code-generation.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { prepareSvgDataForWorker } from "@/lib/svg-to-code"; 3 | import { 4 | codeGeneratorWorkerClient, 5 | type GeneratorType, 6 | } from "@/lib/worker-utils/code-generator-worker-client"; 7 | 8 | export function useCodeGeneration( 9 | activeTab: string, 10 | compressedSvg: string, 11 | fileName: string 12 | ) { 13 | const [generatedCodes, setGeneratedCodes] = useState>( 14 | new Map() 15 | ); 16 | const [isGenerating, setIsGenerating] = useState(false); 17 | 18 | useEffect(() => { 19 | if (!(compressedSvg && activeTab)) { 20 | return; 21 | } 22 | 23 | const validGeneratorTypes: GeneratorType[] = [ 24 | "react-jsx", 25 | "react-tsx", 26 | "vue", 27 | "svelte", 28 | "react-native", 29 | "flutter", 30 | ]; 31 | 32 | const generateCode = async () => { 33 | setIsGenerating(true); 34 | 35 | try { 36 | // Prepare SVG data in main thread (DOM parsing) 37 | const svgData = prepareSvgDataForWorker(compressedSvg, fileName); 38 | 39 | // If "react" tab is active, generate both JSX and TSX 40 | if (activeTab === "react") { 41 | const [jsxCode, tsxCode] = await Promise.all([ 42 | codeGeneratorWorkerClient.generate( 43 | "react-jsx", 44 | svgData, 45 | compressedSvg 46 | ), 47 | codeGeneratorWorkerClient.generate( 48 | "react-tsx", 49 | svgData, 50 | compressedSvg 51 | ), 52 | ]); 53 | 54 | setGeneratedCodes((prev) => { 55 | const newMap = new Map(prev); 56 | newMap.set("react-jsx", jsxCode); 57 | newMap.set("react-tsx", tsxCode); 58 | return newMap; 59 | }); 60 | } else if (validGeneratorTypes.includes(activeTab as GeneratorType)) { 61 | // Generate code for other tabs 62 | const code = await codeGeneratorWorkerClient.generate( 63 | activeTab as GeneratorType, 64 | svgData, 65 | compressedSvg 66 | ); 67 | 68 | setGeneratedCodes((prev) => new Map(prev).set(activeTab, code)); 69 | } 70 | } catch { 71 | // Code generation failed silently 72 | } finally { 73 | setIsGenerating(false); 74 | } 75 | }; 76 | 77 | // Check if we need to generate code 78 | if ( 79 | activeTab === "react" || 80 | validGeneratorTypes.includes(activeTab as GeneratorType) 81 | ) { 82 | generateCode(); 83 | } 84 | }, [activeTab, compressedSvg, fileName]); 85 | 86 | return { generatedCodes, isGenerating }; 87 | } 88 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-drag-and-drop.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | type UseDragAndDropOptions = { 4 | onFileDrop?: (file: File) => void; 5 | }; 6 | 7 | export function useDragAndDrop(options?: UseDragAndDropOptions) { 8 | const [isDragging, setIsDragging] = useState(false); 9 | 10 | useEffect(() => { 11 | const handleDragEnter = (e: DragEvent) => { 12 | e.preventDefault(); 13 | setIsDragging(true); 14 | }; 15 | 16 | const handleDragLeave = (e: DragEvent) => { 17 | e.preventDefault(); 18 | if (e.target === document.body) { 19 | setIsDragging(false); 20 | } 21 | }; 22 | 23 | const handleDragOver = (e: DragEvent) => { 24 | e.preventDefault(); 25 | }; 26 | 27 | const handleDrop = (e: DragEvent) => { 28 | e.preventDefault(); 29 | setIsDragging(false); 30 | 31 | // Handle dropped files 32 | if (options?.onFileDrop && e.dataTransfer?.files.length) { 33 | const file = e.dataTransfer.files[0]; 34 | if (file) { 35 | options.onFileDrop(file); 36 | } 37 | } 38 | }; 39 | 40 | document.addEventListener("dragenter", handleDragEnter); 41 | document.addEventListener("dragleave", handleDragLeave); 42 | document.addEventListener("dragover", handleDragOver); 43 | document.addEventListener("drop", handleDrop); 44 | 45 | return () => { 46 | document.removeEventListener("dragenter", handleDragEnter); 47 | document.removeEventListener("dragleave", handleDragLeave); 48 | document.removeEventListener("dragover", handleDragOver); 49 | document.removeEventListener("drop", handleDrop); 50 | }; 51 | }, [options]); 52 | 53 | return isDragging; 54 | } 55 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-i18n-html-attrs.ts: -------------------------------------------------------------------------------- 1 | import { getHTMLTextDir } from "intlayer"; 2 | import { useEffect } from "react"; 3 | import { useLocale } from "react-intlayer"; 4 | 5 | /** 6 | * Updates the HTML element's `lang` and `dir` attributes based on the current locale. 7 | * - `lang`: Informs browsers and search engines of the page's language. 8 | * - `dir`: Ensures the correct reading order (e.g., 'ltr' for English, 'rtl' for Arabic). 9 | * 10 | * This dynamic update is essential for proper text rendering, accessibility, and SEO. 11 | */ 12 | export const useI18nHTMLAttributes = () => { 13 | const { locale } = useLocale(); 14 | 15 | useEffect(() => { 16 | // Update the language attribute to the current locale. 17 | document.documentElement.lang = locale; 18 | 19 | // Set the text direction based on the current locale. 20 | document.documentElement.dir = getHTMLTextDir(locale); 21 | }, [locale]); 22 | }; 23 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-local-storage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | /** 4 | * Hook to persist state in localStorage 5 | * Handles SSR safely by checking for window availability 6 | */ 7 | export function useLocalStorage( 8 | key: string, 9 | initialValue: T 10 | ): [T, (value: T | ((val: T) => T)) => void] { 11 | // State to store our value 12 | // Pass initial state function to useState so logic is only executed once 13 | const [storedValue, setStoredValue] = useState(() => { 14 | if (typeof window === "undefined") { 15 | return initialValue; 16 | } 17 | 18 | try { 19 | const item = window.localStorage.getItem(key); 20 | return item ? (JSON.parse(item) as T) : initialValue; 21 | } catch { 22 | return initialValue; 23 | } 24 | }); 25 | 26 | // Return a wrapped version of useState's setter function that 27 | // persists the new value to localStorage. 28 | const setValue = (value: T | ((val: T) => T)) => { 29 | try { 30 | // Allow value to be a function so we have same API as useState 31 | const valueToStore = 32 | value instanceof Function ? value(storedValue) : value; 33 | 34 | // Save state 35 | setStoredValue(valueToStore); 36 | 37 | // Save to local storage 38 | if (typeof window !== "undefined") { 39 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 40 | } 41 | } catch { 42 | // Failed to save to localStorage 43 | } 44 | }; 45 | 46 | // Listen for storage changes from other tabs/windows 47 | useEffect(() => { 48 | if (typeof window === "undefined") { 49 | return; 50 | } 51 | 52 | const handleStorageChange = (e: StorageEvent) => { 53 | if (e.key === key && e.newValue) { 54 | try { 55 | setStoredValue(JSON.parse(e.newValue) as T); 56 | } catch { 57 | // Failed to parse storage event 58 | } 59 | } 60 | }; 61 | 62 | window.addEventListener("storage", handleStorageChange); 63 | return () => { 64 | window.removeEventListener("storage", handleStorageChange); 65 | }; 66 | }, [key]); 67 | 68 | return [storedValue, setValue]; 69 | } 70 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-localize-navigate.ts: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "@tanstack/react-router"; 2 | import { useLocale } from "react-intlayer"; 3 | import { LOCALE_ROUTE } from "@/components/intlayer/localized-link"; 4 | import type { FileRouteTypes } from "@/routeTree.gen"; 5 | 6 | type StripLocalePrefix = T extends 7 | | `/${typeof LOCALE_ROUTE}` 8 | | `/${typeof LOCALE_ROUTE}/` 9 | ? "/" 10 | : T extends `/${typeof LOCALE_ROUTE}/${infer Rest}` 11 | ? `/${Rest}` 12 | : never; 13 | 14 | type LocalizedTo = StripLocalePrefix; 15 | 16 | export interface LocalizedNavigate { 17 | (to: LocalizedTo): Promise; 18 | (opts: { to: LocalizedTo } & Record): Promise; 19 | } 20 | 21 | export const useLocalizedNavigate = (): LocalizedNavigate => { 22 | const navigate = useNavigate(); 23 | 24 | const { locale } = useLocale(); 25 | 26 | const localizedNavigate: LocalizedNavigate = (args: any) => { 27 | if (typeof args === "string") { 28 | return navigate({ to: `/${LOCALE_ROUTE}${args}`, params: { locale } }); 29 | } 30 | 31 | const { to, ...rest } = args; 32 | 33 | const localedTo = `/${LOCALE_ROUTE}${to}` as any; 34 | 35 | return navigate({ to: localedTo, params: { locale, ...rest } as any }); 36 | }; 37 | 38 | return localizedNavigate; 39 | }; 40 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-paste-handler.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { useEffect } from "react"; 3 | import { toast } from "sonner"; 4 | import { extractSvgFromBase64, isSvgContent } from "@/lib/file-utils"; 5 | 6 | interface UsePasteHandlerOptions { 7 | setOriginalSvg: (svg: string, name: string) => void; 8 | setHasAutoSwitchedTab?: (value: boolean) => void; 9 | onSuccess?: () => void; 10 | errorMessage?: ReactNode; 11 | } 12 | 13 | export function usePasteHandler({ 14 | setOriginalSvg, 15 | setHasAutoSwitchedTab, 16 | onSuccess, 17 | errorMessage = "Invalid SVG content. Please paste valid SVG code.", 18 | }: UsePasteHandlerOptions) { 19 | useEffect(() => { 20 | const handlePaste = (e: ClipboardEvent) => { 21 | const items = e.clipboardData?.items; 22 | if (!items) { 23 | return; 24 | } 25 | 26 | for (const item of items) { 27 | if (item.type === "text/plain") { 28 | item.getAsString((text) => { 29 | if (isSvgContent(text)) { 30 | setOriginalSvg(text, "pasted.svg"); 31 | setHasAutoSwitchedTab?.(false); 32 | toast.success("SVG pasted successfully!"); 33 | onSuccess?.(); 34 | } else { 35 | const extracted = extractSvgFromBase64(text); 36 | if (extracted) { 37 | setOriginalSvg(extracted, "pasted.svg"); 38 | setHasAutoSwitchedTab?.(false); 39 | toast.success("SVG pasted successfully!"); 40 | onSuccess?.(); 41 | } else { 42 | toast.error(errorMessage); 43 | } 44 | } 45 | }); 46 | } 47 | } 48 | }; 49 | 50 | document.addEventListener("paste", handlePaste); 51 | return () => document.removeEventListener("paste", handlePaste); 52 | }, [setOriginalSvg, setHasAutoSwitchedTab, onSuccess, errorMessage]); 53 | } 54 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-prettified-svg.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { prettierWorkerClient } from "@/lib/worker-utils/prettier-worker-client"; 3 | 4 | export function usePrettifiedSvg(svg: string, shouldPrettify: boolean): string { 5 | const [prettified, setPrettified] = useState(""); 6 | const [_isPrettifying, setIsPrettifying] = useState(false); 7 | 8 | useEffect(() => { 9 | const prettify = async () => { 10 | if (shouldPrettify && svg) { 11 | setIsPrettifying(true); 12 | try { 13 | const result = await prettierWorkerClient.format(svg, "svg"); 14 | setPrettified(result); 15 | } catch (_error) { 16 | setPrettified(svg); 17 | } finally { 18 | setIsPrettifying(false); 19 | } 20 | } else { 21 | setPrettified(svg); 22 | } 23 | }; 24 | prettify(); 25 | }, [svg, shouldPrettify]); 26 | 27 | return prettified; 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-svg-history.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import { toast } from "sonner"; 3 | import { HISTORY_CONSTANTS } from "@/lib/constants/history"; 4 | import { 5 | clearAllHistory, 6 | deleteHistoryEntry, 7 | getAllHistoryEntries, 8 | getHistoryCount, 9 | getRecentHistoryEntries, 10 | saveHistoryEntry, 11 | } from "@/lib/svg-history-storage"; 12 | import type { HistoryEntry, HistoryEntryInput } from "@/types/history"; 13 | 14 | export function useSvgHistory() { 15 | const [entries, setEntries] = useState([]); 16 | const [recentEntries, setRecentEntries] = useState([]); 17 | const [count, setCount] = useState(0); 18 | const [isLoading, setIsLoading] = useState(true); 19 | 20 | const loadEntries = useCallback(async () => { 21 | try { 22 | setIsLoading(true); 23 | const [allEntries, recent, totalCount] = await Promise.all([ 24 | getAllHistoryEntries(), 25 | getRecentHistoryEntries(HISTORY_CONSTANTS.RECENT_ENTRIES_COUNT), 26 | getHistoryCount(), 27 | ]); 28 | setEntries(allEntries); 29 | setRecentEntries(recent); 30 | setCount(totalCount); 31 | } catch (error) { 32 | console.error("Failed to load history:", error); 33 | toast.error("Failed to load history"); 34 | } finally { 35 | setIsLoading(false); 36 | } 37 | }, []); 38 | 39 | useEffect(() => { 40 | loadEntries(); 41 | }, [loadEntries]); 42 | 43 | const saveEntry = useCallback( 44 | async (entry: HistoryEntryInput) => { 45 | try { 46 | await saveHistoryEntry(entry); 47 | await loadEntries(); 48 | toast.success("SVG saved to history"); 49 | } catch (error) { 50 | console.error("Failed to save history entry:", error); 51 | toast.error("Failed to save to history"); 52 | } 53 | }, 54 | [loadEntries] 55 | ); 56 | 57 | const deleteEntry = useCallback( 58 | async (id: string) => { 59 | try { 60 | await deleteHistoryEntry(id); 61 | await loadEntries(); 62 | toast.success("Entry deleted"); 63 | } catch (error) { 64 | console.error("Failed to delete history entry:", error); 65 | toast.error("Failed to delete entry"); 66 | } 67 | }, 68 | [loadEntries] 69 | ); 70 | 71 | const clearAll = useCallback(async () => { 72 | try { 73 | await clearAllHistory(); 74 | await loadEntries(); 75 | toast.success("History cleared"); 76 | } catch (error) { 77 | console.error("Failed to clear history:", error); 78 | toast.error("Failed to clear history"); 79 | } 80 | }, [loadEntries]); 81 | 82 | return { 83 | entries, 84 | recentEntries, 85 | count, 86 | isLoading, 87 | saveEntry, 88 | deleteEntry, 89 | clearAll, 90 | refresh: loadEntries, 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /apps/web/src/hooks/use-svg-pan-zoom.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | 3 | const DEFAULT_ZOOM = 100; 4 | const MAX_ZOOM = 800; 5 | const MIN_ZOOM = 20; 6 | const ZOOM_STEP = 20; 7 | const WHEEL_ZOOM_STEP = 10; 8 | 9 | interface UseSvgPanZoomOptions { 10 | defaultZoom?: number; 11 | maxZoom?: number; 12 | minZoom?: number; 13 | zoomStep?: number; 14 | wheelZoomStep?: number; 15 | } 16 | 17 | export function useSvgPanZoom(options: UseSvgPanZoomOptions = {}) { 18 | const { 19 | defaultZoom = DEFAULT_ZOOM, 20 | maxZoom = MAX_ZOOM, 21 | minZoom = MIN_ZOOM, 22 | zoomStep = ZOOM_STEP, 23 | wheelZoomStep = WHEEL_ZOOM_STEP, 24 | } = options; 25 | 26 | const [zoom, setZoom] = useState(defaultZoom); 27 | const [pan, setPan] = useState({ x: 0, y: 0 }); 28 | const [isDragging, setIsDragging] = useState(false); 29 | const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); 30 | const containerRef = useRef(null); 31 | 32 | const handleZoomIn = () => { 33 | setZoom((prev) => Math.min(prev + zoomStep, maxZoom)); 34 | }; 35 | 36 | const handleZoomOut = () => { 37 | setZoom((prev) => Math.max(prev - zoomStep, minZoom)); 38 | }; 39 | 40 | const handleZoomReset = () => { 41 | setZoom(defaultZoom); 42 | setPan({ x: 0, y: 0 }); 43 | }; 44 | 45 | useEffect(() => { 46 | const container = containerRef.current; 47 | if (!container) { 48 | return; 49 | } 50 | 51 | const handleWheelPassive = (e: WheelEvent) => { 52 | e.preventDefault(); 53 | e.stopPropagation(); 54 | 55 | const delta = e.deltaY > 0 ? -wheelZoomStep : wheelZoomStep; 56 | setZoom((prev) => { 57 | const newZoom = prev + delta; 58 | return Math.max(minZoom, Math.min(maxZoom, newZoom)); 59 | }); 60 | }; 61 | 62 | container.addEventListener("wheel", handleWheelPassive, { 63 | passive: false, 64 | }); 65 | 66 | return () => { 67 | container.removeEventListener("wheel", handleWheelPassive); 68 | }; 69 | }, [maxZoom, minZoom, wheelZoomStep]); 70 | 71 | const handleMouseDown = (e: React.MouseEvent) => { 72 | if (e.button === 0) { 73 | setIsDragging(true); 74 | setDragStart({ x: e.clientX - pan.x, y: e.clientY - pan.y }); 75 | } 76 | }; 77 | 78 | const handleMouseMove = (e: React.MouseEvent) => { 79 | if (isDragging) { 80 | setPan({ 81 | x: e.clientX - dragStart.x, 82 | y: e.clientY - dragStart.y, 83 | }); 84 | } 85 | }; 86 | 87 | const handleMouseUp = () => { 88 | setIsDragging(false); 89 | }; 90 | 91 | const handleMouseLeave = () => { 92 | setIsDragging(false); 93 | }; 94 | 95 | return { 96 | zoom, 97 | pan, 98 | isDragging, 99 | containerRef, 100 | handleZoomIn, 101 | handleZoomOut, 102 | handleZoomReset, 103 | handleMouseDown, 104 | handleMouseMove, 105 | handleMouseUp, 106 | handleMouseLeave, 107 | minZoom, 108 | maxZoom, 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /apps/web/src/lib/animation-utils.ts: -------------------------------------------------------------------------------- 1 | import { HISTORY_CONSTANTS, ROTATION_ANGLES } from "./constants/history"; 2 | 3 | export function getStaggerDelay( 4 | index: number, 5 | delayMs: number = HISTORY_CONSTANTS.ANIMATION_STAGGER_MS 6 | ): string { 7 | return `${index * delayMs}ms`; 8 | } 9 | 10 | export function getCardRotation(index: number): number { 11 | return ROTATION_ANGLES[index % ROTATION_ANGLES.length] ?? 0; 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/src/lib/blog.ts: -------------------------------------------------------------------------------- 1 | import { allPosts } from "content-collections"; 2 | import { sortBy } from "es-toolkit"; 3 | import { Locales } from "intlayer"; 4 | 5 | export function getBlogPosts( 6 | locale: Locales = Locales.ENGLISH, 7 | order: "asc" | "desc" = "asc" 8 | ) { 9 | let readyPosts = sortBy(allPosts, ["createdAt"]); 10 | readyPosts = order === "asc" ? readyPosts : readyPosts.reverse(); 11 | 12 | if (!locale) { 13 | return readyPosts; 14 | } 15 | 16 | return readyPosts.filter((post) => post._meta.locale === locale); 17 | } 18 | 19 | export async function getBlogPost( 20 | slug: string, 21 | locale: Locales = Locales.ENGLISH 22 | ) { 23 | // Get all posts without locale filtering to find all versions of this slug 24 | const collections = allPosts.filter((blog) => blog.slug === slug); 25 | if (collections.length === 0) { 26 | return null; 27 | } 28 | 29 | // Find the post for the requested locale 30 | const content = collections.find((blog) => blog.locale === locale); 31 | 32 | if (!content) { 33 | return null; 34 | } 35 | 36 | return content; 37 | } 38 | 39 | export function getLatestBlogPosts( 40 | limit = 4, 41 | locale: Locales = Locales.ENGLISH, 42 | order: "asc" | "desc" = "asc" 43 | ) { 44 | const posts = getBlogPosts(locale, order); 45 | return posts.slice(0, limit); 46 | } 47 | -------------------------------------------------------------------------------- /apps/web/src/lib/clamp.ts: -------------------------------------------------------------------------------- 1 | export function clamp(val: number, [min, max]: [number, number]): number { 2 | return Math.min(Math.max(val, min), max); 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/src/lib/constants/history.ts: -------------------------------------------------------------------------------- 1 | export const HISTORY_CONSTANTS = { 2 | MAX_ENTRIES: 50, 3 | RECENT_ENTRIES_COUNT: 3, 4 | THUMBNAIL_MAX_SIZE: 200, 5 | GRID_COLUMNS: 3, 6 | ANIMATION_STAGGER_MS: 50, 7 | ANIMATION_RECENT_MS: 100, 8 | } as const; 9 | 10 | export const ROTATION_ANGLES = [-10, 0, 10] as const; 11 | -------------------------------------------------------------------------------- /apps/web/src/lib/data-uri-utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BASE64_DATA_URI_PREFIX, 3 | BYTE_PRECISION, 4 | BYTES_DIVISOR, 5 | DATA_URI_PREFIX, 6 | SIZE_UNITS, 7 | URL_ENCODING_REPLACEMENTS, 8 | } from "./constants"; 9 | 10 | export interface DataUriResult { 11 | minified: string; 12 | base64: string; 13 | urlEncoded: string; 14 | minifiedSize: number; 15 | base64Size: number; 16 | urlEncodedSize: number; 17 | } 18 | 19 | /** 20 | * Converts SVG content to various Data URI formats 21 | * @param svg - The SVG string content 22 | * @returns Object containing different Data URI formats and their sizes 23 | */ 24 | export function svgToDataUri(svg: string): DataUriResult { 25 | // Minified Data URI (using encodeURIComponent with optimizations) 26 | let encoded = encodeURIComponent(svg); 27 | for (const [encoded_char, decoded] of Object.entries( 28 | URL_ENCODING_REPLACEMENTS 29 | )) { 30 | encoded = encoded.replaceAll(encoded_char, decoded); 31 | } 32 | const minified = `data:${DATA_URI_PREFIX},${encoded}`; 33 | 34 | // Base64 Data URI 35 | const base64 = `${BASE64_DATA_URI_PREFIX}${btoa(unescape(encodeURIComponent(svg)))}`; 36 | 37 | // URL Encoded Data URI 38 | const urlEncoded = `data:${DATA_URI_PREFIX},${encodeURIComponent(svg)}`; 39 | 40 | return { 41 | minified, 42 | base64, 43 | urlEncoded, 44 | minifiedSize: new Blob([minified]).size, 45 | base64Size: new Blob([base64]).size, 46 | urlEncodedSize: new Blob([urlEncoded]).size, 47 | }; 48 | } 49 | 50 | /** 51 | * Formats byte size to human-readable format 52 | * @param bytes - The size in bytes 53 | * @returns Formatted string (e.g., "1.23 KB") 54 | */ 55 | export function formatBytes(bytes: number): string { 56 | if (bytes === 0) { 57 | return `0 ${SIZE_UNITS[0]}`; 58 | } 59 | 60 | const i = Math.floor(Math.log(bytes) / Math.log(BYTES_DIVISOR)); 61 | 62 | return `${(bytes / BYTES_DIVISOR ** i).toFixed(BYTE_PRECISION)} ${SIZE_UNITS[i]}`; 63 | } 64 | -------------------------------------------------------------------------------- /apps/web/src/lib/prettify-code.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from "prettier"; 2 | import * as parserBabel from "prettier/plugins/babel"; 3 | import * as parserEstree from "prettier/plugins/estree"; 4 | import * as parserHtml from "prettier/plugins/html"; 5 | import * as parserTypescript from "prettier/plugins/typescript"; 6 | import * as prettier from "prettier/standalone"; 7 | 8 | export type SupportedLanguage = "javascript" | "typescript" | "html" | "dart"; 9 | 10 | /** 11 | * Prettify code using Prettier 12 | */ 13 | export async function prettifyCode( 14 | code: string, 15 | language: SupportedLanguage 16 | ): Promise { 17 | // Dart is not supported by Prettier 18 | if (language === "dart") { 19 | return code; 20 | } 21 | 22 | const parserMap: Record = { 23 | javascript: "babel", 24 | typescript: "typescript", 25 | html: "html", 26 | dart: "", 27 | }; 28 | 29 | const pluginsMap: Record< 30 | SupportedLanguage, 31 | Array | any> 32 | > = { 33 | javascript: [parserBabel, parserEstree], 34 | typescript: [parserTypescript, parserEstree], 35 | html: [parserHtml], 36 | dart: [], 37 | }; 38 | 39 | const formatted = await prettier.format(code, { 40 | parser: parserMap[language], 41 | plugins: pluginsMap[language], 42 | semi: true, 43 | singleQuote: false, 44 | tabWidth: 2, 45 | trailingComma: "es5", 46 | printWidth: 80, 47 | }); 48 | 49 | return formatted; 50 | } 51 | -------------------------------------------------------------------------------- /apps/web/src/lib/seo.ts: -------------------------------------------------------------------------------- 1 | // SEO Configuration and Utilities 2 | 3 | export const siteConfig = { 4 | name: "Tiny SVG", 5 | url: "https://tiny-svg.actnow.dev", 6 | title: "Tiny SVG - Optimize SVGs & Convert to React, Vue, Svelte Components", 7 | description: 8 | "Free online SVG optimizer and converter. Compress SVG files up to 70%, convert to React, Vue, and Svelte components. All processing happens in your browser - secure and fast!", 9 | keywords: [ 10 | "SVG optimizer", 11 | "SVG compressor", 12 | "SVG to React", 13 | "SVG to Vue", 14 | "SVG to Svelte", 15 | "minify SVG", 16 | "optimize SVG online", 17 | "SVG converter", 18 | "web performance", 19 | ], 20 | ogImage: "https://tiny-svg.actnow.dev/og-image.png", 21 | author: "Tiny SVG Team", 22 | social: { 23 | twitter: "@tinysvg", 24 | }, 25 | }; 26 | 27 | export function generateMetaTags({ 28 | title, 29 | description, 30 | image, 31 | url, 32 | type = "website", 33 | publishedTime, 34 | }: { 35 | title?: string; 36 | description?: string; 37 | image?: string; 38 | url?: string; 39 | type?: "website" | "article"; 40 | publishedTime?: string; 41 | }) { 42 | const finalTitle = title ? `${title} | ${siteConfig.name}` : siteConfig.title; 43 | const finalDescription = description || siteConfig.description; 44 | const finalImage = image || siteConfig.ogImage; 45 | const finalUrl = url || siteConfig.url; 46 | 47 | return [ 48 | { title: finalTitle }, 49 | { name: "description", content: finalDescription }, 50 | // Open Graph 51 | { property: "og:type", content: type }, 52 | { property: "og:url", content: finalUrl }, 53 | { property: "og:title", content: finalTitle }, 54 | { property: "og:description", content: finalDescription }, 55 | { property: "og:image", content: finalImage }, 56 | ...(publishedTime 57 | ? [{ property: "article:published_time", content: publishedTime }] 58 | : []), 59 | // Twitter 60 | { property: "twitter:card", content: "summary_large_image" }, 61 | { property: "twitter:url", content: finalUrl }, 62 | { property: "twitter:title", content: finalTitle }, 63 | { property: "twitter:description", content: finalDescription }, 64 | { property: "twitter:image", content: finalImage }, 65 | ]; 66 | } 67 | 68 | export function generateCanonicalLink(url: string) { 69 | return { rel: "canonical", href: url }; 70 | } 71 | 72 | export function generateBlogPostStructuredData({ 73 | title, 74 | description, 75 | image, 76 | slug, 77 | publishedTime, 78 | }: { 79 | title: string; 80 | description: string; 81 | image?: string; 82 | slug: string; 83 | publishedTime: string; 84 | }) { 85 | return { 86 | "@context": "https://schema.org", 87 | "@type": "BlogPosting", 88 | headline: title, 89 | description, 90 | image: image || siteConfig.ogImage, 91 | datePublished: publishedTime, 92 | dateModified: publishedTime, 93 | author: { 94 | "@type": "Organization", 95 | name: siteConfig.author, 96 | url: siteConfig.url, 97 | }, 98 | publisher: { 99 | "@type": "Organization", 100 | name: siteConfig.name, 101 | logo: { 102 | "@type": "ImageObject", 103 | url: `${siteConfig.url}/logo.png`, 104 | }, 105 | }, 106 | mainEntityOfPage: { 107 | "@type": "WebPage", 108 | "@id": `${siteConfig.url}/blog/${slug}`, 109 | }, 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /apps/web/src/lib/svg-history-storage.ts: -------------------------------------------------------------------------------- 1 | import localforage from "localforage"; 2 | import type { HistoryEntry, HistoryEntryInput } from "@/types/history"; 3 | import { HISTORY_CONSTANTS } from "./constants/history"; 4 | 5 | export type { HistoryEntry, HistoryEntryInput } from "@/types/history"; 6 | 7 | const svgHistoryStore = localforage.createInstance({ 8 | name: "tiny-svg", 9 | storeName: "svg_history", 10 | }); 11 | 12 | function generateHash(content: string): string { 13 | let hash = 0; 14 | for (let i = 0; i < content.length; i++) { 15 | const char = content.charCodeAt(i); 16 | // biome-ignore lint/suspicious/noBitwiseOperators: Hash function requires bitwise operations 17 | hash = (hash << 5) - hash + char; 18 | // biome-ignore lint/suspicious/noBitwiseOperators: Hash function requires bitwise operations 19 | hash &= hash; 20 | } 21 | return Math.abs(hash).toString(36); 22 | } 23 | 24 | export async function saveHistoryEntry( 25 | entry: HistoryEntryInput 26 | ): Promise { 27 | const contentHash = generateHash(entry.originalSvg); 28 | const id = `${contentHash}-${Date.now()}`; 29 | const timestamp = Date.now(); 30 | 31 | const newEntry: HistoryEntry = { 32 | ...entry, 33 | id, 34 | timestamp, 35 | }; 36 | 37 | const existingEntries = await getAllHistoryEntries(); 38 | 39 | const isDuplicate = existingEntries.some( 40 | (existing) => 41 | generateHash(existing.originalSvg) === contentHash && 42 | existing.fileName === entry.fileName 43 | ); 44 | 45 | if (isDuplicate) { 46 | return id; 47 | } 48 | 49 | const updatedEntries = [newEntry, ...existingEntries]; 50 | 51 | if (updatedEntries.length > HISTORY_CONSTANTS.MAX_ENTRIES) { 52 | updatedEntries.splice(HISTORY_CONSTANTS.MAX_ENTRIES); 53 | } 54 | 55 | await svgHistoryStore.setItem("entries", updatedEntries); 56 | 57 | return id; 58 | } 59 | 60 | export async function getAllHistoryEntries(): Promise { 61 | const entries = await svgHistoryStore.getItem("entries"); 62 | return entries ?? []; 63 | } 64 | 65 | export async function getRecentHistoryEntries( 66 | count = 3 67 | ): Promise { 68 | const entries = await getAllHistoryEntries(); 69 | return entries.slice(0, count); 70 | } 71 | 72 | export async function getHistoryEntryById( 73 | id: string 74 | ): Promise { 75 | const entries = await getAllHistoryEntries(); 76 | return entries.find((entry) => entry.id === id) ?? null; 77 | } 78 | 79 | export async function deleteHistoryEntry(id: string): Promise { 80 | const entries = await getAllHistoryEntries(); 81 | const updatedEntries = entries.filter((entry) => entry.id !== id); 82 | await svgHistoryStore.setItem("entries", updatedEntries); 83 | } 84 | 85 | export async function clearAllHistory(): Promise { 86 | await svgHistoryStore.removeItem("entries"); 87 | } 88 | 89 | export async function getHistoryCount(): Promise { 90 | const entries = await getAllHistoryEntries(); 91 | return entries.length; 92 | } 93 | -------------------------------------------------------------------------------- /apps/web/src/lib/svgo-config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SVGO configuration utilities 3 | * This file does NOT import 'svgo' to avoid bundling it in the server code 4 | * SVGO is only used in the worker (svgo.worker.ts) 5 | */ 6 | 7 | import type { Config as SvgoConfig } from "svgo"; 8 | import type { SvgoGlobalSettings, SvgoPluginConfig } from "@/lib/svgo-plugins"; 9 | 10 | export const getStandardPreset = (): SvgoConfig => ({ 11 | multipass: true, 12 | plugins: ["preset-default"] as any, 13 | }); 14 | 15 | export const buildSvgoConfig = ( 16 | plugins: SvgoPluginConfig[], 17 | globalSettings: SvgoGlobalSettings 18 | ): SvgoConfig => { 19 | const enabledPlugins = plugins.filter((p) => p.enabled).map((p) => p.name); 20 | 21 | return { 22 | multipass: globalSettings.multipass, 23 | floatPrecision: globalSettings.floatPrecision, 24 | plugins: 25 | enabledPlugins.length > 0 26 | ? (enabledPlugins as any) 27 | : (["preset-default"] as any), 28 | }; 29 | }; 30 | 31 | const PERCENTAGE_MULTIPLIER = 100; 32 | 33 | export const calculateCompressionRate = ( 34 | original: string, 35 | compressed: string 36 | ): number => { 37 | if (!(original && compressed)) { 38 | return 0; 39 | } 40 | const originalSize = new Blob([original]).size; 41 | const compressedSize = new Blob([compressed]).size; 42 | return (1 - compressedSize / originalSize) * PERCENTAGE_MULTIPLIER; 43 | }; 44 | 45 | export const formatBytes = (bytes: number): string => { 46 | if (bytes === 0) { 47 | return "0 Bytes"; 48 | } 49 | const k = 1024; 50 | const sizes = ["Bytes", "KB", "MB"]; 51 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 52 | return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`; 53 | }; 54 | -------------------------------------------------------------------------------- /apps/web/src/lib/svgo-plugins.ts: -------------------------------------------------------------------------------- 1 | export type SvgoPluginConfig = { 2 | name: string; 3 | enabled: boolean; 4 | params?: Record; 5 | }; 6 | 7 | export type SvgoGlobalSettings = { 8 | showOriginal: boolean; 9 | compareGzipped: boolean; 10 | prettifyMarkup: boolean; 11 | multipass: boolean; 12 | floatPrecision: number; 13 | transformPrecision: number; 14 | }; 15 | 16 | export const defaultGlobalSettings: SvgoGlobalSettings = { 17 | showOriginal: false, 18 | compareGzipped: false, 19 | prettifyMarkup: true, 20 | multipass: true, 21 | floatPrecision: 2, 22 | transformPrecision: 4, 23 | }; 24 | 25 | export const allSvgoPlugins: SvgoPluginConfig[] = [ 26 | { name: "removeDoctype", enabled: true }, 27 | { name: "removeXMLProcInst", enabled: true }, 28 | { name: "removeComments", enabled: true }, 29 | { name: "removeMetadata", enabled: true }, 30 | { name: "removeXMLNS", enabled: true }, 31 | { name: "removeEditorsNSData", enabled: true }, 32 | { name: "cleanupAttrs", enabled: true }, 33 | { name: "mergeStyles", enabled: true }, 34 | { name: "inlineStyles", enabled: true }, 35 | { name: "minifyStyles", enabled: true }, 36 | { name: "convertStyleToAttrs", enabled: false }, 37 | { name: "cleanupIds", enabled: true }, 38 | { name: "removeRasterImages", enabled: false }, 39 | { name: "removeUselessDefs", enabled: true }, 40 | { name: "cleanupNumericValues", enabled: true }, 41 | { name: "cleanupListOfValues", enabled: true }, 42 | { name: "convertColors", enabled: true }, 43 | { name: "removeUnknownsAndDefaults", enabled: true }, 44 | { name: "removeNonInheritableGroupAttrs", enabled: true }, 45 | { name: "removeUselessStrokeAndFill", enabled: true }, 46 | { name: "removeViewBox", enabled: false }, 47 | { name: "cleanupEnableBackground", enabled: true }, 48 | { name: "removeHiddenElems", enabled: true }, 49 | { name: "removeEmptyText", enabled: true }, 50 | { name: "convertShapeToPath", enabled: false }, 51 | { name: "moveElemsAttrsToGroup", enabled: true }, 52 | { name: "moveGroupAttrsToElems", enabled: true }, 53 | { name: "collapseGroups", enabled: true }, 54 | { name: "convertPathData", enabled: true }, 55 | { name: "convertEllipseToCircle", enabled: true }, 56 | { name: "convertTransform", enabled: true }, 57 | { name: "removeEmptyAttrs", enabled: true }, 58 | { name: "removeEmptyContainers", enabled: true }, 59 | { name: "mergePaths", enabled: true }, 60 | { name: "removeUnusedNS", enabled: true }, 61 | { name: "reusePaths", enabled: false }, 62 | { name: "sortAttrs", enabled: false }, 63 | { name: "sortDefsChildren", enabled: false }, 64 | { name: "removeTitle", enabled: false }, 65 | { name: "removeDesc", enabled: false }, 66 | { name: "removeDimensions", enabled: false }, 67 | { name: "removeStyleElement", enabled: false }, 68 | { name: "removeScriptElement", enabled: false }, 69 | { name: "removeOffCanvasPaths", enabled: false }, 70 | { name: "removeAttributesBySelector", enabled: false }, 71 | ]; 72 | -------------------------------------------------------------------------------- /apps/web/src/lib/svgo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SVGO wrapper - ONLY for use in workers 3 | * DO NOT import this file in main application code 4 | * Use svgo-config.ts for configuration utilities 5 | */ 6 | 7 | import { optimize, type Config as SvgoConfig } from "svgo"; 8 | 9 | export const compressSvg = (svg: string, config: SvgoConfig): string => { 10 | const result = optimize(svg, config); 11 | return result.data; 12 | }; 13 | 14 | // Re-export types for convenience 15 | export type { Config as SvgoConfig } from "svgo"; 16 | -------------------------------------------------------------------------------- /apps/web/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/src/lib/worker-utils/cache.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Worker result cache utility 3 | * Caches worker computation results to avoid redundant processing 4 | */ 5 | 6 | type CacheEntry = { 7 | result: T; 8 | timestamp: number; 9 | }; 10 | 11 | const SECONDS_PER_MINUTE = 60; 12 | const MS_PER_SECOND = 1000; 13 | const MINUTES_TO_MS = SECONDS_PER_MINUTE * MS_PER_SECOND; 14 | const CACHE_MAX_AGE_MINUTES = 5; 15 | const DEFAULT_MAX_AGE_MS = CACHE_MAX_AGE_MINUTES * MINUTES_TO_MS; 16 | const DEFAULT_MAX_SIZE = 100; 17 | 18 | export class WorkerCache { 19 | private readonly cache = new Map>(); 20 | private readonly maxAge: number; 21 | private readonly maxSize: number; 22 | 23 | constructor(maxAge = DEFAULT_MAX_AGE_MS, maxSize = DEFAULT_MAX_SIZE) { 24 | this.maxAge = maxAge; 25 | this.maxSize = maxSize; 26 | } 27 | 28 | /** 29 | * Generate cache key from input 30 | */ 31 | private generateKey(input: unknown): string { 32 | return JSON.stringify(input); 33 | } 34 | 35 | /** 36 | * Get cached result if available and not expired 37 | */ 38 | get(input: unknown): T | null { 39 | const key = this.generateKey(input); 40 | const entry = this.cache.get(key); 41 | 42 | if (!entry) { 43 | return null; 44 | } 45 | 46 | const now = Date.now(); 47 | if (now - entry.timestamp > this.maxAge) { 48 | this.cache.delete(key); 49 | return null; 50 | } 51 | 52 | return entry.result; 53 | } 54 | 55 | /** 56 | * Set cache entry 57 | */ 58 | set(input: unknown, result: T): void { 59 | // Enforce size limit (LRU-like: delete oldest entries) 60 | if (this.cache.size >= this.maxSize) { 61 | const firstKey = this.cache.keys().next().value; 62 | if (firstKey) { 63 | this.cache.delete(firstKey); 64 | } 65 | } 66 | 67 | const key = this.generateKey(input); 68 | this.cache.set(key, { 69 | result, 70 | timestamp: Date.now(), 71 | }); 72 | } 73 | 74 | /** 75 | * Clear all cache entries 76 | */ 77 | clear(): void { 78 | this.cache.clear(); 79 | } 80 | 81 | /** 82 | * Get cache statistics 83 | */ 84 | getStats() { 85 | return { 86 | size: this.cache.size, 87 | maxSize: this.maxSize, 88 | maxAge: this.maxAge, 89 | }; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /apps/web/src/lib/worker-utils/code-generator-worker-client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Code Generator Worker Client 3 | * Provides a Promise-based API for code generation using Web Workers 4 | */ 5 | 6 | import { WorkerCache } from "@/lib/worker-utils/cache"; 7 | import { WorkerManager } from "@/lib/worker-utils/worker-manager"; 8 | import CodeGeneratorWorker from "@/workers/code-generator.worker?worker&url"; 9 | 10 | export type GeneratorType = 11 | | "react-jsx" 12 | | "react-tsx" 13 | | "vue" 14 | | "svelte" 15 | | "react-native" 16 | | "flutter"; 17 | 18 | export type SvgData = { 19 | innerContent: string; 20 | viewBox: string; 21 | componentName: string; 22 | processedContent: string; 23 | }; 24 | 25 | type CodeGeneratorRequest = { 26 | type: GeneratorType; 27 | svgData: SvgData; 28 | svgString: string; 29 | }; 30 | 31 | type CodeGeneratorResponse = { 32 | type: GeneratorType; 33 | code: string; 34 | }; 35 | 36 | class CodeGeneratorWorkerClient { 37 | private worker: WorkerManager< 38 | CodeGeneratorRequest, 39 | CodeGeneratorResponse 40 | > | null = null; 41 | private readonly cache = new WorkerCache(); 42 | 43 | /** 44 | * Initialize worker (lazy initialization) 45 | */ 46 | private ensureWorker(): WorkerManager< 47 | CodeGeneratorRequest, 48 | CodeGeneratorResponse 49 | > { 50 | if (!this.worker) { 51 | this.worker = new WorkerManager< 52 | CodeGeneratorRequest, 53 | CodeGeneratorResponse 54 | >(new URL(CodeGeneratorWorker, import.meta.url)); 55 | } 56 | return this.worker; 57 | } 58 | 59 | /** 60 | * Generate code for a specific framework 61 | */ 62 | async generate( 63 | type: GeneratorType, 64 | svgData: SvgData, 65 | svgString: string 66 | ): Promise { 67 | // Check cache first 68 | const cacheKey = { type, svgData, svgString }; 69 | const cached = this.cache.get(cacheKey); 70 | if (cached) { 71 | return cached; 72 | } 73 | 74 | // Process in worker 75 | const worker = this.ensureWorker(); 76 | const result = await worker.execute({ type, svgData, svgString }); 77 | 78 | // Cache result 79 | this.cache.set(cacheKey, result.code); 80 | 81 | return result.code; 82 | } 83 | 84 | /** 85 | * Clear cache 86 | */ 87 | clearCache(): void { 88 | this.cache.clear(); 89 | } 90 | 91 | /** 92 | * Get cache statistics 93 | */ 94 | getCacheStats() { 95 | return this.cache.getStats(); 96 | } 97 | 98 | /** 99 | * Terminate worker and clear resources 100 | */ 101 | terminate(): void { 102 | if (this.worker) { 103 | this.worker.terminate(); 104 | this.worker = null; 105 | } 106 | this.cache.clear(); 107 | } 108 | } 109 | 110 | // Export singleton instance 111 | export const codeGeneratorWorkerClient = new CodeGeneratorWorkerClient(); 112 | -------------------------------------------------------------------------------- /apps/web/src/lib/worker-utils/prettier-worker-client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Prettier Worker Client 3 | * Provides a Promise-based API for code formatting using Web Workers 4 | */ 5 | 6 | import { WorkerCache } from "@/lib/worker-utils/cache"; 7 | import { WorkerManager } from "@/lib/worker-utils/worker-manager"; 8 | import PrettierWorker from "@/workers/prettier.worker?worker&url"; 9 | 10 | export type SupportedLanguage = 11 | | "javascript" 12 | | "typescript" 13 | | "html" 14 | | "dart" 15 | | "svg"; 16 | 17 | type PrettierRequest = { 18 | content: string; 19 | language: SupportedLanguage; 20 | }; 21 | 22 | type PrettierResponse = string; 23 | 24 | class PrettierWorkerClient { 25 | private worker: WorkerManager | null = 26 | null; 27 | private readonly cache = new WorkerCache(); 28 | 29 | /** 30 | * Initialize worker (lazy initialization) 31 | */ 32 | private ensureWorker(): WorkerManager { 33 | if (!this.worker) { 34 | this.worker = new WorkerManager( 35 | new URL(PrettierWorker, import.meta.url) 36 | ); 37 | } 38 | return this.worker; 39 | } 40 | 41 | /** 42 | * Format content using Prettier 43 | */ 44 | async format(content: string, language: SupportedLanguage): Promise { 45 | // Check cache first 46 | const cacheKey = { content, language }; 47 | const cached = this.cache.get(cacheKey); 48 | if (cached) { 49 | return cached; 50 | } 51 | 52 | // Process in worker 53 | const worker = this.ensureWorker(); 54 | const result = await worker.execute({ content, language }); 55 | 56 | // Cache result 57 | this.cache.set(cacheKey, result); 58 | 59 | return result; 60 | } 61 | 62 | /** 63 | * Clear cache 64 | */ 65 | clearCache(): void { 66 | this.cache.clear(); 67 | } 68 | 69 | /** 70 | * Get cache statistics 71 | */ 72 | getCacheStats() { 73 | return this.cache.getStats(); 74 | } 75 | 76 | /** 77 | * Terminate worker and clear resources 78 | */ 79 | terminate(): void { 80 | if (this.worker) { 81 | this.worker.terminate(); 82 | this.worker = null; 83 | } 84 | this.cache.clear(); 85 | } 86 | } 87 | 88 | // Export singleton instance 89 | export const prettierWorkerClient = new PrettierWorkerClient(); 90 | -------------------------------------------------------------------------------- /apps/web/src/lib/worker-utils/svgo-worker-client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SVGO Worker Client 3 | * Provides a Promise-based API for SVG optimization using Web Workers 4 | */ 5 | 6 | import type { SvgoConfig } from "@/lib/svgo"; 7 | import { WorkerCache } from "@/lib/worker-utils/cache"; 8 | import { WorkerManager } from "@/lib/worker-utils/worker-manager"; 9 | import SvgoWorker from "@/workers/svgo.worker?worker&url"; 10 | 11 | type SvgoRequest = { 12 | svg: string; 13 | config: SvgoConfig; 14 | }; 15 | 16 | type SvgoResponse = string; 17 | 18 | class SvgoWorkerClient { 19 | private worker: WorkerManager | null = null; 20 | private readonly cache = new WorkerCache(); 21 | 22 | /** 23 | * Initialize worker (lazy initialization) 24 | */ 25 | private ensureWorker(): WorkerManager { 26 | if (!this.worker) { 27 | this.worker = new WorkerManager( 28 | new URL(SvgoWorker, import.meta.url) 29 | ); 30 | } 31 | return this.worker; 32 | } 33 | 34 | /** 35 | * Compress SVG using SVGO in a Web Worker 36 | */ 37 | async compress(svg: string, config: SvgoConfig): Promise { 38 | // Check cache first 39 | const cacheKey = { svg, config }; 40 | const cached = this.cache.get(cacheKey); 41 | if (cached) { 42 | return cached; 43 | } 44 | 45 | // Process in worker 46 | const worker = this.ensureWorker(); 47 | const result = await worker.execute({ svg, config }); 48 | 49 | // Cache result 50 | this.cache.set(cacheKey, result); 51 | 52 | return result; 53 | } 54 | 55 | /** 56 | * Clear cache 57 | */ 58 | clearCache(): void { 59 | this.cache.clear(); 60 | } 61 | 62 | /** 63 | * Get cache statistics 64 | */ 65 | getCacheStats() { 66 | return this.cache.getStats(); 67 | } 68 | 69 | /** 70 | * Terminate worker and clear resources 71 | */ 72 | terminate(): void { 73 | if (this.worker) { 74 | this.worker.terminate(); 75 | this.worker = null; 76 | } 77 | this.cache.clear(); 78 | } 79 | } 80 | 81 | // Export singleton instance 82 | export const svgoWorkerClient = new SvgoWorkerClient(); 83 | -------------------------------------------------------------------------------- /apps/web/src/lib/worker-utils/worker-manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Worker manager utility 3 | * Handles worker lifecycle and message passing with Promise-based API 4 | */ 5 | 6 | type WorkerMessage = { 7 | id: string; 8 | data: T; 9 | }; 10 | 11 | type WorkerResponse = { 12 | id: string; 13 | success: boolean; 14 | data?: T; 15 | error?: string; 16 | }; 17 | 18 | type ResolveReject = { 19 | resolve: (value: T) => void; 20 | reject: (reason: Error) => void; 21 | }; 22 | 23 | export class WorkerManager { 24 | private readonly worker: Worker; 25 | private readonly pendingMessages = new Map< 26 | string, 27 | ResolveReject 28 | >(); 29 | private messageId = 0; 30 | 31 | constructor(workerUrl: URL) { 32 | this.worker = new Worker(workerUrl, { type: "module" }); 33 | this.worker.onmessage = this.handleMessage.bind(this); 34 | this.worker.onerror = this.handleError.bind(this); 35 | } 36 | 37 | /** 38 | * Send message to worker and get Promise for result 39 | */ 40 | async execute(data: TRequest): Promise { 41 | const id = `msg_${this.messageId++}`; 42 | 43 | const promise = new Promise((resolve, reject) => { 44 | this.pendingMessages.set(id, { resolve, reject }); 45 | }); 46 | 47 | const message: WorkerMessage = { id, data }; 48 | this.worker.postMessage(message); 49 | 50 | return promise; 51 | } 52 | 53 | /** 54 | * Handle worker response 55 | */ 56 | private handleMessage(e: MessageEvent>) { 57 | const { id, success, data, error } = e.data; 58 | const pending = this.pendingMessages.get(id); 59 | 60 | if (!pending) { 61 | return; 62 | } 63 | 64 | this.pendingMessages.delete(id); 65 | 66 | if (success && data !== undefined) { 67 | pending.resolve(data); 68 | } else { 69 | pending.reject(new Error(error ?? "Worker operation failed")); 70 | } 71 | } 72 | 73 | /** 74 | * Handle worker error 75 | */ 76 | private handleError(error: ErrorEvent) { 77 | // Reject all pending messages 78 | for (const pending of this.pendingMessages.values()) { 79 | pending.reject(new Error(`Worker error: ${error.message}`)); 80 | } 81 | 82 | this.pendingMessages.clear(); 83 | } 84 | 85 | /** 86 | * Terminate worker 87 | */ 88 | terminate(): void { 89 | this.worker.terminate(); 90 | this.pendingMessages.clear(); 91 | } 92 | 93 | /** 94 | * Get number of pending messages 95 | */ 96 | getPendingCount(): number { 97 | return this.pendingMessages.size; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /apps/web/src/router.tsx: -------------------------------------------------------------------------------- 1 | import { createRouter as createTanStackRouter } from "@tanstack/react-router"; 2 | import Loader from "@/components/loader"; 3 | import { routeTree } from "@/routeTree.gen"; 4 | 5 | export const getRouter = () => { 6 | const router = createTanStackRouter({ 7 | routeTree, 8 | scrollRestoration: true, 9 | defaultPreloadStaleTime: 0, 10 | trailingSlash: "never", 11 | context: {}, 12 | defaultPendingComponent: () => , 13 | defaultNotFoundComponent: () =>
Not Found
, 14 | Wrap: ({ children }) => <>{children}, 15 | defaultViewTransition: true, 16 | }); 17 | return router; 18 | }; 19 | -------------------------------------------------------------------------------- /apps/web/src/routes/og.tsx: -------------------------------------------------------------------------------- 1 | import ImageResponse from "@takumi-rs/image-response"; 2 | import { createFileRoute } from "@tanstack/react-router"; 3 | import BaseTemplate from "@/components/og/base-template"; 4 | 5 | export const Route = createFileRoute("/og")({ 6 | server: { 7 | handlers: { 8 | GET({ request }) { 9 | const host = new URL(request.url).host; 10 | return new ImageResponse( 11 | , 18 | { 19 | width: 1200, 20 | height: 630, 21 | format: "webp", 22 | } 23 | ); 24 | }, 25 | }, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /apps/web/src/routes/{-$locale}/blog/route.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute, Outlet } from "@tanstack/react-router"; 2 | 3 | export const Route = createFileRoute("/{-$locale}/blog")({ 4 | component: BlogLayout, 5 | }); 6 | 7 | function BlogLayout() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/src/routes/{-$locale}/optimize.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute } from "@tanstack/react-router"; 2 | import { OptimizeLayout } from "@/components/optimize/optimize-layout"; 3 | import { useOptimizePage } from "@/hooks/use-optimize-page"; 4 | 5 | export const Route = createFileRoute("/{-$locale}/optimize")({ 6 | component: OptimizeComponent, 7 | }); 8 | 9 | function OptimizeComponent() { 10 | const optimizeProps = useOptimizePage(); 11 | 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/src/routes/{-$locale}/route.tsx: -------------------------------------------------------------------------------- 1 | import { createFileRoute, Outlet } from "@tanstack/react-router"; 2 | import { IntlayerProvider, useLocale } from "react-intlayer"; 3 | import Header from "@/components/header"; 4 | import { useI18nHTMLAttributes } from "@/hooks/use-i18n-html-attrs"; 5 | 6 | export const Route = createFileRoute("/{-$locale}")({ 7 | component: LayoutComponent, 8 | }); 9 | 10 | function LayoutComponent() { 11 | useI18nHTMLAttributes(); 12 | 13 | const { defaultLocale } = useLocale(); 14 | const { locale } = Route.useParams(); 15 | 16 | // 强制使用英语作为默认语言,防止服务器端渲染错误 17 | const safeLocale = locale ?? defaultLocale ?? "en"; 18 | 19 | return ( 20 | 21 |
22 |
23 | 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/web/src/store/svg-store.ts: -------------------------------------------------------------------------------- 1 | import type { Config as SvgoConfig } from "svgo"; 2 | import { create } from "zustand"; 3 | import { 4 | allSvgoPlugins, 5 | defaultGlobalSettings, 6 | type SvgoGlobalSettings, 7 | type SvgoPluginConfig, 8 | } from "@/lib/svgo-plugins"; 9 | 10 | // Note: SvgoConfig type is imported but SVGO library is NOT bundled 11 | // SVGO is only used in workers (svgo.worker.ts) 12 | 13 | export type SvgState = { 14 | originalSvg: string; 15 | compressedSvg: string; 16 | fileName: string; 17 | svgoConfig: SvgoConfig; 18 | plugins: SvgoPluginConfig[]; 19 | globalSettings: SvgoGlobalSettings; 20 | }; 21 | 22 | type SvgActions = { 23 | setOriginalSvg: (svg: string, fileName: string) => void; 24 | setCompressedSvg: (svg: string) => void; 25 | setHistoryEntry: (entry: { 26 | originalSvg: string; 27 | compressedSvg: string; 28 | fileName: string; 29 | }) => void; 30 | setSvgoConfig: (config: SvgoConfig) => void; 31 | togglePlugin: (pluginName: string) => void; 32 | updateGlobalSettings: (settings: Partial) => void; 33 | resetPlugins: () => void; 34 | reset: () => void; 35 | applyTransformation: (transformedSvg: string) => void; 36 | }; 37 | 38 | const defaultSvgoConfig: SvgoConfig = { 39 | multipass: true, 40 | plugins: ["preset-default"], 41 | }; 42 | 43 | const initialState: SvgState = { 44 | originalSvg: "", 45 | compressedSvg: "", 46 | fileName: "", 47 | svgoConfig: defaultSvgoConfig, 48 | plugins: allSvgoPlugins, 49 | globalSettings: defaultGlobalSettings, 50 | }; 51 | 52 | export const useSvgStore = create((set) => ({ 53 | ...initialState, 54 | setOriginalSvg: (svg, fileName) => 55 | set((state) => { 56 | // Check if this is the same content being uploaded again 57 | const isDuplicateContent = 58 | state.originalSvg === svg && state.fileName === fileName; 59 | 60 | if (isDuplicateContent && state.compressedSvg) { 61 | // Same file uploaded again - don't clear compressedSvg 62 | return { originalSvg: svg, fileName }; 63 | } 64 | 65 | // New file or different content - clear compressedSvg for fresh compression 66 | return { originalSvg: svg, fileName, compressedSvg: "" }; 67 | }), 68 | setCompressedSvg: (svg) => set({ compressedSvg: svg }), 69 | setHistoryEntry: ({ originalSvg, compressedSvg, fileName }) => 70 | set({ originalSvg, compressedSvg, fileName }), 71 | setSvgoConfig: (config) => set({ svgoConfig: config }), 72 | togglePlugin: (pluginName) => 73 | set((state) => ({ 74 | plugins: state.plugins.map((p) => 75 | p.name === pluginName ? { ...p, enabled: !p.enabled } : p 76 | ), 77 | })), 78 | updateGlobalSettings: (settings) => 79 | set((state) => ({ 80 | globalSettings: { ...state.globalSettings, ...settings }, 81 | })), 82 | resetPlugins: () => set({ plugins: allSvgoPlugins }), 83 | reset: () => set(initialState), 84 | applyTransformation: (transformedSvg) => 85 | set({ originalSvg: transformedSvg, compressedSvg: "" }), 86 | })); 87 | -------------------------------------------------------------------------------- /apps/web/src/store/ui-store.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | export type ExportScale = 4 | | 0.25 5 | | 0.5 6 | | 0.75 7 | | 1 8 | | 1.5 9 | | 2 10 | | 3 11 | | 4 12 | | 5 13 | | 6 14 | | 7 15 | | 8; 16 | 17 | type UiState = { 18 | activeTab: string; 19 | isCollapsed: boolean; 20 | isMobileSettingsOpen: boolean; 21 | isHistoryPanelOpen: boolean; 22 | exportScale: ExportScale | null; 23 | exportWidth: number; 24 | exportHeight: number; 25 | }; 26 | 27 | type UiActions = { 28 | setActiveTab: (tab: string) => void; 29 | toggleCollapsed: () => void; 30 | setIsCollapsed: (collapsed: boolean) => void; 31 | toggleMobileSettings: () => void; 32 | setIsMobileSettingsOpen: (open: boolean) => void; 33 | toggleHistoryPanel: () => void; 34 | setIsHistoryPanelOpen: (open: boolean) => void; 35 | setExportScale: (scale: ExportScale | null) => void; 36 | setExportWidth: (width: number) => void; 37 | setExportHeight: (height: number) => void; 38 | setExportDimensions: (width: number, height: number) => void; 39 | }; 40 | 41 | const initialState: UiState = { 42 | activeTab: "original", 43 | isCollapsed: false, 44 | isMobileSettingsOpen: false, 45 | isHistoryPanelOpen: false, 46 | exportScale: 2, 47 | exportWidth: 0, 48 | exportHeight: 0, 49 | }; 50 | 51 | export const useUiStore = create((set) => ({ 52 | ...initialState, 53 | setActiveTab: (tab) => set({ activeTab: tab }), 54 | toggleCollapsed: () => set((state) => ({ isCollapsed: !state.isCollapsed })), 55 | setIsCollapsed: (collapsed) => set({ isCollapsed: collapsed }), 56 | toggleMobileSettings: () => 57 | set((state) => ({ isMobileSettingsOpen: !state.isMobileSettingsOpen })), 58 | setIsMobileSettingsOpen: (open) => set({ isMobileSettingsOpen: open }), 59 | toggleHistoryPanel: () => 60 | set((state) => ({ isHistoryPanelOpen: !state.isHistoryPanelOpen })), 61 | setIsHistoryPanelOpen: (open) => set({ isHistoryPanelOpen: open }), 62 | setExportScale: (scale) => set({ exportScale: scale }), 63 | setExportWidth: (width) => set({ exportWidth: width }), 64 | setExportHeight: (height) => set({ exportHeight: height }), 65 | setExportDimensions: (width, height) => 66 | set({ exportWidth: width, exportHeight: height }), 67 | })); 68 | -------------------------------------------------------------------------------- /apps/web/src/types/history.ts: -------------------------------------------------------------------------------- 1 | import type { SvgoConfig } from "@/lib/svgo"; 2 | 3 | export type HistoryEntry = { 4 | id: string; 5 | fileName: string; 6 | originalSvg: string; 7 | compressedSvg: string; 8 | timestamp: number; 9 | thumbnail: string; 10 | config: SvgoConfig; 11 | originalSize: number; 12 | compressedSize: number; 13 | }; 14 | 15 | export type HistoryEntryInput = Omit; 16 | -------------------------------------------------------------------------------- /apps/web/src/types/pwa.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PWA TypeScript Declarations 3 | * Type definitions for PWA features and service worker integration 4 | */ 5 | 6 | // Virtual module for vite-plugin-pwa (uncomment when package is installed) 7 | // declare module "virtual:pwa-register/react" { 8 | // import type { Dispatch, SetStateAction } from "react"; 9 | // 10 | // export interface RegisterSWOptions { 11 | // immediate?: boolean; 12 | // onNeedRefresh?: () => void; 13 | // onOfflineReady?: () => void; 14 | // onRegistered?: ( 15 | // registration: ServiceWorkerRegistration | undefined 16 | // ) => void; 17 | // onRegisterError?: (error: Error) => void; 18 | // } 19 | // 20 | // export function useRegisterSW(options?: RegisterSWOptions): { 21 | // needRefresh: [boolean, Dispatch>]; 22 | // offlineReady: [boolean, Dispatch>]; 23 | // updateServiceWorker: (reloadPage?: boolean) => Promise; 24 | // }; 25 | // } 26 | 27 | // Extend Navigator for standalone detection 28 | interface Navigator { 29 | standalone?: boolean; 30 | } 31 | 32 | // BeforeInstallPromptEvent interface 33 | interface BeforeInstallPromptEvent extends Event { 34 | readonly platforms: string[]; 35 | readonly userChoice: Promise<{ 36 | outcome: "accepted" | "dismissed"; 37 | platform: string; 38 | }>; 39 | prompt: () => Promise; 40 | } 41 | 42 | // Extend WindowEventMap for PWA events 43 | interface WindowEventMap { 44 | beforeinstallprompt: BeforeInstallPromptEvent; 45 | appinstalled: Event; 46 | } 47 | -------------------------------------------------------------------------------- /apps/web/src/workers/prettier.worker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Prettier Web Worker 3 | * Handles code formatting in a separate thread 4 | */ 5 | 6 | import type { Plugin } from "prettier"; 7 | import * as parserBabel from "prettier/plugins/babel"; 8 | import * as parserEstree from "prettier/plugins/estree"; 9 | import * as parserHtml from "prettier/plugins/html"; 10 | import * as parserTypescript from "prettier/plugins/typescript"; 11 | import * as prettier from "prettier/standalone"; 12 | 13 | type SupportedLanguage = "javascript" | "typescript" | "html" | "dart" | "svg"; 14 | 15 | type WorkerMessage = { 16 | id: string; 17 | data: { 18 | content: string; 19 | language: SupportedLanguage; 20 | }; 21 | }; 22 | 23 | type WorkerResponse = { 24 | id: string; 25 | success: boolean; 26 | data?: string; 27 | error?: string; 28 | }; 29 | 30 | async function prettifyContent( 31 | content: string, 32 | language: SupportedLanguage 33 | ): Promise { 34 | // Dart is not supported by Prettier 35 | if (language === "dart") { 36 | return content; 37 | } 38 | 39 | const parserMap: Record = { 40 | javascript: "babel", 41 | typescript: "typescript", 42 | html: "html", 43 | svg: "html", 44 | dart: "", 45 | }; 46 | 47 | const pluginsMap: Record< 48 | SupportedLanguage, 49 | Array | any> 50 | > = { 51 | javascript: [parserBabel, parserEstree], 52 | typescript: [parserTypescript, parserEstree], 53 | html: [parserHtml], 54 | svg: [parserHtml], 55 | dart: [], 56 | }; 57 | 58 | const formatted = await prettier.format(content, { 59 | parser: parserMap[language], 60 | plugins: pluginsMap[language], 61 | semi: true, 62 | singleQuote: false, 63 | tabWidth: 2, 64 | trailingComma: "es5", 65 | printWidth: 80, 66 | }); 67 | 68 | return formatted; 69 | } 70 | 71 | self.onmessage = async (e: MessageEvent) => { 72 | const { id, data } = e.data; 73 | const { content, language } = data; 74 | 75 | try { 76 | const formatted = await prettifyContent(content, language); 77 | 78 | const response: WorkerResponse = { 79 | id, 80 | success: true, 81 | data: formatted, 82 | }; 83 | self.postMessage(response); 84 | } catch (error) { 85 | const response: WorkerResponse = { 86 | id, 87 | success: false, 88 | error: error instanceof Error ? error.message : "Unknown error", 89 | }; 90 | self.postMessage(response); 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /apps/web/src/workers/svgo.worker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SVGO Web Worker 3 | * Handles SVG optimization in a separate thread to avoid blocking the main thread 4 | */ 5 | 6 | import { type Config, optimize } from "svgo"; 7 | 8 | type WorkerMessage = { 9 | id: string; 10 | data: { 11 | svg: string; 12 | config: Config; 13 | }; 14 | }; 15 | 16 | type WorkerResponse = { 17 | id: string; 18 | success: boolean; 19 | data?: string; 20 | error?: string; 21 | }; 22 | 23 | self.onmessage = (e: MessageEvent) => { 24 | const { id, data } = e.data; 25 | const { svg, config } = data; 26 | 27 | try { 28 | const result = optimize(svg, config); 29 | 30 | if ("data" in result) { 31 | const response: WorkerResponse = { 32 | id, 33 | success: true, 34 | data: result.data, 35 | }; 36 | self.postMessage(response); 37 | } else { 38 | const response: WorkerResponse = { 39 | id, 40 | success: false, 41 | error: "Failed to optimize SVG", 42 | }; 43 | self.postMessage(response); 44 | } 45 | } catch (error) { 46 | const response: WorkerResponse = { 47 | id, 48 | success: false, 49 | error: error instanceof Error ? error.message : "Unknown error", 50 | }; 51 | self.postMessage(response); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": [ 4 | "src/**/*.ts", 5 | "src/**/*.tsx", 6 | "**/*.ts", 7 | "**/*.tsx", 8 | ".intlayer/**/*.ts" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "target": "ES2022", 13 | "module": "ESNext", 14 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 15 | "types": ["vite/client"], 16 | "moduleResolution": "bundler", 17 | "allowImportingTsExtensions": true, 18 | "noEmit": true, 19 | "noUncheckedSideEffectImports": true, 20 | "baseUrl": ".", 21 | "paths": { 22 | "@/*": ["./src/*"], 23 | "@content/*": ["./content/*"], 24 | "content-collections": ["./.content-collections/generated"] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/web/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "buildCommand": "pnpm build", 4 | "outputDirectory": ".output/public", 5 | "framework": null, 6 | "rewrites": [ 7 | { 8 | "source": "/(.*)", 9 | "destination": "/index.html" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "files": { 4 | "ignoreUnknown": false, 5 | "includes": [ 6 | "**", 7 | "!**/.next", 8 | "!**/dist", 9 | "!**/.turbo", 10 | "!**/dev-dist", 11 | "!**/.zed", 12 | "!**/.vscode", 13 | "!**/routeTree.gen.ts", 14 | "!bts.jsonc", 15 | "!**/.source", 16 | "!**/index.css", 17 | "!**/.intlayer" 18 | ] 19 | }, 20 | "extends": ["ultracite", "ultracite/core"], 21 | "linter": { 22 | "rules": { 23 | "nursery": { 24 | "noReactForwardRef": "off", 25 | "useMaxParams": "off" 26 | }, 27 | "style": { 28 | "noHeadElement": "off", 29 | "useFilenamingConvention": "off", 30 | "useConsistentTypeDefinitions": "off", 31 | "useUnifiedTypeSignatures": "off", 32 | "noMagicNumbers": "off" 33 | }, 34 | "performance": { 35 | "noNamespaceImport": "off", 36 | "noBarrelFile": "off", 37 | "noImgElement": "off" 38 | }, 39 | "security": { 40 | "noDangerouslySetInnerHtml": "off" 41 | }, 42 | "suspicious": { 43 | "noExplicitAny": "off", 44 | "useAwait": "off", 45 | "noUnknownAtRules": "off", 46 | "noDocumentCookie": "off", 47 | "noConsole": { 48 | "level": "info", 49 | "options": { 50 | "allow": ["warn", "info", "error"] 51 | } 52 | } 53 | }, 54 | "correctness": { 55 | "noUnknownTypeSelector": "off" 56 | }, 57 | "a11y": { 58 | "useSemanticElements": "off" 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /bts.jsonc: -------------------------------------------------------------------------------- 1 | // Better-T-Stack configuration file 2 | // safe to delete 3 | { 4 | "$schema": "https://r2.better-t-stack.dev/schema.json", 5 | "version": "3.2.1", 6 | "createdAt": "2025-10-18T05:18:15.484Z", 7 | "database": "none", 8 | "orm": "none", 9 | "backend": "self", 10 | "runtime": "none", 11 | "frontend": [ 12 | "tanstack-start" 13 | ], 14 | "addons": [ 15 | "ultracite" 16 | ], 17 | "examples": [], 18 | "auth": "none", 19 | "packageManager": "pnpm", 20 | "dbSetup": "none", 21 | "api": "none", 22 | "webDeploy": "none", 23 | "serverDeploy": "none" 24 | } 25 | -------------------------------------------------------------------------------- /docs/images/.gitkeep: -------------------------------------------------------------------------------- 1 | # This directory contains screenshots for the README 2 | # 3 | # Required files: 4 | # - home.png (Home page screenshot) 5 | # - optimize.png (Optimize page screenshot) 6 | # - logo.svg (Optional: Project logo) 7 | # 8 | # See ../SCREENSHOTS.md for instructions on how to take screenshots 9 | -------------------------------------------------------------------------------- /docs/images/home.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehai/tiny-svg/799a72af3978d4150d7b83f9423837e539cf5f03/docs/images/home.webp -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehai/tiny-svg/799a72af3978d4150d7b83f9423837e539cf5f03/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/optimize-code.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehai/tiny-svg/799a72af3978d4150d7b83f9423837e539cf5f03/docs/images/optimize-code.webp -------------------------------------------------------------------------------- /docs/images/optimize.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hehehai/tiny-svg/799a72af3978d4150d7b83f9423837e539cf5f03/docs/images/optimize.webp -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-svg", 3 | "private": true, 4 | "type": "module", 5 | "workspaces": [ 6 | "apps/*", 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "check": "biome check --write .", 11 | "dev": "pnpm -r dev", 12 | "build": "pnpm -r build", 13 | "check-types": "pnpm -r check-types", 14 | "dev:web": "pnpm --filter web dev" 15 | }, 16 | "dependencies": { 17 | "dotenv": "^17.2.2" 18 | }, 19 | "devDependencies": { 20 | "@biomejs/biome": "2.3.0", 21 | "@types/node": "^22.13.11", 22 | "husky": "^9.1.7", 23 | "ultracite": "6.0.0" 24 | }, 25 | "packageManager": "pnpm@10.14.0" 26 | } 27 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'apps/*' 3 | - 'packages/*' 4 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 8 | "jsx": "react-jsx", 9 | "verbatimModuleSyntax": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "resolveJsonModule": true, 13 | "allowSyntheticDefaultImports": true, 14 | "esModuleInterop": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "isolatedModules": true, 17 | "noUncheckedIndexedAccess": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "types": ["node"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./apps/web" 6 | } 7 | ], 8 | "compilerOptions": { 9 | "strictNullChecks": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------