init
This commit is contained in:
commit
451223816b
338 changed files with 9938 additions and 0 deletions
109
src/lib/components/app-account-select.svelte
Normal file
109
src/lib/components/app-account-select.svelte
Normal 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>
|
||||
130
src/lib/components/app-sidebar.svelte
Normal file
130
src/lib/components/app-sidebar.svelte
Normal 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>
|
||||
8
src/lib/components/content-form.svelte
Normal file
8
src/lib/components/content-form.svelte
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<script lang="ts">
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
424
src/lib/components/dashboard-quick-adb.svelte
Normal file
424
src/lib/components/dashboard-quick-adb.svelte
Normal 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>
|
||||
81
src/lib/components/dashboard.svelte
Normal file
81
src/lib/components/dashboard.svelte
Normal 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>
|
||||
11
src/lib/components/error-layout.svelte
Normal file
11
src/lib/components/error-layout.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import {page} from '$app/state';
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<slot/>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
Status: {page.status}
|
||||
</footer>
|
||||
63
src/lib/components/machine-info.svelte
Normal file
63
src/lib/components/machine-info.svelte
Normal 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>
|
||||
114
src/lib/components/recipe-details/columns.ts
Normal file
114
src/lib/components/recipe-details/columns.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
145
src/lib/components/recipe-details/recipe-detail.svelte
Normal file
145
src/lib/components/recipe-details/recipe-detail.svelte
Normal 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>
|
||||
14
src/lib/components/recipe-details/recipelist-isuse.svelte
Normal file
14
src/lib/components/recipe-details/recipelist-isuse.svelte
Normal 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>
|
||||
|
|
@ -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>
|
||||
196
src/lib/components/recipe-details/recipelist-table.svelte
Normal file
196
src/lib/components/recipe-details/recipelist-table.svelte
Normal 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}
|
||||
|
|
@ -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>
|
||||
95
src/lib/components/recipe-editor-dialog.svelte
Normal file
95
src/lib/components/recipe-editor-dialog.svelte
Normal 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}
|
||||
23
src/lib/components/ui/alert/alert-description.svelte
Normal file
23
src/lib/components/ui/alert/alert-description.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="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>
|
||||
20
src/lib/components/ui/alert/alert-title.svelte
Normal file
20
src/lib/components/ui/alert/alert-title.svelte
Normal 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>
|
||||
44
src/lib/components/ui/alert/alert.svelte
Normal file
44
src/lib/components/ui/alert/alert.svelte
Normal 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>
|
||||
14
src/lib/components/ui/alert/index.ts
Normal file
14
src/lib/components/ui/alert/index.ts
Normal 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,
|
||||
};
|
||||
50
src/lib/components/ui/badge/badge.svelte
Normal file
50
src/lib/components/ui/badge/badge.svelte
Normal 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>
|
||||
2
src/lib/components/ui/badge/index.ts
Normal file
2
src/lib/components/ui/badge/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as Badge } from "./badge.svelte";
|
||||
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
|
||||
82
src/lib/components/ui/button/button.svelte
Normal file
82
src/lib/components/ui/button/button.svelte
Normal 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}
|
||||
17
src/lib/components/ui/button/index.ts
Normal file
17
src/lib/components/ui/button/index.ts
Normal 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,
|
||||
};
|
||||
20
src/lib/components/ui/card/card-action.svelte
Normal file
20
src/lib/components/ui/card/card-action.svelte
Normal 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>
|
||||
15
src/lib/components/ui/card/card-content.svelte
Normal file
15
src/lib/components/ui/card/card-content.svelte
Normal 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>
|
||||
20
src/lib/components/ui/card/card-description.svelte
Normal file
20
src/lib/components/ui/card/card-description.svelte
Normal 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>
|
||||
20
src/lib/components/ui/card/card-footer.svelte
Normal file
20
src/lib/components/ui/card/card-footer.svelte
Normal 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>
|
||||
23
src/lib/components/ui/card/card-header.svelte
Normal file
23
src/lib/components/ui/card/card-header.svelte
Normal 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>
|
||||
20
src/lib/components/ui/card/card-title.svelte
Normal file
20
src/lib/components/ui/card/card-title.svelte
Normal 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>
|
||||
23
src/lib/components/ui/card/card.svelte
Normal file
23
src/lib/components/ui/card/card.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="card"
|
||||
class={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
25
src/lib/components/ui/card/index.ts
Normal file
25
src/lib/components/ui/card/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import Root from "./card.svelte";
|
||||
import Content from "./card-content.svelte";
|
||||
import Description from "./card-description.svelte";
|
||||
import Footer from "./card-footer.svelte";
|
||||
import Header from "./card-header.svelte";
|
||||
import Title from "./card-title.svelte";
|
||||
import Action from "./card-action.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Footer,
|
||||
Header,
|
||||
Title,
|
||||
Action,
|
||||
//
|
||||
Root as Card,
|
||||
Content as CardContent,
|
||||
Description as CardDescription,
|
||||
Footer as CardFooter,
|
||||
Header as CardHeader,
|
||||
Title as CardTitle,
|
||||
Action as CardAction,
|
||||
};
|
||||
36
src/lib/components/ui/checkbox/checkbox.svelte
Normal file
36
src/lib/components/ui/checkbox/checkbox.svelte
Normal 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>
|
||||
6
src/lib/components/ui/checkbox/index.ts
Normal file
6
src/lib/components/ui/checkbox/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import Root from "./checkbox.svelte";
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Checkbox,
|
||||
};
|
||||
40
src/lib/components/ui/command/command-dialog.svelte
Normal file
40
src/lib/components/ui/command/command-dialog.svelte
Normal 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>
|
||||
17
src/lib/components/ui/command/command-empty.svelte
Normal file
17
src/lib/components/ui/command/command-empty.svelte
Normal 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}
|
||||
/>
|
||||
32
src/lib/components/ui/command/command-group.svelte
Normal file
32
src/lib/components/ui/command/command-group.svelte
Normal 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>
|
||||
26
src/lib/components/ui/command/command-input.svelte
Normal file
26
src/lib/components/ui/command/command-input.svelte
Normal 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>
|
||||
20
src/lib/components/ui/command/command-item.svelte
Normal file
20
src/lib/components/ui/command/command-item.svelte
Normal 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}
|
||||
/>
|
||||
20
src/lib/components/ui/command/command-link-item.svelte
Normal file
20
src/lib/components/ui/command/command-link-item.svelte
Normal 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}
|
||||
/>
|
||||
17
src/lib/components/ui/command/command-list.svelte
Normal file
17
src/lib/components/ui/command/command-list.svelte
Normal 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}
|
||||
/>
|
||||
7
src/lib/components/ui/command/command-loading.svelte
Normal file
7
src/lib/components/ui/command/command-loading.svelte
Normal 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} />
|
||||
17
src/lib/components/ui/command/command-separator.svelte
Normal file
17
src/lib/components/ui/command/command-separator.svelte
Normal 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}
|
||||
/>
|
||||
20
src/lib/components/ui/command/command-shortcut.svelte
Normal file
20
src/lib/components/ui/command/command-shortcut.svelte
Normal 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>
|
||||
28
src/lib/components/ui/command/command.svelte
Normal file
28
src/lib/components/ui/command/command.svelte
Normal 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}
|
||||
/>
|
||||
37
src/lib/components/ui/command/index.ts
Normal file
37
src/lib/components/ui/command/index.ts
Normal 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,
|
||||
};
|
||||
142
src/lib/components/ui/data-table/data-table.svelte.ts
Normal file
142
src/lib/components/ui/data-table/data-table.svelte.ts
Normal 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] }>;
|
||||
}
|
||||
40
src/lib/components/ui/data-table/flex-render.svelte
Normal file
40
src/lib/components/ui/data-table/flex-render.svelte
Normal 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}
|
||||
3
src/lib/components/ui/data-table/index.ts
Normal file
3
src/lib/components/ui/data-table/index.ts
Normal 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";
|
||||
111
src/lib/components/ui/data-table/render-helpers.ts
Normal file
111
src/lib/components/ui/data-table/render-helpers.ts
Normal 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);
|
||||
}
|
||||
7
src/lib/components/ui/dialog/dialog-close.svelte
Normal file
7
src/lib/components/ui/dialog/dialog-close.svelte
Normal 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} />
|
||||
45
src/lib/components/ui/dialog/dialog-content.svelte
Normal file
45
src/lib/components/ui/dialog/dialog-content.svelte
Normal 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>
|
||||
17
src/lib/components/ui/dialog/dialog-description.svelte
Normal file
17
src/lib/components/ui/dialog/dialog-description.svelte
Normal 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}
|
||||
/>
|
||||
20
src/lib/components/ui/dialog/dialog-footer.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-footer.svelte
Normal 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>
|
||||
20
src/lib/components/ui/dialog/dialog-header.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-header.svelte
Normal 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>
|
||||
20
src/lib/components/ui/dialog/dialog-overlay.svelte
Normal file
20
src/lib/components/ui/dialog/dialog-overlay.svelte
Normal 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}
|
||||
/>
|
||||
7
src/lib/components/ui/dialog/dialog-portal.svelte
Normal file
7
src/lib/components/ui/dialog/dialog-portal.svelte
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: DialogPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<DialogPrimitive.Portal {...restProps} />
|
||||
17
src/lib/components/ui/dialog/dialog-title.svelte
Normal file
17
src/lib/components/ui/dialog/dialog-title.svelte
Normal 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}
|
||||
/>
|
||||
7
src/lib/components/ui/dialog/dialog-trigger.svelte
Normal file
7
src/lib/components/ui/dialog/dialog-trigger.svelte
Normal 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} />
|
||||
7
src/lib/components/ui/dialog/dialog.svelte
Normal file
7
src/lib/components/ui/dialog/dialog.svelte
Normal 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} />
|
||||
34
src/lib/components/ui/dialog/index.ts
Normal file
34
src/lib/components/ui/dialog/index.ts
Normal 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,
|
||||
};
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
@ -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} />
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: DropdownMenuPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<DropdownMenuPrimitive.Portal {...restProps} />
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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} />
|
||||
|
|
@ -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} />
|
||||
7
src/lib/components/ui/dropdown-menu/dropdown-menu.svelte
Normal file
7
src/lib/components/ui/dropdown-menu/dropdown-menu.svelte
Normal 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} />
|
||||
54
src/lib/components/ui/dropdown-menu/index.ts
Normal file
54
src/lib/components/ui/dropdown-menu/index.ts
Normal 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,
|
||||
};
|
||||
7
src/lib/components/ui/input/index.ts
Normal file
7
src/lib/components/ui/input/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import Root from "./input.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
52
src/lib/components/ui/input/input.svelte
Normal file
52
src/lib/components/ui/input/input.svelte
Normal 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}
|
||||
7
src/lib/components/ui/label/index.ts
Normal file
7
src/lib/components/ui/label/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
||||
20
src/lib/components/ui/label/label.svelte
Normal file
20
src/lib/components/ui/label/label.svelte
Normal 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}
|
||||
/>
|
||||
28
src/lib/components/ui/navigation-menu/index.ts
Normal file
28
src/lib/components/ui/navigation-menu/index.ts
Normal 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,
|
||||
};
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
32
src/lib/components/ui/navigation-menu/navigation-menu.svelte
Normal file
32
src/lib/components/ui/navigation-menu/navigation-menu.svelte
Normal 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>
|
||||
31
src/lib/components/ui/pagination/index.ts
Normal file
31
src/lib/components/ui/pagination/index.ts
Normal 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,
|
||||
};
|
||||
20
src/lib/components/ui/pagination/pagination-content.svelte
Normal file
20
src/lib/components/ui/pagination/pagination-content.svelte
Normal 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>
|
||||
22
src/lib/components/ui/pagination/pagination-ellipsis.svelte
Normal file
22
src/lib/components/ui/pagination/pagination-ellipsis.svelte
Normal 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>
|
||||
14
src/lib/components/ui/pagination/pagination-item.svelte
Normal file
14
src/lib/components/ui/pagination/pagination-item.svelte
Normal 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>
|
||||
39
src/lib/components/ui/pagination/pagination-link.svelte
Normal file
39
src/lib/components/ui/pagination/pagination-link.svelte
Normal 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}
|
||||
/>
|
||||
|
|
@ -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}
|
||||
/>
|
||||
29
src/lib/components/ui/pagination/pagination-next.svelte
Normal file
29
src/lib/components/ui/pagination/pagination-next.svelte
Normal 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
|
||||
>
|
||||
|
|
@ -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}
|
||||
/>
|
||||
29
src/lib/components/ui/pagination/pagination-previous.svelte
Normal file
29
src/lib/components/ui/pagination/pagination-previous.svelte
Normal 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
|
||||
>
|
||||
28
src/lib/components/ui/pagination/pagination.svelte
Normal file
28
src/lib/components/ui/pagination/pagination.svelte
Normal 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
Loading…
Add table
Add a link
Reference in a new issue