Merge branch 'main' of https://github.com/appwrite/website into fix-build-warnings

This commit is contained in:
Torsten Dittmann
2025-06-11 16:55:50 +02:00
246 changed files with 7969 additions and 7865 deletions

View File

@@ -24,7 +24,7 @@
"optimize": "node ./scripts/optimize-assets.js",
"optimize:all": "node ./scripts/optimize-all.js"
},
"packageManager": "pnpm@10.8.1",
"packageManager": "pnpm@10.11.1",
"dependencies": {
"h3": "^1.14.0",
"posthog-js": "^1.210.2",
@@ -41,18 +41,20 @@
"@internationalized/date": "3.5.0",
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.86.5",
"@number-flow/svelte": "^0.3.3",
"@number-flow/svelte": "^0.3.7",
"@playwright/test": "^1.50.0",
"@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.1.2",
"@tailwindcss/postcss": "^4.1.4",
"@turf/boolean-point-in-polygon": "^7.2.0",
"@types/compression": "^1.7.5",
"@types/glob": "^8.1.0",
"@types/jsdom": "^21.1.7",
"@types/markdown-it": "^13.0.9",
"@types/morgan": "^1.9.9",
"@types/proj4": "^2.5.6",
"analytics": "^0.8.16",
"appwrite": "^17.0.1",
"bits-ui": "^1.3.19",
@@ -74,9 +76,8 @@
"markdown-it": "^14.1.0",
"meilisearch": "^0.37.0",
"melt": "^0.29.2",
"motion": "^12.7.3",
"motion-legacy": "npm:motion@^10.18.0",
"node-appwrite": "^15.0.1",
"motion": "^12.7.4",
"node-appwrite": "^16.0.0",
"node-fetch": "^3.3.2",
"node-html-parser": "^6.1.13",
"openapi-types": "^12.1.3",
@@ -88,6 +89,7 @@
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"proj4": "^2.17.0",
"remeda": "^2.20.0",
"reodotdev": "^1.0.0",
"sass": "^1.83.4",
@@ -95,9 +97,10 @@
"svelte-check": "^4.0.0",
"svelte-markdoc-preprocess": "3.0.0",
"svelte-markdown": "^0.4.1",
"svg-dotted-map": "^2.0.1",
"svgtofont": "^4.2.3",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.1.2",
"tailwindcss": "^4.1.4",
"tslib": "^2.8.1",
"typescript": "^5.8.2",
"typescript-eslint": "^8.21.0",

1384
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
@import 'tailwindcss';
@import './styles/typography.css';
@variant dark (&:is(.dark *));
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* Colors */
@@ -38,33 +38,33 @@
/* mint */
--color-mint-200: hsl(var(--color-mint-hue) 56% 88%);
--color-mint-500: hsl(calc(var(--color-mint-hue) + 1), 54%, 69%);
--color-mint-700: hsl(calc(var(--color-mint-hue) + 2), 24%, 41%);
--color-mint-500: hsl(calc(var(--color-mint-hue) + 1) 54% 69%);
--color-mint-700: hsl(calc(var(--color-mint-hue) + 2) 24% 41%);
/* purple */
--color-purple-200: hsl(var(--color-purple-hue) 100% 88%);
--color-purple-500: hsl(calc(var(--color-purple-hue) - 1), 99%, 70%);
--color-purple-700: hsl(calc(var(--color-purple-hue) - 1), 42%, 42%);
--color-purple-500: hsl(calc(var(--color-purple-hue) - 1) 99% 70%);
--color-purple-700: hsl(calc(var(--color-purple-hue) - 1) 42% 42%);
/* yellow */
--color-yellow-200: hsl(var(--color-yellow-hue) 100% 88%);
--color-yellow-500: hsl(var(--color-yellow-hue) 99% 70%);
--color-yellow-700: hsl(calc(var(--color-yellow-hue) + 1), 42%, 42%);
--color-yellow-700: hsl(calc(var(--color-yellow-hue) + 1) 42% 42%);
/* blue */
--color-blue-200: hsl(var(--color-blue-hue) 100% 88%);
--color-blue-500: hsl(calc(var(--color-blue-hue) - 1), 99%, 70%);
--color-blue-700: hsl(calc(var(--color-blue-hue) - 1), 42%, 42%);
--color-blue-500: hsl(calc(var(--color-blue-hue) - 1) 99% 70%);
--color-blue-700: hsl(calc(var(--color-blue-hue) - 1) 42% 42%);
/* green */
--color-green-700: #0a714f;
/* secondary */
--color-secondary-100: hsl(var(--color-secondary-hue) 99% 66%);
--color-accent-200: hsl(var(--color-secondary-hue), 78%, 60%, 0.32);
--color-accent-200: hsl(var(--color-secondary-hue) 78% 60% / 0.32);
/* greyscale */
--color-offset: hsl(var(--color-greyscale-hue) 2%, 11%, 0.94);
--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%);
--color-greyscale-100: hsl(var(--color-greyscale-hue) 6% 90%);
@@ -86,26 +86,41 @@
/* Animations */
--animate-scale-in: scale-in 200ms ease-out forwards;
--animate-scale-out: scale-out 200ms ease-out forwards;
--animate-caret-blink: caret-blink 1s ease-in-out infinite;
--animate-enter:
fade-in 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-scroll-x: scroll-x var(--speed, 30s) linear infinite var(--direction, forwards);
--animate-scroll-y: scroll-y 60s linear infinite forwards;
--animate-scroll-y: scroll-y 30s linear infinite forwards;
--animate-fade-in: fade-in 0.5s ease-in-out both;
--animate-marquee: marquee var(--speed, 30s) linear infinite var(--direction, forwards);
--animate-fade-out: fade-out 0.5s ease-in-out both;
--animate-lighting: lighting 1.25s ease-out forwards;
--animate-menu-in: menu-in 0.25s ease-out forwards;
--animate-menu-out: menu-out 0.25s ease-out forwards;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--animate-wipe-in: wipe-in 2s ease-in-out;
/* Keyframes */
@keyframes scale-in {
0% {
transform: scale(0);
transform: rotateX(-10deg) scale(0.9);
}
100% {
transform: scale(1);
transform: rotateX(0) scale(1);
}
}
@keyframes scale-out {
0% {
transform: rotateX(0) scale(1);
}
100% {
transform: rotateX(-10deg) scale(0.9);
}
}
@@ -132,7 +147,7 @@
@keyframes up {
0% {
transform: translateY(8px);
transform: translateY(36px);
}
100% {
transform: translateY(0px);
@@ -148,7 +163,16 @@
}
}
@keyframes scroll {
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes scroll-deprecate {
0% {
transform: translateX(0);
}
@@ -157,9 +181,15 @@
}
}
@keyframes marquee {
@keyframes scroll-x {
to {
transform: translateX(-50%);
transform: translateX(-100%);
}
}
@keyframes scroll-y {
to {
transform: translateY(-100%);
}
}
@@ -202,15 +232,56 @@
}
}
@keyframes scroll-x {
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
transform: translateX(-100%);
opacity: 1;
transform: translateX(0);
}
}
@keyframes scroll-y {
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
transform: translateY(-50%);
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes wipe-in {
0% {
clip-path: polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%);
}
100% {
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
}
}
@@ -224,10 +295,10 @@
/* Font sizes */
--text-x-micro: 0.625rem;
--text-x-micro--line-height: 0.875rem;
--text-x-micro--letter-spacing: var(--tracking-loose);
--text-x-micro--letter-spacing: var(--tracking-tighter);
--text-micro: 0.75rem;
--text-micro--line-height: 1rem;
--text-micro--letter-spacing: var(--tracking-loose);
--text-micro--letter-spacing: var(--tracking-tighter);
--text-caption: 0.875rem;
--text-caption--line-height: 1.375rem;
--text-caption--letter-spacing: var(--tracking-tight);
@@ -251,9 +322,15 @@
--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-title-lg: clamp(2.85rem, 5vw, 3rem);
--text-title-lg--line-height: clamp(2.75rem, 5.5vw, 3.5rem);
--text-title-lg--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-hero: clamp(3.2rem, 7vw, 4.5rem);
--text-hero--line-height: clamp(3.125rem, 7.5vw, 4.25rem);
--text-hero--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);
@@ -276,12 +353,12 @@
@utility border-gradient {
--border-gradient-before: linear-gradient(
180deg,
var(--to, 180deg),
rgba(255, 255, 255, 0.16) 0%,
rgba(255, 255, 255, 0) 100%
);
--border-gradient-after: linear-gradient(
180deg,
var(--to, 180deg),
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 125.11%
);
@@ -310,16 +387,6 @@
}
}
@utility mask {
mask-image: linear-gradient(
to var(--mask-direction, top),
transparent,
black var(--mask-height, 32px),
black calc(100% - var(--mask-height, 32px)),
black
);
}
:root,
.light {
/* pink polyfills */
@@ -340,14 +407,14 @@
/* base */
--color-primary: var(--color-greyscale-900);
--color-secondary: var(--color-greyscale-700);
--color-accent: var(--color-pink-600);
--color-accent: var(--color-pink-500);
--carousel-gradient: transparent;
--color-badge-bg: var(--color-badge-bg-light);
--color-badge-border: var(--color-badge-border-light);
--color-smooth: hsl(var(--color-greyscale-hue) 6%, 10%, 0.04);
--color-smooth: hsl(var(--color-greyscale-hue) 6% 10% / 0.04);
--color-card: var(--color-greyscale-850);
--color-tertiary: hsl(var(--color-greyscale-600));
--color-offset: hsl(var(--color-greyscale-hue) 2%, 11%, 0.94);
--color-offset: hsl(var(--color-greyscale-hue) 2% 11% / 0.94);
--color-subtle: var(--color-greyscale-850);
}
@@ -356,7 +423,7 @@
--color-secondary: var(--color-greyscale-300);
--carousel-gradient: 23, 23, 26;
--color-primary-bg: var(--color-greyscale-900);
--color-smooth: hsl(0 0%, 100%, 0.06);
--color-smooth: hsl(0 0% 100% / 0.06);
--color-tertiary: hsl(var(--color-greyscale-600));
--color-offset: hsl(0 0% 100% / 0.1);
}

View File

