From 23ef77ece6cab9c640b9e4174581ffe3b95c7a1d Mon Sep 17 00:00:00 2001 From: TZGyn Date: Sat, 1 Aug 2026 19:55:11 +0800 Subject: [PATCH] responsive ui + wallet redesign + 401 fix + round cards --- .../.agents/skills/shadcn-svelte/SKILL.md | 227 ++++++ .../skills/shadcn-svelte/agents/openai.yml | 5 + .../assets/shadcn-svelte-small.png | Bin 0 -> 2672 bytes .../shadcn-svelte/assets/shadcn-svelte.png | Bin 0 -> 9410 bytes .../.agents/skills/shadcn-svelte/cli.md | 166 ++++ .../skills/shadcn-svelte/customization.md | 213 +++++ .../skills/shadcn-svelte/evals/evals.json | 47 ++ .../skills/shadcn-svelte/rules/composition.md | 244 ++++++ .../skills/shadcn-svelte/rules/forms.md | 234 ++++++ .../skills/shadcn-svelte/rules/icons.md | 107 +++ .../skills/shadcn-svelte/rules/styling.md | 195 +++++ .../skills/svelte-code-writer/SKILL.md | 64 ++ .../skills/svelte-core-bestpractices/SKILL.md | 176 +++++ .../references/attach.md | 170 ++++ .../references/await-expressions.md | 187 +++++ .../references/bind.md | 22 + .../references/each.md | 42 + .../references/hydratable.md | 103 +++ .../references/inspect.md | 63 ++ .../references/render.md | 35 + .../references/snippet.md | 400 ++++++++++ .../references/svelte-reactivity.md | 61 ++ apps/desktop/opencode.json | 6 + apps/desktop/skills-lock.json | 23 + apps/desktop/src/app.css | 4 +- apps/desktop/src/bun/lib/monero/wallet.ts | 32 +- .../lib/components/ui/card/card-footer.svelte | 2 +- .../lib/components/ui/card/card-header.svelte | 2 +- .../src/lib/components/ui/card/card.svelte | 2 +- apps/desktop/src/routes/index.svelte | 2 +- apps/desktop/src/routes/layout.svelte | 6 +- apps/desktop/src/routes/monero/index.svelte | 2 +- apps/desktop/src/routes/monero/send.svelte | 2 +- .../src/routes/monero/tx/[hash].svelte | 2 +- .../src/routes/monero/wallet/[name].svelte | 727 ++++++------------ apps/desktop/src/routes/settings.svelte | 2 +- apps/desktop/src/routes/xrp/index.svelte | 2 +- apps/desktop/src/routes/xrp/send.svelte | 2 +- apps/desktop/src/routes/xrp/tx/[hash].svelte | 2 +- .../desktop/src/routes/xrp/wallet/[id].svelte | 287 +++---- 40 files changed, 3184 insertions(+), 684 deletions(-) create mode 100644 apps/desktop/.agents/skills/shadcn-svelte/SKILL.md create mode 100644 apps/desktop/.agents/skills/shadcn-svelte/agents/openai.yml create mode 100644 apps/desktop/.agents/skills/shadcn-svelte/assets/shadcn-svelte-small.png create mode 100644 apps/desktop/.agents/skills/shadcn-svelte/assets/shadcn-svelte.png create mode 100644 apps/desktop/.agents/skills/shadcn-svelte/cli.md create mode 100644 apps/desktop/.agents/skills/shadcn-svelte/customization.md create mode 100644 apps/desktop/.agents/skills/shadcn-svelte/evals/evals.json create mode 100644 apps/desktop/.agents/skills/shadcn-svelte/rules/composition.md create mode 100644 apps/desktop/.agents/skills/shadcn-svelte/rules/forms.md create mode 100644 apps/desktop/.agents/skills/shadcn-svelte/rules/icons.md create mode 100644 apps/desktop/.agents/skills/shadcn-svelte/rules/styling.md create mode 100644 apps/desktop/.agents/skills/svelte-code-writer/SKILL.md create mode 100644 apps/desktop/.agents/skills/svelte-core-bestpractices/SKILL.md create mode 100644 apps/desktop/.agents/skills/svelte-core-bestpractices/references/attach.md create mode 100644 apps/desktop/.agents/skills/svelte-core-bestpractices/references/await-expressions.md create mode 100644 apps/desktop/.agents/skills/svelte-core-bestpractices/references/bind.md create mode 100644 apps/desktop/.agents/skills/svelte-core-bestpractices/references/each.md create mode 100644 apps/desktop/.agents/skills/svelte-core-bestpractices/references/hydratable.md create mode 100644 apps/desktop/.agents/skills/svelte-core-bestpractices/references/inspect.md create mode 100644 apps/desktop/.agents/skills/svelte-core-bestpractices/references/render.md create mode 100644 apps/desktop/.agents/skills/svelte-core-bestpractices/references/snippet.md create mode 100644 apps/desktop/.agents/skills/svelte-core-bestpractices/references/svelte-reactivity.md create mode 100644 apps/desktop/opencode.json create mode 100644 apps/desktop/skills-lock.json diff --git a/apps/desktop/.agents/skills/shadcn-svelte/SKILL.md b/apps/desktop/.agents/skills/shadcn-svelte/SKILL.md new file mode 100644 index 0000000..6bbc425 --- /dev/null +++ b/apps/desktop/.agents/skills/shadcn-svelte/SKILL.md @@ -0,0 +1,227 @@ +--- +name: shadcn-svelte +description: Manages shadcn-svelte components and projects — adding, updating, fixing, debugging, styling, and composing UI. Provides project context, component docs, and usage examples. Applies when working with shadcn-svelte, the CLI, design-system presets, or any project with a components.json file. Also triggers for "shadcn-svelte init", "add component", or registry URLs. +user-invocable: false +allowed-tools: Bash(npx shadcn-svelte@latest *), Bash(pnpm dlx shadcn-svelte@latest *), Bash(bunx --bun shadcn-svelte@latest *) +--- + +# shadcn-svelte + +A framework for building UI, components, and design systems for Svelte. Components are added as source to the user's project via the CLI. + +> **IMPORTANT:** Run all CLI commands using the project's package runner: `npx shadcn-svelte@latest`, `pnpm dlx shadcn-svelte@latest`, or `bunx --bun shadcn-svelte@latest` — based on the project's package manager. Examples below use `npx shadcn-svelte@latest` but substitute the correct runner for the project. + +## Current Project Context + +Read `components.json` at the project root and, when you need the live file layout, list the directory given by the `aliases.ui` path (resolved with the same rules as the CLI). + +## Imports (Svelte) + +Each component lives in its own folder with an `index.ts` barrel. Match the [installation docs](https://shadcn-svelte.com/docs/installation): + +- **Multi-part components** (dialog, select, card, field, tabs, …): `import * as Dialog from "$lib/components/ui/dialog"` then `Dialog.Content`, `Dialog.Title`, `Card.Root`, `Card.Header`, etc. — whatever the barrel exports (short names and/or `Root as …` aliases). +- **Single-component barrels** (only one meaningful component in the folder): **named imports** — `import { Button } from "$lib/components/ui/button"` and ` + + +
+ + + + + U + + + ++20.1% +``` + +## Component Selection + +| Need | Use | +| -------------------------- | --------------------------------------------------------------------------------------------------- | +| Button/action | `Button` with appropriate variant (`import { Button }`) | +| Form inputs | `Input`, `Select`, `Combobox`, `Switch`, `Checkbox`, `RadioGroup`, `Textarea`, `InputOTP`, `Slider` | +| Toggle between 2–5 options | `ToggleGroup.Root` + `ToggleGroup.Item` | +| Data display | `Table`, `Card`, `Badge`, `Avatar` | +| Navigation | `Sidebar`, `NavigationMenu`, `Breadcrumb`, `Tabs`, `Pagination` | +| Overlays | `Dialog` (modal), `Sheet` (side panel), `Drawer` (bottom sheet), `AlertDialog` (confirmation) | +| Feedback | `svelte-sonner` (toast), `Alert`, `Progress`, `Skeleton`, `Spinner` | +| Command palette | `Command` inside `Dialog` | +| Charts | `Chart` (LayerChart) | +| Layout | `Card`, `Separator`, `Resizable`, `ScrollArea`, `Accordion`, `Collapsible` | +| Empty states | `Empty` | +| Menus | `DropdownMenu`, `ContextMenu`, `Menubar` | +| Tooltips/info | `Tooltip`, `HoverCard`, `Popover` | + +## Key Fields + +Use `components.json` and the filesystem — not a separate `info` command: + +- **`aliases`** → use the actual alias prefix from config (e.g. `$lib/`), never hardcode unrelated projects. +- **`tailwind.css`** → the global CSS file where theme variables live. Edit this file for theme tweaks; don't add a second globals file unless the user already uses one. +- **`style`** → visual treatment (e.g. `nova`, `vega`, …) and registry style path. +- **`iconLibrary`** → determines icon packages (`@lucide/svelte`, `@tabler/icons-svelte`, etc.). Never assume `@lucide/svelte`. +- **`registry`** → where the CLI fetches components; default official registry at `shadcn-svelte.com`. +- **`resolvedPaths`** (conceptual) → the CLI resolves `aliases` to absolute paths; list `aliases.ui` on disk to see installed components. + +See [cli.md](./cli.md) for commands and flags. + +## Component Docs, Examples, and Usage + +Open `https://shadcn-svelte.com/docs/components/.md` for docs and examples. **When creating, fixing, debugging, or using a component, read the official page first** so you follow the documented APIs. + +## Workflow + +1. **Get project context** — read `components.json` and list the UI components directory when needed. +2. **Check installed components first** — before running `add`, list files under the resolved `ui` path. Don't import components that haven't been added, and don't re-add ones already present unless updating. +3. **Discover components** — `npx shadcn-svelte@latest add` with no arguments (interactive list), or the docs site. +4. **Install or update** — `npx shadcn-svelte@latest add ` or a registry **URL**. To refresh existing files from the registry, use `npx shadcn-svelte@latest update` (see [cli.md](./cli.md)). +5. **Fix imports in third-party / URL-added items** — After adding from a custom registry URL, check for hardcoded paths that don't match the project's `aliases`. Rewrite imports to use the project's `ui` / `lib` aliases from `components.json`. +6. **Review added components** — After adding, **read the added files** and verify composition (groups, titles, validation attrs). Align icon imports with `iconLibrary`. +7. **Remote registry items** — Adding by URL is explicit; if the user wants a component from an unknown source, confirm the registry URL or item before running `add`. + +## Updating Components + +Use the **`update`** command to pull the latest registry versions of components already in the project. Review changes with `git diff` after `update`. + +1. Commit or stash local work. +2. Run `npx shadcn-svelte@latest update [component]` or `--all`. +3. Resolve merge conflicts if you had customized files. +4. **Never use `--overwrite` on `add` without the user's explicit approval** when it would destroy intentional edits. + +## Quick Reference + +```bash +# Initialize shadcn-svelte in your project. +npx shadcn-svelte@latest init + +# Initialize with a preset string from the docs site builder. +npx shadcn-svelte@latest init --preset + +# Add components (interactive when run with no names). +npx shadcn-svelte@latest add +npx shadcn-svelte@latest add button card dialog +npx shadcn-svelte@latest add --all + +# Update components already installed. +npx shadcn-svelte@latest update button +npx shadcn-svelte@latest update --all --yes + +# Build a custom registry (registry authors). +npx shadcn-svelte@latest registry build +``` + +**Registry:** default `https://shadcn-svelte.com/registry` — override in `components.json` if needed. +**Docs:** [shadcn-svelte.com](https://shadcn-svelte.com) + +## Detailed References + +- [rules/forms.md](./rules/forms.md) — Field.FieldGroup, Field.Field, InputGroup, ToggleGroup, Field.FieldSet, validation states +- [rules/composition.md](./rules/composition.md) — Groups, overlays, Card, Tabs, Avatar, Alert, Empty, Toast, Separator, Skeleton, Badge, Button loading +- [rules/icons.md](./rules/icons.md) — data-icon, icon sizing, passing icon components +- [rules/styling.md](./rules/styling.md) — Semantic colors, variants, class, spacing, size, truncate, dark mode, cn(), z-index +- [cli.md](./cli.md) — Commands, flags, registry +- [customization.md](./customization.md) — Theming, CSS variables, extending components diff --git a/apps/desktop/.agents/skills/shadcn-svelte/agents/openai.yml b/apps/desktop/.agents/skills/shadcn-svelte/agents/openai.yml new file mode 100644 index 0000000..612a07b --- /dev/null +++ b/apps/desktop/.agents/skills/shadcn-svelte/agents/openai.yml @@ -0,0 +1,5 @@ +interface: + display_name: "shadcn-svelte" + short_description: "Manages shadcn-svelte components — adding, updating, fixing, debugging, styling, and composing UI." + icon_small: "./assets/shadcn-svelte-small.png" + icon_large: "./assets/shadcn-svelte.png" diff --git a/apps/desktop/.agents/skills/shadcn-svelte/assets/shadcn-svelte-small.png b/apps/desktop/.agents/skills/shadcn-svelte/assets/shadcn-svelte-small.png new file mode 100644 index 0000000000000000000000000000000000000000..17a8bf5c9ba3e1e14355d7b3c0aef89cf4ce3c5f GIT binary patch literal 2672 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 OVb>0K=b~-C(SHM+r2$a@ literal 0 HcmV?d00001 diff --git a/apps/desktop/.agents/skills/shadcn-svelte/assets/shadcn-svelte.png b/apps/desktop/.agents/skills/shadcn-svelte/assets/shadcn-svelte.png new file mode 100644 index 0000000000000000000000000000000000000000..a35eb16e517ab064a89715a985619789e4f41d1a GIT binary patch literal 9410 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 literal 0 HcmV?d00001 diff --git a/apps/desktop/.agents/skills/shadcn-svelte/cli.md b/apps/desktop/.agents/skills/shadcn-svelte/cli.md new file mode 100644 index 0000000..ff19b7c --- /dev/null +++ b/apps/desktop/.agents/skills/shadcn-svelte/cli.md @@ -0,0 +1,166 @@ +# shadcn-svelte CLI Reference + +Configuration is read from `components.json`. See [components.json](https://shadcn-svelte.com/docs/components-json) on the docs site for the full schema. + +> **IMPORTANT:** Always run commands using the project's package runner: `npx shadcn-svelte@latest`, `pnpm dlx shadcn-svelte@latest`, or `bunx --bun shadcn-svelte@latest`. Check `packageManager` from the project (or lockfile) to choose the right one. Examples below use `npx shadcn-svelte@latest` but substitute the correct runner for the project. + +> **IMPORTANT:** Only use the flags documented below. Do not invent or guess flags — if a flag isn't listed here, it doesn't exist. The CLI auto-detects the package manager; there is no `--package-manager` flag. + +## Contents + +- Commands: `init`, `add`, `apply`, `update`, `registry build` +- Proxy / outgoing requests +- Presets (via `init` and `apply`) + +--- + +## Commands + +### `init` — Initialize an existing project + +```bash +npx shadcn-svelte@latest init [options] +``` + +Installs dependencies, adds the `cn` util, creates `components.json`, and sets up CSS variables. Run `init` from the root of your project. + +| Flag | Short | Description | Default | +| --------------------------- | ----- | ------------------------------------------------------------------------- | --------- | +| `--preset ` | — | Encoded design-system preset string from the docs site | — | +| `-c, --cwd ` | `-c` | Working directory | current | +| `-o, --overwrite` | — | Overwrite existing files | `false` | +| `--no-deps` | — | Do not add or install dependencies | — | +| `--skip-preflight` | — | Ignore preflight checks and continue | `false` | +| `--base-color ` | — | Base color: `neutral`, `stone`, `zinc`, `mauve`, `olive`, `mist`, `taupe` | — | +| `--css ` | — | Path to the global CSS file | — | +| `--components-alias ` | — | Import alias for components | — | +| `--lib-alias ` | — | Import alias for lib | — | +| `--utils-alias ` | — | Import alias for utils | — | +| `--hooks-alias ` | — | Import alias for hooks | — | +| `--ui-alias ` | — | Import alias for UI components | — | +| `--proxy ` | — | Fetch registry items through this proxy | env-based | +| `--design-system-url` | — | Optional design-system URL (see docs / preset builder) | — | +| `-h, --help` | `-h` | Help | — | + +--- + +### `add` — Add components + +```bash +npx shadcn-svelte@latest add [options] [components...] +``` + +Adds components from the configured registry. Arguments are component names from the registry index, or a **URL** to a registry JSON item. With **no** component names, the CLI prompts you to pick components interactively. + +| Flag | Short | Description | Default | +| ------------------ | ----- | ----------------------------------------------- | --------- | +| `-c, --cwd ` | `-c` | Working directory | current | +| `--no-deps` | — | Skip adding and installing package dependencies | — | +| `--skip-preflight` | — | Ignore preflight checks and continue | `false` | +| `-a, --all` | — | Install all UI components | `false` | +| `-y, --yes` | — | Skip confirmation prompt | `false` | +| `-o, --overwrite` | — | Overwrite existing files | `false` | +| `--proxy ` | — | Fetch components through this proxy | env-based | +| `-h, --help` | `-h` | Help | — | + +--- + +### `apply` — Apply a preset to an existing project + +```bash +npx shadcn-svelte@latest apply [options] +``` + +Applies a design-system preset to a project that has already been initialized. Updates `components.json` with the preset settings, reinstalls existing components (except `utils`) with the new styles, and installs any required dependencies. + +Use `--only theme` or `--only font` to apply only part of a preset without reinstalling UI components. + +Get a preset code from the builder at [shadcn-svelte.com/create](https://shadcn-svelte.com/create). + +| Flag | Short | Description | Default | +| ------------------- | ----- | ---------------------------------------------- | --------- | +| `--preset ` | — | Encoded design-system preset string (required) | — | +| `--only [parts]` | — | Apply only `theme` or `font` from the preset | — | +| `-c, --cwd ` | `-c` | Working directory | current | +| `-y, --yes` | `-y` | Overwrite existing files without confirmation | `false` | +| `-s, --silent` | `-s` | Mute output | `false` | +| `--skip-preflight` | — | Ignore preflight checks and continue | `false` | +| `--proxy ` | — | Fetch registry items through this proxy | env-based | +| `-h, --help` | `-h` | Help | — | + +Requires an existing `components.json`. Run `init` first if the project is not yet configured. + +--- + +### `update` — Update installed components + +```bash +npx shadcn-svelte@latest update [options] [components...] +``` + +Re-fetches and applies registry content for components **already present** in the project. Run `shadcn-svelte update --help` for options. + +| Flag | Short | Description | Default | +| ------------------ | ----- | ----------------------------------------------- | --------- | +| `-c, --cwd ` | `-c` | Working directory | current | +| `--skip-preflight` | — | Ignore preflight checks and continue | `false` | +| `--no-deps` | — | Skip adding and installing package dependencies | — | +| `-a, --all` | — | Update every installed component | `false` | +| `-y, --yes` | — | Skip confirmation prompt | `false` | +| `--proxy ` | — | Fetch through this proxy | env-based | +| `-h, --help` | `-h` | Help | — | + +Commit your work before updating; overwrites are destructive. + +--- + +### `registry build` — Build a custom registry + +```bash +npx shadcn-svelte@latest registry build [options] [registry] +``` + +Reads a `registry.json` and writes registry JSON files for distribution. Default input: `./registry.json`, default output: `./static/r`. + +| Flag | Short | Description | Default | +| --------------------- | ----- | ------------------------------- | ------------ | +| `-c, --cwd ` | `-c` | Working directory | current | +| `-o, --output ` | `-o` | Output directory for JSON files | `./static/r` | +| `-h, --help` | `-h` | Help | — | + +--- + +## Outgoing Requests + +### Proxy + +The CLI can fetch the registry through a proxy. If `HTTP_PROXY` or `http_proxy` is set, requests respect it. You can also pass `--proxy` on `init`, `add`, `apply`, or `update`. + +```bash +HTTP_PROXY="" npx shadcn-svelte@latest init +``` + +--- + +## Presets + +Design-system options (style, theme, icons, fonts, etc.) can be captured as an encoded **preset** string from the builder on [shadcn-svelte.com/create](https://shadcn-svelte.com/create). + +- **New project:** pass the preset to **`init`** with `--preset `. +- **Existing project:** use **`apply --preset `** to update configuration, restyle installed components, and install any new dependencies. + +--- + +## `components.json` — useful fields for agents + +| Field / path | Meaning | +| -------------------- | ---------------------------------------------------------------- | +| `tailwind.css` | Global CSS file path (Tailwind entry / theme variables) | +| `tailwind.baseColor` | Base palette (cannot change after init) | +| `aliases.*` | Import aliases; must match `svelte.config.js` / `tsconfig` paths | +| `registry` | Base registry URL (default `https://shadcn-svelte.com/registry`) | +| `style` | Registered style name (e.g. `nova`, `vega`, …) | +| `iconLibrary` | Icon set key (`lucide`, `tabler`, …) — drives generated imports | +| `typescript` | Whether TS and optional custom config path | + +Resolved paths (including `tailwindCss`, `ui`, `components`) are computed by the CLI from `components.json` and the filesystem. Read `components.json` and list the UI directory when you need a snapshot of what is installed. diff --git a/apps/desktop/.agents/skills/shadcn-svelte/customization.md b/apps/desktop/.agents/skills/shadcn-svelte/customization.md new file mode 100644 index 0000000..51a52fc --- /dev/null +++ b/apps/desktop/.agents/skills/shadcn-svelte/customization.md @@ -0,0 +1,213 @@ +# Customization & Theming + +Components reference semantic CSS variable tokens. Change the variables to change every component. + +## Contents + +- How it works (CSS variables → Tailwind utilities → components) +- Color variables and OKLCH format +- Dark mode setup +- Changing the theme (presets, CSS variables) +- Adding custom colors (Tailwind v3 and v4) +- Border radius +- Customizing components (variants, class, wrappers) +- Checking for updates + +--- + +## How It Works + +1. CSS variables defined in `:root` (light) and `.dark` (dark mode). +2. Tailwind maps them to utilities: `bg-primary`, `text-muted-foreground`, etc. +3. Components use these utilities — changing a variable changes all components that reference it. + +--- + +## Color Variables + +Every color follows the `name` / `name-foreground` convention. The base variable is for backgrounds, `-foreground` is for text/icons on that background. + +| Variable | Purpose | +| -------------------------------------------- | -------------------------------- | +| `--background` / `--foreground` | Page background and default text | +| `--card` / `--card-foreground` | Card surfaces | +| `--primary` / `--primary-foreground` | Primary buttons and actions | +| `--secondary` / `--secondary-foreground` | Secondary actions | +| `--muted` / `--muted-foreground` | Muted/disabled states | +| `--accent` / `--accent-foreground` | Hover and accent states | +| `--destructive` / `--destructive-foreground` | Error and destructive actions | +| `--border` | Default border color | +| `--input` | Form input borders | +| `--ring` | Focus ring color | +| `--chart-1` through `--chart-5` | Chart/data visualization | +| `--sidebar-*` | Sidebar-specific colors | +| `--surface` / `--surface-foreground` | Secondary surface | + +Colors use OKLCH: `--primary: oklch(0.205 0 0)` where values are lightness (0–1), chroma (0 = gray), and hue (0–360). + +--- + +## Dark Mode + +Class-based toggle via `.dark` on the root element. In SvelteKit, use [mode-watcher](https://github.com/svecosystem/mode-watcher) (see [Dark mode — Svelte](https://shadcn-svelte.com/docs/dark-mode/svelte)): + +```svelte + + + +{@render children?.()} +``` + +--- + +## Changing the Theme + +Use a **preset** from the design-system builder on [shadcn-svelte.com](https://shadcn-svelte.com) and pass it to `init`: + +```bash +npx shadcn-svelte@latest init --preset +``` + +Or edit CSS variables directly in the file set in `components.json` as `tailwind.css` (for example `src/app.css`). + +To align config and components with a new preset, re-run `init` with `--preset` and confirm overwrites when prompted. + +--- + +## Adding Custom Colors + +Add variables to the global CSS file path in `components.json` (`tailwind.css`). Do not create a second global CSS file for theming unless the project already uses that pattern. + +```css +/* 1. Define in the global CSS file. */ +:root { + --warning: oklch(0.84 0.16 84); + --warning-foreground: oklch(0.28 0.07 46); +} +.dark { + --warning: oklch(0.41 0.11 46); + --warning-foreground: oklch(0.99 0.02 95); +} +``` + +```css +/* 2a. Register with Tailwind v4 (@theme inline). */ +@theme inline { + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); +} +``` + +On Tailwind v3, register in `tailwind.config.js` (see the [Tailwind v3 docs](https://tw3.shadcn-svelte.com) if you maintain a legacy setup): + +```js +// 2b. Register with Tailwind v3 (tailwind.config.js). +module.exports = { + theme: { + extend: { + colors: { + warning: "oklch(var(--warning) / )", + "warning-foreground": + "oklch(var(--warning-foreground) / )", + }, + }, + }, +}; +``` + +```svelte + +
Warning
+``` + +--- + +## Border Radius + +`--radius` controls border radius globally. Components derive values from it (`rounded-lg` = `var(--radius)`, `rounded-md` = `calc(var(--radius) - 2px)`). + +--- + +## Customizing Components + +See also: [rules/styling.md](./rules/styling.md) for Incorrect/Correct examples. + +Prefer these approaches in order: + +### 1. Built-in variants + +```svelte + + + +``` + +### 2. Tailwind classes via `class` + +```svelte + + + + ... + +``` + +### 3. Add a new variant + +Edit the component source to add a variant via `tailwind-variants` / `cva` in the `.svelte` or shared variants file: + +```ts +// e.g. in button variants +warning: "bg-warning text-warning-foreground hover:bg-warning/90", +``` + +### 4. Wrapper components + +Compose shadcn-svelte primitives into higher-level `.svelte` files: + +```svelte + + + + + {@render children?.()} + + + + {title} + {description} + + + Cancel + { + onConfirm?.(); + open = false; + }}>Confirm + + + +``` + +--- + +## Checking for Updates + +```bash +npx shadcn-svelte@latest update button +npx shadcn-svelte@latest update --all +``` + +See [Updating Components in SKILL.md](./SKILL.md#updating-components). Review `git diff` after `update` to see what changed. diff --git a/apps/desktop/.agents/skills/shadcn-svelte/evals/evals.json b/apps/desktop/.agents/skills/shadcn-svelte/evals/evals.json new file mode 100644 index 0000000..fcbe497 --- /dev/null +++ b/apps/desktop/.agents/skills/shadcn-svelte/evals/evals.json @@ -0,0 +1,47 @@ +{ + "skill_name": "shadcn-svelte", + "evals": [ + { + "id": 1, + "prompt": "I'm building a SvelteKit app with shadcn-svelte (nova style, lucide icons). Create a settings form component with fields for: full name, email address, and notification preferences (email, SMS, push notifications as toggle options). Add validation states for required fields.", + "expected_output": "A Svelte component using Field.FieldGroup, Field.Field, ToggleGroup, data-invalid/aria-invalid validation, gap-* spacing, and semantic colors.", + "files": [], + "expectations": [ + "Uses Field.FieldGroup and Field.Field for form layout instead of raw div with space-y", + "Uses Switch for independent on/off notification toggles (not looping Button with manual active state)", + "Uses data-invalid on Field and aria-invalid on the input control for validation states", + "Uses gap-* (e.g. gap-4, gap-6) instead of space-y-* or space-x-* for spacing", + "Uses semantic color tokens (e.g. bg-background, text-muted-foreground, text-destructive) instead of raw colors like bg-red-500", + "No manual dark: color overrides" + ] + }, + { + "id": 2, + "prompt": "Create a dialog component for editing a user profile. It should have the user's avatar at the top, input fields for name and bio, and Save/Cancel buttons with appropriate icons. Using shadcn-svelte with tabler icons.", + "expected_output": "A Svelte component with Dialog.Title, Avatar with Avatar.Fallback, data-icon on icon buttons, no icon sizing classes, @tabler/icons-svelte imports.", + "files": [], + "expectations": [ + "Includes Dialog.Title for accessibility (visible or with sr-only class)", + "Avatar includes Avatar.Fallback", + "Icons on buttons use the data-icon attribute (data-icon=\"inline-start\" or data-icon=\"inline-end\")", + "No sizing classes on icons inside components (no size-4, w-4, h-4, etc.)", + "Uses tabler icons (@tabler/icons-svelte) instead of @lucide/svelte when tabler is configured", + "Uses shadcn-svelte Dialog patterns (e.g. Dialog.Trigger wrapping the control, or bind:open on Dialog.Root)" + ] + }, + { + "id": 3, + "prompt": "Create a dashboard component that shows 4 stat cards in a grid. Each card has a title, large number, percentage change badge, and a loading skeleton state. Using shadcn-svelte with lucide icons.", + "expected_output": "A Svelte component with full Card composition, Skeleton for loading, Badge for changes, semantic colors, gap-* spacing.", + "files": [], + "expectations": [ + "Uses full Card composition with Card.Header, Card.Title, Card.Content (not dumping everything into Card.Content)", + "Uses Skeleton component for loading placeholders instead of custom animate-pulse divs", + "Uses Badge component for percentage change instead of custom styled spans", + "Uses semantic color tokens instead of raw color values like bg-green-500 or text-red-600", + "Uses gap-* instead of space-y-* or space-x-* for spacing", + "Uses size-* when width and height are equal instead of separate w-* h-*" + ] + } + ] +} diff --git a/apps/desktop/.agents/skills/shadcn-svelte/rules/composition.md b/apps/desktop/.agents/skills/shadcn-svelte/rules/composition.md new file mode 100644 index 0000000..bfc0a93 --- /dev/null +++ b/apps/desktop/.agents/skills/shadcn-svelte/rules/composition.md @@ -0,0 +1,244 @@ +# Component Composition + +## Contents + +- Items always inside their Group component +- Callouts use Alert +- Empty states use Empty component +- Toast notifications use svelte-sonner +- Choosing between overlay components +- Dialog, Sheet, and Drawer always need a Title +- Card structure +- Button has no isPending or isLoading prop +- Tabs.Trigger must be inside Tabs.List +- Avatar always needs Avatar.Fallback +- Use Separator instead of raw hr or border divs +- Use Skeleton for loading placeholders +- Use Badge instead of custom styled spans + +--- + +## Items always inside their Group component + +Never render items directly inside the content container. + +**Incorrect:** + +```svelte + + + + Apple + Banana + +``` + +**Correct:** + +```svelte + + + + + Apple + Banana + + +``` + +This applies to all group-based components: + +| Item | Group | +| ------------------------------------------------------------- | -------------------- | +| `Select.Item`, `Select.Label` | `Select.Group` | +| `DropdownMenu.Item`, `DropdownMenu.Label`, `DropdownMenu.Sub` | `DropdownMenu.Group` | +| `Menubar.Item` | `Menubar.Group` | +| `ContextMenu.Item` | `ContextMenu.Group` | +| `Command.Item` | `Command.Group` | + +--- + +## Callouts use Alert + +```svelte + + + + Warning + Something needs attention. + +``` + +--- + +## Empty states use Empty component + +```svelte + + + + + + No projects yet + Get started by creating a new project. + + + + + +``` + +--- + +## Toast notifications use svelte-sonner + +```svelte + +``` + +```ts +toast.success("Changes saved."); +toast.error("Something went wrong."); +toast("File deleted.", { + action: { label: "Undo", onClick: () => undoDelete() }, +}); +``` + +Mount the `Toaster` from your UI folder once in the app layout (see [Sonner](https://shadcn-svelte.com/docs/components/sonner)). + +--- + +## Choosing between overlay components + +| Use case | Component | +| ---------------------------------- | ------------- | +| Focused task that requires input | `Dialog` | +| Destructive action confirmation | `AlertDialog` | +| Side panel with details or filters | `Sheet` | +| Mobile-first bottom panel | `Drawer` | +| Quick info on hover | `HoverCard` | +| Small contextual content on click | `Popover` | + +--- + +## Dialog, Sheet, and Drawer always need a Title + +`Dialog.Title`, `Sheet.Title`, `Drawer.Title` are required for accessibility. Use `class="sr-only"` if visually hidden. + +```svelte + + + + + Edit Profile + Update your profile. + + ... + +``` + +--- + +## Card structure + +Use full composition — don't dump everything into `Card.Content`: + +```svelte + + + + + Team Members + Manage your team. + + ... + + + + +``` + +--- + +## Button has no isPending or isLoading prop + +Compose with `Spinner` inside `Button` + `disabled`: + +```svelte + + + +``` + +--- + +## Tabs.Trigger must be inside Tabs.List + +Never render `Tabs.Trigger` directly inside `Tabs.Root` — always wrap in `Tabs.List`: + +```svelte + + + + + Account + Password + + ... + +``` + +--- + +## Avatar always needs Avatar.Fallback + +Always include `Avatar.Fallback` for when the image fails to load: + +```svelte + + + + + JD + +``` + +--- + +## Use existing components instead of custom markup + +| Instead of | Use | +| ---------------------------------------------- | ------------------------------------------------------------------------------------------- | +| `
` or `
` | `` (`import { Separator } from "$lib/components/ui/separator"`) | +| `
` with styled divs | `` (`import { Skeleton } from "$lib/components/ui/skeleton"`) | +| `` | `` (`import { Badge } from "$lib/components/ui/badge"`) | diff --git a/apps/desktop/.agents/skills/shadcn-svelte/rules/forms.md b/apps/desktop/.agents/skills/shadcn-svelte/rules/forms.md new file mode 100644 index 0000000..c779ee4 --- /dev/null +++ b/apps/desktop/.agents/skills/shadcn-svelte/rules/forms.md @@ -0,0 +1,234 @@ +# Forms & Inputs + +## Contents + +- Forms use Field.FieldGroup + Field.Field +- InputGroup requires InputGroup.Input/InputGroup.Textarea +- Buttons inside inputs use InputGroup.Root + InputGroup.Addon +- Option sets (2–7 choices) use ToggleGroup.Root + ToggleGroup.Item +- Field.FieldSet + Field.FieldLegend for grouping related fields +- Field validation and disabled states + +--- + +## Forms use Field.FieldGroup + Field.Field + +Always use `Field.FieldGroup` + `Field.Field` — never raw `div` with `space-y-*`: + +```svelte + + + + + Email + + + + Password + + + +``` + +Use `Field` with `orientation="horizontal"` for settings pages. Use `Field.FieldLabel` with `class="sr-only"` for visually hidden labels. + +**Choosing form controls:** + +- Simple text input → `Input` +- Dropdown with predefined options → `Select` +- Searchable dropdown → `Combobox` +- Native HTML select (no JS) → `native-select` +- Boolean toggle → `Switch` (for settings) or `Checkbox` (for forms) +- Single choice from few options → `RadioGroup` +- Toggle between 2–5 options → `ToggleGroup.Root` + `ToggleGroup.Item` +- OTP/verification code → `InputOTP` +- Multi-line text → `Textarea` + +--- + +## InputGroup requires InputGroup.Input/InputGroup.Textarea + +Never use raw `Input` or `Textarea` inside an `InputGroup.Root`. + +**Incorrect:** + +```svelte + + + + + +``` + +**Correct:** + +```svelte + + + + + +``` + +--- + +## Buttons inside inputs use InputGroup.Root + InputGroup.Addon + +Never place a `Button` directly inside or adjacent to an `Input` with custom positioning. + +**Incorrect:** + +```svelte + + +
+ + +
+``` + +**Correct:** + +```svelte + + + + + + + + +``` + +--- + +## Option sets (2–7 choices) use ToggleGroup.Root + ToggleGroup.Item + +Don't manually loop `Button` components with active state. + +**Incorrect:** + +```svelte + + +
+ {#each ["daily", "weekly", "monthly"] as option (option)} + + {/each} +
+``` + +**Correct:** + +```svelte + + + + Daily + Weekly + Monthly + +``` + +Combine with `Field` for labelled toggle groups: + +```svelte + + + + Theme + + Light + Dark + System + + +``` + +--- + +## Field.FieldSet + Field.FieldLegend for grouping related fields + +Use `Field.FieldSet` + `Field.FieldLegend` for related checkboxes, radios, or switches — not `div` with a heading: + +```svelte + + + + Preferences + Select all that apply. + + + + Dark mode + + + +``` + +--- + +## Field validation and disabled states + +Both attributes are needed — `data-invalid`/`data-disabled` styles the field (label, description), while `aria-invalid`/`disabled` styles the control. + +```svelte + + + + + Email + + Invalid email address. + + + + + Email + + +``` + +Works for all controls: `Input`, `Textarea`, `Select`, `Checkbox`, `RadioGroupItem`, `Switch`, `Slider`, `NativeSelect`, `InputOTP`. diff --git a/apps/desktop/.agents/skills/shadcn-svelte/rules/icons.md b/apps/desktop/.agents/skills/shadcn-svelte/rules/icons.md new file mode 100644 index 0000000..4db886d --- /dev/null +++ b/apps/desktop/.agents/skills/shadcn-svelte/rules/icons.md @@ -0,0 +1,107 @@ +# Icons + +**Always use the project's configured `iconLibrary` for imports.** Check the `iconLibrary` field in `components.json`: `lucide` → `@lucide/svelte`, `tabler` → `@tabler/icons-svelte`, etc. Never assume `@lucide/svelte`. + +--- + +## Icons in Button use data-icon attribute + +Add `data-icon="inline-start"` (prefix) or `data-icon="inline-end"` (suffix) to the icon. No sizing classes on the icon. + +**Incorrect:** + +```svelte + + + +``` + +**Correct:** + +```svelte + + + + + +``` + +--- + +## No sizing classes on icons inside components + +Components handle icon sizing via CSS. Don't add `size-4`, `w-4 h-4`, or other sizing classes to icons inside ` +``` + +**Correct:** + +```svelte + + + +``` + +The same applies to icons inside `DropdownMenu.Item`, sidebar items, and other menu rows — no extra sizing classes on the icon component. + +--- + +## Pass icons as components, not string keys + +Use a component reference, not a string key to a lookup map. + +**Incorrect:** + +```svelte + + +``` + +**Correct:** + +```svelte + + + + + +``` diff --git a/apps/desktop/.agents/skills/shadcn-svelte/rules/styling.md b/apps/desktop/.agents/skills/shadcn-svelte/rules/styling.md new file mode 100644 index 0000000..733e609 --- /dev/null +++ b/apps/desktop/.agents/skills/shadcn-svelte/rules/styling.md @@ -0,0 +1,195 @@ +# Styling & Customization + +See [customization.md](../customization.md) for theming, CSS variables, and adding custom colors. + +## Contents + +- Semantic colors +- Built-in variants first +- class for layout only +- No space-x-_ / space-y-_ +- Prefer size-_ over w-_ h-\* when equal +- Prefer truncate shorthand +- No manual dark: color overrides +- Use cn() for conditional classes +- No manual z-index on overlay components + +--- + +## Semantic colors + +**Incorrect:** + +```svelte +
+

Secondary text

+
+``` + +**Correct:** + +```svelte +
+

Secondary text

+
+``` + +--- + +## No raw color values for status/state indicators + +For positive, negative, or status indicators, use Badge variants, semantic tokens like `text-destructive`, or define custom CSS variables — don't reach for raw Tailwind colors. + +**Incorrect:** + +```svelte ++20.1% +Active +-3.2% +``` + +**Correct:** + +```svelte + + ++20.1% +Active +-3.2% +``` + +If you need a success/positive color that doesn't exist as a semantic token, use a Badge variant or ask the user about adding a custom CSS variable to the theme (see [customization.md](../customization.md)). + +--- + +## Built-in variants first + +**Incorrect:** + +```svelte + + + +``` + +**Correct:** + +```svelte + + + +``` + +--- + +## class for layout only + +Use `class` for layout (e.g. `max-w-md`, `mx-auto`, `mt-4`), **not** for overriding component colors or typography. To change colors, use semantic tokens, built-in variants, or CSS variables. + +**Incorrect:** + +```svelte + + + + Dashboard + +``` + +**Correct:** + +```svelte + + + + Dashboard + +``` + +To customize a component's appearance, prefer these approaches in order: + +1. **Built-in variants** — `variant="outline"`, `variant="destructive"`, etc. +2. **Semantic color tokens** — `bg-primary`, `text-muted-foreground`. +3. **CSS variables** — define custom colors in the global CSS file (see [customization.md](../customization.md)). + +--- + +## No space-x-_ / space-y-_ + +Use `gap-*` instead. `space-y-4` → `flex flex-col gap-4`. `space-x-2` → `flex gap-2`. + +```svelte + + +
+ + + +
+``` + +--- + +## Prefer size-_ over w-_ h-\* when equal + +`size-10` not `w-10 h-10`. Applies to icons, avatars, skeletons, etc. + +--- + +## Prefer truncate shorthand + +`truncate` not `overflow-hidden text-ellipsis whitespace-nowrap`. + +--- + +## No manual dark: color overrides + +Use semantic tokens — they handle light/dark via CSS variables. `bg-background text-foreground` not `bg-white dark:bg-gray-950`. + +--- + +## Use cn() for conditional classes + +Use the `cn()` utility from the project for conditional or merged class names. Don't write manual ternaries in `class` strings. + +**Incorrect:** + +```svelte + + +
+``` + +**Correct:** + +```svelte + + +
+``` + +--- + +## No manual z-index on overlay components + +`Dialog`, `Sheet`, `Drawer`, `AlertDialog`, `DropdownMenu`, `Popover`, `Tooltip`, `HoverCard` handle their own stacking. Never add `z-50` or `z-[999]`. diff --git a/apps/desktop/.agents/skills/svelte-code-writer/SKILL.md b/apps/desktop/.agents/skills/svelte-code-writer/SKILL.md new file mode 100644 index 0000000..56d9383 --- /dev/null +++ b/apps/desktop/.agents/skills/svelte-code-writer/SKILL.md @@ -0,0 +1,64 @@ +--- +name: svelte-code-writer +description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results. +--- + +## CLI tools + +You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`: + +### List documentation sections + +```bash +npx @sveltejs/mcp list-sections +``` + +Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths. + +### Get documentation + +```bash +npx @sveltejs/mcp get-documentation ",,..." +``` + +Retrieves full documentation for specified sections. Use after `list-sections` to fetch relevant docs. + +**Example:** + +```bash +npx @sveltejs/mcp get-documentation "$state,$derived,$effect" +``` + +### Svelte autofixer + +```bash +npx @sveltejs/mcp svelte-autofixer "" [options] +``` + +Analyzes Svelte code and suggests fixes for common issues. + +**Options:** + +- `--async` - Enable async Svelte mode (default: false) +- `--svelte-version` - Target version: 4 or 5 (default: 5) + +**Examples:** + +```bash +# Analyze inline code (escape $ as \$) +npx @sveltejs/mcp svelte-autofixer '' + +# Analyze a file +npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte + +# Target Svelte 4 +npx @sveltejs/mcp svelte-autofixer ./Component.svelte --svelte-version 4 +``` + +**Important:** When passing code with runes (`$state`, `$derived`, etc.) via the terminal, escape the `$` character as `\$` to prevent shell variable substitution. + +## Workflow + +1. **Uncertain about syntax?** Run `list-sections` then `get-documentation` for relevant topics +2. **Reviewing/debugging?** Run `svelte-autofixer` on the code to detect issues +3. **Always validate** - Run `svelte-autofixer` before finalizing any Svelte component diff --git a/apps/desktop/.agents/skills/svelte-core-bestpractices/SKILL.md b/apps/desktop/.agents/skills/svelte-core-bestpractices/SKILL.md new file mode 100644 index 0000000..be855ce --- /dev/null +++ b/apps/desktop/.agents/skills/svelte-core-bestpractices/SKILL.md @@ -0,0 +1,176 @@ +--- +name: svelte-core-bestpractices +description: Guidance on writing fast, robust, modern Svelte code. Load this skill whenever in a Svelte project and asked to write/edit or analyze a Svelte component or module. Covers reactivity, event handling, styling, integration with libraries and more. +--- + +## `$state` + +Only use the `$state` rune for variables that should be _reactive_ — in other words, variables that cause an `$effect`, `$derived` or template expression to update. Everything else can be a normal variable. + +Objects and arrays (`$state({...})` or `$state([...])`) are made deeply reactive, meaning mutation will trigger updates. This has a trade-off: in exchange for fine-grained reactivity, the objects must be proxied, which has performance overhead. In cases where you're dealing with large objects that are only ever reassigned (rather than mutated), use `$state.raw` instead. This is often the case with API responses, for example. + +## `$derived` + +To compute something from state, use `$derived` rather than `$effect`: + +```js +// do this +let square = $derived(num * num); + +// don't do this +let square; + +$effect(() => { + square = num * num; +}); +``` + +> [!NOTE] `$derived` is given an expression, _not_ a function. If you need to use a function (because the expression is complex, for example) use `$derived.by`. + +Deriveds are writable — you can assign to them, just like `$state`, except that they will re-evaluate when their expression changes. + +If the derived expression is an object or array, it will be returned as-is — it is _not_ made deeply reactive. You can, however, use `$state` inside `$derived.by` in the rare cases that you need this. + +## `$effect` + +Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects. + +- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/attach.md) +- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](references/bind.md) as appropriate +- If you need to log values for debugging purposes, use [`$inspect`](references/inspect.md) +- If you need to observe something external to Svelte, use [`createSubscriber`](references/svelte-reactivity.md) + +Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server. + +## `$props` + +Treat props as though they will change. For example, values that depend on props should usually use `$derived`: + +```js +// @errors: 2451 +let { type } = $props(); + +// do this +let color = $derived(type === 'danger' ? 'red' : 'green'); + +// don't do this — `color` will not update if `type` changes +let color = type === 'danger' ? 'red' : 'green'; +``` + +## `$inspect.trace` + +`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update. + +## Events + +Any element attribute starting with `on` is treated as an event listener: + +```svelte + + + + + + + +``` + +If you need to attach listeners to `window` or `document` you can use `` and ``: + +```svelte + + +``` + +Avoid using `onMount` or `$effect` for this. + +## Snippets + +[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/render.md) tag, or passed to components as props. They must be declared within the template. + +```svelte +{#snippet greeting(name)} +

hello {name}!

+{/snippet} + +{@render greeting('world')} +``` + +> [!NOTE] Snippets declared at the top level of a component (i.e. not inside elements or blocks) can be referenced inside ` + +
...
+``` + +An element can have any number of attachments. + +## Attachment factories + +A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment (demo: + +```svelte + + + + + + +``` + +Since the `tooltip(content)` expression runs inside an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).) + +## Inline attachments + +Attachments can also be created inline (demo: + +```svelte + + { + const context = canvas.getContext('2d'); + + $effect(() => { + context.fillStyle = color; + context.fillRect(0, 0, canvas.width, canvas.height); + }); + }} +> +``` + +> [!NOTE] +> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state. + +## Conditional attachments + +Falsy values like `false` or `undefined` are treated as no attachment, enabling conditional usage: + +```svelte +
...
+``` + +## Passing attachments to components + +When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](https://svelte.dev/tutorial/svelte/spread-props/llms.txt) props onto an element, the element will receive those attachments. + +This allows you to create _wrapper components_ that augment elements (demo: + +```svelte + + + + + +``` + +```svelte + + + + + + +``` + +## Controlling when attachments re-run + +Attachments, unlike [actions](https://svelte.dev/docs/svelte/use/llms.txt), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`): + +```js +// @errors: 7006 2304 2552 +function foo(bar) { + return (node) => { + veryExpensiveSetupWork(node); + update(node, bar); + }; +} +``` + +In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect: + +```js +// @errors: 7006 2304 2552 +function foo(+++getBar+++) { + return (node) => { + veryExpensiveSetupWork(node); + ++++ $effect(() => { + update(node, getBar()); + });+++ + } +} +``` + +## Creating attachments programmatically + +To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments/llms.txt#createAttachmentKey). + +## Converting actions to attachments + +If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](https://svelte.dev/docs/svelte/svelte-attachments/llms.txt#fromAction), allowing you to (for example) use them with components. diff --git a/apps/desktop/.agents/skills/svelte-core-bestpractices/references/await-expressions.md b/apps/desktop/.agents/skills/svelte-core-bestpractices/references/await-expressions.md new file mode 100644 index 0000000..dd7d8c6 --- /dev/null +++ b/apps/desktop/.agents/skills/svelte-core-bestpractices/references/await-expressions.md @@ -0,0 +1,187 @@ +As of Svelte 5.36, you can use the `await` keyword inside your components in three places where it was previously unavailable: + +- at the top level of your component's ` + + + + +

