This commit is contained in:
pakintada@gmail.com 2026-02-17 14:30:02 +07:00
commit 451223816b
338 changed files with 9938 additions and 0 deletions

View file

@ -0,0 +1,109 @@
<script lang="ts">
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import { useSidebar } from '$lib/components/ui/sidebar/index';
import { ChevronsUpDownIcon, PlusIcon, LogOutIcon } from '@lucide/svelte/icons';
import { auth as authStore } from '$lib/core/stores/auth';
import { get } from 'svelte/store';
import type { User } from 'firebase/auth';
import { asset } from '$app/paths';
import { auth } from '$lib/core/client/firebase';
import { goto } from '$app/navigation';
import { onDestroy } from 'svelte';
import * as adb from '$lib/core/adb/adb';
import { browser } from '$app/environment';
import { deleteCookiesOnNonBrowser } from '$lib/helpers/cookie';
const sidebar = useSidebar();
let currentUser: User | null = $state(null);
let userImage: any = $state(null);
let unsubAuthStore = authStore.subscribe((user) => {
if (user) {
currentUser = user;
userImage = asset(currentUser?.photoURL ?? '');
}
});
onDestroy(() => {
unsubAuthStore();
});
async function logout() {
let instance = adb.getAdbInstance();
if (instance) {
try {
await adb.executeCmd('rm /sdcard/coffeevending/ignore_pass');
await adb.executeCmd('reboot');
} catch (e) {
console.error('error disconnect device while logging out', e);
}
await adb.disconnect();
}
authStore.set(null);
if (browser && 'cookieStore' in window) await cookieStore.delete('logged_in');
else deleteCookiesOnNonBrowser('logged_in');
await auth.signOut();
await auth.updateCurrentUser(null);
goto('/login');
}
</script>
<Sidebar.Menu>
<Sidebar.MenuItem>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Sidebar.MenuButton
{...props}
size="lg"
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
>
<!-- <activeTeam.logo class="size-4" /> -->
<img src={userImage} alt="" loading="lazy" />
</div>
<div class="grid flex-1 text-start text-sm leading-tight">
<span class="truncate font-medium">
{currentUser?.displayName}
</span>
<span class="truncate text-xs">{currentUser?.email}</span>
</div>
<ChevronsUpDownIcon class="ms-auto" />
</Sidebar.MenuButton>
{/snippet}
</DropdownMenu.Trigger>
<DropdownMenu.Content
class="w-(--bits-dropdown-menu-anchor-width) min-w-56 rounded-lg"
align="start"
side={sidebar.isMobile ? 'bottom' : 'right'}
sideOffset={4}
>
<DropdownMenu.Label class="text-xs text-muted-foreground">Account</DropdownMenu.Label>
<!-- {#each teams as team, index (team.name)}
<DropdownMenu.Item onSelect={() => (activeTeam = team)} class="gap-2 p-2">
<div class="flex size-6 items-center justify-center rounded-md border">
<team.logo class="size-3.5 shrink-0" />
</div>
{team.name}
<DropdownMenu.Shortcut>{index + 1}</DropdownMenu.Shortcut>
</DropdownMenu.Item>
{/each} -->
<DropdownMenu.Separator />
<DropdownMenu.Item class="gap-2 p-2">
<div class="flex size-6 items-center justify-center rounded-md border bg-transparent">
<LogOutIcon class="size-4" />
</div>
<div class="font-medium text-muted-foreground">
<button onclick={logout}> Logout </button>
</div>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Sidebar.MenuItem>
</Sidebar.Menu>

View file

@ -0,0 +1,130 @@
<script lang="ts">
import * as Sidebar from '$lib/components/ui/sidebar/index';
import { onDestroy, type ComponentProps } from 'svelte';
import { asset } from '$app/paths';
import AppAccountSelect from './app-account-select.svelte';
import {
Code,
LayoutDashboard,
LucideEye,
CherryIcon,
DiamondIcon,
BugIcon,
CupSodaIcon
} from '@lucide/svelte/icons';
import TaobinLogo from '$lib/assets/logo.svelte';
import { goto } from '$app/navigation';
import Button from '$lib/components/ui/button/button.svelte';
import { get } from 'svelte/store';
import { sidebarStore } from '$lib/core/stores/sidebar';
let sideBar: HTMLElement | null = $state(null);
let isSideBarOpen: boolean = $state(true);
const data = {
navMain: [
{
title: 'Home',
items: [
{
title: 'Dashboard',
url: '/dashboard',
icon: LayoutDashboard
}
]
},
{
title: 'Recipe',
items: [
{
title: 'Overview',
url: '/recipe/overview',
icon: LucideEye
},
{
title: 'Topping',
url: '/recipe/topping',
icon: CherryIcon
},
{
title: 'Material',
url: '/recipe/material',
icon: DiamondIcon
}
]
},
{
title: 'Tools',
items: [
{
title: 'Brew',
url: '/tools/brew',
icon: CupSodaIcon
},
{
title: 'Debug',
url: '/tools/debug',
icon: BugIcon
}
]
}
// more to add here
]
};
function onClickLogoIcon() {
goto('/departments');
}
let unsubSidebar = sidebarStore.subscribe((state) => {
isSideBarOpen = state;
});
onDestroy(() => {
unsubSidebar();
});
let {
ref = sideBar,
collapsible = 'icon',
...restProps
}: ComponentProps<typeof Sidebar.Root> = $props();
</script>
<Sidebar.Root {collapsible} {...restProps}>
<Sidebar.Header>
<div class="flex items-center justify-center">
<button class="hover:cursor-pointer" onclick={onClickLogoIcon}>
<TaobinLogo size={isSideBarOpen ? 96 : 24} fillColor={'#FFFFFF'} />
</button>
</div>
</Sidebar.Header>
<Sidebar.Content>
{#each data.navMain as nav}
<Sidebar.Group>
<Sidebar.GroupLabel>{nav.title}</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
{#each nav.items as sub}
<Sidebar.MenuItem>
<Sidebar.MenuButton>
{#snippet child({ props })}
<a href={sub.url} {...props}>
{#if sub.icon}
<sub.icon />
{/if}
<span>{sub.title}</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
{/each}
</Sidebar.Content>
<Sidebar.Footer>
<AppAccountSelect />
</Sidebar.Footer>
</Sidebar.Root>

View file

@ -0,0 +1,8 @@
<script lang="ts">
</script>

View file

@ -0,0 +1,424 @@
<script lang="ts">
import { Button, type ButtonVariant } from './ui/button';
import * as Card from './ui/card/index';
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
import Label from '$lib/components/ui/label/label.svelte';
import { LockIcon, UnlockIcon, Circle, AlertCircleIcon } from '@lucide/svelte/icons';
import * as adb from '$lib/core/adb/adb';
import { machineInfoStore } from '$lib/core/stores/machineInfoStore';
import { toast } from 'svelte-sonner';
import { handleIncomingMessages } from '$lib/core/handlers/messageHandler';
import { auth as authStore } from '$lib/core/stores/auth';
import { get } from 'svelte/store';
import { AdbDaemonWebUsbDeviceManager } from '@yume-chan/adb-daemon-webusb';
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
import { onMount } from 'svelte';
import { deviceCredentialManager } from '$lib/core/adb/deviceCredManager';
import { file } from 'zod/mini';
let { enableComponent = true } = $props();
// connection button states
let connectionButtonText = $state('Connect');
let connectionButtonDisable = $state(false);
let connectionButtonVariant: ButtonVariant = $state('default');
let connectDeviceOk = $state(false);
let hasStoredDevice = $state(false);
let openAppBrewWhenConnected = $state(true);
let hasOpenedBrewOnce = $state(false);
// progress
let showLoadProgress = $state(false);
//
let recipe: any | undefined = $state(undefined);
let machineStatus: string = $state('Ok');
// machineInfoStore.subscribe((mInfo) => {});
const essentialFiles = ['/sdcard/coffeevending/versions/'];
async function loadEssentialFiles() {
showLoadProgress = true;
machineStatus = 'Loading infos ...';
let instance = adb.getAdbInstance();
if (instance) {
// check country
let country = await adb.pull('/sdcard/coffeevending/country/short');
machineStatus = `Found country: ${country}`;
// check dev
let devMode = await adb.pull('/sdcard/coffeevending/CURR_TEST');
if (devMode?.includes('1')) {
machineStatus = `Dev mode enabled`;
} else {
machineStatus = `Dev mode disabled`;
}
// check .bid
let boxid = await adb.pull('/sdcard/coffeevending/.bid');
if (boxid) {
machineStatus = `Box id is ${boxid}`;
} else {
machineStatus = 'No box id';
}
machineInfoStore.set({
boxId: boxid,
versions: {
firmware: '',
brew: '',
xmlengine: '',
netcore: '',
devbox: ''
},
devMode: devMode?.includes('1') ?? false,
country: country ?? '',
status: '',
errors: []
});
handleIncomingMessages(
JSON.stringify({
type: 'chat',
payload: `${new Date().toLocaleTimeString()}: ${get(authStore)?.displayName} has connected to ${boxid}`
})
);
} else {
machineStatus = 'Instance lost, try disconnect and re-connect again';
toast.error('Unexpected Error');
}
showLoadProgress = false;
}
async function openBrewApp() {
try {
let instance = adb.getAdbInstance();
if (instance) {
try {
// bypass
await adb.executeCmd('echo -n hurr > /sdcard/coffeevending/ignore_pass');
} catch (e) {}
let result = await adb.executeCmd(
'am start -n com.forthvending.coffeemain/com.forthvending.coffeemain.MainActivity'
);
if (result?.output) {
toast.success('Open app success!');
machineStatus = 'open app success, check the screen and put the password';
} else if (result?.error) {
// case usb connection cutoff
if (result.error === 'ExactReadableEndedError') {
toast.warning('Connection unstable');
machineStatus = 'app maybe opened, check the screen';
} else {
throw new Error(`Exit ${result.exitCode}. ${result.error}`);
}
} else {
throw new Error('Instance not found or error while executing');
}
hasOpenedBrewOnce = true;
try {
// bypass
await adb.executeCmd('echo -n hurr > /sdcard/coffeevending/ignore_pass');
} catch (e) {}
setTimeout(async () => {
try {
// bypass
await adb.executeCmd('input tap 336 795');
} catch (e) {}
}, 3000);
}
} catch (e: any) {
machineStatus = 'Cannot open brew app';
toast.error('Error while trying to open brew app, please check the screen. ', {
description: e.toString()
});
}
}
async function testPushPullFile() {
try {
let instance = adb.getAdbInstance();
if (instance) {
await adb.push(
'/sdcard/coffeevending/test.json',
JSON.stringify({ test: new Date().toLocaleTimeString() })
);
let result = await adb.pull('/sdcard/coffeevending/test.json');
if (result) {
if (result === '') {
console.log('push pull not ok, get empty');
} else {
console.log('push pull ok', result);
}
} else {
console.log('push pull not ok', result);
}
}
} catch (error) {
console.log('test push file failed', error);
}
}
async function connectAdb() {
connectionButtonText = '...';
// lock away no spam
connectionButtonDisable = true;
connectionButtonVariant = 'outline';
try {
if (!AdbDaemonWebUsbDeviceManager.BROWSER) {
throw new Error('WebUSB not supported, try using fallback or different browser');
}
await adb.connnectViaWebUSB();
let instance = adb.getAdbInstance();
if (instance) {
await loadEssentialFiles();
if (openAppBrewWhenConnected) await openBrewApp();
}
} catch (e: any) {
if (e.message === 'CREDENTIAL_EXPIRED') {
try {
await deviceCredentialManager.clearAllCredentials();
hasStoredDevice = false;
} catch (ignored) {}
}
console.log('error on quick adb: ', e);
toast.error(`Machine Connection Error`, {
description: e.toString()
});
connectionButtonText = 'Retry';
connectionButtonVariant = 'default';
connectDeviceOk = false;
}
connectionButtonDisable = false;
}
async function disconnectAdb() {
await adb.disconnect();
connectionButtonText = 'Connect';
connectionButtonVariant = 'default';
connectDeviceOk = false;
handleIncomingMessages(
JSON.stringify({
type: 'chat',
payload: `${new Date().toLocaleTimeString()}: ${get(authStore)?.displayName} has disconnected!`
})
);
}
function checkDeviceConnection() {
try {
let instance = adb.getAdbInstance();
if (instance) {
// ready
connectionButtonText = 'Disconnect';
connectionButtonVariant = 'destructive';
connectDeviceOk = true;
} else {
connectionButtonText = 'Connect';
connectionButtonVariant = 'default';
connectDeviceOk = false;
}
} catch (e: any) {
console.log('error on quick adb: ', e);
toast.error(`Machine Connection Error`, {
description: e.toString()
});
connectionButtonText = 'Retry';
connectionButtonVariant = 'default';
connectDeviceOk = false;
}
connectionButtonDisable = false;
}
async function checkStoredCredentials() {
try {
if (!AdbDaemonWebUsbDeviceManager.BROWSER) {
hasStoredDevice = false;
return;
}
const devices = await AdbDaemonWebUsbDeviceManager.BROWSER.getDevices();
if (!devices || devices.length === 0) {
hasStoredDevice = false;
return;
}
const credentialStore = new AdbWebCredentialStore();
let hasKeys = false;
try {
for await (const key of credentialStore.iterateKeys()) {
hasKeys = true;
break;
}
} catch (e) {
console.log('check stored error', e);
}
hasStoredDevice = devices.length > 0 && hasKeys;
} catch (e) {
console.error('check stored error', e);
hasStoredDevice = false;
}
}
async function tryAutoConnect(): Promise<boolean> {
try {
connectionButtonText = '...';
// lock away no spam
connectionButtonDisable = true;
connectionButtonVariant = 'outline';
if (!AdbDaemonWebUsbDeviceManager.BROWSER) {
throw new Error('WebUSB not supported, try using fallback or different browser');
}
const devices = await AdbDaemonWebUsbDeviceManager.BROWSER?.getDevices();
if (!devices || devices.length == 0) {
throw new Error('No device found');
}
if (devices.length > 1) {
throw new Error('Too many connected devices');
}
const device = devices[0];
const credStore = new AdbWebCredentialStore();
try {
await adb.connectDeviceByCred(device, credStore);
return true;
} catch (e: any) {
if (e.message === 'CREDENTIAL_EXPIRED') {
try {
await deviceCredentialManager.clearAllCredentials();
hasStoredDevice = false;
} catch (ignored) {}
}
return false;
}
} catch (e: any) {
console.log('error on auto connect adb: ', e);
toast.error(`Machine Connection Error`, {
description: e.toString()
});
connectionButtonText = 'Connect';
connectionButtonVariant = 'default';
connectDeviceOk = false;
}
connectionButtonDisable = false;
return false;
}
// update every 1s
setInterval(async function () {
checkDeviceConnection();
}, 1000);
onMount(async () => {
await checkStoredCredentials();
await tryAutoConnect();
});
</script>
<div class="p-4">
<Card.Root class="h-full bg-muted/50">
<Card.Header>
<Card.Title>Machine Shortcuts</Card.Title>
<Card.Description>Shortcuts for machine i.e. connect or hotfix</Card.Description>
{#if enableComponent}
<Card.Action>
<Button
variant={connectionButtonVariant}
disabled={connectionButtonDisable}
onclick={connectDeviceOk ? disconnectAdb : connectAdb}>{connectionButtonText}</Button
>
</Card.Action>
{/if}
</Card.Header>
<Card.Content>
{#if enableComponent}
<!-- -->
<div class="items-center space-y-8">
<!-- <div class="flex w-full items-center gap-2">
<UnlockIcon />
<span>This feature is enabled </span>
</div> -->
<div class="flex w-full items-center justify-between gap-2">
<p>
Device: {connectDeviceOk ? 'Online' : 'Offline'}
</p>
<Circle
class=""
color={connectDeviceOk ? 'green' : 'red'}
fill={connectDeviceOk ? 'green' : 'red'}
size={16}
/>
</div>
<div class="flex w-full items-start gap-2">
<Checkbox
id="open_brew_now"
checked={openAppBrewWhenConnected}
onCheckedChange={() => {
openAppBrewWhenConnected = !openAppBrewWhenConnected;
}}
/>
<div class="grid gap-2">
<Label for="open_brew_now">Show brew app when connected</Label>
<p class="text-sm text-muted-foreground">
Immediately try to open brew app when first connected
</p>
</div>
</div>
<!-- open app brew manual -->
<div class="flex w-full items-center gap-2">
<Button variant="default" onclick={openBrewApp} disabled={!connectDeviceOk}
>Open Brew App</Button
>
<Button variant="default" disabled={true}>Refresh Infos</Button>
<!-- test push file -->
<Button variant="default" onclick={testPushPullFile} disabled={!connectDeviceOk}
>Test Push</Button
>
</div>
</div>
{:else}
<!-- show lock -->
<div class="flex w-full items-center justify-center gap-2">
<LockIcon /> <span>This feature is not enabled </span>
</div>
{/if}
</Card.Content>
<Card.Footer>
<!-- Display status -->
{#if connectDeviceOk}
{#if showLoadProgress}
<Spinner />
{/if}
<p class="mx-4 font-mono text-sm text-muted-foreground">{machineStatus}</p>
{/if}
</Card.Footer>
</Card.Root>
</div>

View file

@ -0,0 +1,81 @@
<script lang="ts">
import { messages as WSMsg } from '$lib/core/handlers/messageHandler';
import * as Card from './ui/card/index';
import { ScrollArea } from './ui/scroll-area/index';
import { Separator } from './ui/separator/index';
import { permission as currentPermissions } from '$lib/core/stores/permissions';
import { get } from 'svelte/store';
import { needPermission, requirePermission } from '$lib/core/handlers/permissionHandler';
import DashboardQuickAdb from './dashboard-quick-adb.svelte';
import MachineInfo from './machine-info.svelte';
import { departmentStore } from '$lib/core/stores/departments.ts';
import { onDestroy } from 'svelte';
let activities: string[] = $state([]);
let activitiesLogElement: HTMLElement | undefined = $state();
function scrollToLatest(node: HTMLElement) {
node.scroll({
top: node.scrollHeight,
behavior: 'smooth'
});
}
let unsubWebSocketMsg = WSMsg.subscribe((history) => {
activities = history;
});
let perms = get(currentPermissions);
$effect.pre(() => {
if (activitiesLogElement && activities) {
}
});
$effect(() => {
if (activitiesLogElement && activities) {
scrollToLatest(activitiesLogElement);
}
});
onDestroy(() => {
unsubWebSocketMsg();
});
console.log('current department: ', get(departmentStore));
</script>
<div class="grid grid-flow-row-dense grid-cols-3 grid-rows-3">
<div class="col-span-2 p-4">
<Card.Root class=" h-full w-full bg-muted/50">
<Card.Header>
<Card.Title>Latest Activity</Card.Title>
<Card.Description>Real time activities. Click to view full activities.</Card.Description>
<!-- add view full activity popup -->
</Card.Header>
<Card.Content>
<ScrollArea class="h-full max-h-50 w-full rounded-md border" ref={activitiesLogElement}>
<div class="h-max p-4">
{#if activities.length > 0}
{#each activities as activity}
<div class="text-sm">
{activity}
</div>
<Separator class="my-2" />
{/each}
{:else}
<div class="text-sm">No ongoing activity right now!</div>
<Separator class="my-2" />
{/if}
</div>
</ScrollArea>
</Card.Content>
</Card.Root>
</div>
<!-- needPermission('tools.core.*') -->
<DashboardQuickAdb enableComponent={true} />
<!-- needPermission('tools.core.*') -->
<MachineInfo enableComponent={true} />
</div>

View file

@ -0,0 +1,11 @@
<script lang="ts">
import {page} from '$app/state';
</script>
<main>
<slot/>
</main>
<footer>
Status: {page.status}
</footer>

View file

@ -0,0 +1,63 @@
<script lang="ts">
import { Button, type ButtonVariant } from './ui/button';
import * as Card from './ui/card/index';
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
import { machineInfoStore } from '$lib/core/stores/machineInfoStore';
import type { AppVersions, MachineInfo } from '$lib/models/machineInfo.model';
import { socketStore } from '$lib/core/stores/websocketStore';
import { get } from 'svelte/store';
import { onDestroy } from 'svelte';
let { enableComponent = true } = $props();
let currentMachineInfo: MachineInfo | undefined = $state();
let infoMap: any | undefined = $state();
let unsubMachineInfo = machineInfoStore.subscribe((mInfo) => {
console.log('get info', JSON.stringify(mInfo));
// check status web socket
currentMachineInfo = mInfo;
infoMap = JSON.parse(JSON.stringify(currentMachineInfo ?? {}));
});
onDestroy(() => {
unsubMachineInfo();
});
</script>
<div class={`p-4 ${enableComponent ? '' : 'hidden'}`}>
<Card.Root class="bg-muted/50">
<Card.Header>
<Card.Title>Machine Infos</Card.Title>
<Card.Description>Informations of current connected machine</Card.Description>
</Card.Header>
<Card.Content>
{#if currentMachineInfo && infoMap}
<div>
<div class="flex w-full items-center justify-between gap-2">
<h1 class="font-bold">Box ID:</h1>
<p>{currentMachineInfo.boxId}</p>
</div>
<h1 class="font-bold">Versions:</h1>
{#each Object.keys(infoMap['versions']) as v}
<div class="flex w-full items-center justify-between gap-2">
<h1 class="px-4 font-bold">{v}</h1>
{#if infoMap['versions'][v] && infoMap['versions'][v] != ''}
<p>{infoMap['versions'][v]}</p>
{:else}
<Spinner />
{/if}
</div>
{/each}
</div>
{:else}
<div class="flex w-full items-center justify-center gap-2">
<Spinner />
</div>
{/if}
</Card.Content>
</Card.Root>
</div>

View file

@ -0,0 +1,114 @@
// {
// "MixOrder": 0,
// "StringParam": "",
// "FeedParameter": 0,
// "FeedPattern": 0,
// "isUse": true,
// "materialPathId": 599501,
// "powderGram": 0,
// "powderTime": 0,
// "stirTime": 110,
// "syrupGram": 0,
// "syrupTime": 0,
// "waterCold": 0,
// "waterYield": 0
// }
import type { ColumnDef } from '@tanstack/table-core';
import { renderComponent, renderSnippet } from '../ui/data-table';
import RecipelistIsuse from './recipelist-isuse.svelte';
import RecipelistValue from './recipelist-value.svelte';
import { DragHandle } from './recipelist-table.svelte';
import { createRawSnippet } from 'svelte';
import RecipelistMatSelect from './recipelist-mat-select.svelte';
export type RecipelistMaterial = {
id: number;
material_id: string;
is_use: boolean;
values: {
string_param: string;
mix_order: number;
feed: {
pattern: number;
parameter: number;
};
powder: {
gram: number;
time: number;
};
syrup: {
gram: number;
time: number;
};
water: {
cold: number;
yield: number;
};
};
};
export const columns: ColumnDef<RecipelistMaterial>[] = [
{
id: 'id',
accessorKey: 'id',
header: () => null,
cell: () => null,
enableSorting: true
},
{
id: 'drag',
header: () =>
renderSnippet(
createRawSnippet(() => ({
render: () => '<div class="w-1 text-start"></div>'
}))
),
cell: () => renderSnippet(DragHandle)
},
{
accessorKey: 'is_use',
id: 'is_use',
header: ({ column }) =>
renderSnippet(
createRawSnippet(() => ({
render: () => '<div class="w-0.5">Enable</div>'
}))
),
cell: ({ row }) => {
return renderComponent(RecipelistIsuse, {
checked: row.original.is_use,
onCheckedChange: (value) => {
row.original.is_use = !!value;
row.toggleSelected(row.original.is_use);
}
});
},
enableSorting: false,
enableHiding: false
},
{
id: 'material_id',
header: ({ column }) => 'Material',
cell: ({ row }) => {
return renderComponent(RecipelistMatSelect, {
currentMat: row.original.material_id,
onMatChange: (value: any) => {
row.original.material_id = value;
console.log('change mat', value);
row.toggleSelected(row.original.is_use);
}
});
}
},
{
accessorKey: 'values',
id: 'values',
header: ({ column }) => 'Values',
cell: ({ row }) => {
return renderComponent(RecipelistValue, {
...row.original.values
});
}
}
];

View file

@ -0,0 +1,145 @@
<script lang="ts">
import * as Tabs from '$lib/components/ui/tabs/index';
import * as Card from '$lib/components/ui/card/index';
import Label from '$lib/components/ui/label/label.svelte';
import Input from '$lib/components/ui/input/input.svelte';
import { onDestroy, onMount } from 'svelte';
import RecipelistTable from './recipelist-table.svelte';
import { columns, type RecipelistMaterial } from './columns';
import { get, readable, writable } from 'svelte/store';
import { materialFromMachineQuery } from '$lib/core/stores/recipeStore';
import { generateIcing } from '$lib/helpers/icingGen';
import { machineInfoStore } from '$lib/core/stores/machineInfoStore';
import MachineInfo from '../machine-info.svelte';
import { v4 as uuidv4 } from 'uuid';
//
let { recipeData, onPendingChange }: { recipeData: any; onPendingChange: any } = $props();
let menuName: string = $state('');
let materialSnapshot: any = $state();
let machineInfoSnapshot: any = $state();
let recipeListMatState: RecipelistMaterial[] = $state([]);
let recipeListOriginal: RecipelistMaterial[] = $state([]);
function remappingToColumn(data: any[]): RecipelistMaterial[] {
let ret: RecipelistMaterial[] = [];
// expect recipelist
if (materialSnapshot) {
let d_cnt = 0;
for (let rpl of data) {
let mat = materialSnapshot.filter(
(x: any) => x['id'].toString() === rpl['materialPathId'].toString()
)[0];
// console.log('mat filter get', Object(mat), Object.keys(mat));
let name = mat ? mat['materialOtherName'] : rpl['materialPathId'];
if (rpl['materialPathId'] === 0) {
name = '-';
}
// let gen_id = generateRowId();
// console.log(`generated for ${rpl['materialPathId']} = ${gen_id}`);
ret.push({
id: d_cnt,
material_id: `${name} (${rpl['materialPathId']})`,
is_use: rpl['isUse'],
values: {
string_param: rpl['StringParam'],
mix_order: rpl['MixOrder'],
feed: {
pattern: rpl['feedPattern'],
parameter: rpl['feedParameter']
},
powder: {
gram: rpl['powderGram'],
time: rpl['powderTime']
},
syrup: {
gram: rpl['syrupGram'],
time: rpl['syrupTime']
},
water: {
cold: rpl['waterCold'],
yield: rpl['waterYield']
}
}
});
d_cnt++;
}
}
return ret;
}
async function checkChanges(original: any) {
console.log('old', original, 'updated', recipeListMatState);
if (recipeListOriginal.length == 0) {
recipeListOriginal = original;
}
if (original !== recipeListMatState) {
await onPendingChange({
target: 'recipeList',
value: original
});
}
}
onMount(() => {
machineInfoSnapshot = get(machineInfoStore);
if (recipeData) {
menuName =
recipeData['name'] ?? (recipeData['otherName'] ? recipeData['otherName'] : 'Not set');
materialSnapshot = get(materialFromMachineQuery);
recipeListMatState = remappingToColumn(recipeData['recipes']);
// save old value\
}
});
</script>
<!-- show info -->
<!-- latest edit date -->
<!-- Menu Status -->
<div class="-mb-4 flex w-full flex-col gap-6">
<Tabs.Root value="info">
<Tabs.List>
<Tabs.Trigger value="info">Info</Tabs.Trigger>
<Tabs.Trigger value="details">Details</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="info">
<Card.Root>
<Card.Header>
<Card.Title>Info</Card.Title>
<Card.Description>Info about this menu</Card.Description>
</Card.Header>
<Card.Content class="grid gap-6">
<div class="grid grid-flow-row gap-3">
<Label for="tabs-menu-name">Name</Label>
<Input id="tabs-menu-name" value={recipeData['name'] ?? ''} />
<Label for="tabs-menu-other-name">Other Name</Label>
<Input id="tabs-menu-other-name" value={recipeData['otherName'] ?? ''} />
</div>
<div class="grid gap-3"></div>
</Card.Content>
</Card.Root>
</Tabs.Content>
<Tabs.Content value="details">
<RecipelistTable data={recipeListMatState} {columns} onStateChange={checkChanges} />
</Tabs.Content>
</Tabs.Root>
</div>

View file

@ -0,0 +1,14 @@
<script lang="ts">
import Checkbox from '../ui/checkbox/checkbox.svelte';
import { onMount, type ComponentProps } from 'svelte';
let {
checked,
onCheckedChange = (v) => (checked = v),
...restProps
}: ComponentProps<typeof Checkbox> = $props();
</script>
<div class="flex w-1.5 items-center justify-center">
<Checkbox {checked} onCheckedChange={(e) => onCheckedChange(e)} {...restProps} />
</div>

View file

@ -0,0 +1,76 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Button } from '../ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index';
import { get } from 'svelte/store';
import {
materialData,
materialFromMachineQuery,
referenceFromPage
} from '$lib/core/stores/recipeStore';
import Input from '$lib/components/ui/input/input.svelte';
import { SearchIcon, PlusIcon, BeanIcon, StarIcon } from '@lucide/svelte/icons';
let { currentMat, onMatChange } = $props();
let allMatData: any = $state();
let refPage: string | undefined = $state();
function changeMat(mat_id: string) {
currentMat = mat_id;
onMatChange(mat_id);
}
function getMaterialTypeIcon(mat_id: number) {}
onMount(() => {
refPage = get(referenceFromPage);
if (refPage === 'brew') allMatData = get(materialFromMachineQuery);
else if (refPage === 'overview') allMatData = get(materialData);
});
</script>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="ghost" class="text-muted-foreground hover:bg-transparent">
{currentMat}
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<div class="sticky top-0 z-10 my-4 flex items-center gap-2 bg-accent">
<SearchIcon />
<Input
type="text"
placeholder="Search by mat id or name"
onchange={(e) => {}}
oninput={(e) => {}}
/>
</div>
<!-- permission create mat -->
<DropdownMenu.Item>
<div class="flex gap-2">
<PlusIcon />
<p>Create Material</p>
</div>
</DropdownMenu.Item>
{#each allMatData as mat}
<DropdownMenu.Item onclick={() => changeMat(`${mat.materialOtherName} (${mat.id})`)}>
<div class="flex gap-2">
{#if mat.BeanChannel}
<BeanIcon />
{:else if mat.id > 8110 && mat.id < 8131}
<StarIcon />
{/if}
<p>{mat.materialOtherName} ({mat.id})</p>
</div>
</DropdownMenu.Item>
{:else}
<DropdownMenu.Item>
<p class="text-muted-foreground">No materials available</p>
</DropdownMenu.Item>
{/each}
</DropdownMenu.Content>
</DropdownMenu.Root>

View file

@ -0,0 +1,196 @@
<script module>
export { DragHandle };
</script>
<script lang="ts">
import * as Card from '$lib/components/ui/card/index';
import Label from '$lib/components/ui/label/label.svelte';
import Input from '$lib/components/ui/input/input.svelte';
import RecipeDetail from './recipe-detail.svelte';
import {
createTable,
getCoreRowModel,
getFacetedRowModel,
getSortedRowModel,
type ColumnDef,
type Row,
type RowSelectionState,
type SortingState
} from '@tanstack/table-core';
import { createSvelteTable } from '../ui/data-table';
import * as Table from '$lib/components/ui/table/index';
import FlexRender from '../ui/data-table/flex-render.svelte';
import ScrollArea from '../ui/scroll-area/scroll-area.svelte';
import { type Attachment } from 'svelte/attachments';
import Button from '../ui/button/button.svelte';
import { GripVerticalIcon } from '@lucide/svelte/icons';
import { DragDropProvider } from '@dnd-kit-svelte/svelte';
import { move } from '@dnd-kit/helpers';
import { RestrictToVerticalAxis } from '@dnd-kit/abstract/modifiers';
import { type UniqueIdentifier } from '@dnd-kit/abstract';
import { type RecipelistMaterial } from './columns';
import { useSortable } from '@dnd-kit-svelte/svelte/sortable';
import { onMount } from 'svelte';
import { get, writable, type Writable } from 'svelte/store';
// type DataTableProps<TData, TValue> = {
// columns: ColumnDef<TData, TValue>[];
// data: TData[];
// };
let {
data,
columns,
onStateChange
}: {
data: RecipelistMaterial[];
columns: ColumnDef<any, any>[];
onStateChange: any;
} = $props();
let sorting = $state<SortingState>([]);
let rowSelection = $state<RowSelectionState>({});
// let recipeMatState = $state<RecipelistMaterial[]>(data);
const table = createSvelteTable({
get data() {
return data;
},
columns,
enableRowSelection: true,
enableMultiRowSelection: true,
getRowId: (row: any) => row.id.toString(),
state: {
get sorting() {
return sorting;
},
get rowSelection() {
return rowSelection;
}
},
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFacetedRowModel(),
onStateChange: async (updater) => {
console.log('table state change', data);
await onStateChange(table.getRowModel().rows.map((x) => x.original));
},
onSortingChange: async (updater) => {
console.log('triggering sorting');
if (typeof updater === 'function') {
sorting = updater(sorting);
} else {
sorting = updater;
}
await onStateChange(table.getRowModel().rows.map((x) => x.original));
},
onRowSelectionChange: async (updater) => {
// table.getRowModel().rows.find((x) => x.original.id == )
if (typeof updater === 'function') {
rowSelection = updater(rowSelection);
let rows = table.getRowModel().rows;
console.log('state size', data, rows);
} else {
rowSelection = updater;
}
await onStateChange(table.getRowModel().rows.map((x) => x.original));
}
});
</script>
<!-- use card -->
<Card.Root>
<Card.Header>
<Card.Title>Recipe List</Card.Title>
<Card.Description>Material used in this menu's brewing process</Card.Description>
<Card.Content>
<!-- table -->
<DragDropProvider
modifiers={[
// @ts-expect-error @dnd-kit/adbstract types are botched atm
RestrictToVerticalAxis
]}
onDragEnd={(e) => {
// snap
data = move(data as any, e as any);
//
}}
>
<ScrollArea class="h-[60vh] w-full rounded-md border" type="always">
<Table.Root class="relative w-full">
<Table.Header class="sticky top-0 z-10">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
<Table.Head colspan={header.colSpan}>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body class="**:data-[slot=table-cell]:first:w-8">
{#if table.getRowModel().rows?.length}
{#each table.getRowModel().rows as row, index (row.id)}
{@render DraggableRow({ row, index })}
{/each}
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center"
>Empty recipe.</Table.Cell
>
</Table.Row>
{/if}
</Table.Body>
</Table.Root>
</ScrollArea>
</DragDropProvider>
</Card.Content>
</Card.Header>
</Card.Root>
{#snippet DraggableRow({ row, index }: { row: Row<any>; index: number })}
{@const { ref, isDragging, handleRef } = useSortable({
id: row.original.id,
index: () => index
})}
<Table.Row
data-state={row.original.is_use ? 'selected' : undefined}
data-dragging={isDragging.current}
class="relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80"
{@attach ref}
>
{#each row.getVisibleCells() as cell (cell.id)}
<Table.Cell>
<FlexRender
attach={handleRef}
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</Table.Cell>
{/each}
</Table.Row>
{/snippet}
{#snippet DragHandle({ attach }: { attach: Attachment })}
<Button
{@attach attach}
variant="ghost"
size="icon"
class="size-7 text-muted-foreground hover:bg-transparent"
>
<GripVerticalIcon class="size-3 text-muted-foreground" />
<span class="sr-only">Drag to reorder</span>
</Button>
{/snippet}

View file

@ -0,0 +1,6 @@
<script lang="ts">
import type { RecipelistMaterial } from './columns';
let { string_param, mix_order, feed, water, powder, syrup }: RecipelistMaterial['values'] =
$props();
</script>

View file

@ -0,0 +1,95 @@
<script lang="ts">
import { recipeFromMachineQuery } from '$lib/core/stores/recipeStore';
import { onMount } from 'svelte';
import { MediaQuery } from 'svelte/reactivity';
import { get } from 'svelte/store';
import * as Dialog from '$lib/components/ui/dialog/index';
import RecipeDetail from './recipe-details/recipe-detail.svelte';
import { MenuStatus, matchMenuStatus } from '$lib/core/types/menuStatus';
import * as adb from '$lib/core/adb/adb';
let open = $state(false);
const isDesktop = new MediaQuery('(min-width: 768px)');
let currentData: any = $state();
let hasAlreadyTestBrewing: boolean = $state(false);
let hasPendingChange: boolean = $state(false);
let currentMenuStatus: MenuStatus = $state(MenuStatus.drafted);
const {
productCode,
refPage
}: {
productCode: string;
refPage: string;
} = $props();
async function onPendingChange(newChange: { target: string; value: any }) {
console.log('detect pending change', matchMenuStatus(currentData.MenuStatus));
hasPendingChange = true;
let originalMenuStatus = matchMenuStatus(currentData.MenuStatus);
// hasAlreadyTestBrewing =
// originalMenuStatus == MenuStatus.pendingOnline || originalMenuStatus == MenuStatus.ready;
//
// if (hasAlreadyTestBrewing) {
// currentMenuStatus = MenuStatus.pendingOnline;
// }
currentData.MenuStatus = MenuStatus.pendingOnline;
if (newChange.target === 'recipeList') {
// currentData.recipes = newChange.value;
//
// TODO: build into structure, flatten fields into 1 layer, strip off `id` (row id)
console.log(newChange);
}
// await adb.push('/sdcard/coffeevending/.curr.brewing.json', JSON.stringify(currentData));
//
//
//
// send data to some channel then if command `brew`, invoke `SetCurrentRecipeToMake`
//
}
onMount(() => {
//
if (refPage === 'brew') {
// fetch from store
let recipeDevSnapshot = get(recipeFromMachineQuery) ?? {};
let recipe01Snap = recipeDevSnapshot['recipe'];
if (recipe01Snap) {
currentData = recipe01Snap[productCode] ?? {};
if (currentData.MenuStatus) {
currentMenuStatus = matchMenuStatus(currentData.MenuStatus);
}
}
} else if (refPage === 'overview') {
}
});
</script>
{#if isDesktop.current}
<Dialog.Root bind:open>
<Dialog.Trigger onselect={(e) => e.preventDefault()}>View</Dialog.Trigger>
<Dialog.Content class="sm:max-w-3/4">
<Dialog.Header>
<Dialog.Title>Edit Recipe {productCode}</Dialog.Title>
<Dialog.Description
>Make changes to selected menu here. Click "save" when done or "test" for testing with
connected machine
</Dialog.Description>
</Dialog.Header>
<!-- render more -->
<RecipeDetail recipeData={currentData} {onPendingChange} />
</Dialog.Content>
</Dialog.Root>
{:else}{/if}

View 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="alert-description"
class={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -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="alert-title"
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,44 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const alertVariants = tv({
base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
},
},
defaultVariants: {
variant: "default",
},
});
export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
</script>
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
variant?: AlertVariant;
} = $props();
</script>
<div
bind:this={ref}
data-slot="alert"
class={cn(alertVariants({ variant }), className)}
{...restProps}
role="alert"
>
{@render children?.()}
</div>

View file

@ -0,0 +1,14 @@
import Root from "./alert.svelte";
import Description from "./alert-description.svelte";
import Title from "./alert-title.svelte";
export { alertVariants, type AlertVariant } from "./alert.svelte";
export {
Root,
Description,
Title,
//
Root as Alert,
Description as AlertDescription,
Title as AlertTitle,
};

View file

@ -0,0 +1,50 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const badgeVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
variants: {
variant: {
default:
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
destructive:
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
});
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
</script>
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
href,
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAnchorAttributes> & {
variant?: BadgeVariant;
} = $props();
</script>
<svelte:element
this={href ? "a" : "span"}
bind:this={ref}
data-slot="badge"
{href}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}
</svelte:element>

View file

@ -0,0 +1,2 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";

View file

@ -0,0 +1,82 @@
<script lang="ts" module>
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
destructive:
"bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs",
outline:
"bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
disabled,
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? "link" : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)}
{type}
{disabled}
{...restProps}
>
{@render children?.()}
</button>
{/if}

View file

@ -0,0 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View 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,
};

View file

@ -0,0 +1,36 @@
<script lang="ts">
import { Checkbox as CheckboxPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check";
import MinusIcon from "@lucide/svelte/icons/minus";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
...restProps
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
</script>
<CheckboxPrimitive.Root
bind:ref
data-slot="checkbox"
class={cn(
"border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shadow-xs peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border outline-none transition-shadow focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:checked
bind:indeterminate
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<div data-slot="checkbox-indicator" class="text-current transition-none">
{#if checked}
<CheckIcon class="size-3.5" />
{:else if indeterminate}
<MinusIcon class="size-3.5" />
{/if}
</div>
{/snippet}
</CheckboxPrimitive.Root>

View file

@ -0,0 +1,6 @@
import Root from "./checkbox.svelte";
export {
Root,
//
Root as Checkbox,
};

View file

@ -0,0 +1,40 @@
<script lang="ts">
import type { Command as CommandPrimitive, Dialog as DialogPrimitive } from "bits-ui";
import type { Snippet } from "svelte";
import Command from "./command.svelte";
import * as Dialog from "$lib/components/ui/dialog/index.js";
import type { WithoutChildrenOrChild } from "$lib/utils.js";
let {
open = $bindable(false),
ref = $bindable(null),
value = $bindable(""),
title = "Command Palette",
description = "Search for a command to run",
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.RootProps> &
WithoutChildrenOrChild<CommandPrimitive.RootProps> & {
portalProps?: DialogPrimitive.PortalProps;
children: Snippet;
title?: string;
description?: string;
} = $props();
</script>
<Dialog.Root bind:open {...restProps}>
<Dialog.Header class="sr-only">
<Dialog.Title>{title}</Dialog.Title>
<Dialog.Description>{description}</Dialog.Description>
</Dialog.Header>
<Dialog.Content class="overflow-hidden p-0" {portalProps}>
<Command
class="**:data-[slot=command-input-wrapper]:h-12 [&_[data-command-group]:not([hidden])_~[data-command-group]]:pt-0 [&_[data-command-group]]:px-2 [&_[data-command-input-wrapper]_svg]:h-5 [&_[data-command-input-wrapper]_svg]:w-5 [&_[data-command-input]]:h-12 [&_[data-command-item]]:px-2 [&_[data-command-item]]:py-3 [&_[data-command-item]_svg]:h-5 [&_[data-command-item]_svg]:w-5"
{...restProps}
bind:value
bind:ref
{children}
/>
</Dialog.Content>
</Dialog.Root>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.EmptyProps = $props();
</script>
<CommandPrimitive.Empty
bind:ref
data-slot="command-empty"
class={cn("py-6 text-center text-sm", className)}
{...restProps}
/>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { Command as CommandPrimitive, useId } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
heading,
value,
...restProps
}: CommandPrimitive.GroupProps & {
heading?: string;
} = $props();
</script>
<CommandPrimitive.Group
bind:ref
data-slot="command-group"
class={cn("text-foreground overflow-hidden p-1", className)}
value={value ?? heading ?? `----${useId()}`}
{...restProps}
>
{#if heading}
<CommandPrimitive.GroupHeading
class="text-muted-foreground px-2 py-1.5 text-xs font-medium"
>
{heading}
</CommandPrimitive.GroupHeading>
{/if}
<CommandPrimitive.GroupItems {children} />
</CommandPrimitive.Group>

View file

@ -0,0 +1,26 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import SearchIcon from "@lucide/svelte/icons/search";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
value = $bindable(""),
...restProps
}: CommandPrimitive.InputProps = $props();
</script>
<div class="flex h-9 items-center gap-2 border-b pe-8 ps-3" data-slot="command-input-wrapper">
<SearchIcon class="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
class={cn(
"placeholder:text-muted-foreground outline-hidden flex h-10 w-full rounded-md bg-transparent py-3 text-sm disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:ref
{...restProps}
bind:value
/>
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.ItemProps = $props();
</script>
<CommandPrimitive.Item
bind:ref
data-slot="command-item"
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.LinkItemProps = $props();
</script>
<CommandPrimitive.LinkItem
bind:ref
data-slot="command-item"
class={cn(
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.ListProps = $props();
</script>
<CommandPrimitive.List
bind:ref
data-slot="command-list"
class={cn("max-h-[300px] scroll-py-1 overflow-y-auto overflow-x-hidden", className)}
{...restProps}
/>

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: CommandPrimitive.LoadingProps = $props();
</script>
<CommandPrimitive.Loading bind:ref {...restProps} />

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { Command as CommandPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CommandPrimitive.SeparatorProps = $props();
</script>
<CommandPrimitive.Separator
bind:ref
data-slot="command-separator"
class={cn("bg-border -mx-1 h-px", className)}
{...restProps}
/>

View file

@ -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<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="command-shortcut"
class={cn("text-muted-foreground ms-auto text-xs tracking-widest", className)}
{...restProps}
>
{@render children?.()}
</span>

View file

@ -0,0 +1,28 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { Command as CommandPrimitive } from "bits-ui";
export type CommandRootApi = CommandPrimitive.Root;
let {
api = $bindable(null),
ref = $bindable(null),
value = $bindable(""),
class: className,
...restProps
}: CommandPrimitive.RootProps & {
api?: CommandRootApi | null;
} = $props();
</script>
<CommandPrimitive.Root
bind:this={api}
bind:value
bind:ref
data-slot="command"
class={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,37 @@
import Root from "./command.svelte";
import Loading from "./command-loading.svelte";
import Dialog from "./command-dialog.svelte";
import Empty from "./command-empty.svelte";
import Group from "./command-group.svelte";
import Item from "./command-item.svelte";
import Input from "./command-input.svelte";
import List from "./command-list.svelte";
import Separator from "./command-separator.svelte";
import Shortcut from "./command-shortcut.svelte";
import LinkItem from "./command-link-item.svelte";
export {
Root,
Dialog,
Empty,
Group,
Item,
LinkItem,
Input,
List,
Separator,
Shortcut,
Loading,
//
Root as Command,
Dialog as CommandDialog,
Empty as CommandEmpty,
Group as CommandGroup,
Item as CommandItem,
LinkItem as CommandLinkItem,
Input as CommandInput,
List as CommandList,
Separator as CommandSeparator,
Shortcut as CommandShortcut,
Loading as CommandLoading,
};

View file

@ -0,0 +1,142 @@
import {
type RowData,
type TableOptions,
type TableOptionsResolved,
type TableState,
createTable,
} from "@tanstack/table-core";
/**
* Creates a reactive TanStack table object for Svelte.
* @param options Table options to create the table with.
* @returns A reactive table object.
* @example
* ```svelte
* <script>
* const table = createSvelteTable({ ... })
* </script>
*
* <table>
* <thead>
* {#each table.getHeaderGroups() as headerGroup}
* <tr>
* {#each headerGroup.headers as header}
* <th colspan={header.colSpan}>
* <FlexRender content={header.column.columnDef.header} context={header.getContext()} />
* </th>
* {/each}
* </tr>
* {/each}
* </thead>
* <!-- ... -->
* </table>
* ```
*/
export function createSvelteTable<TData extends RowData>(options: TableOptions<TData>) {
const resolvedOptions: TableOptionsResolved<TData> = mergeObjects(
{
state: {},
onStateChange() {},
renderFallbackValue: null,
mergeOptions: (
defaultOptions: TableOptions<TData>,
options: Partial<TableOptions<TData>>
) => {
return mergeObjects(defaultOptions, options);
},
},
options
);
const table = createTable(resolvedOptions);
let state = $state<Partial<TableState>>(table.initialState);
function updateOptions() {
table.setOptions((prev) => {
return mergeObjects(prev, options, {
state: mergeObjects(state, options.state || {}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onStateChange: (updater: any) => {
if (updater instanceof Function) state = updater(state);
else state = mergeObjects(state, updater);
options.onStateChange?.(updater);
},
});
});
}
updateOptions();
$effect.pre(() => {
updateOptions();
});
return table;
}
type MaybeThunk<T extends object> = T | (() => T | null | undefined);
type Intersection<T extends readonly unknown[]> = (T extends [infer H, ...infer R]
? H & Intersection<R>
: unknown) & {};
/**
* Lazily merges several objects (or thunks) while preserving
* getter semantics from every source.
*
* Proxy-based to avoid known WebKit recursion issue.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function mergeObjects<Sources extends readonly MaybeThunk<any>[]>(
...sources: Sources
): Intersection<{ [K in keyof Sources]: Sources[K] }> {
const resolve = <T extends object>(src: MaybeThunk<T>): T | undefined =>
typeof src === "function" ? (src() ?? undefined) : src;
const findSourceWithKey = (key: PropertyKey) => {
for (let i = sources.length - 1; i >= 0; i--) {
const obj = resolve(sources[i]);
if (obj && key in obj) return obj;
}
return undefined;
};
return new Proxy(Object.create(null), {
get(_, key) {
const src = findSourceWithKey(key);
return src?.[key as never];
},
has(_, key) {
return !!findSourceWithKey(key);
},
ownKeys(): (string | symbol)[] {
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const all = new Set<string | symbol>();
for (const s of sources) {
const obj = resolve(s);
if (obj) {
for (const k of Reflect.ownKeys(obj) as (string | symbol)[]) {
all.add(k);
}
}
}
return [...all];
},
getOwnPropertyDescriptor(_, key) {
const src = findSourceWithKey(key);
if (!src) return undefined;
return {
configurable: true,
enumerable: true,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: (src as any)[key],
writable: true,
};
},
}) as Intersection<{ [K in keyof Sources]: Sources[K] }>;
}

View file

@ -0,0 +1,40 @@
<script
lang="ts"
generics="TData, TValue, TContext extends HeaderContext<TData, TValue> | CellContext<TData, TValue>"
>
import type { CellContext, ColumnDefTemplate, HeaderContext } from "@tanstack/table-core";
import { RenderComponentConfig, RenderSnippetConfig } from "./render-helpers.js";
import type { Attachment } from "svelte/attachments";
type Props = {
/** The cell or header field of the current cell's column definition. */
content?: TContext extends HeaderContext<TData, TValue>
? ColumnDefTemplate<HeaderContext<TData, TValue>>
: TContext extends CellContext<TData, TValue>
? ColumnDefTemplate<CellContext<TData, TValue>>
: never;
/** The result of the `getContext()` function of the header or cell */
context: TContext;
/** Used to pass attachments that can't be gotten through context */
attach?: Attachment;
};
let { content, context, attach }: Props = $props();
</script>
{#if typeof content === "string"}
{content}
{:else if content instanceof Function}
<!-- It's unlikely that a CellContext will be passed to a Header -->
<!-- eslint-disable-next-line @typescript-eslint/no-explicit-any -->
{@const result = content(context as any)}
{#if result instanceof RenderComponentConfig}
{@const { component: Component, props } = result}
<Component {...props} {attach} />
{:else if result instanceof RenderSnippetConfig}
{@const { snippet, params } = result}
{@render snippet({ ...params, attach })}
{:else}
{result}
{/if}
{/if}

View file

@ -0,0 +1,3 @@
export { default as FlexRender } from "./flex-render.svelte";
export { renderComponent, renderSnippet } from "./render-helpers.js";
export { createSvelteTable } from "./data-table.svelte.js";

View file

@ -0,0 +1,111 @@
import type { Component, ComponentProps, Snippet } from "svelte";
/**
* A helper class to make it easy to identify Svelte components in
* `columnDef.cell` and `columnDef.header` properties.
*
* > NOTE: This class should only be used internally by the adapter. If you're
* reading this and you don't know what this is for, you probably don't need it.
*
* @example
* ```svelte
* {@const result = content(context as any)}
* {#if result instanceof RenderComponentConfig}
* {@const { component: Component, props } = result}
* <Component {...props} />
* {/if}
* ```
*/
export class RenderComponentConfig<TComponent extends Component> {
component: TComponent;
props: ComponentProps<TComponent> | Record<string, never>;
constructor(
component: TComponent,
props: ComponentProps<TComponent> | Record<string, never> = {}
) {
this.component = component;
this.props = props;
}
}
/**
* A helper class to make it easy to identify Svelte Snippets in `columnDef.cell` and `columnDef.header` properties.
*
* > NOTE: This class should only be used internally by the adapter. If you're
* reading this and you don't know what this is for, you probably don't need it.
*
* @example
* ```svelte
* {@const result = content(context as any)}
* {#if result instanceof RenderSnippetConfig}
* {@const { snippet, params } = result}
* {@render snippet(params)}
* {/if}
* ```
*/
export class RenderSnippetConfig<TProps> {
snippet: Snippet<[TProps]>;
params: TProps;
constructor(snippet: Snippet<[TProps]>, params: TProps) {
this.snippet = snippet;
this.params = params;
}
}
/**
* A helper function to help create cells from Svelte components through ColumnDef's `cell` and `header` properties.
*
* This is only to be used with Svelte Components - use `renderSnippet` for Svelte Snippets.
*
* @param component A Svelte component
* @param props The props to pass to `component`
* @returns A `RenderComponentConfig` object that helps svelte-table know how to render the header/cell component.
* @example
* ```ts
* // +page.svelte
* const defaultColumns = [
* columnHelper.accessor('name', {
* header: header => renderComponent(SortHeader, { label: 'Name', header }),
* }),
* columnHelper.accessor('state', {
* header: header => renderComponent(SortHeader, { label: 'State', header }),
* }),
* ]
* ```
* @see {@link https://tanstack.com/table/latest/docs/guide/column-defs}
*/
export function renderComponent<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends Component<any>,
Props extends ComponentProps<T>,
>(component: T, props: Props = {} as Props) {
return new RenderComponentConfig(component, props);
}
/**
* A helper function to help create cells from Svelte Snippets through ColumnDef's `cell` and `header` properties.
*
* The snippet must only take one parameter.
*
* This is only to be used with Snippets - use `renderComponent` for Svelte Components.
*
* @param snippet
* @param params
* @returns - A `RenderSnippetConfig` object that helps svelte-table know how to render the header/cell snippet.
* @example
* ```ts
* // +page.svelte
* const defaultColumns = [
* columnHelper.accessor('name', {
* cell: cell => renderSnippet(nameSnippet, { name: cell.row.name }),
* }),
* columnHelper.accessor('state', {
* cell: cell => renderSnippet(stateSnippet, { state: cell.row.state }),
* }),
* ]
* ```
* @see {@link https://tanstack.com/table/latest/docs/guide/column-defs}
*/
export function renderSnippet<TProps>(snippet: Snippet<[TProps]>, params: TProps = {} as TProps) {
return new RenderSnippetConfig(snippet, params);
}

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DialogPrimitive.CloseProps = $props();
</script>
<DialogPrimitive.Close bind:ref data-slot="dialog-close" {...restProps} />

View file

@ -0,0 +1,45 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import DialogPortal from "./dialog-portal.svelte";
import XIcon from "@lucide/svelte/icons/x";
import type { Snippet } from "svelte";
import * as Dialog from "./index.js";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
portalProps,
children,
showCloseButton = true,
...restProps
}: WithoutChildrenOrChild<DialogPrimitive.ContentProps> & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DialogPortal>>;
children: Snippet;
showCloseButton?: boolean;
} = $props();
</script>
<DialogPortal {...portalProps}>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref
data-slot="dialog-content"
class={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed start-[50%] top-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...restProps}
>
{@render children?.()}
{#if showCloseButton}
<DialogPrimitive.Close
class="ring-offset-background focus:ring-ring rounded-xs focus:outline-hidden absolute end-4 top-4 opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
>
<XIcon />
<span class="sr-only">Close</span>
</DialogPrimitive.Close>
{/if}
</DialogPrimitive.Content>
</DialogPortal>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.DescriptionProps = $props();
</script>
<DialogPrimitive.Description
bind:ref
data-slot="dialog-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>

View file

@ -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="dialog-footer"
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -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="dialog-header"
class={cn("flex flex-col gap-2 text-center sm:text-start", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.OverlayProps = $props();
</script>
<DialogPrimitive.Overlay
bind:ref
data-slot="dialog-overlay"
class={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
let { ...restProps }: DialogPrimitive.PortalProps = $props();
</script>
<DialogPrimitive.Portal {...restProps} />

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DialogPrimitive.TitleProps = $props();
</script>
<DialogPrimitive.Title
bind:ref
data-slot="dialog-title"
class={cn("text-lg font-semibold leading-none", className)}
{...restProps}
/>

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DialogPrimitive.TriggerProps = $props();
</script>
<DialogPrimitive.Trigger bind:ref data-slot="dialog-trigger" {...restProps} />

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { Dialog as DialogPrimitive } from "bits-ui";
let { open = $bindable(false), ...restProps }: DialogPrimitive.RootProps = $props();
</script>
<DialogPrimitive.Root bind:open {...restProps} />

View file

@ -0,0 +1,34 @@
import Root from "./dialog.svelte";
import Portal from "./dialog-portal.svelte";
import Title from "./dialog-title.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
import Trigger from "./dialog-trigger.svelte";
import Close from "./dialog-close.svelte";
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose,
};

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
value = $bindable([]),
...restProps
}: DropdownMenuPrimitive.CheckboxGroupProps = $props();
</script>
<DropdownMenuPrimitive.CheckboxGroup
bind:ref
bind:value
data-slot="dropdown-menu-checkbox-group"
{...restProps}
/>

View file

@ -0,0 +1,43 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CheckIcon from "@lucide/svelte/icons/check";
import MinusIcon from "@lucide/svelte/icons/minus";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { Snippet } from "svelte";
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
children: childrenProp,
...restProps
}: WithoutChildrenOrChild<DropdownMenuPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
</script>
<DropdownMenuPrimitive.CheckboxItem
bind:ref
bind:checked
bind:indeterminate
data-slot="dropdown-menu-checkbox-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 ps-8 pe-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<span
class="pointer-events-none absolute start-2 flex size-3.5 items-center justify-center"
>
{#if indeterminate}
<MinusIcon class="size-4" />
{:else}
<CheckIcon class={cn("size-4", !checked && "text-transparent")} />
{/if}
</span>
{@render childrenProp?.()}
{/snippet}
</DropdownMenuPrimitive.CheckboxItem>

View file

@ -0,0 +1,29 @@
<script lang="ts">
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import DropdownMenuPortal from "./dropdown-menu-portal.svelte";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
sideOffset = 4,
portalProps,
class: className,
...restProps
}: DropdownMenuPrimitive.ContentProps & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DropdownMenuPortal>>;
} = $props();
</script>
<DropdownMenuPortal {...portalProps}>
<DropdownMenuPrimitive.Content
bind:ref
data-slot="dropdown-menu-content"
{sideOffset}
class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--bits-dropdown-menu-content-available-height) min-w-[8rem] origin-(--bits-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md outline-none",
className
)}
{...restProps}
/>
</DropdownMenuPortal>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
inset,
...restProps
}: ComponentProps<typeof DropdownMenuPrimitive.GroupHeading> & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.GroupHeading
bind:ref
data-slot="dropdown-menu-group-heading"
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8", className)}
{...restProps}
/>

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.GroupProps = $props();
</script>
<DropdownMenuPrimitive.Group bind:ref data-slot="dropdown-menu-group" {...restProps} />

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
inset,
variant = "default",
...restProps
}: DropdownMenuPrimitive.ItemProps & {
inset?: boolean;
variant?: "default" | "destructive";
} = $props();
</script>
<DropdownMenuPrimitive.Item
bind:ref
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
class={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,24 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
inset?: boolean;
} = $props();
</script>
<div
bind:this={ref}
data-slot="dropdown-menu-label"
data-inset={inset}
class={cn("px-2 py-1.5 text-sm font-semibold data-[inset]:ps-8", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props();
</script>
<DropdownMenuPrimitive.Portal {...restProps} />

View file

@ -0,0 +1,16 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let {
ref = $bindable(null),
value = $bindable(),
...restProps
}: DropdownMenuPrimitive.RadioGroupProps = $props();
</script>
<DropdownMenuPrimitive.RadioGroup
bind:ref
bind:value
data-slot="dropdown-menu-radio-group"
{...restProps}
/>

View file

@ -0,0 +1,33 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import CircleIcon from "@lucide/svelte/icons/circle";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children: childrenProp,
...restProps
}: WithoutChild<DropdownMenuPrimitive.RadioItemProps> = $props();
</script>
<DropdownMenuPrimitive.RadioItem
bind:ref
data-slot="dropdown-menu-radio-item"
class={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 ps-8 pe-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<span
class="pointer-events-none absolute start-2 flex size-3.5 items-center justify-center"
>
{#if checked}
<CircleIcon class="size-2 fill-current" />
{/if}
</span>
{@render childrenProp?.({ checked })}
{/snippet}
</DropdownMenuPrimitive.RadioItem>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SeparatorProps = $props();
</script>
<DropdownMenuPrimitive.Separator
bind:ref
data-slot="dropdown-menu-separator"
class={cn("bg-border -mx-1 my-1 h-px", className)}
{...restProps}
/>

View file

@ -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<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="dropdown-menu-shortcut"
class={cn("text-muted-foreground ms-auto text-xs tracking-widest", className)}
{...restProps}
>
{@render children?.()}
</span>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: DropdownMenuPrimitive.SubContentProps = $props();
</script>
<DropdownMenuPrimitive.SubContent
bind:ref
data-slot="dropdown-menu-sub-content"
class={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--bits-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,29 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
inset,
children,
...restProps
}: DropdownMenuPrimitive.SubTriggerProps & {
inset?: boolean;
} = $props();
</script>
<DropdownMenuPrimitive.SubTrigger
bind:ref
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
class={cn(
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...restProps}
>
{@render children?.()}
<ChevronRightIcon class="ms-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.SubProps = $props();
</script>
<DropdownMenuPrimitive.Sub bind:open {...restProps} />

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: DropdownMenuPrimitive.TriggerProps = $props();
</script>
<DropdownMenuPrimitive.Trigger bind:ref data-slot="dropdown-menu-trigger" {...restProps} />

View file

@ -0,0 +1,7 @@
<script lang="ts">
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
let { open = $bindable(false), ...restProps }: DropdownMenuPrimitive.RootProps = $props();
</script>
<DropdownMenuPrimitive.Root bind:open {...restProps} />

View file

@ -0,0 +1,54 @@
import Root from "./dropdown-menu.svelte";
import Sub from "./dropdown-menu-sub.svelte";
import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
import Content from "./dropdown-menu-content.svelte";
import Group from "./dropdown-menu-group.svelte";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import Trigger from "./dropdown-menu-trigger.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
import GroupHeading from "./dropdown-menu-group-heading.svelte";
import Portal from "./dropdown-menu-portal.svelte";
export {
CheckboxGroup,
CheckboxItem,
Content,
Portal,
Root as DropdownMenu,
CheckboxGroup as DropdownMenuCheckboxGroup,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
Portal as DropdownMenuPortal,
Group as DropdownMenuGroup,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
RadioGroup as DropdownMenuRadioGroup,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
Shortcut as DropdownMenuShortcut,
Sub as DropdownMenuSub,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
Trigger as DropdownMenuTrigger,
GroupHeading as DropdownMenuGroupHeading,
Group,
GroupHeading,
Item,
Label,
RadioGroup,
RadioItem,
Root,
Separator,
Shortcut,
Sub,
SubContent,
SubTrigger,
Trigger,
};

View file

@ -0,0 +1,7 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
};

View file

@ -0,0 +1,52 @@
<script lang="ts">
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
type Props = WithElementRef<
Omit<HTMLInputAttributes, "type"> &
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
>;
let {
ref = $bindable(null),
value = $bindable(),
type,
files = $bindable(),
class: className,
"data-slot": dataSlot = "input",
...restProps
}: Props = $props();
</script>
{#if type === "file"}
<input
bind:this={ref}
data-slot={dataSlot}
class={cn(
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
type="file"
bind:files
bind:value
{...restProps}
/>
{:else}
<input
bind:this={ref}
data-slot={dataSlot}
class={cn(
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{type}
bind:value
{...restProps}
/>
{/if}

View file

@ -0,0 +1,7 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: LabelPrimitive.RootProps = $props();
</script>
<LabelPrimitive.Root
bind:ref
data-slot="label"
class={cn(
"flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,28 @@
import Root from "./navigation-menu.svelte";
import Content from "./navigation-menu-content.svelte";
import Indicator from "./navigation-menu-indicator.svelte";
import Item from "./navigation-menu-item.svelte";
import Link from "./navigation-menu-link.svelte";
import List from "./navigation-menu-list.svelte";
import Trigger from "./navigation-menu-trigger.svelte";
import Viewport from "./navigation-menu-viewport.svelte";
export {
Root,
Content,
Indicator,
Item,
Link,
List,
Trigger,
Viewport,
//
Root as NavigationMenuRoot,
Content as NavigationMenuContent,
Indicator as NavigationMenuIndicator,
Item as NavigationMenuItem,
Link as NavigationMenuLink,
List as NavigationMenuList,
Trigger as NavigationMenuTrigger,
Viewport as NavigationMenuViewport,
};

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.ContentProps = $props();
</script>
<NavigationMenuPrimitive.Content
bind:ref
data-slot="navigation-menu-content"
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",
"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",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.IndicatorProps = $props();
</script>
<NavigationMenuPrimitive.Indicator
bind:ref
data-slot="navigation-menu-indicator"
class={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className
)}
{...restProps}
>
<div class="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md"></div>
</NavigationMenuPrimitive.Indicator>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.ItemProps = $props();
</script>
<NavigationMenuPrimitive.Item
bind:ref
data-slot="navigation-menu-item"
class={cn("relative", className)}
{...restProps}
/>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.LinkProps = $props();
</script>
<NavigationMenuPrimitive.Link
bind:ref
data-slot="navigation-menu-link"
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",
className
)}
{...restProps}
/>

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.ListProps = $props();
</script>
<NavigationMenuPrimitive.List
bind:ref
data-slot="navigation-menu-list"
class={cn("group flex flex-1 list-none items-center justify-center gap-1", className)}
{...restProps}
/>

View file

@ -0,0 +1,34 @@
<script lang="ts" module>
import { cn } from "$lib/utils.js";
import { tv } from "tailwind-variants";
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",
});
</script>
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: NavigationMenuPrimitive.TriggerProps = $props();
</script>
<NavigationMenuPrimitive.Trigger
bind:ref
data-slot="navigation-menu-trigger"
class={cn(navigationMenuTriggerStyle(), "group", className)}
{...restProps}
>
{@render children?.()}
<ChevronDownIcon
class="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>

View file

@ -0,0 +1,22 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: NavigationMenuPrimitive.ViewportProps = $props();
</script>
<div class={cn("absolute left-0 top-full isolate z-50 flex justify-center")}>
<NavigationMenuPrimitive.Viewport
bind:ref
data-slot="navigation-menu-viewport"
class={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--bits-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--bits-navigation-menu-viewport-width)]",
className
)}
{...restProps}
/>
</div>

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { NavigationMenu as NavigationMenuPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
import NavigationMenuViewport from "./navigation-menu-viewport.svelte";
let {
ref = $bindable(null),
class: className,
viewport = true,
children,
...restProps
}: NavigationMenuPrimitive.RootProps & {
viewport?: boolean;
} = $props();
</script>
<NavigationMenuPrimitive.Root
bind:ref
data-slot="navigation-menu"
data-viewport={viewport}
class={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className
)}
{...restProps}
>
{@render children?.()}
{#if viewport}
<NavigationMenuViewport />
{/if}
</NavigationMenuPrimitive.Root>

View file

@ -0,0 +1,31 @@
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";
import Previous from "./pagination-previous.svelte";
import Next from "./pagination-next.svelte";
export {
Root,
Content,
Item,
Link,
PrevButton, //old
NextButton, //old
Ellipsis,
Previous,
Next,
//
Root as Pagination,
Content as PaginationContent,
Item as PaginationItem,
Link as PaginationLink,
PrevButton as PaginationPrevButton, //old
NextButton as PaginationNextButton, //old
Ellipsis as PaginationEllipsis,
Previous as PaginationPrevious,
Next as PaginationNext,
};

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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}
/>

View file

@ -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:pe-2.5",
}),
className
)}
children={children || Fallback}
{...restProps}
/>

View file

@ -0,0 +1,29 @@
<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,
...restProps
}: PaginationPrimitive.NextButtonProps = $props();
</script>
<PaginationPrimitive.NextButton
bind:ref
aria-label="Go to next page"
class={cn(
buttonVariants({
size: "default",
variant: "ghost",
class: "gap-1 px-2.5 sm:pe-2.5",
}),
className
)}
{...restProps}
>
<span class="hidden sm:block">Next</span>
<ChevronRightIcon /></PaginationPrimitive.NextButton
>

View file

@ -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:ps-2.5",
}),
className
)}
children={children || Fallback}
{...restProps}
/>

View file

@ -0,0 +1,29 @@
<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,
...restProps
}: PaginationPrimitive.PrevButtonProps = $props();
</script>
<PaginationPrimitive.PrevButton
bind:ref
aria-label="Go to previous page"
class={cn(
buttonVariants({
size: "default",
variant: "ghost",
class: "gap-1 px-2.5 sm:ps-2.5",
}),
className
)}
{...restProps}
>
<ChevronLeftIcon />
<span class="hidden sm:block">Previous</span></PaginationPrimitive.PrevButton
>

View file

@ -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}
/>

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