mirror of
https://github.com/LukeHagar/website.git
synced 2025-12-09 21:07:46 +00:00
reusable nav menu
This commit is contained in:
@@ -71,7 +71,7 @@
|
|||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"meilisearch": "^0.37.0",
|
"meilisearch": "^0.37.0",
|
||||||
"melt": "^0.29.2",
|
"melt": "^0.29.2",
|
||||||
"motion": "^10.18.0",
|
"motion": "^12.7.3",
|
||||||
"node-html-parser": "^6.1.13",
|
"node-html-parser": "^6.1.13",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"oslllo-svg-fixer": "^3.0.0",
|
"oslllo-svg-fixer": "^3.0.0",
|
||||||
|
|||||||
117
pnpm-lock.yaml
generated
117
pnpm-lock.yaml
generated
@@ -139,8 +139,8 @@ importers:
|
|||||||
specifier: ^0.29.2
|
specifier: ^0.29.2
|
||||||
version: 0.29.2(@floating-ui/dom@1.6.13)(svelte@5.25.6)
|
version: 0.29.2(@floating-ui/dom@1.6.13)(svelte@5.25.6)
|
||||||
motion:
|
motion:
|
||||||
specifier: ^10.18.0
|
specifier: ^12.7.3
|
||||||
version: 10.18.0
|
version: 12.7.3
|
||||||
node-html-parser:
|
node-html-parser:
|
||||||
specifier: ^6.1.13
|
specifier: ^6.1.13
|
||||||
version: 6.1.13
|
version: 6.1.13
|
||||||
@@ -831,24 +831,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0-next.118
|
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0-next.118
|
||||||
|
|
||||||
'@motionone/animation@10.18.0':
|
|
||||||
resolution: {integrity: sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==}
|
|
||||||
|
|
||||||
'@motionone/dom@10.18.0':
|
|
||||||
resolution: {integrity: sha512-bKLP7E0eyO4B2UaHBBN55tnppwRnaE3KFfh3Ps9HhnAkar3Cb69kUCJY9as8LrccVYKgHA+JY5dOQqJLOPhF5A==}
|
|
||||||
|
|
||||||
'@motionone/easing@10.18.0':
|
|
||||||
resolution: {integrity: sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==}
|
|
||||||
|
|
||||||
'@motionone/generators@10.18.0':
|
|
||||||
resolution: {integrity: sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==}
|
|
||||||
|
|
||||||
'@motionone/types@10.17.1':
|
|
||||||
resolution: {integrity: sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==}
|
|
||||||
|
|
||||||
'@motionone/utils@10.18.0':
|
|
||||||
resolution: {integrity: sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==}
|
|
||||||
|
|
||||||
'@napi-rs/nice-android-arm-eabi@1.0.1':
|
'@napi-rs/nice-android-arm-eabi@1.0.1':
|
||||||
resolution: {integrity: sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==}
|
resolution: {integrity: sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@@ -2263,6 +2245,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
|
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
framer-motion@12.7.3:
|
||||||
|
resolution: {integrity: sha512-dNT4l5gEnUo2ytXLUBUf6AI21dZ77TMclDKE3ElaIHZ8m90nJ/NCcExW51zdSIaS0RhAS5iXcF7bEIxZe8XG2g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fs-extra@11.2.0:
|
fs-extra@11.2.0:
|
||||||
resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
|
resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
|
||||||
engines: {node: '>=14.14'}
|
engines: {node: '>=14.14'}
|
||||||
@@ -2383,9 +2379,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
hey-listen@1.0.8:
|
|
||||||
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
|
|
||||||
|
|
||||||
highlight.js@11.11.1:
|
highlight.js@11.11.1:
|
||||||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -2833,8 +2826,25 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
motion@10.18.0:
|
motion-dom@12.7.3:
|
||||||
resolution: {integrity: sha512-MVAZZmwM/cp77BrNe1TxTMldxRPjwBNHheU5aPToqT4rJdZxLiADk58H+a0al5jKLxkB0OdgNq6DiVn11cjvIQ==}
|
resolution: {integrity: sha512-IjMt1YJHrvyvruFvmpmd6bGXXGCvmygrnvSb3aZ8KhOzF4H3PulU+cMBzH+U8TBJHjC/mnmJFRIA1Cu4vBfcBA==}
|
||||||
|
|
||||||
|
motion-utils@12.7.2:
|
||||||
|
resolution: {integrity: sha512-XhZwqctxyJs89oX00zn3OGCuIIpVevbTa+u82usWBC6pSHUd2AoNWiYa7Du8tJxJy9TFbZ82pcn5t7NOm1PHAw==}
|
||||||
|
|
||||||
|
motion@12.7.3:
|
||||||
|
resolution: {integrity: sha512-EGhzIg7vj+USH9SLNLjHRzglldWEletUZTEtBVKW7IJF+1Ig3RI5LnJmHQBNutuOIyeUbcF36MrNFT00etlc3g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
move-file@2.1.0:
|
move-file@2.1.0:
|
||||||
resolution: {integrity: sha512-i9qLW6gqboJ5Ht8bauZi7KlTnQ3QFpBCvMvFfEcHADKgHGeJ9BZMO7SFCTwHPV9Qa0du9DYY1Yx3oqlGt30nXA==}
|
resolution: {integrity: sha512-i9qLW6gqboJ5Ht8bauZi7KlTnQ3QFpBCvMvFfEcHADKgHGeJ9BZMO7SFCTwHPV9Qa0du9DYY1Yx3oqlGt30nXA==}
|
||||||
@@ -4503,41 +4513,6 @@ snapshots:
|
|||||||
nanoid: 5.1.3
|
nanoid: 5.1.3
|
||||||
svelte: 5.25.6
|
svelte: 5.25.6
|
||||||
|
|
||||||
'@motionone/animation@10.18.0':
|
|
||||||
dependencies:
|
|
||||||
'@motionone/easing': 10.18.0
|
|
||||||
'@motionone/types': 10.17.1
|
|
||||||
'@motionone/utils': 10.18.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@motionone/dom@10.18.0':
|
|
||||||
dependencies:
|
|
||||||
'@motionone/animation': 10.18.0
|
|
||||||
'@motionone/generators': 10.18.0
|
|
||||||
'@motionone/types': 10.17.1
|
|
||||||
'@motionone/utils': 10.18.0
|
|
||||||
hey-listen: 1.0.8
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@motionone/easing@10.18.0':
|
|
||||||
dependencies:
|
|
||||||
'@motionone/utils': 10.18.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@motionone/generators@10.18.0':
|
|
||||||
dependencies:
|
|
||||||
'@motionone/types': 10.17.1
|
|
||||||
'@motionone/utils': 10.18.0
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@motionone/types@10.17.1': {}
|
|
||||||
|
|
||||||
'@motionone/utils@10.18.0':
|
|
||||||
dependencies:
|
|
||||||
'@motionone/types': 10.17.1
|
|
||||||
hey-listen: 1.0.8
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
'@napi-rs/nice-android-arm-eabi@1.0.1':
|
'@napi-rs/nice-android-arm-eabi@1.0.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -5949,6 +5924,12 @@ snapshots:
|
|||||||
es-set-tostringtag: 2.1.0
|
es-set-tostringtag: 2.1.0
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
|
||||||
|
framer-motion@12.7.3:
|
||||||
|
dependencies:
|
||||||
|
motion-dom: 12.7.3
|
||||||
|
motion-utils: 12.7.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
fs-extra@11.2.0:
|
fs-extra@11.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -6093,8 +6074,6 @@ snapshots:
|
|||||||
|
|
||||||
he@1.2.0: {}
|
he@1.2.0: {}
|
||||||
|
|
||||||
hey-listen@1.0.8: {}
|
|
||||||
|
|
||||||
highlight.js@11.11.1: {}
|
highlight.js@11.11.1: {}
|
||||||
|
|
||||||
html-escaper@3.0.3: {}
|
html-escaper@3.0.3: {}
|
||||||
@@ -6535,12 +6514,16 @@ snapshots:
|
|||||||
|
|
||||||
mkdirp@1.0.4: {}
|
mkdirp@1.0.4: {}
|
||||||
|
|
||||||
motion@10.18.0:
|
motion-dom@12.7.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@motionone/animation': 10.18.0
|
motion-utils: 12.7.2
|
||||||
'@motionone/dom': 10.18.0
|
|
||||||
'@motionone/types': 10.17.1
|
motion-utils@12.7.2: {}
|
||||||
'@motionone/utils': 10.18.0
|
|
||||||
|
motion@12.7.3:
|
||||||
|
dependencies:
|
||||||
|
framer-motion: 12.7.3
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
move-file@2.1.0:
|
move-file@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { NavigationMenu } 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 ListItemProps = {
|
|
||||||
className?: string;
|
|
||||||
name: string;
|
|
||||||
href: string;
|
|
||||||
content: string;
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#snippet ListItem({ className, name, content, href }: ListItemProps)}
|
|
||||||
<li>
|
|
||||||
<NavigationMenu.Link
|
|
||||||
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
|
|
||||||
)}
|
|
||||||
{href}
|
|
||||||
>
|
|
||||||
<div class="text-sm leading-none font-medium">{name}</div>
|
|
||||||
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
|
|
||||||
{content}
|
|
||||||
</p>
|
|
||||||
</NavigationMenu.Link>
|
|
||||||
</li>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<NavigationMenu.Root>
|
|
||||||
<NavigationMenu.List class="flex items-center gap-8">
|
|
||||||
<NavigationMenu.Item>
|
|
||||||
<NavigationMenu.Trigger class="group flex items-center gap-3"
|
|
||||||
>Products <Icon
|
|
||||||
name="chevron-down"
|
|
||||||
class="relative size-4 transition-transform duration-200 group-data-[state=open]:-rotate-180"
|
|
||||||
aria-hidden="true"
|
|
||||||
/></NavigationMenu.Trigger
|
|
||||||
>
|
|
||||||
<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 w-full sm:w-auto"
|
|
||||||
>
|
|
||||||
<ul
|
|
||||||
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 component (component.name)}
|
|
||||||
{@render ListItem({
|
|
||||||
href: component.href,
|
|
||||||
name: component.name,
|
|
||||||
content: component.description
|
|
||||||
})}
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</NavigationMenu.Content>
|
|
||||||
</NavigationMenu.Item>
|
|
||||||
<NavigationMenu.Item>
|
|
||||||
<NavigationMenu.Link href="/docs">Docs</NavigationMenu.Link>
|
|
||||||
</NavigationMenu.Item>
|
|
||||||
<NavigationMenu.Item>
|
|
||||||
<NavigationMenu.Link href="/pricing">Pricing</NavigationMenu.Link>
|
|
||||||
</NavigationMenu.Item>
|
|
||||||
<NavigationMenu.Item>
|
|
||||||
<NavigationMenu.Link href="/contact/enterprise">Enterprise</NavigationMenu.Link>
|
|
||||||
</NavigationMenu.Item>
|
|
||||||
</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] rounded-md border 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>
|
|
||||||
54
src/lib/components/layout/navigation/hamburger-menu.svelte
Normal file
54
src/lib/components/layout/navigation/hamburger-menu.svelte
Normal 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)} />
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export let navState = $state<{
|
||||||
|
isOpen: boolean;
|
||||||
|
}>({
|
||||||
|
isOpen: false
|
||||||
|
});
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
export { NavItem, Trigger };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { NavigationMenu } from 'bits-ui';
|
||||||
|
import { classNames } from '$lib/utils/classnames';
|
||||||
|
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 w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
<ul class="grid gap-3 p-3 sm:w-[400px] sm:p-6 md:w-[600px] md:grid-cols-2 lg:w-[800px]">
|
||||||
|
{@render children()}
|
||||||
|
</ul>
|
||||||
|
</NavigationMenu.Content>
|
||||||
|
|
||||||
|
{#snippet NavItem({ class: className, label, content, href, ...rest }: ListItemProps)}
|
||||||
|
<li>
|
||||||
|
<NavigationMenu.Link
|
||||||
|
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
|
||||||
|
)}
|
||||||
|
{href}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<div class="text-sm leading-none font-medium">{name}</div>
|
||||||
|
<p class="text-muted-foreground line-clamp-2 text-sm leading-snug">
|
||||||
|
{content}
|
||||||
|
</p>
|
||||||
|
</NavigationMenu.Link>
|
||||||
|
</li>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet Trigger({ class: className, children, ...rest }: TriggerProps)}
|
||||||
|
<NavigationMenu.Trigger class={classNames('group flex items-center gap-3', className)} {...rest}
|
||||||
|
>{@render children?.()}</NavigationMenu.Trigger
|
||||||
|
>
|
||||||
|
{/snippet}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import MenuWrapper, { NavItem } from './menu-wrapper.svelte';
|
||||||
|
|
||||||
|
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>
|
||||||
|
<ul 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 (product.name)}
|
||||||
|
{@render NavItem({
|
||||||
|
href: product.href,
|
||||||
|
label: product.name,
|
||||||
|
content: product.description
|
||||||
|
})}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</MenuWrapper>
|
||||||
58
src/lib/components/layout/navigation/mobile-nav.svelte
Normal file
58
src/lib/components/layout/navigation/mobile-nav.svelte
Normal 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}
|
||||||
67
src/lib/components/layout/navigation/primary-nav.svelte
Normal file
67
src/lib/components/layout/navigation/primary-nav.svelte
Normal 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?: Component;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const navItems: Array<NavItem> = [
|
||||||
|
{ label: 'Products', menu: ProductMenu },
|
||||||
|
{
|
||||||
|
label: 'Docs',
|
||||||
|
href: '/docs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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}
|
||||||
|
{#if item.menu}
|
||||||
|
{@const Menu = item.menu}
|
||||||
|
<NavigationMenu.Item>
|
||||||
|
<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
|
||||||
|
>
|
||||||
|
<Menu />
|
||||||
|
</NavigationMenu.Item>
|
||||||
|
{:else}
|
||||||
|
<NavigationMenu.Item>
|
||||||
|
<NavigationMenu.Link href={item.href ?? ''}>{item.label}</NavigationMenu.Link>
|
||||||
|
</NavigationMenu.Item>
|
||||||
|
{/if}
|
||||||
|
{/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] 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>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import MainNavigation from './main-navigation.svelte';
|
import { Button } from '../ui';
|
||||||
|
import HamburgerMenu from './navigation/hamburger-menu.svelte';
|
||||||
|
import PrimaryNav from './navigation/primary-nav.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
@@ -15,8 +17,10 @@
|
|||||||
width="130"
|
width="130"
|
||||||
/></a
|
/></a
|
||||||
>
|
>
|
||||||
<MainNavigation />
|
|
||||||
|
|
||||||
<button class="web-button">Start building for free</button>
|
<PrimaryNav class="hidden md:block" />
|
||||||
|
<Button class="hidden! md:flex!">Start building for free</Button>
|
||||||
|
|
||||||
|
<HamburgerMenu class="block md:hidden" />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import MainFooter from '$lib/components/layout/main-footer.svelte';
|
import MainFooter from '$lib/components/layout/site-footer.svelte';
|
||||||
import MainHeader from '$lib/components/layout/main-header.svelte';
|
import MainHeader from '$lib/components/layout/site-header.svelte';
|
||||||
import SubFooter from '$lib/components/layout/sub-footer.svelte';
|
import SubFooter from '$lib/components/layout/sub-footer.svelte';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
|||||||
@@ -57,53 +57,53 @@
|
|||||||
Create: 5
|
Create: 5
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
$effect(() => {
|
// $effect(() => {
|
||||||
let timeout: NodeJS.Timeout;
|
// let timeout: NodeJS.Timeout;
|
||||||
switch (step) {
|
// switch (step) {
|
||||||
case FunctionsState.Stale:
|
// case FunctionsState.Stale:
|
||||||
console.log('stale');
|
// console.log('stale');
|
||||||
timeout = setTimeout(() => {
|
// timeout = setTimeout(() => {
|
||||||
step = FunctionsState.Generate;
|
// step = FunctionsState.Generate;
|
||||||
}, 500);
|
// }, 500);
|
||||||
return () => clearTimeout(timeout);
|
// return () => clearTimeout(timeout);
|
||||||
case FunctionsState.Generate:
|
// case FunctionsState.Generate:
|
||||||
console.log('generate');
|
// console.log('generate');
|
||||||
timeout = setTimeout(() => {
|
// timeout = setTimeout(() => {
|
||||||
step = FunctionsState.Send;
|
// step = FunctionsState.Send;
|
||||||
}, 2000);
|
// }, 2000);
|
||||||
return () => clearTimeout(timeout);
|
// return () => clearTimeout(timeout);
|
||||||
case FunctionsState.Send:
|
// case FunctionsState.Send:
|
||||||
console.log('send');
|
// console.log('send');
|
||||||
timeout = setTimeout(() => {
|
// timeout = setTimeout(() => {
|
||||||
step = FunctionsState.Update;
|
// step = FunctionsState.Update;
|
||||||
}, 1000);
|
// }, 1000);
|
||||||
return () => clearTimeout(timeout);
|
// return () => clearTimeout(timeout);
|
||||||
case FunctionsState.Update:
|
// case FunctionsState.Update:
|
||||||
console.log('update');
|
// console.log('update');
|
||||||
timeout = setTimeout(() => {
|
// timeout = setTimeout(() => {
|
||||||
step = FunctionsState.Delete;
|
// step = FunctionsState.Delete;
|
||||||
}, 200);
|
// }, 200);
|
||||||
return () => clearTimeout(timeout);
|
// return () => clearTimeout(timeout);
|
||||||
case FunctionsState.Delete:
|
// case FunctionsState.Delete:
|
||||||
console.log('delete');
|
// console.log('delete');
|
||||||
timeout = setTimeout(() => {
|
// timeout = setTimeout(() => {
|
||||||
step = FunctionsState.Create;
|
// step = FunctionsState.Create;
|
||||||
}, 100);
|
// }, 100);
|
||||||
return () => clearTimeout(timeout);
|
// return () => clearTimeout(timeout);
|
||||||
case FunctionsState.Create:
|
// case FunctionsState.Create:
|
||||||
console.log('create');
|
// console.log('create');
|
||||||
timeout = setTimeout(() => {
|
// timeout = setTimeout(() => {
|
||||||
step = FunctionsState.Stale;
|
// step = FunctionsState.Stale;
|
||||||
}, 800);
|
// }, 800);
|
||||||
return () => clearTimeout(timeout);
|
// return () => clearTimeout(timeout);
|
||||||
default:
|
// default:
|
||||||
console.log('stale');
|
// console.log('stale');
|
||||||
timeout = setTimeout(() => {
|
// timeout = setTimeout(() => {
|
||||||
step = FunctionsState.Generate;
|
// step = FunctionsState.Generate;
|
||||||
}, 3000);
|
// }, 3000);
|
||||||
return () => clearTimeout(timeout);
|
// return () => clearTimeout(timeout);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
<div class="relative flex min-h-[80vh] flex-col items-center md:flex-row">
|
<div class="relative flex min-h-[80vh] flex-col items-center md:flex-row">
|
||||||
<div
|
<div
|
||||||
class={classNames(
|
class={classNames(
|
||||||
'hero animate-lighting absolute top-0 left-0 z-0 h-screen w-full -translate-x-[25%] translate-y-8 rotate-25 overflow-hidden blur-3xl',
|
'animate-lighting absolute top-0 left-0 -z-10 h-screen w-[200vw] -translate-x-[25%] translate-y-8 rotate-25 overflow-hidden blur-3xl md:w-full',
|
||||||
|
'bg-[image:radial-gradient(ellipse_390px_50px_at_10%_30%,_rgba(254,_149,_103,_0.2)_0%,_rgba(254,_149,_103,_0)_70%),_radial-gradient(ellipse_1100px_170px_at_15%_40%,rgba(253,_54,_110,_0.08)_0%,_rgba(253,_54,_110,_0)_70%),_radial-gradient(ellipse_1200px_180px_at_30%_30%,_rgba(253,_54,_110,_0.08)_0%,_rgba(253,_54,_110,_0)_70%)]',
|
||||||
'bg-position-[0%_0%]'
|
'bg-position-[0%_0%]'
|
||||||
)}
|
)}
|
||||||
style:--speed="500ms"
|
style:--speed="500ms"
|
||||||
@@ -30,25 +31,3 @@
|
|||||||
</div>
|
</div>
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.hero {
|
|
||||||
--first-gradient: radial-gradient(
|
|
||||||
ellipse 390px 50px at 10% 30%,
|
|
||||||
rgba(254, 149, 103, 0.4) 0%,
|
|
||||||
rgba(254, 149, 103, 0) 70%
|
|
||||||
);
|
|
||||||
--second-gradient: radial-gradient(
|
|
||||||
ellipse 1100px 170px at 15% 40%,
|
|
||||||
rgba(253, 54, 110, 0.16) 0%,
|
|
||||||
rgba(253, 54, 110, 0) 70%
|
|
||||||
);
|
|
||||||
--third-gradient: radial-gradient(
|
|
||||||
ellipse 1200px 180px at 30% 30%,
|
|
||||||
rgba(253, 54, 110, 0.16) 0%,
|
|
||||||
rgba(253, 54, 110, 0) 70%
|
|
||||||
);
|
|
||||||
|
|
||||||
background-image: var(--first-gradient), var(--second-gradient), var(--third-gradient);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user