mirror of
https://github.com/pheralb/svgl.git
synced 2025-12-29 08:01:36 +08:00
Compare commits
117 Commits
55199765be
...
v5.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 93aedefe79 | |||
| 63b189bce3 | |||
| bf855d15e0 | |||
| 9cf1677e51 | |||
| 49f860f25f | |||
| b7d05a0a56 | |||
| 138b5293ed | |||
| 195fae38e7 | |||
| 32a6eb4f4f | |||
| c33ef02f36 | |||
| 559eb676d9 | |||
| 45de1a631b | |||
| 60314a9648 | |||
| 26f6bb9061 | |||
| 83ec150266 | |||
| 1e0bb95493 | |||
| 01e0b7c66b | |||
| f505eea909 | |||
| e5d130b0c5 | |||
| c306b57ce7 | |||
| 1bf55e6c6e | |||
| aae0771f18 | |||
| f749358b97 | |||
| 7c2ae97dd9 | |||
| 50677ca3db | |||
| bf90439c63 | |||
| 44fd09efb2 | |||
| 3e507cf7c8 | |||
| 8583871f73 | |||
| 0c78255847 | |||
| 8a61650e4b | |||
| 4bd69b5ede | |||
| aef80d6b7d | |||
| 452d121ac4 | |||
| 894aca0d14 | |||
| 11df0d13bd | |||
| e35bb01927 | |||
| 5f5b3687c8 | |||
| 7f5c374d02 | |||
| b354a61eba | |||
| 5d45c720b4 | |||
| aeeaacd993 | |||
| 077df6f0d5 | |||
| 9b9124b220 | |||
| c7e86dd0f4 | |||
| 25ce756481 | |||
| 1da589f79d | |||
| 56125d2844 | |||
| 0b93f9b613 | |||
| 12038062db | |||
| 2198058131 | |||
| 0f2f026803 | |||
| 789fc0ce72 | |||
| 5bc3616dec | |||
| f411ffef8a | |||
| 3b3d30cd0c | |||
| 8e3ea5150a | |||
| 895052ff7a | |||
| fdb9f91d2a | |||
| 6aac1ebb6c | |||
| 3497a6e1e9 | |||
| f2a61200b1 | |||
| 1c11725e01 | |||
| e7bbd32b56 | |||
| 70ad2e0088 | |||
| 907c2b9892 | |||
| eb548fedc0 | |||
| 7d3cd750de | |||
| 3f99f8a115 | |||
| 380b10690e | |||
| c39c2a2f5b | |||
| 2130d8d316 | |||
| 65651faa5b | |||
| a05e849ddb | |||
| 5c88b29387 | |||
| 31f2cefaba | |||
| de47f2fa03 | |||
| 6f7d3c51f9 | |||
| 074e9231d1 | |||
| edb9ece0e3 | |||
| 3349654f79 | |||
| ada05ad75b | |||
| 5b27f543fa | |||
| 6d031bc995 | |||
| ea4b598f2f | |||
| fff76243ec | |||
| 9825fc2544 | |||
| 04ffbe9bbb | |||
| bc831bed17 | |||
| d3e92602c1 | |||
| 3a11cd3d31 | |||
| e465cd7a51 | |||
| f45b2d9d27 | |||
| 9db8deed28 | |||
| 1832eaceca | |||
| 32558885e6 | |||
| 6ffc890a15 | |||
| aa87b899a6 | |||
| 26f23f7e5b | |||
| 74e42b00dc | |||
| 8e27a8053d | |||
| 1aadeb5604 | |||
| ec6db6d23b | |||
| 803e13001a | |||
| 77356d3215 | |||
| 1591ea3146 | |||
| 2a38b834c3 | |||
| e6d441e9f2 | |||
| bc34bdc904 | |||
| 2692c7d34d | |||
| 82d4967e13 | |||
| 3943e624da | |||
| a1d61d73fa | |||
| 5e2a5e4d50 | |||
| 7b5e1d99da | |||
| 4347dc52c3 | |||
| 144fa7d7c4 |
@@ -37,10 +37,8 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
- name: Install utility dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
working-directory: ./utils/check-size
|
|
||||||
|
|
||||||
- name: Check svgs size
|
- name: Check SVGs size
|
||||||
run: pnpm start
|
run: pnpm check:size
|
||||||
working-directory: ./utils/check-size
|
|
||||||
|
|||||||
@@ -12,10 +12,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup pnpm 10
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
|
||||||
version: 10
|
|
||||||
|
|
||||||
- name: Install global dependencies
|
- name: Install global dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|||||||
+1
-2
@@ -15,7 +15,7 @@ RUN pnpm install --frozen-lockfile
|
|||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV PUBLIC_SVGL_VERSION=beta
|
RUN pnpm run check:size
|
||||||
RUN pnpm run build:prod
|
RUN pnpm run build:prod
|
||||||
|
|
||||||
# Production image
|
# Production image
|
||||||
@@ -29,7 +29,6 @@ COPY package.json ./
|
|||||||
|
|
||||||
# Set production environment
|
# Set production environment
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV PUBLIC_SVGL_VERSION=beta
|
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://svgl.app">
|
<a href="https://svgl.app">
|
||||||
<img src="static/images/readme.png">
|
<img src="static/images/screenshot_dark.png">
|
||||||
</a>
|
</a>
|
||||||
<p></p>
|
<p></p>
|
||||||
</div>
|
</div>
|
||||||
@@ -10,25 +10,29 @@
|
|||||||
Explore
|
Explore
|
||||||
</a>
|
</a>
|
||||||
<span> ✦ </span>
|
<span> ✦ </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+">
|
<a href="https://github.com/sponsors/pheralb">
|
||||||
Request logo
|
Sponsor this project
|
||||||
</a>
|
</a>
|
||||||
<span> ✦ </span>
|
<span> ✦ </span>
|
||||||
<a href="#-getting-started">
|
<a href="#-getting-started">
|
||||||
Submit logo
|
Getting Started
|
||||||
</a>
|
</a>
|
||||||
<span> ✦ </span>
|
<span> ✦ </span>
|
||||||
<a href="#-extensions">
|
<a href="https://svgl.app/extensions" target="_blank">
|
||||||
Extensions
|
Extensions
|
||||||
</a>
|
</a>
|
||||||
<span> ✦ </span>
|
<span> ✦ </span>
|
||||||
<a href="https://svgl.app/api">
|
<a href="#️-stack">
|
||||||
API
|
Stack
|
||||||
</a>
|
</a>
|
||||||
<span> ✦ </span>
|
<span> ✦ </span>
|
||||||
<a href="#%EF%B8%8F-contributing">
|
<a href="#️-contributing">
|
||||||
Contributing
|
Contributing
|
||||||
</a>
|
</a>
|
||||||
|
<span> ✦ </span>
|
||||||
|
<a href="#️-license">
|
||||||
|
License
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
@@ -48,11 +52,12 @@
|
|||||||
|
|
||||||
## 📦 Extensions
|
## 📦 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 |
|
| | 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/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/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/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/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--) |
|
||||||
@@ -62,36 +67,36 @@ A list of extensions that use the [svgl API](https://svgl.app/api), created by t
|
|||||||
| <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://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://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="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/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](https://github.com/abo3skr2019/SVGl-plugin) |
|
| <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
|
## 🛠️ 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.
|
- [**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.
|
- [**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.
|
- [**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.
|
- [**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.
|
- [**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.
|
- [**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.
|
- [**@upstash/redis** + **@upstash/ratelimit**](https://upstash.com/) - Serverless Redis for developers.
|
||||||
- [**Vitest**](https://vitest.dev/) - Blazing Fast Unit Test Framework.
|
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!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:
|
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/).
|
- [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
|
```bash
|
||||||
git clone git@github.com:your_username/svgl.git
|
git clone git@github.com:your_username/svgl.git
|
||||||
@@ -128,7 +133,7 @@ pnpm install
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Logo + wordmark** version:
|
- **Simple logo + wordmark**:
|
||||||
|
|
||||||
```ts
|
```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
|
```ts
|
||||||
{
|
{
|
||||||
@@ -151,14 +184,14 @@ pnpm install
|
|||||||
dark: '/library/your_logo_dark.svg'
|
dark: '/library/your_logo_dark.svg'
|
||||||
},
|
},
|
||||||
wordmark: {
|
wordmark: {
|
||||||
light: '/library/your_wordmark-logo_light.svg',
|
light: '/library/your_logo_wordmark_light.svg',
|
||||||
dark: '/library/your_wordmark-logo_dark.svg'
|
dark: '/library/your_logo_wordmark_dark.svg'
|
||||||
},
|
},
|
||||||
url: 'Website'
|
url: 'Website'
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Add brand guidelines**:
|
- **Add brand guidelines** (where to find the images, how to use it, colors, fonts...):
|
||||||
|
|
||||||
```ts
|
```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.
|
> - 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']`.
|
> - 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 account](https://console.upstash.com/).
|
||||||
- [Create a Upstash Redis Database](https://upstash.com/docs/redis/overall/getstarted).
|
- [Create a Upstash Redis Database](https://upstash.com/docs/redis/overall/getstarted).
|
||||||
|
|
||||||
|
3. Run the development server:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
SVGL_API_REQUESTS = 1
|
pnpm dev
|
||||||
UPSTASH_REDIS_URL = ""
|
|
||||||
UPSTASH_REDIS_TOKEN = ""
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## ✌️ Contributing
|
## ✌️ Contributing
|
||||||
|
|||||||
@@ -11,10 +11,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@upstash/ratelimit": "2.0.6",
|
"@upstash/ratelimit": "2.0.6",
|
||||||
"hono": "4.8.12"
|
"@upstash/redis": "1.35.3",
|
||||||
|
"hono": "4.9.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "4.20250805.0",
|
"@cloudflare/workers-types": "4.20250906.0",
|
||||||
"wrangler": "4.28.0"
|
"wrangler": "4.34.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+74
-71
@@ -10,17 +10,20 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@upstash/ratelimit':
|
'@upstash/ratelimit':
|
||||||
specifier: 2.0.6
|
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:
|
hono:
|
||||||
specifier: 4.8.12
|
specifier: 4.9.6
|
||||||
version: 4.8.12
|
version: 4.9.6
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@cloudflare/workers-types':
|
'@cloudflare/workers-types':
|
||||||
specifier: 4.20250805.0
|
specifier: 4.20250906.0
|
||||||
version: 4.20250805.0
|
version: 4.20250906.0
|
||||||
wrangler:
|
wrangler:
|
||||||
specifier: 4.28.0
|
specifier: 4.34.0
|
||||||
version: 4.28.0(@cloudflare/workers-types@4.20250805.0)
|
version: 4.34.0(@cloudflare/workers-types@4.20250906.0)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -28,47 +31,47 @@ packages:
|
|||||||
resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==}
|
resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@cloudflare/unenv-preset@2.6.0':
|
'@cloudflare/unenv-preset@2.7.2':
|
||||||
resolution: {integrity: sha512-h7Txw0WbDuUbrvZwky6+x7ft+U/Gppfn/rWx6IdR+e9gjygozRJnV26Y2TOr3yrIFa6OsZqqR2lN+jWTrakHXg==}
|
resolution: {integrity: sha512-JY7Uf8GhWcbOMDZX8ke2czp9f9TijvJN4CpRBs3+WYN9U7jHpj3XaV+HHm78iHkAwTm/JeBHqyQNhq/PizynRA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
unenv: 2.0.0-rc.19
|
unenv: 2.0.0-rc.20
|
||||||
workerd: ^1.20250802.0
|
workerd: ^1.20250828.1
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
workerd:
|
workerd:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@cloudflare/workerd-darwin-64@1.20250803.0':
|
'@cloudflare/workerd-darwin-64@1.20250902.0':
|
||||||
resolution: {integrity: sha512-6QciMnJp1p3F1qUiN0LaLfmw7SuZA/gfUBOe8Ft81pw16JYZ3CyiqIKPJvc1SV8jgDx8r+gz/PRi1NwOMt329A==}
|
resolution: {integrity: sha512-mwC/YEtDUGfnjXdbW5Lya+bgODrpJ5RxxqpaTjtMJycqnjR0RZgVpOqISwGfBHIhseykU3ahPugM5t91XkBKTg==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@cloudflare/workerd-darwin-arm64@1.20250803.0':
|
'@cloudflare/workerd-darwin-arm64@1.20250902.0':
|
||||||
resolution: {integrity: sha512-DoIgghDowtqoNhL6OoN/F92SKtrk7mRQKc4YSs/Dst8IwFZq+pCShOlWfB0MXqHKPSoiz5xLSrUKR9H6gQMPvw==}
|
resolution: {integrity: sha512-5Wr6a5/ixoXuMPOvbprN8k9HhAHDBh8f7H5V4DN/Xb4ORoGkI9AbC5QPpYV0wa3Ncf+CRSGobdmZNyO24hRccA==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@cloudflare/workerd-linux-64@1.20250803.0':
|
'@cloudflare/workerd-linux-64@1.20250902.0':
|
||||||
resolution: {integrity: sha512-mYdz4vNWX3+PoqRjssepVQqgh42IBiSrl+wb7vbh7VVWUVzBnQKtW3G+UFiBF62hohCLexGIEi7L0cFfRlcKSQ==}
|
resolution: {integrity: sha512-1yJGt56VQBuG01nrhkRGoa1FGz7xQwJTrgewxt/MRRtigZTf84qJQiPQxyM7PQWCLREKa+JS7G8HFqvOwK7kZA==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@cloudflare/workerd-linux-arm64@1.20250803.0':
|
'@cloudflare/workerd-linux-arm64@1.20250902.0':
|
||||||
resolution: {integrity: sha512-RmrtUYLRUg6djKU7Z6yebS6YGJVnaDVY6bbXca+2s26vw4ibJDOTPLuBHFQF62Grw3fAfsNbjQh5i14vG2mqUg==}
|
resolution: {integrity: sha512-ArDodWzfo0BVqMQGUgaOGV5Mzf8wEMUX8TJonExpGbYavoVXVDbp2rTLFRJg1vkFGpmw1teCtSoOjSDisFZQMg==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@cloudflare/workerd-windows-64@1.20250803.0':
|
'@cloudflare/workerd-windows-64@1.20250902.0':
|
||||||
resolution: {integrity: sha512-uLV8gdudz36o9sUaAKbBxxTwZwLFz1KyW7QpBvOo4+r3Ib8yVKXGiySIMWGD7A0urSMrjf3e5LlLcJKgZUOjMA==}
|
resolution: {integrity: sha512-DT/o8ZSkmze1YGI7vgVt4ST+VYGb3tNChiFnOM9Z8YOejqKqbVvATB4gi/xMSnNR9CsKFqH4hHWDDtz+wf4uZg==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@cloudflare/workers-types@4.20250805.0':
|
'@cloudflare/workers-types@4.20250906.0':
|
||||||
resolution: {integrity: sha512-HOt0lqFiw5WzhvxH/IViMAWI/zwzokCSx33DlRnJqECT9khskK9X4Jrw/+IiAprJ5YloiFxK8Xn1oGbsabdUWg==}
|
resolution: {integrity: sha512-CMRTupQpAdNZJrxRGaM2JzxmpWOnzgxcyTGmjAOcosRfi1ZsNUTAZ0kj1dzY+4bPDIdFwvvJL3t91DEpqitOJg==}
|
||||||
|
|
||||||
'@cspotcode/source-map-support@0.8.1':
|
'@cspotcode/source-map-support@0.8.1':
|
||||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||||
@@ -367,8 +370,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@upstash/redis': ^1.34.3
|
'@upstash/redis': ^1.34.3
|
||||||
|
|
||||||
'@upstash/redis@1.34.0':
|
'@upstash/redis@1.35.3':
|
||||||
resolution: {integrity: sha512-TrXNoJLkysIl8SBc4u9bNnyoFYoILpCcFJcLyWCccb/QSUmaVKdvY0m5diZqc3btExsapcMbaw/s/wh9Sf1pJw==}
|
resolution: {integrity: sha512-hSjv66NOuahW3MisRGlSgoszU2uONAY2l5Qo3Sae8OT3/Tng9K+2/cBRuyPBX8egwEGcNNCF9+r0V6grNnhL+w==}
|
||||||
|
|
||||||
acorn-walk@8.3.2:
|
acorn-walk@8.3.2:
|
||||||
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
|
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
|
||||||
@@ -400,9 +403,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
crypto-js@4.2.0:
|
|
||||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
|
||||||
|
|
||||||
defu@6.1.4:
|
defu@6.1.4:
|
||||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||||
|
|
||||||
@@ -433,8 +433,8 @@ packages:
|
|||||||
glob-to-regexp@0.4.1:
|
glob-to-regexp@0.4.1:
|
||||||
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||||
|
|
||||||
hono@4.8.12:
|
hono@4.9.6:
|
||||||
resolution: {integrity: sha512-MQSKk1Mg7b74k8l+A025LfysnLtXDKkE4pLaSsYRQC5iy85lgZnuyeQ1Wynair9mmECzoLu+FtJtqNZSoogBDQ==}
|
resolution: {integrity: sha512-doVjXhSFvYZ7y0dNokjwwSahcrAfdz+/BCLvAMa/vHLzjj8+CFyV5xteThGUsKdkaasgN+gF2mUxao+SGLpUeA==}
|
||||||
engines: {node: '>=16.9.0'}
|
engines: {node: '>=16.9.0'}
|
||||||
|
|
||||||
is-arrayish@0.3.2:
|
is-arrayish@0.3.2:
|
||||||
@@ -449,8 +449,8 @@ packages:
|
|||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
miniflare@4.20250803.0:
|
miniflare@4.20250902.0:
|
||||||
resolution: {integrity: sha512-1tmCLfmMw0SqRBF9PPII9CVLQRzOrO7uIBmSng8BMSmtgs2kos7OeoM0sg6KbR9FrvP/zAniLyZuCAMAjuu4fQ==}
|
resolution: {integrity: sha512-QHjI17yVDxDXsjDvX6GNRySx2uYsQJyiZ2MRBAsA0CFpAI2BcHd4oz0FIjbqgpZK+4Fhm7OKht/AfBNCd234Zg==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -489,24 +489,27 @@ packages:
|
|||||||
ufo@1.6.1:
|
ufo@1.6.1:
|
||||||
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
||||||
|
|
||||||
|
uncrypto@0.1.3:
|
||||||
|
resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
|
||||||
|
|
||||||
undici@7.13.0:
|
undici@7.13.0:
|
||||||
resolution: {integrity: sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA==}
|
resolution: {integrity: sha512-l+zSMssRqrzDcb3fjMkjjLGmuiiK2pMIcV++mJaAc9vhjSGpvM7h43QgP+OAMb1GImHmbPyG2tBXeuyG5iY4gA==}
|
||||||
engines: {node: '>=20.18.1'}
|
engines: {node: '>=20.18.1'}
|
||||||
|
|
||||||
unenv@2.0.0-rc.19:
|
unenv@2.0.0-rc.20:
|
||||||
resolution: {integrity: sha512-t/OMHBNAkknVCI7bVB9OWjUUAwhVv9vsPIAGnNUxnu3FxPQN11rjh0sksLMzc3g7IlTgvHmOTl4JM7JHpcv5wA==}
|
resolution: {integrity: sha512-8tn4tAl9vD5nWoggAAPz28vf0FY8+pQAayhU94qD+ZkIbVKCBAH/E1MWEEmhb9Whn5EgouYVfBJB20RsTLRDdg==}
|
||||||
|
|
||||||
workerd@1.20250803.0:
|
workerd@1.20250902.0:
|
||||||
resolution: {integrity: sha512-oYH29mE/wNolPc32NHHQbySaNorj6+KASUtOvQHySxB5mO1NWdGuNv49woxNCF5971UYceGQndY+OLT+24C3wQ==}
|
resolution: {integrity: sha512-rM+8ARYoy9gWJNPW89ERWyjbp7+m1hu6PFbehiP8FW9Hm5kNVo71lXFrkCP2HSsTP1OLfIU/IwanYOijJ0mQDw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
wrangler@4.28.0:
|
wrangler@4.34.0:
|
||||||
resolution: {integrity: sha512-y0yHIuScpok9oSErLqDbxkBChC2+/jZpvqMg2NxOto1JCyUtDUuKljOfcVMaI48d9GuhOCSoWSumYxLAHNxaLA==}
|
resolution: {integrity: sha512-iU+T8klWX6M/oN9y2PG8HrekoHwlBs/7wNMouyRToCJGn5EFtVl98a1fxxPCgkuUNZ2sKLrCyx/TlhgilIlqpQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@cloudflare/workers-types': ^4.20250803.0
|
'@cloudflare/workers-types': ^4.20250902.0
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
'@cloudflare/workers-types':
|
'@cloudflare/workers-types':
|
||||||
optional: true
|
optional: true
|
||||||
@@ -538,28 +541,28 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime: 3.0.0
|
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:
|
dependencies:
|
||||||
unenv: 2.0.0-rc.19
|
unenv: 2.0.0-rc.20
|
||||||
optionalDependencies:
|
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
|
optional: true
|
||||||
|
|
||||||
'@cloudflare/workerd-darwin-arm64@1.20250803.0':
|
'@cloudflare/workerd-darwin-arm64@1.20250902.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@cloudflare/workerd-linux-64@1.20250803.0':
|
'@cloudflare/workerd-linux-64@1.20250902.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@cloudflare/workerd-linux-arm64@1.20250803.0':
|
'@cloudflare/workerd-linux-arm64@1.20250902.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@cloudflare/workerd-windows-64@1.20250803.0':
|
'@cloudflare/workerd-windows-64@1.20250902.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@cloudflare/workers-types@4.20250805.0': {}
|
'@cloudflare/workers-types@4.20250906.0': {}
|
||||||
|
|
||||||
'@cspotcode/source-map-support@0.8.1':
|
'@cspotcode/source-map-support@0.8.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -747,16 +750,16 @@ snapshots:
|
|||||||
|
|
||||||
'@upstash/core-analytics@0.0.10':
|
'@upstash/core-analytics@0.0.10':
|
||||||
dependencies:
|
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:
|
dependencies:
|
||||||
'@upstash/core-analytics': 0.0.10
|
'@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:
|
dependencies:
|
||||||
crypto-js: 4.2.0
|
uncrypto: 0.1.3
|
||||||
|
|
||||||
acorn-walk@8.3.2: {}
|
acorn-walk@8.3.2: {}
|
||||||
|
|
||||||
@@ -782,8 +785,6 @@ snapshots:
|
|||||||
|
|
||||||
cookie@1.0.2: {}
|
cookie@1.0.2: {}
|
||||||
|
|
||||||
crypto-js@4.2.0: {}
|
|
||||||
|
|
||||||
defu@6.1.4: {}
|
defu@6.1.4: {}
|
||||||
|
|
||||||
detect-libc@2.0.3: {}
|
detect-libc@2.0.3: {}
|
||||||
@@ -827,7 +828,7 @@ snapshots:
|
|||||||
|
|
||||||
glob-to-regexp@0.4.1: {}
|
glob-to-regexp@0.4.1: {}
|
||||||
|
|
||||||
hono@4.8.12: {}
|
hono@4.9.6: {}
|
||||||
|
|
||||||
is-arrayish@0.3.2: {}
|
is-arrayish@0.3.2: {}
|
||||||
|
|
||||||
@@ -835,7 +836,7 @@ snapshots:
|
|||||||
|
|
||||||
mime@3.0.0: {}
|
mime@3.0.0: {}
|
||||||
|
|
||||||
miniflare@4.20250803.0:
|
miniflare@4.20250902.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cspotcode/source-map-support': 0.8.1
|
'@cspotcode/source-map-support': 0.8.1
|
||||||
acorn: 8.14.0
|
acorn: 8.14.0
|
||||||
@@ -845,7 +846,7 @@ snapshots:
|
|||||||
sharp: 0.33.5
|
sharp: 0.33.5
|
||||||
stoppable: 1.1.0
|
stoppable: 1.1.0
|
||||||
undici: 7.13.0
|
undici: 7.13.0
|
||||||
workerd: 1.20250803.0
|
workerd: 1.20250902.0
|
||||||
ws: 8.18.0
|
ws: 8.18.0
|
||||||
youch: 4.1.0-beta.10
|
youch: 4.1.0-beta.10
|
||||||
zod: 3.22.3
|
zod: 3.22.3
|
||||||
@@ -900,9 +901,11 @@ snapshots:
|
|||||||
|
|
||||||
ufo@1.6.1: {}
|
ufo@1.6.1: {}
|
||||||
|
|
||||||
|
uncrypto@0.1.3: {}
|
||||||
|
|
||||||
undici@7.13.0: {}
|
undici@7.13.0: {}
|
||||||
|
|
||||||
unenv@2.0.0-rc.19:
|
unenv@2.0.0-rc.20:
|
||||||
dependencies:
|
dependencies:
|
||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
exsolve: 1.0.7
|
exsolve: 1.0.7
|
||||||
@@ -910,26 +913,26 @@ snapshots:
|
|||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
ufo: 1.6.1
|
ufo: 1.6.1
|
||||||
|
|
||||||
workerd@1.20250803.0:
|
workerd@1.20250902.0:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@cloudflare/workerd-darwin-64': 1.20250803.0
|
'@cloudflare/workerd-darwin-64': 1.20250902.0
|
||||||
'@cloudflare/workerd-darwin-arm64': 1.20250803.0
|
'@cloudflare/workerd-darwin-arm64': 1.20250902.0
|
||||||
'@cloudflare/workerd-linux-64': 1.20250803.0
|
'@cloudflare/workerd-linux-64': 1.20250902.0
|
||||||
'@cloudflare/workerd-linux-arm64': 1.20250803.0
|
'@cloudflare/workerd-linux-arm64': 1.20250902.0
|
||||||
'@cloudflare/workerd-windows-64': 1.20250803.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:
|
dependencies:
|
||||||
'@cloudflare/kv-asset-handler': 0.4.0
|
'@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
|
blake3-wasm: 2.1.5
|
||||||
esbuild: 0.25.4
|
esbuild: 0.25.4
|
||||||
miniflare: 4.20250803.0
|
miniflare: 4.20250902.0
|
||||||
path-to-regexp: 6.3.0
|
path-to-regexp: 6.3.0
|
||||||
unenv: 2.0.0-rc.19
|
unenv: 2.0.0-rc.20
|
||||||
workerd: 1.20250803.0
|
workerd: 1.20250902.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@cloudflare/workers-types': 4.20250805.0
|
'@cloudflare/workers-types': 4.20250906.0
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
|
|||||||
+83
-59
@@ -1,19 +1,23 @@
|
|||||||
import { Context, Hono } from 'hono';
|
import type { Context } from "hono";
|
||||||
import { env } from 'hono/adapter';
|
import type { BlankInput, Env } from "hono/types";
|
||||||
import { cors } from 'hono/cors';
|
|
||||||
import { BlankInput, Env } from 'hono/types';
|
import type { iSVG } from "../../src/types/svg";
|
||||||
import { Ratelimit } from '@upstash/ratelimit';
|
import type { Category } from "../../src/types/categories";
|
||||||
import { Redis } from '@upstash/redis/cloudflare';
|
|
||||||
|
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 utils:
|
||||||
import { addFullUrl } from './utils';
|
import { addFullUrl } from "./utils";
|
||||||
|
import { optimizeSvg } from "../../src/utils/optimizeSvg";
|
||||||
|
|
||||||
// 📦 Import data from main app:
|
// 📦 Import data from SVGL src:
|
||||||
import { svgsData } from '../../src/data';
|
import { svgsData } from "../../src/data";
|
||||||
import { iSVG } from '../../src/types/svg';
|
|
||||||
import { tCategory } from '../../src/types/categories';
|
|
||||||
|
|
||||||
declare module 'hono' {
|
declare module "hono" {
|
||||||
interface ContextVariableMap {
|
interface ContextVariableMap {
|
||||||
ratelimit: Ratelimit;
|
ratelimit: Ratelimit;
|
||||||
}
|
}
|
||||||
@@ -24,7 +28,7 @@ const fullRouteSvgsData = svgsData.map((svg) => {
|
|||||||
return {
|
return {
|
||||||
...svg,
|
...svg,
|
||||||
route: addFullUrl(svg.route),
|
route: addFullUrl(svg.route),
|
||||||
wordmark: svg.wordmark ? addFullUrl(svg.wordmark) : undefined
|
wordmark: svg.wordmark ? addFullUrl(svg.wordmark) : undefined,
|
||||||
};
|
};
|
||||||
}) as iSVG[];
|
}) as iSVG[];
|
||||||
|
|
||||||
@@ -34,21 +38,24 @@ const cache = new Map();
|
|||||||
|
|
||||||
class RedisRateLimiter {
|
class RedisRateLimiter {
|
||||||
static instance: Ratelimit;
|
static instance: Ratelimit;
|
||||||
static getInstance(c: Context<Env, '/api/*', BlankInput>) {
|
static getInstance(c: Context<Env, "/api/*", BlankInput>) {
|
||||||
if (!this.instance) {
|
if (!this.instance) {
|
||||||
const { UPSTASH_REDIS_URL, UPSTASH_REDIS_TOKEN } = env<{
|
const { UPSTASH_REDIS_URL, UPSTASH_REDIS_TOKEN } = env<{
|
||||||
UPSTASH_REDIS_URL: string;
|
UPSTASH_REDIS_URL: string;
|
||||||
UPSTASH_REDIS_TOKEN: string;
|
UPSTASH_REDIS_TOKEN: string;
|
||||||
}>(c);
|
}>(c);
|
||||||
const cleanRedisUrl = UPSTASH_REDIS_URL.replace(/^['"]|['"]$/g, '').trim();
|
const cleanRedisUrl = UPSTASH_REDIS_URL.replace(
|
||||||
|
/^['"]|['"]$/g,
|
||||||
|
"",
|
||||||
|
).trim();
|
||||||
const redisClient = new Redis({
|
const redisClient = new Redis({
|
||||||
token: UPSTASH_REDIS_TOKEN,
|
token: UPSTASH_REDIS_TOKEN,
|
||||||
url: cleanRedisUrl
|
url: cleanRedisUrl,
|
||||||
});
|
});
|
||||||
const ratelimit = new Ratelimit({
|
const ratelimit = new Ratelimit({
|
||||||
redis: redisClient,
|
redis: redisClient,
|
||||||
limiter: Ratelimit.slidingWindow(5, '5 s'),
|
limiter: Ratelimit.slidingWindow(5, "5 s"),
|
||||||
ephemeralCache: cache
|
ephemeralCache: cache,
|
||||||
});
|
});
|
||||||
this.instance = ratelimit;
|
this.instance = ratelimit;
|
||||||
return this.instance;
|
return this.instance;
|
||||||
@@ -60,22 +67,22 @@ class RedisRateLimiter {
|
|||||||
|
|
||||||
app.use(async (c, next) => {
|
app.use(async (c, next) => {
|
||||||
const ratelimit = RedisRateLimiter.getInstance(c);
|
const ratelimit = RedisRateLimiter.getInstance(c);
|
||||||
c.set('ratelimit', ratelimit);
|
c.set("ratelimit", ratelimit);
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
// 🌱 GET: "/" - Returns all the SVGs data:
|
// 🌱 GET: "/" - Returns all the SVGs data:
|
||||||
app.get('/', async (c) => {
|
app.get("/", async (c) => {
|
||||||
const limit = c.req.query('limit');
|
const limit = c.req.query("limit");
|
||||||
const search = c.req.query('search');
|
const search = c.req.query("search");
|
||||||
const ratelimit = c.get('ratelimit');
|
const ratelimit = c.get("ratelimit");
|
||||||
const ip = c.req.raw.headers.get('CF-Connecting-IP');
|
const ip = c.req.raw.headers.get("CF-Connecting-IP");
|
||||||
const { success } = await ratelimit.limit(ip ?? 'anonymous');
|
const { success } = await ratelimit.limit(ip ?? "anonymous");
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return c.json({ error: '🛑 Too many request' }, 429);
|
return c.json({ error: "🛑 (SVGL - API) Too many request" }, 429);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (limit) {
|
if (limit) {
|
||||||
@@ -87,10 +94,10 @@ app.get('/', async (c) => {
|
|||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
const searchResults = fullRouteSvgsData.filter((svg) =>
|
const searchResults = fullRouteSvgsData.filter((svg) =>
|
||||||
svg.title.toLowerCase().includes(search.toLowerCase())
|
svg.title.toLowerCase().includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
if (searchResults.length === 0) {
|
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);
|
return c.json(searchResults);
|
||||||
}
|
}
|
||||||
@@ -99,19 +106,19 @@ app.get('/', async (c) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 🌱 GET: "/categories" - Return an array with categories:
|
// 🌱 GET: "/categories" - Return an array with categories:
|
||||||
app.get('/categories', async (c) => {
|
app.get("/categories", async (c) => {
|
||||||
const ratelimit = c.get('ratelimit');
|
const ratelimit = c.get("ratelimit");
|
||||||
const ip = c.req.raw.headers.get('CF-Connecting-IP');
|
const ip = c.req.raw.headers.get("CF-Connecting-IP");
|
||||||
const { success } = await ratelimit.limit(ip ?? 'anonymous');
|
const { success } = await ratelimit.limit(ip ?? "anonymous");
|
||||||
|
|
||||||
if (!success) {
|
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> = {};
|
const categoryTotals: Record<string, number> = {};
|
||||||
|
|
||||||
fullRouteSvgsData.forEach((svg) => {
|
fullRouteSvgsData.forEach((svg) => {
|
||||||
if (typeof svg.category === 'string') {
|
if (typeof svg.category === "string") {
|
||||||
categoryTotals[svg.category] = (categoryTotals[svg.category] || 0) + 1;
|
categoryTotals[svg.category] = (categoryTotals[svg.category] || 0) + 1;
|
||||||
} else if (Array.isArray(svg.category)) {
|
} else if (Array.isArray(svg.category)) {
|
||||||
svg.category.forEach((category) => {
|
svg.category.forEach((category) => {
|
||||||
@@ -120,62 +127,79 @@ app.get('/categories', async (c) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const categories = Object.entries(categoryTotals).map(([category, total]) => ({
|
const categories = Object.entries(categoryTotals).map(
|
||||||
|
([category, total]) => ({
|
||||||
category,
|
category,
|
||||||
total
|
total,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return c.json(categories);
|
return c.json(categories);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🌱 GET: /category/:category - Return an list of svgs by specific category:
|
// 🌱 GET: /category/:category - Return an list of svgs by specific category:
|
||||||
app.get('/category/:category', async (c) => {
|
app.get("/category/:category", async (c) => {
|
||||||
const category = c.req.param('category') as string;
|
const category = c.req.param("category") as string;
|
||||||
const targetCategory = category.charAt(0).toUpperCase() + category.slice(1);
|
const targeCategory = category.charAt(0).toUpperCase() + category.slice(1);
|
||||||
const ratelimit = c.get('ratelimit');
|
const ratelimit = c.get("ratelimit");
|
||||||
const ip = c.req.raw.headers.get('CF-Connecting-IP');
|
const ip = c.req.raw.headers.get("CF-Connecting-IP");
|
||||||
const { success } = await ratelimit.limit(ip ?? 'anonymous');
|
const { success } = await ratelimit.limit(ip ?? "anonymous");
|
||||||
|
|
||||||
if (!success) {
|
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) => {
|
const categorySvgs = fullRouteSvgsData.filter((svg) => {
|
||||||
if (typeof svg.category === 'string') {
|
if (typeof svg.category === "string") {
|
||||||
return svg.category === targetCategory;
|
return svg.category === targeCategory;
|
||||||
}
|
}
|
||||||
if (Array.isArray(svg.category)) {
|
if (Array.isArray(svg.category)) {
|
||||||
return svg.category.includes(targetCategory as tCategory);
|
return svg.category.includes(targeCategory as Category);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
if (categorySvgs.length === 0) {
|
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);
|
return c.json(categorySvgs);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 🌱 GET: "/svg/:filename" - Return the SVG file by filename:
|
// 🌱 GET: "/svg/:filename" - Return the SVG code file by filename:
|
||||||
app.get('/svg/:filename', async (c) => {
|
app.get("/svg/:filename", async (c) => {
|
||||||
const fileName = c.req.param('filename') as string;
|
const fileName = c.req.param("filename") as string;
|
||||||
const svgLibrary = 'https://svgl.app/library/';
|
const svgLibrary = "https://svgl.app/library/";
|
||||||
|
|
||||||
const ratelimit = c.get('ratelimit');
|
const ratelimit = c.get("ratelimit");
|
||||||
const ip = c.req.raw.headers.get('CF-Connecting-IP');
|
const returnNoOptimized = c.req.query("no-optimize");
|
||||||
const { success } = await ratelimit.limit(ip ?? 'anonymous');
|
const ip = c.req.raw.headers.get("CF-Connecting-IP");
|
||||||
|
const { success } = await ratelimit.limit(ip ?? "anonymous");
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
return c.json({ error: '🛑 Too many request' }, 429);
|
return c.json({ error: "🛑 (SVGL - API) Too many request" }, 429);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const svg = await fetch(`${svgLibrary}${fileName}`).then((res) => {
|
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 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) {
|
} catch (err) {
|
||||||
return c.json({ error: 'not found' }, 404);
|
return c.json(
|
||||||
|
{ error: `❌ (SVGL - API) SVG file not found - ${err}` },
|
||||||
|
404,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+28
-2
@@ -1,11 +1,20 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import path from "node:path";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
|
||||||
// Content Collections:
|
// Content Collections:
|
||||||
import { compileMarkdown } from "@content-collections/markdown";
|
import { compileMarkdown } from "@content-collections/markdown";
|
||||||
import { defineCollection, defineConfig } from "@content-collections/core";
|
import { defineCollection, defineConfig } from "@content-collections/core";
|
||||||
|
|
||||||
// Shiki:
|
// Plugins:
|
||||||
|
import rehypeSlug from "rehype-slug";
|
||||||
import rehypeShiki from "@shikijs/rehype/core";
|
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";
|
import { shikiHighlighter, rehypeShikiOptions } from "./src/utils/shiki";
|
||||||
|
|
||||||
const docs = defineCollection({
|
const docs = defineCollection({
|
||||||
@@ -18,12 +27,29 @@ const docs = defineCollection({
|
|||||||
}),
|
}),
|
||||||
transform: async (document, context) => {
|
transform: async (document, context) => {
|
||||||
const highlighter = await shikiHighlighter();
|
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, {
|
const html = await compileMarkdown(context, document, {
|
||||||
rehypePlugins: [[rehypeShiki, highlighter, rehypeShikiOptions]],
|
rehypePlugins: [
|
||||||
|
[rehypeShiki, highlighter, rehypeShikiOptions],
|
||||||
|
rehypeExternalLinks,
|
||||||
|
rehypeSlug,
|
||||||
|
rehypeAutolinkHeadings,
|
||||||
|
rehypeCopyBtn,
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
const tableOfContents = getTableOfContents(document.content);
|
||||||
return {
|
return {
|
||||||
...document,
|
...document,
|
||||||
html,
|
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}`,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import svelteConfig from "./svelte.config.js";
|
|||||||
// Ignore files:
|
// Ignore files:
|
||||||
const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url));
|
const gitignorePath = fileURLToPath(new URL("./.gitignore", import.meta.url));
|
||||||
|
|
||||||
export default ts.config(
|
export default [
|
||||||
includeIgnoreFile(gitignorePath),
|
includeIgnoreFile(gitignorePath),
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
...ts.configs.recommended,
|
...ts.configs.recommended,
|
||||||
@@ -24,9 +24,23 @@ export default ts.config(
|
|||||||
{
|
{
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: { ...globals.browser, ...globals.node },
|
globals: { ...globals.browser, ...globals.node },
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: fileURLToPath(new URL(".", import.meta.url)),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"no-undef": "off",
|
"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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -46,4 +60,4 @@ export default ts.config(
|
|||||||
"svelte/no-at-html-tags": "off",
|
"svelte/no-at-html-tags": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
];
|
||||||
+8
-1
@@ -32,13 +32,16 @@
|
|||||||
"prepare": "svelte-kit sync || echo ''",
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"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": "prettier --write \"src/**/*.{ts,js,md,svelte}\" --cache",
|
||||||
"format:check": "prettier --check \"src/**/*.{ts,js,md,svelte}\" --cache",
|
"format:check": "prettier --check \"src/**/*.{ts,js,md,svelte}\" --cache",
|
||||||
"lint": "eslint ./src",
|
"lint": "eslint ./src",
|
||||||
"lint:fix": "eslint ./src --fix",
|
"lint:fix": "eslint ./src --fix",
|
||||||
"build:shadcn": "shadcn build --output ./static/r",
|
"build:shadcn": "shadcn build --output ./static/r",
|
||||||
"build:prod": "pnpm build:registry && vite build",
|
"build:prod": "pnpm build:registry && vite build",
|
||||||
"build:registry": "tsx ./generate-registry.ts"
|
"build:registry": "tsx ./utils/generate-registry.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/langs": "3.12.0",
|
"@shikijs/langs": "3.12.0",
|
||||||
@@ -71,10 +74,13 @@
|
|||||||
"eslint": "9.33.0",
|
"eslint": "9.33.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-plugin-svelte": "3.11.0",
|
"eslint-plugin-svelte": "3.11.0",
|
||||||
|
"github-slugger": "2.0.0",
|
||||||
"globals": "16.3.0",
|
"globals": "16.3.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"prettier-plugin-svelte": "3.4.0",
|
"prettier-plugin-svelte": "3.4.0",
|
||||||
"prettier-plugin-tailwindcss": "0.6.14",
|
"prettier-plugin-tailwindcss": "0.6.14",
|
||||||
|
"rehype-autolink-headings": "7.1.0",
|
||||||
|
"rehype-slug": "6.0.0",
|
||||||
"svelte": "5.38.2",
|
"svelte": "5.38.2",
|
||||||
"svelte-check": "4.3.1",
|
"svelte-check": "4.3.1",
|
||||||
"svelte-sonner": "1.0.5",
|
"svelte-sonner": "1.0.5",
|
||||||
@@ -84,6 +90,7 @@
|
|||||||
"tw-animate-css": "1.3.7",
|
"tw-animate-css": "1.3.7",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
"typescript-eslint": "8.40.0",
|
"typescript-eslint": "8.40.0",
|
||||||
|
"unist-util-visit": "5.0.0",
|
||||||
"vite": "7.1.3",
|
"vite": "7.1.3",
|
||||||
"zod": "4.1.4"
|
"zod": "4.1.4"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+54
@@ -93,6 +93,9 @@ importers:
|
|||||||
eslint-plugin-svelte:
|
eslint-plugin-svelte:
|
||||||
specifier: 3.11.0
|
specifier: 3.11.0
|
||||||
version: 3.11.0(eslint@9.33.0(jiti@2.5.1))(svelte@5.38.2)
|
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:
|
globals:
|
||||||
specifier: 16.3.0
|
specifier: 16.3.0
|
||||||
version: 16.3.0
|
version: 16.3.0
|
||||||
@@ -105,6 +108,12 @@ importers:
|
|||||||
prettier-plugin-tailwindcss:
|
prettier-plugin-tailwindcss:
|
||||||
specifier: 0.6.14
|
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)
|
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:
|
svelte:
|
||||||
specifier: 5.38.2
|
specifier: 5.38.2
|
||||||
version: 5.38.2
|
version: 5.38.2
|
||||||
@@ -132,6 +141,9 @@ importers:
|
|||||||
typescript-eslint:
|
typescript-eslint:
|
||||||
specifier: 8.40.0
|
specifier: 8.40.0
|
||||||
version: 8.40.0(eslint@9.33.0(jiti@2.5.1))(typescript@5.9.2)
|
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:
|
vite:
|
||||||
specifier: 7.1.3
|
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)
|
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:
|
get-tsconfig@4.10.1:
|
||||||
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
|
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
|
||||||
|
|
||||||
|
github-slugger@2.0.0:
|
||||||
|
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -1733,6 +1748,12 @@ packages:
|
|||||||
hast-util-from-parse5@8.0.3:
|
hast-util-from-parse5@8.0.3:
|
||||||
resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==}
|
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:
|
hast-util-parse-selector@4.0.0:
|
||||||
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
|
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
|
||||||
|
|
||||||
@@ -2563,9 +2584,15 @@ packages:
|
|||||||
regex@6.0.1:
|
regex@6.0.1:
|
||||||
resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==}
|
resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==}
|
||||||
|
|
||||||
|
rehype-autolink-headings@7.1.0:
|
||||||
|
resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==}
|
||||||
|
|
||||||
rehype-raw@7.0.0:
|
rehype-raw@7.0.0:
|
||||||
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
|
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:
|
rehype-stringify@10.0.1:
|
||||||
resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
|
resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==}
|
||||||
|
|
||||||
@@ -4761,6 +4788,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
resolve-pkg-maps: 1.0.0
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
|
github-slugger@2.0.0: {}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -4807,6 +4836,14 @@ snapshots:
|
|||||||
vfile-location: 5.0.3
|
vfile-location: 5.0.3
|
||||||
web-namespaces: 2.0.1
|
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:
|
hast-util-parse-selector@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
@@ -5611,12 +5648,29 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
regex-utilities: 2.3.0
|
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:
|
rehype-raw@7.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
hast-util-raw: 9.1.0
|
hast-util-raw: 9.1.0
|
||||||
vfile: 6.0.3
|
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:
|
rehype-stringify@10.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/hast': 3.0.4
|
'@types/hast': 3.0.4
|
||||||
|
|||||||
+32
-9
@@ -11,17 +11,17 @@
|
|||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
type="image/svg+xml"
|
type="image/svg+xml"
|
||||||
href="%sveltekit.assets%/images/logo.svg"
|
href="%sveltekit.assets%/images/svgl_svg.svg"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
type="image/ico"
|
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:type" content="website" />
|
||||||
<meta property="og:title" content="svgl" />
|
<meta property="og:title" content="SVGL" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="A beautiful library with SVG logos"
|
content="A beautiful library with SVG logos"
|
||||||
@@ -29,12 +29,12 @@
|
|||||||
<meta property="og:url" content="https://svgl.app" />
|
<meta property="og:url" content="https://svgl.app" />
|
||||||
<meta
|
<meta
|
||||||
property="og:image"
|
property="og:image"
|
||||||
content="https://svgl.app/images/screenshot.png"
|
content="https://svgl.app/images/screenshot_dark.png"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Twitter -->
|
<!-- Twitter -->
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content="Svgl" />
|
<meta name="twitter:title" content="SVGL" />
|
||||||
<meta
|
<meta
|
||||||
name="twitter:description"
|
name="twitter:description"
|
||||||
content="A beautiful library with SVG logos"
|
content="A beautiful library with SVG logos"
|
||||||
@@ -42,15 +42,38 @@
|
|||||||
<meta name="twitter:creator" content="@pheralb_" />
|
<meta name="twitter:creator" content="@pheralb_" />
|
||||||
<meta
|
<meta
|
||||||
name="twitter:image"
|
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 -->
|
||||||
<title>A beautiful library with SVG logos - Svgl</title>
|
<title>A beautiful library with SVG logos - SVGL</title>
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body
|
<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"
|
data-sveltekit-preload-data="hover"
|
||||||
>
|
>
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
$props();
|
$props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn("container mx-auto px-4", className)}>
|
<div class={cn("container mx-auto px-6 lg:px-4", className)}>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -2,13 +2,21 @@
|
|||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
import { cn } from "@/utils/cn";
|
import { cn } from "@/utils/cn";
|
||||||
|
|
||||||
let { className, children }: { className?: string; children?: Snippet } =
|
interface GridProps {
|
||||||
$props();
|
columns?: "default" | "4" | "3" | "2";
|
||||||
|
className?: string;
|
||||||
|
children?: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { className, columns, children }: GridProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,67 +5,55 @@
|
|||||||
import ModeToggle from "@/components/modeToggle.svelte";
|
import ModeToggle from "@/components/modeToggle.svelte";
|
||||||
|
|
||||||
import Svgl from "@/components/logos/svgl.svelte";
|
import Svgl from "@/components/logos/svgl.svelte";
|
||||||
import Github from "@/components/logos/github.svelte";
|
|
||||||
import Twitter from "@/components/logos/twitter.svelte";
|
import Twitter from "@/components/logos/twitter.svelte";
|
||||||
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
import SvglVersion from "@/components/svglVersion.svelte";
|
|
||||||
import SendIcon from "@/components/ui/moving-icons/send-icon.svelte";
|
import SendIcon from "@/components/ui/moving-icons/send-icon.svelte";
|
||||||
|
import SidebarMobileMenu from "@/components/layout/sidebarMobileMenu.svelte";
|
||||||
interface HeaderProps {
|
import SettingsMenu from "@/components/settings/settingsMenu.svelte";
|
||||||
githubStars: number;
|
import GithubLink from "@/components/githubLink.svelte";
|
||||||
}
|
|
||||||
|
|
||||||
let { githubStars }: HeaderProps = $props();
|
|
||||||
|
|
||||||
const headerItemsClasses = cn(
|
|
||||||
buttonVariants({ variant: "ghost" }),
|
|
||||||
"hover:bg-neutral-200 dark:hover:bg-neutral-800",
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<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">
|
<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
|
<a
|
||||||
href="/"
|
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} />
|
<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>
|
</a>
|
||||||
<SvglVersion />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex h-8 items-center">
|
<div class="flex h-5 items-center space-x-2.5">
|
||||||
<div class="flex items-center space-x-0.5">
|
<div class="flex items-center space-x-1.5">
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="X/Twitter"
|
title="X/Twitter"
|
||||||
href={globals.twitterUrl}
|
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} />
|
<Twitter size={18} />
|
||||||
</a>
|
</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>
|
||||||
<Separator orientation="vertical" class="mx-2 h-8" />
|
<div class="hidden h-5 items-center space-x-2 md:flex">
|
||||||
<a
|
<Separator orientation="vertical" />
|
||||||
target="_blank"
|
<GithubLink />
|
||||||
title="GitHub Repository"
|
<Separator orientation="vertical" />
|
||||||
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
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={globals.submitUrl}
|
href={globals.submitUrl}
|
||||||
@@ -79,5 +67,6 @@
|
|||||||
<span>Submit</span>
|
<span>Submit</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { tCategory } from "@/types/categories";
|
import type { Category } from "@/types/categories";
|
||||||
|
|
||||||
import { cn } from "@/utils/cn";
|
import { cn } from "@/utils/cn";
|
||||||
import { svgs } from "@/data/svgs";
|
import { svgs } from "@/data/svgs";
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
import { sidebarBadgeClasses } from "./sidebarBadgeClasses";
|
import { sidebarBadgeClasses } from "./sidebarBadgeClasses";
|
||||||
|
|
||||||
// Get category counts:
|
// Get category counts:
|
||||||
const categories: tCategory[] = getCategories();
|
const categories: Category[] = getCategories();
|
||||||
let categoryCounts: Record<string, number> = {};
|
let categoryCounts: Record<string, number> = {};
|
||||||
categories.forEach((category) => {
|
categories.forEach((category) => {
|
||||||
categoryCounts[category] = svgs.filter((svg) =>
|
categoryCounts[category] = svgs.filter((svg) =>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from "@/utils/cn";
|
import { cn } from "@/utils/cn";
|
||||||
|
import { globals } from "@/globals";
|
||||||
|
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import favoritesStore from "@/stores/favorites.store";
|
import favoritesStore from "@/stores/favorites.store";
|
||||||
|
|
||||||
@@ -10,6 +12,9 @@
|
|||||||
import House from "@lucide/svelte/icons/house";
|
import House from "@lucide/svelte/icons/house";
|
||||||
import Heart from "@lucide/svelte/icons/heart";
|
import Heart from "@lucide/svelte/icons/heart";
|
||||||
import Cloud from "@lucide/svelte/icons/cloud";
|
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 favorites = $derived($favoritesStore);
|
||||||
let favoritesCount = $derived(favoritesStore.getCount(favorites));
|
let favoritesCount = $derived(favoritesStore.getCount(favorites));
|
||||||
@@ -49,7 +54,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/api"
|
href="/docs/api"
|
||||||
data-sveltekit-preload-data
|
data-sveltekit-preload-data
|
||||||
class={cn(
|
class={cn(
|
||||||
sidebarItemClasses.base,
|
sidebarItemClasses.base,
|
||||||
@@ -60,6 +65,19 @@
|
|||||||
<Cloud size={16} />
|
<Cloud size={16} />
|
||||||
<p class="truncate">API</p>
|
<p class="truncate">API</p>
|
||||||
</a>
|
</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
|
<a
|
||||||
href="/extensions"
|
href="/extensions"
|
||||||
data-sveltekit-preload-data
|
data-sveltekit-preload-data
|
||||||
@@ -72,3 +90,19 @@
|
|||||||
<Box size={16} />
|
<Box size={16} />
|
||||||
<p class="truncate">Extensions</p>
|
<p class="truncate">Extensions</p>
|
||||||
</a>
|
</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>
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
<section>
|
<section>
|
||||||
<aside
|
<aside
|
||||||
class={cn(
|
class={cn(
|
||||||
"fixed left-0 h-[calc(100vh-5.4rem)]",
|
"md:fixed md:left-1 md:h-[calc(100vh-4.5rem)]",
|
||||||
"overflow-x-hidden",
|
"overflow-x-hidden",
|
||||||
"w-54 pr-2 pl-3",
|
"w-54 pr-2 pl-2",
|
||||||
"flex flex-col space-y-3",
|
"hidden flex-col space-y-3 md:flex",
|
||||||
"bg-neutral-100 dark:bg-neutral-950",
|
"bg-neutral-100 dark:bg-neutral-950",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -20,11 +20,11 @@
|
|||||||
<ShowSidebarLinks />
|
<ShowSidebarLinks />
|
||||||
</nav>
|
</nav>
|
||||||
<Separator orientation="horizontal" />
|
<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 />
|
<ShowCategories />
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</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 />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ import { cn } from "@/utils/cn";
|
|||||||
|
|
||||||
export const sidebarBadgeClasses = cn(
|
export const sidebarBadgeClasses = cn(
|
||||||
"animate-in zoom-in-20 fade-in",
|
"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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -5,70 +5,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
|
name="SVGL Logo"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width={props.size}
|
width={props.size}
|
||||||
height={props.size}
|
height={props.size}
|
||||||
name="SVGL Logo"
|
fill="currentColor"
|
||||||
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"
|
viewBox="0 0 256 256"
|
||||||
x="101"
|
><path d="M216,136c-8,24-56,72-80,80V184a48,48,0,0,1,48-48Z" opacity="0.2"
|
||||||
y="101"
|
></path><path
|
||||||
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"
|
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
|
></path>
|
||||||
>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,17 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { toggleMode } from "mode-watcher";
|
||||||
|
|
||||||
import SunIcon from "@lucide/svelte/icons/sun";
|
import SunIcon from "@lucide/svelte/icons/sun";
|
||||||
import MoonIcon from "@lucide/svelte/icons/moon";
|
import MoonIcon from "@lucide/svelte/icons/moon";
|
||||||
|
|
||||||
import { toggleMode } from "mode-watcher";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { className }: Props = $props();
|
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>
|
</script>
|
||||||
|
|
||||||
<button class={className} onclick={toggleMode} title="Mode Toggle">
|
<button class={className} onclick={toggleMode} title="Mode Toggle (Cmd + l)">
|
||||||
<SunIcon
|
<SunIcon
|
||||||
size={20}
|
size={20}
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
|
|||||||
@@ -11,20 +11,24 @@
|
|||||||
let { children, contentCardClass, containerClass }: PageCardProps = $props();
|
let { children, contentCardClass, containerClass }: PageCardProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="p-[1px]">
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={cn(
|
||||||
"mt-2.5 overflow-hidden",
|
"overflow-hidden",
|
||||||
"rounded-md border border-neutral-200 dark:border-neutral-800",
|
"rounded-md border border-neutral-200 dark:border-neutral-800",
|
||||||
"bg-white dark:bg-neutral-900/40",
|
"bg-white dark:bg-neutral-900/40",
|
||||||
|
"shadow-xs",
|
||||||
containerClass,
|
containerClass,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={cn(
|
class={cn(
|
||||||
"max-h-[calc(100vh-8.6rem)] min-h-[calc(100vh-8.6rem)] overflow-y-auto",
|
"max-h-[calc(100vh-4.5rem)] min-h-[calc(100vh-4.5rem)]",
|
||||||
|
"overflow-hidden overflow-y-auto",
|
||||||
contentCardClass,
|
contentCardClass,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -2,36 +2,29 @@
|
|||||||
import { cn } from "@/utils/cn";
|
import { cn } from "@/utils/cn";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
import { page } from "$app/state";
|
import { addParams } from "@/utils/searchParams";
|
||||||
import { goto } from "$app/navigation";
|
|
||||||
|
|
||||||
import SearchIcon from "@lucide/svelte/icons/search";
|
import SearchIcon from "@lucide/svelte/icons/search";
|
||||||
import CommandIcon from "@lucide/svelte/icons/command";
|
import CommandIcon from "@lucide/svelte/icons/command";
|
||||||
import { SvelteURLSearchParams } from "svelte/reactivity";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
searchValue: string;
|
searchValue: string;
|
||||||
onSearch: (value: string) => void;
|
onSearch: (value: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
iconSize?: number;
|
||||||
|
inputClass?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { searchValue, onSearch, placeholder }: Props = $props();
|
let { searchValue, onSearch, placeholder, iconSize, inputClass }: Props =
|
||||||
|
$props();
|
||||||
let inputElement: HTMLInputElement;
|
let inputElement: HTMLInputElement;
|
||||||
|
|
||||||
const onInput = (event: Event) => {
|
const onInput = (event: Event) => {
|
||||||
const param = "search";
|
|
||||||
const value = (event.target as HTMLInputElement).value;
|
const value = (event.target as HTMLInputElement).value;
|
||||||
onSearch(value);
|
onSearch(value);
|
||||||
const params = new SvelteURLSearchParams(page.url.searchParams);
|
addParams({
|
||||||
if (value) {
|
params: {
|
||||||
params.set(param, value);
|
search: value,
|
||||||
} else {
|
},
|
||||||
params.delete(param);
|
|
||||||
}
|
|
||||||
goto(`?${params.toString()}`, {
|
|
||||||
keepFocus: true,
|
|
||||||
noScroll: true,
|
|
||||||
replaceState: true,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,7 +45,7 @@
|
|||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<SearchIcon
|
<SearchIcon
|
||||||
size={20}
|
size={iconSize ? iconSize : 20}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
class={cn(
|
class={cn(
|
||||||
"pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 transition-colors",
|
"pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 transition-colors",
|
||||||
@@ -67,6 +60,7 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
placeholder={placeholder || "Search..."}
|
placeholder={placeholder || "Search..."}
|
||||||
oninput={onInput}
|
oninput={onInput}
|
||||||
|
name="search"
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
class={cn(
|
class={cn(
|
||||||
"overflow-hidden shadow-sm",
|
"overflow-hidden shadow-sm",
|
||||||
@@ -75,6 +69,7 @@
|
|||||||
"bg-white dark:bg-neutral-900",
|
"bg-white dark:bg-neutral-900",
|
||||||
"rounded-md border border-neutral-200 dark:border-neutral-800",
|
"rounded-md border border-neutral-200 dark:border-neutral-800",
|
||||||
"focus:border-neutral-400 focus:outline-none dark:focus:border-neutral-600",
|
"focus:border-neutral-400 focus:outline-none dark:focus:border-neutral-600",
|
||||||
|
inputClass,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{#if !searchValue}
|
{#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>
|
||||||
+13
-7
@@ -1,16 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { Component } from "svelte";
|
||||||
import * as Select from "@/components/ui/select";
|
import * as Select from "@/components/ui/select";
|
||||||
import { pkgManager, type PackageManager } from "@/stores/pkgManager.store";
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
|
import { settingsStore, type PackageManager } from "@/stores/settings.store";
|
||||||
|
|
||||||
import Npm from "@/components/logos/npm.svelte";
|
import Npm from "@/components/logos/npm.svelte";
|
||||||
import Pnpm from "@/components/logos/pnpm.svelte";
|
import Pnpm from "@/components/logos/pnpm.svelte";
|
||||||
import Yarn from "@/components/logos/yarn.svelte";
|
import Yarn from "@/components/logos/yarn.svelte";
|
||||||
import Bun from "@/components/logos/bun.svelte";
|
import Bun from "@/components/logos/bun.svelte";
|
||||||
import { buttonVariants } from "./ui/button";
|
|
||||||
|
|
||||||
let pkg = $derived($pkgManager);
|
let pkg = $derived($settingsStore.packageManager);
|
||||||
|
|
||||||
const managers = {
|
const managers: Record<PackageManager, { label: string; Icon: Component }> = {
|
||||||
npm: { label: "npm", Icon: Npm },
|
npm: { label: "npm", Icon: Npm },
|
||||||
pnpm: { label: "pnpm", Icon: Pnpm },
|
pnpm: { label: "pnpm", Icon: Pnpm },
|
||||||
yarn: { label: "yarn", Icon: Yarn },
|
yarn: { label: "yarn", Icon: Yarn },
|
||||||
@@ -19,18 +21,22 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Select.Root type="single" bind:value={pkg}>
|
<Select.Root type="single" bind:value={pkg}>
|
||||||
<Select.Trigger class={buttonVariants({ variant: "outline", size: "sm" })}>
|
<Select.Trigger
|
||||||
|
class={buttonVariants({ variant: "outline", class: "justify-between" })}
|
||||||
|
>
|
||||||
{#if managers[pkg]}
|
{#if managers[pkg]}
|
||||||
{@const { Icon, label } = managers[pkg]}
|
{@const { Icon, label } = managers[pkg]}
|
||||||
<Icon size={14} />
|
<div class="flex items-center space-x-2.5">
|
||||||
|
<Icon size={16} />
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Select.Trigger>
|
</Select.Trigger>
|
||||||
<Select.Content sideOffset={1.5}>
|
<Select.Content sideOffset={1.5}>
|
||||||
{#each Object.entries(managers) as [value, { Icon, label }] (value)}
|
{#each Object.entries(managers) as [value, { Icon, label }] (value)}
|
||||||
<Select.Item
|
<Select.Item
|
||||||
{value}
|
{value}
|
||||||
onclick={() => pkgManager.set(value as PackageManager)}
|
onclick={() => settingsStore.setPackageManager(value as PackageManager)}
|
||||||
>
|
>
|
||||||
<Icon size={16} />
|
<Icon size={16} />
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
|
interface SettingsCardProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
children: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { title, description, children }: SettingsCardProps = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h3 class="mb-0.5 font-medium">{title}</h3>
|
||||||
|
<p class="mb-3 text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,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>
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { PUBLIC_SVGL_VERSION } from "$env/static/public";
|
|
||||||
import Badge from "@/components/ui/badge/badge.svelte";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if PUBLIC_SVGL_VERSION}
|
|
||||||
<Badge variant="outline">{PUBLIC_SVGL_VERSION}</Badge>
|
|
||||||
{/if}
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<button
|
<button
|
||||||
class={cn(
|
class={cn(
|
||||||
"cursor-pointer transition-colors hover:animate-pulse",
|
"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",
|
isFavorite && "text-red-500",
|
||||||
)}
|
)}
|
||||||
onclick={toggleFavorite}
|
onclick={toggleFavorite}
|
||||||
@@ -36,6 +36,6 @@
|
|||||||
<HeartIcon
|
<HeartIcon
|
||||||
size={16}
|
size={16}
|
||||||
strokeWidth={1.8}
|
strokeWidth={1.8}
|
||||||
class={cn(isFavorite && "fill-red-500")}
|
class={cn(isFavorite && "fill-red-500 dark:fill-red-600")}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from "@/components/ui/button";
|
import { globals } from "@/globals";
|
||||||
import Shadcn from "@/components/logos/shadcn.svelte";
|
|
||||||
import SelectPkgManager from "@/components/selectPkgManager.svelte";
|
|
||||||
|
|
||||||
import { pkgManager, type PackageManager } from "@/stores/pkgManager.store";
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
import CodeBlock from "@/components/codeBlock.svelte";
|
import CodeBlock from "@/components/codeBlock.svelte";
|
||||||
import SetupShadcnRegistry from "@/components/svgs/setupShadcnRegistry.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 {
|
interface Props {
|
||||||
svgTitle: string;
|
svgTitle: string;
|
||||||
@@ -20,7 +23,7 @@
|
|||||||
bun: "bunx shadcn@latest add",
|
bun: "bunx shadcn@latest add",
|
||||||
};
|
};
|
||||||
|
|
||||||
let pkg = $derived($pkgManager);
|
let pkg = $derived($settingsStore.packageManager);
|
||||||
let shadcnCommand = $derived(shadcnCommands[pkg]);
|
let shadcnCommand = $derived(shadcnCommands[pkg]);
|
||||||
const svgFormatTitle = svgTitle
|
const svgFormatTitle = svgTitle
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -28,12 +31,29 @@
|
|||||||
.replace(/[^a-z0-9-]/g, "");
|
.replace(/[^a-z0-9-]/g, "");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center justify-between space-x-2">
|
<div class="flex w-full items-center space-x-2">
|
||||||
<SetupShadcnRegistry>
|
<a
|
||||||
<Button variant="outline" size="sm">
|
target="_blank"
|
||||||
|
href="/docs/shadcn-ui"
|
||||||
|
class={buttonVariants({ variant: "outline", class: "w-full" })}
|
||||||
|
>
|
||||||
<span>Setup Registry</span>
|
<span>Setup Registry</span>
|
||||||
</Button>
|
<ArrowUpRightIcon
|
||||||
</SetupShadcnRegistry>
|
size={14}
|
||||||
<SelectPkgManager />
|
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>
|
</div>
|
||||||
<CodeBlock code={`${shadcnCommand} @svgl/${svgFormatTitle}`} Icon={Shadcn} />
|
<CodeBlock code={`${shadcnCommand} @svgl/${svgFormatTitle}`} Icon={Shadcn} />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { clipboard } from "@/utils/clipboard";
|
import { clipboard } from "@/utils/clipboard";
|
||||||
import { getPrefixFromSvgUrl, prefixSvgIds } from "@/utils/prefixSvgIds";
|
import { getPrefixFromSvgUrl, prefixSvgIds } from "@/utils/prefixSvgIds";
|
||||||
import { copyToClipboard as figmaCopyToClipboard } from "@/figma/copy-to-clipboard";
|
import { copyToClipboard as figmaCopyToClipboard } from "@/figma/copy-to-clipboard";
|
||||||
|
import { settingsStore } from "@/stores/settings.store";
|
||||||
|
|
||||||
// Icons:
|
// Icons:
|
||||||
import XIcon from "@lucide/svelte/icons/x";
|
import XIcon from "@lucide/svelte/icons/x";
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
|
|
||||||
// UI Components:
|
// UI Components:
|
||||||
import { toast } from "svelte-sonner";
|
import { toast } from "svelte-sonner";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
import * as Tabs from "@/components/ui/tabs";
|
import * as Tabs from "@/components/ui/tabs";
|
||||||
import { Button, buttonVariants } from "@/components/ui/button";
|
import { Button, buttonVariants } from "@/components/ui/button";
|
||||||
import * as Popover from "@/components/ui/popover";
|
import * as Popover from "@/components/ui/popover";
|
||||||
@@ -36,6 +38,7 @@
|
|||||||
import React from "@/components/logos/react.svelte";
|
import React from "@/components/logos/react.svelte";
|
||||||
import Astro from "@/components/logos/astro.svelte";
|
import Astro from "@/components/logos/astro.svelte";
|
||||||
import Svelte from "@/components/logos/svelte.svelte";
|
import Svelte from "@/components/logos/svelte.svelte";
|
||||||
|
import Shadcn from "@/components/logos/shadcn.svelte";
|
||||||
import Angular from "@/components/logos/angular.svelte";
|
import Angular from "@/components/logos/angular.svelte";
|
||||||
import WebComponents from "@/components/logos/webComponents.svelte";
|
import WebComponents from "@/components/logos/webComponents.svelte";
|
||||||
|
|
||||||
@@ -59,6 +62,7 @@
|
|||||||
// States:
|
// States:
|
||||||
let optionsOpen = $state<boolean>(false);
|
let optionsOpen = $state<boolean>(false);
|
||||||
let isLoading = $state<boolean>(false);
|
let isLoading = $state<boolean>(false);
|
||||||
|
let optimize = $derived($settingsStore.optimizeSvgs);
|
||||||
|
|
||||||
const getSvgUrl = () => {
|
const getSvgUrl = () => {
|
||||||
let svgUrlToCopy;
|
let svgUrlToCopy;
|
||||||
@@ -105,6 +109,7 @@
|
|||||||
|
|
||||||
let content = await getSource({
|
let content = await getSource({
|
||||||
url: svgUrlToCopy,
|
url: svgUrlToCopy,
|
||||||
|
optimize,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (svgUrlToCopy) {
|
if (svgUrlToCopy) {
|
||||||
@@ -150,6 +155,7 @@
|
|||||||
const title = svgInfo.title.split(" ").join("");
|
const title = svgInfo.title.split(" ").join("");
|
||||||
let content = await getSource({
|
let content = await getSource({
|
||||||
url: svgUrlToCopy,
|
url: svgUrlToCopy,
|
||||||
|
optimize,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (svgUrlToCopy) {
|
if (svgUrlToCopy) {
|
||||||
@@ -186,6 +192,7 @@
|
|||||||
|
|
||||||
let content = await getSource({
|
let content = await getSource({
|
||||||
url: svgUrlToCopy,
|
url: svgUrlToCopy,
|
||||||
|
optimize,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (svgUrlToCopy) {
|
if (svgUrlToCopy) {
|
||||||
@@ -223,6 +230,7 @@
|
|||||||
|
|
||||||
let content = await getSource({
|
let content = await getSource({
|
||||||
url: svgUrlToCopy,
|
url: svgUrlToCopy,
|
||||||
|
optimize,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (svgUrlToCopy) {
|
if (svgUrlToCopy) {
|
||||||
@@ -260,6 +268,7 @@
|
|||||||
const svgUrlToCopy = getSvgUrl();
|
const svgUrlToCopy = getSvgUrl();
|
||||||
let content = await getSource({
|
let content = await getSource({
|
||||||
url: svgUrlToCopy,
|
url: svgUrlToCopy,
|
||||||
|
optimize,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (svgUrlToCopy) {
|
if (svgUrlToCopy) {
|
||||||
@@ -297,6 +306,7 @@
|
|||||||
const svgUrlToCopy = getSvgUrl();
|
const svgUrlToCopy = getSvgUrl();
|
||||||
let content = await getSource({
|
let content = await getSource({
|
||||||
url: svgUrlToCopy,
|
url: svgUrlToCopy,
|
||||||
|
optimize,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (svgUrlToCopy) {
|
if (svgUrlToCopy) {
|
||||||
@@ -333,6 +343,7 @@
|
|||||||
const svgUrlToCopy = getSvgUrl();
|
const svgUrlToCopy = getSvgUrl();
|
||||||
let content = await getSource({
|
let content = await getSource({
|
||||||
url: svgUrlToCopy,
|
url: svgUrlToCopy,
|
||||||
|
optimize,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (svgUrlToCopy) {
|
if (svgUrlToCopy) {
|
||||||
@@ -380,42 +391,43 @@
|
|||||||
<CopyIcon {size} strokeWidth={iconStroke} />
|
<CopyIcon {size} strokeWidth={iconStroke} />
|
||||||
{/if}
|
{/if}
|
||||||
</Popover.Trigger>
|
</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.Root value="source" class="flex w-full flex-col space-y-1">
|
||||||
<Tabs.List class="w-fit border-none bg-transparent">
|
<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="source">Source</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="shadcn">shadcn/ui</Tabs.Trigger>
|
<Tabs.Trigger value="shadcn" title="shadcn/ui">
|
||||||
<div
|
<Shadcn size={18} />
|
||||||
class="ml-3 flex flex-row space-x-1 border-l border-neutral-200 pl-3 dark:border-neutral-800"
|
</Tabs.Trigger>
|
||||||
>
|
</div>
|
||||||
<Tabs.Trigger
|
<div class="flex items-center space-x-1">
|
||||||
class="px-2.5"
|
<Tabs.Trigger value="web-component" title="Web Component">
|
||||||
value="web-component"
|
|
||||||
title="Web Component"
|
|
||||||
>
|
|
||||||
<WebComponents size={21} />
|
<WebComponents size={21} />
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger class="px-2.5" value="react" title="React">
|
<Tabs.Trigger value="react" title="React">
|
||||||
<React size={20} />
|
<React size={20} />
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger class="px-2.5" value="vue" title="Vue">
|
<Tabs.Trigger value="vue" title="Vue">
|
||||||
<Vue size={20} />
|
<Vue size={20} />
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger class="px-2.5" value="svelte" title="Svelte">
|
<Tabs.Trigger value="svelte" title="Svelte">
|
||||||
<Svelte size={20} />
|
<Svelte size={20} />
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger class="px-2.5" value="angular" title="Angular">
|
<Tabs.Trigger value="angular" title="Angular">
|
||||||
<Angular size={20} />
|
<Angular size={20} />
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
<Tabs.Trigger
|
<Tabs.Trigger
|
||||||
value="astro"
|
value="astro"
|
||||||
title="Astro"
|
title="Astro"
|
||||||
class="px-2.5 text-black dark:text-white"
|
class="text-black dark:text-white"
|
||||||
>
|
>
|
||||||
<Astro size={21} />
|
<Astro size={21} />
|
||||||
</Tabs.Trigger>
|
</Tabs.Trigger>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
<Separator class="block md:hidden" />
|
||||||
<!-- Source -->
|
<!-- Source -->
|
||||||
<Tabs.Content value="source">
|
<Tabs.Content value="source">
|
||||||
<section class="flex flex-col space-y-2">
|
<section class="flex flex-col space-y-2">
|
||||||
@@ -477,17 +489,6 @@
|
|||||||
<span>Copy JS</span>
|
<span>Copy JS</span>
|
||||||
</Button>
|
</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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="justify-start"
|
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"
|
class="mt-1 flex w-full items-center text-center text-[12px] text-neutral-600 dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Remember to request permission from the creators for the use of the SVG.
|
Please ensure you have permission from the creators before using the
|
||||||
Modification is not allowed.
|
SVG. Modifications are not permitted.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
|
|||||||
@@ -23,7 +23,10 @@
|
|||||||
let iconSize = 16;
|
let iconSize = 16;
|
||||||
let iconStroke = 2;
|
let iconStroke = 2;
|
||||||
let cardDownloadStyles =
|
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:
|
// Functions:
|
||||||
const handleDownloadSvg = async (url?: string) => {
|
const handleDownloadSvg = async (url?: string) => {
|
||||||
@@ -109,28 +112,23 @@
|
|||||||
>
|
>
|
||||||
<DownloadIcon size={iconSize} strokeWidth={iconStroke} />
|
<DownloadIcon size={iconSize} strokeWidth={iconStroke} />
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Content class="max-w-[630px]">
|
<Dialog.Content>
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title>Download {svgInfo.title} SVG</Dialog.Title>
|
<Dialog.Title>Download {svgInfo.title} SVGs</Dialog.Title>
|
||||||
<Dialog.Description>
|
<Dialog.Description>
|
||||||
This logo has multiple options to download:
|
This logo has multiple options to download:
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
<div
|
<div class={cn("flex flex-col gap-4 md:flex-row")}>
|
||||||
class={cn(
|
|
||||||
"flex h-full flex-col space-y-2 pt-2 pb-0.5",
|
|
||||||
"md:flex-row md:items-center md:justify-center md:space-y-0 md:space-x-2",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{#if typeof svgInfo.route === "string"}
|
{#if typeof svgInfo.route === "string"}
|
||||||
<div class={cardDownloadStyles}>
|
<div class={cardDownloadStyles}>
|
||||||
<img
|
<img
|
||||||
src={isDarkTheme() ? svgInfo.route : svgInfo.route}
|
src={isDarkTheme() ? svgInfo.route : svgInfo.route}
|
||||||
alt={svgInfo.title}
|
alt={svgInfo.title}
|
||||||
class="my-4 h-8"
|
class={imgStyles}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
class="justify-start"
|
class="justify-between"
|
||||||
title="Download logo"
|
title="Download logo"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
@@ -140,8 +138,11 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DownloadIcon class="mr-2" size={iconSize} />
|
<div class="flex items-center space-x-2">
|
||||||
|
<DownloadIcon size={iconSize} />
|
||||||
<p>Icon logo</p>
|
<p>Icon logo</p>
|
||||||
|
</div>
|
||||||
|
<span class={badgeButtonStyles}>.svg</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -149,10 +150,10 @@
|
|||||||
<img
|
<img
|
||||||
src={isDarkTheme() ? svgInfo.route.dark : svgInfo.route.light}
|
src={isDarkTheme() ? svgInfo.route.dark : svgInfo.route.light}
|
||||||
alt={svgInfo.title}
|
alt={svgInfo.title}
|
||||||
class="my-4 h-10"
|
class={imgStyles}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
class="justify-start"
|
class="justify-between"
|
||||||
title="Logo with light & dark variants"
|
title="Logo with light & dark variants"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
@@ -164,12 +165,15 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
<DownloadIcon size={iconSize} />
|
<DownloadIcon size={iconSize} />
|
||||||
<p>Light & dark variants</p>
|
<p>Light & dark variants</p>
|
||||||
|
</div>
|
||||||
|
<span class={badgeButtonStyles}>.zip</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
class="justify-start"
|
class="justify-between"
|
||||||
title="Download light variant"
|
title="Download light variant"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
@@ -179,12 +183,15 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DownloadIcon class="mr-2" size={iconSize} />
|
<div class="flex items-center space-x-2">
|
||||||
|
<DownloadIcon size={iconSize} />
|
||||||
<p>Only light variant</p>
|
<p>Only light variant</p>
|
||||||
|
</div>
|
||||||
|
<span class={badgeButtonStyles}>.svg</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
class="justify-start"
|
class="justify-between"
|
||||||
title="Download dark variant"
|
title="Download dark variant"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
@@ -194,8 +201,11 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DownloadIcon class="mr-2" size={iconSize} />
|
<div class="flex items-center space-x-2">
|
||||||
|
<DownloadIcon size={iconSize} />
|
||||||
<p>Only dark variant</p>
|
<p>Only dark variant</p>
|
||||||
|
</div>
|
||||||
|
<span class={badgeButtonStyles}>.svg</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -205,10 +215,10 @@
|
|||||||
<img
|
<img
|
||||||
src={isDarkTheme() ? svgInfo.wordmark : svgInfo.wordmark}
|
src={isDarkTheme() ? svgInfo.wordmark : svgInfo.wordmark}
|
||||||
alt={svgInfo.title}
|
alt={svgInfo.title}
|
||||||
class="my-4 h-8"
|
class={imgStyles}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
class="justify-start"
|
class="justify-between"
|
||||||
title="Download Wordmark logo"
|
title="Download Wordmark logo"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
@@ -218,8 +228,11 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DownloadIcon class="mr-2" size={iconSize} />
|
<div class="flex items-center space-x-2">
|
||||||
|
<DownloadIcon size={iconSize} />
|
||||||
<p>Wordmark logo</p>
|
<p>Wordmark logo</p>
|
||||||
|
</div>
|
||||||
|
<span class={badgeButtonStyles}>.svg</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -231,10 +244,10 @@
|
|||||||
? svgInfo.wordmark.dark
|
? svgInfo.wordmark.dark
|
||||||
: svgInfo.wordmark.light}
|
: svgInfo.wordmark.light}
|
||||||
alt={svgInfo.title}
|
alt={svgInfo.title}
|
||||||
class="my-4 h-10"
|
class={imgStyles}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
class="justify-start"
|
class="justify-between"
|
||||||
title="Download Wordmark light variant"
|
title="Download Wordmark light variant"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
@@ -248,12 +261,15 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DownloadIcon class="mr-2" size={iconSize} />
|
<div class="flex items-center space-x-2">
|
||||||
|
<DownloadIcon size={iconSize} />
|
||||||
<p>Light & dark variants</p>
|
<p>Light & dark variants</p>
|
||||||
|
</div>
|
||||||
|
<span class={badgeButtonStyles}>.zip</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
class="justify-start"
|
class="justify-between"
|
||||||
title="Download Wordmark light variant"
|
title="Download Wordmark light variant"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
@@ -263,12 +279,15 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DownloadIcon class="mr-2" size={iconSize} />
|
<div class="flex items-center space-x-2">
|
||||||
|
<DownloadIcon size={iconSize} />
|
||||||
<p>Wordmark light variant</p>
|
<p>Wordmark light variant</p>
|
||||||
|
</div>
|
||||||
|
<span class={badgeButtonStyles}>.svg</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
class="justify-start"
|
class="justify-between"
|
||||||
title="Download Wordmark dark variant"
|
title="Download Wordmark dark variant"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
@@ -278,19 +297,24 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DownloadIcon class="mr-2" size={iconSize} />
|
<div class="flex items-center space-x-2">
|
||||||
|
<DownloadIcon size={iconSize} />
|
||||||
<p>Wordmark dark variant</p>
|
<p>Wordmark dark variant</p>
|
||||||
|
</div>
|
||||||
|
<span class={badgeButtonStyles}>.svg</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Dialog.Footer
|
<Dialog.Footer>
|
||||||
class="mt-3 text-xs text-neutral-600 dark:text-neutral-400"
|
<div
|
||||||
|
class="mt-2 flex w-full items-center justify-center text-sm text-neutral-600 dark:text-neutral-400"
|
||||||
>
|
>
|
||||||
<p>
|
<p class="w-full text-center text-sm">
|
||||||
Remember to request permission from the creators for the use of the
|
Please ensure you have permission from the creators before using the
|
||||||
SVG. Modification is not allowed.
|
SVG. Modifications are not permitted.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Snippet } from "svelte";
|
|
||||||
import * as Dialog from "@/components/ui/dialog";
|
|
||||||
import CodeBlock from "@/components/codeBlock.svelte";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
children: Snippet;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { children }: Props = $props();
|
|
||||||
|
|
||||||
let registryCode = `
|
|
||||||
"registries": {
|
|
||||||
"@svgl": "https://svgl.app/r/{name}.json"
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Dialog.Root>
|
|
||||||
<Dialog.Trigger>
|
|
||||||
{@render children()}
|
|
||||||
</Dialog.Trigger>
|
|
||||||
<Dialog.Content class="text-sm">
|
|
||||||
<Dialog.Header>
|
|
||||||
<Dialog.Title>Setup shadcn/ui registry</Dialog.Title>
|
|
||||||
<Dialog.Description>
|
|
||||||
Use the code below to configure the @svgl registry for your project.
|
|
||||||
</Dialog.Description>
|
|
||||||
</Dialog.Header>
|
|
||||||
<p>
|
|
||||||
1. Copy and paste the code into
|
|
||||||
<span class="font-mono">components.json</span>:
|
|
||||||
</p>
|
|
||||||
<CodeBlock code={registryCode} />
|
|
||||||
<p class="mt-2">2. Then use the following command to add SVGs:</p>
|
|
||||||
<CodeBlock code="npx shadcn@latest add @svgl/[svg-name]" />
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Root>
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { iSVG } from "@/types/svg";
|
import type { iSVG } from "@/types/svg";
|
||||||
|
|
||||||
import { cn } from "@/utils/cn";
|
import { cn } from "@/utils/cn";
|
||||||
import { mode } from "mode-watcher";
|
import { mode } from "mode-watcher";
|
||||||
|
import { getSvgImgUrl } from "@/data";
|
||||||
|
|
||||||
// Icons:
|
// Icons:
|
||||||
import XIcon from "@lucide/svelte/icons/x";
|
import XIcon from "@lucide/svelte/icons/x";
|
||||||
@@ -32,7 +34,6 @@
|
|||||||
// States:
|
// States:
|
||||||
let wordmarkSvg = $state<boolean>(false);
|
let wordmarkSvg = $state<boolean>(false);
|
||||||
let moreTagsOptions = $state<boolean>(false);
|
let moreTagsOptions = $state<boolean>(false);
|
||||||
let changeThemeMode = $state<boolean>(false);
|
|
||||||
|
|
||||||
// Icon Stroke & Size:
|
// Icon Stroke & Size:
|
||||||
let iconStroke = 1.8;
|
let iconStroke = 1.8;
|
||||||
@@ -45,9 +46,9 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class={cn(
|
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",
|
"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 -->
|
<!-- Image Options -->
|
||||||
@@ -69,69 +70,43 @@
|
|||||||
<AddToFavorite svg={svgInfo} />
|
<AddToFavorite svg={svgInfo} />
|
||||||
</div>
|
</div>
|
||||||
<!-- Image -->
|
<!-- Image -->
|
||||||
{#if wordmarkSvg == true && svgInfo.wordmark !== undefined}
|
{#if wordmarkSvg && svgInfo.wordmark !== undefined}
|
||||||
{#if changeThemeMode}
|
|
||||||
<img
|
<img
|
||||||
class={cn("block", globalImageStyles)}
|
loading="lazy"
|
||||||
src={typeof svgInfo.wordmark !== "string"
|
class={cn("hidden dark:block", globalImageStyles)}
|
||||||
? mode.current === "dark"
|
src={getSvgImgUrl({ url: svgInfo.wordmark, isDark: true })}
|
||||||
? svgInfo.wordmark?.light || ""
|
|
||||||
: svgInfo.wordmark?.dark || ""
|
|
||||||
: svgInfo.wordmark || ""}
|
|
||||||
alt={svgInfo.title}
|
alt={svgInfo.title}
|
||||||
title={svgInfo.title}
|
title={svgInfo.title}
|
||||||
|
width="140"
|
||||||
|
height="40"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
class={cn("block dark:hidden", globalImageStyles)}
|
||||||
|
src={getSvgImgUrl({ url: svgInfo.wordmark, isDark: false })}
|
||||||
|
alt={svgInfo.title}
|
||||||
|
title={svgInfo.title}
|
||||||
|
width="140"
|
||||||
|
height="40"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<img
|
<img
|
||||||
|
loading="lazy"
|
||||||
class={cn("hidden dark:block", globalImageStyles)}
|
class={cn("hidden dark:block", globalImageStyles)}
|
||||||
src={typeof svgInfo.wordmark !== "string"
|
src={getSvgImgUrl({ url: svgInfo.route, isDark: true })}
|
||||||
? svgInfo.wordmark?.dark || ""
|
|
||||||
: svgInfo.wordmark || ""}
|
|
||||||
alt={svgInfo.title}
|
alt={svgInfo.title}
|
||||||
title={svgInfo.title}
|
title={svgInfo.title}
|
||||||
loading="lazy"
|
width="140"
|
||||||
|
height="40"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
|
loading="lazy"
|
||||||
class={cn("block dark:hidden", globalImageStyles)}
|
class={cn("block dark:hidden", globalImageStyles)}
|
||||||
src={typeof svgInfo.wordmark !== "string"
|
src={getSvgImgUrl({ url: svgInfo.route, isDark: false })}
|
||||||
? svgInfo.wordmark?.light || ""
|
|
||||||
: svgInfo.wordmark || ""}
|
|
||||||
alt={svgInfo.title}
|
alt={svgInfo.title}
|
||||||
title={svgInfo.title}
|
title={svgInfo.title}
|
||||||
loading="lazy"
|
width="140"
|
||||||
/>
|
height="40"
|
||||||
{/if}
|
|
||||||
{:else if changeThemeMode}
|
|
||||||
<img
|
|
||||||
class={cn("block", globalImageStyles)}
|
|
||||||
src={typeof svgInfo.route !== "string"
|
|
||||||
? mode.current === "dark"
|
|
||||||
? svgInfo.route.light
|
|
||||||
: svgInfo.route.dark
|
|
||||||
: svgInfo.route}
|
|
||||||
alt={svgInfo.title}
|
|
||||||
title={svgInfo.title}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<img
|
|
||||||
class={cn("hidden dark:block", globalImageStyles)}
|
|
||||||
src={typeof svgInfo.route !== "string"
|
|
||||||
? svgInfo.route.dark
|
|
||||||
: svgInfo.route}
|
|
||||||
alt={svgInfo.title}
|
|
||||||
title={svgInfo.title}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
class={cn("block dark:hidden", globalImageStyles)}
|
|
||||||
src={typeof svgInfo.route !== "string"
|
|
||||||
? svgInfo.route.light
|
|
||||||
: svgInfo.route}
|
|
||||||
alt={svgInfo.title}
|
|
||||||
title={svgInfo.title}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
@@ -148,14 +123,14 @@
|
|||||||
href={`/directory/${c.toLowerCase()}`}
|
href={`/directory/${c.toLowerCase()}`}
|
||||||
class={badgeVariants({
|
class={badgeVariants({
|
||||||
variant: "outline",
|
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`}
|
title={`This icon is part of the ${svgInfo.category} category`}
|
||||||
>
|
>
|
||||||
{c}
|
{c}
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if svgInfo.category.length > maxVisibleCategories}
|
{#if svgInfo.category.length > maxVisibleCategories}
|
||||||
<Popover.Root
|
<Popover.Root
|
||||||
open={moreTagsOptions}
|
open={moreTagsOptions}
|
||||||
@@ -164,7 +139,8 @@
|
|||||||
<Popover.Trigger
|
<Popover.Trigger
|
||||||
class={badgeVariants({
|
class={badgeVariants({
|
||||||
variant: "outline",
|
variant: "outline",
|
||||||
class: "cursor-pointer font-mono",
|
class:
|
||||||
|
"cursor-pointer font-mono hover:border-neutral-400 dark:hover:border-neutral-600",
|
||||||
})}
|
})}
|
||||||
title="More Tags"
|
title="More Tags"
|
||||||
>
|
>
|
||||||
@@ -175,11 +151,14 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
<Popover.Content class="flex w-auto flex-col space-y-2">
|
<Popover.Content class="flex w-auto flex-col space-y-2">
|
||||||
<p class="font-medium">More tags:</p>
|
<p class="font-medium">More tags</p>
|
||||||
{#each svgInfo.category.slice(maxVisibleCategories) as c (c)}
|
{#each svgInfo.category.slice(maxVisibleCategories) as c (c)}
|
||||||
<a
|
<a
|
||||||
href={`/directory/${c.toLowerCase()}`}
|
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} />
|
<TagIcon size={15} strokeWidth={1.5} />
|
||||||
<span>{c}</span>
|
<span>{c}</span>
|
||||||
@@ -193,7 +172,8 @@
|
|||||||
href={`/directory/${svgInfo.category.toLowerCase()}`}
|
href={`/directory/${svgInfo.category.toLowerCase()}`}
|
||||||
class={badgeVariants({
|
class={badgeVariants({
|
||||||
variant: "outline",
|
variant: "outline",
|
||||||
class: "cursor-pointer font-mono",
|
class:
|
||||||
|
"cursor-pointer font-mono hover:border-neutral-400 dark:hover:border-neutral-600",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{svgInfo.category}
|
{svgInfo.category}
|
||||||
@@ -221,13 +201,7 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<DownloadSvg
|
<DownloadSvg {svgInfo} isDarkTheme={() => mode.current === "dark"} />
|
||||||
{svgInfo}
|
|
||||||
isDarkTheme={() => {
|
|
||||||
const dark = document.documentElement.classList.contains("dark");
|
|
||||||
return dark;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={svgInfo.url}
|
href={svgInfo.url}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ const buttonVariants = tv({
|
|||||||
default:
|
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",
|
"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:
|
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:
|
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",
|
"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:
|
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}
|
||||||
|
/>
|
||||||
@@ -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
|
bind:ref
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
class={cn(
|
class={cn(
|
||||||
"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 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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
@@ -35,7 +36,11 @@
|
|||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{#if showCloseButton}
|
{#if showCloseButton}
|
||||||
<DialogPrimitive.Close
|
<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 />
|
<XIcon />
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="dialog-title"
|
data-slot="dialog-title"
|
||||||
class={cn("text-lg leading-none font-semibold", className)}
|
class={cn("text-xl leading-none font-semibold", className)}
|
||||||
{...restProps}
|
{...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}
|
||||||
|
/>
|
||||||
@@ -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}
|
{sideOffset}
|
||||||
{align}
|
{align}
|
||||||
class={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|||||||
@@ -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} />
|
||||||
@@ -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 };
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
Toaster as Sonner,
|
Toaster as Sonner,
|
||||||
type ToasterProps as SonnerProps,
|
type ToasterProps as SonnerProps,
|
||||||
} from "svelte-sonner";
|
} from "svelte-sonner";
|
||||||
|
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
import { mode } from "mode-watcher";
|
import { mode } from "mode-watcher";
|
||||||
|
|
||||||
let { ...restProps }: SonnerProps = $props();
|
let { ...restProps }: SonnerProps = $props();
|
||||||
@@ -10,12 +12,19 @@
|
|||||||
|
|
||||||
<Sonner
|
<Sonner
|
||||||
theme={mode.current}
|
theme={mode.current}
|
||||||
class="toaster group"
|
position="bottom-center"
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
|
unstyled: true,
|
||||||
classes: {
|
classes: {
|
||||||
toast:
|
toast: cn(
|
||||||
"group toast dark:group-[.toaster]:bg-neutral-900 group-[.toaster]:font-sans",
|
"w-full max-w-md",
|
||||||
description: "group-[.toast]:text-xs font-mono",
|
"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}
|
{...restProps}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import Root from "./switch.svelte";
|
||||||
|
|
||||||
|
export { Root, Root as Switch };
|
||||||
@@ -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>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
bind:ref
|
bind:ref
|
||||||
data-slot="tabs-list"
|
data-slot="tabs-list"
|
||||||
class={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
bind:ref
|
bind:ref
|
||||||
data-slot="tabs-trigger"
|
data-slot="tabs-trigger"
|
||||||
class={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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
@@ -2,14 +2,70 @@ import type { Extension } from "@/types/extensions";
|
|||||||
|
|
||||||
export const extensions: Extension[] = [
|
export const extensions: Extension[] = [
|
||||||
{
|
{
|
||||||
name: "SVGL for Raycast",
|
name: "SVGL CLI",
|
||||||
description: "Search SVG logos via svgl using Raycast.",
|
description: "A CLI for easily adding SVG icons to your project.",
|
||||||
url: "https://www.raycast.com/1weiho/svgl",
|
url: "https://github.com/sujjeee/svgls",
|
||||||
image:
|
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: {
|
created_by: {
|
||||||
name: "1weiho",
|
name: "sujjeee",
|
||||||
socialUrl: "https://x.com/1weiho",
|
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",
|
"https://github.com/pheralb/svgl/raw/main/static/library/powertoys.svg",
|
||||||
created_by: {
|
created_by: {
|
||||||
name: "SameerJS6",
|
name: "SameerJS6",
|
||||||
socialUrl: "https://svgl.sameerjs.com/",
|
socialUrl: "https://x.com/Sameerjs6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "SVGL for React",
|
name: "SVGL for Raycast",
|
||||||
description:
|
description: "Search SVG logos via svgl.",
|
||||||
"An open-source NPM package that offers a collection of high-quality SVGL logos for React.",
|
url: "https://www.raycast.com/1weiho/svgl",
|
||||||
url: "https://github.com/ridemountainpig/svgl-react?tab=readme-ov-file",
|
|
||||||
image:
|
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: {
|
created_by: {
|
||||||
name: "ridemountainpig",
|
name: "1weiho",
|
||||||
socialUrl: "https://twitter.com/ridemountainpig",
|
socialUrl: "https://x.com/1weiho",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "SVGL Badges",
|
name: "SVGL for Visual Studio Code",
|
||||||
description: "A beautiful badges with svgl SVG logos.",
|
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/",
|
url: "https://svgl-badge.vercel.app/",
|
||||||
image:
|
image:
|
||||||
"https://camo.githubusercontent.com/b516f0f725ad1827dd854f16ec08626569d02ab827cd06b4f42163e519b813c7/68747470733a2f2f7376676c2d62616467652e76657263656c2e6170702f6170692f4c6962726172792f5376676c3f7468656d653d6c69676874",
|
"https://camo.githubusercontent.com/b516f0f725ad1827dd854f16ec08626569d02ab827cd06b4f42163e519b813c7/68747470733a2f2f7376676c2d62616467652e76657263656c2e6170702f6170692f4c6962726172792f5376676c3f7468656d653d6c69676874",
|
||||||
created_by: {
|
created_by: {
|
||||||
name: "ridemountainpig",
|
name: "ridemountainpig",
|
||||||
socialUrl: "https://twitter.com/ridemountainpig",
|
socialUrl: "https://x.com/ridemountainpig",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Magic",
|
name: "SVGL on Magic by 21st",
|
||||||
description: "AI extension for Cursor & other IDEs.",
|
description: "Integrate company logos and icons via SVGL on Magic.",
|
||||||
url: "https://21st.dev/magic",
|
url: "https://21st.dev/magic",
|
||||||
image:
|
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: {
|
created_by: {
|
||||||
name: "serafim",
|
name: "serafimcloud",
|
||||||
socialUrl: "https://x.com/serafimcloud",
|
socialUrl: "https://x.com/serafimcloud",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "svgls",
|
name: "SVGL for PowerShell",
|
||||||
description: "A CLI for easily adding SVG icons to your project.",
|
description: "PowerShell extension to quickly get svgl logos anywhere.",
|
||||||
url: "https://github.com/sujjeee/svgls",
|
url: "https://github.com/spaansba/SVGL-PowerShell",
|
||||||
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",
|
|
||||||
image:
|
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: {
|
created_by: {
|
||||||
name: "GiR",
|
name: "Bart Spaans",
|
||||||
socialUrl: "https://x.com/girlazote",
|
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
@@ -1,11 +1,19 @@
|
|||||||
import type { iSVG } from "@/types/svg";
|
import type { iSVG, ThemeOptions } from "@/types/svg";
|
||||||
import { svgs } from "./svgs";
|
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) => {
|
export const svgsData = svgs.map((svg: iSVG, index: number) => {
|
||||||
return { id: index, ...svg };
|
return { id: index, ...svg };
|
||||||
}) as iSVG[];
|
}) 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
|
const categories = svgs
|
||||||
.flatMap((svg) =>
|
.flatMap((svg) =>
|
||||||
Array.isArray(svg.category) ? svg.category : [svg.category],
|
Array.isArray(svg.category) ? svg.category : [svg.category],
|
||||||
@@ -14,14 +22,23 @@ export const getCategories = () => {
|
|||||||
return categories;
|
return categories;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCategoriesForDirectory = () => {
|
export const getSvgsByCategory = (category: string): iSVG[] =>
|
||||||
const categories = svgs
|
svgsData.filter((svg: iSVG) => {
|
||||||
.flatMap((svg) =>
|
if (Array.isArray(svg.category)) {
|
||||||
Array.isArray(svg.category) ? svg.category : [svg.category],
|
return svg.category.some(
|
||||||
)
|
(categoryItem) => categoryItem.toLowerCase() === category.toLowerCase(),
|
||||||
.filter((category, index, array) => array.indexOf(category) === index)
|
);
|
||||||
.map((category) => ({
|
} else {
|
||||||
slug: category.toLowerCase(),
|
return svg.category.toLowerCase() === category.toLowerCase();
|
||||||
}));
|
}
|
||||||
return categories;
|
});
|
||||||
|
|
||||||
|
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
@@ -84,6 +84,12 @@ export const svgs: iSVG[] = [
|
|||||||
route: "/library/amazon-q.svg",
|
route: "/library/amazon-q.svg",
|
||||||
url: "https://aws.amazon.com/q",
|
url: "https://aws.amazon.com/q",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Mulesoft",
|
||||||
|
category: "Software",
|
||||||
|
route: "/library/mulesoft.svg",
|
||||||
|
url: "https://www.mulesoft.com/",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "UV",
|
title: "UV",
|
||||||
category: "Devtool",
|
category: "Devtool",
|
||||||
@@ -103,7 +109,6 @@ export const svgs: iSVG[] = [
|
|||||||
},
|
},
|
||||||
url: "https://milanote.com",
|
url: "https://milanote.com",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "Together AI",
|
title: "Together AI",
|
||||||
category: "AI",
|
category: "AI",
|
||||||
@@ -260,9 +265,68 @@ export const svgs: iSVG[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Nuxt",
|
title: "Nuxt",
|
||||||
category: "Framework",
|
category: ["Framework", "Nuxt"],
|
||||||
route: "/library/nuxt.svg",
|
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",
|
brandUrl: "https://nuxt.com/design-kit",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2323,12 +2387,6 @@ export const svgs: iSVG[] = [
|
|||||||
url: "https://www.webflow.com",
|
url: "https://www.webflow.com",
|
||||||
brandUrl: "https://brand-at.webflow.io/resources",
|
brandUrl: "https://brand-at.webflow.io/resources",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "Sanity",
|
|
||||||
category: "CMS",
|
|
||||||
route: "/library/sanity.svg",
|
|
||||||
url: "https://www.sanity.io",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "sky",
|
title: "sky",
|
||||||
category: "Entertainment",
|
category: "Entertainment",
|
||||||
@@ -3596,19 +3654,6 @@ export const svgs: iSVG[] = [
|
|||||||
brandUrl:
|
brandUrl:
|
||||||
"https://www.figma.com/file/YYn36CxVpcT6aPKDXIH9JG/CurseForge-Brandbook?type=design&node-id=0-1&t=dvC0gPtyP36PQdsi-0",
|
"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",
|
title: "Ghostty",
|
||||||
category: ["Software"],
|
category: ["Software"],
|
||||||
@@ -3659,6 +3704,12 @@ export const svgs: iSVG[] = [
|
|||||||
route: "/library/firebase-studio.svg",
|
route: "/library/firebase-studio.svg",
|
||||||
url: "https://firebase.studio/",
|
url: "https://firebase.studio/",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Dingocoin",
|
||||||
|
category: "Crypto",
|
||||||
|
route: "/library/dingocoin.svg",
|
||||||
|
url: "https://dingocoin.com/",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "HeroUI",
|
title: "HeroUI",
|
||||||
category: "Library",
|
category: "Library",
|
||||||
@@ -3778,6 +3829,15 @@ export const svgs: iSVG[] = [
|
|||||||
route: "/library/google-cloud.svg",
|
route: "/library/google-cloud.svg",
|
||||||
url: "https://cloud.google.com/",
|
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",
|
title: "Effect TS",
|
||||||
category: "Library",
|
category: "Library",
|
||||||
@@ -3807,6 +3867,17 @@ export const svgs: iSVG[] = [
|
|||||||
route: "/library/eslint.svg",
|
route: "/library/eslint.svg",
|
||||||
url: "https://eslint.org/",
|
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",
|
title: "PlainSignal",
|
||||||
category: "Analytics",
|
category: "Analytics",
|
||||||
@@ -3865,4 +3936,55 @@ export const svgs: iSVG[] = [
|
|||||||
url: "https://ahrefs.com/",
|
url: "https://ahrefs.com/",
|
||||||
brandUrl: "https://ahrefs.com/logo",
|
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
@@ -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.
|
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
|
```bash
|
||||||
https://api.svgl.app
|
https://api.svgl.app
|
||||||
# or
|
```
|
||||||
|
|
||||||
|
Categories URL:
|
||||||
|
|
||||||
|
```bash
|
||||||
https://api.svgl.app/categories
|
https://api.svgl.app/categories
|
||||||
```
|
```
|
||||||
|
|
||||||
## Typescript usage
|
## Typescript
|
||||||
|
|
||||||
- For categories:
|
You can use the following types for the SVG responses:
|
||||||
|
|
||||||
```ts
|
|
||||||
export interface Category {
|
|
||||||
category: string;
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- For SVGs:
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
export type ThemeOptions = {
|
export type ThemeOptions = {
|
||||||
@@ -42,27 +37,27 @@ export type ThemeOptions = {
|
|||||||
light: string;
|
light: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface iSVG {
|
export interface SVG {
|
||||||
id?: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
category: tCategory | tCategory[];
|
category: string | string[];
|
||||||
route: string | ThemeOptions;
|
route: string | ThemeOptions;
|
||||||
|
url: string;
|
||||||
wordmark?: string | ThemeOptions;
|
wordmark?: string | ThemeOptions;
|
||||||
brandUrl?: string;
|
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
|
## Endpoints
|
||||||
|
|
||||||
|
### Get all SVGs
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
https://api.svgl.app
|
https://api.svgl.app
|
||||||
```
|
```
|
||||||
|
|
||||||
<p></p>
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
// Returns:
|
// Returns:
|
||||||
[
|
[
|
||||||
@@ -77,12 +72,12 @@ https://api.svgl.app
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Get all SVGs with limit
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
https://api.svgl.app?limit=10
|
https://api.svgl.app?limit=10
|
||||||
```
|
```
|
||||||
|
|
||||||
<p></p>
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
// Returns:
|
// Returns:
|
||||||
[
|
[
|
||||||
@@ -97,12 +92,12 @@ https://api.svgl.app?limit=10
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Get SVGs by category
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
https://api.svgl.app/category/software
|
https://api.svgl.app/category/software
|
||||||
```
|
```
|
||||||
|
|
||||||
<p></p>
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
// Returns:
|
// 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
|
```bash
|
||||||
https://api.svgl.app/svg/adobe.svg
|
https://api.svgl.app/svg/adobe.svg
|
||||||
```
|
```
|
||||||
|
|
||||||
<p></p>
|
No optimized SVG:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
https://api.svgl.app/svg/adobe.svg?no-optimize
|
||||||
|
```
|
||||||
|
|
||||||
```html
|
```html
|
||||||
// Returns:
|
<!-- Returns: -->
|
||||||
<svg
|
<svg
|
||||||
width="91"
|
width="91"
|
||||||
height="80"
|
height="80"
|
||||||
@@ -150,33 +153,12 @@ https://api.svgl.app/svg/adobe.svg
|
|||||||
</svg>
|
</svg>
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
### Search SVG by title
|
||||||
https://api.svgl.app/categories
|
|
||||||
```
|
|
||||||
|
|
||||||
<p></p>
|
|
||||||
|
|
||||||
```json
|
|
||||||
// Returns:
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"category": "Software",
|
|
||||||
"total": 97
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"category": "Library",
|
|
||||||
"total": 25
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
https://api.svgl.app?search=axiom
|
https://api.svgl.app?search=axiom
|
||||||
```
|
```
|
||||||
|
|
||||||
<p></p>
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
// Returns:
|
// 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
|
||||||
|
}
|
||||||
|
//...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|||||||
@@ -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
-1
@@ -1,7 +1,14 @@
|
|||||||
export const globals = {
|
export const globals = {
|
||||||
githubUrl: "https://github.com/pheralb/svgl",
|
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_",
|
twitterUrl: "https://x.com/pheralb_",
|
||||||
submitUrl:
|
submitUrl:
|
||||||
"https://github.com/pheralb/svgl?tab=readme-ov-file#-getting-started",
|
"https://github.com/pheralb/svgl?tab=readme-ov-file#-getting-started",
|
||||||
|
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=",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -10,17 +10,14 @@
|
|||||||
// Providers:
|
// Providers:
|
||||||
import { ModeWatcher } from "mode-watcher";
|
import { ModeWatcher } from "mode-watcher";
|
||||||
import Sidebar from "@/components/layout/sidebar.svelte";
|
import Sidebar from "@/components/layout/sidebar.svelte";
|
||||||
import ViewTransitions from "@/components/viewTransitions.svelte";
|
|
||||||
import Sonner from "@/components/ui/sonner/sonner.svelte";
|
import Sonner from "@/components/ui/sonner/sonner.svelte";
|
||||||
|
|
||||||
// SSR Data:
|
let { children }: LayoutProps = $props();
|
||||||
let { data, children }: LayoutProps = $props();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModeWatcher />
|
<ModeWatcher />
|
||||||
<ViewTransitions />
|
|
||||||
<Sonner />
|
<Sonner />
|
||||||
<Header githubStars={data?.stars} />
|
<Header />
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|||||||
+65
-14
@@ -1,10 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { iSVG } from "@/types/svg";
|
import type { iSVG } from "@/types/svg";
|
||||||
import type { PageProps } from "./$types";
|
import type { PageProps } from "./$types";
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
import { cn } from "@/utils/cn";
|
import { cn } from "@/utils/cn";
|
||||||
|
import { deleteParam } from "@/utils/searchParams";
|
||||||
import { svgsData } from "@/data";
|
import { svgsData } from "@/data";
|
||||||
import { searchWithFuse } from "@/utils/searchWithFuse";
|
import { searchSvgsWithFuse } from "@/utils/searchWithFuse";
|
||||||
|
|
||||||
// Components:
|
// Components:
|
||||||
import Grid from "@/components/grid.svelte";
|
import Grid from "@/components/grid.svelte";
|
||||||
@@ -12,11 +14,16 @@
|
|||||||
import SvgCard from "@/components/svgs/svgCard.svelte";
|
import SvgCard from "@/components/svgs/svgCard.svelte";
|
||||||
import SortSvgs from "@/components/svgs/sortSvgs.svelte";
|
import SortSvgs from "@/components/svgs/sortSvgs.svelte";
|
||||||
import Container from "@/components/container.svelte";
|
import Container from "@/components/container.svelte";
|
||||||
|
import SearchXIcon from "@lucide/svelte/icons/search-x";
|
||||||
|
|
||||||
import PageCard from "@/components/pageCard.svelte";
|
import PageCard from "@/components/pageCard.svelte";
|
||||||
import PageHeader from "@/components/pageHeader.svelte";
|
|
||||||
import FolderIcon from "@lucide/svelte/icons/folder";
|
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:
|
// SSR Data:
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
@@ -41,17 +48,10 @@
|
|||||||
updateDisplaySvgs();
|
updateDisplaySvgs();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (searchTerm.length < 3) {
|
const baseData = sorted ? alphabeticallySorted : latestSorted;
|
||||||
filteredSvgs = (sorted ? alphabeticallySorted : latestSorted).filter(
|
filteredSvgs = searchSvgsWithFuse(baseData)
|
||||||
(svg: iSVG) =>
|
|
||||||
svg.title.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
filteredSvgs = searchWithFuse(filteredSvgs)
|
|
||||||
.search(searchTerm)
|
.search(searchTerm)
|
||||||
.map((result) => result.item);
|
.map((result) => result.item);
|
||||||
}
|
|
||||||
|
|
||||||
updateDisplaySvgs();
|
updateDisplaySvgs();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,6 +60,13 @@
|
|||||||
searchSvgs();
|
searchSvgs();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClearSearch = () => {
|
||||||
|
searchTerm = "";
|
||||||
|
filteredSvgs = sorted ? alphabeticallySorted : latestSorted;
|
||||||
|
deleteParam("search");
|
||||||
|
updateDisplaySvgs();
|
||||||
|
};
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
updateDisplaySvgs();
|
updateDisplaySvgs();
|
||||||
});
|
});
|
||||||
@@ -75,7 +82,10 @@
|
|||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageCard>
|
<PageCard
|
||||||
|
containerClass="mt-2"
|
||||||
|
contentCardClass="max-h-[calc(100vh-7.6rem)] min-h-[calc(100vh-7.6rem)]"
|
||||||
|
>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<div
|
<div
|
||||||
class="flex items-center space-x-2 text-neutral-500 dark:text-neutral-400"
|
class="flex items-center space-x-2 text-neutral-500 dark:text-neutral-400"
|
||||||
@@ -87,13 +97,21 @@
|
|||||||
<span>logos</span>
|
<span>logos</span>
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{: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>
|
<p>
|
||||||
<span class="font-mono">{filteredSvgs.length}</span>
|
<span class="font-mono">{filteredSvgs.length}</span>
|
||||||
<span>logos</span>
|
<span>logos</span>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
<SortSvgs
|
<SortSvgs
|
||||||
className={cn(filteredSvgs.length === 0 && "hidden")}
|
className={cn(filteredSvgs.length === 0 && "hidden")}
|
||||||
isSorted={sorted}
|
isSorted={sorted}
|
||||||
@@ -102,12 +120,45 @@
|
|||||||
searchSvgs();
|
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>
|
</PageHeader>
|
||||||
|
{#if browser}
|
||||||
|
<WarningMessage />
|
||||||
|
{/if}
|
||||||
<Container className="my-6">
|
<Container className="my-6">
|
||||||
<Grid>
|
<Grid>
|
||||||
{#each displaySvgs as svg (svg.id)}
|
{#each displaySvgs as svg (svg.id)}
|
||||||
<SvgCard svgInfo={svg} />
|
<SvgCard svgInfo={svg} />
|
||||||
{/each}
|
{/each}
|
||||||
</Grid>
|
</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>
|
</Container>
|
||||||
</PageCard>
|
</PageCard>
|
||||||
|
|||||||
+2
-8
@@ -2,7 +2,7 @@ import type { iSVG } from "@/types/svg";
|
|||||||
import type { Load } from "@sveltejs/kit";
|
import type { Load } from "@sveltejs/kit";
|
||||||
|
|
||||||
import { svgsData } from "@/data";
|
import { svgsData } from "@/data";
|
||||||
import { searchWithFuse } from "@/utils/searchWithFuse";
|
import { searchSvgsWithFuse } from "@/utils/searchWithFuse";
|
||||||
|
|
||||||
export const load: Load = ({ url }) => {
|
export const load: Load = ({ url }) => {
|
||||||
const searchParam = url.searchParams.get("search") || "";
|
const searchParam = url.searchParams.get("search") || "";
|
||||||
@@ -17,17 +17,11 @@ export const load: Load = ({ url }) => {
|
|||||||
if (!searchParam) {
|
if (!searchParam) {
|
||||||
filteredSvgs = sortParam ? alphabeticallySorted : latestSorted;
|
filteredSvgs = sortParam ? alphabeticallySorted : latestSorted;
|
||||||
} else {
|
} else {
|
||||||
if (searchParam.length < 3) {
|
|
||||||
const baseData = sortParam ? alphabeticallySorted : latestSorted;
|
const baseData = sortParam ? alphabeticallySorted : latestSorted;
|
||||||
filteredSvgs = baseData.filter((svg: iSVG) =>
|
filteredSvgs = searchSvgsWithFuse(baseData)
|
||||||
svg.title.toLowerCase().includes(searchParam.toLowerCase()),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
filteredSvgs = searchWithFuse(filteredSvgs)
|
|
||||||
.search(searchParam)
|
.search(searchParam)
|
||||||
.map((result) => result.item);
|
.map((result) => result.item);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
searchTerm: searchParam,
|
searchTerm: searchParam,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -12,14 +12,18 @@ export const POST = async ({ request }: RequestEvent) => {
|
|||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|
||||||
const svgCode = body.code;
|
let svgCode = body.code;
|
||||||
const typescript = body.typescript;
|
const typescript = body.typescript;
|
||||||
const name = body.name.replace(/[^a-zA-Z0-9]/g, "");
|
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({
|
const code = await parseReactSvgContent({
|
||||||
componentName: name,
|
componentName: name,
|
||||||
svgCode: optimizedSvg,
|
svgCode: svgCode,
|
||||||
typescript,
|
typescript,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { redirect } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const load = async () => {
|
||||||
|
return redirect(307, "/");
|
||||||
|
};
|
||||||
@@ -2,113 +2,113 @@
|
|||||||
import type { iSVG } from "@/types/svg";
|
import type { iSVG } from "@/types/svg";
|
||||||
import type { PageProps } from "./$types";
|
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 { cn } from "@/utils/cn";
|
||||||
import { searchWithFuse } from "@/utils/searchWithFuse";
|
import { searchSvgsWithFuse } from "@/utils/searchWithFuse";
|
||||||
|
|
||||||
// Components:
|
// Components:
|
||||||
import Grid from "@/components/grid.svelte";
|
import Grid from "@/components/grid.svelte";
|
||||||
import Search from "@/components/search.svelte";
|
import Search from "@/components/search.svelte";
|
||||||
import SvgCard from "@/components/svgs/svgCard.svelte";
|
import SvgCard from "@/components/svgs/svgCard.svelte";
|
||||||
import Container from "@/components/container.svelte";
|
import Container from "@/components/container.svelte";
|
||||||
|
import SearchXIcon from "@lucide/svelte/icons/search-x";
|
||||||
|
|
||||||
import PageCard from "@/components/pageCard.svelte";
|
import PageCard from "@/components/pageCard.svelte";
|
||||||
import PageHeader from "@/components/pageHeader.svelte";
|
import PageHeader from "@/components/pageHeader.svelte";
|
||||||
import FolderIcon from "@lucide/svelte/icons/folder-open";
|
import FolderIcon from "@lucide/svelte/icons/folder-open";
|
||||||
import ArrowLeftIcon from "@lucide/svelte/icons/arrow-left";
|
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:
|
// SSR Data:
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
const directoryData = $derived(data);
|
const directoryData = $derived(data);
|
||||||
|
|
||||||
// States:
|
// States:
|
||||||
|
let maxDisplay = 30;
|
||||||
let searchTerm = $state<string>(data.searchTerm || "");
|
let searchTerm = $state<string>(data.searchTerm || "");
|
||||||
let filteredSvgs = $derived<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 = () => {
|
const searchSvgs = () => {
|
||||||
if (!searchTerm) {
|
if (!searchTerm) {
|
||||||
filteredSvgs = data.svgs;
|
filteredSvgs = sorted ? data.alphabeticallySorted : data.latestSorted;
|
||||||
|
updateDisplaySvgs();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (searchTerm.length < 3) {
|
const baseData = sorted ? data.alphabeticallySorted : data.latestSorted;
|
||||||
filteredSvgs = data.svgs.filter((svg: iSVG) =>
|
filteredSvgs = searchSvgsWithFuse(baseData)
|
||||||
svg.title.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
filteredSvgs = searchWithFuse(data.svgs)
|
|
||||||
.search(searchTerm)
|
.search(searchTerm)
|
||||||
.map((result) => result.item);
|
.map((result) => result.item);
|
||||||
}
|
updateDisplaySvgs();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = (value: string) => {
|
const handleSearch = (value: string) => {
|
||||||
searchTerm = value;
|
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();
|
searchSvgs();
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatCategory = (category: string) =>
|
const handleClearSearch = () => {
|
||||||
category.charAt(0).toUpperCase() + category.slice(1);
|
searchTerm = "";
|
||||||
|
deleteParam("search");
|
||||||
|
updateDisplaySvgs();
|
||||||
|
};
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
filteredSvgs = data.svgs.filter((svg: iSVG) =>
|
updateDisplaySvgs();
|
||||||
svg.title.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{formatCategory(directoryData.category)} SVG logos - Svgl</title>
|
<title>{directoryData.category} SVG logos - Svgl</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<Search
|
<Search
|
||||||
searchValue={searchTerm}
|
searchValue={searchTerm}
|
||||||
onSearch={handleSearch}
|
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>
|
<PageHeader>
|
||||||
<div
|
<div
|
||||||
class="flex items-center space-x-2 font-medium text-neutral-950 dark:text-neutral-50"
|
class="flex items-center space-x-2 font-medium text-neutral-950 dark:text-neutral-50"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class={cn(
|
class={cn(buttonVariants({ variant: "ghost", size: "icon" }))}
|
||||||
buttonVariants({ class: "group", variant: "ghost", size: "icon" }),
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon
|
<ArrowLeftIcon size={18} strokeWidth={1.5} />
|
||||||
size={18}
|
|
||||||
strokeWidth={1.5}
|
|
||||||
class="transition-transform group-hover:translate-x-[-2px]"
|
|
||||||
/>
|
|
||||||
</a>
|
</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>
|
<p>
|
||||||
{formatCategory(directoryData.category)}
|
{directoryData.category}
|
||||||
</p>
|
</p>
|
||||||
<span>-</span>
|
<span>-</span>
|
||||||
{#if !searchTerm}
|
{#if !searchTerm}
|
||||||
<p>
|
<p>
|
||||||
<span>{data.svgs.length} SVGs </span>
|
<span>{data.initialSvgs.length} SVGs </span>
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p>
|
<p>
|
||||||
@@ -117,6 +117,14 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<SortSvgs
|
||||||
|
className={cn(filteredSvgs.length === 0 && "hidden")}
|
||||||
|
isSorted={sorted}
|
||||||
|
onSortedChange={(value) => {
|
||||||
|
sorted = value;
|
||||||
|
searchSvgs();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Container className="my-6">
|
<Container className="my-6">
|
||||||
<Grid>
|
<Grid>
|
||||||
@@ -124,5 +132,8 @@
|
|||||||
<SvgCard svgInfo={svg} />
|
<SvgCard svgInfo={svg} />
|
||||||
{/each}
|
{/each}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{#if filteredSvgs.length === 0}
|
||||||
|
<SvgNotFound svgTitle={searchTerm} category={directoryData.category} />
|
||||||
|
{/if}
|
||||||
</Container>
|
</Container>
|
||||||
</PageCard>
|
</PageCard>
|
||||||
|
|||||||
@@ -1,48 +1,43 @@
|
|||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
import type { iSVG } from "@/types/svg";
|
import type { iSVG } from "@/types/svg";
|
||||||
|
|
||||||
import { svgs } from "@/data/svgs";
|
|
||||||
import { error } from "@sveltejs/kit";
|
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 }) => {
|
export const load: PageLoad = (async ({ params, url }) => {
|
||||||
const { category } = params;
|
const { category } = params;
|
||||||
const searchParam = url.searchParams.get("search") || "";
|
const searchParam = url.searchParams.get("search") || "";
|
||||||
|
const sortParam = url.searchParams.get("sort") === "alphabetical";
|
||||||
|
|
||||||
const svgsByCategory = svgs.filter((svg: iSVG) => {
|
const svgsByCategory = getSvgsByCategory(category);
|
||||||
if (Array.isArray(svg.category)) {
|
|
||||||
return svg.category.some(
|
|
||||||
(categoryItem) => categoryItem.toLowerCase() === category.toLowerCase(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return svg.category.toLowerCase() === category.toLowerCase();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (svgsByCategory.length === 0) {
|
if (!svgsByCategory.length) {
|
||||||
throw error(404, "Category not found");
|
throw error(404, "Category not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
let filteredSvgs: iSVG[] = [];
|
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) {
|
if (!searchParam) {
|
||||||
filteredSvgs = svgsByCategory;
|
filteredSvgs = sortParam ? alphabeticallySorted : latestSorted;
|
||||||
} else {
|
} else {
|
||||||
if (searchParam.length < 3) {
|
const baseData = sortParam ? alphabeticallySorted : latestSorted;
|
||||||
filteredSvgs = svgsByCategory.filter((svg: iSVG) =>
|
filteredSvgs = searchSvgsWithFuse(baseData)
|
||||||
svg.title.toLowerCase().includes(searchParam.toLowerCase()),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
filteredSvgs = searchWithFuse(svgsByCategory)
|
|
||||||
.search(searchParam)
|
.search(searchParam)
|
||||||
.map((result) => result.item);
|
.map((result) => result.item);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
category: category,
|
category: formatCategory,
|
||||||
searchTerm: searchParam,
|
searchTerm: searchParam,
|
||||||
svgs: svgsByCategory,
|
sorted: sortParam,
|
||||||
filteredSvgs: filteredSvgs,
|
initialSvgs: filteredSvgs,
|
||||||
|
latestSorted,
|
||||||
|
alphabeticallySorted,
|
||||||
};
|
};
|
||||||
}) satisfies PageLoad;
|
}) satisfies PageLoad;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { redirect } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const load = async () => {
|
||||||
|
return redirect(307, "/");
|
||||||
|
};
|
||||||
@@ -1,13 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageProps } from "./$types";
|
import type { PageProps } from "./$types";
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
|
||||||
import PageCard from "@/components/pageCard.svelte";
|
import PageCard from "@/components/pageCard.svelte";
|
||||||
import PageHeader from "@/components/pageHeader.svelte";
|
|
||||||
|
|
||||||
import Container from "@/components/container.svelte";
|
import Container from "@/components/container.svelte";
|
||||||
|
import PageHeader from "@/components/pageHeader.svelte";
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
import * as Collapsible from "@/components/ui/collapsible";
|
||||||
|
import DocumentSettings from "@/components/documentSettings.svelte";
|
||||||
|
import TableOfContents from "@/components/tableOfContents/tableOfContents.svelte";
|
||||||
|
|
||||||
import FileText from "@lucide/svelte/icons/file-text";
|
import FileText from "@lucide/svelte/icons/file-text";
|
||||||
|
import ChevronDown from "@lucide/svelte/icons/chevron-down";
|
||||||
|
|
||||||
|
// Markdown:
|
||||||
|
import "@/styles/markdown.css";
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
|
let tocOpen = $state(false);
|
||||||
const document = $derived(data.document);
|
const document = $derived(data.document);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -26,8 +36,48 @@
|
|||||||
{document.title}
|
{document.title}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<DocumentSettings
|
||||||
|
rawUrl={document.rawUrl}
|
||||||
|
documentContent={document.content}
|
||||||
|
documentUrl={document.documentUrl}
|
||||||
|
/>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Container className="my-6">
|
<Collapsible.Root class="block lg:hidden" bind:open={tocOpen}>
|
||||||
<article>{@html document.html}</article>
|
<Collapsible.Trigger
|
||||||
|
class={buttonVariants({
|
||||||
|
variant: "outline",
|
||||||
|
class: "mb-4 w-full rounded-none border-x-0 border-t-0",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span>On this page</span>
|
||||||
|
<ChevronDown
|
||||||
|
size={14}
|
||||||
|
class={cn(
|
||||||
|
"transition-transform duration-200",
|
||||||
|
tocOpen ? "rotate-180" : "rotate-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Collapsible.Trigger>
|
||||||
|
<Collapsible.Content class="px-4">
|
||||||
|
<TableOfContents toc={document.tableOfContents} />
|
||||||
|
</Collapsible.Content>
|
||||||
|
</Collapsible.Root>
|
||||||
|
<div class="flex min-h-screen gap-8 lg:gap-12">
|
||||||
|
<Container
|
||||||
|
className={cn(
|
||||||
|
"flex-1 mt-8 mb-6 max-w-3xl",
|
||||||
|
"animate-in duration-500 fill-mode-backwards fade-in slide-in-from-bottom-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<article class="markdown">{@html document.html}</article>
|
||||||
</Container>
|
</Container>
|
||||||
|
<aside class="sticky top-20 hidden w-60 flex-shrink-0 self-start lg:block">
|
||||||
|
<div
|
||||||
|
class="mb-2 text-sm font-medium text-neutral-900 dark:text-neutral-50"
|
||||||
|
>
|
||||||
|
On this page
|
||||||
|
</div>
|
||||||
|
<TableOfContents toc={document.tableOfContents} />
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
</PageCard>
|
</PageCard>
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PageProps } from "./$types";
|
||||||
|
import type { Extension } from "@/types/extensions";
|
||||||
|
|
||||||
|
// Utils:
|
||||||
|
import { cn } from "@/utils/cn";
|
||||||
|
import { searchExtensionsWithFuse } from "@/utils/searchWithFuse";
|
||||||
|
|
||||||
|
// UI Components:
|
||||||
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
|
||||||
|
// Page components:
|
||||||
|
import PageCard from "@/components/pageCard.svelte";
|
||||||
|
import PageHeader from "@/components/pageHeader.svelte";
|
||||||
|
import Grid from "@/components/grid.svelte";
|
||||||
|
import Container from "@/components/container.svelte";
|
||||||
|
import ExtensionItem from "@/components/extension.svelte";
|
||||||
|
|
||||||
|
// Svgs:
|
||||||
|
import RocketIcon from "@lucide/svelte/icons/rocket";
|
||||||
|
import PackageOpen from "@lucide/svelte/icons/package-open";
|
||||||
|
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||||
|
import Search from "@/components/search.svelte";
|
||||||
|
|
||||||
|
// SSR Data:
|
||||||
|
let { data }: PageProps = $props();
|
||||||
|
|
||||||
|
// States:
|
||||||
|
let searchTerm = $state<string>(data.searchTerm || "");
|
||||||
|
let filteredExtensions = $state<Extension[]>(data.initialExtensions);
|
||||||
|
|
||||||
|
const searchExtensions = () => {
|
||||||
|
if (!searchTerm) {
|
||||||
|
filteredExtensions = data.allExtensions;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
filteredExtensions = searchExtensionsWithFuse(data.allExtensions)
|
||||||
|
.search(searchTerm)
|
||||||
|
.map((result) => result.item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
searchTerm = value;
|
||||||
|
searchExtensions();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Extensions - Svgl</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Integrate SVGL with your favorite tools and apps to streamline your workflow. Created by the community."
|
||||||
|
/>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<PageCard>
|
||||||
|
<PageHeader>
|
||||||
|
<div
|
||||||
|
class="flex items-center space-x-2 font-medium text-neutral-950 dark:text-neutral-50"
|
||||||
|
>
|
||||||
|
<PackageOpen size={18} strokeWidth={1.5} />
|
||||||
|
<p>Extensions</p>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<section class="flex flex-col px-4 py-12">
|
||||||
|
<h2
|
||||||
|
class={cn(
|
||||||
|
"mb-4 text-center",
|
||||||
|
"text-4xl font-semibold",
|
||||||
|
"animate-in delay-100 duration-500 fill-mode-backwards fade-in slide-in-from-bottom-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Extensions
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
"flex w-full flex-col items-center justify-center space-y-4",
|
||||||
|
"animate-in delay-300 duration-500 fill-mode-backwards fade-in slide-in-from-bottom-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Integrate SVGL with your favorite tools and apps to streamline your
|
||||||
|
workflow. Created by the community.
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<a href="/docs/api" class={cn(buttonVariants({ variant: "radial" }))}>
|
||||||
|
<RocketIcon size={16} strokeWidth={2} />
|
||||||
|
<span>Start Building</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/pheralb/svgl/issues/new/choose"
|
||||||
|
class={cn(buttonVariants({ variant: "outline" }))}
|
||||||
|
>
|
||||||
|
<PlusIcon size={16} strokeWidth={2} />
|
||||||
|
<span>Add your extension</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<Container
|
||||||
|
className={cn(
|
||||||
|
"max-w-4xl",
|
||||||
|
"animate-in delay-500 duration-500 fill-mode-backwards fade-in slide-in-from-bottom-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="sticky top-12 z-10 bg-white/80 pt-4 backdrop-blur-sm dark:bg-transparent"
|
||||||
|
>
|
||||||
|
<Search
|
||||||
|
iconSize={18}
|
||||||
|
searchValue={searchTerm}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
placeholder="Search..."
|
||||||
|
inputClass="text-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Grid columns="2" className="mt-5 mb-7">
|
||||||
|
{#each filteredExtensions as extension (extension.id)}
|
||||||
|
<ExtensionItem data={extension} />
|
||||||
|
{/each}
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
</PageCard>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { Load } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
import { extensionsData } from "@/data";
|
||||||
|
import { searchExtensionsWithFuse } from "@/utils/searchWithFuse";
|
||||||
|
|
||||||
|
export const load: Load = ({ url }) => {
|
||||||
|
const searchParam = url.searchParams.get("search") || "";
|
||||||
|
let filteredExtensions = [...extensionsData];
|
||||||
|
|
||||||
|
if (searchParam) {
|
||||||
|
const fuseSearch = searchExtensionsWithFuse(extensionsData);
|
||||||
|
filteredExtensions = fuseSearch
|
||||||
|
.search(searchParam)
|
||||||
|
.map((result) => result.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchTerm: searchParam,
|
||||||
|
initialExtensions: filteredExtensions,
|
||||||
|
allExtensions: extensionsData,
|
||||||
|
};
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user