Bring packages and UI library up to date

This commit is contained in:
daylily
2026-01-12 16:08:10 -06:00
parent 260d838cb5
commit 2af6e1726b
66 changed files with 1611 additions and 1425 deletions
+5 -6
View File
@@ -1,17 +1,16 @@
{ {
"$schema": "https://next.shadcn-svelte.com/schema.json", "$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": { "tailwind": {
"config": "tailwind.config.ts",
"css": "src/app.css", "css": "src/app.css",
"baseColor": "neutral" "baseColor": "stone"
}, },
"aliases": { "aliases": {
"components": "$lib/components", "components": "$lib/components",
"utils": "$lib/utils", "utils": "$lib/utils",
"ui": "$lib/components/ui", "ui": "$lib/components/ui",
"hooks": "$lib/hooks" "hooks": "$lib/hooks",
"lib": "$lib"
}, },
"typescript": true, "typescript": true,
"registry": "https://next.shadcn-svelte.com/registry" "registry": "https://shadcn-svelte.com/registry"
} }
+21 -19
View File
@@ -9,30 +9,32 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {
"@fontsource-variable/ibm-plex-sans": "^5.2.5", "@fontsource-variable/ibm-plex-sans": "^5.2.8",
"@iconify/json": "^2.2.323", "@iconify/json": "^2.2.427",
"@lucide/svelte": "^0.487.0", "@internationalized/date": "^3.10.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@lucide/svelte": "^0.561.0",
"@tsconfig/svelte": "^5.0.4", "@sveltejs/vite-plugin-svelte": "^6.2.4",
"@types/node": "^22.14.0", "@tailwindcss/vite": "^4.1.18",
"@tsconfig/svelte": "^5.0.6",
"@types/node": "^22.19.5",
"@types/w3c-web-hid": "^1.0.6", "@types/w3c-web-hid": "^1.0.6",
"@variegated-coffee/serde-postcard-ts": "^0.1.4", "@variegated-coffee/serde-postcard-ts": "^0.1.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.23",
"bits-ui": "^1.3.16", "bits-ui": "^2.15.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"mode-watcher": "^0.5.1", "mode-watcher": "^1.1.0",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"prettier-plugin-svelte": "^3.4.1", "prettier-plugin-svelte": "^3.4.1",
"svelte": "^5.25.7", "svelte": "^5.46.1",
"svelte-sonner": "^0.3.28", "svelte-sonner": "^1.0.7",
"tailwind-merge": "^3.1.0", "tailwind-merge": "^3.4.0",
"tailwind-variants": "^1.0.0", "tailwind-variants": "^3.2.2",
"tailwindcss": "^3.4.17", "tailwindcss": "^4.1.18",
"tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.4.0",
"typescript": "^5.8.3", "typescript": "^5.9.3",
"unplugin-icons": "^22.1.0", "unplugin-icons": "^22.5.0",
"vite": "^6.2.5", "vite": "^7.3.1",
"wrangler": "^4.34.0" "wrangler": "^4.58.0"
}, },
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a" "packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
} }
+803 -884
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
onlyBuiltDependencies:
- esbuild
- workerd
-6
View File
@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
+3 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { ModeWatcher } from 'mode-watcher' import { mode, ModeWatcher } from 'mode-watcher'
import { Toaster } from 'svelte-sonner' import { Toaster } from 'svelte-sonner'
import Footer from '$lib/layouts/Footer.svelte' import Footer from '$lib/layouts/Footer.svelte'
@@ -10,9 +10,9 @@ const unsupported = navigator.requestMIDIAccess === undefined
</script> </script>
<ModeWatcher /> <ModeWatcher />
<Toaster position="bottom-center" duration={2000} /> <Toaster position="bottom-center" duration={2000} theme={mode.current} invert />
<div class="max-w-screen-xl min-h-dvh m-auto p-6 stack gap-4"> <div class="max-w-7xl min-h-dvh m-auto p-6 stack gap-4">
{#if unsupported} {#if unsupported}
<Unsupported /> <Unsupported />
{:else} {:else}
+127 -76
View File
@@ -1,73 +1,126 @@
/* ibm-plex-sans-latin-wght-normal */ @import 'tailwindcss';
@font-face {
font-family: 'IBM Plex Sans Variable'; @import 'tw-animate-css';
font-style: normal; @import '@fontsource-variable/ibm-plex-sans';
font-display: swap;
font-weight: 100 700; @custom-variant dark (&:is(.dark *));
src: url(@fontsource-variable/ibm-plex-sans/files/ibm-plex-sans-latin-wght-normal.woff2) format('woff2-variations');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, :root {
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; --radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.147 0.004 49.25);
--card: oklch(1 0 0);
--card-foreground: oklch(0.147 0.004 49.25);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.147 0.004 49.25);
--primary: oklch(0.216 0.006 56.043);
--primary-foreground: oklch(0.985 0.001 106.423);
--secondary: oklch(0.97 0.001 106.424);
--secondary-foreground: oklch(0.216 0.006 56.043);
--muted: oklch(0.97 0.001 106.424);
--muted-foreground: oklch(0.553 0.013 58.071);
--accent: oklch(0.97 0.001 106.424);
--accent-foreground: oklch(0.216 0.006 56.043);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.923 0.003 48.717);
--input: oklch(0.923 0.003 48.717);
--ring: oklch(0.709 0.01 56.259);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0.001 106.423);
--sidebar-foreground: oklch(0.147 0.004 49.25);
--sidebar-primary: oklch(0.216 0.006 56.043);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.97 0.001 106.424);
--sidebar-accent-foreground: oklch(0.216 0.006 56.043);
--sidebar-border: oklch(0.923 0.003 48.717);
--sidebar-ring: oklch(0.709 0.01 56.259);
} }
@tailwind base; .dark {
@tailwind components; --background: oklch(0.147 0.004 49.25);
@tailwind utilities; --foreground: oklch(0.985 0.001 106.423);
--card: oklch(0.216 0.006 56.043);
--card-foreground: oklch(0.985 0.001 106.423);
--popover: oklch(0.216 0.006 56.043);
--popover-foreground: oklch(0.985 0.001 106.423);
--primary: oklch(0.923 0.003 48.717);
--primary-foreground: oklch(0.216 0.006 56.043);
--secondary: oklch(0.268 0.007 34.298);
--secondary-foreground: oklch(0.985 0.001 106.423);
--muted: oklch(0.268 0.007 34.298);
--muted-foreground: oklch(0.709 0.01 56.259);
--accent: oklch(0.268 0.007 34.298);
--accent-foreground: oklch(0.985 0.001 106.423);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.553 0.013 58.071);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.216 0.006 56.043);
--sidebar-foreground: oklch(0.985 0.001 106.423);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.268 0.007 34.298);
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.553 0.013 58.071);
}
@layer base { @theme inline {
:root { --font-sans: 'IBM Plex Sans Variable', 'IBM Plex Sans', 'Fira Sans', sans-serif;
--background: 0 0% 100%; --breakpoint-2xs: 24rem;
--foreground: 20 14.3% 4.1%; --breakpoint-xs: 32rem;
--card: 0 0% 100%; --radius-sm: calc(var(--radius) - 4px);
--card-foreground: 20 14.3% 4.1%; --radius-md: calc(var(--radius) - 2px);
--popover: 0 0% 100%; --radius-lg: var(--radius);
--popover-foreground: 20 14.3% 4.1%; --radius-xl: calc(var(--radius) + 4px);
--primary: 24 9.8% 10%; --color-background: var(--background);
--primary-foreground: 60 9.1% 97.8%; --color-foreground: var(--foreground);
--secondary: 60 4.8% 95.9%; --color-card: var(--card);
--secondary-foreground: 24 9.8% 10%; --color-card-foreground: var(--card-foreground);
--muted: 60 4.8% 95.9%; --color-popover: var(--popover);
--muted-foreground: 25 5.3% 44.7%; --color-popover-foreground: var(--popover-foreground);
--accent: 60 4.8% 95.9%; --color-primary: var(--primary);
--accent-foreground: 24 9.8% 10%; --color-primary-foreground: var(--primary-foreground);
--destructive: 0 72.22% 50.59%; --color-secondary: var(--secondary);
--destructive-foreground: 60 9.1% 97.8%; --color-secondary-foreground: var(--secondary-foreground);
--border: 20 5.9% 90%; --color-muted: var(--muted);
--input: 20 5.9% 90%; --color-muted-foreground: var(--muted-foreground);
--ring: 20 14.3% 4.1%; --color-accent: var(--accent);
--radius: 0.5rem; --color-accent-foreground: var(--accent-foreground);
} --color-destructive: var(--destructive);
--color-border: var(--border);
.dark { --color-input: var(--input);
--background: 20 14.3% 4.1%; --color-ring: var(--ring);
--foreground: 60 9.1% 97.8%; --color-chart-1: var(--chart-1);
--card: 20 14.3% 4.1%; --color-chart-2: var(--chart-2);
--card-foreground: 60 9.1% 97.8%; --color-chart-3: var(--chart-3);
--popover: 20 14.3% 4.1%; --color-chart-4: var(--chart-4);
--popover-foreground: 60 9.1% 97.8%; --color-chart-5: var(--chart-5);
--primary: 60 9.1% 97.8%; --color-sidebar: var(--sidebar);
--primary-foreground: 24 9.8% 10%; --color-sidebar-foreground: var(--sidebar-foreground);
--secondary: 12 6.5% 15.1%; --color-sidebar-primary: var(--sidebar-primary);
--secondary-foreground: 60 9.1% 97.8%; --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--muted: 12 6.5% 15.1%; --color-sidebar-accent: var(--sidebar-accent);
--muted-foreground: 24 5.4% 63.9%; --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--accent: 12 6.5% 15.1%; --color-sidebar-border: var(--sidebar-border);
--accent-foreground: 60 9.1% 97.8%; --color-sidebar-ring: var(--sidebar-ring);
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 24 5.7% 82.9%;
}
} }
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
font-family: IBM Plex Sans Variable, IBM Plex Sans, Fira Sans, sans-serif;
} }
::selection { ::selection {
@@ -79,20 +132,18 @@
} }
} }
@layer utilities { @utility stack {
.stack { @apply flex flex-col items-stretch;
@apply flex flex-col; }
}
@utility stack-h {
.stack-h { @apply flex flex-row items-stretch;
@apply flex; }
}
@utility row {
.row { @apply flex flex-row items-center;
@apply flex items-center; }
}
@utility col {
.col { @apply flex flex-col items-center;
@apply flex flex-col items-center;
}
} }
@@ -10,4 +10,9 @@
}: AlertDialogPrimitive.ActionProps = $props(); }: AlertDialogPrimitive.ActionProps = $props();
</script> </script>
<AlertDialogPrimitive.Action bind:ref class={cn(buttonVariants(), className)} {...restProps} /> <AlertDialogPrimitive.Action
bind:ref
data-slot="alert-dialog-action"
class={cn(buttonVariants(), className)}
{...restProps}
/>
@@ -12,6 +12,7 @@
<AlertDialogPrimitive.Cancel <AlertDialogPrimitive.Cancel
bind:ref bind:ref
class={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)} data-slot="alert-dialog-cancel"
class={cn(buttonVariants({ variant: "outline" }), className)}
{...restProps} {...restProps}
/> />
@@ -1,7 +1,9 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive, type WithoutChild } from "bits-ui"; import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import AlertDialogPortal from "./alert-dialog-portal.svelte";
import AlertDialogOverlay from "./alert-dialog-overlay.svelte"; import AlertDialogOverlay from "./alert-dialog-overlay.svelte";
import { cn } from "$lib/utils.js"; import { cn, type WithoutChild, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -9,18 +11,19 @@
portalProps, portalProps,
...restProps ...restProps
}: WithoutChild<AlertDialogPrimitive.ContentProps> & { }: WithoutChild<AlertDialogPrimitive.ContentProps> & {
portalProps?: AlertDialogPrimitive.PortalProps; portalProps?: WithoutChildrenOrChild<ComponentProps<typeof AlertDialogPortal>>;
} = $props(); } = $props();
</script> </script>
<AlertDialogPrimitive.Portal {...portalProps}> <AlertDialogPortal {...portalProps}>
<AlertDialogOverlay /> <AlertDialogOverlay />
<AlertDialogPrimitive.Content <AlertDialogPrimitive.Content
bind:ref bind:ref
data-slot="alert-dialog-content"
class={cn( 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 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg", "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 top-[50%] left-[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 className
)} )}
{...restProps} {...restProps}
/> />
</AlertDialogPrimitive.Portal> </AlertDialogPortal>
@@ -11,6 +11,7 @@
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description
bind:ref bind:ref
data-slot="alert-dialog-description"
class={cn("text-muted-foreground text-sm", className)} class={cn("text-muted-foreground text-sm", className)}
{...restProps} {...restProps}
/> />
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { WithElementRef } from "bits-ui"; import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -13,7 +12,8 @@
<div <div
bind:this={ref} bind:this={ref}
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} data-slot="alert-dialog-footer"
class={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js"; import { cn, type WithElementRef } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -13,7 +12,8 @@
<div <div
bind:this={ref} bind:this={ref}
class={cn("flex flex-col space-y-2 text-center sm:text-left", className)} data-slot="alert-dialog-header"
class={cn("flex flex-col gap-2 text-center sm:text-start", className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -11,8 +11,9 @@
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Overlay
bind:ref bind:ref
data-slot="alert-dialog-overlay"
class={cn( 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/80", "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 className
)} )}
{...restProps} {...restProps}
@@ -0,0 +1,7 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
let { ...restProps }: AlertDialogPrimitive.PortalProps = $props();
</script>
<AlertDialogPrimitive.Portal {...restProps} />
@@ -5,14 +5,13 @@
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
level = 3,
...restProps ...restProps
}: AlertDialogPrimitive.TitleProps = $props(); }: AlertDialogPrimitive.TitleProps = $props();
</script> </script>
<AlertDialogPrimitive.Title <AlertDialogPrimitive.Title
bind:ref bind:ref
data-slot="alert-dialog-title"
class={cn("text-lg font-semibold", className)} class={cn("text-lg font-semibold", className)}
{level}
{...restProps} {...restProps}
/> />
@@ -0,0 +1,7 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: AlertDialogPrimitive.TriggerProps = $props();
</script>
<AlertDialogPrimitive.Trigger bind:ref data-slot="alert-dialog-trigger" {...restProps} />
@@ -0,0 +1,7 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
let { open = $bindable(false), ...restProps }: AlertDialogPrimitive.RootProps = $props();
</script>
<AlertDialogPrimitive.Root bind:open {...restProps} />
@@ -1,4 +1,6 @@
import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; import Root from "./alert-dialog.svelte";
import Portal from "./alert-dialog-portal.svelte";
import Trigger from "./alert-dialog-trigger.svelte";
import Title from "./alert-dialog-title.svelte"; import Title from "./alert-dialog-title.svelte";
import Action from "./alert-dialog-action.svelte"; import Action from "./alert-dialog-action.svelte";
import Cancel from "./alert-dialog-cancel.svelte"; import Cancel from "./alert-dialog-cancel.svelte";
@@ -8,10 +10,6 @@ import Overlay from "./alert-dialog-overlay.svelte";
import Content from "./alert-dialog-content.svelte"; import Content from "./alert-dialog-content.svelte";
import Description from "./alert-dialog-description.svelte"; import Description from "./alert-dialog-description.svelte";
const Root = AlertDialogPrimitive.Root;
const Trigger = AlertDialogPrimitive.Trigger;
const Portal = AlertDialogPrimitive.Portal;
export { export {
Root, Root,
Title, Title,
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js"; import { cn, type WithElementRef } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -11,6 +10,14 @@
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script> </script>
<div bind:this={ref} class={cn("text-sm [&_p]:leading-relaxed", className)} {...restProps}> <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?.()} {@render children?.()}
</div> </div>
@@ -1,24 +1,19 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui"; import { cn, type WithElementRef } from "$lib/utils.js";
import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
level = 5,
children, children,
...restProps ...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
level?: 1 | 2 | 3 | 4 | 5 | 6;
} = $props();
</script> </script>
<div <div
role="heading"
aria-level={level}
bind:this={ref} bind:this={ref}
class={cn("mb-1 font-medium leading-none tracking-tight", className)} data-slot="alert-title"
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -2,12 +2,12 @@
import { type VariantProps, tv } from "tailwind-variants"; import { type VariantProps, tv } from "tailwind-variants";
export const alertVariants = tv({ export const alertVariants = tv({
base: "[&>svg]:text-foreground relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg~*]:pl-7", 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: { variants: {
variant: { variant: {
default: "bg-background text-foreground", default: "bg-card text-card-foreground",
destructive: destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", "text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -20,8 +20,7 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui"; import { cn, type WithElementRef } from "$lib/utils.js";
import { cn } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -34,6 +33,12 @@
} = $props(); } = $props();
</script> </script>
<div bind:this={ref} class={cn(alertVariants({ variant }), className)} {...restProps} role="alert"> <div
bind:this={ref}
data-slot="alert"
class={cn(alertVariants({ variant }), className)}
{...restProps}
role="alert"
>
{@render children?.()} {@render children?.()}
</div> </div>
@@ -1,25 +1,28 @@
<script lang="ts" module> <script lang="ts" module>
import type { WithElementRef } from "bits-ui"; import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements"; import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants"; import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({ export const buttonVariants = tv({
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 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: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", 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: outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border", "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", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
default: "h-10 px-4 py-2", default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-9 rounded-md px-3", sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-11 rounded-md px-8", lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "h-10 w-10", icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -39,8 +42,6 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { cn } from "$lib/utils.js";
let { let {
class: className, class: className,
variant = "default", variant = "default",
@@ -48,6 +49,7 @@
ref = $bindable(null), ref = $bindable(null),
href = undefined, href = undefined,
type = "button", type = "button",
disabled,
children, children,
...restProps ...restProps
}: ButtonProps = $props(); }: ButtonProps = $props();
@@ -56,8 +58,12 @@
{#if href} {#if href}
<a <a
bind:this={ref} bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)} class={cn(buttonVariants({ variant, size }), className)}
{href} href={disabled ? undefined : href}
aria-disabled={disabled}
role={disabled ? "link" : undefined}
tabindex={disabled ? -1 : undefined}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
@@ -65,8 +71,10 @@
{:else} {:else}
<button <button
bind:this={ref} bind:this={ref}
data-slot="button"
class={cn(buttonVariants({ variant, size }), className)} class={cn(buttonVariants({ variant, size }), className)}
{type} {type}
{disabled}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
+44 -37
View File
@@ -1,45 +1,52 @@
<script lang="ts"> <script lang="ts">
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements' import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
import type { WithElementRef } from 'bits-ui' import { cn, type WithElementRef } from "$lib/utils.js";
import { cn } from '$lib/utils.js'
type InputType = Exclude<HTMLInputTypeAttribute, 'file'> type InputType = Exclude<HTMLInputTypeAttribute, "file">;
type Props = WithElementRef< type Props = WithElementRef<
Omit<HTMLInputAttributes, 'type'> & ({ type: 'file'; files?: FileList } | { type?: InputType; files?: undefined }) Omit<HTMLInputAttributes, "type"> &
> ({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
>;
let { let {
ref = $bindable(null), ref = $bindable(null),
value = $bindable(), value = $bindable(),
type, type,
files = $bindable(), files = $bindable(),
class: className, class: className,
...restProps "data-slot": dataSlot = "input",
}: Props = $props() ...restProps
}: Props = $props();
</script> </script>
{#if type === 'file'} {#if type === "file"}
<input <input
bind:this={ref} bind:this={ref}
class={cn( data-slot={dataSlot}
'border-input bg-secondary text-secondary-foreground ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm hover:cursor-pointer file:hover:cursor-pointer hover:bg-secondary/80 transition-colors', class={cn(
className, "selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50",
)} "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
type="file" "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
bind:files className
bind:value )}
{...restProps} type="file"
/> bind:files
bind:value
{...restProps}
/>
{:else} {:else}
<input <input
bind:this={ref} bind:this={ref}
class={cn( data-slot={dataSlot}
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', class={cn(
className, "border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
)} "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
{type} "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
bind:value className
{...restProps} )}
/> {type}
bind:value
{...restProps}
/>
{/if} {/if}
@@ -1,12 +1,20 @@
<script lang="ts"> <script lang="ts">
import { Label as LabelPrimitive } from 'bits-ui' import { Label as LabelPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js' import { cn } from "$lib/utils.js";
let { ref = $bindable(null), class: className, ...restProps }: LabelPrimitive.RootProps = $props() let {
ref = $bindable(null),
class: className,
...restProps
}: LabelPrimitive.RootProps = $props();
</script> </script>
<LabelPrimitive.Root <LabelPrimitive.Root
bind:ref bind:ref
class={cn('text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', className)} data-slot="label"
{...restProps} class={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...restProps}
/> />
@@ -1,17 +1,19 @@
import { Popover as PopoverPrimitive } from "bits-ui"; import Root from "./popover.svelte";
import Close from "./popover-close.svelte";
import Content from "./popover-content.svelte"; import Content from "./popover-content.svelte";
const Root = PopoverPrimitive.Root; import Trigger from "./popover-trigger.svelte";
const Trigger = PopoverPrimitive.Trigger; import Portal from "./popover-portal.svelte";
const Close = PopoverPrimitive.Close;
export { export {
Root, Root,
Content, Content,
Trigger, Trigger,
Close, Close,
Portal,
// //
Root as Popover, Root as Popover,
Content as PopoverContent, Content as PopoverContent,
Trigger as PopoverTrigger, Trigger as PopoverTrigger,
Close as PopoverClose, Close as PopoverClose,
Portal as PopoverPortal,
}; };
@@ -0,0 +1,7 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
</script>
<PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} />
@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import { cn } from "$lib/utils.js";
import { Popover as PopoverPrimitive } from "bits-ui"; import { Popover as PopoverPrimitive } from "bits-ui";
import PopoverPortal from "./popover-portal.svelte";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -10,19 +12,20 @@
portalProps, portalProps,
...restProps ...restProps
}: PopoverPrimitive.ContentProps & { }: PopoverPrimitive.ContentProps & {
portalProps?: PopoverPrimitive.PortalProps; portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>;
} = $props(); } = $props();
</script> </script>
<PopoverPrimitive.Portal {...portalProps}> <PopoverPortal {...portalProps}>
<PopoverPrimitive.Content <PopoverPrimitive.Content
bind:ref bind:ref
data-slot="popover-content"
{sideOffset} {sideOffset}
{align} {align}
class={cn( 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-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none", "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 w-72 origin-(--bits-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className className
)} )}
{...restProps} {...restProps}
/> />
</PopoverPrimitive.Portal> </PopoverPortal>
@@ -0,0 +1,7 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from "bits-ui";
let { ...restProps }: PopoverPrimitive.PortalProps = $props();
</script>
<PopoverPrimitive.Portal {...restProps} />
@@ -0,0 +1,17 @@
<script lang="ts">
import { cn } from "$lib/utils.js";
import { Popover as PopoverPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
...restProps
}: PopoverPrimitive.TriggerProps = $props();
</script>
<PopoverPrimitive.Trigger
bind:ref
data-slot="popover-trigger"
class={cn("", className)}
{...restProps}
/>
@@ -0,0 +1,7 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from "bits-ui";
let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();
</script>
<PopoverPrimitive.Root bind:open {...restProps} />
+11 -8
View File
@@ -1,34 +1,37 @@
import { Select as SelectPrimitive } from "bits-ui"; import Root from "./select.svelte";
import Group from "./select-group.svelte";
import GroupHeading from "./select-group-heading.svelte"; import Label from "./select-label.svelte";
import Item from "./select-item.svelte"; import Item from "./select-item.svelte";
import Content from "./select-content.svelte"; import Content from "./select-content.svelte";
import Trigger from "./select-trigger.svelte"; import Trigger from "./select-trigger.svelte";
import Separator from "./select-separator.svelte"; import Separator from "./select-separator.svelte";
import ScrollDownButton from "./select-scroll-down-button.svelte"; import ScrollDownButton from "./select-scroll-down-button.svelte";
import ScrollUpButton from "./select-scroll-up-button.svelte"; import ScrollUpButton from "./select-scroll-up-button.svelte";
import GroupHeading from "./select-group-heading.svelte";
const Root = SelectPrimitive.Root; import Portal from "./select-portal.svelte";
const Group = SelectPrimitive.Group;
export { export {
Root, Root,
Group, Group,
GroupHeading, Label,
Item, Item,
Content, Content,
Trigger, Trigger,
Separator, Separator,
ScrollDownButton, ScrollDownButton,
ScrollUpButton, ScrollUpButton,
GroupHeading,
Portal,
// //
Root as Select, Root as Select,
Group as SelectGroup, Group as SelectGroup,
GroupHeading as SelectGroupHeading, Label as SelectLabel,
Item as SelectItem, Item as SelectItem,
Content as SelectContent, Content as SelectContent,
Trigger as SelectTrigger, Trigger as SelectTrigger,
Separator as SelectSeparator, Separator as SelectSeparator,
ScrollDownButton as SelectScrollDownButton, ScrollDownButton as SelectScrollDownButton,
ScrollUpButton as SelectScrollUpButton, ScrollUpButton as SelectScrollUpButton,
GroupHeading as SelectGroupHeading,
Portal as SelectPortal,
}; };
@@ -1,8 +1,11 @@
<script lang="ts"> <script lang="ts">
import { Select as SelectPrimitive, type WithoutChild } from "bits-ui"; import { Select as SelectPrimitive } from "bits-ui";
import SelectPortal from "./select-portal.svelte";
import SelectScrollUpButton from "./select-scroll-up-button.svelte"; import SelectScrollUpButton from "./select-scroll-up-button.svelte";
import SelectScrollDownButton from "./select-scroll-down-button.svelte"; import SelectScrollDownButton from "./select-scroll-down-button.svelte";
import { cn } from "$lib/utils.js"; import { cn, type WithoutChild } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
import type { WithoutChildrenOrChild } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -10,18 +13,21 @@
sideOffset = 4, sideOffset = 4,
portalProps, portalProps,
children, children,
preventScroll = true,
...restProps ...restProps
}: WithoutChild<SelectPrimitive.ContentProps> & { }: WithoutChild<SelectPrimitive.ContentProps> & {
portalProps?: SelectPrimitive.PortalProps; portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SelectPortal>>;
} = $props(); } = $props();
</script> </script>
<SelectPrimitive.Portal {...portalProps}> <SelectPortal {...portalProps}>
<SelectPrimitive.Content <SelectPrimitive.Content
bind:ref bind:ref
{sideOffset} {sideOffset}
{preventScroll}
data-slot="select-content"
class={cn( class={cn(
"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-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", "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 relative z-50 max-h-(--bits-select-content-available-height) min-w-[8rem] origin-(--bits-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className className
)} )}
{...restProps} {...restProps}
@@ -29,11 +35,11 @@
<SelectScrollUpButton /> <SelectScrollUpButton />
<SelectPrimitive.Viewport <SelectPrimitive.Viewport
class={cn( class={cn(
"h-[var(--bits-select-anchor-height)] w-full min-w-[var(--bits-select-anchor-width)] p-1" "h-(--bits-select-anchor-height) w-full min-w-(--bits-select-anchor-width) scroll-my-1 p-1"
)} )}
> >
{@render children?.()} {@render children?.()}
</SelectPrimitive.Viewport> </SelectPrimitive.Viewport>
<SelectScrollDownButton /> <SelectScrollDownButton />
</SelectPrimitive.Content> </SelectPrimitive.Content>
</SelectPrimitive.Portal> </SelectPortal>
@@ -1,16 +1,21 @@
<script lang="ts"> <script lang="ts">
import { Select as SelectPrimitive } from "bits-ui"; import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children,
...restProps ...restProps
}: SelectPrimitive.GroupHeadingProps = $props(); }: ComponentProps<typeof SelectPrimitive.GroupHeading> = $props();
</script> </script>
<SelectPrimitive.GroupHeading <SelectPrimitive.GroupHeading
bind:ref bind:ref
class={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} data-slot="select-group-heading"
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...restProps} {...restProps}
/> >
{@render children?.()}
</SelectPrimitive.GroupHeading>
@@ -0,0 +1,7 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: SelectPrimitive.GroupProps = $props();
</script>
<SelectPrimitive.Group bind:ref data-slot="select-group" {...restProps} />
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Check from "@lucide/svelte/icons/check"; import CheckIcon from "@lucide/svelte/icons/check";
import { Select as SelectPrimitive, type WithoutChild } from "bits-ui"; import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn, type WithoutChild } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -16,16 +16,17 @@
<SelectPrimitive.Item <SelectPrimitive.Item
bind:ref bind:ref
{value} {value}
data-slot="select-item"
class={cn( class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 ps-2 pe-8 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 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className className
)} )}
{...restProps} {...restProps}
> >
{#snippet children({ selected, highlighted })} {#snippet children({ selected, highlighted })}
<span class="absolute left-2 flex size-3.5 items-center justify-center"> <span class="absolute end-2 flex size-3.5 items-center justify-center">
{#if selected} {#if selected}
<Check class="size-4" /> <CheckIcon class="size-4" />
{/if} {/if}
</span> </span>
{#if childrenProp} {#if childrenProp}
@@ -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="select-label"
class={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,7 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
let { ...restProps }: SelectPrimitive.PortalProps = $props();
</script>
<SelectPrimitive.Portal {...restProps} />
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import ChevronDown from "@lucide/svelte/icons/chevron-down"; import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
import { Select as SelectPrimitive, type WithoutChildrenOrChild } from "bits-ui"; import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -12,8 +12,9 @@
<SelectPrimitive.ScrollDownButton <SelectPrimitive.ScrollDownButton
bind:ref bind:ref
data-slot="select-scroll-down-button"
class={cn("flex cursor-default items-center justify-center py-1", className)} class={cn("flex cursor-default items-center justify-center py-1", className)}
{...restProps} {...restProps}
> >
<ChevronDown class="size-4" /> <ChevronDownIcon class="size-4" />
</SelectPrimitive.ScrollDownButton> </SelectPrimitive.ScrollDownButton>
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import ChevronUp from "@lucide/svelte/icons/chevron-up"; import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
import { Select as SelectPrimitive, type WithoutChildrenOrChild } from "bits-ui"; import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -12,8 +12,9 @@
<SelectPrimitive.ScrollUpButton <SelectPrimitive.ScrollUpButton
bind:ref bind:ref
data-slot="select-scroll-up-button"
class={cn("flex cursor-default items-center justify-center py-1", className)} class={cn("flex cursor-default items-center justify-center py-1", className)}
{...restProps} {...restProps}
> >
<ChevronUp class="size-4" /> <ChevronUpIcon class="size-4" />
</SelectPrimitive.ScrollUpButton> </SelectPrimitive.ScrollUpButton>
@@ -10,4 +10,9 @@
}: SeparatorPrimitive.RootProps = $props(); }: SeparatorPrimitive.RootProps = $props();
</script> </script>
<Separator bind:ref class={cn("bg-muted -mx-1 my-1 h-px", className)} {...restProps} /> <Separator
bind:ref
data-slot="select-separator"
class={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...restProps}
/>
@@ -1,24 +1,29 @@
<script lang="ts"> <script lang="ts">
import { Select as SelectPrimitive, type WithoutChild } from "bits-ui"; import { Select as SelectPrimitive } from "bits-ui";
import ChevronDown from "@lucide/svelte/icons/chevron-down"; import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
import { cn } from "$lib/utils.js"; import { cn, type WithoutChild } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
size = "default",
...restProps ...restProps
}: WithoutChild<SelectPrimitive.TriggerProps> = $props(); }: WithoutChild<SelectPrimitive.TriggerProps> & {
size?: "sm" | "default";
} = $props();
</script> </script>
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
bind:ref bind:ref
data-slot="select-trigger"
data-size={size}
class={cn( class={cn(
"border-input bg-background ring-offset-background data-[placeholder]:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none select-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className className
)} )}
{...restProps} {...restProps}
> >
{@render children?.()} {@render children?.()}
<ChevronDown class="size-4 opacity-50" /> <ChevronDownIcon class="size-4 opacity-50" />
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
@@ -0,0 +1,11 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
let {
open = $bindable(false),
value = $bindable(),
...restProps
}: SelectPrimitive.RootProps = $props();
</script>
<SelectPrimitive.Root bind:open bind:value={value as never} {...restProps} />
@@ -5,18 +5,17 @@
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
orientation = "horizontal", "data-slot": dataSlot = "separator",
...restProps ...restProps
}: SeparatorPrimitive.RootProps = $props(); }: SeparatorPrimitive.RootProps = $props();
</script> </script>
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
bind:ref bind:ref
data-slot={dataSlot}
class={cn( class={cn(
"bg-border shrink-0", "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:min-h-full data-[orientation=vertical]:w-px",
orientation === "horizontal" ? "h-[1px] w-full" : "min-h-full w-[1px]",
className className
)} )}
{orientation}
{...restProps} {...restProps}
/> />
@@ -1,15 +1,14 @@
<script lang="ts"> <script lang="ts">
import { Slider as SliderPrimitive, type WithoutChildrenOrChild } from 'bits-ui' import { Slider as SliderPrimitive } from "bits-ui";
import { cn } from '$lib/utils.js' import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
value = $bindable(), value = $bindable(),
orientation = 'horizontal', orientation = "horizontal",
class: className, class: className,
'aria-labelledby': ariaLabelledby, ...restProps
...restProps }: WithoutChildrenOrChild<SliderPrimitive.RootProps> = $props();
}: WithoutChildrenOrChild<SliderPrimitive.RootProps> = $props()
</script> </script>
<!-- <!--
@@ -17,30 +16,37 @@ Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`. get along, so we shut typescript up by casting `value` to `never`.
--> -->
<SliderPrimitive.Root <SliderPrimitive.Root
bind:ref bind:ref
bind:value={value as never} bind:value={value as never}
{orientation} data-slot="slider"
class={cn( {orientation}
"relative flex touch-none select-none items-center data-[orientation='vertical']:h-full data-[orientation='vertical']:min-h-44 data-[orientation='horizontal']:w-full data-[orientation='vertical']:w-auto data-[orientation='vertical']:flex-col", class={cn(
className, "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
)} className
{...restProps} )}
{...restProps}
> >
{#snippet children({ thumbs })} {#snippet children({ thumbs })}
<span <span
data-orientation={orientation} data-orientation={orientation}
class="bg-secondary relative grow overflow-hidden rounded-full data-[orientation='horizontal']:h-2 data-[orientation='vertical']:h-full data-[orientation='horizontal']:w-full data-[orientation='vertical']:w-2 hover:cursor-pointer" data-slot="slider-track"
> class={cn(
<SliderPrimitive.Range "bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
class="bg-primary absolute data-[orientation='horizontal']:h-full data-[orientation='vertical']:w-full" )}
/> >
</span> <SliderPrimitive.Range
{#each thumbs as thumb (thumb)} data-slot="slider-range"
<SliderPrimitive.Thumb class={cn(
index={thumb} "bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
class="border-primary bg-background ring-offset-background focus-visible:ring-ring block size-5 rounded-full border-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:cursor-pointer" )}
aria-labelledby={ariaLabelledby} />
/> </span>
{/each} {#each thumbs as thumb (thumb)}
{/snippet} <SliderPrimitive.Thumb
data-slot="slider-thumb"
index={thumb}
class="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
{/each}
{/snippet}
</SliderPrimitive.Root> </SliderPrimitive.Root>
@@ -1,4 +1,10 @@
<script lang="ts"> <script lang="ts">
import CircleCheckIcon from "@lucide/svelte/icons/circle-check";
import InfoIcon from "@lucide/svelte/icons/info";
import Loader2Icon from "@lucide/svelte/icons/loader-2";
import OctagonXIcon from "@lucide/svelte/icons/octagon-x";
import TriangleAlertIcon from "@lucide/svelte/icons/triangle-alert";
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner"; import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
import { mode } from "mode-watcher"; import { mode } from "mode-watcher";
@@ -6,15 +12,23 @@
</script> </script>
<Sonner <Sonner
theme={$mode} theme={mode.current}
class="toaster group" class="toaster group"
toastOptions={{ style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
classes: {
toast: "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...restProps} {...restProps}
/> >{#snippet loadingIcon()}
<Loader2Icon class="size-4 animate-spin" />
{/snippet}
{#snippet successIcon()}
<CircleCheckIcon class="size-4" />
{/snippet}
{#snippet errorIcon()}
<OctagonXIcon class="size-4" />
{/snippet}
{#snippet infoIcon()}
<InfoIcon class="size-4" />
{/snippet}
{#snippet warningIcon()}
<TriangleAlertIcon class="size-4" />
{/snippet}
</Sonner>
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Switch as SwitchPrimitive, type WithoutChildrenOrChild } from "bits-ui"; import { Switch as SwitchPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
@@ -13,15 +13,17 @@
<SwitchPrimitive.Root <SwitchPrimitive.Root
bind:ref bind:ref
bind:checked bind:checked
data-slot="switch"
class={cn( class={cn(
"focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", "data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 peer inline-flex h-6 w-11 shrink-0 items-center rounded-full border-2 border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
{...restProps} {...restProps}
> >
<SwitchPrimitive.Thumb <SwitchPrimitive.Thumb
data-slot="switch-thumb"
class={cn( class={cn(
"bg-background pointer-events-none block size-5 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0" "bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-5 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)} )}
/> />
</SwitchPrimitive.Root> </SwitchPrimitive.Root>
@@ -18,11 +18,16 @@
<ToggleGroupPrimitive.Item <ToggleGroupPrimitive.Item
bind:ref bind:ref
data-slot="toggle-group-item"
data-variant={ctx.variant || variant}
data-size={ctx.size || size}
data-spacing={ctx.spacing}
class={cn( class={cn(
toggleVariants({ toggleVariants({
variant: ctx.variant || variant, variant: ctx.variant || variant,
size: ctx.size || size, size: ctx.size || size,
}), }),
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10 data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
className className
)} )}
{value} {value}
@@ -1,12 +1,20 @@
<script lang="ts" module> <script lang="ts" module>
import { getContext, setContext } from "svelte"; import { getContext, setContext } from "svelte";
import type { ToggleVariants } from "$lib/components/ui/toggle/index.js"; import type { VariantProps } from "tailwind-variants";
export function setToggleGroupCtx(props: ToggleVariants) { import { toggleVariants } from "$lib/components/ui/toggle/index.js";
type ToggleVariants = VariantProps<typeof toggleVariants>;
interface ToggleGroupContext extends ToggleVariants {
spacing?: number;
}
export function setToggleGroupCtx(props: ToggleGroupContext) {
setContext("toggleGroup", props); setContext("toggleGroup", props);
} }
export function getToggleGroupCtx() { export function getToggleGroupCtx() {
return getContext<ToggleVariants>("toggleGroup"); return getContext<Required<ToggleGroupContext>>("toggleGroup");
} }
</script> </script>
@@ -19,13 +27,15 @@
value = $bindable(), value = $bindable(),
class: className, class: className,
size = "default", size = "default",
spacing = 0,
variant = "default", variant = "default",
...restProps ...restProps
}: ToggleGroupPrimitive.RootProps & ToggleVariants = $props(); }: ToggleGroupPrimitive.RootProps & ToggleVariants & { spacing?: number } = $props();
setToggleGroupCtx({ setToggleGroupCtx({
variant, variant,
size, size,
spacing,
}); });
</script> </script>
@@ -36,6 +46,14 @@ get along, so we shut typescript up by casting `value` to `never`.
<ToggleGroupPrimitive.Root <ToggleGroupPrimitive.Root
bind:value={value as never} bind:value={value as never}
bind:ref bind:ref
class={cn("flex items-center justify-center gap-1", className)} data-slot="toggle-group"
data-variant={variant}
data-size={size}
data-spacing={spacing}
style={`--gap: ${spacing}`}
class={cn(
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
className
)}
{...restProps} {...restProps}
/> />
@@ -2,17 +2,17 @@
import { type VariantProps, tv } from "tailwind-variants"; import { type VariantProps, tv } from "tailwind-variants";
export const toggleVariants = tv({ export const toggleVariants = tv({
base: "ring-offset-background hover:bg-muted hover:text-muted-foreground focus-visible:ring-ring data-[state=on]:bg-accent data-[state=on]:text-accent-foreground inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", base: "hover:bg-muted hover:text-muted-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground 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 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variants: { variants: {
variant: { variant: {
default: "bg-transparent", default: "bg-transparent",
outline: outline:
"border-input hover:bg-accent hover:text-accent-foreground border bg-transparent", "border-input hover:bg-accent hover:text-accent-foreground border bg-transparent shadow-xs",
}, },
size: { size: {
default: "h-10 min-w-10 px-3", default: "h-9 min-w-9 px-2",
sm: "h-9 min-w-9 px-2.5", sm: "h-8 min-w-8 px-1.5",
lg: "h-11 min-w-11 px-5", lg: "h-10 min-w-10 px-2.5",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -46,6 +46,7 @@
<TogglePrimitive.Root <TogglePrimitive.Root
bind:ref bind:ref
bind:pressed bind:pressed
data-slot="toggle"
class={cn(toggleVariants({ variant, size }), className)} class={cn(toggleVariants({ variant, size }), className)}
{...restProps} {...restProps}
/> />
@@ -1,18 +1,19 @@
import { Tooltip as TooltipPrimitive } from "bits-ui"; import Root from "./tooltip.svelte";
import Trigger from "./tooltip-trigger.svelte";
import Content from "./tooltip-content.svelte"; import Content from "./tooltip-content.svelte";
import Provider from "./tooltip-provider.svelte";
const Root = TooltipPrimitive.Root; import Portal from "./tooltip-portal.svelte";
const Trigger = TooltipPrimitive.Trigger;
const Provider = TooltipPrimitive.Provider;
export { export {
Root, Root,
Trigger, Trigger,
Content, Content,
Provider, Provider,
Portal,
// //
Root as Tooltip, Root as Tooltip,
Content as TooltipContent, Content as TooltipContent,
Trigger as TooltipTrigger, Trigger as TooltipTrigger,
Provider as TooltipProvider, Provider as TooltipProvider,
Portal as TooltipPortal,
}; };
@@ -1,21 +1,52 @@
<script lang="ts"> <script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui"; import { Tooltip as TooltipPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js"; import { cn } from "$lib/utils.js";
import TooltipPortal from "./tooltip-portal.svelte";
import type { ComponentProps } from "svelte";
import type { WithoutChildrenOrChild } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
sideOffset = 4, sideOffset = 0,
side = "top",
children,
arrowClasses,
portalProps,
...restProps ...restProps
}: TooltipPrimitive.ContentProps = $props(); }: TooltipPrimitive.ContentProps & {
arrowClasses?: string;
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof TooltipPortal>>;
} = $props();
</script> </script>
<TooltipPrimitive.Content <TooltipPortal {...portalProps}>
bind:ref <TooltipPrimitive.Content
{sideOffset} bind:ref
class={cn( data-slot="tooltip-content"
"bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md", {sideOffset}
className {side}
)} class={cn(
{...restProps} "bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 w-fit origin-(--bits-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
/> className
)}
{...restProps}
>
{@render children?.()}
<TooltipPrimitive.Arrow>
{#snippet child({ props })}
<div
class={cn(
"bg-foreground z-50 size-2.5 rotate-45 rounded-[2px]",
"data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%_+_2px)]",
"data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%_+_1px)]",
"data-[side=right]:translate-x-[calc(50%_+_2px)] data-[side=right]:translate-y-1/2",
"data-[side=left]:-translate-y-[calc(50%_-_3px)]",
arrowClasses
)}
{...props}
></div>
{/snippet}
</TooltipPrimitive.Arrow>
</TooltipPrimitive.Content>
</TooltipPortal>
@@ -0,0 +1,7 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
let { ...restProps }: TooltipPrimitive.PortalProps = $props();
</script>
<TooltipPrimitive.Portal {...restProps} />
@@ -0,0 +1,7 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
let { ...restProps }: TooltipPrimitive.ProviderProps = $props();
</script>
<TooltipPrimitive.Provider {...restProps} />
@@ -0,0 +1,7 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: TooltipPrimitive.TriggerProps = $props();
</script>
<TooltipPrimitive.Trigger bind:ref data-slot="tooltip-trigger" {...restProps} />
@@ -0,0 +1,7 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from "bits-ui";
let { open = $bindable(false), ...restProps }: TooltipPrimitive.RootProps = $props();
</script>
<TooltipPrimitive.Root bind:open {...restProps} />
+5 -5
View File
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button' import { Button } from '$lib/components/ui/button'
import IconLightMode from '~icons/material-symbols/light-mode' import IconLightMode from '~icons/material-symbols/light-mode'
import IconDarkMode from '~icons/material-symbols/dark-mode' import IconDarkMode from '~icons/material-symbols/dark-mode'
import { mode, toggleMode } from 'mode-watcher' import { mode, toggleMode } from 'mode-watcher'
</script> </script>
<footer class="row text-sm text-muted-foreground"> <footer class="row text-sm text-muted-foreground">
@@ -12,7 +12,7 @@
<a class="no-underline hover:underline" href="https://dayli.ly">daylily</a> <a class="no-underline hover:underline" href="https://dayli.ly">daylily</a>
</div> </div>
<Button size="icon" variant="ghost" onclick={toggleMode} aria-label="Toggle color scheme"> <Button size="icon" variant="ghost" onclick={toggleMode} aria-label="Toggle color scheme">
{#if $mode === 'dark'} {#if mode.current === 'dark'}
<IconDarkMode aria-hidden /> <IconDarkMode aria-hidden />
{:else} {:else}
<IconLightMode aria-hidden /> <IconLightMode aria-hidden />
@@ -1,35 +1,35 @@
<script lang="ts"> <script lang="ts">
import { Transform } from '$lib/image/transform' import { Transform } from '$lib/image/transform'
import { Button } from '$lib/components/ui/button' import { Button } from '$lib/components/ui/button'
import Separator from '$lib/components/ui/separator/separator.svelte' import Separator from '$lib/components/ui/separator/separator.svelte'
import IconEditOff from '~icons/material-symbols/edit-off' import IconEditOff from '~icons/material-symbols/edit-off'
import AspectRatioAlert from './dimensions/AspectRatioAlert.svelte' import AspectRatioAlert from './dimensions/AspectRatioAlert.svelte'
import ScaleModeToggleGroup from './dimensions/ScaleModeToggleGroup.svelte' import ScaleModeToggleGroup from './dimensions/ScaleModeToggleGroup.svelte'
import TransformControls from './dimensions/TransformControls.svelte' import TransformControls from './dimensions/TransformControls.svelte'
import BackgroundColorSlider from './BackgroundColorSlider.svelte' import BackgroundColorSlider from './BackgroundColorSlider.svelte'
import DitherControls from './conversion/dither/DitherControls.svelte' import DitherControls from './conversion/dither/DitherControls.svelte'
import ContrastSlider from './conversion/ContrastSlider.svelte' import ContrastSlider from './conversion/ContrastSlider.svelte'
import BrightnessSlider from './conversion/BrightnessSlider.svelte' import BrightnessSlider from './conversion/BrightnessSlider.svelte'
import { getConversionConfig } from '$lib/contexts/config.svelte' import { getConversionConfig } from '$lib/contexts/config.svelte'
import { getImageContext, imageIsCorrectRatio } from '$lib/contexts/image.svelte' import { getImageContext, imageIsCorrectRatio } from '$lib/contexts/image.svelte'
import { DEFAULT_DITHERING_KERNEL } from '$lib/image/quantizer' import { DEFAULT_DITHERING_KERNEL } from '$lib/image/quantizer'
const imageCtx = getImageContext() const imageCtx = getImageContext()
const config = getConversionConfig() const config = getConversionConfig()
const transformDisabled = $derived(imageCtx.image === null) const transformDisabled = $derived(imageCtx.image === null)
const imageNonSquare = $derived(!imageIsCorrectRatio(imageCtx.image)) const imageNonSquare = $derived(!imageIsCorrectRatio(imageCtx.image))
function restoreDefaultImageSettings() { function restoreDefaultImageSettings() {
config.scaleMode = 'fit' config.scaleMode = 'fit'
config.transform = new Transform() config.transform = new Transform()
config.backgroundColor = 0xff config.backgroundColor = 0xff
config.ditheringKernel = DEFAULT_DITHERING_KERNEL config.ditheringKernel = DEFAULT_DITHERING_KERNEL
config.contrast = 0 config.contrast = 0
config.brightness = 0 config.brightness = 0
} }
</script> </script>
<section class="grow stack gap-4" aria-labelledby="controls-section-label"> <section class="grow stack gap-4" aria-labelledby="controls-section-label">
@@ -39,7 +39,7 @@
<AspectRatioAlert /> <AspectRatioAlert />
{/if} {/if}
<div class="row gap-4"> <div class="stack 2xs:row gap-4">
{#if imageNonSquare} {#if imageNonSquare}
<ScaleModeToggleGroup /> <ScaleModeToggleGroup />
{/if} {/if}
@@ -1,31 +1,31 @@
<script lang="ts"> <script lang="ts">
import { type DitheringKernel } from '$lib/image/quantizer' import { type DitheringKernel } from '$lib/image/quantizer'
import { Label } from '$lib/components/ui/label' import { Label } from '$lib/components/ui/label'
import * as Select from '$lib/components/ui/select' import * as Select from '$lib/components/ui/select'
import MoreInfo from '$lib/components/MoreInfo.svelte' import MoreInfo from '$lib/components/MoreInfo.svelte'
import { cn } from '$lib/utils' import { cn } from '$lib/utils'
interface Props { interface Props {
hidden: boolean hidden: boolean
value: DitheringKernel value: DitheringKernel
onchange: (v: DitheringKernel) => void onchange: (v: DitheringKernel) => void
} }
const { hidden, value, onchange }: Props = $props() const { hidden, value, onchange }: Props = $props()
const ditheringKernels: Record<DitheringKernel, string> = { const ditheringKernels: Record<DitheringKernel, string> = {
FloydSteinberg: 'Floyd-Steinberg', FloydSteinberg: 'Floyd-Steinberg',
FalseFloydSteinberg: 'False Floyd-Steinberg', FalseFloydSteinberg: 'False Floyd-Steinberg',
Stucki: 'Stucki', Stucki: 'Stucki',
Atkinson: 'Atkinson', Atkinson: 'Atkinson',
Jarvis: 'Jarvis', Jarvis: 'Jarvis',
Burkes: 'Burkes', Burkes: 'Burkes',
Sierra: 'Sierra', Sierra: 'Sierra',
TwoSierra: '2-Row Sierra', TwoSierra: '2-Row Sierra',
SierraLite: 'Sierra Lite', SierraLite: 'Sierra Lite',
} }
</script> </script>
<div <div
@@ -43,7 +43,9 @@
</div> </div>
<Select.Root type="single" {value} onValueChange={v => onchange(v as DitheringKernel)}> <Select.Root type="single" {value} onValueChange={v => onchange(v as DitheringKernel)}>
<Select.Trigger aria-labelledby="dithering-kernel-select-label">{ditheringKernels[value]}</Select.Trigger> <Select.Trigger class="w-full" aria-labelledby="dithering-kernel-select-label">
{ditheringKernels[value]}
</Select.Trigger>
<Select.Content> <Select.Content>
{#each Object.entries(ditheringKernels) as [kernel, name]} {#each Object.entries(ditheringKernels) as [kernel, name]}
<Select.Item value={kernel}>{name}</Select.Item> <Select.Item value={kernel}>{name}</Select.Item>
@@ -1,28 +1,28 @@
<script lang="ts"> <script lang="ts">
import type { ScaleMode } from '$lib/image/scaler' import type { ScaleMode } from '$lib/image/scaler'
import { Label } from '$lib/components/ui/label' import { Label } from '$lib/components/ui/label'
import * as ToggleGroup from '$lib/components/ui/toggle-group' import * as ToggleGroup from '$lib/components/ui/toggle-group'
import * as Tooltip from '$lib/components/ui/tooltip' import * as Tooltip from '$lib/components/ui/tooltip'
import { getConversionConfig } from '$lib/contexts/config.svelte' import { getConversionConfig } from '$lib/contexts/config.svelte'
const config = getConversionConfig() const config = getConversionConfig()
const scaleModes: Record<ScaleMode, { name: string; description: string }> = { const scaleModes: Record<ScaleMode, { name: string; description: string }> = {
fit: { fit: {
name: 'Fit', name: 'Fit',
description: 'Fit the entire image onto the display. May introduce letterboxing.', description: 'Fit the entire image onto the display. May introduce letterboxing.',
}, },
crop: { crop: {
name: 'Crop', name: 'Crop',
description: 'Fill the display and crop out-of-frame parts of the image.', description: 'Fill the display and crop out-of-frame parts of the image.',
}, },
distort: { distort: {
name: 'Distort', name: 'Distort',
description: 'Stretch the image to fill the display. Distorts the aspect ratio.', description: 'Stretch the image to fill the display. Distorts the aspect ratio.',
}, },
} }
</script> </script>
<div class="stack gap-2"> <div class="stack gap-2">
@@ -30,6 +30,7 @@
<ToggleGroup.Root <ToggleGroup.Root
type="single" type="single"
spacing={1}
bind:value={ bind:value={
() => config.scaleMode, () => config.scaleMode,
v => { v => {
@@ -28,7 +28,7 @@ $effect(() => {
<div class="bg-[#ccc] shadow-md rounded-lg p-2 w-full aspect-square relative" role="group"> <div class="bg-[#ccc] shadow-md rounded-lg p-2 w-full aspect-square relative" role="group">
<div <div
class="shadow-sm shadow-[inset#888] p-1 size-full" class="inset-shadow-sm inset-shadow-stone-400 p-1 size-full"
role="img" role="img"
aria-label={makeAltText(filesCtx, imageCtx, config)} aria-label={makeAltText(filesCtx, imageCtx, config)}
> >
@@ -1,15 +1,29 @@
<script lang="ts"> <script lang="ts">
import { Input } from '$lib/components/ui/input' import { Input } from '$lib/components/ui/input'
import { getFilesContext } from '$lib/contexts/files.svelte' import { getFilesContext } from '$lib/contexts/files.svelte'
interface Props { interface Props {
ref?: HTMLInputElement | null ref?: HTMLInputElement | null
} }
let { ref = $bindable(null) }: Props = $props() let { ref = $bindable(null) }: Props = $props()
const filesCtx = getFilesContext() const filesCtx = getFilesContext()
</script> </script>
<Input bind:ref type="file" accept="image/*" bind:files={filesCtx.files} aria-labelledby="preview-section-label" /> <Input
bind:ref
type="file"
accept="image/*"
bind:files={filesCtx.files}
class="font-normal file-input"
aria-labelledby="preview-section-label"
/>
<style>
:global(.file-input::file-selector-button) {
font-weight: 500;
padding-right: 0.25rem;
}
</style>
@@ -1,40 +1,49 @@
<script lang="ts"> <script lang="ts">
import { Label } from '$lib/components/ui/label' import { Label } from '$lib/components/ui/label'
import IconHideImage from '~icons/material-symbols/hide-image' import IconHideImage from '~icons/material-symbols/hide-image'
import { cn } from '$lib/utils' import { cn } from '$lib/utils'
import { drawQuantizedData, freshContext, makeAltText } from './common.svelte' import { drawQuantizedData, freshContext, makeAltText } from './common.svelte'
import { getRenderedContext } from '$lib/contexts/rendered.svelte' import { getRenderedContext } from '$lib/contexts/rendered.svelte'
import { DEVICE_HEIGHT, DEVICE_WIDTH } from '$lib/constants' import { DEVICE_HEIGHT, DEVICE_WIDTH } from '$lib/constants'
import { getFilesContext } from '$lib/contexts/files.svelte' import { getFilesContext } from '$lib/contexts/files.svelte'
import { getConversionConfig } from '$lib/contexts/config.svelte' import { getConversionConfig } from '$lib/contexts/config.svelte'
import { getImageContext } from '$lib/contexts/image.svelte' import { getImageContext } from '$lib/contexts/image.svelte'
const filesCtx = getFilesContext() const filesCtx = getFilesContext()
const imageCtx = getImageContext() const imageCtx = getImageContext()
const config = getConversionConfig() const config = getConversionConfig()
const renderedCtx = getRenderedContext() const renderedCtx = getRenderedContext()
const hasRendered = $derived(renderedCtx.rendered !== null) const hasRendered = $derived(renderedCtx.rendered !== null)
interface Props { interface Props {
scale: number scale: number
} }
const { scale }: Props = $props() const { scale }: Props = $props()
let canvasEl: HTMLCanvasElement = $state(undefined!) let canvasEl: HTMLCanvasElement = $state(undefined!)
const ctx = $derived(freshContext(canvasEl)) const ctx = $derived(freshContext(canvasEl))
$effect(() => { $effect(() => {
if (renderedCtx.rendered === null) return if (renderedCtx.rendered === null) return
drawQuantizedData(ctx, renderedCtx.rendered) drawQuantizedData(ctx, renderedCtx.rendered)
}) })
</script> </script>
<div> <div>
<div class="bg-[#ccc] shadow-md rounded-lg p-2 w-fit relative" role="group" aria-labelledby="preview-{scale}x-label"> <div
<div class="shadow-sm shadow-[inset#888]" style:padding="{scale * 3}px" role="img" aria-label={makeAltText(filesCtx, imageCtx, config)}> class="bg-[#ccc] shadow-md rounded-lg p-2 w-fit relative mb-1"
role="group"
aria-labelledby="preview-{scale}x-label"
>
<div
class="inset-shadow-sm inset-shadow-stone-400"
style:padding="{scale * 3}px"
role="img"
aria-label={makeAltText(filesCtx, imageCtx, config)}
>
<div class="aspect-square" style:width="{DEVICE_WIDTH * scale}px"> <div class="aspect-square" style:width="{DEVICE_WIDTH * scale}px">
{#if hasRendered} {#if hasRendered}
<canvas <canvas
+8 -1
View File
@@ -1,10 +1,17 @@
import { type ClassValue, clsx } from 'clsx' import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null }
export function showNumber(n: number): string { export function showNumber(n: number): string {
return String(n).replace('-', '').replace('Infinity', '∞').replace('NaN', '⁇') return String(n).replace('-', '').replace('Infinity', '∞').replace('NaN', '⁇')
} }
-96
View File
@@ -1,96 +0,0 @@
import { fontFamily } from 'tailwindcss/defaultTheme'
import type { Config } from 'tailwindcss'
import tailwindcssAnimate from 'tailwindcss-animate'
const config: Config = {
darkMode: ['class'],
content: ['./src/**/*.{html,js,svelte,ts}'],
safelist: ['dark'],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
colors: {
border: 'hsl(var(--border) / <alpha-value>)',
input: 'hsl(var(--input) / <alpha-value>)',
ring: 'hsl(var(--ring) / <alpha-value>)',
background: 'hsl(var(--background) / <alpha-value>)',
foreground: 'hsl(var(--foreground) / <alpha-value>)',
primary: {
DEFAULT: 'hsl(var(--primary) / <alpha-value>)',
foreground: 'hsl(var(--primary-foreground) / <alpha-value>)',
},
secondary: {
DEFAULT: 'hsl(var(--secondary) / <alpha-value>)',
foreground: 'hsl(var(--secondary-foreground) / <alpha-value>)',
},
destructive: {
DEFAULT: 'hsl(var(--destructive) / <alpha-value>)',
foreground: 'hsl(var(--destructive-foreground) / <alpha-value>)',
},
muted: {
DEFAULT: 'hsl(var(--muted) / <alpha-value>)',
foreground: 'hsl(var(--muted-foreground) / <alpha-value>)',
},
accent: {
DEFAULT: 'hsl(var(--accent) / <alpha-value>)',
foreground: 'hsl(var(--accent-foreground) / <alpha-value>)',
},
popover: {
DEFAULT: 'hsl(var(--popover) / <alpha-value>)',
foreground: 'hsl(var(--popover-foreground) / <alpha-value>)',
},
card: {
DEFAULT: 'hsl(var(--card) / <alpha-value>)',
foreground: 'hsl(var(--card-foreground) / <alpha-value>)',
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))',
},
},
borderRadius: {
xl: 'calc(var(--radius) + 4px)',
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
fontFamily: {
sans: [...fontFamily.sans],
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--bits-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--bits-accordion-content-height)' },
to: { height: '0' },
},
'caret-blink': {
'0%,70%,100%': { opacity: '1' },
'20%,50%': { opacity: '0' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
'caret-blink': 'caret-blink 1.25s ease-out infinite',
},
},
},
plugins: [tailwindcssAnimate],
}
export default config
+2
View File
@@ -3,10 +3,12 @@ import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte' import { svelte } from '@sveltejs/vite-plugin-svelte'
import { resolve } from 'path' import { resolve } from 'path'
import icons from 'unplugin-icons/vite' import icons from 'unplugin-icons/vite'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
tailwindcss(),
svelte(), svelte(),
icons({ icons({
compiler: 'svelte', compiler: 'svelte',