started implementing navbar
This commit is contained in:
7
index.js
7
index.js
@@ -115,8 +115,8 @@ async function main() {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9]+/g, '-')
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
.replace(/^-+|-+$/g, '');
|
.replace(/^-+|-+$/g, '');
|
||||||
const rootDir = path.resolve(process.cwd(), kebabName);
|
const rootDir = path.join(process.cwd(), kebabName);
|
||||||
const pmx = project.packageManager === 'bun' ? 'bunx' : 'npx';
|
const pmx = project.packageManager === 'bun' ? 'bun x' : 'npx';
|
||||||
const pm = project.packageManager === 'bun' ? 'bun' : 'npm';
|
const pm = project.packageManager === 'bun' ? 'bun' : 'npm';
|
||||||
const studioDir = path.join(rootDir, 'apps', 'studio');
|
const studioDir = path.join(rootDir, 'apps', 'studio');
|
||||||
|
|
||||||
@@ -124,8 +124,7 @@ async function main() {
|
|||||||
{
|
{
|
||||||
title: `${color.yellow('📁 Copying template contents to root')}`,
|
title: `${color.yellow('📁 Copying template contents to root')}`,
|
||||||
task: async () => {
|
task: async () => {
|
||||||
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
const templateDir = path.join(process.cwd(), 'template');
|
||||||
const templateDir = path.resolve(__dirname, 'template');
|
|
||||||
|
|
||||||
// Ensure root directory exists with proper permissions
|
// Ensure root directory exists with proper permissions
|
||||||
await fs.ensureDir(rootDir);
|
await fs.ensureDir(rootDir);
|
||||||
|
|||||||
129
template/apps/client/src/lib/components/navbar.svelte
Normal file
129
template/apps/client/src/lib/components/navbar.svelte
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<script context="module" lang="ts">
|
||||||
|
export interface NavigationSubItem {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavigationItem {
|
||||||
|
name: string;
|
||||||
|
url?: string;
|
||||||
|
subitems?: NavigationSubItem[];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuRoot,
|
||||||
|
NavigationMenuTrigger
|
||||||
|
} from '$lib/components/ui/navigation-menu';
|
||||||
|
import { cn } from '$lib/utils';
|
||||||
|
import { navigationMenuTriggerStyle } from './ui/navigation-menu/navigation-menu-trigger.svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { onMount, tick } from 'svelte';
|
||||||
|
import { tweened } from 'svelte/motion';
|
||||||
|
import { cubicOut } from 'svelte/easing';
|
||||||
|
|
||||||
|
export let items: NavigationItem[] = [
|
||||||
|
{ name: 'Home', url: '/' },
|
||||||
|
{ name: 'Test', url: '/test' },
|
||||||
|
{
|
||||||
|
name: 'Test Subitems',
|
||||||
|
subitems: [
|
||||||
|
{ name: 'Test Page', url: '#' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
let activeIndex = 0;
|
||||||
|
let navigationList: HTMLElement;
|
||||||
|
|
||||||
|
const indicatorPosition = tweened({ left: 0, width: 0 }, {
|
||||||
|
duration: 300,
|
||||||
|
easing: cubicOut
|
||||||
|
});
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const currentPath = $page.url.pathname;
|
||||||
|
let newActiveIndex = -1;
|
||||||
|
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
if (item.url === currentPath) {
|
||||||
|
newActiveIndex = index;
|
||||||
|
} else if (item.subitems) {
|
||||||
|
const hasActiveSubitem = item.subitems.some(subitem =>
|
||||||
|
currentPath === subitem.url || currentPath.startsWith(subitem.url + '/')
|
||||||
|
);
|
||||||
|
if (hasActiveSubitem) {
|
||||||
|
newActiveIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newActiveIndex !== -1) {
|
||||||
|
activeIndex = newActiveIndex;
|
||||||
|
tick().then(() => updateIndicator());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateIndicator() {
|
||||||
|
if (!navigationList || activeIndex === -1) return;
|
||||||
|
|
||||||
|
const menuItems = navigationList.querySelectorAll('[data-navigation-menu-item]');
|
||||||
|
const activeMenuItem = menuItems[activeIndex];
|
||||||
|
|
||||||
|
if (activeMenuItem) {
|
||||||
|
const activeButton = activeMenuItem.querySelector('a, button');
|
||||||
|
if (activeButton) {
|
||||||
|
const listRect = navigationList.getBoundingClientRect();
|
||||||
|
const buttonRect = activeButton.getBoundingClientRect();
|
||||||
|
|
||||||
|
indicatorPosition.set({
|
||||||
|
left: buttonRect.left - listRect.left,
|
||||||
|
width: buttonRect.width
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setTimeout(updateIndicator, 200);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<NavigationMenuRoot class="px-2 py-1 min-w-full bg-primary" viewport={false}>
|
||||||
|
<div bind:this={navigationList}>
|
||||||
|
<NavigationMenuList class="relative">
|
||||||
|
<div
|
||||||
|
class="absolute top-0 h-full bg-white/10 rounded-md pointer-events-none z-10"
|
||||||
|
style="left: {$indicatorPosition.left}px; width: {$indicatorPosition.width}px;"
|
||||||
|
></div>
|
||||||
|
{#each items as item}
|
||||||
|
<NavigationMenuItem data-navigation-menu-item>
|
||||||
|
{#if item.url && !item.subitems}
|
||||||
|
<NavigationMenuLink
|
||||||
|
class={cn(navigationMenuTriggerStyle(), "relative z-20")}
|
||||||
|
href={item.url}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</NavigationMenuLink>
|
||||||
|
{:else if item.subitems}
|
||||||
|
<NavigationMenuTrigger class="relative z-20">
|
||||||
|
{item.name}
|
||||||
|
</NavigationMenuTrigger>
|
||||||
|
<NavigationMenuContent class="absolute left-1/2 -translate-x-1/2 mt-2 z-50 min-w-full border border-white/20 bg-primary shadow-lg rounded-md">
|
||||||
|
{#each item.subitems as subitem}
|
||||||
|
<NavigationMenuLink href={subitem.url}>
|
||||||
|
{subitem.name}
|
||||||
|
</NavigationMenuLink>
|
||||||
|
{/each}
|
||||||
|
</NavigationMenuContent>
|
||||||
|
{/if}
|
||||||
|
</NavigationMenuItem>
|
||||||
|
{/each}
|
||||||
|
</NavigationMenuList>
|
||||||
|
</div>
|
||||||
|
</NavigationMenuRoot>
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
bind:ref
|
bind:ref
|
||||||
data-slot="navigation-menu-content"
|
data-slot="navigation-menu-content"
|
||||||
class={cn(
|
class={cn(
|
||||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 left-0 top-0 w-full md:absolute md:w-auto",
|
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 left-0 top-0 w-full md:absolute md:w-auto md:left-1/2 md:-translate-x-1/2",
|
||||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200",
|
"group-data-[viewport=false]/navigation-menu:bg-primary group-data-[viewport=false]/navigation-menu:text-white group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border-white/20 group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
bind:ref
|
bind:ref
|
||||||
data-slot="navigation-menu-link"
|
data-slot="navigation-menu-link"
|
||||||
class={cn(
|
class={cn(
|
||||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm outline-none transition-all focus-visible:outline-1 focus-visible:ring-[3px] [&_svg:not([class*='size-'])]:size-4",
|
"data-[active=true]:focus:bg-white/5 cursor-pointer data-[active=true]:hover:bg-white/5 data-[active=true]:bg-primary data-[active=true]:text-white hover:bg-white/5 hover:text-white focus:bg-white/5 focus:text-white active:bg-white/8 focus-visible:ring-white/20 [&_svg:not([class*='text-'])]:text-white flex flex-col gap-1 rounded-sm p-2 text-sm text-white outline-none transition-all focus-visible:outline-1 focus-visible:ring-[3px] [&_svg:not([class*='size-'])]:size-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { tv } from "tailwind-variants";
|
import { tv } from "tailwind-variants";
|
||||||
|
|
||||||
export const navigationMenuTriggerStyle = tv({
|
export const navigationMenuTriggerStyle = tv({
|
||||||
base: "bg-background hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 group inline-flex h-9 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium outline-none transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50",
|
base: "text-white hover:bg-white/5 hover:text-white focus:bg-white/5 focus:text-white data-[state=open]:hover:bg-white/5 data-[state=open]:text-white data-[state=open]:focus:bg-white/5 data-[state=open]:bg-primary active:bg-white/8 focus-visible:ring-white/20 group inline-flex h-9 w-max items-center justify-center rounded-md px-4 py-2 text-sm font-medium outline-none transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50",
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -22,7 +22,11 @@
|
|||||||
<NavigationMenuPrimitive.Trigger
|
<NavigationMenuPrimitive.Trigger
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="navigation-menu-trigger"
|
data-slot="navigation-menu-trigger"
|
||||||
class={cn(navigationMenuTriggerStyle(), "group", className)}
|
class={cn(
|
||||||
|
navigationMenuTriggerStyle(),
|
||||||
|
"group",
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -1,35 +1,36 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {VisualEditing} from '@sanity/visual-editing/svelte'
|
import { VisualEditing } from '@sanity/visual-editing/svelte';
|
||||||
import {LiveMode} from '@sanity/svelte-loader'
|
import { LiveMode } from '@sanity/svelte-loader';
|
||||||
import {client} from '$lib/sanity'
|
import { client } from '$lib/sanity';
|
||||||
import Footer from '$lib/components/footer.svelte'
|
import Footer from '$lib/components/footer.svelte';
|
||||||
import '../app.css'
|
import '../app.css';
|
||||||
|
import Navbar, { type NavigationItem } from '$lib/components/navbar.svelte';
|
||||||
|
let { children, data } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
let {children, data} = $props();
|
<svelte:head>
|
||||||
|
<title>{data.settings?.title || 'Website'}</title>
|
||||||
|
<meta name="description" content={data.settings?.description || ''} />
|
||||||
|
<meta property="og:title" content={data.settings?.longTitle || data.settings?.title || ''} />
|
||||||
|
<meta property="og:description" content={data.settings?.description || ''} />
|
||||||
|
<meta name="twitter:title" content={data.settings?.longTitle || data.settings?.title || ''} />
|
||||||
|
<meta name="twitter:description" content={data.settings?.description || ''} />
|
||||||
|
<meta name="robots" content="index, follow" />
|
||||||
|
{#if data.logo}
|
||||||
|
<link rel="icon" href={data.logo.url} />
|
||||||
|
<link rel="apple-touch-icon" href={data.logo.url} />
|
||||||
|
{/if}
|
||||||
|
<link rel="manifest" href="/manifest.webmanifest" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="scroll-smooth font-sans antialiased">
|
||||||
<svelte:head>
|
<Navbar />
|
||||||
<title>{data.settings?.title || 'Website'}</title>
|
{@render children()}
|
||||||
<meta name="description" content={data.settings?.description || ''} />
|
{#if data.settings}
|
||||||
<meta property="og:title" content={data.settings?.longTitle || data.settings?.title || ''} />
|
<Footer settings={data.settings} />
|
||||||
<meta property="og:description" content={data.settings?.description || ''} />
|
{/if}
|
||||||
<meta name="twitter:title" content={data.settings?.longTitle || data.settings?.title || ''} />
|
{#if data.preview}
|
||||||
<meta name="twitter:description" content={data.settings?.description || ''} />
|
<VisualEditing />
|
||||||
<meta name="robots" content="index, follow" />
|
<LiveMode {client} />
|
||||||
{#if data.logo}
|
{/if}
|
||||||
<link rel="icon" href={data.logo.url} />
|
</div>
|
||||||
<link rel="apple-touch-icon" href={data.logo.url} />
|
|
||||||
{/if}
|
|
||||||
<link rel="manifest" href="/manifest.webmanifest" />
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<div class="scroll-smooth antialiased font-sans">
|
|
||||||
{@render children()}
|
|
||||||
{#if data.settings}
|
|
||||||
<Footer settings={data.settings} />
|
|
||||||
{/if}
|
|
||||||
{#if data.preview}
|
|
||||||
<VisualEditing />
|
|
||||||
<LiveMode {client} />
|
|
||||||
{/if}
|
|
||||||
|
|||||||
1
template/apps/client/src/routes/test/+page.svelte
Normal file
1
template/apps/client/src/routes/test/+page.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<p>Test</p>
|
||||||
Reference in New Issue
Block a user