{a} + {b} = {await add(a, b)}

+``` + + + +...if you increment `a`, the contents of the `

` will _not_ immediately update to read this — + +```html +

2 + 2 = 3

+``` + +— instead, the text will update to `2 + 2 = 4` when `add(a, b)` resolves. + +Updates can overlap — a fast update will be reflected in the UI while an earlier slow update is still ongoing. + +## Concurrency + +Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup... + +```svelte +

{await one(x)}

+

{await two(y)}

+``` + +...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential. + +This does not apply to sequential `await` expressions inside your ` + + + +{#if open} + + open = false} /> +{/if} +``` + +## Caveats + +As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum. + +## Breaking changes + +Effects run in a slightly different order when the `experimental.async` option is `true`. Specifically, _block_ effects like `{#if ...}` and `{#each ...}` now run before an `$effect.pre` or `beforeUpdate` in the same component, which means that in very rare situations. diff --git a/apps/desktop/.agents/skills/svelte-core-bestpractices/references/bind.md b/apps/desktop/.agents/skills/svelte-core-bestpractices/references/bind.md new file mode 100644 index 0000000..f847cdc --- /dev/null +++ b/apps/desktop/.agents/skills/svelte-core-bestpractices/references/bind.md @@ -0,0 +1,22 @@ +## Function bindings + +You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation: + +```svelte + value, + (v) => value = v.toLowerCase()} +/> +``` + +In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`: + +```svelte +
...
+``` + +> [!NOTE] +> Function bindings are available in Svelte 5.9.0 and newer. diff --git a/apps/desktop/.agents/skills/svelte-core-bestpractices/references/each.md b/apps/desktop/.agents/skills/svelte-core-bestpractices/references/each.md new file mode 100644 index 0000000..283b754 --- /dev/null +++ b/apps/desktop/.agents/skills/svelte-core-bestpractices/references/each.md @@ -0,0 +1,42 @@ +## Keyed each blocks + +```svelte + +{#each expression as name (key)}...{/each} +``` + +```svelte + +{#each expression as name, index (key)}...{/each} +``` + +If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to intelligently update the list when data changes by inserting, moving and deleting items, rather than adding or removing items at the end and updating the state in the middle. + +The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change. + +```svelte +{#each items as item (item.id)} +
  • {item.name} x {item.qty}
  • +{/each} + + +{#each items as item, i (item.id)} +
  • {i + 1}: {item.name} x {item.qty}
  • +{/each} +``` + +You can freely use destructuring and rest patterns in each blocks. + +```svelte +{#each items as { id, name, qty }, i (id)} +
  • {i + 1}: {name} x {qty}
  • +{/each} + +{#each objects as { id, ...rest }} +
  • {id}
  • +{/each} + +{#each items as [id, ...rest]} +
  • {id}
  • +{/each} +``` diff --git a/apps/desktop/.agents/skills/svelte-core-bestpractices/references/hydratable.md b/apps/desktop/.agents/skills/svelte-core-bestpractices/references/hydratable.md new file mode 100644 index 0000000..acc665e --- /dev/null +++ b/apps/desktop/.agents/skills/svelte-core-bestpractices/references/hydratable.md @@ -0,0 +1,103 @@ +In Svelte, when you want to render asynchronous content data on the server, you can simply `await` it. This is great! However, it comes with a pitfall: when hydrating that content on the client, Svelte has to redo the asynchronous work, which blocks hydration for however long it takes: + +```svelte + + +

    {user.name}

    +``` + +That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](https://svelte.dev/docs/kit/remote-functions/llms.txt). + +To fix the example above: + +```svelte + + +

    {user.name}

    +``` + +This API can also be used to provide access to random or time-based values that are stable between server rendering and hydration. For example, to get a random number that doesn't update on hydration: + +```ts +import { hydratable } from 'svelte'; +const rand = hydratable('random', () => Math.random()); +``` + +If you're a library author, be sure to prefix the keys of your `hydratable` values with the name of your library so that your keys don't conflict with other libraries. + +## Serialization + +All data returned from a `hydratable` function must be serializable. But this doesn't mean you're limited to JSON — Svelte uses [`devalue`](https://npmjs.com/package/devalue), which can serialize all sorts of things including `Map`, `Set`, `URL`, and `BigInt`. Check the documentation page for a full list. In addition to these, thanks to some Svelte magic, you can also fearlessly use promises: + +```svelte + + +{await promises.one} +{await promises.two} +``` + +## CSP + +`hydratable` adds an inline ` + + + +``` + + + +On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations). + +## $inspect(...).with + +`$inspect(...)` returns an object with a `with` method, which you can invoke with a callback that will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect`: + + + +```svelte + + + + +``` + + + +## $inspect.trace(...) + +This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) or a [derived](https://svelte.dev/docs/svelte/$derived/llms.txt), information will be printed to the console about which pieces of reactive state caused the effect to fire. + +```svelte + +``` + +`$inspect.trace` takes an optional first argument which will be used as the label. diff --git a/apps/desktop/.agents/skills/svelte-core-bestpractices/references/render.md b/apps/desktop/.agents/skills/svelte-core-bestpractices/references/render.md new file mode 100644 index 0000000..f8290e9 --- /dev/null +++ b/apps/desktop/.agents/skills/svelte-core-bestpractices/references/render.md @@ -0,0 +1,35 @@ +To render a [snippet](https://svelte.dev/docs/svelte/snippet/llms.txt), use a `{@render ...}` tag. + +```svelte +{#snippet sum(a, b)} +

    {a} + {b} = {a + b}

    +{/snippet} + +{@render sum(1, 2)} +{@render sum(3, 4)} +{@render sum(5, 6)} +``` + +The expression can be an identifier like `sum`, or an arbitrary JavaScript expression: + +```svelte +{@render (cool ? coolSnippet : lameSnippet)()} +``` + +## Optional snippets + +If the snippet is potentially undefined — for example, because it's an incoming prop — then you can use optional chaining to only render it when it _is_ defined: + +```svelte +{@render children?.()} +``` + +Alternatively, use an [`{#if ...}`](https://svelte.dev/docs/svelte/if/llms.txt) block with an `:else` clause to render fallback content: + +```svelte +{#if children} + {@render children()} +{:else} +

    fallback content

    +{/if} +``` diff --git a/apps/desktop/.agents/skills/svelte-core-bestpractices/references/snippet.md b/apps/desktop/.agents/skills/svelte-core-bestpractices/references/snippet.md new file mode 100644 index 0000000..4cdece3 --- /dev/null +++ b/apps/desktop/.agents/skills/svelte-core-bestpractices/references/snippet.md @@ -0,0 +1,400 @@ +```svelte + +{#snippet name()}...{/snippet} +``` + +```svelte + +{#snippet name(param1, param2, paramN)}...{/snippet} +``` + +Snippets, and [render tags](https://svelte.dev/docs/svelte/@render/llms.txt), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this... + +```svelte +{#each images as image} + {#if image.href} + +
    + {image.caption} +
    {image.caption}
    +
    +
    + {:else} +
    + {image.caption} +
    {image.caption}
    +
    + {/if} +{/each} +``` + +...you can write this: + +```svelte +{#snippet figure(image)} +
    + {image.caption} +
    {image.caption}
    +
    +{/snippet} + +{#each images as image} + {#if image.href} + + {@render figure(image)} + + {:else} + {@render figure(image)} + {/if} +{/each} +``` + +Like function declarations, snippets can have an arbitrary number of parameters, which can have default values, and you can destructure each parameter. You cannot use rest parameters, however. + +## Snippet scope + +Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the ` + +{#snippet hello(name)} +

    hello {name}! {message}!

    +{/snippet} + +{@render hello('alice')} +{@render hello('bob')} +``` + + + +...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings): + +```svelte +
    + {#snippet x()} + {#snippet y()}...{/snippet} + + + {@render y()} + {/snippet} + + + {@render y()} +
    + + +{@render x()} +``` + +Snippets can reference themselves and each other: + + + +```svelte + +{#snippet blastoff()} + 🚀 +{/snippet} + +{#snippet countdown(n)} + {#if n > 0} + {n}... + {@render countdown(n - 1)} + {:else} + {@render blastoff()} + {/if} +{/snippet} + +{@render countdown(10)} +``` + + + +## Passing snippets to components + +### Explicit props + +Within the template, snippets are values just like any other. As such, they can be passed to components as props: + + + +```svelte + + + +{#snippet header()} + fruit + qty + price + total +{/snippet} + +{#snippet row(d)} + {d.name} + {d.qty} + {d.price} + {d.qty * d.price} +{/snippet} + + +``` + +```svelte + + + +
    + {#if header} + + {@render header()} + + {/if} + + + {#each data as d} + {@render row(d)} + {/each} + +
    + + +``` + + + +Think about it like passing content instead of data to a component. The concept is similar to slots in web components. + +### Implicit props + +As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component: + + + +```svelte + + + + + {#snippet header()} + + + + + {/snippet} + + {#snippet row(d)} + + + + + {/snippet} +
    fruitqtypricetotal{d.name}{d.qty}{d.price}{d.qty * d.price}
    +``` + +```svelte + + + + + {#if header} + + {@render header()} + + {/if} + + + {#each data as d} + {@render row(d)} + {/each} + +
    + + +``` + + + +### Implicit `children` snippet + +Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet: + + + +```svelte + + + + +``` + +```svelte + + + + + +``` + + + +> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name + +### Optional snippet props + +You can declare snippet props as being optional. You can either use optional chaining to not render anything if the snippet isn't set... + +```svelte + + +{@render children?.()} +``` + +...or use an `#if` block to render fallback content: + +```svelte + + +{#if children} + {@render children()} +{:else} + fallback content +{/if} +``` + +## Typing snippets + +Snippets implement the `Snippet` interface imported from `'svelte'`: + +```svelte + +``` + +With this change, red squigglies will appear if you try and use the component without providing a `data` prop and a `row` snippet. Notice that the type argument provided to `Snippet` is a tuple, since snippets can have multiple parameters. + +We can tighten things up further by declaring a generic, so that `data` and `row` refer to the same type: + +```svelte + +``` + +## Exporting snippets + +Snippets declared at the top level of a `.svelte` file can be exported from a ` + +{@render add(1, 2)} + +``` + +```svelte + + + +{#snippet add(a, b)} + {a} + {b} = {a + b} +{/snippet} +``` + + + +> [!NOTE] +> This requires Svelte 5.5.0 or newer + +## Programmatic snippets + +Snippets can be created programmatically with the [`createRawSnippet`](https://svelte.dev/docs/svelte/svelte/llms.txt#createRawSnippet) API. This is intended for advanced use cases. + +## Snippets and slots + +In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/svelte/legacy-slots/llms.txt). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5. diff --git a/apps/desktop/.agents/skills/svelte-core-bestpractices/references/svelte-reactivity.md b/apps/desktop/.agents/skills/svelte-core-bestpractices/references/svelte-reactivity.md new file mode 100644 index 0000000..7f2cd5a --- /dev/null +++ b/apps/desktop/.agents/skills/svelte-core-bestpractices/references/svelte-reactivity.md @@ -0,0 +1,61 @@ +## createSubscriber + +
    + +Available since 5.7.0 + +
    + +Returns a `subscribe` function that integrates external event-based systems with Svelte's reactivity. +It's particularly useful for integrating with web APIs like `MediaQuery`, `IntersectionObserver`, or `WebSocket`. + +If `subscribe` is called inside an effect (including indirectly, for example inside a getter), +the `start` callback will be called with an `update` function. Whenever `update` is called, the effect re-runs. + +If `start` returns a cleanup function, it will be called when the effect is destroyed. + +If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects +are active, and the returned teardown function will only be called when all effects are destroyed. + +It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity/llms.txt#MediaQuery): + +```js +// @errors: 7031 +import { createSubscriber } from 'svelte/reactivity'; +import { on } from 'svelte/events'; + +export class MediaQuery { + #query; + #subscribe; + + constructor(query) { + this.#query = window.matchMedia(`(${query})`); + + this.#subscribe = createSubscriber((update) => { + // when the `change` event occurs, re-run any effects that read `this.current` + const off = on(this.#query, 'change', update); + + // stop listening when all the effects are destroyed + return () => off(); + }); + } + + get current() { + // This makes the getter reactive, if read in an effect + this.#subscribe(); + + // Return the current state of the query, whether or not we're in an effect + return this.#query.matches; + } +} +``` + +
    + +```dts +function createSubscriber( + start: (update: () => void) => (() => void) | void +): () => void; +``` + +
    diff --git a/apps/desktop/opencode.json b/apps/desktop/opencode.json new file mode 100644 index 0000000..4bf567f --- /dev/null +++ b/apps/desktop/opencode.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://opencode.ai/config.json", + "skills": { + "paths": [".agents/skills"] + } +} \ No newline at end of file diff --git a/apps/desktop/skills-lock.json b/apps/desktop/skills-lock.json new file mode 100644 index 0000000..3a51244 --- /dev/null +++ b/apps/desktop/skills-lock.json @@ -0,0 +1,23 @@ +{ + "version": 1, + "skills": { + "shadcn-svelte": { + "source": "huntabyte/shadcn-svelte", + "sourceType": "github", + "skillPath": "skills/shadcn-svelte/SKILL.md", + "computedHash": "d4f3f983a71466a86649985a8a7fbce47da29f44de3a9d02540bbcf3167134e0" + }, + "svelte-code-writer": { + "source": "sveltejs/ai-tools", + "sourceType": "github", + "skillPath": "plugins/claude/svelte/skills/svelte-code-writer/SKILL.md", + "computedHash": "6f78193a79906175595ce8e4ca8d726da350eb99035a9544b6553620df5673a1" + }, + "svelte-core-bestpractices": { + "source": "sveltejs/ai-tools", + "sourceType": "github", + "skillPath": "plugins/claude/svelte/skills/svelte-core-bestpractices/SKILL.md", + "computedHash": "98b4ee346ac45e13740c9f3353b604d5c6e844c96cdc8867052fa961cbdb525d" + } + } +} diff --git a/apps/desktop/src/app.css b/apps/desktop/src/app.css index 5081455..d4217b5 100644 --- a/apps/desktop/src/app.css +++ b/apps/desktop/src/app.css @@ -123,14 +123,14 @@ @apply border-border outline-ring/50; } html { - @apply font-sans; + @apply font-sans h-full overflow-hidden; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; font-feature-settings: "ss01", "cv01"; } body { - @apply bg-background text-foreground; + @apply bg-background text-foreground h-full overflow-hidden; } ::selection { background: oklch(0.5 0 0 / 25%); diff --git a/apps/desktop/src/bun/lib/monero/wallet.ts b/apps/desktop/src/bun/lib/monero/wallet.ts index 4eb2739..fa4b455 100644 --- a/apps/desktop/src/bun/lib/monero/wallet.ts +++ b/apps/desktop/src/bun/lib/monero/wallet.ts @@ -24,7 +24,7 @@ export function createMoneroWalletState(): MoneroWalletState { process: null, rpcPort: 28084, rpcUser: 'koins', - rpcPassword: crypto.randomUUID(), + rpcPassword: 'koins-local-rpc', daemonAddress: DEFAULT_DAEMON, } } @@ -97,6 +97,20 @@ export async function start( ) state.daemonAddress = daemonAddress + // Kill any existing monero-wallet-rpc on the port (stale process from dev reloads) + try { + const proc = Bun.spawnSync(['lsof', '-ti', `tcp:${state.rpcPort}`], { + stdout: 'pipe', + }) + const pids = proc.stdout.toString().trim().split('\n').filter(Boolean) + for (const pid of pids) { + try { + process.kill(Number(pid), 'SIGKILL') + console.log(`[monero] killed stale process ${pid} on port ${state.rpcPort}`) + } catch {} + } + } catch {} + console.log(`[monero] starting wallet-rpc process...`) console.log(`[monero] daemon address: ${state.daemonAddress}`) console.log(`[monero] wallet dir: ${getWalletDir()}`) @@ -356,7 +370,9 @@ export async function getTransferDetails( let t: any = null let dests: any[] = [] try { + console.log('[monero] trying get_transfer_by_txid for', txid) const result = await rawRpc(state, 'get_transfer_by_txid', { txid }) + console.log('[monero] get_transfer_by_txid result:', result ? 'has result' : 'null') if (result?.transfer) { t = result.transfer dests = @@ -365,10 +381,11 @@ export async function getTransferDetails( amount: d.amount?.toString() ?? '0', })) ?? [] } - } catch { - // get_transfer_by_txid can fail for various reasons; fall back + } catch (e: any) { + console.log('[monero] get_transfer_by_txid failed:', e?.message?.slice(0, 100)) } if (!t) { + console.log('[monero] falling back to get_transfers scan') const params: Record = { in: true, out: true, @@ -376,9 +393,11 @@ export async function getTransferDetails( pool: true, } const result = await rawRpc(state, 'get_transfers', params) + console.log('[monero] get_transfers result keys:', Object.keys(result ?? {})) let found: any = null for (const dir of ['in', 'out', 'pending', 'pool'] as const) { const transfers = result[dir] ?? [] + console.log(`[monero] ${dir}: ${transfers.length} transfers`) for (const tr of transfers) { if (tr.txid === txid) { found = { ...tr, type: dir === 'in' ? 'in' : 'out' } @@ -387,7 +406,12 @@ export async function getTransferDetails( } if (found) break } - if (!found) return null + if (!found) { + console.log('[monero] txid not found in get_transfers. Available txids:', + ['in', 'out', 'pending', 'pool'].flatMap(d => (result[d] ?? []).map((t: any) => t.txid?.slice(0, 12)))) + return null + } + console.log('[monero] found transfer in get_transfers') t = found } const isIn = t.type === 'in' diff --git a/apps/desktop/src/lib/components/ui/card/card-footer.svelte b/apps/desktop/src/lib/components/ui/card/card-footer.svelte index 17aa09a..40dc16e 100644 --- a/apps/desktop/src/lib/components/ui/card/card-footer.svelte +++ b/apps/desktop/src/lib/components/ui/card/card-footer.svelte @@ -13,7 +13,7 @@
    {@render children?.()} diff --git a/apps/desktop/src/lib/components/ui/card/card-header.svelte b/apps/desktop/src/lib/components/ui/card/card-header.svelte index 4ec42c5..9bcbd1c 100644 --- a/apps/desktop/src/lib/components/ui/card/card-header.svelte +++ b/apps/desktop/src/lib/components/ui/card/card-header.svelte @@ -14,7 +14,7 @@ bind:this={ref} data-slot="card-header" class={cn( - "gap-1 rounded-none px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]", + "gap-1 rounded-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]", className )} {...restProps} diff --git a/apps/desktop/src/lib/components/ui/card/card.svelte b/apps/desktop/src/lib/components/ui/card/card.svelte index 86226bb..1f2e7c7 100644 --- a/apps/desktop/src/lib/components/ui/card/card.svelte +++ b/apps/desktop/src/lib/components/ui/card/card.svelte @@ -15,7 +15,7 @@ bind:this={ref} data-slot="card" data-size={size} - class={cn("ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-none py-4 text-xs/relaxed ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none group/card flex flex-col", className)} + class={cn("ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-xs/relaxed ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-xl *:[img:last-child]:rounded-xl group/card flex flex-col", className)} {...restProps} > {@render children?.()} diff --git a/apps/desktop/src/routes/index.svelte b/apps/desktop/src/routes/index.svelte index 0363f59..469cce9 100644 --- a/apps/desktop/src/routes/index.svelte +++ b/apps/desktop/src/routes/index.svelte @@ -190,7 +190,7 @@ {/if}
    -
    +
    -
    -
    +
    +
    {@render children?.()}
    diff --git a/apps/desktop/src/routes/monero/index.svelte b/apps/desktop/src/routes/monero/index.svelte index 6e5e110..e370505 100644 --- a/apps/desktop/src/routes/monero/index.svelte +++ b/apps/desktop/src/routes/monero/index.svelte @@ -46,7 +46,7 @@ }) -
    +
    {#if !w.ready}

    diff --git a/apps/desktop/src/routes/monero/send.svelte b/apps/desktop/src/routes/monero/send.svelte index 101cb6f..89806a3 100644 --- a/apps/desktop/src/routes/monero/send.svelte +++ b/apps/desktop/src/routes/monero/send.svelte @@ -92,7 +92,7 @@ } -

    +
    Send XMR diff --git a/apps/desktop/src/routes/monero/tx/[hash].svelte b/apps/desktop/src/routes/monero/tx/[hash].svelte index 9fadf77..9c7b3e9 100644 --- a/apps/desktop/src/routes/monero/tx/[hash].svelte +++ b/apps/desktop/src/routes/monero/tx/[hash].svelte @@ -35,7 +35,7 @@ }) -
    +
    Transaction Details diff --git a/apps/desktop/src/routes/monero/wallet/[name].svelte b/apps/desktop/src/routes/monero/wallet/[name].svelte index feba7f4..d8f2d7a 100644 --- a/apps/desktop/src/routes/monero/wallet/[name].svelte +++ b/apps/desktop/src/routes/monero/wallet/[name].svelte @@ -16,15 +16,14 @@ import { Card, CardContent, - CardDescription, CardHeader, CardTitle, } from '$lib/components/ui/card/index.js' import ArrowDown from '@lucide/svelte/icons/arrow-down' import ArrowUp from '@lucide/svelte/icons/arrow-up' - import SettingsIcon from '@lucide/svelte/icons/settings' - import Copy from '@lucide/svelte/icons/copy' + import Send from '@lucide/svelte/icons/send' import QrCodeIcon from '@lucide/svelte/icons/qr-code' + import Plus from '@lucide/svelte/icons/plus' import Loader from '$lib/components/loader.svelte' import QrCode from '$lib/components/qr.svelte' import { navigate, route } from 'sv-router/generated' @@ -104,8 +103,7 @@ const walletName = $derived(route.params.name) - let autoRefreshTimer: ReturnType | undefined = - $state() + let autoRefreshTimer: ReturnType | undefined = $state() $effect(() => { if (w.walletOpen && !autoRefreshTimer) { @@ -146,555 +144,306 @@ }) -
    -
    +
    +
    + {#if w.loading} +
    + {:else} +

    Balance

    +

    + {w.balance} XMR +

    + {#if moneroPrice} +

    + ≈ ${(parseFloat(w.balance) * parseFloat(moneroPrice.usd)).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} +

    + {/if} + {#if w.unlockedAtomic !== w.balAtomic} +

    + Unlocked: {w.unlocked} XMR +

    + {/if} +
    +
    + + {w.connected ? 'Connected' : 'Disconnected'} + {#if w.height < w.daemonHeight} + · Scanning {w.height.toLocaleString()} / {w.daemonHeight.toLocaleString()} + {/if} + +
    + {/if} +
    + +
    + +
    + +
    +
    +
    +

    Address

    +

    + {chunkAddress(w.address).start} + {chunkAddress(w.address).mid} + ... + {chunkAddress(w.address).end} +

    +
    +
    + + + + + +
    + +

    {w.address}

    +
    +
    +
    + +
    +
    + {#if feeEstimate} +

    + Fee: ~{atomicToXmr(((BigInt(feeEstimate.fees[2]) * 2500n) / 1000n).toString())} XMR (Normal) +

    + {/if} +
    + + {#if balanceHistory.length > 1} - {@const a = chunkAddress(w.address)}
    - Monero Wallet -
    -
    -
    - - {w.connected ? 'Connected' : 'Disconnected'} - + Balance History +
    + {#each ['1D', '1W', '1M', '1Y', 'ALL'] as const as range} + + {/each}
    - - {#if w.height < w.daemonHeight} - Scanning: {w.height.toLocaleString()} / {w.daemonHeight.toLocaleString()} - {:else} - Height: {w.height.toLocaleString()} - {/if} - -
    -
    -
    -

    Address

    -

    - - {a.start} - - {a.mid} - ... - - {a.end} - -

    -
    - - - - - -
    - -

    - {w.address} -

    -
    -
    -
    - - -
    -
    -
    -

    Balance

    -
    -

    {w.balance} XMR

    - {#if moneroPrice} -

    - ≈ ${( - parseFloat(w.balance) * parseFloat(moneroPrice.usd) - ).toLocaleString(undefined, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })} -

    - {/if} -
    - {#if w.unlockedAtomic !== w.balAtomic} -

    - Unlocked: {w.unlocked} XMR -

    - {/if} - {#if feeEstimate} - {@const labels = [ - 'Unimportant', - 'Normal', - 'Elevated', - 'Priority', - ]} -
    - {#each feeEstimate.fees as f, i} - {@const feeAtomic = (BigInt(f) * 2500n) / 1000n} -

    - {labels[i]}: ~{atomicToXmr(feeAtomic.toString())} XMR -

    - {/each} -
    - {/if} -
    - - {#if moneroMnemonic} -
    -

    - Seed phrase (save this!) -

    -

    - {moneroMnemonic} -

    -
    + {#if chartData.length > 1} + + (a.balance < b.balance ? a : b)).balance, + chartData.reduce((a, b) => (a.balance > b.balance ? a : b)).balance, + ]} + props={{ + spline: { curve: curveLinear, motion: 'tween', strokeWidth: 2 }, + yAxis: {}, + xAxis: { format: (v: Date) => v.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) }, + highlight: { points: { r: 4 } }, + }}> + {#snippet tooltip()} + value.toLocaleDateString()} + valueFormatter={(value: Number) => value.toString().slice(0, 8)} /> + {/snippet} + + + {:else} +

    No data in this range

    {/if} - -
    - - - -
    + {/if} - - - - Transactions + + +
    + Transactions {#if w.accounts.length > 0} - -
    - {#each w.accounts as acct} - - {/each} -
    -
    - {/if} - - - {#if w.loading} -
    - -
    - {:else if w.txs.length === 0} -

    - No transactions found -

    - {:else} -
    - {#each w.txs as tx} +
    + {#each w.accounts as acct} + {/each} +
    + {/if} +
    + + + {#if w.loading} +
    + {:else if w.txs.length === 0} +

    No transactions yet

    + {:else} +
    + {#each w.txs as tx} + - {/each} -
    - {/if} -
    - +

    +
    +

    + {tx.direction === 'in' ? '+' : '-'}{atomicToXmr(tx.amount)} XMR +

    + + {/each} +
    + {/if} + +
    - {#if balanceHistory.length > 1} - - -
    - Balance -
    - {#each ['1D', '1W', '1M', '1Y', 'ALL'] as const as range} - - {/each} -
    -
    -
    - - {#if chartData.length > 1} - - - a.balance < b.balance ? a : b, - ).balance, - chartData.reduce((a, b) => - a.balance > b.balance ? a : b, - ).balance, - ]} - props={{ - spline: { - curve: curveLinear, - motion: 'tween', - strokeWidth: 2, - }, - yAxis: {}, - xAxis: { - format: (v: Date) => - v.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - }), - }, - highlight: { points: { r: 4 } }, - }}> - {#snippet tooltip()} - { - return value.toLocaleDateString() - }} - valueFormatter={(value: Number) => { - return value.toString().slice(0, 8) - }} /> - {/snippet} - - - {:else} -

    - No transactions in this time range. -

    - {/if} -
    -
    - {/if} - - - - - Accounts & Addresses - - {w.accounts.length} account{w.accounts.length !== 1 - ? 's' - : ''} - - - -
    + + +
    + Accounts & Addresses +
    - + Create Account - - Optional label for the new account - + Optional label for the new account
    - +
    - - + +
    - Create Subaddress - - Account index and optional label - + Account index and optional label
    - - {subaddressAccountIndex - ? `Account ${subaddressAccountIndex}` - : 'Select account'} + {subaddressAccountIndex ? `Account ${subaddressAccountIndex}` : 'Select account'} {#each w.accounts as acct} - Account {acct.index}{acct.label - ? ` - ${acct.label}` - : ''} + Account {acct.index}{acct.label ? ` - ${acct.label}` : ''} {/each}
    - +
    - - + +
    +
    +
    + +
    {#each w.accounts as account} {@const a = chunkAddress(account.primaryAddress)} -
    +
    -

    - Account {account.index} -

    -

    - {atomicToXmr(account.balance)} XMR -

    +

    Account {account.index}{account.label ? ` — ${account.label}` : ''}

    +

    {atomicToXmr(account.balance)} XMR

    -

    - - {a.start} - - {a.mid} - ... - {a.end} -

    -
    - { - subQrDialogOpen = v ? account.primaryAddress : null - }}> - - - - -
    - -

    - {account.primaryAddress} -

    -
    -
    -
    - - - +
    +

    + {a.start}{a.mid}...{a.end} +

    +
    + { subQrDialogOpen = v ? account.primaryAddress : null }}> + + + + +
    + +

    {account.primaryAddress}

    +
    +
    +
    + +
    {#if account.subaddresses.length > 1} -
    -

    - Subaddresses -

    +
    {#each account.subaddresses as sub} - {@const a = chunkAddress(sub.address)} -
    -
    -
    -
    -

    - #{sub.index}{sub.label - ? ` - ${sub.label}` - : ''} -

    -

    - {atomicToXmr(sub.balance)} XMR -

    -
    -

    - - {a.start} - - - {a.mid} - - - ... - - - {a.end} - -

    -
    - { - subQrDialogOpen = v ? sub.address : null - }}> - - - - -
    - -

    - {sub.address} -

    -
    -
    -
    - - -
    + {@const sa = chunkAddress(sub.address)} +
    +
    +

    #{sub.index}{sub.label ? ` - ${sub.label}` : ''}

    +

    {atomicToXmr(sub.balance)} XMR

    +
    +
    +

    + {sa.start}{sa.mid}...{sa.end} +

    +
    + { subQrDialogOpen = v ? sub.address : null }}> + + + + +
    + +

    {sub.address}

    +
    +
    +
    +
    @@ -703,7 +452,7 @@ {/if}
    {/each} - - -
    -
    +
    + + +
    \ No newline at end of file diff --git a/apps/desktop/src/routes/settings.svelte b/apps/desktop/src/routes/settings.svelte index 2cb5d53..aad87b2 100644 --- a/apps/desktop/src/routes/settings.svelte +++ b/apps/desktop/src/routes/settings.svelte @@ -23,7 +23,7 @@ } -
    +
    Settings diff --git a/apps/desktop/src/routes/xrp/index.svelte b/apps/desktop/src/routes/xrp/index.svelte index ea4efbd..8cbd184 100644 --- a/apps/desktop/src/routes/xrp/index.svelte +++ b/apps/desktop/src/routes/xrp/index.svelte @@ -59,7 +59,7 @@ } -
    +
    {#if !w.ready}

    diff --git a/apps/desktop/src/routes/xrp/send.svelte b/apps/desktop/src/routes/xrp/send.svelte index 46fdb79..71d8bbf 100644 --- a/apps/desktop/src/routes/xrp/send.svelte +++ b/apps/desktop/src/routes/xrp/send.svelte @@ -59,7 +59,7 @@ } -

    +
    Send XRP diff --git a/apps/desktop/src/routes/xrp/tx/[hash].svelte b/apps/desktop/src/routes/xrp/tx/[hash].svelte index 3c41faa..6c5c817 100644 --- a/apps/desktop/src/routes/xrp/tx/[hash].svelte +++ b/apps/desktop/src/routes/xrp/tx/[hash].svelte @@ -35,7 +35,7 @@ }) -
    +
    Transaction Details diff --git a/apps/desktop/src/routes/xrp/wallet/[id].svelte b/apps/desktop/src/routes/xrp/wallet/[id].svelte index 61d92c7..006f3a1 100644 --- a/apps/desktop/src/routes/xrp/wallet/[id].svelte +++ b/apps/desktop/src/routes/xrp/wallet/[id].svelte @@ -5,15 +5,14 @@ import { Card, CardContent, - CardDescription, CardHeader, CardTitle, } from '$lib/components/ui/card/index.js' import ArrowDown from '@lucide/svelte/icons/arrow-down' import ArrowUp from '@lucide/svelte/icons/arrow-up' - import SettingsIcon from '@lucide/svelte/icons/settings' import QrCodeIcon from '@lucide/svelte/icons/qr-code' import ExternalLink from '@lucide/svelte/icons/external-link' + import Send from '@lucide/svelte/icons/send' import Loader from '$lib/components/loader.svelte' import QrCode from '$lib/components/qr.svelte' import * as Dialog from '$lib/components/ui/dialog/index.js' @@ -26,14 +25,11 @@ const walletId = $derived(route.params.id) let qrDialogOpen = $state(false) - let autoRefreshTimer: ReturnType | undefined = - $state() + let autoRefreshTimer: ReturnType | undefined = $state() $effect(() => { if (w.seed && !autoRefreshTimer) { - autoRefreshTimer = setInterval(() => { - w.refresh() - }, 30000) + autoRefreshTimer = setInterval(() => w.refresh(), 30000) } else if (!w.seed && autoRefreshTimer) { clearInterval(autoRefreshTimer) autoRefreshTimer = undefined @@ -55,181 +51,122 @@ }) -
    -
    - - -
    - - {w.currentWallet?.name ?? 'XRP Wallet'} - - -
    - XRP Ledger -
    - -
    -
    -
    -

    Address

    -

    {w.address}

    -
    - - - - - -
    - -

    - {w.address} -

    -
    -
    -
    - - - -
    -
    -
    -

    Balance

    - {#if w.loadingBalance} -
    - {:else} -
    -

    {w.balance} XRP

    - {#if w.price} -

    - ≈ ${( - parseFloat(w.balance) * parseFloat(w.price) - ).toLocaleString(undefined, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })} -

    - {/if} -
    - {#if !w.funded} -

    - Account not activated — it needs at least 10 XRP to - meet the base reserve. -

    - {/if} - {#if w.fee} -

    - Network fee: ~{w.fee} XRP -

    - {/if} - {/if} -
    +
    +
    + {#if w.loadingBalance} +
    + {:else} +

    Balance

    +

    + {w.balance} XRP +

    + {#if w.price} +

    + ≈ ${(parseFloat(w.balance) * parseFloat(w.price)).toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} +

    + {/if} + {#if !w.funded} +

    + Account not activated — needs at least 10 XRP +

    + {/if} + {/if} +
    -
    - - - -
    - - +
    + +
    - - - Transactions - - - {#if w.loadingTransactions} -
    - -
    - {:else if w.transactions.length === 0} -

    - No transactions found -

    - {:else} -
    - {#each w.transactions as tx} - + + +
    + +

    {w.address}

    +
    +
    + + + +
    +
    + {#if w.fee} +

    + Network fee: ~{w.fee} XRP +

    + {/if} +
    + + + + Transactions + + + {#if w.loadingTransactions} +
    + {:else if w.transactions.length === 0} +

    No transactions yet

    + {:else} +
    + {#each w.transactions as tx} + - {/each} -
    - {/if} -
    -
    -
    -
    + {/if} +

    +
    +

    + {tx.direction === 'in' ? '+' : '-'}{tx.amount} XRP +

    + + {/each} +
    + {/if} + + +
    \ No newline at end of file