finished navbar and blog. todo: finishing touches
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<blockquote
|
||||
class="my-6 rounded-r-lg border-l-4 border-primary bg-muted/30 py-4 pl-6 text-lg text-muted-foreground italic"
|
||||
>
|
||||
{@render children()}
|
||||
</blockquote>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
let { portableText, children } = $props();
|
||||
const { value } = portableText;
|
||||
|
||||
const getHeadingClass = (style: string) => {
|
||||
const baseClasses = 'font-bold tracking-tight';
|
||||
|
||||
switch (style) {
|
||||
case 'h1':
|
||||
return cn(baseClasses, 'text-4xl md:text-5xl lg:text-6xl mb-6 mt-8');
|
||||
case 'h2':
|
||||
return cn(baseClasses, 'text-3xl md:text-4xl mb-4 mt-8');
|
||||
case 'h3':
|
||||
return cn(baseClasses, 'text-2xl md:text-3xl mb-3 mt-6');
|
||||
case 'h4':
|
||||
return cn(baseClasses, 'text-xl md:text-2xl mb-3 mt-6');
|
||||
case 'h5':
|
||||
return cn(baseClasses, 'text-lg md:text-xl mb-2 mt-4');
|
||||
case 'h6':
|
||||
return cn(baseClasses, 'text-base md:text-lg mb-2 mt-4');
|
||||
default:
|
||||
return cn(baseClasses, 'text-xl mb-3 mt-6');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if value.style === 'h1'}
|
||||
<h1 class={getHeadingClass(value.style)}>
|
||||
{@render children()}
|
||||
</h1>
|
||||
{:else if value.style === 'h2'}
|
||||
<h2 class={getHeadingClass(value.style)}>
|
||||
{@render children()}
|
||||
</h2>
|
||||
{:else if value.style === 'h3'}
|
||||
<h3 class={getHeadingClass(value.style)}>
|
||||
{@render children()}
|
||||
</h3>
|
||||
{:else if value.style === 'h4'}
|
||||
<h4 class={getHeadingClass(value.style)}>
|
||||
{@render children()}
|
||||
</h4>
|
||||
{:else if value.style === 'h5'}
|
||||
<h5 class={getHeadingClass(value.style)}>
|
||||
{@render children()}
|
||||
</h5>
|
||||
{:else if value.style === 'h6'}
|
||||
<h6 class={getHeadingClass(value.style)}>
|
||||
{@render children()}
|
||||
</h6>
|
||||
{/if}
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<p class="mb-4 leading-relaxed text-foreground">
|
||||
{@render children()}
|
||||
</p>
|
||||
58
template/apps/client/src/lib/components/cover-image.svelte
Normal file
58
template/apps/client/src/lib/components/cover-image.svelte
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { getImageDimensions } from '@sanity/asset-utils';
|
||||
import { client } from '$lib/sanity';
|
||||
import imageUrlBuilder from '@sanity/image-url';
|
||||
|
||||
let { image, alt = '', width = 1200, height = 600, class: className = '' } = $props();
|
||||
|
||||
const { projectId, dataset } = client.config();
|
||||
const builder = imageUrlBuilder({ projectId: projectId ?? '', dataset: dataset ?? '' });
|
||||
|
||||
function generateCoverImageUrl(
|
||||
imageAsset: any,
|
||||
targetWidth: number,
|
||||
targetHeight: number
|
||||
): string {
|
||||
if (!imageAsset || !projectId || !dataset) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const imageRef = imageAsset.asset?._ref || imageAsset.asset?._id || imageAsset.asset;
|
||||
|
||||
if (!imageRef) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const imageBuilder = builder
|
||||
.image(imageRef)
|
||||
.fit('crop')
|
||||
.crop('center')
|
||||
.width(targetWidth)
|
||||
.height(targetHeight)
|
||||
.format('webp')
|
||||
.auto('format');
|
||||
|
||||
return imageBuilder.url();
|
||||
} catch (error) {
|
||||
console.error('Error generating cover image URL:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const imageUrl = $derived(generateCoverImageUrl(image, width, height));
|
||||
const dimensions = $derived(
|
||||
image?.asset ? getImageDimensions(image) : { width: width, height: height }
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if imageUrl}
|
||||
<img
|
||||
src={imageUrl}
|
||||
{alt}
|
||||
{width}
|
||||
{height}
|
||||
loading="lazy"
|
||||
class="h-full w-full object-cover object-center {className}"
|
||||
/>
|
||||
{/if}
|
||||
@@ -45,17 +45,7 @@
|
||||
items?: NavigationItem[];
|
||||
}
|
||||
|
||||
let {
|
||||
settings,
|
||||
items = [
|
||||
{ name: 'Home', url: '/' },
|
||||
{ name: 'Test', url: '/test' },
|
||||
{
|
||||
name: 'Test Subitems',
|
||||
subitems: [{ name: 'Test Page', url: '#' }]
|
||||
}
|
||||
]
|
||||
}: Props = $props();
|
||||
let { settings, items = [] }: Props = $props();
|
||||
|
||||
let activeIndex = $state(0);
|
||||
let navigationList: HTMLElement;
|
||||
@@ -74,7 +64,7 @@
|
||||
const currentPath = $page.url.pathname;
|
||||
let newActiveIndex = -1;
|
||||
|
||||
items.forEach((item, index) => {
|
||||
items?.forEach((item, index) => {
|
||||
if (item.url === currentPath) {
|
||||
newActiveIndex = index;
|
||||
} else if (item.subitems) {
|
||||
@@ -125,15 +115,17 @@
|
||||
function toggleSubmenu(itemName: string) {
|
||||
expandedSubmenu = expandedSubmenu === itemName ? null : itemName;
|
||||
}
|
||||
|
||||
const shouldReload = $derived($page.url.pathname.startsWith('/blog/'));
|
||||
</script>
|
||||
|
||||
<nav class="bg-primary py-3">
|
||||
<nav class="relative z-[50] bg-primary py-3">
|
||||
<div class="container px-4 md:mx-auto md:px-0">
|
||||
<div class="relative flex items-center">
|
||||
<div class="flex items-center">
|
||||
{#if settings}
|
||||
<a href="/">
|
||||
<Button variant="ghost" class="px-1">
|
||||
<Button variant="ghost">
|
||||
<Logo {settings} class="text-white" />
|
||||
</Button>
|
||||
</a>
|
||||
@@ -143,35 +135,56 @@
|
||||
</div>
|
||||
|
||||
<div class="absolute left-1/2 hidden -translate-x-1/2 transform md:flex">
|
||||
<NavigationMenuRoot class="" viewport={false}>
|
||||
<NavigationMenuRoot class="relative z-[50]" viewport={false}>
|
||||
<div bind:this={navigationList}>
|
||||
<NavigationMenuList class="relative">
|
||||
<div
|
||||
class="pointer-events-none absolute top-0 z-10 h-full rounded-md bg-white/10"
|
||||
class="pointer-events-none absolute top-1/2 z-10 h-9 -translate-y-1/2 rounded-md bg-white/10"
|
||||
style="left: {$indicatorPosition.left}px; width: {$indicatorPosition.width}px;"
|
||||
></div>
|
||||
{#each items as item}
|
||||
{#each items || [] as item}
|
||||
<NavigationMenuItem data-navigation-menu-item>
|
||||
{#if item.url && !item.subitems}
|
||||
<NavigationMenuLink
|
||||
class={cn('relative z-20 text-white transition-colors hover:text-white/80')}
|
||||
href={item.url}
|
||||
data-sveltekit-reload={item.url?.startsWith('/blog/') && shouldReload
|
||||
? 'true'
|
||||
: undefined}
|
||||
>
|
||||
{item.name}
|
||||
</NavigationMenuLink>
|
||||
{:else if item.subitems}
|
||||
<NavigationMenuTrigger
|
||||
class="relative z-20 text-white transition-colors hover:text-white/80"
|
||||
>
|
||||
{item.name}
|
||||
</NavigationMenuTrigger>
|
||||
{#if item.url}
|
||||
<NavigationMenuTrigger
|
||||
class="relative z-20 cursor-pointer px-2 text-white transition-colors hover:text-white/80"
|
||||
>
|
||||
<NavigationMenuLink
|
||||
href={item.url}
|
||||
data-sveltekit-reload={item.url?.startsWith('/blog/') && shouldReload
|
||||
? 'true'
|
||||
: undefined}
|
||||
>
|
||||
{item.name}
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuTrigger>
|
||||
{:else}
|
||||
<NavigationMenuTrigger
|
||||
class="relative z-20 cursor-pointer text-white transition-colors hover:text-white/80"
|
||||
>
|
||||
{item.name}
|
||||
</NavigationMenuTrigger>
|
||||
{/if}
|
||||
<NavigationMenuContent
|
||||
class="bg-primary absolute left-1/2 z-50 mt-2 min-w-full -translate-x-1/2 rounded-md border border-white/20 shadow-lg"
|
||||
class="absolute left-1/2 z-[50] mt-2 min-w-max -translate-x-1/2 rounded-md border border-white/20 bg-primary shadow-lg"
|
||||
>
|
||||
{#each item.subitems as subitem}
|
||||
<NavigationMenuLink
|
||||
href={subitem.url}
|
||||
class="block rounded-md px-4 py-2 text-white transition-colors hover:text-white/80"
|
||||
data-sveltekit-reload={subitem.url?.startsWith('/blog/') && shouldReload
|
||||
? 'true'
|
||||
: undefined}
|
||||
>
|
||||
{subitem.name}
|
||||
</NavigationMenuLink>
|
||||
@@ -191,7 +204,7 @@
|
||||
<MoreHorizontal size={24} />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="top" class="bg-primary border-primary h-full w-full text-white">
|
||||
<SheetContent side="top" class="h-full w-full border-primary bg-primary text-white">
|
||||
<SheetHeader class="border-b border-white/20 pb-4">
|
||||
<SheetTitle class="flex items-center justify-start">
|
||||
{#if settings}
|
||||
@@ -206,7 +219,7 @@
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div class="relative flex flex-col space-y-2 pt-6">
|
||||
{#each items as item, index}
|
||||
{#each items || [] as item, index}
|
||||
{#if item.url && !item.subitems}
|
||||
<div class="relative">
|
||||
<a
|
||||
@@ -216,6 +229,9 @@
|
||||
index === activeIndex && 'bg-white/20'
|
||||
)}
|
||||
onclick={closeMobileMenu}
|
||||
data-sveltekit-reload={item.url?.startsWith('/blog/') && shouldReload
|
||||
? 'true'
|
||||
: undefined}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
@@ -224,7 +240,7 @@
|
||||
<div class="relative space-y-1">
|
||||
{#if index === activeIndex}
|
||||
<div
|
||||
class="absolute bottom-0 left-0 top-0 w-1 rounded-r-md bg-white/20"
|
||||
class="absolute top-0 bottom-0 left-0 w-1 rounded-r-md bg-white/20"
|
||||
></div>
|
||||
{/if}
|
||||
<Button
|
||||
@@ -246,11 +262,26 @@
|
||||
class="ml-4 space-y-1"
|
||||
transition:slide={{ duration: 300, easing: quintOut }}
|
||||
>
|
||||
{#if item.url}
|
||||
<a
|
||||
href={item.url}
|
||||
class="block rounded-md px-6 py-2 text-sm text-white/80 transition-colors hover:text-white"
|
||||
onclick={closeMobileMenu}
|
||||
data-sveltekit-reload={item.url?.startsWith('/blog/') && shouldReload
|
||||
? 'true'
|
||||
: undefined}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
{/if}
|
||||
{#each item.subitems as subitem}
|
||||
<a
|
||||
href={subitem.url}
|
||||
class="block rounded-md px-6 py-2 text-sm text-white/80 transition-colors hover:text-white"
|
||||
onclick={closeMobileMenu}
|
||||
data-sveltekit-reload={subitem.url?.startsWith('/blog/') && shouldReload
|
||||
? 'true'
|
||||
: undefined}
|
||||
>
|
||||
{subitem.name}
|
||||
</a>
|
||||
|
||||
@@ -6,13 +6,24 @@
|
||||
import SanityButton from './blocks/sanity-button.svelte';
|
||||
import Callout from './blocks/callout.svelte';
|
||||
import LinkMark from './blocks/link-mark.svelte';
|
||||
import Heading from './blocks/heading.svelte';
|
||||
import Blockquote from './blocks/blockquote.svelte';
|
||||
import Paragraph from './blocks/paragraph.svelte';
|
||||
|
||||
let { body }: { body: BlockContent } = $props();
|
||||
</script>
|
||||
|
||||
<div class="prose prose-lg max-w-none">
|
||||
<PortableText
|
||||
value={body}
|
||||
<div
|
||||
class="prose prose-lg prose-strong:font-bold
|
||||
prose-em:italic prose-code:bg-muted
|
||||
prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-mono prose-a:text-primary
|
||||
prose-a:hover:underline prose-ul:list-disc
|
||||
prose-ul:ml-6 prose-ul:space-y-2 prose-ol:list-decimal
|
||||
prose-ol:ml-6 prose-ol:space-y-2 prose-li:leading-relaxed
|
||||
max-w-none"
|
||||
>
|
||||
<PortableText
|
||||
value={body}
|
||||
components={{
|
||||
types: {
|
||||
callout: Callout,
|
||||
@@ -21,9 +32,19 @@
|
||||
file: SanityFile,
|
||||
button: SanityButton
|
||||
},
|
||||
block: {
|
||||
h1: Heading,
|
||||
h2: Heading,
|
||||
h3: Heading,
|
||||
h4: Heading,
|
||||
h5: Heading,
|
||||
h6: Heading,
|
||||
blockquote: Blockquote,
|
||||
normal: Paragraph
|
||||
},
|
||||
marks: {
|
||||
link: LinkMark
|
||||
}
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-action"
|
||||
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
||||
</script>
|
||||
|
||||
<p
|
||||
bind:this={ref}
|
||||
data-slot="card-description"
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</p>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-footer"
|
||||
class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-header"
|
||||
class={cn(
|
||||
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card-title"
|
||||
class={cn("font-semibold leading-none", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
23
template/apps/client/src/lib/components/ui/card/card.svelte
Normal file
23
template/apps/client/src/lib/components/ui/card/card.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card"
|
||||
class={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
25
template/apps/client/src/lib/components/ui/card/index.ts
Normal file
25
template/apps/client/src/lib/components/ui/card/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import Root from "./card.svelte";
|
||||
import Content from "./card-content.svelte";
|
||||
import Description from "./card-description.svelte";
|
||||
import Footer from "./card-footer.svelte";
|
||||
import Header from "./card-header.svelte";
|
||||
import Title from "./card-title.svelte";
|
||||
import Action from "./card-action.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction,
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import Root from "./pagination.svelte";
|
||||
import Content from "./pagination-content.svelte";
|
||||
import Item from "./pagination-item.svelte";
|
||||
import Link from "./pagination-link.svelte";
|
||||
import PrevButton from "./pagination-prev-button.svelte";
|
||||
import NextButton from "./pagination-next-button.svelte";
|
||||
import Ellipsis from "./pagination-ellipsis.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Item,
|
||||
Link,
|
||||
PrevButton,
|
||||
NextButton,
|
||||
Ellipsis,
|
||||
//
|
||||
Root as Pagination,
|
||||
Content as PaginationContent,
|
||||
Item as PaginationItem,
|
||||
Link as PaginationLink,
|
||||
PrevButton as PaginationPrevButton,
|
||||
NextButton as PaginationNextButton,
|
||||
Ellipsis as PaginationEllipsis,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLUListElement>> = $props();
|
||||
</script>
|
||||
|
||||
<ul
|
||||
bind:this={ref}
|
||||
data-slot="pagination-content"
|
||||
class={cn("flex flex-row items-center gap-1", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ul>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
|
||||
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
aria-hidden="true"
|
||||
data-slot="pagination-ellipsis"
|
||||
class={cn("flex size-9 items-center justify-center", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<EllipsisIcon class="size-4" />
|
||||
<span class="sr-only">More pages</span>
|
||||
</span>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLLiAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLLiAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<li bind:this={ref} data-slot="pagination-item" {...restProps}>
|
||||
{@render children?.()}
|
||||
</li>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { type Props, buttonVariants } from "$lib/components/ui/button/index.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
size = "icon",
|
||||
isActive,
|
||||
page,
|
||||
children,
|
||||
...restProps
|
||||
}: PaginationPrimitive.PageProps &
|
||||
Props & {
|
||||
isActive: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
{page.value}
|
||||
{/snippet}
|
||||
|
||||
<PaginationPrimitive.Page
|
||||
bind:ref
|
||||
{page}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from "bits-ui";
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: PaginationPrimitive.NextButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<span>Next</span>
|
||||
<ChevronRightIcon class="size-4" />
|
||||
{/snippet}
|
||||
|
||||
<PaginationPrimitive.NextButton
|
||||
bind:ref
|
||||
aria-label="Go to next page"
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
size: "default",
|
||||
variant: "ghost",
|
||||
class: "gap-1 px-2.5 sm:pr-2.5",
|
||||
}),
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from "bits-ui";
|
||||
import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
|
||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: PaginationPrimitive.PrevButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<ChevronLeftIcon class="size-4" />
|
||||
<span>Previous</span>
|
||||
{/snippet}
|
||||
|
||||
<PaginationPrimitive.PrevButton
|
||||
bind:ref
|
||||
aria-label="Go to previous page"
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
size: "default",
|
||||
variant: "ghost",
|
||||
class: "gap-1 px-2.5 sm:pl-2.5",
|
||||
}),
|
||||
className
|
||||
)}
|
||||
children={children || Fallback}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from "bits-ui";
|
||||
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
count = 0,
|
||||
perPage = 10,
|
||||
page = $bindable(1),
|
||||
siblingCount = 1,
|
||||
...restProps
|
||||
}: PaginationPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<PaginationPrimitive.Root
|
||||
bind:ref
|
||||
bind:page
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
class={cn("mx-auto flex w-full justify-center", className)}
|
||||
{count}
|
||||
{perPage}
|
||||
{siblingCount}
|
||||
{...restProps}
|
||||
/>
|
||||
Reference in New Issue
Block a user