mirror of
https://github.com/LukeHagar/website.git
synced 2025-12-06 04:22:07 +00:00
reusable nav menu
This commit is contained in:
@@ -71,7 +71,7 @@
|
||||
"markdown-it": "^14.1.0",
|
||||
"meilisearch": "^0.37.0",
|
||||
"melt": "^0.29.2",
|
||||
"motion": "^10.18.0",
|
||||
"motion": "^12.7.3",
|
||||
"node-html-parser": "^6.1.13",
|
||||
"openapi-types": "^12.1.3",
|
||||
"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
|
||||
version: 0.29.2(@floating-ui/dom@1.6.13)(svelte@5.25.6)
|
||||
motion:
|
||||
specifier: ^10.18.0
|
||||
version: 10.18.0
|
||||
specifier: ^12.7.3
|
||||
version: 12.7.3
|
||||
node-html-parser:
|
||||
specifier: ^6.1.13
|
||||
version: 6.1.13
|
||||
@@ -831,24 +831,6 @@ packages:
|
||||
peerDependencies:
|
||||
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':
|
||||
resolution: {integrity: sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -2263,6 +2245,20 @@ packages:
|
||||
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
|
||||
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:
|
||||
resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
|
||||
engines: {node: '>=14.14'}
|
||||
@@ -2383,9 +2379,6 @@ packages:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
|
||||
hey-listen@1.0.8:
|
||||
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
|
||||
|
||||
highlight.js@11.11.1:
|
||||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -2833,8 +2826,25 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
motion@10.18.0:
|
||||
resolution: {integrity: sha512-MVAZZmwM/cp77BrNe1TxTMldxRPjwBNHheU5aPToqT4rJdZxLiADk58H+a0al5jKLxkB0OdgNq6DiVn11cjvIQ==}
|
||||
motion-dom@12.7.3:
|
||||
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:
|
||||
resolution: {integrity: sha512-i9qLW6gqboJ5Ht8bauZi7KlTnQ3QFpBCvMvFfEcHADKgHGeJ9BZMO7SFCTwHPV9Qa0du9DYY1Yx3oqlGt30nXA==}
|
||||
@@ -4503,41 +4513,6 @@ snapshots:
|
||||
nanoid: 5.1.3
|
||||
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':
|
||||
optional: true
|
||||
|
||||
@@ -5949,6 +5924,12 @@ snapshots:
|
||||
es-set-tostringtag: 2.1.0
|
||||
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:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
@@ -6093,8 +6074,6 @@ snapshots:
|
||||
|
||||
he@1.2.0: {}
|
||||
|
||||
hey-listen@1.0.8: {}
|
||||
|
||||
highlight.js@11.11.1: {}
|
||||
|
||||
html-escaper@3.0.3: {}
|
||||
@@ -6535,12 +6514,16 @@ snapshots:
|
||||
|
||||
mkdirp@1.0.4: {}
|
||||
|
||||
motion@10.18.0:
|
||||
motion-dom@12.7.3:
|
||||
dependencies:
|
||||
'@motionone/animation': 10.18.0
|
||||
'@motionone/dom': 10.18.0
|
||||
'@motionone/types': 10.17.1
|
||||
'@motionone/utils': 10.18.0
|
||||
motion-utils: 12.7.2
|
||||
|
||||
motion-utils@12.7.2: {}
|
||||
|
||||
motion@12.7.3:
|
||||
dependencies:
|
||||
framer-motion: 12.7.3
|
||||
tslib: 2.8.1
|
||||
|
||||
move-file@2.1.0:
|
||||
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>
|
||||
import MainNavigation from './main-navigation.svelte';
|
||||
<script lang="ts">
|
||||
import { Button } from '../ui';
|
||||
import HamburgerMenu from './navigation/hamburger-menu.svelte';
|
||||
import PrimaryNav from './navigation/primary-nav.svelte';
|
||||
</script>
|
||||
|
||||
<header
|
||||
@@ -15,8 +17,10 @@
|
||||
width="130"
|
||||
/></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>
|
||||
</header>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import MainFooter from '$lib/components/layout/main-footer.svelte';
|
||||
import MainHeader from '$lib/components/layout/main-header.svelte';
|
||||
import MainFooter from '$lib/components/layout/site-footer.svelte';
|
||||
import MainHeader from '$lib/components/layout/site-header.svelte';
|
||||
import SubFooter from '$lib/components/layout/sub-footer.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
|
||||
@@ -57,53 +57,53 @@
|
||||
Create: 5
|
||||
} as const;
|
||||
|
||||
$effect(() => {
|
||||
let timeout: NodeJS.Timeout;
|
||||
switch (step) {
|
||||
case FunctionsState.Stale:
|
||||
console.log('stale');
|
||||
timeout = setTimeout(() => {
|
||||
step = FunctionsState.Generate;
|
||||
}, 500);
|
||||
return () => clearTimeout(timeout);
|
||||
case FunctionsState.Generate:
|
||||
console.log('generate');
|
||||
timeout = setTimeout(() => {
|
||||
step = FunctionsState.Send;
|
||||
}, 2000);
|
||||
return () => clearTimeout(timeout);
|
||||
case FunctionsState.Send:
|
||||
console.log('send');
|
||||
timeout = setTimeout(() => {
|
||||
step = FunctionsState.Update;
|
||||
}, 1000);
|
||||
return () => clearTimeout(timeout);
|
||||
case FunctionsState.Update:
|
||||
console.log('update');
|
||||
timeout = setTimeout(() => {
|
||||
step = FunctionsState.Delete;
|
||||
}, 200);
|
||||
return () => clearTimeout(timeout);
|
||||
case FunctionsState.Delete:
|
||||
console.log('delete');
|
||||
timeout = setTimeout(() => {
|
||||
step = FunctionsState.Create;
|
||||
}, 100);
|
||||
return () => clearTimeout(timeout);
|
||||
case FunctionsState.Create:
|
||||
console.log('create');
|
||||
timeout = setTimeout(() => {
|
||||
step = FunctionsState.Stale;
|
||||
}, 800);
|
||||
return () => clearTimeout(timeout);
|
||||
default:
|
||||
console.log('stale');
|
||||
timeout = setTimeout(() => {
|
||||
step = FunctionsState.Generate;
|
||||
}, 3000);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
});
|
||||
// $effect(() => {
|
||||
// let timeout: NodeJS.Timeout;
|
||||
// switch (step) {
|
||||
// case FunctionsState.Stale:
|
||||
// console.log('stale');
|
||||
// timeout = setTimeout(() => {
|
||||
// step = FunctionsState.Generate;
|
||||
// }, 500);
|
||||
// return () => clearTimeout(timeout);
|
||||
// case FunctionsState.Generate:
|
||||
// console.log('generate');
|
||||
// timeout = setTimeout(() => {
|
||||
// step = FunctionsState.Send;
|
||||
// }, 2000);
|
||||
// return () => clearTimeout(timeout);
|
||||
// case FunctionsState.Send:
|
||||
// console.log('send');
|
||||
// timeout = setTimeout(() => {
|
||||
// step = FunctionsState.Update;
|
||||
// }, 1000);
|
||||
// return () => clearTimeout(timeout);
|
||||
// case FunctionsState.Update:
|
||||
// console.log('update');
|
||||
// timeout = setTimeout(() => {
|
||||
// step = FunctionsState.Delete;
|
||||
// }, 200);
|
||||
// return () => clearTimeout(timeout);
|
||||
// case FunctionsState.Delete:
|
||||
// console.log('delete');
|
||||
// timeout = setTimeout(() => {
|
||||
// step = FunctionsState.Create;
|
||||
// }, 100);
|
||||
// return () => clearTimeout(timeout);
|
||||
// case FunctionsState.Create:
|
||||
// console.log('create');
|
||||
// timeout = setTimeout(() => {
|
||||
// step = FunctionsState.Stale;
|
||||
// }, 800);
|
||||
// return () => clearTimeout(timeout);
|
||||
// default:
|
||||
// console.log('stale');
|
||||
// timeout = setTimeout(() => {
|
||||
// step = FunctionsState.Generate;
|
||||
// }, 3000);
|
||||
// return () => clearTimeout(timeout);
|
||||
// }
|
||||
// });
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
<div class="relative flex min-h-[80vh] flex-col items-center md:flex-row">
|
||||
<div
|
||||
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%]'
|
||||
)}
|
||||
style:--speed="500ms"
|
||||
@@ -30,25 +31,3 @@
|
||||
</div>
|
||||
<Dashboard />
|
||||
</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