@@ -37,7 +37,7 @@ const securityheaders: Handle = async ({ event, resolve }) => {
});
// `true` if deployed via Coolify.
const isPreview = !!process.env.COOLIFY_FQDN;
const isPreview = !!process.env.COOLIFY_FQDN || process.env.NODE_ENV === 'development';
// COOLIFY_FQDN already includes `http`.
const previewDomain = isPreview ? `${process.env.COOLIFY_FQDN}` : null;
const join = (arr: string[]) => arr.join(' ');
@@ -122,13 +122,6 @@ const securityheaders: Handle = async ({ event, resolve }) => {
return response;
};
// const bannerRewriter: Handle = async ({ event, resolve }) => {
// const response = await resolve(event, {
// transformPageChunk: ({ html }) => html.replace('%aw_banner_key%', BANNER_KEY)
// });
// return response;
// };
const initSession: Handle = async ({ event, resolve }) => {
const session = await createInitSessionClient(event.cookies);

View File

@@ -1 +1 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.925 4.028 C 9.521 4.066,8.877 4.204,8.524 4.328 C 8.123 4.469,7.392 4.849,7.035 5.101 C 6.608 5.403,5.892 6.119,5.591 6.545 C 5.321 6.929,4.984 7.584,4.835 8.018 C 4.401 9.277,4.401 10.708,4.835 11.967 C 5.340 13.435,6.497 14.723,7.918 15.400 C 10.018 16.401,12.497 16.102,14.298 14.630 C 14.650 14.342,15.135 13.821,15.408 13.438 C 15.675 13.063,16.010 12.412,16.165 11.967 C 16.599 10.715,16.599 9.270,16.165 8.018 C 15.445 5.946,13.523 4.360,11.374 4.065 C 10.964 4.009,10.306 3.992,9.925 4.028 M11.305 5.269 C 12.589 5.477,13.793 6.266,14.514 7.372 C 15.950 9.575,15.343 12.526,13.153 13.986 C 12.377 14.505,11.454 14.781,10.500 14.781 C 9.605 14.781,8.772 14.552,8.033 14.104 C 6.863 13.395,6.055 12.239,5.786 10.891 C 5.702 10.471,5.702 9.514,5.786 9.094 C 6.040 7.821,6.785 6.700,7.840 6.005 C 8.366 5.658,9.088 5.368,9.663 5.271 C 10.068 5.204,10.894 5.203,11.305 5.269 " fill="currentColor" stroke="none" fill-rule="evenodd"></path></svg>
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.233 2.465 C 3.093 2.527,2.942 2.724,2.917 2.882 C 2.864 3.205,2.798 3.125,4.745 5.075 C 5.735 6.067,6.545 6.888,6.545 6.899 C 6.545 6.910,6.456 7.052,6.347 7.213 C 5.989 7.746,5.709 8.426,5.576 9.091 C 5.484 9.547,5.493 10.505,5.593 10.961 C 5.817 11.991,6.256 12.801,6.983 13.528 C 7.726 14.272,8.706 14.769,9.748 14.930 C 10.245 15.006,11.107 14.982,11.550 14.880 C 12.187 14.732,12.812 14.461,13.315 14.116 L 13.592 13.925 15.380 15.709 C 16.363 16.690,17.204 17.511,17.249 17.534 C 17.294 17.557,17.398 17.576,17.480 17.576 C 17.887 17.576,18.153 17.252,18.079 16.846 C 18.058 16.728,17.850 16.507,16.255 14.910 C 15.265 13.918,14.455 13.096,14.455 13.083 C 14.455 13.070,14.551 12.917,14.667 12.742 C 15.023 12.211,15.283 11.564,15.423 10.866 C 15.515 10.405,15.507 9.482,15.407 9.024 C 15.184 8.000,14.743 7.180,14.037 6.474 C 13.330 5.767,12.455 5.296,11.463 5.089 C 11.016 4.996,9.984 4.996,9.538 5.089 C 8.884 5.226,8.252 5.491,7.699 5.861 L 7.405 6.058 5.619 4.275 C 4.636 3.294,3.798 2.475,3.756 2.454 C 3.652 2.401,3.366 2.407,3.233 2.465 M10.982 6.231 C 11.798 6.333,12.514 6.681,13.131 7.273 C 13.710 7.829,14.079 8.505,14.225 9.275 C 14.302 9.677,14.296 10.365,14.214 10.763 C 13.933 12.115,12.893 13.248,11.581 13.628 C 10.701 13.883,9.800 13.826,8.963 13.463 C 8.213 13.138,7.483 12.444,7.114 11.708 C 6.840 11.160,6.720 10.638,6.720 9.992 C 6.720 9.159,6.926 8.493,7.392 7.816 C 7.594 7.523,8.085 7.039,8.375 6.848 C 9.162 6.329,10.062 6.116,10.982 6.231 " fill="#19191C" stroke="none" fill-rule="evenodd"></path></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.359 3.533 C 10.347 3.579,10.328 3.729,10.317 3.867 C 10.231 4.927,9.870 6.002,9.308 6.867 C 8.254 8.489,6.294 9.627,4.050 9.919 C 3.830 9.947,3.590 9.983,3.517 9.998 L 3.383 10.025 3.562 10.029 C 3.809 10.035,4.375 10.120,4.833 10.221 C 5.428 10.352,6.067 10.579,6.650 10.865 C 7.440 11.252,7.890 11.560,8.429 12.081 C 9.566 13.183,10.161 14.485,10.331 16.247 C 10.370 16.646,10.400 16.743,10.400 16.468 C 10.400 16.020,10.630 14.922,10.849 14.319 C 11.096 13.642,11.417 13.020,11.796 12.485 C 12.064 12.105,12.659 11.502,13.067 11.195 C 13.456 10.902,14.234 10.497,14.683 10.353 C 15.214 10.184,15.999 10.035,16.375 10.034 C 16.647 10.033,16.548 9.983,16.190 9.940 C 15.261 9.830,14.172 9.448,13.373 8.953 C 11.565 7.831,10.538 5.988,10.391 3.600 C 10.383 3.480,10.377 3.467,10.359 3.533 " fill="#EDEDF0" stroke="none" fill-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 961 B

View File

@@ -15,43 +15,49 @@ $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-remix: "\ea2d";
$web-icon-rest: "\ea2e";
$web-icon-search: "\ea2f";
$web-icon-sendgrid: "\ea30";
$web-icon-star: "\ea31";
$web-icon-system: "\ea32";
$web-icon-textmagic: "\ea33";
$web-icon-tiktok: "\ea34";
$web-icon-twitter: "\ea35";
$web-icon-vue: "\ea36";
$web-icon-x: "\ea37";
$web-icon-ycombinator: "\ea38";
$web-icon-youtube: "\ea39";
$web-icon-customize: "\ea12";
$web-icon-daily-dev: "\ea13";
$web-icon-dark: "\ea14";
$web-icon-discord: "\ea15";
$web-icon-divider-vertical: "\ea16";
$web-icon-download: "\ea17";
$web-icon-edge: "\ea18";
$web-icon-ext-link: "\ea19";
$web-icon-firebase: "\ea1a";
$web-icon-github: "\ea1b";
$web-icon-google: "\ea1c";
$web-icon-hamburger-menu: "\ea1d";
$web-icon-instagram: "\ea1e";
$web-icon-light: "\ea1f";
$web-icon-linkedin: "\ea20";
$web-icon-location: "\ea21";
$web-icon-logout-left: "\ea22";
$web-icon-logout-right: "\ea23";
$web-icon-mailgun: "\ea24";
$web-icon-mcp: "\ea25";
$web-icon-message: "\ea26";
$web-icon-microsoft: "\ea27";
$web-icon-minus: "\ea28";
$web-icon-nuxt: "\ea29";
$web-icon-platform: "\ea2a";
$web-icon-play: "\ea2b";
$web-icon-plus: "\ea2c";
$web-icon-pop-locations: "\ea2d";
$web-icon-product-hunt: "\ea2e";
$web-icon-refine: "\ea2f";
$web-icon-regions: "\ea30";
$web-icon-remix: "\ea31";
$web-icon-rest: "\ea32";
$web-icon-search: "\ea33";
$web-icon-sendgrid: "\ea34";
$web-icon-sparkle: "\ea35";
$web-icon-star: "\ea36";
$web-icon-system: "\ea37";
$web-icon-textmagic: "\ea38";
$web-icon-ticket: "\ea39";
$web-icon-tiktok: "\ea3a";
$web-icon-twitter: "\ea3b";
$web-icon-vue: "\ea3c";
$web-icon-x: "\ea3d";
$web-icon-ycombinator: "\ea3e";
$web-icon-youtube: "\ea3f";

View File

@@ -101,244 +101,280 @@
"className": "web-icon-copy",
"unicode": "&#59921;"
},
"daily-dev": {
"customize": {
"encodedCode": "\\ea12",
"prefix": "web-icon",
"className": "web-icon-daily-dev",
"className": "web-icon-customize",
"unicode": "&#59922;"
},
"dark": {
"daily-dev": {
"encodedCode": "\\ea13",
"prefix": "web-icon",
"className": "web-icon-dark",
"className": "web-icon-daily-dev",
"unicode": "&#59923;"
},
"discord": {
"dark": {
"encodedCode": "\\ea14",
"prefix": "web-icon",
"className": "web-icon-discord",
"className": "web-icon-dark",
"unicode": "&#59924;"
},
"divider-vertical": {
"discord": {
"encodedCode": "\\ea15",
"prefix": "web-icon",
"className": "web-icon-divider-vertical",
"className": "web-icon-discord",
"unicode": "&#59925;"
},
"download": {
"divider-vertical": {
"encodedCode": "\\ea16",
"prefix": "web-icon",
"className": "web-icon-download",
"className": "web-icon-divider-vertical",
"unicode": "&#59926;"
},
"ext-link": {
"download": {
"encodedCode": "\\ea17",
"prefix": "web-icon",
"className": "web-icon-ext-link",
"className": "web-icon-download",
"unicode": "&#59927;"
},
"firebase": {
"edge": {
"encodedCode": "\\ea18",
"prefix": "web-icon",
"className": "web-icon-firebase",
"className": "web-icon-edge",
"unicode": "&#59928;"
},
"github": {
"ext-link": {
"encodedCode": "\\ea19",
"prefix": "web-icon",
"className": "web-icon-github",
"className": "web-icon-ext-link",
"unicode": "&#59929;"
},
"google": {
"firebase": {
"encodedCode": "\\ea1a",
"prefix": "web-icon",
"className": "web-icon-google",
"className": "web-icon-firebase",
"unicode": "&#59930;"
},
"hamburger-menu": {
"github": {
"encodedCode": "\\ea1b",
"prefix": "web-icon",
"className": "web-icon-hamburger-menu",
"className": "web-icon-github",
"unicode": "&#59931;"
},
"instagram": {
"google": {
"encodedCode": "\\ea1c",
"prefix": "web-icon",
"className": "web-icon-instagram",
"className": "web-icon-google",
"unicode": "&#59932;"
},
"light": {
"hamburger-menu": {
"encodedCode": "\\ea1d",
"prefix": "web-icon",
"className": "web-icon-light",
"className": "web-icon-hamburger-menu",
"unicode": "&#59933;"
},
"linkedin": {
"instagram": {
"encodedCode": "\\ea1e",
"prefix": "web-icon",
"className": "web-icon-linkedin",
"className": "web-icon-instagram",
"unicode": "&#59934;"
},
"location": {
"light": {
"encodedCode": "\\ea1f",
"prefix": "web-icon",
"className": "web-icon-location",
"className": "web-icon-light",
"unicode": "&#59935;"
},
"logout-left": {
"linkedin": {
"encodedCode": "\\ea20",
"prefix": "web-icon",
"className": "web-icon-logout-left",
"className": "web-icon-linkedin",
"unicode": "&#59936;"
},
"logout-right": {
"location": {
"encodedCode": "\\ea21",
"prefix": "web-icon",
"className": "web-icon-logout-right",
"className": "web-icon-location",
"unicode": "&#59937;"
},
"mailgun": {
"logout-left": {
"encodedCode": "\\ea22",
"prefix": "web-icon",
"className": "web-icon-mailgun",
"className": "web-icon-logout-left",
"unicode": "&#59938;"
},
"mcp": {
"logout-right": {
"encodedCode": "\\ea23",
"prefix": "web-icon",
"className": "web-icon-mcp",
"className": "web-icon-logout-right",
"unicode": "&#59939;"
},
"message": {
"mailgun": {
"encodedCode": "\\ea24",
"prefix": "web-icon",
"className": "web-icon-message",
"className": "web-icon-mailgun",
"unicode": "&#59940;"
},
"microsoft": {
"mcp": {
"encodedCode": "\\ea25",
"prefix": "web-icon",
"className": "web-icon-microsoft",
"className": "web-icon-mcp",
"unicode": "&#59941;"
},
"minus": {
"message": {
"encodedCode": "\\ea26",
"prefix": "web-icon",
"className": "web-icon-minus",
"className": "web-icon-message",
"unicode": "&#59942;"
},
"nuxt": {
"microsoft": {
"encodedCode": "\\ea27",
"prefix": "web-icon",
"className": "web-icon-nuxt",
"className": "web-icon-microsoft",
"unicode": "&#59943;"
},
"platform": {
"minus": {
"encodedCode": "\\ea28",
"prefix": "web-icon",
"className": "web-icon-platform",
"className": "web-icon-minus",
"unicode": "&#59944;"
},
"play": {
"nuxt": {
"encodedCode": "\\ea29",
"prefix": "web-icon",
"className": "web-icon-play",
"className": "web-icon-nuxt",
"unicode": "&#59945;"
},
"plus": {
"platform": {
"encodedCode": "\\ea2a",
"prefix": "web-icon",
"className": "web-icon-plus",
"className": "web-icon-platform",
"unicode": "&#59946;"
},
"product-hunt": {
"play": {
"encodedCode": "\\ea2b",
"prefix": "web-icon",
"className": "web-icon-product-hunt",
"className": "web-icon-play",
"unicode": "&#59947;"
},
"refine": {
"plus": {
"encodedCode": "\\ea2c",
"prefix": "web-icon",
"className": "web-icon-refine",
"className": "web-icon-plus",
"unicode": "&#59948;"
},
"remix": {
"pop-locations": {
"encodedCode": "\\ea2d",
"prefix": "web-icon",
"className": "web-icon-remix",
"className": "web-icon-pop-locations",
"unicode": "&#59949;"
},
"rest": {
"product-hunt": {
"encodedCode": "\\ea2e",
"prefix": "web-icon",
"className": "web-icon-rest",
"className": "web-icon-product-hunt",
"unicode": "&#59950;"
},
"search": {
"refine": {
"encodedCode": "\\ea2f",
"prefix": "web-icon",
"className": "web-icon-search",
"className": "web-icon-refine",
"unicode": "&#59951;"
},
"sendgrid": {
"regions": {
"encodedCode": "\\ea30",
"prefix": "web-icon",
"className": "web-icon-sendgrid",
"className": "web-icon-regions",
"unicode": "&#59952;"
},
"star": {
"remix": {
"encodedCode": "\\ea31",
"prefix": "web-icon",
"className": "web-icon-star",
"className": "web-icon-remix",
"unicode": "&#59953;"
},
"system": {
"rest": {
"encodedCode": "\\ea32",
"prefix": "web-icon",
"className": "web-icon-system",
"className": "web-icon-rest",
"unicode": "&#59954;"
},
"textmagic": {
"search": {
"encodedCode": "\\ea33",
"prefix": "web-icon",
"className": "web-icon-textmagic",
"className": "web-icon-search",
"unicode": "&#59955;"
},
"tiktok": {
"sendgrid": {
"encodedCode": "\\ea34",
"prefix": "web-icon",
"className": "web-icon-tiktok",
"className": "web-icon-sendgrid",
"unicode": "&#59956;"
},
"twitter": {
"sparkle": {
"encodedCode": "\\ea35",
"prefix": "web-icon",
"className": "web-icon-twitter",
"className": "web-icon-sparkle",
"unicode": "&#59957;"
},
"vue": {
"star": {
"encodedCode": "\\ea36",
"prefix": "web-icon",
"className": "web-icon-vue",
"className": "web-icon-star",
"unicode": "&#59958;"
},
"x": {
"system": {
"encodedCode": "\\ea37",
"prefix": "web-icon",
"className": "web-icon-x",
"className": "web-icon-system",
"unicode": "&#59959;"
},
"ycombinator": {
"textmagic": {
"encodedCode": "\\ea38",
"prefix": "web-icon",
"className": "web-icon-ycombinator",
"className": "web-icon-textmagic",
"unicode": "&#59960;"
},
"youtube": {
"ticket": {
"encodedCode": "\\ea39",
"prefix": "web-icon",
"className": "web-icon-youtube",
"className": "web-icon-ticket",
"unicode": "&#59961;"
},
"tiktok": {
"encodedCode": "\\ea3a",
"prefix": "web-icon",
"className": "web-icon-tiktok",
"unicode": "&#59962;"
},
"twitter": {
"encodedCode": "\\ea3b",
"prefix": "web-icon",
"className": "web-icon-twitter",
"unicode": "&#59963;"
},
"vue": {
"encodedCode": "\\ea3c",
"prefix": "web-icon",
"className": "web-icon-vue",
"unicode": "&#59964;"
},
"x": {
"encodedCode": "\\ea3d",
"prefix": "web-icon",
"className": "web-icon-x",
"unicode": "&#59965;"
},
"ycombinator": {
"encodedCode": "\\ea3e",
"prefix": "web-icon",
"className": "web-icon-ycombinator",
"unicode": "&#59966;"
},
"youtube": {
"encodedCode": "\\ea3f",
"prefix": "web-icon",
"className": "web-icon-youtube",
"unicode": "&#59967;"
}
}

View File

@@ -71,123 +71,141 @@
.web-icon-copy:before {
content: '\ea11';
}
.web-icon-daily-dev:before {
.web-icon-customize:before {
content: '\ea12';
}
.web-icon-dark:before {
.web-icon-daily-dev:before {
content: '\ea13';
}
.web-icon-discord:before {
.web-icon-dark:before {
content: '\ea14';
}
.web-icon-divider-vertical:before {
.web-icon-discord:before {
content: '\ea15';
}
.web-icon-download:before {
.web-icon-divider-vertical:before {
content: '\ea16';
}
.web-icon-ext-link:before {
.web-icon-download:before {
content: '\ea17';
}
.web-icon-firebase:before {
.web-icon-edge:before {
content: '\ea18';
}
.web-icon-github:before {
.web-icon-ext-link:before {
content: '\ea19';
}
.web-icon-google:before {
.web-icon-firebase:before {
content: '\ea1a';
}
.web-icon-hamburger-menu:before {
.web-icon-github:before {
content: '\ea1b';
}
.web-icon-instagram:before {
.web-icon-google:before {
content: '\ea1c';
}
.web-icon-light:before {
.web-icon-hamburger-menu:before {
content: '\ea1d';
}
.web-icon-linkedin:before {
.web-icon-instagram:before {
content: '\ea1e';
}
.web-icon-location:before {
.web-icon-light:before {
content: '\ea1f';
}
.web-icon-logout-left:before {
.web-icon-linkedin:before {
content: '\ea20';
}
.web-icon-logout-right:before {
.web-icon-location:before {
content: '\ea21';
}
.web-icon-mailgun:before {
.web-icon-logout-left:before {
content: '\ea22';
}
.web-icon-mcp:before {
.web-icon-logout-right:before {
content: '\ea23';
}
.web-icon-message:before {
.web-icon-mailgun:before {
content: '\ea24';
}
.web-icon-microsoft:before {
.web-icon-mcp:before {
content: '\ea25';
}
.web-icon-minus:before {
.web-icon-message:before {
content: '\ea26';
}
.web-icon-nuxt:before {
.web-icon-microsoft:before {
content: '\ea27';
}
.web-icon-platform:before {
.web-icon-minus:before {
content: '\ea28';
}
.web-icon-play:before {
.web-icon-nuxt:before {
content: '\ea29';
}
.web-icon-plus:before {
.web-icon-platform:before {
content: '\ea2a';
}
.web-icon-product-hunt:before {
.web-icon-play:before {
content: '\ea2b';
}
.web-icon-refine:before {
.web-icon-plus:before {
content: '\ea2c';
}
.web-icon-remix:before {
.web-icon-pop-locations:before {
content: '\ea2d';
}
.web-icon-rest:before {
.web-icon-product-hunt:before {
content: '\ea2e';
}
.web-icon-search:before {
.web-icon-refine:before {
content: '\ea2f';
}
.web-icon-sendgrid:before {
.web-icon-regions:before {
content: '\ea30';
}
.web-icon-star:before {
.web-icon-remix:before {
content: '\ea31';
}
.web-icon-system:before {
.web-icon-rest:before {
content: '\ea32';
}
.web-icon-textmagic:before {
.web-icon-search:before {
content: '\ea33';
}
.web-icon-tiktok:before {
.web-icon-sendgrid:before {
content: '\ea34';
}
.web-icon-twitter:before {
.web-icon-sparkle:before {
content: '\ea35';
}
.web-icon-vue:before {
.web-icon-star:before {
content: '\ea36';
}
.web-icon-x:before {
.web-icon-system:before {
content: '\ea37';
}
.web-icon-ycombinator:before {
.web-icon-textmagic:before {
content: '\ea38';
}
.web-icon-youtube:before {
.web-icon-ticket:before {
content: '\ea39';
}
.web-icon-tiktok:before {
content: '\ea3a';
}
.web-icon-twitter:before {
content: '\ea3b';
}
.web-icon-vue:before {
content: '\ea3c';
}
.web-icon-x:before {
content: '\ea3d';
}
.web-icon-ycombinator:before {
content: '\ea3e';
}
.web-icon-youtube:before {
content: '\ea3f';
}

Binary file not shown.

View File

@@ -33,46 +33,52 @@
.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-remix:before { content: "\ea2d"; }
.web-icon-rest:before { content: "\ea2e"; }
.web-icon-search:before { content: "\ea2f"; }
.web-icon-sendgrid:before { content: "\ea30"; }
.web-icon-star:before { content: "\ea31"; }
.web-icon-system:before { content: "\ea32"; }
.web-icon-textmagic:before { content: "\ea33"; }
.web-icon-tiktok:before { content: "\ea34"; }
.web-icon-twitter:before { content: "\ea35"; }
.web-icon-vue:before { content: "\ea36"; }
.web-icon-x:before { content: "\ea37"; }
.web-icon-ycombinator:before { content: "\ea38"; }
.web-icon-youtube:before { content: "\ea39"; }
.web-icon-customize:before { content: "\ea12"; }
.web-icon-daily-dev:before { content: "\ea13"; }
.web-icon-dark:before { content: "\ea14"; }
.web-icon-discord:before { content: "\ea15"; }
.web-icon-divider-vertical:before { content: "\ea16"; }
.web-icon-download:before { content: "\ea17"; }
.web-icon-edge:before { content: "\ea18"; }
.web-icon-ext-link:before { content: "\ea19"; }
.web-icon-firebase:before { content: "\ea1a"; }
.web-icon-github:before { content: "\ea1b"; }
.web-icon-google:before { content: "\ea1c"; }
.web-icon-hamburger-menu:before { content: "\ea1d"; }
.web-icon-instagram:before { content: "\ea1e"; }
.web-icon-light:before { content: "\ea1f"; }
.web-icon-linkedin:before { content: "\ea20"; }
.web-icon-location:before { content: "\ea21"; }
.web-icon-logout-left:before { content: "\ea22"; }
.web-icon-logout-right:before { content: "\ea23"; }
.web-icon-mailgun:before { content: "\ea24"; }
.web-icon-mcp:before { content: "\ea25"; }
.web-icon-message:before { content: "\ea26"; }
.web-icon-microsoft:before { content: "\ea27"; }
.web-icon-minus:before { content: "\ea28"; }
.web-icon-nuxt:before { content: "\ea29"; }
.web-icon-platform:before { content: "\ea2a"; }
.web-icon-play:before { content: "\ea2b"; }
.web-icon-plus:before { content: "\ea2c"; }
.web-icon-pop-locations:before { content: "\ea2d"; }
.web-icon-product-hunt:before { content: "\ea2e"; }
.web-icon-refine:before { content: "\ea2f"; }
.web-icon-regions:before { content: "\ea30"; }
.web-icon-remix:before { content: "\ea31"; }
.web-icon-rest:before { content: "\ea32"; }
.web-icon-search:before { content: "\ea33"; }
.web-icon-sendgrid:before { content: "\ea34"; }
.web-icon-sparkle:before { content: "\ea35"; }
.web-icon-star:before { content: "\ea36"; }
.web-icon-system:before { content: "\ea37"; }
.web-icon-textmagic:before { content: "\ea38"; }
.web-icon-ticket:before { content: "\ea39"; }
.web-icon-tiktok:before { content: "\ea3a"; }
.web-icon-twitter:before { content: "\ea3b"; }
.web-icon-vue:before { content: "\ea3c"; }
.web-icon-x:before { content: "\ea3d"; }
.web-icon-ycombinator:before { content: "\ea3e"; }
.web-icon-youtube:before { content: "\ea3f"; }
$web-icon-apple: "\ea01";
$web-icon-appwrite: "\ea02";
@@ -91,43 +97,49 @@ $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-remix: "\ea2d";
$web-icon-rest: "\ea2e";
$web-icon-search: "\ea2f";
$web-icon-sendgrid: "\ea30";
$web-icon-star: "\ea31";
$web-icon-system: "\ea32";
$web-icon-textmagic: "\ea33";
$web-icon-tiktok: "\ea34";
$web-icon-twitter: "\ea35";
$web-icon-vue: "\ea36";
$web-icon-x: "\ea37";
$web-icon-ycombinator: "\ea38";
$web-icon-youtube: "\ea39";
$web-icon-customize: "\ea12";
$web-icon-daily-dev: "\ea13";
$web-icon-dark: "\ea14";
$web-icon-discord: "\ea15";
$web-icon-divider-vertical: "\ea16";
$web-icon-download: "\ea17";
$web-icon-edge: "\ea18";
$web-icon-ext-link: "\ea19";
$web-icon-firebase: "\ea1a";
$web-icon-github: "\ea1b";
$web-icon-google: "\ea1c";
$web-icon-hamburger-menu: "\ea1d";
$web-icon-instagram: "\ea1e";
$web-icon-light: "\ea1f";
$web-icon-linkedin: "\ea20";
$web-icon-location: "\ea21";
$web-icon-logout-left: "\ea22";
$web-icon-logout-right: "\ea23";
$web-icon-mailgun: "\ea24";
$web-icon-mcp: "\ea25";
$web-icon-message: "\ea26";
$web-icon-microsoft: "\ea27";
$web-icon-minus: "\ea28";
$web-icon-nuxt: "\ea29";
$web-icon-platform: "\ea2a";
$web-icon-play: "\ea2b";
$web-icon-plus: "\ea2c";
$web-icon-pop-locations: "\ea2d";
$web-icon-product-hunt: "\ea2e";
$web-icon-refine: "\ea2f";
$web-icon-regions: "\ea30";
$web-icon-remix: "\ea31";
$web-icon-rest: "\ea32";
$web-icon-search: "\ea33";
$web-icon-sendgrid: "\ea34";
$web-icon-sparkle: "\ea35";
$web-icon-star: "\ea36";
$web-icon-system: "\ea37";
$web-icon-textmagic: "\ea38";
$web-icon-ticket: "\ea39";
$web-icon-tiktok: "\ea3a";
$web-icon-twitter: "\ea3b";
$web-icon-vue: "\ea3c";
$web-icon-x: "\ea3d";
$web-icon-ycombinator: "\ea3e";
$web-icon-youtube: "\ea3f";

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 212 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,14 +1,5 @@
<svg
width="21"
height="20"
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.5 14.8C13.151 14.8 15.3 12.651 15.3 10C15.3 7.34903 13.151 5.2 10.5 5.2C7.84903 5.2 5.7 7.34903 5.7 10C5.7 12.651 7.84903 14.8 10.5 14.8ZM10.5 16C13.8137 16 16.5 13.3137 16.5 10C16.5 6.68629 13.8137 4 10.5 4C7.18629 4 4.5 6.68629 4.5 10C4.5 13.3137 7.18629 16 10.5 16Z"
fill="currentColor"
/>
d="M3.07619 2.57566C3.28117 2.37086 3.59729 2.3449 3.83009 2.49851L3.92384 2.57566L7.41505 6.06687C8.26493 5.39934 9.3355 4.99948 10.5 4.99948L10.7569 5.00632C13.3987 5.14004 15.4998 7.32442 15.5 9.99948L15.4932 10.2563C15.4393 11.3213 15.0517 12.2984 14.4336 13.0854L17.9238 16.5757L18.001 16.6694C18.1547 16.9022 18.1287 17.2183 17.9238 17.4233C17.7188 17.6283 17.4028 17.6543 17.1699 17.5005L17.0762 17.4233L13.586 13.9331C12.7361 14.6009 11.6648 14.9995 10.5 14.9995L10.2432 14.9926C7.68619 14.8633 5.63624 12.8133 5.50685 10.2563L5.50001 9.99948C5.50011 8.83482 5.89864 7.76343 6.56642 6.91355L3.07619 3.42331L2.99904 3.32956C2.84545 3.0967 2.87123 2.78061 3.07619 2.57566ZM10.5 6.19968C8.40148 6.19968 6.70044 7.901 6.70021 9.99948C6.70021 12.0982 8.40133 13.7993 10.5 13.7993C12.5987 13.7992 14.2998 12.0981 14.2998 9.99948C14.2996 7.90103 12.5985 6.19972 10.5 6.19968Z"
fill="#19191C" />
</svg>

Before

Width:  |  Height:  |  Size: 509 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3 10C5.45614 10 10.3684 8.6 10.3684 3C10.3684 8.6 14.7895 10 17 10C11.6947 10 10.3684 14.6667 10.3684 17C10.3684 11.4 5.45614 10 3 10Z"
fill="#EDEDF0" />
</svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@@ -59,7 +59,7 @@ export type TrackEventArgs = { name: string; data?: object };
export const trackEvent = (eventArgs?: string | TrackEventArgs): void => {
if (!eventArgs || ENV.TEST) return;
const path = page.route.id ?? '';
const path = page.route.id?.replace(/\(([^()]*)\)/g, '') ?? '';
const name = typeof eventArgs === 'string' ? eventArgs : eventArgs.name;
const data = typeof eventArgs === 'string' ? { path } : { ...eventArgs.data, path };

View File

@@ -1,17 +1,13 @@
import { inView, type InViewOptions } from 'motion-legacy';
import { inView } from 'motion';
import { writable } from 'svelte/store';
export const useAnimateInView = ({ options }: { options?: InViewOptions }) => {
export const useAnimateInView = () => {
let animate = writable<boolean>(false);
const action = (node: HTMLElement) => {
inView(
node,
() => {
inView(node, () => {
animate.set(true);
},
{ ...options }
);
});
};
return {

View File

@@ -0,0 +1,41 @@
import { hover } from 'motion';
import { writable } from 'svelte/store';
export interface Position {
x: number;
y: number;
}
export const useMousePosition = () => {
let position = $state<Position>({
x: 0,
y: 0
});
const action = (node: HTMLElement | SVGSVGElement) => {
const handleMouseMove = (event: MouseEvent) => {
const { clientX, clientY } = event;
position = {
x: clientX - 12, // Remove rect.left
y: clientY + -350 // Remove rect.top
};
};
hover(node, () => {
document.addEventListener('mousemove', handleMouseMove);
});
return {
destroy() {
document.removeEventListener('mousemove', handleMouseMove);
}
};
};
return {
action,
position: () => {
return position;
}
};
};

View File

@@ -1,34 +0,0 @@
import { inView } from 'motion-legacy';
import { type Writable, writable } from 'svelte/store';
export const useMousePosition = () => {
let position = writable<{ x: number; y: number }>({
x: 0,
y: 0
});
const action = (node: HTMLElement) => {
const handleMouseMove = (event: MouseEvent) => {
position.set({
x: event.clientX,
y: event.clientY
});
};
inView(
node,
() => {
node.addEventListener('mousemove', handleMouseMove);
},
{ amount: 'any' }
);
return {
destroy() {
node.removeEventListener('mousemove', handleMouseMove);
}
};
};
return { action, position };
};

View File

@@ -1,34 +0,0 @@
<script lang="ts">
import { rect } from '$lib/actions';
import { writable } from 'svelte/store';
const bodyRect = writable<DOMRect | null>(null);
</script>
<div class="relative">
<div
class="true-body"
style:width={`${$bodyRect?.width ?? 0}px`}
style:height={`${$bodyRect?.height ?? 0}px`}
></div>
<div class="body" use:rect={bodyRect}>
<slot />
</div>
</div>
<style lang="scss">
.relative {
position: relative;
overflow: hidden;
}
.body {
position: absolute;
left: 0;
top: 0;
}
.true-body {
transition: 0.2s ease;
}
</style>

View File

@@ -1,11 +0,0 @@
<script lang="ts">
import '$scss/hljs.css';
import { getCodeHtml } from '$lib/utils/code';
export let content: string;
$: codeHtml = getCodeHtml({ content, language: 'js' });
</script>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html codeHtml}

View File

@@ -1,96 +0,0 @@
<script>
import AutoBox from '../AutoBox.svelte';
import Code from './Code.svelte';
</script>
<div class="code-console">
<div class="header">
<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>
</div>
<style lang="scss">
@use '$scss/abstract/mixins/border-gradient' as gradients;
.code-console {
@include gradients.border-gradient;
--p-radius: 16px;
display: flex;
flex-direction: column;
background-color: hsl(var(--web-color-card));
border-radius: var(--p-radius);
--m-border-radius: var(--p-radius);
--m-border-gradient-before: linear-gradient(
180deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 125.11%
);
backdrop-filter: blur(8px);
min-width: 330px;
width: fit-content;
padding: 0 0.25rem 0.25rem;
}
.block {
width: 100%;
flex-grow: 1;
text-align: left;
border-radius: 12px;
background: linear-gradient(129deg, rgba(0, 0, 0, 0.48) 22.38%, rgba(0, 0, 0, 0) 136.5%);
padding: 20px;
position: relative;
}
.header {
position: relative;
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.75rem;
.ellipse {
background-color: #ec6a5e;
width: 8px;
height: 8px;
top: 12px;
border-radius: 4px;
}
.ellipse-2 {
background-color: #f5bf4f;
width: 8px;
height: 8px;
top: 12px;
border-radius: 4px;
}
.ellipse-3 {
background-color: #61c554;
width: 8px;
height: 8px;
top: 12px;
border-radius: 4px;
}
}
.code-console :global(code) {
white-space: pre;
tab-size: 4;
}
</style>

View File

@@ -1,395 +0,0 @@
<script lang="ts">
import { toScale, type Scale } from '$lib/utils/toScale';
import { spring, type AnimationListOptions, type SpringOptions } from 'motion-legacy';
import { animation, createScrollHandler, scroll, type Animation } from '.';
import { SOCIAL_STATS } from '$lib/constants';
const springOptions: SpringOptions = {
stiffness: 58.78,
mass: 1,
damping: 17.14
};
const animationOptions: AnimationListOptions = {
x: { easing: spring(springOptions) },
y: { easing: spring(springOptions) }
};
const animations: {
mobile: {
main: Animation;
reversed: Animation;
};
desktop: {
main: Animation;
reversed: Animation;
};
}[] = [
{
mobile: {
main: animation(
'#oss-discord',
{ x: 0, y: [1200, 0], rotate: 1 },
animationOptions
),
reversed: animation('#oss-discord', { y: 1200, x: 0, rotate: 1 }, animationOptions)
},
desktop: {
main: animation(
'#oss-discord',
{ x: 20, y: '-75vh', rotate: 15 },
animationOptions
),
reversed: animation(
'#oss-discord',
{ x: -100, y: '0vh', rotate: 15 },
animationOptions
)
}
},
{
mobile: {
main: animation(
'#oss-github',
{ x: 0, y: [1200, -10], rotate: -2 },
animationOptions
),
reversed: animation('#oss-github', { y: 1200, x: 10, rotate: -2 }, animationOptions)
},
desktop: {
main: animation(
'#oss-github',
{ x: -100, y: '-50vh', rotate: 6.26 },
animationOptions
),
reversed: animation(
'#oss-github',
{ x: 0, y: '0vh', rotate: 6.26 },
animationOptions
)
}
},
{
mobile: {
main: animation(
'#oss-twitter',
{ x: 0, y: [1200, 10], rotate: -3 },
animationOptions
),
reversed: animation(
'#oss-twitter',
{ y: 1200, x: -10, rotate: -3 },
animationOptions
)
},
desktop: {
main: animation(
'#oss-twitter',
{ x: 100, y: '-65vh', rotate: -15 },
animationOptions
),
reversed: animation(
'#oss-twitter',
{ x: 0, y: '0vh', rotate: -15 },
animationOptions
)
}
},
{
mobile: {
main: animation(
'#oss-youtube',
{ x: 0, y: [1200, 5], rotate: 2 },
animationOptions
),
reversed: animation('#oss-youtube', { y: 1200, x: -5, rotate: 2 }, animationOptions)
},
desktop: {
main: animation(
'#oss-youtube',
{ x: -100, y: '-50vh', rotate: -3.77 },
animationOptions
),
reversed: animation(
'#oss-youtube',
{ x: 0, y: '0vh', rotate: -3.77 },
animationOptions
)
}
},
{
mobile: {
main: animation(
'#oss-commits',
{ x: 0, y: [1200, -4], rotate: -1 },
animationOptions
),
reversed: animation('#oss-commits', { y: 1200, x: 4, rotate: -1 }, animationOptions)
},
desktop: {
main: animation(
'#oss-commits',
{ x: 100, y: '-70vh', rotate: -10.2 },
animationOptions
),
reversed: animation(
'#oss-commits',
{ x: 0, y: '0vh', rotate: -10.2 },
animationOptions
)
}
}
];
function isMobile(): boolean {
if (typeof window === 'undefined') return false;
return window?.innerWidth < 1024;
}
const animScale: Scale = [0, animations.length - 1];
const percentScale: Scale = [0.1, 0.8];
const scrollHandler = createScrollHandler(
animations.map(({ mobile, desktop }, i) => {
return {
percentage: isMobile() ? toScale(i, animScale, percentScale) : 0.1,
whenAfter() {
const { main, reversed } = isMobile() ? mobile : desktop;
main.play();
return reversed.play;
}
};
})
);
</script>
<div
id="open-source"
use:scroll
on:web-scroll={({ detail }) => {
const { percentage } = detail;
scrollHandler(percentage);
}}
on:web-resize={({ detail }) => {
scrollHandler.reset();
const { percentage } = detail;
scrollHandler(percentage);
}}
>
<div class="sticky-wrapper">
<h3 class="text-display font-aeonik-pro text-primary">Powered by Open Source</h3>
<div class="cards-wrapper">
<a
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"
id="oss-discord"
>
<div class="flex flex-col justify-between gap-8">
<span
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>
</a>
<a
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
id="oss-github"
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.STAT} GitHub Stars
</div>
</a>
<a
href={SOCIAL_STATS.TWITTER.LINK}
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
id="oss-twitter"
>
<div class="flex flex-col justify-between gap-8">
<span
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>
</a>
<a
href={SOCIAL_STATS.YOUTUBE.LINK}
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
id="oss-youtube"
>
<div class="flex flex-col justify-between gap-8">
<span
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>
</a>
<a
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
id="oss-commits"
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>
</a>
</div>
</div>
</div>
<style lang="scss">
#open-source {
min-height: 150vh;
height: 3500px;
position: relative;
@media (min-width: 1024px) {
height: 1500px;
}
}
.sticky-wrapper {
position: sticky;
top: -15vh;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1.25rem;
padding-inline: 1.25rem;
width: 100%;
height: 130vh;
text-align: center;
&::after {
background: linear-gradient(
to top,
hsl(var(--web-color-background)) 0%,
hsl(var(--web-color-background) / 0) 5%
);
position: absolute;
inset: 0;
}
.cards-wrapper {
position: relative;
height: 22.5rem;
margin-top: 80px;
}
@media (min-width: 1024px) {
h3 {
max-width: 61.375rem;
}
.cards-wrapper {
position: absolute;
height: 100vh;
width: clamp(1024px, 90vw, 1440px);
top: 0;
left: 50%;
transform: translateX(-50%);
margin-top: 0;
}
}
}
.oss-card {
background: linear-gradient(
106deg,
rgba(255, 255, 255, 0.72) 0%,
rgba(255, 255, 255, 0.8) 41.9%,
rgba(255, 255, 255, 0.6) 100%
);
backdrop-filter: blur(10px);
--card-padding: 2rem;
--w: clamp(306px, 50vw, 22.125rem);
--h: 20.125rem;
width: var(--w);
height: var(--h);
text-align: left;
display: flex;
flex-direction: column;
justify-content: space-between;
position: absolute;
left: calc(50% - calc(var(--w) / 2));
transform: translateX(-1200px);
[class*='icon'] {
opacity: 48%;
}
}
@media (min-width: 1024px) {
.oss-card {
left: unset;
transform: unset;
}
#oss-discord {
bottom: -400px;
left: 1%;
transform: rotate(15deg);
}
#oss-github {
bottom: -400px;
left: 19%;
}
#oss-twitter {
bottom: -400px;
left: clamp(20%, 22vw, 29%);
}
#oss-youtube {
bottom: -400px;
left: clamp(64%, calc(1024px - 10vw), 70%);
}
#oss-commits {
bottom: -400px;
right: 10%;
}
}
</style>

View File

@@ -1,9 +0,0 @@
<script lang="ts">
export let id: string | undefined = undefined;
</script>
<div class="phone" {id}>
<div class="inner">
<slot />
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -1,55 +0,0 @@
<script lang="ts">
import AutoHeight from '../AutoBox.svelte';
</script>
<div class="anim-box">
<div class="top"><slot name="top" /></div>
<div class="content">
<AutoHeight>
<slot />
</AutoHeight>
</div>
</div>
<style lang="scss">
@use '$scss/abstract/mixins/border-gradient' as gradients;
.anim-box {
@include gradients.border-gradient;
--m-border-radius: 1rem;
--m-border-gradient-before: linear-gradient(
180deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 125.11%
);
border-radius: var(--m-border-radius);
background: hsl(var(--web-color-card));
backdrop-filter: blur(8px);
padding: 0.5rem;
padding-block-start: 0;
text-align: left;
.top {
color: var(--greyscale-50, #ededf0);
font-family: Aeonik Pro;
font-size: 1.25rem;
font-style: normal;
font-weight: 400;
line-height: 2rem; /* 160% */
letter-spacing: -0.0125rem;
padding: 1rem;
text-align: left;
}
.content {
border-radius: 0.75rem;
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(30px);
position: relative;
}
}
</style>

View File

@@ -1,695 +0,0 @@
<script lang="ts" context="module">
import AuthShot from './(assets)/auth-shot.png?enhanced';
import DatabasesShot from './(assets)/db-shot.png?enhanced';
import FunctionsShot from './(assets)/fn-shot.png?enhanced';
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);
export function getElSelector(el: string) {
return `#${el}-${get(elId)}`;
}
/* Products infos */
const products = [
'auth',
'databases',
'storage',
'functions',
'messaging',
'realtime',
'post'
] as const;
type Product = (typeof products)[number];
type ProductInfo = {
icon: {
active: string;
inactive: string;
};
title: string;
subtitle: string;
description: string;
features: string[];
shot?: EnhancedImgAttributes['src'];
};
export const infos: { [K in Product]?: ProductInfo } = {
auth: {
icon: {
active: './images/icons/illustrated/dark/auth.png',
inactive: './images/icons/illustrated/dark/auth-transparent.png'
},
title: 'Auth',
subtitle: 'Secure login for all users',
description:
'Authenticate users securely with multiple login methods like Email/Password, SMS, OAuth, Anonymous, Magic URLs and more.',
features: [
'30+ login methods',
'Support for teams, roles and user labels',
'Rate-limits and advanced user protection',
'Custom SMTP and email templates'
],
shot: AuthShot
},
databases: {
icon: {
active: './images/icons/illustrated/dark/databases.png',
inactive: './images/icons/illustrated/dark/databases-transparent.png'
},
title: 'Databases',
subtitle: 'Store, query and manage data',
description: 'Scalable and robust database backed by your favorite technologies.',
features: [
'Never paused',
'Fast in-memory caching',
'Advanced permission models',
'Custom data validation',
'Relationships support'
],
shot: DatabasesShot
},
functions: {
icon: {
active: './images/icons/illustrated/dark/functions.png',
inactive: './images/icons/illustrated/dark/functions-transparent.png'
},
title: 'Functions',
subtitle: 'Customize and extend your backend',
description: 'Deploy and scale serverless functions in secure, isolated runtimes.',
features: [
'Automatic deployment from GitHub',
'Trigger using GitHub, CLI, Event Listeners or HTTP requests',
'Support for 30+ runtimes in 13 languages',
'Custom domain support'
],
shot: FunctionsShot
},
messaging: {
icon: {
active: './images/icons/illustrated/dark/messaging.png',
inactive: './images/icons/illustrated/dark/messaging-transparent.png'
},
title: 'Messaging',
subtitle: 'Communicate with your users',
description:
'Set up a full-functioning messaging service for your application that covers multiple channels under one unified platform.',
features: [
'Draft and preview your messages before delivery',
'Segment your users for specific targeting',
'Send push notifications, emails, and SMS',
'Supports real-time and location-based messaging'
],
shot: MessagingShot
},
storage: {
icon: {
active: './images/icons/illustrated/dark/storage.png',
inactive: './images/icons/illustrated/dark/storage-transparent.png'
},
title: 'Storage',
subtitle: 'Upload and manage files',
description:
'Securely store files with advanced compression, encryption and image transformations.',
features: [
'File encryption at rest and transit',
'Built-in image transformation capabilities',
'Advanced compression with WebP/Brotli support'
],
shot: StorageShot
},
realtime: {
icon: {
active: './images/icons/illustrated/dark/realtime.png',
inactive: './images/icons/illustrated/dark/realtime-transparent.png'
},
title: 'Realtime',
subtitle: 'Realtime events for every service',
description: 'Subscribe and react to any Appwrite event using the Realtime API.',
features: [
'Unlimited subscriptions',
'Built-in permission management',
'Support for DBs, Auth, Storage & Functions'
],
shot: RealtimeShot
}
};
</script>
<script lang="ts">
import { toScale, type Scale } from '$lib/utils/toScale';
import { fly, slide } from 'svelte/transition';
import { scroll } from '..';
import ScrollIndicator from '../scroll-indicator.svelte';
import { Auth, authController } from './auth';
import AnimatedBox from './AnimatedBox.svelte';
import { tick } from 'svelte';
import CodeWindow from '../CodeWindow/CodeWindow.svelte';
import { Databases, databasesController } from './databases';
import { objectKeys } from '$lib/utils/object';
import { get, writable } from 'svelte/store';
import { Storage, storageController } from './storage';
import { Functions, functionsController } from './functions';
import { Realtime, realtimeController } from './realtime';
import { Messaging, messagingController } from './messaging';
import { postController } from './post';
import Post from './post/post.svelte';
import { anyify } from '$lib/utils/anyify';
/* Basic Animation setup */
let scrollInfo = {
percentage: 0,
traversed: 0,
remaning: Infinity
};
const animScale: Scale = [0.075, 1];
const productsScales = products.map((_, idx) => {
const diff = animScale[1] - animScale[0];
const step = diff / products.length;
return [animScale[0] + step * idx, animScale[0] + step * (idx + 1)] as Scale;
});
$: active = (function getActiveInfo() {
let activeIdx = productsScales.findIndex(([min, max]) => {
return scrollInfo.percentage >= min && scrollInfo.percentage < max;
});
const product = products[activeIdx] as Product;
const scale = productsScales[activeIdx] as Scale;
const percent = scale ? toScale(scrollInfo.percentage, scale, [0, 1]) : 0;
return {
product,
scale,
percent
};
})();
let lastActive: Product | undefined = undefined;
const controllers = {
auth: authController,
databases: databasesController,
storage: storageController,
functions: functionsController,
messaging: messagingController,
realtime: realtimeController,
post: postController
};
$: (async () => {
const fixedLast = lastActive;
lastActive = active.product;
objectKeys(controllers).forEach(async (key) => {
const controller = controllers[key];
if (active.product === key && fixedLast !== key) {
if (!(fixedLast === 'realtime' && key === 'post')) {
elId.update((n) => n + 1);
}
await tick();
controller.execute();
}
});
})();
</script>
<div
id="products"
use:scroll
on:web-scroll={({ detail }) => {
scrollInfo = detail;
}}
>
<div class="sticky-wrapper">
<!-- <div class="debug">
<pre>{scrollInfo.percentage}</pre>
</div> -->
{#if scrollInfo.percentage < 0.075}
<div
class="main-text"
out:fly={{ duration: 250, y: -300 }}
in:fly={{ duration: 250, delay: 250, y: -300 }}
>
{#if scrollInfo.percentage > -0.1}
<span
class="web-badges text-micro !text-white uppercase"
transition:slide={{ axis: 'x' }}>Products_</span
>
<h2
class="text-display font-aeonik-pro text-primary"
transition:fly={{ y: 16, delay: 250 }}
>
Your backend, minus the hassle
</h2>
<p
class="text-description mx-auto max-w-[700px]"
transition:fly={{
y: 16,
delay: 400
}}
>
Build secure and scalable applications with less code. Add authentication,
databases, storage, and more using Appwrite's development platform.
</p>
{/if}
</div>
{:else}
<div
class="products"
out:fly={{ duration: 250, y: 300 }}
in:fly={{ duration: 500, delay: 250, y: 300 }}
data-active={scrollInfo.percentage > 0.075 ? '' : undefined}
>
<div class="text" id="pd-{$elId}">
<ScrollIndicator
percentage={toScale(scrollInfo.percentage, animScale, [0, 1])}
/>
<ul class="descriptions">
{#each products as product}
{@const copy = infos[product]}
{@const isActive = active.product === product}
{#if copy}
<li data-active={isActive ? '' : undefined}>
<h3>
<img
src={isActive ? copy.icon.active : copy.icon.inactive}
alt=""
width="32"
height="32"
/>
<span class="text-label">{copy.title}</span>
</h3>
{#if isActive}
<div transition:slide>
<h4 class="text-title font-aeonik-pro">
{copy.subtitle}
</h4>
<p>
{copy.description}
</p>
<ul class="features">
{#each copy.features as feature}
<li>{feature}</li>
{/each}
</ul>
</div>
{/if}
</li>
{/if}
{/each}
</ul>
</div>
<div class="animated">
<div class="box-wrapper" id="box-{$elId}">
<AnimatedBox>
<div class="top" slot="top">
<p class="title">
{#if active.product === 'auth'}
Users
{:else if active.product === 'databases'}
Tasks
{:else if active.product === 'storage'}
Files
{:else if active.product === 'functions'}
<!-- oblivion -->
{:else if active.product === 'messaging'}
Messages
{:else if active.product === 'realtime'}
Realtime
{/if}
</p>
</div>
{#if active.product === 'auth'}
<Auth.Box />
{:else if active.product === 'messaging'}
<Messaging.Box />
{:else if active.product === 'databases'}
<Databases.Box />
{:else if active.product === 'storage'}
<Storage.Box />
{/if}
</AnimatedBox>
</div>
<div class="code-window" id="code-{$elId}">
<CodeWindow>
{#if active.product === 'auth'}
<Auth.Code />
{:else if active.product === 'databases'}
<Databases.Code />
{:else if active.product === 'storage'}
<Storage.Code />
{:else if active.product === 'functions'}
<Functions.Code />
{:else if active.product === 'messaging'}
<Messaging.Code />
{/if}
</CodeWindow>
</div>
{#if active.product === 'auth'}
<div class="controls" id="controls-{$elId}">
<Auth.Controls />
</div>
{/if}
</div>
<div class="phone" id="phone-{$elId}">
<div class="inner">
{#if active.product === 'auth'}
<Auth.Phone />
{:else if active.product === 'databases'}
<Databases.Phone />
{:else if active.product === 'storage'}
<Storage.Phone />
{:else if active.product === 'messaging'}
<Messaging.Phone />
{:else if active.product === 'functions'}
<Functions.Phone />
{:else if !['auth', 'databases', 'storage', 'messaging', 'functions'].includes(active.product)}
<Realtime.Phone />
{/if}
</div>
</div>
{#if !['auth', 'databases', 'storage', 'functions', 'messaging', 'realtime'].includes(anyify(active.product))}
<Post />
{/if}
</div>
{/if}
</div>
</div>
<style lang="scss">
@use '$scss/abstract/mixins/border-gradient' as gradients;
#products {
min-height: 500vh;
height: fit-content;
position: relative;
--debug-bg: transparent;
display: none;
}
@media (min-width: 1400px) {
#products {
display: block;
}
}
.sticky-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
gap: 1rem;
position: sticky;
top: 0;
min-height: 50rem;
overflow: hidden;
padding-inline: 1.25rem;
width: 100%;
height: 60vh;
> .main-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
min-height: 5rem;
text-align: center;
h2 {
white-space: nowrap;
margin-top: 1.5rem;
}
p {
margin-top: 1.25rem;
max-width: 48.875rem;
}
@media (min-width: 1024px) {
h2 {
max-width: 61.375rem;
}
}
}
}
.products:not([data-active]) {
opacity: 0;
}
.products[data-active] {
opacity: 1;
}
.products {
background: var(--debug-bg, hsla(250, 50%, 50%, 0.25));
display: flex;
justify-content: space-between;
width: 100%;
max-width: 77.75rem;
position: relative;
transition: 200ms ease;
.text {
background: var(--debug-bg, hsla(200, 50%, 50%, 0.25));
display: flex;
flex-grow: 1;
max-width: 25rem;
position: relative;
.descriptions {
margin-inline-start: 2rem;
text-align: left;
position: absolute;
width: 100%;
> li {
&:not(:first-child) {
padding-block-start: 1.5rem;
}
transition: 100ms ease;
&[data-active] {
h3 {
color: hsl(var(--web-color-primary));
margin-block-end: 0.75rem;
}
}
}
h3 {
display: flex;
align-items: center;
gap: 0.75rem;
}
h4 {
color: hsl(var(--web-color-primary));
}
p {
margin-block-start: 1rem;
}
.features {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-block-start: 2rem;
li {
--marker-size: 1.25rem;
--margin-left: calc(var(--marker-size) + 0.75rem);
position: relative;
margin-inline-start: var(--margin-left);
&::before {
content: '';
position: absolute;
left: calc(var(--margin-left) * -1);
top: 50%;
width: var(--marker-size);
height: var(--marker-size);
transform: translateY(-50%);
background: url('/images/icons/colored/check.svg') no-repeat;
}
}
}
}
}
}
.animated {
background: var(--debug-bg, hsl(100, 50%, 50%, 0.25));
width: min(42rem, 50vw);
height: min(38.75rem, 90vh);
position: relative;
}
.phone {
@include gradients.border-gradient;
--m-border-size: 1px;
--m-border-radius: 2.5rem;
--m-border-gradient-after: linear-gradient(
180deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 125.11%
);
background: rgba(255, 255, 255, 0.08);
//backdrop-filter: blur(8px);
padding: 0.5rem;
width: 275px;
height: 550px;
flex-shrink: 0;
position: absolute;
top: 0;
left: calc(50%);
z-index: 10;
opacity: 1;
.inner {
background-color: white;
border-radius: 2rem;
width: 100%;
height: 100%;
position: relative;
&::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 0.5rem;
border-radius: 100rem;
background: var(--label-color-light-primary, #000);
width: 6.25rem;
height: 0.25rem;
}
}
}
.box-wrapper {
position: absolute;
top: 0;
z-index: 0;
opacity: 0;
width: 25rem;
transform: translateX(16.5rem) translateY(2rem);
}
.box-wrapper :global(.pseudo-table) {
:global(.header),
:global(.row) {
display: grid;
grid-template-columns: 10rem 1fr;
justify-content: space-between;
align-items: center;
gap: 1.5rem 3rem;
}
:global(.header) {
border-bottom: 1px solid hsl(var(--web-color-greyscale-700));
color: var(--greyscale-400, #adadb1);
text-transform: uppercase;
padding: 1rem;
}
:global(.row) {
padding-block: 0.5rem;
padding-inline: 1rem;
color: var(--greyscale-400, #adadb1);
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem; /* 142.857% */
}
:global(.avatar) {
background-color: hsl(var(--web-color-greyscale-700));
border-color: hsl(var(--web-color-greyscale-700));
}
:global(.truncated) {
display: block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.code-window {
position: absolute;
z-index: 20;
top: 0;
left: 0;
opacity: 0;
}
.controls {
@include gradients.border-gradient;
--m-border-radius: 1rem;
--m-border-gradient-before: linear-gradient(
180deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 125.11%
);
position: absolute;
top: 0;
left: 0;
z-index: 10;
opacity: 0;
background: rgba(255, 255, 255, 0.08);
box-shadow:
0px 0px 0px 0px rgba(0, 0, 0, 0.06),
-2px 4px 9px 0px rgba(0, 0, 0, 0.06),
-8px 15px 17px 0px rgba(0, 0, 0, 0.05),
-19px 34px 23px 0px rgba(0, 0, 0, 0.03),
-33px 60px 27px 0px rgba(0, 0, 0, 0.01),
-52px 94px 30px 0px rgba(0, 0, 0, 0);
backdrop-filter: blur(20px);
}
</style>

View File

@@ -1,218 +0,0 @@
<script>
import { objectKeys } from '$lib/utils/object';
import { infos } from './Products.svelte';
</script>
<div class="outside">
<div class="wrapper">
<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
</h2>
<p class="text-description mt-4">
Build secure and scalable applications with less code. Add authentication, databases,
storage, and more using Appwrite's development platform.
</p>
<div class="infos">
{#each objectKeys(infos) as prod, i}
{@const info = infos[prod]}
{@const isLast = i === objectKeys(infos).length - 1}
{#if info}
<div class="info">
<h3>
<img src={info.icon.active} alt="" />
<span class="text-label text-primary">{info.title}</span>
</h3>
<h4 class="text-title font-aeonik-pro">{info.subtitle}</h4>
<p>
{info.description}
</p>
<ul class="features">
{#each info.features as feature}
<li>{feature}</li>
{/each}
</ul>
{#if info.shot}
<enhanced:img class="img" src={info.shot} alt="" />
{/if}
</div>
{#if !isLast}
<hr />
{/if}
{/if}
{/each}
</div>
<div class="post-wrapper">
<img src="/images/products/post.png" alt="" />
<h2>See your products grow</h2>
<p>
Keep track of your projects progress on the Appwrite Console and see them grow into
products users love and use every day.
</p>
</div>
</div>
<div class="img-overlay"></div>
</div>
<style lang="scss">
.outside {
position: relative;
overflow: hidden;
display: none;
.img-overlay {
content: '';
background: linear-gradient(to bottom, transparent 0%, black 40%);
position: absolute;
bottom: 0;
width: 100vw;
height: 30rem;
z-index: 10;
}
}
@media (max-width: 1399px) {
.outside {
display: block;
}
}
.wrapper {
--padding-inline: 1.25rem;
padding-block-start: 5rem;
padding-inline: var(--padding-inline);
max-width: 600px;
margin-inline: auto;
}
.infos {
margin-block-start: 3rem;
display: flex;
flex-direction: column;
gap: 3rem;
.info {
h3 {
display: flex;
align-items: center;
gap: 0.75rem;
}
h4 {
color: hsl(var(--web-color-primary));
margin-block-start: 0.75rem;
}
p {
margin-block-start: 1rem;
}
.features {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-block-start: 2rem;
li {
--marker-size: 1.25rem;
--margin-left: calc(var(--marker-size) + 0.75rem);
position: relative;
margin-inline-start: var(--margin-left);
&::before {
content: '';
position: absolute;
left: calc(var(--margin-left) * -1);
top: 50%;
width: var(--marker-size);
height: var(--marker-size);
transform: translateY(-50%);
background: url('/images/icons/colored/check.svg') no-repeat;
}
}
}
.img {
inline-size: 100%;
block-size: auto;
margin-block-start: 2.5rem;
}
}
hr {
border: 1px solid hsl(var(--web-color-smooth));
margin-inline: calc(var(--padding-inline) * -1);
}
}
.post-wrapper {
display: flex;
flex-direction: column;
align-items: center;
overflow: visible;
position: relative;
width: 100%;
/* overflow: hidden; */
padding-block-start: 35rem;
padding-block-end: 5rem;
img {
display: block;
max-block-size: unset;
max-inline-size: unset;
top: 5rem;
left: 50%;
transform: translateX(-50%);
width: 37.5rem;
position: absolute;
}
h2 {
color: var(--greyscale-50, #ededf0);
text-align: center;
/* Responsive/Display */
font-family: Aeonik Pro;
font-size: 48px;
font-style: normal;
font-weight: 400;
line-height: 50px; /* 104.167% */
letter-spacing: -0.48px;
max-width: 20rem;
position: relative;
z-index: 100;
}
p {
color: var(--greyscale-400, #97979b);
text-align: center;
font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 22px; /* 137.5% */
letter-spacing: -0.072px;
margin-block-start: 1rem;
max-width: 20rem;
z-index: 100;
}
}
</style>

View File

@@ -1,61 +0,0 @@
<script lang="ts">
import { createCheckbox, melt } from '@melt-ui/svelte';
export let checked = false;
const {
elements: { root },
states: { checked: localChecked },
helpers: { isChecked }
} = createCheckbox({
onCheckedChange({ next }) {
if (typeof next === 'boolean') {
checked = next;
}
return next;
}
});
$: localChecked.set(checked);
</script>
<div class="wrapper">
<button use:melt={$root} class="anim-checkbox">
{#if $isChecked}
<span class="web-icon-check"></span>
{/if}
</button>
</div>
<style lang="scss">
.wrapper {
display: grid;
place-items: center;
}
.anim-checkbox {
width: 1rem;
height: 1rem;
flex-shrink: 0;
border-radius: 0.125rem;
border: 1.5px solid var(--greyscale-500, #818186);
position: relative;
&:global(.anim-checkbox[data-state='checked']) {
background-color: #7c67fe;
border-color: #7c67fe;
}
}
[class*='icon-'] {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 1rem;
}
</style>

View File

@@ -1,64 +0,0 @@
<script lang="ts">
import { getInitials } from '$lib/animations';
import { fly } from 'svelte/transition';
import { authController } from '.';
import { flip } from '$lib/utils/flip';
const { state } = authController;
type AuthEntry = {
avatar: string;
name: string;
email: string;
id: number;
};
$: authData = [
$state.submitted
? {
avatar: getInitials($state.name),
name: $state.name,
email: $state.email,
id: 0
}
: undefined,
{
avatar: 'BD',
name: 'Benjamin Davis',
email: 'benjamin.davis@example.com',
id: 1
},
{
avatar: 'OS',
name: 'Olivia Smith',
email: 'olivia.smith@example.com',
id: 2
},
{
avatar: 'EW',
name: 'Ethan Wilson',
email: 'ethan.wilson@example.com',
id: 3
}
].filter(Boolean) as AuthEntry[];
</script>
<div class="pseudo-table">
<div class="header">
<span class="text-micro uppercase">Name</span>
<span class="text-micro uppercase">Identifier</span>
</div>
{#each authData as user (user.id)}
<div
class="row"
in:fly={{ duration: 100, x: -16, delay: 100 }}
out:fly={{ duration: 100, x: -16 }}
animate:flip={{ duration: 150 }}
>
<div class="flex items-center gap-3">
<div class="avatar is-size-small">{user.avatar}</div>
<span class="truncated">{user.name}</span>
</div>
<span class="truncated">{user.email}</span>
</div>
{/each}
</div>

View File

@@ -1,16 +0,0 @@
<script lang="ts">
import Code from '$lib/animations/CodeWindow/Code.svelte';
import { authController } from '.';
const { state } = authController;
$: content = `
const result = account.create(
ID.unique(),
'${$state.email}',
'${$state.password}',
"${$state.name}"
);`.trim();
</script>
<Code {content} />

View File

@@ -1,81 +0,0 @@
<script lang="ts">
import { Switch } from '$lib/components';
import { objectKeys } from '$lib/utils/object';
import { authController } from '.';
const { state } = authController;
const getIcon = (provider: string) => {
return `web-icon-${provider.toLowerCase()}`;
};
</script>
<div class="auth-controls">
{#each objectKeys($state.controls) as provider, i}
{@const isLast = i === objectKeys($state.controls).length - 1}
<div>
<span class={getIcon(provider)}></span>
<span>{provider}</span>
<Switch bind:checked={$state.controls[provider]} />
</div>
{#if !isLast}
<div class="sep"></div>
{/if}
{/each}
</div>
<style lang="scss">
.auth-controls {
display: flex;
flex-direction: column;
padding: 0.75rem;
width: 12.5rem;
> div {
display: flex;
align-items: center;
> :nth-child(2) {
margin-left: 0.75rem;
color: hsl(var(--web-color-white));
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 1.375rem; /* 157.143% */
letter-spacing: -0.00394rem;
}
> :global(:nth-child(3)) {
margin-left: auto;
}
}
.sep {
width: 100%;
height: 1px;
background-color: rgba(255, 255, 255, 0.12);
margin-block: 0.5rem;
}
[class*='icon-'] {
--size: 2rem;
font-size: var(--size);
width: var(--size);
height: var(--size);
color: hsl(var(--web-color-greayscale-50));
position: relative;
&::before {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
}
</style>

View File

@@ -1,95 +0,0 @@
import Box from './box.svelte';
import Code from './code.svelte';
import Controls from './controls.svelte';
import Phone from './phone.svelte';
export const Auth = {
Phone,
Box,
Code,
Controls
};
import { safeAnimate, sleep, write } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { getElSelector } from '../Products.svelte';
type State = {
email: string;
password: string;
name: string;
showControls: boolean;
submitted: boolean;
controls: {
GitHub: boolean;
Google: boolean;
Apple: boolean;
Microsoft: boolean;
};
};
const state = createResettable<State>({
email: '',
password: '',
name: "Walter O'Brien",
showControls: false,
submitted: false,
controls: {
GitHub: true,
Google: false,
Apple: false,
Microsoft: false
}
});
const emailToSet = 'walterobrian@example.com';
const passwordToSet = 'password';
const execute = async () => {
const phone = getElSelector('phone');
const box = getElSelector('box');
const code = getElSelector('code');
const controls = getElSelector('controls');
// Reset
const { update } = state.reset();
await Promise.all([
safeAnimate(box, { x: 310, y: 140, opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(code, { x: 200, y: 460, opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(phone, { x: 0, y: 0 }, { duration: 0.5 })?.finished,
safeAnimate(controls, { x: 420, y: 0, opacity: 0 }, { duration: 0.5 })?.finished
]);
// Start
await safeAnimate(box, { y: [48, 140], opacity: 1 }, { duration: 0.25, delay: 0.25 })?.finished;
await sleep(50);
await write(emailToSet, (v) => update((p) => ({ ...p, email: v })), 300);
await sleep(50);
await write(passwordToSet, (v) => update((p) => ({ ...p, password: v })), 300);
await sleep(50);
await safeAnimate(
code,
{ x: [200, 200], y: [460 + 16, 460], opacity: [0, 1] },
{ duration: 0.25 }
)?.finished;
await sleep(350);
update((p) => ({ ...p, submitted: true }));
await sleep(1000);
update((p) => ({ ...p, showControls: true }));
safeAnimate(controls, { x: [420, 420], y: [16, 0], opacity: 1 }, { duration: 0.5 });
};
export const authController = {
execute,
state
};

View File

@@ -1,220 +0,0 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { authController } from '.';
import { objectKeys } from '$lib/utils/object';
import { flip } from '$lib/utils/flip';
const { state } = authController;
$: controlsEnabled = $state.showControls && Object.values($state.controls).some(Boolean);
</script>
<div data-theme-ignore class="inner-phone light">
<p class="title">Create an Account</p>
<p class="subtitle">Please enter your details</p>
<div class="inputs">
<fieldset>
<label for="name">Your Name</label>
<input type="name" id="name" placeholder="Enter your name" bind:value={$state.name} />
</fieldset>
<fieldset>
<label for="email">Your Email</label>
<input
type="email"
id="email"
placeholder="Enter your email"
bind:value={$state.email}
/>
</fieldset>
<fieldset>
<label for="password">Create Password</label>
<input
type="password"
id="password"
placeholder="Enter Password"
bind:value={$state.password}
/>
</fieldset>
</div>
<button class="sign-up">Sign Up</button>
{#if controlsEnabled}
<span class="with-sep" transition:fade={{ duration: 100 }}>or sign up with</span>
<div class="oauth-btns" transition:fade={{ duration: 100 }}>
{#each objectKeys($state.controls).filter((p) => $state.controls[p]) as provider (provider)}
<button
class="oauth"
transition:fade={{ duration: 100 }}
animate:flip={{ duration: 250 }}
>
<div class="inner">
<span class="web-icon-{provider.toLowerCase()}"></span>
<span>{provider}</span>
</div>
</button>
{/each}
</div>
{/if}
</div>
<style lang="scss">
.inner-phone {
padding-block: 3rem;
padding-inline: 1rem;
color: rgba(67, 67, 71, 1);
text-align: left;
.title {
color: #434347;
font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 22px; /* 137.5% */
letter-spacing: -0.224px;
}
.subtitle {
color: var(--greyscale-700, var(--color-greyscale-700, #56565c));
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.196px;
}
.inputs {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-block-start: 1.5rem;
fieldset {
display: flex;
flex-direction: column;
gap: 0.3125rem;
width: 100%;
label {
color: var(--color-greyscale-700, #56565c);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 133.333% */
letter-spacing: -0.168px;
}
input {
all: unset;
display: flex;
padding: 8px 12px;
align-items: flex-start;
align-self: stretch;
border-radius: 8px;
border: 1px solid #d8d8db;
color: #434347;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 133.333% */
letter-spacing: -0.168px;
}
}
}
.sign-up {
padding: 0.375rem 0.75rem;
text-align: center;
width: 100%;
margin-block-start: 1.25rem;
border-radius: 0.5rem;
background: var(--appwrite-purple, #7c67fe);
box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.06);
color: var(--color-bw-white, #fff);
text-align: center;
/* Responsive/SubBody-500 */
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
.with-sep {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem; /* 166.667% */
letter-spacing: -0.0105rem;
color: hsl(var(--web-color-greyscale-500));
margin-block-start: 0.75rem;
&::before,
&::after {
content: '';
height: 1px;
flex-grow: 1;
background-color: hsl(var(--web-color-greyscale-200));
}
}
.oauth-btns {
--gap: 0.5rem;
display: flex;
flex-wrap: wrap;
gap: var(--gap);
margin-block-start: 0.75rem;
}
.oauth {
display: flex;
justify-content: center;
align-items: center;
border-radius: 0.5rem;
border: 1px solid #d9d9d9;
color: hsl(var(--web-color-greyscale-750));
text-align: center;
/* Responsive/Caption-500 */
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 1.375rem; /* 157.143% */
letter-spacing: -0.01575rem;
flex: 1 1 calc(50% - var(--gap));
padding-block: 0.375rem;
position: relative;
height: 2.125rem;
.inner {
position: absolute;
left: 50%;
top: 50%;
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
transform: translate(-50%, -50%) scale(var(--inverse-sx, 1), var(--inverse-sy, 1));
}
}
}
</style>

View File

@@ -1,54 +0,0 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import { databasesController } from '.';
import { flip } from '$lib/utils/flip';
const { state } = databasesController;
</script>
<div class="pseudo-table">
<div class="header">
<span class="text-micro uppercase">Document ID</span>
<span class="text-micro uppercase">Task</span>
</div>
{#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>
<span>{task.id}</span>
</div>
<span class="truncated">{task.title}</span>
</div>
{/each}
</div>
<style lang="scss">
.copy-button {
display: flex;
padding: 0.25rem 0.5rem;
align-items: center;
gap: 0.375rem;
border-radius: 62.4375rem;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(2.6666667461395264px);
[class*='icon-'] {
font-size: 1.25rem;
color: hsl(var(--web-color-greyscale-600));
}
span:not([class*='icon-']) {
color: var(--greyscale-400, #adadb1);
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem; /* 142.857% */
overflow: hidden;
text-overflow: ellipsis;
}
}
</style>

View File

@@ -1,16 +0,0 @@
<script lang="ts">
import Code from '$lib/animations/CodeWindow/Code.svelte';
let content = `
const result = databases.createDocument(
'Your-tasks',
tasks,
ID.unique(),
{
'description': 'Research user needs',
'tags': ['UX', 'design'],
}
);`.trim();
</script>
<Code {content} />

View File

@@ -1,94 +0,0 @@
import Box from './box.svelte';
import Code from './code.svelte';
import Phone from './phone.svelte';
import { safeAnimate, sleep } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { getElSelector } from '../Products.svelte';
type Task = {
id: string;
title: string;
checked: boolean;
};
type State = {
tasks: Task[];
tableSlice: number;
};
const state = createResettable<State>({
tasks: [
{
id: '3397fecdedb13397fecdedb1',
title: 'Research user needs',
checked: true
}
],
tableSlice: 1
});
const execute = async () => {
const phone = getElSelector('phone');
const box = getElSelector('box');
const code = getElSelector('code');
const { update } = state.reset();
await Promise.all([
safeAnimate(phone, { x: 390, y: 0 }, { duration: 0.5 })?.finished,
safeAnimate(box, { x: 0, y: 32, opacity: 1 }, { duration: 0.5 })?.finished,
safeAnimate(code, { x: 80, y: 320, opacity: 1 }, { duration: 0.5 })?.finished
]);
await sleep(250);
update((p) => ({
...p,
tasks: [
...p.tasks,
{
id: '3397fecdedb13397fecdedb2',
title: 'Create wireframes',
checked: false
}
]
}));
await sleep(250);
update((p) => ({
...p,
tableSlice: p.tableSlice + 1
}));
await sleep(250);
update((p) => ({
...p,
tasks: [
...p.tasks,
{
id: '3397fecdedb13397fecdedb3',
title: 'Create visual design',
checked: false
}
]
}));
await sleep(250);
update((p) => ({
...p,
tableSlice: p.tableSlice + 1
}));
};
export const databasesController = {
execute,
state
};
export const Databases = {
Phone,
Box,
Code
};

View File

@@ -1,127 +0,0 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { databasesController } from '.';
import TaskCheckbox from '../TaskCheckbox.svelte';
const { state } = databasesController;
</script>
<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>
</div>
<div class="date">Today</div>
<div class="tasks">
{#each $state.tasks as task (task.id)}
<div class="task" data-checked={task.checked ? '' : undefined} in:fly={{ x: -16 }}>
<TaskCheckbox bind:checked={task.checked} />
<span class="title">{task.title}</span>
</div>
{/each}
</div>
<div class="add-btn">
<span class="web-icon-plus"></span>
</div>
</div>
<style lang="scss">
.inner-phone {
padding-block: 3rem;
padding-inline: 1rem;
color: rgba(67, 67, 71, 1);
text-align: left;
position: relative;
height: 100%;
.header {
display: flex;
justify-content: space-between;
align-items: center;
.title {
color: var(--color-greyscale-800, #2d2d31);
font-family: Inter;
font-size: 1rem;
font-style: normal;
font-weight: 600;
line-height: 1.375rem; /* 137.5% */
letter-spacing: -0.014rem;
}
[class*='icon-'] {
font-size: 1.25rem;
color: hsl(var(--web-color-greyscale-500));
}
}
.date {
margin-block-start: 3rem;
color: hsl(var(--web-color-greyscale-600));
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 500;
line-height: 1.25rem; /* 166.667% */
}
.tasks {
margin-block-start: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
.task {
display: flex;
align-items: center;
gap: 0.75rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--web-color-greyscale-50));
background: hsl(var(--web-color-white));
color: var(--greyscale-700, var(--color-greyscale-700, #56565c));
padding-block: 0.55rem;
padding-inline: 0.88rem;
/* Responsive/SubBody-400 */
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.375rem; /* 157.143% */
letter-spacing: -0.00394rem;
transition: opacity 200ms ease;
&[data-checked] {
opacity: 0.6;
}
}
}
.add-btn {
position: absolute;
right: 1rem;
bottom: 2.5rem;
width: 2.5rem;
height: 2.5rem;
flex-shrink: 0;
box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.12);
background-color: rgba(124, 103, 254, 1);
color: rgba(237, 237, 240, 1);
font-size: 1.5rem;
display: grid;
place-items: center;
border-radius: 100%;
}
}
</style>

View File

@@ -1,56 +0,0 @@
<script lang="ts">
import { portal } from '$lib/actions';
import Code from '$lib/animations/CodeWindow/Code.svelte';
import { fade } from 'svelte/transition';
import { functionsController } from '.';
let content = `
const userId = req.headers['user-id'];
if (req.path === '/subscribe') {
const session = await stripe.checkout(userId);
return res.redirect(session.url, 303);
}
if (req.path === '/webhook') {
await appwrite.addSubscriberLabel(userId);
}
return res.json({ success: true });`.trim();
const { state } = functionsController;
</script>
<Code {content} />
<div use:portal={{ target: '#code-bottom' }} class="bottom">
{#if $state.submit !== 'idle'}
<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>
{:else if $state.submit === 'success'}
<span>Deployed to Appwrite Cloud</span>
<span class="web-icon-check"></span>
{/if}
</div>
<style lang="scss">
.bottom {
display: flex;
align-items: center;
gap: 0.5rem;
height: 3rem;
padding-inline: 1rem;
color: var(--color-bw-white, #fff);
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 150%; /* 1.3125rem */
letter-spacing: -0.00875rem;
}
</style>

View File

@@ -1,58 +0,0 @@
import Code from './code.svelte';
import Phone from './phone.svelte';
import { safeAnimate, sleep } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { getElSelector } from '../Products.svelte';
type State = {
submit: 'idle' | 'loading' | 'success';
};
const state = createResettable<State>({
submit: 'idle'
});
const execute = async () => {
const phone = getElSelector('phone');
const box = getElSelector('box');
const code = getElSelector('code');
const { update } = state.reset();
await Promise.all([
safeAnimate(phone, { x: 430, y: 0, width: '275px' }, { duration: 0.5 })?.finished,
safeAnimate(code, { x: 0, y: 200, opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(box, { opacity: 0 }, { duration: 0.5 })?.finished
]);
await sleep(250);
await safeAnimate(code, { zIndex: 0 }, { duration: 0 })?.finished;
await safeAnimate(code, { y: [200 - 16, 200], opacity: 1 }, { duration: 0.5 })?.finished;
await sleep(250);
update((p) => ({
...p,
submit: 'loading'
}));
await sleep(1500);
update((p) => ({
...p,
submit: 'success'
}));
};
export const functionsController = {
execute,
state
};
export const Functions = {
Phone,
Code
};

View File

@@ -1,325 +0,0 @@
<script lang="ts">
import { flip } from '$lib/utils/flip';
import { scale, slide } from 'svelte/transition';
import { functionsController } from '.';
const { state } = functionsController;
type Method = {
icon: string;
label: string;
};
$: methods = [
$state.submit === 'success' && {
icon: '/images/animations/stripe.png',
label: 'Stripe'
},
{
icon: '/images/animations/credit-card.svg',
label: 'Card'
},
{
icon: '/images/animations/paypal.svg',
label: 'PayPal'
},
{
icon: '/images/animations/apple.svg',
label: 'Apple'
}
].filter(Boolean) as Method[];
</script>
<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>
</div>
<div class="plan">
<p class="title">Premium plan</p>
<div class="subscription">
<p class="price">$20</p>
<p class="period">/month</p>
</div>
<ul>
<li>Premium plan</li>
<li>Premium plan</li>
<li>Premium plan</li>
</ul>
</div>
<ul class="methods">
{#each methods as method, i (method.label)}
<li
in:scale={{ delay: 150 }}
animate:flip={{ duration: 500 }}
data-active={i == 0 ? '' : undefined}
>
<img src={method.icon} alt="" />
<p>{method.label}</p>
</li>
{/each}
</ul>
{#if $state.submit !== 'success'}
<div class="form">
<p>Card information</p>
<div class="bordered">
<div>
<p>placeholder</p>
<img src="/images/animations/visa.png" alt="" />
<img src="/images/animations/mastercard.png" alt="" />
</div>
<div>
<p>MM/YY</p>
<p>CVV</p>
</div>
</div>
</div>
{/if}
<button>
Pay $20.00
{#if $state.submit === 'success'}
<span in:slide={{ axis: 'x' }}>on Stripe</span>
{/if}
</button>
</div>
<style lang="scss">
.inner-phone {
padding-block: 3rem 1.5rem;
padding-inline: 1rem;
color: rgba(67, 67, 71, 1);
text-align: left;
position: relative;
height: 100%;
overflow: visible;
display: flex;
flex-direction: column;
.header {
display: flex;
justify-content: space-between;
align-items: center;
.title {
color: #434347;
font-family: Inter;
font-size: 1rem;
font-style: normal;
font-weight: 600;
line-height: 1.375rem; /* 137.5% */
letter-spacing: -0.014rem;
}
[class*='icon-'] {
font-size: 1.25rem;
color: hsl(var(--web-color-greyscale-500));
}
}
.plan {
display: flex;
padding: 0.75rem 1rem;
flex-direction: column;
justify-content: center;
align-items: flex-start;
border-radius: 0.75rem;
background: rgba(237, 237, 240, 0.5);
margin-block-start: 1rem;
.title {
color: var(--color-greyscale-700, #56565c);
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 700;
line-height: 1.25rem; /* 166.667% */
}
.subscription {
display: flex;
align-items: baseline;
margin-block-start: 0.15rem;
.price {
color: var(--greyscale-700, var(--color-greyscale-700, #56565c));
font-family: Inter;
font-size: 1.125rem;
font-style: normal;
font-weight: 700;
line-height: 1.25rem; /* 111.111% */
}
.period {
color: var(--greyscale-700, var(--color-greyscale-700, #56565c));
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem;
}
}
ul {
display: flex;
flex-direction: column;
gap: 0.125rem;
margin-block-start: 0.75rem;
li {
color: var(--color-greyscale-500, #818186);
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem; /* 166.667% */
padding-inline-start: 1.5rem;
position: relative;
&::before {
content: '';
display: block;
position: absolute;
left: 0;
width: 1rem;
height: 1rem;
top: 50%;
transform: translateY(-50%);
background-image: url('/images/animations/check-circle.svg');
}
}
}
}
.methods {
margin-block-start: 1.25rem;
display: flex;
gap: 0.75rem;
overflow: hidden;
margin-inline: -1rem;
padding-inline: 1rem;
li {
flex-shrink: 0;
display: flex;
width: 5.5rem;
padding: 0.75rem 0.75rem 0.625rem 0.75rem;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 0.125rem;
border-radius: 0.75rem;
border: 1px solid var(--greyscale-50, #ededf0);
color: var(--greyscale-700, var(--color-greyscale-700, #56565c));
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem; /* 166.667% */
&[data-active] {
border-color: var(--appwrite-purple, #7c67fe);
}
}
}
.form {
margin-block-start: 1.25rem;
> p {
color: var(--dark-neutrals-150, #373b4d);
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 150%; /* 1.125rem */
}
.bordered {
margin-block-start: 0.25rem;
border-radius: 0.5rem;
border: 1px solid #ededf0;
background: var(--color-bw-white, #fff);
> div:first-child {
display: flex;
gap: 0.25rem;
padding: 0.65rem 0.75rem;
border-bottom: 1px solid #ededf0;
> :nth-child(2) {
margin-inline-start: auto;
}
}
> div:nth-child(2) {
display: flex;
> p {
padding: 0.65rem 0.75rem;
}
> p:first-child {
flex-grow: 3;
}
> p:last-child {
flex-grow: 1;
border-inline-start: 1px solid #ededf0;
}
}
p {
color: var(--light-neutrals-50, #c4c6d7);
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 150%; /* 1.125rem */
}
}
}
> button {
display: flex;
text-align: center;
padding: 0.5rem 1rem;
justify-content: center;
align-items: center;
gap: 0.25rem;
border-radius: 0.5rem;
background: var(--appwrite-purple, #7c67fe);
box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.06);
color: var(--color-bw-white, #fff);
text-align: center;
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 1.375rem; /* 157.143% */
letter-spacing: -0.01225rem;
margin-block-start: auto;
white-space: nowrap;
}
}
</style>

View File

@@ -1,116 +0,0 @@
<script lang="ts">
import { fade, slide } from 'svelte/transition';
import { messagingController } from '.';
import { flip } from '$lib/utils/flip';
const { state } = messagingController;
</script>
<div class="pseudo-table">
<div class="header">
<span class="text-micro uppercase">Message ID</span>
<span class="text-micro uppercase">Type</span>
<span class="text-micro uppercase" style:text-align="center">Status</span>
</div>
{#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>
<span>{task.id}</span>
</div>
<div class="icon-button">
<div class="icon">
<img src={task.icon} alt="" width="16" height="16" />
</div>
<span class="truncated">{task.type}</span>
</div>
<div class="status-indicator">
{#if task.status === 'sending'}
<div class="loader is-small" in:fade></div>
{:else}
<span class="web-icon-check"></span>
{/if}
</div>
</div>
{/each}
</div>
<style lang="scss">
.header,
.row {
grid-template-columns: 7rem 1fr 1fr !important;
gap: 1.5rem 3rem;
}
.copy-button {
display: flex;
padding: 0.25rem 0.5rem;
align-items: center;
justify-content: space-between;
gap: 0.375rem;
border-radius: 62.4375rem;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(2.6666667461395264px);
[class*='icon-'] {
font-size: 1.25rem;
color: hsl(var(--web-color-greyscale-600));
}
span:not([class*='icon-']) {
color: var(--greyscale-400, #adadb1);
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem; /* 142.857% */
overflow: hidden;
text-overflow: ellipsis;
}
}
.icon-button {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
.icon {
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(2.6666667461395264px);
border-radius: 100%;
height: 2rem;
width: 2rem;
display: flex;
align-items: center;
justify-content: center;
}
span:not([class*='icon-']) {
color: var(--greyscale-400, #adadb1);
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem; /* 142.857% */
overflow: hidden;
text-overflow: ellipsis;
}
}
.status-indicator {
display: flex;
align-items: center;
justify-content: center;
[class*='icon-'] {
font-size: 1.25rem;
color: hsl(var(--web-color-greyscale-600));
}
}
</style>

View File

@@ -1,15 +0,0 @@
<script lang="ts">
import { messagingController } from '.';
import Code from '$lib/animations/CodeWindow/Code.svelte';
const { state } = messagingController;
$: content = `
await messaging.createPush(
ID.unique(),
'${$state.heading}',
'${$state.message}',
);`.trim();
</script>
<Code {content} />

View File

@@ -1,164 +0,0 @@
import Box from './box.svelte';
import Code from './code.svelte';
import Phone from './phone.svelte';
import { safeAnimate, sleep, write } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { getElSelector } from '../Products.svelte';
type Task = {
id: string;
title: string;
checked: boolean;
};
type Message = {
id: string;
type: string;
icon: string;
status: 'sending' | 'sent';
};
type State = {
heading: string;
message: string;
tasks: Task[];
messages: Message[];
tableSlice: number;
submit: 'loading' | 'success';
};
const state = createResettable<State>({
heading: '',
message: '',
tasks: [
{
id: '3397fecdedb13397fecdedb1',
title: 'Research user needs',
checked: true
}
],
messages: [
{
id: '...3397fecdedb1',
type: 'SMS',
icon: './images/icons/illustrated/dark/sms.svg',
status: 'sent'
},
{
id: '...2224gabjger4',
type: 'Email',
icon: './images/icons/illustrated/dark/email.svg',
status: 'sent'
}
],
tableSlice: 2,
submit: 'loading'
});
const execute = async () => {
const phone = getElSelector('phone');
const box = getElSelector('box');
const code = getElSelector('code');
const { update } = state.reset();
await Promise.all([
safeAnimate(phone, { x: 365, y: 0, width: '275px' }, { duration: 0.5 })?.finished,
safeAnimate(code, { x: 80, y: 325, opacity: 0, zIndex: 100 }, { duration: 0.5 })?.finished,
safeAnimate(box, { x: 0, y: 32, opacity: 1 }, { duration: 0.5, delay: 1 })?.finished
]);
await sleep(250);
update((p) => ({
...p,
tasks: [
...p.tasks,
{
id: '3397fecdedb13397fecdedb2',
title: 'Create wireframes',
checked: false
}
]
}));
await sleep(250);
update((p) => ({
...p,
tableSlice: p.tableSlice + 1
}));
await sleep(250);
update((p) => ({
...p,
tasks: [
...p.tasks,
{
id: '3397fecdedb13397fecdedb3',
title: 'Create visual design',
checked: false
}
]
}));
await sleep(250);
update((p) => ({
...p,
tableSlice: p.tableSlice + 1
}));
await sleep(250);
safeAnimate(code, { opacity: 1 }, { duration: 0.5 })?.finished, await sleep(250);
await write(
'New task assigned to you',
(v) => {
state.update((n) => ({ ...n, heading: v }));
},
300
);
await write(
'You were assigned a new task in your board. Tap to check it out.',
(v) => {
state.update((n) => ({ ...n, message: v }));
},
300
);
await sleep(250);
update((p) => ({
...p,
messages: [
...p.messages,
{
id: '...5689fdoerre2',
type: 'Push',
icon: './images/icons/illustrated/dark/push.svg',
status: 'sending'
}
]
}));
await sleep(1250);
update((p) => ({
...p,
submit: 'success',
messages: p.messages.map((m) => (m.id === '...5689fdoerre2' ? { ...m, status: 'sent' } : m))
}));
};
export const messagingController = {
execute,
state
};
export const Messaging = {
Phone,
Box,
Code
};

View File

@@ -1,205 +0,0 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { messagingController } from '.';
import TaskCheckbox from '../TaskCheckbox.svelte';
const { state } = messagingController;
</script>
{#if $state.submit === 'success'}
<div class="push-notification" in:fly={{ y: -20 }}>
<div class="icon"></div>
<div class="content">
<div class="header">
<h3 class="title">New task assigned to you</h3>
<span class="time">now</span>
</div>
<p class="message">You were assigned a new task in your board. Tap to check it out.</p>
</div>
</div>
{/if}
<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>
</div>
<div class="date">Today</div>
<div class="tasks">
{#each $state.tasks as task (task.id)}
<div class="task" data-checked={task.checked ? '' : undefined} in:fly={{ x: -16 }}>
<TaskCheckbox bind:checked={task.checked} />
<span class="title">{task.title}</span>
</div>
{/each}
</div>
<div class="add-btn">
<span class="web-icon-plus"></span>
</div>
</div>
<style lang="scss">
.push-notification {
position: absolute;
display: flex;
justify-content: space-between;
align-items: center;
top: 20px;
padding: 0.5rem;
margin: 0 auto;
width: 125%;
height: 60px;
gap: 0.75rem;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
border-radius: 20px;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(8px);
box-shadow: 3px -8px 32px 0px rgba(0, 0, 0, 0.24);
.icon {
height: 38px;
width: 38px;
flex-shrink: 0;
border-radius: 10px;
background-image: linear-gradient(180deg, #7c67fe, #4a3e98);
}
.header {
display: flex;
justify-content: space-between;
.title {
color: var(--color-greyscale-800, #2d2d31);
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 600;
line-height: 1rem; /* 137.5% */
letter-spacing: -0.014rem;
}
.time {
color: var(--color-greyscale-700, #56565c);
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 1rem; /* 137.5% */
letter-spacing: -0.014rem;
}
}
.message {
color: var(--color-greyscale-700, #56565c);
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 1rem; /* 137.5% */
letter-spacing: -0.014rem;
}
}
.inner-phone {
padding-block: 3rem 1.5rem;
padding-inline: 1rem;
color: rgba(67, 67, 71, 1);
text-align: left;
position: relative;
height: 100%;
overflow: visible;
display: flex;
flex-direction: column;
.header {
display: flex;
justify-content: space-between;
align-items: center;
.title {
color: var(--color-greyscale-800, #2d2d31);
font-family: Inter;
font-size: 1rem;
font-style: normal;
font-weight: 600;
line-height: 1.375rem; /* 137.5% */
letter-spacing: -0.014rem;
}
[class*='icon-'] {
font-size: 1.25rem;
color: hsl(var(--web-color-greyscale-500));
}
}
.date {
margin-block-start: 3rem;
color: hsl(var(--web-color-greyscale-600));
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 500;
line-height: 1.25rem; /* 166.667% */
}
.tasks {
margin-block-start: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
.task {
display: flex;
align-items: center;
gap: 0.75rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--web-color-greyscale-50));
background: hsl(var(--web-color-white));
color: var(--greyscale-700, var(--color-greyscale-700, #56565c));
padding-block: 0.55rem;
padding-inline: 0.88rem;
/* Responsive/SubBody-400 */
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.375rem; /* 157.143% */
letter-spacing: -0.00394rem;
transition: opacity 200ms ease;
&[data-checked] {
opacity: 0.6;
}
}
}
.add-btn {
position: absolute;
right: 1rem;
bottom: 2.5rem;
width: 2.5rem;
height: 2.5rem;
flex-shrink: 0;
box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.12);
background-color: rgba(124, 103, 254, 1);
color: rgba(237, 237, 240, 1);
font-size: 1.5rem;
display: grid;
place-items: center;
border-radius: 100%;
}
}
</style>

View File

@@ -1,53 +0,0 @@
import { safeAnimate } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { animate } from 'motion-legacy';
import { getElSelector } from '../Products.svelte';
const requests = createResettable(0);
const databases = createResettable(0);
const authentication = createResettable(0);
const storage = createResettable(0);
const bandwidth = createResettable(0);
const executions = createResettable(0);
const realtime = createResettable(0);
const execute = async () => {
const phone = getElSelector('phone');
const pd = getElSelector('pd');
const graphBox = getElSelector('graph-box');
const boxesAndStates = [
{ box: getElSelector('post-auth'), state: authentication.reset() },
{ box: getElSelector('post-storage'), state: storage.reset() },
{ box: getElSelector('post-bandwidth'), state: bandwidth.reset() },
{ box: getElSelector('post-functions'), state: executions.reset() },
{ box: getElSelector('post-databases'), state: databases.reset() },
{ box: getElSelector('post-realtime'), state: realtime.reset() },
{ box: getElSelector('post-requests'), state: requests.reset() }
];
await Promise.all([
safeAnimate(pd, { opacity: 0, y: -16 }, { duration: 0.5 })?.finished,
safeAnimate(graphBox, { opacity: 0, visibility: 'hidden' }, { duration: 0.5 })?.finished,
safeAnimate(phone, { x: '-50%', width: '660px' }, { duration: 1, delay: 0.5 })?.finished
]);
boxesAndStates.forEach(({ box, state }, i) => {
safeAnimate(box, { opacity: 1, y: [1200, 0] }, { duration: 0.5, delay: i * 0.1 })?.finished;
animate(state.set, { duration: 2, delay: (i + 1) * 0.25 });
});
};
export const postController = {
execute,
state: {
requests,
databases,
authentication,
storage,
bandwidth,
executions,
realtime
}
};

View File

@@ -1,308 +0,0 @@
<script lang="ts">
import { toScale } from '$lib/utils/toScale';
import { postController } from '.';
import { elId } from '../Products.svelte';
const {
state: { authentication, bandwidth, databases, executions, requests, storage, realtime }
} = postController;
const formatK = (num: number) => {
if (num > 999) {
return `${(num / 1000).toFixed(1)}K`;
}
return Math.floor(num);
};
</script>
<div class="gradient-box auth" id="post-auth-{$elId}">
<div class="flex items-center gap-2">
<p class="icon-user-group"></p>
<p class="f-eyebrow">Authentication</p>
</div>
<p class="f-display mbs-16">
{formatK(toScale($authentication, [0, 1], [0, 4000]))}
</p>
<div class="mbs-4 flex items-center justify-between">
<p class="f-sub">Users</p>
<p class="f-idk">Sessions: 20K</p>
</div>
</div>
<div class="gradient-box storage" id="post-storage-{$elId}">
<div class="flex items-center gap-2">
<p class="icon-folder"></p>
<p class="f-eyebrow">Storage</p>
</div>
<p class="f-display mbs-16">
{toScale($storage, [0, 1], [0, 8]).toFixed(1)}
<span class="f-tiny-display">GB</span>
</p>
<div class="mbs-4 flex items-center justify-between">
<p class="f-sub">Storage</p>
<p class="f-idk">Buckets: 44</p>
</div>
</div>
<div class="gradient-box bandwidth" id="post-bandwidth-{$elId}">
<p class="f-display">
{toScale($bandwidth, [0, 1], [0, 1.2]).toFixed(2)}
<span class="f-tiny-display">GB</span>
</p>
<p class="f-sub">Bandwidth</p>
<img class="mbs-16" src="./images/animations/bandwidth-graph.svg" alt="" />
</div>
<div class="gradient-box functions" id="post-functions-{$elId}">
<div class="flex items-center gap-2">
<p class="icon-lightning-bolt"></p>
<p class="f-eyebrow">Functions</p>
</div>
<p class="f-display mbs-16">
{toScale($executions, [0, 1], [0, 846]).toFixed(0)}
</p>
<div class="mbs-4 flex items-center justify-between">
<p class="f-sub">Executions</p>
</div>
</div>
<div class="gradient-box databases" id="post-databases-{$elId}">
<div class="flex items-center gap-2">
<p class="icon-database"></p>
<p class="f-eyebrow">Databases</p>
</div>
<p class="f-display mbs-16">
{toScale($databases, [0, 1], [0, 8]).toFixed(0)}
</p>
<div class="mbs-4 flex items-center justify-between">
<p class="f-sub">Databases</p>
<p class="f-idk">Documents: 20</p>
</div>
</div>
<div class="gradient-box requests" id="post-requests-{$elId}">
<p class="f-display">{formatK(toScale($requests, [0, 1], [0, 6849]))}</p>
<p class="f-sub">Requests</p>
<img class="mbs-16" src="./images/animations/requests-graph.svg" alt="" />
</div>
<div class="gradient-box realtime" id="post-realtime-{$elId}">
<p class="f-display">{formatK(toScale($realtime, [0, 1], [0, 100000]))}</p>
<p class="f-sub">Realtime connections</p>
<img class="mbs-16" src="./images/animations/realtime-graph.svg" alt="" />
</div>
<div class="gradient-overlay flex flex-col">
<h3>See your products grow</h3>
<p>
Keep track of your projects progress on the Appwrite Console and see them grow into products
users love and use every day.
</p>
</div>
<style lang="scss">
@use '$scss/abstract/mixins/border-gradient' as gradients;
// Utilities
.f-eyebrow {
color: #adadb0;
/* Eyebrow headings/level 3 */
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 500;
line-height: 150%; /* 1.125rem */
letter-spacing: 0.09rem;
text-transform: uppercase;
}
.f-display {
color: #ededf0;
font-family: Aeonik Pro;
font-size: 2rem;
font-style: normal;
font-weight: 500;
line-height: 2.25rem; /* 112.5% */
}
.f-tiny-display {
color: var(--greyscale-50, #ededf0);
text-align: center;
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 1rem; /* 114.286% */
}
.f-sub {
color: #97979b;
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.125rem; /* 128.571% */
}
.f-idk {
color: var(--primary, #e4e4e7);
text-align: right;
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 150%; /* 1.3125rem */
opacity: 40%;
}
.mbs-16 {
margin-block-start: 1rem;
}
.mbs-4 {
margin-block-start: 0.25rem;
}
.justify-between {
justify-content: space-between;
}
// Components
.gradient-box {
@include gradients.border-gradient;
--m-border-gradient-before: linear-gradient(
180deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 125.11%
);
--m-border-radius: 1rem;
position: absolute;
background: var(--card, rgba(35, 35, 37, 0.9));
box-shadow:
0px 0px 0px 0px rgba(0, 0, 0, 0.06),
-2px 4px 9px 0px rgba(0, 0, 0, 0.06),
-8px 15px 17px 0px rgba(0, 0, 0, 0.05),
-19px 34px 23px 0px rgba(0, 0, 0, 0.03),
-33px 60px 27px 0px rgba(0, 0, 0, 0.01),
-52px 94px 30px 0px rgba(0, 0, 0, 0);
backdrop-filter: blur(8px);
padding: 1.5rem;
z-index: 9999;
min-width: 20rem;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.gradient-overlay {
position: absolute;
z-index: 100;
bottom: -7.5rem;
width: 100%;
height: 30rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.25rem;
padding-block-start: 15rem;
opacity: 0;
animation: fadeIn 0.75s ease-in-out 1.5s forwards;
&::before {
content: '';
inset: 0;
position: absolute;
//background: #19191d; // old bg
//filter: blur(125px); // break Safari
background: #19191dcc;
filter: blur(67px);
}
h3 {
position: relative;
color: var(--primary, #e4e4e7);
text-align: center;
/* Desktop/Display */
font-family: Aeonik Pro;
font-size: 4rem;
font-style: normal;
font-weight: 400;
line-height: 4.25rem; /* 106.25% */
letter-spacing: -0.04rem;
}
p {
position: relative;
color: var(--secondary, #adadb0);
text-align: center;
/* Desktop/Description */
font-family: Inter;
font-size: 1.25rem;
font-style: normal;
font-weight: 500;
line-height: 1.75rem; /* 140% */
letter-spacing: -0.0175rem;
max-width: 40rem;
text-align: center;
}
}
// Specifics
.auth {
opacity: 0;
left: 4rem;
top: -11rem;
}
.storage {
opacity: 0;
left: -10rem;
top: -2rem;
}
.bandwidth {
opacity: 0;
left: -4rem;
top: 11rem;
}
.functions {
opacity: 0;
left: -6rem;
top: 35rem;
}
.databases {
opacity: 0;
top: -13rem;
right: 10rem;
}
.requests {
opacity: 0;
top: 17rem;
right: -18rem;
}
.realtime {
opacity: 0;
top: -1rem;
right: -7rem;
}
</style>

View File

@@ -1,238 +0,0 @@
import Phone from './phone.svelte';
import { safeAnimate, sleep } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { getElSelector } from '../Products.svelte';
import { animate } from 'motion-legacy';
type Task = {
title: string;
tags: string[];
images?: string[];
};
type User = {
name: string;
color: string;
};
type State = {
tasks: {
todo: Task[];
doing: Task[];
done: Task[];
};
users: User[];
};
const state = createResettable<State>({
tasks: {
todo: [
{
title: 'Edit images for website',
tags: ['design', 'content'],
images: ['./images/animations/storage-2.png', './images/animations/storage-3.png']
}
],
doing: [
{
title: 'Handoff meet',
tags: ['design', 'dev']
}
],
done: []
},
users: []
});
export const connectionsProg = createResettable(0);
const addUser = (update: typeof state.update, user: User) => {
update((p) => ({
...p,
users: [...p.users, user]
}));
};
const addTask = (update: typeof state.update, group: keyof State['tasks'], task: Task) => {
update((p) => ({
...p,
tasks: {
...p.tasks,
[group]: [task, ...p.tasks[group]]
}
}));
};
const execute = async () => {
const phone = getElSelector('phone');
const code = getElSelector('code');
const box = getElSelector('box');
const walter = getElSelector('user-Walter');
const aditya = getElSelector('user-Aditya');
const sara = getElSelector('user-Sara');
const addTodo = getElSelector('add-todo');
const addDoing = getElSelector('add-doing');
const addDone = getElSelector('add-done');
const graphBox = getElSelector('graph-box');
const pd = getElSelector('pd');
const { update } = state.reset();
const { set: setConn } = connectionsProg.reset();
await Promise.all([
safeAnimate(box, { opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(phone, { x: 0, y: 0, width: '660px' }, { duration: 0.5 })?.finished,
safeAnimate(code, { opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(graphBox, { opacity: 0, x: 0, y: 0, visibility: 'visible' }, { duration: 0 })
?.finished,
safeAnimate(pd, { opacity: 1, y: 0 }, { duration: 0.5 })?.finished
]);
// Graphbox
sleep(1250).then(async () => {
await safeAnimate(graphBox, { opacity: 1 }, { duration: 0.5 })?.finished;
animate(
(y) => {
setConn(y);
},
{ duration: 2.5, easing: 'ease-in' }
);
});
// Walter
sleep(500).then(async () => {
addUser(update, { name: 'Walter', color: '#fd366e' });
await sleep(500);
await safeAnimate(walter, { x: -200, y: -100, scale: 1 }, { duration: 0.5 })?.finished;
await Promise.all([
safeAnimate(walter, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished,
safeAnimate(addTodo, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished
]);
addTask(update, 'todo', {
title: 'Handoff meet',
tags: ['design', 'dev']
});
await safeAnimate(walter, { scale: 1, x: -180, y: -160 }, { duration: 0.75, delay: 0.5 })
?.finished;
await sleep(500);
await safeAnimate(walter, { x: 210, y: -100, scale: 1 }, { duration: 0.5 })?.finished;
await Promise.all([
safeAnimate(walter, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished,
safeAnimate(addDone, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished
]);
addTask(update, 'done', {
title: 'Create migrations script',
tags: ['Dev']
});
safeAnimate(walter, { scale: 1, x: 230, y: -20 }, { duration: 0.75, delay: 0.5 });
await sleep(750);
await safeAnimate(walter, { x: -10, y: -100, scale: 1 }, { duration: 0.5 })?.finished;
await Promise.all([
safeAnimate(walter, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished,
safeAnimate(addDoing, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished
]);
addTask(update, 'doing', {
title: 'Configure blog SEO',
tags: ['dev', 'content']
});
await safeAnimate(walter, { scale: 1, x: -70, y: 80 }, { duration: 0.75, delay: 0.25 });
});
// Aditya
sleep(1500).then(async () => {
addUser(update, { name: 'Aditya', color: 'rgba(124, 103, 254, 1)' });
await sleep(500);
await safeAnimate(aditya, { x: 200, y: -100, scale: 1 }, { duration: 0.5 })?.finished;
await Promise.all([
safeAnimate(aditya, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished,
safeAnimate(addDone, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished
]);
addTask(update, 'done', {
title: 'Write up briefing',
tags: ['dev-rel']
});
await safeAnimate(aditya, { scale: 1, x: 180, y: 60 }, { duration: 0.75, delay: 0.5 })
?.finished;
await sleep(750);
await safeAnimate(aditya, { x: -210, y: -100, scale: 1 }, { duration: 0.5 })?.finished;
await Promise.all([
safeAnimate(aditya, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished,
safeAnimate(addTodo, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished
]);
addTask(update, 'todo', {
title: 'Review branding blog post',
tags: ['dev-rel']
});
await safeAnimate(aditya, { scale: 1, x: 70, y: -220 }, { duration: 0.75, delay: 0.5 })
?.finished;
});
// Sara
sleep(2500).then(async () => {
addUser(update, { name: 'Sara', color: 'rgba(103, 163, 254, 1)' });
await sleep(500);
await safeAnimate(sara, { x: 0, y: -100, scale: 1 }, { duration: 0.5 })?.finished;
await Promise.all([
safeAnimate(sara, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished,
safeAnimate(addDoing, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished
]);
addTask(update, 'doing', {
title: 'Prepare design system presentation',
tags: ['design']
});
await safeAnimate(sara, { scale: 1, y: 60, x: -50 }, { duration: 0.75, delay: 0.5 })
?.finished;
await sleep(250);
await safeAnimate(sara, { x: 200, y: -100, scale: 1 }, { duration: 0.5 })?.finished;
await Promise.all([
safeAnimate(sara, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished,
safeAnimate(addDone, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished
]);
addTask(update, 'done', {
title: 'QA branding animations',
tags: ['Dev']
});
await safeAnimate(sara, { scale: 1, x: 180, y: 60 }, { duration: 0.75, delay: 0.5 })
?.finished;
});
};
export const realtimeController = {
execute,
state
};
export const Realtime = {
Phone
};

View File

@@ -1,657 +0,0 @@
<script lang="ts">
import { objectKeys } from '$lib/utils/object';
import { scale } from 'svelte/transition';
import { connectionsProg, realtimeController } from '.';
import { elId } from '../Products.svelte';
import { flip } from '$lib/utils/flip';
import { toScale } from '$lib/utils/toScale';
const { state } = realtimeController;
const getInitial = (name: string) => name[0].toUpperCase();
$: connections = toScale($connectionsProg, [0, 1], [0, 10 ** 5]);
const lines = [9, 14, 44, 54, 50, 46, 52, 60, 66, 74, 86, 110];
$: progressedLines = (function getPL(): number[] {
const pl = lines.map(() => 0);
const total = lines.reduce((acc, curr) => acc + curr, 0);
const curr = total * $connectionsProg;
// Fill the lines until the current progress
let filled = 0;
for (let i = 0; i < pl.length; i++) {
const line = lines[i];
if (filled + line < curr) {
pl[i] = line;
filled += line;
} else {
pl[i] = curr - filled;
break;
}
}
return pl;
})();
const formatNumber = (num: number) => {
if (num < 1000) return Math.floor(num);
return `${Math.floor(num / 1000)}k`;
};
</script>
<div class="wrapper">
<div data-theme-ignore class="inner-phone light">
<div class="header">
<div class="row">
<p class="title">My Team's tasks</p>
<div class="flow gap-8">
<div class="tgl-avatars">
{#each $state.users as user}
<div class="tgl-avatar" style:--color={user.color} in:scale>
{getInitial(user.name)}
</div>
{/each}
</div>
<div class="vertical-sep"></div>
<span class="icon-menu"></span>
</div>
</div>
<div class="row">
<div class="search">
<span class="web-icon-search"></span>
<span class="text"> Search </span>
</div>
<div class="flow gap-8">
<button class="btn">Filter</button>
<button class="btn">Sort</button>
</div>
</div>
</div>
<hr />
<div class="content">
{#each objectKeys($state.tasks) as col, i}
{@const tasks = $state.tasks[col]}
{@const isLast = i === objectKeys($state.tasks).length - 1}
<div class="column">
<div class="title">
<span class="text capitalize">{col}</span>
<span class="tgl-inline-tag">{tasks.length}</span>
<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>
<span class="text">New Task</span>
</button>
{#each tasks as task (task.title)}
<div
class="task"
animate:flip={{ duration: 250 }}
in:scale={{ delay: 150 }}
>
{#if task.images}
<ul class="flow gap-8">
{#each task.images as image}
<img class="sq-32" src={image} alt="" />
{/each}
</ul>
{/if}
<p class="text">{task.title}</p>
<ul class="flow wrap gap-8">
{#each task.tags as tag}
<li class="tgl-tag">{tag}</li>
{/each}
</ul>
</div>
{/each}
</div>
</div>
{#if !isLast}
<div class="vertical-sep"></div>
{/if}
{/each}
</div>
{#each $state.users as user}
<div class="user" style:--color={user.color} id="user-{user.name}-{$elId}" in:scale>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<path
d="M2.58814 0.558469C1.60242 0.224627 1.10955 0.0577053 0.782928 0.173472C0.498743 0.274197 0.275173 0.497766 0.174449 0.781951C0.0586818 1.10858 0.225603 1.60144 0.559445 2.58716L4.67494 14.7388C5.13698 16.1031 5.368 16.7852 5.71194 16.9722C6.00951 17.134 6.36873 17.1341 6.66644 16.9726C7.01055 16.7859 7.24216 16.104 7.70539 14.7402L9.23555 10.235C9.32861 9.96103 9.37513 9.82404 9.45345 9.7101C9.52283 9.60918 9.61015 9.52185 9.71108 9.45248C9.82502 9.37416 9.96201 9.32763 10.236 9.23457L14.7411 7.70441C16.105 7.24118 16.7869 7.00957 16.9736 6.66547C17.1351 6.36776 17.1349 6.00853 16.9732 5.71096C16.7862 5.36702 16.1041 5.136 14.7398 4.67396L2.58814 0.558469Z"
/>
</svg>
<p class="text">{user.name}</p>
</div>
{/each}
</div>
<div class="graph-box" id="graph-box-{$elId}">
<p class="title">{formatNumber(connections)}</p>
<p class="subtitle">Realtime Connections</p>
<svg
width="324"
height="133"
viewBox="0 0 324 133"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_3981_106717)">
<path
d="M15.8661 4.27273V13H14.8093V5.38068H14.7582L12.6275 6.79545V5.72159L14.8093 4.27273H15.8661ZM21.2227 13.1193C20.5806 13.1193 20.0337 12.9446 19.582 12.5952C19.1303 12.2429 18.7852 11.733 18.5465 11.0653C18.3079 10.3949 18.1886 9.58523 18.1886 8.63636C18.1886 7.69318 18.3079 6.88778 18.5465 6.22017C18.788 5.54972 19.1346 5.03835 19.5863 4.68608C20.0408 4.33097 20.5863 4.15341 21.2227 4.15341C21.859 4.15341 22.4031 4.33097 22.8548 4.68608C23.3093 5.03835 23.6559 5.54972 23.8945 6.22017C24.136 6.88778 24.2567 7.69318 24.2567 8.63636C24.2567 9.58523 24.1374 10.3949 23.8988 11.0653C23.6602 11.733 23.315 12.2429 22.8633 12.5952C22.4116 12.9446 21.8647 13.1193 21.2227 13.1193ZM21.2227 12.1818C21.859 12.1818 22.3533 11.875 22.7056 11.2614C23.0579 10.6477 23.234 9.77273 23.234 8.63636C23.234 7.88068 23.1531 7.23722 22.9911 6.70597C22.832 6.17472 22.6019 5.76989 22.3008 5.49148C22.0025 5.21307 21.6431 5.07386 21.2227 5.07386C20.592 5.07386 20.0991 5.38494 19.744 6.0071C19.3888 6.62642 19.2113 7.50284 19.2113 8.63636C19.2113 9.39205 19.2908 10.0341 19.4499 10.5625C19.609 11.0909 19.8377 11.4929 20.136 11.7685C20.4371 12.044 20.7994 12.1818 21.2227 12.1818ZM28.7227 13.1193C28.0806 13.1193 27.5337 12.9446 27.082 12.5952C26.6303 12.2429 26.2852 11.733 26.0465 11.0653C25.8079 10.3949 25.6886 9.58523 25.6886 8.63636C25.6886 7.69318 25.8079 6.88778 26.0465 6.22017C26.288 5.54972 26.6346 5.03835 27.0863 4.68608C27.5408 4.33097 28.0863 4.15341 28.7227 4.15341C29.359 4.15341 29.9031 4.33097 30.3548 4.68608C30.8093 5.03835 31.1559 5.54972 31.3945 6.22017C31.636 6.88778 31.7567 7.69318 31.7567 8.63636C31.7567 9.58523 31.6374 10.3949 31.3988 11.0653C31.1602 11.733 30.815 12.2429 30.3633 12.5952C29.9116 12.9446 29.3647 13.1193 28.7227 13.1193ZM28.7227 12.1818C29.359 12.1818 29.8533 11.875 30.2056 11.2614C30.5579 10.6477 30.734 9.77273 30.734 8.63636C30.734 7.88068 30.6531 7.23722 30.4911 6.70597C30.332 6.17472 30.1019 5.76989 29.8008 5.49148C29.5025 5.21307 29.1431 5.07386 28.7227 5.07386C28.092 5.07386 27.5991 5.38494 27.244 6.0071C26.8888 6.62642 26.7113 7.50284 26.7113 8.63636C26.7113 9.39205 26.7908 10.0341 26.9499 10.5625C27.109 11.0909 27.3377 11.4929 27.636 11.7685C27.9371 12.044 28.2994 12.1818 28.7227 12.1818ZM34.3306 10.6136L34.3136 9.36932H34.5181L37.3817 6.45455H38.6261L35.5749 9.53977H35.4897L34.3306 10.6136ZM33.3931 13V4.27273H34.3988V13H33.3931ZM37.5522 13L34.9954 9.76136L35.7113 9.0625L38.8306 13H37.5522Z"
fill="#6C6C71"
/>
<g filter="url(#filter0_b_3981_106717)">
<line
x1="55.1016"
y1="9.27344"
x2="324.001"
y2="9.27344"
stroke="white"
stroke-opacity="0.06"
/>
</g>
<path
d="M19.4847 52L23.3881 44.2784V44.2102H18.8881V43.2727H24.479V44.2614L20.5927 52H19.4847ZM28.7266 52.1193C28.0845 52.1193 27.5376 51.9446 27.0859 51.5952C26.6342 51.2429 26.2891 50.733 26.0504 50.0653C25.8118 49.3949 25.6925 48.5852 25.6925 47.6364C25.6925 46.6932 25.8118 45.8878 26.0504 45.2202C26.2919 44.5497 26.6385 44.0384 27.0902 43.6861C27.5447 43.331 28.0902 43.1534 28.7266 43.1534C29.3629 43.1534 29.907 43.331 30.3587 43.6861C30.8132 44.0384 31.1598 44.5497 31.3984 45.2202C31.6399 45.8878 31.7607 46.6932 31.7607 47.6364C31.7607 48.5852 31.6413 49.3949 31.4027 50.0653C31.1641 50.733 30.8189 51.2429 30.3672 51.5952C29.9155 51.9446 29.3686 52.1193 28.7266 52.1193ZM28.7266 51.1818C29.3629 51.1818 29.8572 50.875 30.2095 50.2614C30.5618 49.6477 30.7379 48.7727 30.7379 47.6364C30.7379 46.8807 30.657 46.2372 30.495 45.706C30.3359 45.1747 30.1058 44.7699 29.8047 44.4915C29.5064 44.2131 29.147 44.0739 28.7266 44.0739C28.0959 44.0739 27.603 44.3849 27.2479 45.0071C26.8928 45.6264 26.7152 46.5028 26.7152 47.6364C26.7152 48.392 26.7947 49.0341 26.9538 49.5625C27.1129 50.0909 27.3416 50.4929 27.6399 50.7685C27.9411 51.044 28.3033 51.1818 28.7266 51.1818ZM34.3345 49.6136L34.3175 48.3693H34.522L37.3857 45.4545H38.63L35.5788 48.5398H35.4936L34.3345 49.6136ZM33.397 52V43.2727H34.4027V52H33.397ZM37.5561 52L34.9993 48.7614L35.7152 48.0625L38.8345 52H37.5561Z"
fill="#6C6C71"
/>
<g filter="url(#filter1_b_3981_106717)">
<line
x1="55.1016"
y1="48.0391"
x2="324.001"
y2="48.0391"
stroke="white"
stroke-opacity="0.06"
/>
</g>
<path
d="M21.3026 90.1193C20.8026 90.1193 20.3523 90.0199 19.9517 89.821C19.5511 89.6222 19.2301 89.3494 18.9886 89.0028C18.7472 88.6562 18.6151 88.2614 18.5923 87.8182H19.6151C19.6548 88.2131 19.8338 88.5398 20.152 88.7983C20.473 89.054 20.8565 89.1818 21.3026 89.1818C21.6605 89.1818 21.9787 89.098 22.2571 88.9304C22.5384 88.7628 22.7585 88.5327 22.9176 88.2401C23.0795 87.9446 23.1605 87.6108 23.1605 87.2386C23.1605 86.858 23.0767 86.5185 22.9091 86.2202C22.7443 85.919 22.517 85.6818 22.2273 85.5085C21.9375 85.3352 21.6065 85.2472 21.2344 85.2443C20.9673 85.2415 20.6932 85.2827 20.4119 85.3679C20.1307 85.4503 19.8991 85.5568 19.7173 85.6875L18.7287 85.5682L19.2571 81.2727H23.7912V82.2102H20.1435L19.8366 84.7841H19.8878C20.0668 84.642 20.2912 84.5241 20.5611 84.4304C20.831 84.3366 21.1122 84.2898 21.4048 84.2898C21.9389 84.2898 22.4148 84.4176 22.8324 84.6733C23.2528 84.9261 23.5824 85.2727 23.821 85.7131C24.0625 86.1534 24.1832 86.6562 24.1832 87.2216C24.1832 87.7784 24.0582 88.2756 23.8082 88.7131C23.5611 89.1477 23.2202 89.4915 22.7855 89.7443C22.3509 89.9943 21.8565 90.1193 21.3026 90.1193ZM28.7227 90.1193C28.0806 90.1193 27.5337 89.9446 27.082 89.5952C26.6303 89.2429 26.2852 88.733 26.0465 88.0653C25.8079 87.3949 25.6886 86.5852 25.6886 85.6364C25.6886 84.6932 25.8079 83.8878 26.0465 83.2202C26.288 82.5497 26.6346 82.0384 27.0863 81.6861C27.5408 81.331 28.0863 81.1534 28.7227 81.1534C29.359 81.1534 29.9031 81.331 30.3548 81.6861C30.8093 82.0384 31.1559 82.5497 31.3945 83.2202C31.636 83.8878 31.7567 84.6932 31.7567 85.6364C31.7567 86.5852 31.6374 87.3949 31.3988 88.0653C31.1602 88.733 30.815 89.2429 30.3633 89.5952C29.9116 89.9446 29.3647 90.1193 28.7227 90.1193ZM28.7227 89.1818C29.359 89.1818 29.8533 88.875 30.2056 88.2614C30.5579 87.6477 30.734 86.7727 30.734 85.6364C30.734 84.8807 30.6531 84.2372 30.4911 83.706C30.332 83.1747 30.1019 82.7699 29.8008 82.4915C29.5025 82.2131 29.1431 82.0739 28.7227 82.0739C28.092 82.0739 27.5991 82.3849 27.244 83.0071C26.8888 83.6264 26.7113 84.5028 26.7113 85.6364C26.7113 86.392 26.7908 87.0341 26.9499 87.5625C27.109 88.0909 27.3377 88.4929 27.636 88.7685C27.9371 89.044 28.2994 89.1818 28.7227 89.1818ZM34.3306 87.6136L34.3136 86.3693H34.5181L37.3817 83.4545H38.6261L35.5749 86.5398H35.4897L34.3306 87.6136ZM33.3931 90V81.2727H34.3988V90H33.3931ZM37.5522 90L34.9954 86.7614L35.7113 86.0625L38.8306 90H37.5522Z"
fill="#6C6C71"
/>
<g filter="url(#filter2_b_3981_106717)">
<line
x1="55.1016"
y1="85.8125"
x2="324.001"
y2="85.8125"
stroke="white"
stroke-opacity="0.06"
/>
</g>
<path
d="M35.2539 128.119C34.6119 128.119 34.065 127.945 33.6133 127.595C33.1616 127.243 32.8164 126.733 32.5778 126.065C32.3391 125.395 32.2198 124.585 32.2198 123.636C32.2198 122.693 32.3391 121.888 32.5778 121.22C32.8192 120.55 33.1658 120.038 33.6175 119.686C34.0721 119.331 34.6175 119.153 35.2539 119.153C35.8903 119.153 36.4343 119.331 36.886 119.686C37.3406 120.038 37.6871 120.55 37.9258 121.22C38.1673 121.888 38.288 122.693 38.288 123.636C38.288 124.585 38.1687 125.395 37.93 126.065C37.6914 126.733 37.3462 127.243 36.8945 127.595C36.4428 127.945 35.896 128.119 35.2539 128.119ZM35.2539 127.182C35.8903 127.182 36.3846 126.875 36.7369 126.261C37.0891 125.648 37.2653 124.773 37.2653 123.636C37.2653 122.881 37.1843 122.237 37.0224 121.706C36.8633 121.175 36.6332 120.77 36.332 120.491C36.0337 120.213 35.6744 120.074 35.2539 120.074C34.6232 120.074 34.1303 120.385 33.7752 121.007C33.4201 121.626 33.2425 122.503 33.2425 123.636C33.2425 124.392 33.3221 125.034 33.4812 125.562C33.6403 126.091 33.869 126.493 34.1673 126.768C34.4684 127.044 34.8306 127.182 35.2539 127.182Z"
fill="#6C6C71"
/>
<g filter="url(#filter3_b_3981_106717)">
<line
x1="55.1016"
y1="123.586"
x2="324.001"
y2="123.586"
stroke="white"
stroke-opacity="0.06"
/>
</g>
{#each progressedLines as line, i}
{@const x = 57 + i * 24}
{@const y = 124 - line}
{#if line > 3}
<circle cx={x} cy={y} r="3" fill="#FD366E" />
<line x1={x} y1={y} x2={x} y2="124" stroke="#FD366E" stroke-width="6" />
{/if}
{/each}
</g>
<defs>
<filter
id="filter0_b_3981_106717"
x="-144.898"
y="-191.227"
width="668.899"
height="401"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="100" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_3981_106717"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_3981_106717"
result="shape"
/>
</filter>
<filter
id="filter1_b_3981_106717"
x="-144.898"
y="-152.461"
width="668.899"
height="401"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="100" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_3981_106717"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_3981_106717"
result="shape"
/>
</filter>
<filter
id="filter2_b_3981_106717"
x="-144.898"
y="-114.688"
width="668.899"
height="401"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="100" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_3981_106717"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_3981_106717"
result="shape"
/>
</filter>
<filter
id="filter3_b_3981_106717"
x="-144.898"
y="-76.9141"
width="668.899"
height="401"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="100" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_3981_106717"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_3981_106717"
result="shape"
/>
</filter>
<clipPath id="clip0_3981_106717">
<rect width="324" height="133" fill="white" />
</clipPath>
</defs>
</svg>
</div>
</div>
<style lang="scss">
@use '$scss/abstract/mixins/border-gradient' as gradients;
// Utilities
.flow {
display: flex;
align-items: center;
}
.flow-v {
display: flex;
flex-direction: column;
}
.gap-8 {
gap: 0.5rem;
}
.gap-12 {
gap: 0.75rem;
}
.sq-32 {
width: 2rem;
height: 2rem;
}
.wrap {
flex-wrap: wrap;
}
.capitalize {
text-transform: capitalize;
}
.mbs-8 {
margin-block-start: 0.5rem;
}
// Components
.tgl-avatars {
display: flex;
.tgl-avatar:not(:first-child) {
margin-inline-start: -0.5rem;
}
}
.tgl-avatar {
--size: 1.25rem;
width: var(--size);
height: var(--size);
display: grid;
place-items: center;
text-align: center;
border-radius: 100%;
color: white;
font-size: 0.65rem;
background-color: var(--color);
}
.tgl-tag {
padding: 0.25rem 0.4375rem;
border-radius: 0.21713rem;
border: 0.869px solid #ededf0;
box-shadow: 0px 1.73704px 3.47408px 0px rgba(0, 0, 0, 0.06);
color: #818186;
font-family: Inter;
font-size: 0.625rem;
font-style: normal;
font-weight: 500;
line-height: 0.75rem; /* 120% */
letter-spacing: -0.00875rem;
text-transform: capitalize;
}
.vertical-sep {
width: 1px;
height: 100%;
background-color: rgba(237, 237, 240, 1);
}
hr {
border-bottom: 1px solid hsl(var(--web-color-greyscale-50));
margin-block: 1rem;
}
.dashed-btn {
display: flex;
padding: 0.5rem 0.875rem;
justify-content: center;
align-items: center;
gap: 0.5rem;
align-self: stretch;
border-radius: 0.625rem;
border: 1px dashed hsl(var(--web-color-greyscale-50));
color: #56565c;
.text {
font-family: Inter;
font-size: 0.8rem;
font-style: normal;
font-weight: 400;
line-height: 1.08563rem; /* 142.857% */
}
[class*='icon'] {
font-size: 1rem;
}
}
.task {
display: flex;
padding: 0.625rem 0.875rem;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 0.5rem;
border-radius: 0.625rem;
border: 1px solid #ededf0;
background: var(--color-bw-white, #fff);
.text {
color: #56565c;
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 500;
line-height: 1rem; /* 133.333% */
}
}
.user {
position: absolute;
left: 50%;
top: 50%;
fill: var(--color);
.text {
position: absolute;
left: 100%;
bottom: 0;
transform: translateX(-4px) translateY(50%);
border-radius: 0rem 0.375rem 0.375rem 0.375rem;
background: var(--color);
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.06);
padding: 0.25rem 0.5rem;
color: var(--color-bw-white, #fff);
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 1rem; /* 114.286% */
letter-spacing: -0.01225rem;
}
}
// Specifics
.wrapper {
height: 100%;
position: relative;
}
.inner-phone {
display: flex;
flex-direction: column;
height: 100%;
padding: 1.25rem;
overflow: hidden;
.header {
display: flex;
flex-direction: column;
gap: 0.5rem;
.row {
display: flex;
justify-content: space-between;
align-items: center;
.title {
color: var(--color-greyscale-800, #2d2d31);
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 600;
line-height: 1.25rem; /* 142.857% */
letter-spacing: -0.01225rem;
}
.search {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem;
width: 12rem;
border-radius: 0.5rem;
border: 1.869px solid var(--greyscale-50, #ededf0);
[class*='icon'] {
font-size: 1rem;
}
.text {
color: var(--greyscale-300, #adadb0);
font-family: Inter;
font-size: 0.7rem;
font-style: normal;
font-weight: 500;
line-height: 0.875rem; /* 140% */
}
}
.btn {
display: inline-flex;
padding: 0.125rem 0.25rem;
justify-content: center;
align-items: center;
gap: 0.125rem;
border-radius: 0.25rem;
border: 0.869px solid var(--greyscale-50, #ededf0);
color: var(--greyscale-600, #6c6c71);
font-family: Inter;
font-size: 0.625rem;
font-style: normal;
font-weight: 500;
line-height: 0.875rem; /* 140% */
}
}
}
.content {
display: grid;
grid-template-columns: 1fr 1px 1fr 1px 1fr;
gap: 1rem;
height: 100%;
.column {
flex: 1 0 auto;
.title {
display: flex;
align-items: center;
gap: 0.5rem;
.text {
color: var(--greyscale-600, #6c6c71);
font-family: Inter;
font-size: 0.625rem;
font-style: normal;
font-weight: 500;
line-height: 0.875rem; /* 140% */
}
.tgl-inline-tag {
display: grid;
place-items: center;
padding: 0rem 0.21713rem 0.1rem;
border-radius: 0.21713rem;
background: var(--greyscale-50, #ededf0);
color: var(--greyscale-600, #6c6c71);
font-family: Inter;
font-size: 0.625rem;
font-style: normal;
font-weight: 500;
line-height: 0.875rem; /* 140% */
}
[class*='icon'] {
font-size: 1rem;
margin-inline-start: auto;
}
}
}
}
}
.graph-box {
@include gradients.border-gradient;
--m-border-gradient-before: linear-gradient(
180deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 125.11%
);
--m-border-radius: 1rem;
position: absolute;
right: 2rem;
bottom: -10rem;
background: var(--card, rgba(35, 35, 37, 0.9));
box-shadow:
0px 0px 0px 0px rgba(0, 0, 0, 0.06),
-2px 4px 9px 0px rgba(0, 0, 0, 0.06),
-8px 15px 17px 0px rgba(0, 0, 0, 0.05),
-19px 34px 23px 0px rgba(0, 0, 0, 0.03),
-33px 60px 27px 0px rgba(0, 0, 0, 0.01),
-52px 94px 30px 0px rgba(0, 0, 0, 0);
backdrop-filter: blur(8px);
padding-block: 1.5rem;
z-index: 9999;
.title,
.subtitle {
padding-inline: 1.88rem;
}
.title {
color: #e4e4e7;
font-family: Aeonik Pro;
font-size: 2rem;
font-style: normal;
font-weight: 400;
line-height: 2.25rem; /* 112.5% */
}
.subtitle {
color: #adadb0;
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.125rem; /* 128.571% */
}
> svg {
margin-block-start: 1.25rem;
margin-inline: 0.5rem 1.5rem;
}
}
</style>

View File

@@ -1,39 +0,0 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import { storageController } from '.';
import { flip } from '$lib/utils/flip';
const { state } = storageController;
</script>
<div class="pseudo-table">
<div class="header">
<span class="text-micro uppercase">Filename</span>
<span class="text-micro uppercase">Type</span>
<span class="text-micro uppercase">Size</span>
</div>
{#each $state.files as file (file.src)}
<div class="row" in:slide={{ duration: 150 }} animate:flip={{ duration: 150 }}>
<div class="img-wrapper">
<img src={file.src} alt="" />
<span>{file.filename}</span>
</div>
<span class="truncated">{file.type}</span>
<span class="truncated">{file.size}</span>
</div>
{/each}
</div>
<style lang="scss">
.header,
.row {
grid-template-columns: 7rem 1fr 1fr !important;
gap: 1.5rem 3rem;
}
.img-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
}
</style>

View File

@@ -1,12 +0,0 @@
<script lang="ts">
import Code from '$lib/animations/CodeWindow/Code.svelte';
let content = `
const result = storage.createFile(
'my-bucket',
ID.unique(),
document.getElementById("uploader").files[0]
);`.trim();
</script>
<Code {content} />

View File

@@ -1,137 +0,0 @@
import Box from './box.svelte';
import Code from './code.svelte';
import Phone from './phone.svelte';
import { safeAnimate, sleep } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { getElSelector } from '../Products.svelte';
type File = {
src: string;
filename: string;
type: string;
size: string;
};
type State = {
files: File[];
};
const state = createResettable<State>({
files: []
});
const execute = async () => {
const phone = getElSelector('phone');
const box = getElSelector('box');
const code = getElSelector('code');
const overlay = getElSelector('overlay');
const drawer = getElSelector('drawer');
const upload = getElSelector('upload');
const uploadBtn = getElSelector('upload-btn');
const uploadImg = getElSelector('upload-img');
const uploadLoading = getElSelector('upload-loading');
const uploadText = getElSelector('upload-text');
const { update } = state.reset();
await Promise.all([
safeAnimate(phone, { x: 0, y: 0 }, { duration: 0.5 })?.finished,
safeAnimate(box, { opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(code, { opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(uploadLoading, { opacity: 0 }, { duration: 0 })?.finished
]);
await safeAnimate(code, { zIndex: 20 }, { duration: 0 })?.finished;
update((p) => ({
...p,
files: [
...p.files,
{
src: '/images/animations/storage-1.png',
filename: 'Profile.png',
type: 'image/png',
size: '362.6 KB'
}
]
}));
await sleep(250);
await Promise.all([
safeAnimate(overlay, { opacity: 1 }, { duration: 0.25 })?.finished,
safeAnimate(drawer, { y: [128, 0], opacity: 1 }, { duration: 0.5 })?.finished
]);
await sleep(250);
await safeAnimate(uploadBtn, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished;
await safeAnimate(code, { x: 300, y: 32 }, { duration: 0 })?.finished;
await Promise.all([
safeAnimate(code, { y: [32 - 16, 32], opacity: 1 }, { duration: 0.5 })?.finished,
safeAnimate(upload, { y: [-16, 0], opacity: 1 }, { duration: 0.5 })?.finished
]);
await sleep(250);
await safeAnimate(box, { x: 300, y: 300 }, { duration: 0 })?.finished;
await Promise.all([
safeAnimate(uploadImg, { x: [64, 48], y: [80, 64], opacity: 1 }, { duration: 0.5 })
?.finished,
safeAnimate(box, { y: [300 - 16, 300], opacity: 1 }, { duration: 1 })?.finished
]);
await sleep(250);
await Promise.all([
safeAnimate(uploadText, { opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(uploadLoading, { opacity: 1 }, { duration: 0.5 })?.finished,
safeAnimate(uploadImg, { opacity: 0, y: 64 + 8 }, { duration: 0.5 })?.finished
]);
await sleep(250);
await safeAnimate(upload, { opacity: 0, y: 48 }, { duration: 0.5 })?.finished;
update((p) => ({
...p,
files: [
...p.files,
{
src: '/images/animations/storage-2.png',
filename: 'Vector.svg',
type: 'vector/svg',
size: '1.5 KB'
}
]
}));
await sleep(250);
update((p) => ({
...p,
files: [
...p.files,
{
src: '/images/animations/storage-3.png',
filename: 'img2.webp',
type: 'image/webp',
size: '3.2 MB'
}
]
}));
};
export const storageController = {
execute,
state
};
export const Storage = {
Phone,
Box,
Code
};

View File

@@ -1,284 +0,0 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { storageController } from '.';
import { elId } from '../Products.svelte';
import TaskCheckbox from '../TaskCheckbox.svelte';
import { databasesController } from '../databases';
const { state: dbState } = databasesController;
const fixedTasks = $dbState.tasks;
const { state } = storageController;
</script>
<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>
</div>
<div class="date">Today</div>
<div class="tasks">
{#each fixedTasks as task (task.id)}
<div class="task" data-checked={task.checked ? '' : undefined} in:fly={{ x: -16 }}>
<TaskCheckbox bind:checked={task.checked} />
<span class="title">{task.title}</span>
</div>
{/each}
</div>
<div class="add-btn">
<span class="web-icon-plus"></span>
</div>
<div class="overlay" id="overlay-{$elId}">
<div class="drawer" id="drawer-{$elId}">
<p class="title">Edit images for website</p>
<p class="subtitle">Edit the attached images to use in the website</p>
<div class="upload" id="upload-btn-{$elId}">Upload media...</div>
<div class="images">
{#each $state.files.slice(1) as file}
<img src={file.src} alt="" transition:fly={{ x: 16 }} />
{/each}
</div>
</div>
</div>
</div>
<div class="upload-media" id="upload-{$elId}">
<p class="title">Upload media</p>
<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>
</div>
</div>
<img id="upload-img-{$elId}" src="/images/animations/storage-2.png" alt="" />
</div>
<style lang="scss">
.inner-phone {
padding-block: 3rem;
padding-inline: 1rem;
color: rgba(67, 67, 71, 1);
text-align: left;
position: relative;
height: 100%;
overflow: visible;
.header {
display: flex;
justify-content: space-between;
align-items: center;
.title {
color: var(--color-greyscale-800, #2d2d31);
font-family: Inter;
font-size: 1rem;
font-style: normal;
font-weight: 600;
line-height: 1.375rem; /* 137.5% */
letter-spacing: -0.014rem;
}
[class*='icon-'] {
font-size: 1.25rem;
color: hsl(var(--web-color-greyscale-500));
}
}
.date {
margin-block-start: 3rem;
color: hsl(var(--web-color-greyscale-600));
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 500;
line-height: 1.25rem; /* 166.667% */
}
.tasks {
margin-block-start: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
.task {
display: flex;
align-items: center;
gap: 0.75rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--web-color-greyscale-50));
background: hsl(var(--web-color-white));
color: var(--greyscale-700, var(--color-greyscale-700, #56565c));
padding-block: 0.55rem;
padding-inline: 0.88rem;
/* Responsive/SubBody-400 */
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.375rem; /* 157.143% */
letter-spacing: -0.00394rem;
transition: opacity 200ms ease;
&[data-checked] {
opacity: 0.6;
}
}
}
.add-btn {
position: absolute;
right: 1rem;
bottom: 2.5rem;
width: 2.5rem;
height: 2.5rem;
flex-shrink: 0;
box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.12);
background-color: rgba(124, 103, 254, 1);
color: rgba(237, 237, 240, 1);
font-size: 1.5rem;
display: grid;
place-items: center;
border-radius: 100%;
}
}
.overlay {
opacity: 0;
position: absolute;
inset: 0;
overflow: hidden;
background: rgba(0, 0, 0, 0.32);
border-radius: 2rem;
.drawer {
position: absolute;
bottom: 0;
height: 60%;
opacity: 0;
background-color: white;
border-radius: 0.88463rem 0.88463rem 2rem 2rem;
padding: 1rem;
.title {
color: #434347;
font-family: Inter;
font-size: 0.88463rem;
font-style: normal;
font-weight: 600;
line-height: 1.21638rem; /* 137.5% */
letter-spacing: -0.01238rem;
}
.subtitle {
color: var(--greyscale-500, var(--color-greyscale-500, #818186));
font-family: Inter;
font-size: 0.77406rem;
font-style: normal;
font-weight: 400;
line-height: 1.10575rem; /* 142.857% */
letter-spacing: -0.01081rem;
margin-block-start: 0.2rem;
}
.upload {
display: flex;
padding: 0.44231rem 0.66344rem;
justify-content: center;
align-items: center;
align-self: stretch;
border-radius: 0.66344rem;
border: 1px dashed #d9d9d9;
color: var(--greyscale-500, var(--color-greyscale-500, #818186));
font-family: Inter;
font-size: 0.77406rem;
font-style: normal;
font-weight: 400;
line-height: 1.10575rem; /* 142.857% */
letter-spacing: -0.01081rem;
margin-block-start: 2rem;
}
}
}
.upload-media {
position: absolute;
right: calc(0% - 24px);
bottom: 8rem;
background-color: white;
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05);
border-radius: 1rem;
padding: 0.75rem;
opacity: 0;
.title {
color: var(--color-greyscale-800, #2d2d31);
font-family: Inter;
font-size: 0.85rem;
font-style: normal;
font-weight: 600;
line-height: 1.375rem; /* 137.5% */
letter-spacing: -0.014rem;
}
.drop-zone {
display: grid;
place-items: center;
border: 0.885px dashed #d9d9d9;
border-radius: 0.5rem;
color: var(--greyscale-500, var(--color-greyscale-500, #818186));
padding: 2rem 1.25rem;
margin-block-start: 0.5rem;
font-size: 0.65rem;
font-family: Inter;
position: relative;
overflow: hidden;
.loading-overlay {
position: absolute;
inset: 0;
opacity: 0;
z-index: 100;
display: grid;
place-items: center;
}
}
img {
position: absolute;
left: 0;
top: 0;
opacity: 0;
}
}
.images {
display: flex;
margin-block-start: 0.5rem;
gap: 0.5rem;
}
</style>

View File

@@ -1,222 +1,3 @@
import type { Action } from 'svelte/action';
import {
animate as motionAnimate,
type ElementOrSelector,
type MotionKeyframesDefinition,
type AnimationOptionsWithOverrides,
animate
} from 'motion-legacy';
export function animation(
elementOrSelector: ElementOrSelector,
keyframes: MotionKeyframesDefinition,
options?: AnimationOptionsWithOverrides
) {
const play = () => {
const played = motionAnimate(elementOrSelector, keyframes, options);
return played;
};
const reverse = () => {
const reversedKeyframes = Object.fromEntries(
Object.entries(keyframes).map(([key, keyframe]) => {
return [key, Array.isArray(keyframe) ? [...keyframe].reverse() : keyframe];
})
) as typeof keyframes;
const reversed = motionAnimate(elementOrSelector, reversedKeyframes, options);
return reversed;
};
return {
play,
reverse
};
}
export type Animation = ReturnType<typeof animation>;
export const safeAnimate = (
elementOrSelector: ElementOrSelector,
keyframes: MotionKeyframesDefinition,
options?: AnimationOptionsWithOverrides
) => {
try {
return animate(elementOrSelector, keyframes, options);
} catch {
// do nothing lol
}
};
type Unsubscriber = () => void;
type PreviousScroll = 'before' | 'after' | undefined;
type ScrollCallbackState = {
previous?: PreviousScroll;
unsubscribe?: Unsubscriber;
executedCount: number;
};
export type ScrollCallback = {
percentage: number;
whenAfter?: (args: Omit<ScrollCallbackState, 'unsubscribe'>) => Unsubscriber | void;
};
export function createScrollHandler(callbacks: ScrollCallback[]) {
const states: ScrollCallbackState[] = callbacks.map(() => ({
executedCount: 0
}));
const handler = function (scrollPercentage: number) {
callbacks.forEach((callback, i) => {
const { percentage, whenAfter } = callback;
const { previous, unsubscribe, executedCount } = states[i];
if (scrollPercentage >= percentage && previous !== 'after') {
// Execute whenAfter
states[i].unsubscribe = whenAfter?.({ previous, executedCount }) ?? undefined;
states[i].previous = 'after';
if (whenAfter) {
states[i].executedCount++;
}
} else if (scrollPercentage < percentage && previous === 'after') {
unsubscribe?.();
states[i].unsubscribe = undefined;
states[i].previous = 'before';
}
});
};
handler.reset = () => {
states.forEach((state) => {
// state.unsubscribe?.();
state.unsubscribe = undefined;
state.previous = undefined;
state.executedCount = 0;
});
};
return handler;
}
export type ScrollInfo = {
percentage: number;
traversed: number;
remaning: number;
};
export const scroll: Action<
HTMLElement,
undefined,
{
'on:web-scroll': (e: CustomEvent<ScrollInfo>) => void;
'on:web-resize': (e: CustomEvent<ScrollInfo>) => void;
}
> = (node) => {
function getScrollInfo(): ScrollInfo {
const { top, height } = node.getBoundingClientRect();
const { innerHeight } = window;
const scrollHeight = height - innerHeight;
const scrollPercentage = (-1 * top) / scrollHeight;
const traversed = scrollPercentage * scrollHeight;
const remaning = scrollHeight - traversed;
return {
percentage: scrollPercentage,
traversed,
remaning
};
}
const createHandler = (eventName: 'web-scroll' | 'web-resize') => {
return () => {
node.dispatchEvent(
new CustomEvent<ScrollInfo>(eventName, {
detail: getScrollInfo()
})
);
};
};
const handleScroll = createHandler('web-scroll');
const handleResize = createHandler('web-resize');
handleScroll();
handleResize();
window.addEventListener('scroll', handleScroll);
window.addEventListener('resize', handleResize);
return {
destroy() {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', handleResize);
}
};
};
type TimelineEvent = {
at: number;
callback: () => void;
};
export function createTimeline(events: TimelineEvent[]) {
let timeoutIds: NodeJS.Timeout[] = [];
const play = () => {
events.forEach((event) => {
const timeoutId = setTimeout(event.callback, event.at);
timeoutIds.push(timeoutId);
});
};
const cancel = () => {
timeoutIds.forEach(clearTimeout);
timeoutIds = [];
};
return { play, cancel };
}
type ProgressEvent = {
percentage: number;
callback: () => void;
};
/**
* Given a list of events, create a sequence of events that will be executed
* when a given percentage is greater than the event percentage, and before
* the next event percentage.
* e.g. const handler = createProgressSequence(events) // where there's an event for each 0.1 percentage
* handler(0.45) // will execute the event with percentage 0.4.
*/
export function createProgressSequence(events: ProgressEvent[]) {
// Sort from highest to lowest percentage
const sortedEvents = [...events].sort((a, b) => b.percentage - a.percentage);
let lastEventIdx = -1;
const handler = (percentage: number) => {
const idx = sortedEvents.findIndex((event) => event.percentage <= percentage);
if (idx === lastEventIdx) {
return;
}
const event = sortedEvents[idx];
event?.callback();
lastEventIdx = idx;
};
handler.resetLastEventIdx = () => {
lastEventIdx = -1;
};
return handler;
}
export type ProgressSequence = ReturnType<typeof createProgressSequence>;
export function write(text: string, cb: (v: string) => void, duration = 500) {
if (text.length === 0) {
cb('');
@@ -258,11 +39,3 @@ export function sleep(duration: number) {
setTimeout(resolve, duration);
});
}
export function getInitials(name: string) {
return name
.split(' ')
.map((word) => word?.[0]?.toUpperCase() ?? '')
.join('')
.slice(0, 2);
}

View File

@@ -1,45 +0,0 @@
<script lang="ts">
import { rect } from '$lib/actions';
import { clamp } from '$lib/utils/clamp';
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
export let percentage = 0;
let easedPercentage = 0;
const elRect = writable<DOMRect | null>(null);
$: y = $elRect ? clamp(0, easedPercentage, 1) * $elRect.height : 0;
onMount(() => {
let frame: number | null = null;
const ease = () => {
easedPercentage += percentage - easedPercentage;
frame = window.requestAnimationFrame(ease);
};
ease();
return () => {
frame && window.cancelAnimationFrame(frame);
};
});
</script>
<div
class="scroll-indicator relative h-full w-px shrink-0 rounded-full"
use:rect={elRect}
style:--y={`${y}px`}
style:--percentage={`${easedPercentage * 100}%`}
>
<div class="absolute -top-[8px] left-1/2"></div>
</div>
<style lang="scss">
.scroll-indicator {
background: linear-gradient(
to bottom,
hsl(var(--web-color-accent)) 0%,
hsl(var(--web-color-greyscale-700)) var(--percentage),
hsl(var(--web-color-greyscale-700)) 100%
);
}
</style>

View File

@@ -14,7 +14,8 @@
<Button
action={trigger}
event="intro-video-btn_hero-click"
class="cursor-pointer shadow-[0_2px_40px_rgba(0,0,0,0.5)] transition-opacity hover:opacity-90 active:scale-95"
variant="secondary"
class="w-full! cursor-pointer shadow-[0_2px_40px_rgba(0,0,0,0.5)] transition-opacity hover:opacity-90 active:scale-95 lg:w-fit!"
>
Appwrite in 100 seconds

View File

@@ -1,5 +1,15 @@
<script lang="ts">
export let title = "Trusted by developers from the world's leading organizations";
import { classNames } from '$lib/utils/classnames';
type Props = {
title?: string;
class?: string;
};
const {
title = "Trusted by developers from the world's leading organizations",
class: className
}: Props = $props();
const logos = [
{
@@ -77,14 +87,12 @@
];
</script>
<div class="my-32">
<div class={classNames('py-32', className)}>
<div class="container">
<h2
class="font-aeonik-pro text-greyscale-100 mx-auto max-w-xl text-center text-4xl leading-10"
>
<h2 class="font-aeonik-pro text-greyscale-100 text-label mx-auto max-w-md text-center">
{title}
</h2>
<ul class="grid grid-cols-3 gap-10 pt-20 text-center md:grid-cols-6">
<ul class="grid grid-cols-3 gap-10 pt-10 text-center md:grid-cols-6">
{#each logos as { src, alt, width, height }}
<li class="grid place-content-center">
<img {src} {alt} {width} {height} />

View File

@@ -9,7 +9,7 @@
</script>
{#if variant === 'homepage'}
<footer class="web-main-footer relative mt-12">
<footer class="web-main-footer relative mt-12 flex flex-col justify-between gap-10 lg:flex-row">
<ul class="flex gap-2">
{#each socials as social}
<li>
@@ -25,11 +25,11 @@
</li>
{/each}
</ul>
<div class="e-main-footer">
<div class="mt-1 grid grid-cols-2 gap-y-4 md:grid-cols-3">
<div>Copyright © {year} Appwrite</div>
<iframe
class="status w-full md:w-fit md:max-w-[230px]"
class="w-full md:w-fit md:max-w-[230px]"
title="Appwrite Status"
src="https://status.appwrite.online/badge?theme=dark"
height="35"
@@ -39,7 +39,7 @@
style:margin-top="-4px"
></iframe>
<ul class="flex gap-4">
<ul class="flex gap-4 text-right md:justify-end">
<li>
<a
class="web-link"
@@ -85,7 +85,7 @@
<div class="web-main-footer-grid-1-column-2">
<ThemeSelect />
</div>
<ul class="web-main-footer-grid-1-column-3 web-main-footer-links items-start">
<ul class="web-main-footer-grid-1-column-3 web-main-footer-links items-end text-right">
<li>
<a
href="/discord"
@@ -125,21 +125,4 @@
margin-bottom: 6px; /* balancing due to style:margin-top="-4px" & the `iframe` has some spacings too I think */
}
}
.e-main-footer {
display: flex;
@media #{devices.$break1} {
flex-direction: column;
> * {
padding-block: 1rem;
&:not(:first-child) {
border-block-start: solid 0.0625rem hsl(var(--web-color-border));
}
}
}
@media #{devices.$break2open} {
display: flex;
gap: 2rem;
}
}
</style>

View File

@@ -15,12 +15,15 @@
let { selected = $bindable('js'), data = [], width = null, height = null }: Props = $props();
let snippets = $derived(writable(new Set(data.map((d) => d.language))));
const getSnippets = () => {
return snippets;
};
let content = $derived(data.find((d) => d.language === selected)?.content ?? '');
let platform = $derived(data.find((d) => d.language === selected)?.platform ?? '');
snippets?.subscribe((n) => {
getSnippets().subscribe((n) => {
if (selected === null && n.size > 0) {
selected = Array.from(n)[0] as Language;
}
@@ -33,7 +36,7 @@
type CopyStatusType = keyof typeof CopyStatus;
type CopyStatusValue = (typeof CopyStatus)[CopyStatusType];
let copyText: CopyStatusValue = CopyStatus.Copy;
let copyText = $state<CopyStatusValue>(CopyStatus.Copy);
async function handleCopy() {
await copy(content);
@@ -60,7 +63,7 @@
</script>
<section
class="dark web-code-snippet mx-auto lg:!max-w-[90vw]"
class="dark web-code-snippet mx-auto w-full lg:!max-w-[90vw]"
aria-label="code-snippet panel"
style={`width: ${width ? width / 16 + 'rem' : 'inherit'}; height: ${
height ? height / 16 + 'rem' : 'inherit'

View File

@@ -116,6 +116,7 @@
{plan.description}
</p>
<Button
href={plan.buttonLink}
event={plan.eventName}
variant={plan.buttonVariant}
class="w-full! flex-3 self-end md:w-fit"

View File

@@ -77,6 +77,7 @@
import { trackEvent } from '$lib/actions/analytics';
import { classNames } from '$lib/utils/classnames';
import { createDropdownMenu, melt } from '@melt-ui/svelte';
import Icon from './ui/icon';
const {
elements: { trigger, menu, item, overlay },
@@ -165,15 +166,16 @@
>
<header class="flex items-center justify-between">
<span
class="font-aeonik-fono tracking-loose text-secondary block text-xs uppercase"
class="font-aeonik-fono tracking-loose text-primary block text-xs uppercase"
>Customer Stories<span class="text-accent">_</span></span
>
<a
href="/blog/category/customer-stories"
class="text-primary text-caption flex items-center gap-2"
>See more <span
class="web-icon-chevron-right transition-transform group-hover:translate-x-0.5"
></span></a
class="text-secondary text-caption flex items-center"
>Read more customer stories <Icon
name="chevron-right"
class="transition-transform group-hover:translate-x-0.5"
></Icon></a
>
</header>

View File

@@ -1,16 +0,0 @@
<script lang="ts">
export let text: string;
const words = text.split(' ');
</script>
<span class="sr-only">{text}</span>
<span class="relative">
{#each words as word, i}
<span
class="animate-enter mr-2 inline-block"
style:animation-delay="{i * 75}ms
">{word}</span
>
{/each}
</span>

View File

@@ -5,9 +5,7 @@ export const pins = {
lng: -77.49,
city: 'Ashburn',
code: 'ASH',
available: true,
offsetX: 10,
offsetY: -10
available: true
},
{
lat: 33.75,
@@ -562,9 +560,7 @@ export const pins = {
lng: -74.01,
city: 'New York',
code: 'NYC',
available: true,
offsetX: 10,
offsetY: -10
available: true
},
{
lat: 50.11,
@@ -579,6 +575,48 @@ export const pins = {
city: 'Sydney',
code: 'AUS',
available: true
},
{
lat: 1.35,
lng: 103.82,
city: 'Singapore',
code: 'SIN',
date: 'Q4 2025'
},
{
lat: 37.77,
lng: -122.42,
city: 'San Francisco',
code: 'SFO',
date: 'Q4 2025'
},
{
lat: 12.97,
lng: 77.59,
city: 'Bangalore',
code: 'BLR',
date: 'Planned'
},
{
lat: 52.37,
lng: 4.9,
city: 'Amsterdam',
code: 'AMS',
date: 'Planned'
},
{
lat: 51.51,
lng: -0.13,
city: 'London',
code: 'LON',
date: 'Planned'
},
{
lat: 43.65,
lng: -79.38,
city: 'Toronto',
code: 'TOR',
date: 'Planned'
}
],
regions: [
@@ -587,9 +625,7 @@ export const pins = {
lng: -74.01,
city: 'New York',
code: 'NYC',
available: true,
offsetX: 10,
offsetY: -10
available: true
},
{
lat: 50.11,
@@ -598,13 +634,54 @@ export const pins = {
code: 'FRA',
available: true
},
{
lat: -33.87,
lng: 151.21,
city: 'Sydney',
code: 'AUS',
available: true
},
{
lat: 1.35,
lng: 103.82,
city: 'Singapore',
code: 'SIN',
date: 'Q4 2025'
},
{
lat: 37.77,
lng: -122.42,
city: 'San Francisco',
code: 'SFO',
date: 'Q4 2025'
},
{
lat: 12.97,
lng: 77.59,
city: 'Bangalore',
code: 'BLR',
date: 'Planned'
},
{
lat: 52.37,
lng: 4.9,
city: 'Amsterdam',
code: 'AMS',
date: 'Planned'
},
{
lat: 51.51,
lng: -0.13,
city: 'London',
code: 'LON',
date: 'Planned'
},
{
lat: 43.65,
lng: -79.38,
city: 'Toronto',
code: 'TOR',
date: 'Planned'
}
]
};

View File

@@ -1,64 +0,0 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
import { slugify } from '$lib/utils/slugify';
import { latLongToSvgPosition } from './utils/projections';
import { tooltipData } from './map-tooltip.svelte';
interface Props {
city: string;
code: string;
index: number;
lat: number;
lng: number;
bounds: {
north: number;
south: number;
west: number;
east: number;
};
available: boolean;
class?: string;
animate?: boolean;
}
const { city, code, index = 0, lat, lng, available, animate = false }: Props = $props();
const position = $derived(latLongToSvgPosition({ latitude: lat, longitude: lng }));
const handleSetActiveMarker = () => {
tooltipData.set({
city,
code,
available
});
};
const handleResetActiveMarker = () => {
tooltipData.set({
city: null,
code: null,
available: null
});
};
</script>
<button
class={classNames(
'group absolute z-10 flex size-2 cursor-pointer items-center justify-center opacity-0 [animation-delay:var(--delay)]',
{ 'animate-fade-in': animate }
)}
style="left: {position.x}%; top: {position.y}%;--delay: {index * 10}ms;"
data-region={slugify(city)}
onmouseenter={handleSetActiveMarker}
onfocus={handleSetActiveMarker}
onmouseleave={handleResetActiveMarker}
onblur={handleResetActiveMarker}
aria-label={city}
>
<span
class="from-accent/20 to-accent/10 border-gradient ease-spring pointer-events-none absolute inline-flex h-5 w-5 rounded-full bg-gradient-to-b opacity-0 transition-opacity group-hover:animate-ping group-hover:opacity-75 before:rounded-full"
style:animation-duration="1.5s"
></span>
<span class="bg-accent absolute inline-flex h-full w-full rounded-full"></span>
<span class="absolute size-1/2 rounded-full bg-white/80 transition-all"></span>
</button>

View File

@@ -3,6 +3,7 @@
import type { IconType } from '../ui';
import Icon from '../ui/icon/icon.svelte';
import { classNames } from '$lib/utils/classnames';
import { trackEvent } from '$lib/actions/analytics';
const {
onValueChange,
@@ -13,31 +14,41 @@
} = $props();
const navItems = [
{ label: 'PoP Locations', value: 'pop-locations', icon: 'pop-locations' },
{ label: 'Edges', value: 'edges', icon: 'edge' },
{ label: 'Regions', value: 'regions', icon: 'regions' }
] satisfies Array<{ label: string; value: string; icon: IconType }>;
let selectedTab = $state('pop-locations');
function getDescription() {
switch (selectedTab) {
case 'pop-locations':
return 'Points of presence ensure <50ms ping around the globe.';
case 'edges':
return 'Edges bring compute closer to users for faster response times.';
case 'regions':
return 'Regions offer data residency and redundancy across continents.';
default:
return '';
}
{
label: 'PoP Locations',
value: 'pop-locations',
icon: 'pop-locations',
href: '/docs/products/network/cdn',
description: 'Points of presence ensure <50ms ping around the globe.'
},
{
label: 'Edges',
value: 'edges',
icon: 'edge',
href: '/docs/products/network/edges#list',
description: 'Edges bring compute closer to users for faster response times.'
},
{
label: 'Regions',
value: 'regions',
icon: 'regions',
href: '/docs/products/network/regions#list',
description: 'Regions offer data residency and redundancy across continents.'
}
] satisfies Array<{
label: string;
value: string;
icon: IconType;
href: string;
description: string;
}>;
let activeIndex = $state(0);
</script>
<div class="flex flex-col gap-4 text-center">
<Tabs.Root
onValueChange={(value) => {
selectedTab = value;
activeIndex = navItems.findIndex((item) => item.value === value);
onValueChange(value);
}}
value={navItems[0]?.value}
@@ -45,8 +56,8 @@
>
<Tabs.List
class={classNames(
'border-smooth animate-fade-in relative grid w-full max-w-xl grid-cols-1 place-content-center gap-3 p-1 px-8 drop-shadow-md md:grid-cols-3 md:rounded-full md:border md:px-1',
theme === 'light' ? 'md:bg[var(--card, rgba(255,255,255,0.90))]' : 'md:bg-card'
'border-smooth animate-fade-in relative grid w-full max-w-xl grid-cols-1 place-content-center gap-3 overflow-hidden p-1 px-8 shadow-[0px_4px_8p_rgba(0,0,0,0.04)] md:grid-cols-3 md:rounded-full md:border md:px-1',
theme === 'light' ? 'md:bg-white' : 'md:bg-card'
)}
>
{#each navItems as { label, icon, value }, index}
@@ -57,6 +68,11 @@
'group data-[state="active"]:bg-accent/4 data-[state="active"]:border-accent/36 data-[state="active"]:text-accent'
)}
style="animation-delay:{index * 75}ms;"
onclick={() => {
trackEvent(
`network-map-nav-${value.toLowerCase().replace(' ', '-')}-click`
);
}}
>
<Icon
name={icon}
@@ -72,9 +88,17 @@
</Tabs.List>
</Tabs.Root>
{#key selectedTab}
<p class="animate-enter text-caption px-4">
{getDescription()}
<p class="text-caption text-secondary px-4">
{navItems[activeIndex].description}
<a
class="text-primary group mt-2 flex items-center justify-center gap-0.25 md:hidden"
href={navItems[activeIndex].href}
>Learn more about {navItems[activeIndex].label}
<Icon
name="arrow-right"
class="-rotate-45 transition-all group-hover:translate-x-0.25 group-hover:-translate-y-0.25 group-hover:opacity-100 group-focus:translate-x-0.25 group-focus:-translate-y-0.25 group-focus:opacity-100 xl:opacity-0"
/>
</a>
</p>
{/key}
</div>

View File

@@ -1,61 +1,124 @@
<script lang="ts" module>
import { classNames } from '$lib/utils/classnames';
import { writable } from 'svelte/store';
import { animate } from 'motion';
export const tooltipData = writable<{
let tooltipData = $state<{
city: string | null;
code: string | null;
available: boolean | null;
available?: boolean | null;
date?: string | null;
}>({
city: null,
code: null,
available: null
available: null,
date: null
});
export const handleSetActiveTooltip = (
city: string,
code: string,
available?: boolean,
date?: string
) => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
tooltipData = {
city,
code,
available,
date
};
};
let timeoutId: ReturnType<typeof setTimeout> | null = null;
export const handleResetActiveTooltip = (delay?: number) => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (delay) {
timeoutId = setTimeout(() => {
tooltipData = {
city: null,
code: null,
available: null,
date: null
};
timeoutId = null;
}, delay);
} else {
tooltipData = {
city: null,
code: null,
available: null,
date: null
};
}
};
</script>
<script lang="ts">
type Props = {
coords: {
import { classNames } from '$lib/utils/classnames';
interface TooltipProps {
x: number;
y: number;
};
theme?: 'light' | 'dark';
}
theme: 'light' | 'dark';
};
const { x, y, theme = 'light' }: TooltipProps = $props();
const { coords, theme = 'dark' }: Props = $props();
let city = $state<HTMLElement | null>(null);
$effect(() => {
if (!city) return;
animate(city, { y: [-5, 0], filter: ['blur(4px)', '0px'] }, { duration: 0.2 });
});
</script>
{#if $tooltipData.city}
<div
class="pointer-events-none absolute"
style:left="{coords.x - 50}px"
style:top="{coords.y - 50}px"
>
<div class={classNames('pointer-events-none absolute z-100 hidden md:block', theme)}>
{#if tooltipData.city}
<div
class={classNames(
'border-gradient relative z-100 flex w-[190px] flex-col gap-2 rounded-[10px] p-2 backdrop-blur-lg before:rounded-[10px] after:rounded-[10px]',
'data-[state="closed"]:animate-menu-out data-[state="instant-open"]:animate-menu-in data-[state="delayed-open"]:animate-menu-in',
theme === 'dark' ? 'bg-card/90' : 'bg[var(--card, rgba(255,255,255))]'
{ 'bg-transparent': theme === 'dark', 'bg-white': theme === 'light' }
)}
style:transform={`translateX(${x + 20}px) translateY(${y - 425}px)`}
>
<span class="text-primary text-caption w-fit">
{$tooltipData.city}
({$tooltipData.code})
{#key tooltipData.city}
<span class="text-primary text-caption w-fit" bind:this={city}>
{tooltipData.city}
({tooltipData.code})
</span>
{#if $tooltipData.available}
{/key}
{#if tooltipData.available}
<div
class="text-caption flex h-5 items-center justify-center place-self-start rounded-md bg-[#10B981]/24 p-1 text-center text-[#B4F8E2]"
class={classNames(
'text-caption flex h-5 items-center justify-center place-self-start rounded-md p-1 text-center',
{
'bg-[#10B981]/16 text-[#0A714F]': theme === 'light',
'bg-[#10B981]/24 text-[#B4F8E2]': theme === 'dark'
}
)}
>
<span class="text-micro -tracking-tight">Available now</span>
</div>
{:else}
<div
class="text-caption flex h-5 items-center justify-center place-self-start rounded-md bg-white/6 p-1 text-center text-white/60"
class={classNames(
'text-caption text-primary flex h-5 items-center justify-center place-self-start rounded-md bg-black/6 p-1 text-center',
{
'text-primary bg-black/6': theme === 'light',
'text-primary bg-white/6': theme === 'dark'
}
)}
>
<span class="text-micro -tracking-tight">Planned</span>
<span class="text-micro -tracking-tight">{tooltipData.date}</span>
</div>
{/if}
</div>
</div>
{/if}
{/if}
</div>

View File

@@ -1,35 +1,25 @@
<script lang="ts" module>
export const MAP_BOUNDS = $state({
west: -138,
east: 167,
north: 74,
south: -62
});
</script>
<script lang="ts">
import MapMarker from './map-marker.svelte';
import { slugify } from '$lib/utils/slugify';
import { classNames } from '$lib/utils/classnames';
import MapNav from './map-nav.svelte';
import { useMousePosition } from '$lib/actions/mouse-position';
import { useMousePosition } from '$lib/actions/mouse-position.svelte';
import { useAnimateInView } from '$lib/actions/animate-in-view';
import { pins, type PinSegment } from './data/pins';
import MapTooltip from './map-tooltip.svelte';
import MapTooltip, {
handleSetActiveTooltip,
handleResetActiveTooltip
} from './map-tooltip.svelte';
import { createMap } from 'svg-dotted-map';
import { browser } from '$app/environment';
let dimensions = $state({
width: 0,
height: 0
});
let activeRegion = $state<string | null>(null);
let activeMarker: HTMLElement | null = null;
let activeSegment = $state<string>('pop-locations');
let activeMarkers = $derived(pins[activeSegment as PinSegment]);
let activeRegion = $state<string | null>(null);
let activeMarker: SVGGElement | null = null;
const { action: mousePosition, position } = useMousePosition();
const { action: inView, animate } = useAnimateInView({});
const { action: inView } = useAnimateInView();
const scrollMarkerIntoView = (marker: HTMLElement) => {
const scrollMarkerIntoView = (marker: SVGGElement) => {
return new Promise<void>((resolve) => {
marker.scrollIntoView({
behavior: 'smooth',
@@ -68,65 +58,81 @@
}
};
type Props = { theme: 'light' | 'dark' };
const radius = 0.4;
const height = 75;
const { points, addMarkers } = createMap({
width: height * 2,
height,
mapSamples: 5000,
radius
});
const markers = $derived(
addMarkers<{ city: string; code: string; available?: boolean; date?: string }>(
activeMarkers
)
);
type Props = {
theme: 'light' | 'dark';
};
const { theme = 'dark' }: Props = $props();
</script>
<div class="-mt-8 w-full overflow-x-scroll [scrollbar-width:none] md:overflow-x-hidden">
<div class="relative w-full overflow-x-scroll [scrollbar-width:none]">
<div class="relative mx-auto h-full [scrollbar-width:none] md:w-full" use:inView>
<div
class="sticky left-0 mx-auto block max-w-[calc(100vw_-_calc(var(--spacing)_*-2))] md:hidden"
>
<select
class="web-input-text mx-auto appearance-none"
onchange={(e) => handleSetActiveMarker(e.currentTarget.value)}
>
{#each pins[activeSegment as PinSegment] as pin}
<option value={pin.city}>{pin.city}-({pin.code})</option>
{/each}
</select>
</div>
<div
class="relative mx-auto h-full w-[250vw] [scrollbar-width:none] md:w-fit"
use:inView
class="relative mx-auto my-10 h-fit w-full max-w-5xl origin-bottom transform-[perspective(25px)_rotateX(1deg)_scale3d(1.2,_1.2,_1)] transition-all [scrollbar-width:none] md:my-0 md:-translate-x-20"
use:mousePosition
>
<div
class="relative w-full origin-bottom transform-[perspective(25px)_rotateX(1deg)_scale3d(1.4,_1.4,_1)] transition-all [scrollbar-width:none]"
bind:clientWidth={dimensions.width}
bind:clientHeight={dimensions.height}
>
<div
class="absolute inset-0 mask-[image:url('/images/appwrite-network/map.svg')] mask-contain mask-no-repeat"
>
<div
class={classNames(
'relative block aspect-square size-40 rounded-full blur-3xl transition-opacity',
'from-accent bg-radial-[circle_at_center] via-white/70 to-white/70',
'transform-[translate3d(calc(var(--mouse-x,_-100%)_*_1_-_16rem),_calc(var(--mouse-y,_-100%)_*_1_-_28rem),0)]'
)}
style:--mouse-x="{$position.x}px"
style:--mouse-y="{$position.y}px"
></div>
</div>
<!-- TODO: reusing the same image but inverted! use a variable -->
<img
draggable="false"
alt="Map of the world"
src="/images/appwrite-network/map.svg"
style:filter={theme === 'light' ? 'invert()' : undefined}
class="pointer-events-none relative -z-10 w-full opacity-10 md:max-h-[525px]"
<svg viewBox={`0 0 ${height * 2} ${height}`}>
{#each points as point}
<ellipse
cx={point.x}
cy={point.y}
rx={radius}
ry={radius * 1.25}
fill={theme === 'dark' ? 'rgba(255,255,255,.1)' : '#dadadd'}
/>
{#each pins[activeSegment] as pin, index}
<MapMarker {...pin} animate={$animate} {index} bounds={MAP_BOUNDS} />
{/each}
{#each markers as marker}
<g
role="tooltip"
class="animate-fade-in outline-none"
aria-label={`${marker.city} (${marker.code})`}
onmouseover={() =>
handleSetActiveTooltip(
marker.city,
marker.code,
marker.available,
marker.date
)}
onfocus={() =>
handleSetActiveTooltip(
marker.city,
marker.code,
marker.available,
marker.date
)}
onblur={() => handleResetActiveTooltip()}
onmouseout={() => handleResetActiveTooltip()}
data-region={slugify(marker.city)}
>
<circle cx={marker.x} cy={marker.y} r={radius * 1.25} class="fill-accent" />
<circle cx={marker.x} cy={marker.y} r={radius * 0.5} class="fill-white" />
<circle
cx={marker.x}
cy={marker.y}
r={radius * 4}
class="fill-transparent"
/>
</g>
{/each}
</svg>
</div>
</div>
</div>
<MapTooltip {theme} coords={$position} />
<MapTooltip {theme} {...position()} />
<MapNav {theme} onValueChange={(value) => (activeSegment = value)} />

View File

@@ -1,24 +0,0 @@
import { MAP_BOUNDS } from '../map.svelte';
const MAP_WIDTH = 1048.25;
const MAP_HEIGHT = 525;
type Coordinates = {
latitude: number;
longitude: number;
};
export const latLongToSvgPosition = ({ latitude, longitude }: Coordinates) => {
const { west, east, north, south } = MAP_BOUNDS;
const lngRatio = (longitude - west) / (east - west);
const latRatio = (latitude - south) / (north - south);
const clampedLngRatio = Math.max(0, Math.min(1, lngRatio));
const clampedLatRatio = Math.max(0, Math.min(1, latRatio));
const x = clampedLngRatio * 100;
const y = (1 - clampedLatRatio) * 100;
return { x, y }; // percentages, e.g., { x: 42.3, y: 71.8 }
};

View File

@@ -1,20 +1,25 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
export let text: string;
const words = text.split(' ');
interface Props {
text: string;
class?: string;
}
let className: string = '';
export { className as class };
let { text, class: className = '' }: Props = $props();
const words = text.split(' ');
</script>
<span class="sr-only">{text}</span>
<span class={classNames('relative', className)}>
<span class={classNames('relative overflow-hidden', className)}>
{#each words as word, i}
<span class="relative overflow-hidden">
<span
class="animate-text mr-2 inline-block"
class="animate-enter mr-2 inline-block"
style:animation-delay="{i * 75}ms
">{word}</span
>
</span>
{/each}
</span>

View File

@@ -0,0 +1,67 @@
<script lang="ts">
import type { SvelteHTMLElements } from 'svelte/elements';
import { animate, type AnimationSequence } from 'motion';
interface CheckmarkProps extends Omit<SvelteHTMLElements['svg'], 'viewBox' | 'xmlns'> {
play?: boolean;
duration?: number;
}
const { class: classNames, duration, play, ...restProps }: CheckmarkProps = $props();
const sequence: AnimationSequence = [
[
'.circle',
{
strokeDashoffset: [-360, 0]
}
],
[
'.check',
{
strokeDashoffset: [48, 0]
}
],
['.checkmark', { scale: [1, 0.98, 1] }, { at: 0.75 }]
];
$effect(() => {
const controls = animate(sequence, {
defaultTransition: { duration: duration ?? 0.5, ease: 'circInOut' }
});
if (play) {
controls.play();
} else {
controls.pause();
}
});
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 32 32"
class={classNames}
{...restProps}
>
<path
class="circle origin-center translate-0 -rotate-180"
d="M16 28C22.6274 28 28 22.6274 28 16C28 9.37258 22.6274 4 16 4C9.37258 4 4 9.37258 4 16C4 22.6274 9.37258 28 16 28Z"
stroke="currentColor"
stroke-width="1.5"
stroke-dasharray="360"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
class="check"
d="M11 17L14 20L21 13"
stroke="currentColor"
stroke-width="1.5"
stroke-dasharray="48"
stroke-dashoffset="48"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

View File

@@ -1,17 +1,22 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
import AnimatedText from './animated-text.svelte';
import type { Snippet } from 'svelte';
import type { SvelteHTMLElements } from 'svelte/elements';
const className = '';
type Props = {
class?: string;
children: Snippet;
} & SvelteHTMLElements['span'];
export { className as class };
const { class: className, children, ...rest }: Props = $props();
</script>
<span
class={classNames(
'-mb-1 block bg-[linear-gradient(6deg,_#f8a1ba,_#fff_35%)] bg-clip-text pb-1 text-transparent',
'-mb-1 block bg-linear-145 from-[#f8a1ba] to-white to-50% bg-clip-text pb-1 text-transparent',
className
)}
{...rest}
>
<slot />
{@render children()}
</span>

View File

@@ -1,20 +1,22 @@
<script lang="ts">
import type { SvelteHTMLElements } from 'svelte/elements';
import { classNames } from '$lib/utils/classnames';
interface $$Props {
type Props = {
invert?: boolean;
opacity?: number;
grainSize?: number;
animate?: boolean;
class?: string;
}
} & SvelteHTMLElements['svg'];
let className: string = '';
export let invert: boolean = false;
export let opacity: number = 1;
export let grainSize: number = 2.5;
export let animate: boolean = false;
export { className as class };
const {
invert = false,
opacity = 1,
grainSize = 2.5,
animate = false,
class: className = '',
...rest
}: Props = $props();
const baseFrequency = grainSize! / 1;
</script>
@@ -23,9 +25,10 @@
width="100%"
height="100%"
xmlns="http://www.w3.org/2000/svg"
class={classNames('pointer-events-none absolute inset-0 rounded-[inherit]', className)}
class={classNames('pointer-events-none absolute inset-0', className)}
style:opacity
style:filter={invert ? 'invert(1)' : 'none'}
{...rest}
>
<filter id="noise">
<feTurbulence

View File

@@ -0,0 +1,54 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
import type { SvelteHTMLElements } from 'svelte/elements';
import { navState } from './menu-state.svelte';
type Props = {
class?: string;
} & SvelteHTMLElements['button'];
const {
class: className,
'aria-label': ariaLabel = 'Open navigation menu',
onclick,
...rest
}: Props = $props();
const toggleMobileNav = () => {
navState.isOpen = !navState.isOpen;
};
</script>
<button
aria-label={ariaLabel}
class={classNames(
'focus:ring-accent flex size-7 cursor-pointer appearance-none items-center justify-center rounded-xs',
className
)}
onclick={(e) => {
toggleMobileNav();
onclick?.(e);
}}
{...rest}
>
<span class="h-4.5 w-7">
<span
class={classNames(
'dark:bg-primary relative block h-px w-6 translate-y-1 bg-gray-800 transition-all duration-200 ease-in-out',
'before:bg-primary before:absolute before:bottom-1 before:left-0 before:block before:h-px before:w-7',
'before:ease-in-out before:[transition:bottom_200ms_200ms,transform_200ms]',
'after:bg-primary after:absolute after:top-1 after:left-0 after:block after:h-px after:w-7',
'after:ease-in-out after:[transition:top_200ms_200ms,transform_200ms]',
{
'!bg-transparent': navState.isOpen,
'before:bottom-0 before:rotate-45 before:[transition:bottom_200ms,transform_200ms_200ms]':
navState.isOpen,
'after:top-0 after:-rotate-45 after:[transition:top_200ms,transform_200ms_200ms]':
navState.isOpen
}
)}
></span>
</span>
</button>
<svelte:window onresize={() => (navState.isOpen = false)} />

View File

@@ -0,0 +1,5 @@
export let navState = $state<{
isOpen: boolean;
}>({
isOpen: false
});

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import { NavigationMenu } from 'bits-ui';
import type { Snippet } from 'svelte';
type TriggerProps = NavigationMenu.TriggerProps;
type ListItemProps = {
label: string;
content: string;
} & NavigationMenu.LinkProps;
type Props = { children: Snippet } & NavigationMenu.RootProps;
const { class: className, children, ...rest }: Props = $props();
</script>
<NavigationMenu.Content
class="data-[motion=from-end]:animate-enter-from-right data-[motion=from-start]:animate-enter-from-left data-[motion=to-end]:animate-exit-to-right data-[motion=to-start]:animate-exit-to-left absolute top-0 left-0 w-full sm:w-auto"
>
<div class="grid gap-3 p-3 sm:w-[400px] sm:p-6 md:w-[600px] md:grid-cols-2 lg:w-[800px]">
{@render children()}
</div>
</NavigationMenu.Content>

View File

@@ -0,0 +1,54 @@
<script lang="ts">
import { NavigationMenu } from 'bits-ui';
import MenuWrapper from './menu-wrapper.svelte';
import { classNames } from '$lib/utils/classnames';
const products: { name: string; href: string; description: string }[] = [
{
name: 'Auth',
href: '/products/auth',
description: 'Secure login with multi-factor auth.'
},
{
name: 'Databases',
href: '/docs/products/databases',
description: 'Scalable and robust databases.'
},
{
name: 'Storage',
href: '/products/storage',
description: 'Advanced compression and encryption.'
},
{
name: 'Functions',
href: '/products/functions',
description: 'Deploy & scale serverless functions.'
},
{
name: 'Messaging',
href: '/products/messaging',
description: 'Set up a full-functioning messaging service.'
},
{
name: 'Realtime',
href: '/docs/apis/realtime',
description: 'Subscribe and react to any event.'
}
];
</script>
<MenuWrapper>
<div class="grid gap-3 p-3 sm:w-[400px] sm:p-6 md:w-[600px] md:grid-cols-2 lg:w-[800px]">
{#each products as product}
<NavigationMenu.Link
class="hover:bg-muted hover:text-accent-foreground focus:bg-muted focus:text-accent-foreground block space-y-1 rounded-md p-3 leading-none no-underline outline-hidden transition-colors select-none"
href={product.href}
>
<div class="text-sm leading-none font-medium">{product.name}</div>
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
{product.description}
</p>
</NavigationMenu.Link>
{/each}
</div>
</MenuWrapper>

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import { Collapsible } from 'bits-ui';
import { Icon } from '$lib/components/ui';
import { classNames } from '$lib/utils/classnames';
const products: { name: string; href: string; description: string }[] = [
{
name: 'Auth',
href: '/products/auth',
description: 'Secure login with multi-factor auth.'
},
{
name: 'Databases',
href: '/docs/products/databases',
description: 'Scalable and robust databases.'
},
{
name: 'Storage',
href: '/products/storage',
description: 'Advanced compression and encryption.'
},
{
name: 'Functions',
href: '/products/functions',
description: 'Deploy & scale serverless functions.'
},
{
name: 'Messaging',
href: '/products/messaging',
description: 'Set up a full-functioning messaging service.'
},
{
name: 'Realtime',
href: '/docs/apis/realtime',
description: 'Subscribe and react to any event.'
}
];
type CollapsibleItemProps = {
className?: string;
label: string;
content: string;
};
</script>
{#snippet ListItem({ className, label }: CollapsibleItemProps)}
<Collapsible.Root>
<Collapsible.Trigger>{label}</Collapsible.Trigger>
<Collapsible.Content
class={classNames(
'hover:bg-muted hover:text-accent-foreground focus:bg-muted focus:text-accent-foreground block space-y-1 rounded-md p-3 leading-none no-underline outline-hidden transition-colors select-none',
className
)}
>
<div class="text-sm leading-none font-medium">{name}</div>
</Collapsible.Content>
</Collapsible.Root>
{/snippet}

View File

@@ -0,0 +1,67 @@
<script lang="ts">
import { NavigationMenu } from 'bits-ui';
import { classNames } from '$lib/utils/classnames';
import type { Component, SvelteComponent } from 'svelte';
import ProductMenu from './menus/product-menu.svelte';
import { Icon } from '$lib/components/ui';
type NavItem =
| ({ label: string; href: string } & { menu?: never })
| {
label: string;
menu: Component;
href?: never;
};
export const navItems: Array<NavItem> = [
{ label: 'Products', menu: ProductMenu },
{
label: 'Docs',
menu: ProductMenu
},
{
label: 'Pricing',
href: '/pricing'
},
{
label: 'Enterprise',
href: '/enterprise'
}
];
type Props = NavigationMenu.RootProps;
const { class: className, ...rest }: Props = $props();
</script>
<NavigationMenu.Root class={className} {...rest}>
<NavigationMenu.List class="flex items-center gap-8">
{#each navItems as item}
<NavigationMenu.Item class="hover:text-accent transition-colors">
{#if item.menu}
{@const Submenu = item.menu}
<NavigationMenu.Trigger class="group flex items-center gap-3"
>{item.label}
<Icon
name="chevron-down"
class="relative size-4 transition-transform duration-200 group-data-[state=open]:-rotate-180"
aria-hidden="true"
/></NavigationMenu.Trigger
>
<Submenu />
{:else}
<NavigationMenu.Link href={item.href}>{item.label}</NavigationMenu.Link>
{/if}
</NavigationMenu.Item>
{/each}
</NavigationMenu.List>
<div class="absolute top-full left-0 flex w-full justify-center perspective-[2000px]">
<NavigationMenu.Viewport
class={classNames(
'bg-greyscale-850 border-smooth relative w-full origin-[top_center] overflow-hidden rounded-b-md border-x border-b opacity-100 shadow-lg backdrop-blur-2xl transition-[width,_height] duration-200 before:rounded-md after:rounded-md',
'data-[state=open]:animate-scale-in data-[state=closed]:hidden',
'h-(--bits-navigation-menu-viewport-height) sm:w-(--bits-navigation-menu-viewport-width)'
)}
/>
</div>
</NavigationMenu.Root>

View File

@@ -0,0 +1,158 @@
<script lang="ts">
import { createAccordion, melt } from '@melt-ui/svelte';
import { slide } from 'svelte/transition';
export let noBorder = false;
const {
elements: { content, heading, item, root, trigger },
helpers: { isSelected }
} = createAccordion({
multiple: true,
forceVisible: true
});
const links: Record<string, { label: string; href: string; target?: string; rel?: string }[]> =
{
'Quick starts': [
{ label: 'Web', href: '/docs/quick-starts/web' },
{ label: 'Next.js', href: '/docs/quick-starts/nextjs' },
{ label: 'React', href: '/docs/quick-starts/react' },
{ label: 'Vue.js', href: '/docs/quick-starts/vue' },
{ label: 'Nuxt', href: '/docs/quick-starts/nuxt' },
{ label: 'SvelteKit', href: '/docs/quick-starts/sveltekit' },
{ label: 'Refine', href: '/docs/quick-starts/refine' },
{ label: 'Angular', href: '/docs/quick-starts/angular' },
{ label: 'React Native', href: '/docs/quick-starts/react-native' },
{ label: 'Flutter', href: '/docs/quick-starts/flutter' },
{ label: 'Apple', href: '/docs/quick-starts/apple' },
{ label: 'Android', href: '/docs/quick-starts/android' },
{ label: 'Qwik', href: '/docs/quick-starts/qwik' },
{ label: 'Astro', href: '/docs/quick-starts/astro' },
{ label: 'Solid', href: '/docs/quick-starts/solid' }
],
Products: [
{ label: 'Auth', href: '/products/auth' },
{ label: 'Databases', href: '/docs/products/databases' },
{ label: 'Functions', href: '/products/functions' },
{ label: 'Messaging', href: '/products/messaging' },
{ label: 'Storage', href: '/products/storage' },
{ label: 'Realtime', href: '/docs/apis/realtime' }
],
Learn: [
{ label: 'Docs', href: '/docs' },
{ label: 'Integrations', href: '/integrations' },
{ label: 'Community', href: '/community' },
{ label: 'Init', href: '/init' },
{ label: 'Threads', href: '/threads' },
{ label: 'Blog', href: '/blog' },
{ label: 'Changelog', href: '/changelog' },
{
label: 'Roadmap',
href: 'https://github.com/orgs/appwrite/projects',
target: '_blank',
rel: 'noopener noreferrer'
},
{
label: 'Source code',
href: 'https://github.com/appwrite',
target: '_blank',
rel: 'noopener noreferrer'
}
// {
// label: 'Status',
// href: 'https://appwrite.online',
// target: '_blank',
// rel: 'noopener noreferrer'
// }
],
Programs: [
{ label: 'Heroes', href: '/heroes' },
{ label: 'Startups', href: '/startups' },
{ label: 'Education', href: '/education' }
],
About: [
{ label: 'Company', href: '/company' },
{ label: 'Pricing', href: '/pricing' },
{
label: 'Careers',
href: 'https://appwrite.careers',
target: '_blank',
rel: 'noopener noreferrer'
},
{
label: 'Store',
href: 'https://appwrite.store',
target: '_blank',
rel: 'noopener noreferrer'
},
{ label: 'Contact us', href: '/contact-us' },
{ label: 'Assets', href: '/assets' }
]
};
</script>
<nav
aria-label="Footer"
class="border-smooth relative border-t"
class:web-u-sep-block-start={!noBorder}
>
<div class="web-footer-nav container">
<img
class="web-logo"
src="/images/logos/appwrite.svg"
alt="appwrite"
height="24"
width="130"
/>
<ul class="web-footer-nav-main-list" use:melt={$root}>
{#each Object.entries(links) as [title, items]}
<li class="web-footer-nav-main-item web-is-not-mobile">
<h2
class="web-footer-nav-main-title web-is-not-mobile text-caption font-medium uppercase"
>
{title}
</h2>
<ul class="web-footer-nav-secondary-list text-sub-body">
{#each items as { href, label, target, rel }}
<li>
<a class="web-link" {href} {target} {rel}>{label}</a>
</li>
{/each}
</ul>
</li>
<li
class="web-footer-nav-main-item web-is-only-mobile"
use:melt={$item({ value: title })}
>
<h5 use:melt={$heading({ level: 5 })}>
<button
class="web-footer-nav-button web-is-only-mobile"
use:melt={$trigger({ value: title })}
>
<span class="text-caption font-medium uppercase">{title}</span>
<span
class="web-icon-chevron-down web-u-transition"
class:web-u-rotate-180={$isSelected(title)}
style:font-size="1rem"
/>
</button>
</h5>
{#if $isSelected(title)}
<ul
class="web-footer-nav-secondary-list text-sub-body"
use:melt={$content({ value: title })}
transition:slide={{ duration: 250 }}
>
{#each items as { href, label, target, rel }}
<li>
<a class="web-link" {href} {target} {rel}>{label}</a>
</li>
{/each}
</ul>
{/if}
</li>
{/each}
</ul>
</div>
</nav>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { Button } from '../ui';
import HamburgerMenu from './navigation/hamburger-menu.svelte';
import PrimaryNav from './navigation/primary-nav.svelte';
</script>
<header
class="border-smooth sticky top-0 z-1000 flex h-[4.5rem] items-center border-b backdrop-blur-md"
>
<div class="container flex flex-1 items-center justify-between">
<a href="/">
<img
class="hidden dark:block"
src="/images/logos/appwrite.svg"
alt="appwrite"
height="24"
width="130"
/></a
>
<PrimaryNav class="hidden md:block" />
<Button class="hidden! md:flex!">Start building for free</Button>
<HamburgerMenu class="block md:hidden" />
</div>
</header>

View File

@@ -0,0 +1,43 @@
<script lang="ts">
import { socials } from '$lib/constants';
const year = new Date().getFullYear();
</script>
<footer
class="border-smooth text-secondary text-micro relative container mt-12 flex items-center justify-between gap-4 border-t py-6"
>
<ul class="flex items-center gap-2">
{#each socials as social}
<li>
<a
href={social.link}
class="web-icon-button"
aria-label={social.label}
target="_blank"
rel="noopener noreferrer"
>
<span class={social.icon} aria-hidden="true" />
</a>
</li>
{/each}
</ul>
<div class="flex items-center gap-3">
<div>Copyright © {year} Appwrite</div>
<iframe
class="status w-fit max-w-[230px]"
title="Appwrite Status"
src="https://status.appwrite.online/badge?theme=dark"
height="35"
frameborder="0"
scrolling="no"
style:color-scheme="none"
/>
<ul class="flex gap-4">
<li><a class="web-link" href="/terms">Terms</a></li>
<li><a class="web-link" href="/privacy">Privacy</a></li>
<li><a class="web-link" href="/cookies">Cookies</a></li>
</ul>
</div>
</footer>

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
import type { SvelteHTMLElements } from 'svelte/elements';
type BlockquoteElementProps = SvelteHTMLElements['blockquote'];
interface PullquoteProps extends BlockquoteElementProps {
name: string;
title: string;
avatar: string;
}
const { class: className, children, title, avatar, name }: PullquoteProps = $props();
</script>
<div class="container mx-auto">
<blockquote
class={classNames(
className,
'/font-aeonik-pro mx-auto flex w-full max-w-[30rem] flex-col items-center justify-center gap-3 pb-16 text-center'
)}
>
<h2 class="text-description text-primary font-medium">
<span class="text-accent -mr-1"></span>
{@render children?.()}
<span class="text-accent -ml-1"></span>
</h2>
<div class="flex items-center gap-2">
<img src={avatar} alt={name} class="size-6 rounded-full" />
<h5 class="text-caption text-primary font-medium">
{name}, <span class="text-secondary">{title}</span>
</h5>
</div>
</blockquote>
</div>

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import {
getInlinedScriptTag,
softwareAppSchema,
organizationJsonSchema,
DEFAULT_HOST
} from '$lib/utils/metadata';
type Props = {
title?: string;
description?: string;
ogImage?: string;
};
const {
title = 'Appwrite',
description = 'Appwrite is an open-source platform for building applications at any scale, using your preferred programming languages and tools.',
ogImage = `${DEFAULT_HOST}/images/open-graph/website.png`
}: Props = $props();
</script>
<svelte:head>
<title>{title}</title>
<meta property="og:title" content={title} />
<meta name="twitter:title" content={title} />
<meta name="description" content={description} />
<meta property="og:description" content={description} />
<meta name="twitter:description" content={description} />
<meta property="og:image" content={ogImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:image" content={ogImage} />
<meta name="twitter:card" content="summary_large_image" />
{@html getInlinedScriptTag(softwareAppSchema())}
{@html getInlinedScriptTag(organizationJsonSchema())}
</svelte:head>

View File

@@ -1,4 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
const allProducts = {
messaging: {
title: 'Messaging',
@@ -44,21 +46,8 @@
}
} as const;
type Props = {
exclude?:
| 'messaging'
| 'functions'
| 'databases'
| 'storage'
| 'auth'
| 'realtime'
| 'sites';
};
const { exclude }: Props = $props();
const products = Object.entries(allProducts)
.filter(([key]) => key !== exclude)
.filter(([key]) => key !== page.url.pathname.replace('/products/', ''))
.map(([_, value]) => value);
</script>

View File

@@ -42,7 +42,7 @@
<div class="group light flex w-fit gap-4">
{#each Array.from({ length: 4 }) as _, i}
<div
class="animate-scroll group-hover:[animation-play-state:paused;] flex items-center gap-8"
class="animate-scroll-deprecategroup-hover:[animation-play-state:paused;] flex items-center gap-8"
aria-hidden={i !== 0}
>
{#each testimonials as testimonial}

View File

@@ -2,7 +2,7 @@
import { classNames } from '$lib/utils/classnames';
import type { HTMLButtonAttributes, HTMLAnchorAttributes } from 'svelte/elements';
import { cva, type VariantProps } from 'cva';
import InlineTag from '../ui/InlineTag.svelte';
import InlineTag from '../ui/inline-tag.svelte';
const button = cva(
[

View File

@@ -27,14 +27,14 @@
{@render children()}
</div>
{:else}
<div
<button
class="contents cursor-pointer"
on:click={() => {
onclick={() => {
if (browser && window) window.open(url, '_blank');
}}
>
{@render children()}
</div>
</button>
{/if}
{#if $open}

View File

@@ -1,13 +1,11 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
import type { SvelteHTMLElements } from 'svelte/elements';
import type { SVGAttributes } from 'svelte/elements';
import type { IconType } from './types';
type Props = SvelteHTMLElements['svg'] & {
type Props = SVGAttributes<SVGElement> & {
class?: string;
name: IconType;
name?: IconType;
};
const {
xmlns = 'http://www.w3.org/2000/svg',
viewBox = '0 0 24 24',

View File

@@ -189,7 +189,7 @@
</symbol>
<symbol id="edge" stroke="currentColor" viewBox="0 0 21 20">
<path
d="M9.925 4.028 C 9.521 4.066,8.877 4.204,8.524 4.328 C 8.123 4.469,7.392 4.849,7.035 5.101 C 6.608 5.403,5.892 6.119,5.591 6.545 C 5.321 6.929,4.984 7.584,4.835 8.018 C 4.401 9.277,4.401 10.708,4.835 11.967 C 5.340 13.435,6.497 14.723,7.918 15.400 C 10.018 16.401,12.497 16.102,14.298 14.630 C 14.650 14.342,15.135 13.821,15.408 13.438 C 15.675 13.063,16.010 12.412,16.165 11.967 C 16.599 10.715,16.599 9.270,16.165 8.018 C 15.445 5.946,13.523 4.360,11.374 4.065 C 10.964 4.009,10.306 3.992,9.925 4.028 M11.305 5.269 C 12.589 5.477,13.793 6.266,14.514 7.372 C 15.950 9.575,15.343 12.526,13.153 13.986 C 12.377 14.505,11.454 14.781,10.500 14.781 C 9.605 14.781,8.772 14.552,8.033 14.104 C 6.863 13.395,6.055 12.239,5.786 10.891 C 5.702 10.471,5.702 9.514,5.786 9.094 C 6.040 7.821,6.785 6.700,7.840 6.005 C 8.366 5.658,9.088 5.368,9.663 5.271 C 10.068 5.204,10.894 5.203,11.305 5.269 "
d="M3.233 2.465 C 3.093 2.527,2.942 2.724,2.917 2.882 C 2.864 3.205,2.798 3.125,4.745 5.075 C 5.735 6.067,6.545 6.888,6.545 6.899 C 6.545 6.910,6.456 7.052,6.347 7.213 C 5.989 7.746,5.709 8.426,5.576 9.091 C 5.484 9.547,5.493 10.505,5.593 10.961 C 5.817 11.991,6.256 12.801,6.983 13.528 C 7.726 14.272,8.706 14.769,9.748 14.930 C 10.245 15.006,11.107 14.982,11.550 14.880 C 12.187 14.732,12.812 14.461,13.315 14.116 L 13.592 13.925 15.380 15.709 C 16.363 16.690,17.204 17.511,17.249 17.534 C 17.294 17.557,17.398 17.576,17.480 17.576 C 17.887 17.576,18.153 17.252,18.079 16.846 C 18.058 16.728,17.850 16.507,16.255 14.910 C 15.265 13.918,14.455 13.096,14.455 13.083 C 14.455 13.070,14.551 12.917,14.667 12.742 C 15.023 12.211,15.283 11.564,15.423 10.866 C 15.515 10.405,15.507 9.482,15.407 9.024 C 15.184 8.000,14.743 7.180,14.037 6.474 C 13.330 5.767,12.455 5.296,11.463 5.089 C 11.016 4.996,9.984 4.996,9.538 5.089 C 8.884 5.226,8.252 5.491,7.699 5.861 L 7.405 6.058 5.619 4.275 C 4.636 3.294,3.798 2.475,3.756 2.454 C 3.652 2.401,3.366 2.407,3.233 2.465 M10.982 6.231 C 11.798 6.333,12.514 6.681,13.131 7.273 C 13.710 7.829,14.079 8.505,14.225 9.275 C 14.302 9.677,14.296 10.365,14.214 10.763 C 13.933 12.115,12.893 13.248,11.581 13.628 C 10.701 13.883,9.800 13.826,8.963 13.463 C 8.213 13.138,7.483 12.444,7.114 11.708 C 6.840 11.160,6.720 10.638,6.720 9.992 C 6.720 9.159,6.926 8.493,7.392 7.816 C 7.594 7.523,8.085 7.039,8.375 6.848 C 9.162 6.329,10.062 6.116,10.982 6.231 "
fill="currentColor"
stroke="none"
fill-rule="evenodd"
@@ -385,6 +385,14 @@
fill="currentColor"
></path>
</symbol>
<symbol id="remix" stroke="currentColor" viewBox="0 0 20 20">
<path
d="M4.500 4.430 L 4.500 5.864 7.558 5.875 C 10.958 5.886,10.756 5.873,11.250 6.116 C 11.552 6.264,11.697 6.387,11.864 6.637 C 12.064 6.937,12.138 7.207,12.141 7.650 C 12.144 8.103,12.095 8.350,11.942 8.643 C 11.800 8.914,11.631 9.070,11.317 9.219 C 10.830 9.450,10.919 9.444,7.542 9.458 L 4.500 9.470 4.500 10.917 L 4.500 12.364 7.425 12.375 C 10.607 12.388,10.509 12.382,10.893 12.579 C 11.334 12.804,11.577 13.269,11.664 14.050 C 11.726 14.604,11.771 15.911,11.748 16.458 L 11.724 17.000 13.434 17.000 L 15.144 17.000 15.123 16.025 C 15.075 13.762,14.977 13.042,14.624 12.350 C 14.520 12.148,14.418 12.014,14.202 11.800 C 13.868 11.468,13.649 11.331,13.213 11.181 C 12.873 11.064,12.813 11.033,12.926 11.033 C 13.030 11.033,13.525 10.872,13.717 10.775 C 14.682 10.288,15.313 9.298,15.470 8.023 C 15.521 7.616,15.487 6.775,15.404 6.363 C 15.064 4.683,14.059 3.644,12.344 3.201 C 11.653 3.022,11.706 3.025,7.975 3.010 L 4.500 2.996 4.500 4.430 M4.500 15.916 L 4.500 17.000 6.652 17.000 L 8.804 17.000 8.794 16.158 L 8.783 15.317 8.692 15.161 C 8.642 15.076,8.544 14.971,8.475 14.928 L 8.350 14.850 6.425 14.841 L 4.500 14.831 4.500 15.916 "
fill="currentColor"
stroke="none"
fill-rule="evenodd"
></path>
</symbol>
<symbol id="rest" stroke="currentColor" viewBox="0 0 24 24">
<path
d="M4.640 2.425 C 4.589 2.435,4.499 2.466,4.440 2.494 C 4.316 2.553,3.392 3.456,2.373 4.511 C 1.626 5.286,1.627 5.285,1.627 5.597 C 1.627 5.875,1.814 6.169,2.080 6.310 C 2.215 6.382,2.612 6.380,2.760 6.307 C 2.820 6.278,3.113 6.012,3.432 5.698 L 3.998 5.140 4.006 7.470 L 4.013 9.800 4.075 9.926 C 4.204 10.188,4.528 10.400,4.800 10.400 C 5.072 10.400,5.396 10.188,5.525 9.926 L 5.587 9.800 5.594 7.471 L 5.602 5.141 6.181 5.711 C 6.513 6.037,6.807 6.304,6.867 6.333 C 6.935 6.366,7.048 6.389,7.178 6.395 C 7.353 6.404,7.404 6.396,7.527 6.340 C 7.903 6.168,8.088 5.754,7.968 5.354 C 7.925 5.209,7.538 4.795,6.303 3.571 C 5.480 2.757,5.264 2.556,5.146 2.500 C 4.987 2.425,4.787 2.395,4.640 2.425 M11.027 5.623 C 10.785 5.686,10.570 5.861,10.471 6.076 C 10.414 6.199,10.413 6.210,10.406 8.530 L 10.398 10.861 9.872 10.341 C 9.248 9.725,9.114 9.627,8.873 9.607 C 8.518 9.579,8.227 9.737,8.078 10.039 C 7.999 10.200,7.994 10.228,8.005 10.420 C 8.014 10.563,8.037 10.664,8.079 10.747 C 8.169 10.927,10.688 13.436,10.860 13.518 C 11.018 13.593,11.263 13.618,11.403 13.573 C 11.586 13.515,11.781 13.342,12.743 12.388 C 13.917 11.225,14.299 10.815,14.342 10.673 C 14.391 10.507,14.380 10.245,14.318 10.107 C 14.197 9.838,13.880 9.627,13.600 9.627 C 13.277 9.627,13.193 9.684,12.488 10.382 L 12.002 10.863 11.994 8.531 C 11.987 6.348,11.984 6.193,11.938 6.097 C 11.866 5.943,11.668 5.746,11.522 5.683 C 11.376 5.620,11.145 5.592,11.027 5.623 "
@@ -409,6 +417,14 @@
fill-rule="evenodd"
></path>
</symbol>
<symbol id="sparkle" stroke="currentColor" viewBox="0 0 20 20">
<path
d="M10.359 3.533 C 10.347 3.579,10.328 3.729,10.317 3.867 C 10.231 4.927,9.870 6.002,9.308 6.867 C 8.254 8.489,6.294 9.627,4.050 9.919 C 3.830 9.947,3.590 9.983,3.517 9.998 L 3.383 10.025 3.562 10.029 C 3.809 10.035,4.375 10.120,4.833 10.221 C 5.428 10.352,6.067 10.579,6.650 10.865 C 7.440 11.252,7.890 11.560,8.429 12.081 C 9.566 13.183,10.161 14.485,10.331 16.247 C 10.370 16.646,10.400 16.743,10.400 16.468 C 10.400 16.020,10.630 14.922,10.849 14.319 C 11.096 13.642,11.417 13.020,11.796 12.485 C 12.064 12.105,12.659 11.502,13.067 11.195 C 13.456 10.902,14.234 10.497,14.683 10.353 C 15.214 10.184,15.999 10.035,16.375 10.034 C 16.647 10.033,16.548 9.983,16.190 9.940 C 15.261 9.830,14.172 9.448,13.373 8.953 C 11.565 7.831,10.538 5.988,10.391 3.600 C 10.383 3.480,10.377 3.467,10.359 3.533 "
fill="currentColor"
stroke="none"
fill-rule="evenodd"
></path>
</symbol>
<symbol id="star" stroke="currentColor" viewBox="0 0 20 20">
<path
d="M9.793 3.034 C 9.582 3.070,9.317 3.219,9.177 3.382 C 9.112 3.458,8.722 4.194,8.283 5.067 C 7.855 5.919,7.485 6.643,7.461 6.675 C 7.425 6.723,7.098 6.779,5.633 6.988 C 4.523 7.146,3.784 7.266,3.675 7.307 C 3.458 7.387,3.209 7.623,3.103 7.850 C 2.928 8.221,2.985 8.694,3.243 9.001 C 3.320 9.094,3.931 9.689,4.600 10.323 C 5.333 11.018,5.821 11.504,5.826 11.547 C 5.832 11.586,5.706 12.352,5.546 13.250 C 5.375 14.214,5.256 14.980,5.254 15.119 C 5.250 15.548,5.498 15.916,5.919 16.106 C 6.118 16.195,6.519 16.210,6.700 16.135 C 6.764 16.108,7.526 15.722,8.393 15.276 C 9.259 14.831,9.982 14.467,9.999 14.467 C 10.016 14.467,10.739 14.831,11.606 15.276 C 12.474 15.722,13.236 16.108,13.300 16.135 C 13.481 16.210,13.882 16.195,14.081 16.106 C 14.345 15.987,14.518 15.823,14.639 15.577 C 14.732 15.387,14.748 15.321,14.746 15.119 C 14.744 14.980,14.625 14.214,14.454 13.250 C 14.294 12.352,14.168 11.585,14.174 11.546 C 14.180 11.502,14.654 11.029,15.400 10.322 C 16.069 9.689,16.680 9.094,16.757 9.002 C 17.233 8.432,16.961 7.497,16.257 7.283 C 16.161 7.254,15.299 7.119,14.340 6.982 C 13.380 6.846,12.581 6.726,12.564 6.715 C 12.547 6.704,12.174 5.980,11.735 5.106 C 11.260 4.159,10.890 3.460,10.818 3.376 C 10.593 3.115,10.164 2.972,9.793 3.034 M10.812 5.933 C 11.524 7.336,11.623 7.515,11.765 7.642 C 11.852 7.720,11.989 7.808,12.071 7.837 C 12.152 7.867,12.994 8.002,13.942 8.137 C 14.891 8.272,15.663 8.392,15.658 8.404 C 15.654 8.415,15.140 8.908,14.517 9.498 C 13.182 10.762,13.149 10.796,13.038 11.033 C 12.904 11.319,12.923 11.530,13.243 13.317 C 13.397 14.178,13.519 14.888,13.514 14.894 C 13.510 14.900,12.801 14.544,11.940 14.102 C 10.238 13.228,10.142 13.190,9.813 13.259 C 9.684 13.286,9.133 13.551,8.060 14.102 C 7.199 14.544,6.490 14.900,6.485 14.894 C 6.480 14.888,6.603 14.178,6.757 13.317 C 7.077 11.530,7.096 11.319,6.962 11.033 C 6.851 10.796,6.821 10.765,5.483 9.498 C 4.860 8.908,4.346 8.415,4.342 8.404 C 4.337 8.392,5.109 8.272,6.058 8.137 C 7.006 8.002,7.848 7.867,7.929 7.837 C 8.011 7.808,8.148 7.720,8.235 7.642 C 8.377 7.515,8.476 7.337,9.188 5.933 C 9.625 5.072,9.990 4.367,10.000 4.367 C 10.009 4.367,10.374 5.072,10.812 5.933 "
@@ -469,7 +485,6 @@
<path
d="M1.224 2.042 C 1.706 2.660,7.867 10.737,7.867 10.749 C 7.867 10.759,6.449 12.387,4.717 14.367 C 2.984 16.346,1.567 17.974,1.567 17.983 C 1.567 17.992,2.171 18.000,2.910 18.000 L 4.254 18.000 6.685 15.219 C 8.023 13.690,9.132 12.442,9.150 12.447 C 9.168 12.451,10.136 13.701,11.300 15.225 L 13.417 17.996 16.142 17.998 C 17.640 17.999,18.867 17.992,18.867 17.983 C 18.867 17.974,17.306 15.903,15.398 13.381 C 13.490 10.859,11.934 8.783,11.940 8.767 C 11.945 8.751,13.238 7.265,14.813 5.466 C 16.387 3.667,17.712 2.151,17.757 2.098 L 17.839 2.000 16.478 2.002 L 15.117 2.004 12.950 4.487 C 11.758 5.852,10.753 6.998,10.716 7.033 C 10.648 7.098,10.644 7.092,8.720 4.549 L 6.792 2.000 3.992 2.000 C 1.757 2.000,1.198 2.008,1.224 2.042 M10.759 9.867 C 13.388 13.341,15.568 16.225,15.605 16.275 L 15.671 16.367 14.922 16.367 L 14.172 16.367 9.298 9.992 C 6.617 6.485,4.409 3.598,4.391 3.574 C 4.364 3.540,4.517 3.534,5.169 3.541 L 5.980 3.550 10.759 9.867 "
stroke="none"
fill="currentColor"
fill-rule="evenodd"
></path>
</symbol>

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -47,9 +47,11 @@ export type IconType =
| 'product-hunt'
| 'refine'
| 'regions'
| 'remix'
| 'rest'
| 'search'
| 'sendgrid'
| 'sparkle'
| 'star'
| 'system'
| 'textmagic'

View File

@@ -2,7 +2,7 @@
</script>
<div class="group relative my-8 w-full overflow-clip">
<div class="animate-marquee flex w-max gap-4 pl-4 group-hover:[animation-play-state:paused]">
<div class="animate-scroll-x flex w-max gap-4 pl-4 group-hover:[animation-play-state:paused]">
<slot />
</div>
</div>

View File

@@ -0,0 +1,89 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
import { tick, type Snippet } from 'svelte';
interface MasonryProps {
colWidth?: string;
children: Snippet;
}
interface GridItem {
el: HTMLElement;
gap: number;
items: HTMLElement[];
ncol: number;
mod: number;
}
const { children }: MasonryProps = $props();
let grids: GridItem[] = [];
let masonryElement: HTMLElement;
const refreshLayout = async (): Promise<void> => {
grids.forEach(async (grid) => {
const ncol = getComputedStyle(grid.el).gridTemplateColumns.split(' ').length;
grid.items.forEach((c) => {
const newHeight = c.getBoundingClientRect().height;
const currentHeight = parseFloat(c.dataset.h || '0');
if (newHeight !== currentHeight) {
c.dataset.h = newHeight.toString();
grid.mod++;
}
});
if (grid.ncol !== ncol || grid.mod) {
grid.ncol = ncol;
grid.items.forEach((c) => c.style.removeProperty('margin-top'));
if (grid.ncol > 1) {
grid.items.slice(ncol).forEach((c, i) => {
const prevBottom = grid.items[i].getBoundingClientRect().bottom;
const currTop = c.getBoundingClientRect().top;
c.style.marginTop = `${prevBottom + grid.gap - currTop}px`;
});
}
grid.mod = 0;
}
});
};
const calcGrid = async (masonryArr: HTMLElement[]): Promise<void> => {
await tick();
if (masonryArr.length && getComputedStyle(masonryArr[0]).gridTemplateRows !== 'masonry') {
grids = masonryArr.map((grid) => ({
el: grid,
gap: parseFloat(getComputedStyle(grid).gridRowGap),
items: Array.from(grid.childNodes).filter(
(c): c is HTMLElement =>
c instanceof HTMLElement && +getComputedStyle(c).gridColumnEnd !== -1
),
ncol: 0,
mod: 0
}));
refreshLayout();
}
};
$effect(() => {
if (masonryElement) {
calcGrid([masonryElement]);
}
});
</script>
<svelte:window onresize={refreshLayout} />
<div
bind:this={masonryElement}
class={classNames(
'grid grid-cols-[repeat(auto-fit,_minmax(min(20em,_100%),_1fr))] [grid-template-rows:masonry] justify-center [grid-gap:20px] *:self-start'
)}
>
{@render children()}
</div>

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