Merge branch 'main' into eldadfux-patch-network
@@ -2,9 +2,9 @@ PUBLIC_APPWRITE_COL_MESSAGES_ID=
|
||||
PUBLIC_APPWRITE_COL_THREADS_ID=
|
||||
PUBLIC_APPWRITE_DB_MAIN_ID=
|
||||
PUBLIC_APPWRITE_FN_TLDR_ID=
|
||||
PUBLIC_APPWRITE_DASHBOARD=
|
||||
PUBLIC_APPWRITE_ENDPOINT=
|
||||
PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
|
||||
PUBLIC_APPWRITE_PROJECT_ID=
|
||||
PUBLIC_APPWRITE_DASHBOARD=https://cloud.appwrite.io
|
||||
PUBLIC_APPWRITE_PROJECT_INIT_ID=
|
||||
PUBLIC_GROWTH_ENDPOINT=
|
||||
PUBLIC_POSTHOG_API_KEY=
|
||||
|
||||
1
.github/workflows/production.yml
vendored
@@ -46,6 +46,7 @@ jobs:
|
||||
"APPWRITE_API_KEY_INIT=${{ secrets.APPWRITE_API_KEY_INIT }}"
|
||||
"GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}"
|
||||
"SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}"
|
||||
"SENTRY_RELEASE=${{ github.event.release.tag_name }}"
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
|
||||
46
.github/workflows/tests.yml
vendored
@@ -35,7 +35,51 @@ jobs:
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Check formatting
|
||||
run: pnpm format:check
|
||||
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install corepack
|
||||
run: npm i -g corepack@latest
|
||||
- name: Install pnpm
|
||||
run: corepack enable
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Install playwright dependencies
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
- name: Run tests
|
||||
env:
|
||||
PUBLIC_APPWRITE_ENDPOINT: ${{ vars.PUBLIC_APPWRITE_ENDPOINT }}
|
||||
PUBLIC_APPWRITE_DASHBOARD: ${{ vars.PUBLIC_APPWRITE_DASHBOARD }}
|
||||
PUBLIC_APPWRITE_PROJECT_ID: ${{ vars.PUBLIC_APPWRITE_PROJECT_ID }}
|
||||
PUBLIC_APPWRITE_DB_MAIN_ID: ${{ vars.PUBLIC_APPWRITE_DB_MAIN_ID }}
|
||||
PUBLIC_APPWRITE_COL_THREADS_ID: ${{ vars.PUBLIC_APPWRITE_COL_THREADS_ID }}
|
||||
PUBLIC_APPWRITE_COL_MESSAGES_ID: ${{ vars.PUBLIC_APPWRITE_COL_MESSAGES_ID }}
|
||||
PUBLIC_APPWRITE_FN_TLDR_ID: ${{ vars.PUBLIC_APPWRITE_FN_TLDR_ID }}
|
||||
PUBLIC_APPWRITE_PROJECT_INIT_ID: ${{ vars.PUBLIC_APPWRITE_PROJECT_INIT_ID }}
|
||||
PUBLIC_GROWTH_ENDPOINT: ${{ vars.PUBLIC_GROWTH_ENDPOINT }}
|
||||
PUBLIC_POSTHOG_API_KEY: ${{ vars.PUBLIC_POSTHOG_API_KEY }}
|
||||
APPWRITE_DB_INIT_ID: ${{ secrets.APPWRITE_DB_INIT_ID }}
|
||||
APPWRITE_COL_INIT_ID: ${{ secrets.APPWRITE_COL_INIT_ID }}
|
||||
APPWRITE_API_KEY_INIT: ${{ secrets.APPWRITE_API_KEY_INIT }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: pnpm test
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
2
.gitignore
vendored
@@ -3,6 +3,7 @@ node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/.idea
|
||||
/.zed
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
@@ -20,3 +21,4 @@ terraform/**/.t*
|
||||
terraform/**/.env
|
||||
terraform/**/**/*.tfstate*
|
||||
/.cache
|
||||
/test-results
|
||||
|
||||
@@ -45,6 +45,9 @@ ENV GITHUB_TOKEN ${GITHUB_TOKEN}
|
||||
ARG SENTRY_AUTH_TOKEN
|
||||
ENV SENTRY_AUTH_TOKEN ${SENTRY_AUTH_TOKEN}
|
||||
|
||||
ARG SENTRY_RELEASE
|
||||
ENV SENTRY_RELEASE ${SENTRY_RELEASE}
|
||||
|
||||
ARG PUBLIC_POSTHOG_API_KEY
|
||||
ENV PUBLIC_POSTHOG_API_KEY ${PUBLIC_POSTHOG_API_KEY}
|
||||
|
||||
|
||||
42
STYLE.md
@@ -250,12 +250,19 @@ Split content such that each piece makes sense without reading dependents or exp
|
||||
|
||||
### Headings
|
||||
|
||||
Prefer simple nouns and root form verbs.
|
||||
Navigation labels should be short (ideally, one word) and not have verbs or a directive to keep labels concise. Also make sure not to repeat a term if already under certain context
|
||||
|
||||
✅ Create screen (root verb, noun)
|
||||
✅ Authentication (noun)
|
||||
❌ Authenticating (present participle verb)
|
||||
❌ Create a new screen (too wordy)
|
||||
- ✅ User verification
|
||||
- ✅ Release (under a Policies section)
|
||||
- ❌ Verify user
|
||||
- ❌ Release policies (under a Policies section)
|
||||
|
||||
For content headings, prefer simple nouns and root form verbs.
|
||||
|
||||
- ✅ Create screen (root verb, noun)
|
||||
- ✅ Authentication (noun)
|
||||
- ❌ Authenticating (present participle verb)
|
||||
- ❌ Create a new screen (too wordy)
|
||||
|
||||
Try your best to stick to simple headings, if it's not possible, don't worry and write a full heading if need be.
|
||||
|
||||
@@ -264,19 +271,19 @@ Try your best to stick to simple headings, if it's not possible, don't worry and
|
||||
Avoid unclear [links](https://www.youtube.com/watch?v=dQw4w9WgXcQ) such as learn more [here](https://www.youtube.com/watch?v=dQw4w9WgXcQ).
|
||||
Readers will be unsure where a link may take them. Those using a screen reader will find it especially difficult.
|
||||
|
||||
✅ [Learn more about authentication](https://appwrite.io/docs/products/auth/email-password#login)
|
||||
❌ Learn more about authentication [here](https://www.youtube.com/watch?v=dQw4w9WgXcQ)
|
||||
- ✅ [Learn more about authentication](https://appwrite.io/docs/products/auth/email-password#login)
|
||||
- ❌ Learn more about authentication [here](https://www.youtube.com/watch?v=dQw4w9WgXcQ)
|
||||
|
||||
### Sentences
|
||||
|
||||
Use a directive that's straight to the point when providing an action a developer must perform.
|
||||
The action and verb always comes first, the explanation after.
|
||||
|
||||
✅ Create a new database.
|
||||
✅ Update a document so its permissions include your new users.
|
||||
❌ To allow access, update your permissions.
|
||||
❌ You can create a new database for each tenant.
|
||||
❌ Creating a new bucket lets you set different permissions for images uploaded by users.
|
||||
- ✅ Create a new database.
|
||||
- ✅ Update a document so its permissions include your new users.
|
||||
- ❌ To allow access, update your permissions.
|
||||
- ❌ You can create a new database for each tenant.
|
||||
- ❌ Creating a new bucket lets you set different permissions for images uploaded by users.
|
||||
|
||||
The action always comes first and is in the beginning of the sentence, which makes important steps easier to follow.
|
||||
|
||||
@@ -288,11 +295,11 @@ to skip and scan a document.
|
||||
Like sentences, important information always comes first.
|
||||
This makes it easier to scan through the page.
|
||||
|
||||
✅ Clear, important information such as actions come first
|
||||
- ✅ Clear, important information such as actions come first
|
||||
|
||||
> Store secrets as environment variables in vaults by navigating to **settings** > **security** > **vault**. Your secrets should never be shared. You must ensure data privacy, sharing secrets can compromise security during development.
|
||||
|
||||
❌ Unclear, important information is in the middle of the paragraph
|
||||
- ❌ Unclear, important information is in the middle of the paragraph
|
||||
|
||||
> Security is important in development. That's why you should take care to protect secrets. Secrets should be safely stored as a environment variable in a vault. You can find vaults under **settings** > **security** > **vault**. Don't share this with anyone!
|
||||
|
||||
@@ -301,6 +308,13 @@ Even if your paragraph is just one or two sentences, shorter paragraphs are easi
|
||||
|
||||
### Diction
|
||||
|
||||
Avoid using possession as it is less welcoming.
|
||||
|
||||
- ✅ read the documentation
|
||||
- ✅ the API
|
||||
- ❌ read on our documentation
|
||||
- ❌ our API
|
||||
|
||||
If you're unsure about which word to use to describe a concept, you shuold look for precedence in the following order.
|
||||
|
||||
1. Appwrite docs
|
||||
|
||||
@@ -1,33 +1,51 @@
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import js from '@eslint/js';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import globals from 'globals';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import ts from 'typescript-eslint';
|
||||
import svelteConfig from './svelte.config.js';
|
||||
|
||||
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||
|
||||
export default ts.config(
|
||||
includeIgnoreFile(gitignorePath),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs['flat/recommended'],
|
||||
...svelte.configs.recommended,
|
||||
prettier,
|
||||
...svelte.configs['flat/prettier'],
|
||||
...svelte.configs.prettier,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
globals: { ...globals.browser, ...globals.node }
|
||||
},
|
||||
rules: {
|
||||
// TODO: remove them one by one
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-duplicate-enum-values': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'svelte/infinite-reactive-loop': 'off',
|
||||
'svelte/require-each-key': 'off',
|
||||
'svelte/no-immutable-reactive-statements': 'off',
|
||||
'svelte/no-at-html-tags': 'off',
|
||||
'svelte/no-useless-mustaches': 'off',
|
||||
'svelte/no-reactive-reassign': 'off',
|
||||
'svelte/no-reactive-literals': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
|
||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||
ignores: ['eslint.config.js', 'svelte.config.js'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: ts.parser
|
||||
// Only uncomment this if you want it to take 3 minutes https://github.com/sveltejs/eslint-plugin-svelte/issues/1084
|
||||
// projectService: true,
|
||||
extraFileExtensions: ['.svelte'],
|
||||
parser: ts.parser,
|
||||
svelteConfig
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ['build/', '.svelte-kit/', 'dist/']
|
||||
}
|
||||
);
|
||||
|
||||
1
global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'reodotdev';
|
||||
45
package.json
@@ -17,17 +17,17 @@
|
||||
"icons:optimize": "node ./src/icons/optimize.js",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"preview": "vite preview",
|
||||
"test": "npm run test:integration && npm run test:unit",
|
||||
"test": "npm run test:integration",
|
||||
"test:integration": "playwright test",
|
||||
"test:unit": "vitest",
|
||||
"optimize": "node ./scripts/optimize-assets.js",
|
||||
"optimize:all": "node ./scripts/optimize-all.js"
|
||||
},
|
||||
"packageManager": "pnpm@10.2.0+sha512.0d27364e0139c6aadeed65ada153135e0ca96c8da42123bd50047f961339dc7a758fc2e944b428f52be570d1bd3372455c1c65fa2e7aa0bfbf931190f9552001",
|
||||
"packageManager": "pnpm@10.6.2",
|
||||
"dependencies": {
|
||||
"@number-flow/svelte": "^0.3.3",
|
||||
"@sentry/sveltekit": "^8.51.0",
|
||||
"h3": "^1.14.0",
|
||||
"melt": "^0.28.2",
|
||||
"posthog-js": "^1.210.2",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
@@ -36,16 +36,18 @@
|
||||
"@appwrite.io/pink": "~0.26.0",
|
||||
"@appwrite.io/pink-icons": "~0.26.0",
|
||||
"@appwrite.io/repo": "github:appwrite/appwrite#feat-multi-region-docs",
|
||||
"@eslint/compat": "^1.2.7",
|
||||
"@eslint/js": "^9.21.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.5.1",
|
||||
"@internationalized/date": "3.5.0",
|
||||
"@melt-ui/pp": "^0.3.2",
|
||||
"@melt-ui/svelte": "^0.86.2",
|
||||
"@melt-ui/svelte": "^0.86.5",
|
||||
"@playwright/test": "^1.50.0",
|
||||
"@sveltejs/adapter-node": "^4.0.1",
|
||||
"@sveltejs/enhanced-img": "^0.1.9",
|
||||
"@sveltejs/kit": "^2.16.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
"@tailwindcss/postcss": "4.0.0-alpha.17",
|
||||
"@sveltejs/adapter-node": "^5.2.12",
|
||||
"@sveltejs/enhanced-img": "^0.4.4",
|
||||
"@sveltejs/kit": "^2.20.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tailwindcss/postcss": "^4.0.17",
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/markdown-it": "^13.0.9",
|
||||
@@ -79,21 +81,32 @@
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"remeda": "^2.20.0",
|
||||
"reodotdev": "^1.0.0",
|
||||
"sass": "^1.83.4",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-check": "^3.8.6",
|
||||
"svelte-markdoc-preprocess": "^2.1.0",
|
||||
"svelte": "^5.25.6",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-markdoc-preprocess": "3.0.0",
|
||||
"svelte-markdown": "^0.4.1",
|
||||
"svgtofont": "^4.2.3",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss": "4.0.0-alpha.17",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^4.0.17",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.21.0",
|
||||
"vite": "^5.4.14",
|
||||
"vite": "^6.2.4",
|
||||
"vite-plugin-dynamic-import": "^1.6.0",
|
||||
"vite-plugin-image-optimizer": "^1.1.8",
|
||||
"vite-plugin-manifest-sri": "^0.2.0",
|
||||
"vitest": "^1.6.0"
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"core-js",
|
||||
"esbuild",
|
||||
"sharp",
|
||||
"svelte-preprocess",
|
||||
"ttf2woff2"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
webServer: {
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
command: 'pnpm run dev',
|
||||
port: 5173
|
||||
},
|
||||
fullyParallel: true,
|
||||
testDir: 'tests',
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/
|
||||
};
|
||||
|
||||
4162
pnpm-lock.yaml
generated
@@ -1,12 +1,13 @@
|
||||
import { createApp, fromNodeMiddleware, toNodeListener } from 'h3';
|
||||
import { sitemaps } from './sitemap.js';
|
||||
import { createServer } from 'node:http';
|
||||
import { handler } from '../build/handler.js';
|
||||
import { sitemap } from './sitemap.js';
|
||||
import { createApp, fromNodeMiddleware, toNodeListener } from 'h3';
|
||||
|
||||
async function main() {
|
||||
const port = process.env.PORT || 3000;
|
||||
const app = createApp();
|
||||
app.use('/sitemap.xml', await sitemap());
|
||||
app.use(['/sitemap.xml', '/sitemaps'], await sitemaps());
|
||||
|
||||
app.use(fromNodeMiddleware(handler));
|
||||
const server = createServer(toNodeListener(app)).listen(port);
|
||||
server.addListener('listening', () => {
|
||||
|
||||
@@ -1,43 +1,154 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
import { defineEventHandler, setResponseHeader } from 'h3';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { readFile, stat } from 'node:fs/promises';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import {
|
||||
defineEventHandler,
|
||||
getRequestURL,
|
||||
sendRedirect,
|
||||
serveStatic,
|
||||
setResponseHeader
|
||||
} from 'h3';
|
||||
|
||||
/**
|
||||
* @returns {Promise<import('h3').EventHandler>}
|
||||
*/
|
||||
export async function sitemap() {
|
||||
const MAX_THREADS_PER_FILE = 1000;
|
||||
const BASE_URL = 'https://appwrite.io';
|
||||
const BASE_DIR = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const SITEMAP_DIR = join(BASE_DIR, './sitemaps');
|
||||
const THREADS_DIR = join(SITEMAP_DIR, 'threads');
|
||||
const NAMED_GROUPS = {
|
||||
blog: '/blog',
|
||||
docs: '/docs',
|
||||
integrations: '/integrations'
|
||||
};
|
||||
|
||||
export async function sitemaps() {
|
||||
console.info('Preparing Sitemap...');
|
||||
const manifest = await import('../build/server/manifest.js');
|
||||
const prerendered = manifest.prerendered;
|
||||
const file_route_extensions = ['.json', '.xml'];
|
||||
const routes = [...prerendered, ...collectThreads()].filter(
|
||||
(route) => !file_route_extensions.some((ext) => route.endsWith(ext))
|
||||
const { manifest } = await import('../build/server/manifest.js');
|
||||
const threads = collectThreads().map((id) => `/threads/${id}`);
|
||||
const otherRoutes = manifest._.routes
|
||||
.filter((r) => r.params.length === 0)
|
||||
.map((r) => r.id)
|
||||
.filter(
|
||||
(id) => !id.startsWith('/threads/') && !id.endsWith('.json') && !id.endsWith('.xml')
|
||||
);
|
||||
console.info(`Sitemap loaded with ${routes.length} routes!`);
|
||||
|
||||
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${routes
|
||||
mkdirSync(SITEMAP_DIR, { recursive: true });
|
||||
mkdirSync(THREADS_DIR, { recursive: true });
|
||||
|
||||
let totalCount = 0;
|
||||
const sitemapIndexOrder = [];
|
||||
|
||||
const grouped = {},
|
||||
fallback = [];
|
||||
|
||||
for (const route of otherRoutes) {
|
||||
const match = Object.entries(NAMED_GROUPS).find(([, prefix]) => route.startsWith(prefix));
|
||||
if (match) {
|
||||
const [group] = match;
|
||||
grouped[group] ??= [];
|
||||
grouped[group].push(route);
|
||||
} else fallback.push(route);
|
||||
}
|
||||
|
||||
totalCount += writeSitemap('pages.xml', fallback, SITEMAP_DIR);
|
||||
sitemapIndexOrder.push('pages.xml');
|
||||
|
||||
for (const group of ['docs', 'blog', 'integrations']) {
|
||||
if (grouped[group]?.length) {
|
||||
const filename = `${group}.xml`;
|
||||
totalCount += writeSitemap(filename, grouped[group], SITEMAP_DIR);
|
||||
sitemapIndexOrder.push(filename);
|
||||
}
|
||||
}
|
||||
|
||||
const threadChunks = chunkArray(threads, MAX_THREADS_PER_FILE);
|
||||
threadChunks.forEach((chunk, i) => {
|
||||
const filename = `${i + 1}.xml`;
|
||||
totalCount += writeSitemap(filename, chunk, THREADS_DIR);
|
||||
sitemapIndexOrder.push(`threads/${filename}`);
|
||||
});
|
||||
|
||||
const sitemapIndex = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<sitemapindex xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${sitemapIndexOrder
|
||||
.map(
|
||||
(route) => `<url>
|
||||
<loc>https://appwrite.io${route}</loc>
|
||||
</url>
|
||||
`
|
||||
(name) => `
|
||||
<sitemap>
|
||||
<loc>${BASE_URL}/sitemaps/${name}</loc>
|
||||
</sitemap>`
|
||||
)
|
||||
.join('')}
|
||||
</urlset>`;
|
||||
.join('\n')}
|
||||
</sitemapindex>`.trim();
|
||||
|
||||
return defineEventHandler((event) => {
|
||||
console.info(`✅ Sitemap generation complete — ${totalCount} URLs in total.\n`);
|
||||
|
||||
return defineEventHandler(async (event) => {
|
||||
const url = getRequestURL(event);
|
||||
|
||||
if (url.pathname === '/sitemap.xml') {
|
||||
setResponseHeader(event, 'Content-Type', 'application/xml');
|
||||
return sitemapIndex;
|
||||
}
|
||||
|
||||
return sitemap;
|
||||
if (url.pathname === '/sitemaps') {
|
||||
return sendRedirect(event, '/sitemap.xml', 307);
|
||||
}
|
||||
|
||||
if (url.pathname === '/sitemaps/threads') {
|
||||
return sendRedirect(event, '/sitemaps/threads/1.xml', 307);
|
||||
}
|
||||
|
||||
const dir = import.meta.resolve('./sitemaps');
|
||||
return serveStatic(event, {
|
||||
fallthrough: true,
|
||||
indexNames: undefined,
|
||||
getContents: (id) => readFile(new URL(dir + id)),
|
||||
getMeta: async (id) => {
|
||||
const stats = await stat(new URL(dir + id)).catch(() => null);
|
||||
if (!stats?.isFile()) return;
|
||||
return {
|
||||
size: stats.size,
|
||||
mtime: stats.mtimeMs
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function collectThreads() {
|
||||
const threads = createRequire(import.meta.url)('../build/prerendered/threads/data.json');
|
||||
function writeSitemap(filename, routes, dir) {
|
||||
const body = `
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<urlset
|
||||
xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:xhtml="https://www.w3.org/1999/xhtml"
|
||||
xmlns:mobile="https://www.google.com/schemas/sitemap-mobile/1.0"
|
||||
xmlns:news="https://www.google.com/schemas/sitemap-news/0.9"
|
||||
xmlns:image="https://www.google.com/schemas/sitemap-image/1.1"
|
||||
xmlns:video="https://www.google.com/schemas/sitemap-video/1.1"
|
||||
>
|
||||
${routes.map((route) => ` <url>\n <loc>${BASE_URL}${route}</loc>\n </url>`).join('\n')}
|
||||
</urlset>`.trim();
|
||||
|
||||
return threads.map((id) => `/threads/${id}`);
|
||||
const filepath = join(dir, filename);
|
||||
writeFileSync(filepath, body);
|
||||
|
||||
const label = filepath.replace(BASE_DIR + '/sitemaps', '');
|
||||
console.info(` └── Generated ${label} with ${routes.length} URLs`);
|
||||
|
||||
return routes.length;
|
||||
}
|
||||
|
||||
function chunkArray(arr, size) {
|
||||
const chunks = [];
|
||||
for (let i = 0; i < arr.length; i += size) {
|
||||
chunks.push(arr.slice(i, i + size));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function collectThreads() {
|
||||
return createRequire(import.meta.url)('../build/prerendered/threads/data.json');
|
||||
}
|
||||
|
||||
185
src/app.css
@@ -1,4 +1,5 @@
|
||||
@import 'tailwindcss';
|
||||
@import './styles/typography.css';
|
||||
@variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
@@ -6,8 +7,13 @@
|
||||
--color-*: initial;
|
||||
|
||||
/* base */
|
||||
--color-primary: hsl(var(--color-primary));
|
||||
--color-secondary: hsl(var(--color-secondary));
|
||||
--color-black: #000;
|
||||
--color-white: #fff;
|
||||
--color-transparent: transparent;
|
||||
|
||||
/* theme */
|
||||
--color-primary: var(--color-primary);
|
||||
--color-secondary: var(--color-secondary);
|
||||
--color-accent: var(--color-secondary);
|
||||
--color-smooth: var(--color-smooth);
|
||||
|
||||
@@ -55,9 +61,6 @@
|
||||
--color-accent-200: hsl(var(--color-secondary-hue), 78%, 60%, 0.32);
|
||||
|
||||
/* greyscale */
|
||||
--color-white: hsl(0 0% 100%);
|
||||
--color-black: hsl(0 0% 0%);
|
||||
--color-transparent: rgba(0, 0, 0, 0);
|
||||
--color-offset: hsl(var(--color-greyscale-hue) 2%, 11%, 0.94);
|
||||
--color-greyscale-25: hsl(var(--color-greyscale-hue) 11% 98%);
|
||||
--color-greyscale-50: hsl(var(--color-greyscale-hue) 11% 94%);
|
||||
@@ -74,47 +77,19 @@
|
||||
--color-greyscale-850: hsl(var(--color-greyscale-hue) 3% 14%);
|
||||
--color-greyscale-900: hsl(var(--color-greyscale-hue) 5.7% 10.4%);
|
||||
|
||||
/* utility colors */
|
||||
--color-badge-bg-light: #f2c8d6;
|
||||
--color-badge-border-light: #f69db7;
|
||||
--color-badge-bg-dark: #2c2c2f;
|
||||
--color-badge-border-dark: #39393c;
|
||||
|
||||
/* Easings */
|
||||
--transition-timing-function-bounce: linear(
|
||||
0,
|
||||
0.063,
|
||||
0.25 18.2%,
|
||||
1 36.4%,
|
||||
0.813,
|
||||
0.75,
|
||||
0.813,
|
||||
1,
|
||||
0.938,
|
||||
1,
|
||||
1
|
||||
);
|
||||
--transition-timing-function-spring: linear(
|
||||
0,
|
||||
0.938 16.7%,
|
||||
1.149 24.3%,
|
||||
1.154 29.9%,
|
||||
0.977 51%,
|
||||
1
|
||||
);
|
||||
--easing-bounce: linear(0, 0.063, 0.25 18.2%, 1 36.4%, 0.813, 0.75, 0.813, 1, 0.938, 1, 1);
|
||||
--easing-spring: linear(0, 0.938 16.7%, 1.149 24.3%, 1.154 29.9%, 0.977 51%, 1);
|
||||
|
||||
/* Animations */
|
||||
--animate-scale-in: scale-in 200ms ease-out forwards;
|
||||
--animate-caret-blink: caret-blink 1s ease-in-out infinite;
|
||||
--animate-text: fade 0.75s ease-in-out both, blur 0.75s ease-in-out both,
|
||||
up 0.75s ease-in-out both;
|
||||
--animate-text:
|
||||
fade 0.75s ease-in-out both, blur 0.75s ease-in-out both, up 0.75s ease-in-out both;
|
||||
--animate-scroll: scroll 60s linear infinite;
|
||||
--animate-fade-in: fade-in 0.5s ease-in-out both;
|
||||
--animate-marquee: marquee var(--speed, 30s) linear infinite var(--direction, forwards);
|
||||
|
||||
/* Pink polyfills */
|
||||
--transition: 0.2s;
|
||||
|
||||
/* Keyframes */
|
||||
@keyframes scale-in {
|
||||
0% {
|
||||
@@ -180,62 +155,84 @@
|
||||
}
|
||||
|
||||
/* Fonts */
|
||||
--font-family-sans: 'Inter', arial, sans-serif;
|
||||
--font-family-mono: 'Fira Code', monospace;
|
||||
--font-family-aeonik-fono: 'Aenoik Fono', monospace;
|
||||
--font-family-aeonik-pro: 'Aeonik Pro', var(--font-family-sans);
|
||||
--font-family-archia: 'Archia', arial, sans-serif;
|
||||
--font-sans: 'Inter', arial, sans-serif;
|
||||
--font-mono: 'Fira Code', monospace;
|
||||
--font-aeonik-fono: 'Aenoik Fono', monospace;
|
||||
--font-aeonik-pro: 'Aeonik Pro', var(--font-sans);
|
||||
--font-archia: 'Archia', arial, sans-serif;
|
||||
|
||||
/* Font sizes */
|
||||
--font-size-x-micro: 0.625rem;
|
||||
--font-size-x-micro--line-height: 0.875rem;
|
||||
--font-size-x-micro--letter-spacing: var(--letter-spacing-tighter);
|
||||
--font-size-micro: 0.75rem;
|
||||
--font-size-micro--line-height: 1rem;
|
||||
--font-size-micro--letter-spacing: var(--letter-spacing-tighter);
|
||||
--font-size-caption: 0.875rem;
|
||||
--font-size-caption--line-height: 1.375rem;
|
||||
--font-size-caption--letter-spacing: var(--letter-spacing-tight);
|
||||
--font-size-sub-body: clamp(0.875rem, 2vw, 1rem);
|
||||
--font-size-sub-body--line-height: 1.375rem;
|
||||
--font-size-sub-body--letter-spacing: var(--letter-spacing-tight);
|
||||
--font-size-body: clamp(1rem, 2.5vw, 1.125rem);
|
||||
--font-size-body--line-height: clamp(1.375rem, 3vw, 1.625rem);
|
||||
--font-size-body--letter-spacing: var(--letter-spacing-tight);
|
||||
--font-size-paragraph-md: 1rem;
|
||||
--font-size-paragraph-md--line-height: 1.625rem;
|
||||
--font-size-paragraph-md--letter-spacing: var(--letter-spacing-tight);
|
||||
--font-size-paragraph-lg: 1.125rem;
|
||||
--font-size-paragraph-lg--line-height: 1.75rem;
|
||||
--font-size-paragraph-lg--letter-spacing: var(--letter-spacing-tight);
|
||||
--font-size-description: clamp(1.125rem, 3vw, 1.25rem);
|
||||
--font-size-description--line-height: clamp(1.625rem, 3.5vw, 1.75rem);
|
||||
--font-size-description--letter-spacing: var(--letter-spacing-tighter);
|
||||
--font-size-label: 1.5rem;
|
||||
--font-size-label--line-height: 1.75rem;
|
||||
--font-size-title: clamp(2rem, 5vw, 2.5rem);
|
||||
--font-size-title--line-height: clamp(2.125rem, 5.5vw, 2.75rem);
|
||||
--font-size-title--letter-spacing: var(--letter-spacing-squeezed);
|
||||
--font-size-display: clamp(3rem, 7vw, 4rem);
|
||||
--font-size-display--line-height: clamp(3.125rem, 7.5vw, 4.25rem);
|
||||
--font-size-display--letter-spacing: var(--letter-spacing-compressed);
|
||||
--font-size-headline: clamp(3.5rem, 8vw, 5.5rem);
|
||||
--font-size-headline--line-height: clamp(3.5rem, 8.5vw, 5.75rem);
|
||||
--font-size-headline--letter-spacing: var(--letter-spacing-compressed);
|
||||
--text-x-micro: 0.625rem;
|
||||
--text-x-micro--line-height: 0.875rem;
|
||||
--text-x-micro--letter-spacing: var(--tracking-tighter);
|
||||
--text-micro: 0.75rem;
|
||||
--text-micro--line-height: 1rem;
|
||||
--text-micro--letter-spacing: var(--tracking-tighter);
|
||||
--text-caption: 0.875rem;
|
||||
--text-caption--line-height: 1.375rem;
|
||||
--text-caption--letter-spacing: var(--tracking-tight);
|
||||
--text-sub-body: clamp(0.875rem, 2vw, 1rem);
|
||||
--text-sub-body--line-height: 1.375rem;
|
||||
--text-sub-body--letter-spacing: var(--tracking-tight);
|
||||
--text-body: clamp(1rem, 2.5vw, 1.125rem);
|
||||
--text-body--line-height: clamp(1.375rem, 3vw, 1.625rem);
|
||||
--text-body--letter-spacing: var(--tracking-tight);
|
||||
--text-paragraph-md: 1rem;
|
||||
--text-paragraph-md--line-height: 1.625rem;
|
||||
--text-paragraph-md--letter-spacing: var(--tracking-tight);
|
||||
--text-paragraph-lg: 1.125rem;
|
||||
--text-paragraph-lg--line-height: 1.75rem;
|
||||
--text-paragraph-lg--letter-spacing: var(--tracking-tight);
|
||||
--text-description: clamp(1.125rem, 3vw, 1.25rem);
|
||||
--text-description--line-height: clamp(1.625rem, 3.5vw, 1.75rem);
|
||||
--text-description--letter-spacing: var(--tracking-tighter);
|
||||
--text-label: 1.5rem;
|
||||
--text-label--line-height: 1.75rem;
|
||||
--text-title: clamp(2rem, 5vw, 2.5rem);
|
||||
--text-title--line-height: clamp(2.125rem, 5.5vw, 2.75rem);
|
||||
--text-title--letter-spacing: var(--tracking-squeezed);
|
||||
--text-display: clamp(3rem, 7vw, 4rem);
|
||||
--text-display--line-height: clamp(3.125rem, 7.5vw, 4.25rem);
|
||||
--text-display--letter-spacing: var(--tracking-compressed);
|
||||
--text-headline: clamp(3.5rem, 8vw, 5.5rem);
|
||||
--text-headline--line-height: clamp(3.5rem, 8.5vw, 5.75rem);
|
||||
--text-headline--letter-spacing: var(--tracking-compressed);
|
||||
|
||||
/* Letter spacing */
|
||||
--letter-spacing-*: initial;
|
||||
--letter-spacing-compressed: -0.022em;
|
||||
--letter-spacing-squeezed: -0.01em;
|
||||
--letter-spacing-tighter: -0.018em;
|
||||
--letter-spacing-tight: -0.0045em;
|
||||
--letter-spacing-none: 0em;
|
||||
--letter-spacing-loose: 0.08em;
|
||||
--tracking-*: initial;
|
||||
--tracking-compressed: -0.022em;
|
||||
--tracking-squeezed: -0.01em;
|
||||
--tracking-tighter: -0.018em;
|
||||
--tracking-tight: -0.0045em;
|
||||
--tracking-none: 0em;
|
||||
--tracking-loose: 0.08em;
|
||||
}
|
||||
|
||||
@utility container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 1.25rem;
|
||||
box-sizing: content-box;
|
||||
max-width: 75rem;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.mask {
|
||||
mask-image: linear-gradient(
|
||||
to var(--mask-direction, top),
|
||||
transparent,
|
||||
black var(--mask-height, 32px),
|
||||
black calc(100% - var(--mask-height, 32px)),
|
||||
black
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Themes */
|
||||
:root,
|
||||
.light {
|
||||
/* pink polyfills */
|
||||
--transition: 0.2s;
|
||||
|
||||
/* color hues */
|
||||
--color-pink-hue: 343;
|
||||
--color-secondary-hue: 351;
|
||||
--color-red-hue: 3;
|
||||
@@ -255,7 +252,6 @@
|
||||
--color-smooth: hsl(var(--color-greyscale-hue) 6%, 10%, 0.04);
|
||||
}
|
||||
|
||||
/* dark theme */
|
||||
.dark {
|
||||
--color-primary: var(--color-greyscale-100);
|
||||
--color-secondary: var(--color-greyscale-300);
|
||||
@@ -263,20 +259,3 @@
|
||||
--color-badge-border: var(--color-badge-border-dark);
|
||||
--color-smooth: hsl(0 0%, 100%, 0.06);
|
||||
}
|
||||
|
||||
/* Container */
|
||||
@layer components {
|
||||
.container {
|
||||
@apply mx-auto box-content max-w-[75rem] px-5;
|
||||
}
|
||||
|
||||
.mask {
|
||||
mask-image: linear-gradient(
|
||||
to top,
|
||||
transparent,
|
||||
black var(--mask-height, 32px),
|
||||
black calc(100% - var(--mask-height, 32px)),
|
||||
black
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { dev } from '$app/environment';
|
||||
import { SENTRY_DSN } from '$lib/constants';
|
||||
import { handleErrorWithSentry, replayIntegration } from '@sentry/sveltekit';
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
|
||||
Sentry.init({
|
||||
enabled: !dev,
|
||||
dsn: SENTRY_DSN,
|
||||
allowUrls: [/appwrite\.io/],
|
||||
tracesSampleRate: 1.0,
|
||||
|
||||
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
// in development and sample at a lower rate in production
|
||||
replaysSessionSampleRate: 0,
|
||||
|
||||
// If the entire session is not sampled, use the below sample rate to sample
|
||||
// sessions when an error occurs.
|
||||
replaysOnErrorSampleRate: 0
|
||||
});
|
||||
|
||||
// If you have a custom error handler, pass it to `handleErrorWithSentry`
|
||||
export const handleError = handleErrorWithSentry();
|
||||
@@ -1,106 +0,0 @@
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
import redirects from './redirects.json';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
import { BANNER_KEY, SENTRY_DSN } from '$lib/constants';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
Sentry.init({
|
||||
enabled: !dev,
|
||||
dsn: SENTRY_DSN,
|
||||
tracesSampleRate: 1,
|
||||
allowUrls: [/appwrite\.io/]
|
||||
});
|
||||
|
||||
const redirectMap = new Map(redirects.map(({ link, redirect }) => [link, redirect]));
|
||||
|
||||
const redirecter: Handle = async ({ event, resolve }) => {
|
||||
const currentPath = event.url.pathname;
|
||||
if (redirectMap.has(currentPath)) {
|
||||
return new Response(null, {
|
||||
status: 308,
|
||||
headers: {
|
||||
location: redirectMap.get(currentPath) ?? ''
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return await resolve(event);
|
||||
};
|
||||
|
||||
const securityheaders: Handle = async ({ event, resolve }) => {
|
||||
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
|
||||
(event.locals as { nonce: string }).nonce = nonce;
|
||||
|
||||
const response = await resolve(event, {
|
||||
transformPageChunk: ({ html }) => {
|
||||
return html.replace(/%sveltekit.nonce%/g, nonce);
|
||||
}
|
||||
});
|
||||
|
||||
// `true` if deployed via Coolify.
|
||||
const isPreview = !!process.env.COOLIFY_FQDN;
|
||||
// COOLIFY_FQDN already includes `http`.
|
||||
const previewDomain = isPreview ? `${process.env.COOLIFY_FQDN}` : null;
|
||||
|
||||
const cspDirectives: Record<string, string> = {
|
||||
'default-src': "'self'",
|
||||
'script-src':
|
||||
"'self' 'unsafe-inline' 'unsafe-eval' https://*.posthog.com https://*.plausible.io https://plausible.io",
|
||||
'style-src': "'self' 'unsafe-inline'",
|
||||
'img-src': "'self' data: https:",
|
||||
'font-src': "'self'",
|
||||
'object-src': "'none'",
|
||||
'base-uri': "'self'",
|
||||
'form-action': "'self'",
|
||||
'frame-ancestors': "'self' https://www.youtube.com https://*.vimeo.com",
|
||||
'block-all-mixed-content': '',
|
||||
'upgrade-insecure-requests': '',
|
||||
'connect-src':
|
||||
"'self' https://*.appwrite.io https://*.appwrite.org https://*.posthog.com https://*.sentry.io https://*.plausible.io https://plausible.io",
|
||||
'frame-src':
|
||||
"'self' https://www.youtube.com https://status.appwrite.online https://www.youtube-nocookie.com https://player.vimeo.com"
|
||||
};
|
||||
|
||||
if (isPreview) {
|
||||
delete cspDirectives['block-all-mixed-content'];
|
||||
delete cspDirectives['upgrade-insecure-requests'];
|
||||
['default-src', 'script-src', 'style-src', 'img-src', 'font-src', 'connect-src'].forEach(
|
||||
(key) => {
|
||||
cspDirectives[key] += ` ${previewDomain}`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const cspDirectivesString = Object.entries(cspDirectives)
|
||||
.map(([key, value]) => `${key} ${value}`.trim())
|
||||
.join('; ');
|
||||
|
||||
// Set security headers
|
||||
response.headers.set('Content-Security-Policy', cspDirectivesString);
|
||||
|
||||
// HTTP Strict Transport Security
|
||||
// max-age is set to 1 year in seconds
|
||||
response.headers.set(
|
||||
'Strict-Transport-Security',
|
||||
'max-age=31536000; includeSubDomains; preload'
|
||||
);
|
||||
|
||||
// X-Content-Type-Options
|
||||
response.headers.set('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
// X-Frame-Options
|
||||
response.headers.set('X-Frame-Options', 'DENY');
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const bannerRewriter: Handle = async ({ event, resolve }) => {
|
||||
const response = await resolve(event, {
|
||||
transformPageChunk: ({ html }) => html.replace('%aw_banner_key%', BANNER_KEY)
|
||||
});
|
||||
return response;
|
||||
};
|
||||
|
||||
export const handle = sequence(Sentry.sentryHandle(), redirecter, bannerRewriter, securityheaders);
|
||||
export const handleError = Sentry.handleErrorWithSentry();
|
||||
1
src/icons/optimized/bluesky.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none"><path d="M4.015 4.066 C 3.386 4.200,2.770 4.680,2.463 5.277 C 2.236 5.716,2.064 6.483,2.023 7.233 C 1.935 8.835,2.638 17.067,2.973 18.357 C 3.589 20.735,5.311 22.511,7.700 23.232 C 8.757 23.552,9.349 23.630,10.709 23.632 L 11.985 23.633 11.309 23.811 C 9.494 24.287,8.019 25.049,7.075 25.999 C 5.448 27.635,5.423 29.739,7.004 32.000 C 7.474 32.673,9.092 34.267,9.833 34.789 C 12.203 36.458,14.280 36.389,16.049 34.584 C 16.654 33.967,16.770 33.822,17.217 33.133 C 17.895 32.087,18.469 30.836,19.112 29.002 C 19.311 28.435,19.483 27.962,19.494 27.951 C 19.505 27.939,19.657 28.343,19.831 28.848 C 20.459 30.672,21.101 32.082,21.783 33.133 C 22.230 33.822,22.346 33.967,22.951 34.584 C 24.131 35.788,25.508 36.235,26.930 35.876 C 28.186 35.558,29.303 34.831,30.735 33.401 C 31.772 32.364,32.258 31.722,32.666 30.848 C 33.293 29.506,33.328 28.241,32.768 27.135 C 32.028 25.676,30.176 24.463,27.691 23.811 L 27.015 23.633 28.291 23.632 C 29.651 23.630,30.243 23.552,31.300 23.232 C 33.689 22.511,35.411 20.735,36.027 18.357 C 36.329 17.194,36.950 10.234,36.961 7.900 C 36.966 6.701,36.951 6.514,36.816 6.045 C 36.533 5.072,36.180 4.596,35.482 4.253 C 35.055 4.043,35.005 4.034,34.301 4.041 C 33.713 4.047,33.447 4.084,32.967 4.228 C 30.211 5.051,27.121 7.741,23.683 12.309 C 22.331 14.106,20.761 16.557,19.901 18.215 L 19.502 18.983 19.077 18.175 C 17.210 14.622,14.087 10.393,11.391 7.765 C 9.470 5.892,7.671 4.706,6.037 4.235 C 5.380 4.046,4.469 3.970,4.015 4.066 " fill="#19191C" stroke="none" fill-rule="evenodd"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
src/icons/optimized/instagram.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30" fill="none"><path d="M10.375 0.031 C 9.250 0.064,7.859 0.154,7.350 0.229 C 5.392 0.514,3.783 1.277,2.553 2.502 C 1.586 3.465,1.038 4.367,0.597 5.719 C 0.088 7.282,0.000 8.647,0.000 15.000 C 0.000 21.460,0.086 22.729,0.633 24.391 C 1.424 26.792,3.137 28.515,5.556 29.343 C 7.226 29.914,8.467 30.000,15.018 30.000 C 21.544 29.999,22.704 29.918,24.425 29.345 C 26.565 28.633,28.221 27.121,29.091 25.086 C 29.908 23.175,30.035 21.556,29.984 13.750 C 29.953 8.945,29.910 7.999,29.668 6.800 C 29.482 5.878,29.077 4.749,28.694 4.086 C 27.595 2.185,25.765 0.894,23.444 0.383 C 22.093 0.085,21.209 0.045,15.675 0.029 C 13.063 0.022,10.678 0.023,10.375 0.031 M21.147 2.797 C 22.819 2.913,23.916 3.209,24.775 3.776 C 25.544 4.284,26.291 5.189,26.632 6.026 C 27.069 7.096,27.176 7.907,27.255 10.713 C 27.321 13.071,27.286 19.608,27.200 21.022 C 27.078 23.016,26.694 24.234,25.881 25.205 C 24.921 26.349,23.631 26.958,21.761 27.147 C 20.680 27.257,17.315 27.312,13.575 27.283 C 8.791 27.245,7.856 27.189,6.778 26.875 C 5.764 26.580,5.080 26.193,4.446 25.556 C 3.505 24.610,3.027 23.461,2.854 21.725 C 2.698 20.170,2.663 11.634,2.801 9.100 C 2.888 7.513,3.055 6.701,3.477 5.807 C 3.757 5.217,4.041 4.820,4.507 4.370 C 5.314 3.590,6.285 3.142,7.650 2.919 C 8.064 2.852,9.417 2.763,10.525 2.731 C 11.806 2.694,20.435 2.747,21.147 2.797 M22.514 5.270 C 21.719 5.529,21.239 6.180,21.239 7.000 C 21.239 7.839,21.771 8.518,22.593 8.728 C 23.215 8.886,23.866 8.689,24.316 8.206 C 24.683 7.811,24.775 7.570,24.775 7.000 C 24.775 6.590,24.757 6.486,24.641 6.240 C 24.357 5.633,23.775 5.245,23.107 5.217 C 22.875 5.207,22.642 5.228,22.514 5.270 M13.941 7.373 C 12.249 7.616,10.798 8.346,9.572 9.572 C 8.915 10.229,8.502 10.798,8.102 11.598 C 7.065 13.671,7.038 16.178,8.030 18.250 C 8.471 19.170,8.803 19.650,9.497 20.368 C 10.613 21.524,12.030 22.283,13.625 22.582 C 14.320 22.712,15.680 22.712,16.375 22.582 C 19.542 21.989,21.989 19.542,22.582 16.375 C 22.712 15.680,22.712 14.320,22.582 13.625 C 22.283 12.030,21.524 10.613,20.368 9.497 C 19.215 8.383,17.889 7.698,16.351 7.419 C 15.751 7.311,14.537 7.287,13.941 7.373 M16.175 10.144 C 17.970 10.592,19.337 11.932,19.836 13.732 C 19.958 14.171,19.973 14.310,19.973 15.000 C 19.973 15.851,19.896 16.256,19.596 16.978 C 19.142 18.068,18.068 19.142,16.978 19.596 C 16.256 19.896,15.851 19.973,15.000 19.973 C 14.310 19.973,14.171 19.958,13.732 19.836 C 10.960 19.068,9.387 16.230,10.235 13.526 C 10.801 11.721,12.266 10.434,14.167 10.071 C 14.656 9.977,15.653 10.014,16.175 10.144 " fill="#19191C" stroke="none" fill-rule="evenodd"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
1
src/icons/optimized/mcp.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="156" height="174" viewBox="0 0 156 174" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M85.450 1.455 C 81.141 2.234,76.461 4.304,72.822 7.040 C 71.652 7.920,56.551 22.842,35.705 43.717 C 5.175 74.290,0.524 79.047,0.282 79.950 C -0.169 81.632,-0.054 82.990,0.663 84.428 C 2.141 87.394,5.753 88.659,8.631 87.218 C 9.103 86.981,25.343 70.962,44.720 51.620 C 71.709 24.680,80.316 16.236,81.512 15.526 C 82.372 15.016,84.010 14.273,85.152 13.876 C 87.017 13.227,87.590 13.152,90.740 13.141 C 94.566 13.129,96.050 13.437,98.846 14.823 C 102.071 16.422,105.112 19.380,106.815 22.576 C 109.478 27.571,109.369 34.670,106.551 39.774 C 105.561 41.568,103.245 43.951,78.625 68.510 C 63.859 83.239,51.597 95.658,51.376 96.108 C 49.799 99.320,50.831 102.870,53.796 104.435 C 55.168 105.159,57.489 105.251,58.963 104.639 C 59.643 104.357,67.967 96.221,86.970 77.269 C 107.275 57.017,114.383 50.082,115.507 49.427 C 121.192 46.113,128.442 46.115,134.091 49.432 C 136.073 50.596,139.508 54.083,140.732 56.173 C 142.241 58.751,142.787 60.761,142.936 64.293 C 143.131 68.931,142.427 71.626,140.068 75.270 C 139.379 76.334,128.857 87.003,106.991 108.810 L 74.924 140.790 73.854 143.035 C 71.870 147.199,71.877 151.367,73.873 155.480 C 74.582 156.940,75.725 158.209,82.262 164.795 C 86.424 168.989,90.165 172.597,90.574 172.812 C 91.627 173.366,93.786 173.532,95.160 173.164 C 97.440 172.555,99.329 169.878,99.314 167.280 C 99.302 165.187,98.587 164.263,91.377 157.029 C 85.048 150.678,84.500 150.056,84.500 149.232 C 84.500 148.390,86.387 146.448,116.181 116.613 C 133.605 99.166,148.384 84.188,149.022 83.330 C 155.704 74.345,156.964 63.298,152.485 52.957 C 149.873 46.927,144.189 41.025,138.190 38.115 C 133.355 35.769,130.143 35.034,124.667 35.019 L 120.764 35.009 120.765 31.220 C 120.766 22.678,117.870 15.632,111.940 9.750 C 107.451 5.297,102.090 2.487,96.087 1.440 C 93.271 0.949,88.212 0.956,85.450 1.455 M88.400 25.498 C 87.493 25.906,81.620 31.626,62.308 50.907 C 37.311 75.865,34.383 78.887,32.634 81.522 C 30.557 84.651,28.818 88.943,28.052 92.831 C 27.565 95.301,27.643 101.341,28.193 103.870 C 29.577 110.226,32.363 115.302,36.833 119.616 C 47.455 129.867,63.723 130.867,75.660 122.002 C 76.819 121.142,88.184 109.995,103.668 94.532 C 124.060 74.168,129.839 68.256,130.247 67.340 C 131.563 64.387,130.220 60.827,127.259 59.422 C 126.130 58.886,123.449 58.902,122.239 59.451 C 121.628 59.729,111.809 69.333,94.640 86.446 C 70.071 110.937,67.847 113.084,66.158 113.952 C 63.380 115.379,60.769 116.005,57.634 115.996 C 51.934 115.979,47.144 113.709,43.646 109.368 C 40.910 105.971,39.778 102.684,39.784 98.155 C 39.791 93.042,41.016 89.812,44.429 85.904 C 45.541 84.631,57.567 72.534,71.154 59.020 C 88.076 42.189,96.001 34.134,96.315 33.446 C 96.897 32.167,96.912 29.733,96.344 28.536 C 94.911 25.516,91.369 24.162,88.400 25.498 " stroke="none" fill-rule="evenodd" fill="black"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
src/icons/optimized/tiktok.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none"><path d="M20.933 15.427 C 20.933 27.068,20.957 26.455,20.471 27.433 C 19.554 29.280,17.606 30.221,15.707 29.735 C 14.100 29.324,12.853 27.987,12.526 26.324 C 12.414 25.752,12.486 24.537,12.662 24.033 C 13.214 22.458,14.427 21.383,15.983 21.089 L 16.600 20.972 16.600 18.419 L 16.600 15.867 16.417 15.868 C 16.316 15.869,15.918 15.912,15.533 15.964 C 11.480 16.512,8.319 19.669,7.623 23.866 C 7.487 24.688,7.487 26.179,7.623 27.001 C 8.204 30.507,10.559 33.375,13.768 34.487 C 14.884 34.873,15.425 34.961,16.700 34.964 C 17.921 34.967,18.480 34.885,19.500 34.555 C 21.214 34.001,22.910 32.774,24.036 31.274 C 24.872 30.160,25.559 28.557,25.839 27.067 C 25.947 26.497,25.961 25.860,25.984 20.721 L 26.009 15.009 26.569 15.374 C 28.134 16.394,30.132 17.074,31.878 17.180 L 32.535 17.220 32.517 14.660 L 32.500 12.100 32.033 12.053 C 30.985 11.946,29.712 11.463,28.843 10.843 C 27.985 10.230,27.175 9.279,26.692 8.317 C 26.217 7.372,26.024 6.668,25.896 5.417 L 25.854 5.000 23.393 5.000 L 20.933 5.000 20.933 15.427 " fill="#19191C" stroke="none" fill-rule="evenodd"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -5,48 +5,52 @@ $web-icon-arrow-ext-link: "\ea04";
|
||||
$web-icon-arrow-left: "\ea05";
|
||||
$web-icon-arrow-right: "\ea06";
|
||||
$web-icon-arrow-up: "\ea07";
|
||||
$web-icon-calendar: "\ea08";
|
||||
$web-icon-check: "\ea09";
|
||||
$web-icon-chevron-down: "\ea0a";
|
||||
$web-icon-chevron-left: "\ea0b";
|
||||
$web-icon-chevron-right: "\ea0c";
|
||||
$web-icon-chevron-up: "\ea0d";
|
||||
$web-icon-close: "\ea0e";
|
||||
$web-icon-command: "\ea0f";
|
||||
$web-icon-copy: "\ea10";
|
||||
$web-icon-daily-dev: "\ea11";
|
||||
$web-icon-dark: "\ea12";
|
||||
$web-icon-discord: "\ea13";
|
||||
$web-icon-divider-vertical: "\ea14";
|
||||
$web-icon-download: "\ea15";
|
||||
$web-icon-ext-link: "\ea16";
|
||||
$web-icon-firebase: "\ea17";
|
||||
$web-icon-github: "\ea18";
|
||||
$web-icon-google: "\ea19";
|
||||
$web-icon-hamburger-menu: "\ea1a";
|
||||
$web-icon-light: "\ea1b";
|
||||
$web-icon-linkedin: "\ea1c";
|
||||
$web-icon-location: "\ea1d";
|
||||
$web-icon-logout-left: "\ea1e";
|
||||
$web-icon-logout-right: "\ea1f";
|
||||
$web-icon-mailgun: "\ea20";
|
||||
$web-icon-message: "\ea21";
|
||||
$web-icon-microsoft: "\ea22";
|
||||
$web-icon-minus: "\ea23";
|
||||
$web-icon-nuxt: "\ea24";
|
||||
$web-icon-platform: "\ea25";
|
||||
$web-icon-play: "\ea26";
|
||||
$web-icon-plus: "\ea27";
|
||||
$web-icon-product-hunt: "\ea28";
|
||||
$web-icon-refine: "\ea29";
|
||||
$web-icon-rest: "\ea2a";
|
||||
$web-icon-search: "\ea2b";
|
||||
$web-icon-sendgrid: "\ea2c";
|
||||
$web-icon-star: "\ea2d";
|
||||
$web-icon-system: "\ea2e";
|
||||
$web-icon-textmagic: "\ea2f";
|
||||
$web-icon-twitter: "\ea30";
|
||||
$web-icon-vue: "\ea31";
|
||||
$web-icon-x: "\ea32";
|
||||
$web-icon-ycombinator: "\ea33";
|
||||
$web-icon-youtube: "\ea34";
|
||||
$web-icon-bluesky: "\ea08";
|
||||
$web-icon-calendar: "\ea09";
|
||||
$web-icon-check: "\ea0a";
|
||||
$web-icon-chevron-down: "\ea0b";
|
||||
$web-icon-chevron-left: "\ea0c";
|
||||
$web-icon-chevron-right: "\ea0d";
|
||||
$web-icon-chevron-up: "\ea0e";
|
||||
$web-icon-close: "\ea0f";
|
||||
$web-icon-command: "\ea10";
|
||||
$web-icon-copy: "\ea11";
|
||||
$web-icon-daily-dev: "\ea12";
|
||||
$web-icon-dark: "\ea13";
|
||||
$web-icon-discord: "\ea14";
|
||||
$web-icon-divider-vertical: "\ea15";
|
||||
$web-icon-download: "\ea16";
|
||||
$web-icon-ext-link: "\ea17";
|
||||
$web-icon-firebase: "\ea18";
|
||||
$web-icon-github: "\ea19";
|
||||
$web-icon-google: "\ea1a";
|
||||
$web-icon-hamburger-menu: "\ea1b";
|
||||
$web-icon-instagram: "\ea1c";
|
||||
$web-icon-light: "\ea1d";
|
||||
$web-icon-linkedin: "\ea1e";
|
||||
$web-icon-location: "\ea1f";
|
||||
$web-icon-logout-left: "\ea20";
|
||||
$web-icon-logout-right: "\ea21";
|
||||
$web-icon-mailgun: "\ea22";
|
||||
$web-icon-mcp: "\ea23";
|
||||
$web-icon-message: "\ea24";
|
||||
$web-icon-microsoft: "\ea25";
|
||||
$web-icon-minus: "\ea26";
|
||||
$web-icon-nuxt: "\ea27";
|
||||
$web-icon-platform: "\ea28";
|
||||
$web-icon-play: "\ea29";
|
||||
$web-icon-plus: "\ea2a";
|
||||
$web-icon-product-hunt: "\ea2b";
|
||||
$web-icon-refine: "\ea2c";
|
||||
$web-icon-rest: "\ea2d";
|
||||
$web-icon-search: "\ea2e";
|
||||
$web-icon-sendgrid: "\ea2f";
|
||||
$web-icon-star: "\ea30";
|
||||
$web-icon-system: "\ea31";
|
||||
$web-icon-textmagic: "\ea32";
|
||||
$web-icon-tiktok: "\ea33";
|
||||
$web-icon-twitter: "\ea34";
|
||||
$web-icon-vue: "\ea35";
|
||||
$web-icon-x: "\ea36";
|
||||
$web-icon-ycombinator: "\ea37";
|
||||
$web-icon-youtube: "\ea38";
|
||||
|
||||
@@ -41,274 +41,298 @@
|
||||
"className": "web-icon-arrow-up",
|
||||
"unicode": ""
|
||||
},
|
||||
"calendar": {
|
||||
"bluesky": {
|
||||
"encodedCode": "\\ea08",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-calendar",
|
||||
"className": "web-icon-bluesky",
|
||||
"unicode": ""
|
||||
},
|
||||
"check": {
|
||||
"calendar": {
|
||||
"encodedCode": "\\ea09",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-check",
|
||||
"className": "web-icon-calendar",
|
||||
"unicode": ""
|
||||
},
|
||||
"chevron-down": {
|
||||
"check": {
|
||||
"encodedCode": "\\ea0a",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-chevron-down",
|
||||
"className": "web-icon-check",
|
||||
"unicode": ""
|
||||
},
|
||||
"chevron-left": {
|
||||
"chevron-down": {
|
||||
"encodedCode": "\\ea0b",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-chevron-left",
|
||||
"className": "web-icon-chevron-down",
|
||||
"unicode": ""
|
||||
},
|
||||
"chevron-right": {
|
||||
"chevron-left": {
|
||||
"encodedCode": "\\ea0c",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-chevron-right",
|
||||
"className": "web-icon-chevron-left",
|
||||
"unicode": ""
|
||||
},
|
||||
"chevron-up": {
|
||||
"chevron-right": {
|
||||
"encodedCode": "\\ea0d",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-chevron-up",
|
||||
"className": "web-icon-chevron-right",
|
||||
"unicode": ""
|
||||
},
|
||||
"close": {
|
||||
"chevron-up": {
|
||||
"encodedCode": "\\ea0e",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-close",
|
||||
"className": "web-icon-chevron-up",
|
||||
"unicode": ""
|
||||
},
|
||||
"command": {
|
||||
"close": {
|
||||
"encodedCode": "\\ea0f",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-command",
|
||||
"className": "web-icon-close",
|
||||
"unicode": ""
|
||||
},
|
||||
"copy": {
|
||||
"command": {
|
||||
"encodedCode": "\\ea10",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-copy",
|
||||
"className": "web-icon-command",
|
||||
"unicode": ""
|
||||
},
|
||||
"daily-dev": {
|
||||
"copy": {
|
||||
"encodedCode": "\\ea11",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-daily-dev",
|
||||
"className": "web-icon-copy",
|
||||
"unicode": ""
|
||||
},
|
||||
"dark": {
|
||||
"daily-dev": {
|
||||
"encodedCode": "\\ea12",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-dark",
|
||||
"className": "web-icon-daily-dev",
|
||||
"unicode": ""
|
||||
},
|
||||
"discord": {
|
||||
"dark": {
|
||||
"encodedCode": "\\ea13",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-discord",
|
||||
"className": "web-icon-dark",
|
||||
"unicode": ""
|
||||
},
|
||||
"divider-vertical": {
|
||||
"discord": {
|
||||
"encodedCode": "\\ea14",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-divider-vertical",
|
||||
"className": "web-icon-discord",
|
||||
"unicode": ""
|
||||
},
|
||||
"download": {
|
||||
"divider-vertical": {
|
||||
"encodedCode": "\\ea15",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-download",
|
||||
"className": "web-icon-divider-vertical",
|
||||
"unicode": ""
|
||||
},
|
||||
"ext-link": {
|
||||
"download": {
|
||||
"encodedCode": "\\ea16",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-ext-link",
|
||||
"className": "web-icon-download",
|
||||
"unicode": ""
|
||||
},
|
||||
"firebase": {
|
||||
"ext-link": {
|
||||
"encodedCode": "\\ea17",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-firebase",
|
||||
"className": "web-icon-ext-link",
|
||||
"unicode": ""
|
||||
},
|
||||
"github": {
|
||||
"firebase": {
|
||||
"encodedCode": "\\ea18",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-github",
|
||||
"className": "web-icon-firebase",
|
||||
"unicode": ""
|
||||
},
|
||||
"google": {
|
||||
"github": {
|
||||
"encodedCode": "\\ea19",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-google",
|
||||
"className": "web-icon-github",
|
||||
"unicode": ""
|
||||
},
|
||||
"hamburger-menu": {
|
||||
"google": {
|
||||
"encodedCode": "\\ea1a",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-hamburger-menu",
|
||||
"className": "web-icon-google",
|
||||
"unicode": ""
|
||||
},
|
||||
"light": {
|
||||
"hamburger-menu": {
|
||||
"encodedCode": "\\ea1b",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-light",
|
||||
"className": "web-icon-hamburger-menu",
|
||||
"unicode": ""
|
||||
},
|
||||
"linkedin": {
|
||||
"instagram": {
|
||||
"encodedCode": "\\ea1c",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-linkedin",
|
||||
"className": "web-icon-instagram",
|
||||
"unicode": ""
|
||||
},
|
||||
"location": {
|
||||
"light": {
|
||||
"encodedCode": "\\ea1d",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-location",
|
||||
"className": "web-icon-light",
|
||||
"unicode": ""
|
||||
},
|
||||
"logout-left": {
|
||||
"linkedin": {
|
||||
"encodedCode": "\\ea1e",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-logout-left",
|
||||
"className": "web-icon-linkedin",
|
||||
"unicode": ""
|
||||
},
|
||||
"logout-right": {
|
||||
"location": {
|
||||
"encodedCode": "\\ea1f",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-logout-right",
|
||||
"className": "web-icon-location",
|
||||
"unicode": ""
|
||||
},
|
||||
"mailgun": {
|
||||
"logout-left": {
|
||||
"encodedCode": "\\ea20",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-mailgun",
|
||||
"className": "web-icon-logout-left",
|
||||
"unicode": ""
|
||||
},
|
||||
"message": {
|
||||
"logout-right": {
|
||||
"encodedCode": "\\ea21",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-message",
|
||||
"className": "web-icon-logout-right",
|
||||
"unicode": ""
|
||||
},
|
||||
"microsoft": {
|
||||
"mailgun": {
|
||||
"encodedCode": "\\ea22",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-microsoft",
|
||||
"className": "web-icon-mailgun",
|
||||
"unicode": ""
|
||||
},
|
||||
"minus": {
|
||||
"mcp": {
|
||||
"encodedCode": "\\ea23",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-minus",
|
||||
"className": "web-icon-mcp",
|
||||
"unicode": ""
|
||||
},
|
||||
"nuxt": {
|
||||
"message": {
|
||||
"encodedCode": "\\ea24",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-nuxt",
|
||||
"className": "web-icon-message",
|
||||
"unicode": ""
|
||||
},
|
||||
"platform": {
|
||||
"microsoft": {
|
||||
"encodedCode": "\\ea25",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-platform",
|
||||
"className": "web-icon-microsoft",
|
||||
"unicode": ""
|
||||
},
|
||||
"play": {
|
||||
"minus": {
|
||||
"encodedCode": "\\ea26",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-play",
|
||||
"className": "web-icon-minus",
|
||||
"unicode": ""
|
||||
},
|
||||
"plus": {
|
||||
"nuxt": {
|
||||
"encodedCode": "\\ea27",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-plus",
|
||||
"className": "web-icon-nuxt",
|
||||
"unicode": ""
|
||||
},
|
||||
"product-hunt": {
|
||||
"platform": {
|
||||
"encodedCode": "\\ea28",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-product-hunt",
|
||||
"className": "web-icon-platform",
|
||||
"unicode": ""
|
||||
},
|
||||
"refine": {
|
||||
"play": {
|
||||
"encodedCode": "\\ea29",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-refine",
|
||||
"className": "web-icon-play",
|
||||
"unicode": ""
|
||||
},
|
||||
"rest": {
|
||||
"plus": {
|
||||
"encodedCode": "\\ea2a",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-rest",
|
||||
"className": "web-icon-plus",
|
||||
"unicode": ""
|
||||
},
|
||||
"search": {
|
||||
"product-hunt": {
|
||||
"encodedCode": "\\ea2b",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-search",
|
||||
"className": "web-icon-product-hunt",
|
||||
"unicode": ""
|
||||
},
|
||||
"sendgrid": {
|
||||
"refine": {
|
||||
"encodedCode": "\\ea2c",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-sendgrid",
|
||||
"className": "web-icon-refine",
|
||||
"unicode": ""
|
||||
},
|
||||
"star": {
|
||||
"rest": {
|
||||
"encodedCode": "\\ea2d",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-star",
|
||||
"className": "web-icon-rest",
|
||||
"unicode": ""
|
||||
},
|
||||
"system": {
|
||||
"search": {
|
||||
"encodedCode": "\\ea2e",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-system",
|
||||
"className": "web-icon-search",
|
||||
"unicode": ""
|
||||
},
|
||||
"textmagic": {
|
||||
"sendgrid": {
|
||||
"encodedCode": "\\ea2f",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-textmagic",
|
||||
"className": "web-icon-sendgrid",
|
||||
"unicode": ""
|
||||
},
|
||||
"twitter": {
|
||||
"star": {
|
||||
"encodedCode": "\\ea30",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-twitter",
|
||||
"className": "web-icon-star",
|
||||
"unicode": ""
|
||||
},
|
||||
"vue": {
|
||||
"system": {
|
||||
"encodedCode": "\\ea31",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-vue",
|
||||
"className": "web-icon-system",
|
||||
"unicode": ""
|
||||
},
|
||||
"x": {
|
||||
"textmagic": {
|
||||
"encodedCode": "\\ea32",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-x",
|
||||
"className": "web-icon-textmagic",
|
||||
"unicode": ""
|
||||
},
|
||||
"ycombinator": {
|
||||
"tiktok": {
|
||||
"encodedCode": "\\ea33",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-ycombinator",
|
||||
"className": "web-icon-tiktok",
|
||||
"unicode": ""
|
||||
},
|
||||
"youtube": {
|
||||
"twitter": {
|
||||
"encodedCode": "\\ea34",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-youtube",
|
||||
"className": "web-icon-twitter",
|
||||
"unicode": ""
|
||||
},
|
||||
"vue": {
|
||||
"encodedCode": "\\ea35",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-vue",
|
||||
"unicode": ""
|
||||
},
|
||||
"x": {
|
||||
"encodedCode": "\\ea36",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-x",
|
||||
"unicode": ""
|
||||
},
|
||||
"ycombinator": {
|
||||
"encodedCode": "\\ea37",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-ycombinator",
|
||||
"unicode": ""
|
||||
},
|
||||
"youtube": {
|
||||
"encodedCode": "\\ea38",
|
||||
"prefix": "web-icon",
|
||||
"className": "web-icon-youtube",
|
||||
"unicode": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,138 +41,150 @@
|
||||
.web-icon-arrow-up:before {
|
||||
content: '\ea07';
|
||||
}
|
||||
.web-icon-calendar:before {
|
||||
.web-icon-bluesky:before {
|
||||
content: '\ea08';
|
||||
}
|
||||
.web-icon-check:before {
|
||||
.web-icon-calendar:before {
|
||||
content: '\ea09';
|
||||
}
|
||||
.web-icon-chevron-down:before {
|
||||
.web-icon-check:before {
|
||||
content: '\ea0a';
|
||||
}
|
||||
.web-icon-chevron-left:before {
|
||||
.web-icon-chevron-down:before {
|
||||
content: '\ea0b';
|
||||
}
|
||||
.web-icon-chevron-right:before {
|
||||
.web-icon-chevron-left:before {
|
||||
content: '\ea0c';
|
||||
}
|
||||
.web-icon-chevron-up:before {
|
||||
.web-icon-chevron-right:before {
|
||||
content: '\ea0d';
|
||||
}
|
||||
.web-icon-close:before {
|
||||
.web-icon-chevron-up:before {
|
||||
content: '\ea0e';
|
||||
}
|
||||
.web-icon-command:before {
|
||||
.web-icon-close:before {
|
||||
content: '\ea0f';
|
||||
}
|
||||
.web-icon-copy:before {
|
||||
.web-icon-command:before {
|
||||
content: '\ea10';
|
||||
}
|
||||
.web-icon-daily-dev:before {
|
||||
.web-icon-copy:before {
|
||||
content: '\ea11';
|
||||
}
|
||||
.web-icon-dark:before {
|
||||
.web-icon-daily-dev:before {
|
||||
content: '\ea12';
|
||||
}
|
||||
.web-icon-discord:before {
|
||||
.web-icon-dark:before {
|
||||
content: '\ea13';
|
||||
}
|
||||
.web-icon-divider-vertical:before {
|
||||
.web-icon-discord:before {
|
||||
content: '\ea14';
|
||||
}
|
||||
.web-icon-download:before {
|
||||
.web-icon-divider-vertical:before {
|
||||
content: '\ea15';
|
||||
}
|
||||
.web-icon-ext-link:before {
|
||||
.web-icon-download:before {
|
||||
content: '\ea16';
|
||||
}
|
||||
.web-icon-firebase:before {
|
||||
.web-icon-ext-link:before {
|
||||
content: '\ea17';
|
||||
}
|
||||
.web-icon-github:before {
|
||||
.web-icon-firebase:before {
|
||||
content: '\ea18';
|
||||
}
|
||||
.web-icon-google:before {
|
||||
.web-icon-github:before {
|
||||
content: '\ea19';
|
||||
}
|
||||
.web-icon-hamburger-menu:before {
|
||||
.web-icon-google:before {
|
||||
content: '\ea1a';
|
||||
}
|
||||
.web-icon-light:before {
|
||||
.web-icon-hamburger-menu:before {
|
||||
content: '\ea1b';
|
||||
}
|
||||
.web-icon-linkedin:before {
|
||||
.web-icon-instagram:before {
|
||||
content: '\ea1c';
|
||||
}
|
||||
.web-icon-location:before {
|
||||
.web-icon-light:before {
|
||||
content: '\ea1d';
|
||||
}
|
||||
.web-icon-logout-left:before {
|
||||
.web-icon-linkedin:before {
|
||||
content: '\ea1e';
|
||||
}
|
||||
.web-icon-logout-right:before {
|
||||
.web-icon-location:before {
|
||||
content: '\ea1f';
|
||||
}
|
||||
.web-icon-mailgun:before {
|
||||
.web-icon-logout-left:before {
|
||||
content: '\ea20';
|
||||
}
|
||||
.web-icon-message:before {
|
||||
.web-icon-logout-right:before {
|
||||
content: '\ea21';
|
||||
}
|
||||
.web-icon-microsoft:before {
|
||||
.web-icon-mailgun:before {
|
||||
content: '\ea22';
|
||||
}
|
||||
.web-icon-minus:before {
|
||||
.web-icon-mcp:before {
|
||||
content: '\ea23';
|
||||
}
|
||||
.web-icon-nuxt:before {
|
||||
.web-icon-message:before {
|
||||
content: '\ea24';
|
||||
}
|
||||
.web-icon-platform:before {
|
||||
.web-icon-microsoft:before {
|
||||
content: '\ea25';
|
||||
}
|
||||
.web-icon-play:before {
|
||||
.web-icon-minus:before {
|
||||
content: '\ea26';
|
||||
}
|
||||
.web-icon-plus:before {
|
||||
.web-icon-nuxt:before {
|
||||
content: '\ea27';
|
||||
}
|
||||
.web-icon-product-hunt:before {
|
||||
.web-icon-platform:before {
|
||||
content: '\ea28';
|
||||
}
|
||||
.web-icon-refine:before {
|
||||
.web-icon-play:before {
|
||||
content: '\ea29';
|
||||
}
|
||||
.web-icon-rest:before {
|
||||
.web-icon-plus:before {
|
||||
content: '\ea2a';
|
||||
}
|
||||
.web-icon-search:before {
|
||||
.web-icon-product-hunt:before {
|
||||
content: '\ea2b';
|
||||
}
|
||||
.web-icon-sendgrid:before {
|
||||
.web-icon-refine:before {
|
||||
content: '\ea2c';
|
||||
}
|
||||
.web-icon-star:before {
|
||||
.web-icon-rest:before {
|
||||
content: '\ea2d';
|
||||
}
|
||||
.web-icon-system:before {
|
||||
.web-icon-search:before {
|
||||
content: '\ea2e';
|
||||
}
|
||||
.web-icon-textmagic:before {
|
||||
.web-icon-sendgrid:before {
|
||||
content: '\ea2f';
|
||||
}
|
||||
.web-icon-twitter:before {
|
||||
.web-icon-star:before {
|
||||
content: '\ea30';
|
||||
}
|
||||
.web-icon-vue:before {
|
||||
.web-icon-system:before {
|
||||
content: '\ea31';
|
||||
}
|
||||
.web-icon-x:before {
|
||||
.web-icon-textmagic:before {
|
||||
content: '\ea32';
|
||||
}
|
||||
.web-icon-ycombinator:before {
|
||||
.web-icon-tiktok:before {
|
||||
content: '\ea33';
|
||||
}
|
||||
.web-icon-youtube:before {
|
||||
.web-icon-twitter:before {
|
||||
content: '\ea34';
|
||||
}
|
||||
.web-icon-vue:before {
|
||||
content: '\ea35';
|
||||
}
|
||||
.web-icon-x:before {
|
||||
content: '\ea36';
|
||||
}
|
||||
.web-icon-ycombinator:before {
|
||||
content: '\ea37';
|
||||
}
|
||||
.web-icon-youtube:before {
|
||||
content: '\ea38';
|
||||
}
|
||||
|
||||
@@ -23,51 +23,55 @@
|
||||
.web-icon-arrow-left:before { content: "\ea05"; }
|
||||
.web-icon-arrow-right:before { content: "\ea06"; }
|
||||
.web-icon-arrow-up:before { content: "\ea07"; }
|
||||
.web-icon-calendar:before { content: "\ea08"; }
|
||||
.web-icon-check:before { content: "\ea09"; }
|
||||
.web-icon-chevron-down:before { content: "\ea0a"; }
|
||||
.web-icon-chevron-left:before { content: "\ea0b"; }
|
||||
.web-icon-chevron-right:before { content: "\ea0c"; }
|
||||
.web-icon-chevron-up:before { content: "\ea0d"; }
|
||||
.web-icon-close:before { content: "\ea0e"; }
|
||||
.web-icon-command:before { content: "\ea0f"; }
|
||||
.web-icon-copy:before { content: "\ea10"; }
|
||||
.web-icon-daily-dev:before { content: "\ea11"; }
|
||||
.web-icon-dark:before { content: "\ea12"; }
|
||||
.web-icon-discord:before { content: "\ea13"; }
|
||||
.web-icon-divider-vertical:before { content: "\ea14"; }
|
||||
.web-icon-download:before { content: "\ea15"; }
|
||||
.web-icon-ext-link:before { content: "\ea16"; }
|
||||
.web-icon-firebase:before { content: "\ea17"; }
|
||||
.web-icon-github:before { content: "\ea18"; }
|
||||
.web-icon-google:before { content: "\ea19"; }
|
||||
.web-icon-hamburger-menu:before { content: "\ea1a"; }
|
||||
.web-icon-light:before { content: "\ea1b"; }
|
||||
.web-icon-linkedin:before { content: "\ea1c"; }
|
||||
.web-icon-location:before { content: "\ea1d"; }
|
||||
.web-icon-logout-left:before { content: "\ea1e"; }
|
||||
.web-icon-logout-right:before { content: "\ea1f"; }
|
||||
.web-icon-mailgun:before { content: "\ea20"; }
|
||||
.web-icon-message:before { content: "\ea21"; }
|
||||
.web-icon-microsoft:before { content: "\ea22"; }
|
||||
.web-icon-minus:before { content: "\ea23"; }
|
||||
.web-icon-nuxt:before { content: "\ea24"; }
|
||||
.web-icon-platform:before { content: "\ea25"; }
|
||||
.web-icon-play:before { content: "\ea26"; }
|
||||
.web-icon-plus:before { content: "\ea27"; }
|
||||
.web-icon-product-hunt:before { content: "\ea28"; }
|
||||
.web-icon-refine:before { content: "\ea29"; }
|
||||
.web-icon-rest:before { content: "\ea2a"; }
|
||||
.web-icon-search:before { content: "\ea2b"; }
|
||||
.web-icon-sendgrid:before { content: "\ea2c"; }
|
||||
.web-icon-star:before { content: "\ea2d"; }
|
||||
.web-icon-system:before { content: "\ea2e"; }
|
||||
.web-icon-textmagic:before { content: "\ea2f"; }
|
||||
.web-icon-twitter:before { content: "\ea30"; }
|
||||
.web-icon-vue:before { content: "\ea31"; }
|
||||
.web-icon-x:before { content: "\ea32"; }
|
||||
.web-icon-ycombinator:before { content: "\ea33"; }
|
||||
.web-icon-youtube:before { content: "\ea34"; }
|
||||
.web-icon-bluesky:before { content: "\ea08"; }
|
||||
.web-icon-calendar:before { content: "\ea09"; }
|
||||
.web-icon-check:before { content: "\ea0a"; }
|
||||
.web-icon-chevron-down:before { content: "\ea0b"; }
|
||||
.web-icon-chevron-left:before { content: "\ea0c"; }
|
||||
.web-icon-chevron-right:before { content: "\ea0d"; }
|
||||
.web-icon-chevron-up:before { content: "\ea0e"; }
|
||||
.web-icon-close:before { content: "\ea0f"; }
|
||||
.web-icon-command:before { content: "\ea10"; }
|
||||
.web-icon-copy:before { content: "\ea11"; }
|
||||
.web-icon-daily-dev:before { content: "\ea12"; }
|
||||
.web-icon-dark:before { content: "\ea13"; }
|
||||
.web-icon-discord:before { content: "\ea14"; }
|
||||
.web-icon-divider-vertical:before { content: "\ea15"; }
|
||||
.web-icon-download:before { content: "\ea16"; }
|
||||
.web-icon-ext-link:before { content: "\ea17"; }
|
||||
.web-icon-firebase:before { content: "\ea18"; }
|
||||
.web-icon-github:before { content: "\ea19"; }
|
||||
.web-icon-google:before { content: "\ea1a"; }
|
||||
.web-icon-hamburger-menu:before { content: "\ea1b"; }
|
||||
.web-icon-instagram:before { content: "\ea1c"; }
|
||||
.web-icon-light:before { content: "\ea1d"; }
|
||||
.web-icon-linkedin:before { content: "\ea1e"; }
|
||||
.web-icon-location:before { content: "\ea1f"; }
|
||||
.web-icon-logout-left:before { content: "\ea20"; }
|
||||
.web-icon-logout-right:before { content: "\ea21"; }
|
||||
.web-icon-mailgun:before { content: "\ea22"; }
|
||||
.web-icon-mcp:before { content: "\ea23"; }
|
||||
.web-icon-message:before { content: "\ea24"; }
|
||||
.web-icon-microsoft:before { content: "\ea25"; }
|
||||
.web-icon-minus:before { content: "\ea26"; }
|
||||
.web-icon-nuxt:before { content: "\ea27"; }
|
||||
.web-icon-platform:before { content: "\ea28"; }
|
||||
.web-icon-play:before { content: "\ea29"; }
|
||||
.web-icon-plus:before { content: "\ea2a"; }
|
||||
.web-icon-product-hunt:before { content: "\ea2b"; }
|
||||
.web-icon-refine:before { content: "\ea2c"; }
|
||||
.web-icon-rest:before { content: "\ea2d"; }
|
||||
.web-icon-search:before { content: "\ea2e"; }
|
||||
.web-icon-sendgrid:before { content: "\ea2f"; }
|
||||
.web-icon-star:before { content: "\ea30"; }
|
||||
.web-icon-system:before { content: "\ea31"; }
|
||||
.web-icon-textmagic:before { content: "\ea32"; }
|
||||
.web-icon-tiktok:before { content: "\ea33"; }
|
||||
.web-icon-twitter:before { content: "\ea34"; }
|
||||
.web-icon-vue:before { content: "\ea35"; }
|
||||
.web-icon-x:before { content: "\ea36"; }
|
||||
.web-icon-ycombinator:before { content: "\ea37"; }
|
||||
.web-icon-youtube:before { content: "\ea38"; }
|
||||
|
||||
$web-icon-apple: "\ea01";
|
||||
$web-icon-appwrite: "\ea02";
|
||||
@@ -76,48 +80,52 @@ $web-icon-arrow-ext-link: "\ea04";
|
||||
$web-icon-arrow-left: "\ea05";
|
||||
$web-icon-arrow-right: "\ea06";
|
||||
$web-icon-arrow-up: "\ea07";
|
||||
$web-icon-calendar: "\ea08";
|
||||
$web-icon-check: "\ea09";
|
||||
$web-icon-chevron-down: "\ea0a";
|
||||
$web-icon-chevron-left: "\ea0b";
|
||||
$web-icon-chevron-right: "\ea0c";
|
||||
$web-icon-chevron-up: "\ea0d";
|
||||
$web-icon-close: "\ea0e";
|
||||
$web-icon-command: "\ea0f";
|
||||
$web-icon-copy: "\ea10";
|
||||
$web-icon-daily-dev: "\ea11";
|
||||
$web-icon-dark: "\ea12";
|
||||
$web-icon-discord: "\ea13";
|
||||
$web-icon-divider-vertical: "\ea14";
|
||||
$web-icon-download: "\ea15";
|
||||
$web-icon-ext-link: "\ea16";
|
||||
$web-icon-firebase: "\ea17";
|
||||
$web-icon-github: "\ea18";
|
||||
$web-icon-google: "\ea19";
|
||||
$web-icon-hamburger-menu: "\ea1a";
|
||||
$web-icon-light: "\ea1b";
|
||||
$web-icon-linkedin: "\ea1c";
|
||||
$web-icon-location: "\ea1d";
|
||||
$web-icon-logout-left: "\ea1e";
|
||||
$web-icon-logout-right: "\ea1f";
|
||||
$web-icon-mailgun: "\ea20";
|
||||
$web-icon-message: "\ea21";
|
||||
$web-icon-microsoft: "\ea22";
|
||||
$web-icon-minus: "\ea23";
|
||||
$web-icon-nuxt: "\ea24";
|
||||
$web-icon-platform: "\ea25";
|
||||
$web-icon-play: "\ea26";
|
||||
$web-icon-plus: "\ea27";
|
||||
$web-icon-product-hunt: "\ea28";
|
||||
$web-icon-refine: "\ea29";
|
||||
$web-icon-rest: "\ea2a";
|
||||
$web-icon-search: "\ea2b";
|
||||
$web-icon-sendgrid: "\ea2c";
|
||||
$web-icon-star: "\ea2d";
|
||||
$web-icon-system: "\ea2e";
|
||||
$web-icon-textmagic: "\ea2f";
|
||||
$web-icon-twitter: "\ea30";
|
||||
$web-icon-vue: "\ea31";
|
||||
$web-icon-x: "\ea32";
|
||||
$web-icon-ycombinator: "\ea33";
|
||||
$web-icon-youtube: "\ea34";
|
||||
$web-icon-bluesky: "\ea08";
|
||||
$web-icon-calendar: "\ea09";
|
||||
$web-icon-check: "\ea0a";
|
||||
$web-icon-chevron-down: "\ea0b";
|
||||
$web-icon-chevron-left: "\ea0c";
|
||||
$web-icon-chevron-right: "\ea0d";
|
||||
$web-icon-chevron-up: "\ea0e";
|
||||
$web-icon-close: "\ea0f";
|
||||
$web-icon-command: "\ea10";
|
||||
$web-icon-copy: "\ea11";
|
||||
$web-icon-daily-dev: "\ea12";
|
||||
$web-icon-dark: "\ea13";
|
||||
$web-icon-discord: "\ea14";
|
||||
$web-icon-divider-vertical: "\ea15";
|
||||
$web-icon-download: "\ea16";
|
||||
$web-icon-ext-link: "\ea17";
|
||||
$web-icon-firebase: "\ea18";
|
||||
$web-icon-github: "\ea19";
|
||||
$web-icon-google: "\ea1a";
|
||||
$web-icon-hamburger-menu: "\ea1b";
|
||||
$web-icon-instagram: "\ea1c";
|
||||
$web-icon-light: "\ea1d";
|
||||
$web-icon-linkedin: "\ea1e";
|
||||
$web-icon-location: "\ea1f";
|
||||
$web-icon-logout-left: "\ea20";
|
||||
$web-icon-logout-right: "\ea21";
|
||||
$web-icon-mailgun: "\ea22";
|
||||
$web-icon-mcp: "\ea23";
|
||||
$web-icon-message: "\ea24";
|
||||
$web-icon-microsoft: "\ea25";
|
||||
$web-icon-minus: "\ea26";
|
||||
$web-icon-nuxt: "\ea27";
|
||||
$web-icon-platform: "\ea28";
|
||||
$web-icon-play: "\ea29";
|
||||
$web-icon-plus: "\ea2a";
|
||||
$web-icon-product-hunt: "\ea2b";
|
||||
$web-icon-refine: "\ea2c";
|
||||
$web-icon-rest: "\ea2d";
|
||||
$web-icon-search: "\ea2e";
|
||||
$web-icon-sendgrid: "\ea2f";
|
||||
$web-icon-star: "\ea30";
|
||||
$web-icon-system: "\ea31";
|
||||
$web-icon-textmagic: "\ea32";
|
||||
$web-icon-tiktok: "\ea33";
|
||||
$web-icon-twitter: "\ea34";
|
||||
$web-icon-vue: "\ea35";
|
||||
$web-icon-x: "\ea36";
|
||||
$web-icon-ycombinator: "\ea37";
|
||||
$web-icon-youtube: "\ea38";
|
||||
|
||||
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 78 KiB |
4
src/icons/svg/bluesky.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none">
|
||||
<path d="M9.58671 6.15399C13.5994 9.28212 17.9155 15.6246 19.5001 19.0284C21.0849 15.6249 25.4007 9.28205 29.4135 6.15399C32.3088 3.89686 37 2.15043 37 7.70768C37 8.81754 36.3872 17.0311 36.0278 18.3645C34.7785 23.0005 30.226 24.1829 26.1766 23.4672C33.255 24.7182 35.0556 28.8619 31.1669 33.0056C23.7813 40.8752 20.5517 31.031 19.7237 28.5086C19.572 28.0462 19.501 27.8298 19.5 28.0138C19.4989 27.8298 19.428 28.0462 19.2763 28.5086C18.4487 31.031 15.2191 40.8755 7.83314 33.0056C3.9443 28.8619 5.74492 24.718 12.8234 23.4672C8.77385 24.1829 4.22134 23.0005 2.97221 18.3645C2.61279 17.031 2 8.81742 2 7.70768C2 2.15043 6.6913 3.89686 9.58651 6.15399H9.58671Z" fill="#19191C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 823 B |
6
src/icons/svg/instagram.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30" fill="none">
|
||||
<path d="M15.0029 2.7017C19.0115 2.7017 19.4862 2.71928 21.0627 2.78961C22.5278 2.85407 23.319 3.10022 23.8465 3.30533C24.5439 3.57492 25.0479 3.90311 25.5694 4.42469C26.0969 4.95214 26.4192 5.45028 26.6888 6.14769C26.8939 6.67513 27.1401 7.47216 27.2045 8.93143C27.2749 10.5138 27.2924 10.9885 27.2924 14.9912C27.2924 18.9998 27.2749 19.4745 27.2045 21.051C27.1401 22.5161 26.8939 23.3073 26.6888 23.8347C26.4192 24.5321 26.091 25.0361 25.5694 25.5577C25.042 26.0852 24.5439 26.4075 23.8465 26.6771C23.319 26.8822 22.522 27.1283 21.0627 27.1928C19.4804 27.2631 19.0057 27.2807 15.0029 27.2807C10.9943 27.2807 10.5196 27.2631 8.94315 27.1928C7.47802 27.1283 6.68685 26.8822 6.15941 26.6771C5.46201 26.4075 4.958 26.0793 4.43641 25.5577C3.90897 25.0303 3.58664 24.5321 3.31705 23.8347C3.11194 23.3073 2.86579 22.5103 2.80133 21.051C2.731 19.4686 2.71342 18.9939 2.71342 14.9912C2.71342 10.9826 2.731 10.5079 2.80133 8.93143C2.86579 7.4663 3.11194 6.67513 3.31705 6.14769C3.58664 5.45028 3.91483 4.94628 4.43641 4.42469C4.96386 3.89725 5.46201 3.57492 6.15941 3.30533C6.68685 3.10022 7.48388 2.85407 8.94315 2.78961C10.5196 2.71928 10.9943 2.7017 15.0029 2.7017ZM15.0029 0C10.9299 0 10.42 0.0175816 8.82008 0.0879078C7.22602 0.158234 6.1301 0.416097 5.1807 0.78531C4.19027 1.1721 3.35222 1.68197 2.52002 2.52002C1.68197 3.35222 1.1721 4.19027 0.78531 5.17484C0.416097 6.1301 0.158234 7.22016 0.0879078 8.81422C0.0175816 10.42 0 10.9299 0 15.0029C0 19.076 0.0175816 19.5859 0.0879078 21.1858C0.158234 22.7798 0.416097 23.8758 0.78531 24.8252C1.1721 25.8156 1.68197 26.6536 2.52002 27.4858C3.35222 28.318 4.19027 28.8338 5.17484 29.2147C6.1301 29.5839 7.22016 29.8418 8.81422 29.9121C10.4141 29.9824 10.924 30 14.9971 30C19.0701 30 19.58 29.9824 21.1799 29.9121C22.774 29.8418 23.8699 29.5839 24.8193 29.2147C25.8039 28.8338 26.6419 28.318 27.4741 27.4858C28.3063 26.6536 28.822 25.8156 29.203 24.831C29.5722 23.8758 29.83 22.7857 29.9004 21.1916C29.9707 19.5917 29.9883 19.0819 29.9883 15.0088C29.9883 10.9357 29.9707 10.4259 29.9004 8.82594C29.83 7.23188 29.5722 6.13596 29.203 5.18656C28.8338 4.19027 28.3239 3.35222 27.4858 2.52002C26.6536 1.68783 25.8156 1.1721 24.831 0.79117C23.8758 0.421957 22.7857 0.164095 21.1916 0.0937683C19.5859 0.0175816 19.076 0 15.0029 0Z" fill="#19191C"/>
|
||||
<path d="M15.0029 7.29633C10.7482 7.29633 7.29636 10.7482 7.29636 15.0029C7.29636 19.2576 10.7482 22.7095 15.0029 22.7095C19.2577 22.7095 22.7095 19.2576 22.7095 15.0029C22.7095 10.7482 19.2577 7.29633 15.0029 7.29633ZM15.0029 20.0019C12.2426 20.0019 10.0039 17.7632 10.0039 15.0029C10.0039 12.2426 12.2426 10.0039 15.0029 10.0039C17.7632 10.0039 20.002 12.2426 20.002 15.0029C20.002 17.7632 17.7632 20.0019 15.0029 20.0019Z" fill="#19191C"/>
|
||||
<path d="M24.8134 6.99156C24.8134 7.98785 24.0047 8.79075 23.0143 8.79075C22.018 8.79075 21.2151 7.98199 21.2151 6.99156C21.2151 5.99528 22.0238 5.19238 23.0143 5.19238C24.0047 5.19238 24.8134 6.00114 24.8134 6.99156Z" fill="#19191C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
5
src/icons/svg/mcp.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="156" height="174" viewBox="0 0 156 174" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 81.8531L73.8823 13.9709C83.255 4.59826 98.451 4.59826 107.823 13.9709C117.196 23.3434 117.196 38.5394 107.823 47.912L56.5581 99.1773" stroke="black" stroke-width="12" stroke-linecap="round"/>
|
||||
<path d="M57.2656 98.4706L107.823 47.9123C117.196 38.5397 132.392 38.5397 141.765 47.9123L142.118 48.2658C151.491 57.6384 151.491 72.8344 142.118 82.2069L80.7251 143.601C77.6009 146.725 77.6009 151.79 80.7251 154.914L93.3313 167.521" stroke="black" stroke-width="12" stroke-linecap="round"/>
|
||||
<path d="M90.8533 30.9414L40.6485 81.146C31.276 90.5183 31.276 105.714 40.6485 115.087C50.0211 124.459 65.2171 124.459 74.5897 115.087L124.794 64.8825" stroke="black" stroke-width="12" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 813 B |
4
src/icons/svg/tiktok.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none">
|
||||
<path d="M25.8811 5H20.9485V25.4347C20.9485 27.8696 19.0515 29.8696 16.6906 29.8696C14.3297 29.8696 12.4325 27.8696 12.4325 25.4347C12.4325 23.0435 14.2875 21.0869 16.5641 21V15.8696C11.5472 15.9565 7.5 20.1739 7.5 25.4347C7.5 30.7392 11.6315 35 16.7327 35C21.8339 35 25.9654 30.6957 25.9654 25.4347V14.9565C27.8204 16.3478 30.0969 17.1739 32.5 17.2174V12.087C28.7901 11.9565 25.8811 8.82608 25.8811 5Z" fill="#19191C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 566 B |
@@ -10,7 +10,7 @@
|
||||
class="true-body"
|
||||
style:width={`${$bodyRect?.width ?? 0}px`}
|
||||
style:height={`${$bodyRect?.height ?? 0}px`}
|
||||
/>
|
||||
></div>
|
||||
<div class="body" use:rect={bodyRect}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
|
||||
<div class="code-console">
|
||||
<div class="header">
|
||||
<div class="ellipse" />
|
||||
<div class="ellipse-2" />
|
||||
<div class="ellipse-3" />
|
||||
<div class="ellipse"></div>
|
||||
<div class="ellipse-2"></div>
|
||||
<div class="ellipse-3"></div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<AutoBox>
|
||||
<slot {Code} />
|
||||
</AutoBox>
|
||||
</div>
|
||||
<div id="code-bottom" />
|
||||
<div id="code-bottom"></div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { toScale, type Scale } from '$lib/utils/toScale';
|
||||
import { spring, type AnimationListOptions, type SpringOptions } from 'motion';
|
||||
import { animation, createScrollHandler, scroll, type Animation } from '.';
|
||||
import { GITHUB_REPO_LINK, GITHUB_STARS } from '$lib/constants';
|
||||
import { SOCIAL_STATS } from '$lib/constants';
|
||||
|
||||
const springOptions: SpringOptions = {
|
||||
stiffness: 58.78,
|
||||
@@ -180,7 +180,7 @@
|
||||
|
||||
<div class="cards-wrapper">
|
||||
<a
|
||||
href="/discord"
|
||||
href={SOCIAL_STATS.DISCORD.LINK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
|
||||
@@ -191,30 +191,32 @@
|
||||
class="web-icon-discord web-u-font-size-40"
|
||||
aria-hidden="true"
|
||||
aria-label="Discord"
|
||||
/>
|
||||
></span>
|
||||
</div>
|
||||
<div class="text-title font-aeonik-pro mt-auto">
|
||||
{SOCIAL_STATS.DISCORD.STAT} Discord Members
|
||||
</div>
|
||||
<div class="text-title font-aeonik-pro mt-auto">17k+ Discord Members</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
|
||||
id="oss-github"
|
||||
href={GITHUB_REPO_LINK}
|
||||
href={SOCIAL_STATS.GITHUB.LINK}
|
||||
>
|
||||
<div class="flex flex-col justify-between gap-8">
|
||||
<span
|
||||
class="web-icon-github web-u-font-size-40"
|
||||
aria-hidden="true"
|
||||
aria-label="GitHub"
|
||||
/>
|
||||
></span>
|
||||
</div>
|
||||
<div class="text-title font-aeonik-pro mt-auto">
|
||||
{GITHUB_STARS}+ GitHub Stars
|
||||
{SOCIAL_STATS.GITHUB.STAT} GitHub Stars
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://twitter.com/appwrite"
|
||||
href={SOCIAL_STATS.TWITTER.LINK}
|
||||
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
|
||||
id="oss-twitter"
|
||||
>
|
||||
@@ -223,13 +225,15 @@
|
||||
class="web-icon-x web-u-font-size-40"
|
||||
aria-hidden="true"
|
||||
aria-label="Twitter"
|
||||
/>
|
||||
></span>
|
||||
</div>
|
||||
<div class="text-title font-aeonik-pro mt-auto">
|
||||
{SOCIAL_STATS.TWITTER.STAT} Twitter Followers
|
||||
</div>
|
||||
<div class="text-title font-aeonik-pro mt-auto">128k+ Twitter Followers</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://www.youtube.com/@Appwrite"
|
||||
href={SOCIAL_STATS.YOUTUBE.LINK}
|
||||
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
|
||||
id="oss-youtube"
|
||||
>
|
||||
@@ -238,24 +242,28 @@
|
||||
class="web-icon-youtube web-u-font-size-40"
|
||||
aria-hidden="true"
|
||||
aria-label="YouTube"
|
||||
/>
|
||||
></span>
|
||||
</div>
|
||||
<div class="text-title font-aeonik-pro mt-auto">
|
||||
{SOCIAL_STATS.YOUTUBE.STAT} Youtube Subscribers
|
||||
</div>
|
||||
<div class="text-title font-aeonik-pro mt-auto">7k+ Youtube Subscribers</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
|
||||
id="oss-commits"
|
||||
href={GITHUB_REPO_LINK}
|
||||
href={SOCIAL_STATS.GITHUB.LINK}
|
||||
>
|
||||
<div class="flex flex-col justify-between gap-8">
|
||||
<span
|
||||
class="web-icon-github web-u-font-size-40"
|
||||
aria-hidden="true"
|
||||
aria-label="GitHub"
|
||||
/>
|
||||
></span>
|
||||
</div>
|
||||
<div class="text-title font-aeonik-pro mt-auto">
|
||||
{SOCIAL_STATS.GITHUB.EXTRA?.COMMITS} Code Commits
|
||||
</div>
|
||||
<div class="text-title font-aeonik-pro mt-auto">21k+ Code Commits</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import StorageShot from './(assets)/storage-shot.png?enhanced';
|
||||
import RealtimeShot from './(assets)/realtime-shot.png?enhanced';
|
||||
import MessagingShot from './(assets)/messaging-shot.png?enhanced';
|
||||
import type { EnhancedImgAttributes } from '@sveltejs/enhanced-img';
|
||||
|
||||
export const elId = writable(0);
|
||||
|
||||
@@ -33,7 +34,7 @@
|
||||
subtitle: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
shot?: string;
|
||||
shot?: EnhancedImgAttributes['src'];
|
||||
};
|
||||
export const infos: { [K in Product]?: ProductInfo } = {
|
||||
auth: {
|
||||
@@ -237,7 +238,7 @@
|
||||
>
|
||||
{#if scrollInfo.percentage > -0.1}
|
||||
<span
|
||||
class="web-badges text-micro uppercase !text-white"
|
||||
class="web-badges text-micro !text-white uppercase"
|
||||
transition:slide={{ axis: 'x' }}>Products_</span
|
||||
>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<div class="outside">
|
||||
<div class="wrapper">
|
||||
<span class="web-badges text-micro uppercase !text-white">Products_</span>
|
||||
<span class="web-badges text-micro !text-white uppercase">Products_</span>
|
||||
|
||||
<h2 class="text-display font-aeonik-pro text-primary mt-4">
|
||||
Your backend, minus the hassle
|
||||
@@ -60,7 +60,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="img-overlay" />
|
||||
<div class="img-overlay"></div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="wrapper">
|
||||
<button use:melt={$root} class="anim-checkbox">
|
||||
{#if $isChecked}
|
||||
<span class="web-icon-check" />
|
||||
<span class="web-icon-check"></span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
{#each objectKeys($state.controls) as provider, i}
|
||||
{@const isLast = i === objectKeys($state.controls).length - 1}
|
||||
<div>
|
||||
<span class={getIcon(provider)} />
|
||||
<span class={getIcon(provider)}></span>
|
||||
<span>{provider}</span>
|
||||
<Switch bind:checked={$state.controls[provider]} />
|
||||
</div>
|
||||
{#if !isLast}
|
||||
<div class="sep" />
|
||||
<div class="sep"></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
animate:flip={{ duration: 250 }}
|
||||
>
|
||||
<div class="inner">
|
||||
<span class="web-icon-{provider.toLowerCase()}" />
|
||||
<span class="web-icon-{provider.toLowerCase()}"></span>
|
||||
<span>{provider}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{#each $state.tasks.slice(0, $state.tableSlice) as task (task.id)}
|
||||
<div class="row" transition:slide={{ duration: 150 }} animate:flip={{ duration: 150 }}>
|
||||
<div class="copy-button">
|
||||
<span class="web-icon-copy" />
|
||||
<span class="web-icon-copy"></span>
|
||||
<span>{task.id}</span>
|
||||
</div>
|
||||
<span class="truncated">{task.title}</span>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div data-theme-ignore class="inner-phone light">
|
||||
<div class="header">
|
||||
<p class="title">Your tasks</p>
|
||||
<span class="icon-menu" aria-label="menu" />
|
||||
<span class="icon-menu" aria-label="menu"></span>
|
||||
</div>
|
||||
|
||||
<div class="date">Today</div>
|
||||
@@ -22,9 +22,9 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button class="add-btn">
|
||||
<span class="web-icon-plus" />
|
||||
</button>
|
||||
<div class="add-btn">
|
||||
<span class="web-icon-plus"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -25,14 +25,14 @@ return res.json({ success: true });`.trim();
|
||||
|
||||
<div use:portal={{ target: '#code-bottom' }} class="bottom">
|
||||
{#if $state.submit !== 'idle'}
|
||||
<span class="web-icon-github" in:fade />
|
||||
<span class="web-icon-github" in:fade></span>
|
||||
{/if}
|
||||
{#if $state.submit === 'loading'}
|
||||
<span in:fade>Pushing to GitHub...</span>
|
||||
<div class="loader is-small" in:fade />
|
||||
<div class="loader is-small" in:fade></div>
|
||||
{:else if $state.submit === 'success'}
|
||||
<span>Deployed to Appwrite Cloud</span>
|
||||
<span class="web-icon-check" />
|
||||
<span class="web-icon-check"></span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<div data-theme-ignore class="inner-phone light">
|
||||
<div class="header">
|
||||
<p class="title">Upgrade plan</p>
|
||||
<span class="icon-menu" aria-label="menu" />
|
||||
<span class="icon-menu" aria-label="menu"></span>
|
||||
</div>
|
||||
|
||||
<div class="plan">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{#each $state.messages.slice(0, $state.tableSlice) as task (task.id)}
|
||||
<div class="row" transition:slide={{ duration: 150 }} animate:flip={{ duration: 150 }}>
|
||||
<div class="copy-button">
|
||||
<span class="web-icon-copy" />
|
||||
<span class="web-icon-copy"></span>
|
||||
<span>{task.id}</span>
|
||||
</div>
|
||||
<div class="icon-button">
|
||||
@@ -27,9 +27,9 @@
|
||||
|
||||
<div class="status-indicator">
|
||||
{#if task.status === 'sending'}
|
||||
<div class="loader is-small" in:fade />
|
||||
<div class="loader is-small" in:fade></div>
|
||||
{:else}
|
||||
<span class="web-icon-check" />
|
||||
<span class="web-icon-check"></span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
{#if $state.submit === 'success'}
|
||||
<div class="push-notification" in:fly={{ y: -20 }}>
|
||||
<div class="icon" />
|
||||
<div class="icon"></div>
|
||||
<div class="content">
|
||||
<div class="header">
|
||||
<h3 class="title">New task assigned to you</h3>
|
||||
@@ -22,7 +22,7 @@
|
||||
<div data-theme-ignore class="inner-phone light">
|
||||
<div class="header">
|
||||
<p class="title">Your tasks</p>
|
||||
<span class="icon-menu" aria-label="menu" />
|
||||
<span class="icon-menu" aria-label="menu"></span>
|
||||
</div>
|
||||
|
||||
<div class="date">Today</div>
|
||||
@@ -34,9 +34,9 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<button class="add-btn">
|
||||
<span class="web-icon-plus" />
|
||||
</button>
|
||||
<div class="add-btn">
|
||||
<span class="web-icon-plus"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<div class="gradient-box auth" id="post-auth-{$elId}">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="icon-user-group" />
|
||||
<p class="icon-user-group"></p>
|
||||
<p class="f-eyebrow">Authentication</p>
|
||||
</div>
|
||||
<p class="f-display mbs-16">
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<div class="gradient-box storage" id="post-storage-{$elId}">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="icon-folder" />
|
||||
<p class="icon-folder"></p>
|
||||
<p class="f-eyebrow">Storage</p>
|
||||
</div>
|
||||
<p class="f-display mbs-16">
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
<div class="gradient-box functions" id="post-functions-{$elId}">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="icon-lightning-bolt" />
|
||||
<p class="icon-lightning-bolt"></p>
|
||||
<p class="f-eyebrow">Functions</p>
|
||||
</div>
|
||||
<p class="f-display mbs-16">
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
<div class="gradient-box databases" id="post-databases-{$elId}">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="icon-database" />
|
||||
<p class="icon-database"></p>
|
||||
<p class="f-eyebrow">Databases</p>
|
||||
</div>
|
||||
<p class="f-display mbs-16">
|
||||
|
||||
@@ -55,13 +55,13 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="vertical-sep" />
|
||||
<span class="icon-menu" />
|
||||
<div class="vertical-sep"></div>
|
||||
<span class="icon-menu"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="search">
|
||||
<span class="web-icon-search" />
|
||||
<span class="web-icon-search"></span>
|
||||
<span class="text"> Search </span>
|
||||
</div>
|
||||
<div class="flow gap-8">
|
||||
@@ -81,11 +81,11 @@
|
||||
<div class="title">
|
||||
<span class="text capitalize">{col}</span>
|
||||
<span class="tgl-inline-tag">{tasks.length}</span>
|
||||
<span class="icon-dots-horizontal" />
|
||||
<span class="icon-dots-horizontal"></span>
|
||||
</div>
|
||||
<div class="flow-v mbs-8 gap-12">
|
||||
<button class="dashed-btn" id="add-{col}-{$elId}">
|
||||
<span class="icon-plus" />
|
||||
<span class="icon-plus"></span>
|
||||
<span class="text">New Task</span>
|
||||
</button>
|
||||
{#each tasks as task (task.title)}
|
||||
@@ -112,7 +112,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{#if !isLast}
|
||||
<div class="vertical-sep" />
|
||||
<div class="vertical-sep"></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div data-theme-ignore class="inner-phone light">
|
||||
<div class="header">
|
||||
<p class="title">Your tasks</p>
|
||||
<span class="icon-menu" aria-label="menu" />
|
||||
<span class="icon-menu" aria-label="menu"></span>
|
||||
</div>
|
||||
|
||||
<div class="date">Today</div>
|
||||
@@ -27,9 +27,9 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button class="add-btn">
|
||||
<span class="web-icon-plus" />
|
||||
</button>
|
||||
<div class="add-btn">
|
||||
<span class="web-icon-plus"></span>
|
||||
</div>
|
||||
|
||||
<div class="overlay" id="overlay-{$elId}">
|
||||
<div class="drawer" id="drawer-{$elId}">
|
||||
@@ -50,7 +50,7 @@
|
||||
<div class="drop-zone">
|
||||
<span id="upload-text-{$elId}"> Drop media here </span>
|
||||
<div class="loading-overlay" id="upload-loading-{$elId}">
|
||||
<div class="loader" />
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<img id="upload-img-{$elId}" src="/images/animations/storage-2.png" alt="" />
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
style:--y={`${y}px`}
|
||||
style:--percentage={`${easedPercentage * 100}%`}
|
||||
>
|
||||
<div class="absolute -top-[8px] left-1/2" />
|
||||
<div class="absolute -top-[8px] left-1/2"></div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PUBLIC_APPWRITE_ENDPOINT, PUBLIC_APPWRITE_PROJECT_ID } from '$env/static/public';
|
||||
import { Client, Databases, Functions } from '@appwrite.io/console';
|
||||
import { Client, Databases, Functions, Storage } from '@appwrite.io/console';
|
||||
|
||||
export const client = new Client();
|
||||
|
||||
@@ -7,3 +7,4 @@ client.setEndpoint(PUBLIC_APPWRITE_ENDPOINT).setProject(PUBLIC_APPWRITE_PROJECT_
|
||||
|
||||
export const databases = new Databases(client);
|
||||
export const functions = new Functions(client);
|
||||
export const storage = new Storage(client);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
>
|
||||
<span class="text-primary text-sub-body font-medium">{title}</span>
|
||||
<div class="icon text-primary transition-transform group-[&[open]]:rotate-180">
|
||||
<span class="icon-cheveron-down" aria-hidden="true" />
|
||||
<span class="icon-cheveron-down" aria-hidden="true"></span>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="collapsible-content text-secondary text-sub-body flex flex-col">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
aria-label="close discord message"
|
||||
on:click={hideTopBanner}
|
||||
>
|
||||
<span class="web-icon-close" aria-hidden="true" />
|
||||
<span class="web-icon-close" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -19,15 +19,16 @@
|
||||
posthog: { name: 'intro-video-btn_hero_click' }
|
||||
});
|
||||
}}
|
||||
class="web-button is-secondary cursor-pointer"
|
||||
class="web-button cursor-pointer transition-opacity hover:opacity-90 active:scale-95"
|
||||
style:box-shadow="0 2px 40px rgba(0, 0, 0, 0.5)"
|
||||
>
|
||||
<span class="web-icon-play" style:color="unset" />
|
||||
<span class="web-icon-play"></span>
|
||||
<span>Appwrite in 100 seconds</span>
|
||||
</button>
|
||||
|
||||
{#if $open}
|
||||
<div use:melt={$portalled}>
|
||||
<div use:melt={$overlay} class="overlay" transition:fade={{ duration: 150 }} />
|
||||
<div use:melt={$overlay} class="overlay" transition:fade={{ duration: 150 }}></div>
|
||||
|
||||
<div
|
||||
class="web-media content"
|
||||
@@ -40,7 +41,7 @@
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
/>
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
|
||||
|
||||
export let heading: string = 'Start building with Appwrite today';
|
||||
export let url: string = 'https://cloud.appwrite.io';
|
||||
export let label: string = 'Get started';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="bg relative mt-12 !-mb-28 flex min-h-[12rem] items-center justify-center overflow-hidden border-t border-[hsl(var(--web-color-subtle))] py-12 lg:!-mb-[184px]"
|
||||
class="bg relative mt-12 -mb-16 flex min-h-[12rem] items-center justify-center overflow-hidden border-t border-[hsl(var(--web-color-subtle))] py-12"
|
||||
>
|
||||
<div class="flex max-w-3xs flex-col items-center justify-center gap-5 text-center">
|
||||
<h2 class="text-label">{heading}</h2>
|
||||
<a href={url} class="web-button">{label}</a>
|
||||
<h2 class="text-label text-primary font-aeonik-pro">{heading}</h2>
|
||||
<a href={getAppwriteDashboardUrl()} class="web-button">{label}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let carousel: HTMLElement;
|
||||
|
||||
export let size: 'default' | 'medium' | 'big' = 'default';
|
||||
export let gap = 32;
|
||||
interface Props {
|
||||
size?: 'default' | 'medium' | 'big';
|
||||
gap?: number;
|
||||
header?: Snippet;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
const { size = 'default', gap = 32, header, children }: Props = $props();
|
||||
let scroll = 0;
|
||||
|
||||
function calculateScrollAmount(prev = false) {
|
||||
@@ -32,8 +40,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
let isEnd = false;
|
||||
let isStart = true;
|
||||
let isEnd = $state(false);
|
||||
let isStart = $state(true);
|
||||
|
||||
function handleScroll() {
|
||||
isStart = carousel.scrollLeft <= 0;
|
||||
@@ -43,23 +51,25 @@
|
||||
|
||||
<div>
|
||||
<div class="mt-2 flex flex-wrap items-center">
|
||||
<slot name="header" />
|
||||
{#if header}
|
||||
{@render header()}
|
||||
{/if}
|
||||
<div class="nav ml-auto flex items-end gap-3">
|
||||
<button
|
||||
class="web-icon-button"
|
||||
class="web-icon-button cursor-pointer"
|
||||
aria-label="Move carousel backward"
|
||||
disabled={isStart}
|
||||
on:click={prev}
|
||||
onclick={prev}
|
||||
>
|
||||
<span class="web-icon-arrow-left" aria-hidden="true" />
|
||||
<span class="web-icon-arrow-left" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button
|
||||
class="web-icon-button"
|
||||
class="web-icon-button cursor-pointer"
|
||||
aria-label="Move carousel forward"
|
||||
disabled={isEnd}
|
||||
on:click={next}
|
||||
onclick={next}
|
||||
>
|
||||
<span class="web-icon-arrow-right" aria-hidden="true" />
|
||||
<span class="web-icon-arrow-right" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,9 +81,9 @@
|
||||
class:is-big={size === 'big'}
|
||||
style:gap="{gap}px"
|
||||
bind:this={carousel}
|
||||
on:scroll={handleScroll}
|
||||
onscroll={handleScroll}
|
||||
>
|
||||
<slot />
|
||||
{@render children()}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { loggedIn, user } from '$lib/utils/console';
|
||||
import { PUBLIC_GROWTH_ENDPOINT } from '$env/static/public';
|
||||
@@ -28,7 +28,7 @@
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
type: feedbackType,
|
||||
route: $page.route.id,
|
||||
route: page.route.id,
|
||||
comment,
|
||||
metaFields: {
|
||||
userId
|
||||
@@ -80,7 +80,7 @@
|
||||
feedbackType = 'positive';
|
||||
}}
|
||||
>
|
||||
<span class="icon-thumb-up" />
|
||||
<span class="icon-thumb-up"></span>
|
||||
</button>
|
||||
<button
|
||||
class="web-radio-button"
|
||||
@@ -91,7 +91,7 @@
|
||||
}}
|
||||
>
|
||||
<!-- TODO: fix the icon name on pink -->
|
||||
<span class="icon-thumb-dowm" />
|
||||
<span class="icon-thumb-dowm"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,12 +102,12 @@
|
||||
{/if}
|
||||
<li>
|
||||
<a
|
||||
href={`https://github.com/appwrite/website/tree/main/src/routes${$page.route.id}`}
|
||||
href={`https://github.com/appwrite/website/tree/main/src/routes${page.route.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="web-link flex items-baseline gap-1"
|
||||
>
|
||||
<span class="icon-pencil-alt contents" aria-hidden="true" />
|
||||
<span class="icon-pencil-alt contents" aria-hidden="true"></span>
|
||||
<span>Update on GitHub</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -133,7 +133,7 @@
|
||||
id="message"
|
||||
placeholder="Write your message"
|
||||
bind:value={comment}
|
||||
/>
|
||||
></textarea>
|
||||
<label for="message" class="mt-2">
|
||||
<span class="text-primary">Email</span>
|
||||
</label>
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
class="web-icon-chevron-down web-u-transition"
|
||||
class:web-u-rotate-180={$isSelected(title)}
|
||||
style:font-size="1rem"
|
||||
/>
|
||||
></span>
|
||||
</button>
|
||||
</h5>
|
||||
{#if $isSelected(title)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let mounted = false;
|
||||
@@ -28,7 +28,7 @@
|
||||
const randomDelay = () => Math.floor(Math.random() * 750);
|
||||
</script>
|
||||
|
||||
<div class="banner" class:hidden={$page.url.pathname.includes('init')}>
|
||||
<div class="banner" class:hidden={page.url.pathname.includes('init')}>
|
||||
<div class="content text-primary">
|
||||
<div class="headings">
|
||||
<span style:font-weight="500"
|
||||
@@ -39,16 +39,16 @@
|
||||
> has started
|
||||
</span>
|
||||
<span class="web-u-color-text-secondary">The start of something new</span>
|
||||
<div class="shadow" />
|
||||
<div class="shadow"></div>
|
||||
</div>
|
||||
<a href="/init" rel="noopener noreferrer" class="action">
|
||||
<span class="text-caption font-medium">Join now</span>
|
||||
<span class="web-icon-arrow-right" aria-hidden="true" />
|
||||
<div class="shadow" />
|
||||
<span class="web-icon-arrow-right" aria-hidden="true"></span>
|
||||
<div class="shadow"></div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="shine" />
|
||||
<div class="border" />
|
||||
<div class="shine"></div>
|
||||
<div class="border"></div>
|
||||
<div class="lines">
|
||||
{#if mounted}
|
||||
{#each Array.from({ length: groups.length }) as _, i}
|
||||
@@ -57,7 +57,7 @@
|
||||
<div
|
||||
class="line"
|
||||
style={`--width:${getRandomWidth(index)}px;--initial-delay:${randomDelay()}ms;left:${getRandomXValue()}px;`}
|
||||
/>
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -111,7 +111,8 @@
|
||||
backdrop-filter: blur(6px);
|
||||
background-color: hsl(var(--web-color-background) / 50%);
|
||||
mask-composite: intersect;
|
||||
mask-image: linear-gradient(
|
||||
mask-image:
|
||||
linear-gradient(
|
||||
to top,
|
||||
transparent,
|
||||
rgba(0, 0, 0, 1) 25%,
|
||||
@@ -148,7 +149,8 @@
|
||||
backdrop-filter: blur(6px);
|
||||
background-color: hsl(var(--web-color-background) / 50%);
|
||||
mask-composite: intersect;
|
||||
mask-image: linear-gradient(
|
||||
mask-image:
|
||||
linear-gradient(
|
||||
to top,
|
||||
transparent,
|
||||
rgba(0, 0, 0, 1) 25%,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { browser } from '$app/environment';
|
||||
import { page } from '$app/stores';
|
||||
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
|
||||
|
||||
export let classes = '';
|
||||
|
||||
@@ -16,7 +15,7 @@
|
||||
|
||||
<a
|
||||
class={classNames('web-button web-u-inline-width-100-percent-mobile', classes)}
|
||||
href={PUBLIC_APPWRITE_DASHBOARD}
|
||||
href={getAppwriteDashboardUrl()}
|
||||
on:click={() =>
|
||||
trackEvent({
|
||||
plausible: { name: `${getTrackingEventName()} in header` },
|
||||
|
||||
@@ -84,9 +84,7 @@
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<ul
|
||||
class="web-u-padding-block-start-80 grid grid-cols-3 text-center md:grid-cols-6 md:gap-10"
|
||||
>
|
||||
<ul class="grid grid-cols-3 gap-10 pt-20 text-center md:grid-cols-6">
|
||||
{#each logos as { src, alt, width, height }}
|
||||
<li class="grid place-content-center">
|
||||
<img {src} {alt} {width} {height} />
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span class={social.icon} aria-hidden="true" />
|
||||
<span class={social.icon} aria-hidden="true"></span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
@@ -36,7 +36,7 @@
|
||||
scrolling="no"
|
||||
style:color-scheme="none"
|
||||
style:margin-top="-4px"
|
||||
/>
|
||||
></iframe>
|
||||
|
||||
<ul class="flex gap-4">
|
||||
<li><a class="web-link" href="/terms">Terms</a></li>
|
||||
@@ -58,7 +58,7 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span class={social.icon} aria-hidden="true" />
|
||||
<span class={social.icon} aria-hidden="true"></span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts" context="module">
|
||||
import type { ComponentType } from 'svelte';
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
export type NavLink = {
|
||||
label: string;
|
||||
href?: string;
|
||||
showBadge?: boolean;
|
||||
submenu?: ComponentType;
|
||||
mobileSubmenu?: ComponentType;
|
||||
submenu?: Component<{ label: string }>;
|
||||
mobileSubmenu?: Component<{ label: string }>;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { IsLoggedIn } from '$lib/components';
|
||||
import { GITHUB_REPO_LINK, GITHUB_STARS } from '$lib/constants';
|
||||
import { SOCIAL_STATS } from '$lib/constants';
|
||||
import type { NavLink } from './MainNav.svelte';
|
||||
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
|
||||
|
||||
export let open = false;
|
||||
export let links: NavLink[];
|
||||
@@ -17,7 +18,7 @@
|
||||
<nav class="web-side-nav web-is-not-desktop" class:hidden={!open}>
|
||||
<div class="web-side-nav-wrapper ps-4 pe-4">
|
||||
<div class="flex items-center gap-2 px-4">
|
||||
<a href="https://cloud.appwrite.io/register" class="web-button is-secondary flex-1">
|
||||
<a href={getAppwriteDashboardUrl('/register')} class="web-button is-secondary flex-1">
|
||||
Sign up
|
||||
</a>
|
||||
<IsLoggedIn classes="flex-1" />
|
||||
@@ -41,14 +42,14 @@
|
||||
</div>
|
||||
<div class="web-side-nav-mobile-footer-buttons">
|
||||
<a
|
||||
href={GITHUB_REPO_LINK}
|
||||
href={SOCIAL_STATS.GITHUB.LINK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="web-button is-text web-u-inline-width-100-percent-mobile"
|
||||
>
|
||||
<span class="web-icon-star" aria-hidden="true" />
|
||||
<span class="web-icon-star" aria-hidden="true"></span>
|
||||
<span class="text">Star on GitHub</span>
|
||||
<span class="web-inline-tag text-sub-body">{GITHUB_STARS}</span>
|
||||
<span class="web-inline-tag text-sub-body">{SOCIAL_STATS.GITHUB.STAT}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<!-- @migration-task Error while migrating Svelte code: Cannot use `export let` in runes mode — use `$props()` instead
|
||||
https://svelte.dev/e/legacy_export_invalid -->
|
||||
<!-- @migration-task Error while migrating Svelte code: Cannot use `export let` in runes mode — use `$props()` instead
|
||||
https://svelte.dev/e/legacy_export_invalid -->
|
||||
<script lang="ts">
|
||||
import { platformMap } from '$lib/utils/references';
|
||||
import { writable } from 'svelte/store';
|
||||
import { Select, Tooltip } from '$lib/components';
|
||||
import { getCodeHtml, type Language } from '$lib/utils/code';
|
||||
import { copy } from '$lib/utils/copy';
|
||||
import { Select, Tooltip } from '$lib/components';
|
||||
import { platformMap } from '$lib/utils/references';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let selected: Language = 'js';
|
||||
export let data: { language: string; content: string; platform?: string }[] = [];
|
||||
@@ -22,11 +26,15 @@
|
||||
}
|
||||
});
|
||||
|
||||
enum CopyStatus {
|
||||
Copy = 'Copy',
|
||||
Copied = 'Copied!'
|
||||
}
|
||||
let copyText = CopyStatus.Copy;
|
||||
const CopyStatus = {
|
||||
Copy: 'Copy',
|
||||
Copied: 'Copied!'
|
||||
} as const;
|
||||
type CopyStatusType = keyof typeof CopyStatus;
|
||||
type CopyStatusValue = (typeof CopyStatus)[CopyStatusType];
|
||||
|
||||
let copyText: CopyStatusValue = CopyStatus.Copy;
|
||||
|
||||
async function handleCopy() {
|
||||
await copy(content);
|
||||
|
||||
@@ -75,11 +83,13 @@
|
||||
on:click={handleCopy}
|
||||
class="web-icon-button"
|
||||
aria-label="copy code from code-snippet"
|
||||
><span class="web-icon-copy" aria-hidden="true" /></button
|
||||
><span class="web-icon-copy" aria-hidden="true"></span></button
|
||||
>
|
||||
<svelte:fragment slot="tooltip">
|
||||
{#snippet tooltip()}
|
||||
<span>
|
||||
{copyText}
|
||||
</svelte:fragment>
|
||||
</span>
|
||||
{/snippet}
|
||||
</Tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
|
||||
</script>
|
||||
|
||||
<img
|
||||
@@ -17,7 +17,7 @@
|
||||
Start building with Appwrite today
|
||||
</h2>
|
||||
<a
|
||||
href={PUBLIC_APPWRITE_DASHBOARD}
|
||||
href={getAppwriteDashboardUrl()}
|
||||
class="web-button is-transparent web-self-center"
|
||||
on:click={() => trackEvent({ plausible: { name: 'Get started in pre footer' } })}
|
||||
>
|
||||
@@ -39,13 +39,13 @@
|
||||
<div class="web-strip-plans-plan">
|
||||
<h4 class="title text-description">Free</h4>
|
||||
<div class="text-title font-aeonik-pro text-primary">$0</div>
|
||||
<div class="info text-caption font-medium" />
|
||||
<div class="info text-caption font-medium"></div>
|
||||
</div>
|
||||
<p class="web-strip-plans-info text-caption font-medium">
|
||||
A great fit for passion projects and small applications.
|
||||
</p>
|
||||
<a
|
||||
href={`${PUBLIC_APPWRITE_DASHBOARD}/register`}
|
||||
href={getAppwriteDashboardUrl('/register')}
|
||||
class="web-button is-secondary is-full-width-mobile web-u-cross-child-end"
|
||||
on:click={() =>
|
||||
trackEvent({
|
||||
@@ -78,7 +78,7 @@
|
||||
to scale.
|
||||
</p>
|
||||
<a
|
||||
href={`${PUBLIC_APPWRITE_DASHBOARD}/console?type=create&plan=tier-1`}
|
||||
href={getAppwriteDashboardUrl('/console?type=create&plan=tier-1')}
|
||||
class="web-button is-full-width-mobile web-u-cross-child-end"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -111,7 +111,7 @@
|
||||
and support.
|
||||
</p>
|
||||
<a
|
||||
href={`${PUBLIC_APPWRITE_DASHBOARD}/console?type=create&plan=tier-2`}
|
||||
href={getAppwriteDashboardUrl('/console?type=create&plan=tier-2')}
|
||||
class="web-button is-secondary is-full-width-mobile web-u-cross-child-end"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { melt, createCollapsible } from '@melt-ui/svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { products, sublinks } from './ProductsSubmenu.svelte';
|
||||
import { dev } from '$app/environment';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
export let label: string;
|
||||
@@ -24,18 +23,18 @@
|
||||
class={classNames('web-icon-chevron-down transition-transform', {
|
||||
'rotate-180': $open
|
||||
})}
|
||||
/></button
|
||||
></span></button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if $open}
|
||||
<div use:melt={$content} transition:slide class="py-3 px-4">
|
||||
<div use:melt={$content} transition:slide class="px-2 py-3">
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each products as product}
|
||||
<a
|
||||
href={product.href}
|
||||
class="group flex gap-3 rounded-xl p-2 text-white outline-none transition-colors focus:bg-white/8"
|
||||
class="group flex gap-3 rounded-xl p-2 text-white transition-colors outline-none focus:bg-white/8"
|
||||
on:click={() =>
|
||||
trackEvent({
|
||||
plausible: {
|
||||
@@ -58,7 +57,7 @@
|
||||
|
||||
{#if product.beta}
|
||||
<span
|
||||
class="text-caption bg-accent/24 ml-1 rounded py-1 px-2 font-medium text-white"
|
||||
class="text-caption bg-accent/24 ml-1 rounded px-2 py-1 font-medium text-white"
|
||||
>Coming soon</span
|
||||
>
|
||||
{/if}
|
||||
@@ -70,11 +69,11 @@
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{#if dev}
|
||||
|
||||
<div class="mt-8">
|
||||
<span
|
||||
class="font-aeonik-fono tracking-loose text-secondary block text-xs uppercase"
|
||||
>This is a title<span class="text-accent">_</span></span
|
||||
>Compare Appwrite<span class="text-accent">_</span></span
|
||||
>
|
||||
<div class="mt-3 space-y-3">
|
||||
{#each sublinks as sublink}
|
||||
@@ -82,12 +81,11 @@
|
||||
href={sublink.href}
|
||||
class="text-caption text-primary flex items-center gap-2"
|
||||
>
|
||||
{sublink.label} <span class="web-icon-chevron-right" />
|
||||
{sublink.label} <span class="web-icon-chevron-right"></span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -53,25 +53,21 @@
|
||||
|
||||
export const sublinks: Array<SubLink> = [
|
||||
{
|
||||
label: 'See more',
|
||||
href: '/'
|
||||
label: 'Appwrite vs. Supabase',
|
||||
href: '/blog/post/appwrite-compared-to-supabase'
|
||||
},
|
||||
{
|
||||
label: 'See more',
|
||||
href: '/'
|
||||
},
|
||||
{
|
||||
label: 'See more',
|
||||
href: '/'
|
||||
label: 'Appwrite vs. Firebase',
|
||||
href: '/blog/post/open-source-firebase-alternative'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { dev } from '$app/environment';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import { createDropdownMenu, melt } from '@melt-ui/svelte';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
const {
|
||||
elements: { trigger, menu, item, overlay },
|
||||
@@ -98,13 +94,13 @@
|
||||
class={classNames('web-icon-chevron-down block transition-transform', {
|
||||
'rotate-180': $open
|
||||
})}
|
||||
/>
|
||||
></span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
use:melt={$menu}
|
||||
class={classNames(
|
||||
'data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in relative !left-1/2 z-10 mt-6 mx-auto hidden w-full -translate-x-1/2 flex-col items-center p-0 outline-none [max-inline-size:86.875rem] md:flex'
|
||||
'data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in relative !left-1/2 z-10 mx-auto mt-6 hidden w-full -translate-x-1/2 flex-col items-center p-0 outline-none [max-inline-size:86.875rem] md:flex'
|
||||
)}
|
||||
>
|
||||
<div class="is-special-padding w-full rounded-2xl border border-white/8 bg-[#232325] p-6">
|
||||
@@ -127,7 +123,7 @@
|
||||
name: `${product.name} in products submenu`
|
||||
}
|
||||
})}
|
||||
class="group flex gap-3 rounded-xl p-1 text-white outline-none transition-colors focus:bg-white/8"
|
||||
class="group flex gap-3 rounded-xl p-1 text-white transition-colors outline-none focus:bg-white/8"
|
||||
>
|
||||
<div
|
||||
class="flex size-12 shrink-0 items-center justify-center rounded-lg border border-white/12 bg-white/6"
|
||||
@@ -144,7 +140,7 @@
|
||||
|
||||
{#if product.beta}
|
||||
<span
|
||||
class="text-caption bg-accent/24 ml-1 rounded py-1 px-2 font-medium text-white"
|
||||
class="text-caption bg-accent/24 ml-1 rounded px-2 py-1 font-medium text-white"
|
||||
>Coming soon</span
|
||||
>
|
||||
{/if}
|
||||
@@ -158,57 +154,64 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-4 -ml-12 border-l border-white/6 pl-12">
|
||||
<a
|
||||
href="/blog/post/case-study-undo"
|
||||
<div
|
||||
use:melt={$item}
|
||||
class="block rounded-2xl border border-white/12 bg-white/6 p-4 outline-none focus-within:bg-white/12"
|
||||
class="group block rounded-2xl border border-white/12 bg-white/6 p-4 outline-none focus-within:bg-white/12"
|
||||
>
|
||||
<header class="flex items-center justify-between">
|
||||
<span
|
||||
class="font-aeonik-fono tracking-loose text-secondary block text-xs uppercase"
|
||||
>Customer stories<span class="text-accent">_</span></span
|
||||
>Customer Stories<span class="text-accent">_</span></span
|
||||
>
|
||||
<a
|
||||
href="/blog/category/case-studies"
|
||||
href="/blog/category/customer-stories"
|
||||
class="text-primary text-caption flex items-center gap-2"
|
||||
>See more <span class="web-icon-chevron-right" /></a
|
||||
>See more <span
|
||||
class="web-icon-chevron-right transition-transform group-hover:translate-x-0.5"
|
||||
></span></a
|
||||
>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 outline-none">
|
||||
<a
|
||||
href="/blog/post/customer-story-storealert"
|
||||
class="my-4 flex flex-1 gap-3 outline-none"
|
||||
>
|
||||
<img
|
||||
src="/images/blog/case-study-undo/cover.png"
|
||||
src="/images/blog/customer-story-storealert/cover.png"
|
||||
alt="Case study cover"
|
||||
class="my-6 aspect-[3/1] rounded-xl object-cover"
|
||||
class="aspect-[3/1] max-w-[7.5rem] shrink-0 rounded-xl object-cover"
|
||||
/>
|
||||
<p>
|
||||
Pioneering asset management solutions for the circular economy with UNDŌ
|
||||
<p class="text-pretty">
|
||||
Empowering Shopify merchants with real-time store monitoring using
|
||||
StoreAlert
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{#if dev}
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<span
|
||||
class="font-aeonik-fono tracking-loose text-secondary block text-xs uppercase"
|
||||
>This is a title<span class="text-accent">_</span></span
|
||||
>Compare Appwrite<span class="text-accent">_</span></span
|
||||
>
|
||||
<div class="mt-3 space-y-3">
|
||||
{#each sublinks as sublink}
|
||||
<a
|
||||
href={sublink.href}
|
||||
class="text-caption text-primary flex items-center gap-2"
|
||||
class="text-caption text-primary group flex items-center gap-1"
|
||||
>
|
||||
{sublink.label} <span class="web-icon-chevron-right" />
|
||||
{sublink.label}
|
||||
<span
|
||||
class="web-icon-chevron-right transition-transform group-hover:translate-x-0.5"
|
||||
></span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
use:melt={$overlay}
|
||||
class="data-[state=closed]:animate-fade-out fixed inset-0 bg-black/60"
|
||||
/>
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@@ -160,11 +160,12 @@
|
||||
on:click={handleExit}
|
||||
>
|
||||
<div class="web-input-text-search-wrapper web-u-margin-inline-20 w-full max-w-[680px]">
|
||||
<span class="web-icon-search z-[5]" aria-hidden="true" style="inset-block-start:0.9rem" />
|
||||
<div id="searchbox" />
|
||||
<span class="web-icon-search z-[5]" aria-hidden="true" style="inset-block-start:0.9rem"
|
||||
></span>
|
||||
<div id="searchbox"></div>
|
||||
|
||||
<input
|
||||
class="web-input-button relative z-1 !rounded-b-none !pl-10"
|
||||
class="web-input-button bg-greyscale-800/75! relative z-1 !rounded-b-none !pl-10"
|
||||
type="text"
|
||||
id="search"
|
||||
bind:value
|
||||
@@ -222,7 +223,7 @@
|
||||
</div>
|
||||
{#if hit.p}
|
||||
<div
|
||||
class="web-u-color-text-secondary w-full overflow-hidden text-ellipsis whitespace-nowrap text-left"
|
||||
class="web-u-color-text-secondary w-full overflow-hidden text-left text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{hit.p}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import { createSelect, melt, type CreateSelectProps } from '@melt-ui/svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { fly, type FlyParams } from 'svelte/transition';
|
||||
@@ -21,6 +22,9 @@
|
||||
export let id: string | undefined = undefined;
|
||||
export let preventScroll = false;
|
||||
export let placement: NonNullable<CreateSelectProps['positioning']>['placement'] = 'bottom';
|
||||
let className: string = '';
|
||||
|
||||
export { className as class };
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
change: unknown;
|
||||
@@ -85,7 +89,7 @@
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="web-select is-colored"
|
||||
class={classNames('web-select is-colored', className)}
|
||||
{id}
|
||||
class:web-is-not-mobile={nativeMobile}
|
||||
use:melt={$trigger}
|
||||
@@ -93,11 +97,11 @@
|
||||
>
|
||||
<div class="physical-select">
|
||||
{#if selectedOption?.icon}
|
||||
<span class={selectedOption.icon} aria-hidden="true" />
|
||||
<span class={selectedOption.icon} aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{$selectedLabel || initialLabel}</span>
|
||||
</div>
|
||||
<span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true" />
|
||||
<span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
{#if $open}
|
||||
@@ -115,7 +119,7 @@
|
||||
{#each group.options as option}
|
||||
<button class="web-select-option" use:melt={$optionEl(option)}>
|
||||
{#if option.icon}
|
||||
<span class={option.icon} aria-hidden="true" />
|
||||
<span class={option.icon} aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{option.label}</span>
|
||||
</button>
|
||||
@@ -130,7 +134,7 @@
|
||||
{#each group.options as option}
|
||||
<button class="web-select-option" use:melt={$optionEl(option)}>
|
||||
{#if option.icon}
|
||||
<span class={option.icon} aria-hidden="true" />
|
||||
<span class={option.icon} aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span style:text-transform="capitalize">{option.label}</span>
|
||||
</button>
|
||||
@@ -146,7 +150,7 @@
|
||||
style:display={nativeMobile ? undefined : 'none'}
|
||||
>
|
||||
{#if selectedOption?.icon}
|
||||
<span class={selectedOption.icon} aria-hidden="true" />
|
||||
<span class={selectedOption.icon} aria-hidden="true"></span>
|
||||
{/if}
|
||||
<select {id} bind:value>
|
||||
{#each groups as group}
|
||||
@@ -168,7 +172,7 @@
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
<span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true" />
|
||||
<span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true"></span>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<div class="melt-switch">
|
||||
<button use:melt={$root}>
|
||||
<span class="thumb" />
|
||||
<span class="thumb"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -85,7 +85,9 @@
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<svelte:fragment slot="tooltip">{platform.name}</svelte:fragment>
|
||||
{#snippet tooltip()}
|
||||
{platform.name}
|
||||
{/snippet}
|
||||
</Tooltip>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { createTooltip, melt } from '@melt-ui/svelte';
|
||||
import type { FloatingConfig } from '@melt-ui/svelte/internal/actions';
|
||||
import { type Snippet } from 'svelte';
|
||||
import { fly, type FlyParams } from 'svelte/transition';
|
||||
|
||||
export let placement: NonNullable<FloatingConfig>['placement'] = 'top';
|
||||
export let disabled = false;
|
||||
export let closeOnPointerDown = false;
|
||||
export let disableHoverableContent = false;
|
||||
interface Props {
|
||||
placement?: NonNullable<FloatingConfig>['placement'];
|
||||
disabled?: boolean;
|
||||
closeOnPointerDown?: boolean;
|
||||
disableHoverableContent?: boolean;
|
||||
asChild?: Snippet<[any]>;
|
||||
children?: Snippet;
|
||||
tooltip: Snippet;
|
||||
}
|
||||
|
||||
const {
|
||||
placement = 'top',
|
||||
disabled = false,
|
||||
closeOnPointerDown = false,
|
||||
disableHoverableContent = false,
|
||||
asChild,
|
||||
children,
|
||||
tooltip
|
||||
}: Props = $props();
|
||||
|
||||
const {
|
||||
elements: { trigger, content, arrow },
|
||||
@@ -21,7 +37,8 @@
|
||||
disableHoverableContent
|
||||
});
|
||||
|
||||
$: flyParams = (function getFlyParams() {
|
||||
let flyParams = $derived(
|
||||
(function getFlyParams() {
|
||||
const params: FlyParams = {
|
||||
duration: 150
|
||||
};
|
||||
@@ -51,20 +68,21 @@
|
||||
}
|
||||
|
||||
return params;
|
||||
})();
|
||||
})()
|
||||
);
|
||||
</script>
|
||||
|
||||
<slot name="asChild" trigger={$trigger} />
|
||||
|
||||
{#if !$$slots.asChild}
|
||||
{#if asChild}
|
||||
{@render asChild({ trigger: $trigger })}
|
||||
{:else if children}
|
||||
<span use:melt={$trigger}>
|
||||
<slot />
|
||||
{@render children()}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if $open && !disabled}
|
||||
{#if tooltip && $open && !disabled}
|
||||
<div use:melt={$content} class="web-tooltip text-sub-body" transition:fly={flyParams}>
|
||||
<div use:melt={$arrow} />
|
||||
<slot name="tooltip" />
|
||||
<div use:melt={$arrow}></div>
|
||||
{@render tooltip()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
47
src/lib/components/blog/article.svelte
Normal file
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import Media from '$lib/UI/Media.svelte';
|
||||
import { formatDate } from '$lib/utils/date';
|
||||
|
||||
export let title: string;
|
||||
export let cover: string;
|
||||
export let href: string;
|
||||
export let date: Date;
|
||||
export let timeToRead: number;
|
||||
export let author: string;
|
||||
export let avatar: string;
|
||||
</script>
|
||||
|
||||
<a class="group flex w-full flex-col gap-8 bg-transparent pb-3 transition" {href}>
|
||||
<div class="overflow-hidden rounded-lg">
|
||||
<Media
|
||||
src={cover}
|
||||
class="aspect-video transition duration-250 ease-in-out group-hover:scale-105"
|
||||
alt={title}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-5">
|
||||
<h4 class="text-label text-primary line-clamp-2">
|
||||
{title}
|
||||
</h4>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<img
|
||||
class="size-6 rounded-full"
|
||||
loading="lazy"
|
||||
src={avatar}
|
||||
width="24"
|
||||
height="24"
|
||||
alt={author}
|
||||
/>
|
||||
<div class="flex items-baseline gap-3">
|
||||
<h4 class="text-sub-body text-primary">{author}</h4>
|
||||
<ul class="text-caption flex items-center gap-2">
|
||||
<li>
|
||||
{formatDate(date)}
|
||||
</li>
|
||||
<li>{timeToRead} min</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
9
src/lib/components/blog/breadcrumbs.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
export let title: string;
|
||||
</script>
|
||||
|
||||
<div class="text-caption text-secondary flex gap-3 pt-8 pb-16">
|
||||
<a href="/blog">Blog</a>
|
||||
<span>/</span>
|
||||
<span class="text-primary line-clamp-1">{title}</span>
|
||||
</div>
|
||||
302
src/lib/components/blog/newsletter.svelte
Normal file
@@ -0,0 +1,302 @@
|
||||
<script context="module" lang="ts">
|
||||
import { PUBLIC_GROWTH_ENDPOINT } from '$env/static/public';
|
||||
|
||||
export const subscribeToNewsletter = async (email: string) => {
|
||||
const response = await fetch(`${PUBLIC_GROWTH_ENDPOINT}/newsletter/subscribe`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email
|
||||
})
|
||||
});
|
||||
return response;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
let email = '';
|
||||
let submitted = false;
|
||||
let error: string | undefined;
|
||||
let submitting = false;
|
||||
|
||||
const handleSubmit = async () => {
|
||||
submitting = true;
|
||||
error = undefined;
|
||||
const response = await subscribeToNewsletter(email);
|
||||
submitting = false;
|
||||
if (response.status >= 400) {
|
||||
error = response.status >= 500 ? 'Server Error.' : 'Error submitting form.';
|
||||
return;
|
||||
}
|
||||
submitted = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="relative max-h-[35vh]">
|
||||
<div class="relative z-10 py-10">
|
||||
<div class="mx-auto flex w-full max-w-4xl flex-col items-center gap-8 text-center">
|
||||
<section class="flex flex-col gap-5">
|
||||
<h3 class="text-display font-aeonik-pro text-primary tracking-compressed">
|
||||
Subscribe to our newsletter
|
||||
</h3>
|
||||
<p class="text-description text-primary">
|
||||
Sign up to our company blog and get the latest insights from Appwrite. Learn
|
||||
more about engineering, product design, building community, and tips & tricks
|
||||
for using Appwrite.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{#if submitted}
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="9"
|
||||
cy="9"
|
||||
r="8"
|
||||
fill="#FD366E"
|
||||
fill-opacity="0.08"
|
||||
stroke="#FD366E"
|
||||
stroke-opacity="0.32"
|
||||
stroke-width="1.2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M5.25 10.5L7.75 12.5L12.75 6"
|
||||
stroke="#E4E4E7"
|
||||
stroke-width="1.2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span class="text">
|
||||
Thank you for subscribing! An email has been sent to your inbox.
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<form method="post" class="contents" on:submit|preventDefault={handleSubmit}>
|
||||
<div
|
||||
class="border-primary/12 focus-within:border-primary/48 flex w-full max-w-sm justify-between gap-1 rounded-xl border bg-white/4 py-1 pr-1 pl-4 backdrop-blur-2xl transition-colors"
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
class="text-primary flex-1 appearance-none border-none outline-none"
|
||||
required
|
||||
id="email"
|
||||
name="email"
|
||||
bind:value={email}
|
||||
/>
|
||||
<button type="submit" class="web-button" disabled={submitting}
|
||||
>Sign up</button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<span class="text"> Something went wrong. Please try again later. </span>
|
||||
{/if}
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gradient-container relative z-0">
|
||||
<div class="gradient gradient-1"></div>
|
||||
<div class="gradient gradient-2"></div>
|
||||
<div class="gradient gradient-3"></div>
|
||||
<div class="gradient gradient-4"></div>
|
||||
<div class="gradient gradient-5"></div>
|
||||
<div class="gradient gradient-6"></div>
|
||||
<div class="gradient gradient-7"></div>
|
||||
<div class="gradient gradient-8"></div>
|
||||
<div class="gradient gradient-9"></div>
|
||||
<div class="gradient gradient-10"></div>
|
||||
<div class="gradient gradient-11"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.gradient-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.gradient {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
transform-origin: center;
|
||||
filter: blur(100px);
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
.gradient-1 {
|
||||
width: 120%;
|
||||
height: 60%;
|
||||
top: 70%;
|
||||
left: -30%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(253, 54, 110, 0.55) 0%,
|
||||
rgba(253, 54, 110, 0) 70%
|
||||
);
|
||||
filter: blur(180px);
|
||||
transform: rotate(-15deg);
|
||||
}
|
||||
|
||||
.gradient-2 {
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
top: 40%;
|
||||
left: 20%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(254, 149, 103, 0.55) 0%,
|
||||
rgba(254, 149, 103, 0) 70%
|
||||
);
|
||||
filter: blur(130px);
|
||||
transform: rotate(25deg);
|
||||
}
|
||||
|
||||
.gradient-3 {
|
||||
width: 120%;
|
||||
height: 30%;
|
||||
top: 30%;
|
||||
left: -30%;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(254, 149, 103, 0.55) 0%,
|
||||
rgba(240, 46, 101, 0.55) 100%
|
||||
);
|
||||
filter: blur(130px);
|
||||
transform: rotate(-10deg);
|
||||
}
|
||||
|
||||
.gradient-4 {
|
||||
width: 80%;
|
||||
height: 40%;
|
||||
top: 15%;
|
||||
left: 30%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(255, 255, 255, 0.4) 0%,
|
||||
rgba(255, 255, 255, 0) 70%
|
||||
);
|
||||
filter: blur(108px);
|
||||
}
|
||||
|
||||
.gradient-5 {
|
||||
width: 140%;
|
||||
height: 80%;
|
||||
top: 20%;
|
||||
left: -5%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(253, 54, 110, 0.6) 0%,
|
||||
rgba(253, 54, 110, 0) 70%
|
||||
);
|
||||
filter: blur(250px);
|
||||
transform: rotate(15deg);
|
||||
}
|
||||
|
||||
.gradient-6 {
|
||||
width: 150%;
|
||||
height: 100%;
|
||||
top: -40%;
|
||||
left: 20%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(253, 54, 110, 0.6) 0%,
|
||||
rgba(253, 54, 110, 0) 70%
|
||||
);
|
||||
filter: blur(250px);
|
||||
transform: rotate(-25deg);
|
||||
}
|
||||
|
||||
.gradient-7 {
|
||||
width: 70%;
|
||||
height: 10%;
|
||||
top: 10%;
|
||||
left: 15%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(237, 237, 240, 0.45) 0%,
|
||||
rgba(237, 237, 240, 0) 70%
|
||||
);
|
||||
filter: blur(63px);
|
||||
mix-blend-mode: soft-light;
|
||||
transform: rotate(-15deg);
|
||||
}
|
||||
|
||||
.gradient-8 {
|
||||
width: 60%;
|
||||
height: 10%;
|
||||
top: 20%;
|
||||
left: 0%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(237, 237, 240, 0.35) 0%,
|
||||
rgba(237, 237, 240, 0) 70%
|
||||
);
|
||||
filter: blur(50px);
|
||||
mix-blend-mode: overlay;
|
||||
transform: rotate(-15deg);
|
||||
}
|
||||
|
||||
.gradient-9 {
|
||||
width: 60%;
|
||||
height: 8%;
|
||||
top: 20%;
|
||||
left: 30%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(237, 237, 240, 0.45) 0%,
|
||||
rgba(237, 237, 240, 0) 70%
|
||||
);
|
||||
filter: blur(63px);
|
||||
mix-blend-mode: soft-light;
|
||||
transform: rotate(-15deg);
|
||||
}
|
||||
|
||||
.gradient-10 {
|
||||
width: 55%;
|
||||
height: 8%;
|
||||
top: 35%;
|
||||
left: 25%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(237, 237, 240, 0.45) 0%,
|
||||
rgba(237, 237, 240, 0) 70%
|
||||
);
|
||||
filter: blur(63px);
|
||||
mix-blend-mode: soft-light;
|
||||
transform: rotate(-15deg);
|
||||
}
|
||||
|
||||
.gradient-11 {
|
||||
width: 120%;
|
||||
height: 40%;
|
||||
top: 0%;
|
||||
left: -10%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(254, 149, 103, 0.5) 0%,
|
||||
rgba(254, 149, 103, 0) 70%
|
||||
);
|
||||
filter: blur(108px);
|
||||
transform: rotate(-5deg);
|
||||
opacity: 0.65;
|
||||
}
|
||||
</style>
|
||||
91
src/lib/components/blog/post-meta.svelte
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import { socialSharingOptions, type SocialShareOption } from '$lib/constants';
|
||||
import { handleCopy } from '$lib/utils/copy';
|
||||
import { formatDate } from '$lib/utils/date';
|
||||
import type { AuthorData } from '$routes/blog/content';
|
||||
|
||||
export let date: string = new Date().toISOString();
|
||||
export let timeToRead: string = '0';
|
||||
export let title: string = '';
|
||||
export let description: string = '';
|
||||
export let authorData: Partial<AuthorData> = {};
|
||||
export let currentURL: string = '';
|
||||
|
||||
const getShareLink = (shareOption: SocialShareOption) => {
|
||||
const blogPostUrl = encodeURI(currentURL);
|
||||
return shareOption.link.replace('{TITLE}', title + '.').replace('{URL}', blogPostUrl);
|
||||
};
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-caption flex gap-2">
|
||||
<time datetime={date}>{formatDate(date)}</time>
|
||||
<span>•</span>
|
||||
{#if timeToRead}
|
||||
<span>{timeToRead} min</span>
|
||||
{/if}
|
||||
</div>
|
||||
<h1 class="text-title font-aeonik-pro text-primary">
|
||||
{title}
|
||||
</h1>
|
||||
{#if description}
|
||||
<p class="text-description text-secondary mt-2">
|
||||
{description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="border-smooth mb-8 flex flex-col justify-between border-b py-8 md:flex-row">
|
||||
{#if authorData}
|
||||
<a href={authorData.href} class="flex items-center gap-2">
|
||||
{#if authorData.avatar}
|
||||
<img
|
||||
class="size-11 rounded-full"
|
||||
src={authorData.avatar}
|
||||
alt={authorData.name}
|
||||
loading="lazy"
|
||||
width="44"
|
||||
height="44"
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex flex-col">
|
||||
<h4 class="text-sub-body text-primary">
|
||||
{authorData.name}
|
||||
</h4>
|
||||
<p class="text-caption">{authorData.role}</p>
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<div class="mt-4 flex items-center gap-4">
|
||||
<span class="text-micro text-secondary uppercase">SHARE</span>
|
||||
|
||||
<ul class="flex gap-2">
|
||||
{#each socialSharingOptions as sharingOption}
|
||||
<li
|
||||
class="bg-smooth flex size-7 items-center justify-center rounded-lg text-white"
|
||||
>
|
||||
{#if sharingOption.type === 'link'}
|
||||
<a
|
||||
aria-label={sharingOption.label}
|
||||
href={getShareLink(sharingOption)}
|
||||
target="_blank"
|
||||
rel="noopener, noreferrer"
|
||||
>
|
||||
<span class={sharingOption.icon} aria-hidden="true"></span>
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
aria-label={sharingOption.label}
|
||||
on:click={() => handleCopy(currentURL)}
|
||||
>
|
||||
<span class={sharingOption.icon} aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
99
src/lib/components/blog/table-of-contents.svelte
Normal file
@@ -0,0 +1,99 @@
|
||||
<script lang="ts" context="module">
|
||||
export type TocItem = {
|
||||
title: string;
|
||||
href: string;
|
||||
step?: number;
|
||||
selected?: boolean;
|
||||
level?: number;
|
||||
children?: Array<{
|
||||
title: string;
|
||||
href: string;
|
||||
selected: boolean;
|
||||
level?: number;
|
||||
}>;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const backToTop = () => {
|
||||
window.scrollTo({ top: 0 });
|
||||
};
|
||||
|
||||
let isScrolled: boolean = false;
|
||||
|
||||
const handleIsScrolled = () => {
|
||||
isScrolled = !!window.scrollY;
|
||||
};
|
||||
|
||||
export let toc: Array<TocItem> = [];
|
||||
export let heading: string = 'Table of Contents';
|
||||
</script>
|
||||
|
||||
<svelte:window on:scroll={handleIsScrolled} />
|
||||
|
||||
<nav class="sticky top-32 col-span-3 mt-2 -ml-4 hidden h-[800px] flex-col gap-6 lg:flex">
|
||||
<span class="text-micro tracking-loose text-primary font-aeonik-fono ps-6 uppercase"
|
||||
>{heading}</span
|
||||
>
|
||||
<div class="relative">
|
||||
<ul
|
||||
class="mask text-caption flex max-h-[600px] flex-col gap-4 overflow-scroll pb-11 [scrollbar-width:none]"
|
||||
>
|
||||
{#each toc as parent (parent.href)}
|
||||
<li
|
||||
class={classNames(
|
||||
parent.selected ? 'text-primary' : 'text-secondary',
|
||||
'relative ps-6 transition-colors',
|
||||
'before:bg-greyscale-300 before:absolute before:top-0 before:left-0 before:h-6 before:w-px before:rounded-full before:opacity-0 before:transition-opacity',
|
||||
{
|
||||
'font-medium': parent.level && parent.level === 1,
|
||||
'pl-12': parent.level && parent.level === 2,
|
||||
'ps-16': parent.level && parent.level >= 3,
|
||||
'before:opacity-100': parent.selected
|
||||
}
|
||||
)}
|
||||
>
|
||||
<a href={parent.href} class="line-clamp-1">{parent.title}</a>
|
||||
|
||||
{#if parent.children}
|
||||
<ul
|
||||
class="border-smooth text-caption mt-11 ml-9 flex flex-col gap-7 border-b pb-10"
|
||||
>
|
||||
{#each parent.children as child}
|
||||
<li
|
||||
class={classNames(
|
||||
parent.selected ? 'text-primary' : 'text-secondary',
|
||||
'relative transition-colors',
|
||||
'before:bg-primary before:absolute before:top-0 before:left-0 before:h-6 before:w-px before:rounded-full before:opacity-0 before:transition-opacity',
|
||||
{
|
||||
'before:opacity-100': parent.selected
|
||||
}
|
||||
)}
|
||||
>
|
||||
<a href={child.href} class="line-clamp-1">
|
||||
{child.title}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{#if isScrolled}
|
||||
<button
|
||||
class="text-primary group border-smooth text-caption ms-6 -mt-4 flex cursor-pointer items-center gap-2 border-t pt-10 font-medium transition-all"
|
||||
on:click={backToTop}
|
||||
out:fade
|
||||
in:fade
|
||||
>
|
||||
<span class="web-icon-arrow-up transition group-hover:-translate-y-0.5"></span>
|
||||
Back to Top
|
||||
</button>
|
||||
{/if}
|
||||
</nav>
|
||||
@@ -131,12 +131,20 @@
|
||||
<div class="embla web-carousel relative overflow-hidden">
|
||||
{#if showArrows}
|
||||
{#if hasPrev}
|
||||
<button class="web-carousel-button web-carousel-button-start" on:click={onPrev}>
|
||||
<button
|
||||
class="web-carousel-button web-carousel-button-start"
|
||||
on:click={onPrev}
|
||||
aria-label="Previous slide"
|
||||
>
|
||||
<span class="web-icon-arrow-left" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if hasNext}
|
||||
<button class="web-carousel-button web-carousel-button-end" on:click={onNext}>
|
||||
<button
|
||||
class="web-carousel-button web-carousel-button-end"
|
||||
on:click={onNext}
|
||||
aria-label="Next slide"
|
||||
>
|
||||
<span class="web-icon-arrow-right" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -147,7 +155,7 @@
|
||||
<slot />
|
||||
</ul>
|
||||
</div>
|
||||
<div class="shadow" />
|
||||
<div class="shadow"></div>
|
||||
</div>
|
||||
|
||||
{#if showBullets}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="border-smooth box-content flex items-center border-b bg-[url(/images/bgs/mobile-hero.png)] bg-cover bg-bottom py-12 px-5 md:bg-[url(/images/bgs/hero.png)] md:bg-center md:pt-32 md:pb-40 lg:px-8 xl:px-16"
|
||||
class="border-smooth box-content flex items-center border-b bg-[url(/images/bgs/mobile-hero.png)] bg-cover bg-bottom px-5 py-12 md:bg-[url(/images/bgs/hero.png)] md:bg-center md:pt-32 md:pb-40 lg:px-8 xl:px-16"
|
||||
>
|
||||
<div class="mx-auto grid max-w-[75rem] items-center gap-16 md:grid-cols-2">
|
||||
<div class="space-y-8">
|
||||
@@ -51,7 +51,7 @@
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
<p class="text-description text-secondary text-pretty font-medium">
|
||||
<p class="text-description text-secondary font-medium text-pretty">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<img src={product.icon} alt="auth" width="32" height="32" />
|
||||
<h4 class="text-body text-primary">{product.title}</h4>
|
||||
<span class="web-icon-arrow-right ml-auto" aria-hidden="true" />
|
||||
<span class="web-icon-arrow-right ml-auto" aria-hidden="true"></span>
|
||||
</div>
|
||||
<p class="text-sub-body">
|
||||
{product.description}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import type { HTMLButtonAttributes, HTMLAnchorAttributes } from 'svelte/elements';
|
||||
import { cva, type VariantProps } from 'cva';
|
||||
import InlineTag from './InlineTag.svelte';
|
||||
|
||||
const button = cva(
|
||||
[
|
||||
'flex w-fit justify-center px-[0.875rem] h-10 transition-all text-center no-underline select-none min-w-10 bg-origin-border text-white font-medium items-center gap-2 rounded-lg border border-transparent button duration-200'
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: [
|
||||
'bg-[linear-gradient(135deg,_var(--color-accent)_0%,_var(--color-accent)_61%,_var(--color-secondary-100)_100%)]',
|
||||
'hover:shadow-[0_0_2rem_var(--color-accent-200)] active:not:disabled:shadow-[0_0_2rem_var(--color-accent-200)]'
|
||||
],
|
||||
secondary: [
|
||||
'bg-[#fd366e0a] relative',
|
||||
'hover:shadow-[0_-6px_10px_0px_rgba(253,54,110,0.08)_inset]'
|
||||
],
|
||||
text: [
|
||||
'bg-transparent border-transparent text-white',
|
||||
'hover:backdrop-blur-md hover:bg-[linear-gradient(135deg,_rgba(255,_255,_255,_0.06)_0%,_rgba(255,_255,_255,_0.10)_54.74%,_rgba(255,_255,_255,_0.06)_100%)]'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
type ButtonProps =
|
||||
| (HTMLButtonAttributes & { href?: undefined })
|
||||
| (HTMLAnchorAttributes & { href: string });
|
||||
|
||||
type $$Props = ButtonProps & VariantProps<typeof button>;
|
||||
|
||||
export let href: $$Props['href'] = undefined;
|
||||
export let variant: $$Props['variant'] = 'primary';
|
||||
const { class: classes, ...props } = $$restProps;
|
||||
|
||||
const buttonClasses = classNames(button({ variant }), classes, {
|
||||
secondary: variant === 'secondary',
|
||||
'leading-tight': $$slots.icon
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a {...props} {href} class={buttonClasses}>
|
||||
{#if $$slots.icon}
|
||||
<slot name="icon" />
|
||||
{/if}
|
||||
<slot />
|
||||
{#if $$slots.tag}
|
||||
<InlineTag>
|
||||
<slot name="tag" />
|
||||
</InlineTag>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button {...props} class={buttonClasses}>
|
||||
{#if $$slots.icon}
|
||||
<slot name="icon" />
|
||||
{/if}
|
||||
<slot />
|
||||
{#if $$slots.tag}
|
||||
<InlineTag>
|
||||
<slot name="tag" />
|
||||
</InlineTag>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
@@ -21,8 +21,8 @@
|
||||
on:blur
|
||||
bind:value
|
||||
class={classNames(
|
||||
'focus:border-greyscale-100 bg-greyscale-800 border-greyscale-700 flex items-center gap-1 rounded-lg border py-2 px-3 text-sm font-light transition-colors focus-within:border-white active:shadow-sm active:shadow-black/30',
|
||||
'focus:border-greyscale-100 bg-greyscale-800 border-greyscale-700 flex items-center gap-1 rounded-lg border px-3 py-2 text-sm font-light transition-colors focus-within:border-white active:shadow-sm active:shadow-black/30',
|
||||
classes
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
></textarea>
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
import Eyebrow from './eyebrow.svelte';
|
||||
|
||||
type $$Props = SvelteHTMLElements['span'];
|
||||
|
||||
const { class: classes, ...props } = $$restProps;
|
||||
</script>
|
||||
|
||||
<span
|
||||
<Eyebrow class="text-white">
|
||||
<span
|
||||
class={classNames(
|
||||
'badge font-aeonik-fono self-start rounded-[0.375rem] py-[0.375rem] px-3 text-xs uppercase text-white backdrop-blur-2xl',
|
||||
'badge self-start rounded-[0.375rem] px-3 py-[0.375rem] backdrop-blur-2xl',
|
||||
classes
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</span></Eyebrow
|
||||
>
|
||||
|
||||
<style>
|
||||
:root,
|
||||
97
src/lib/components/ui/button.svelte
Normal file
@@ -0,0 +1,97 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import { emptyMeltElement, melt, type AnyMeltElement } from '@melt-ui/svelte';
|
||||
import { cva, type VariantProps } from 'cva';
|
||||
import type { Action } from 'svelte/action';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
import { createBubbler } from 'svelte/legacy';
|
||||
import InlineTag from './inline-tag.svelte';
|
||||
|
||||
// TODO: replace _button.scss with Tailwind classes for long-term maintainability
|
||||
const button = cva(['web-button'], {
|
||||
variants: {
|
||||
variant: {
|
||||
primary: [''],
|
||||
secondary: ['is-secondary'],
|
||||
text: ['is-text']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
type ButtonProps =
|
||||
| (HTMLButtonAttributes & { href?: undefined })
|
||||
| (HTMLAnchorAttributes & { href: string });
|
||||
|
||||
type $$Props = ButtonProps &
|
||||
VariantProps<typeof button> & {
|
||||
use?:
|
||||
| Action<HTMLButtonElement | HTMLAnchorElement, any>
|
||||
| [Action<HTMLButtonElement | HTMLAnchorElement, any>, any];
|
||||
} & { element?: AnyMeltElement };
|
||||
|
||||
interface Props {
|
||||
href?: $$Props['href'];
|
||||
variant?: $$Props['variant'];
|
||||
use?: $$Props['use'];
|
||||
element?: AnyMeltElement | undefined;
|
||||
icon?: import('svelte').Snippet;
|
||||
children: import('svelte').Snippet;
|
||||
tag?: import('svelte').Snippet;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const {
|
||||
href = undefined,
|
||||
variant = 'primary',
|
||||
use = undefined,
|
||||
element = undefined,
|
||||
icon,
|
||||
children,
|
||||
tag,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
let meltElement = $derived(element ?? emptyMeltElement);
|
||||
|
||||
const { class: classes, href: _ } = rest;
|
||||
|
||||
const buttonClasses = classNames(button({ variant }), classes);
|
||||
|
||||
const applyAction = (node: HTMLButtonElement | HTMLAnchorElement) => {
|
||||
if (!use) return { destroy: () => {} };
|
||||
|
||||
if (typeof use === 'function') {
|
||||
return use(node);
|
||||
} else if (Array.isArray(use)) {
|
||||
const [action, params] = use;
|
||||
return action(node, params);
|
||||
}
|
||||
|
||||
return { destroy: () => {} };
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a {...rest} {href} class={buttonClasses} use:melt={$meltElement} use:applyAction>
|
||||
{#if icon}
|
||||
{@render icon()}
|
||||
{/if}
|
||||
{@render children()}
|
||||
{#if tag}
|
||||
<InlineTag>
|
||||
{@render tag()}
|
||||
</InlineTag>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button {...rest} class={buttonClasses} use:melt={$meltElement} use:applyAction>
|
||||
{#if icon}
|
||||
{@render icon()}
|
||||
{/if}
|
||||
{@render children()}
|
||||
{#if tag}
|
||||
<InlineTag>
|
||||
{@render tag()}
|
||||
</InlineTag>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import type { HTMLButtonAttributes, HTMLAnchorAttributes } from 'svelte/elements';
|
||||
import { cva, type VariantProps } from 'cva';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
const card = cva(
|
||||
[
|
||||
41
src/lib/components/ui/eyebrow.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
|
||||
// html text elements
|
||||
type ElementType =
|
||||
| 'p'
|
||||
| 'h1'
|
||||
| 'h2'
|
||||
| 'h3'
|
||||
| 'h4'
|
||||
| 'h5'
|
||||
| 'h6'
|
||||
| 'span'
|
||||
| 'div'
|
||||
| 'strong'
|
||||
| 'em'
|
||||
| 'small'
|
||||
| 'mark'
|
||||
| 'abbr'
|
||||
| 'blockquote'
|
||||
| 'cite'
|
||||
| 'code'
|
||||
| 'kbd'
|
||||
| 'var'
|
||||
| 'samp'
|
||||
| 'sub'
|
||||
| 'sup';
|
||||
|
||||
let className: string = '';
|
||||
export let as: ElementType = 'span';
|
||||
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={as}
|
||||
class={classNames(
|
||||
'text-micro tracking-loose text-primary font-aeonik-fono uppercase',
|
||||
className
|
||||
)}><slot /></svelte:element
|
||||
>
|
||||
16
src/lib/components/ui/icon/icon.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
import type { Icon } from './types';
|
||||
|
||||
type $$Props = {
|
||||
icon?: Icon;
|
||||
class?: string;
|
||||
} & SvelteHTMLElements['span'];
|
||||
|
||||
let className: $$Props['class'] = '';
|
||||
export { className as class };
|
||||
export let icon: $$Props['icon'] = 'arrow-right';
|
||||
</script>
|
||||
|
||||
<span class={classNames(`web-icon-${icon}`, className)} {...$$restProps}></span>
|
||||
1
src/lib/components/ui/icon/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './icon.svelte';
|
||||
57
src/lib/components/ui/icon/types.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
export type Icon =
|
||||
| 'apple'
|
||||
| 'appwrite'
|
||||
| 'arrow-down'
|
||||
| 'arrow-ext-link'
|
||||
| 'arrow-left'
|
||||
| 'arrow-right'
|
||||
| 'arrow-up'
|
||||
| 'bluesky'
|
||||
| 'calendar'
|
||||
| 'check'
|
||||
| 'chevron-down'
|
||||
| 'chevron-left'
|
||||
| 'chevron-right'
|
||||
| 'chevron-up'
|
||||
| 'close'
|
||||
| 'command'
|
||||
| 'copy'
|
||||
| 'daily-dev'
|
||||
| 'dark'
|
||||
| 'discord'
|
||||
| 'divider-vertical'
|
||||
| 'download'
|
||||
| 'ext-link'
|
||||
| 'firebase'
|
||||
| 'github'
|
||||
| 'google'
|
||||
| 'hamburger-menu'
|
||||
| 'instagram'
|
||||
| 'light'
|
||||
| 'linkedin'
|
||||
| 'location'
|
||||
| 'logout-left'
|
||||
| 'logout-right'
|
||||
| 'mailgun'
|
||||
| 'mcp'
|
||||
| 'message'
|
||||
| 'microsoft'
|
||||
| 'minus'
|
||||
| 'nuxt'
|
||||
| 'platform'
|
||||
| 'play'
|
||||
| 'plus'
|
||||
| 'product-hunt'
|
||||
| 'refine'
|
||||
| 'rest'
|
||||
| 'search'
|
||||
| 'sendgrid'
|
||||
| 'star'
|
||||
| 'system'
|
||||
| 'textmagic'
|
||||
| 'tiktok'
|
||||
| 'twitter'
|
||||
| 'vue'
|
||||
| 'x'
|
||||
| 'ycombinator'
|
||||
| 'youtube';
|
||||
@@ -1,5 +1,5 @@
|
||||
<div
|
||||
class="tracking-none text-sub-body text-greyscale-900 dark:text-greyscale-100 py-0.25 -mr-0.5 rounded-[.25rem] bg-black/8 px-1 dark:bg-white/[0.12]"
|
||||
class="tracking-none text-sub-body text-greyscale-900 dark:text-greyscale-100 -mr-0.5 rounded-[.25rem] bg-black/8 px-1 py-0.25 dark:bg-white/[0.12]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,36 +1,41 @@
|
||||
<!-- @migration-task Error while migrating Svelte code: migrating this component would require adding a `$props` rune but there's already a variable named props.
|
||||
Rename the variable and try again or migrate by hand. -->
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
|
||||
type $$Props = HTMLInputAttributes & {
|
||||
interface Props extends HTMLInputAttributes {
|
||||
label?: string;
|
||||
};
|
||||
icon?: Snippet;
|
||||
}
|
||||
|
||||
export let label: $$Props['label'] = '';
|
||||
export let type: $$Props['type'] = 'text';
|
||||
export let value: $$Props['value'] = '';
|
||||
const { class: classes, name, ...props } = $$restProps;
|
||||
let {
|
||||
label = '',
|
||||
type = 'text',
|
||||
value = $bindable(''),
|
||||
icon,
|
||||
class: classes,
|
||||
name,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if $$slots.icon}
|
||||
{#if icon}
|
||||
<label
|
||||
class={classNames(
|
||||
'focus:border-greyscale-100 bg-greyscale-800 border-greyscale-700 flex items-center gap-1 rounded-lg border py-2 px-3 text-sm font-light transition-colors focus-within:border-white active:shadow-sm active:shadow-black/30',
|
||||
'focus:border-greyscale-100 bg-greyscale-800 border-greyscale-700 flex items-center gap-1 rounded-lg border px-3 py-2 text-sm font-light transition-colors focus-within:border-white active:shadow-sm active:shadow-black/30',
|
||||
classes
|
||||
)}
|
||||
>
|
||||
<slot name="icon" />
|
||||
{@render icon?.()}
|
||||
{#key type}
|
||||
<input
|
||||
{name}
|
||||
{...{ type }}
|
||||
bind:value
|
||||
on:input
|
||||
on:change
|
||||
on:focus
|
||||
on:blur
|
||||
class="w-full border-0 ring-0 outline-none"
|
||||
{...props}
|
||||
{...rest}
|
||||
/>
|
||||
{/key}
|
||||
</label>
|
||||
@@ -45,15 +50,11 @@
|
||||
{name}
|
||||
{...{ type }}
|
||||
bind:value
|
||||
on:input
|
||||
on:change
|
||||
on:focus
|
||||
on:blur
|
||||
class={classNames(
|
||||
'focus:border-greyscale-100 bg-greyscale-800 border-greyscale-700 mt-2 flex w-full items-center gap-1 rounded-lg border py-2 px-3 text-sm font-light transition-colors focus-within:border-white active:shadow-sm active:shadow-black/30',
|
||||
'focus:border-greyscale-100 bg-greyscale-800 border-greyscale-700 mt-2 flex w-full items-center gap-1 rounded-lg border px-3 py-2 text-sm font-light transition-colors focus-within:border-white active:shadow-sm active:shadow-black/30',
|
||||
classes
|
||||
)}
|
||||
{...props}
|
||||
{...rest}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
35
src/lib/components/ui/switch.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import { createSwitch, melt } from '@melt-ui/svelte';
|
||||
|
||||
export let checked = false;
|
||||
|
||||
const {
|
||||
elements: { root },
|
||||
states: { checked: meltChecked }
|
||||
} = createSwitch({
|
||||
onCheckedChange({ next }) {
|
||||
checked = next;
|
||||
return next;
|
||||
}
|
||||
});
|
||||
|
||||
$: meltChecked.set(checked);
|
||||
</script>
|
||||
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
use:melt={$root}
|
||||
class={classNames(
|
||||
'bg-smooth group relative h-6 w-9 cursor-default rounded-full transition duration-150',
|
||||
'data-[state="checked"]:bg-accent'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
class={classNames(
|
||||
'absolute top-1/2 block size-5 translate-x-0.5 -translate-y-1/2 rounded-full bg-white transition duration-150',
|
||||
'group-[data-state="checked"]:translate-x-3.5'
|
||||
)}
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,8 +1,40 @@
|
||||
export const GITHUB_STARS = '47K';
|
||||
export const GITHUB_REPO_LINK = 'https://github.com/appwrite/appwrite';
|
||||
type SocialStats = {
|
||||
[K in 'GITHUB' | 'DISCORD' | 'TWITTER' | 'YOUTUBE']: {
|
||||
STAT: string;
|
||||
LINK: string;
|
||||
EXTRA?: Record<string, string> | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
export const SOCIAL_STATS: SocialStats = {
|
||||
GITHUB: {
|
||||
STAT: '48K',
|
||||
LINK: 'https://github.com/appwrite/appwrite',
|
||||
EXTRA: {
|
||||
COMMITS: '24K+',
|
||||
PULL_REQUESTS: '4.5K+',
|
||||
ISSUES: '3K+',
|
||||
OPEN_ISSUES: '500+',
|
||||
CLOSED_ISSUES: '3.3K+',
|
||||
FORKS: '4.3K+',
|
||||
CONTRIBUTORS: '800+'
|
||||
}
|
||||
},
|
||||
DISCORD: {
|
||||
STAT: '22K+',
|
||||
LINK: '/discord'
|
||||
},
|
||||
TWITTER: {
|
||||
STAT: '128K+',
|
||||
LINK: 'https://twitter.com/intent/follow?screen_name=appwrite'
|
||||
},
|
||||
YOUTUBE: {
|
||||
STAT: '11K+',
|
||||
LINK: 'https://www.youtube.com/c/appwrite?sub_confirmation=1'
|
||||
}
|
||||
};
|
||||
|
||||
export const BANNER_KEY: Banners = 'discord-banner-01'; // Change key to force banner to show again
|
||||
export const SENTRY_DSN =
|
||||
'https://27d41dc8bb67b596f137924ab8599e59@o1063647.ingest.us.sentry.io/4507497727000576';
|
||||
|
||||
export const BLOG_POSTS_PER_PAGE = 12;
|
||||
|
||||
@@ -127,16 +159,31 @@ export const socials: Array<Social> = [
|
||||
{
|
||||
icon: 'web-icon-linkedin',
|
||||
label: 'LinkedIn',
|
||||
link: 'https://www.linkedin.com/company/appwrite'
|
||||
link: 'https://linkedin.com/company/appwrite'
|
||||
},
|
||||
{
|
||||
icon: 'web-icon-youtube',
|
||||
label: 'YouTube',
|
||||
link: 'https://www.youtube.com/c/appwrite?sub_confirmation=1'
|
||||
link: 'https://youtube.com/c/appwrite?sub_confirmation=1'
|
||||
},
|
||||
{
|
||||
icon: 'web-icon-daily-dev',
|
||||
label: 'Daily.dev',
|
||||
link: 'https://app.daily.dev/squads/appwrite'
|
||||
},
|
||||
{
|
||||
icon: 'web-icon-bluesky',
|
||||
label: 'Bluesky',
|
||||
link: 'https://bsky.app/profile/appwrite.io'
|
||||
},
|
||||
{
|
||||
icon: 'web-icon-tiktok',
|
||||
label: 'Tiktok',
|
||||
link: 'https://tiktok.com/@appwrite'
|
||||
},
|
||||
{
|
||||
icon: 'web-icon-instagram',
|
||||
label: 'Instagram',
|
||||
link: 'https://instagram.com/appwrite.io'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
import { Search, IsLoggedIn } from '$lib/components';
|
||||
import { isMac } from '$lib/utils/platform';
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { GITHUB_REPO_LINK, GITHUB_STARS } from '$lib/constants';
|
||||
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
|
||||
import { page } from '$app/stores';
|
||||
import { SOCIAL_STATS } from '$lib/constants';
|
||||
import { page } from '$app/state';
|
||||
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
|
||||
|
||||
export let variant: DocsLayoutVariant = 'default';
|
||||
export let isReferences = false;
|
||||
@@ -67,7 +67,7 @@
|
||||
}));
|
||||
});
|
||||
|
||||
const key = $page.route.id?.includes('tutorials') ? TUT_CTX_KEY : CTX_KEY;
|
||||
const key = page.route.id?.includes('tutorials') ? TUT_CTX_KEY : CTX_KEY;
|
||||
setContext(key, true);
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
@@ -105,7 +105,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="web-mobile-header-end">
|
||||
<a href={PUBLIC_APPWRITE_DASHBOARD} class="web-button web-is-only-desktop">
|
||||
<a href={getAppwriteDashboardUrl()} class="web-button web-is-only-desktop">
|
||||
<span class="text-sub-body font-medium">Go to Console</span>
|
||||
</a>
|
||||
<button
|
||||
@@ -114,9 +114,9 @@
|
||||
on:click={toggleSidenav}
|
||||
>
|
||||
{#if $layoutState.showSidenav}
|
||||
<span aria-hidden="true" class="web-icon-close" />
|
||||
<span aria-hidden="true" class="web-icon-close"></span>
|
||||
{:else}
|
||||
<span aria-hidden="true" class="web-icon-hamburger-menu" />
|
||||
<span aria-hidden="true" class="web-icon-hamburger-menu"></span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -155,7 +155,7 @@
|
||||
class="web-input-button web-u-flex-basis-400"
|
||||
on:click={() => ($layoutState.showSearch = true)}
|
||||
>
|
||||
<span class="web-icon-search" aria-hidden="true" />
|
||||
<span class="web-icon-search" aria-hidden="true"></span>
|
||||
<span class="text">Search in docs</span>
|
||||
|
||||
<div class="ml-auto flex gap-1">
|
||||
@@ -172,14 +172,14 @@
|
||||
<div class="web-main-header-end">
|
||||
<div class="flex gap-2">
|
||||
<a
|
||||
href={GITHUB_REPO_LINK}
|
||||
href={SOCIAL_STATS.GITHUB.LINK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="web-button is-text"
|
||||
>
|
||||
<span class="web-icon-star" aria-hidden="true" />
|
||||
<span class="web-icon-star" aria-hidden="true"></span>
|
||||
<span class="text">Star on GitHub</span>
|
||||
<span class="web-inline-tag text-sub-body">{GITHUB_STARS}</span>
|
||||
<span class="web-inline-tag text-sub-body">{SOCIAL_STATS.GITHUB.STAT}</span>
|
||||
</a>
|
||||
<IsLoggedIn />
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
href: string;
|
||||
step?: number;
|
||||
selected?: boolean;
|
||||
level?: number;
|
||||
children?: Array<{
|
||||
title: string;
|
||||
href: string;
|
||||
selected: boolean;
|
||||
level?: number;
|
||||
}>;
|
||||
};
|
||||
</script>
|
||||
@@ -16,17 +18,13 @@
|
||||
import { setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { Feedback } from '$lib/components';
|
||||
import { scrollToTop } from '$lib/actions/scrollToTop';
|
||||
import type { Language } from '$lib/utils/code';
|
||||
import TableOfContents from '$lib/components/blog/table-of-contents.svelte';
|
||||
|
||||
export let title: string;
|
||||
export let toc: Array<TocItem>;
|
||||
export let back: string | undefined = undefined;
|
||||
export let date: string | undefined = undefined;
|
||||
|
||||
// Shared writable store for a selected language.
|
||||
setContext('language-context', writable<Language>());
|
||||
|
||||
const reducedArticleSize = setContext('articleHasNumericBadge', writable(false));
|
||||
</script>
|
||||
|
||||
@@ -40,7 +38,7 @@
|
||||
class="web-icon-button web-is-only-mobile"
|
||||
aria-label="previous page"
|
||||
>
|
||||
<span class="icon-cheveron-left" aria-hidden="true" />
|
||||
<span class="icon-cheveron-left" aria-hidden="true"></span>
|
||||
</a>
|
||||
{/if}
|
||||
<ul class="web-metadata text-caption">
|
||||
@@ -58,13 +56,13 @@
|
||||
<span
|
||||
class="icon-cheveron-left web-u-font-size-24 text-primary web-is-not-mobile"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
></span>
|
||||
</a>
|
||||
{/if}
|
||||
<h1 class="text-title font-aeonik-pro text-primary">{title}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="web-article-header-end" />
|
||||
<div class="web-article-header-end"></div>
|
||||
</header>
|
||||
<div class="web-article-content" class:web-reduced-article-size={$reducedArticleSize}>
|
||||
<slot />
|
||||
@@ -74,45 +72,7 @@
|
||||
<aside class="web-references-menu ps-6">
|
||||
<div class="web-references-menu-content">
|
||||
{#if toc && toc.length > 0}
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<h5 class="web-references-menu-title text-micro uppercase">On This Page</h5>
|
||||
</div>
|
||||
<ol class="web-references-menu-list">
|
||||
{#each toc as parent (parent.href)}
|
||||
<li class="web-references-menu-item">
|
||||
<a
|
||||
href={parent.href}
|
||||
class="web-references-menu-link"
|
||||
class:is-selected={parent.selected}
|
||||
>
|
||||
{#if parent?.step}
|
||||
<span class="web-numeric-badge">{parent.step}</span>
|
||||
{/if}
|
||||
<span class="text-caption">{parent.title}</span>
|
||||
</a>
|
||||
{#if parent.children}
|
||||
<ol class="web-references-menu-list mt-4 ml-8">
|
||||
{#each parent.children as child}
|
||||
<li class="web-references-menu-item">
|
||||
<a
|
||||
href={child.href}
|
||||
class="web-references-menu-link"
|
||||
>
|
||||
<span class="text-caption">{child.title}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
<div class="border-greyscale-900/4 border-t pt-5">
|
||||
<button class="web-link inline-flex items-center gap-2" use:scrollToTop>
|
||||
<span class="web-icon-arrow-up" aria-hidden="true" />
|
||||
<span class="text-caption">Back to top</span>
|
||||
</button>
|
||||
</div>
|
||||
<TableOfContents heading="On This Page" {toc} />
|
||||
{/if}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||