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

114
src/app.css Normal file
View file

@ -0,0 +1,114 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--background: hsl(0 0% 100%) /* <- Wrap in HSL */;
--foreground: hsl(240 10% 3.9%);
--muted: hsl(240 4.8% 95.9%);
--muted-foreground: hsl(240 3.8% 46.1%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(240 10% 3.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(240 10% 3.9%);
--border: hsl(240 5.9% 90%);
--input: hsl(240 5.9% 90%);
--primary: hsl(240 5.9% 10%);
--primary-foreground: hsl(0 0% 98%);
--secondary: hsl(240 4.8% 95.9%);
--secondary-foreground: hsl(240 5.9% 10%);
--accent: hsl(240 4.8% 95.9%);
--accent-foreground: hsl(240 5.9% 10%);
--destructive: hsl(0 72.2% 50.6%);
--destructive-foreground: hsl(0 0% 98%);
--ring: hsl(240 10% 3.9%);
--sidebar: hsl(0 0% 98%);
--sidebar-foreground: hsl(240 5.3% 26.1%);
--sidebar-primary: hsl(240 5.9% 10%);
--sidebar-primary-foreground: hsl(0 0% 98%);
--sidebar-accent: hsl(240 4.8% 95.9%);
--sidebar-accent-foreground: hsl(240 5.9% 10%);
--sidebar-border: hsl(220 13% 91%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
--radius: 0.5rem;
}
.dark {
--background: hsl(240 10% 3.9%);
--foreground: hsl(0 0% 98%);
--muted: hsl(240 3.7% 15.9%);
--muted-foreground: hsl(240 5% 64.9%);
--popover: hsl(240 10% 3.9%);
--popover-foreground: hsl(0 0% 98%);
--card: hsl(240 10% 3.9%);
--card-foreground: hsl(0 0% 98%);
--border: hsl(240 3.7% 15.9%);
--input: hsl(240 3.7% 15.9%);
--primary: hsl(0 0% 98%);
--primary-foreground: hsl(240 5.9% 10%);
--secondary: hsl(240 3.7% 15.9%);
--secondary-foreground: hsl(0 0% 98%);
--accent: hsl(240 3.7% 15.9%);
--accent-foreground: hsl(0 0% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(0 0% 98%);
--ring: hsl(240 4.9% 83.9%);
--sidebar: hsl(240 5.9% 10%);
--sidebar-foreground: hsl(240 4.8% 95.9%);
--sidebar-primary: hsl(224.3 76.3% 48%);
--sidebar-primary-foreground: hsl(0 0% 100%);
--sidebar-accent: hsl(240 3.7% 15.9%);
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
--sidebar-border: hsl(240 3.7% 15.9%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
}
@theme inline {
/* Radius (for rounded-*) */
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
/* Colors */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-ring: var(--ring);
--color-radius: var(--radius);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

13
src/app.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

11
src/app.html Normal file
View file

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

7
src/demo.spec.ts Normal file
View file

@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="-0.5 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Google-color</title>
<desc>Created with Sketch.</desc>
<defs>
</defs>
<g id="Icons" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Color-" transform="translate(-401.000000, -860.000000)">
<g id="Google" transform="translate(401.000000, 860.000000)">
<path d="M9.82727273,24 C9.82727273,22.4757333 10.0804318,21.0144 10.5322727,19.6437333 L2.62345455,13.6042667 C1.08206818,16.7338667 0.213636364,20.2602667 0.213636364,24 C0.213636364,27.7365333 1.081,31.2608 2.62025,34.3882667 L10.5247955,28.3370667 C10.0772273,26.9728 9.82727273,25.5168 9.82727273,24" id="Fill-1" fill="#FBBC05">
</path>
<path d="M23.7136364,10.1333333 C27.025,10.1333333 30.0159091,11.3066667 32.3659091,13.2266667 L39.2022727,6.4 C35.0363636,2.77333333 29.6954545,0.533333333 23.7136364,0.533333333 C14.4268636,0.533333333 6.44540909,5.84426667 2.62345455,13.6042667 L10.5322727,19.6437333 C12.3545909,14.112 17.5491591,10.1333333 23.7136364,10.1333333" id="Fill-2" fill="#EB4335">
</path>
<path d="M23.7136364,37.8666667 C17.5491591,37.8666667 12.3545909,33.888 10.5322727,28.3562667 L2.62345455,34.3946667 C6.44540909,42.1557333 14.4268636,47.4666667 23.7136364,47.4666667 C29.4455,47.4666667 34.9177955,45.4314667 39.0249545,41.6181333 L31.5177727,35.8144 C29.3995682,37.1488 26.7323182,37.8666667 23.7136364,37.8666667" id="Fill-3" fill="#34A853">
</path>
<path d="M46.1454545,24 C46.1454545,22.6133333 45.9318182,21.12 45.6113636,19.7333333 L23.7136364,19.7333333 L23.7136364,28.8 L36.3181818,28.8 C35.6879545,31.8912 33.9724545,34.2677333 31.5177727,35.8144 L39.0249545,41.6181333 C43.3393409,37.6138667 46.1454545,31.6490667 46.1454545,24" id="Fill-4" fill="#4285F4">
</path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M240-40q-33 0-56.5-23.5T160-120v-440q0-33 23.5-56.5T240-640h120v80H240v440h480v-440H600v-80h120q33 0 56.5 23.5T800-560v440q0 33-23.5 56.5T720-40H240Zm200-280v-447l-64 64-56-57 160-160 160 160-56 57-64-64v447h-80Z"/></svg>

After

Width:  |  Height:  |  Size: 318 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h280v80H200Zm440-160-55-58 102-102H360v-80h327L585-622l55-58 200 200-200 200Z"/></svg>

After

Width:  |  Height:  |  Size: 258 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h168q13-36 43.5-58t68.5-22q38 0 68.5 22t43.5 58h168q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm80-80h280v-80H280v80Zm0-160h400v-80H280v80Zm0-160h400v-80H280v80Zm200-190q13 0 21.5-8.5T510-820q0-13-8.5-21.5T480-850q-13 0-21.5 8.5T450-820q0 13 8.5 21.5T480-790ZM200-200v-560 560Z"/></svg>

After

Width:  |  Height:  |  Size: 477 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M520-600v-240h320v240H520ZM120-440v-400h320v400H120Zm400 320v-400h320v400H520Zm-400 0v-240h320v240H120Zm80-400h160v-240H200v240Zm400 320h160v-240H600v240Zm0-480h160v-80H600v80ZM200-200h160v-80H200v80Zm160-320Zm240-160Zm0 240ZM360-280Z"/></svg>

After

Width:  |  Height:  |  Size: 340 B

BIN
src/lib/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

128
src/lib/assets/logo.svelte Normal file
View file

@ -0,0 +1,128 @@
<script>
export let size = 24;
export let fillColor = '#513C2F';
$: sizePx = `${size}px`;
$: fillColor = fillColor;
</script>
<svg
width={sizePx}
height={sizePx}
viewBox="0 0 120 143"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4.30555 122.027V99.6045H0V96.1101H12.3328V99.6045H8.02729V122.027H4.30555Z"
fill={fillColor}
/>
<path
d="M25.335 112.963L23.5106 103.572H23.4376L21.6132 112.963H25.335ZM16.1401 122.027L21.9416 96.1101H25.0431L30.8446 122.027H27.1229L26.0283 116.458H20.9565L19.8618 122.027H16.1401Z"
fill={fillColor}
/>
<path
d="M38.2964 102.225C38.2964 101.182 38.4788 100.26 38.8437 99.4589C39.2086 98.6581 39.6951 97.9908 40.3032 97.4569C40.887 96.9473 41.5438 96.5591 42.2735 96.2921C43.0276 96.0252 43.7817 95.8917 44.5358 95.8917C45.2899 95.8917 46.0318 96.0252 46.7615 96.2921C47.5156 96.5591 48.1967 96.9473 48.8048 97.4569C49.3886 97.9908 49.863 98.6581 50.2279 99.4589C50.5927 100.26 50.7752 101.182 50.7752 102.225V115.912C50.7752 117.004 50.5927 117.938 50.2279 118.715C49.863 119.491 49.3886 120.134 48.8048 120.644C48.1967 121.178 47.5156 121.578 46.7615 121.845C46.0318 122.112 45.2899 122.245 44.5358 122.245C43.7817 122.245 43.0276 122.112 42.2735 121.845C41.5438 121.578 40.887 121.178 40.3032 120.644C39.6951 120.134 39.2086 119.491 38.8437 118.715C38.4788 117.938 38.2964 117.004 38.2964 115.912V102.225ZM42.0181 115.912C42.0181 116.81 42.2614 117.477 42.7479 117.914C43.2587 118.326 43.8547 118.533 44.5358 118.533C45.2169 118.533 45.8007 118.326 46.2872 117.914C46.798 117.477 47.0534 116.81 47.0534 115.912V102.225C47.0534 101.327 46.798 100.672 46.2872 100.26C45.8007 99.8229 45.2169 99.6045 44.5358 99.6045C43.8547 99.6045 43.2587 99.8229 42.7479 100.26C42.2614 100.672 42.0181 101.327 42.0181 102.225V115.912Z"
fill={fillColor}
/>
<path
d="M72.4973 122.027V96.1101H77.934C79.1259 96.1101 80.1354 96.2921 80.9624 96.6561C81.8138 97.0201 82.5071 97.5055 83.0423 98.1121C83.5774 98.7188 83.9544 99.4225 84.1734 100.223C84.4166 101 84.5382 101.813 84.5382 102.662V103.609C84.5382 104.312 84.4774 104.907 84.3558 105.392C84.2585 105.877 84.1004 106.302 83.8815 106.666C83.4679 107.346 82.8355 107.928 81.9841 108.413C82.8598 108.826 83.5044 109.433 83.918 110.233C84.3315 111.034 84.5382 112.126 84.5382 113.509V114.965C84.5382 117.246 83.9788 118.994 82.8598 120.207C81.7652 121.42 80.0016 122.027 77.5691 122.027H72.4973ZM76.219 110.015V118.314H77.8245C78.5786 118.314 79.1624 118.205 79.5759 117.987C80.0138 117.768 80.3422 117.465 80.5611 117.077C80.78 116.688 80.9138 116.227 80.9624 115.693C81.0111 115.159 81.0354 114.577 81.0354 113.946C81.0354 113.291 80.9989 112.721 80.926 112.235C80.853 111.75 80.707 111.337 80.4881 110.998C80.2449 110.658 79.9165 110.415 79.5029 110.27C79.0894 110.1 78.5421 110.015 77.861 110.015H76.219ZM76.219 99.6045V106.739H77.8975C79.1381 106.739 79.9651 106.436 80.3786 105.829C80.8165 105.198 81.0354 104.288 81.0354 103.099C81.0354 101.934 80.7922 101.061 80.3057 100.478C79.8435 99.8957 78.9921 99.6045 77.7515 99.6045H76.219Z"
fill={fillColor}
/>
<path d="M93.9806 122.027V96.1101H97.7023V122.027H93.9806Z" fill={fillColor} />
<path
d="M107.01 122.027V96.1101H110.586L116.205 111.726H116.278V96.1101H120V122.027H116.497L110.805 106.448H110.732V122.027H107.01Z"
fill={fillColor}
/>
<path
d="M25.1975 142.873H29.298C30.0638 142.873 30.7027 142.622 31.2147 142.118C31.7311 141.637 31.9938 140.983 32.0027 140.155C32.0027 139.656 31.8758 139.195 31.6221 138.77C31.3505 138.359 30.9498 138.085 30.42 137.949V137.923C30.7004 137.792 30.9364 137.645 31.1279 137.483C31.3193 137.33 31.4662 137.168 31.5686 136.997C31.769 136.639 31.8647 136.262 31.8558 135.868C31.8558 135.098 31.6132 134.479 31.1279 134.01C30.647 133.546 29.928 133.31 28.9708 133.301H25.1975V142.873ZM28.944 138.626C29.4917 138.634 29.8924 138.777 30.1461 139.052C30.3999 139.332 30.5268 139.67 30.5268 140.063C30.5268 140.449 30.3999 140.781 30.1461 141.061C29.8924 141.346 29.4917 141.492 28.944 141.501H26.6734V138.626H28.944ZM28.8038 134.595C29.3425 134.603 29.7388 134.732 29.9925 134.982C30.2508 135.249 30.3799 135.575 30.3799 135.96C30.3799 136.345 30.2508 136.665 29.9925 136.919C29.7388 137.194 29.3425 137.332 28.8038 137.332H26.6734V134.595H28.8038Z"
fill={fillColor}
/>
<path
d="M34.6396 142.873H40.8571V141.501H36.1155V138.691H40.1626V137.404H36.1155V134.673H40.8571V133.301H34.6396V142.873Z"
fill={fillColor}
/>
<path
d="M45.5442 142.873H46.6929L49.9118 133.301H48.3491L46.1319 140.589H46.1052L43.8947 133.301H42.332L45.5442 142.873Z"
fill={fillColor}
/>
<path
d="M52.0545 142.873H58.272V141.501H53.5304V138.691H57.5775V137.404H53.5304V134.673H58.272V133.301H52.0545V142.873Z"
fill={fillColor}
/>
<path
d="M62.2379 134.595H64.5486C65.0205 134.595 65.3812 134.693 65.6305 134.89C65.9466 135.113 66.1091 135.474 66.118 135.973C66.118 136.389 65.9822 136.739 65.7106 137.024C65.4346 137.33 65.0205 137.488 64.4685 137.496H62.2379V134.595ZM60.762 142.873H62.2379V138.783H64.1212L66.1314 142.873H67.8878L65.6305 138.626C66.8682 138.157 67.496 137.273 67.5138 135.973C67.4871 135.089 67.1643 134.409 66.5454 133.931C66.0334 133.511 65.3723 133.301 64.562 133.301H60.762V142.873Z"
fill={fillColor}
/>
<path
d="M71.9271 139.446L73.3362 135.264H73.3629L74.772 139.446H71.9271ZM75.9474 142.873H77.5035L73.964 133.301H72.7285L69.189 142.873H70.7517L71.4997 140.733H75.1928L75.9474 142.873Z"
fill={fillColor}
/>
<path
d="M82.5512 138.974H84.5347V139.473C84.5258 140.072 84.3343 140.556 83.9603 140.923C83.5864 141.309 83.1122 141.501 82.5379 141.501C82.1906 141.501 81.8923 141.429 81.643 141.285C81.3892 141.162 81.1844 141.002 81.0286 140.805C80.8371 140.6 80.7102 140.33 80.6479 139.998C80.5767 139.665 80.541 139.028 80.541 138.087C80.541 137.146 80.5767 136.505 80.6479 136.164C80.7102 135.84 80.8371 135.575 81.0286 135.369C81.1844 135.172 81.3892 135.008 81.643 134.877C81.8923 134.75 82.1906 134.682 82.5379 134.673C83.0098 134.682 83.4105 134.816 83.74 135.074C84.0561 135.345 84.2742 135.673 84.3944 136.059H85.9572C85.8013 135.258 85.4273 134.586 84.8352 134.043C84.2431 133.505 83.4773 133.231 82.5379 133.222C81.7721 133.231 81.1332 133.411 80.6212 133.761C80.1003 134.107 79.7218 134.5 79.4859 134.943C79.3389 135.174 79.2299 135.479 79.1586 135.855C79.0918 136.231 79.0585 136.976 79.0585 138.087C79.0585 139.181 79.0918 139.921 79.1586 140.306C79.1942 140.512 79.2388 140.683 79.2922 140.818C79.3501 140.95 79.4146 141.088 79.4859 141.232C79.7218 141.674 80.1003 142.064 80.6212 142.401C81.1332 142.751 81.7721 142.935 82.5379 142.952C83.5307 142.935 84.3544 142.604 85.0088 141.961C85.6589 141.313 85.9928 140.51 86.0106 139.551V137.601H82.5512V138.974Z"
fill={fillColor}
/>
<path
d="M88.8345 142.873H95.052V141.501H90.3104V138.691H94.3574V137.404H90.3104V134.673H95.052V133.301H88.8345V142.873Z"
fill={fillColor}
/>
<path
d="M104.532 139.709C105.497 139.709 106.279 138.927 106.279 137.963C106.279 136.998 105.497 136.216 104.532 136.216C103.568 136.216 102.786 136.998 102.786 137.963C102.786 138.927 103.568 139.709 104.532 139.709Z"
fill={fillColor}
/>
<path
d="M15.7173 139.709C16.6818 139.709 17.4637 138.927 17.4637 137.963C17.4637 136.998 16.6818 136.216 15.7173 136.216C14.7528 136.216 13.9709 136.998 13.9709 137.963C13.9709 138.927 14.7528 139.709 15.7173 139.709Z"
fill={fillColor}
/>
<path
d="M16.1155 30.6692V52.0877L16.154 51.3757L47.2378 40.2835L37.5601 28.6635H17.6833L16.1155 30.6692Z"
fill={fillColor}
/>
<path
d="M50.1994 39.2266L60.3848 35.5919L70.2272 39.1664L80.0505 27.36L69.8331 14.288H50.5465L40.329 27.36L50.1994 39.2266Z"
fill={fillColor}
/>
<path
d="M73.1775 40.2378L103.884 51.3894V30.1831L102.696 28.6635H82.8195L73.1775 40.2378Z"
fill={fillColor}
/>
<path
d="M103.47 25.0667C101.547 13.5656 93.1078 4.27715 82.0853 1.14078C82.1758 1.16652 82.2661 1.19273 82.3562 1.21931L72.881 13.3416L82.8195 26.0566H102.696L103.47 25.0667Z"
fill={fillColor}
/>
<path
d="M78.8463 0.411703C77.2271 0.14089 75.564 0 73.868 0H46.1316C44.3884 0 42.6801 0.148882 41.0182 0.434658C41.071 0.42555 41.1239 0.416628 41.1768 0.407799L50.1322 11.8651H69.8939L78.8463 0.411703Z"
fill={fillColor}
/>
<path
d="M37.8593 1.15658C31.1984 3.06565 25.4851 7.22236 21.5885 12.7555L21.4348 12.9425L21.4582 12.9421C19.0658 16.3944 17.3767 20.3728 16.5987 24.6691L17.6833 26.0566H37.5601L47.441 13.4153L37.8593 1.15658Z"
fill={fillColor}
/>
<path
d="M72.2502 68.607C74.342 66.562 75.9216 63.9975 76.7792 61.1227H67.5016L59.9998 55.5094L52.498 61.1227H43.2205C44.078 63.9975 45.6576 66.562 47.7494 68.607H72.2502Z"
fill={fillColor}
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M77.3268 58.6279C77.4436 57.813 77.5041 56.9801 77.5041 56.1331C77.5041 46.4882 69.6671 38.6695 59.9998 38.6695C50.3325 38.6695 42.4956 46.4882 42.4956 56.1331C42.4956 56.9801 42.556 57.813 42.6728 58.6279H51.2477L59.9998 52.3908L68.7519 58.6279H77.3268ZM68.7519 53.6383C70.133 53.6383 71.2525 52.5213 71.2525 51.1435C71.2525 49.7657 70.133 48.6487 68.7519 48.6487C67.3709 48.6487 66.2514 49.7657 66.2514 51.1435C66.2514 52.5213 67.3709 53.6383 68.7519 53.6383ZM53.7483 51.1435C53.7483 52.5213 52.6287 53.6383 51.2477 53.6383C49.8667 53.6383 48.7471 52.5213 48.7471 51.1435C48.7471 49.7657 49.8667 48.6487 51.2477 48.6487C52.6287 48.6487 53.7483 49.7657 53.7483 51.1435Z"
fill={fillColor}
/>
<path d="M76.2853 81.0811L86.2877 71.1019H61.2501V81.0811H76.2853Z" fill={fillColor} />
<path d="M58.7495 71.1019H34.2613L44.2638 81.0811H58.7495V71.1019Z" fill={fillColor} />
<path
d="M79.8217 81.0811H89.3348C92.9402 81.0811 96.3701 79.5283 98.7443 76.8213L103.76 71.1019H89.8241L79.8217 81.0811Z"
fill={fillColor}
/>
<path
d="M30.725 71.1019H16.2393L21.3692 76.8789C23.7422 79.5512 27.1492 81.0811 30.7278 81.0811H40.7274L30.725 71.1019Z"
fill={fillColor}
/>
<path
d="M31.2429 53.1522H8.73738C8.04687 53.1522 7.48715 53.7107 7.48715 54.3996C7.48715 55.0885 8.04687 55.647 8.73738 55.647H11.238V56.8944H8.73738C7.35635 56.8944 6.23682 57.7321 6.23682 58.7655C6.23682 59.7989 7.35635 60.6366 8.73738 60.6366H11.238V61.884H8.73738C7.35635 61.884 6.23682 62.7217 6.23682 63.7551C6.23682 64.7885 7.35635 65.6262 8.73738 65.6262H11.238V66.8736H8.73738C8.04687 66.8736 7.48715 67.4321 7.48715 68.121C7.48715 68.8099 8.04687 69.3684 8.73738 69.3684H31.2429V69.3448C31.3533 69.3531 31.4643 69.3593 31.576 69.3633C31.6729 69.3666 31.7702 69.3684 31.868 69.3684C36.3564 69.3684 39.995 65.7383 39.995 61.2603C39.995 56.7823 36.3564 53.1522 31.868 53.1522C31.8238 53.1522 31.7798 53.1525 31.7358 53.1532C31.6967 53.1538 31.6576 53.1548 31.6186 53.1559L31.5655 53.1577L31.5254 53.1593C31.4758 53.1613 31.4263 53.1638 31.377 53.1668C31.3322 53.1694 31.2875 53.1724 31.2429 53.1758V53.1522Z"
fill={fillColor}
/>
<path
d="M111.262 69.8545H88.7568V69.8308C88.5505 69.8465 88.342 69.8545 88.1316 69.8545C83.6432 69.8545 80.0046 66.2243 80.0046 61.7463C80.0046 57.2684 83.6432 53.6383 88.1316 53.6383C88.342 53.6383 88.5505 53.6462 88.7568 53.6619V53.6383H111.262C111.953 53.6383 112.512 54.1967 112.512 54.8856C112.512 55.5746 111.953 56.1331 111.262 56.1331H108.762V57.3804H111.262C112.643 57.3804 113.763 58.2181 113.763 59.2515C113.763 60.2849 112.643 61.1227 111.262 61.1227H108.762V62.3701H111.262C112.643 62.3701 113.763 63.2078 113.763 64.2411C113.763 65.2745 112.643 66.1122 111.262 66.1122H108.762V67.3597H111.262C111.953 67.3597 112.512 67.9182 112.512 68.607C112.512 69.296 111.953 69.8545 111.262 69.8545Z"
fill={fillColor}
/>
</svg>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -0,0 +1,10 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_743_9407)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.81414 2.10472C9.17675 1.0169 10.662 0.967449 11.1266 1.95638L11.1859 2.10472L14.9987 13.5475L16.3141 9.60472C16.4688 9.14069 16.8777 8.81397 17.3549 8.7584L17.5 8.75H18.75C19.4404 8.75 20 9.30964 20 10C20 10.641 19.5174 11.1694 18.8958 11.2416L18.75 11.25H18.4L16.1859 17.8953C15.8232 18.9831 14.338 19.0326 13.8734 18.0436L13.8141 17.8953L10 6.45125L7.43585 14.1453C7.08436 15.1998 5.65996 15.2891 5.15506 14.3547L5.0894 14.2142L3.70125 10.745L3.61272 10.8541C3.41377 11.0667 3.14416 11.2057 2.84932 11.2411L2.7 11.25H1.25C0.559642 11.25 -1.90735e-06 10.6904 -1.90735e-06 10C-1.90735e-06 9.35896 0.482548 8.83062 1.10422 8.75841L1.25 8.75H1.86875L2.59752 7.01596C3.00984 6.03426 4.3582 5.99888 4.84436 6.89445L4.91059 7.03576L6.1425 10.1175L8.81414 2.10472Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_743_9407">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-description"
class={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-title"
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-action"
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="card-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
>
{@render children?.()}
</p>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-footer"
class={cn("[.border-t]:pt-6 flex items-center px-6", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-header"
class={cn(
"@container/card-header has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6 grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-title"
class={cn("font-semibold leading-none", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card"
class={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,25 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
import Action from "./card-action.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction,
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script>
<span
bind:this={ref}
data-slot="command-shortcut"
class={cn("text-muted-foreground ms-auto text-xs tracking-widest", className)}
{...restProps}
>
{@render children?.()}
</span>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,20 @@
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="dialog-footer"
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="dialog-header"
class={cn("flex flex-col gap-2 text-center sm:text-start", className)}
{...restProps}
>
{@render children?.()}
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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