feat(ui,infra): SvelteKit ui scaffold, Makefile, pre-commit, claude hooks
ui/: SvelteKit 2 + Svelte 5 + Tailwind 4 + shadcn-svelte (luma, lucide) on bun with ultracite; adapter-static with SPA fallback; theme tokens taken from msos. Dockerfile gains a bun stage that builds ui/build. Makefile and pre-commit cover ruff, ty, ultracite and svelte-check; .claude settings run the matching fixer after edits. docs/PRODUCT.md captures product truth for the UI work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Vendored
+13
@@ -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 {};
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<meta content="scale" name="text-scale" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
height="128"
|
||||
viewBox="0 0 107 128"
|
||||
width="107"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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.6 KiB |
@@ -0,0 +1,90 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type {
|
||||
HTMLAnchorAttributes,
|
||||
HTMLButtonAttributes,
|
||||
} from "svelte/elements";
|
||||
|
||||
export const buttonVariants = tv({
|
||||
base: "rounded-4xl border border-transparent bg-clip-padding text-sm font-medium focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-transparent dark:hover:bg-input/30",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5",
|
||||
xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
lg: "h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
||||
icon: "size-9",
|
||||
"icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3",
|
||||
"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}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: default
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||
// biome-ignore lint/suspicious/noExplicitAny: default
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import "./layout.css";
|
||||
import favicon from "$lib/assets/favicon.svg";
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head><link href={favicon} rel="icon"></svelte:head>
|
||||
{@render children()}
|
||||
@@ -0,0 +1,2 @@
|
||||
export const ssr = false;
|
||||
export const prerender = false;
|
||||
@@ -0,0 +1,5 @@
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>
|
||||
Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read
|
||||
the documentation
|
||||
</p>
|
||||
@@ -0,0 +1,202 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn-svelte/tailwind.css";
|
||||
@import "@fontsource-variable/inter";
|
||||
|
||||
@plugin "@tailwindcss/forms";
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(0.99 0.004 340);
|
||||
--foreground: oklch(0.24 0.03 340);
|
||||
--card: oklch(0.985 0.006 340);
|
||||
--card-foreground: oklch(0.24 0.03 340);
|
||||
--popover: oklch(0.985 0.006 340);
|
||||
--popover-foreground: oklch(0.24 0.03 340);
|
||||
--primary: oklch(0.47 0.13 340);
|
||||
--primary-foreground: oklch(0.99 0.005 340);
|
||||
--secondary: oklch(0.96 0.012 340);
|
||||
--secondary-foreground: oklch(0.24 0.03 340);
|
||||
--muted: oklch(0.96 0.012 340);
|
||||
--muted-foreground: oklch(0.5 0.04 340);
|
||||
--accent: oklch(0.96 0.012 340);
|
||||
--accent-foreground: oklch(0.24 0.03 340);
|
||||
--destructive: oklch(0.58 0.22 25);
|
||||
--destructive-foreground: oklch(0.99 0 0);
|
||||
--border: oklch(0.9 0.02 340);
|
||||
--input: oklch(0.9 0.02 340);
|
||||
--ring: oklch(0.55 0.2 342);
|
||||
--icon: oklch(0.55 0.03 340);
|
||||
--signal: oklch(0.48 0.21 342);
|
||||
--note: oklch(0.52 0.13 78);
|
||||
|
||||
--status-new: oklch(0.55 0.22 342);
|
||||
--status-done: oklch(0.55 0.14 155);
|
||||
--status-skip: oklch(0.5 0.04 340);
|
||||
--status-reply: oklch(0.55 0.14 235);
|
||||
--status-snooze: oklch(0.66 0.13 75);
|
||||
--status-meeting: oklch(0.5 0.2 300);
|
||||
--status-work: oklch(0.55 0.13 195);
|
||||
|
||||
--radius: 0.45rem;
|
||||
--sidebar: oklch(0.975 0.01 340);
|
||||
--sidebar-foreground: oklch(0.24 0.03 340);
|
||||
--sidebar-primary: oklch(0.47 0.13 340);
|
||||
--sidebar-primary-foreground: oklch(0.99 0.005 340);
|
||||
--sidebar-accent: oklch(0.94 0.016 340);
|
||||
--sidebar-accent-foreground: oklch(0.24 0.03 340);
|
||||
--sidebar-border: oklch(0.9 0.02 340);
|
||||
--sidebar-ring: oklch(0.55 0.2 342);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #22111e;
|
||||
--foreground: #f5f5f5;
|
||||
--card: #341d2f;
|
||||
--card-foreground: #f5f5f5;
|
||||
--popover: #341d2f;
|
||||
--popover-foreground: #f5f5f5;
|
||||
--primary: #7c3871;
|
||||
--primary-foreground: #f5f5f5;
|
||||
--secondary: #341d2f;
|
||||
--secondary-foreground: #f5f5f5;
|
||||
--muted: #40283a;
|
||||
--muted-foreground: #a989a3;
|
||||
--accent: #40283a;
|
||||
--accent-foreground: #f5f5f5;
|
||||
--destructive: oklch(0.62 0.2 25);
|
||||
--destructive-foreground: #f5f5f5;
|
||||
--border: oklch(0.92 0.04 340 / 10%);
|
||||
--input: oklch(0.92 0.04 340 / 14%);
|
||||
--ring: #ff82f3;
|
||||
--icon: #877384;
|
||||
--signal: #ff82f3;
|
||||
--note: oklch(0.84 0.14 88);
|
||||
|
||||
--status-new: #ff82f3;
|
||||
--status-done: oklch(0.74 0.14 155);
|
||||
--status-skip: #a989a3;
|
||||
--status-reply: oklch(0.72 0.13 235);
|
||||
--status-snooze: oklch(0.8 0.13 75);
|
||||
--status-meeting: oklch(0.72 0.16 300);
|
||||
--status-work: oklch(0.75 0.12 195);
|
||||
|
||||
--sidebar: #2c1b29;
|
||||
--sidebar-foreground: #f5f5f5;
|
||||
--sidebar-primary: #ff82f3;
|
||||
--sidebar-primary-foreground: #22111e;
|
||||
--sidebar-accent: #341d2f;
|
||||
--sidebar-accent-foreground: #f5f5f5;
|
||||
--sidebar-border: oklch(0.92 0.04 340 / 10%);
|
||||
--sidebar-ring: #ff82f3;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: "Inter Variable", sans-serif;
|
||||
--font-heading: "Inter Variable", sans-serif;
|
||||
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.8125rem;
|
||||
--text-base: 0.875rem;
|
||||
--text-lg: 1rem;
|
||||
--text-xl: 1.25rem;
|
||||
--text-2xl: 1.5rem;
|
||||
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
|
||||
--color-icon: var(--icon);
|
||||
--color-signal: var(--signal);
|
||||
--color-note: var(--note);
|
||||
|
||||
--color-status-new: var(--status-new);
|
||||
--color-status-done: var(--status-done);
|
||||
--color-status-skip: var(--status-skip);
|
||||
--color-status-reply: var(--status-reply);
|
||||
--color-status-snooze: var(--status-snooze);
|
||||
--color-status-meeting: var(--status-meeting);
|
||||
--color-status-work: var(--status-work);
|
||||
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv11", "ss01";
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
::selection {
|
||||
background: color-mix(in oklab, var(--primary) 28%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@utility touch-shown {
|
||||
@media (hover: none) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@utility tabular {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@utility hue-chip {
|
||||
color: oklch(0.45 0.13 var(--hue));
|
||||
background: oklch(0.6 0.09 var(--hue) / 0.12);
|
||||
border-color: oklch(0.6 0.09 var(--hue) / 0.4);
|
||||
.dark & {
|
||||
color: oklch(0.82 0.08 var(--hue));
|
||||
}
|
||||
}
|
||||
|
||||
@utility scrollbar-none {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user