From d33039f49719b54f555a62e335d9c799a0691869 Mon Sep 17 00:00:00 2001 From: h Date: Thu, 27 Aug 2026 23:22:18 +0200 Subject: [PATCH] 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. --- .claude/CLAUDE.md | 22 + .claude/rules/svelte.md | 23 + .claude/rules/ultracite.md | 123 +++++ .claude/settings.json | 15 + .dockerignore | 7 + .mcp.json | 8 + .pre-commit-config.yaml | 28 +- Dockerfile | 9 + Makefile | 31 ++ ui/.gitignore | 23 + ui/.npmrc | 1 + ui/.vscode/extensions.json | 3 + ui/.vscode/settings.json | 57 ++ ui/README.md | 42 ++ ui/biome.jsonc | 55 ++ ui/bun.lock | 522 ++++++++++++++++++ ui/components.json | 20 + ui/package.json | 42 ++ ui/src/app.d.ts | 13 + ui/src/app.html | 12 + ui/src/lib/assets/favicon.svg | 16 + ui/src/lib/components/ui/button/button.svelte | 90 +++ ui/src/lib/components/ui/button/index.ts | 17 + ui/src/lib/utils.ts | 17 + ui/src/routes/+layout.svelte | 9 + ui/src/routes/+layout.ts | 2 + ui/src/routes/+page.svelte | 5 + ui/src/routes/layout.css | 202 +++++++ ui/static/robots.txt | 3 + ui/tsconfig.json | 20 + ui/vite.config.ts | 19 + 31 files changed, 1449 insertions(+), 7 deletions(-) create mode 100644 .claude/CLAUDE.md create mode 100644 .claude/rules/svelte.md create mode 100644 .claude/rules/ultracite.md create mode 100644 .claude/settings.json create mode 100644 .mcp.json create mode 100644 Makefile create mode 100644 ui/.gitignore create mode 100644 ui/.npmrc create mode 100644 ui/.vscode/extensions.json create mode 100644 ui/.vscode/settings.json create mode 100644 ui/README.md create mode 100644 ui/biome.jsonc create mode 100644 ui/bun.lock create mode 100644 ui/components.json create mode 100644 ui/package.json create mode 100644 ui/src/app.d.ts create mode 100644 ui/src/app.html create mode 100644 ui/src/lib/assets/favicon.svg create mode 100644 ui/src/lib/components/ui/button/button.svelte create mode 100644 ui/src/lib/components/ui/button/index.ts create mode 100644 ui/src/lib/utils.ts create mode 100644 ui/src/routes/+layout.svelte create mode 100644 ui/src/routes/+layout.ts create mode 100644 ui/src/routes/+page.svelte create mode 100644 ui/src/routes/layout.css create mode 100644 ui/static/robots.txt create mode 100644 ui/tsconfig.json create mode 100644 ui/vite.config.ts diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..4989437 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,22 @@ +## Code Principles +- **Simplicity**: Write simple, straightforward code +- **Readability**: Make code easy to understand +- **Maintainability**: Write code that's easy to update +- **Less Code = Less Debt**: Minimize code footprint +- **NEVER write comments** - code should be self-documenting + +## Layout +- `src/beaver_gateway` - python, unopinionated primitives (no vault paths, no agent names) +- `ui` - SvelteKit SPA (admin + Obsidian panel), served by the gateway from `ui/build` +- `docs/MISSION.md`, `docs/PRD.md`, `docs/PLAN.md`, `docs/PROGRESS.md` - keep PROGRESS.md current + +## Checking commands +After writing code, always run (or `make check`): +```shell +ruff format # python formatter +ruff check --fix # python linter +ty check # python type-checker +cd ui && bun fix # frontend linter+formatter (ultracite) +cd ui && bun check # svelte-check +``` +Dependencies: `uv add` / `cd ui && bun add`, never edit lock files by hand. diff --git a/.claude/rules/svelte.md b/.claude/rules/svelte.md new file mode 100644 index 0000000..4711cd0 --- /dev/null +++ b/.claude/rules/svelte.md @@ -0,0 +1,23 @@ +You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively: + +## Available Svelte MCP Tools: + +### 1. list-sections + +Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths. +When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections. + +### 2. get-documentation + +Retrieves full documentation content for specific sections. Accepts single or multiple sections. +After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task. + +### 3. svelte-autofixer + +Analyzes Svelte code and returns issues and suggestions. +You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned. + +### 4. playground-link + +Generates a Svelte Playground link with the provided code. +After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project. diff --git a/.claude/rules/ultracite.md b/.claude/rules/ultracite.md new file mode 100644 index 0000000..05ab812 --- /dev/null +++ b/.claude/rules/ultracite.md @@ -0,0 +1,123 @@ +# Ultracite Code Standards + +This project uses **Ultracite**, a zero-config preset that enforces strict code quality standards through automated formatting and linting. + +## Quick Reference + +- **Format code**: `bun x ultracite fix` +- **Check for issues**: `bun x ultracite check` +- **Diagnose setup**: `bun x ultracite doctor` + +Biome (the underlying engine) provides robust linting and formatting. Most issues are automatically fixable. + +--- + +## Core Principles + +Write code that is **accessible, performant, type-safe, and maintainable**. Focus on clarity and explicit intent over brevity. + +### Type Safety & Explicitness + +- Use explicit types for function parameters and return values when they enhance clarity +- Prefer `unknown` over `any` when the type is genuinely unknown +- Use const assertions (`as const`) for immutable values and literal types +- Leverage TypeScript's type narrowing instead of type assertions +- Use meaningful variable names instead of magic numbers - extract constants with descriptive names + +### Modern JavaScript/TypeScript + +- Use arrow functions for callbacks and short functions +- Prefer `for...of` loops over `.forEach()` and indexed `for` loops +- Use optional chaining (`?.`) and nullish coalescing (`??`) for safer property access +- Prefer template literals over string concatenation +- Use destructuring for object and array assignments +- Use `const` by default, `let` only when reassignment is needed, never `var` + +### Async & Promises + +- Always `await` promises in async functions - don't forget to use the return value +- Use `async/await` syntax instead of promise chains for better readability +- Handle errors appropriately in async code with try-catch blocks +- Don't use async functions as Promise executors + +### React & JSX + +- Use function components over class components +- Call hooks at the top level only, never conditionally +- Specify all dependencies in hook dependency arrays correctly +- Use the `key` prop for elements in iterables (prefer unique IDs over array indices) +- Nest children between opening and closing tags instead of passing as props +- Don't define components inside other components +- Use semantic HTML and ARIA attributes for accessibility: + - Provide meaningful alt text for images + - Use proper heading hierarchy + - Add labels for form inputs + - Include keyboard event handlers alongside mouse events + - Use semantic elements (` +{/if} diff --git a/ui/src/lib/components/ui/button/index.ts b/ui/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..872d97c --- /dev/null +++ b/ui/src/lib/components/ui/button/index.ts @@ -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, +}; diff --git a/ui/src/lib/utils.ts b/ui/src/lib/utils.ts new file mode 100644 index 0000000..b7606ad --- /dev/null +++ b/ui/src/lib/utils.ts @@ -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 extends { child?: any } ? Omit : T; +// biome-ignore lint/suspicious/noExplicitAny: default +export type WithoutChildren = T extends { children?: any } + ? Omit + : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { + ref?: U | null; +}; diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte new file mode 100644 index 0000000..ab928bf --- /dev/null +++ b/ui/src/routes/+layout.svelte @@ -0,0 +1,9 @@ + + + +{@render children()} diff --git a/ui/src/routes/+layout.ts b/ui/src/routes/+layout.ts new file mode 100644 index 0000000..83addb7 --- /dev/null +++ b/ui/src/routes/+layout.ts @@ -0,0 +1,2 @@ +export const ssr = false; +export const prerender = false; diff --git a/ui/src/routes/+page.svelte b/ui/src/routes/+page.svelte new file mode 100644 index 0000000..3d70440 --- /dev/null +++ b/ui/src/routes/+page.svelte @@ -0,0 +1,5 @@ +

Welcome to SvelteKit

+

+ Visit svelte.dev/docs/kit to read + the documentation +

diff --git a/ui/src/routes/layout.css b/ui/src/routes/layout.css new file mode 100644 index 0000000..658cb38 --- /dev/null +++ b/ui/src/routes/layout.css @@ -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; + } +} diff --git a/ui/static/robots.txt b/ui/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/ui/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..c7b9df5 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts new file mode 100644 index 0000000..bd694f9 --- /dev/null +++ b/ui/vite.config.ts @@ -0,0 +1,19 @@ +import adapter from "@sveltejs/adapter-static"; +import { sveltekit } from "@sveltejs/kit/vite"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + tailwindcss(), + sveltekit({ + adapter: adapter({ fallback: "index.html" }), + compilerOptions: { + // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + runes: ({ filename }) => + // biome-ignore lint/performance/useTopLevelRegex: svelte default + filename.split(/[/\\]/).includes("node_modules") ? undefined : true, + }, + }), + ], +});