134 Commits

Author SHA1 Message Date
pheralb 93aedefe79 🛠️ Update extensions data 2025-09-24 17:49:04 +01:00
pheralb 63b189bce3 🛠️ (API) Remove version specification for pnpm setup in deployment workflow 2025-09-24 17:14:58 +01:00
Pablo Hdez bf855d15e0 Merge pull request #772 from pheralb/dev
 svgl v5
2025-09-24 17:08:06 +01:00
pheralb 9cf1677e51 🛠️ Refactor svgr endpoint, disable optimization with `optimize` property
📦 Build / 🛠️ Build app (push) Has been cancelled
🧑‍🚀 Check / ⚙️ Linting (push) Has been cancelled
🧑‍🚀 Check / 📦 SVGs Size (push) Has been cancelled
2025-09-24 17:00:57 +01:00
pheralb 49f860f25f 🛠️ Fix Intello SVG routes + rename svg files 2025-09-24 16:47:23 +01:00
pheralb b7d05a0a56 🛠️ Merge branch 'main' of github.com:pheralb/svgl into dev 2025-09-24 16:43:39 +01:00
pheralb 138b5293ed 📦 Update Cursor SVGs 2025-09-24 16:41:13 +01:00
pheralb 195fae38e7 🔎 Add umami analytics script 2025-09-24 16:36:57 +01:00
pheralb 32a6eb4f4f New OG images + update static files 2025-09-24 15:16:08 +01:00
pheralb c33ef02f36 🐋 Remove env variable from Dockerfile 2025-09-24 14:17:01 +01:00
pheralb 559eb676d9 Add screenshot images 2025-09-24 14:09:30 +01:00
pheralb 45de1a631b 🛠️ Refactor favorites store, improve add/remove functions 2025-09-24 14:09:03 +01:00
pheralb 60314a9648 🛠️ Update GitHub link title, remove unused SvglVersion component, improve search input with name attribute & dialog, popover and tabs improvements 2025-09-24 14:08:07 +01:00
pheralb 26f6bb9061 Add open with v0 support 2025-09-24 13:58:05 +01:00
pheralb 83ec150266 🛠️ Update global layout and styling for header, sidebar, and page cards + improve responsive 2025-09-24 11:25:46 +01:00
Pablo Hdez 1e0bb95493 Merge pull request #769 from vladiantio/feat/add-kilo
📦 Add Kilo Code logo
2025-09-24 11:19:28 +01:00
Vladimir Antonio F. C. 01e0b7c66b 📦 Add Kilo Code logo 2025-09-23 10:48:12 -03:00
pheralb f505eea909 ⚙️ Refactor GitHub star count feature and update layout components 2025-09-22 23:37:03 +01:00
pheralb e5d130b0c5 🛠️ Fixed eslint errors 2025-09-22 19:14:05 +01:00
pheralb c306b57ce7 ️ Initial favorites page - show, delete & clear all SVGs 2025-09-22 19:12:33 +01:00
pheralb 1bf55e6c6e ⚙️ Refactor getInitialSettings(), delete unused functions 2025-09-22 19:11:14 +01:00
pheralb aae0771f18 🎨 Remove lazy loading from images and update sidebar badge classes for consistency 2025-09-22 18:57:20 +01:00
pheralb f749358b97 🛠️ Add warning store & message 2025-09-22 18:53:46 +01:00
pheralb 7c2ae97dd9 🎨 Remove unused font-face declaration 2025-09-21 19:20:12 +01:00
pheralb 50677ca3db 🎨 Update dialog title font size for improved readability 2025-09-21 19:16:56 +01:00
pheralb bf90439c63 🎨 Add switch UI component 2025-09-21 19:16:42 +01:00
pheralb 44fd09efb2 🔧 Refactor import statement for prettier and update formatting method 2025-09-21 19:16:17 +01:00
pheralb 3e507cf7c8 Add settings management with package manager and SVG optimization options 2025-09-21 19:15:11 +01:00
Pablo Hdez 8583871f73 Merge pull request #747 from RavianXReaver/dingocoinlogo
Add Dingocoin logo
2025-09-20 12:37:18 +01:00
pheralb 0c78255847 🎨 Update container and grid components for improved responsive design 2025-09-18 16:11:30 +01:00
pheralb 8a61650e4b 📄 Update shadcn/ui docs + add shadcn MCP server config 2025-09-18 12:48:44 +01:00
pheralb 4bd69b5ede Create <DocumentSettings /> component and integrate it into the docs page layout 2025-09-18 12:08:46 +01:00
pheralb aef80d6b7d 🎨 Add claude, markdown & openai svelte components 2025-09-18 12:08:16 +01:00
pheralb 452d121ac4 🛠️ Return `rawUrl & documentUrl` properties in content-collections config 2025-09-18 12:04:16 +01:00
pheralb 894aca0d14 🎨 Add dropdown-menu UI component 2025-09-18 12:03:20 +01:00
Pablo Hdez 11df0d13bd Merge pull request #766 from emanuelghdev/feat/add-webgl
Add WebGL logo
2025-09-18 09:09:51 +01:00
Pablo Hdez e35bb01927 Merge branch 'main' into feat/add-webgl 2025-09-18 09:09:22 +01:00
Pablo Hdez 5f5b3687c8 Merge pull request #767 from Jcampillo1207/main
Added Intello Logo
2025-09-18 09:08:26 +01:00
José Campillo 7f5c374d02 chagnes 2025-09-17 22:35:59 -06:00
pheralb b354a61eba Add link to shadcn/ui docs 2025-09-17 15:55:52 +01:00
pheralb 5d45c720b4 Improve `SVG not found` component with category context and update search placeholder 2025-09-17 12:03:56 +01:00
pheralb aeeaacd993 🛠️ Add font preloading, improve sveltekit assets URL 2025-09-17 10:02:24 +01:00
pheralb 077df6f0d5 🛠️ Remove unused font "OnestMedium" and clean up related styles 2025-09-17 10:00:20 +01:00
pheralb 9b9124b220 Add width and height attributes to SVG images for better layout control 2025-09-17 10:00:07 +01:00
emanuelghdev c7e86dd0f4 Add WebGL logo 2025-09-17 01:31:20 +02:00
pheralb 25ce756481 🐋 Add size check before production build
📦 Build / 🛠️ Build app (push) Has been cancelled
🧑‍🚀 Check / ⚙️ Linting (push) Has been cancelled
🧑‍🚀 Check / 📦 SVGs Size (push) Has been cancelled
2025-09-16 14:08:40 +01:00
pheralb 1da589f79d 🛠️ Rename `tCategory to Category` 2025-09-16 14:08:24 +01:00
pheralb 56125d2844 📦 Fixed SVGs, add viewbox property 2025-09-16 14:05:26 +01:00
pheralb 0b93f9b613 🔧 Refactor GitHub Actions workflow to streamline SVG size checking process 2025-09-16 14:04:59 +01:00
pheralb 12038062db Migrate /utils to Typescript + use tsx to run all scripts + improve types 2025-09-16 14:04:49 +01:00
pheralb 2198058131 🛠️ Merge branch 'main' of github.com:pheralb/svgl into dev 2025-09-16 13:46:39 +01:00
pheralb 0f2f026803 🎨 Add SvgNotFound component and integrate it into search results; update globals for SVG request links 2025-09-16 13:45:23 +01:00
pheralb 789fc0ce72 🎨 Design improvements + fixed content page size + improve accessibility 2025-09-16 13:28:44 +01:00
pheralb 5bc3616dec 🎨 Add Table of Contents component and type definitions 2025-09-16 13:27:45 +01:00
pheralb f411ffef8a 🛠️ Refactor API documentation for clarity and consistency; update URLs and TypeScript types 2025-09-16 13:24:13 +01:00
pheralb 3b3d30cd0c 🎨 Add collapsible UI component 2025-09-16 13:20:59 +01:00
pheralb 8e3ea5150a 🛠️ Remove "All" category from Category type definition 2025-09-16 08:39:02 +01:00
pheralb 895052ff7a 🎨 Update code styling for line numbers and add markdown styles 2025-09-16 08:38:51 +01:00
pheralb fdb9f91d2a 💚 Create Nuxt category & update svgs data
🧑‍🚀 Check / ⚡ Testing with Vitest (push) Has been cancelled
🧑‍🚀 Check / 📦 SVGs Size (push) Has been cancelled
📦 Build / 🛠️ Build app (push) Has been cancelled
🚀 Deploy / ☁️ API (push) Has been cancelled
2025-09-15 17:52:02 +01:00
Pablo Hdez 6aac1ebb6c Merge pull request #763 from HugoRCD/fix/nuxthub-wordmark
📦 Build / 🛠️ Build app (push) Has been cancelled
🧑‍🚀 Check / ⚡ Testing with Vitest (push) Has been cancelled
🧑‍🚀 Check / 📦 SVGs Size (push) Has been cancelled
🚀 Deploy / ☁️ API (push) Has been cancelled
Fix: URLs for NuxtHub wordmarks
2025-09-14 21:01:19 +01:00
Hugo 3497a6e1e9 Fix URLs for Nuxt Hub wordmarks 2025-09-14 20:46:30 +02:00
pheralb f2a61200b1 🛠️ (api) Rename query parameter from "raw" to "no-optimize" in SVG retrieval endpoint 2025-09-14 19:00:39 +01:00
pheralb 1c11725e01 🛠️ Update copyBtn styles for rehypeCopyBtn plugin + create generateToC() + return createdAt, updatedAt & tableOfContents 2025-09-14 19:00:20 +01:00
pheralb e7bbd32b56 📦 Add github-slugger dependency 2025-09-14 18:55:01 +01:00
pheralb 70ad2e0088 🎨 New svgl brand assets + update manifest & global app metatags 2025-09-14 18:53:24 +01:00
Pablo Hdez 907c2b9892 Merge pull request #761 from Mooshay105/patch-1-google-maps
📦 Build / 🛠️ Build app (push) Has been cancelled
🧑‍🚀 Check / ⚡ Testing with Vitest (push) Has been cancelled
🧑‍🚀 Check / 📦 SVGs Size (push) Has been cancelled
🚀 Deploy / ☁️ API (push) Has been cancelled
Add google maps
2025-09-14 18:09:05 +01:00
Pablo Hdez eb548fedc0 Merge pull request #762 from HugoRCD/feat/nuxt-ecosystem
feat: add nuxt ecosystem logo
2025-09-14 18:03:06 +01:00
Hugo Richard 7d3cd750de up 2025-09-14 15:10:16 +01:00
Hugo Richard 3f99f8a115 up 2025-09-14 15:10:06 +01:00
Hugo Richard 380b10690e feat: add nuxt ecosystem logo 2025-09-14 14:57:56 +01:00
Hugo Richard c39c2a2f5b feat: add nuxt ecosystem logo 2025-09-14 14:57:24 +01:00
Malcolm Hauser 2130d8d316 remove trailing comma 2025-09-12 12:53:13 +10:00
Malcolm Hauser 65651faa5b Add google maps 2025-09-12 12:33:52 +10:00
pheralb a05e849ddb 🛠️ Create rehypeCopyBtn & rehypeExternalLinks with custom types 2025-09-08 17:14:42 +01:00
pheralb 5c88b29387 📝 Update readme, add getting-started with API, improve header links & add framer extension 2025-09-08 12:35:08 +01:00
pheralb 31f2cefaba 📦 Add rehype-autolink-headings, rehype-slug & unist-util-visit dependencies 2025-09-08 12:19:43 +01:00
pheralb de47f2fa03 🛠️ (API) format & fixed eslint errors + improve types + improve error messages + add ?raw property 2025-09-08 12:19:15 +01:00
pheralb 6f7d3c51f9 ⬆️ (API) Upgrade upstash & hono dependencies & types 2025-09-08 11:42:13 +01:00
pheralb 074e9231d1 ⚙️ Merge branch 'main' of github.com:pheralb/svgl into dev 2025-09-07 13:41:21 +01:00
pheralb edb9ece0e3 ⚙️ Upgrade to eslint.config.ts + add typescript-eslint for typesafety + delete deprecated ts.config(), use export default instead 2025-09-06 17:58:30 +01:00
Pablo Hdez 3349654f79 Merge pull request #701 from agriyakhetarpal/add-zulip
📦 Build / 🛠️ Build app (push) Has been cancelled
🧑‍🚀 Check / ⚡ Testing with Vitest (push) Has been cancelled
🧑‍🚀 Check / 📦 SVGs Size (push) Has been cancelled
🚀 Deploy / ☁️ API (push) Has been cancelled
Add Zulip and Zulip wordmark logos
2025-09-06 11:16:26 +01:00
Pablo Hdez ada05ad75b Merge pull request #710 from estebancastano/main
feat: added mulesoft svg icon
2025-09-06 11:15:09 +01:00
Pablo Hdez 5b27f543fa Merge pull request #757 from ria-ahyoung/feature/add-apache-kafka
feat: 📦 Add Kafka logo and wordmark svg
2025-09-06 11:14:16 +01:00
ria-ang 6d031bc995 feat: 📦 Add Kafka logo and wordmark svg 2025-09-06 11:48:19 +09:00
pheralb ea4b598f2f Update Shiki configuration to include JSON and HTML languages
📦 Build / 🛠️ Build app (push) Has been cancelled
🧑‍🚀 Check / ⚙️ Linting (push) Has been cancelled
🧑‍🚀 Check / 📦 SVGs Size (push) Has been cancelled
2025-09-05 11:27:00 +01:00
pheralb fff76243ec ⚙️ Improve mode toggle functionality with keyboard shortcut support (CMD + L) 2025-09-05 11:26:44 +01:00
pheralb 9825fc2544 📦 Add SVGL for Framer extension with description and creator details 2025-09-05 11:14:39 +01:00
pheralb 04ffbe9bbb Refactor setup registry component and update documentation for shadcn/ui integration 2025-09-05 11:14:15 +01:00
pheralb bc831bed17 🎨 Initial responsive design + add sidebar menu to header component 2025-09-05 11:14:05 +01:00
pheralb d3e92602c1 📦 Add Sheet UI component 2025-09-05 11:07:09 +01:00
pheralb 3a11cd3d31 ⚙️ Update dialog title to reflect multiple SVG download options 2025-09-05 09:15:00 +01:00
pheralb e465cd7a51 🛠️ Merge branch 'main' of github.com:pheralb/svgl into dev 2025-09-05 09:11:45 +01:00
Pablo Hdez f45b2d9d27 Merge pull request #756 from jonahsnider/sanity-rebrand
📦 Build / 🛠️ Build app (push) Has been cancelled
🧑‍🚀 Check / ⚡ Testing with Vitest (push) Has been cancelled
🧑‍🚀 Check / 📦 SVGs Size (push) Has been cancelled
🚀 Deploy / ☁️ API (push) Has been cancelled
Update Sanity wordmark and add logo
2025-09-05 09:00:38 +01:00
Jonah Snider 9db8deed28 Update Sanity wordmark and add logo 2025-09-04 17:31:53 -07:00
pheralb 1832eaceca ⚙️ Add getParamValue() function to get searchParam value
📦 Build / 🛠️ Build app (push) Has been cancelled
🧑‍🚀 Check / ⚙️ Linting (push) Has been cancelled
🧑‍🚀 Check / 📦 SVGs Size (push) Has been cancelled
2025-09-04 19:18:29 +01:00
pheralb 32558885e6 🛠️ Delete unused displaySvgs state, simplify updateDisplaySvgs utility 2025-09-04 19:17:55 +01:00
pheralb 6ffc890a15 🛠️ Adjust height in sidebar and pageCard components 2025-09-04 18:56:57 +01:00
pheralb aa87b899a6 🛠️ Update radial button variant styles for improved dark mode support 2025-09-04 10:46:02 +01:00
pheralb 26f23f7e5b Add extension page, create extension component for displaying items & add search functionality 2025-09-04 10:45:56 +01:00
pheralb 74e42b00dc 🎨 Refactor grid and header components; improve props handling and layout consistency, update search functions to use new naming convention, and remove unused view transitions component 2025-09-04 10:45:12 +01:00
pheralb 8e27a8053d 🛠️ Remove unnecessary tracking-tight class from body element for improved layout consistency 2025-09-04 09:19:35 +01:00
pheralb 1aadeb5604 🛠️ Update extensions data; add new extensions for various frameworks and improve existing entries 2025-09-04 09:15:17 +01:00
pheralb ec6db6d23b 🛠️ Refactor search functions in searchWithFuse.ts; rename searchWithFuse to searchSvgsWithFuse and add searchExtensionsWithFuse for improved extension searching 2025-09-04 09:15:01 +01:00
pheralb 803e13001a 🛠️ Refactor header component; streamline button classes and improve layout consistency
📦 Build / 🛠️ Build app (push) Has been cancelled
🧑‍🚀 Check / ⚙️ Linting (push) Has been cancelled
🧑‍🚀 Check / 📦 SVGs Size (push) Has been cancelled
2025-09-01 17:44:16 +01:00
pheralb 77356d3215 🛠️ Refactor SVG filtering logic in load function; ensure base data is used for `searchWithFuse` 2025-09-01 11:49:09 +01:00
pheralb 1591ea3146 🛠️ Refactor svgCard and index files; streamline image handling and improve type definitions 2025-09-01 11:48:51 +01:00
pheralb 2a38b834c3 🛠️ Refactor search handling in search.svelte and +page.svelte; implement custom addParams and deleteParam utility 2025-09-01 11:34:24 +01:00
pheralb e6d441e9f2 🛠️ Refactor load function in +page.ts to use getSvgsByCategory for improved category filtering and sorting logic 2025-09-01 11:27:53 +01:00
pheralb bc34bdc904 🛠️ Update PageCard component to include container and content card classes for improved styling 2025-09-01 11:27:33 +01:00
pheralb 2692c7d34d 🛠️ Rename type tCategory to Category for consistency in type definitions 2025-09-01 11:27:22 +01:00
pheralb 55199765be 🛠️ Improve parseSvgFilename to support firstUpperCase option for component naming
📦 Build / 🛠️ Build app (push) Has been cancelled
🧑‍🚀 Check / ⚙️ Linting (push) Has been cancelled
🧑‍🚀 Check / 📦 SVGs Size (push) Has been cancelled
2025-08-31 16:35:30 +01:00
pheralb 4cd2c84273 🛠️ Refactor parseSvgFilename function for improved component name formatting; add directory and file existence checks in generate-registry 2025-08-31 16:19:44 +01:00
pheralb 733e136b3a 🛠️ Update registry command & logs 2025-08-31 15:46:43 +01:00
pheralb 2c3fdf79fe 🐋 Initial Dockerfile config with .dockerignore 2025-08-31 15:35:11 +01:00
pheralb 374fb8f2d5 🛠️ Trying nixpacks config for Node & PNPM setup 2025-08-31 14:56:11 +01:00
pheralb 2927e42659 🛠️ Trying nixpacks config for Node & PNPM setup 2025-08-31 14:45:42 +01:00
pheralb d947f7f907 🛠️ Remove nixpacks config file 2025-08-31 14:29:10 +01:00
pheralb d2e418363d 🛠️ Refactor ESLint rules for Svelte components to improve linting accuracy 2025-08-31 14:26:04 +01:00
pheralb 5df7a336ba 🛠️ Fixed eslint errors 2025-08-31 14:25:57 +01:00
pheralb a488bd4c7a 🛠️ Update workflows to use Eslint for linting and set `PUBLIC_SVGL_VERSION` env variable
📦 Build / 🛠️ Build app (push) Has been cancelled
🧑‍🚀 Check / 📦 SVGs Size (push) Has been cancelled
🧑‍🚀 Check / ⚙️ Linting (push) Has been cancelled
2025-08-31 13:59:13 +01:00
pheralb 411ad69a8f 🛠️ Add nixpacks configuration for PNPM setup and build process 2025-08-31 13:48:10 +01:00
pheralb 68e399d99c 🎨 Design improvements 2025-08-31 13:43:53 +01:00
pheralb d06c87037a 🛠️ Improve svgCard component to support theme-based image rendering 2025-08-31 13:43:31 +01:00
pheralb 73bd5a4f78 🛠️ Create codeBlock UI component + setup shadcn/ui registry 2025-08-31 13:43:17 +01:00
pheralb 26b8f0a2ae 🛠️ Create custom svgl version component 2025-08-30 22:31:51 +01:00
pheralb 85e6bb33b8 🛠️ Update pnpm setup to version 10 in deploy API workflow 2025-08-30 16:26:09 +01:00
pheralb 2f3ef58218 🛠️ Remove version specification for pnpm setup in workflows 2025-08-30 16:25:56 +01:00
RavianXReaver 82d4967e13 Merge branch 'main' into dingocoinlogo 2025-08-30 15:39:47 +09:00
RavianXReaver 3943e624da Merge branch 'main' into dingocoinlogo 2025-08-27 10:39:54 +09:00
RavianXReaver a1d61d73fa Added Dingocoin logo 2025-08-26 17:31:31 +09:00
Agriya Khetarpal 5e2a5e4d50 Merge main 2025-07-31 17:20:03 +05:30
estebancastano 7b5e1d99da feat: added mulesoft svg icon 2025-07-19 19:37:39 -05:00
Agriya Khetarpal 4347dc52c3 Register Zulip set of logos 2025-07-15 05:26:27 +05:30
Agriya Khetarpal 144fa7d7c4 Add Zulip and Zulip wordmark logos 2025-07-15 05:25:02 +05:30
193 changed files with 3905 additions and 1803 deletions
+19
View File
@@ -0,0 +1,19 @@
Dockerfile
.dockerignore
.git
.gitignore
.gitattributes
README.md
.npmrc
.prettierrc
prettier.config.mjs
.eslintrc.cjs
eslint.config.mjs
.graphqlrc
.editorconfig
.svelte-kit
.vscode
node_modules
build
package
**/.env
+3 -3
View File
@@ -13,10 +13,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup pnpm 10
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Get pnpm store directory
shell: bash
@@ -36,3 +34,5 @@ jobs:
- name: Build app
run: pnpm build
env:
PUBLIC_SVGL_VERSION: v5
+11 -15
View File
@@ -11,22 +11,22 @@ on:
- dev
jobs:
vitest:
lint:
runs-on: ubuntu-latest
name: ⚡ Testing with Vitest
name: ⚙️ Linting
steps:
- uses: actions/checkout@v4
- name: Setup pnpm 10
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install dependencies
run: pnpm install
- name: Run Vitest
run: pnpm test
- name: Run Eslint
run: pnpm lint
env:
PUBLIC_SVGL_VERSION: v5
svgs-size:
runs-on: ubuntu-latest
@@ -34,15 +34,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup pnpm 10
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Install utility dependencies
- name: Install dependencies
run: pnpm install
working-directory: ./utils/check-size
- name: Check svgs size
run: pnpm start
working-directory: ./utils/check-size
- name: Check SVGs size
run: pnpm check:size
+2 -4
View File
@@ -12,10 +12,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup pnpm 9
uses: pnpm/action-setup@v2
with:
version: 9
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Install global dependencies
run: pnpm install
+37
View File
@@ -0,0 +1,37 @@
FROM node:22.17.0-alpine AS base
# Install pnpm
RUN npm install -g pnpm@10.13.1
# Set working directory
WORKDIR /app
# Install dependencies with cache
FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Build the application
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm run check:size
RUN pnpm run build:prod
# Production image
FROM node:22.17.0-alpine AS runner
WORKDIR /app
# Copy necessary files from builder
COPY --from=builder /app/build ./build
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./
# Set production environment
ENV NODE_ENV=production
# Expose port
EXPOSE 3000
# Start the server
CMD ["node", "build"]
+94 -42
View File
@@ -1,6 +1,6 @@
<div align="center">
<a href="https://svgl.app">
<img src="static/images/readme.png">
<img src="static/images/screenshot_dark.png">
</a>
<p></p>
</div>
@@ -10,25 +10,29 @@
Explore
</a>
<span>&nbsp;✦&nbsp;</span>
<a href="https://github.com/pheralb/svgl/issues/new?assignees=&labels=request&projects=&template=request-svg.yml&title=%5B%F0%9F%94%94+Request+SVG%5D%3A+">
Request logo
<a href="https://github.com/sponsors/pheralb">
Sponsor this project
</a>
<span>&nbsp;✦&nbsp;</span>
<a href="#-getting-started">
Submit logo
Getting Started
</a>
<span>&nbsp;✦&nbsp;</span>
<a href="#-extensions">
<a href="https://svgl.app/extensions" target="_blank">
Extensions
</a>
<span>&nbsp;✦&nbsp;</span>
<a href="https://svgl.app/api">
API
<a href="#-stack">
Stack
</a>
<span>&nbsp;✦&nbsp;</span>
<a href="#%EF%B8%8F-contributing">
<a href="#-contributing">
Contributing
</a>
<span>&nbsp;✦&nbsp;</span>
<a href="#-license">
License
</a>
</div>
</p>
@@ -48,50 +52,51 @@
## 📦 Extensions
A list of extensions that use the [svgl API](https://svgl.app/api), created by the community:
A list of extensions that use the [**SVGL API**](https://svgl.app/docs/api), created by the community:
| | Extension | Description | Created by | Link |
| ---------------------------------------------------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/svgl.svg" height="25" /> | SVGL CLI | A CLI for easily adding SVG icons to your project. | [sujjeee](https://twitter.com/sujjeeee) | [GitHub Repository](https://github.com/sujjeee/svgls) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/react_light.svg" height="25" /> | SVGL for React | An open-source NPM package that offers a SVGL Logos for React. | [ridemountainpig](https://x.com/ridemountainpig) | [GitHub Repository](https://github.com/ridemountainpig/svgl-react?tab=readme-ov-file#svgl-react) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/vue.svg" height="25" /> | SVGL for Vue | An open-source NPM package that offers a SVGL Logos for Vue. | [selemondev](https://x.com/selemondev) | [GitHub Repository](https://github.com/selemondev/svgl-vue?tab=readme-ov-file#--svgl-vue--) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/svelte.svg" height="25" /> | SVGL for Svelte | An open-source NPM package that offers a SVGL Logos for Svelte. | [selemondev](https://x.com/selemondev) | [GitHub Repository](https://github.com/selemondev/svgl-svelte#--svgl-svelte--) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/figma.svg" height="25" /> | SVGL for Figma | Add svgs from svgl to your Figma project. | [quilljou](https://twitter.com/quillzhou) | [Figma Plugin](https://www.figma.com/community/plugin/1320306989350693206/svgl) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/powertoys.svg" height="25" /> | SVGL for PowerToys | Search & copy SVG logos in PowerToys Run. | [SameerJS6](https://x.com/Sameerjs6) | [Website](https://svgl.sameerjs.com/) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/raycast.svg" height="25" /> | SVGL for Raycast | Search SVG logos via svgl. | [1weiho](https://twitter.com/1weiho) | [Raycast Store](https://www.raycast.com/1weiho/svgl) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/vscode.svg" height="25" /> | SVGL for VSCode | SVGL directly in your VSCode. | [girlazote](https://twitter.com/girlazote) | [VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=EsteveSegura.svgl) |
| <img src="https://svgl-badge.vercel.app/api/Library/Svgl?theme=light" height="25" /> | SVGL Badge | A beautiful badges with svgl SVG logos. | [ridemountainpig](https://twitter.com/ridemountainpig) | [Website](https://svgl-badge.vercel.app/) |
| <img src="https://github.com/serafimcloud/21st/blob/main/apps/web/public/icon.png?raw=true" height="25" /> | Magic | AI extension for Cursor & other IDEs | [serafimcloud](https://x.com/serafimcloud) | [Website](https://21st.dev/magic) |
| <img src="/static/library/powershell.svg" height="25" /> | SVGL for PowerShell | PowerShell extension to quickly get svgl logos anywhere | [Bart Spaans](https://bsky.app/profile/bartspaans.bsky.social) | [GitHub](https://github.com/spaansba/SVGL-PowerShell) |
| <img src="/static/library/FlowLauncher.svg" height="25"> | SVGL for Flow Launcher | Search & copy SVG logos in Flow Launcher | [AF_Askar](https://x.com/Askar_AF) | [GitHub](https://github.com/abo3skr2019/SVGl-plugin) |
| | Extension | Description | Created by | Link |
| ---------------------------------------------------------------------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/svgl.svg" height="25" /> | SVGL CLI | A CLI for easily adding SVG icons to your project. | [sujjeee](https://twitter.com/sujjeeee) | [GitHub Repository](https://github.com/sujjeee/svgls) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/framer_dark.svg" height="25" /> | SVGL for Framer | Our SVGL plugin for Framer simplifies the use of SVG-based colourful logos. Easily import and easy to use. | [Krishna Singh](https://x.com/krishnasinghdev) | [Framer Marketplace](https://www.framer.com/marketplace/plugins/svgl/) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/react_light.svg" height="25" /> | SVGL for React | An open-source NPM package that offers a SVGL Logos for React. | [ridemountainpig](https://x.com/ridemountainpig) | [GitHub Repository](https://github.com/ridemountainpig/svgl-react?tab=readme-ov-file#svgl-react) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/vue.svg" height="25" /> | SVGL for Vue | An open-source NPM package that offers a SVGL Logos for Vue. | [selemondev](https://x.com/selemondev) | [GitHub Repository](https://github.com/selemondev/svgl-vue?tab=readme-ov-file#--svgl-vue--) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/svelte.svg" height="25" /> | SVGL for Svelte | An open-source NPM package that offers a SVGL Logos for Svelte. | [selemondev](https://x.com/selemondev) | [GitHub Repository](https://github.com/selemondev/svgl-svelte#--svgl-svelte--) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/figma.svg" height="25" /> | SVGL for Figma | Add svgs from svgl to your Figma project. | [quilljou](https://twitter.com/quillzhou) | [Figma Plugin](https://www.figma.com/community/plugin/1320306989350693206/svgl) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/powertoys.svg" height="25" /> | SVGL for PowerToys | Search & copy SVG logos in PowerToys Run. | [SameerJS6](https://x.com/Sameerjs6) | [Website](https://svgl.sameerjs.com/) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/raycast.svg" height="25" /> | SVGL for Raycast | Search SVG logos via svgl. | [1weiho](https://twitter.com/1weiho) | [Raycast Store](https://www.raycast.com/1weiho/svgl) |
| <img src="https://github.com/pheralb/svgl/blob/main/static/library/vscode.svg" height="25" /> | SVGL for VSCode | SVGL directly in your VSCode. | [girlazote](https://twitter.com/girlazote) | [VSCode Marketplace](https://marketplace.visualstudio.com/items?itemName=EsteveSegura.svgl) |
| <img src="https://svgl-badge.vercel.app/api/Library/Svgl?theme=light" height="25" /> | SVGL Badge | A beautiful badges with svgl SVG logos. | [ridemountainpig](https://twitter.com/ridemountainpig) | [Website](https://svgl-badge.vercel.app/) |
| <img src="https://github.com/serafimcloud/21st/blob/main/apps/web/public/icon.png?raw=true" height="25" /> | Magic | AI extension for Cursor & other IDEs | [serafimcloud](https://x.com/serafimcloud) | [Website](https://21st.dev/magic) |
| <img src="/static/library/powershell.svg" height="25" /> | SVGL for PowerShell | PowerShell extension to quickly get svgl logos anywhere | [Bart Spaans](https://bsky.app/profile/bartspaans.bsky.social) | [GitHub Repository](https://github.com/spaansba/SVGL-PowerShell) |
| <img src="/static/library/FlowLauncher.svg" height="25"> | SVGL for Flow Launcher | Search & copy SVG logos in Flow Launcher | [AF_Askar](https://x.com/Askar_AF) | [GitHub Repository](https://github.com/abo3skr2019/SVGl-plugin) |
## 🛠️ Stack
- [**Sveltekit**](https://kit.svelte.dev/) - Web development, streamlined.
- [**Sveltekit** + **Svelte 5**](https://kit.svelte.dev/) - Web development, streamlined.
- [**Typescript**](https://www.typescriptlang.org/) - JavaScript with syntax for types.
- [**mdsvex**](https://mdsvex.com/) - Markdown for Svelte apps.
- [**Content-Collections**](https://www.content-collections.dev/) - Transform your content into type-safe data collections and say goodbye to manual data fetching and parsing.
- [**Shiki**](https://github.com/shikijs/shiki) - A beautiful Syntax Highlighter.
- [**Tailwindcss**](https://tailwindcss.com/) - A utility-first CSS framework for rapidly building custom designs.
- [**Tailwind CSS**](https://tailwindcss.com/) - A utility-first CSS framework for rapidly building custom designs.
- [**bits-ui**](https://www.bits-ui.com) - A collection of headless components for Svelte.
- [**clsx**](https://github.com/lukeed/clsx) + [**tailwind-merge**](https://github.com/dcastil/tailwind-merge) inspired by [shadcn/ui](https://ui.shadcn.com) - A tiny utility for constructing `className` strings conditionally.
- [**Prettier**](https://prettier.io/) + [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) - An opinionated code formatter.
- [**Lucide Icons**](https://lucide.dev/) + [**phosphor-svelte**](https://github.com/haruaki07/phosphor-svelte) - A clean and friendly icons libraries.
- [**Lucide Icons**](https://lucide.dev/) - Beautiful &
consistent icons.
- [**svelte-sonner**](https://github.com/wobsoriano/svelte-sonner) - An opinionated toast component for Svelte.
- [**@svgr/core**](https://react-svgr.com/) - Node.js utility to transform SVGs into React components.
- [**Hono**](https://hono.dev/) - Fast, lightweight, built on Web Standards. Support for any JavaScript runtime.
- [**@upstash/redis** + **@upstash/ratelimit**](https://upstash.com/) - Serverless Redis for developers.
- [**Vitest**](https://vitest.dev/) - Blazing Fast Unit Test Framework.
## 🚀 Getting Started
> [!IMPORTANT]
> Before submitting the SVG, **make sure that you have permission** or that the license of the SVG allows you to add it to svgl. If you are not sure, please contact the company or author.
> Before submitting an SVG, ensure you have the right to use it and that its license permits adding it to svgl. If you are uncertain, please contact the author or the company.
You will need:
- [Node.js 18+ (recommended 20 LTS)](https://nodejs.org/en/).
- [Node.js 20+](https://nodejs.org/en/).
- [Git](https://git-scm.com/).
1. [Fork](https://github.com/pheralb/svgl/fork) this repository and clone it locally:
1. [**Fork this repository**](https://github.com/pheralb/svgl/fork) and clone it locally:
```bash
git clone git@github.com:your_username/svgl.git
@@ -128,7 +133,7 @@ pnpm install
}
```
- **Logo + wordmark** version:
- **Simple logo + wordmark**:
```ts
{
@@ -140,7 +145,35 @@ pnpm install
}
```
- **Logo + wordmark** & **light + dark mode**:
- **Logo (light & dark mode)**:
```ts
{
title: 'Title',
category: 'Category',
route: {
light: '/library/your_logo_light.svg',
dark: '/library/your_logo_dark.svg'
},
url: 'Website'
}
```
- **Wordmark (light & dark mode)**:
```ts
{
title: 'Title',
category: 'Category',
wordmark: {
light: '/library/your_logo_light.svg',
dark: '/library/your_logo_dark.svg'
},
url: 'Website'
}
```
- **Full example with all properties**:
```ts
{
@@ -151,14 +184,14 @@ pnpm install
dark: '/library/your_logo_dark.svg'
},
wordmark: {
light: '/library/your_wordmark-logo_light.svg',
dark: '/library/your_wordmark-logo_dark.svg'
light: '/library/your_logo_wordmark_light.svg',
dark: '/library/your_logo_wordmark_dark.svg'
},
url: 'Website'
}
```
- **Add brand guidelines**:
- **Add brand guidelines** (where to find the images, how to use it, colors, fonts...):
```ts
{
@@ -176,17 +209,36 @@ pnpm install
> - The list of categories is here: [`src/types/categories.ts`](https://github.com/pheralb/svgl/blob/main/src/types/categories.ts). You can add a new category if you need it.
> - You can add multiple categories to the same logo, for example: `category: ['Social', 'Design']`.
And create a pull request with your logo 🚀.
And create a pull request with your logo .
5. (Optional) If you want to run the [API](https://svgl.app/api) locally, you will need to create a `.dev.vars` file in the [`/api-routes`](https://github.com/pheralb/svgl/tree/main/api-routes) folder with the following variables:
## 🧑‍🚀 Getting Started with API
> [!WARNING]
> This section is how to run API locally. For all API endpoints, check the [**API documentation**](https://svgl.app/api).
1. Go to the [**`api-routes`**](https://github.com/pheralb/svgl/tree/main/api-routes) folder and install the dependencies with [pnpm](https://pnpm.io/):
```bash
cd api-routes
pnpm install
```
2. Create a `.dev.vars` env file in the `api-routes` folder with the following variables:
```bash
# .dev.vars
SVGL_API_REQUESTS = 1
UPSTASH_REDIS_URL = ""
UPSTASH_REDIS_TOKEN = ""
```
- [Create a Upstash account](https://console.upstash.com/).
- [Create a Upstash Redis Database](https://upstash.com/docs/redis/overall/getstarted).
3. Run the development server:
```bash
SVGL_API_REQUESTS = 1
UPSTASH_REDIS_URL = ""
UPSTASH_REDIS_TOKEN = ""
pnpm dev
```
## ✌️ Contributing
+4 -3
View File
@@ -11,10 +11,11 @@
},
"dependencies": {
"@upstash/ratelimit": "2.0.6",
"hono": "4.8.12"
"@upstash/redis": "1.35.3",
"hono": "4.9.6"
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250805.0",
"wrangler": "4.28.0"
"@cloudflare/workers-types": "4.20250906.0",
"wrangler": "4.34.0"
}
}
+74 -71
View File
@@ -10,17 +10,20 @@ importers:
dependencies:
'@upstash/ratelimit':
specifier: 2.0.6
version: 2.0.6(@upstash/redis@1.34.0)
version: 2.0.6(@upstash/redis@1.35.3)
'@upstash/redis':
specifier: 1.35.3
version: 1.35.3
hono:
specifier: 4.8.12
version: 4.8.12
specifier: 4.9.6
version: 4.9.6
devDependencies:
'@cloudflare/workers-types':
specifier: 4.20250805.0
version: 4.20250805.0
specifier: 4.20250906.0
version: 4.20250906.0
wrangler:
specifier: 4.28.0
version: 4.28.0(@cloudflare/workers-types@4.20250805.0)
specifier: 4.34.0
version: 4.34.0(@cloudflare/workers-types@4.20250906.0)
packages:
@@ -28,47 +31,47 @@ packages:
resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==}
engines: {node: '>=18.0.0'}
'@cloudflare/unenv-preset@2.6.0':
resolution: {integrity: sha512-h7Txw0WbDuUbrvZwky6+x7ft+U/Gppfn/rWx6IdR+e9gjygozRJnV26Y2TOr3yrIFa6OsZqqR2lN+jWTrakHXg==}
'@cloudflare/unenv-preset@2.7.2':
resolution: {integrity: sha512-JY7Uf8GhWcbOMDZX8ke2czp9f9TijvJN4CpRBs3+WYN9U7jHpj3XaV+HHm78iHkAwTm/JeBHqyQNhq/PizynRA==}
peerDependencies:
unenv: 2.0.0-rc.19
workerd: ^1.20250802.0
unenv: 2.0.0-rc.20
workerd: ^1.20250828.1
peerDependenciesMeta:
workerd:
optional: true
'@cloudflare/workerd-darwin-64@1.20250803.0':
resolution: {integrity: sha512-6QciMnJp1p3F1qUiN0LaLfmw7SuZA/gfUBOe8Ft81pw16JYZ3CyiqIKPJvc1SV8jgDx8r+gz/PRi1NwOMt329A==}
'@cloudflare/workerd-darwin-64@1.20250902.0':
resolution: {integrity: sha512-mwC/YEtDUGfnjXdbW5Lya+bgODrpJ5RxxqpaTjtMJycqnjR0RZgVpOqISwGfBHIhseykU3ahPugM5t91XkBKTg==}
engines: {node: '>=16'}
cpu: [x64]
os: [darwin]
'@cloudflare/workerd-darwin-arm64@1.20250803.0':
resolution: {integrity: sha512-DoIgghDowtqoNhL6OoN/F92SKtrk7mRQKc4YSs/Dst8IwFZq+pCShOlWfB0MXqHKPSoiz5xLSrUKR9H6gQMPvw==}
'@cloudflare/workerd-darwin-arm64@1.20250902.0':
resolution: {integrity: sha512-5Wr6a5/ixoXuMPOvbprN8k9HhAHDBh8f7H5V4DN/Xb4ORoGkI9AbC5QPpYV0wa3Ncf+CRSGobdmZNyO24hRccA==}
engines: {node: '>=16'}
cpu: [arm64]
os: [darwin]
'@cloudflare/workerd-linux-64@1.20250803.0':
resolution: {integrity: sha512-mYdz4vNWX3+PoqRjssepVQqgh42IBiSrl+wb7vbh7VVWUVzBnQKtW3G+UFiBF62hohCLexGIEi7L0cFfRlcKSQ==}
'@cloudflare/workerd-linux-64@1.20250902.0':
resolution: {integrity: sha512-1yJGt56VQBuG01nrhkRGoa1FGz7xQwJTrgewxt/MRRtigZTf84qJQiPQxyM7PQWCLREKa+JS7G8HFqvOwK7kZA==}
engines: {node: '>=16'}
cpu: [x64]
os: [linux]
'@cloudflare/workerd-linux-arm64@1.20250803.0':
resolution: {integrity: sha512-RmrtUYLRUg6djKU7Z6yebS6YGJVnaDVY6bbXca+2s26vw4ibJDOTPLuBHFQF62Grw3fAfsNbjQh5i14vG2mqUg==}
'@cloudflare/workerd-linux-arm64@1.20250902.0':
resolution: {integrity: sha512-ArDodWzfo0BVqMQGUgaOGV5Mzf8wEMUX8TJonExpGbYavoVXVDbp2rTLFRJg1vkFGpmw1teCtSoOjSDisFZQMg==}
engines: {node: '>=16'}
cpu: [arm64]
os: [linux]
'@cloudflare/workerd-windows-64@1.20250803.0':
resolution: {integrity: sha512-uLV8gdudz36o9sUaAKbBxxTwZwLFz1KyW7QpBvOo4+r3Ib8yVKXGiySIMWGD7A0urSMrjf3e5LlLcJKgZUOjMA==}
'@cloudflare/workerd-windows-64@1.20250902.0':
resolution: {integrity: sha512-DT/o8ZSkmze1YGI7vgVt4ST+VYGb3tNChiFnOM9Z8YOejqKqbVvATB4gi/xMSnNR9CsKFqH4hHWDDtz+wf4uZg==}
engines: {node: '>=16'}
cpu: [x64]
os: [win32]
'@cloudflare/workers-types@4.20250805.0':
resolution: {integrity: sha512-HOt0lqFiw5WzhvxH/IViMAWI/zwzokCSx33DlRnJqECT9khskK9X4Jrw/+IiAprJ5YloiFxK8Xn1oGbsabdUWg==}
'@cloudflare/workers-types@4.20250906.0':
resolution: {integrity: sha512-CMRTupQpAdNZJrxRGaM2JzxmpWOnzgxcyTGmjAOcosRfi1ZsNUTAZ0kj1dzY+4bPDIdFwvvJL3t91DEpqitOJg==}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
@@ -367,8 +370,8 @@ packages:
peerDependencies:
'@upstash/redis': ^1.34.3
'@upstash/redis@1.34.0':
resolution: {integrity: sha512-TrXNoJLkysIl8SBc4u9bNnyoFYoILpCcFJcLyWCccb/QSUmaVKdvY0m5diZqc3btExsapcMbaw/s/wh9Sf1pJw==}
'@upstash/redis@1.35.3':
resolution: {integrity: sha512-hSjv66NOuahW3MisRGlSgoszU2uONAY2l5Qo3Sae8OT3/Tng9K+2/cBRuyPBX8egwEGcNNCF9+r0V6grNnhL+w==}
acorn-walk@8.3.2:
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
@@ -400,9 +403,6 @@ packages:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
@@ -433,8 +433,8 @@ packages:
glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
hono@4.8.12:
resolution: {integrity: sha512-MQSKk1Mg7b74k8l+A025LfysnLtXDKkE4pLaSsYRQC5iy85lgZnuyeQ1Wynair9mmECzoLu+FtJtqNZSoogBDQ==}
hono@4.9.6:
resolution: {integrity: sha512-doVjXhSFvYZ7y0dNokjwwSahcrAfdz+/BCLvAMa/vHLzjj8+CFyV5xteThGUsKdkaasgN+gF2mUxao+SGLpUeA==}
engines: {node: '>=16.9.0'}
is-arrayish@0.3.2:
@@ -449,8 +449,8 @@ packages:
engines: {node: '>=10.0.0'}
hasBin: true
miniflare@4.20250803.0:
resolution: {integrity: sha512-1tmCLfmMw0SqRBF9PPII9CVLQRzOrO7uIBmSng8BMSmtgs2kos7OeoM0sg6KbR9FrvP/zAniLyZuCAMAjuu4fQ==}
miniflare@4.20250902.0:
resolution: {integrity: sha512-QHjI17yVDxDXsjDvX6GNRySx2uYsQJyiZ2MRBAsA0CFpAI2BcHd4oz0FIjbqgpZK+4Fhm7OKht/AfBNCd234Zg==}
engines: {node: '>=18.0.0'}
hasBin: true
@@ -489,24 +489,27 @@ packages:
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
uncrypto@0.1.3:
resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
undici@7.13.0:
resolution: {integrity: sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA==}
engines: {node: '>=20.18.1'}
unenv@2.0.0-rc.19:
resolution: {integrity: sha512-t/OMHBNAkknVCI7bVB9OWjUUAwhVv9vsPIAGnNUxnu3FxPQN11rjh0sksLMzc3g7IlTgvHmOTl4JM7JHpcv5wA==}
unenv@2.0.0-rc.20:
resolution: {integrity: sha512-8tn4tAl9vD5nWoggAAPz28vf0FY8+pQAayhU94qD+ZkIbVKCBAH/E1MWEEmhb9Whn5EgouYVfBJB20RsTLRDdg==}
workerd@1.20250803.0:
resolution: {integrity: sha512-oYH29mE/wNolPc32NHHQbySaNorj6+KASUtOvQHySxB5mO1NWdGuNv49woxNCF5971UYceGQndY+OLT+24C3wQ==}
workerd@1.20250902.0:
resolution: {integrity: sha512-rM+8ARYoy9gWJNPW89ERWyjbp7+m1hu6PFbehiP8FW9Hm5kNVo71lXFrkCP2HSsTP1OLfIU/IwanYOijJ0mQDw==}
engines: {node: '>=16'}
hasBin: true
wrangler@4.28.0:
resolution: {integrity: sha512-y0yHIuScpok9oSErLqDbxkBChC2+/jZpvqMg2NxOto1JCyUtDUuKljOfcVMaI48d9GuhOCSoWSumYxLAHNxaLA==}
wrangler@4.34.0:
resolution: {integrity: sha512-iU+T8klWX6M/oN9y2PG8HrekoHwlBs/7wNMouyRToCJGn5EFtVl98a1fxxPCgkuUNZ2sKLrCyx/TlhgilIlqpQ==}
engines: {node: '>=18.0.0'}
hasBin: true
peerDependencies:
'@cloudflare/workers-types': ^4.20250803.0
'@cloudflare/workers-types': ^4.20250902.0
peerDependenciesMeta:
'@cloudflare/workers-types':
optional: true
@@ -538,28 +541,28 @@ snapshots:
dependencies:
mime: 3.0.0
'@cloudflare/unenv-preset@2.6.0(unenv@2.0.0-rc.19)(workerd@1.20250803.0)':
'@cloudflare/unenv-preset@2.7.2(unenv@2.0.0-rc.20)(workerd@1.20250902.0)':
dependencies:
unenv: 2.0.0-rc.19
unenv: 2.0.0-rc.20
optionalDependencies:
workerd: 1.20250803.0
workerd: 1.20250902.0
'@cloudflare/workerd-darwin-64@1.20250803.0':
'@cloudflare/workerd-darwin-64@1.20250902.0':
optional: true
'@cloudflare/workerd-darwin-arm64@1.20250803.0':
'@cloudflare/workerd-darwin-arm64@1.20250902.0':
optional: true
'@cloudflare/workerd-linux-64@1.20250803.0':
'@cloudflare/workerd-linux-64@1.20250902.0':
optional: true
'@cloudflare/workerd-linux-arm64@1.20250803.0':
'@cloudflare/workerd-linux-arm64@1.20250902.0':
optional: true
'@cloudflare/workerd-windows-64@1.20250803.0':
'@cloudflare/workerd-windows-64@1.20250902.0':
optional: true
'@cloudflare/workers-types@4.20250805.0': {}
'@cloudflare/workers-types@4.20250906.0': {}
'@cspotcode/source-map-support@0.8.1':
dependencies:
@@ -747,16 +750,16 @@ snapshots:
'@upstash/core-analytics@0.0.10':
dependencies:
'@upstash/redis': 1.34.0
'@upstash/redis': 1.35.3
'@upstash/ratelimit@2.0.6(@upstash/redis@1.34.0)':
'@upstash/ratelimit@2.0.6(@upstash/redis@1.35.3)':
dependencies:
'@upstash/core-analytics': 0.0.10
'@upstash/redis': 1.34.0
'@upstash/redis': 1.35.3
'@upstash/redis@1.34.0':
'@upstash/redis@1.35.3':
dependencies:
crypto-js: 4.2.0
uncrypto: 0.1.3
acorn-walk@8.3.2: {}
@@ -782,8 +785,6 @@ snapshots:
cookie@1.0.2: {}
crypto-js@4.2.0: {}
defu@6.1.4: {}
detect-libc@2.0.3: {}
@@ -827,7 +828,7 @@ snapshots:
glob-to-regexp@0.4.1: {}
hono@4.8.12: {}
hono@4.9.6: {}
is-arrayish@0.3.2: {}
@@ -835,7 +836,7 @@ snapshots:
mime@3.0.0: {}
miniflare@4.20250803.0:
miniflare@4.20250902.0:
dependencies:
'@cspotcode/source-map-support': 0.8.1
acorn: 8.14.0
@@ -845,7 +846,7 @@ snapshots:
sharp: 0.33.5
stoppable: 1.1.0
undici: 7.13.0
workerd: 1.20250803.0
workerd: 1.20250902.0
ws: 8.18.0
youch: 4.1.0-beta.10
zod: 3.22.3
@@ -900,9 +901,11 @@ snapshots:
ufo@1.6.1: {}
uncrypto@0.1.3: {}
undici@7.13.0: {}
unenv@2.0.0-rc.19:
unenv@2.0.0-rc.20:
dependencies:
defu: 6.1.4
exsolve: 1.0.7
@@ -910,26 +913,26 @@ snapshots:
pathe: 2.0.3
ufo: 1.6.1
workerd@1.20250803.0:
workerd@1.20250902.0:
optionalDependencies:
'@cloudflare/workerd-darwin-64': 1.20250803.0
'@cloudflare/workerd-darwin-arm64': 1.20250803.0
'@cloudflare/workerd-linux-64': 1.20250803.0
'@cloudflare/workerd-linux-arm64': 1.20250803.0
'@cloudflare/workerd-windows-64': 1.20250803.0
'@cloudflare/workerd-darwin-64': 1.20250902.0
'@cloudflare/workerd-darwin-arm64': 1.20250902.0
'@cloudflare/workerd-linux-64': 1.20250902.0
'@cloudflare/workerd-linux-arm64': 1.20250902.0
'@cloudflare/workerd-windows-64': 1.20250902.0
wrangler@4.28.0(@cloudflare/workers-types@4.20250805.0):
wrangler@4.34.0(@cloudflare/workers-types@4.20250906.0):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.0
'@cloudflare/unenv-preset': 2.6.0(unenv@2.0.0-rc.19)(workerd@1.20250803.0)
'@cloudflare/unenv-preset': 2.7.2(unenv@2.0.0-rc.20)(workerd@1.20250902.0)
blake3-wasm: 2.1.5
esbuild: 0.25.4
miniflare: 4.20250803.0
miniflare: 4.20250902.0
path-to-regexp: 6.3.0
unenv: 2.0.0-rc.19
workerd: 1.20250803.0
unenv: 2.0.0-rc.20
workerd: 1.20250902.0
optionalDependencies:
'@cloudflare/workers-types': 4.20250805.0
'@cloudflare/workers-types': 4.20250906.0
fsevents: 2.3.3
transitivePeerDependencies:
- bufferutil
+84 -60
View File
@@ -1,19 +1,23 @@
import { Context, Hono } from 'hono';
import { env } from 'hono/adapter';
import { cors } from 'hono/cors';
import { BlankInput, Env } from 'hono/types';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis/cloudflare';
import type { Context } from "hono";
import type { BlankInput, Env } from "hono/types";
import type { iSVG } from "../../src/types/svg";
import type { Category } from "../../src/types/categories";
import { Hono } from "hono";
import { env } from "hono/adapter";
import { cors } from "hono/cors";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis/cloudflare";
// 🌿 Import utils:
import { addFullUrl } from './utils';
import { addFullUrl } from "./utils";
import { optimizeSvg } from "../../src/utils/optimizeSvg";
// 📦 Import data from main app:
import { svgsData } from '../../src/data';
import { iSVG } from '../../src/types/svg';
import { tCategory } from '../../src/types/categories';
// 📦 Import data from SVGL src:
import { svgsData } from "../../src/data";
declare module 'hono' {
declare module "hono" {
interface ContextVariableMap {
ratelimit: Ratelimit;
}
@@ -24,7 +28,7 @@ const fullRouteSvgsData = svgsData.map((svg) => {
return {
...svg,
route: addFullUrl(svg.route),
wordmark: svg.wordmark ? addFullUrl(svg.wordmark) : undefined
wordmark: svg.wordmark ? addFullUrl(svg.wordmark) : undefined,
};
}) as iSVG[];
@@ -34,21 +38,24 @@ const cache = new Map();
class RedisRateLimiter {
static instance: Ratelimit;
static getInstance(c: Context<Env, '/api/*', BlankInput>) {
static getInstance(c: Context<Env, "/api/*", BlankInput>) {
if (!this.instance) {
const { UPSTASH_REDIS_URL, UPSTASH_REDIS_TOKEN } = env<{
UPSTASH_REDIS_URL: string;
UPSTASH_REDIS_TOKEN: string;
}>(c);
const cleanRedisUrl = UPSTASH_REDIS_URL.replace(/^['"]|['"]$/g, '').trim();
const cleanRedisUrl = UPSTASH_REDIS_URL.replace(
/^['"]|['"]$/g,
"",
).trim();
const redisClient = new Redis({
token: UPSTASH_REDIS_TOKEN,
url: cleanRedisUrl
url: cleanRedisUrl,
});
const ratelimit = new Ratelimit({
redis: redisClient,
limiter: Ratelimit.slidingWindow(5, '5 s'),
ephemeralCache: cache
limiter: Ratelimit.slidingWindow(5, "5 s"),
ephemeralCache: cache,
});
this.instance = ratelimit;
return this.instance;
@@ -60,22 +67,22 @@ class RedisRateLimiter {
app.use(async (c, next) => {
const ratelimit = RedisRateLimiter.getInstance(c);
c.set('ratelimit', ratelimit);
c.set("ratelimit", ratelimit);
await next();
});
app.use(cors());
// 🌱 GET: "/" - Returns all the SVGs data:
app.get('/', async (c) => {
const limit = c.req.query('limit');
const search = c.req.query('search');
const ratelimit = c.get('ratelimit');
const ip = c.req.raw.headers.get('CF-Connecting-IP');
const { success } = await ratelimit.limit(ip ?? 'anonymous');
app.get("/", async (c) => {
const limit = c.req.query("limit");
const search = c.req.query("search");
const ratelimit = c.get("ratelimit");
const ip = c.req.raw.headers.get("CF-Connecting-IP");
const { success } = await ratelimit.limit(ip ?? "anonymous");
if (!success) {
return c.json({ error: '🛑 Too many request' }, 429);
return c.json({ error: "🛑 (SVGL - API) Too many request" }, 429);
}
if (limit) {
@@ -87,10 +94,10 @@ app.get('/', async (c) => {
if (search) {
const searchResults = fullRouteSvgsData.filter((svg) =>
svg.title.toLowerCase().includes(search.toLowerCase())
svg.title.toLowerCase().includes(search.toLowerCase()),
);
if (searchResults.length === 0) {
return c.json({ error: 'not found' }, 404);
return c.json({ error: "❌ (SVGL - API) SVG not found" }, 404);
}
return c.json(searchResults);
}
@@ -99,19 +106,19 @@ app.get('/', async (c) => {
});
// 🌱 GET: "/categories" - Return an array with categories:
app.get('/categories', async (c) => {
const ratelimit = c.get('ratelimit');
const ip = c.req.raw.headers.get('CF-Connecting-IP');
const { success } = await ratelimit.limit(ip ?? 'anonymous');
app.get("/categories", async (c) => {
const ratelimit = c.get("ratelimit");
const ip = c.req.raw.headers.get("CF-Connecting-IP");
const { success } = await ratelimit.limit(ip ?? "anonymous");
if (!success) {
return c.json({ error: '🛑 Too many request' }, 429);
return c.json({ error: "❌ (SVGL - API) Too many request" }, 429);
}
const categoryTotals: Record<string, number> = {};
fullRouteSvgsData.forEach((svg) => {
if (typeof svg.category === 'string') {
if (typeof svg.category === "string") {
categoryTotals[svg.category] = (categoryTotals[svg.category] || 0) + 1;
} else if (Array.isArray(svg.category)) {
svg.category.forEach((category) => {
@@ -120,62 +127,79 @@ app.get('/categories', async (c) => {
}
});
const categories = Object.entries(categoryTotals).map(([category, total]) => ({
category,
total
}));
const categories = Object.entries(categoryTotals).map(
([category, total]) => ({
category,
total,
}),
);
return c.json(categories);
});
// 🌱 GET: /category/:category - Return an list of svgs by specific category:
app.get('/category/:category', async (c) => {
const category = c.req.param('category') as string;
const targetCategory = category.charAt(0).toUpperCase() + category.slice(1);
const ratelimit = c.get('ratelimit');
const ip = c.req.raw.headers.get('CF-Connecting-IP');
const { success } = await ratelimit.limit(ip ?? 'anonymous');
app.get("/category/:category", async (c) => {
const category = c.req.param("category") as string;
const targeCategory = category.charAt(0).toUpperCase() + category.slice(1);
const ratelimit = c.get("ratelimit");
const ip = c.req.raw.headers.get("CF-Connecting-IP");
const { success } = await ratelimit.limit(ip ?? "anonymous");
if (!success) {
return c.json({ error: '🛑 Too many request' }, 429);
return c.json({ error: "🛑 (SVGL - API) Too many request" }, 429);
}
const categorySvgs = fullRouteSvgsData.filter((svg) => {
if (typeof svg.category === 'string') {
return svg.category === targetCategory;
if (typeof svg.category === "string") {
return svg.category === targeCategory;
}
if (Array.isArray(svg.category)) {
return svg.category.includes(targetCategory as tCategory);
return svg.category.includes(targeCategory as Category);
}
return false;
});
if (categorySvgs.length === 0) {
return c.json({ error: 'not found' }, 404);
return c.json({ error: "❌ (SVGL - API) Category not found" }, 404);
}
return c.json(categorySvgs);
});
// 🌱 GET: "/svg/:filename" - Return the SVG file by filename:
app.get('/svg/:filename', async (c) => {
const fileName = c.req.param('filename') as string;
const svgLibrary = 'https://svgl.app/library/';
// 🌱 GET: "/svg/:filename" - Return the SVG code file by filename:
app.get("/svg/:filename", async (c) => {
const fileName = c.req.param("filename") as string;
const svgLibrary = "https://svgl.app/library/";
const ratelimit = c.get('ratelimit');
const ip = c.req.raw.headers.get('CF-Connecting-IP');
const { success } = await ratelimit.limit(ip ?? 'anonymous');
const ratelimit = c.get("ratelimit");
const returnNoOptimized = c.req.query("no-optimize");
const ip = c.req.raw.headers.get("CF-Connecting-IP");
const { success } = await ratelimit.limit(ip ?? "anonymous");
if (!success) {
return c.json({ error: '🛑 Too many request' }, 429);
return c.json({ error: "🛑 (SVGL - API) Too many request" }, 429);
}
try {
const svg = await fetch(`${svgLibrary}${fileName}`).then((res) => {
if (!res.ok) throw new Error('Network response was not ok');
if (!res.ok)
throw new Error("❌ (SVGL - API) Network response was not ok");
return res.text();
});
return c.body(svg, 200);
if (returnNoOptimized) {
return c.body(svg, 200, {
"Content-Type": "image/svg+xml; charset=utf-8",
});
}
const optimizedSvg = optimizeSvg({ svgCode: svg });
return c.body(optimizedSvg, 200, {
"Content-Type": "image/svg+xml; charset=utf-8",
});
} catch (err) {
return c.json({ error: 'not found' }, 404);
return c.json(
{ error: `❌ (SVGL - API) SVG file not found - ${err}` },
404,
);
}
});
+28 -2
View File
@@ -1,11 +1,20 @@
import { z } from "zod";
import path from "node:path";
import fs from "node:fs/promises";
// Content Collections:
import { compileMarkdown } from "@content-collections/markdown";
import { defineCollection, defineConfig } from "@content-collections/core";
// Shiki:
// Plugins:
import rehypeSlug from "rehype-slug";
import rehypeShiki from "@shikijs/rehype/core";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
// Custom Plugins:
import { rehypeCopyBtn } from "./src/markdown/rehypeCopyBtn";
import { getTableOfContents } from "./src/markdown/generateToC";
import { rehypeExternalLinks } from "./src/markdown/rehypeExternalLinks";
import { shikiHighlighter, rehypeShikiOptions } from "./src/utils/shiki";
const docs = defineCollection({
@@ -18,12 +27,29 @@ const docs = defineCollection({
}),
transform: async (document, context) => {
const highlighter = await shikiHighlighter();
const filePath = path.join(
context.collection.directory,
document._meta.filePath,
);
const { mtimeMs, birthtimeMs } = await fs.stat(filePath);
const html = await compileMarkdown(context, document, {
rehypePlugins: [[rehypeShiki, highlighter, rehypeShikiOptions]],
rehypePlugins: [
[rehypeShiki, highlighter, rehypeShikiOptions],
rehypeExternalLinks,
rehypeSlug,
rehypeAutolinkHeadings,
rehypeCopyBtn,
],
});
const tableOfContents = getTableOfContents(document.content);
return {
...document,
html,
createdAt: new Date(birthtimeMs),
updatedAt: new Date(mtimeMs),
tableOfContents,
rawUrl: `https://svgl.app/api/docs/${document._meta.path}`,
documentUrl: `https://svgl.app/docs/${document._meta.path}`,
};
},
});
+19 -10
View File
@@ -14,7 +14,7 @@ import svelteConfig from "./svelte.config.js";
// Ignore files:
const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url));
export default ts.config(
export default [
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
@@ -24,9 +24,23 @@ export default ts.config(
{
languageOptions: {
globals: { ...globals.browser, ...globals.node },
parserOptions: {
tsconfigRootDir: fileURLToPath(new URL(".", import.meta.url)),
},
},
rules: {
"no-undef": "off",
"@typescript-eslint/array-type": "off",
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/consistent-type-imports": [
"warn",
{ prefer: "type-imports", fixStyle: "inline-type-imports" },
],
"@typescript-eslint/no-unused-vars": [
"warn",
{ argsIgnorePattern: "^_" },
],
"@typescript-eslint/require-await": "off",
},
},
{
@@ -41,14 +55,9 @@ export default ts.config(
},
rules: {
"svelte/no-unused-svelte-ignore": "warn",
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^(_|\\$\\$)",
destructuredArrayIgnorePattern: "^_",
},
],
"svelte/no-useless-mustaches": "warn",
"svelte/require-each-key": "warn",
"svelte/no-at-html-tags": "off",
},
},
);
];
+9 -2
View File
@@ -32,13 +32,16 @@
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"check:size": "tsx ./utils/check-size.ts",
"check:links": " lychee --base . ./src/data/svgs.ts --cache --max-cache-age 3d . --include 'https://svgl.app'",
"fix:viewbox": "tsx ./utils/fix-viewbox.ts",
"format": "prettier --write \"src/**/*.{ts,js,md,svelte}\" --cache",
"format:check": "prettier --check \"src/**/*.{ts,js,md,svelte}\" --cache",
"lint": "eslint ./src",
"lint:fix": "eslint ./src --fix",
"build:shadcn": "shadcn build --output ./static/r",
"build:prod": "vite build && pnpm build:registry",
"build:registry": "tsx ./generate-registry.ts"
"build:prod": "pnpm build:registry && vite build",
"build:registry": "tsx ./utils/generate-registry.ts"
},
"dependencies": {
"@shikijs/langs": "3.12.0",
@@ -71,10 +74,13 @@
"eslint": "9.33.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-svelte": "3.11.0",
"github-slugger": "2.0.0",
"globals": "16.3.0",
"prettier": "3.6.2",
"prettier-plugin-svelte": "3.4.0",
"prettier-plugin-tailwindcss": "0.6.14",
"rehype-autolink-headings": "7.1.0",
"rehype-slug": "6.0.0",
"svelte": "5.38.2",
"svelte-check": "4.3.1",
"svelte-sonner": "1.0.5",
@@ -84,6 +90,7 @@
"tw-animate-css": "1.3.7",
"typescript": "5.9.2",
"typescript-eslint": "8.40.0",
"unist-util-visit": "5.0.0",
"vite": "7.1.3",
"zod": "4.1.4"
}
+54
View File
@@ -93,6 +93,9 @@ importers:
eslint-plugin-svelte:
specifier: 3.11.0
version: 3.11.0(eslint@9.33.0(jiti@2.5.1))(svelte@5.38.2)
github-slugger:
specifier: 2.0.0
version: 2.0.0
globals:
specifier: 16.3.0
version: 16.3.0
@@ -105,6 +108,12 @@ importers:
prettier-plugin-tailwindcss:
specifier: 0.6.14
version: 0.6.14(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.38.2))(prettier@3.6.2)
rehype-autolink-headings:
specifier: 7.1.0
version: 7.1.0
rehype-slug:
specifier: 6.0.0
version: 6.0.0
svelte:
specifier: 5.38.2
version: 5.38.2
@@ -132,6 +141,9 @@ importers:
typescript-eslint:
specifier: 8.40.0
version: 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
unist-util-visit:
specifier: 5.0.0
version: 5.0.0
vite:
specifier: 7.1.3
version: 7.1.3(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5)(yaml@2.8.1)
@@ -1684,6 +1696,9 @@ packages:
get-tsconfig@4.10.1:
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -1733,6 +1748,12 @@ packages:
hast-util-from-parse5@8.0.3:
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
hast-util-heading-rank@3.0.0:
resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==}
hast-util-is-element@3.0.0:
resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
hast-util-parse-selector@4.0.0:
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
@@ -2563,9 +2584,15 @@ packages:
regex@6.0.1:
resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==}
rehype-autolink-headings@7.1.0:
resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==}
rehype-raw@7.0.0:
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
rehype-slug@6.0.0:
resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==}
rehype-stringify@10.0.1:
resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
@@ -4761,6 +4788,8 @@ snapshots:
dependencies:
resolve-pkg-maps: 1.0.0
github-slugger@2.0.0: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -4807,6 +4836,14 @@ snapshots:
vfile-location: 5.0.3
web-namespaces: 2.0.1
hast-util-heading-rank@3.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-is-element@3.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-parse-selector@4.0.0:
dependencies:
'@types/hast': 3.0.4
@@ -5611,12 +5648,29 @@ snapshots:
dependencies:
regex-utilities: 2.3.0
rehype-autolink-headings@7.1.0:
dependencies:
'@types/hast': 3.0.4
'@ungap/structured-clone': 1.3.0
hast-util-heading-rank: 3.0.0
hast-util-is-element: 3.0.0
unified: 11.0.5
unist-util-visit: 5.0.0
rehype-raw@7.0.0:
dependencies:
'@types/hast': 3.0.4
hast-util-raw: 9.1.0
vfile: 6.0.3
rehype-slug@6.0.0:
dependencies:
'@types/hast': 3.0.4
github-slugger: 2.0.0
hast-util-heading-rank: 3.0.0
hast-util-to-string: 3.0.1
unist-util-visit: 5.0.0
rehype-stringify@10.0.1:
dependencies:
'@types/hast': 3.0.4
+32 -9
View File
@@ -11,17 +11,17 @@
<link
rel="icon"
type="image/svg+xml"
href="%sveltekit.assets%/images/logo.svg"
href="%sveltekit.assets%/images/svgl_svg.svg"
/>
<link
rel="icon"
type="image/ico"
href="%sveltekit.assets%/images/logo_ico.ico"
href="%sveltekit.assets%/images/svgl_ico.ico"
/>
<!-- OG -->
<!-- OG Images -->
<meta property="og:type" content="website" />
<meta property="og:title" content="svgl" />
<meta property="og:title" content="SVGL" />
<meta
property="og:description"
content="A beautiful library with SVG logos"
@@ -29,12 +29,12 @@
<meta property="og:url" content="https://svgl.app" />
<meta
property="og:image"
content="https://svgl.app/images/screenshot.png"
content="https://svgl.app/images/screenshot_dark.png"
/>
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Svgl" />
<meta name="twitter:title" content="SVGL" />
<meta
name="twitter:description"
content="A beautiful library with SVG logos"
@@ -42,15 +42,38 @@
<meta name="twitter:creator" content="@pheralb_" />
<meta
name="twitter:image"
content="https://svgl.app/images/screenshot.png"
content="https://svgl.app/images/screenshot_dark.png"
/>
<!-- Preload Fonts -->
<link
rel="preload"
href="%sveltekit.assets%/fonts/Geist.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<link
rel="preload"
href="%sveltekit.assets%/fonts/GeistMono.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
<!-- Analytics -->
<script
defer
src="https://umami.pheralb.dev/script.js"
data-website-id="c0310f77-a87c-4c80-ba04-c6767cf94507"
></script>
<!-- Title -->
<title>A beautiful library with SVG logos - Svgl</title>
<title>A beautiful library with SVG logos - SVGL</title>
%sveltekit.head%
</head>
<body
class="overscroll-none bg-neutral-100 font-sans tracking-tight text-black antialiased selection:bg-neutral-300 dark:bg-neutral-950 dark:text-white dark:selection:bg-neutral-700"
class="overscroll-none bg-neutral-100 font-sans text-black antialiased selection:bg-neutral-300 dark:bg-neutral-950 dark:text-white dark:selection:bg-neutral-700"
data-sveltekit-preload-data="hover"
>
<div style="display: contents">%sveltekit.body%</div>
+67
View File
@@ -0,0 +1,67 @@
<script lang="ts">
import type { Component } from "svelte";
import { cn } from "@/utils/cn";
import CopyIcon from "@lucide/svelte/icons/copy";
import CheckIcon from "@lucide/svelte/icons/check";
import { clipboard } from "@/utils/clipboard";
interface Props {
code: string;
className?: string;
Icon?: Component;
copyDuration?: number;
}
let { Icon, className, code, copyDuration = 2000 }: Props = $props();
let copied = $state(false);
let timeoutId: ReturnType<typeof setTimeout> | null = null;
const handleCopy = async () => {
await clipboard(code);
if (timeoutId) {
clearTimeout(timeoutId);
}
copied = true;
timeoutId = setTimeout(() => {
copied = false;
timeoutId = null;
}, copyDuration);
};
$effect(() => {
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
});
</script>
<div
class={cn(
"relative flex items-center space-x-2 rounded-md border border-neutral-200 p-2.5 dark:border-neutral-800",
className,
)}
>
<button
class="absolute right-2 transition-colors hover:text-neutral-600 dark:hover:text-neutral-400"
onclick={handleCopy}
disabled={copied}
title={copied ? "Copied" : "Copy code"}
>
{#if copied}
<CheckIcon size={14} />
{:else}
<CopyIcon size={14} />
{/if}
</button>
{#if Icon}
<Icon size={14} class="text-neutral-500" />
{/if}
<code class="pr-8 font-mono text-sm select-all">
{code}
</code>
</div>
+1 -1
View File
@@ -6,6 +6,6 @@
$props();
</script>
<div class={cn("container mx-auto px-4", className)}>
<div class={cn("container mx-auto px-6 lg:px-4", className)}>
{@render children?.()}
</div>
+116
View File
@@ -0,0 +1,116 @@
<script lang="ts">
import type { Component } from "svelte";
import { cn } from "@/utils/cn";
import { clipboard } from "@/utils/clipboard";
import * as DropdownMenu from "@/components/ui/dropdown-menu";
import { Button, buttonVariants } from "@/components/ui/button";
import CopyIcon from "@lucide/svelte/icons/copy";
import Openai from "@/components/logos/openai.svelte";
import Claude from "@/components/logos/claude.svelte";
import CheckCheck from "@lucide/svelte/icons/check-check";
import ChevronDown from "@lucide/svelte/icons/chevron-down";
import ArrowUpRight from "@lucide/svelte/icons/arrow-up-right";
import Markdown from "./logos/markdown.svelte";
interface DocumentSettingsProps {
documentContent: string;
documentUrl: string;
rawUrl: string;
}
let isCopied = $state<boolean>(false);
let settingsOpen = $state<boolean>(false);
let { documentContent, documentUrl, rawUrl }: DocumentSettingsProps =
$props();
const handleCopyPage = () => {
clipboard(documentContent);
isCopied = true;
setTimeout(() => {
isCopied = false;
}, 2000);
};
interface AiOption {
name: string;
href: string;
icon: Component;
}
const aiPrompt = `The following is a documentation page from SVGL, a web app with SVG logos: ${documentUrl}. Help me understand how to use it. Be ready to explain concepts, give examples, or help debug based on it.`;
const aiOptions: AiOption[] = [
{
name: "ChatGPT",
href: `https://chatgpt.com/?q=${encodeURIComponent(aiPrompt)}`,
icon: Openai,
},
{
name: "Claude",
href: `https://claude.ai/new?q=${encodeURIComponent(aiPrompt)}`,
icon: Claude,
},
];
</script>
{#snippet LinkItem({ href, icon, name }: AiOption)}
<DropdownMenu.Item>
{#snippet child({ props })}
{@const Icon = icon}
<a {href} target="_blank" {...props}>
<div class="flex items-center space-x-2">
<Icon size={14} />
<span>{name}</span>
<ArrowUpRight size={12} class="opacity-50" />
</div>
</a>
{/snippet}
</DropdownMenu.Item>
{/snippet}
<div class="flex items-center">
<Button
size="sm"
variant="outline"
class="rounded-r-none border-r-0 px-2 md:px-3"
onclick={handleCopyPage}
>
{#if isCopied}
<CheckCheck size={14} />
{:else}
<CopyIcon size={14} />
{/if}
<span class="hidden md:block">Copy Page</span>
</Button>
<DropdownMenu.Root bind:open={settingsOpen}>
<DropdownMenu.Trigger
class={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"rounded-l-none px-2",
)}
>
<ChevronDown
size={14}
class={cn(
"transition-transform duration-200",
settingsOpen ? "rotate-180" : "rotate-0",
)}
/>
</DropdownMenu.Trigger>
<DropdownMenu.Content side="bottom" align="end" sideOffset={6}>
<DropdownMenu.Group>
{@render LinkItem({
href: rawUrl,
icon: Markdown,
name: "View as Markdown",
})}
{#each aiOptions as option (option.name)}
{@render LinkItem(option)}
{/each}
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
+66
View File
@@ -0,0 +1,66 @@
<script lang="ts">
import type { Extension } from "@/types/extensions";
import { cn } from "@/utils/cn";
import { buttonVariants } from "@/components/ui/button";
import ArrowUpRightIcon from "@lucide/svelte/icons/arrow-up-right";
interface ExtensionProps {
data: Extension;
}
let { data }: ExtensionProps = $props();
</script>
<div
class={cn(
"relative h-48 max-h-48",
"rounded-md transition-shadow hover:shadow-sm",
"bg-white dark:bg-neutral-900",
"border border-neutral-200 dark:border-neutral-800",
"flex flex-col",
)}
>
<div class="p-4">
<div class="mb-4 flex items-center justify-between">
<img
src={data.image}
alt={data.name}
class="h-8 w-8 rounded-md object-contain"
/>
<a
href={data.url}
target="_blank"
rel="noopener noreferrer"
class={buttonVariants({ variant: "outline" })}
>
<span>Install</span>
<ArrowUpRightIcon
size={12}
strokeWidth={2}
class="text-neutral-600 dark:text-neutral-400"
/>
</a>
</div>
<p class="mb-1 font-medium">{data.name}</p>
<p
title={data.description}
class="truncate text-sm text-pretty text-neutral-700 dark:text-neutral-300"
>
{data.description}
</p>
</div>
<div
class="absolute bottom-0 flex w-full items-center justify-end gap-1 border-t border-neutral-200 bg-neutral-100/60 px-4 py-2 text-sm dark:border-neutral-800 dark:bg-neutral-800/40"
>
<span>Created by</span>
<a
href={data.created_by.socialUrl}
target="_blank"
rel="noopener noreferrer"
class="font-medium text-pretty decoration-neutral-400 underline-offset-2 transition-colors hover:text-neutral-600 hover:underline dark:decoration-neutral-600 dark:hover:text-neutral-400"
>
{data.created_by.name}
</a>
</div>
</div>
+40
View File
@@ -0,0 +1,40 @@
<script lang="ts">
import { onMount } from "svelte";
import { cn } from "@/utils/cn";
import { globals } from "@/globals";
import { buttonVariants } from "@/components/ui/button";
import Github from "@/components/logos/github.svelte";
async function getGithubStarCount() {
try {
const res = await fetch(globals.apiGithub.url);
const data = await res.json();
return data.repo?.stars ?? globals.apiGithub.fallback;
} catch (error) {
console.error(error);
return globals.apiGithub.fallback;
}
}
let stars = $state(globals.apiGithub.fallback);
onMount(async () => {
stars = await getGithubStarCount();
});
</script>
<a
target="_blank"
title="pheralb/svgl Repository"
href={globals.githubUrl}
class={cn(
buttonVariants({ variant: "ghost" }),
"w-fit hover:bg-neutral-200 dark:hover:bg-neutral-800",
)}
>
<Github size={20} />
<span class="text-neutral-600 dark:text-neutral-400">
{stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : stars.toLocaleString()}
</span>
</a>
+11 -3
View File
@@ -2,13 +2,21 @@
import type { Snippet } from "svelte";
import { cn } from "@/utils/cn";
let { className, children }: { className?: string; children?: Snippet } =
$props();
interface GridProps {
columns?: "default" | "4" | "3" | "2";
className?: string;
children?: Snippet;
}
let { className, columns, children }: GridProps = $props();
</script>
<div
class={cn(
"grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5",
"grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5",
columns === "4" && "lg:grid-cols-3 xl:grid-cols-4",
columns === "3" && "lg:grid-cols-2 xl:grid-cols-3",
columns === "2" && "md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2",
className,
)}
>
+38 -49
View File
@@ -5,79 +5,68 @@
import ModeToggle from "@/components/modeToggle.svelte";
import Svgl from "@/components/logos/svgl.svelte";
import Github from "@/components/logos/github.svelte";
import Twitter from "@/components/logos/twitter.svelte";
import { Separator } from "@/components/ui/separator";
import Badge from "@/components/ui/badge/badge.svelte";
import { buttonVariants } from "@/components/ui/button";
import SendIcon from "@/components/ui/moving-icons/send-icon.svelte";
interface HeaderProps {
githubStars: number;
}
let { githubStars }: HeaderProps = $props();
const headerItemsClasses = cn(
buttonVariants({ variant: "ghost" }),
"hover:bg-neutral-200 dark:hover:bg-neutral-800",
);
import SidebarMobileMenu from "@/components/layout/sidebarMobileMenu.svelte";
import SettingsMenu from "@/components/settings/settingsMenu.svelte";
import GithubLink from "@/components/githubLink.svelte";
</script>
<header
class="sticky top-0 w-full bg-neutral-100 px-4 py-4 dark:bg-neutral-950"
class="sticky top-0 z-50 w-full bg-neutral-100 px-2 py-3 md:px-4 md:py-4 dark:bg-neutral-950"
>
<nav class="flex w-full items-center justify-between">
<div class="flex items-center space-x-3">
<div class="flex items-center space-x-2">
<SidebarMobileMenu className="md:hidden" />
<a
href="/"
class="flex items-center space-x-2.5 transition-colors hover:text-neutral-700 dark:hover:text-neutral-300"
class="flex items-center space-x-2 transition-colors hover:text-neutral-700 dark:hover:text-neutral-300"
>
<Svgl size={28} />
<h2 class="font-onest text-xl font-medium tracking-tight">svgl</h2>
<h2 class="text-xl font-medium tracking-tight">svgl</h2>
</a>
<Badge variant="outline">{globals.currentVersion}</Badge>
</div>
<div class="flex h-8 items-center">
<div class="flex items-center space-x-0.5">
<div class="flex h-5 items-center space-x-2.5">
<div class="flex items-center space-x-1.5">
<a
target="_blank"
title="X/Twitter"
href={globals.twitterUrl}
class={cn(headerItemsClasses, "h-9 w-9")}
class={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"hover:bg-neutral-200 dark:hover:bg-neutral-800",
)}
>
<Twitter size={18} />
</a>
<ModeToggle className={cn(headerItemsClasses, "h-9 w-9")} />
<ModeToggle
className={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"hover:bg-neutral-200 dark:hover:bg-neutral-800",
)}
/>
<SettingsMenu />
</div>
<div class="hidden h-5 items-center space-x-2 md:flex">
<Separator orientation="vertical" />
<GithubLink />
<Separator orientation="vertical" />
<a
target="_blank"
href={globals.submitUrl}
class={cn(
buttonVariants({
variant: mode.current === "dark" ? "default" : "radial",
}),
)}
>
<SendIcon size={14} />
<span>Submit</span>
</a>
</div>
<Separator orientation="vertical" class="mx-2 h-8" />
<a
target="_blank"
title="GitHub Repository"
href={globals.githubUrl}
class={cn(headerItemsClasses, "h-9 w-fit")}
>
<Github size={20} />
<span class="text-neutral-600 dark:text-neutral-400">
{githubStars >= 1000
? `${(githubStars / 1000).toFixed(1)}k`
: githubStars.toLocaleString()}
</span>
</a>
<Separator orientation="vertical" class="mr-3 ml-2" />
<a
target="_blank"
href={globals.submitUrl}
class={cn(
buttonVariants({
variant: mode.current === "dark" ? "default" : "radial",
}),
)}
>
<SendIcon size={14} />
<span>Submit</span>
</a>
</div>
</nav>
</header>
+3 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import type { tCategory } from "@/types/categories";
import type { Category } from "@/types/categories";
import { cn } from "@/utils/cn";
import { svgs } from "@/data/svgs";
@@ -10,7 +10,7 @@
import { sidebarBadgeClasses } from "./sidebarBadgeClasses";
// Get category counts:
const categories: tCategory[] = getCategories();
const categories: Category[] = getCategories();
let categoryCounts: Record<string, number> = {};
categories.forEach((category) => {
categoryCounts[category] = svgs.filter((svg) =>
@@ -19,7 +19,7 @@
});
</script>
{#each categories.sort() as category}
{#each categories.sort() as category (category)}
<a
href={`/directory/${category.toLowerCase()}`}
data-sveltekit-preload-data
+35 -1
View File
@@ -1,5 +1,7 @@
<script lang="ts">
import { cn } from "@/utils/cn";
import { globals } from "@/globals";
import { page } from "$app/state";
import favoritesStore from "@/stores/favorites.store";
@@ -10,6 +12,9 @@
import House from "@lucide/svelte/icons/house";
import Heart from "@lucide/svelte/icons/heart";
import Cloud from "@lucide/svelte/icons/cloud";
import Submit from "@lucide/svelte/icons/send";
import Github from "@/components/logos/github.svelte";
import Shadcn from "@/components/logos/shadcn.svelte";
let favorites = $derived($favoritesStore);
let favoritesCount = $derived(favoritesStore.getCount(favorites));
@@ -49,7 +54,7 @@
{/if}
</a>
<a
href="/api"
href="/docs/api"
data-sveltekit-preload-data
class={cn(
sidebarItemClasses.base,
@@ -60,6 +65,19 @@
<Cloud size={16} />
<p class="truncate">API</p>
</a>
<a
href="/docs/shadcn-ui"
data-sveltekit-preload-data
class={cn(
sidebarItemClasses.base,
"justify-start space-x-3",
String(page.url.pathname) === "/docs/shadcn-ui" &&
sidebarItemClasses.active,
)}
>
<Shadcn size={14} />
<p class="truncate">shadcn/ui</p>
</a>
<a
href="/extensions"
data-sveltekit-preload-data
@@ -72,3 +90,19 @@
<Box size={16} />
<p class="truncate">Extensions</p>
</a>
<a
href={globals.submitUrl}
target="_blank"
class={cn(sidebarItemClasses.base, "flex justify-start space-x-3 md:hidden")}
>
<Submit size={16} />
<p class="truncate">Submit SVG</p>
</a>
<a
href={globals.githubUrl}
target="_blank"
class={cn(sidebarItemClasses.base, "flex justify-start space-x-3 md:hidden")}
>
<Github size={16} />
<p class="truncate">GitHub Repository</p>
</a>
+5 -5
View File
@@ -9,10 +9,10 @@
<section>
<aside
class={cn(
"fixed left-0 h-[calc(100vh-5.4rem)]",
"md:fixed md:left-1 md:h-[calc(100vh-4.5rem)]",
"overflow-x-hidden",
"w-54 pr-2 pl-3",
"flex flex-col space-y-3",
"w-54 pr-2 pl-2",
"hidden flex-col space-y-3 md:flex",
"bg-neutral-100 dark:bg-neutral-950",
)}
>
@@ -20,11 +20,11 @@
<ShowSidebarLinks />
</nav>
<Separator orientation="horizontal" />
<nav class="flex flex-col space-y-0.5 overflow-y-auto">
<nav class="relative flex h-auto flex-col space-y-0.5 overflow-y-auto">
<ShowCategories />
</nav>
</aside>
<main class={cn("mr-4 mb-4 ml-56", "overflow-hidden")}>
<main class={cn("px-2 md:mr-4 md:ml-56 md:px-0", "overflow-hidden")}>
<slot />
</main>
</section>
+1 -1
View File
@@ -2,5 +2,5 @@ import { cn } from "@/utils/cn";
export const sidebarBadgeClasses = cn(
"animate-in zoom-in-20 fade-in",
"dark:bg-dark rounded-lg border border-neutral-200 bg-white px-2 py-0.5 font-mono text-xs font-medium text-neutral-600 shadow-sm dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-400",
"rounded-lg border border-neutral-300 bg-white px-2 py-0.5 font-mono text-xs font-medium text-neutral-600 shadow-sm dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-400 drop-shadow",
);
@@ -0,0 +1,42 @@
<script lang="ts">
import { cn } from "@/utils/cn";
import * as Sheet from "@/components/ui/sheet";
import { buttonVariants } from "@/components/ui/button";
import Separator from "@/components/ui/separator/separator.svelte";
import MenuIcon from "@lucide/svelte/icons/menu";
import Svgl from "@/components/logos/svgl.svelte";
import ShowCategories from "@/components/layout/showCategories.svelte";
import ShowSidebarLinks from "@/components/layout/showSidebarLinks.svelte";
interface Props {
className?: string;
}
let { className }: Props = $props();
</script>
<Sheet.Root>
<Sheet.Trigger
title="Open SVGL Menu"
class={cn(buttonVariants({ variant: "ghost", size: "icon" }), className)}
>
<MenuIcon class="size-5" />
<span class="sr-only">Open Menu</span>
</Sheet.Trigger>
<Sheet.Content class="gap-0.5" side="left">
<Sheet.Header>
<Sheet.Title class="flex items-center space-x-2">
<Svgl size={28} />
<h2 class="text-xl font-medium tracking-tight">svgl</h2>
</Sheet.Title>
</Sheet.Header>
<nav class="flex flex-col space-y-0.5 overflow-y-auto px-3 pb-3">
<ShowSidebarLinks />
<Separator orientation="horizontal" class="my-3" />
<ShowCategories />
</nav>
</Sheet.Content>
</Sheet.Root>
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts">
import type { IconProps } from "@/types/icon";
let props: IconProps = $props();
</script>
<svg
preserveAspectRatio="xMidYMid"
viewBox="0 0 256 257"
width={props.size}
height={props.size}
>
<path
fill="currentColor"
d="m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"
/>
</svg>
+18
View File
@@ -0,0 +1,18 @@
<script lang="ts">
import type { IconProps } from "@/types/icon";
let props: IconProps = $props();
</script>
<svg viewBox="0 0 208 128" fill="none" width={props.size} height={props.size}>
<g fill="currentColor">
<path
fill-rule="evenodd"
d="M15 10a5 5 0 0 0-5 5v98a5 5 0 0 0 5 5h178a5 5 0 0 0 5-5V15a5 5 0 0 0-5-5zM0 15A15 15 0 0 1 15 0h178a15 15 0 0 1 15 15v98a15 15 0 0 1-15 15H15a15 15 0 0 1-15-15z"
clip-rule="evenodd"
/>
<path
d="M30 98V30h20l20 25 20-25h20v68H90V59L70 84 50 59v39zm125 0-30-33h20V30h20v35h20z"
/>
</g>
</svg>
+18
View File
@@ -0,0 +1,18 @@
<script lang="ts">
import type { IconProps } from "@/types/icon";
let props: IconProps = $props();
</script>
<svg
preserveAspectRatio="xMidYMid"
viewBox="0 0 256 260"
width={props.size}
height={props.size}
fill="none"
>
<path
fill="currentColor"
d="M239.184 106.203a64.716 64.716 0 0 0-5.576-53.103C219.452 28.459 191 15.784 163.213 21.74A65.586 65.586 0 0 0 52.096 45.22a64.716 64.716 0 0 0-43.23 31.36c-14.31 24.602-11.061 55.634 8.033 76.74a64.665 64.665 0 0 0 5.525 53.102c14.174 24.65 42.644 37.324 70.446 31.36a64.72 64.72 0 0 0 48.754 21.744c28.481.025 53.714-18.361 62.414-45.481a64.767 64.767 0 0 0 43.229-31.36c14.137-24.558 10.875-55.423-8.083-76.483Zm-97.56 136.338a48.397 48.397 0 0 1-31.105-11.255l1.535-.87 51.67-29.825a8.595 8.595 0 0 0 4.247-7.367v-72.85l21.845 12.636c.218.111.37.32.409.563v60.367c-.056 26.818-21.783 48.545-48.601 48.601Zm-104.466-44.61a48.345 48.345 0 0 1-5.781-32.589l1.534.921 51.722 29.826a8.339 8.339 0 0 0 8.441 0l63.181-36.425v25.221a.87.87 0 0 1-.358.665l-52.335 30.184c-23.257 13.398-52.97 5.431-66.404-17.803ZM23.549 85.38a48.499 48.499 0 0 1 25.58-21.333v61.39a8.288 8.288 0 0 0 4.195 7.316l62.874 36.272-21.845 12.636a.819.819 0 0 1-.767 0L41.353 151.53c-23.211-13.454-31.171-43.144-17.804-66.405v.256Zm179.466 41.695-63.08-36.63L161.73 77.86a.819.819 0 0 1 .768 0l52.233 30.184a48.6 48.6 0 0 1-7.316 87.635v-61.391a8.544 8.544 0 0 0-4.4-7.213Zm21.742-32.69-1.535-.922-51.619-30.081a8.39 8.39 0 0 0-8.492 0L99.98 99.808V74.587a.716.716 0 0 1 .307-.665l52.233-30.133a48.652 48.652 0 0 1 72.236 50.391v.205ZM88.061 139.097l-21.845-12.585a.87.87 0 0 1-.41-.614V65.685a48.652 48.652 0 0 1 79.757-37.346l-1.535.87-51.67 29.825a8.595 8.595 0 0 0-4.246 7.367l-.051 72.697Zm11.868-25.58 28.138-16.217 28.188 16.218v32.434l-28.086 16.218-28.188-16.218-.052-32.434Z"
/>
</svg>
+8 -64
View File
@@ -5,70 +5,14 @@
</script>
<svg
name="SVGL Logo"
xmlns="http://www.w3.org/2000/svg"
width={props.size}
height={props.size}
name="SVGL Logo"
viewBox="0 0 512 512"
fill="none"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
class={props.className}
>
<rect
id="svgl__r4"
width="512"
height="512"
x="0"
y="0"
rx="128"
fill="#222"
stroke="#FFFFFF"
stroke-width="0"
stroke-opacity="100%"
paint-order="stroke"
/><rect
width="512"
height="512"
x="0"
y="0"
fill="url(#svgl__r6)"
rx="128"
style="mix-blend-mode: overlay;"
/><clipPath id="svgl__clip"><use xlink:href="#svgl__r4" /></clipPath><defs
><linearGradient
id="svgl__r5"
gradientUnits="userSpaceOnUse"
gradientTransform="rotate(135)"
style="transform-origin: center center;"
><stop stop-color="#222" /><stop
offset="1"
stop-color="#222222"
/></linearGradient
><radialGradient
id="svgl__r6"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(256) rotate(90) scale(512)"
><stop stop-color="white" /><stop
offset="1"
stop-color="white"
stop-opacity="0"
/></radialGradient
></defs
><svg
xmlns="http://www.w3.org/2000/svg"
width="310"
height="310"
fill="#e8e8e8"
viewBox="0 0 256 256"
x="101"
y="101"
alignment-baseline="middle"
style="color: rgb(255, 255, 255);"
><path
d="M168,32H88A56.06,56.06,0,0,0,32,88v80a56.06,56.06,0,0,0,56,56h48a8.07,8.07,0,0,0,2.53-.41c26.23-8.75,76.31-58.83,85.06-85.06A8.07,8.07,0,0,0,224,136V88A56.06,56.06,0,0,0,168,32ZM48,168V88A40,40,0,0,1,88,48h80a40,40,0,0,1,40,40v40H184a56.06,56.06,0,0,0-56,56v24H88A40,40,0,0,1,48,168Zm96,35.14V184a40,40,0,0,1,40-40h19.14C191,163.5,163.5,191,144,203.14Z"
/></svg
>
fill="currentColor"
viewBox="0 0 256 256"
><path d="M216,136c-8,24-56,72-80,80V184a48,48,0,0,1,48-48Z" opacity="0.2"
></path><path
d="M168,32H88A56.06,56.06,0,0,0,32,88v80a56.06,56.06,0,0,0,56,56h48a8.07,8.07,0,0,0,2.53-.41c26.23-8.75,76.31-58.83,85.06-85.06A8.07,8.07,0,0,0,224,136V88A56.06,56.06,0,0,0,168,32ZM48,168V88A40,40,0,0,1,88,48h80a40,40,0,0,1,40,40v40H184a56.06,56.06,0,0,0-56,56v24H88A40,40,0,0,1,48,168Zm96,35.14V184a40,40,0,0,1,40-40h19.14C191,163.5,163.5,191,144,203.14Z"
></path>
</svg>
+19
View File
@@ -0,0 +1,19 @@
<script lang="ts">
import type { IconProps } from "@/types/icon";
let props: IconProps = $props();
</script>
<svg
stroke-linejoin="round"
viewBox="0 0 16 16"
width={props.size}
height={props.size}
>
<path
clip-rule="evenodd"
d="M9.50321 5.5H13.2532C13.3123 5.5 13.3704 5.5041 13.4273 5.51203L9.51242 9.42692C9.50424 9.36912 9.5 9.31006 9.5 9.25L9.5 5.5L8 5.5L8 9.25C8 10.7688 9.23122 12 10.75 12H14.5V10.5L10.75 10.5C10.6899 10.5 10.6309 10.4958 10.5731 10.4876L14.4904 6.57028C14.4988 6.62897 14.5032 6.68897 14.5032 6.75V10.5H16.0032V6.75C16.0032 5.23122 14.772 4 13.2532 4H9.50321V5.5ZM0 5V5.00405L5.12525 11.5307C5.74119 12.3151 7.00106 11.8795 7.00106 10.8822V5H5.50106V9.58056L1.90404 5H0Z"
fill="currentColor"
fill-rule="evenodd"
/>
</svg>
+18 -3
View File
@@ -1,17 +1,32 @@
<script lang="ts">
import { onMount } from "svelte";
import { toggleMode } from "mode-watcher";
import SunIcon from "@lucide/svelte/icons/sun";
import MoonIcon from "@lucide/svelte/icons/moon";
import { toggleMode } from "mode-watcher";
interface Props {
className?: string;
}
let { className }: Props = $props();
const handleKeydown = (event: KeyboardEvent) => {
if (event.ctrlKey && event.key === "l") {
event.preventDefault();
toggleMode();
}
};
onMount(() => {
window.addEventListener("keydown", handleKeydown);
return () => {
window.removeEventListener("keydown", handleKeydown);
};
});
</script>
<button class={className} onclick={toggleMode} title="Mode Toggle">
<button class={className} onclick={toggleMode} title="Mode Toggle (Cmd + l)">
<SunIcon
size={20}
strokeWidth={1.5}
+15 -11
View File
@@ -11,20 +11,24 @@
let { children, contentCardClass, containerClass }: PageCardProps = $props();
</script>
<div
class={cn(
"mt-2.5 overflow-hidden",
"rounded-md border border-neutral-200 dark:border-neutral-800",
"bg-white dark:bg-neutral-900/40",
containerClass,
)}
>
<div class="p-[1px]">
<div
class={cn(
"max-h-[calc(100vh-8.6rem)] min-h-[calc(100vh-8.6rem)] overflow-y-auto",
contentCardClass,
"overflow-hidden",
"rounded-md border border-neutral-200 dark:border-neutral-800",
"bg-white dark:bg-neutral-900/40",
"shadow-xs",
containerClass,
)}
>
{@render children?.()}
<div
class={cn(
"max-h-[calc(100vh-4.5rem)] min-h-[calc(100vh-4.5rem)]",
"overflow-hidden overflow-y-auto",
contentCardClass,
)}
>
{@render children?.()}
</div>
</div>
</div>
+12 -17
View File
@@ -2,36 +2,29 @@
import { cn } from "@/utils/cn";
import { onMount } from "svelte";
import { page } from "$app/state";
import { goto } from "$app/navigation";
import { addParams } from "@/utils/searchParams";
import SearchIcon from "@lucide/svelte/icons/search";
import CommandIcon from "@lucide/svelte/icons/command";
import { SvelteURLSearchParams } from "svelte/reactivity";
interface Props {
searchValue: string;
onSearch: (value: string) => void;
placeholder?: string;
iconSize?: number;
inputClass?: string;
}
let { searchValue, onSearch, placeholder }: Props = $props();
let { searchValue, onSearch, placeholder, iconSize, inputClass }: Props =
$props();
let inputElement: HTMLInputElement;
const onInput = (event: Event) => {
const param = "search";
const value = (event.target as HTMLInputElement).value;
onSearch(value);
const params = new SvelteURLSearchParams(page.url.searchParams);
if (value) {
params.set(param, value);
} else {
params.delete(param);
}
goto(`?${params.toString()}`, {
keepFocus: true,
noScroll: true,
replaceState: true,
addParams({
params: {
search: value,
},
});
};
@@ -52,7 +45,7 @@
<div class="relative">
<SearchIcon
size={20}
size={iconSize ? iconSize : 20}
strokeWidth={2}
class={cn(
"pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 transition-colors",
@@ -67,6 +60,7 @@
autocomplete="off"
placeholder={placeholder || "Search..."}
oninput={onInput}
name="search"
value={searchValue}
class={cn(
"overflow-hidden shadow-sm",
@@ -75,6 +69,7 @@
"bg-white dark:bg-neutral-900",
"rounded-md border border-neutral-200 dark:border-neutral-800",
"focus:border-neutral-400 focus:outline-none dark:focus:border-neutral-600",
inputClass,
)}
/>
{#if !searchValue}
@@ -0,0 +1,19 @@
<script lang="ts">
import { Switch } from "@/components/ui/switch";
import { settingsStore } from "@/stores/settings.store";
let optimize = $derived($settingsStore.optimizeSvgs);
const handleOptimizeChange = (checked: boolean) => {
settingsStore.setOptimizeSvgs(checked);
};
</script>
<div class="flex items-center gap-3">
<Switch
id="optimize"
checked={optimize}
onCheckedChange={handleOptimizeChange}
/>
<label for="optimize">Optimize SVGs</label>
</div>
@@ -1,16 +1,18 @@
<script lang="ts">
import type { Component } from "svelte";
import * as Select from "@/components/ui/select";
import { pkgManager, type PackageManager } from "@/stores/pkgManager.store";
import { buttonVariants } from "@/components/ui/button";
import { settingsStore, type PackageManager } from "@/stores/settings.store";
import Npm from "@/components/logos/npm.svelte";
import Pnpm from "@/components/logos/pnpm.svelte";
import Yarn from "@/components/logos/yarn.svelte";
import Bun from "@/components/logos/bun.svelte";
import { buttonVariants } from "./ui/button";
let pkg = $derived($pkgManager);
let pkg = $derived($settingsStore.packageManager);
const managers = {
const managers: Record<PackageManager, { label: string; Icon: Component }> = {
npm: { label: "npm", Icon: Npm },
pnpm: { label: "pnpm", Icon: Pnpm },
yarn: { label: "yarn", Icon: Yarn },
@@ -19,18 +21,22 @@
</script>
<Select.Root type="single" bind:value={pkg}>
<Select.Trigger class={buttonVariants({ variant: "outline", size: "sm" })}>
<Select.Trigger
class={buttonVariants({ variant: "outline", class: "justify-between" })}
>
{#if managers[pkg]}
{@const { Icon, label } = managers[pkg]}
<Icon size={14} />
<span>{label}</span>
<div class="flex items-center space-x-2.5">
<Icon size={16} />
<span>{label}</span>
</div>
{/if}
</Select.Trigger>
<Select.Content sideOffset={1.5}>
{#each Object.entries(managers) as [value, { Icon, label }]}
{#each Object.entries(managers) as [value, { Icon, label }] (value)}
<Select.Item
{value}
onclick={() => pkgManager.set(value as PackageManager)}
onclick={() => settingsStore.setPackageManager(value as PackageManager)}
>
<Icon size={16} />
<span>{label}</span>
@@ -0,0 +1,19 @@
<script lang="ts">
import type { Snippet } from "svelte";
interface SettingsCardProps {
title: string;
description: string;
children: Snippet;
}
let { title, description, children }: SettingsCardProps = $props();
</script>
<div class="flex flex-col">
<h3 class="mb-0.5 font-medium">{title}</h3>
<p class="mb-3 text-sm text-neutral-600 dark:text-neutral-400">
{description}
</p>
{@render children?.()}
</div>
@@ -0,0 +1,61 @@
<script lang="ts">
import { cn } from "@/utils/cn";
import { toast } from "svelte-sonner";
import { settingsStore } from "@/stores/settings.store";
import SettingsCard from "@/components/settings/settingsCard.svelte";
import SettingsIcon from "@lucide/svelte/icons/settings";
import * as Dialog from "@/components/ui/dialog";
import { Button, buttonVariants } from "@/components/ui/button";
import Separator from "@/components/ui/separator/separator.svelte";
import OptimizeSvgs from "@/components/settings/options/optimizeSvgs.svelte";
import SelectPkgManager from "@/components/settings/options/selectPkgManager.svelte";
const handleResetSettings = () => {
settingsStore.reset();
toast.success("Settings have been reset to default");
};
</script>
<Dialog.Root>
<Dialog.Trigger
title="Settings"
class={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
"hover:bg-neutral-200 dark:hover:bg-neutral-800",
)}
>
<SettingsIcon size={20} strokeWidth={1.5} />
</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Settings</Dialog.Title>
<Dialog.Description>Customize your preferences.</Dialog.Description>
</Dialog.Header>
<Separator />
<div class="my-3 flex flex-col space-y-8">
<SettingsCard
title="Package Manager"
description="Select your preferred package manager for all installations commands"
>
<SelectPkgManager />
</SettingsCard>
<SettingsCard
title="Copy SVGs"
description="Use SVGO to optimize your SVGs when you copy source code (including all frameworks)"
>
<OptimizeSvgs />
</SettingsCard>
</div>
<Dialog.Footer>
<Button variant="outline" onclick={handleResetSettings}>
<span>Reset</span>
</Button>
<Dialog.Close class={buttonVariants({ variant: "default" })}>
<span>Save</span>
</Dialog.Close>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
+2 -2
View File
@@ -22,7 +22,7 @@
<button
class={cn(
"cursor-pointer transition-colors hover:animate-pulse",
"text-neutral-500 hover:text-red-700 dark:text-neutral-400 dark:hover:text-red-400",
"text-neutral-500 hover:text-red-700 dark:text-neutral-400 dark:hover:text-red-600",
isFavorite && "text-red-500",
)}
onclick={toggleFavorite}
@@ -36,6 +36,6 @@
<HeartIcon
size={16}
strokeWidth={1.8}
class={cn(isFavorite && "fill-red-500")}
class={cn(isFavorite && "fill-red-500 dark:fill-red-600")}
/>
</button>
+35 -25
View File
@@ -1,11 +1,14 @@
<script lang="ts">
import { clipboard } from "@/utils/clipboard";
import { globals } from "@/globals";
import CopyIcon from "@lucide/svelte/icons/copy";
import { Button } from "@/components/ui/button";
import SelectPkgManager from "@/components/selectPkgManager.svelte";
import { pkgManager, type PackageManager } from "@/stores/pkgManager.store";
import Shadcn from "../logos/shadcn.svelte";
import { buttonVariants } from "@/components/ui/button";
import CodeBlock from "@/components/codeBlock.svelte";
import ArrowUpRightIcon from "@lucide/svelte/icons/arrow-up-right";
import { settingsStore, type PackageManager } from "@/stores/settings.store";
import V0 from "@/components/logos/v0.svelte";
import Shadcn from "@/components/logos/shadcn.svelte";
interface Props {
svgTitle: string;
@@ -20,30 +23,37 @@
bun: "bunx shadcn@latest add",
};
let pkg = $derived($pkgManager);
let pkg = $derived($settingsStore.packageManager);
let shadcnCommand = $derived(shadcnCommands[pkg]);
const svgFormatTitle = svgTitle
.toLowerCase()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
const handleCopy = () => {
clipboard(`${shadcnCommand} @svgl/${svgFormatTitle}`);
};
</script>
<div class="flex items-center justify-between space-x-2">
<SelectPkgManager />
<Button variant="outline" onclick={handleCopy} size="sm">
<CopyIcon size={14} />
<span>Copy</span>
</Button>
</div>
<div
class="flex items-center space-x-2 rounded-md border border-neutral-200 p-2.5 dark:border-neutral-800"
>
<Shadcn size={14} />
<code class="font-mono text-sm select-all">
{shadcnCommand} @svgl/{svgFormatTitle}
</code>
<div class="flex w-full items-center space-x-2">
<a
target="_blank"
href="/docs/shadcn-ui"
class={buttonVariants({ variant: "outline", class: "w-full" })}
>
<span>Setup Registry</span>
<ArrowUpRightIcon
size={14}
class="text-neutral-500 dark:text-neutral-400"
/>
</a>
<a
target="_blank"
href={`${globals.v0Url}${globals.registryUrl}${svgFormatTitle}.json`}
class={buttonVariants({ variant: "outline", class: "w-full" })}
>
<span>Open with</span>
<V0 size={20} />
<ArrowUpRightIcon
size={14}
class="text-neutral-500 dark:text-neutral-400"
/>
</a>
</div>
<CodeBlock code={`${shadcnCommand} @svgl/${svgFormatTitle}`} Icon={Shadcn} />
+31 -30
View File
@@ -6,6 +6,7 @@
import { clipboard } from "@/utils/clipboard";
import { getPrefixFromSvgUrl, prefixSvgIds } from "@/utils/prefixSvgIds";
import { copyToClipboard as figmaCopyToClipboard } from "@/figma/copy-to-clipboard";
import { settingsStore } from "@/stores/settings.store";
// Icons:
import XIcon from "@lucide/svelte/icons/x";
@@ -15,6 +16,7 @@
// UI Components:
import { toast } from "svelte-sonner";
import { Separator } from "@/components/ui/separator";
import * as Tabs from "@/components/ui/tabs";
import { Button, buttonVariants } from "@/components/ui/button";
import * as Popover from "@/components/ui/popover";
@@ -36,6 +38,7 @@
import React from "@/components/logos/react.svelte";
import Astro from "@/components/logos/astro.svelte";
import Svelte from "@/components/logos/svelte.svelte";
import Shadcn from "@/components/logos/shadcn.svelte";
import Angular from "@/components/logos/angular.svelte";
import WebComponents from "@/components/logos/webComponents.svelte";
@@ -59,6 +62,7 @@
// States:
let optionsOpen = $state<boolean>(false);
let isLoading = $state<boolean>(false);
let optimize = $derived($settingsStore.optimizeSvgs);
const getSvgUrl = () => {
let svgUrlToCopy;
@@ -105,6 +109,7 @@
let content = await getSource({
url: svgUrlToCopy,
optimize,
});
if (svgUrlToCopy) {
@@ -150,6 +155,7 @@
const title = svgInfo.title.split(" ").join("");
let content = await getSource({
url: svgUrlToCopy,
optimize,
});
if (svgUrlToCopy) {
@@ -186,6 +192,7 @@
let content = await getSource({
url: svgUrlToCopy,
optimize,
});
if (svgUrlToCopy) {
@@ -223,6 +230,7 @@
let content = await getSource({
url: svgUrlToCopy,
optimize,
});
if (svgUrlToCopy) {
@@ -260,6 +268,7 @@
const svgUrlToCopy = getSvgUrl();
let content = await getSource({
url: svgUrlToCopy,
optimize,
});
if (svgUrlToCopy) {
@@ -297,6 +306,7 @@
const svgUrlToCopy = getSvgUrl();
let content = await getSource({
url: svgUrlToCopy,
optimize,
});
if (svgUrlToCopy) {
@@ -333,6 +343,7 @@
const svgUrlToCopy = getSvgUrl();
let content = await getSource({
url: svgUrlToCopy,
optimize,
});
if (svgUrlToCopy) {
@@ -380,42 +391,43 @@
<CopyIcon {size} strokeWidth={iconStroke} />
{/if}
</Popover.Trigger>
<Popover.Content class="flex w-fit flex-col space-y-2 p-4" sideOffset={2}>
<Popover.Content class="flex flex-col space-y-2 p-4" sideOffset={2}>
<Tabs.Root value="source" class="flex w-full flex-col space-y-1">
<Tabs.List class="w-fit border-none bg-transparent">
<Tabs.Trigger value="source">Source</Tabs.Trigger>
<Tabs.Trigger value="shadcn">shadcn/ui</Tabs.Trigger>
<div
class="ml-3 flex flex-row space-x-1 border-l border-neutral-200 pl-3 dark:border-neutral-800"
>
<Tabs.Trigger
class="px-2.5"
value="web-component"
title="Web Component"
>
<Tabs.List
class="flex h-auto w-auto flex-col space-y-2 space-x-0 border-0 md:h-9 md:flex-row md:space-y-0 md:space-x-1"
>
<div class="flex items-center space-x-1">
<Tabs.Trigger value="source">Source</Tabs.Trigger>
<Tabs.Trigger value="shadcn" title="shadcn/ui">
<Shadcn size={18} />
</Tabs.Trigger>
</div>
<div class="flex items-center space-x-1">
<Tabs.Trigger value="web-component" title="Web Component">
<WebComponents size={21} />
</Tabs.Trigger>
<Tabs.Trigger class="px-2.5" value="react" title="React">
<Tabs.Trigger value="react" title="React">
<React size={20} />
</Tabs.Trigger>
<Tabs.Trigger class="px-2.5" value="vue" title="Vue">
<Tabs.Trigger value="vue" title="Vue">
<Vue size={20} />
</Tabs.Trigger>
<Tabs.Trigger class="px-2.5" value="svelte" title="Svelte">
<Tabs.Trigger value="svelte" title="Svelte">
<Svelte size={20} />
</Tabs.Trigger>
<Tabs.Trigger class="px-2.5" value="angular" title="Angular">
<Tabs.Trigger value="angular" title="Angular">
<Angular size={20} />
</Tabs.Trigger>
<Tabs.Trigger
value="astro"
title="Astro"
class="px-2.5 text-black dark:text-white"
class="text-black dark:text-white"
>
<Astro size={21} />
</Tabs.Trigger>
</div>
</Tabs.List>
<Separator class="block md:hidden" />
<!-- Source -->
<Tabs.Content value="source">
<section class="flex flex-col space-y-2">
@@ -477,17 +489,6 @@
<span>Copy JS</span>
</Button>
<Button
variant="outline"
class="justify-start"
title="Copy as Svelte component"
disabled={isLoading}
onclick={() => convertSvgSvelteComponent(false)}
>
<Svelte size={18} />
<span>Copy JS</span>
</Button>
<Button
variant="outline"
class="justify-start"
@@ -575,8 +576,8 @@
class="mt-1 flex w-full items-center text-center text-[12px] text-neutral-600 dark:text-neutral-400"
>
<p>
Remember to request permission from the creators for the use of the SVG.
Modification is not allowed.
Please ensure you have permission from the creators before using the
SVG. Modifications are not permitted.
</p>
</div>
</Popover.Content>
+68 -36
View File
@@ -23,7 +23,10 @@
let iconSize = 16;
let iconStroke = 2;
let cardDownloadStyles =
"flex w-full h-full flex-col p-4 rounded-md shadow-sm dark:bg-neutral-800/20 bg-neutral-200/10 border border-neutral-200 dark:border-neutral-800 space-y-2";
"flex w-full h-full flex-col p-4 rounded-md shadow-sm dark:bg-neutral-800/20 bg-neutral-200/10 border border-neutral-200 dark:border-neutral-800 space-y-1.5";
let imgStyles = "my-7 h-10 select-none pointer-events-none";
let badgeButtonStyles =
"font-mono text-neutral-600 dark:text-neutral-400 text-xs";
// Functions:
const handleDownloadSvg = async (url?: string) => {
@@ -109,27 +112,23 @@
>
<DownloadIcon size={iconSize} strokeWidth={iconStroke} />
</Dialog.Trigger>
<Dialog.Content class="max-w-[630px]">
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Download {svgInfo.title} SVG</Dialog.Title>
<Dialog.Title>Download {svgInfo.title} SVGs</Dialog.Title>
<Dialog.Description>
This logo has multiple options to download:
</Dialog.Description>
</Dialog.Header>
<div
class={cn(
"flex h-full flex-col space-y-2 pt-4 pb-0.5",
"md:flex-row md:items-center md:justify-center md:space-y-0 md:space-x-2",
)}
>
<div class={cn("flex flex-col gap-4 md:flex-row")}>
{#if typeof svgInfo.route === "string"}
<div class={cardDownloadStyles}>
<img
src={isDarkTheme() ? svgInfo.route : svgInfo.route}
alt={svgInfo.title}
class="my-4 h-8"
class={imgStyles}
/>
<Button
class="justify-between"
title="Download logo"
variant="outline"
onclick={() => {
@@ -139,8 +138,11 @@
}
}}
>
<DownloadIcon class="mr-2" size={iconSize} />
<p>Icon logo</p>
<div class="flex items-center space-x-2">
<DownloadIcon size={iconSize} />
<p>Icon logo</p>
</div>
<span class={badgeButtonStyles}>.svg</span>
</Button>
</div>
{:else}
@@ -148,9 +150,10 @@
<img
src={isDarkTheme() ? svgInfo.route.dark : svgInfo.route.light}
alt={svgInfo.title}
class="my-4 h-10"
class={imgStyles}
/>
<Button
class="justify-between"
title="Logo with light & dark variants"
variant="outline"
onclick={() => {
@@ -162,11 +165,15 @@
}
}}
>
<DownloadIcon size={iconSize} />
<p>Light & dark variants</p>
<div class="flex items-center space-x-2">
<DownloadIcon size={iconSize} />
<p>Light & dark variants</p>
</div>
<span class={badgeButtonStyles}>.zip</span>
</Button>
<Button
class="justify-between"
title="Download light variant"
variant="outline"
onclick={() => {
@@ -176,11 +183,15 @@
}
}}
>
<DownloadIcon class="mr-2" size={iconSize} />
<p>Only light variant</p>
<div class="flex items-center space-x-2">
<DownloadIcon size={iconSize} />
<p>Only light variant</p>
</div>
<span class={badgeButtonStyles}>.svg</span>
</Button>
<Button
class="justify-between"
title="Download dark variant"
variant="outline"
onclick={() => {
@@ -190,8 +201,11 @@
}
}}
>
<DownloadIcon class="mr-2" size={iconSize} />
<p>Only dark variant</p>
<div class="flex items-center space-x-2">
<DownloadIcon size={iconSize} />
<p>Only dark variant</p>
</div>
<span class={badgeButtonStyles}>.svg</span>
</Button>
</div>
{/if}
@@ -201,9 +215,10 @@
<img
src={isDarkTheme() ? svgInfo.wordmark : svgInfo.wordmark}
alt={svgInfo.title}
class="my-4 h-8"
class={imgStyles}
/>
<Button
class="justify-between"
title="Download Wordmark logo"
variant="outline"
onclick={() => {
@@ -213,8 +228,11 @@
}
}}
>
<DownloadIcon class="mr-2" size={iconSize} />
<p>Wordmark logo</p>
<div class="flex items-center space-x-2">
<DownloadIcon size={iconSize} />
<p>Wordmark logo</p>
</div>
<span class={badgeButtonStyles}>.svg</span>
</Button>
</div>
{/if}
@@ -226,9 +244,10 @@
? svgInfo.wordmark.dark
: svgInfo.wordmark.light}
alt={svgInfo.title}
class="my-4 h-10"
class={imgStyles}
/>
<Button
class="justify-between"
title="Download Wordmark light variant"
variant="outline"
onclick={() => {
@@ -242,11 +261,15 @@
}
}}
>
<DownloadIcon class="mr-2" size={iconSize} />
<p>Light & dark variants</p>
<div class="flex items-center space-x-2">
<DownloadIcon size={iconSize} />
<p>Light & dark variants</p>
</div>
<span class={badgeButtonStyles}>.zip</span>
</Button>
<Button
class="justify-between"
title="Download Wordmark light variant"
variant="outline"
onclick={() => {
@@ -256,11 +279,15 @@
}
}}
>
<DownloadIcon class="mr-2" size={iconSize} />
<p>Wordmark light variant</p>
<div class="flex items-center space-x-2">
<DownloadIcon size={iconSize} />
<p>Wordmark light variant</p>
</div>
<span class={badgeButtonStyles}>.svg</span>
</Button>
<Button
class="justify-between"
title="Download Wordmark dark variant"
variant="outline"
onclick={() => {
@@ -270,19 +297,24 @@
}
}}
>
<DownloadIcon class="mr-2" size={iconSize} />
<p>Wordmark dark variant</p>
<div class="flex items-center space-x-2">
<DownloadIcon size={iconSize} />
<p>Wordmark dark variant</p>
</div>
<span class={badgeButtonStyles}>.svg</span>
</Button>
</div>
{/if}
</div>
<Dialog.Footer
class="mt-3 text-xs text-neutral-600 dark:text-neutral-400"
>
<p>
Remember to request permission from the creators for the use of the
SVG. Modification is not allowed.
</p>
<Dialog.Footer>
<div
class="mt-2 flex w-full items-center justify-center text-sm text-neutral-600 dark:text-neutral-400"
>
<p class="w-full text-center text-sm">
Please ensure you have permission from the creators before using the
SVG. Modifications are not permitted.
</p>
</div>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>
+36 -34
View File
@@ -1,6 +1,9 @@
<script lang="ts">
import type { iSVG } from "@/types/svg";
import { cn } from "@/utils/cn";
import { mode } from "mode-watcher";
import { getSvgImgUrl } from "@/data";
// Icons:
import XIcon from "@lucide/svelte/icons/x";
@@ -43,9 +46,9 @@
<div
class={cn(
"group flex flex-col items-center justify-center px-3.5 py-3",
"flex flex-col items-center justify-center px-3.5 py-3",
"rounded-md border border-neutral-200 dark:border-neutral-800",
"transition-colors duration-100 hover:bg-neutral-100/80 dark:hover:bg-neutral-800/20",
"hover:bg-neutral-100/80 dark:hover:bg-neutral-800/20",
)}
>
<!-- Image Options -->
@@ -67,43 +70,43 @@
<AddToFavorite svg={svgInfo} />
</div>
<!-- Image -->
{#if wordmarkSvg == true && svgInfo.wordmark !== undefined}
{#if wordmarkSvg && svgInfo.wordmark !== undefined}
<img
loading="lazy"
class={cn("hidden dark:block", globalImageStyles)}
src={typeof svgInfo.wordmark !== "string"
? svgInfo.wordmark?.dark || ""
: svgInfo.wordmark || ""}
src={getSvgImgUrl({ url: svgInfo.wordmark, isDark: true })}
alt={svgInfo.title}
title={svgInfo.title}
loading="lazy"
width="140"
height="40"
/>
<img
loading="lazy"
class={cn("block dark:hidden", globalImageStyles)}
src={typeof svgInfo.wordmark !== "string"
? svgInfo.wordmark?.light || ""
: svgInfo.wordmark || ""}
src={getSvgImgUrl({ url: svgInfo.wordmark, isDark: false })}
alt={svgInfo.title}
title={svgInfo.title}
loading="lazy"
width="140"
height="40"
/>
{:else}
<img
loading="lazy"
class={cn("hidden dark:block", globalImageStyles)}
src={typeof svgInfo.route !== "string"
? svgInfo.route.dark
: svgInfo.route}
src={getSvgImgUrl({ url: svgInfo.route, isDark: true })}
alt={svgInfo.title}
title={svgInfo.title}
loading="lazy"
width="140"
height="40"
/>
<img
loading="lazy"
class={cn("block dark:hidden", globalImageStyles)}
src={typeof svgInfo.route !== "string"
? svgInfo.route.light
: svgInfo.route}
src={getSvgImgUrl({ url: svgInfo.route, isDark: false })}
alt={svgInfo.title}
title={svgInfo.title}
loading="lazy"
width="140"
height="40"
/>
{/if}
<!-- Title -->
@@ -115,19 +118,19 @@
</p>
<div class="flex items-center justify-center space-x-1">
{#if Array.isArray(svgInfo.category)}
{#each svgInfo.category.slice(0, maxVisibleCategories) as c, index}
{#each svgInfo.category.slice(0, maxVisibleCategories) as c (c)}
<a
href={`/directory/${c.toLowerCase()}`}
class={badgeVariants({
variant: "outline",
class: "cursor-pointer font-mono",
class:
"cursor-pointer font-mono hover:border-neutral-400 dark:hover:border-neutral-600",
})}
title={`This icon is part of the ${svgInfo.category} category`}
>
{c}
</a>
{/each}
{#if svgInfo.category.length > maxVisibleCategories}
<Popover.Root
open={moreTagsOptions}
@@ -136,7 +139,8 @@
<Popover.Trigger
class={badgeVariants({
variant: "outline",
class: "cursor-pointer font-mono",
class:
"cursor-pointer font-mono hover:border-neutral-400 dark:hover:border-neutral-600",
})}
title="More Tags"
>
@@ -147,11 +151,14 @@
{/if}
</Popover.Trigger>
<Popover.Content class="flex w-auto flex-col space-y-2">
<p class="font-medium">More tags:</p>
{#each svgInfo.category.slice(maxVisibleCategories) as c}
<p class="font-medium">More tags</p>
{#each svgInfo.category.slice(maxVisibleCategories) as c (c)}
<a
href={`/directory/${c.toLowerCase()}`}
class={cn(buttonVariants({ variant: "outline" }), "w-full")}
class={cn(
buttonVariants({ variant: "outline" }),
"w-full justify-start",
)}
>
<TagIcon size={15} strokeWidth={1.5} />
<span>{c}</span>
@@ -165,7 +172,8 @@
href={`/directory/${svgInfo.category.toLowerCase()}`}
class={badgeVariants({
variant: "outline",
class: "cursor-pointer font-mono",
class:
"cursor-pointer font-mono hover:border-neutral-400 dark:hover:border-neutral-600",
})}
>
{svgInfo.category}
@@ -193,13 +201,7 @@
/>
{/if}
<DownloadSvg
{svgInfo}
isDarkTheme={() => {
const dark = document.documentElement.classList.contains("dark");
return dark;
}}
/>
<DownloadSvg {svgInfo} isDarkTheme={() => mode.current === "dark"} />
<a
href={svgInfo.url}
+57
View File
@@ -0,0 +1,57 @@
<script lang="ts">
import { globals } from "@/globals";
import { buttonVariants } from "@/components/ui/button";
import SearchIcon from "@lucide/svelte/icons/search";
import ArrowUpRight from "@lucide/svelte/icons/arrow-up-right";
import BoxesIcon from "@/components/ui/moving-icons/boxes-icon.svelte";
interface Props {
svgTitle: string;
category?: string;
searchGlobally?: boolean;
}
let { svgTitle, category, searchGlobally }: Props = $props();
</script>
<div class="flex w-full flex-col items-center justify-center space-y-4 py-6">
<BoxesIcon size={48} strokeWidth={1} />
<h2 class="text-xl font-semibold">SVG not found</h2>
{#if category}
<p class="text-neutral-600 dark:text-neutral-400">
"{svgTitle}" not found in "{category}" category
</p>
{:else}
<p class="text-neutral-600 dark:text-neutral-400">
"{svgTitle}" not found
</p>
{/if}
<div class="flex items-center justify-center space-x-2">
{#if category || searchGlobally}
<a
href={`/?search=${svgTitle}`}
class={buttonVariants({ variant: "outline" })}
>
<SearchIcon size={14} strokeWidth={1.5} />
<span>Search globally</span>
</a>
{/if}
<a
target="_blank"
href={globals.requestSvgUrl}
class={buttonVariants({ variant: "outline" })}
>
<span>Request SVG</span>
<ArrowUpRight size={14} strokeWidth={1.5} />
</a>
<a
target="_blank"
href={globals.submitUrl}
class={buttonVariants({ variant: "outline" })}
>
<span>Submit SVG</span>
<ArrowUpRight size={14} strokeWidth={1.5} />
</a>
</div>
</div>
@@ -0,0 +1,28 @@
<script lang="ts">
import type { TableOfContentsProps } from "./toc.types";
import { cn } from "@/utils/cn";
let { toc, className }: TableOfContentsProps = $props();
</script>
<div
class={cn(
"flex flex-col text-sm text-neutral-600 dark:text-neutral-400",
className,
)}
>
{#each toc as tocItem (tocItem.id)}
<a
href={"#" + tocItem.slug}
class={cn(
"pt-1 pb-1.5 transition-colors hover:text-neutral-900 dark:hover:text-neutral-50",
tocItem.level === 2 && "ml-0 font-medium",
tocItem.level === 3 &&
"border-l border-neutral-200 pl-4 dark:border-neutral-800",
tocItem.level === 4 && "ml-8",
)}
>
{tocItem.text}
</a>
{/each}
</div>
@@ -0,0 +1,6 @@
import type { ToCItem } from "@/markdown/generateToC";
export interface TableOfContentsProps {
toc: ToCItem[];
className?: string;
}
+1 -1
View File
@@ -3,7 +3,7 @@
HTMLAnchorAttributes,
HTMLButtonAttributes,
} from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
import type { VariantProps } from "tailwind-variants";
import type { WithElementRef } from "@/types/components";
import { cn } from "@/utils/cn";
+1 -1
View File
@@ -7,7 +7,7 @@ const buttonVariants = tv({
default:
"bg-neutral-900 text-neutral-50 shadow hover:bg-neutral-900/90 dark:bg-neutral-800 dark:text-neutral-50 dark:hover:bg-neutral-800/70",
radial:
"bg-radial-[at_52%_-52%] **:[text-shadow:0_1px_0_var(--color-neutral-950)] border-neutral-950 from-neutral-950/70 to-neutral-950/95 text-white inset-shadow-2xs inset-shadow-white/25 border text-sm shadow-md shadow-neutral-950/30 ring-0 transition-[filter] duration-200 hover:brightness-125 active:brightness-95 dark:border-0",
"bg-radial-[at_52%_-52%] **:[text-shadow:0_1px_0_var(--color-neutral-950)] border-neutral-950 from-neutral-950/70 to-neutral-950/95 text-white inset-shadow-2xs inset-shadow-white/25 border text-sm shadow-md shadow-neutral-950/30 ring-0 transition-[filter] duration-200 hover:brightness-125 active:brightness-95 dark:bg-white dark:text-neutral-50 dark:shadow-none dark:border-0",
destructive:
"bg-red-500 text-neutral-50 shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90",
outline:
@@ -0,0 +1,14 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
let {
ref = $bindable(null),
...restProps
}: CollapsiblePrimitive.ContentProps = $props();
</script>
<CollapsiblePrimitive.Content
bind:ref
data-slot="collapsible-content"
{...restProps}
/>
@@ -0,0 +1,14 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
let {
ref = $bindable(null),
...restProps
}: CollapsiblePrimitive.TriggerProps = $props();
</script>
<CollapsiblePrimitive.Trigger
bind:ref
data-slot="collapsible-trigger"
{...restProps}
/>
@@ -0,0 +1,16 @@
<script lang="ts">
import { Collapsible as CollapsiblePrimitive } from "bits-ui";
let {
ref = $bindable(null),
open = $bindable(false),
...restProps
}: CollapsiblePrimitive.RootProps = $props();
</script>
<CollapsiblePrimitive.Root
bind:ref
bind:open
data-slot="collapsible"
{...restProps}
/>
+13
View File
@@ -0,0 +1,13 @@
import Root from "./collapsible.svelte";
import Trigger from "./collapsible-trigger.svelte";
import Content from "./collapsible-content.svelte";
export {
Root,
Content,
Trigger,
//
Root as Collapsible,
Content as CollapsibleContent,
Trigger as CollapsibleTrigger,
};
@@ -27,7 +27,8 @@
bind:ref
data-slot="dialog-content"
class={cn(
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-neutral-200 bg-white p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg dark:border-neutral-800 dark:bg-neutral-900",
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-2 rounded-lg p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-2xl",
"border border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900",
className,
)}
{...restProps}
@@ -35,7 +36,11 @@
{@render children?.()}
{#if showCloseButton}
<DialogPrimitive.Close
class="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:ring-2 focus:ring-neutral-900 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-neutral-100 data-[state=open]:text-neutral-500 dark:ring-offset-neutral-900 dark:focus:ring-neutral-300 dark:data-[state=open]:bg-neutral-800 dark:data-[state=open]:text-neutral-400 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
title="Close"
class={cn(
"absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"ring-offset-white focus:ring-neutral-400 focus:ring-offset-2 data-[state=open]:bg-white data-[state=open]:text-neutral-500 dark:ring-offset-neutral-300 dark:focus:ring-neutral-700 dark:data-[state=open]:bg-neutral-900 dark:data-[state=open]:text-neutral-400",
)}
>
<XIcon />
<span class="sr-only">Close</span>
@@ -14,7 +14,7 @@
<div
bind:this={ref}
data-slot="dialog-header"
class={cn("flex flex-col gap-2 text-center sm:text-left", className)}
class={cn("mb-2 flex flex-col gap-1 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
+1 -1
View File
@@ -12,6 +12,6 @@
<DialogPrimitive.Title
bind:ref
data-slot="dialog-title"
class={cn("text-lg leading-none font-semibold", className)}
class={cn("text-xl leading-none font-semibold", className)}
{...restProps}
/>
@@ -0,0 +1,27 @@
<script lang="ts">
import { cn } from "@/utils/cn";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: DropdownMenuPrimitive.PortalProps;
} = $props();
</script>
<DropdownMenuPrimitive.Portal {...portalProps}>
<DropdownMenuPrimitive.Content
bind:ref
data-slot="dropdown-menu-content"
{sideOffset}
class={cn(
"z-50 max-h-(--bits-dropdown-menu-content-available-height) min-w-[8rem] origin-(--bits-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-neutral-200 bg-neutral-50 p-1 text-neutral-950 shadow-md outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-50",
className,
)}
{...restProps}
/>
</DropdownMenuPrimitive.Portal>
@@ -0,0 +1,14 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
...restProps
}: DropdownMenuPrimitive.GroupProps = $props();
</script>
<DropdownMenuPrimitive.Group
bind:ref
data-slot="dropdown-menu-group"
{...restProps}
/>
@@ -0,0 +1,27 @@
<script lang="ts">
import { cn } from "@/utils/cn";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
inset,
variant = "default",
...restProps
}: DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
variant?: "default" | "destructive";
} = $props();
</script>
<DropdownMenuPrimitive.Item
bind:ref
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
class={cn(
"data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-highlighted:bg-neutral-200 data-highlighted:text-neutral-950 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 dark:data-highlighted:bg-neutral-800 dark:data-highlighted:text-neutral-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className,
)}
{...restProps}
/>
@@ -0,0 +1,25 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "@/types/components";
import { cn } from "@/utils/cn";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
inset?: boolean;
} = $props();
</script>
<div
bind:this={ref}
data-slot="dropdown-menu-label"
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:pl-8", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,17 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "@/utils/cn";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
</script>
<DropdownMenuPrimitive.Separator
bind:ref
data-slot="dropdown-menu-separator"
class={cn("-mx-1 my-1 h-px bg-neutral-200 dark:bg-neutral-800", className)}
{...restProps}
/>
@@ -0,0 +1,24 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "@/types/components";
import { cn } from "@/utils/cn";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="dropdown-menu-shortcut"
class={cn(
"ml-auto text-xs tracking-widest text-neutral-700 dark:text-neutral-300",
className,
)}
{...restProps}
>
{@render children?.()}
</span>
@@ -0,0 +1,14 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
...restProps
}: DropdownMenuPrimitive.TriggerProps = $props();
</script>
<DropdownMenuPrimitive.Trigger
bind:ref
data-slot="dropdown-menu-trigger"
{...restProps}
/>
+33
View File
@@ -0,0 +1,33 @@
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import Group from "./dropdown-menu-group.svelte";
import Trigger from "./dropdown-menu-trigger.svelte";
import Content from "./dropdown-menu-content.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import Separator from "./dropdown-menu-separator.svelte";
const Sub = DropdownMenuPrimitive.Sub;
const Root = DropdownMenuPrimitive.Root;
export {
Content,
Root as DropdownMenu,
Content as DropdownMenuContent,
Group as DropdownMenuGroup,
Item as DropdownMenuItem,
Separator as DropdownMenuSeparator,
Shortcut as DropdownMenuShortcut,
Label as DropdownMenuLabel,
Sub as DropdownMenuSub,
Trigger as DropdownMenuTrigger,
Group,
Item,
Root,
Separator,
Shortcut,
Sub,
Trigger,
Label,
};
@@ -21,7 +21,7 @@
{sideOffset}
{align}
class={cn(
"z-50 w-fit max-w-[480px] origin-(--bits-popover-content-transform-origin) rounded-md border border-neutral-200 bg-white p-4 text-neutral-900 shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-50",
"z-50 w-auto max-w-96 origin-(--bits-popover-content-transform-origin) rounded-md border border-neutral-200 bg-white p-4 text-neutral-900 shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 md:max-w-[480px] dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-50",
className,
)}
{...restProps}
+36
View File
@@ -0,0 +1,36 @@
import { Dialog as SheetPrimitive } from "bits-ui";
import Trigger from "./sheet-trigger.svelte";
import Close from "./sheet-close.svelte";
import Overlay from "./sheet-overlay.svelte";
import Content from "./sheet-content.svelte";
import Header from "./sheet-header.svelte";
import Footer from "./sheet-footer.svelte";
import Title from "./sheet-title.svelte";
import Description from "./sheet-description.svelte";
const Root = SheetPrimitive.Root;
const Portal = SheetPrimitive.Portal;
export {
Root,
Close,
Trigger,
Portal,
Overlay,
Content,
Header,
Footer,
Title,
Description,
//
Root as Sheet,
Close as SheetClose,
Trigger as SheetTrigger,
Portal as SheetPortal,
Overlay as SheetOverlay,
Content as SheetContent,
Header as SheetHeader,
Footer as SheetFooter,
Title as SheetTitle,
Description as SheetDescription,
};
@@ -0,0 +1,8 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: SheetPrimitive.CloseProps =
$props();
</script>
<SheetPrimitive.Close bind:ref data-slot="sheet-close" {...restProps} />
@@ -0,0 +1,45 @@
<script lang="ts">
import type { Snippet } from "svelte";
import type { WithoutChildrenOrChild } from "@/types/components";
import SheetOverlay from "./sheet-overlay.svelte";
import { sheetVariants, type Side } from "./sheet.variants";
import { cn } from "@/utils/cn";
import { Dialog as SheetPrimitive } from "bits-ui";
import XIcon from "@lucide/svelte/icons/x";
let {
ref = $bindable(null),
class: className,
side = "right",
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
portalProps?: SheetPrimitive.PortalProps;
side?: Side;
children: Snippet;
} = $props();
</script>
<SheetPrimitive.Portal {...portalProps}>
<SheetOverlay />
<SheetPrimitive.Content
bind:ref
data-slot="sheet-content"
class={cn(sheetVariants({ side }), className)}
{...restProps}
>
{@render children?.()}
<SheetPrimitive.Close
class={cn(
"ring-offset-neutral-300 focus-visible:ring-neutral-300 dark:ring-offset-neutral-700 dark:focus-visible:ring-neutral-700",
"absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none",
)}
>
<XIcon class="size-4" />
<span class="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPrimitive.Portal>
@@ -0,0 +1,17 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "@/utils/cn";
let {
ref = $bindable(null),
class: className,
...restProps
}: SheetPrimitive.DescriptionProps = $props();
</script>
<SheetPrimitive.Description
bind:ref
data-slot="sheet-description"
class={cn("text-sm text-neutral-700 dark:text-neutral-300", className)}
{...restProps}
/>
@@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "@/types/components";
import { cn } from "@/utils/cn";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="sheet-footer"
class={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "@/types/components";
import { cn } from "@/utils/cn";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="sheet-header"
class={cn("flex flex-col gap-1.5 p-4", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "@/utils/cn";
let {
ref = $bindable(null),
class: className,
...restProps
}: SheetPrimitive.OverlayProps = $props();
</script>
<SheetPrimitive.Overlay
bind:ref
data-slot="sheet-overlay"
class={cn(
"fixed inset-0 z-50 bg-neutral-200/50 backdrop-blur-[4px] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 dark:bg-neutral-900/50",
className,
)}
{...restProps}
/>
@@ -0,0 +1,17 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "@/utils/cn";
let {
ref = $bindable(null),
class: className,
...restProps
}: SheetPrimitive.TitleProps = $props();
</script>
<SheetPrimitive.Title
bind:ref
data-slot="sheet-title"
class={cn("font-semibold text-neutral-950 dark:text-neutral-50", className)}
{...restProps}
/>
@@ -0,0 +1,8 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: SheetPrimitive.TriggerProps =
$props();
</script>
<SheetPrimitive.Trigger bind:ref data-slot="sheet-trigger" {...restProps} />
+22
View File
@@ -0,0 +1,22 @@
import { tv, type VariantProps } from "tailwind-variants";
const sheetVariants = tv({
base: "bg-white dark:bg-neutral-900 border-neutral-200 dark:border-neutral-800 data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
variants: {
side: {
top: "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
bottom:
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
left: "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
right:
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
},
},
defaultVariants: {
side: "left",
},
});
type Side = VariantProps<typeof sheetVariants>["side"];
export { sheetVariants, type Side };
+13 -4
View File
@@ -3,6 +3,8 @@
Toaster as Sonner,
type ToasterProps as SonnerProps,
} from "svelte-sonner";
import { cn } from "@/utils/cn";
import { mode } from "mode-watcher";
let { ...restProps }: SonnerProps = $props();
@@ -10,12 +12,19 @@
<Sonner
theme={mode.current}
class="toaster group"
position="bottom-center"
toastOptions={{
unstyled: true,
classes: {
toast:
"group toast dark:group-[.toaster]:bg-neutral-900 group-[.toaster]:font-sans",
description: "group-[.toast]:text-xs font-mono",
toast: cn(
"w-full max-w-md",
"flex items-center gap-3 p-4 rounded-lg shadow-md font-sans",
"bg-neutral-50 dark:bg-neutral-900",
"border border-neutral-200 dark:border-neutral-800",
"text-neutral-900 dark:text-neutral-100",
),
title: "font-medium",
description: "text-sm text-pretty text-neutral-600 dark:text-neutral-400",
},
}}
{...restProps}
+3
View File
@@ -0,0 +1,3 @@
import Root from "./switch.svelte";
export { Root, Root as Switch };
+31
View File
@@ -0,0 +1,31 @@
<script lang="ts">
import type { WithoutChildrenOrChild } from "@/types/components";
import { cn } from "@/utils/cn";
import { Switch as SwitchPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
checked = $bindable(false),
...restProps
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
</script>
<SwitchPrimitive.Root
bind:ref
bind:checked
data-slot="switch"
class={cn(
"peer inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:border-neutral-500 focus-visible:ring-[3px] focus-visible:ring-neutral-300/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=unchecked]:bg-neutral-200 dark:focus-visible:ring-neutral-500/50 dark:data-[state=checked]:bg-neutral-100 dark:data-[state=unchecked]:bg-neutral-700",
className,
)}
{...restProps}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
class={cn(
"pointer-events-none block size-4 rounded-full bg-white ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=checked]:bg-white data-[state=unchecked]:translate-x-0 dark:bg-neutral-900 dark:data-[state=checked]:bg-neutral-900",
)}
/>
</SwitchPrimitive.Root>
+1 -1
View File
@@ -13,7 +13,7 @@
bind:ref
data-slot="tabs-list"
class={cn(
"inline-flex h-9 items-center justify-center rounded-md border border-neutral-200 bg-white p-1 text-neutral-500 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-400",
"inline-flex h-9 items-center justify-center rounded-md border border-neutral-200 bg-white text-neutral-500 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-400",
className,
)}
{...restProps}
+1 -1
View File
@@ -13,7 +13,7 @@
bind:ref
data-slot="tabs-trigger"
class={cn(
"inline-flex cursor-pointer items-center justify-center rounded-md px-3 py-1 text-sm font-medium whitespace-nowrap ring-offset-white transition-all hover:text-black focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-neutral-200 data-[state=active]:text-neutral-950 data-[state=active]:shadow dark:ring-offset-neutral-950 dark:hover:text-white dark:focus-visible:ring-neutral-300 dark:data-[state=active]:bg-neutral-800 dark:data-[state=active]:text-neutral-50",
"inline-flex cursor-pointer items-center justify-center rounded-md px-3 py-1 text-sm font-medium whitespace-nowrap ring-offset-white transition-all hover:bg-neutral-200 hover:text-black focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-neutral-200 data-[state=active]:text-neutral-950 data-[state=active]:shadow dark:ring-offset-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-white dark:focus-visible:ring-neutral-300 dark:data-[state=active]:bg-neutral-800 dark:data-[state=active]:text-neutral-50",
className,
)}
{...restProps}
-12
View File
@@ -1,12 +0,0 @@
<script lang="ts">
import { onNavigate } from "$app/navigation";
onNavigate((navigation) => {
if (!document.startViewTransition) return;
return new Promise((resolve) => {
document.startViewTransition(async () => {
resolve();
await navigation.complete;
});
});
});
</script>
+41
View File
@@ -0,0 +1,41 @@
<script lang="ts">
import { cn } from "@/utils/cn";
import Button from "@/components/ui/button/button.svelte";
import CheckIcon from "@lucide/svelte/icons/check";
import AlertTriangleIcon from "@lucide/svelte/icons/alert-triangle";
import { warningStore, acceptWarning } from "@/stores/warning.store";
</script>
{#if !$warningStore}
<div
class={cn(
"flex w-full flex-col items-center justify-between space-y-2 px-3 py-4 text-sm md:flex-row md:space-y-0 md:space-x-2 md:py-2",
"bg-white dark:bg-neutral-900",
"border-b border-neutral-200 dark:border-neutral-800",
)}
>
<div class="flex flex-col items-center gap-2 md:flex-row">
<AlertTriangleIcon
size={18}
strokeWidth={2}
class="flex-shrink-0 animate-pulse text-yellow-600 dark:text-yellow-500"
/>
<p>
Each SVG includes a link to its respective product. Permission must be
obtained before using a logo. For removal requests,
<a
href="https://github.com/pheralb/svgl/issues/new"
target="_blank"
class="underline decoration-neutral-500 underline-offset-4"
>
please open an issue on GitHub
</a>.
</p>
</div>
<Button size="sm" onclick={acceptWarning}>
<CheckIcon size={14} strokeWidth={2} />
<span>Accept</span>
</Button>
</div>
{/if}
+103 -47
View File
@@ -2,14 +2,70 @@ import type { Extension } from "@/types/extensions";
export const extensions: Extension[] = [
{
name: "SVGL for Raycast",
description: "Search SVG logos via svgl using Raycast.",
url: "https://www.raycast.com/1weiho/svgl",
name: "SVGL CLI",
description: "A CLI for easily adding SVG icons to your project.",
url: "https://github.com/sujjeee/svgls",
image:
"https://github.com/pheralb/svgl/raw/main/static/library/raycast.svg",
"https://raw.githubusercontent.com/pheralb/svgl/refs/heads/main/static/images/svgl_svg.svg",
created_by: {
name: "1weiho",
socialUrl: "https://x.com/1weiho",
name: "sujjeee",
socialUrl: "https://x.com/sujjeeee",
},
},
{
name: "SVGL for React",
description:
"An open-source NPM package that offers a SVGL Logos for React.",
url: "https://github.com/ridemountainpig/svgl-react",
image:
"https://raw.githubusercontent.com/pheralb/svgl/refs/heads/main/static/library/react_light.svg",
created_by: {
name: "ridemountainpig",
socialUrl: "https://x.com/ridemountainpig",
},
},
{
name: "SVGL for Framer",
description:
"Import colorful SVG logos, fast and easy using our plugin for Framer.",
url: "https://www.framer.com/marketplace/plugins/svgl/",
image:
"https://raw.githubusercontent.com/pheralb/svgl/refs/heads/main/static/library/framer.svg",
created_by: {
name: "Krishna Singh",
socialUrl: "https://x.com/krishnasinghdev",
},
},
{
name: "SVGL for Vue",
description: "An open-source NPM package that offers a SVGL Logos for Vue.",
url: "https://github.com/selemondev/svgl-vue",
image:
"https://raw.githubusercontent.com/pheralb/svgl/refs/heads/main/static/library/vue.svg",
created_by: {
name: "selemondev",
socialUrl: "https://x.com/selemondev",
},
},
{
name: "SVGL for Svelte",
description:
"An open-source NPM package that offers a SVGL Logos for Svelte.",
url: "https://github.com/selemondev/svgl-svelte",
image: "https://github.com/pheralb/svgl/raw/main/static/library/svelte.svg",
created_by: {
name: "selemondev",
socialUrl: "https://x.com/selemondev",
},
},
{
name: "SVGL for Figma",
description: "Add svgs from svgl to your Figma project.",
url: "https://www.figma.com/community/plugin/1320306989350693206/svgl",
image: "https://github.com/pheralb/svgl/raw/main/static/library/figma.svg",
created_by: {
name: "quilljou",
socialUrl: "https://x.com/quillzhou",
},
},
{
@@ -20,72 +76,72 @@ export const extensions: Extension[] = [
"https://github.com/pheralb/svgl/raw/main/static/library/powertoys.svg",
created_by: {
name: "SameerJS6",
socialUrl: "https://svgl.sameerjs.com/",
socialUrl: "https://x.com/Sameerjs6",
},
},
{
name: "SVGL for React",
description:
"An open-source NPM package that offers a collection of high-quality SVGL logos for React.",
url: "https://github.com/ridemountainpig/svgl-react?tab=readme-ov-file",
name: "SVGL for Raycast",
description: "Search SVG logos via svgl.",
url: "https://www.raycast.com/1weiho/svgl",
image:
"https://raw.githubusercontent.com/pheralb/svgl/0d4514c9521688e76c6a1b426f3054a424d96aa5/static/library/react_dark.svg",
"https://github.com/pheralb/svgl/raw/main/static/library/raycast.svg",
created_by: {
name: "ridemountainpig",
socialUrl: "https://twitter.com/ridemountainpig",
name: "1weiho",
socialUrl: "https://x.com/1weiho",
},
},
{
name: "SVGL Badges",
description: "A beautiful badges with svgl SVG logos.",
name: "SVGL for Visual Studio Code",
description: "SVGL directly in your VSCode.",
url: "https://marketplace.visualstudio.com/items?itemName=EsteveSegura.svgl",
image: "https://github.com/pheralb/svgl/raw/main/static/library/vscode.svg",
created_by: {
name: "girlazote",
socialUrl: "https://x.com/girlazote",
},
},
{
name: "SVGL Badge",
description: "A beautiful badges with SVGL SVGs logos.",
url: "https://svgl-badge.vercel.app/",
image:
"https://camo.githubusercontent.com/b516f0f725ad1827dd854f16ec08626569d02ab827cd06b4f42163e519b813c7/68747470733a2f2f7376676c2d62616467652e76657263656c2e6170702f6170692f4c6962726172792f5376676c3f7468656d653d6c69676874",
created_by: {
name: "ridemountainpig",
socialUrl: "https://twitter.com/ridemountainpig",
socialUrl: "https://x.com/ridemountainpig",
},
},
{
name: "Magic",
description: "AI extension for Cursor & other IDEs.",
name: "SVGL on Magic by 21st",
description: "Integrate company logos and icons via SVGL on Magic.",
url: "https://21st.dev/magic",
image:
"https://github.com/serafimcloud/21st/blob/main/apps/web/public/icon.png?raw=true",
"https://github.com/serafimcloud/21st/raw/main/apps/web/public/icon.png?raw=true",
created_by: {
name: "serafim",
name: "serafimcloud",
socialUrl: "https://x.com/serafimcloud",
},
},
{
name: "svgls",
description: "A CLI for easily adding SVG icons to your project.",
url: "https://github.com/sujjeee/svgls",
image: "https://github.com/pheralb/svgl/raw/main/static/library/svgl.svg",
created_by: {
name: "Sujjeee",
socialUrl: "https://twitter.com/sujjeeee",
},
},
{
name: "SVGL for Figma",
description: "Add svgs from svgl to your Figma project.",
url: "https://www.figma.com/community/plugin/1320306989350693206/svgl",
image: "https://github.com/pheralb/svgl/raw/main/static/library/figma.svg",
created_by: {
name: "Quill",
socialUrl: "https://x.com/quillzhou",
},
},
{
name: "SVGL for VSCode",
description: "SVGL directly in your VSCode.",
url: "https://marketplace.visualstudio.com/items?itemName=EsteveSegura.svgl",
name: "SVGL for PowerShell",
description: "PowerShell extension to quickly get svgl logos anywhere.",
url: "https://github.com/spaansba/SVGL-PowerShell",
image:
"https://github.com/pheralb/svgl/blob/main/static/library/vscode.svg",
"https://github.com/pheralb/svgl/raw/main/static/library/powershell.svg",
created_by: {
name: "GiR",
socialUrl: "https://x.com/girlazote",
name: "Bart Spaans",
socialUrl: "https://bsky.app/profile/bartspaans.bsky.social",
},
},
{
name: "SVGL for Flow Launcher",
description: "Search & copy SVG logos in Flow Launcher.",
url: "https://github.com/spaansba/SVGL-Flow-Launcher",
image:
"https://github.com/pheralb/svgl/raw/main/static/library/FlowLauncher.svg",
created_by: {
name: "AF_Askar",
socialUrl: "https://x.com/Askar_AF",
},
},
];
+30 -13
View File
@@ -1,11 +1,19 @@
import type { iSVG } from "@/types/svg";
import { svgs } from "./svgs";
import type { iSVG, ThemeOptions } from "@/types/svg";
import type { Category } from "@/types/categories";
import type { Extension } from "@/types/extensions";
import { svgs } from "@/data/svgs";
import { extensions } from "@/data/extensions";
export const svgsData = svgs.map((svg: iSVG, index: number) => {
return { id: index, ...svg };
}) as iSVG[];
export const getCategories = () => {
export const extensionsData = extensions.map((extension, index) => {
return { id: index, ...extension };
}) as Extension[];
export const getCategories = (): Category[] => {
const categories = svgs
.flatMap((svg) =>
Array.isArray(svg.category) ? svg.category : [svg.category],
@@ -14,14 +22,23 @@ export const getCategories = () => {
return categories;
};
export const getCategoriesForDirectory = () => {
const categories = svgs
.flatMap((svg) =>
Array.isArray(svg.category) ? svg.category : [svg.category],
)
.filter((category, index, array) => array.indexOf(category) === index)
.map((category) => ({
slug: category.toLowerCase(),
}));
return categories;
export const getSvgsByCategory = (category: string): iSVG[] =>
svgsData.filter((svg: iSVG) => {
if (Array.isArray(svg.category)) {
return svg.category.some(
(categoryItem) => categoryItem.toLowerCase() === category.toLowerCase(),
);
} else {
return svg.category.toLowerCase() === category.toLowerCase();
}
});
interface GetSvgImgUrl {
url: string | ThemeOptions;
isDark: boolean;
}
export const getSvgImgUrl = ({ url, isDark }: GetSvgImgUrl) => {
if (typeof url === "string") return url;
return isDark ? url.dark : url.light;
};
+144 -22
View File
@@ -84,6 +84,12 @@ export const svgs: iSVG[] = [
route: "/library/amazon-q.svg",
url: "https://aws.amazon.com/q",
},
{
title: "Mulesoft",
category: "Software",
route: "/library/mulesoft.svg",
url: "https://www.mulesoft.com/",
},
{
title: "UV",
category: "Devtool",
@@ -103,7 +109,6 @@ export const svgs: iSVG[] = [
},
url: "https://milanote.com",
},
{
title: "Together AI",
category: "AI",
@@ -260,9 +265,68 @@ export const svgs: iSVG[] = [
},
{
title: "Nuxt",
category: "Framework",
category: ["Framework", "Nuxt"],
route: "/library/nuxt.svg",
url: "https://nuxtjs.org/",
url: "https://nuxt.com/",
wordmark: {
light: "/library/nuxt-wordmark-light.svg",
dark: "/library/nuxt-wordmark-dark.svg",
},
brandUrl: "https://nuxt.com/design-kit",
},
{
title: "Nuxt UI",
category: ["Library", "Nuxt"],
url: "https://ui.nuxt.com/",
route: {
light: "/library/nuxt-ui-wordmark-light.svg",
dark: "/library/nuxt-ui-wordmark-dark.svg",
},
brandUrl: "https://nuxt.com/design-kit",
},
{
title: "Nuxt Content",
category: ["Library", "Nuxt"],
url: "https://content.nuxt.com/",
route: {
light: "/library/nuxt-content-wordmark-light.svg",
dark: "/library/nuxt-content-wordmark-dark.svg",
},
brandUrl: "https://nuxt.com/design-kit",
},
{
title: "Nuxt Studio",
category: ["Library", "Nuxt"],
url: "https://studio.nuxt.com/",
route: {
light: "/library/nuxt-studio-wordmark-light.svg",
dark: "/library/nuxt-studio-wordmark-dark.svg",
},
brandUrl: "https://nuxt.com/design-kit",
},
{
title: "NuxtHub",
category: ["Library", "Nuxt"],
url: "https://hub.nuxt.com/",
route: "/library/nuxthub.svg",
wordmark: {
light: "/library/nuxthub-wordmark-light.svg",
dark: "/library/nuxthub-wordmark-dark.svg",
},
brandUrl: "https://nuxt.com/design-kit",
},
{
title: "Docus",
category: ["Software", "Nuxt"],
url: "https://docus.dev/",
route: {
light: "/library/docus-light.svg",
dark: "/library/docus-dark.svg",
},
wordmark: {
light: "/library/docus-wordmark-light.svg",
dark: "/library/docus-wordmark-dark.svg",
},
brandUrl: "https://nuxt.com/design-kit",
},
{
@@ -2323,12 +2387,6 @@ export const svgs: iSVG[] = [
url: "https://www.webflow.com",
brandUrl: "https://brand-at.webflow.io/resources",
},
{
title: "Sanity",
category: "CMS",
route: "/library/sanity.svg",
url: "https://www.sanity.io",
},
{
title: "sky",
category: "Entertainment",
@@ -3596,19 +3654,6 @@ export const svgs: iSVG[] = [
brandUrl:
"https://www.figma.com/file/YYn36CxVpcT6aPKDXIH9JG/CurseForge-Brandbook?type=design&node-id=0-1&t=dvC0gPtyP36PQdsi-0",
},
{
title: "Cursor",
category: ["Software"],
route: {
light: "/library/cursor_light.svg",
dark: "/library/cursor_dark.svg",
},
wordmark: {
light: "/library/cursor_wordmark_light.svg",
dark: "/library/cursor_wordmark_dark.svg",
},
url: "https://www.cursor.com",
},
{
title: "Ghostty",
category: ["Software"],
@@ -3659,6 +3704,12 @@ export const svgs: iSVG[] = [
route: "/library/firebase-studio.svg",
url: "https://firebase.studio/",
},
{
title: "Dingocoin",
category: "Crypto",
route: "/library/dingocoin.svg",
url: "https://dingocoin.com/",
},
{
title: "HeroUI",
category: "Library",
@@ -3778,6 +3829,15 @@ export const svgs: iSVG[] = [
route: "/library/google-cloud.svg",
url: "https://cloud.google.com/",
},
{
title: "Zulip",
category: ["Software", "Social"],
route: "/library/zulip.svg",
wordmark: "/library/zulip-wordmark.svg",
url: "https://zulip.com/",
brandUrl:
"https://github.com/zulip/zulip/tree/bd29fb3e2691daef570ba5661351922a16782dd2/static/images/logo",
},
{
title: "Effect TS",
category: "Library",
@@ -3807,6 +3867,17 @@ export const svgs: iSVG[] = [
route: "/library/eslint.svg",
url: "https://eslint.org/",
},
{
title: "Apache Kafka",
category: "Analytics",
route: "/library/apache-kafka-logo.svg",
wordmark: {
light: "/library/apache-kafka-wordmark-light.svg",
dark: "/library/apache-kafka-wordmark-dark.svg",
},
url: "https://kafka.apache.org/",
brandUrl: "https://kafka.apache.org/brand",
},
{
title: "PlainSignal",
category: "Analytics",
@@ -3865,4 +3936,55 @@ export const svgs: iSVG[] = [
url: "https://ahrefs.com/",
brandUrl: "https://ahrefs.com/logo",
},
{
title: "Google Maps",
category: "Google",
route: "/library/googleMaps.svg",
url: "https://www.google.com/maps/",
},
{
title: "WebGL",
category: "Library",
route: {
light: "/library/webgl.svg",
dark: "/library/webgl_dark.svg",
},
url: "https://www.khronos.org/webgl/",
brandUrl: "https://www.khronos.org/legal/trademarks/",
},
{
title: "Intello",
category: "Platform",
route: {
light: "/library/intello-dark.svg",
dark: "/library/intello-light.svg",
},
wordmark: {
light: "/library/intello_wordmark_dark.svg",
dark: "/library/intello_wordmark_light.svg",
},
url: "https://intelloai.com/",
},
{
title: "Kilo Code",
category: ["AI", "Devtool"],
route: {
light: "/library/kilocode-light.svg",
dark: "/library/kilocode-dark.svg",
},
url: "https://kilocode.ai/",
},
{
title: "Cursor",
category: ["Software"],
route: {
light: "/library/cursor_light.svg",
dark: "/library/cursor_dark.svg",
},
wordmark: {
light: "/library/cursor_wordmark_light.svg",
dark: "/library/cursor_wordmark_dark.svg",
},
url: "https://www.cursor.com",
},
];
+54 -51
View File
@@ -11,30 +11,25 @@ SVGL API is a RESTFul API that allows you to get all the information of the SVGs
The API is currently open to everyone and does not require any authentication. However, to prevent abusive use of the API, there is a limit to the number of requests.
Don't use the API for create the same product as SVGL. The API is intended to be used for extensions, plugins, or other tools that can help the community.
> Don't use the API for create the same product as SVGL. The API is intended to be used for extensions, plugins, or other tools that can help the community.
## Base URL
## Base URLs
The base URL for the API is:
SVGs URL:
```bash
https://api.svgl.app
# or
```
Categories URL:
```bash
https://api.svgl.app/categories
```
## Typescript usage
## Typescript
- For categories:
```ts
export interface Category {
category: string;
total: number;
}
```
- For SVGs:
You can use the following types for the SVG responses:
```ts
export type ThemeOptions = {
@@ -42,27 +37,27 @@ export type ThemeOptions = {
light: string;
};
export interface iSVG {
id?: number;
export interface SVG {
id: number;
title: string;
category: tCategory | tCategory[];
category: string | string[];
route: string | ThemeOptions;
url: string;
wordmark?: string | ThemeOptions;
brandUrl?: string;
url: string;
}
```
- `tCategory` is a large list of categories that can be found [here](https://github.com/pheralb/svgl/blob/main/src/types/categories.ts#L1).
> If you need types for the `category`, you can find them [here](https://github.com/pheralb/svgl/blob/main/src/types/categories.ts). Change the type of `category` to `Category | Category[]`.
## Endpoints
### Get all SVGs
```bash
https://api.svgl.app
```
<p></p>
```json
// Returns:
[
@@ -77,12 +72,12 @@ https://api.svgl.app
]
```
### Get all SVGs with limit
```bash
https://api.svgl.app?limit=10
```
<p></p>
```json
// Returns:
[
@@ -97,12 +92,12 @@ https://api.svgl.app?limit=10
]
```
### Get SVGs by category
```bash
https://api.svgl.app/category/software
```
<p></p>
```json
// Returns:
[
@@ -117,16 +112,24 @@ https://api.svgl.app/category/software
]
```
The list of categories is available [here](https://github.com/pheralb/svgl/blob/main/src/types/categories.ts) (except for the _all_ category).
> The list of categories is available [here](https://github.com/pheralb/svgl/blob/main/src/types/categories.ts).
### Get the SVG code
Optimized SVG using [svgo](https://github.com/svg/svgo):
```bash
https://api.svgl.app/svg/adobe.svg
```
<p></p>
No optimized SVG:
```bash
https://api.svgl.app/svg/adobe.svg?no-optimize
```
```html
// Returns:
<!-- Returns: -->
<svg
width="91"
height="80"
@@ -150,33 +153,12 @@ https://api.svgl.app/svg/adobe.svg
</svg>
```
```bash
https://api.svgl.app/categories
```
<p></p>
```json
// Returns:
[
{
"category": "Software",
"total": 97
},
{
"category": "Library",
"total": 25
},
...
]
```
### Search SVG by title
```bash
https://api.svgl.app?search=axiom
```
<p></p>
```json
// Returns:
[
@@ -192,3 +174,24 @@ https://api.svgl.app?search=axiom
}
]
```
### Get the list of categories
```bash
https://api.svgl.app/categories
```
```json
// Returns:
[
{
"category": "Software",
"total": 97
},
{
"category": "Library",
"total": 25
}
//...
]
```
+96
View File
@@ -0,0 +1,96 @@
---
title: shadcn/ui
description: How to use shadcn/ui to add SVGs to your project.
---
## shadcn/ui
SVGL v5 support [shadcn/ui](https://ui.shadcn.com/) registry 🎉, so you can easily add SVGs to your project using [their CLI](https://ui.shadcn.com/docs/cli). Add the registry config once and you will be able to install any SVG in **`.tsx`** using `npm`, `yarn`, `bun` or `pnpm`.
## Add registry
Add the SVGL registry to your `components.json` file:
```json
{
"registries": {
"@svgl": "https://svgl.app/r/{name}.json"
}
}
```
[shadcn/ui Namespaces](https://ui.shadcn.com/docs/registry/namespace) documentation.
## Usage
Add SVGs using the [shadcn/ui CLI](https://ui.shadcn.com/docs/cli/installation):
```bash
npx shadcn@latest add @svgl/sanity
# or
pnpm dlx shadcn@latest add @svgl/sanity
# or
yarn dlx shadcn@latest add @svgl/sanity
# or
bunx shadcn@latest add @svgl/sanity
```
Add multiple SVGs at once:
```bash
pnpm dlx shadcn@latest add @svgl/sanity @svgl/github @svgl/supabase @svgl/vercel
```
## MCP Server
You can use the [shadcn MCP server](https://ui.shadcn.com/docs/mcp) to browse, search, and add React SVGs from SVGL registry:
### Prerequisites
You need to have `@svgl` in your `components.json` file:
```json
{
"registries": {
"@svgl": "https://svgl.app/r/{name}.json"
}
}
```
### Quick Start
**With Claude Code**:
```bash
pnpm dlx shadcn@latest mcp init --client claude
```
Then, restart Claude Code. You can use `/mcp` command in Claude Code to debug the MCP server.
**With Cursor**:
```bash
pnpm dlx shadcn@latest mcp init --client cursor
```
Then, open Cursor Settings and Enable the MCP server for shadcn.
**With VSCode**:
```bash
pnpm dlx shadcn@latest mcp init --client vscode
```
Then, open `.vscode/mcp.json` and click Start next to the shadcn server.
### Example Prompts
Here are some example prompts you can use to add SVGs from SVGL registry:
```
Can you add the "GitHub" SVG from SVGL registry?
```
```
Please add React, Svelte and Vue SVGs from SVGL registry.
```
+8 -2
View File
@@ -1,8 +1,14 @@
export const globals = {
githubUrl: "https://github.com/pheralb/svgl",
apiGithubUrl: "https://api.github.com/repos/pheralb/svgl",
apiGithub: {
url: "https://ungh.cc/repos/pheralb/svgl",
fallback: 5000,
},
twitterUrl: "https://x.com/pheralb_",
submitUrl:
"https://github.com/pheralb/svgl?tab=readme-ov-file#-getting-started",
currentVersion: "beta",
requestSvgUrl:
"https://github.com/pheralb/svgl/issues/new?template=request-svg.yml",
registryUrl: "https://svgl.app/r/",
v0Url: "https://v0.dev/chat/api/open?url=",
};
+39
View File
@@ -0,0 +1,39 @@
import GithubSlugger from "github-slugger";
type ToCItem = {
id: number;
level: number;
text: string;
slug: string;
};
const getTableOfContents = (markdown: string): ToCItem[] => {
const slugger = new GithubSlugger();
const regXHeader = /(?:^|\n)(?<flag>#+)\s+(?<content>.+)/g;
// Delete # from code blocks and inline code:
let clean = markdown.replace(/<pre[\s\S]*?<\/pre>/gi, "");
clean = clean.replace(/<code[\s\S]*?<\/code>/gi, "");
return Array.from(clean.matchAll(regXHeader))
.map((match, idx): ToCItem | null => {
const groups = match.groups;
if (
groups &&
typeof groups.flag === "string" &&
typeof groups.content === "string" &&
groups.flag.length > 1
) {
return {
id: idx,
level: groups.flag.length,
text: groups.content,
slug: slugger.slug(groups.content),
};
}
return null;
})
.filter((x): x is ToCItem => x !== null);
};
export { getTableOfContents, type ToCItem };
+126
View File
@@ -0,0 +1,126 @@
import type { UnistNode, UnistTree } from "@/types/unist";
import { visit } from "unist-util-visit";
import { cn } from "@/utils/cn";
export const rehypeCopyBtn = () => {
return (tree: UnistTree) => {
visit(tree, "element", (node: UnistNode, index, parent) => {
if (node.tagName === "pre" && parent && typeof index === "number") {
const iconSize = 14;
const copyIcon = {
type: "element",
tagName: "svg",
properties: {
xmlns: "http://www.w3.org/2000/svg",
width: iconSize,
height: iconSize,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
style: "display: inline;",
},
children: [
{
type: "element",
tagName: "rect",
properties: {
width: iconSize,
height: iconSize,
x: "8",
y: "8",
rx: "2",
ry: "2",
},
children: [],
},
{
type: "element",
tagName: "path",
properties: {
d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",
},
children: [],
},
],
};
const successIcon = {
type: "element",
tagName: "svg",
properties: {
xmlns: "http://www.w3.org/2000/svg",
width: iconSize,
height: iconSize,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
class: "hidden",
},
children: [
{
type: "element",
tagName: "path",
properties: {
d: "M18 6 7 17l-5-5",
},
children: [],
},
{
type: "element",
tagName: "path",
properties: {
d: "m22 10-7.5 7.5L13 16",
},
children: [],
},
],
};
const copyButton = {
type: "element",
tagName: "button",
title: "Copy code to clipboard",
"aria-label": "Copy code to clipboard",
properties: {
type: "button",
title: "Copy code to clipboard",
class: cn(
"cursor-pointer z-40 absolute top-[1px] right-[1px] px-1.5 py-0.5 rounded-bl-md",
"border-b border-l border-neutral-200 dark:border-neutral-800",
"transition-colors text-neutral-500 dark:text-neutral-400 hover:text-black dark:hover:text-white",
),
onclick: `
const button = this;
const copyIcon = button.querySelector('svg:first-child');
const successIcon = button.querySelector('svg:last-child');
const codeBlock = button.nextElementSibling;
navigator.clipboard.writeText(codeBlock.innerText).then(() => {
copyIcon.style.display = 'none';
successIcon.style.display = 'inline';
setTimeout(() => {
copyIcon.style.display = 'inline';
successIcon.style.display = 'none';
}, 2000);
}).catch((err) => {
console.error('Error copying:', err);
});
`,
},
children: [copyIcon, successIcon],
};
const wrapper = {
type: "element",
tagName: "div",
properties: { class: "relative" },
children: [copyButton, node],
};
parent.children[index] = wrapper;
}
});
};
};
+18
View File
@@ -0,0 +1,18 @@
import type { UnistNode, UnistTree } from "@/types/unist";
import { visit } from "unist-util-visit";
const APP_DOMAIN = "svgl.app";
export const rehypeExternalLinks = () => {
return (tree: UnistTree) => {
visit(tree, "element", (node: UnistNode) => {
if (node.tagName === "a" && node.properties?.href) {
const href = String(node.properties.href);
if (!href.includes(APP_DOMAIN)) {
node.properties.target = "_blank";
node.properties.rel = "noopener noreferrer";
}
}
});
};
};
-29
View File
@@ -1,29 +0,0 @@
import type { LayoutServerLoad } from "./$types";
import { globals } from "@/globals";
export const load: LayoutServerLoad = async ({ fetch, setHeaders }) => {
try {
const response = await fetch(globals.apiGithubUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// 1 day cache:
setHeaders({
"cache-control": "public, max-age=86400",
});
return {
stars: data.stargazers_count,
};
} catch (error) {
console.error("Error fetching GitHub data:", error);
return {
stars: 0,
error: "Failed to fetch repository data",
};
}
};
+4 -5
View File
@@ -10,15 +10,14 @@
// Providers:
import { ModeWatcher } from "mode-watcher";
import Sidebar from "@/components/layout/sidebar.svelte";
import ViewTransitions from "@/components/viewTransitions.svelte";
import Sonner from "@/components/ui/sonner/sonner.svelte";
// SSR Data:
let { data, children }: LayoutProps = $props();
let { children }: LayoutProps = $props();
</script>
<ModeWatcher />
<ViewTransitions />
<Header githubStars={data?.stars} />
<Sonner />
<Header />
<Sidebar>
{@render children?.()}
</Sidebar>
+76 -25
View File
@@ -1,10 +1,12 @@
<script lang="ts">
import type { iSVG } from "@/types/svg";
import type { PageProps } from "./$types";
import { browser } from "$app/environment";
import { cn } from "@/utils/cn";
import { deleteParam } from "@/utils/searchParams";
import { svgsData } from "@/data";
import { searchWithFuse } from "@/utils/searchWithFuse";
import { searchSvgsWithFuse } from "@/utils/searchWithFuse";
// Components:
import Grid from "@/components/grid.svelte";
@@ -12,11 +14,16 @@
import SvgCard from "@/components/svgs/svgCard.svelte";
import SortSvgs from "@/components/svgs/sortSvgs.svelte";
import Container from "@/components/container.svelte";
import SearchXIcon from "@lucide/svelte/icons/search-x";
import PageCard from "@/components/pageCard.svelte";
import PageHeader from "@/components/pageHeader.svelte";
import FolderIcon from "@lucide/svelte/icons/folder";
import FolderSearchIcon from "@lucide/svelte/icons/folder-search";
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
import PageHeader from "@/components/pageHeader.svelte";
import Button from "@/components/ui/button/button.svelte";
import SvgNotFound from "@/components/svgs/svgNotFound.svelte";
import WarningMessage from "@/components/warningMessage.svelte";
// SSR Data:
let { data }: PageProps = $props();
@@ -41,17 +48,10 @@
updateDisplaySvgs();
return;
}
if (searchTerm.length < 3) {
filteredSvgs = (sorted ? alphabeticallySorted : latestSorted).filter(
(svg: iSVG) =>
svg.title.toLowerCase().includes(searchTerm.toLowerCase()),
);
} else {
filteredSvgs = searchWithFuse(filteredSvgs)
.search(searchTerm)
.map((result) => result.item);
}
const baseData = sorted ? alphabeticallySorted : latestSorted;
filteredSvgs = searchSvgsWithFuse(baseData)
.search(searchTerm)
.map((result) => result.item);
updateDisplaySvgs();
};
@@ -60,6 +60,13 @@
searchSvgs();
};
const handleClearSearch = () => {
searchTerm = "";
filteredSvgs = sorted ? alphabeticallySorted : latestSorted;
deleteParam("search");
updateDisplaySvgs();
};
$effect(() => {
updateDisplaySvgs();
});
@@ -75,7 +82,10 @@
placeholder="Search..."
/>
<PageCard>
<PageCard
containerClass="mt-2"
contentCardClass="max-h-[calc(100vh-7.6rem)] min-h-[calc(100vh-7.6rem)]"
>
<PageHeader>
<div
class="flex items-center space-x-2 text-neutral-500 dark:text-neutral-400"
@@ -87,27 +97,68 @@
<span>logos</span>
</p>
{:else}
<FolderSearchIcon size={18} strokeWidth={1.5} />
<Button
title="Clear Search"
onclick={handleClearSearch}
variant="ghost"
size="icon"
>
<SearchXIcon size={18} strokeWidth={1.5} />
</Button>
<p>
<span class="font-mono">{filteredSvgs.length}</span>
<span>logos</span>
</p>
{/if}
</div>
<SortSvgs
className={cn(filteredSvgs.length === 0 && "hidden")}
isSorted={sorted}
onSortedChange={(value) => {
sorted = value;
searchSvgs();
}}
/>
<div class="flex items-center space-x-2">
<SortSvgs
className={cn(filteredSvgs.length === 0 && "hidden")}
isSorted={sorted}
onSortedChange={(value) => {
sorted = value;
searchSvgs();
}}
/>
{#if showAll && filteredSvgs.length > maxDisplay}
<Button
variant="ghost"
class="px-2.5"
onclick={() => (showAll = false)}
>
<span>Show Less</span>
<ChevronUpIcon size={16} strokeWidth={2} />
</Button>
{/if}
</div>
</PageHeader>
{#if browser}
<WarningMessage />
{/if}
<Container className="my-6">
<Grid>
{#each displaySvgs as svg}
{#each displaySvgs as svg (svg.id)}
<SvgCard svgInfo={svg} />
{/each}
</Grid>
{#if !showAll && filteredSvgs.length > maxDisplay}
<div class="mt-6 flex justify-center">
<Button
variant="outline"
size="lg"
class="px-2.5"
onclick={() => (showAll = true)}
>
<span>Show All</span>
<span class="text-neutral-600 dark:text-neutral-400">
(+ {filteredSvgs.length - maxDisplay} SVGs)
</span>
<ChevronDownIcon size={16} strokeWidth={2} />
</Button>
</div>
{/if}
{#if filteredSvgs.length === 0}
<SvgNotFound svgTitle={searchTerm} />
{/if}
</Container>
</PageCard>
+5 -11
View File
@@ -2,7 +2,7 @@ import type { iSVG } from "@/types/svg";
import type { Load } from "@sveltejs/kit";
import { svgsData } from "@/data";
import { searchWithFuse } from "@/utils/searchWithFuse";
import { searchSvgsWithFuse } from "@/utils/searchWithFuse";
export const load: Load = ({ url }) => {
const searchParam = url.searchParams.get("search") || "";
@@ -17,16 +17,10 @@ export const load: Load = ({ url }) => {
if (!searchParam) {
filteredSvgs = sortParam ? alphabeticallySorted : latestSorted;
} else {
if (searchParam.length < 3) {
const baseData = sortParam ? alphabeticallySorted : latestSorted;
filteredSvgs = baseData.filter((svg: iSVG) =>
svg.title.toLowerCase().includes(searchParam.toLowerCase()),
);
} else {
filteredSvgs = searchWithFuse(filteredSvgs)
.search(searchParam)
.map((result) => result.item);
}
const baseData = sortParam ? alphabeticallySorted : latestSorted;
filteredSvgs = searchSvgsWithFuse(baseData)
.search(searchParam)
.map((result) => result.item);
}
return {
+15
View File
@@ -0,0 +1,15 @@
import type { RequestEvent } from "@sveltejs/kit";
import { error } from "@sveltejs/kit";
import { allDocs } from "content-collections";
export const GET = async ({ params }: RequestEvent) => {
const document = allDocs.find((doc) => doc._meta.path === params.slug);
if (!document) {
throw error(404, `Could not find ${params.slug}`);
}
return new Response(document.content, {
headers: {
"Content-Type": "text/markdown; charset=utf-8",
},
});
};
+7 -3
View File
@@ -12,14 +12,18 @@ export const POST = async ({ request }: RequestEvent) => {
try {
const body = await request.json();
const svgCode = body.code;
let svgCode = body.code;
const typescript = body.typescript;
const name = body.name.replace(/[^a-zA-Z0-9]/g, "");
const optimizedSvg = optimizeSvg({ svgCode });
const shouldOptimize = body.optimize !== false;
if (shouldOptimize) {
svgCode = optimizeSvg({ svgCode });
}
const code = await parseReactSvgContent({
componentName: name,
svgCode: optimizedSvg,
svgCode: svgCode,
typescript,
});
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from "@sveltejs/kit";
export const load = async () => {
return redirect(307, "/");
};
+62 -51
View File
@@ -2,113 +2,113 @@
import type { iSVG } from "@/types/svg";
import type { PageProps } from "./$types";
import { page } from "$app/state";
import { goto } from "$app/navigation";
import { SvelteURLSearchParams } from "svelte/reactivity";
import { cn } from "@/utils/cn";
import { searchWithFuse } from "@/utils/searchWithFuse";
import { searchSvgsWithFuse } from "@/utils/searchWithFuse";
// Components:
import Grid from "@/components/grid.svelte";
import Search from "@/components/search.svelte";
import SvgCard from "@/components/svgs/svgCard.svelte";
import Container from "@/components/container.svelte";
import SearchXIcon from "@lucide/svelte/icons/search-x";
import PageCard from "@/components/pageCard.svelte";
import PageHeader from "@/components/pageHeader.svelte";
import FolderIcon from "@lucide/svelte/icons/folder-open";
import ArrowLeftIcon from "@lucide/svelte/icons/arrow-left";
import { buttonVariants } from "@/components/ui/button";
import { Button, buttonVariants } from "@/components/ui/button";
import SortSvgs from "@/components/svgs/sortSvgs.svelte";
import { deleteParam } from "@/utils/searchParams";
import SvgNotFound from "@/components/svgs/svgNotFound.svelte";
// SSR Data:
let { data }: PageProps = $props();
const directoryData = $derived(data);
// States:
let maxDisplay = 30;
let searchTerm = $state<string>(data.searchTerm || "");
let filteredSvgs = $state<iSVG[]>(data.filteredSvgs);
let filteredSvgs = $derived<iSVG[]>(data.initialSvgs);
let sorted = $state<boolean>(data.sorted);
let showAll = $state<boolean>(false);
const updateDisplaySvgs = () => {
const data = showAll ? filteredSvgs : filteredSvgs.slice(0, maxDisplay);
return data;
};
const searchSvgs = () => {
if (!searchTerm) {
filteredSvgs = data.svgs;
filteredSvgs = sorted ? data.alphabeticallySorted : data.latestSorted;
updateDisplaySvgs();
return;
}
if (searchTerm.length < 3) {
filteredSvgs = data.svgs.filter((svg: iSVG) =>
svg.title.toLowerCase().includes(searchTerm.toLowerCase()),
);
} else {
filteredSvgs = searchWithFuse(data.svgs)
.search(searchTerm)
.map((result) => result.item);
}
const baseData = sorted ? data.alphabeticallySorted : data.latestSorted;
filteredSvgs = searchSvgsWithFuse(baseData)
.search(searchTerm)
.map((result) => result.item);
updateDisplaySvgs();
};
const handleSearch = (value: string) => {
searchTerm = value;
const params = new SvelteURLSearchParams(page.url.searchParams);
if (value) {
params.set("search", value);
} else {
params.delete("search");
}
goto(`?${params.toString()}`, {
keepFocus: true,
noScroll: true,
replaceState: true,
});
searchSvgs();
};
const formatCategory = (category: string) =>
category.charAt(0).toUpperCase() + category.slice(1);
const handleClearSearch = () => {
searchTerm = "";
deleteParam("search");
updateDisplaySvgs();
};
$effect(() => {
filteredSvgs = data.svgs.filter((svg: iSVG) =>
svg.title.toLowerCase().includes(searchTerm.toLowerCase()),
);
updateDisplaySvgs();
});
</script>
<svelte:head>
<title>{formatCategory(directoryData.category)} SVG logos - Svgl</title>
<title>{directoryData.category} SVG logos - Svgl</title>
</svelte:head>
<Search
searchValue={searchTerm}
onSearch={handleSearch}
placeholder="Search..."
placeholder={`Search ${directoryData.category}'s SVGs...`}
/>
<PageCard>
<PageCard
containerClass="mt-2"
contentCardClass="max-h-[calc(100vh-7.6rem)] min-h-[calc(100vh-7.6rem)]"
>
<PageHeader>
<div
class="flex items-center space-x-2 font-medium text-neutral-950 dark:text-neutral-50"
>
<a
href="/"
class={cn(
buttonVariants({ class: "group", variant: "ghost", size: "icon" }),
)}
class={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
>
<ArrowLeftIcon
size={18}
strokeWidth={1.5}
class="transition-transform group-hover:translate-x-[-2px]"
/>
<ArrowLeftIcon size={18} strokeWidth={1.5} />
</a>
<FolderIcon size={18} strokeWidth={1.5} />
{#if searchTerm}
<Button
title="Clear Search"
onclick={handleClearSearch}
variant="ghost"
size="icon"
>
<SearchXIcon size={18} strokeWidth={1.5} />
</Button>
{:else}
<FolderIcon class="ml-1" size={18} strokeWidth={1.5} />
{/if}
<p>
{formatCategory(directoryData.category)}
{directoryData.category}
</p>
<span>-</span>
{#if !searchTerm}
<p>
<span>{data.svgs.length} SVGs </span>
<span>{data.initialSvgs.length} SVGs </span>
</p>
{:else}
<p>
@@ -117,12 +117,23 @@
</p>
{/if}
</div>
<SortSvgs
className={cn(filteredSvgs.length === 0 && "hidden")}
isSorted={sorted}
onSortedChange={(value) => {
sorted = value;
searchSvgs();
}}
/>
</PageHeader>
<Container className="my-6">
<Grid>
{#each filteredSvgs as svg}
{#each filteredSvgs as svg (svg.id)}
<SvgCard svgInfo={svg} />
{/each}
</Grid>
{#if filteredSvgs.length === 0}
<SvgNotFound svgTitle={searchTerm} category={directoryData.category} />
{/if}
</Container>
</PageCard>
+20 -25
View File
@@ -1,48 +1,43 @@
import type { PageLoad } from "./$types";
import type { iSVG } from "@/types/svg";
import { svgs } from "@/data/svgs";
import { error } from "@sveltejs/kit";
import { searchWithFuse } from "@/utils/searchWithFuse";
import { getSvgsByCategory } from "@/data";
import { searchSvgsWithFuse } from "@/utils/searchWithFuse";
export const load: PageLoad = (async ({ params, url }) => {
const { category } = params;
const searchParam = url.searchParams.get("search") || "";
const sortParam = url.searchParams.get("sort") === "alphabetical";
const svgsByCategory = svgs.filter((svg: iSVG) => {
if (Array.isArray(svg.category)) {
return svg.category.some(
(categoryItem) => categoryItem.toLowerCase() === category.toLowerCase(),
);
} else {
return svg.category.toLowerCase() === category.toLowerCase();
}
});
const svgsByCategory = getSvgsByCategory(category);
if (svgsByCategory.length === 0) {
if (!svgsByCategory.length) {
throw error(404, "Category not found");
}
let filteredSvgs: iSVG[] = [];
const latestSorted = [...svgsByCategory].sort((a, b) => b.id! - a.id!);
const alphabeticallySorted = [...svgsByCategory].sort((a, b) =>
a.title.localeCompare(b.title),
);
const formatCategory = category.charAt(0).toUpperCase() + category.slice(1);
if (!searchParam) {
filteredSvgs = svgsByCategory;
filteredSvgs = sortParam ? alphabeticallySorted : latestSorted;
} else {
if (searchParam.length < 3) {
filteredSvgs = svgsByCategory.filter((svg: iSVG) =>
svg.title.toLowerCase().includes(searchParam.toLowerCase()),
);
} else {
filteredSvgs = searchWithFuse(svgsByCategory)
.search(searchParam)
.map((result) => result.item);
}
const baseData = sortParam ? alphabeticallySorted : latestSorted;
filteredSvgs = searchSvgsWithFuse(baseData)
.search(searchParam)
.map((result) => result.item);
}
return {
category: category,
category: formatCategory,
searchTerm: searchParam,
svgs: svgsByCategory,
filteredSvgs: filteredSvgs,
sorted: sortParam,
initialSvgs: filteredSvgs,
latestSorted,
alphabeticallySorted,
};
}) satisfies PageLoad;
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from "@sveltejs/kit";
export const load = async () => {
return redirect(307, "/");
};

Some files were not shown because too many files have changed in this diff Show More