diff --git a/.agents/skills/shadcn-svelte/SKILL.md b/.agents/skills/shadcn-svelte/SKILL.md new file mode 100644 index 00000000..5e123edd --- /dev/null +++ b/.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/.agents/skills/shadcn-svelte/agents/openai.yml b/.agents/skills/shadcn-svelte/agents/openai.yml new file mode 100644 index 00000000..53b19ff1 --- /dev/null +++ b/.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/.agents/skills/shadcn-svelte/assets/shadcn-svelte-small.png b/.agents/skills/shadcn-svelte/assets/shadcn-svelte-small.png new file mode 100644 index 00000000..17a8bf5c Binary files /dev/null and b/.agents/skills/shadcn-svelte/assets/shadcn-svelte-small.png differ diff --git a/.agents/skills/shadcn-svelte/assets/shadcn-svelte.png b/.agents/skills/shadcn-svelte/assets/shadcn-svelte.png new file mode 100644 index 00000000..a35eb16e Binary files /dev/null and b/.agents/skills/shadcn-svelte/assets/shadcn-svelte.png differ diff --git a/.agents/skills/shadcn-svelte/cli.md b/.agents/skills/shadcn-svelte/cli.md new file mode 100644 index 00000000..ff19b7cb --- /dev/null +++ b/.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/.agents/skills/shadcn-svelte/customization.md b/.agents/skills/shadcn-svelte/customization.md new file mode 100644 index 00000000..c278cb52 --- /dev/null +++ b/.agents/skills/shadcn-svelte/customization.md @@ -0,0 +1,211 @@ +# 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/.agents/skills/shadcn-svelte/evals/evals.json b/.agents/skills/shadcn-svelte/evals/evals.json new file mode 100644 index 00000000..2b6067d8 --- /dev/null +++ b/.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/.agents/skills/shadcn-svelte/rules/composition.md b/.agents/skills/shadcn-svelte/rules/composition.md new file mode 100644 index 00000000..ca8ff45e --- /dev/null +++ b/.agents/skills/shadcn-svelte/rules/composition.md @@ -0,0 +1,242 @@ +# 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/.agents/skills/shadcn-svelte/rules/forms.md b/.agents/skills/shadcn-svelte/rules/forms.md new file mode 100644 index 00000000..933f53c8 --- /dev/null +++ b/.agents/skills/shadcn-svelte/rules/forms.md @@ -0,0 +1,232 @@ +# 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/.agents/skills/shadcn-svelte/rules/icons.md b/.agents/skills/shadcn-svelte/rules/icons.md new file mode 100644 index 00000000..15b54c9c --- /dev/null +++ b/.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/.agents/skills/shadcn-svelte/rules/styling.md b/.agents/skills/shadcn-svelte/rules/styling.md new file mode 100644 index 00000000..f12415cd --- /dev/null +++ b/.agents/skills/shadcn-svelte/rules/styling.md @@ -0,0 +1,193 @@ +# 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/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..fb29c281 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm check *)", + "Bash(pnpm lint *)", + "Bash(node_modules/.bin/prettier --check *)", + "Bash(npm run check *)", + "Bash(npm run lint *)", + "Bash(pnpm exec prettier --check *)", + "Bash(pnpm vitest --run *)" + ] + } +} diff --git a/.env.example b/.env.example index 97b30aac..9b86496a 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # Environment Configuration -NODE_ENV="dev" # Options: dev, test, production +NODE_ENV="development" # Options: development, test, production # Server Configuration # BASE_URL="" # Base path for reverse proxy (e.g., "/tracktor" for https://example.com/tracktor/) @@ -11,7 +11,6 @@ DB_PATH="./tracktor.db" UPLOADS_DIR="./uploads" # Application Features -BASE_URL=http://localhost:3000 TRACKTOR_DEMO_MODE=false FORCE_DATA_SEED=false TRACKTOR_DISABLE_AUTH=false @@ -29,3 +28,4 @@ BODY_SIZE_LIMIT="10mb" # Security Configuration APP_SECRET="" # Secret key for encrypting sensitive data (generate with: openssl rand -hex 32) +HTTP_MODE="http" # Set to "https" when served over TLS, so auth cookies get the secure flag diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index ceceac4e..00000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,60 +0,0 @@ -# Copilot Instructions for Tracktor - -## Project Overview - -- **Tracktor** is a SvelteKit-based web app for comprehensive vehicle management: fuel, maintenance, insurance, and regulatory tracking. -- **Frontend:** SvelteKit (Svelte 5), Tailwind CSS. - **Backend:** SvelteKit server routes, SQLite (via Drizzle ORM). -- **i18n:** Managed via [inlang](https://inlang.com) with config in `project.inlang/` and translation files in `messages/`. - -## Key Architecture & Patterns - -- **Domain logic** is organized under `src/lib/domain/` (e.g., vehicles, reminders, documents). -- **UI components** are in `src/lib/components/` (subfolders: `app/`, `feature/`, `layout/`, `ui/`). -- **API/server logic**: `src/routes/api/` and `src/server/` (config, db, services, middlewares). -- **Config/constants**: `src/lib/config/`, `src/lib/constants/`. -- **Feature toggles**: Controlled via config in `src/lib/config/` and documented in `docs/feature-toggles.md`. -- **i18n**: Use `src/lib/paraglide/` and `messages/` for translations. Reference `project.inlang/settings.json` for locale/plugin setup. - -## Developer Workflows - -- **Install dependencies:** `pnpm install` -- **Run dev server:** `pnpm dev` -- **Build for production:** `pnpm build` -- **Run tests:** `pnpm test` (see `src/__tests__/`) -- **Lint:** `pnpm lint` -- **Format:** `pnpm format` -- **Migrations:** SQL files in `migrations/`, managed by Drizzle ORM. See `drizzle.config.js`. -- **Docker:** Use `Dockerfile` for container builds. See `docs/installation.md` for details. - -## Project Conventions - -- **Type safety:** Use TypeScript throughout (`.ts`, `.svelte`). -- **State management:** Svelte stores in `src/lib/stores/`. -- **Utilities/helpers:** `src/lib/utils/`, `src/server/utils/`. -- **Testing:** Place tests in `src/__tests__/`. -- **Environment config:** See `src/lib/config/env.ts` and `docs/environment.md`. -- **Authentication:** See `docs/authentication.md` and `src/server/services/`. -- **Changelogs:** Versioned in `changelogs/`. - -## Integration Points - -- **i18n:** Managed by inlang, configured in `project.inlang/settings.json`. -- **Database:** SQLite via Drizzle ORM, config in `server/db/` and `drizzle.config.js`. -- **Feature toggles:** See `src/lib/config/` and `docs/feature-toggles.md`. - -## Examples - -- Add a new vehicle: see `src/lib/domain/vehicle.ts` and related UI in `src/lib/components/feature/`. -- Add a translation: update `messages/en.json` and reference in `project.inlang/settings.json`. -- Add a migration: create a new SQL file in `migrations/` and update Drizzle config. - -## References - -- [README.md](../README.md) — project intro, features, and docs -- [project.inlang/README.md](../project.inlang/README.md) — i18n setup -- [docs/](../docs/) — guides for installation, auth, env, toggles, contributing - ---- - -For more, see the linked documentation files. Keep changes consistent with the above structure and conventions. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd30257c..c0fbf30b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Compile Paraglide messages - run: pnpm paraglide-js compile --outdir ./src/lib/paraglide + run: pnpm paraglide-js compile --project ./i18n/project.inlang --outdir ./src/lib/paraglide - name: Run linting run: pnpm run lint diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 91873959..9d5db66d 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -40,6 +40,11 @@ jobs: tags: | type=ref,event=branch + - name: Resolve branch version + id: branch-version + run: | + echo "value=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + - name: Build and push Docker image (amd64 only) uses: docker/build-push-action@v6 with: @@ -48,6 +53,8 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: | + APP_VERSION=${{ steps.branch-version.outputs.value }} cache-from: type=gha cache-to: type=gha,mode=max platforms: linux/amd64 @@ -101,17 +108,54 @@ jobs: - name: Update package version run: npm pkg set version="${{ steps.release-version.outputs.value }}" - - name: Commit version bump + - name: Update OpenWiki docs run: | - if git diff --quiet package.json; then - exit 0 + npm install --global openwiki + openwiki code --update --print + env: + OPENWIKI_PROVIDER: openai-compatible + OPENAI_COMPATIBLE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + OPENAI_COMPATIBLE_BASE_URL: https://opencode.ai/zen/v1 + OPENWIKI_MODEL_ID: deepseek-v4-flash-free + OPENWIKI_TELEMETRY_DISABLED: true + + - name: Format updated docs + run: | + npm install --global prettier + paths="" + for p in docs/openwiki CLAUDE.md AGENTS.md; do + [ -e "$p" ] && paths="$paths $p" + done + if [ -n "$paths" ]; then + prettier --write $paths fi + - name: Commit version bump and doc updates + id: release-commit + run: | git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add package.json - git commit -m "chore: bump version to ${{ steps.release-version.outputs.value }}" + + paths="package.json" + for p in docs/openwiki CLAUDE.md AGENTS.md; do + [ -e "$p" ] && paths="$paths $p" + done + git add $paths + + if git diff --cached --quiet; then + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + git commit -m "chore: release ${{ steps.release-version.outputs.value }} (version bump + doc update)" git push origin "HEAD:${{ steps.release-branch.outputs.value }}" + echo "changed=true" >> "$GITHUB_OUTPUT" + + - name: Move release tag to the updated commit + if: steps.release-commit.outputs.changed == 'true' + run: | + git tag -f "$GITHUB_REF_NAME" HEAD + git push origin "refs/tags/$GITHUB_REF_NAME" --force - name: Set up QEMU uses: docker/setup-qemu-action@v3 diff --git a/.github/workflows/openwiki-update.yml b/.github/workflows/openwiki-update.yml new file mode 100644 index 00000000..14850ce0 --- /dev/null +++ b/.github/workflows/openwiki-update.yml @@ -0,0 +1,57 @@ +name: OpenWiki Update + +on: + # push: + # branches: + # - main + workflow_dispatch: + +concurrency: + group: openwiki-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +jobs: + openwiki-update: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install OpenWiki + run: npm install --global openwiki + + - name: Run OpenWiki + run: openwiki code --update --print + env: + OPENWIKI_PROVIDER: openai-compatible + OPENAI_COMPATIBLE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + OPENAI_COMPATIBLE_BASE_URL: https://opencode.ai/zen/v1 + OPENWIKI_MODEL_ID: deepseek-v4-flash-free + OPENWIKI_TELEMETRY_DISABLED: true + + - name: Format Generated Docs + run: npm run format + + - name: Create OpenWiki update pull request + uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7 + with: + add-paths: | + openwiki + AGENTS.md + CLAUDE.md + branch: openwiki/update + commit-message: 'docs: update OpenWiki' + title: 'docs: update OpenWiki' + body: | + Automated OpenWiki documentation update. + + This PR was generated by a push to the dev branch with openwiki workflow. diff --git a/.gitignore b/.gitignore index 7b20f999..d54df9a2 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,7 @@ uploads/ # Paraglide src/lib/paraglide project.inlang/cache/ +.opencode +opencode.json +.app/ +docs/planning diff --git a/.opencode/opencode.json b/.opencode/opencode.json deleted file mode 100644 index 28fc41db..00000000 --- a/.opencode/opencode.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://opencode.ai/config.json", - "plugin": ["@sveltejs/opencode"] -} diff --git a/.opencode/svelte.json b/.opencode/svelte.json deleted file mode 100644 index 2fe9b47c..00000000 --- a/.opencode/svelte.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "$schema": "https://svelte.dev/opencode/schema.json" -} diff --git a/.prettierignore b/.prettierignore index 5fb313c2..abd030e5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,5 +9,9 @@ bun.lockb /static/ # Ignore specific files for linting -project.inlang/.meta.json -project.inlang/README.md \ No newline at end of file +i18n/project.inlang/.meta.json +i18n/project.inlang/README.md + +# Vendored agent skills and local tool config — not ours to reformat +.agents/ +.claude/ diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 56dab39d..00000000 --- a/AGENTS.md +++ /dev/null @@ -1,144 +0,0 @@ -Tracktor repository instructions for agentic coding work. - -## Svelte MCP Usage - -Use the Svelte MCP server for any Svelte, SvelteKit, or `.svelte`/ -`.svelte.ts` work. - -### 1. `list-sections` - -Use this first to discover docs sections. Always start Svelte tasks here. - -### 2. `get-documentation` - -After `list-sections`, inspect `use_cases` and fetch all relevant sections at -once when possible. - -### 3. `svelte-autofixer` - -Use this whenever writing or editing Svelte code. Iterate until clean. - -### 4. `playground-link` - -Only use after the user asks for a playground link. Never use it for code -already written to the repo. - -## Project Snapshot - -- Tracktor is a vehicle management app built with SvelteKit, Svelte 5, Vite, - Tailwind CSS, SQLite, and Drizzle ORM. -- i18n uses inlang / Paraglide. -- TypeScript is the default, with strict checking enabled. - -## Key Paths - -- `src/routes/` pages, layouts, and endpoints. -- `src/lib/components/` reusable UI. -- `src/lib/domain/` business rules and models. -- `src/lib/services/` orchestration and data access. -- `src/lib/helper/` shared helpers. -- `src/lib/config/` app config and feature toggles. -- `src/server/` server-only helpers. -- `messages/`, `project.inlang/`, `migrations/` for i18n and DB work. - -## Core Commands - -- Install: `pnpm install` -- Dev: `pnpm dev` -- Build: `pnpm build` -- Preview: `pnpm preview` -- Check: `pnpm check` -- Watch check: `pnpm check:watch` -- Lint: `pnpm lint` -- Format: `pnpm format` -- Test: `pnpm test` -- Test watch: `pnpm test:watch` -- Coverage: `pnpm test:coverage` -- DB: `pnpm db:generate`, `pnpm db:migrate`, `pnpm db:seed` -- Clean: `pnpm clean` - -## Single-Test Commands - -- Run one file: `pnpm test -- path/to/file.test.ts` -- Run one file directly: `pnpm vitest --run path/to/file.test.ts` -- Run by name: `pnpm test -- -t "test name"` -- Run a focused pattern: `pnpm vitest --run -t "test name"` -- Run a folder: `pnpm vitest --run src/__tests__/feature` - -## Tooling Expectations - -- Use `pnpm` for package commands. -- Prefer repo scripts over raw binaries. -- Run `pnpm check` and `pnpm lint` before finishing. -- For test or logic changes, run the narrowest relevant test first. - -## Code Style - -- Use ESM only; the repo is `type: module`. -- Keep TypeScript strict; avoid `any` unless the surrounding code already uses it. -- Remove unused imports; ESLint fails on them. -- Prefer small, composable functions and clear names. -- Use `camelCase` for values/functions, `PascalCase` for components/types, - and `SCREAMING_SNAKE_CASE` for constants. -- Keep route/server code aligned with SvelteKit conventions. -- Prefer aliases from `svelte.config.js` over long relative paths. -- Group imports: external, aliases, then local. - -## Formatting Rules - -- Follow the existing Prettier + ESLint setup. -- Let `pnpm format` handle spacing, wrapping, and ordering. -- Match the repo's quote and semicolon style. -- Add comments only when something is non-obvious. - -## Svelte Conventions - -- Assume Svelte 5 semantics where the file already uses them. -- Use runes only where the project already expects them. -- Keep props and events simple. -- Prefer derived values/helpers over complex template logic. -- Split busy `.svelte` markup into smaller components. - -## TypeScript Conventions - -- Keep `strict`-compatible types in mind. -- Prefer inference for obvious locals; annotate public APIs and shared helpers. -- Use `unknown` for untrusted data. -- Narrow before accessing API/request/JSON values. -- Preserve file-name casing. - -## Error Handling - -- Fail fast on invalid inputs. -- Prefer existing response helpers over ad hoc shapes. -- Return clear, actionable error messages. -- Log enough context to debug, but never leak secrets or raw user data. -- Prefer typed branches and validation over broad catch-alls. - -## Testing Guidance - -- Keep tests close to the behavior they cover. -- Name tests clearly so `-t` and file filters stay useful. -- Add regression tests for bug fixes when practical. -- Prefer deterministic tests with minimal external dependencies. - -## Repo-Specific Rules - -- Respect boundaries under `src/lib/domain`, `src/lib/services`, and - `src/lib/components`. -- Keep translation changes in `messages/` aligned with inlang. -- Use the Drizzle migration workflow for schema changes. -- Do not edit generated or ignored directories unless required. - -## Existing Guidance To Preserve - -- Copy the intent of `.github/copilot-instructions.md`. -- If `.cursor/rules/` or `.cursorrules` exist, incorporate them too. -- Keep changes consistent with the repo architecture and docs. - -## When In Doubt - -- Read the nearest module, test, or route first. -- Match patterns in the same folder. -- Prefer the smallest safe change. -- Validate with checks/tests before handing work back. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..2eaa7586 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,87 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Svelte MCP Usage + +Use the Svelte MCP server for any Svelte, SvelteKit, or `.svelte`/`.svelte.ts` work: + +1. **`list-sections`** — call first to discover docs sections; always start Svelte tasks here. +2. **`get-documentation`** — after `list-sections`, inspect `use_cases` and fetch all relevant sections at once when possible. +3. **`svelte-autofixer`** — use whenever writing or editing Svelte code; iterate until clean. +4. **`playground-link`** — only after the user explicitly asks for one; never for code already written to the repo. + +## Project Snapshot + +Tracktor is a self-hosted vehicle management app (fuel, maintenance, insurance, PUCC/pollution certs, reminders) built with SvelteKit + Svelte 5, Tailwind CSS, SQLite (via `@libsql/client`), and Drizzle ORM. i18n uses inlang/Paraglide. TypeScript is strict throughout. + +## Core Commands + +- Install: `pnpm install` +- Dev server: `pnpm dev` (host mode) / `pnpm local` (localhost only) +- Build: `pnpm build` / Preview build: `pnpm preview` +- Type/svelte check: `pnpm check` (watch: `pnpm check:watch`) +- Lint: `pnpm lint` (eslint + prettier check) / Autofix: `pnpm format` +- Test: `pnpm test` (watch: `pnpm test:watch`, coverage: `pnpm test:coverage`) +- DB: `pnpm db:generate` (drizzle migration from schema changes), `pnpm db:migrate`, `pnpm db:seed` +- Clean: `pnpm clean` (removes build artifacts, db file, node_modules, etc.) + +Always run `pnpm check` and `pnpm lint` before considering a change finished. ESLint fails the build on unused imports/vars. + +### Single-Test Commands + +- Run one file: `pnpm vitest --run path/to/file.test.ts` +- Run by test name: `pnpm vitest --run -t "test name"` +- Run a folder: `pnpm vitest --run src/__tests__/feature` + +Note: test coverage is currently minimal (essentially a placeholder in `src/__tests__/index.test.ts`) — don't assume extensive existing test patterns exist for a given module. + +## Architecture + +### Path aliases (defined in `svelte.config.js`) + +`$lib` → `src/lib`, `$ui` → `src/lib/components/ui`, `$appui` → `src/lib/components/app`, `$layout` → `src/lib/components/layout`, `$feature` → `src/lib/components/feature`, `$stores` → `src/lib/stores`, `$services` → `src/lib/services`, `$helper` → `src/lib/helper`, `$dashboard` → `src/lib/components/dashboard`, `$server` → `src/server`. Prefer these aliases over long relative paths. + +### Two parallel "service" layers — don't confuse them + +- **`src/lib/services/*.service.ts`** — browser/client-side code. Calls the app's own `/api/*` REST endpoints via `$lib/helper/api.helper` (`apiClient`) and returns a `Response` shape (`{ status: 'OK' | 'ERROR', data?, error? }`). Used from `.svelte` pages/components. +- **`src/server/services/*Service.ts`** — server-only code. Talks directly to the Drizzle DB (`src/server/db`), does business logic, and is called from `+server.ts` route handlers (or `+page.server.ts`). Never import these from client-facing `.svelte` code. + +`src/lib/domain/*` holds shared types/models and pure business rules (e.g. `domain/fuel/mileage.ts` mileage math) usable from both client and server code. + +### Request pipeline + +`src/hooks.server.ts` runs one-time app init (ensure directories, `initializeDatabase()` — runs Drizzle migrations, seeding, then patches — and starts the notification scheduler cron) and wires a `MiddlewareChain` (`src/server/middlewares`, chain-of-responsibility pattern via `BaseMiddleware`/`setNext`): `CorsMiddleware` → `AuthMiddleware` → `RateLimitMiddleware` → `LoggingMiddleware`. `AuthMiddleware` checks session cookie/Bearer token against `authService`, bypassing `/api/auth`, `/api/health`, `/api/config/branding`, and everything when `TRACKTOR_DISABLE_AUTH`/`env.DISABLE_AUTH` is set. + +### Data layer + +- Drizzle schema lives in `src/server/db/schema/*.ts` (one file per domain entity: `vehicle`, `fuel-log`, `insurance`, `maintenance-logs`, `pucc`, `reminder`, `notification`, `notification-provider`, `config`, `audit`, `auth`). +- SQLite dialect, `snake_case` column casing, migrations generated to `src/server/db/migrations` via `pnpm db:generate` — never hand-edit generated migrations. +- One-off data fixups live under `src/server/db/patch` and run via `applyPatches()` at startup, after migrations/seeding. + +### Routes + +- `src/routes/(app)/*` — authenticated app pages (dashboard, fuel, maintenance, insurance, pollution, reminders, vehicles, expenses, reports, settings), sharing the app shell (`AppSidebar`, `+layout.svelte`). +- `src/routes/(auth)/*` — login/register, outside the app shell. +- `src/routes/api/*` — REST endpoints as `+server.ts` files; vehicle-scoped resources nest under `api/vehicles/[id]/...`. + +The app recently moved from a `/dashboard/*` nested-route structure to top-level feature routes (`/fuel`, `/insurance`, `/maintenance`, `/pollution`, `/reminders`) with a single sidebar app shell — the old `dashboard/(feature)` routes are being removed in favor of this flatter structure with fleet-wide/vehicle-selector support baked into each page. + +### UI components + +`src/lib/components/ui` is a shadcn-svelte install (`components.json`, baseColor `zinc`, registry `shadcn-svelte.com`) built on `bits-ui` + `tailwind-variants` — treat it as generated/vendored (it's excluded from lint) and prefer composing it from `$feature`/`$dashboard`/`$appui` rather than editing it directly. Charts use `layerchart`/`d3-*`. + +### Feature toggles + +Features (Fuel Log, Maintenance, PUCC, Reminders, Insurance, Overview) are stored as string `'true'/'false'` values in the `configs` table under keys like `featureFuelLog`, and gated in the UI with the `FeatureGate` component (`feature="fuelLog"` or `requireAll={[...]}`). See `docs/feature-toggles.md`. + +## Code Style + +- ESM only (`"type": "module"`); use `pnpm`, not raw `npm`/`yarn`/`npx`. +- `camelCase` for values/functions, `PascalCase` for components/types, `SCREAMING_SNAKE_CASE` for constants. +- Group imports: external, then aliases, then local relative. +- Assume Svelte 5 runes mode where a file already uses it; keep props/events simple; prefer derived values/helpers over complex template logic; split busy `.svelte` markup into smaller components. +- Fail fast on invalid inputs; prefer existing response/error helpers (`$server/exceptions/AppError`, `service-response.helper.ts`) over ad hoc shapes; narrow `unknown` before accessing API/request/JSON values. +- Respect the boundaries between `src/lib/domain`, `src/lib/services`, `src/server/services`, and `src/lib/components` described above. +- Keep translation changes in `messages/` aligned with inlang; regenerate via the Paraglide vite plugin (runs automatically through `vite dev`/`vite build`). +- Add comments only when something is genuinely non-obvious. diff --git a/Dockerfile b/Dockerfile index a92c1936..d46f0ef5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,16 @@ # Stage 1: Build the application FROM node:22-alpine AS builder +ARG APP_VERSION + # Set working directory WORKDIR /app # Install pnpm RUN npm install -g pnpm -# Copy package files -COPY package.json pnpm-lock.yaml ./ +# Copy package files and pnpm workspace config +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ # Install dependencies RUN pnpm install --frozen-lockfile @@ -25,6 +27,8 @@ RUN pnpm prune --prod # Stage 2: Create the production image FROM node:22-alpine +ARG APP_VERSION + # Set working directory WORKDIR /app @@ -32,7 +36,7 @@ WORKDIR /app COPY --from=builder /app/build ./build COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./package.json -COPY --from=builder /app/migrations ./migrations +COPY --from=builder /app/src/server/db/migrations ./migrations # Expose the port the app runs on EXPOSE 3000 @@ -43,6 +47,7 @@ RUN mkdir -p /data/logs RUN mkdir -p /data/uploads # Set environment variables +ENV APP_VERSION=${APP_VERSION} ENV NODE_ENV=production ENV HOST=0.0.0.0 ENV PORT=3000 diff --git a/README.md b/README.md index 0a2c772c..8ad5399d 100644 --- a/README.md +++ b/README.md @@ -17,74 +17,73 @@
-

- Tracktor is an open-source web application for comprehensive vehicle management.
- Easily track ⛽ fuel consumption, 🛠️ maintenance, 🛡️ insurance, and 📄 regulatory documents for all your vehicles in one place. -

+If you own more than one vehicle, you know the drill: fuel receipts in a drawer, insurance PDFs buried in email, a maintenance date you meant to write down somewhere. Tracktor is a self-hosted app that keeps all of that in one place — fuel logs, service history, insurance and pollution certs, reminders before they lapse, and a dashboard that actually shows you what's going on across your fleet. + +Run it on a Raspberry Pi, a home server, or a $5 VPS. Your data stays yours.

- - - - Dashboard - + Dashboard

-## ✨ Features +## What it does -- 🚗 **Vehicle Management:** Add, edit, and manage multiple vehicles with support for different fuel types. -- ⛽ **Fuel Tracking:** Log fuel refills and monitor fuel efficiency over time. -- 🛠️ **Maintenance Log:** Record and view maintenance history for each vehicle. -- 📄 **Document Tracking:** Track insurance and pollution certificates with renewal dates. -- 🔔 **Reminders:** Set and manage reminders for maintenance, renewals, and other vehicle events. -- 📊 **Dashboard:** Visualize key metrics, analytics, and upcoming renewals. -- 🔒 **User Authentication:** Secure username/password authentication with session management. -- 🎨 **Feature Toggles:** Enable or disable specific features based on your needs. +- **Garage** — Add and manage multiple vehicles, each with its own fuel type and history. +- **Fuel tracking** — Log every fill-up and watch your mileage/efficiency trends over time. +- **Maintenance log** — Keep a full service history per vehicle, and know what's coming up next. +- **Compliance** — Track insurance and pollution (PUCC) certificates, with renewal dates that don't sneak up on you. +- **Reminders** — Get nudged before something expires or a service is due. +- **Expenses & reports** — See what your vehicles actually cost you. +- **Dashboard** — A fleet-wide overview with widgets you can rearrange to your liking. +- **Auth & feature toggles** — Username/password login with sessions, and the ability to turn off features you don't need. +- **10 languages** — English, Hindi, Spanish, French, German, Italian, Arabic, Romanian, Hungarian, and Finnish. -## 🛠️ Tech Stack +## Tech stack -- 🎨 **Frontend:** SvelteKit, Tailwind CSS, Svelte 5 -- 🖥️ **Backend:** SvelteKit Server Routes -- 🗄️ **Database:** SQLite with Drizzle ORM -- 🐳 **Deployment:** Docker & Docker Compose +SvelteKit (Svelte 5) + Tailwind CSS on the frontend, SvelteKit server routes on the backend, SQLite via Drizzle ORM for storage, shipped as a Docker image. -## 🚀 Getting Started +## Getting started -Refer to the [installation guide](./docs/installation.md) for setup instructions. +The fastest way to try Tracktor is Docker Compose: -## 📚 Documentation +```yaml +services: + app: + image: ghcr.io/javedh-dev/tracktor:latest + container_name: tracktor-app + restart: always + ports: + - '3333:3000' + volumes: + - tracktor-data:/data +volumes: + tracktor-data: +``` -- [Installation Guide](./docs/installation.md) - Setup instructions for Docker, local development, and Proxmox LXC -- [Authentication](./docs/authentication.md) - User authentication and session management -- [Environment Variables](./docs/environment.md) - Configuration options -- [Feature Toggles](./docs/feature-toggles.md) - Customizing enabled features -- [Contributing](./docs/contributing.md) - Guidelines for contributing +```bash +docker-compose up -d +``` -## 🤝 Contributing +Then open `http://:3333`. -Contributions are welcome! Please read the [contributing guidelines](./docs/contributing.md) before submitting a pull request. +For local development, Proxmox LXC setup, reverse proxies, and every configuration option, see the [installation guide](./docs/installation.md). -Consider supporting this project by giving it a star ⭐ or [sponsoring](https://github.com/sponsors/javedh-dev). +## Documentation -## 📄 License +- [Installation Guide](./docs/installation.md) — Docker, local dev, Proxmox LXC +- [Environment Variables](./docs/environment.md) — every config option, explained +- [Authentication](./docs/authentication.md) — how login and sessions work +- [Feature Toggles](./docs/feature-toggles.md) — turning features on/off +- [Contributing](./docs/contributing.md) — how to get involved -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. +## Contributing -## 📊 Repository activity +PRs and issues are welcome — read the [contributing guide](./docs/contributing.md) first. If Tracktor is useful to you, a star ⭐ or a [sponsorship](https://github.com/sponsors/javedh-dev) helps keep it going. -![Activities](https://repobeats.axiom.co/api/embed/d41931a72a5373ee0d2073e72279862171468023.svg 'Repobeats analytics image') +## License -## ⭐ Star History - - - - - - Star History Chart - - +MIT — see [LICENSE](LICENSE). -## 🤝 Contributors +## Contributors diff --git a/changelogs/1.0.0.md b/changelogs/1.0.0.md deleted file mode 100644 index f7b9f865..00000000 --- a/changelogs/1.0.0.md +++ /dev/null @@ -1,46 +0,0 @@ -# Release Notes – v1.0.0 (2025-12-17) - -## Major Changes - -- Migrated from separate backend/frontend to a full-stack SvelteKit app. -- Switched package management from npm to pnpm. -- Refactored middleware using the Chain of Responsibility pattern. -- Removed extra controller layer and cleaned up middlewares. -- Removed all shadcn components and updated UI components. -- Removed SSR and improved UI. -- Replaced common package APIResponse and updated imports. -- Added user/password authentication (single user mode). -- Added migration script and demo user seeding for auth. -- Dropped legacy auth table and removed crypto dependency from frontend forms. - -## Features & Improvements - -- Added support for attachments for all logs and entries. -- Added alerts for expiry of PUCC and insurance. -- Added functionality to export/import data in JSON format. -- Added file upload limitation. -- Added HTTP mode and defaulted logging requests as true. -- Added preview for attached files and image upload improvements. -- Added --host to preview command. -- Created new Dockerfile and improved Docker support (fixed CORS). -- Refactored environment variable handling (separate client/server). -- Removed dotenvx dependency and updated build configuration. -- Upgraded Node.js to 24 and pnpm to 10 in CI workflow. -- Updated GitHub Actions and improved CI/CD. - -## Bug Fixes - -- Fixed data seeding and data table rendering issues. -- Fixed warnings, linting issues, and broken components. -- Fixed error in mileage calculation. -- Fixed editing in attachment and form submitting issues. -- Fixed broken env for demo mode and improved logging. -- Fixed auth check and made auth single user. -- Fixed loggings and added DB patch step in initialization. - -## Other - -- Removed tests and updated environment variables. -- General cleanup and code quality improvements. - -For a full list of changes, see the [compare view](https://github.com/javedh-dev/tracktor/compare/0.5.1...1.0.0). diff --git a/changelogs/1.1.0.md b/changelogs/1.1.0.md deleted file mode 100644 index be793677..00000000 --- a/changelogs/1.1.0.md +++ /dev/null @@ -1,108 +0,0 @@ -# Release Notes – v1.1.0 (2025-12-30) - -## Overview - -v1.1.0 introduces comprehensive internationalization (i18n) support across the entire application, enhanced customization capabilities, improved vehicle management, and significant UI/UX refinements. This release also includes important bug fixes and code quality improvements. - -## Major Features - -### Internationalization (i18n) & Localization Support - -- Complete i18n infrastructure implementation for multilingual support -- Localized messages across all UI components, forms, and notifications -- Support for English, German, Spanish, French, and Hindi -- Dynamic message functions for: - - Fuel types and fuel log management - - Recurrence and reminder labels - - Insurance and pollution certificate expiry alerts - - Maintenance and pollution tracking - - Custom field labels and descriptions - - Delete confirmation dialogs and form validations -- Consistent localization across dashboard, settings, vehicle details, and all log management sections - -### Data Import & Recurrence Management - -- CSV file import support for bulk fuel log imports -- Recurrence support for insurances, PUCC (Technical Examination), and reminders -- Automated next due date calculation for insurances and pollution certificates -- Enhanced tracking for recurring maintenance and compliance activities - -### Advanced Customization & Configuration System - -- Custom styling support with configurable CSS classes for UI elements -- Feature flag system for granular enable/disable functionality across features -- Configuration category management for organizing settings -- Enhanced settings interface with tabbed structure for better organization -- Support for custom fields in vehicles for extended data capture - -### Vehicle Management Enhancements - -- Image upload and management improvements with default image support -- Option to remove existing vehicle images during edits -- Proper image preservation and state management in forms -- Enhanced vehicle details presentation with localized information - -### UI/UX Improvements - -- Mobile-optimized tab navigation with improved responsiveness -- Improved file upload experience with refactored FileDropZone component -- Better file preview functionality on mobile devices -- Enhanced color consistency across VehicleCard, AppSheet, Header, and Notifications components -- Reintroduced attachment field in FuelLogForm for better usability -- Precomposed Apple touch icon for improved PWA experience - -## Bug Fixes & Improvements - -### Critical Fixes - -- Fixed authentication disabled issue preventing user login -- Fixed critical bug preventing fuel log creation -- Corrected file drop zone ID bug affecting file uploads - -### UI/UX Fixes - -- Fixed mobile file preview rendering issues -- Improved form error handling and validation messaging -- Enhanced error recovery in form submission handlers - -### Code Quality & Performance - -- Refactored addAction handlers to remove unnecessary parameters and reduce complexity -- Refactored components to leverage localized message functions, improving maintainability -- Removed unused FeatureGateExample component and unnecessary dashboard page -- Updated all dependencies to latest stable versions -- Improved app directory initialization on startup -- Cleaned up environment variable configuration - -### DevOps & Infrastructure - -- Upgraded Docker Build-Push Action to v6 for improved CI/CD reliability -- Enhanced Docker configuration and CORS handling - -## Technical Changes - -### Component & Architecture Refactoring - -- Updated FuelLogForm, FuelLogList, and FuelLogTab to use message localization -- Refactored MaintenanceLogList, PollutionCertificateForm, and related components for localization -- Enhanced AreaChart, CostChart, and MileageChart with localized titles -- Improved Notifications component with localized notification text -- Better separation of concerns with message functions handling all text content - -### Data Management - -- Refactored technical examination schema (previously separate, now integrated into recurrence support) -- Improved database initialization process -- Enhanced logging for better debugging and monitoring - -## Migration Notes - -- No breaking changes from v1.0.0 -- Existing data structures remain compatible -- New localization system is transparent to users - application automatically uses system language preferences - -## Known Limitations - -- None reported in this release - -For a full list of commits and changes, see the [commit history](https://github.com/javedh-dev/tracktor/compare/1.0.0...1.1.0). diff --git a/changelogs/1.2.0.md b/changelogs/1.2.0.md deleted file mode 100644 index 892f547c..00000000 --- a/changelogs/1.2.0.md +++ /dev/null @@ -1,112 +0,0 @@ -# Release Notes – v1.2.0 (2026-01-21) - -## Overview - -v1.2.0 brings significant enhancements to settings management, improved localization support with new languages, enhanced form flexibility, and better deployment capabilities. This release focuses on user customization, accessibility improvements, and making Tracktor more adaptable to diverse deployment scenarios and user preferences. - -## Major Features - -### Enhanced Settings Management - -- **Settings:** Comprehensive settings interface with organized configuration options -- **Fuel Unit Configuration:** Configurable fuel units for different fuel types (CNG, LPG) -- **Mileage Unit Formats:** Support for both distance-per-fuel (km/L, mpg) and fuel-per-distance (L/100km) display formats -- **Auto-complete Inputs:** Improved form experience with auto-complete support for common fields -- **Timezone Management:** Canonical IANA timezone list for consistent cross-platform time handling - -### Expanded Internationalization Support - -- **New Languages:** - - Italian (it) localization added with complete translations - - Hungarian (hu) localization support - - Arabic (ar) localization with RTL (Right-to-Left) support -- **RTL Language Support:** Enhanced UI components to properly handle right-to-left languages -- **Improved i18n Infrastructure:** - - Added `languageTags` and `sourceLanguageTag` to settings for better localization support - - Enhanced submit button with localized login button text - - Updated message handling across all forms and components - -### Flexible Data Input - -- **Optional Fields:** Made odometer and fuel volume optional in fuel logs for greater flexibility -- **Extended Vehicle Years:** Updated vehicle year constraint to support vehicles from 1900 onwards -- **Validation Improvements:** Auto-switch to tabs containing validation errors for better user feedback - -### Deployment & Infrastructure - -- **Reverse Proxy Support:** Added base URL configuration for deployment behind reverse proxies -- **Enhanced Documentation:** New comprehensive guide for reverse proxy deployment scenarios - -## UI/UX Improvements - -### Enhanced Components - -- **Pagination:** Added pagination ellipsis in AppTable component for improved navigation of large datasets -- **Dialog Positioning:** Improved positioning and styling for dialog components and vehicle details modal -- **Import Layout:** Refactored import button layout with enhanced loading state handling in FuelLogImportForm -- **Form Feedback:** Better error messaging and auto-navigation to fields with validation errors - -### Styling & Accessibility - -- **RTL Language Support:** Proper text alignment and layout for right-to-left languages -- **Consistent Formatting:** Improved submit button formatting across all forms -- **Mobile Responsiveness:** Enhanced mobile experience for settings and configuration screens - -## Bug Fixes & Improvements - -### Critical Fixes - -- Fixed localization support with proper language tag configuration -- Corrected Italian translation typos in recurrence messages -- Fixed formatting and linting issues across the codebase - -### Code Quality - -- Upgraded all dependencies to latest stable versions (performed twice during release cycle) -- Removed unnecessary dependencies for improved bundle size -- Fixed various formatting and linting errors for better code maintainability -- Enhanced TypeScript type safety across components - -## Technical Changes - -### Database Migrations - -- **20260120190621:** Made odometer and volume fields optional in fuel_logs table -- **20260120190820:** Added configuration entries for: - - Mileage unit format (distance-per-fuel vs fuel-per-distance) - - LPG fuel unit configuration (litre) - - CNG fuel unit configuration (kilogram) - -### Configuration System - -- Enhanced settings schema to support fuel type-specific unit configurations -- Added mileage display format preferences -- Improved configuration category organization - -### Localization Files - -- Added complete Italian translation file (messages/it.json) -- Added complete Hungarian translation file (messages/hu.json) -- Updated Arabic translation file with RTL support enhancements -- Fixed translation inconsistencies across all language files - -## Migration Notes - -- No breaking changes from v1.1.0 -- Optional fields in fuel logs maintain backward compatibility -- New configuration entries are automatically seeded during migration -- Existing data remains fully compatible with new optional field structure - -## Contributors - -Special thanks to: - -- @albanobattistella for Italian localization -- @daunera for settings modal implementation and fuel unit configurations -- All community members who reported issues and provided feedback - -## Known Issues - -- None reported at release time - -For detailed commit history, see the [compare view](https://github.com/javedh-dev/tracktor/compare/v1.1.0...v1.2.0). diff --git a/changelogs/1.3.0.md b/changelogs/1.3.0.md deleted file mode 100644 index 1bdb9aa6..00000000 --- a/changelogs/1.3.0.md +++ /dev/null @@ -1,115 +0,0 @@ -# Release Notes – v1.3.0 (2026-03-19) - -## Overview - -v1.3.0 focuses on the settings experience, notification delivery controls, clearer overview charts, and a broad cleanup of shared helpers and notification flows. It also improves demo seed data so mileage and cost trends look more realistic over time. - -## Major Features - -### Settings UX Improvements - -- Expanded all settings accordions by default for quicker access -- Made settings forms more responsive with two- and three-column layouts where space allows -- Added a compact switch-based style for feature flags -- Updated notification provider forms to default new providers to enabled -- Simplified channel subscription controls in the provider dialog -- Extracted reusable settings sections, field helpers, and display blocks -- Reused shared tab shell and form composition patterns in settings - -### Notification Delivery Controls - -- Added a toggle to enable or disable scheduled notification delivery -- Kept the delivery schedule tied to the notification settings state -- Disabled schedule inputs automatically when scheduling is turned off -- Added webhook and Gotify providers alongside email -- Improved provider toggling, cron scheduling, and notification send templates -- Allowed editing providers without exposing keys/tokens - -### Overview Charts - -- Added an average reference line to mileage and cost graphs -- Displayed the average as a top-right label with unit-aware formatting -- Formatted tooltip values with the correct units for mileage and currency -- Added a unit-aware average formatter for chart tooltips and labels -- Improved the chart presentation with clearer dashed average lines - -### Demo Seed Data - -- Updated seeded mileage values to progress more naturally over time -- Added small mileage deviations and realistic fuel cost variation -- Made the overall seed data better reflect real-world usage patterns -- Refined seeded notifications and maintenance history for more natural trends - -## Configuration Changes - -- Added `notificationProcessingEnabled` to control scheduled notification delivery -- Kept `notificationProcessingSchedule` as the cron expression for delivery timing -- Added default config values for LPG and CNG fuel units -- Preserved mileage unit format settings for distance-per-fuel and fuel-per-distance -- Expanded settings schema and defaults for feature flags and notification delivery -- Updated chart formatting helpers to support unit-aware mileage and currency display -- Added shared config handling and merge helpers for notification provider settings -- Seeded the new config entries automatically for demo setups - -## Environment/Runtime Changes - -- Demo seeding now generates more realistic mileage and cost trends -- Notification scheduler respects the new enabled/disabled config state -- Overview charts now format tooltip and average values using app units -- The app now keeps chart and settings formatting aligned with the active locale/config - -## UI/UX Improvements - -- Compact provider channel subscriptions in the add/edit provider dialog -- Better chart labeling and readability in the overview section -- More consistent settings layout across personalization, units, and feature flags -- Better mobile behavior and tighter spacing across settings and dialogs -- Improved loading skeletons and shared record card layouts across the UI - -## Architecture & Shared Helpers - -- Consolidated route error helpers and standardized backend service responses -- Added typed payload helpers for domain and service layers -- Reduced shared store and form `any` usage -- Extracted reusable table, skeleton, and formatter helpers -- Reused shared resource state and feature card layouts across the UI -- Improved notification provider config merge and service date helpers -- Added helper reuse across vehicle, fuel, maintenance, insurance, and reminders - -## Localization & Messaging - -- Continued moving hardcoded UI text into i18n message functions -- Added or refined translated messages for settings, notifications, and charts -- Improved localized labels across dashboard, forms, and notifications - -## Developer Experience - -- Added MCP support for the repo's Svelte workflow -- Upgraded dependencies and fixed follow-up lint/check issues -- Cleaned up formatting, typing, and shared abstractions across the codebase - -## Bug Fixes & Improvements - -- Fixed reactive binding issues in settings forms so inputs update correctly -- Fixed notification delivery scheduling state so it no longer re-enables unexpectedly after save -- Improved unit display for mileage and cost values in chart tooltips and labels -- Fixed settings accordions to stay expanded by default -- Fixed provider add/edit flow to keep new providers enabled by default -- Fixed fuel and maintenance sorting when records share dates -- Fixed NaN-prone calculations and lint issues carried over from refactors - -## Migration Notes - -- No breaking changes were introduced -- Existing settings and data remain compatible - -## Environment Variables - -- No new environment variables were required for this release -- Existing runtime behavior continues to use the current app configuration and demo flags - -## Known Issues - -- None reported at release time - -For detailed commit history, see the [compare view](https://github.com/javedh-dev/tracktor/compare/v1.2.0...v1.3.0). diff --git a/docs/environment.md b/docs/environment.md index 8792b232..fed295a3 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -8,8 +8,8 @@ Configure Tracktor by setting environment variables in a `.env` file in the root Application environment. -- **Values**: `dev`, `production`, `test` -- **Default**: `dev` +- **Values**: `development`, `production`, `test` +- **Default**: `development` ### HOST @@ -70,6 +70,21 @@ Disable authentication (not recommended for production). - **Values**: `true`, `false` - **Default**: `false` +### APP_SECRET + +Secret key used to encrypt stored credentials (e.g. notification provider passwords). Required once you configure any notification provider — the app refuses to store or read credentials without it. + +- **Values**: Any random string +- **Default**: none (must be set to use notification providers) +- **Generate**: `openssl rand -hex 32` + +### HTTP_MODE + +Controls whether auth cookies are marked `secure`. Set to `https` when Tracktor is served over HTTPS (e.g. behind a TLS-terminating reverse proxy) so the session cookie gets the `secure` flag. + +- **Values**: `http`, `https` +- **Default**: `http` + ## Demo Mode ### TRACKTOR_DEMO_MODE @@ -113,8 +128,8 @@ Directory for log files. Limits the upload size of image/documents. -- **values**: Any number (size in bytes) -- **Defaut**: 512Kb +- **Values**: Any number (size in bytes) +- **Default**: 512Kb - **Docker**: Infinity (removes restriction) ## Notes diff --git a/docs/feature-toggles.md b/docs/feature-toggles.md index 2312b123..fdc74b04 100644 --- a/docs/feature-toggles.md +++ b/docs/feature-toggles.md @@ -17,10 +17,9 @@ The following features can be toggled: 1. **Fuel Log** - Track and manage fuel consumption and refueling history 2. **Maintenance** - Record and schedule vehicle maintenance activities -3. **PUCC** - Manage Pollution Under Control Certificate records +3. **Compliance** - Manage insurance and Pollution Under Control Certificate (PUCC) records 4. **Reminders** - Set and receive reminders for important vehicle events -5. **Insurance** - Manage vehicle insurance details and renewals -6. **Overview** - Display overview dashboard with key vehicle metrics +5. **Overview** - Display overview dashboard with key vehicle metrics ## Configuration @@ -32,9 +31,8 @@ Feature toggles are stored in the `configs` table with the following keys: - `featureFuelLog` - `featureMaintenance` -- `featurePucc` +- `featureCompliance` - `featureReminders` -- `featureInsurance` - `featureOverview` Values are stored as strings: `'true'` or `'false'` @@ -61,7 +59,7 @@ The easiest way to conditionally show/hide components based on feature flags: - + @@ -104,7 +102,7 @@ if (areAllFeaturesEnabled([Features.FUEL_LOG, Features.MAINTENANCE])) { } // Check if any feature is enabled -if (isAnyFeatureEnabled([Features.INSURANCE, Features.PUCC])) { +if (isAnyFeatureEnabled([Features.COMPLIANCE, Features.REMINDERS])) { // Show documents section } ``` @@ -128,7 +126,7 @@ You can use feature flags to conditionally show/hide navigation items: {#if configStore.configs.featureFuelLog} - Fuel Log + Fuel Log {/if} {#if configStore.configs.featureMaintenance} diff --git a/docs/i18n.md b/docs/i18n.md index ca0b6d7e..b4d86f9e 100644 --- a/docs/i18n.md +++ b/docs/i18n.md @@ -22,7 +22,7 @@ Tracktor uses Paraglide (inlang) with the Svelte 5 addon for runtime-localized U ## Adding a new language -1. Add the language in your Paraglide source/messages and regenerate the compiled outputs under `src/lib/paraglide/messages/`. +1. Add the language in your Paraglide source/messages `i18n/messages` and regenerate the compiled outputs under `src/lib/paraglide/messages/`. 2. Ensure the new language code is included in Paraglide's generated `locales` array (in `src/lib/paraglide/runtime.js`). 3. Optionally add a human-readable label in `src/lib/components/feature/settings/SettingsForm.svelte` in the `localeLabels` map. 4. Provide translations for your messages via inlang tooling (VS Code Sherlock, Fink, etc.). diff --git a/docs/images/intro.gif b/docs/images/intro.gif new file mode 100644 index 00000000..73ca6067 Binary files /dev/null and b/docs/images/intro.gif differ diff --git a/docs/images/tracktor-demo.gif b/docs/images/tracktor-demo.gif new file mode 100644 index 00000000..3a28a73e Binary files /dev/null and b/docs/images/tracktor-demo.gif differ diff --git a/docs/installation.md b/docs/installation.md index 48713c61..f76bd319 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -171,7 +171,7 @@ For Proxmox LXC container setup, use [Community-Scripts](https://community-scrip ### Prerequisites -- Node.js (v18 or higher) +- Node.js (v22 or higher) - pnpm package manager ### Steps diff --git a/docs/openwiki/.last-update.json b/docs/openwiki/.last-update.json new file mode 100644 index 00000000..8fa5577e --- /dev/null +++ b/docs/openwiki/.last-update.json @@ -0,0 +1,6 @@ +{ + "updatedAt": "2026-07-22T09:01:32.532Z", + "command": "update", + "gitHead": "96c4e30bad88b741b42ff821162c671070c8c941", + "model": "deepseek-v4-flash-free" +} diff --git a/docs/openwiki/INSTRUCTIONS.md b/docs/openwiki/INSTRUCTIONS.md new file mode 100644 index 00000000..b0e3e331 --- /dev/null +++ b/docs/openwiki/INSTRUCTIONS.md @@ -0,0 +1 @@ +A code wiki for this local repository. Prioritize a concise quickstart, architecture overview, source map, key workflows, domain concepts, operations/runbook notes, testing guidance, and integration points. Inspect git history to understand reasoning behind code changes and the progression of the repository. Keep pages grounded in the repository structure and recent code changes. Prefer practical navigation for engineers over generic summaries. diff --git a/docs/openwiki/architecture/index.md b/docs/openwiki/architecture/index.md new file mode 100644 index 00000000..d7f95e96 --- /dev/null +++ b/docs/openwiki/architecture/index.md @@ -0,0 +1,4 @@ +# Files + +- [Architecture Overview](overview.md) +- [Routing and API Surface](routing-and-api.md) diff --git a/docs/openwiki/architecture/overview.md b/docs/openwiki/architecture/overview.md new file mode 100644 index 00000000..3d20ce39 --- /dev/null +++ b/docs/openwiki/architecture/overview.md @@ -0,0 +1,110 @@ +--- +type: 'Reference' +title: 'Architecture Overview' +openwiki_generated: true +--- + +# Architecture Overview + +Tracktor is a conventional SvelteKit application with the server-side rendering path used for bootstrapping authenticated state, and API routes used for mutations. Business logic is split between client-side stores/services and server-side services that wrap Drizzle ORM queries. + +## High-level layers + +``` +Browser + │ + ▼ +SvelteKit pages + layouts (src/routes/) + │ + ├─ Client stores (src/lib/stores/*.svelte.ts) + ├─ Client services (src/lib/services/*.ts) + └─ UI components (src/lib/components/) + │ + ▼ +SvelteKit API routes (src/routes/api/**/+server.ts) + │ + ▼ +Server middleware chain (src/server/middlewares/) + │ + ▼ +Server services (src/server/services/) + │ + ▼ +Drizzle ORM + SQLite (src/server/db/) +``` + +## Request lifecycle + +1. `src/hooks.server.ts` runs for every request. + - Logs startup banner and an env snapshot once. + - Ensures `UPLOADS_DIR` and `LOG_DIR` exist. + - Runs Drizzle migrations, optional seeding, and DB patches. + - Starts the notification scheduler. + - Applies the middleware chain: CORS → Auth → Rate Limit → Logging. + - Wraps unhandled errors with `HandleServerError`. +2. For API routes, the `AuthMiddleware` validates the session cookie (or `Authorization: Bearer ...` header) and sets `event.locals.user`. +3. The route handler parses input, calls a server service, and returns JSON. +4. On the client, Svelte 5 runes-based stores call the API through helpers in `src/lib/helper/api.helper.ts` and update local state. + +## Server-side rendering bootstrap + +Authenticated pages rely on server `load` functions to avoid a flash of unauthenticated content: + +- `src/routes/+page.server.ts` redirects `/` to `/dashboard` when already logged in or auth is disabled, otherwise to `/login`. +- `src/routes/(auth)/login/+page.server.ts` redirects to `/register` if no users exist yet, and to `/dashboard` if the session is valid. +- `src/routes/dashboard/+layout.server.ts` validates the session, then fetches `rawConfigs` and `vehicles` in parallel and returns them to the layout. +- `src/routes/dashboard/+layout.svelte` hydrates `configStore`, `vehicleStore`, and `authStore` from `data` in an `$effect.pre`. + +This is a recent change: the previous client-only `src/routes/dashboard/+layout.ts` was removed and replaced by the server layout load. + +## Code organization + +### Client + +| Directory | Purpose | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `src/lib/components/ui/` | Shadcn-svelte-style primitive components (button, input, table, chart, skeleton, etc.). | +| `src/lib/components/app/` | App-level composites (TabContainer, FeatureTabShell, LabelWithIcon). | +| `src/lib/components/layout/` | Layout chrome (Header, DashboardNav, AppSheet, Notifications). | +| `src/lib/components/feature/` | Domain-specific components: `fuel/`, `maintenance/`, `insurance/`, `pucc/`, `reminders/`, `overview/`, `settings/`, `vehicle/`. | +| `src/lib/domain/` | Zod schemas and TypeScript interfaces for every business entity. | +| `src/lib/services/` | Client-side orchestration (API calls, autocomplete, file handling). | +| `src/lib/helper/` | Shared helpers: API client, formatting, CSV parsing, feature flags, HTTP helpers. | +| `src/lib/stores/` | Svelte 5 runes stores for global UI state. | + +### Server + +| Directory | Purpose | +| ------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `src/server/db/schema/` | Drizzle table definitions. | +| `src/server/db/seeders/` | Demo data seeding. | +| `src/server/db/patch/` | Post-migration patches applied after `seedData()`. | +| `src/server/services/` | Business logic for auth, vehicles, fuel, maintenance, insurance, PUCC, reminders, notifications, config, files. | +| `src/server/middlewares/` | `BaseMiddleware`, `MiddlewareChain`, and concrete middlewares. | +| `src/server/exceptions/` | `AppError` and HTTP-status mapping. | +| `src/server/utils/` | Error handling, route wrappers, session utilities, filesystem helpers. | +| `src/server/config/` | Logger, app version, and other runtime config. | + +## Conventions + +- **ESM only** (`"type": "module"` in `package.json`). +- **Aliases** from `svelte.config.js` are preferred over `../../../` relative paths. +- **Svelte 5 runes** are used in newer components and stores; legacy store usage is being phased out. +- **Strict TypeScript** is expected; annotate public APIs and shared helpers. +- **Zod** is the canonical validation library for domain entities. +- **Responses** use the `ApiResponse` shape defined in `src/lib/response.ts` and the server helpers in `src/server/services/service-response.helper.ts`. + +## Extension points + +- New features usually need: a DB schema file, a domain file, a server service, an API route, a client service/store, and feature-gate wiring. +- New notification providers implement the interface used by `src/server/services/notificationDispatchService.ts` and are configured through `src/lib/domain/notification-provider.ts`. +- New i18n keys go in `messages/*.json` and are consumed through `$lib/paraglide/messages`. + +## Source references + +- Entry pipeline: `src/hooks.server.ts` +- Middleware base: `src/server/middlewares/base.ts` +- Auth middleware: `src/server/middlewares/auth.ts` +- Env config: `src/lib/config/env.server.ts` +- DB init: `src/server/db/init.ts` +- App version: `src/server/config/appVersion.ts` diff --git a/docs/openwiki/architecture/routing-and-api.md b/docs/openwiki/architecture/routing-and-api.md new file mode 100644 index 00000000..56547a71 --- /dev/null +++ b/docs/openwiki/architecture/routing-and-api.md @@ -0,0 +1,160 @@ +--- +type: 'Reference' +title: 'Routing and API Surface' +openwiki_generated: true +--- + +# Routing and API Surface + +Tracktor uses SvelteKit file-system routing. Pages live under `src/routes/`, API endpoints under `src/routes/api/`, and authentication flows under `src/routes/(auth)/`. + +## Page routes + +| Route | Purpose | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | +| `/` | Landing redirect. `+page.server.ts` sends authenticated users to `/dashboard` and unauthenticated users to `/login`. | +| `/login` | Username/password login. `+page.server.ts` redirects to `/register` when no users exist, or to `/dashboard` when already logged in. | +| `/register` | First-time account creation. Only available while the users table is empty (unless auth is disabled). | +| `/dashboard` | Dashboard shell. `+layout.server.ts` validates auth, loads configs and vehicles, and the layout hydrates stores. | +| `/dashboard/overview` | Dashboard charts and summary cards. | +| `/dashboard/fuel` | Fuel log list, form, and import. | +| `/dashboard/maintenance` | Maintenance log list, form, PDF export. | +| `/dashboard/insurance` | Insurance records and renewals. | +| `/dashboard/pollution` | PUCC records and renewals. | +| `/dashboard/reminders` | Reminders list and management. | +| `/settings` | App settings, feature toggles, notification providers, branding. | + +## API routes + +All API routes return the shared `ApiResponse` shape (`success`, `data`, `message`). Route handlers wrap service calls with `withRouteErrorHandling` from `src/server/utils/route-handler.ts`. + +### Auth + +- `POST /api/auth` — login, sets `session` cookie. +- `GET /api/auth` — returns user count, current user, and `isAuthenticated` / `isAuthDisabled`. +- `DELETE /api/auth` — logout, clears cookie. +- `POST /api/auth/register` — first user registration. +- `GET /api/auth/profile` — current user profile. +- `PUT /api/auth/profile` — update username/password. + +### Vehicles + +- `GET /api/vehicles` — list all vehicles. +- `POST /api/vehicles` — create vehicle. +- `PUT /api/vehicles` — update vehicle. +- `GET /api/vehicles/[id]` — get one vehicle. +- `DELETE /api/vehicles/[id]` — delete vehicle. + +### Fuel logs + +- `GET /api/vehicles/[id]/fuel-logs` — list fuel logs for a vehicle with computed `distanceDriven` and mileage. +- `POST /api/vehicles/[id]/fuel-logs` — create fuel log. +- `PUT /api/vehicles/[id]/fuel-logs/[logId]` — update fuel log. +- `DELETE /api/vehicles/[id]/fuel-logs/[logId]` — delete fuel log. + +### Maintenance logs + +- `GET /api/vehicles/[id]/maintenance-logs` — list maintenance logs. +- `POST /api/vehicles/[id]/maintenance-logs` — create maintenance log. +- `PUT /api/vehicles/[id]/maintenance-logs/[logId]` — update maintenance log. +- `DELETE /api/vehicles/[id]/maintenance-logs/[logId]` — delete maintenance log. +- `GET /api/vehicles/[id]/maintenance-logs/export-pdf` — PDF export (added in `d34021f`). + +### Insurance + +- `GET /api/vehicles/[id]/insurance` +- `POST /api/vehicles/[id]/insurance` +- `PUT /api/vehicles/[id]/insurance/[insuranceId]` +- `DELETE /api/vehicles/[id]/insurance/[insuranceId]` + +### PUCC (pollution certificates) + +- `GET /api/vehicles/[id]/pucc` +- `POST /api/vehicles/[id]/pucc` +- `PUT /api/vehicles/[id]/pucc/[puccId]` +- `DELETE /api/vehicles/[id]/pucc/[puccId]` + +### Reminders + +- `GET /api/vehicles/[id]/reminders` +- `POST /api/vehicles/[id]/reminders` +- `PUT /api/vehicles/[id]/reminders/[reminderId]` +- `DELETE /api/vehicles/[id]/reminders/[reminderId]` + +### Notifications + +- `GET /api/vehicles/[id]/notifications` +- `PUT /api/vehicles/[id]/notifications/[notificationId]` +- `GET /api/notifications/test-enabled-providers` — trigger a test dispatch. +- `POST /api/test-email-digest` — send a test email digest. + +### Notification providers + +- `GET /api/notification-providers` +- `POST /api/notification-providers` +- `PUT /api/notification-providers/[id]` +- `DELETE /api/notification-providers/[id]` +- `POST /api/notification-providers/[id]/test` + +### Config and data + +- `GET /api/config` — all configs. +- `PUT /api/config` — bulk update configs. +- `GET /api/config/[key]` — single config. +- `PUT /api/config/[key]` — update single config. +- `GET /api/config/branding` — custom CSS value. +- `GET /api/autocomplete` — autocomplete suggestions (service centers, providers, etc.). +- `POST /api/data/import` — import data (CSV/JSON). +- `GET /api/data/export` — export data. +- `POST /api/files` — upload files. +- `GET /api/files/[filename]` — download files. +- `GET /api/health` — health check. +- `GET /api/cron/reload` — reload the notification scheduler. + +## Auth middleware + +`src/server/middlewares/auth.ts` guards API routes: + +- Auth is skipped entirely when `env.DISABLE_AUTH` is true. +- Only paths starting with `/api` are protected. +- Bypass paths: `/api/auth`, `/api/files/`, `/api/health`, `/api/config/branding`. +- Accepts session via `Authorization: Bearer ` or the `session` cookie. +- On success, sets `event.locals.user` with `id` and `username`. +- If no users exist, returns `400` with "Please create a user account first." + +## Layout data flow + +``` ++layout.server.ts (root) + └─ returns { appVersion } + ++page.server.ts (/) + └─ redirects to /dashboard or /login + +(auth)/login/+page.server.ts + └─ redirects to /register, /dashboard, or renders login + +(auth)/register/+page.server.ts + └─ redirects to /dashboard when auth disabled + or blocks registration when users already exist + +dashboard/+layout.server.ts + └─ returns { user, rawConfigs, configs, vehicles } + +dashboard/+layout.svelte + └─ hydrates configStore, vehicleStore, authStore from data +``` + +This server-side bootstrap is the current state after the removal of `src/routes/dashboard/+layout.ts`. + +## Source references + +- Route handlers: `src/routes/api/**/+server.ts` +- Auth middleware: `src/server/middlewares/auth.ts` +- Middleware chain: `src/server/middlewares/base.ts` +- Route error wrapper: `src/server/utils/route-handler.ts` +- Response shape: `src/lib/response.ts` +- Root layout server load: `src/routes/+layout.server.ts` +- Landing redirect: `src/routes/+page.server.ts` +- Dashboard layout load: `src/routes/dashboard/+layout.server.ts` +- Dashboard layout hydration: `src/routes/dashboard/+layout.svelte` diff --git a/docs/openwiki/domain/data-models.md b/docs/openwiki/domain/data-models.md new file mode 100644 index 00000000..52fb3cdf --- /dev/null +++ b/docs/openwiki/domain/data-models.md @@ -0,0 +1,147 @@ +--- +type: 'Reference' +title: 'Domain and Data Models' +openwiki_generated: true +--- + +# Domain and Data Models + +Tracktor's business entities are defined as TypeScript interfaces and Zod schemas in `src/lib/domain/`. The server uses matching Drizzle ORM tables in `src/server/db/schema/`. + +## Vehicle + +A vehicle is the top-level aggregate. Every other record belongs to a vehicle. + +- Domain: `src/lib/domain/vehicle.ts` +- Schema: `src/server/db/schema/vehicle.ts` +- Server service: `src/server/services/vehicleService.ts` + +Fields: `id`, `make`, `model`, `year`, `licensePlate`, `vin`, `color`, `odometer`, `image`, `fuelType`, `customFields`. + +Fuel types: `petrol`, `diesel`, `electric`, `lpg`, `cng`. + +Computed status fields (`insuranceStatus`, `puccStatus`) are derived from related records and added by the service layer; they are not stored on the vehicle table. + +### Odometer and mileage + +The service layer computes the "latest" odometer as the maximum of: + +1. the vehicle's base `odometer`, +2. the highest odometer in fuel logs, +3. the highest odometer in maintenance logs. + +Overall mileage is computed from fuel logs using full-tank fills as anchors; partial fills between two full fills are summed into total fuel. See `calculateOverallMileage` in `src/server/services/vehicleService.ts`. + +## Fuel log + +- Domain: `src/lib/domain/fuel.ts` +- Schema: `src/server/db/schema/fuel-log.ts` +- Server service: `src/server/services/fuelLogService.ts` +- Client service: `src/lib/services/fuel.service.ts` + +Fields: `id`, `vehicleId`, `date`, `odometer`, `filled`, `missedLast`, `fuelAmount`, `rate`, `cost`, `notes`, `attachment`. + +Recent additions: + +- `rate` (added in `debf6e5`) supports per-liter/per-kg price and auto-calculation. +- `distanceDriven` is computed when listing logs by comparing the current odometer with the previous log. +- `mileage` is computed only for full-tank fills that have a valid preceding full-tank anchor; missed fills act as barriers. + +## Maintenance log + +- Domain: `src/lib/domain/maintenance.ts` +- Schema: `src/server/db/schema/maintenance-logs.ts` +- Server service: `src/server/services/maintenanceLogService.ts` +- Client service: `src/lib/services/maintenance.service.ts` +- PDF export: `src/server/services/maintenanceLogPdfService.ts` + `src/routes/api/vehicles/[id]/maintenance-logs/export-pdf/+server.ts` + +Fields: `id`, `vehicleId`, `date`, `odometer`, `serviceCenter`, `cost`, `notes`, `attachment`. + +PDF export generates a tabular report with date, odometer, service center, cost, and notes. + +## Insurance + +- Domain: `src/lib/domain/insurance.ts` +- Schema: `src/server/db/schema/insurance.ts` +- Server service: `src/server/services/insuranceService.ts` + +Fields: `id`, `vehicleId`, `provider`, `policyNumber`, `startDate`, `endDate`, `recurrenceType`, `recurrenceInterval`, `cost`, `notes`, `attachment`. + +Recurrence types: `none`, `yearly`, `monthly`, `no_end`. Recurring policies drive reminder generation. + +## PUCC (Pollution Under Control Certificate) + +- Domain: `src/lib/domain/pucc.ts` +- Schema: `src/server/db/schema/pucc.ts` +- Server service: `src/server/services/puccService.ts` + +Fields: `id`, `vehicleId`, `certificateNumber`, `issueDate`, `expiryDate`, `cost`, `notes`, `attachment`. + +## Reminder + +- Domain: `src/lib/domain/reminder.ts` +- Schema: `src/server/db/schema/reminder.ts` +- Server service: `src/server/services/reminderService.ts` +- Client service: `src/lib/services/reminder.service.ts` + +Reminders are tied to a vehicle and trigger notifications. They can be one-off or recurring and carry `dueDate`, `dueOdometer`, `type`, `status`, and `recurringInterval` fields. + +## Notification + +- Domain: `src/lib/domain/notification.ts` +- Schema: `src/server/db/schema/notification.ts` +- Server service: `src/server/services/notificationService.ts` + +Notifications are the concrete messages produced from reminders and sent through configured notification providers. + +## Notification provider + +- Domain: `src/lib/domain/notification-provider.ts` +- Schema: `src/server/db/schema/notification-provider.ts` +- Server service: `src/server/services/notificationProviderService.ts` +- Dispatch: `src/server/services/notificationDispatchService.ts` + +Providers are configurable backends (e.g., email, webhook) with type-specific fields. The scheduler dispatches pending notifications through enabled providers. + +## Config + +- Domain: `src/lib/domain/config.ts` +- Schema: `src/server/db/schema/config.ts` +- Server service: `src/server/services/configService.ts` +- Client store: `src/lib/stores/config.svelte.ts` + +Configs are key/value pairs. Boolean configs are stored as `'true'`/`'false'` strings; the client store coerces them. + +Important keys: + +- `featureFuelLog`, `featureMaintenance`, `featurePucc`, `featureReminders`, `featureInsurance`, `featureOverview` +- `notificationProcessingEnabled`, `notificationProcessingSchedule` +- `dateFormat`, `currency`, `unitOfDistance`, `unitOfVolume`, `unitOfLpg`, `unitOfCng`, `mileageUnitFormat` +- `locale`, `timezone` +- `customCss` + +## Auth + +- Schema: `src/server/db/schema/auth.ts` +- Server service: `src/server/services/authService.ts` +- Session utils: `src/server/utils/session.ts` + +Tables: + +- `users` — `id`, `username`, `passwordHash`, timestamps. +- `sessions` — `id`, `userId`, `expiresAt`, timestamps. +- `auth` — legacy table kept for migration compatibility. + +Sessions are 30-day HTTP-only secure cookies. Bcrypt is used with salt rounds `10`. + +## Audit timestamps + +Most tables include `createdAt`/`updatedAt` via the shared `timestamps` helper in `src/server/db/schema/audit.ts`. + +## Source references + +- Domain definitions: `src/lib/domain/*.ts` +- Drizzle schemas: `src/server/db/schema/*.ts` +- DB connection: `src/server/db/index.ts` +- DB initialization: `src/server/db/init.ts` +- Server services: `src/server/services/*.ts` diff --git a/docs/openwiki/domain/index.md b/docs/openwiki/domain/index.md new file mode 100644 index 00000000..435906ca --- /dev/null +++ b/docs/openwiki/domain/index.md @@ -0,0 +1,3 @@ +# Files + +- [Domain and Data Models](data-models.md) diff --git a/docs/openwiki/index.md b/docs/openwiki/index.md new file mode 100644 index 00000000..17db6c7d --- /dev/null +++ b/docs/openwiki/index.md @@ -0,0 +1,15 @@ +--- +okf_version: '0.1' +--- + +# Files + +- [Tracktor — OpenWiki Quickstart](quickstart.md) +- [Testing Guidance](testing.md) + +# Directories + +- [architecture](architecture/) +- [domain](domain/) +- [operations](operations/) +- [workflows](workflows/) diff --git a/docs/openwiki/operations/index.md b/docs/openwiki/operations/index.md new file mode 100644 index 00000000..308eff85 --- /dev/null +++ b/docs/openwiki/operations/index.md @@ -0,0 +1,3 @@ +# Files + +- [Operations Runbook](runbook.md) diff --git a/docs/openwiki/operations/runbook.md b/docs/openwiki/operations/runbook.md new file mode 100644 index 00000000..de387b09 --- /dev/null +++ b/docs/openwiki/operations/runbook.md @@ -0,0 +1,162 @@ +--- +type: 'Reference' +title: 'Operations Runbook' +openwiki_generated: true +--- + +# Operations Runbook + +This page covers deployment, environment configuration, database operations, and runtime behavior. + +## Environment variables + +Defined and typed in `src/lib/config/env.server.ts`. Public variables must be prefixed with `TRACKTOR_` per `svelte.config.js`. + +| Variable | Default | Purpose | +| ----------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------- | +| `NODE_ENV` | `dev` | `dev`, `production`, or `test`. | +| `HOST` | `localhost` | Bind host (production/build only). | +| `PORT` | `3000` | Bind port (production/build only). | +| `BASE_URL` | `""` | Sub-path when behind a reverse proxy. No trailing slash. | +| `DB_PATH` | `./tracktor.db` (prod), `./tracktor.dev.db` (dev), `./tracktor.test.db` (test) | SQLite file path. | +| `UPLOADS_DIR` | `./uploads` | Uploaded files directory. | +| `CORS_ORIGINS` | `*` | Comma-separated allowed origins. | +| `TRACKTOR_DISABLE_AUTH` | `false` | Disable auth entirely. | +| `TRACKTOR_DEMO_MODE` | `false` | Enable demo mode banner and sample data. | +| `FORCE_DATA_SEED` | `false` | Overwrite data with demo seed (requires demo mode). | +| `LOG_REQUESTS` | `true` | Enable HTTP request logging. | +| `LOG_LEVEL` | `info` | Log verbosity. | +| `LOG_DIR` | `./logs` | Log directory. | +| `HTTP_MODE` | `http` | Set to `https` for secure cookies. | +| `APP_VERSION` | branch name (dev) / package version (prod) | Override displayed version. | +| `TRACKTOR_API_BASE_URL` | — | Optional external API base URL. | + +> Do not commit secrets. The repo includes `.env.example` with placeholder values. + +## Docker deployment + +The `Dockerfile` is a two-stage build: + +1. `builder` — installs dependencies with pnpm, runs `pnpm run build`, then prunes dev dependencies. +2. Runtime — copies `build/`, `node_modules/`, `package.json`, and `migrations/`, exposes port `3000`, and runs `node build`. + +Default container paths: + +- Database: `/data/tracktor.db` +- Logs: `/data/logs` +- Uploads: `/data/uploads` + +Example `docker-compose.yml` is in `docs/installation.md`. + +Build-time version: + +```bash +docker build --build-arg APP_VERSION=1.4.1 -t tracktor . +``` + +`APP_VERSION` is read at runtime from the env var; `src/server/config/appVersion.ts` resolves branch name in dev and package version in production. + +## Database lifecycle + +On every server start, `src/hooks.server.ts` calls `initializeDatabase()`: + +1. Run Drizzle migrations from `migrations/`. +2. Seed demo data if applicable (`seedData()` in `src/server/db/seeders/`). +3. Apply post-migration patches (`applyPatches()` in `src/server/db/patch/index.ts`). + +Commands: + +```bash +pnpm db:generate # Generate a new migration from schema changes +pnpm db:migrate # Run pending migrations +pnpm db:seed # Run the demo seeder script +``` + +### Adding a schema change + +1. Edit the relevant file in `src/server/db/schema/`. +2. Run `pnpm db:generate`. +3. Review the generated SQL in `migrations/`. +4. Run `pnpm db:migrate` locally. +5. Commit both schema and migration files. + +### Patches + +`src/server/db/patch/index.ts` is for idempotent fixes that should run after migrations and seeding. Use it sparingly and ensure it is safe to run multiple times. + +## Demo mode + +When `TRACKTOR_DEMO_MODE=true`: + +- A banner is shown in `src/routes/+layout.svelte`. +- Demo credentials are shown in the banner unless auth is disabled. +- `seedData()` may insert sample vehicles, logs, and configs. +- `FORCE_DATA_SEED=true` causes seeding to overwrite existing data. + +## Notification scheduler + +A `node-cron` scheduler starts in `src/hooks.server.ts`: + +- Reads `notificationProcessingEnabled` and `notificationProcessingSchedule` from configs. +- Default schedule is `0 9 * * *` (daily at 09:00). +- Calls `dispatchScheduledNotifications()` from `src/server/services/notificationDispatchService.ts`. +- Can be reloaded at runtime via `GET /api/cron/reload`. + +If notification processing is disabled, the scheduler logs that fact and exits. + +## Logs + +Logs are written under `LOG_DIR`. The logger is configured in `src/server/config/logger.ts`. Request logging is controlled by `LOG_REQUESTS`. + +## File uploads + +Uploads are stored under `UPLOADS_DIR` and served via `/api/files/[filename]`. The file API is unauthenticated (`/api/files/...` is in the auth middleware bypass list) so attachments can be downloaded with a direct link; in production, serve Tracktor behind a reverse proxy and consider additional access controls. + +## Backup checklist + +Before updates, back up: + +- The SQLite database file (`DB_PATH`). +- The uploads directory (`UPLOADS_DIR`). +- Any custom environment variables. + +## Common commands + +```bash +# Dev +pnpm dev + +# Build / run production +pnpm build +pnpm start + +# Checks +pnpm check +pnpm lint +pnpm format + +# Tests +pnpm test + +# Database +pnpm db:generate +pnpm db:migrate +pnpm db:seed + +# Clean everything (destructive) +pnpm clean +``` + +## Source references + +- Env config: `src/lib/config/env.server.ts` +- Dockerfile: `Dockerfile` +- DB init: `src/server/db/init.ts` +- DB connection: `src/server/db/index.ts` +- DB schema: `src/server/db/schema/` +- DB patches: `src/server/db/patch/index.ts` +- Seeders: `src/server/db/seeders/` +- Hooks/server startup: `src/hooks.server.ts` +- Notification scheduler: `src/server/services/notificationSchedulerService.ts` +- Logger: `src/server/config/logger.ts` +- User docs: `docs/installation.md`, `docs/environment.md` diff --git a/docs/openwiki/quickstart.md b/docs/openwiki/quickstart.md new file mode 100644 index 00000000..f74774f6 --- /dev/null +++ b/docs/openwiki/quickstart.md @@ -0,0 +1,115 @@ +--- +type: 'Reference' +title: 'Tracktor — OpenWiki Quickstart' +openwiki_generated: true +--- + +# Tracktor — OpenWiki Quickstart + +Tracktor is an open-source vehicle management web application. It lets a single user or household track fuel consumption, maintenance history, insurance and pollution-under-control (PUCC) certificates, reminders, and dashboard analytics for one or more vehicles. + +> **Stability note:** The project is under active development and README marks it as not yet stable for production. Keep backups of `DB_PATH` and `UPLOADS_DIR`. + +## What this wiki covers + +This wiki is a navigation layer over the source code and the existing `docs/` guides. It is aimed at engineers who need to understand the architecture, find the right file quickly, and change things safely. + +- [Architecture overview](./architecture/overview.md) +- [Routing and API surface](./architecture/routing-and-api.md) +- [Domain and data models](./domain/data-models.md) +- [Feature toggles workflow](./workflows/feature-toggles.md) +- [Authentication workflow](./workflows/authentication.md) +- [Operations runbook](./operations/runbook.md) +- [Testing guidance](./testing.md) + +## Tech stack + +| Layer | Technology | +| ----------- | --------------------------------------- | +| Framework | SvelteKit 2 + Svelte 5 runes | +| Styling | Tailwind CSS v4 | +| Build / dev | Vite 8 | +| i18n | inlang / Paraglide JS | +| ORM / DB | Drizzle ORM + SQLite (`@libsql/client`) | +| Auth | Bcrypt + custom DB sessions | +| Cron | `node-cron` for scheduled notifications | +| PDF | `pdfkit` for maintenance log export | +| Container | Docker / Docker Compose | + +Source of truth: `package.json`, `svelte.config.js`, `vite.config.js`, `drizzle.config.js`. + +## Run the project locally + +```bash +# Install dependencies (pnpm is required) +pnpm install + +# Start the dev server (uses SQLite at ./tracktor.dev.db) +pnpm dev + +# Or the local variant without --host +pnpm local +``` + +The dev server exposes the app on the configured host/port (default `localhost:5173` unless overridden). The production build uses `HOST`/`PORT` env vars and defaults to `0.0.0.0:3000` in Docker. + +## Useful commands + +```bash +pnpm build # Production build into ./build +pnpm preview # Preview production build +pnpm check # svelte-kit sync + svelte-check +pnpm lint # ESLint + Prettier check +pnpm format # Auto-fix and format +pnpm test # Run Vitest once +pnpm test:watch # Run Vitest in watch mode +pnpm db:generate # Generate Drizzle migration +pnpm db:migrate # Run Drizzle migrations +pnpm db:seed # Seed demo data +pnpm clean # Remove build artifacts, DBs, uploads, logs +``` + +## Key directories + +- `src/routes/` — pages, layouts, and API endpoints. +- `src/lib/components/` — UI components split into `ui/`, `app/`, `layout/`, `feature/`. +- `src/lib/domain/` — Zod schemas and TypeScript interfaces for business entities. +- `src/lib/services/` — client-side service orchestration and API callers. +- `src/lib/stores/` — Svelte 5 runes-based stores (config, vehicle, auth, theme, sheet, etc.). +- `src/server/services/` — server-side business logic (auth, vehicles, fuel, maintenance, etc.). +- `src/server/db/` — Drizzle schema, DB connection, migrations, seeders, patches. +- `src/server/middlewares/` — request middleware (auth, CORS, rate limit, logging). +- `src/server/utils/` — shared server helpers (route error handling, sessions, file utilities). +- `messages/` — translation source files. +- `migrations/` — Drizzle migration SQL and metadata. +- `docs/` — user-facing setup and feature docs. +- `.github/workflows/` — CI, Docker publish, and OpenWiki update automation. + +## Recent changes worth knowing + +As of `HEAD` (`0674858`) and the current working tree: + +- Dashboard config loading moved from a client-side `+layout.ts` to a server `+layout.server.ts`; the dashboard layout now receives `rawConfigs`, typed `configs`, `vehicles`, and `user` in `data`. +- `configStore.setConfigs(...)` and `vehicleStore.setVehicles(...)` were added so the dashboard layout can hydrate stores directly from server data instead of triggering extra client fetches. +- A `+page.server.ts` was added to `/login` to gate registration and redirect already-authenticated users. +- Recent merged work added fuel-log `rate` and `distanceDriven` fields, maintenance-log PDF export, dependency/chart upgrades, and broader i18n translations. + +See `git log --oneline` and `git diff HEAD` for the exact delta. + +## Where to start reading code + +1. `src/hooks.server.ts` — request pipeline, DB init, middleware chain. +2. `src/routes/dashboard/+layout.server.ts` — how every authenticated page is bootstrapped. +3. `src/lib/domain/` — business entities and validation rules. +4. `src/server/services/` — server business logic for the area you are changing. +5. `src/routes/api/.../+server.ts` — HTTP contract for each resource. + +## Agent notes + +- The repo uses ESM only (`"type": "module"`). +- Aliases are defined in `svelte.config.js`; prefer them over deep relative paths. +- Svelte 5 runes are used in newer `.svelte` and `.svelte.ts` files. +- Strict TypeScript is expected; avoid `any` unless the surrounding code already uses it. +- ESLint fails on unused imports. +- Run `pnpm check` and `pnpm lint` before finishing changes. +- Database changes require a Drizzle migration; see [Operations runbook](./operations/runbook.md). diff --git a/docs/openwiki/testing.md b/docs/openwiki/testing.md new file mode 100644 index 00000000..5f93be22 --- /dev/null +++ b/docs/openwiki/testing.md @@ -0,0 +1,80 @@ +--- +type: 'Reference' +title: 'Testing Guidance' +openwiki_generated: true +--- + +# Testing Guidance + +Tracktor uses [Vitest](https://vitest.dev/) with [jsdom](https://github.com/jsdom/jsdom) for unit and component testing. Coverage is provided by `@vitest/coverage-v8`. + +## Test setup + +- Config: inferred from `vite.config.js`. +- Test directory: `src/__tests__/`. +- Current test file: `src/__tests__/index.test.ts` (a single placeholder test). + +## Running tests + +```bash +# Run all tests once +pnpm test + +# Run in watch mode +pnpm test:watch + +# Run with coverage +pnpm test:coverage + +# Run a single file +pnpm test -- path/to/file.test.ts +pnpm vitest --run path/to/file.test.ts + +# Run tests matching a name +pnpm test -- -t "test name" +pnpm vitest --run -t "test name" + +# Run all tests in a folder +pnpm vitest --run src/__tests__/feature +``` + +## Current coverage + +As of this writing, the repository has a minimal test suite (`src/__tests__/index.test.ts` contains one trivial assertion). Most business logic is tested only through manual and integration usage. + +## Where to add tests + +When changing the following areas, add or extend tests in `src/__tests__/`: + +- Domain helpers and Zod schemas: `src/lib/domain/` +- Formatting, CSV, feature helpers: `src/lib/helper/` +- Server services: `src/server/services/` +- API route validation and response shapes: `src/routes/api/` +- Svelte components: use `@testing-library/svelte` and jsdom. + +## Testing conventions + +- Keep tests deterministic and independent of external services. +- Name tests clearly so `vitest -t` filters stay useful. +- Use the test database path when testing service logic (`./tracktor.test.db` in `test` env via `src/lib/config/env.server.ts`). +- Prefer narrow unit tests for pure helpers; use component tests for UI behavior. +- When testing stores, instantiate a fresh store instance or reset state between tests to avoid shared runes state. + +## Pre-commit checks + +Run before finishing work: + +```bash +pnpm check +pnpm lint +pnpm test +``` + +`pnpm check` runs `svelte-kit sync` and `svelte-check`, which catches type errors in `.svelte` files. + +## Source references + +- Test file: `src/__tests__/index.test.ts` +- Vite config: `vite.config.js` +- Package scripts: `package.json` +- Agent guidance: `AGENTS.md` (Testing Guidance section) diff --git a/docs/openwiki/workflows/authentication.md b/docs/openwiki/workflows/authentication.md new file mode 100644 index 00000000..5f122950 --- /dev/null +++ b/docs/openwiki/workflows/authentication.md @@ -0,0 +1,75 @@ +--- +type: 'Reference' +title: 'Authentication Workflow' +openwiki_generated: true +--- + +# Authentication Workflow + +Tracktor uses username/password authentication with bcrypt-hashed passwords and database-backed sessions. Authentication can be disabled entirely via `TRACKTOR_DISABLE_AUTH` for local/demo use. + +## User model + +- Table: `users` in `src/server/db/schema/auth.ts` +- Service: `src/server/services/authService.ts` +- Session utils: `src/server/utils/session.ts` + +A user has `id`, `username`, `passwordHash`, and audit timestamps. Passwords are hashed with bcrypt at salt rounds `10`. + +## Registration gate + +The first user is created through the web UI at `/register`. Once at least one user exists, registration is closed: + +- `src/routes/(auth)/register/+page.server.ts` checks `getUsersCount()` and redirects to `/login` if users already exist. +- `AuthMiddleware` (`src/server/middlewares/auth.ts`) also checks `getUsersCount()` and returns `400` if an API call is made before any user exists. + +This is the only multi-user model: there is no admin-invite flow; the first account is the owner account. + +## Login flow + +1. `POST /api/auth` receives `{ username, password }`. +2. `authService.loginUser` loads the user by username, compares the password with bcrypt, and creates a session. +3. The route sets an HTTP-only `session` cookie with: + - `sameSite: 'lax'` + - `secure` when `HTTP_MODE === 'https'` + - `maxAge` of 30 days +4. `src/routes/(auth)/login/+page.server.ts` redirects an already-authenticated user to `/dashboard`. + +## Session validation + +- API requests: `AuthMiddleware` reads the `session` cookie or `Authorization: Bearer ` header and calls `validateSession`. +- On success, `event.locals.user` is populated with `{ id, username }`. +- On failure, a `401` or `500` JSON error is returned. + +## Logout + +`DELETE /api/auth` clears the `session` cookie with `maxAge: 0`. + +## Disable-auth mode + +Set `TRACKTOR_DISABLE_AUTH=true` (either the public or private env var works). When disabled: + +- `AuthMiddleware` skips all API protection. +- `+page.server.ts` and `+layout.server.ts` redirect directly to `/dashboard`. +- The demo banner in `src/routes/+layout.svelte` hides the default login credentials. + +This is intended for local development or single-user trusted deployments, not production. + +## Password and profile changes + +`PUT /api/auth/profile` supports: + +- changing the username, +- changing the password after verifying the current password. + +## Source references + +- Auth schema: `src/server/db/schema/auth.ts` +- Auth service: `src/server/services/authService.ts` +- Session utilities: `src/server/utils/session.ts` +- Auth middleware: `src/server/middlewares/auth.ts` +- Auth API route: `src/routes/api/auth/+server.ts` +- Login page load: `src/routes/(auth)/login/+page.server.ts` +- Register page load: `src/routes/(auth)/register/+page.server.ts` +- Env config: `src/lib/config/env.server.ts` +- User docs: `docs/authentication.md` diff --git a/docs/openwiki/workflows/feature-toggles.md b/docs/openwiki/workflows/feature-toggles.md new file mode 100644 index 00000000..d50e62ad --- /dev/null +++ b/docs/openwiki/workflows/feature-toggles.md @@ -0,0 +1,89 @@ +--- +type: 'Reference' +title: 'Feature Toggles Workflow' +openwiki_generated: true +--- + +# Feature Toggles Workflow + +Tracktor can disable entire feature areas from the Settings page. Toggles are stored in the `configs` table and consumed by both server and client code. + +## Available features + +| Feature key | UI route | Area | +| -------------------- | ------------------------ | ------------------ | +| `featureFuelLog` | `/dashboard/fuel` | Fuel logs | +| `featureMaintenance` | `/dashboard/maintenance` | Maintenance logs | +| `featurePucc` | `/dashboard/pollution` | PUCC records | +| `featureReminders` | `/dashboard/reminders` | Reminders | +| `featureInsurance` | `/dashboard/insurance` | Insurance records | +| `featureOverview` | `/dashboard/overview` | Dashboard overview | + +Source of truth for keys: `src/lib/domain/config.ts` (`BOOLEAN_CONFIG_KEYS`) and `docs/feature-toggles.md`. + +## How toggles flow through the app + +1. **Storage:** `configs` table stores each feature key as `'true'` or `'false'`. +2. **Server load:** `src/routes/dashboard/+layout.server.ts` fetches all configs, builds a `configs` map where `feature*` keys are booleans, and returns it in `data`. +3. **Client hydration:** `src/routes/dashboard/+layout.svelte` runs `configStore.setConfigs(data.rawConfigs)` in `$effect.pre`. +4. **Coercion:** `configStore` (`src/lib/stores/config.svelte.ts`) converts boolean keys from strings and falls back to `DEFAULT_CONFIGS`, which default all features to `true`. +5. **Gating:** Components use `FeatureGate.svelte` or helper functions from `src/lib/helper/feature.helper.ts`. + +## Component gating + +`src/lib/components/feature/FeatureGate.svelte` supports three modes: + +```svelte + + + + + + + + + + {#snippet children()} + + {/snippet} + {#snippet fallback()} +

Feature disabled

+ {/snippet} +
+``` + +## Programmatic checks + +```ts +import { isFeatureEnabled, Features, getEnabledFeatures } from '$lib/helper/feature.helper'; + +if (isFeatureEnabled(Features.FUEL_LOG)) { ... } +const enabled = getEnabledFeatures(); +``` + +`isFeatureEnabled` maps a camelCase feature name to the `featureXxx` config key. + +## Dashboard redirect behavior + +`src/routes/dashboard/+layout.svelte` watches `configStore.configs` and the current path. If the user navigates to a route whose feature is disabled, the layout redirects to the first enabled feature route (in order: overview, fuel, maintenance, insurance, pollution, reminders). + +This means disabling a feature both hides its UI and prevents landing on its route. + +## Adding a new feature toggle + +1. Add the key to `BOOLEAN_CONFIG_KEYS` in `src/lib/domain/config.ts`. +2. Add a default value to `DEFAULT_CONFIGS` in `src/lib/stores/config.svelte.ts`. +3. Add the route-to-feature mapping in `src/routes/dashboard/+layout.svelte` if it needs redirect protection. +4. Gate the UI with `FeatureGate` or `isFeatureEnabled`. +5. Add a settings checkbox bound to the config key. +6. Add translations in `messages/*.json` if the feature has a settings label. + +## Source references + +- Config domain: `src/lib/domain/config.ts` +- Config store: `src/lib/stores/config.svelte.ts` +- Feature helper: `src/lib/helper/feature.helper.ts` +- Feature gate component: `src/lib/components/feature/FeatureGate.svelte` +- Dashboard layout with redirect logic: `src/routes/dashboard/+layout.svelte` +- Dashboard server load: `src/routes/dashboard/+layout.server.ts` +- User docs: `docs/feature-toggles.md` diff --git a/docs/openwiki/workflows/index.md b/docs/openwiki/workflows/index.md new file mode 100644 index 00000000..cbccf028 --- /dev/null +++ b/docs/openwiki/workflows/index.md @@ -0,0 +1,4 @@ +# Files + +- [Authentication Workflow](authentication.md) +- [Feature Toggles Workflow](feature-toggles.md) diff --git a/drizzle.config.js b/drizzle.config.js index 98602968..a4dd6acb 100644 --- a/drizzle.config.js +++ b/drizzle.config.js @@ -1,7 +1,7 @@ import { defineConfig } from 'drizzle-kit'; export default defineConfig({ - out: './migrations', + out: './src/server/db/migrations', migrations: { prefix: 'timestamp', table: '_migrations' diff --git a/messages/ar.json b/i18n/messages/ar.json similarity index 99% rename from messages/ar.json rename to i18n/messages/ar.json index 13384fec..b6788748 100644 --- a/messages/ar.json +++ b/i18n/messages/ar.json @@ -163,6 +163,7 @@ "fuel_add_title": "إضافة سجل وقود", "col_date": "التاريخ", "col_odometer": "عداد المسافة", + "col_distance_driven": "المسافة المقطوعة", "col_filled": "مملوء", "col_missed_last": "أفتقدت الأخير", "col_fuel_amount": "كمية الوقود", @@ -180,6 +181,7 @@ "form_odometer_desc": "قراءة عداد المسافة الحالية للمركبة", "form_volume_fuel": "حجم الوقود", "form_volume_energy": "الطاقة المستهلكة", + "form_rate": "سعر الوحدة", "form_cost": "التكلفة", "form_cost_desc": "تكلفة التعبئة", "form_cost_desc_ev": "تكلفة الشحن", diff --git a/messages/de.json b/i18n/messages/de.json similarity index 56% rename from messages/de.json rename to i18n/messages/de.json index 7a038e7b..2862677f 100644 --- a/messages/de.json +++ b/i18n/messages/de.json @@ -41,7 +41,7 @@ "settings_desc_unit_distance": "Maßeinheit für Entfernung", "settings_desc_unit_volume": "Maßeinheit für Volumen", "settings_label_mileage_format": "Kraftstoffverbrauch-Anzeigeformat", - "settings_desc_mileage_format": "Wählen Sie, wie die Kraftstoffeffizienz angezeigt wird", + "settings_desc_mileage_format": "Wähle, wie der Kraftstoffverbrauch angezeigt wird", "settings_mileage_format_distance_per_fuel": "Entfernung pro Kraftstoff (z.B. km/L, mpg)", "settings_mileage_format_fuel_per_distance": "Kraftstoff pro Entfernung (z.B. L/100km)", "settings_desc_theme": "Bevorzugtes Design wählen", @@ -72,13 +72,6 @@ "common_no_match_found": "Keine Übereinstimmung gefunden", "common_search_placeholder": "{name} suchen", "common_select_placeholder": "{name} auswählen...", - "nav_overview": "Übersicht", - "nav_fuel_logs": "Kraftstoffprotokolle", - "nav_maintenance": "Wartung", - "nav_insurance": "Versicherung", - "nav_pollution": "Abgaszertifikat", - "nav_reminders": "Erinnerungen", - "nav_settings": "Einstellungen", "tools_export_data": "Daten exportieren", "tools_import_data": "Daten importieren", "vehicle_form_make_label": "Marke", @@ -101,7 +94,7 @@ "vehicle_toast_saved": "Fahrzeug erfolgreich gespeichert", "vehicle_toast_updated": "Fahrzeug erfolgreich aktualisiert", "vehicle_toast_error_prefix": "Fehler beim Speichern: ", - "vehicle_list_empty": "Hier ist es leer. Fügen Sie Ihr erstes Fahrzeug hinzu, um zu starten.", + "vehicle_list_empty": "Hier ist es noch leer. Füge dein erstes Fahrzeug hinzu, um zu starten.", "vehicle_delete_success": "Fahrzeug erfolgreich gelöscht.", "vehicle_delete_error": "Beim Löschen des Fahrzeugs ist ein Fehler aufgetreten.", "vehicle_action_add_fuel_log": "Tankvorgang hinzufügen", @@ -142,17 +135,17 @@ "tools_import_error": "Daten konnten nicht importiert werden", "tools_import_invalid_json": "Ungültiges JSON-Format", "feature_overview_disabled_title": "Übersicht-Funktion deaktiviert", - "feature_overview_disabled_hint": "Aktivieren Sie diese Funktion in den Einstellungen, um das Übersichts-Dashboard zu sehen", + "feature_overview_disabled_hint": "Aktiviere diese Funktion in den Einstellungen, um das Übersichts-Dashboard zu sehen", "feature_fuel_disabled_title": "Kraftstoff-Funktion deaktiviert", - "feature_fuel_disabled_hint": "Aktivieren Sie diese Funktion in den Einstellungen, um den Verbrauch zu verfolgen", + "feature_fuel_disabled_hint": "Aktiviere diese Funktion in den Einstellungen, um den Verbrauch zu verfolgen", "feature_maintenance_disabled_title": "Wartungs-Funktion deaktiviert", - "feature_maintenance_disabled_hint": "Aktivieren Sie diese Funktion, um Wartungen zu verwalten", + "feature_maintenance_disabled_hint": "Aktiviere diese Funktion, um Wartungen zu verwalten", "feature_pucc_disabled_title": "PUCC-Funktion deaktiviert", - "feature_pucc_disabled_hint": "Aktivieren Sie diese Funktion, um Verschmutzungszertifikate zu verwalten", + "feature_pucc_disabled_hint": "Aktiviere diese Funktion, um Abgaszertifikate zu verwalten", "feature_reminders_disabled_title": "Erinnerungen-Funktion deaktiviert", - "feature_reminders_disabled_hint": "Aktivieren Sie diese Funktion, um Fahrzeugerinnerungen zu verwalten", + "feature_reminders_disabled_hint": "Aktiviere diese Funktion, um Fahrzeugerinnerungen zu verwalten", "feature_insurance_disabled_title": "Versicherungs-Funktion deaktiviert", - "feature_insurance_disabled_hint": "Aktivieren Sie diese Funktion, um Versicherungsdetails zu verwalten", + "feature_insurance_disabled_hint": "Aktiviere diese Funktion, um Versicherungsdetails zu verwalten", "overview_chart_no_data": "Keine Daten verfügbar", "overview_chart_cost_label": "Kosten", "overview_chart_cost_title": "Kosten über die Zeit ({currency})", @@ -162,6 +155,7 @@ "fuel_add_title": "Kraftstoffprotokoll hinzufügen", "col_date": "Datum", "col_odometer": "Kilometerzähler", + "col_distance_driven": "Gefahrene Strecke", "col_filled": "Gefüllt", "col_missed_last": "Zuletzt verpasst", "col_fuel_amount": "Kraftstoffmenge", @@ -179,6 +173,7 @@ "form_odometer_desc": "Aktueller Kilometerzählerstand", "form_volume_fuel": "Kraftstoffvolumen", "form_volume_energy": "Verbrauchte Energie", + "form_rate": "Einheitspreis", "form_cost": "Kosten", "form_cost_desc": "Kosten der Betankung", "form_cost_desc_ev": "Kosten des Ladens", @@ -219,19 +214,6 @@ "notifications_severity_due_soon": "Bald fällig", "notifications_severity_upcoming": "Bevorstehend", "settings_custom_css_placeholder": "Füge hier dein benutzerdefiniertes CSS hinzu...", - "settings_features_intro": "Aktiviere oder deaktiviere Funktionen zur Anpassung deiner Erfahrung", - "feature_label_fuel": "Kraftstoffprotokoll", - "feature_desc_fuel": "Verbrauch und Betankungen verwalten und nachverfolgen", - "feature_label_maintenance": "Wartung", - "feature_desc_maintenance": "Wartungsaktivitäten erfassen und planen", - "feature_label_pucc": "Verschmutzung", - "feature_desc_pucc": "PUCC-Zertifikate verwalten", - "feature_label_reminders": "Erinnerungen", - "feature_desc_reminders": "Erinnerungen für wichtige Ereignisse setzen und erhalten", - "feature_label_insurance": "Versicherung", - "feature_desc_insurance": "Versicherungsdetails und -verlängerungen verwalten", - "feature_label_overview": "Übersicht", - "feature_desc_overview": "Übersichts-Dashboard mit wichtigen Fahrzeugkennzahlen anzeigen", "settings_error_date_format_invalid": "Format ungültig", "settings_error_timezone_invalid": "Ungültiger Zeitzonenwert.", "settings_error_currency_required": "Währung ist erforderlich", @@ -247,11 +229,11 @@ "maintenance_form_cost_desc": "Wartungskosten", "maintenance_form_notes_label": "Notizen", "maintenance_form_notes_desc": "Weitere Details", - "maintenance_form_notes_placeholder": "Fügen Sie bei Bedarf weitere Details hinzu...", + "maintenance_form_notes_placeholder": "Füge bei Bedarf weitere Details hinzu...", "maintenance_toast_saved": "Wartungseintrag erfolgreich gespeichert", "maintenance_toast_updated": "Wartungseintrag erfolgreich aktualisiert", "maintenance_toast_error_prefix": "Fehler beim Speichern: ", - "maintenance_form_error_fix": "Bitte beheben Sie die Fehler im Formular, bevor Sie absenden.", + "maintenance_form_error_fix": "Bitte behebe die Fehler im Formular, bevor du absendest.", "maintenance_list_empty": "Keine Wartungseinträge für dieses Fahrzeug gefunden.", "maintenance_col_service_center": "Werkstatt", "maintenance_menu_open": "Menü öffnen", @@ -281,7 +263,7 @@ "insurance_form_notes_placeholder": "Weitere Details zur Versicherung hinzufügen...", "insurance_form_attachment_label": "Versicherungsdokument", "insurance_form_attachment_desc": "Versicherungsdokument hochladen", - "insurance_form_error_fix": "Bitte beheben Sie die Fehler im Formular, bevor Sie absenden.", + "insurance_form_error_fix": "Bitte behebe die Fehler im Formular, bevor du absendest.", "insurance_toast_saved": "Versicherung erfolgreich gespeichert", "insurance_toast_updated": "Versicherung erfolgreich aktualisiert", "insurance_toast_error_prefix": "Fehler beim Speichern: ", @@ -319,7 +301,7 @@ "pollution_form_notes_placeholder": "Weitere Notizen hinzufügen...", "pollution_form_attachment_label": "Zertifikatdokument", "pollution_form_attachment_desc": "Zertifikatdokument hochladen", - "pollution_form_error_fix": "Bitte beheben Sie die Fehler im Formular, bevor Sie absenden.", + "pollution_form_error_fix": "Bitte behebe die Fehler im Formular, bevor du absendest.", "pollution_toast_saved": "Abgasprüfzertifikat erfolgreich gespeichert", "pollution_toast_updated": "Abgasprüfzertifikat erfolgreich aktualisiert", "pollution_toast_error_prefix": "Fehler beim Speichern: ", @@ -345,7 +327,7 @@ "reminder_form_type_label": "Typ", "reminder_form_type_desc": "Erinnerungstyp wählen", "reminder_form_schedule_label": "Erinnerungsplan", - "reminder_form_schedule_desc": "Wann sollten wir Sie erinnern?", + "reminder_form_schedule_desc": "Wann sollen wir dich erinnern?", "reminder_form_recurrence_type_label": "Wiederholung", "reminder_form_recurrence_type_desc": "Sollte sich diese Erinnerung wiederholen?", "reminder_form_recurrence_interval_label": "Alle wiederholen", @@ -353,15 +335,15 @@ "reminder_form_recurrence_end_date_label": "Enddatum", "reminder_form_recurrence_end_date_desc": "Wann sollte die Wiederholung beendet werden? (optional)", "reminder_form_note_label": "Notiz", - "reminder_form_note_desc": "Fügen Sie mehr Kontext hinzu", + "reminder_form_note_desc": "Füge mehr Kontext hinzu", "reminder_form_note_placeholder": "Details, die Aufmerksamkeit benötigen", "reminder_form_is_completed_label": "Als erledigt markieren", "reminder_toast_created": "Erinnerung erfolgreich erstellt.", "reminder_toast_updated": "Erinnerung erfolgreich aktualisiert", "reminder_toast_error_prefix": "Fehler beim Speichern: ", - "reminder_list_empty": "Noch keine Erinnerungen. Erstellen Sie eine, um bevorstehenden Verlängerungen voraus zu sein.", - "reminder_list_select_vehicle": "Wählen Sie ein Fahrzeug, um Erinnerungen zu sehen.", - "reminder_list_select_hint": "Wählen Sie oben ein Fahrzeug aus, um bevorstehende Erinnerungen zu laden.", + "reminder_list_empty": "Noch keine Erinnerungen. Erstelle eine, um bevorstehenden Fälligkeiten voraus zu sein.", + "reminder_list_select_vehicle": "Wähle ein Fahrzeug, um Erinnerungen zu sehen.", + "reminder_list_select_hint": "Wähle oben ein Fahrzeug aus, um bevorstehende Erinnerungen zu laden.", "reminder_col_due_date": "Fälligkeitsdatum", "reminder_col_reminder_schedule": "Erinnerungsplan", "reminder_col_recurrence": "Wiederholung", @@ -383,36 +365,38 @@ "reminder_status_pending": "Ausstehend", "reminder_status_overdue": "Überfällig", "settings_sheet_title": "Einstellungen", - "settings_features_intro": "Aktivieren oder deaktivieren Sie Funktionen, um Ihr Erlebnis anzupassen", + "settings_features_intro": "Aktiviere oder deaktiviere Funktionen, um dein Erlebnis anzupassen", "feature_label_fuel": "Tankbuch", "feature_desc_fuel": "Überwachung und Verwaltung des Kraftstoffverbrauchs und der Betankungshistorie", "feature_label_maintenance": "Wartung", "feature_desc_maintenance": "Verzeichnis und Planung von Fahrzeugwartungsaktivitäten", "feature_label_pollution": "Schadstoffkontrolle", "feature_desc_pollution": "Verwaltung von Registern für Schadstoffkontrollzertifikate", + "feature_label_pucc": "Schadstoffkontrolle", + "feature_desc_pucc": "Abgaszertifikate (PUCC) verwalten", "feature_label_reminders": "Erinnerungen", - "feature_desc_reminders": "Legen Sie Erinnerungen für wichtige Fahrzeugereignisse fest und erhalten Sie diese", + "feature_desc_reminders": "Erinnerungen für wichtige Fahrzeugereignisse setzen und erhalten", "feature_label_insurance": "Versicherung", "feature_desc_insurance": "Verwaltung von Fahrzeugversicherungsdetails und -erneuerungen", "feature_label_overview": "Überblick", - "feature_desc_overview": "Zeigen Sie das Überblicks-Dashboard mit wichtigen Fahrzeugmetriken an", + "feature_desc_overview": "Überblicks-Dashboard mit wichtigen Fahrzeugmetriken anzeigen", "profile_menu_item": "Profil", "profile_sheet_title": "Profil", - "profile_sheet_desc": "Aktualisieren Sie Ihren Benutzernamen und Ihr Passwort", + "profile_sheet_desc": "Aktualisiere deinen Benutzernamen und dein Passwort", "profile_username": "Benutzername", - "profile_username_desc": "Ihr Anzeigename", - "profile_password_hint": "Lassen Sie die Passwortfelder leer, um Ihr aktuelles Passwort zu behalten", + "profile_username_desc": "Dein Anzeigename", + "profile_password_hint": "Lass die Passwortfelder leer, um dein aktuelles Passwort zu behalten", "profile_current_password": "Aktuelles Passwort", "profile_current_password_desc": "Erforderlich zum Ändern des Passworts", "profile_new_password": "Neues Passwort", "profile_new_password_desc": "Mindestens 6 Zeichen", "profile_confirm_password": "Passwort bestätigen", - "profile_confirm_password_desc": "Geben Sie Ihr neues Passwort erneut ein", + "profile_confirm_password_desc": "Gib dein neues Passwort erneut ein", "profile_update_button": "Profil aktualisieren", "tools_menu": "Werkzeuge", "data_export_import_menu_item": "Daten exportieren/importieren", "data_export_import_sheet_title": "Datenexport/-import", - "data_export_import_sheet_desc": "Exportieren oder importieren Sie Ihre Datenbank mit optionaler Verschlüsselung", + "data_export_import_sheet_desc": "Exportiere oder importiere deine Datenbank mit optionaler Verschlüsselung", "logout_menu_item": "Abmelden", "nav_overview": "Überblick", "nav_fuel_logs": "Tankbuch", @@ -425,7 +409,7 @@ "custom_fields_name_placeholder": "Feldname", "custom_fields_value_placeholder": "Feldwert", "custom_fields_remove_aria": "Feld entfernen", - "custom_fields_empty_message": "Keine benutzerdefinierten Felder hinzugefügt. Klicken Sie auf \"Feld hinzufügen\", um zu beginnen.", + "custom_fields_empty_message": "Keine benutzerdefinierten Felder hinzugefügt. Klicke auf \"Feld hinzufügen\", um zu beginnen.", "vehicle_details_vin": "FIN", "vehicle_details_not_specified": "Nicht angegeben", "vehicle_details_section_title": "Details", @@ -465,5 +449,260 @@ "pollution_recurrence_type_fixed": "Festes Enddatum", "pollution_recurrence_type_yearly": "Verlängert sich jährlich", "pollution_recurrence_type_monthly": "Verlängert sich monatlich", - "pollution_recurrence_type_no_end": "Kein Enddatum" + "pollution_recurrence_type_no_end": "Kein Enddatum", + "app_new_update_available": "Neues Update verfügbar. Seite wird neu geladen…", + "settings_mileage_format_uk_mpg": "UK MPG (Meilen pro imperiale Gallone)", + "common_confirm": "Bestätigen", + "notifications_mark_all_read_title": "Alle als gelesen markieren", + "notifications_mark_all_read_aria": "Alle Benachrichtigungen als gelesen markieren", + "file_drop_existing_note": "Vorhandener Anhang (Klicken zum Anzeigen)", + "fuel_import_step_1_title": "Schritt 1: CSV-Datei hochladen", + "fuel_import_step_1_desc": "Wähle eine Textdatei mit Trennzeichen aus, die deine Tankdaten enthält, um den Import zu starten.", + "fuel_import_drop_placeholder": "Datei hierher ziehen oder klicken zum Durchsuchen", + "fuel_import_headers_checkbox": "Erste Zeile enthält Spaltenüberschriften", + "fuel_import_delimiter_title": "Trennzeichen", + "fuel_import_delimiter_desc": "Wähle das Zeichen, das die Felder trennt", + "fuel_import_date_format_title": "Datumsformat", + "fuel_import_date_format_desc": "Gib das Datumsformat an, das in deiner CSV-Datei verwendet wird.", + "fuel_import_error_no_headers": "Keine Spaltenüberschriften erkannt. Aktualisiere csv.helper.ts, um Überschriften zurückzugeben.", + "fuel_import_step_2_title": "Schritt 2: CSV-Spalten zuordnen", + "fuel_import_step_2_desc": "Ordne die Spalten deiner CSV-Datei den entsprechenden Tankbuch-Feldern zu. Pflichtfelder sind mit * markiert.", + "fuel_import_step_3_title": "Schritt 3: Vorschau & Import", + "fuel_import_step_3_desc": "Überprüfe die Vorschau der zu importierenden Daten.", + "fuel_import_no_preview": "Noch keine Vorschaudaten. Implementiere das Parsen in csv.helper.ts, um Zeilen zu befüllen.", + "fuel_import_success": "{count} Tankprotokoll(e) erfolgreich importiert.", + "fuel_import_failed_count": "{imported} importiert, {failed} fehlgeschlagen", + "fuel_import_error_generic": "Import der Tankprotokolle fehlgeschlagen.", + "fuel_import_vehicle_label": "Fahrzeug:", + "fuel_import_delimiter_comma": "Komma ( , )", + "fuel_import_delimiter_semicolon": "Semikolon ( ; )", + "fuel_import_delimiter_tab": "Tabulator ( \\t )", + "fuel_import_delimiter_pipe": "Pipe ( | )", + "fuel_import_delimiter_custom": "Benutzerdefiniert", + "fuel_import_date_error": "Einige Zeilen haben ungültige Daten für das Format \"{format}\"", + "fuel_import_date_format_placeholder": "z.B. DD.MM.YYYY", + "fuel_import_date_invalid": "Ungültiges Datum", + "fuel_import_no_vehicle": "Kein Fahrzeug ausgewählt", + "fuel_import_col_date_hint": "Datum der Betankung", + "fuel_import_col_odometer_hint": "Kilometerstand bei der Betankung", + "fuel_import_col_fuel_hint": "Volumen oder geladene Energie", + "fuel_import_col_cost_hint": "Gesamtkosten für den Eintrag", + "fuel_import_col_filled_hint": "Wurde vollgetankt/vollgeladen?", + "fuel_import_col_missed_hint": "Wurde der vorherige Eintrag verpasst?", + "fuel_import_col_notes_hint": "Zusätzliche Notizen", + "autocomplete_placeholder": "Tippen oder auswählen…", + "autocomplete_loading": "Vorschläge werden geladen…", + "autocomplete_no_results": "Keine Vorschläge gefunden. Du kannst einen neuen Wert eingeben.", + "input_date_placeholder": "Datum auswählen", + "loading_default_message": "Wird geladen…", + "dropzone_placeholder_image": "Bild hierher ziehen oder klicken zum Hochladen", + "dropzone_placeholder_attachment": "Datei hierher ziehen oder klicken zum Auswählen", + "dropzone_placeholder_default": "Dateien hierher ziehen oder klicken zum Hochladen", + "dropzone_error_single_file": "Bitte nur eine Datei hochladen.", + "dropzone_error_file_size": "Dateigröße überschreitet das Maximum von {size}.", + "dropzone_unknown_file": "Unbekannte Datei", + "dropzone_uploading": "Wird hochgeladen…", + "dropzone_supports": "Unterstützt: {types}", + "dropzone_max_size": "Max. Größe: {size}", + "dropzone_error_file_type": "Dateityp nicht erlaubt", + "dropzone_hint_accept_limit": "{types} bis {size}", + "vehicle_details_color_aria": "Farbe", + "vehicle_details_close_aria": "Schließen", + "attachment_link_view_title": "Anhang anzeigen", + "file_preview_not_available": "Vorschau nicht verfügbar", + "file_preview_download_hint": "Dieser Dateityp kann nicht direkt angezeigt werden. Bitte lade die Datei herunter.", + "file_preview_download_button": "Datei herunterladen", + "file_preview_aria_download": "Herunterladen", + "file_preview_aria_close": "Schließen", + "theme_toggle_label": "Design umschalten", + "fuel_log_edit": "Bearbeiten", + "fuel_log_delete": "Löschen", + "fuel_log_menu_open": "Menü öffnen", + "fuel_log_delete_success": "Tankprotokoll gelöscht", + "fuel_log_menu_sheet_title": "Tankprotokoll aktualisieren", + "fuel_log_delete_error": "Beim Löschen des Tankprotokolls ist ein Fehler aufgetreten.", + "color_picker_label": "Farbe auswählen", + "reminder_type_maintenance": "Wartung", + "reminder_type_insurance": "Versicherungsverlängerung", + "reminder_type_pollution": "Abgasprüfung / PUCC", + "reminder_type_registration": "Zulassung / Steuer", + "reminder_type_inspection": "Hauptuntersuchung", + "reminder_type_custom": "Benutzerdefiniert", + "alert_type_insurance": "Versicherung", + "alert_type_pucc": "Abgaszertifikat", + "alert_status_expired_ago": "{label} vor {days} Tagen abgelaufen", + "alert_status_expires_in": "{label} läuft in {days} Tagen ab", + "alert_status_valid_for": "{label} noch {days} Tage gültig", + "alert_insurance_active_no_end": "Versicherung ist aktiv ohne Enddatum", + "alert_pucc_active_no_end": "Abgaszertifikat ist aktiv ohne Enddatum", + "alert_record_not_found": "Kein {label}-Eintrag gefunden. Füge Details hinzu, um die Vorschriften einzuhalten.", + "settings_error_format_not_valid": "Format ungültig", + "common_kilogram_unit": "Kilogramm (kg)", + "common_pound_unit": "Pfund (lb)", + "settings_section_fuel_types": "Kraftstoffarten", + "settings_section_fuel_types_desc": "Wähle die Maßeinheit für jeden Kraftstoff.", + "fuel_type_petrol_diesel": "Benzin/Diesel", + "theme_slate": "Schiefer", + "theme_stone": "Stein", + "theme_red": "Rot", + "theme_rose": "Rosé", + "theme_blue": "Blau", + "theme_green": "Grün", + "theme_purple": "Lila", + "theme_orange": "Orange", + "theme_yellow": "Gelb", + "theme_teal": "Türkis", + "theme_indigo": "Indigo", + "theme_pink": "Pink", + "settings_tab_notifications": "Benachrichtigungen", + "settings_personalization_desc": "Passe dein Erlebnis mit Designs, Sprachen und Formaten an.", + "settings_section_general": "Allgemein", + "settings_section_general_desc": "Darstellung, Sprache und Anzeigeformate anpassen", + "settings_units_desc": "Maßeinheiten für Entfernung, Volumen und Kraftstoffarten konfigurieren.", + "settings_section_units": "Einheiten", + "settings_section_units_desc": "Bevorzugte Einheiten für Entfernung, Verbrauch und Kraftstoffarten wählen", + "settings_section_feature_flags": "Funktionsmodule", + "settings_section_feature_flags_desc": "Hauptmodule der App aktivieren oder deaktivieren", + "settings_notifications_desc": "Anbieter-Abonnements und die tägliche Verarbeitungszeit für geplante Zustellung konfigurieren.", + "settings_error_fix_errors": "Bitte behebe die folgenden Fehler:", + "settings_fuel_types_label": "Kraftstoffarten", + "settings_fuel_types_desc": "Wähle die Maßeinheit für jeden Kraftstoff.", + "fuel_type_label_petrol_diesel": "Benzin/Diesel", + "fuel_type_label_lpg": "Flüssiggas (LPG)", + "fuel_type_label_cng": "Erdgas (CNG)", + "notif_scheduled_delivery": "Geplante Zustellung", + "notif_scheduled_delivery_desc": "In-App-Benachrichtigungen bleiben in Echtzeit. Dieser Zeitplan steuert nur die Anbieter-Zustellung.", + "notif_processing_schedule": "Verarbeitungszeitplan", + "notif_processing_schedule_desc": "Anbieter-Benachrichtigungen nach Zeitplan zustellen.", + "notif_send_now": "Jetzt senden", + "notif_providers": "Anbieter", + "notif_providers_desc": "Benachrichtigungsanbieter erstellen, bearbeiten, testen und aktivieren.", + "notif_providers_channels_info": "Jeder Anbieter kann Erinnerungs-, Alarm- und Informationskanäle abonnieren.", + "notif_add_provider": "Anbieter hinzufügen", + "notif_empty_title": "Noch keine Benachrichtigungsanbieter konfiguriert", + "notif_empty_desc": "Füge einen Anbieter hinzu, um geplante Erinnerungs-, Alarm- oder Informationsbenachrichtigungen zu erhalten.", + "notif_load_failed": "Benachrichtigungsanbieter konnten nicht geladen werden", + "notif_select_provider_type": "Bitte wähle einen Anbietertyp", + "notif_select_channel": "Wähle mindestens einen Benachrichtigungskanal", + "notif_provider_updated": "Anbieter erfolgreich aktualisiert", + "notif_provider_created": "Anbieter erfolgreich erstellt", + "notif_provider_deleted": "Anbieter erfolgreich gelöscht", + "notif_provider_delete_failed": "Anbieter konnte nicht gelöscht werden", + "notif_channel_reminder": "Erinnerungen", + "notif_channel_reminder_desc": "Fälligkeitsdaten und Erinnerungsbenachrichtigungen", + "notif_channel_alert": "Alarm", + "notif_channel_alert_desc": "Dringende oder ablaufende Einträge, die Aufmerksamkeit erfordern", + "notif_channel_information": "Information", + "notif_channel_information_desc": "Allgemeine Informationsmeldungen", + "notif_provider_enabled": "Aktiviert", + "notif_provider_test": "Anbieter testen", + "notif_provider_edit": "Anbieter bearbeiten", + "notif_provider_delete": "Anbieter löschen", + "notif_dialog_add_title": "Benachrichtigungsanbieter hinzufügen", + "notif_dialog_edit_title": "Benachrichtigungsanbieter bearbeiten", + "notif_dialog_desc": "Wähle einen Anbietertyp, konfiguriere das Ziel und abonniere Kanäle. Anbieter sind standardmäßig aktiviert und können über die Anbieterkarten deaktiviert werden.", + "notif_provider_name": "Anbietername", + "notif_provider_name_placeholder": "Tägliche Zusammenfassung per E-Mail", + "notif_provider_type": "Anbietertyp", + "notif_provider_type_email": "E-Mail (SMTP)", + "notif_provider_type_webhook": "Webhook", + "notif_provider_type_gotify": "Gotify", + "notif_provider_type_select": "Anbietertyp auswählen", + "notif_dialog_cancel": "Abbrechen", + "notif_dialog_create": "Anbieter erstellen", + "notif_dialog_update": "Anbieter aktualisieren", + "notif_channel_subscriptions": "Kanal-Abonnements", + "notif_channel_subscriptions_desc": "Wähle, welche Benachrichtigungskanäle dieser Anbieter erhalten soll.", + "notif_test_title": "Anbieter testen", + "notif_test_success": "Testbenachrichtigung erfolgreich gesendet", + "notif_test_email_label": "Test-E-Mail", + "notif_test_email_placeholder": "empfaenger@example.com", + "notif_test_email_desc": "Optionaler Empfänger für diese Testnachricht.", + "notif_test_message_label": "Testnachricht", + "notif_test_message_placeholder": "Dies ist eine Testbenachrichtigung von Tracktor", + "notif_test_send": "Test senden", + "notif_cron_presets": "Vorlagen", + "notif_cron_every_minute": "Jede Minute", + "notif_cron_every_5_min": "Alle 5 Minuten", + "notif_cron_every_15_min": "Alle 15 Minuten", + "notif_cron_every_30_min": "Alle 30 Minuten", + "notif_cron_every_hour": "Jede Stunde", + "notif_cron_every_2_hours": "Alle 2 Stunden", + "notif_cron_every_6_hours": "Alle 6 Stunden", + "notif_cron_every_12_hours": "Alle 12 Stunden", + "notif_cron_daily_midnight": "Täglich um Mitternacht", + "notif_cron_daily_2am": "Täglich um 2:00 Uhr", + "notif_cron_daily_8am": "Täglich um 8:00 Uhr", + "notif_cron_daily_noon": "Täglich um 12:00 Uhr", + "notif_cron_weekly_monday": "Wöchentlich (Montag)", + "notif_cron_monthly_1st": "Monatlich (1.)", + "notif_cron_expression_required": "Ausdruck erforderlich", + "notif_cron_must_have_5_parts": "Muss 5 Teile haben", + "notif_cron_invalid_chars": "Ungültige Zeichen", + "notif_cron_valid": "Gültiger Ausdruck", + "notif_cron_invalid": "Ungültiger Ausdruck", + "notif_cron_custom": "Benutzerdefinierter Zeitplan", + "notif_email_smtp_settings": "SMTP-Server-Einstellungen", + "notif_email_host": "Host", + "notif_email_port": "Port", + "notif_email_use_ssl": "SSL/TLS verwenden", + "notif_email_use_ssl_desc": "Sichere Verbindung aktivieren", + "notif_email_auth": "Authentifizierung", + "notif_email_username": "Benutzername / E-Mail", + "notif_email_password": "Passwort", + "notif_email_password_keep": "Passwort (leer lassen zum Beibehalten)", + "notif_email_sender_info": "Absender-/Empfängerinformationen", + "notif_email_from": "Absender-E-Mail", + "notif_email_from_name": "Absendername (Optional)", + "notif_email_from_name_placeholder": "Tracktor-Benachrichtigungen", + "notif_email_recipient": "Empfänger-E-Mail", + "notif_email_recipient_desc": "E-Mail-Adresse des Empfängers", + "notif_webhook_config": "Webhook-Konfiguration", + "notif_webhook_url": "Webhook-URL", + "notif_webhook_method": "HTTP-Methode", + "notif_webhook_headers": "Benutzerdefinierte Header (JSON)", + "notif_webhook_headers_desc": "Zusätzliche Header für die Webhook-Anfrage", + "notif_webhook_auth_type": "Authentifizierungstyp", + "notif_webhook_auth_none": "Keine", + "notif_webhook_auth_basic": "Basic Auth", + "notif_webhook_auth_bearer": "Bearer Token", + "notif_webhook_auth_apikey": "API-Schlüssel", + "notif_webhook_username": "Benutzername", + "notif_webhook_apikey_header": "API-Schlüssel Header-Name", + "notif_gotify_config": "Gotify-Server-Konfiguration", + "notif_gotify_url": "Server-URL", + "notif_gotify_url_desc": "URL deiner Gotify-Server-Instanz", + "notif_gotify_token": "App-Token", + "notif_gotify_token_keep": "App-Token (leer lassen zum Beibehalten)", + "notif_gotify_token_desc": "Anwendungstoken deiner Gotify-App (nicht der Client-Token)", + "notif_gotify_priority": "Priorität (0-10)", + "notif_gotify_priority_desc": "Nachrichtenpriorität. Höhere Priorität = auffälligere Benachrichtigung", + "notif_cleared_success_one": "1 gelesene Benachrichtigung gelöscht", + "notif_cleared_success_other": "{count} gelesene Benachrichtigungen gelöscht", + "notif_cleared_partial": "{success} gelöscht, {failed} fehlgeschlagen", + "notif_clear_failed": "Gelesene Benachrichtigungen konnten nicht gelöscht werden", + "notif_all_marked_read": "Alle Benachrichtigungen als gelesen markiert", + "notif_mark_read_failed": "Benachrichtigungen konnten nicht als gelesen markiert werden", + "notif_button_mark_all_read": "Alle gelesen", + "notif_button_clear_read": "Gelesene löschen", + "notif_button_clear_all_read_title": "Alle gelesenen Benachrichtigungen löschen", + "notif_status_read": "Gelesen", + "notif_status_unread": "Ungelesen", + "notif_due_prefix": "Fällig: {date}", + "header_home_aria": "Zur Startseite", + "header_settings_aria": "Einstellungen öffnen", + "header_account_aria": "Kontomenü", + "header_account_title": "Konto", + "notif_save_provider_failed": "Anbieter konnte nicht gespeichert werden", + "notif_update_provider_failed": "Anbieter konnte nicht aktualisiert werden", + "notif_send_all_failed": "Benachrichtigungen konnten nicht gesendet werden", + "notif_send_all_success": "{notifCount} Benachrichtigungen an {successCount}/{providerCount} aktive Anbieter gesendet", + "notif_confirm_delete": "Bist du sicher, dass du \"{name}\" löschen möchtest?", + "notif_webhook_bearer_keep": "Bearer Token (leer lassen zum Beibehalten)", + "notif_webhook_apikey_keep": "API-Schlüssel (leer lassen zum Beibehalten)", + "notif_test_failed": "Testbenachrichtigung konnte nicht gesendet werden", + "notif_test_send_desc": "Testbenachrichtigung über {name} senden", + "notif_cron_every_n_minutes": "Alle {n} Minuten", + "notif_cron_hourly_at_minute": "Stündlich bei Minute {n}", + "notif_cron_daily_at": "Täglich um {time}" } diff --git a/messages/en.json b/i18n/messages/en.json similarity index 54% rename from messages/en.json rename to i18n/messages/en.json index c7536046..fb0349ca 100644 --- a/messages/en.json +++ b/i18n/messages/en.json @@ -5,6 +5,10 @@ "app_title": "Your Garage", "app_new_update_available": "New Update is available. Reloading..!", "app_add_vehicle": "Add Vehicle", + "vehicle_scope_all": "All vehicles", + "vehicle_scope_manage": "Manage vehicles", + "form_vehicle_label": "Vehicle", + "form_vehicle_desc": "Which vehicle this record belongs to", "app_empty_select_message": "Select a vehicle to view its details", "app_empty_select_hint": "Choose one from the garage above to load its dashboard.", "demo_banner": "This is a demo instance. Data will be reset periodically and is not saved permanently. Please avoid adding any personal info.", @@ -19,11 +23,16 @@ "auth_login_loading": "Signing in...", "auth_signup_loading": "Creating account...", "auth_password_mismatch": "Passwords do not match!!!", + "auth_login_title": "Welcome back", + "auth_login_subtitle": "Sign in to keep your vehicles, fuel, and paperwork on track.", "settings_tab_personalization": "Personalization", "settings_tab_interface": "Interface", + "settings_tab_localization": "Localization", + "settings_tab_advanced": "Advanced", "settings_tab_features": "Features", "settings_tab_units": "Units", "settings_title": "Settings", + "settings_page_description": "Configure appearance, language, regional formats, and interface behavior.", "settings_label_date_format": "Date Format", "settings_label_locale": "Locale", "settings_label_timezone": "Timezone", @@ -31,10 +40,12 @@ "settings_label_unit_distance": "Unit of Distance", "settings_label_unit_volume": "Unit of Fuel", "settings_label_theme": "Theme", + "settings_label_dark_mode": "Dark Mode Style", "settings_label_custom_css": "Custom CSS", "settings_update_button": "Update Settings", "settings_select_unit_system": "Select unit system", "settings_select_theme": "Select theme", + "settings_select_dark_mode": "Select dark mode style", "settings_desc_date_format": "Choose your preferred date format", "settings_desc_locale": "Choose the language for the interface", "settings_desc_timezone": "Choose your timezone for date display", @@ -47,6 +58,7 @@ "settings_mileage_format_fuel_per_distance": "Fuel per Distance (e.g., L/100km)", "settings_mileage_format_uk_mpg": "UK MPG (miles per imperial gallon)", "settings_desc_theme": "Choose your preferred theme", + "settings_desc_dark_mode": "Choose the contrast style used when dark mode is active", "settings_desc_custom_css": "CSS Styles for customizing the interface", "settings_select_language": "Select language", "settings_updated_success": "Configuration updated successfully!", @@ -57,6 +69,13 @@ "common_litre": "Litre", "common_gallon": "Gallon", "common_submit": "Submit", + "common_details": "Details", + "common_close": "Close", + "common_view": "View", + "common_view_all": "View All", + "common_view_less": "Show Less", + "common_loading": "Loading...", + "common_add": "Add", "common_yes": "Yes", "common_no": "No", "common_cancel": "Cancel", @@ -71,15 +90,17 @@ "common_columns": "Columns", "common_rows_per_page": "Rows per page", "common_no_data_available": "No data available", + "common_no_records_found": "No records found", "common_add_new": "Add New", "common_no_match_found": "No match found", "common_search_placeholder": "Search {name}", "common_select_placeholder": "Select {name}...", + "common_date_from": "From", + "common_date_to": "To", + "common_export_csv": "Export CSV", "nav_overview": "Overview", "nav_fuel_logs": "Fuel Logs", "nav_maintenance": "Maintenance", - "nav_insurance": "Insurance", - "nav_pollution": "Pollution Certificate", "nav_reminders": "Reminders", "tools_export_data": "Export Data", "tools_import_data": "Import Data", @@ -94,6 +115,9 @@ "vehicle_form_fuel_type_label": "Fuel Type", "vehicle_form_fuel_type_desc": "Type of fuel used by the vehicle", "vehicle_form_fuel_type_placeholder": "Select fuel type", + "vehicle_form_vehicle_type_label": "Vehicle Type", + "vehicle_form_vehicle_type_desc": "Category of the vehicle", + "vehicle_form_vehicle_type_placeholder": "Select vehicle type", "vehicle_form_odometer_label": "Odometer", "vehicle_form_odometer_desc": "Current vehicle odometer reading", "vehicle_form_license_label": "License Plate", @@ -108,8 +132,6 @@ "vehicle_delete_error": "Some error occurred while deleting vehicle.", "vehicle_action_add_fuel_log": "Add Fuel Log", "vehicle_action_add_maintenance_log": "Add Maintenance Log", - "vehicle_action_add_insurance": "Add Insurance", - "vehicle_action_add_pollution": "Add Pollution Certificate", "vehicle_action_add_reminder": "Add Reminder", "vehicle_action_more_info": "More info", "vehicle_action_update_vehicle": "Update Vehicle", @@ -149,12 +171,8 @@ "feature_fuel_disabled_hint": "Enable this feature in Settings to track fuel consumption", "feature_maintenance_disabled_title": "Maintenance Feature Disabled", "feature_maintenance_disabled_hint": "Enable this feature in Settings to manage maintenance records", - "feature_pucc_disabled_title": "PUCC Feature Disabled", - "feature_pucc_disabled_hint": "Enable this feature in Settings to manage pollution certificates", "feature_reminders_disabled_title": "Reminders Feature Disabled", "feature_reminders_disabled_hint": "Enable this feature in Settings to manage vehicle reminders", - "feature_insurance_disabled_title": "Insurance Feature Disabled", - "feature_insurance_disabled_hint": "Enable this feature in Settings to manage insurance details", "overview_chart_no_data": "No data available", "overview_chart_cost_label": "Cost", "overview_chart_cost_title": "Cost over Time in ({currency})", @@ -164,6 +182,7 @@ "fuel_add_title": "Add Fuel Log", "col_date": "Date", "col_odometer": "Odometer", + "col_distance_driven": "Distance Driven", "col_filled": "Filled", "col_missed_last": "Missed Last", "col_fuel_amount": "Fuel Amount", @@ -181,6 +200,7 @@ "form_odometer_desc": "Current vehicle odometer reading", "form_volume_fuel": "Volume of fuel", "form_volume_energy": "Energy consumed", + "form_rate": "Rate", "form_cost": "Cost", "form_cost_desc": "Cost of refill", "form_cost_desc_ev": "Cost of charging", @@ -198,7 +218,6 @@ "fuel_toast_error_prefix": "Error while saving : ", "notifications_title": "Notifications", "notifications_new": "new", - "notifications_select_vehicle_hint": "Select a vehicle to load reminders and alerts.", "notifications_syncing": "Syncing latest data...", "notifications_caught_up": "You're all caught up.", "notifications_section_reminders": "Reminders", @@ -228,12 +247,8 @@ "feature_desc_fuel": "Track and manage fuel consumption and refueling history", "feature_label_maintenance": "Maintenance", "feature_desc_maintenance": "Record and schedule vehicle maintenance activities", - "feature_label_pucc": "Pollution", - "feature_desc_pucc": "Manage Pollution Under Control Certificate records", "feature_label_reminders": "Reminders", "feature_desc_reminders": "Set and receive reminders for important vehicle events", - "feature_label_insurance": "Insurance", - "feature_desc_insurance": "Manage vehicle insurance details and renewals", "feature_label_overview": "Overview", "feature_desc_overview": "Display overview dashboard with key vehicle metrics", "settings_error_date_format_invalid": "Format not valid", @@ -264,86 +279,9 @@ "maintenance_menu_sheet_title": "Update Maintenance Log", "maintenance_tab_title": "Maintenance History", "maintenance_add_action": "Add Maintenance Log", + "maintenance_export_pdf": "Export PDF", "maintenance_delete_success": "Deleted maintenance log.", "maintenance_delete_error": "Some error occurred while deleting maintenance log.", - "insurance_form_provider_label": "Insurance Provider", - "insurance_form_provider_desc": "Name of the insurance company", - "insurance_form_policy_number_label": "Insurance Policy Number", - "insurance_form_policy_number_desc": "Policy number from your insurance document", - "insurance_form_start_date_label": "Insurance Start Date", - "insurance_form_start_date_desc": "Date when coverage begins", - "insurance_form_recurrence_type_label": "How should this insurance renew?", - "insurance_form_recurrence_type_desc": "Renewal type for this insurance", - "insurance_form_recurrence_interval_label": "Renewal Frequency", - "insurance_form_recurrence_interval_desc": "How often the insurance renews", - "insurance_form_end_date_label": "Insurance End Date", - "insurance_form_end_date_desc": "Date when coverage expires", - "insurance_form_cost_label": "Insurance Cost", - "insurance_form_cost_desc": "Annual or policy period cost", - "insurance_form_notes_label": "Additional Notes", - "insurance_form_notes_desc": "Any extra information about the policy", - "insurance_form_notes_placeholder": "Add more details about the insurance...", - "insurance_form_attachment_label": "Policy Document", - "insurance_form_attachment_desc": "Upload policy document", - "insurance_form_error_fix": "Please fix the errors in the form before submitting.", - "insurance_toast_saved": "Insurance saved successfully", - "insurance_toast_updated": "Insurance updated successfully", - "insurance_toast_error_prefix": "Error while saving: ", - "insurance_list_empty": "No Insurance found for this vehicle.", - "insurance_col_policy_number": "Policy Number", - "insurance_col_cost": "Cost", - "insurance_col_start_date": "Start Date", - "insurance_col_end_date": "End Date", - "insurance_col_next_due": "Next Due", - "insurance_col_recurrence": "Recurrence", - "insurance_col_notes": "Notes", - "insurance_menu_open": "Open menu", - "insurance_menu_edit": "Edit", - "insurance_menu_delete": "Delete", - "insurance_menu_sheet_title": "Update Insurance", - "insurance_tab_title": "Insurance Details", - "insurance_add_action": "Add Insurance", - "insurance_delete_success": "Deleted Insurance.", - "insurance_delete_error": "Some error occurred while deleting insurance.", - "insurance_col_view_document": "View Document", - "pollution_form_certificate_number_label": "Certificate Number", - "pollution_form_certificate_number_desc": "Pollution certificate number", - "pollution_form_issue_date_label": "Issue Date", - "pollution_form_issue_date_desc": "Certificate issue date", - "pollution_form_recurrence_type_label": "How should this certificate renew?", - "pollution_form_recurrence_type_desc": "Renewal type for this certificate", - "pollution_form_recurrence_interval_label": "Renewal Frequency", - "pollution_form_recurrence_interval_desc": "How often the certificate renews", - "pollution_form_expiry_date_label": "Expiry Date", - "pollution_form_expiry_date_desc": "PUCC expiry date", - "pollution_form_testing_center_label": "Testing Center", - "pollution_form_testing_center_desc": "Testing center name", - "pollution_form_notes_label": "Additional Notes", - "pollution_form_notes_desc": "Any extra information", - "pollution_form_notes_placeholder": "Add additional notes...", - "pollution_form_attachment_label": "Certificate Document", - "pollution_form_attachment_desc": "Upload certificate document", - "pollution_form_error_fix": "Please fix the errors in the form before submitting.", - "pollution_toast_saved": "Pollution Certificate saved successfully", - "pollution_toast_updated": "Pollution Certificate updated successfully", - "pollution_toast_error_prefix": "Error while saving: ", - "pollution_list_empty": "No Pollution Certificates for this vehicle.", - "pollution_col_certificate_number": "Certificate Number", - "pollution_col_issue_date": "Issue Date", - "pollution_col_expiry_date": "Expiry Date", - "pollution_col_next_due": "Next Due", - "pollution_col_testing_center": "Testing Center", - "pollution_col_notes": "Notes", - "pollution_col_view_certificate": "View Certificate", - "pollution_col_recurrence": "Recurrence", - "pollution_menu_open": "Open menu", - "pollution_menu_edit": "Edit", - "pollution_menu_delete": "Delete", - "pollution_menu_sheet_title": "Update Pollution Certificate", - "pollution_tab_title": "Pollution Certificate Details", - "pollution_add_action": "Add Pollution Certificate", - "pollution_delete_success": "Deleted PUCC.", - "pollution_delete_error": "Some error occurred while deleting PUCC.", "reminder_form_due_date_label": "Due Date", "reminder_form_due_date_desc": "When should this reminder trigger?", "reminder_form_type_label": "Type", @@ -377,7 +315,6 @@ "reminder_menu_delete": "Delete", "reminder_menu_open": "Open menu", "reminder_menu_sheet_title": "Update Reminder", - "reminder_tab_title": "Reminders", "reminder_add_action": "Add Reminder", "reminder_delete_success": "Deleted reminder.", "reminder_delete_error": "Some error occurred while deleting reminder.", @@ -387,8 +324,6 @@ "reminder_status_error": "Unable to update reminder status.", "reminder_toast_error_fallback": "Failed to save reminder.", "settings_sheet_title": "Settings", - "feature_label_pollution": "Pollution", - "feature_desc_pollution": "Manage Pollution Under Control Certificate records", "profile_menu_item": "Profile", "profile_sheet_title": "Profile", "profile_sheet_desc": "Update your username and password", @@ -415,6 +350,7 @@ "custom_fields_empty_message": "No custom fields added. Click \"Add Field\" to get started.", "vehicle_details_vin": "VIN", "vehicle_details_not_specified": "Not specified", + "vehicle_details_not_available": "Not available", "vehicle_details_section_title": "Details", "vehicle_details_license_plate": "License Plate", "vehicle_details_fuel_type": "Fuel Type", @@ -422,6 +358,16 @@ "vehicle_details_not_recorded": "Not recorded", "vehicle_details_color": "Color", "vehicle_details_year": "Year", + "vehicle_type_car": "Car", + "vehicle_type_motorcycle": "Motorcycle", + "vehicle_type_scooter": "Scooter", + "vehicle_type_truck": "Truck", + "vehicle_type_van": "Van", + "vehicle_type_bus": "Bus", + "vehicle_type_farm_vehicle": "Farm Vehicle", + "vehicle_type_yacht": "Yacht", + "vehicle_type_rv": "RV / Caravan", + "vehicle_type_other": "Other", "fuel_type_diesel": "Diesel", "fuel_type_petrol": "Petrol", "fuel_type_electric": "Electric", @@ -445,14 +391,6 @@ "recurrence_interval_months": "months", "recurrence_interval_years": "years", "recurrence_until": "Until", - "insurance_recurrence_type_fixed": "Fixed end date", - "insurance_recurrence_type_yearly": "Renews yearly", - "insurance_recurrence_type_monthly": "Renews monthly", - "insurance_recurrence_type_no_end": "No end date", - "pollution_recurrence_type_fixed": "Fixed end date", - "pollution_recurrence_type_yearly": "Renews yearly", - "pollution_recurrence_type_monthly": "Renews monthly", - "pollution_recurrence_type_no_end": "No end date", "file_drop_existing_note": "Existing attachment (Click to view)", "fuel_import_step_1_title": "Step 1 : Upload CSV file", "fuel_import_step_1_desc": "Select a delimited text file containing your fuel log data to begin the import process.", @@ -526,13 +464,9 @@ "reminder_type_registration": "Registration / Tax", "reminder_type_inspection": "Inspection", "reminder_type_custom": "Custom", - "alert_type_insurance": "Insurance", - "alert_type_pucc": "Pollution Certificate", "alert_status_expired_ago": "{label} expired {days} days ago", "alert_status_expires_in": "{label} expires in {days} days", "alert_status_valid_for": "{label} valid for {days} days", - "alert_insurance_active_no_end": "Insurance is active with no end date", - "alert_pucc_active_no_end": "PUCC is active with no end date", "alert_record_not_found": "{label} record not found. Add details to stay compliant.", "settings_error_format_not_valid": "Format not valid", "common_kilogram_unit": "Kilogram (kg)", @@ -551,5 +485,329 @@ "theme_yellow": "Yellow", "theme_teal": "Teal", "theme_indigo": "Indigo", - "theme_pink": "Pink" + "theme_pink": "Pink", + "dark_variant_default": "Default", + "dark_variant_dim": "Dim", + "dark_variant_oled": "OLED (True Black)", + "settings_tab_notifications": "Notifications", + "settings_personalization_desc": "Customize your experience with themes, languages, and formats.", + "settings_section_general": "General", + "settings_section_general_desc": "Customize appearance, localization, and display formats", + "settings_units_desc": "Configure measurement units for distance, volume, and fuel types.", + "settings_section_units": "Units", + "settings_section_units_desc": "Choose preferred units for distance, mileage, and fuel types", + "settings_section_feature_flags": "Feature Flags", + "settings_section_feature_flags_desc": "Enable or disable major app modules", + "settings_notifications_desc": "Configure provider subscriptions and the daily processing time for scheduled delivery.", + "settings_localization_desc": "Set your language and regional preferences.", + "settings_advanced_desc": "Fine-tune the interface with custom styles.", + "settings_nav_desc_personalization": "Theme, display and style", + "settings_nav_desc_localization": "Language and regional formats", + "settings_nav_desc_advanced": "Custom CSS and advanced options", + "settings_nav_desc_notifications": "Alerts and reminders", + "settings_nav_desc_units": "Measurement units", + "settings_nav_desc_features": "Feature preferences", + "settings_reset_defaults": "Reset to Defaults", + "settings_cancel_button": "Cancel", + "settings_secure_note": "Your preferences are stored securely and applied across all your devices.", + "settings_error_fix_errors": "Please fix the following errors:", + "settings_fuel_types_label": "Fuel types", + "settings_fuel_types_desc": "Choose the measurement for each fuel.", + "fuel_type_label_petrol_diesel": "Petrol/Diesel", + "fuel_type_label_lpg": "LPG", + "fuel_type_label_cng": "CNG", + "notif_scheduled_delivery": "Scheduled delivery", + "notif_scheduled_delivery_desc": "In-app notifications stay real-time. This schedule only controls provider delivery.", + "notif_processing_schedule": "Processing schedule", + "notif_processing_schedule_desc": "Run provider notification delivery on a schedule.", + "notif_send_now": "Send Now", + "notif_providers": "Providers", + "notif_providers_desc": "Create, edit, test, and enable notification providers.", + "notif_providers_channels_info": "Each provider can subscribe to Reminder, Alert, and Information channels.", + "notif_add_provider": "Add Provider", + "notif_empty_title": "No notification providers configured yet", + "notif_empty_desc": "Add a provider to receive scheduled Reminder, Alert, or Information notifications.", + "notif_load_failed": "Failed to load notification providers", + "notif_select_provider_type": "Please select a provider type", + "notif_select_channel": "Select at least one notification channel", + "notif_provider_updated": "Provider updated successfully", + "notif_provider_created": "Provider created successfully", + "notif_provider_deleted": "Provider deleted successfully", + "notif_provider_delete_failed": "Failed to delete provider", + "notif_channel_reminder": "Reminder", + "notif_channel_reminder_desc": "Due dates and reminder-style notifications", + "notif_channel_alert": "Alert", + "notif_channel_alert_desc": "Urgent or expiring items that need attention", + "notif_channel_information": "Information", + "notif_channel_information_desc": "General informational updates", + "notif_provider_enabled": "Enabled", + "notif_provider_test": "Test provider", + "notif_provider_edit": "Edit provider", + "notif_provider_delete": "Delete provider", + "notif_dialog_add_title": "Add Notification Provider", + "notif_dialog_edit_title": "Edit Notification Provider", + "notif_dialog_desc": "Choose a provider type, configure its destination, and subscribe it to compact channel toggles. Providers are enabled by default and can be disabled from the provider cards.", + "notif_provider_name": "Provider Name", + "notif_provider_name_placeholder": "Daily digest email", + "notif_provider_type": "Provider Type", + "notif_provider_type_email": "Email (SMTP)", + "notif_provider_type_webhook": "Webhook", + "notif_provider_type_gotify": "Gotify", + "notif_provider_type_select": "Select Provider Type", + "notif_dialog_cancel": "Cancel", + "notif_dialog_create": "Create Provider", + "notif_dialog_update": "Update Provider", + "notif_channel_subscriptions": "Channel subscriptions", + "notif_channel_subscriptions_desc": "Pick which notification channels this provider should receive.", + "notif_test_title": "Test Provider", + "notif_test_success": "Test notification sent successfully", + "notif_test_email_label": "Test Email", + "notif_test_email_placeholder": "recipient@example.com", + "notif_test_email_desc": "Optional override recipient for this test message.", + "notif_test_message_label": "Test Message", + "notif_test_message_placeholder": "This is a test notification from Tracktor", + "notif_test_send": "Send Test", + "notif_cron_presets": "Presets", + "notif_cron_every_minute": "Every minute", + "notif_cron_every_5_min": "Every 5 minutes", + "notif_cron_every_15_min": "Every 15 minutes", + "notif_cron_every_30_min": "Every 30 minutes", + "notif_cron_every_hour": "Every hour", + "notif_cron_every_2_hours": "Every 2 hours", + "notif_cron_every_6_hours": "Every 6 hours", + "notif_cron_every_12_hours": "Every 12 hours", + "notif_cron_daily_midnight": "Daily at midnight", + "notif_cron_daily_2am": "Daily at 2:00 AM", + "notif_cron_daily_8am": "Daily at 8:00 AM", + "notif_cron_daily_noon": "Daily at noon", + "notif_cron_weekly_monday": "Weekly (Monday)", + "notif_cron_monthly_1st": "Monthly (1st)", + "notif_cron_expression_required": "Expression required", + "notif_cron_must_have_5_parts": "Must have 5 parts", + "notif_cron_invalid_chars": "Invalid characters", + "notif_cron_valid": "Valid expression", + "notif_cron_invalid": "Invalid expression", + "notif_cron_custom": "Custom schedule", + "notif_email_smtp_settings": "SMTP Server Settings", + "notif_email_host": "Host", + "notif_email_port": "Port", + "notif_email_use_ssl": "Use SSL/TLS", + "notif_email_use_ssl_desc": "Enable secure connection", + "notif_email_auth": "Authentication", + "notif_email_username": "Username / Email", + "notif_email_password": "Password", + "notif_email_password_keep": "Password (leave blank to keep current)", + "notif_email_sender_info": "Sender/Recipient Information", + "notif_email_from": "From Email", + "notif_email_from_name": "From Name (Optional)", + "notif_email_from_name_placeholder": "Tracktor Notifications", + "notif_email_recipient": "Recipient Email", + "notif_email_recipient_desc": "Email address of the recipient", + "notif_webhook_config": "Webhook Configuration", + "notif_webhook_url": "Webhook URL", + "notif_webhook_method": "HTTP Method", + "notif_webhook_headers": "Custom Headers (JSON)", + "notif_webhook_headers_desc": "Additional headers to include in the webhook request", + "notif_webhook_auth_type": "Auth Type", + "notif_webhook_auth_none": "None", + "notif_webhook_auth_basic": "Basic Auth", + "notif_webhook_auth_bearer": "Bearer Token", + "notif_webhook_auth_apikey": "API Key", + "notif_webhook_username": "Username", + "notif_webhook_apikey_header": "API Key Header Name", + "notif_gotify_config": "Gotify Server Configuration", + "notif_gotify_url": "Server URL", + "notif_gotify_url_desc": "URL of your Gotify server instance", + "notif_gotify_token": "App Token", + "notif_gotify_token_keep": "App Token (leave blank to keep current)", + "notif_gotify_token_desc": "Application token from your Gotify app (not the client token)", + "notif_gotify_priority": "Priority (0-10)", + "notif_gotify_priority_desc": "Message priority level. Higher priority = more prominent notification", + "notif_cleared_success_one": "Cleared 1 read notification", + "notif_cleared_success_other": "Cleared {count} read notifications", + "notif_cleared_partial": "Cleared {success}, {failed} failed", + "notif_clear_failed": "Failed to clear read notifications", + "notif_all_marked_read": "All notifications marked as read", + "notif_mark_read_failed": "Failed to mark notifications as read", + "notif_button_mark_all_read": "Mark All Read", + "notif_button_clear_read": "Clear Read", + "notif_button_clear_all_read_title": "Clear all read notifications", + "notif_status_read": "Read", + "notif_status_unread": "Unread", + "notif_due_prefix": "Due: {date}", + "notif_save_provider_failed": "Failed to save provider", + "notif_update_provider_failed": "Failed to update provider", + "notif_send_all_failed": "Failed to send notifications", + "notif_send_all_success": "Sent {notifCount} notifications to {successCount}/{providerCount} enabled providers", + "notif_confirm_delete": "Are you sure you want to delete \"{name}\"?", + "notif_webhook_bearer_keep": "Bearer Token (leave blank to keep current)", + "notif_webhook_apikey_keep": "API Key (leave blank to keep current)", + "notif_test_failed": "Failed to send test notification", + "notif_test_send_desc": "Send a test notification using {name}", + "notif_cron_every_n_minutes": "Every {n} minutes", + "notif_cron_hourly_at_minute": "Every hour at minute {n}", + "notif_cron_daily_at": "Daily at {time}", + "vehicle_hub_back_to_vehicles": "Back to Vehicles", + "vehicle_hub_plate_copied": "License plate copied", + "vehicle_hub_insurance_valid_till": "Insurance Valid Till", + "vehicle_hub_vehicle_type": "Vehicle Type", + "vehicle_hub_activity_title": "Recent Activity", + "vehicle_hub_activity_empty": "No recent activity", + "vehicle_hub_activity_fuel_added": "Fuel Added", + "vehicle_hub_activity_maintenance": "Maintenance", + "vehicle_hub_records_count": "{count} records", + "vehicle_hub_valid_till": "Valid till {date}", + "vehicle_hub_upcoming_count": "{count} upcoming", + "vehicle_hub_view_details": "View Details", + "vehicle_hub_manage_title": "Manage Vehicle", + "vehicle_hub_stat_odometer": "Odometer", + "vehicle_hub_stat_mileage": "Overall Mileage", + "vehicle_hub_stat_fuel_logs": "Fuel Logs", + "vehicle_hub_stat_maintenance_logs": "Maintenance Logs", + "col_vehicle": "Vehicle", + "overview_chart_pick_vehicle": "Select a vehicle to see this chart", + "fuel_page_title": "Fuel Tracking", + "fuel_page_description": "Monitor fuel consumption and costs", + "fuel_stat_used": "Fuel Used", + "fuel_stat_spent": "Total Spent", + "fuel_stat_avg_mileage": "Avg Mileage", + "fuel_stat_entries": "Total Entries", + "maintenance_page_title": "Maintenance", + "maintenance_page_description": "Track service history and upcoming maintenance", + "maintenance_stat_last_service": "Last Service", + "maintenance_stat_next_service": "Next Service", + "maintenance_stat_odometer": "Odometer", + "maintenance_stat_total_services": "Total Services", + "maintenance_stat_total_spent": "Total Spent", + "maintenance_stat_due_soon": "Due Soon", + "maintenance_tab_overview": "Overview", + "maintenance_tab_history": "Service History", + "maintenance_timeline_title": "Maintenance Timeline", + "maintenance_upcoming_empty": "Nothing scheduled", + "maintenance_history_empty": "No service history yet", + "maintenance_next_service_fallback": "General maintenance service", + "maintenance_also_upcoming": "Also coming up", + "reminder_page_title": "Reminders", + "reminder_page_description": "Stay on top of upcoming service, insurance and PUC dates", + "reminder_filter_all": "All", + "reminder_filter_all_types": "All Types", + "reminder_filter_service": "Service", + "reminder_filter_puc": "PUC", + "reminder_filter_insurance": "Insurance", + "reminder_filter_others": "Others", + "reminder_section_upcoming": "Upcoming", + "reminder_section_completed": "Completed", + "reminder_section_marked_done": "Marked Done", + "reminder_empty_title": "No reminders set up yet", + "reminder_stat_overdue": "Overdue", + "reminder_stat_due_soon": "Due Soon", + "reminder_stat_upcoming": "Upcoming", + "reminder_stat_completed": "Completed", + "reminder_calendar_title": "Calendar View", + "reminder_quick_actions_title": "Quick Actions", + "reminder_manage_all_action": "Manage all reminders", + "reminder_list_title": "Reminders", + "reminder_list_title_for_date": "Reminders on {date}", + "reminder_clear_filter": "Clear", + "reminder_calendar_empty_day": "No reminders on this date.", + "reports_page_title": "Reports", + "reports_page_description": "Costs and exports for your fleet", + "reports_section_costs": "Costs", + "reports_section_details": "Detailed Report", + "reports_section_exports": "Exports", + "reports_stat_fuel_costs": "Fuel Costs", + "reports_stat_maintenance_costs": "Maintenance Costs", + "reports_chart_breakdown_title": "Expense Breakdown", + "reports_chart_trend_title": "Monthly Expense Trend", + "reports_chart_trend_unavailable": "Monthly trend is only available for all vehicles combined", + "reports_export_maintenance_title": "Maintenance History", + "reports_export_maintenance_description": "Export maintenance history as PDF", + "reports_export_maintenance_hint": "Select a vehicle to export its maintenance history", + "reports_export_data_title": "Full Data Export", + "reports_export_data_description": "Export all fleet data as JSON", + "reports_type_fuel": "Fuel", + "reports_type_maintenance": "Maintenance", + "reports_type_compliance": "Compliance", + "nav_compliance": "Compliance", + "vehicle_action_add_compliance": "Add Compliance Document", + "feature_compliance_disabled_title": "Compliance Feature Disabled", + "feature_compliance_disabled_hint": "Enable this feature in Settings to manage insurance, emissions, roadworthiness and registration records", + "feature_label_compliance": "Compliance", + "feature_desc_compliance": "Manage insurance, emissions, roadworthiness and registration records", + "compliance_type_insurance": "Insurance", + "compliance_type_emissions": "Emissions / Pollution", + "compliance_type_roadworthiness": "Roadworthiness / Safety Inspection", + "compliance_type_registration": "Registration / Road Tax", + "compliance_type_other": "Other", + "compliance_field_policy_number": "Policy Number", + "compliance_field_certificate_number": "Certificate Number", + "compliance_field_registration_number": "Registration Number", + "compliance_field_document_number": "Document Number", + "compliance_field_provider": "Insurance Provider", + "compliance_field_testing_center": "Testing Center", + "compliance_field_inspection_center": "Inspection Center", + "compliance_field_issuing_authority": "Issuing Authority", + "compliance_recurrence_type_fixed": "Fixed end date", + "compliance_recurrence_type_yearly": "Renews yearly", + "compliance_recurrence_type_monthly": "Renews monthly", + "compliance_recurrence_type_no_end": "No end date", + "compliance_form_type_label": "Compliance Type", + "compliance_form_type_desc": "What kind of compliance document is this", + "compliance_form_other_label_label": "Type Name", + "compliance_form_other_label_desc": "Name this compliance type, e.g. \"WOF (New Zealand)\" or \"TÜV (Germany)\"", + "compliance_form_attachment_label": "Document", + "compliance_form_attachment_desc": "Upload the document", + "compliance_form_issuer_desc": "The provider, testing center, or authority that issued this document", + "compliance_form_document_number_desc": "The number printed on the document", + "compliance_form_start_date_label": "Start Date", + "compliance_form_start_date_desc": "Date when this document takes effect", + "compliance_form_recurrence_type_label": "How should this renew?", + "compliance_form_recurrence_type_desc": "Renewal type for this document", + "compliance_form_recurrence_interval_desc": "How often the document renews", + "compliance_form_end_date_label": "End Date", + "compliance_form_end_date_desc": "Date when this document expires", + "compliance_form_cost_label": "Cost", + "compliance_form_cost_desc": "Cost for this document, if any", + "compliance_form_notes_label": "Additional Notes", + "compliance_form_notes_desc": "Any extra information", + "compliance_form_notes_placeholder": "Add more details...", + "compliance_form_error_fix": "Please fix the errors in the form before submitting.", + "compliance_toast_saved": "Compliance document saved successfully", + "compliance_toast_updated": "Compliance document updated successfully", + "compliance_toast_error_prefix": "Error while saving: ", + "compliance_list_empty": "No compliance documents found for this vehicle.", + "compliance_col_cost": "Cost", + "compliance_col_start_date": "Start Date", + "compliance_col_end_date": "End Date", + "compliance_col_next_due": "Next Due", + "compliance_col_recurrence": "Recurrence", + "compliance_col_notes": "Notes", + "compliance_col_view_document": "View Document", + "compliance_menu_open": "Open menu", + "compliance_menu_edit": "Edit", + "compliance_menu_delete": "Delete", + "compliance_menu_sheet_title": "Update Compliance Document", + "compliance_delete_success": "Deleted compliance document.", + "compliance_delete_error": "Some error occurred while deleting the compliance document.", + "compliance_page_title": "Compliance", + "compliance_page_description": "Track insurance, emissions, roadworthiness and registration compliance", + "compliance_add_action": "Add Compliance Document", + "compliance_filter_all_types": "All Types", + "compliance_filter_all": "All", + "compliance_filter_valid": "Valid", + "compliance_filter_expiring_soon": "Expiring Soon", + "compliance_filter_expired": "Expired", + "compliance_stat_total": "Total", + "compliance_stat_valid": "Valid", + "compliance_stat_expiring_soon": "Expiring Soon", + "compliance_stat_expired": "Expired", + "compliance_cta_heading": "Keep your vehicles compliant", + "compliance_cta_description": "Keep your compliance documents updated to avoid penalties and stay road-legal.", + "vehicle_hub_other_compliance_valid_till": "Other Compliance Valid Till", + "vehicle_hub_activity_compliance_updated": "Compliance Document Updated", + "vehicle_hub_activity_document_prefix": "Doc #", + "reports_stat_compliance_costs": "Compliance Costs", + "compliance_col_document": "Compliance", + "compliance_col_status": "Status", + "compliance_col_days_left": "Days Left" } diff --git a/messages/es.json b/i18n/messages/es.json similarity index 99% rename from messages/es.json rename to i18n/messages/es.json index 45197b72..29b5f60e 100644 --- a/messages/es.json +++ b/i18n/messages/es.json @@ -162,6 +162,7 @@ "fuel_add_title": "Agregar registro de combustible", "col_date": "Fecha", "col_odometer": "Odómetro", + "col_distance_driven": "Distancia recorrida", "col_filled": "Llenado", "col_missed_last": "Último omitido", "col_fuel_amount": "Cantidad de combustible", @@ -179,6 +180,7 @@ "form_odometer_desc": "Lectura actual del odómetro", "form_volume_fuel": "Volumen de combustible", "form_volume_energy": "Energía consumida", + "form_rate": "Tarifa", "form_cost": "Costo", "form_cost_desc": "Costo del llenado", "form_cost_desc_ev": "Costo de la carga", diff --git a/messages/fi.json b/i18n/messages/fi.json similarity index 55% rename from messages/fi.json rename to i18n/messages/fi.json index 5333a127..1b140bd8 100644 --- a/messages/fi.json +++ b/i18n/messages/fi.json @@ -18,7 +18,7 @@ "auth_signup_button": "Rekisteröidy", "auth_login_loading": "Kirjaudutaan sisään...", "auth_signup_loading": "Luodaan tiliä...", - "auth_password_mismatch": "Salasanat eivät täsmää", + "auth_password_mismatch": "Salasanat eivät täsmää!", "settings_tab_personalization": "Mukauta", "settings_tab_interface": "Käyttöliittymä", "settings_tab_features": "Ominaisuudet", @@ -35,17 +35,18 @@ "settings_update_button": "Päivitä asetukset", "settings_select_unit_system": "Valitse järjestelmän yksikkö", "settings_select_theme": "Valitse teema", - "settings_desc_date_format": "Valitse haluttu päivämäärän muoto", - "settings_desc_locale": "Valitse haluttu käyttöliittymän kieli", - "settings_desc_timezone": "Valitse haluttu aikavyöhyke", - "settings_desc_currency": "Valitse haluttu valuutta", + "settings_desc_date_format": "Valitse päivämäärän muoto", + "settings_desc_locale": "Valitse käyttöliittymän kieli", + "settings_desc_timezone": "Valitse aikavyöhyke", + "settings_desc_currency": "Valitse valuutta", "settings_desc_unit_distance": "Etäisyyden mittayksikkö", "settings_desc_unit_volume": "Tilavuuden yksikkö", "settings_label_mileage_format": "Matkamittarin näyttömuoto", "settings_desc_mileage_format": "Valitse kulutuksen esitystapa", "settings_mileage_format_distance_per_fuel": "Kulutus (l/100km)", "settings_mileage_format_fuel_per_distance": "Ajomatka (km/l)", - "settings_desc_theme": "Valitse haluttu teema", + "settings_mileage_format_uk_mpg": "UK MPG (mailia per imperiaalinen gallona)", + "settings_desc_theme": "Valitse teema", "settings_desc_custom_css": "CSS-tyylit käyttöliittymän mukauttamiseen", "settings_select_language": "Valitse kieli", "settings_updated_success": "Asetukset päivitetty", @@ -98,17 +99,17 @@ "vehicle_form_license_label": "Rekisterinumero", "vehicle_form_license_desc": "Ajoneuvon rekisterinumero", "vehicle_form_vin_label": "VIN", - "vehicle_form_vin_desc": "Ajoneuvon tunnistenumero", + "vehicle_form_vin_desc": "Ajoneuvon valmistenumero", "vehicle_toast_saved": "Ajoneuvo tallennettu", "vehicle_toast_updated": "Ajoneuvo päivitetty", "vehicle_toast_error_prefix": "Tallennusvirhe: ", - "vehicle_list_empty": "Melko hiljaista. Lisää ajoneuvo aloittaaksesi.", + "vehicle_list_empty": "Täällä on melko tyhjää. Lisää ajoneuvo aloittaaksesi.", "vehicle_delete_success": "Ajoneuvo poistettu", "vehicle_delete_error": "Ajoneuvon poistossa tapahtui virhe", - "vehicle_action_add_fuel_log": "Lisää polttoaineloki", - "vehicle_action_add_maintenance_log": "Lisää huoltoloki", + "vehicle_action_add_fuel_log": "Lisää tankkaus", + "vehicle_action_add_maintenance_log": "Lisää huolto", "vehicle_action_add_insurance": "Lisää vakuutus", - "vehicle_action_add_pollution": "Lisää päästösertifikaatti", + "vehicle_action_add_pollution": "Lisää päästötodistus", "vehicle_action_add_reminder": "Lisää muistutus", "vehicle_action_more_info": "Lisätietoa", "vehicle_action_update_vehicle": "Päivitä ajoneuvo", @@ -120,7 +121,7 @@ "tools_export_password_hint": "Pidä salasana tallessa - tarvitset sitä tietojen tuontia varten", "tools_export_status_exporting": "Viedään...", "tools_export_button": "Vie tietokanta", - "tools_export_info_title": "Vie tietoja", + "tools_export_info_title": "Vietävät tiedot", "tools_export_info_bullet_1": "Vie kaikki tietokannat ja tiedot", "tools_export_info_bullet_2": "Sisältää ajoneuvot, polttoainelokit, huoltotiedot jne.", "tools_export_info_bullet_3": "Valinnainen salaus henkilökohtaisia tietoja varten", @@ -130,13 +131,13 @@ "tools_import_upload_label": "Lataa JSON-tiedosto", "tools_import_paste_label": "Tai syötä JSON-data", "tools_import_paste_placeholder": "Syötä viety JSON-data tähän", - "tools_import_password_label": "Salauksen salasana (jos käytetty)", + "tools_import_password_label": "Salauksen salasana (jos salattu)", "tools_import_password_placeholder": "Syötä salasana, jos data on salattu", "tools_import_status_importing": "Tuodaan...", "tools_import_button": "Tuo tietokanta", "tools_import_warning_title": "⚠️ Tuontivaroitus", - "tools_import_warning_bullet_1": "Tämä korvaa KAIKEN datan", - "tools_import_warning_bullet_2": "Varmista, että olet varmuuskopioinut datan ensin", + "tools_import_warning_bullet_1": "Tämä korvaa KAIKKI tiedot", + "tools_import_warning_bullet_2": "Varmista, että olet varmuuskopioinut tiedot ensin", "tools_import_warning_bullet_3": "Tuontia ei voi perua", "tools_import_warning_bullet_4": "Varmista, että JSON-formaatti on oikein", "tools_import_success": "Datan tuonti onnistui", @@ -163,11 +164,12 @@ "fuel_add_title": "Lisää tankkaus", "col_date": "Päivämäärä", "col_odometer": "Matkamittari", + "col_distance_driven": "Ajettu matka", "col_filled": "Täysi tankki", "col_missed_last": "Edellinen ohitettu", "col_fuel_amount": "Polttoainemäärä", "col_cost": "Kustannus", - "col_mileage": "Matkamittarilukema", + "col_mileage": "Kulutus", "col_notes": "Muistiinpanot", "col_attachment": "Liitteet", "col_no_end_date": "Ei päättymispäivämäärää", @@ -180,6 +182,7 @@ "form_odometer_desc": "Nykyisen ajoneuvon matkamittarilukema", "form_volume_fuel": "Polttoainemäärä", "form_volume_energy": "Ladattu sähkömäärä", + "form_rate": "Yksikköhinta", "form_cost": "Kustannus", "form_cost_desc": "Tankkauksen kustannus", "form_cost_desc_ev": "Latauksen kustannus", @@ -244,8 +247,8 @@ "maintenance_form_date_desc": "Huollon päivämäärä", "maintenance_form_odometer_label": "Matkamittari", "maintenance_form_odometer_desc": "Nykyisen ajoneuvon matkamittarilukema", - "maintenance_form_service_center_label": "Huoltoasema", - "maintenance_form_service_center_desc": "Huoltoaseman nimi", + "maintenance_form_service_center_label": "Autokorjaamo", + "maintenance_form_service_center_desc": "Autokorjaamon nimi", "maintenance_form_cost_label": "Kustannus", "maintenance_form_cost_desc": "Huollon kustannus", "maintenance_form_notes_label": "Muistiinpanot", @@ -256,13 +259,14 @@ "maintenance_toast_error_prefix": "Tallennusvirhe: ", "maintenance_form_error_fix": "Korjaa virheet lomakkeella ennen tallennusta", "maintenance_list_empty": "Tälle ajoneuvolle ei löytynyt huoltotietoja", - "maintenance_col_service_center": "Huoltoasema", + "maintenance_col_service_center": "Autokorjaamo", "maintenance_menu_open": "Avaa valikko", "maintenance_menu_edit": "Muokkaa", "maintenance_menu_delete": "Poista", "maintenance_menu_sheet_title": "Päivitä huoltotieto", "maintenance_tab_title": "Huoltohistoria", "maintenance_add_action": "Lisää huolto", + "maintenance_export_pdf": "Vie PDF:nä", "maintenance_delete_success": "Huoltotieto poistettu", "maintenance_delete_error": "Huoltotiedon poistossa tapahtui virhe", "insurance_form_provider_label": "Vakuutusyhtiö", @@ -271,69 +275,69 @@ "insurance_form_policy_number_desc": "Vakuutusnumero vakuutusasiakirjasta", "insurance_form_start_date_label": "Vakuutuksen alkamispäivä", "insurance_form_start_date_desc": "Päivämäärä, jolloin vakuutus on voimassa", - "insurance_form_recurrence_type_label": "Miten vakuutus uusiutuu?", + "insurance_form_recurrence_type_label": "Miten vakuutus uusitaan?", "insurance_form_recurrence_type_desc": "Vakuutuksen uusimistapa", "insurance_form_recurrence_interval_label": "Uusimisväli", "insurance_form_recurrence_interval_desc": "Kuinka tiheään vakuutus uusitaan", "insurance_form_end_date_label": "Vakuutuksen päättymispäivä", "insurance_form_end_date_desc": "Päivämäärä, jolloin vakuutus raukeaa", - "insurance_form_cost_label": "Vakuutuksen kustannus", + "insurance_form_cost_label": "Kustannus", "insurance_form_cost_desc": "Vuosittainen tai vakuutuskauden kustannus", "insurance_form_notes_label": "Muistiinpanot", "insurance_form_notes_desc": "Lisätietoja vakuutuksesta", "insurance_form_notes_placeholder": "Lisää lisätietoja vakuutuksesta...", "insurance_form_attachment_label": "Vakuutuskirja", "insurance_form_attachment_desc": "Lisää vakuutuskirja", - "insurance_form_error_fix": "Korjaa virheet lomakkeella ennen tallennusta", - "insurance_toast_saved": "Vakuutus tallennettu", - "insurance_toast_updated": "Vakuutus päivitetty", - "insurance_toast_error_prefix": "Tallennusvirhe: ", - "insurance_list_empty": "Tälle ajoneuvolle ei löytynyt vakuutusta", + "insurance_form_error_fix": "Korjaa muotoiluvirheet ennen lähetystä.", + "insurance_toast_saved": "Vakuutustiedot tallennettu onnistuneesti", + "insurance_toast_updated": "Vakuutustiedot päivitetty onnistuneesti", + "insurance_toast_error_prefix": "Tallennuksessa tapahtui virhe: ", + "insurance_list_empty": "Ajoneuvolle ei löytynyt vakuutuksia", "insurance_col_policy_number": "Vakuutusnumero", - "insurance_col_cost": "Kustannus", - "insurance_col_start_date": "Alkamispäivä", - "insurance_col_end_date": "Päättymispäivä", - "insurance_col_next_due": "Seuraava määräaika", + "insurance_col_cost": "Hinta", + "insurance_col_start_date": "Alkupäivä", + "insurance_col_end_date": "Loppupäivä", + "insurance_col_next_due": "Seuraava eräpäivä", "insurance_col_recurrence": "Toistuvuus", "insurance_col_notes": "Muistiinpanot", "insurance_menu_open": "Avaa valikko", "insurance_menu_edit": "Muokkaa", "insurance_menu_delete": "Poista", - "insurance_menu_sheet_title": "Päivitä vakuutus", + "insurance_menu_sheet_title": "Päivitä vakuutustiedot", "insurance_tab_title": "Vakuutustiedot", "insurance_add_action": "Lisää vakuutus", - "insurance_delete_success": "Vakuutus poistettu", + "insurance_delete_success": "Vakuutus poistettu.", "insurance_delete_error": "Vakuutuksen poistossa tapahtui virhe", "insurance_col_view_document": "Näytä dokumentti", - "pollution_form_certificate_number_label": "Todistuksen numero", - "pollution_form_certificate_number_desc": "Päästötodistuksen numero", + "pollution_form_certificate_number_label": "Päästömittaustodistuksen numero", + "pollution_form_certificate_number_desc": "Päästömittaustodistuksen numero", "pollution_form_issue_date_label": "Myöntämispäivä", - "pollution_form_issue_date_desc": "Todistuksen myöntämispäivä", - "pollution_form_recurrence_type_label": "Miten todistus uusiutuu?", - "pollution_form_recurrence_type_desc": "Todistuksen uusimistapa", + "pollution_form_issue_date_desc": "Todistuksen myöntämispäivämäärä", + "pollution_form_recurrence_type_label": "Miten tämä todistus uusitaan?", + "pollution_form_recurrence_type_desc": "Tämän todistuksen uusimistyyppi", "pollution_form_recurrence_interval_label": "Uusimisväli", - "pollution_form_recurrence_interval_desc": "Kuinka usein todistus uusiutuu", - "pollution_form_expiry_date_label": "Päättymispäivä", - "pollution_form_expiry_date_desc": "Todistuksen päättymispäivä", + "pollution_form_recurrence_interval_desc": "Kuinka usein todistus uusitaan", + "pollution_form_expiry_date_label": "Voimassaolopäivä", + "pollution_form_expiry_date_desc": "Päästötodistuksen viimeinen voimassaolopäivä", "pollution_form_testing_center_label": "Katsastusasema", "pollution_form_testing_center_desc": "Katsastusaseman nimi", - "pollution_form_notes_label": "Muistiinpanot", - "pollution_form_notes_desc": "Lisätietoja", - "pollution_form_notes_placeholder": "Lisää lisätietoja, jos tarpeen...", - "pollution_form_attachment_label": "Todistusasiakirja", - "pollution_form_attachment_desc": "Lisää todistusasiakirja", - "pollution_form_error_fix": "Korjaa virheet lomakkeella ennen tallennusta", - "pollution_toast_saved": "Päästötodistus tallennettu", - "pollution_toast_updated": "Päästötodistus päivitetty", - "pollution_toast_error_prefix": "Tallennusvirhe: ", - "pollution_list_empty": "Tälle ajoneuvolle ei löytynyt päästötodistuksia", + "pollution_form_notes_label": "Lisämerkinnät", + "pollution_form_notes_desc": "Muita lisätietoja", + "pollution_form_notes_placeholder": "Lisää muistiinpanoja...", + "pollution_form_attachment_label": "Päästömittaustodistus", + "pollution_form_attachment_desc": "Lataa päästömittaustodistus", + "pollution_form_error_fix": "Korjaa lomakkeen virheet ennen lähettämistä.", + "pollution_toast_saved": "Päästötodistus tallennettu onnistuneesti", + "pollution_toast_updated": "Päästötodistus päivitetty onnistuneesti", + "pollution_toast_error_prefix": "Virhe tallennettaessa: ", + "pollution_list_empty": "Tälle ajoneuvolle ei ole päästötodistuksia.", "pollution_col_certificate_number": "Todistuksen numero", "pollution_col_issue_date": "Myöntämispäivä", - "pollution_col_expiry_date": "Päättymispäivä", - "pollution_col_next_due": "Seuraava määräaika", - "pollution_col_testing_center": "Katsastusasema", + "pollution_col_expiry_date": "Viimeinen voimassaolopäivä", + "pollution_col_next_due": "Seuraava eräpäivä", + "pollution_col_testing_center": "Testausasema", "pollution_col_notes": "Muistiinpanot", - "pollution_col_view_certificate": "Katso todistus", + "pollution_col_view_certificate": "Näytä todistus", "pollution_col_recurrence": "Toistuvuus", "pollution_menu_open": "Avaa valikko", "pollution_menu_edit": "Muokkaa", @@ -341,69 +345,69 @@ "pollution_menu_sheet_title": "Päivitä päästötodistus", "pollution_tab_title": "Päästötodistuksen tiedot", "pollution_add_action": "Lisää päästötodistus", - "pollution_delete_success": "Päästötodistus poistettu", - "pollution_delete_error": "Päästötodistuksen poistossa tapahtui virhe", + "pollution_delete_success": "Päästötodistus poistettu.", + "pollution_delete_error": "Päästötodistuksen poistamisessa tapahtui virhe.", "reminder_form_due_date_label": "Määräpäivä", - "reminder_form_due_date_desc": "Milloin tämä muistutus laukeaa?", + "reminder_form_due_date_desc": "Milloin tämän muistutuksen pitäisi aktivoitua?", "reminder_form_type_label": "Tyyppi", "reminder_form_type_desc": "Valitse muistutuksen tyyppi", "reminder_form_schedule_label": "Muistutusaikataulu", - "reminder_form_schedule_desc": "Milloin muistutamme sinua?", + "reminder_form_schedule_desc": "Milloin haluat muistutuksen?", "reminder_form_recurrence_type_label": "Toistuvuus", - "reminder_form_recurrence_type_desc": "Toistuuko tämä muistutus?", - "reminder_form_recurrence_interval_label": "Toista joka", + "reminder_form_recurrence_type_desc": "Pitäisikö tämän muistutuksen toistua?", + "reminder_form_recurrence_interval_label": "Toista välein", "reminder_form_recurrence_interval_desc": "Toistumisen tiheys", "reminder_form_recurrence_end_date_label": "Päättymispäivä", - "reminder_form_recurrence_end_date_desc": "Milloin toistuminen loppuu? (valinnainen)", + "reminder_form_recurrence_end_date_desc": "Milloin toistuvuus loppuu? (valinnainen)", "reminder_form_note_label": "Muistiinpano", "reminder_form_note_desc": "Lisää lisätietoja", - "reminder_form_note_placeholder": "Lisätietoja...", - "reminder_form_is_completed_label": "Merkitse valmiiksi", - "reminder_toast_created": "Muistutus luotu", - "reminder_toast_updated": "Muistutus päivitetty", - "reminder_toast_error_prefix": "Tallennusvirhe: ", - "reminder_list_empty": "Ei muistutuksia. Luo uusi pysyäksesi ajan tasalla", - "reminder_list_select_vehicle": "Valitse ajoneuvo nähdäksesi muistutukset", - "reminder_list_select_hint": "Valitse ajoneuvo yllä ladataksesi sen tulevat muistutukset", + "reminder_form_note_placeholder": "Tarkenna, mikä vaatii huomiota", + "reminder_form_is_completed_label": "Merkitse tehdyksi", + "reminder_toast_created": "Muistutus luotu onnistuneesti.", + "reminder_toast_updated": "Muistutus päivitetty onnistuneesti", + "reminder_toast_error_prefix": "Virhe tallennettaessa: ", + "reminder_list_empty": "Ei vielä muistutuksia. Luo muistutus pysyäksesi ajan tasalla esimerkiksi umpeutuvista todistuksista.", + "reminder_list_select_vehicle": "Valitse ajoneuvo nähdäksesi muistutukset.", + "reminder_list_select_hint": "Valitse ajoneuvo ylhäältä ladataksesi sen tulevat muistutukset.", "reminder_col_due_date": "Määräpäivä", "reminder_col_reminder_schedule": "Muistutusaikataulu", "reminder_col_recurrence": "Toistuvuus", "reminder_col_note": "Muistiinpanot", - "reminder_menu_toggle_done": "Merkitse {status}", - "reminder_menu_toggle_done_done": "odottavaksi", - "reminder_menu_toggle_done_pending": "valmiiksi", + "reminder_menu_toggle_done": "Merkitse tilaksi {status}", + "reminder_menu_toggle_done_done": "Merkitse odottavaksi", + "reminder_menu_toggle_done_pending": "Merkitse tehdyksi", "reminder_menu_edit": "Muokkaa", "reminder_menu_delete": "Poista", "reminder_menu_open": "Avaa valikko", "reminder_menu_sheet_title": "Päivitä muistutus", "reminder_tab_title": "Muistutukset", "reminder_add_action": "Lisää muistutus", - "reminder_delete_success": "Muistutus poistettu", - "reminder_delete_error": "Muistutuksen poistossa tapahtui virhe", - "reminder_status_completed": "Valmis", + "reminder_delete_success": "Muistutus poistettu.", + "reminder_delete_error": "Muistutuksen poistamisessa tapahtui virhe.", + "reminder_status_completed": "Tehty", "reminder_status_pending": "Odottaa", - "reminder_status_overdue": "Myöhässä", - "reminder_status_error": "Muistutuksen tilan päivitys epäonnistui", - "reminder_toast_error_fallback": "Muistutuksen tallennus epäonnistui", + "reminder_status_overdue": "Rastissa", + "reminder_status_error": "Muistutuksen tilaa ei voitu päivittää.", + "reminder_toast_error_fallback": "Muistutuksen tallentaminen epäonnistui.", "settings_sheet_title": "Asetukset", "feature_label_pollution": "Päästöt", - "feature_desc_pollution": "Hallitse päästösertifikaattitietueita", + "feature_desc_pollution": "Hallitse päästötodistusten tietoja", "profile_menu_item": "Profiili", "profile_sheet_title": "Profiili", "profile_sheet_desc": "Päivitä käyttäjätunnuksesi ja salasanasi", "profile_username": "Käyttäjätunnus", "profile_username_desc": "Näyttönimesi", - "profile_password_hint": "Jätä salasanakentät tyhjiksi säilyttääksesi nykyisen salasanasi", + "profile_password_hint": "Jätä salasanakentät tyhjiksi, jos haluat säilyttää nykyisen salasanasi", "profile_current_password": "Nykyinen salasana", - "profile_current_password_desc": "Vaaditaan salasanan vaihtamiseen", + "profile_current_password_desc": "Vaaditaan salasanan vaihtamiseksi", "profile_new_password": "Uusi salasana", "profile_new_password_desc": "Vähintään 6 merkkiä", "profile_confirm_password": "Vahvista salasana", - "profile_confirm_password_desc": "Syötä uusi salasanasi uudelleen", + "profile_confirm_password_desc": "Kirjoita uusi salasana uudelleen", "profile_update_button": "Päivitä profiili", "tools_menu": "Työkalut", - "data_export_import_menu_item": "Vie/Tuo data", - "data_export_import_sheet_title": "Datan vienti/tuonti", + "data_export_import_menu_item": "Vie/tuo tietoja", + "data_export_import_sheet_title": "Tietojen vienti/tuonti", "data_export_import_sheet_desc": "Vie tai tuo tietokantasi valinnaisella salauksella", "logout_menu_item": "Kirjaudu ulos", "custom_fields_label": "Mukautetut kentät", @@ -411,22 +415,22 @@ "custom_fields_name_placeholder": "Kentän nimi", "custom_fields_value_placeholder": "Kentän arvo", "custom_fields_remove_aria": "Poista kenttä", - "custom_fields_empty_message": "Mukautettuja kenttiä ei ole lisätty. Pääset alkuun klikkaamalla \"Lisää kenttä\"", - "vehicle_details_vin": "VIN", - "vehicle_details_not_specified": "Ei määritetty", + "custom_fields_empty_message": "Ei mukautettuja kenttiä. Aloita napauttamalla \"Lisää kenttä\".", + "vehicle_details_vin": "Valmistenumero (VIN)", + "vehicle_details_not_specified": "Ei määritelty", "vehicle_details_section_title": "Tiedot", - "vehicle_details_license_plate": "Rekisterinumero", - "vehicle_details_fuel_type": "Käyttövoima", + "vehicle_details_license_plate": "Rekisteritunnus", + "vehicle_details_fuel_type": "Polttoainetyyppi", "vehicle_details_odometer": "Matkamittari", - "vehicle_details_not_recorded": "Ei kirjattu", + "vehicle_details_not_recorded": "Ei tallennettu", "vehicle_details_color": "Väri", - "vehicle_details_year": "Vuosi", + "vehicle_details_year": "Vuosimalli", "fuel_type_diesel": "Diesel", "fuel_type_petrol": "Bensiini", "fuel_type_electric": "Sähkö", - "fuel_type_lpg": "LPG", - "fuel_type_cng": "CNG", - "fuel_type_ev": "Sähkö (EV)", + "fuel_type_lpg": "Nestekaasu (LPG)", + "fuel_type_cng": "Maakaasu (CNG)", + "fuel_type_ev": "Sähköauto (EV)", "reminder_schedule_same_day": "Määräpäivänä", "reminder_schedule_one_day_before": "1 päivä ennen", "reminder_schedule_three_days_before": "3 päivää ennen", @@ -447,27 +451,27 @@ "insurance_recurrence_type_fixed": "Kiinteä päättymispäivä", "insurance_recurrence_type_yearly": "Uusiutuu vuosittain", "insurance_recurrence_type_monthly": "Uusiutuu kuukausittain", - "insurance_recurrence_type_no_end": "Ei päättymispäivämäärää", + "insurance_recurrence_type_no_end": "Ei päättymispäivää", "pollution_recurrence_type_fixed": "Kiinteä päättymispäivä", "pollution_recurrence_type_yearly": "Uusiutuu vuosittain", "pollution_recurrence_type_monthly": "Uusiutuu kuukausittain", - "pollution_recurrence_type_no_end": "Ei päättymispäivämäärää", - "file_drop_existing_note": "Liitetiedosto (Napauta nähdäksesi)", - "fuel_import_step_1_title": "Vaihe 1 : Lataa CSV tiedosto", - "fuel_import_step_1_desc": "Valitse erotinmerkkiä käyttävä tekstitiedosto, joka sisältää polttoaineen kulutustietosi, aloittaaksesi tuontiprosessin", - "fuel_import_drop_placeholder": "Liitä erotinmerkillinen teksti tai napauta selataksesi", + "pollution_recurrence_type_no_end": "Ei päättymispäivää", + "file_drop_existing_note": "Olemassa oleva liite (Napauta näyttääksesi)", + "fuel_import_step_1_title": "Vaihe 1 : Lataa CSV-tiedosto", + "fuel_import_step_1_desc": "Valitse erotinmerkillä eroteltu tekstitiedosto, joka sisältää polttoainelokitietosi, aloittaaksesi tuonnin.", + "fuel_import_drop_placeholder": "Pudota erotinmerkillä eroteltu tekstitiedosto tähän tai napauta selataksesi", "fuel_import_headers_checkbox": "Ensimmäinen rivi sisältää otsikot", - "fuel_import_delimiter_title": "Erotin", - "fuel_import_delimiter_desc": "Valitse merkki, joka erottaa kentät", + "fuel_import_delimiter_title": "Erotinmerkki", + "fuel_import_delimiter_desc": "Valitse merkki, joka erottaa kentät toisistaan", "fuel_import_date_format_title": "Päivämäärän muoto", - "fuel_import_date_format_desc": "Määritä CSV-tiedostosi päivämäärille käytettävä muoto.", - "fuel_import_error_no_headers": "Otsikoita ei havaittu. Päivitä csv.helper.ts palauttaaksesi otsikot.", - "fuel_import_step_2_title": "Vaihe 2 : CSV-sarakkeiden kartoitus", - "fuel_import_step_2_desc": "Määritä CSV-tiedostosi sarakkeet vastaaviin polttoainelokin kenttiin. Pakolliset kentät on merkitty merkinnällä *.", - "fuel_import_step_3_title": "Vaihe 3 : Esikatsele ja tuo", - "fuel_import_step_3_desc": "Tarkista tuotavat tiedot esikatselusta.", - "fuel_import_no_preview": "Ei esikatseltavaa tietoa vielä. Toteuta jäsentely csv.helper.ts-tiedostossa rivien täyttämiseksi.", - "fuel_import_success": "Tuotu onnistuneesti {count} polttoainelokia.", + "fuel_import_date_format_desc": "Määritä CSV-tiedostossasi käytetty päivämäärän muoto.", + "fuel_import_error_no_headers": "Otsikoita ei havaittu. Päivitä csv.helper.ts palauttamaan otsikot.", + "fuel_import_step_2_title": "Vaihe 2 : Kohdista CSV-sarakkeet", + "fuel_import_step_2_desc": "Kohdista CSV-tiedostosi sarakkeet vastaaviin polttoainelokin kenttiin. Pakolliset kentät on merkitty merkillä *.", + "fuel_import_step_3_title": "Vaihe 3 : Esikatselu ja tuonti", + "fuel_import_step_3_desc": "Tarkista tuotavien tietojen esikatselu.", + "fuel_import_no_preview": "Ei vielä esikatselutietoja. Toteuta jäsennys tiedostossa csv.helper.ts rivien täyttämiseksi.", + "fuel_import_success": "{count} polttoaineloki(a) tuotu onnistuneesti.", "fuel_import_failed_count": "Tuotu {imported}, epäonnistui {failed}", "fuel_import_error_generic": "Polttoainelokin tuonti epäonnistui.", "fuel_import_vehicle_label": "Ajoneuvo:", @@ -475,39 +479,39 @@ "fuel_import_delimiter_semicolon": "Puolipiste ( ; )", "fuel_import_delimiter_tab": "Sarkain ( \\t )", "fuel_import_delimiter_pipe": "Pystyviiva ( | )", - "fuel_import_delimiter_custom": "Muokattu", - "fuel_import_date_error": "Jotkin rivit sisältävät virheellisesti muotoiltuja päivämääriä \"{format}\"", - "fuel_import_date_format_placeholder": "esim., MM/DD/YYYY", + "fuel_import_delimiter_custom": "Mukautettu", + "fuel_import_date_error": "Joillakin riveillä on virheellisiä päivämääriä muodolle \"{format}\"", + "fuel_import_date_format_placeholder": "esim. PP.KK.VVVV tai MM/DD/YYYY", "fuel_import_date_invalid": "Virheellinen päivämäärä", - "fuel_import_no_vehicle": "Ajoneuvoa ei valittu", + "fuel_import_no_vehicle": "Ajoneuvoa ei ole valittu", "fuel_import_col_date_hint": "Tankkauspäivämäärä", "fuel_import_col_odometer_hint": "Mittarilukema tankkaushetkellä", - "fuel_import_col_fuel_hint": "Tankattu määrä tai energia", - "fuel_import_col_cost_hint": "Kokonaiskustannus", - "fuel_import_col_filled_hint": "Onko tämä täysi tankki/lataus?", - "fuel_import_col_missed_hint": "Jäikö edellinen merkintä väliin?", - "fuel_import_col_notes_hint": "Lisätietoja", + "fuel_import_col_fuel_hint": "Tankattu määrä tai ladattu energia", + "fuel_import_col_cost_hint": "Merkinnän kokonaiskustannus", + "fuel_import_col_filled_hint": "Onko tankki/akku ladattu täyteen?", + "fuel_import_col_missed_hint": "Jäikö edellinen merkintä välistä?", + "fuel_import_col_notes_hint": "Mahdolliset lisämerkinnät", "autocomplete_placeholder": "Kirjoita tai valitse...", "autocomplete_loading": "Ladataan ehdotuksia...", - "autocomplete_no_results": "Ehdotuksia ei löydy. Voit kirjoittaa uuden arvon.", + "autocomplete_no_results": "Ehdotuksia ei löytynyt. Voit kirjoittaa uuden arvon.", "input_date_placeholder": "Valitse päivämäärä", "loading_default_message": "Ladataan...", "dropzone_placeholder_image": "Napauta tai vedä kuva ladataksesi", - "dropzone_placeholder_attachment": "Pudota kuva tänne tai napauta valitaksesi", + "dropzone_placeholder_attachment": "Pudota tiedosto tähän tai napauta valitaksesi", "dropzone_placeholder_default": "Napauta tai vedä tiedostoja ladataksesi", "dropzone_error_single_file": "Lataa vain yksi tiedosto.", - "dropzone_error_file_size": "Tiedoston koko ylittää sallitun enimmäiskoon {size}.", + "dropzone_error_file_size": "Tiedostokoko ylittää enimmäisrajan {size}.", "dropzone_unknown_file": "Tuntematon tiedosto", "dropzone_uploading": "Ladataan...", - "dropzone_supports": "Tuettu: {types}", + "dropzone_supports": "Tuetut muodot: {types}", "dropzone_max_size": "Enimmäiskoko: {size}", - "dropzone_error_file_type": "Tiedoston tyyppi ei ole tuettu", + "dropzone_error_file_type": "Tiedostotyyppi ei ole sallittu", "dropzone_hint_accept_limit": "{types} enintään {size}", "vehicle_details_color_aria": "Väri", "vehicle_details_close_aria": "Sulje", "attachment_link_view_title": "Näytä liite", - "file_preview_not_available": "Esikatselu ei ole saatavilla", - "file_preview_download_hint": "Tätä tiedostotyyppiä ei voi esikatsella suoraan. Lataa tiedosto tarkastellaksesi sitä.", + "file_preview_not_available": "Esikatselu ei ole käytettävissä", + "file_preview_download_hint": "Tätä tiedostotyyppiä ei voida esikatsella suoraan. Lataa tiedosto nähdäksesi sen.", "file_preview_download_button": "Lataa tiedosto", "file_preview_aria_download": "Lataa", "file_preview_aria_close": "Sulje", @@ -515,16 +519,16 @@ "fuel_log_edit": "Muokkaa", "fuel_log_delete": "Poista", "fuel_log_menu_open": "Avaa valikko", - "fuel_log_delete_success": "Poistetut polttoainelokimerkinnät", - "fuel_log_menu_sheet_title": "Päivitä polttoainelokimerkintä", - "fuel_log_delete_error": "Polttoainelokimerkinnän poistamisessa tapahtui virhe", + "fuel_log_delete_success": "Polttoaineloki poistettu", + "fuel_log_menu_sheet_title": "Päivitä polttoaineloki", + "fuel_log_delete_error": "Polttoainelokin poistamisessa tapahtui virhe.", "color_picker_label": "Valitse väri", "reminder_type_maintenance": "Huolto", "reminder_type_insurance": "Vakuutuksen uusiminen", "reminder_type_pollution": "Päästömittaus / katsastus", "reminder_type_registration": "Rekisteröinti / ajoneuvovero", "reminder_type_inspection": "Katsastus", - "reminder_type_custom": "Custom", + "reminder_type_custom": "Mukautettu", "alert_type_insurance": "Vakuutus", "alert_type_pucc": "Päästötodistus", "alert_status_expired_ago": "{label} vanheni {days} päivää sitten", @@ -537,18 +541,169 @@ "common_kilogram_unit": "Kilogrammaa (kg)", "common_pound_unit": "Paunaa (lb)", "settings_section_fuel_types": "Polttoaineen tyyppi", - "settings_section_fuel_types_desc": "Valitse mittayksikkö kullekin polttoaineelle.", + "settings_section_fuel_types_desc": "Hallitse autotallisi ajoneuvojen käyttämiä polttoainetyyppejä", "fuel_type_petrol_diesel": "Bensiini/Diesel", "theme_slate": "Liuskekivi", "theme_stone": "Kivi", "theme_red": "Punainen", - "theme_rose": "Ruusu", + "theme_rose": "Roosa", "theme_blue": "Sininen", "theme_green": "Vihreä", - "theme_purple": "Violetti", + "theme_purple": "Purppura", "theme_orange": "Oranssi", "theme_yellow": "Keltainen", "theme_teal": "Sinivihreä", "theme_indigo": "Indigo", - "theme_pink": "Vaaleanpunainen" + "theme_pink": "Vaaleanpunainen", + "settings_tab_notifications": "Ilmoitukset", + "settings_personalization_desc": "Mukauta ulkoasu teeman, kielen ja formaattien avulla.", + "settings_section_general": "Yleiset asetukset", + "settings_section_general_desc": "Mukauta ulkoasu, lokalisaatio ja näyttöformaatit", + "settings_units_desc": "Määritä mittayksiköt etäisyydelle, tilavuudelle ja polttoainetyypeille.", + "settings_section_units": "Yksiköt", + "settings_section_units_desc": "Valitse yksikkö etäisyydelle, kulutuksen ilmaisimelle ja polttoainetyypille.", + "settings_section_feature_flags": "Moduulit", + "settings_section_feature_flags_desc": "Ota käyttöön tai poista käytöstä sovelluksen päämoduuleja", + "settings_notifications_desc": "Määritä palveluntarjoajien tilaukset ja ajastettujen ilmoitusten päivittäinen lähetysaika.", + "settings_error_fix_errors": "Korjaa seuraavat virheet:", + "settings_fuel_types_label": "Polttoainetyypit", + "settings_fuel_types_desc": "Valitse mittayksikkö kullekin polttoaineelle.", + "fuel_type_label_petrol_diesel": "Bensiini/Diesel", + "fuel_type_label_lpg": "LPG", + "fuel_type_label_cng": "CNG", + "notif_scheduled_delivery": "Ajastettu lähetys", + "notif_scheduled_delivery_desc": "Sovelluksen sisäiset ilmoitukset näytetään reaaliajassa. Tämä aikataulu ohjaa vain ilmoitusten lähetystä ulkopuolisiin palveluihin.", + "notif_processing_schedule": "Käsittelyaikataulu", + "notif_processing_schedule_desc": "Suorita ilmoitusten lähetys palveluntarjoajille aikataulun mukaisesti.", + "notif_send_now": "Lähetä nyt", + "notif_providers": "Palveluntarjoajat", + "notif_providers_desc": "Luo, muokkaa, testaa ja ota käyttöön ilmoituspalveluita.", + "notif_providers_channels_info": "Jokainen palveluntarjoaja voi tilata Muistutukset-, Hälytykset- ja Tiedotteet-kanavia.", + "notif_add_provider": "Lisää palveluntarjoaja", + "notif_empty_title": "Ilmoituspalveluita ei ole vielä määritetty", + "notif_empty_desc": "Lisää palveluntarjoaja saadaksesi ajastettuja muistutuksia, hälytyksiä tai tiedotteita.", + "notif_load_failed": "Ilmoituspalveluiden lataaminen epäonnistui", + "notif_select_provider_type": "Valitse palveluntarjoajan tyyppi", + "notif_select_channel": "Valitse vähintään yksi ilmoituskanava", + "notif_provider_updated": "Palveluntarjoaja päivitetty onnistuneesti", + "notif_provider_created": "Palveluntarjoaja luotu onnistuneesti", + "notif_provider_deleted": "Palveluntarjoaja poistettu onnistuneesti", + "notif_provider_delete_failed": "Palveluntarjoajan poistaminen epäonnistui", + "notif_channel_reminder": "Muistutus", + "notif_channel_reminder_desc": "Määräpäivät ja muistutustyyppiset ilmoitukset", + "notif_channel_alert": "Hälytys", + "notif_channel_alert_desc": "Kiireelliset tai vanhentuvat asiat, jotka vaativat huomiota", + "notif_channel_information": "Tiedote", + "notif_channel_information_desc": "Yleiset tiedotteet ja päivitykset", + "notif_provider_enabled": "Käytössä", + "notif_provider_test": "Testaa palveluntarjoajaa", + "notif_provider_edit": "Muokkaa palveluntarjoajaa", + "notif_provider_delete": "Poista palveluntarjoaja", + "notif_dialog_add_title": "Lisää ilmoituspalvelu", + "notif_dialog_edit_title": "Muokkaa ilmoituspalvelua", + "notif_dialog_desc": "Valitse palveluntarjoajan tyyppi, määritä kohde ja tilaa haluamasi kanavat. Palveluntarjoajat ovat oletuksena käytössä, ja ne voidaan poistaa käytöstä palveluntarjoajakorteista.", + "notif_provider_name": "Palveluntarjoajan nimi", + "notif_provider_name_placeholder": "Päivittäinen koontisähköposti", + "notif_provider_type": "Palveluntarjoajan tyyppi", + "notif_provider_type_email": "Sähköposti (SMTP)", + "notif_provider_type_webhook": "Webhook", + "notif_provider_type_gotify": "Gotify", + "notif_provider_type_select": "Valitse palveluntarjoajan tyyppi", + "notif_dialog_cancel": "Peruuta", + "notif_dialog_create": "Luo palveluntarjoaja", + "notif_dialog_update": "Päivitä palveluntarjoaja", + "notif_channel_subscriptions": "Kanavatilaukset", + "notif_channel_subscriptions_desc": "Valitse, mitä ilmoituskanavia tämä palveluntarjoaja vastaanottaa.", + "notif_test_title": "Testaa palveluntarjoajaa", + "notif_test_success": "Testi-ilmoitus lähetetty onnistuneesti", + "notif_test_email_label": "Testisähköposti", + "notif_test_email_placeholder": "vastaanottaja@esimerkki.fi", + "notif_test_email_desc": "Valinnainen korvaava vastaanottaja tälle testimestille.", + "notif_test_message_label": "Testiviesti", + "notif_test_message_placeholder": "Tämä on testimuotoinen ilmoitus Tracktorista", + "notif_test_send": "Lähetä testi", + "notif_cron_presets": "Esiasetukset", + "notif_cron_every_minute": "Minuuteittain", + "notif_cron_every_5_min": "Viiden minuutin välein", + "notif_cron_every_15_min": "15 minuutin välein", + "notif_cron_every_30_min": "30 minuutin välein", + "notif_cron_every_hour": "Tunneittain", + "notif_cron_every_2_hours": "Kahden tunnin välein", + "notif_cron_every_6_hours": "Kuuden tunnin välein", + "notif_cron_every_12_hours": "12 tunnin välein", + "notif_cron_daily_midnight": "Päivittäin keskiyöllä", + "notif_cron_daily_2am": "Päivittäin klo 2:00", + "notif_cron_daily_8am": "Päivittäin klo 8:00", + "notif_cron_daily_noon": "Päivittäin keskipäivällä", + "notif_cron_weekly_monday": "Viikoittain (maanantaisin)", + "notif_cron_monthly_1st": "Kuukausittain (kuukauden 1. päivä)", + "notif_cron_expression_required": "Lauseke vaaditaan", + "notif_cron_must_have_5_parts": "Täytyy sisältää 5 osaa", + "notif_cron_invalid_chars": "Virheellisiä merkkejä", + "notif_cron_valid": "Kelvollinen lauseke", + "notif_cron_invalid": "Virheellinen lauseke", + "notif_cron_custom": "Mukautettu aikataulu", + "notif_email_smtp_settings": "SMTP-palvelimen asetukset", + "notif_email_host": "Isäntä (Host)", + "notif_email_port": "Portti", + "notif_email_use_ssl": "Käytä SSL/TLS-suojausta", + "notif_email_use_ssl_desc": "Ota suojattu yhteys käyttöön", + "notif_email_auth": "Autentikointi", + "notif_email_username": "Käyttäjätunnus / Sähköposti", + "notif_email_password": "Salasana", + "notif_email_password_keep": "Salasana (jätä tyhjäksi säilyttääksesi nykyisen)", + "notif_email_sender_info": "Lähettäjän/Vastaanottajan tiedot", + "notif_email_from": "Lähettäjän sähköposti", + "notif_email_from_name": "Lähettäjän nimi (valinnainen)", + "notif_email_from_name_placeholder": "Tracktor-ilmoitukset", + "notif_email_recipient": "Vastaanottajan sähköposti", + "notif_email_recipient_desc": "Vastaanottajan sähköpostiosoite", + "notif_webhook_config": "Webhook-määritykset", + "notif_webhook_url": "Webhook-URL", + "notif_webhook_method": "HTTP-metodi", + "notif_webhook_headers": "Mukautetut otsakkeet (JSON)", + "notif_webhook_headers_desc": "Ylimääräiset otsakkeet (headers), jotka sisällytetään webhook-pyyntöön", + "notif_webhook_auth_type": "Tunnistautumistyyppi", + "notif_webhook_auth_none": "Ei mitään", + "notif_webhook_auth_basic": "Basic Auth", + "notif_webhook_auth_bearer": "Bearer Token", + "notif_webhook_auth_apikey": "API-avain", + "notif_webhook_username": "Käyttäjätunnus", + "notif_webhook_apikey_header": "API-avaimen otsakenimi (Header Name)", + "notif_gotify_config": "Gotify-palvelimen määritykset", + "notif_gotify_url": "Palvelimen URL", + "notif_gotify_url_desc": "Gotify-palvelimesi instanssin URL-osoite", + "notif_gotify_token": "App-tunniste (Token)", + "notif_gotify_token_keep": "App-tunniste (jätä tyhjäksi säilyttääksesi nykyisen)", + "notif_gotify_token_desc": "Sovellustunniste (Application token) Gotify-sovelluksestasi (ei client-tunniste)", + "notif_gotify_priority": "Prioriteetti (0-10)", + "notif_gotify_priority_desc": "Viestin prioriteettitaso. Korkeampi prioriteetti = näkyvämpi ilmoitus", + "notif_cleared_success_one": "Poistettu 1 luettu ilmoitus", + "notif_cleared_success_other": "Poistettu {count} luettua ilmoitusta", + "notif_cleared_partial": "Poistettu {success}, {failed} epäonnistui", + "notif_clear_failed": "Luettujen ilmoitusten poistaminen epäonnistui", + "notif_all_marked_read": "Kaikki ilmoitukset merkitty luetuiksi", + "notif_mark_read_failed": "Ilmoitusten merkitseminen luetuiksi epäonnistui", + "notif_button_mark_all_read": "Merkitse kaikki luetuiksi", + "notif_button_clear_read": "Tyhjennä luetut", + "notif_button_clear_all_read_title": "Tyhjennä kaikki luetut ilmoitukset", + "notif_status_read": "Luettu", + "notif_status_unread": "Lukematta", + "notif_due_prefix": "Erääntyy: {date}", + "header_home_aria": "Siirry etusivulle", + "header_settings_aria": "Avaa asetukset", + "header_account_aria": "Tilin valikko", + "header_account_title": "Tili", + "notif_save_provider_failed": "Palveluntarjoajan tallentaminen epäonnistui", + "notif_update_provider_failed": "Palveluntarjoajan päivittäminen epäonnistui", + "notif_send_all_failed": "Ilmoitusten lähettäminen epäonnistui", + "notif_send_all_success": "Lähetetty {notifCount} ilmoitusta {successCount}/{providerCount} käytössä olevalle palveluntarjoajalle", + "notif_confirm_delete": "Haluatko varmasti poistaa palveluntarjoajan \"{name}\"?", + "notif_webhook_bearer_keep": "Bearer Token (jätä tyhjäksi säilyttääksesi nykyisen)", + "notif_webhook_apikey_keep": "API-avain (jätä tyhjäksi säilyttääksesi nykyisen)", + "notif_test_failed": "Testi-ilmoituksen lähettäminen epäonnistui", + "notif_test_send_desc": "Lähetä testi-ilmoitus käyttäen palvelua {name}", + "notif_cron_every_n_minutes": "{n} minuutin välein", + "notif_cron_hourly_at_minute": "Tunnittain minuutilla {n}", + "notif_cron_daily_at": "Päivittäin klo {time}" } diff --git a/messages/fr.json b/i18n/messages/fr.json similarity index 99% rename from messages/fr.json rename to i18n/messages/fr.json index 9a99c670..65537fa6 100644 --- a/messages/fr.json +++ b/i18n/messages/fr.json @@ -162,6 +162,7 @@ "fuel_add_title": "Ajouter un journal de carburant", "col_date": "Date", "col_odometer": "Odomètre", + "col_distance_driven": "Distance parcourue", "col_filled": "Rempli", "col_missed_last": "Dernier manqué", "col_fuel_amount": "Quantité de carburant", @@ -179,6 +180,7 @@ "form_odometer_desc": "Lecture actuelle de l'odomètre", "form_volume_fuel": "Volume de carburant", "form_volume_energy": "Énergie consommée", + "form_rate": "Prix unitaire", "form_cost": "Coût", "form_cost_desc": "Coût du remplissage", "form_cost_desc_ev": "Coût de la recharge", diff --git a/messages/hi.json b/i18n/messages/hi.json similarity index 99% rename from messages/hi.json rename to i18n/messages/hi.json index 8449de7a..0795d5e1 100644 --- a/messages/hi.json +++ b/i18n/messages/hi.json @@ -162,6 +162,7 @@ "fuel_add_title": "फ्यूल लॉग जोड़ें", "col_date": "तिथि", "col_odometer": "ओडोमीटर", + "col_distance_driven": "चलाई गई दूरी", "col_filled": "भराव", "col_missed_last": "पिछला छूटा", "col_fuel_amount": "ईंधन मात्रा", @@ -179,6 +180,7 @@ "form_odometer_desc": "वर्तमान ओडोमीटर रीडिंग", "form_volume_fuel": "ईंधन की मात्रा", "form_volume_energy": "खपत ऊर्जा", + "form_rate": "दर", "form_cost": "खर्च", "form_cost_desc": "रिफिल की लागत", "form_cost_desc_ev": "चार्जिंग की लागत", diff --git a/messages/hu.json b/i18n/messages/hu.json similarity index 71% rename from messages/hu.json rename to i18n/messages/hu.json index ed2722cc..82c60cd8 100644 --- a/messages/hu.json +++ b/i18n/messages/hu.json @@ -3,11 +3,11 @@ "hello_world": "Helló, {name}!", "app_name": "Tracktor", "app_title": "Járművek", - "app_new_update_available": "Új frissítés elérhető. Újratöltés..!", + "app_new_update_available": "Új frissítés elérhető. Újratöltés...", "app_add_vehicle": "Jármű hozzáadása", "app_empty_select_message": "Válassz egy járművet a részletek megtekintéséhez", "app_empty_select_hint": "Válassz egyet a fenti járművekből az irányítópult betöltéséhez!", - "demo_banner": "Ez egy demó példány. Az adatok időszakosan törlődnek, és nem kerülnek végleges mentésre. Kérjük, ne adj meg személyes adatokat!", + "demo_banner": "Ez egy demó példány. Az adatok időszakosan törlődnek, és nem kerülnek végleges mentésre. Kérlek, ne adj meg személyes adatokat!", "default_login": "Alapértelmezett bejelentkezés: demo / demo", "auth_username": "Felhasználónév", "auth_username_placeholder": "felhasználónév", @@ -41,8 +41,8 @@ "settings_desc_currency": "Válaszd ki a preferált pénznemet", "settings_desc_unit_distance": "A távolság mérésére használt egység", "settings_desc_unit_volume": "A térfogat mérésére használt egység", - "settings_label_mileage_format": "Fogyasztás megjelenítési formátum", - "settings_desc_mileage_format": "Válassza ki, hogyan jelenjen meg az üzemanyag-hatékonyság", + "settings_label_mileage_format": "Fogyasztás megjelenítési formátuma", + "settings_desc_mileage_format": "Válaszd ki, hogyan jelenjen meg az üzemanyag-hatékonyság", "settings_mileage_format_distance_per_fuel": "Távolság üzemanyagonként (pl. km/L, mpg)", "settings_mileage_format_fuel_per_distance": "Üzemanyag távolságonként (pl. L/100km)", "settings_desc_theme": "Válaszd ki a preferált témát", @@ -104,8 +104,8 @@ "vehicle_toast_updated": "Jármű sikeresen frissítve", "vehicle_toast_error_prefix": "Hiba a mentés során: ", "vehicle_list_empty": "Itt még üres minden. Add hozzá az első járművedet a kezdéshez!", - "vehicle_delete_success": "Jármű sikeresen törölve", - "vehicle_delete_error": "Hiba történt a jármű törlése közben", + "vehicle_delete_success": "Jármű sikeresen törölve.", + "vehicle_delete_error": "Hiba történt a jármű törlése közben.", "vehicle_action_add_fuel_log": "Tankolás hozzáadása", "vehicle_action_add_maintenance_log": "Karbantartás hozzáadása", "vehicle_action_add_insurance": "Biztosítás hozzáadása", @@ -151,19 +151,20 @@ "feature_maintenance_disabled_hint": "Engedélyezd ezt a funkciót a Beállításokban a karbantartási rekordok kezeléséhez!", "feature_pucc_disabled_title": "Környezetvédelmi igazolás funkció letiltva", "feature_pucc_disabled_hint": "Engedélyezd ezt a funkciót a Beállításokban a környezetvédelmi igazolások kezeléséhez!", - "feature_reminders_disabled_title": " funkció letiltva", + "feature_reminders_disabled_title": "Emlékeztető funkció letiltva", "feature_reminders_disabled_hint": "Engedélyezd ezt a funkciót a Beállításokban a jármű emlékeztetők kezeléséhez!", "feature_insurance_disabled_title": "Biztosítás funkció letiltva", "feature_insurance_disabled_hint": "Engedélyezd ezt a funkciót a Beállításokban a biztosítási részletek kezeléséhez!", "overview_chart_no_data": "Nincs elérhető adat", "overview_chart_cost_label": "Költség", - "overview_chart_cost_title": "Költségek idővel ({currency})", + "overview_chart_cost_title": "Költségek alakulása ({currency})", "overview_chart_mileage_label": "Fogyasztás", - "overview_chart_mileage_title": "Futásteljesítmény idővel ({unit})", + "overview_chart_mileage_title": "Fogyasztás alakulása ({unit})", "fuel_import_title": "Tankolások importálása", "fuel_add_title": "Tankolás hozzáadása", "col_date": "Dátum", "col_odometer": "Km-óra", + "col_distance_driven": "Megtett távolság", "col_filled": "Tele tank", "col_missed_last": "Előző kimaradt", "col_fuel_amount": "Mennyiség", @@ -171,16 +172,17 @@ "col_mileage": "Fogyasztás", "col_notes": "Megjegyzés", "col_attachment": "Csatolmány", - "col_no_end_date": "Nincs végső dátum", + "col_no_end_date": "Nincs befejezési dátum", "fuel_volume_label_fuel": "Üzemanyag", "fuel_volume_label_energy": "Energia", "fuel_empty_list": "Nincs tankolási bejegyzés ehhez a járműhöz.", "form_date": "Dátum", "form_date_desc": "A tankolás dátuma", "form_odometer": "Kilométeróra", - "form_odometer_desc": "Aktuális kilométeróra állás", + "form_odometer_desc": "Aktuális kilométeróra-állás", "form_volume_fuel": "Üzemanyag mennyisége", "form_volume_energy": "Felhasznált energia", + "form_rate": "Egységár", "form_cost": "Költség", "form_cost_desc": "Tankolás költsége", "form_cost_desc_ev": "Töltés költsége", @@ -193,14 +195,14 @@ "form_notes": "Megjegyzés", "form_notes_placeholder": "További részletek, ha vannak...", "form_attachment": "Csatolmány", - "fuel_toast_saved": "Tankolás sikeresen mentve", - "fuel_toast_updated": "Tankolás sikeresen frissítve", + "fuel_toast_saved": "Tankolás sikeresen mentve.", + "fuel_toast_updated": "Tankolás sikeresen frissítve.", "fuel_toast_error_prefix": "Hiba a mentés során: ", "notifications_title": "Értesítések", "notifications_new": "új", "notifications_select_vehicle_hint": "Válassz járművet az emlékeztetők és riasztások betöltéséhez!", "notifications_syncing": "Legfrissebb adatok szinkronizálása...", - "notifications_caught_up": "Minden naprakész", + "notifications_caught_up": "Minden naprakész.", "notifications_section_reminders": "Emlékeztetők", "notifications_section_alerts": "Riasztások", "notifications_mark_done_title": "Emlékeztető megjelölése készként", @@ -216,9 +218,9 @@ "alerts_status_valid": "Rendben", "alerts_status_missing": "Hiányzik", "notifications_expires": "Lejár", - "notifications_error_no_id": "Nem lehet frissíteni az emlékeztetőt azonosító nélkül", - "notifications_error_update_failed": "Az emlékeztető frissítése sikertelen", - "notifications_success_marked_done": "Emlékeztető készként megjelölve", + "notifications_error_no_id": "Nem lehet frissíteni az emlékeztetőt azonosító nélkül.", + "notifications_error_update_failed": "Az emlékeztető frissítése sikertelen.", + "notifications_success_marked_done": "Emlékeztető készként megjelölve.", "notifications_severity_overdue": "Lejárt", "notifications_severity_due_soon": "Hamarosan esedékes", "notifications_severity_upcoming": "Közelgő", @@ -237,7 +239,7 @@ "feature_label_overview": "Áttekintés", "feature_desc_overview": "Áttekintő vezérlőpult megjelenítése a legfontosabb járműadatokkal", "settings_error_date_format_invalid": "Érvénytelen formátum", - "settings_error_timezone_invalid": "Érvénytelen időzóna érték", + "settings_error_timezone_invalid": "Érvénytelen időzóna érték.", "settings_error_currency_required": "Pénznem megadása kötelező", "maintenance_form_attachment_label": "Csatolmány", "maintenance_form_attachment_desc": "Számla vagy karbantartási dokumentum feltöltése", @@ -255,7 +257,7 @@ "maintenance_toast_saved": "Karbantartás sikeresen mentve", "maintenance_toast_updated": "Karbantartás sikeresen frissítve", "maintenance_toast_error_prefix": "Hiba a mentés során: ", - "maintenance_form_error_fix": "Kérjük, javítsd a hibákat az űrlapon a mentés előtt!", + "maintenance_form_error_fix": "Kérlek, javítsd a hibákat az űrlapon a mentés előtt!", "maintenance_list_empty": "Nincs karbantartási bejegyzés ehhez a járműhöz.", "maintenance_col_service_center": "Szerviz", "maintenance_menu_open": "Menü megnyitása", @@ -264,8 +266,8 @@ "maintenance_menu_sheet_title": "Karbantartás frissítése", "maintenance_tab_title": "Karbantartási napló", "maintenance_add_action": "Karbantartás hozzáadása", - "maintenance_delete_success": "Karbantartás törölve", - "maintenance_delete_error": "Hiba történt a karbantartás törlése közben", + "maintenance_delete_success": "Karbantartás törölve.", + "maintenance_delete_error": "Hiba történt a karbantartás törlése közben.", "insurance_form_provider_label": "Biztosító", "insurance_form_provider_desc": "A biztosítótársaság neve", "insurance_form_policy_number_label": "Kötvényszám", @@ -285,7 +287,7 @@ "insurance_form_notes_placeholder": "További részletek a biztosításról...", "insurance_form_attachment_label": "Kötvény dokumentum", "insurance_form_attachment_desc": "Kötvény feltöltése", - "insurance_form_error_fix": "Kérjük, javítsd a hibákat az űrlapon a mentés előtt!", + "insurance_form_error_fix": "Kérlek, javítsd a hibákat az űrlapon a mentés előtt!", "insurance_toast_saved": "Biztosítás sikeresen mentve", "insurance_toast_updated": "Biztosítás sikeresen frissítve", "insurance_toast_error_prefix": "Hiba a mentés során: ", @@ -303,8 +305,8 @@ "insurance_menu_sheet_title": "Biztosítás frissítése", "insurance_tab_title": "Biztosítások", "insurance_add_action": "Biztosítás hozzáadása", - "insurance_delete_success": "Biztosítás törölve", - "insurance_delete_error": "Hiba történt a biztosítás törlése közben", + "insurance_delete_success": "Biztosítás törölve.", + "insurance_delete_error": "Hiba történt a biztosítás törlése közben.", "insurance_col_view_document": "Dokumentum megtekintése", "pollution_form_certificate_number_label": "Bizonyítvány száma", "pollution_form_certificate_number_desc": "Környezetvédelmi igazolás száma", @@ -323,7 +325,7 @@ "pollution_form_notes_placeholder": "További megjegyzések hozzáadása...", "pollution_form_attachment_label": "Bizonyítvány dokumentum", "pollution_form_attachment_desc": "Bizonyítvány feltöltése", - "pollution_form_error_fix": "Kérjük, javítsd a hibákat az űrlapon a küldés előtt!", + "pollution_form_error_fix": "Kérlek, javítsd a hibákat az űrlapon a küldés előtt!", "pollution_toast_saved": "Igazolás sikeresen mentve", "pollution_toast_updated": "Igazolás sikeresen frissítve", "pollution_toast_error_prefix": "Hiba a mentés során: ", @@ -342,8 +344,8 @@ "pollution_menu_sheet_title": "Környezetvédelmi igazolás frissítése", "pollution_tab_title": "Környezetvédelmi igazolások", "pollution_add_action": "Igazolás hozzáadása", - "pollution_delete_success": "Igazolás törölve", - "pollution_delete_error": "Hiba történt az igazolás törlése közben", + "pollution_delete_success": "Igazolás törölve.", + "pollution_delete_error": "Hiba történt az igazolás törlése közben.", "reminder_form_due_date_label": "Esedékesség dátuma", "reminder_form_due_date_desc": "Mikor jelezzen az emlékeztető?", "reminder_form_type_label": "Típus", @@ -360,7 +362,7 @@ "reminder_form_note_desc": "További kontextus hozzáadása", "reminder_form_note_placeholder": "Részletezd, mi igényel figyelmet", "reminder_form_is_completed_label": "Megjelölés készként", - "reminder_toast_created": "Emlékeztető sikeresen létrehozva", + "reminder_toast_created": "Emlékeztető sikeresen létrehozva.", "reminder_toast_updated": "Emlékeztető sikeresen frissítve", "reminder_toast_error_prefix": "Hiba a mentés során: ", "reminder_list_empty": "Még nincsenek emlékeztetők. Hozz létre egyet, hogy ne maradj le a közelgő megújításokról!", @@ -379,13 +381,13 @@ "reminder_menu_sheet_title": "Emlékeztető frissítése", "reminder_tab_title": "Emlékeztetők", "reminder_add_action": "Emlékeztető hozzáadása", - "reminder_delete_success": "Emlékeztető törölve", - "reminder_delete_error": "Hiba történt az emlékeztető törlése közben", + "reminder_delete_success": "Emlékeztető törölve.", + "reminder_delete_error": "Hiba történt az emlékeztető törlése közben.", "reminder_status_completed": "Kész", "reminder_status_pending": "Függőben", "reminder_status_overdue": "Lejárt", - "reminder_status_error": "Nem sikerült frissíteni az emlékeztető állapotát", - "reminder_toast_error_fallback": "Nem sikerült menteni az emlékeztetőt", + "reminder_status_error": "Nem sikerült frissíteni az emlékeztető állapotát.", + "reminder_toast_error_fallback": "Nem sikerült menteni az emlékeztetőt.", "settings_sheet_title": "Beállítások", "feature_label_pollution": "Környezetvédelmi igazolás", "feature_desc_pollution": "Környezetvédelmi igazolások kezelése", @@ -455,7 +457,7 @@ "pollution_recurrence_type_no_end": "Nincs lejárati dátum", "alert_insurance_active_no_end": "A biztosítás aktív, nincs lejárati dátum", "alert_pucc_active_no_end": "A környezetvédelmi tanúsítvány aktív, nincs lejárati dátum", - "alert_record_not_found": "Nincs {label} adat. Adj meg részleteket a megfelelőség érdekében.", + "alert_record_not_found": "Nincs {label} adat. Add meg a részleteket a megfelelőség érdekében.", "alert_status_expired_ago": "{label} lejárt {days} napja", "alert_status_expires_in": "{label} lejár {days} nap múlva", "alert_status_valid_for": "{label} érvényes még {days} napig", @@ -492,8 +494,8 @@ "fuel_import_col_missed_hint": "Előző bejegyzés kimaradt?", "fuel_import_col_notes_hint": "Megjegyzések", "fuel_import_col_odometer_hint": "Km-óra állás a tankoláskor", - "fuel_import_date_error": "Érvénytelen dátum", - "fuel_import_date_format_desc": "Add meg a dátum formátumát a CSV-ben", + "fuel_import_date_error": "Néhány sornál érvénytelen a dátum a megadott formátumhoz: \"{format}\"", + "fuel_import_date_format_desc": "Add meg a dátum formátumát a CSV-ben.", "fuel_import_date_format_placeholder": "pl. YYYY-MM-DD", "fuel_import_date_format_title": "Dátum formátum", "fuel_import_date_invalid": "Érvénytelen dátum formátum", @@ -505,20 +507,20 @@ "fuel_import_delimiter_tab": "Tabulátor", "fuel_import_delimiter_title": "Elválasztó karakter", "fuel_import_drop_placeholder": "Húzd ide a fájlt vagy kattints a kiválasztáshoz", - "fuel_import_error_generic": "Hiba történt az importálás során", + "fuel_import_error_generic": "Hiba történt az importálás során.", "fuel_import_error_no_headers": "A fejlécek nem találhatóak. Kérlek, ellenőrizd a fájlt.", "fuel_import_failed_count": "Sikeres: {imported}, sikertelen: {failed}", "fuel_import_headers_checkbox": "A fájl tartalmazzon fejléceket", - "fuel_import_no_preview": "Nincs elérhető előnézet", + "fuel_import_no_preview": "Nincs elérhető előnézet.", "fuel_import_no_vehicle": "Nincs kiválasztott jármű", - "fuel_import_step_1_desc": "Válaszd ki a tankolási naplókat tartalmazó CSV fájlt", + "fuel_import_step_1_desc": "Válaszd ki a tankolási naplókat tartalmazó CSV fájlt.", "fuel_import_step_1_title": "CSV feltöltése", - "fuel_import_step_2_desc": "Párosítsd a CSV oszlopait a mezőkkel", + "fuel_import_step_2_desc": "Párosítsd a CSV oszlopait a megfelelő tankolási mezőkkel. A kötelező mezőket egy * jelöli.", "fuel_import_step_2_title": "Oszlopok párosítása", - "fuel_import_step_3_desc": "Ellenőrizd az adatokat és importáld a tankolásokat", + "fuel_import_step_3_desc": "Ellenőrizd az adatokat és importáld a tankolásokat.", "fuel_import_step_3_title": "Előnézet és importálás", - "fuel_import_success": "Sikeres importálás!", - "fuel_import_vehicle_label": "Jármű kiválasztása", + "fuel_import_success": "Sikeresen importálva {count} tankolás.", + "fuel_import_vehicle_label": "Jármű kiválasztása:", "fuel_log_delete": "Törlés", "fuel_log_delete_error": "Hiba történt a tankolás törlése során.", "fuel_log_delete_success": "Tankolás törölve", @@ -551,5 +553,157 @@ "theme_yellow": "Sárga", "theme_teal": "Türkiz", "theme_indigo": "Indigó", - "theme_pink": "Rózsaszín" + "theme_pink": "Rózsaszín", + "settings_mileage_format_uk_mpg": "UK MPG (mérföld per birodalmi gallon)", + "settings_tab_notifications": "Értesítések", + "settings_personalization_desc": "Testreszabhatod az élményt a témák, nyelvek és formátumok segítségével.", + "settings_section_general": "Általános", + "settings_section_general_desc": "Megjelenés, lokalizáció és megjelenítési formátumok testreszabása", + "settings_units_desc": "Mértékegységek beállítása a távolsághoz, térfogathoz és üzemanyagtípusokhoz.", + "settings_section_units": "Mértékegységek", + "settings_section_units_desc": "Válaszd ki a preferált mértékegységeket", + "settings_section_feature_flags": "Funkciók engedélyezése", + "settings_section_feature_flags_desc": "Főbb alkalmazásmodulok engedélyezése vagy letiltása", + "settings_notifications_desc": "Állítsd be a szolgáltatói feliratkozásokat és az ütemezett kézbesítés napi feldolgozási idejét.", + "settings_error_fix_errors": "Kérlek, javítsd a következő hibákat:", + "settings_fuel_types_label": "Üzemanyag típusok", + "settings_fuel_types_desc": "Válaszd ki a mértékegységet minden üzemanyaghoz.", + "fuel_type_label_petrol_diesel": "Benzin/Dízel", + "fuel_type_label_lpg": "LPG", + "fuel_type_label_cng": "CNG", + "notif_scheduled_delivery": "Ütemezett kézbesítés", + "notif_scheduled_delivery_desc": "Az alkalmazáson belüli értesítések valós idejűek maradnak. Ez az ütemezés csak a szolgáltatói kézbesítést szabályozza.", + "notif_processing_schedule": "Feldolgozási ütemezés", + "notif_processing_schedule_desc": "Futtasd a szolgáltatói értesítések kézbesítését ütemterv szerint.", + "notif_send_now": "Küldés most", + "notif_providers": "Szolgáltatók", + "notif_providers_desc": "Értesítési szolgáltatók létrehozása, szerkesztése, tesztelése és engedélyezése.", + "notif_providers_channels_info": "Minden szolgáltató feliratkozhat az Emlékeztető, Riasztás és Információs csatornákra.", + "notif_add_provider": "Szolgáltató hozzáadása", + "notif_empty_title": "Még nincsenek értesítési szolgáltatók beállítva", + "notif_empty_desc": "Adj hozzá egy szolgáltatót ütemezett emlékeztetők, riasztások vagy információs értesítések fogadásához.", + "notif_load_failed": "Nem sikerült betölteni az értesítési szolgáltatókat", + "notif_select_provider_type": "Kérlek, válassz szolgáltató típust", + "notif_select_channel": "Válasszon ki legalább egy értesítési csatornát", + "notif_provider_updated": "Szolgáltató sikeresen frissítve", + "notif_provider_created": "Szolgáltató sikeresen létrehozva", + "notif_provider_deleted": "Szolgáltató sikeresen törölve", + "notif_provider_delete_failed": "A szolgáltató törlése sikertelen", + "notif_channel_reminder": "Emlékeztető", + "notif_channel_reminder_desc": "Esedékességi dátumok és emlékeztető jellegű értesítések", + "notif_channel_alert": "Riasztás", + "notif_channel_alert_desc": "Sürgős vagy lejáró tételek, amelyek figyelmet igényelnek", + "notif_channel_information": "Információ", + "notif_channel_information_desc": "Általános információs frissítések", + "notif_provider_enabled": "Engedélyezve", + "notif_provider_test": "Szolgáltató tesztelése", + "notif_provider_edit": "Szolgáltató szerkesztése", + "notif_provider_delete": "Szolgáltató törlése", + "notif_dialog_add_title": "Értesítési szolgáltató hozzáadása", + "notif_dialog_edit_title": "Értesítési szolgáltató szerkesztése", + "notif_dialog_desc": "Válassz szolgáltatót, állítsd be a célállomást és iratkozz fel a csatornákra. A szolgáltatók alapértelmezés szerint engedélyezve vannak.", + "notif_provider_name": "Szolgáltató neve", + "notif_provider_name_placeholder": "Napi összefoglaló e-mail", + "notif_provider_type": "Szolgáltató típusa", + "notif_provider_type_email": "E-mail (SMTP)", + "notif_provider_type_webhook": "Webhook", + "notif_provider_type_gotify": "Gotify", + "notif_provider_type_select": "Válasszon szolgáltató típust", + "notif_dialog_cancel": "Mégse", + "notif_dialog_create": "Szolgáltató létrehozása", + "notif_dialog_update": "Szolgáltató frissítése", + "notif_channel_subscriptions": "Csatornafeliratkozások", + "notif_channel_subscriptions_desc": "Válaszd ki, mely értesítési csatornákat kapja meg ez a szolgáltató.", + "notif_test_title": "Szolgáltató tesztelése", + "notif_test_success": "A tesztértesítés sikeresen elküldve", + "notif_test_email_label": "Teszt e-mail", + "notif_test_email_placeholder": "cimzett@example.com", + "notif_test_email_desc": "Opcionális egyedi címzett ehhez a tesztüzenethez.", + "notif_test_message_label": "Tesztüzenet", + "notif_test_message_placeholder": "Ez egy tesztértesítés a Tracktorból", + "notif_test_send": "Teszt küldése", + "notif_cron_presets": "Előbeállítások", + "notif_cron_every_minute": "Minden percben", + "notif_cron_every_5_min": "Minden 5 percben", + "notif_cron_every_15_min": "Minden 15 percben", + "notif_cron_every_30_min": "Minden 30 percben", + "notif_cron_every_hour": "Minden órában", + "notif_cron_every_2_hours": "Minden 2 órában", + "notif_cron_every_6_hours": "Minden 6 órában", + "notif_cron_every_12_hours": "Minden 12 órában", + "notif_cron_daily_midnight": "Naponta éjfélkor", + "notif_cron_daily_2am": "Naponta 2:00-kor", + "notif_cron_daily_8am": "Naponta 8:00-kor", + "notif_cron_daily_noon": "Naponta délben", + "notif_cron_weekly_monday": "Hetente (hétfőn)", + "notif_cron_monthly_1st": "Havonta (elsején)", + "notif_cron_expression_required": "Kifejezés szükséges", + "notif_cron_must_have_5_parts": "5 résznek kell lennie", + "notif_cron_invalid_chars": "Érvénytelen karakterek", + "notif_cron_valid": "Érvényes kifejezés", + "notif_cron_invalid": "Érvénytelen kifejezés", + "notif_cron_custom": "Egyéni ütemezés", + "notif_email_smtp_settings": "SMTP szerver beállításai", + "notif_email_host": "Host", + "notif_email_port": "Port", + "notif_email_use_ssl": "SSL/TLS használata", + "notif_email_use_ssl_desc": "Biztonságos kapcsolat engedélyezése", + "notif_email_auth": "Hitelesítés", + "notif_email_username": "Felhasználónév / E-mail", + "notif_email_password": "Jelszó", + "notif_email_password_keep": "Jelszó (hagyd üresen a jelenlegi megtartásához)", + "notif_email_sender_info": "Feladó/címzett információk", + "notif_email_from": "Feladó e-mail címe", + "notif_email_from_name": "Feladó neve (opcionális)", + "notif_email_from_name_placeholder": "Tracktor értesítések", + "notif_email_recipient": "Címzett e-mail címe", + "notif_email_recipient_desc": "A címzett e-mail címe", + "notif_webhook_config": "Webhook konfiguráció", + "notif_webhook_url": "Webhook URL", + "notif_webhook_method": "HTTP metódus", + "notif_webhook_headers": "Egyéni fejlécek (JSON)", + "notif_webhook_headers_desc": "További fejlécek a webhook kéréshez", + "notif_webhook_auth_type": "Hitelesítés típusa", + "notif_webhook_auth_none": "Nincs", + "notif_webhook_auth_basic": "Basic Auth", + "notif_webhook_auth_bearer": "Bearer Token", + "notif_webhook_auth_apikey": "API kulcs", + "notif_webhook_username": "Felhasználónév", + "notif_webhook_apikey_header": "API kulcs fejlécének neve", + "notif_gotify_config": "Gotify szerver beállításai", + "notif_gotify_url": "Szerver URL", + "notif_gotify_url_desc": "A Gotify szerverpéldány URL-je", + "notif_gotify_token": "App token", + "notif_gotify_token_keep": "App token (hagyd üresen a jelenlegi megtartásához)", + "notif_gotify_token_desc": "Alkalmazás token a Gotify alkalmazásból (nem a kliens token)", + "notif_gotify_priority": "Prioritás (0-10)", + "notif_gotify_priority_desc": "Üzenet prioritási szintje. Magasabb prioritás = kiemeltebb értesítés", + "notif_cleared_success_one": "1 olvasott értesítés törölve", + "notif_cleared_success_other": "{count} olvasott értesítés törölve", + "notif_cleared_partial": "{success} törölve, {failed} sikertelen", + "notif_clear_failed": "Olvasott értesítések törlése sikertelen", + "notif_all_marked_read": "Minden értesítés olvasottként megjelölve", + "notif_mark_read_failed": "Értesítések olvasottként való megjelölése sikertelen", + "notif_button_mark_all_read": "Összes megjelölése olvasottként", + "notif_button_clear_read": "Olvasottak törlése", + "notif_button_clear_all_read_title": "Összes olvasott értesítés törlése", + "notif_status_read": "Olvasott", + "notif_status_unread": "Olvasatlan", + "notif_due_prefix": "Esedékes: {date}", + "header_home_aria": "Ugrás a kezdőlapra", + "header_settings_aria": "Beállítások megnyitása", + "header_account_aria": "Fiók menü", + "header_account_title": "Fiók", + "notif_save_provider_failed": "Szolgáltató mentése sikertelen", + "notif_update_provider_failed": "Szolgáltató frissítése sikertelen", + "notif_send_all_failed": "Értesítések küldése sikertelen", + "notif_send_all_success": "{notifCount} értesítés elküldve {successCount}/{providerCount} engedélyezett szolgáltatónak", + "notif_confirm_delete": "Biztosan törölni szeretnéd: \"{name}\"?", + "notif_webhook_bearer_keep": "Bearer Token (hagyd üresen a jelenlegi megtartásához)", + "notif_webhook_apikey_keep": "API kulcs (hagyd üresen a jelenlegi megtartásához)", + "notif_test_failed": "A tesztértesítés küldése sikertelen", + "notif_test_send_desc": "Tesztértesítés küldése a(z) {name} használatával", + "notif_cron_every_n_minutes": "Minden {n}. percben", + "notif_cron_hourly_at_minute": "Minden óra {n}. percében", + "notif_cron_daily_at": "Naponta {time}-kor" } diff --git a/messages/it.json b/i18n/messages/it.json similarity index 99% rename from messages/it.json rename to i18n/messages/it.json index 0e54dee5..1c2384b4 100644 --- a/messages/it.json +++ b/i18n/messages/it.json @@ -163,6 +163,7 @@ "fuel_add_title": "Aggiungi Rifornimento", "col_date": "Data", "col_odometer": "Contachilometri", + "col_distance_driven": "Distanza percorsa", "col_filled": "Pieno", "col_missed_last": "Ultimo Mancato", "col_fuel_amount": "Quantità di Carburante", @@ -180,6 +181,7 @@ "form_odometer_desc": "Lettura corrente del contachilometri del veicolo", "form_volume_fuel": "Volume di carburante", "form_volume_energy": "Energia consumata", + "form_rate": "Prezzo unitario", "form_cost": "Costo", "form_cost_desc": "Costo del rifornimento", "form_cost_desc_ev": "Costo della ricarica", diff --git a/i18n/messages/ro.json b/i18n/messages/ro.json new file mode 100644 index 00000000..e6cd73ec --- /dev/null +++ b/i18n/messages/ro.json @@ -0,0 +1,709 @@ +{ + "$schema": "https://inlang.com/schema/inlang-message-format", + "hello_world": "Salut, {name} din ro!", + "app_name": "Tracktor", + "app_title": "Garajul tău", + "app_new_update_available": "O nouă actualizare este disponibilă. Se reîncărcă...!", + "app_add_vehicle": "Adaugă vehicul", + "app_empty_select_message": "Selectează un vehicul pentru a-i vedea detaliile", + "app_empty_select_hint": "Alege unul din garajul de mai sus pentru a-i încărca panoul de control.", + "demo_banner": "Aceasta este o instanță demo. Datele vor fi resetate periodic și nu sunt salvate permanent. Te rugăm să eviți adăugarea de informații personale.", + "default_login": "Autentificare implicită: demo / demo", + "auth_username": "Nume utilizator", + "auth_username_placeholder": "nume utilizator", + "auth_password": "Parolă", + "auth_password_placeholder": "********", + "auth_confirm_password": "Confirmă parola", + "auth_login_button": "Autentificare", + "auth_signup_button": "Înregistrare", + "auth_login_loading": "Se autentifică...", + "auth_signup_loading": "Se creează contul...", + "auth_password_mismatch": "Parolele nu se potrivesc!!!", + "settings_tab_personalization": "Personalizare", + "settings_tab_interface": "Interfață", + "settings_tab_features": "Funcționalități", + "settings_tab_units": "Unități", + "settings_title": "Setări", + "settings_label_date_format": "Format dată", + "settings_label_locale": "Limbă / Localizare", + "settings_label_timezone": "Fus orar", + "settings_label_currency": "Monedă", + "settings_label_unit_distance": "Unitate de distanță", + "settings_label_unit_volume": "Unitate de combustibil", + "settings_label_theme": "Temă", + "settings_label_custom_css": "CSS personalizat", + "settings_update_button": "Actualizează setările", + "settings_select_unit_system": "Selectează sistemul de unități", + "settings_select_theme": "Selectează tema", + "settings_desc_date_format": "Alege formatul preferat pentru dată", + "settings_desc_locale": "Alege limba pentru interfață", + "settings_desc_timezone": "Alege fusul orar pentru afișarea datei", + "settings_desc_currency": "Alege moneda preferată", + "settings_desc_unit_distance": "Unitatea de măsură pentru distanță", + "settings_desc_unit_volume": "Unitatea de măsură pentru volum", + "settings_label_mileage_format": "Format afișare consum", + "settings_desc_mileage_format": "Alege modul în care este afișată eficiența combustibilului", + "settings_mileage_format_distance_per_fuel": "Distanță per combustibil (de ex., km/L, mpg)", + "settings_mileage_format_fuel_per_distance": "Combustibil per distanță (de ex., L/100km)", + "settings_mileage_format_uk_mpg": "UK MPG (mile per galon imperial)", + "settings_desc_theme": "Alege tema preferată", + "settings_desc_custom_css": "Stiluri CSS pentru personalizarea interfeței", + "settings_select_language": "Selectează limba", + "settings_updated_success": "Configurație actualizată cu succes!", + "common_example_prefix": "Exemplu - ", + "common_invalid_format": "Format nevalid...", + "common_kilometer": "Kilometru", + "common_mile": "Mile", + "common_litre": "Litru", + "common_gallon": "Galon", + "common_submit": "Trimite", + "common_yes": "Da", + "common_no": "Nu", + "common_cancel": "Anulează", + "common_confirm": "Confirmă", + "common_continue": "Continuă", + "common_skip": "Omite", + "delete_dialog_title": "Șterge", + "delete_dialog_message": "Sigur dorești să ștergi?", + "common_select_column": "Selectează coloana", + "common_import": "Importă", + "common_search": "Caută", + "common_columns": "Coloane", + "common_rows_per_page": "Rânduri pe pagină", + "common_no_data_available": "Nu există date disponibile", + "common_add_new": "Adaugă nou", + "common_no_match_found": "Nu s-a găsit nicio potrivire", + "common_search_placeholder": "Caută {name}", + "common_select_placeholder": "Selectează {name}...", + "nav_overview": "Prezentare generală", + "nav_fuel_logs": "Jurnal combustibil", + "nav_maintenance": "Întreținere", + "nav_insurance": "Asigurare", + "nav_pollution": "Certificat poluare (ITP)", + "nav_reminders": "Mementouri", + "tools_export_data": "Exportă datele", + "tools_import_data": "Importă datele", + "vehicle_form_make_label": "Marcă", + "vehicle_form_make_desc": "Producătorul vehiculului", + "vehicle_form_model_label": "Model", + "vehicle_form_model_desc": "Modelul vehiculului", + "vehicle_form_year_label": "An", + "vehicle_form_year_desc": "Anul de fabricație", + "vehicle_form_color_label": "Culoare", + "vehicle_form_color_desc": "Culoarea vehiculului", + "vehicle_form_fuel_type_label": "Tip combustibil", + "vehicle_form_fuel_type_desc": "Tipul de combustibil utilizat de vehicul", + "vehicle_form_fuel_type_placeholder": "Selectează tipul de combustibil", + "vehicle_form_odometer_label": "Odometru", + "vehicle_form_odometer_desc": "Citirea curentă a odometrului vehiculului", + "vehicle_form_license_label": "Număr de înmatriculare", + "vehicle_form_license_desc": "Numărul de înmatriculare al vehiculului", + "vehicle_form_vin_label": "VIN", + "vehicle_form_vin_desc": "Numărul de identificare al vehiculului (serie șasiu)", + "vehicle_toast_saved": "Vehicul salvat cu succes", + "vehicle_toast_updated": "Vehicul actualizat cu succes", + "vehicle_toast_error_prefix": "Eroare la salvare: ", + "vehicle_list_empty": "Este gol aici. Te rugăm să adaugi primul vehicul pentru a începe.", + "vehicle_delete_success": "Vehiculul a fost șters cu succes.", + "vehicle_delete_error": "A apărut o eroare la ștergerea vehiculului.", + "vehicle_action_add_fuel_log": "Adaugă alimentare", + "vehicle_action_add_maintenance_log": "Adaugă istoric întreținere", + "vehicle_action_add_insurance": "Adaugă asigurare", + "vehicle_action_add_pollution": "Adaugă certificat poluare", + "vehicle_action_add_reminder": "Adaugă memento", + "vehicle_action_more_info": "Mai multe info", + "vehicle_action_update_vehicle": "Actualizează vehiculul", + "vehicle_action_edit": "Editează", + "vehicle_action_delete": "Șterge", + "tools_export_encrypt_label": "Criptează datele exportate", + "tools_export_password_label": "Parolă de criptare", + "tools_export_password_placeholder": "Introdu parola pentru criptare", + "tools_export_password_hint": "Păstrează această parolă în siguranță – vei avea nevoie de ea pentru a decripta datele la import.", + "tools_export_status_exporting": "Se exportă...", + "tools_export_button": "Exportă baza de date", + "tools_export_info_title": "Informații despre export", + "tools_export_info_bullet_1": "Exportă toate tabelele și datele din baza de date", + "tools_export_info_bullet_2": "Include vehiculele, jurnalele de combustibil, înregistrările de întreținere etc.", + "tools_export_info_bullet_3": "Criptare opțională pentru protecția datelor sensibile", + "tools_export_info_bullet_4": "Se descarcă ca fișier JSON", + "tools_export_success": "Date exportate cu succes", + "tools_export_error": "Exportul datelor a eșuat", + "tools_import_upload_label": "Încarcă fișier JSON", + "tools_import_paste_label": "Sau lipește datele JSON", + "tools_import_paste_placeholder": "Lipește datele JSON exportate aici...", + "tools_import_password_label": "Parolă de decriptare (dacă sunt criptate)", + "tools_import_password_placeholder": "Introdu parola dacă datele sunt criptate", + "tools_import_status_importing": "Se importă...", + "tools_import_button": "Importă baza de date", + "tools_import_warning_title": "⚠️ Avertisment import", + "tools_import_warning_bullet_1": "Această acțiune va înlocui TOATE datele existente", + "tools_import_warning_bullet_2": "Asigură-te că ai făcut o copie de rezervă a datelor curente mai întâi", + "tools_import_warning_bullet_3": "Importul nu poate fi anulat", + "tools_import_warning_bullet_4": "Verifică dacă formatul JSON este corect", + "tools_import_success": "Date importate cu succes", + "tools_import_error": "Importul datelor a eșuat", + "tools_import_invalid_json": "Format JSON nevalid", + "feature_overview_disabled_title": "Funcția de prezentare generală este dezactivată", + "feature_overview_disabled_hint": "Activează această funcție din Setări pentru a vedea panoul de prezentare generală", + "feature_fuel_disabled_title": "Funcția de jurnal combustibil este dezactivată", + "feature_fuel_disabled_hint": "Activează această funcție din Setări pentru a urmări consumul de combustibil", + "feature_maintenance_disabled_title": "Funcția de întreținere este dezactivată", + "feature_maintenance_disabled_hint": "Activează această funcție din Setări pentru a gestiona înregistrările de întreținere", + "feature_pucc_disabled_title": "Funcția de certificat poluare (ITP) este dezactivată", + "feature_pucc_disabled_hint": "Activează această funcție din Setări pentru a gestiona certificatele de poluare", + "feature_reminders_disabled_title": "Funcția de mementouri este dezactivată", + "feature_reminders_disabled_hint": "Activează această funcție din Setări pentru a gestiona mementourile vehiculului", + "feature_insurance_disabled_title": "Funcția de asigurare este dezactivată", + "feature_insurance_disabled_hint": "Activează această funcție din Setări pentru a gestiona detaliile asigurării", + "overview_chart_no_data": "Nu există date disponibile", + "overview_chart_cost_label": "Cost", + "overview_chart_cost_title": "Costul în timp în ({currency})", + "overview_chart_mileage_label": "Consum", + "overview_chart_mileage_title": "Consumul în timp în ({unit})", + "fuel_import_title": "Importă jurnale de combustibil", + "fuel_add_title": "Adaugă jurnal combustibil", + "col_date": "Dată", + "col_odometer": "Odometru", + "col_distance_driven": "Distanță parcursă", + "col_filled": "Alimentat", + "col_missed_last": "Alimentare ratată", + "col_fuel_amount": "Cantitate combustibil", + "col_cost": "Cost", + "col_mileage": "Eficiență / Consum", + "col_notes": "Note", + "col_attachment": "Atașament", + "col_no_end_date": "Fără dată de sfârșit", + "fuel_volume_label_fuel": "Cantitate combustibil", + "fuel_volume_label_energy": "Energie", + "fuel_empty_list": "Nu s-au găsit jurnale de alimentare pentru acest vehicul.", + "form_date": "Dată", + "form_date_desc": "Data alimentării cu combustibil", + "form_odometer": "Odometru", + "form_odometer_desc": "Citirea curentă a odometrului vehiculului", + "form_volume_fuel": "Volum combustibil", + "form_volume_energy": "Energie consumată", + "form_rate": "Preț unitar", + "form_cost": "Cost", + "form_cost_desc": "Costul alimentării", + "form_cost_desc_ev": "Costul încărcării", + "form_full_charge": "Încărcare completă", + "form_full_tank": "Rezervor plin", + "form_full_charge_desc": "Este bateria încărcată complet?", + "form_full_tank_desc": "Este rezervorul umplut până sus?", + "form_missed_last": "Alimentare anterioară ratată", + "form_missed_last_desc": "A fost omisă vreuna dintre înregistrările anterioare?", + "form_notes": "Note", + "form_notes_placeholder": "Adaugă mai multe detalii, dacă există...", + "form_attachment": "Atașament", + "fuel_toast_saved": "Jurnalul de combustibil a fost salvat cu succes...!!!", + "fuel_toast_updated": "Jurnalul de combustibil a fost actualizat cu succes...!!!", + "fuel_toast_error_prefix": "Eroare la salvare: ", + "notifications_title": "Notificări", + "notifications_new": "nou", + "notifications_select_vehicle_hint": "Selectează un vehicul pentru a încărca mementourile și alertele.", + "notifications_syncing": "Se sincronizează ultimele date...", + "notifications_caught_up": "Ești la curent cu toate.", + "notifications_section_reminders": "Mementouri", + "notifications_section_alerts": "Alerte de conformitate", + "notifications_mark_done_title": "Marchează mementoul ca finalizat", + "notifications_mark_done_aria": "Marchează mementoul {type} ca finalizat", + "notifications_mark_all_read_title": "Marchează-le pe toate ca citite", + "notifications_mark_all_read_aria": "Marchează toate notificările ca citite", + "notifications_overdue_days": "Restant de {days} zi{plural}", + "notifications_due_today": "Scade astăzi", + "notifications_due_tomorrow": "Scade mâine", + "notifications_due_in_days": "Scade în {days} zile", + "alerts_status_expired": "Expirat", + "alerts_status_expiring": "Expiră în curând", + "alerts_status_valid": "În regulă", + "alerts_status_missing": "Lipsește", + "notifications_expires": "Expiră", + "notifications_error_no_id": "Nu se poate actualiza mementoul fără un ID.", + "notifications_error_update_failed": "Actualizarea mementoului a eșuat.", + "notifications_success_marked_done": "Memento marcat ca finalizat.", + "notifications_severity_overdue": "Restant", + "notifications_severity_due_soon": "Scadență în curând", + "notifications_severity_upcoming": "Viitor", + "settings_custom_css_placeholder": "Adaugă codul CSS personalizat aici...", + "settings_features_intro": "Activează sau dezactivează funcționalități pentru a-ți personaliza experiența", + "feature_label_fuel": "Jurnal combustibil", + "feature_desc_fuel": "Urmărește și gestionează consumul de combustibil și istoricul alimentărilor", + "feature_label_maintenance": "Întreținere", + "feature_desc_maintenance": "Înregistrează și programează activitățile de întreținere a vehiculului", + "feature_label_pucc": "Poluare", + "feature_desc_pucc": "Gestionează înregistrările certificatelor de poluare (ITP / noxe)", + "feature_label_reminders": "Mementouri", + "feature_desc_reminders": "Setează și primește mementouri pentru evenimente importante ale vehiculului", + "feature_label_insurance": "Asigurare", + "feature_desc_insurance": "Gestionează detaliile și reînnoirea asigurărilor auto", + "feature_label_overview": "Prezentare generală", + "feature_desc_overview": "Afișează un panou de prezentare generală cu indicatorii cheie ai vehiculului", + "settings_error_date_format_invalid": "Formatul nu este valid", + "settings_error_timezone_invalid": "Valoarea fusului orar este nevalidă.", + "settings_error_currency_required": "Moneda este obligatorie", + "maintenance_form_attachment_label": "Atașament", + "maintenance_form_attachment_desc": "Încarcă chitanța sau documentul de întreținere", + "maintenance_form_date_label": "Dată", + "maintenance_form_date_desc": "Data întreținerii", + "maintenance_form_odometer_label": "Odometru", + "maintenance_form_odometer_desc": "Citirea curentă a odometrului vehiculului", + "maintenance_form_service_center_label": "Centru de service", + "maintenance_form_service_center_desc": "Numele centrului de service", + "maintenance_form_cost_label": "Cost", + "maintenance_form_cost_desc": "Costul întreținerii", + "maintenance_form_notes_label": "Note", + "maintenance_form_notes_desc": "Mai multe detalii", + "maintenance_form_notes_placeholder": "Adaugă mai multe detalii, dacă există...", + "maintenance_toast_saved": "Jurnalul de întreținere a fost salvat cu succes", + "maintenance_toast_updated": "Jurnalul de întreținere a fost actualizat cu succes", + "maintenance_toast_error_prefix": "Eroare la salvare: ", + "maintenance_form_error_fix": "Te rugăm să corectezi erorile din formular înainte de trimitere.", + "maintenance_list_empty": "Nu s-au găsit jurnale de întreținere pentru acest vehicul.", + "maintenance_col_service_center": "Centru de service", + "maintenance_menu_open": "Deschide meniul", + "maintenance_menu_edit": "Editează", + "maintenance_menu_delete": "Șterge", + "maintenance_menu_sheet_title": "Actualizează jurnalul de întreținere", + "maintenance_tab_title": "Istoric întreținere", + "maintenance_add_action": "Adaugă jurnal întreținere", + "maintenance_export_pdf": "Exportă PDF", + "maintenance_delete_success": "Jurnalul de întreținere a fost șters.", + "maintenance_delete_error": "A apărut o eroare la ștergerea jurnalului de întreținere.", + "insurance_form_provider_label": "Furnizor de asigurări", + "insurance_form_provider_desc": "Numele companiei de asigurări", + "insurance_form_policy_number_label": "Număr poliță de asigurare", + "insurance_form_policy_number_desc": "Numărul poliței din documentul de asigurare", + "insurance_form_start_date_label": "Dată început asigurare", + "insurance_form_start_date_desc": "Data la care începe acoperirea", + "insurance_form_recurrence_type_label": "Cum ar trebui să se reînnoiască această asigurare?", + "insurance_form_recurrence_type_desc": "Tipul de reînnoire pentru această asigurare", + "insurance_form_recurrence_interval_label": "Frecvență reînnoire", + "insurance_form_recurrence_interval_desc": "Cât de des se reînnoiește asigurarea", + "insurance_form_end_date_label": "Dată sfârșit asigurare", + "insurance_form_end_date_desc": "Data la care expiră acoperirea", + "insurance_form_cost_label": "Cost asigurare", + "insurance_form_cost_desc": "Costul anual sau pentru perioada poliței", + "insurance_form_notes_label": "Note suplimentare", + "insurance_form_notes_desc": "Orice informații suplimentare despre poliță", + "insurance_form_notes_placeholder": "Adaugă mai multe detalii despre asigurare...", + "insurance_form_attachment_label": "Document poliță", + "insurance_form_attachment_desc": "Încarcă documentul poliței", + "insurance_form_error_fix": "Te rugăm să corectezi erorile din formular înainte de trimitere.", + "insurance_toast_saved": "Asigurarea a fost salvată cu succes", + "insurance_toast_updated": "Asigurarea a fost actualizată cu succes", + "insurance_toast_error_prefix": "Eroare la salvare: ", + "insurance_list_empty": "Nu s-a găsit nicio asigurare pentru acest vehicul.", + "insurance_col_policy_number": "Număr poliță", + "insurance_col_cost": "Cost", + "insurance_col_start_date": "Dată început", + "insurance_col_end_date": "Dată sfârșit", + "insurance_col_next_due": "Următoarea scadență", + "insurance_col_recurrence": "Recurență", + "insurance_col_notes": "Note", + "insurance_menu_open": "Deschide meniul", + "insurance_menu_edit": "Editează", + "insurance_menu_delete": "Șterge", + "insurance_menu_sheet_title": "Actualizează asigurarea", + "insurance_tab_title": "Detalii asigurare", + "insurance_add_action": "Adaugă asigurare", + "insurance_delete_success": "Asigurarea a fost ștearsă.", + "insurance_delete_error": "A apărut o eroare la ștergerea asigurării.", + "insurance_col_view_document": "Vezi documentul", + "pollution_form_certificate_number_label": "Număr certificat", + "pollution_form_certificate_number_desc": "Numărul certificatului de poluare", + "pollution_form_issue_date_label": "Dată emitere", + "pollution_form_issue_date_desc": "Data emiterii certificatului", + "pollution_form_recurrence_type_label": "Cum ar trebui să se reînnoiască acest certificat?", + "pollution_form_recurrence_type_desc": "Tipul de reînnoire pentru acest certificat", + "pollution_form_recurrence_interval_label": "Frecvență reînnoire", + "pollution_form_recurrence_interval_desc": "Cât de des se reînnoiește certificatul", + "pollution_form_expiry_date_label": "Dată expirare", + "pollution_form_expiry_date_desc": "Data de expirare a certificatului (ITP)", + "pollution_form_testing_center_label": "Centru de testare", + "pollution_form_testing_center_desc": "Numele centrului de testare", + "pollution_form_notes_label": "Note suplimentare", + "pollution_form_notes_desc": "Orice informații suplimentare", + "pollution_form_notes_placeholder": "Adaugă note suplimentare...", + "pollution_form_attachment_label": "Document certificat", + "pollution_form_attachment_desc": "Încarcă documentul certificatului", + "pollution_form_error_fix": "Te rugăm să corectezi erorile din formular înainte de trimitere.", + "pollution_toast_saved": "Certificatul de poluare a fost salvat cu succes", + "pollution_toast_updated": "Certificatul de poluare a fost actualizat cu succes", + "pollution_toast_error_prefix": "Eroare la salvare: ", + "pollution_list_empty": "Nu există certificate de poluare pentru acest vehicul.", + "pollution_col_certificate_number": "Număr certificat", + "pollution_col_issue_date": "Dată emitere", + "pollution_col_expiry_date": "Dată expirare", + "pollution_col_next_due": "Următoarea scadență", + "pollution_col_testing_center": "Centru de testare", + "pollution_col_notes": "Note", + "pollution_col_view_certificate": "Vezi certificatul", + "pollution_col_recurrence": "Recurență", + "pollution_menu_open": "Deschide meniul", + "pollution_menu_edit": "Editează", + "pollution_menu_delete": "Șterge", + "pollution_menu_sheet_title": "Actualizează certificatul de poluare", + "pollution_tab_title": "Detalii certificat poluare", + "pollution_add_action": "Adaugă certificat poluare", + "pollution_delete_success": "Certificatul de poluare a fost șters.", + "pollution_delete_error": "A apărut o eroare la ștergerea certificatului de poluare.", + "reminder_form_due_date_label": "Dată scadență", + "reminder_form_due_date_desc": "Când ar trebui să se declanșeze acest memento?", + "reminder_form_type_label": "Tip", + "reminder_form_type_desc": "Alege tipul de memento", + "reminder_form_schedule_label": "Programare memento", + "reminder_form_schedule_desc": "Când dorești să te anunțăm?", + "reminder_form_recurrence_type_label": "Recurență", + "reminder_form_recurrence_type_desc": "Ar trebui să se repete acest memento?", + "reminder_form_recurrence_interval_label": "Repetă la fiecare", + "reminder_form_recurrence_interval_desc": "Frecvența recurenței", + "reminder_form_recurrence_end_date_label": "Dată sfârșit", + "reminder_form_recurrence_end_date_desc": "Când ar trebui să se oprească repetarea? (opțional)", + "reminder_form_note_label": "Notă", + "reminder_form_note_desc": "Adaugă mai mult context", + "reminder_form_note_placeholder": "Detaliază ce anume necesită atenție", + "reminder_form_is_completed_label": "Marchează ca finalizat", + "reminder_toast_created": "Memento creat cu succes.", + "reminder_toast_updated": "Memento actualizat cu succes", + "reminder_toast_error_prefix": "Eroare la salvare: ", + "reminder_list_empty": "Nu există mementouri încă. Creează unul pentru a fi în avans cu viitoarele reînnoiri.", + "reminder_list_select_vehicle": "Selectează un vehicul pentru a vedea mementourile.", + "reminder_list_select_hint": "Alege un vehicul de mai sus pentru a-i încărca mementourile viitoare.", + "reminder_col_due_date": "Dată scadență", + "reminder_col_reminder_schedule": "Programare memento", + "reminder_col_recurrence": "Recurență", + "reminder_col_note": "Note", + "reminder_menu_toggle_done": "Marchează ca {status}", + "reminder_menu_toggle_done_done": "Marchează ca în așteptare", + "reminder_menu_toggle_done_pending": "Marchează ca finalizat", + "reminder_menu_edit": "Editează", + "reminder_menu_delete": "Șterge", + "reminder_menu_open": "Deschide meniul", + "reminder_menu_sheet_title": "Actualizează mementoul", + "reminder_tab_title": "Mementouri", + "reminder_add_action": "Adaugă memento", + "reminder_delete_success": "Memento șters.", + "reminder_delete_error": "A apărut o eroare la ștergerea mementoului.", + "reminder_status_completed": "Finalizat", + "reminder_status_pending": "În așteptare", + "reminder_status_overdue": "Restant", + "reminder_status_error": "Nu s-a putut actualiza starea mementoului.", + "reminder_toast_error_fallback": "Salvarea mementoului a eșuat.", + "settings_sheet_title": "Setări", + "feature_label_pollution": "Poluare", + "feature_desc_pollution": "Gestionează înregistrările certificatelor de poluare", + "profile_menu_item": "Profil", + "profile_sheet_title": "Profil", + "profile_sheet_desc": "Actualizează-ți numele de utilizator și parola", + "profile_username": "Nume utilizator", + "profile_username_desc": "Numele tău afișat", + "profile_password_hint": "Lasă câmpurile de parolă goale pentru a păstra parola curentă", + "profile_current_password": "Parolă curentă", + "profile_current_password_desc": "Necesară pentru a schimba parola", + "profile_new_password": "Parolă nouă", + "profile_new_password_desc": "Minimum 6 caractere", + "profile_confirm_password": "Confirmă parola", + "profile_confirm_password_desc": "Reintrodu parola nouă", + "profile_update_button": "Actualizează profilul", + "tools_menu": "Instrumente", + "data_export_import_menu_item": "Export/Import date", + "data_export_import_sheet_title": "Export/Import date", + "data_export_import_sheet_desc": "Exportă sau importă baza ta de date cu criptare opțională", + "logout_menu_item": "Deconectare", + "custom_fields_label": "Câmpuri personalizate", + "custom_fields_add_button": "Adaugă câmp", + "custom_fields_name_placeholder": "Nume câmp", + "custom_fields_value_placeholder": "Valoare câmp", + "custom_fields_remove_aria": "Elimină câmpul", + "custom_fields_empty_message": "Nu au fost adăugate câmpuri personalizate. Apasă pe „Adaugă câmp” pentru a începe.", + "vehicle_details_vin": "VIN", + "vehicle_details_not_specified": "Nespecificat", + "vehicle_details_section_title": "Detalii", + "vehicle_details_license_plate": "Număr de înmatriculare", + "vehicle_details_fuel_type": "Tip combustibil", + "vehicle_details_odometer": "Odometru", + "vehicle_details_not_recorded": "Neînregistrat", + "vehicle_details_color": "Culoare", + "vehicle_details_year": "An", + "fuel_type_diesel": "Diesel", + "fuel_type_petrol": "Benzină", + "fuel_type_electric": "Electric", + "fuel_type_lpg": "GPL", + "fuel_type_cng": "GNC", + "fuel_type_ev": "Electric (EV)", + "reminder_schedule_same_day": "La data scadenței", + "reminder_schedule_one_day_before": "Cu 1 zi înainte", + "reminder_schedule_three_days_before": "Cu 3 zile înainte", + "reminder_schedule_one_week_before": "Cu 1 săptămână înainte", + "reminder_schedule_one_month_before": "Cu 1 lună înainte", + "recurrence_type_none": "Fără repetare", + "recurrence_type_daily": "Zilnic", + "recurrence_type_weekly": "Săptămânal", + "recurrence_type_monthly": "Lunar", + "recurrence_type_yearly": "Anual", + "recurrence_every": "la fiecare", + "recurrence_renew_every": "Reînnoiește la fiecare", + "recurrence_interval_days": "zile", + "recurrence_interval_weeks": "săptămâni", + "recurrence_interval_months": "luni", + "recurrence_interval_years": "ani", + "recurrence_until": "Până la", + "insurance_recurrence_type_fixed": "Dată de încheiere fixă", + "insurance_recurrence_type_yearly": "Se reînnoiește anual", + "insurance_recurrence_type_monthly": "Se reînnoiește lunar", + "insurance_recurrence_type_no_end": "Fără dată de sfârșit", + "pollution_recurrence_type_fixed": "Dată de încheiere fixă", + "pollution_recurrence_type_yearly": "Se reînnoiește anual", + "pollution_recurrence_type_monthly": "Se reînnoiește lunar", + "pollution_recurrence_type_no_end": "Fără dată de sfârșit", + "file_drop_existing_note": "Atașament existent (Fă clic pentru a vizualiza)", + "fuel_import_step_1_title": "Pasul 1: Încarcă fișierul CSV", + "fuel_import_step_1_desc": "Selectează un fișier text delimitat care conține datele jurnalului de combustibil pentru a începe procesul de import.", + "fuel_import_drop_placeholder": "Trage orice fișier text delimitat aici sau fă clic pentru a răsfoi", + "fuel_import_headers_checkbox": "Primul rând conține anteturi", + "fuel_import_delimiter_title": "Delimitator", + "fuel_import_delimiter_desc": "Alege caracterul care separă câmpurile", + "fuel_import_date_format_title": "Format dată", + "fuel_import_date_format_desc": "Specifică formatul utilizat pentru date în fișierul tău CSV.", + "fuel_import_error_no_headers": "Nu s-au detectat anteturi. Actualizează csv.helper.ts pentru a returna anteturi.", + "fuel_import_step_2_title": "Pasul 2: Asociază coloanele CSV", + "fuel_import_step_2_desc": "Asociază coloanele din fișierul tău CSV cu câmpurile corespunzătoare din jurnalul de combustibil. Câmpurile obligatorii sunt marcate cu *.", + "fuel_import_step_3_title": "Pasul 3: Previzualizare și Import", + "fuel_import_step_3_desc": "Verifică o previzualizare a datelor care urmează să fie importate.", + "fuel_import_no_preview": "Nu există date pentru previzualizare încă. Implementează parsarea în csv.helper.ts pentru a popula rândurile.", + "fuel_import_success": "S-au importat cu succes {count} jurnale de combustibil.", + "fuel_import_failed_count": "Importate: {imported}, eșuate: {failed}", + "fuel_import_error_generic": "Importul jurnalului de combustibil a eșuat.", + "fuel_import_vehicle_label": "Vehicul:", + "fuel_import_delimiter_comma": "Virgulă ( , )", + "fuel_import_delimiter_semicolon": "Punct și virgulă ( ; )", + "fuel_import_delimiter_tab": "Tab ( \\t )", + "fuel_import_delimiter_pipe": "Bară verticală ( | )", + "fuel_import_delimiter_custom": "Personalizat", + "fuel_import_date_error": "Unele rânduri au date nevalide pentru formatul \"{format}\"", + "fuel_import_date_format_placeholder": "de ex., LL/ZZ/AAAA", + "fuel_import_date_invalid": "Dată nevalidă", + "fuel_import_no_vehicle": "Niciun vehicul selectat", + "fuel_import_col_date_hint": "Data alimentării cu combustibil", + "fuel_import_col_odometer_hint": "Citirea odometrului în momentul alimentării", + "fuel_import_col_fuel_hint": "Volumul sau energia încărcată", + "fuel_import_col_cost_hint": "Costul total pentru această înregistrare", + "fuel_import_col_filled_hint": "Este o alimentare/încărcare completă?", + "fuel_import_col_missed_hint": "A fost omisă înregistrarea anterioară?", + "fuel_import_col_notes_hint": "Orice note suplimentare", + "autocomplete_placeholder": "Scrie sau selectează...", + "autocomplete_loading": "Se încarcă sugestiile...", + "autocomplete_no_results": "Nu s-au găsit sugestii. Poți introduce o valoare nouă.", + "input_date_placeholder": "Alege o dată", + "loading_default_message": "Se încarcă...", + "dropzone_placeholder_image": "Fă clic sau trage imaginea aici pentru a o încărca", + "dropzone_placeholder_attachment": "Trage fișierul aici sau fă clic pentru a-l selecta", + "dropzone_placeholder_default": "Fă clic sau trage fișierele aici pentru a le încărca", + "dropzone_error_single_file": "Te rugăm să încarci un singur fișier.", + "dropzone_error_file_size": "Dimensiunea fișierului depășește limita maximă de {size}.", + "dropzone_unknown_file": "Fișier necunoscut", + "dropzone_uploading": "Se încarcă...", + "dropzone_supports": "Suportă: {types}", + "dropzone_max_size": "Dimensiune max: {size}", + "dropzone_error_file_type": "Tipul de fișier nu este permis", + "dropzone_hint_accept_limit": "{types} până la {size}", + "vehicle_details_color_aria": "Culoare", + "vehicle_details_close_aria": "Închide", + "attachment_link_view_title": "Vezi atașamentul", + "file_preview_not_available": "Previzualizarea nu este disponibilă", + "file_preview_download_hint": "Acest tip de fișier nu poate fi previzualizat direct. Te rugăm să îl descarci pentru a-l vizualiza.", + "file_preview_download_button": "Descarcă fișierul", + "file_preview_aria_download": "Descarcă", + "file_preview_aria_close": "Închide", + "theme_toggle_label": "Comută tema", + "fuel_log_edit": "Editează", + "fuel_log_delete": "Șterge", + "fuel_log_menu_open": "Deschide meniul", + "fuel_log_delete_success": "Jurnalul de combustibil a fost șters", + "fuel_log_menu_sheet_title": "Actualizează jurnalul de combustibil", + "fuel_log_delete_error": "A apărut o eroare la ștergerea jurnalului de combustibil.", + "color_picker_label": "Alege o culoare", + "reminder_type_maintenance": "Întreținere", + "reminder_type_insurance": "Reînnoire asigurare", + "reminder_type_pollution": "Emisii / ITP", + "reminder_type_registration": "Înmatriculare / Taxe auto", + "reminder_type_inspection": "Inspecție", + "reminder_type_custom": "Personalizat", + "alert_type_insurance": "Asigurare", + "alert_type_pucc": "Certificat poluare", + "alert_status_expired_ago": "{label} a expirat acum {days} zile", + "alert_status_expires_in": "{label} expiră în {days} zile", + "alert_status_valid_for": "{label} este valabil încă {days} zile", + "alert_insurance_active_no_end": "Asigurarea este activă fără o dată de încheiere", + "alert_pucc_active_no_end": "Certificatul de poluare este activ fără o dată de încheiere", + "alert_record_not_found": "Înregistrarea {label} nu a fost găsită. Adaugă detalii pentru a rămâne în conformitate.", + "settings_error_format_not_valid": "Formatul nu este valid", + "common_kilogram_unit": "Kilogram (kg)", + "common_pound_unit": "Livră / Pound (lb)", + "settings_section_fuel_types": "Tipuri de combustibil", + "settings_section_fuel_types_desc": "Alege unitatea de măsură pentru fiecare tip de combustibil.", + "fuel_type_petrol_diesel": "Benzină/Motorină", + "theme_slate": "Ardezie (Slate)", + "theme_stone": "Piatră (Stone)", + "theme_red": "Roșu", + "theme_rose": "Roz trandafir", + "theme_blue": "Albastru", + "theme_green": "Verde", + "theme_purple": "Violet", + "theme_orange": "Portocaliu", + "theme_yellow": "Galben", + "theme_teal": "Opal (Teal)", + "theme_indigo": "Indigo", + "theme_pink": "Roz", + "settings_tab_notifications": "Notificări", + "settings_personalization_desc": "Personalizează-ți experiența cu teme, limbi și formate.", + "settings_section_general": "General", + "settings_section_general_desc": "Personalizează aspectul, localizarea și formatele de afișare", + "settings_units_desc": "Configurează unitățile de măsură pentru distanță, volum și tipuri de combustibil.", + "settings_section_units": "Unități", + "settings_section_units_desc": "Alege unitățile preferate pentru distanță, consum și tipuri de combustibil", + "settings_section_feature_flags": "Module aplicație (Feature Flags)", + "settings_section_feature_flags_desc": "Activează sau dezactivează modulele majore ale aplicației", + "settings_notifications_desc": "Configurează abonamentele la furnizori și ora zilnică de procesare pentru livrarea programată.", + "settings_error_fix_errors": "Te rugăm să corectezi următoarele erori:", + "settings_fuel_types_label": "Tipuri de combustibil", + "settings_fuel_types_desc": "Alege unitatea de măsură pentru fiecare tip de combustibil.", + "fuel_type_label_petrol_diesel": "Benzină/Motorină", + "fuel_type_label_lpg": "GPL", + "fuel_type_label_cng": "GNC", + "notif_scheduled_delivery": "Livrare programată", + "notif_scheduled_delivery_desc": "Notificările din aplicație rămân în timp real. Această programare controlează doar livrarea prin furnizori externi.", + "notif_processing_schedule": "Program de procesare", + "notif_processing_schedule_desc": "Rulează livrarea notificărilor prin furnizor conform unui program.", + "notif_send_now": "Trimite acum", + "notif_providers": "Furnizori", + "notif_providers_desc": "Creează, editează, testează și activează furnizorii de notificări.", + "notif_providers_channels_info": "Fiecare furnizor se poate abona la canalele de Mementouri, Alerte și Informații.", + "notif_add_provider": "Adaugă furnizor", + "notif_empty_title": "Nu a fost configurat niciun furnizor de notificări", + "notif_empty_desc": "Adaugă un furnizor pentru a primi notificări programate de tip Memento, Alertă sau Informație.", + "notif_load_failed": "Încărcarea furnizorilor de notificări a eșuat", + "notif_select_provider_type": "Te rugăm să selectezi un tip de furnizor", + "notif_select_channel": "Selectează cel puțin un canal de notificări", + "notif_provider_updated": "Furnizor actualizat cu succes", + "notif_provider_created": "Furnizor creat cu succes", + "notif_provider_deleted": "Furnizor șters cu succes", + "notif_provider_delete_failed": "Ștergerea furnizorului a eșuat", + "notif_channel_reminder": "Memento", + "notif_channel_reminder_desc": "Notificări de tip dată scadentă și memento", + "notif_channel_alert": "Alertă", + "notif_channel_alert_desc": "Elemente urgente sau care expiră și necesită atenție imediata", + "notif_channel_information": "Informație", + "notif_channel_information_desc": "Actualizări și informații cu caracter general", + "notif_provider_enabled": "Activat", + "notif_provider_test": "Testează furnizorul", + "notif_provider_edit": "Editează furnizorul", + "notif_provider_delete": "Șterge furnizorul", + "notif_dialog_add_title": "Adaugă furnizor de notificări", + "notif_dialog_edit_title": "Editează furnizor de notificări", + "notif_dialog_desc": "Alege un tip de furnizor, configurează-i destinația și abonează-l la canalele dorite. Furnizorii sunt activați implicit și pot fi dezactivați din cardurile lor.", + "notif_provider_name": "Nume furnizor", + "notif_provider_name_placeholder": "Email rezumat zilnic", + "notif_provider_type": "Tip furnizor", + "notif_provider_type_email": "Email (SMTP)", + "notif_provider_type_webhook": "Webhook", + "notif_provider_type_gotify": "Gotify", + "notif_provider_type_select": "Selectează tipul de furnizor", + "notif_dialog_cancel": "Anulează", + "notif_dialog_create": "Creează furnizor", + "notif_dialog_update": "Actualizează furnizor", + "notif_channel_subscriptions": "Abonamente la canale", + "notif_channel_subscriptions_desc": "Alege ce canale de notificări ar trebui să primească acest furnizor.", + "notif_test_title": "Testează furnizorul", + "notif_test_success": "Notificarea de test a fost trimisă cu succes", + "notif_test_email_label": "Email de test", + "notif_test_email_placeholder": "destinatar@example.com", + "notif_test_email_desc": "Destinatar opțional pentru a suprascrie adresa în cazul acestui mesaj de test.", + "notif_test_message_label": "Mesaj de test", + "notif_test_message_placeholder": "Aceasta este o notificare de test de la Tracktor", + "notif_test_send": "Trimite test", + "notif_cron_presets": "Preconfigurări", + "notif_cron_every_minute": "În fiecare minut", + "notif_cron_every_5_min": "La fiecare 5 minute", + "notif_cron_every_15_min": "La fiecare 15 minute", + "notif_cron_every_30_min": "La fiecare 30 de minute", + "notif_cron_every_hour": "În fiecare oră", + "notif_cron_every_2_hours": "La fiecare 2 ore", + "notif_cron_every_6_hours": "La fiecare 6 ore", + "notif_cron_every_12_hours": "La fiecare 12 ore", + "notif_cron_daily_midnight": "Zilnic la miezul nopții", + "notif_cron_daily_2am": "Zilnic la ora 2:00 AM", + "notif_cron_daily_8am": "Zilnic la ora 8:00 AM", + "notif_cron_daily_noon": "Zilnic la prânz (12:00 PM)", + "notif_cron_weekly_monday": "Săptămânal (Luni)", + "notif_cron_monthly_1st": "Lunar (pe data de 1)", + "notif_cron_expression_required": "Expresia este obligatorie", + "notif_cron_must_have_5_parts": "Trebuie să conțină 5 părți", + "notif_cron_invalid_chars": "Caractere nevalide", + "notif_cron_valid": "Expresie validă", + "notif_cron_invalid": "Expresie nevalidă", + "notif_cron_custom": "Program personalizat", + "notif_email_smtp_settings": "Setări server SMTP", + "notif_email_host": "Gazdă (Host)", + "notif_email_port": "Port", + "notif_email_use_ssl": "Utilizează SSL/TLS", + "notif_email_use_ssl_desc": "Activează conexiunea securizată", + "notif_email_auth": "Autentificare", + "notif_email_username": "Utilizator / Email", + "notif_email_password": "Parolă", + "notif_email_password_keep": "Parolă (lasă gol pentru a o păstra pe cea curentă)", + "notif_email_sender_info": "Informații expeditor/destinatar", + "notif_email_from": "De la (Adresă Email)", + "notif_email_from_name": "Nume expeditor (Opțional)", + "notif_email_from_name_placeholder": "Notificări Tracktor", + "notif_email_recipient": "Email destinatar", + "notif_email_recipient_desc": "Adresa de email a destinatarului", + "notif_webhook_config": "Configurare Webhook", + "notif_webhook_url": "URL Webhook", + "notif_webhook_method": "Metodă HTTP", + "notif_webhook_headers": "Anteturi personalizate (JSON)", + "notif_webhook_headers_desc": "Anteturi suplimentare de inclus în cererea webhook-ului", + "notif_webhook_auth_type": "Tip autentificare", + "notif_webhook_auth_none": "Niciuna", + "notif_webhook_auth_basic": "Autentificare de bază (Basic Auth)", + "notif_webhook_auth_bearer": "Token Bearer", + "notif_webhook_auth_apikey": "Cheie API (API Key)", + "notif_webhook_username": "Nume utilizator", + "notif_webhook_apikey_header": "Nume antet cheie API", + "notif_gotify_config": "Configurare server Gotify", + "notif_gotify_url": "URL Server", + "notif_gotify_url_desc": "URL-ul instanței serverului tău Gotify", + "notif_gotify_token": "Token aplicație", + "notif_gotify_token_keep": "Token aplicație (lasă gol pentru a-l păstra pe cel curent)", + "notif_gotify_token_desc": "Tokenul de aplicație din aplicația ta Gotify (nu tokenul de client)", + "notif_gotify_priority": "Prioritate (0-10)", + "notif_gotify_priority_desc": "Nivelul de prioritate al mesajului. Prioritate mai mare = notificare mai proeminentă", + "notif_cleared_success_one": "S-a șters 1 notificare citită", + "notif_cleared_success_other": "S-au șters {count} notificări citite", + "notif_cleared_partial": "S-au șters {success}, {failed} au eșuat", + "notif_clear_failed": "Ștergerea notificărilor citite a eșuat", + "notif_all_marked_read": "Toate notificările au fost marcate ca citite", + "notif_mark_read_failed": "Marcarea notificărilor ca citite a eșuat", + "notif_button_mark_all_read": "Marchează tot ca citit", + "notif_button_clear_read": "Șterge cele citite", + "notif_button_clear_all_read_title": "Șterge toate notificările citite", + "notif_status_read": "Citit", + "notif_status_unread": "Necitit", + "notif_due_prefix": "Scadență: {date}", + "header_home_aria": "Mergi la pagina principală", + "header_settings_aria": "Deschide setările", + "header_account_aria": "Meniu cont", + "header_account_title": "Cont", + "notif_save_provider_failed": "Salvarea furnizorului a eșuat", + "notif_update_provider_failed": "Actualizarea furnizorului a eșuat", + "notif_send_all_failed": "Trimiterea notificărilor a eșuat", + "notif_send_all_success": "S-au trimis {notifCount} notificări către {successCount}/{providerCount} furnizori activați", + "notif_confirm_delete": "Sigur dorești să ștergi „{name}”?", + "notif_webhook_bearer_keep": "Token Bearer (lasă gol pentru a-l păstra pe cel curent)", + "notif_webhook_apikey_keep": "Cheie API (lasă gol pentru a o păstra pe cea curentă)", + "notif_test_failed": "Trimiterea notificării de test a eșuat", + "notif_test_send_desc": "Trimite o notificare de test folosind {name}", + "notif_cron_every_n_minutes": "La fiecare {n} minute", + "notif_cron_hourly_at_minute": "În fiecare oră la minutul {n}", + "notif_cron_daily_at": "Zilnic la {time}" +} diff --git a/project.inlang/settings.json b/i18n/project.inlang/settings.json similarity index 79% rename from project.inlang/settings.json rename to i18n/project.inlang/settings.json index 662d1d0b..aed47185 100644 --- a/project.inlang/settings.json +++ b/i18n/project.inlang/settings.json @@ -8,7 +8,5 @@ "pathPattern": "./messages/{locale}.json" }, "baseLocale": "en", - "locales": ["en", "ar", "hi", "es", "fr", "de", "it", "hu", "fi"], - "sourceLanguageTag": "en", - "languageTags": ["en", "ar", "hi", "es", "fr", "de", "it", "hu", "fi"] + "locales": ["en", "ar", "hi", "es", "fr", "de", "it", "hu", "fi", "ro"] } diff --git a/package.json b/package.json index c093f8aa..8705596a 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "tracktor", "private": true, - "version": "1.4.1", + "version": "2.0.0", "type": "module", "scripts": { "dev": "vite dev --host", + "local": "vite dev", "build": "vite build", "preview": "vite preview --host", "test": "vitest --run", @@ -17,88 +18,77 @@ "clean": "rm -rf build .svelte-kit *.db uploads logs coverage node_modules", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", - "db:seed": "tsx scripts/seed.ts", "start": "node build" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@inlang/paraglide-js": "^2.15.0", - "@internationalized/date": "^3.12.0", - "@lucide/svelte": "^0.577.0", - "@sveltejs/adapter-node": "^5.5.4", - "@sveltejs/kit": "^2.55.0", - "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@faker-js/faker": "^10.5.0", + "@inlang/paraglide-js": "^2.23.2", + "@internationalized/date": "^3.12.3", + "@lucide/svelte": "^1.30.0", + "@sveltejs/adapter-node": "^5.5.7", + "@sveltejs/kit": "^2.70.2", + "@sveltejs/vite-plugin-svelte": "^7.2.0", "@tailwindcss/forms": "^0.5.11", - "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.2.2", + "@tailwindcss/typography": "^0.5.20", + "@tailwindcss/vite": "^4.3.3", "@tanstack/table-core": "^8.21.3", - "@testing-library/svelte": "^5.3.1", - "@types/node": "^25.5.0", + "@types/node": "^26.1.2", "@types/node-cron": "^3.0.11", - "@types/nodemailer": "^7.0.11", - "@types/supertest": "^7.2.0", - "@typescript-eslint/eslint-plugin": "^8.57.1", - "@typescript-eslint/parser": "^8.57.1", - "@vitest/coverage-v8": "^4.1.0", - "bits-ui": "^2.16.3", + "@types/nodemailer": "^8.0.1", + "@types/pdfkit": "^0.17.6", + "@typescript-eslint/eslint-plugin": "^8.66.0", + "@typescript-eslint/parser": "^8.66.0", + "@vitest/coverage-v8": "^4.1.10", + "bits-ui": "^2.18.1", "clsx": "^2.1.1", - "concurrently": "^9.2.1", "currency-codes": "^2.2.0", "d3-array": "^3.2.4", "d3-scale": "^4.0.2", "d3-shape": "^3.2.0", - "date-fns": "^4.1.0", + "date-fns": "^4.4.0", "date-fns-tz": "^3.2.0", "drizzle-kit": "^0.31.10", - "drizzle-orm": "^0.45.1", - "eslint": "^10.0.3", + "drizzle-orm": "^0.45.2", + "eslint": "^10.8.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-svelte": "^3.15.2", + "eslint-plugin-svelte": "^3.22.0", "eslint-plugin-unused-imports": "^4.4.1", "formsnap": "^2.0.1", - "globals": "^17.4.0", - "jsdom": "^29.0.0", - "layerchart": "2.0.0-next.27", - "prettier": "^3.8.1", - "prettier-plugin-svelte": "^3.5.1", - "prettier-plugin-tailwindcss": "^0.7.2", - "svelte": "^5.54.0", - "svelte-awesome-color-picker": "^4.1.1", - "svelte-check": "^4.4.5", - "svelte-eslint-parser": "^1.6.0", - "svelte-sonner": "^1.1.0", - "sveltekit-superforms": "^2.30.0", - "tailwind-merge": "^3.5.0", - "tailwind-variants": "^3.2.2", - "tailwindcss": "^4.2.2", - "tsx": "^4.21.0", + "globals": "^17.9.0", + "layerchart": "2.1.0", + "prettier": "^3.9.6", + "prettier-plugin-svelte": "^4.1.1", + "prettier-plugin-tailwindcss": "^0.8.1", + "svelte": "^5.56.8", + "svelte-awesome-color-picker": "^4.1.3", + "svelte-check": "^4.7.5", + "svelte-eslint-parser": "^1.8.0", + "svelte-sonner": "^1.1.1", + "sveltekit-superforms": "^2.30.2", + "tailwind-merge": "^3.6.0", + "tailwind-variants": "^3.3.1", + "tailwindcss": "^4.3.3", "tw-animate-css": "^1.4.0", - "typescript": "^5.9.3", - "vite": "^8.0.1", - "vitest": "^4.1.0" + "typescript": "^6.0.3", + "vite": "^8.2.1", + "vitest": "^4.1.10" }, "dependencies": { - "@faker-js/faker": "^10.3.0", - "@libsql/client": "^0.17.0", + "@libsql/client": "^0.17.4", "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0", "@types/bcrypt": "^6.0.0", - "@types/cors": "^2.8.19", "@types/d3-array": "^3.2.2", "@types/d3-scale": "^4.0.9", "@types/d3-shape": "^3.1.8", - "@types/express": "^5.0.6", - "@types/multer": "^2.1.0", "bcrypt": "^6.0.0", - "cors": "^2.8.6", - "csv-parse": "^6.2.0", - "dotenv": "^17.3.1", - "helmet": "^8.1.0", + "csv-parse": "^7.0.2", "mode-watcher": "^1.1.0", - "multer": "^2.1.1", - "node-cron": "^4.2.1", - "nodemailer": "^8.0.3", - "winston": "^3.19.0", - "zod": "^4.3.6" + "node-cron": "^4.6.0", + "nodemailer": "^9.0.5", + "pdfkit": "^0.19.1", + "svelte-dnd-action": "^0.9.78", + "zod": "^4.4.3" } } diff --git a/pnpm b/pnpm deleted file mode 100644 index 7399c645..00000000 --- a/pnpm +++ /dev/null @@ -1 +0,0 @@ -zsh:1: command not found: pntml:parameter diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cc83e9d..f4a28bf2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,9 @@ importers: .: dependencies: - '@faker-js/faker': - specifier: ^10.3.0 - version: 10.3.0 '@libsql/client': - specifier: ^0.17.0 - version: 0.17.0 + specifier: ^0.17.4 + version: 0.17.4 '@oslojs/crypto': specifier: ^1.0.1 version: 1.0.1 @@ -23,9 +20,6 @@ importers: '@types/bcrypt': specifier: ^6.0.0 version: 6.0.0 - '@types/cors': - specifier: ^2.8.19 - version: 2.8.19 '@types/d3-array': specifier: ^3.2.2 version: 3.2.2 @@ -35,112 +29,94 @@ importers: '@types/d3-shape': specifier: ^3.1.8 version: 3.1.8 - '@types/express': - specifier: ^5.0.6 - version: 5.0.6 - '@types/multer': - specifier: ^2.1.0 - version: 2.1.0 bcrypt: specifier: ^6.0.0 version: 6.0.0 - cors: - specifier: ^2.8.6 - version: 2.8.6 csv-parse: - specifier: ^6.2.0 - version: 6.2.0 - dotenv: - specifier: ^17.3.1 - version: 17.3.1 - helmet: - specifier: ^8.1.0 - version: 8.1.0 + specifier: ^7.0.2 + version: 7.0.2 mode-watcher: specifier: ^1.1.0 - version: 1.1.0(svelte@5.54.0) - multer: - specifier: ^2.1.1 - version: 2.1.1 + version: 1.1.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) node-cron: - specifier: ^4.2.1 - version: 4.2.1 + specifier: ^4.6.0 + version: 4.6.0 nodemailer: - specifier: ^8.0.3 - version: 8.0.3 - winston: - specifier: ^3.19.0 - version: 3.19.0 + specifier: ^9.0.5 + version: 9.0.5 + pdfkit: + specifier: ^0.19.1 + version: 0.19.1 + svelte-dnd-action: + specifier: ^0.9.78 + version: 0.9.78(svelte@5.56.8(@typescript-eslint/types@8.66.0)) zod: - specifier: ^4.3.6 - version: 4.3.6 + specifier: ^4.4.3 + version: 4.4.3 devDependencies: '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.0.3(jiti@2.6.1)) + version: 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) + '@faker-js/faker': + specifier: ^10.5.0 + version: 10.5.0 '@inlang/paraglide-js': - specifier: ^2.15.0 - version: 2.15.0 + specifier: ^2.23.2 + version: 2.23.2(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)) '@internationalized/date': - specifier: ^3.12.0 - version: 3.12.0 + specifier: ^3.12.3 + version: 3.12.3 '@lucide/svelte': - specifier: ^0.577.0 - version: 0.577.0(svelte@5.54.0) + specifier: ^1.30.0 + version: 1.30.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) '@sveltejs/adapter-node': - specifier: ^5.5.4 - version: 5.5.4(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))) + specifier: ^5.5.7 + version: 5.5.7(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1))) '@sveltejs/kit': - specifier: ^2.55.0 - version: 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + specifier: ^2.70.2 + version: 2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)) '@sveltejs/vite-plugin-svelte': - specifier: ^7.0.0 - version: 7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + specifier: ^7.2.0 + version: 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)) '@tailwindcss/forms': specifier: ^0.5.11 - version: 0.5.11(tailwindcss@4.2.2) + version: 0.5.11(tailwindcss@4.3.3) '@tailwindcss/typography': - specifier: ^0.5.19 - version: 0.5.19(tailwindcss@4.2.2) + specifier: ^0.5.20 + version: 0.5.20(tailwindcss@4.3.3) '@tailwindcss/vite': - specifier: ^4.2.2 - version: 4.2.2(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + specifier: ^4.3.3 + version: 4.3.3(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)) '@tanstack/table-core': specifier: ^8.21.3 version: 8.21.3 - '@testing-library/svelte': - specifier: ^5.3.1 - version: 5.3.1(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))(vitest@4.1.0(@types/node@25.5.0)(jsdom@29.0.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))) '@types/node': - specifier: ^25.5.0 - version: 25.5.0 + specifier: ^26.1.2 + version: 26.1.2 '@types/node-cron': specifier: ^3.0.11 version: 3.0.11 '@types/nodemailer': - specifier: ^7.0.11 - version: 7.0.11 - '@types/supertest': - specifier: ^7.2.0 - version: 7.2.0 + specifier: ^8.0.1 + version: 8.0.1 + '@types/pdfkit': + specifier: ^0.17.6 + version: 0.17.6 '@typescript-eslint/eslint-plugin': - specifier: ^8.57.1 - version: 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.66.0 + version: 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@typescript-eslint/parser': - specifier: ^8.57.1 - version: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + specifier: ^8.66.0 + version: 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) '@vitest/coverage-v8': - specifier: ^4.1.0 - version: 4.1.0(vitest@4.1.0(@types/node@25.5.0)(jsdom@29.0.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))) + specifier: ^4.1.10 + version: 4.1.10(vitest@4.1.10) bits-ui: - specifier: ^2.16.3 - version: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0) + specifier: ^2.18.1 + version: 2.18.1(@internationalized/date@3.12.3)(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) clsx: specifier: ^2.1.1 version: 2.1.1 - concurrently: - specifier: ^9.2.1 - version: 9.2.1 currency-codes: specifier: ^2.2.0 version: 2.2.0 @@ -154,115 +130,102 @@ importers: specifier: ^3.2.0 version: 3.2.0 date-fns: - specifier: ^4.1.0 - version: 4.1.0 + specifier: ^4.4.0 + version: 4.4.0 date-fns-tz: specifier: ^3.2.0 - version: 3.2.0(date-fns@4.1.0) + version: 3.2.0(date-fns@4.4.0) drizzle-kit: specifier: ^0.31.10 version: 0.31.10 drizzle-orm: - specifier: ^0.45.1 - version: 0.45.1(@libsql/client@0.17.0)(kysely@0.27.6) + specifier: ^0.45.2 + version: 0.45.2(@libsql/client@0.17.4)(kysely@0.28.16) eslint: - specifier: ^10.0.3 - version: 10.0.3(jiti@2.6.1) + specifier: ^10.8.0 + version: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.0.3(jiti@2.6.1)) + version: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) eslint-plugin-svelte: - specifier: ^3.15.2 - version: 3.15.2(eslint@10.0.3(jiti@2.6.1))(svelte@5.54.0) + specifier: ^3.22.0 + version: 3.22.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) eslint-plugin-unused-imports: specifier: ^4.4.1 - version: 4.4.1(@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1)) + version: 4.4.1(@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) formsnap: specifier: ^2.0.1 - version: 2.0.1(svelte@5.54.0)(sveltekit-superforms@2.30.0(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@types/json-schema@7.0.15)(svelte@5.54.0)(typescript@5.9.3)) + version: 2.0.1(svelte@5.56.8(@typescript-eslint/types@8.66.0))(sveltekit-superforms@2.30.2(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(@types/json-schema@7.0.15)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)) globals: - specifier: ^17.4.0 - version: 17.4.0 - jsdom: - specifier: ^29.0.0 - version: 29.0.0 + specifier: ^17.9.0 + version: 17.9.0 layerchart: - specifier: 2.0.0-next.27 - version: 2.0.0-next.27(svelte@5.54.0) + specifier: 2.1.0 + version: 2.1.0(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(zod@4.4.3) prettier: - specifier: ^3.8.1 - version: 3.8.1 + specifier: ^3.9.6 + version: 3.9.6 prettier-plugin-svelte: - specifier: ^3.5.1 - version: 3.5.1(prettier@3.8.1)(svelte@5.54.0) + specifier: ^4.1.1 + version: 4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)) prettier-plugin-tailwindcss: - specifier: ^0.7.2 - version: 0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.54.0))(prettier@3.8.1) + specifier: ^0.8.1 + version: 0.8.1(prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)))(prettier@3.9.6) svelte: - specifier: ^5.54.0 - version: 5.54.0 + specifier: ^5.56.8 + version: 5.56.8(@typescript-eslint/types@8.66.0) svelte-awesome-color-picker: - specifier: ^4.1.1 - version: 4.1.1(svelte@5.54.0) + specifier: ^4.1.3 + version: 4.1.3(svelte@5.56.8(@typescript-eslint/types@8.66.0)) svelte-check: - specifier: ^4.4.5 - version: 4.4.5(picomatch@4.0.3)(svelte@5.54.0)(typescript@5.9.3) + specifier: ^4.7.5 + version: 4.7.5(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3) svelte-eslint-parser: - specifier: ^1.6.0 - version: 1.6.0(svelte@5.54.0) + specifier: ^1.8.0 + version: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) svelte-sonner: - specifier: ^1.1.0 - version: 1.1.0(svelte@5.54.0) + specifier: ^1.1.1 + version: 1.1.1(svelte@5.56.8(@typescript-eslint/types@8.66.0)) sveltekit-superforms: - specifier: ^2.30.0 - version: 2.30.0(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@types/json-schema@7.0.15)(svelte@5.54.0)(typescript@5.9.3) + specifier: ^2.30.2 + version: 2.30.2(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(@types/json-schema@7.0.15)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3) tailwind-merge: - specifier: ^3.5.0 - version: 3.5.0 + specifier: ^3.6.0 + version: 3.6.0 tailwind-variants: - specifier: ^3.2.2 - version: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2) + specifier: ^3.3.1 + version: 3.3.1(tailwind-merge@3.6.0)(tailwindcss@4.3.3) tailwindcss: - specifier: ^4.2.2 - version: 4.2.2 - tsx: - specifier: ^4.21.0 - version: 4.21.0 + specifier: ^4.3.3 + version: 4.3.3 tw-animate-css: specifier: ^1.4.0 version: 1.4.0 typescript: - specifier: ^5.9.3 - version: 5.9.3 + specifier: ^6.0.3 + version: 6.0.3 vite: - specifier: ^8.0.1 - version: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + specifier: ^8.2.1 + version: 8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1) vitest: - specifier: ^4.1.0 - version: 4.1.0(@types/node@25.5.0)(jsdom@29.0.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + specifier: ^4.1.10 + version: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.0(@noble/hashes@1.8.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)) packages: - '@ark/schema@0.56.0': - resolution: {integrity: sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==} - - '@ark/util@0.56.0': - resolution: {integrity: sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==} - - '@asamuzakjp/css-color@5.0.1': - resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + '@ark/schema@0.56.2': + resolution: {integrity: sha512-Qx4D2JFbBWpntiHZaTv7bGG4H/M2rigiknezKg/WVyDSaLdE4YCcWAOoFB7pjjDqHbbV2OqRfntm1nnXvwMexg==} - '@asamuzakjp/dom-selector@7.0.3': - resolution: {integrity: sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + '@ark/util@0.56.2': + resolution: {integrity: sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ==} - '@asamuzakjp/nwsapi@2.3.9': - resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@asamuzakjp/css-color@6.0.5': + resolution: {integrity: sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==} + engines: {node: ^22.13.0 || >=24.0.0} - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} + '@asamuzakjp/dom-selector@8.3.0': + resolution: {integrity: sha512-UJLfKXBhrc8i1vH2eJXuYQMwlsLKWFw3O+CPqXSuVEiikeAim3UgrfWX0k4tA/X8cRFM8iZ7OaqBokFGbYusdg==} + engines: {node: ^22.13.0 || >=24.0.0} '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} @@ -293,23 +256,19 @@ packages: resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true - '@colors/colors@1.6.0': - resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} - engines: {node: '>=0.1.90'} - - '@csstools/color-helpers@6.0.2': - resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} engines: {node: '>=20.19.0'} - '@csstools/css-calc@3.1.1': - resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} + '@csstools/css-calc@3.3.0': + resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} engines: {node: '>=20.19.0'} peerDependencies: '@csstools/css-parser-algorithms': ^4.0.0 '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-color-parser@4.0.2': - resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==} + '@csstools/css-color-parser@4.1.10': + resolution: {integrity: sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==} engines: {node: '>=20.19.0'} peerDependencies: '@csstools/css-parser-algorithms': ^4.0.0 @@ -321,8 +280,8 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-syntax-patches-for-csstree@1.1.1': - resolution: {integrity: sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==} + '@csstools/css-syntax-patches-for-csstree@1.1.7': + resolution: {integrity: sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==} peerDependencies: css-tree: ^3.2.1 peerDependenciesMeta: @@ -333,35 +292,22 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} - '@dabh/diagnostics@2.0.8': - resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} + '@dagrejs/dagre@2.0.4': + resolution: {integrity: sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA==} - '@dagrejs/dagre@1.1.8': - resolution: {integrity: sha512-5SEDlndt4W/LaVzPYJW+bSmSEZc9EzTf8rJ20WCKvjS5EAZAN0b+x0Yww7VMT4R3Wootkg+X9bUfUxazYw6Blw==} - - '@dagrejs/graphlib@2.2.4': - resolution: {integrity: sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==} - engines: {node: '>17.0.0'} + '@dagrejs/graphlib@3.0.4': + resolution: {integrity: sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg==} '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} - '@emnapi/core@1.9.0': - resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==} - - '@emnapi/runtime@1.9.0': - resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} - - '@emnapi/wasi-threads@1.2.0': - resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} - '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.is' + deprecated: 'Merged into tsx: https://tsx.hirok.io' '@esbuild-kit/esm-loader@2.6.5': resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.is' + deprecated: 'Merged into tsx: https://tsx.hirok.io' '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} @@ -369,8 +315,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.4': - resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -387,8 +333,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.4': - resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -405,8 +351,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.4': - resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -423,8 +369,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.4': - resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -441,8 +387,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.4': - resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -459,8 +405,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.4': - resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -477,8 +423,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.4': - resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -495,8 +441,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.4': - resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -513,8 +459,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.4': - resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -531,8 +477,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.4': - resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -549,8 +495,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.4': - resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -567,8 +513,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.4': - resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -585,8 +531,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.4': - resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -603,8 +549,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.4': - resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -621,8 +567,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.4': - resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -639,8 +585,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.4': - resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -657,8 +603,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.4': - resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -669,8 +615,8 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.27.4': - resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -687,8 +633,8 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.4': - resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -699,8 +645,8 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.27.4': - resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -717,8 +663,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.4': - resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -729,8 +675,8 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.27.4': - resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -747,8 +693,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.4': - resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -765,8 +711,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.4': - resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -783,8 +729,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.4': - resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -801,8 +747,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.4': - resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -817,16 +763,16 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.23.3': - resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==} + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/config-helpers@0.5.3': - resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==} + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/core@1.1.1': - resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/js@10.0.1': @@ -838,16 +784,16 @@ packages: eslint: optional: true - '@eslint/object-schema@3.0.3': - resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==} + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.6.1': - resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@exodus/bytes@1.15.0': - resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: '@noble/hashes': ^1.8.0 || ^2.0.0 @@ -858,8 +804,8 @@ packages: '@exodus/schemasafe@1.3.0': resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} - '@faker-js/faker@10.3.0': - resolution: {integrity: sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw==} + '@faker-js/faker@10.5.0': + resolution: {integrity: sha512-bsxD8WLS5lIj7aaoCx1YJkktqYj5vlBUE6HWzu2Q51ksrGJ0H737ECCKlFU7Yf8Br45z9t99frBp/J7kzbMPAg==} engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} '@floating-ui/core@1.7.5': @@ -893,19 +839,27 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@inlang/paraglide-js@2.15.0': - resolution: {integrity: sha512-2ZOa9nssVn4tjkKskqb88KP5A7cTIjo8AiM9xnPvH+vBhRIRenO+ftAbVOHhHcHjcFxy2QFcOfBAH/Cw1LIsUg==} + '@inlang/paraglide-js@2.23.2': + resolution: {integrity: sha512-tzWnZ6DEQ3JRxpSM/lsmfUFSNXLymvlfUei4uX1lo/Qh3a8OfxZ5nchGrEM+p0aLX4m5ZMrVuMKfaD0qXJFeoA==} hasBin: true + peerDependencies: + typescript: '>=5.6' + vite: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + vite: + optional: true '@inlang/recommend-sherlock@0.2.1': resolution: {integrity: sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==} - '@inlang/sdk@2.8.0': - resolution: {integrity: sha512-w1jysvUDTMgCaONklIgOJAp9dUDl0UhLbsdqfWEwY/GIqoc9IwpuHsrP3pzC+h3DfOpkMMDnDkTpPv8kIZ98iA==} - engines: {node: '>=18.0.0'} + '@inlang/sdk@2.10.2': + resolution: {integrity: sha512-O1ki72SNK6LPagaGrvlioBb1mWKvump7cO7P85hfGZjdFTmDdn3icI0A6MvaBsB3P9KQHAjzyubnN1OslGufTw==} + engines: {node: '>=20.0.0'} - '@internationalized/date@3.12.0': - resolution: {integrity: sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ==} + '@internationalized/date@3.12.3': + resolution: {integrity: sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q==} '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -923,107 +877,115 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@layerstack/svelte-actions@1.0.1-next.12': - resolution: {integrity: sha512-dndWTlYu8b1u6vw2nrO7NssccoACArGG75WoNlyVC13KuENZlWdKE9Q79/wlnbq00NeQMNKMjJwRMsrKQj2ULA==} + '@layerstack/svelte-actions@1.0.1-next.18': + resolution: {integrity: sha512-gxPzCnJ1c9LTfWtRqLUzefCx+k59ZpxDUQ2XB+LokveZQPe7IDSOwHaBOEMlaGoGrtwc3Ft8dSZq+2WT2o9u/g==} - '@layerstack/svelte-state@0.1.0-next.17': - resolution: {integrity: sha512-z7e6mPJnypD80LEI/UDuH0bI6s8/nut06MB7rEkRcEfHJekhKSJgFhMnrYzLED7Mc2gTTD0X/wcYlakauWlU8A==} + '@layerstack/svelte-state@0.1.0-next.23': + resolution: {integrity: sha512-7O4umv+gXwFfs3/vjzFWYHNXGwYnnjBapWJ5Y+9u99F4eVk6rh4ocNwqkqQNkpMZ5tUJBlRTWjPE1So6+hEzIg==} - '@layerstack/tailwind@2.0.0-next.15': - resolution: {integrity: sha512-7tqKE3OV7/ybeDOORX++USYYCBJa7IgTya2czFpzbgXGo7CQDVyuv+0J1DggjRcEqhhXQA4MUhgnhcRaZvHxWg==} + '@layerstack/tailwind@2.0.0-next.21': + resolution: {integrity: sha512-Qgp2EpmEHmjtura8MQzWicR6ztBRSsRvddakFtx9ShrLMz6jWzd6bCMVVRu44Q3ZOrtXmSu4QxjCZWu1ytvuPg==} - '@layerstack/utils@2.0.0-next.12': - resolution: {integrity: sha512-fhGZUlSr3N+D44BYm37WKMGSEFyZBW+dwIqtGU8Cl54mR4TLQ/UwyGhdpgIHyH/x/8q1abE0fP0Dn6ZsrDE3BA==} + '@layerstack/utils@2.0.0-next.18': + resolution: {integrity: sha512-EYILHpfBRYMMEahajInu9C2AXQom5IcAEdtCeucD3QIl/fdDgRbtzn6/8QW9ewumfyNZetdUvitOksmI1+gZYQ==} - '@libsql/client@0.17.0': - resolution: {integrity: sha512-TLjSU9Otdpq0SpKHl1tD1Nc9MKhrsZbCFGot3EbCxRa8m1E5R1mMwoOjKMMM31IyF7fr+hPNHLpYfwbMKNusmg==} + '@libsql/client@0.17.4': + resolution: {integrity: sha512-lYayFWasDV78A+TjlEhr6ubb3odBV6OHjb+wdp8VQcyWWAEIjuwbCHaraEUS4m4yWoo0BvZo96It4VdzZRmRWw==} - '@libsql/core@0.17.0': - resolution: {integrity: sha512-hnZRnJHiS+nrhHKLGYPoJbc78FE903MSDrFJTbftxo+e52X+E0Y0fHOCVYsKWcg6XgB7BbJYUrz/xEkVTSaipw==} + '@libsql/core@0.17.4': + resolution: {integrity: sha512-LqF9gIvnJ38nmAH1y/ChizHqDO/MO1wLgA96XrraulEEbqXxLjleSH92YWTolbuJKgPUmGu4aJk9W3UnAcxLOQ==} - '@libsql/darwin-arm64@0.5.22': - resolution: {integrity: sha512-4B8ZlX3nIDPndfct7GNe0nI3Yw6ibocEicWdC4fvQbSs/jdq/RC2oCsoJxJ4NzXkvktX70C1J4FcmmoBy069UA==} + '@libsql/darwin-arm64@0.5.29': + resolution: {integrity: sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==} cpu: [arm64] os: [darwin] - '@libsql/darwin-x64@0.5.22': - resolution: {integrity: sha512-ny2HYWt6lFSIdNFzUFIJ04uiW6finXfMNJ7wypkAD8Pqdm6nAByO+Fdqu8t7sD0sqJGeUCiOg480icjyQ2/8VA==} + '@libsql/darwin-x64@0.5.29': + resolution: {integrity: sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==} cpu: [x64] os: [darwin] - '@libsql/hrana-client@0.9.0': - resolution: {integrity: sha512-pxQ1986AuWfPX4oXzBvLwBnfgKDE5OMhAdR/5cZmRaB4Ygz5MecQybvwZupnRz341r2CtFmbk/BhSu7k2Lm+Jw==} + '@libsql/hrana-client@0.10.0': + resolution: {integrity: sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw==} '@libsql/isomorphic-ws@0.1.5': resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} - '@libsql/linux-arm-gnueabihf@0.5.22': - resolution: {integrity: sha512-3Uo3SoDPJe/zBnyZKosziRGtszXaEtv57raWrZIahtQDsjxBVjuzYQinCm9LRCJCUT5t2r5Z5nLDPJi2CwZVoA==} + '@libsql/linux-arm-gnueabihf@0.5.29': + resolution: {integrity: sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==} cpu: [arm] os: [linux] - '@libsql/linux-arm-musleabihf@0.5.22': - resolution: {integrity: sha512-LCsXh07jvSojTNJptT9CowOzwITznD+YFGGW+1XxUr7fS+7/ydUrpDfsMX7UqTqjm7xG17eq86VkWJgHJfvpNg==} + '@libsql/linux-arm-musleabihf@0.5.29': + resolution: {integrity: sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==} cpu: [arm] os: [linux] - '@libsql/linux-arm64-gnu@0.5.22': - resolution: {integrity: sha512-KSdnOMy88c9mpOFKUEzPskSaF3VLflfSUCBwas/pn1/sV3pEhtMF6H8VUCd2rsedwoukeeCSEONqX7LLnQwRMA==} + '@libsql/linux-arm64-gnu@0.5.29': + resolution: {integrity: sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==} cpu: [arm64] os: [linux] - '@libsql/linux-arm64-musl@0.5.22': - resolution: {integrity: sha512-mCHSMAsDTLK5YH//lcV3eFEgiR23Ym0U9oEvgZA0667gqRZg/2px+7LshDvErEKv2XZ8ixzw3p1IrBzLQHGSsw==} + '@libsql/linux-arm64-musl@0.5.29': + resolution: {integrity: sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==} cpu: [arm64] os: [linux] - '@libsql/linux-x64-gnu@0.5.22': - resolution: {integrity: sha512-kNBHaIkSg78Y4BqAdgjcR2mBilZXs4HYkAmi58J+4GRwDQZh5fIUWbnQvB9f95DkWUIGVeenqLRFY2pcTmlsew==} + '@libsql/linux-x64-gnu@0.5.29': + resolution: {integrity: sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==} cpu: [x64] os: [linux] - '@libsql/linux-x64-musl@0.5.22': - resolution: {integrity: sha512-UZ4Xdxm4pu3pQXjvfJiyCzZop/9j/eA2JjmhMaAhe3EVLH2g11Fy4fwyUp9sT1QJYR1kpc2JLuybPM0kuXv/Tg==} + '@libsql/linux-x64-musl@0.5.29': + resolution: {integrity: sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==} cpu: [x64] os: [linux] - '@libsql/win32-x64-msvc@0.5.22': - resolution: {integrity: sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA==} + '@libsql/win32-x64-msvc@0.5.29': + resolution: {integrity: sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==} cpu: [x64] os: [win32] - '@lix-js/sdk@0.4.7': - resolution: {integrity: sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ==} + '@lix-js/sdk@0.4.10': + resolution: {integrity: sha512-0dMInAJK/67guTG5rRZaCEhvzC5cCXENOjaePA5AqMXrCE97kaY7SRor9e2vnoGsFIiGqXKlT0MCIoZj36G0gg==} engines: {node: '>=18'} '@lix-js/server-protocol-schema@0.1.1': resolution: {integrity: sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==} - '@lucide/svelte@0.577.0': - resolution: {integrity: sha512-0P6mkySd2MapIEgq08tADPmcN4DHndC/02PWwaLkOerXlx5Sv9aT4BxyXLIY+eccr0g/nEyCYiJesqS61YdBZQ==} + '@lucide/svelte@1.30.0': + resolution: {integrity: sha512-KtVrqWT3BD41kf1CPaP4QTAyofw0xm5hw/JGwd76+wMk1l0ZcXskA2Wi9ANVWCsk7mGrpFo2SqL3Hl29xTkV3w==} peerDependencies: svelte: ^5 - '@napi-rs/wasm-runtime@1.1.1': - resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} - '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@oslojs/asn1@1.0.0': resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@oslojs/binary@1.0.0': resolution: {integrity: sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@oslojs/crypto@1.0.1': resolution: {integrity: sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} - '@oxc-project/types@0.120.0': - resolution: {integrity: sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==} + '@oxc-project/types@0.142.0': + resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1031,103 +993,98 @@ packages: '@poppinss/macroable@1.1.2': resolution: {integrity: sha512-FAVBRzzWhYP5mA3lCwLH1A0fKBqq5anyjGet90Z81aRK5c/+LTGUE1zJhZrErjaenBSOOI9BVUs3WVmotneFQA==} - '@rolldown/binding-android-arm64@1.0.0-rc.10': - resolution: {integrity: sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==} + '@rolldown/binding-android-arm64@1.2.2': + resolution: {integrity: sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.10': - resolution: {integrity: sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==} + '@rolldown/binding-darwin-arm64@1.2.2': + resolution: {integrity: sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.10': - resolution: {integrity: sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==} + '@rolldown/binding-darwin-x64@1.2.2': + resolution: {integrity: sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.10': - resolution: {integrity: sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==} + '@rolldown/binding-freebsd-x64@1.2.2': + resolution: {integrity: sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': - resolution: {integrity: sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.2': + resolution: {integrity: sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==} + '@rolldown/binding-linux-arm64-gnu@1.2.2': + resolution: {integrity: sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': - resolution: {integrity: sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==} + '@rolldown/binding-linux-arm64-musl@1.2.2': + resolution: {integrity: sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==} + '@rolldown/binding-linux-ppc64-gnu@1.2.2': + resolution: {integrity: sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==} + '@rolldown/binding-linux-s390x-gnu@1.2.2': + resolution: {integrity: sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': - resolution: {integrity: sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==} + '@rolldown/binding-linux-x64-gnu@1.2.2': + resolution: {integrity: sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': - resolution: {integrity: sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==} + '@rolldown/binding-linux-x64-musl@1.2.2': + resolution: {integrity: sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': - resolution: {integrity: sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==} + '@rolldown/binding-openharmony-arm64@1.2.2': + resolution: {integrity: sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': - resolution: {integrity: sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': - resolution: {integrity: sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==} + '@rolldown/binding-win32-arm64-msvc@1.2.2': + resolution: {integrity: sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': - resolution: {integrity: sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==} + '@rolldown/binding-win32-x64-msvc@1.2.2': + resolution: {integrity: sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.10': - resolution: {integrity: sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} '@rollup/plugin-commonjs@29.0.2': resolution: {integrity: sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==} @@ -1156,6 +1113,15 @@ packages: rollup: optional: true + '@rollup/plugin-replace@6.0.3': + resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -1315,9 +1281,6 @@ packages: '@sinclair/typebox@0.31.28': resolution: {integrity: sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==} - '@so-ric/colorspace@1.1.6': - resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} - '@sqlite.org/sqlite-wasm@3.48.0-build4': resolution: {integrity: sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==} hasBin: true @@ -1325,25 +1288,25 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@sveltejs/acorn-typescript@1.0.9': - resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + '@sveltejs/acorn-typescript@1.0.11': + resolution: {integrity: sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==} peerDependencies: acorn: ^8.9.0 - '@sveltejs/adapter-node@5.5.4': - resolution: {integrity: sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==} + '@sveltejs/adapter-node@5.5.7': + resolution: {integrity: sha512-uOfc9eVlI3A37RRSaKcgrheBYPrfJwC9VMqDp8x/O6tlKdcLLvHThSWD0KNIbjQ/d+7bwLGx3vx6aowAcRfd2g==} peerDependencies: '@sveltejs/kit': ^2.4.0 - '@sveltejs/kit@2.55.0': - resolution: {integrity: sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==} + '@sveltejs/kit@2.70.2': + resolution: {integrity: sha512-RzRoRpuR2KXqc5yMO0akQHDZeT4AslOlznGITURsqHaVbtyYP4Wn3eE3gxj9JcDyNYO0crkxhdwFHc+2vkVm6w==} engines: {node: '>=18.13'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.0.0 '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: ^5.3.3 + typescript: ^5.3.3 || ^6.0.0 vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 peerDependenciesMeta: '@opentelemetry/api': @@ -1351,8 +1314,12 @@ packages: typescript: optional: true - '@sveltejs/vite-plugin-svelte@7.0.0': - resolution: {integrity: sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==} + '@sveltejs/load-config@0.2.2': + resolution: {integrity: sha512-K7dsJDQxBOF+f+epuhMactcjK2VP4MRkLKtwSykNtEI+cKVEyzrmmhQ1pmoxI800m4JKcyJ05L2M4yPwAGiBNw==} + engines: {node: '>= 18.0.0'} + + '@sveltejs/vite-plugin-svelte@7.2.0': + resolution: {integrity: sha512-1SpkuMSRLfugrVX+IrKfE1RUegzo8AQzKQ6qQPfVzbcWi5IhuTPaKb5ZrLpucleFznkc4/RTeSPoRnGWFxX+EQ==} engines: {node: ^20.19 || ^22.12 || >=24} peerDependencies: svelte: ^5.46.4 @@ -1366,69 +1333,69 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' - '@tailwindcss/node@4.2.2': - resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} - '@tailwindcss/oxide-android-arm64@4.2.2': - resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.2.2': - resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.2.2': - resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.2.2': - resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': - resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': - resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.2.2': - resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.2.2': - resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.2.2': - resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.2.2': - resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -1439,29 +1406,29 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': - resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.2.2': - resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.2.2': - resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} engines: {node: '>= 20'} - '@tailwindcss/typography@0.5.19': - resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + '@tailwindcss/typography@0.5.20': + resolution: {integrity: sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==} peerDependencies: - tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + tailwindcss: '>=3.0.0 || >=4.0.0 || insiders' - '@tailwindcss/vite@4.2.2': - resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 @@ -1469,59 +1436,21 @@ packages: resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} engines: {node: '>=12'} - '@testing-library/dom@10.4.1': - resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} - engines: {node: '>=18'} - - '@testing-library/svelte-core@1.0.0': - resolution: {integrity: sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ==} - engines: {node: '>=16'} - peerDependencies: - svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 - - '@testing-library/svelte@5.3.1': - resolution: {integrity: sha512-8Ez7ZOqW5geRf9PF5rkuopODe5RGy3I9XR+kc7zHh26gBiktLaxTfKmhlGaSHYUOTQE7wFsLMN9xCJVCszw47w==} - engines: {node: '>= 10'} - peerDependencies: - svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 - vite: '*' - vitest: '*' - peerDependenciesMeta: - vite: - optional: true - vitest: - optional: true - - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - - '@types/aria-query@5.0.4': - resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - '@types/bcrypt@6.0.0': resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==} - '@types/body-parser@1.19.6': - resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - '@types/cookiejar@2.1.5': - resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} - - '@types/cors@2.8.19': - resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} - '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + '@types/d3-path@3.1.1': resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} @@ -1543,57 +1472,27 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/express-serve-static-core@5.1.1': - resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} - - '@types/express@5.0.6': - resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} - - '@types/http-errors@2.0.5': - resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/methods@1.1.4': - resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} - - '@types/multer@2.1.0': - resolution: {integrity: sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==} - '@types/node-cron@3.0.11': resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} - '@types/node@25.5.0': - resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} - - '@types/nodemailer@7.0.11': - resolution: {integrity: sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==} + '@types/node@26.1.2': + resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} - '@types/qs@6.15.0': - resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} + '@types/nodemailer@8.0.1': + resolution: {integrity: sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==} - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/pdfkit@0.17.6': + resolution: {integrity: sha512-tIwzxk2uWKp0Cq9JIluQXJid77lYhF52EsIOwhsMF4iWLA6YneoBR1xVKYYdAysHuepUB0OX4tdwMiUDdGKmig==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@types/send@1.2.1': - resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - - '@types/serve-static@2.2.0': - resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} - - '@types/superagent@8.1.9': - resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} - - '@types/supertest@7.2.0': - resolution: {integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==} - - '@types/triple-beam@1.3.5': - resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} - '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -1619,69 +1518,69 @@ packages: '@types/json-schema': optional: true - '@typescript-eslint/eslint-plugin@8.57.1': - resolution: {integrity: sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==} + '@typescript-eslint/eslint-plugin@8.66.0': + resolution: {integrity: sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.57.1 + '@typescript-eslint/parser': ^8.66.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.57.1': - resolution: {integrity: sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==} + '@typescript-eslint/parser@8.66.0': + resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.57.1': - resolution: {integrity: sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==} + '@typescript-eslint/project-service@8.66.0': + resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.57.1': - resolution: {integrity: sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==} + '@typescript-eslint/scope-manager@8.66.0': + resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.57.1': - resolution: {integrity: sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==} + '@typescript-eslint/tsconfig-utils@8.66.0': + resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.57.1': - resolution: {integrity: sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==} + '@typescript-eslint/type-utils@8.66.0': + resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.57.1': - resolution: {integrity: sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==} + '@typescript-eslint/types@8.66.0': + resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.57.1': - resolution: {integrity: sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==} + '@typescript-eslint/typescript-estree@8.66.0': + resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.57.1': - resolution: {integrity: sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==} + '@typescript-eslint/utils@8.66.0': + resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.57.1': - resolution: {integrity: sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==} + '@typescript-eslint/visitor-keys@8.66.0': + resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@valibot/to-json-schema@1.6.0': - resolution: {integrity: sha512-d6rYyK5KVa2XdqamWgZ4/Nr+cXhxjy7lmpe6Iajw15J/jmU+gyxl2IEd1Otg1d7Rl3gOQL5reulnSypzBtYy1A==} + '@valibot/to-json-schema@1.7.1': + resolution: {integrity: sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A==} peerDependencies: - valibot: ^1.3.0 + valibot: ^1.4.0 '@vinejs/compiler@3.0.0': resolution: {integrity: sha512-v9Lsv59nR56+bmy2p0+czjZxsLHwaibJ+SV5iK9JJfehlJMa501jUJQqqz4X/OqKXrxtE3uTQmSqjUqzF3B2mw==} @@ -1691,43 +1590,43 @@ packages: resolution: {integrity: sha512-ZtvYkYpZOYdvbws3uaOAvTFuvFXoQGAtmzeiXu+XSMGxi5GVsODpoI9Xu9TplEMuD/5fmAtBbKb9cQHkWkLXDQ==} engines: {node: '>=18.16.0'} - '@vitest/coverage-v8@4.1.0': - resolution: {integrity: sha512-nDWulKeik2bL2Va/Wl4x7DLuTKAXa906iRFooIRPR+huHkcvp9QDkPQ2RJdmjOFrqOqvNfoSQLF68deE3xC3CQ==} + '@vitest/coverage-v8@4.1.10': + resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} peerDependencies: - '@vitest/browser': 4.1.0 - vitest: 4.1.0 + '@vitest/browser': 4.1.10 + vitest: 4.1.10 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.0': - resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - '@vitest/mocker@4.1.0': - resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.1.0': - resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - '@vitest/runner@4.1.0': - resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - '@vitest/snapshot@4.1.0': - resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - '@vitest/spy@4.1.0': - resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - '@vitest/utils@4.1.0': - resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -1742,33 +1641,15 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - - append-field@1.0.0: - resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} - - aria-query@5.3.0: - resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} - aria-query@5.3.1: resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} engines: {node: '>= 0.4'} - arkregex@0.0.5: - resolution: {integrity: sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==} + arkregex@0.0.8: + resolution: {integrity: sha512-PJcx6G1kQTgLKPUbeYlYecDRaKq15AMSGVajlKFYWlPeJRQL+j3dKE6tyMs40HZ99djS1l9Vhl3ezAHy9JBIqQ==} - arktype@2.2.0: - resolution: {integrity: sha512-t54MZ7ti5BhOEvzEkgKnWvqj+UbDfWig+DHr5I34xatymPusKLS0lQpNJd8M6DzmIto2QGszHfNKoFIT8tMCZQ==} + arktype@2.2.3: + resolution: {integrity: sha512-7W+0RLTUNJiBFIIZXwOQxSR8Z273IAd6IvqBeG9+gHnQKFsIx2C0iOtGTmMrPnlX4qLXyc5+ll7A0BIj9WrbTg==} array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} @@ -1780,12 +1661,6 @@ packages: ast-v8-to-istanbul@1.0.0: resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -1794,6 +1669,13 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + bcrypt@6.0.0: resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==} engines: {node: '>= 18'} @@ -1801,8 +1683,8 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} - bits-ui@2.16.3: - resolution: {integrity: sha512-5hJ5dEhf5yPzkRFcxzgQHScGodeo0gK0MUUXrdLlRHWaBOBGZiacWLG96j/wwFatKwZvouw7q+sn14i0fx3RIg==} + bits-ui@2.18.1: + resolution: {integrity: sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA==} engines: {node: '>=20'} peerDependencies: '@internationalized/date': ^3.8.1 @@ -1812,16 +1694,18 @@ packages: resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} engines: {node: 18 || 20 || >=22} - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} - busboy@1.6.0: - resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} - engines: {node: '>=10.16.0'} + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} + browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} camelcase@8.0.0: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} @@ -1831,10 +1715,6 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1842,44 +1722,17 @@ packages: class-validator@0.14.4: resolution: {integrity: sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==} - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-convert@3.1.3: - resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} - engines: {node: '>=14.6'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - color-name@2.1.0: - resolution: {integrity: sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==} - engines: {node: '>=12.20'} - - color-string@2.1.4: - resolution: {integrity: sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==} - engines: {node: '>=18'} - - color@5.0.3: - resolution: {integrity: sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==} - engines: {node: '>=18'} - colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} @@ -1895,15 +1748,6 @@ packages: commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - concat-stream@2.0.0: - resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} - engines: {'0': node >= 6.0} - - concurrently@9.2.1: - resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} - engines: {node: '>=18'} - hasBin: true - consola@3.4.0: resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -1915,13 +1759,6 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - - cross-fetch@4.1.0: - resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1935,8 +1772,8 @@ packages: engines: {node: '>=4'} hasBin: true - csv-parse@6.2.0: - resolution: {integrity: sha512-Zv8KRHccD1q3BJlK4VcQiEn/+suOOp++89g/fpqOxB2U2tU66uC3yM+ZwU6nQQEJp8AqBIiNqB+pUTKNz4QzKg==} + csv-parse@7.0.2: + resolution: {integrity: sha512-uKZghv9UmPkMVLYy//KZ9HFAIJsl7wkhoEdIL0+rhuSY9pZQlhaeGEDPIe+/w7eh81MOql8Q/9+inAGWG6ZHYA==} currency-codes@2.2.0: resolution: {integrity: sha512-vpbQc5sEYHGdTVAYUhHnKv0DWiYLRvzl/KKyqeHzBh7HD/j3UlWoScpZ9tN/jG6w2feddWoObsBbaNVu5yDapg==} @@ -1948,10 +1785,18 @@ packages: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} engines: {node: '>=12'} + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + d3-color@3.1.0: resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} engines: {node: '>=12'} + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + d3-delaunay@6.0.4: resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} engines: {node: '>=12'} @@ -1999,6 +1844,10 @@ packages: resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} engines: {node: '>=12'} + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + d3-quadtree@3.0.1: resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} engines: {node: '>=12'} @@ -2044,10 +1893,6 @@ packages: resolution: {integrity: sha512-G7gHKj89n2owmkGb6WX6ixcnQ0Kf/0wpa9VIh9DGdbHu8wdrlaHU4ir3/bFNERl8N8nn4G7e7qbtBG8N9caihQ==} engines: {node: '>=12'} - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - data-urls@7.0.0: resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -2057,8 +1902,8 @@ packages: peerDependencies: date-fns: ^3.0.0 || ^4.0.0 - date-fns@4.1.0: - resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} dayjs@1.11.20: resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} @@ -2093,10 +1938,6 @@ packages: delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2109,25 +1950,21 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.6.4: - resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==} + devalue@5.8.1: + resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + + dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - dom-accessibility-api@0.5.16: - resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} - - dotenv@17.3.1: - resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} - engines: {node: '>=12'} - drizzle-kit@0.31.10: resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} hasBin: true - drizzle-orm@0.45.1: - resolution: {integrity: sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==} + drizzle-orm@0.45.2: + resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=4' @@ -2218,46 +2055,20 @@ packages: sqlite3: optional: true - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - effect@3.20.0: - resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} + effect@3.21.4: + resolution: {integrity: sha512-B89v/xSgPbl1J2Ai2u18jxq3odpFauU1rC6/eSs4FeNHi72kwKdJp12VGigvRV2lK+kRnx+OOz41XV8guZd4gQ==} - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - enabled@2.0.0: - resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} - - enhanced-resolve@5.20.1: - resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + enhanced-resolve@5.24.3: + resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} engines: {node: '>=10.13.0'} - entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} @@ -2268,15 +2079,11 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.27.4: - resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} engines: {node: '>=18'} hasBin: true - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2287,8 +2094,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-svelte@3.15.2: - resolution: {integrity: sha512-k4Nsjs3bHujeEnnckoTM4mFYR1e8Mb9l2rTwNdmYiamA+Tjzn8X+2F+fuSP2w4VbXYhn2bmySyACQYdmUDW2Cg==} + eslint-plugin-svelte@3.22.0: + resolution: {integrity: sha512-O3qn0NePTWta+1o25dIThqeEP/hEQ3VxDK2LVO8SQ5wG9umLMvulK+m1yQ4JGOb2Pkl8IB0G1lpRV/HXDXSLTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.1 || ^9.0.0 || ^10.0.0 @@ -2326,8 +2133,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.0.3: - resolution: {integrity: sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==} + eslint@10.8.0: + resolution: {integrity: sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -2356,8 +2163,13 @@ packages: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} - esrap@2.2.4: - resolution: {integrity: sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==} + esrap@2.2.13: + resolution: {integrity: sha512-m8jH5hZgJE2RRUK/jjkGPcJEDAV+dYnZYFkosQaPTcE+Yw4xynXHOo6FUdwaWBtdR3b1MMa7wEDTSHeR2VWsGA==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -2403,13 +2215,6 @@ packages: picomatch: optional: true - fecha@4.2.3: - resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} - - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2428,16 +2233,8 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - fn.name@1.1.0: - resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} - - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} - - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} + fontkit@2.0.4: + resolution: {integrity: sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==} formsnap@2.0.1: resolution: {integrity: sha512-iJSe4YKd/W6WhLwKDVJU9FQeaJRpEFuolhju7ZXlRpUVyDdqFdMP8AUBICgnVvQPyP41IPAlBa/v0Eo35iE6wQ==} @@ -2454,18 +2251,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} @@ -2477,14 +2262,10 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} - globals@17.4.0: - resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} + globals@17.9.0: + resolution: {integrity: sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==} engines: {node: '>=18'} - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -2492,22 +2273,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - helmet@8.1.0: - resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} - engines: {node: '>=18.0.0'} - html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -2535,9 +2304,6 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -2556,10 +2322,6 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2576,10 +2338,6 @@ packages: is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2595,30 +2353,30 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true - joi@17.13.3: - resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + joi@17.13.4: + resolution: {integrity: sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==} js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + js-md5@0.8.3: + resolution: {integrity: sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==} + js-sha256@0.11.1: resolution: {integrity: sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==} js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - jsdom@29.0.0: - resolution: {integrity: sha512-9FshNB6OepopZ08unmmGpsF7/qCjxGPbo3NbgfJAnPeHXnsODE9WWffXZtRFRFe0ntzaAOcSKNJFz8wiyvF1jQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + jsdom@30.0.0: + resolution: {integrity: sha512-JQHfRGmmKmaZoUAvIgff5jjG/0SzTQlGz8c7t72KzBzo8ZULEjAjnYE0sNwBOUA4QtWwYE2xoYitg8NFsmiYxA==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} peerDependencies: - canvas: ^3.0.0 + canvas: ^3.2.3 peerDependenciesMeta: canvas: optional: true @@ -2651,15 +2409,12 @@ packages: known-css-properties@0.37.0: resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} - kuler@2.0.0: - resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} - - kysely@0.27.6: - resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==} - engines: {node: '>=14.0.0'} + kysely@0.28.16: + resolution: {integrity: sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww==} + engines: {node: '>=20.0.0'} - layerchart@2.0.0-next.27: - resolution: {integrity: sha512-yt28xU8WzXq0AliX7eiC0JKZGQtO8M9FmHvt8sESNitSc/yC+fYeTghaO9lMRwcYCmi6D1NjbFyD9mWFeazNIQ==} + layerchart@2.1.0: + resolution: {integrity: sha512-6mNwveayU6tYwdjlP+AP6pzj0rdF3oupoK6EEhpw9+zBep/myzhDSXl96/N96XuMaVA0Bd8MjaUd3aYJl9iBwg==} peerDependencies: svelte: ^5.0.0 @@ -2670,8 +2425,8 @@ packages: libphonenumber-js@1.12.40: resolution: {integrity: sha512-HKGs7GowShNls3Zh+7DTr6wYpPk5jC78l508yQQY3e8ZgJChM3A9JZghmMJZuK+5bogSfuTafpjksGSR3aMIEg==} - libsql@0.5.22: - resolution: {integrity: sha512-NscWthMQt7fpU8lqd7LXMvT9pi+KhhmTHAJWUB/Lj6MWa0MKFv0F2V4C6WKKpjCVZl0VwcDz4nOI3CyaT1DDiA==} + libsql@0.5.29: + resolution: {integrity: sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==} cpu: [x64, arm64, wasm32, arm] os: [darwin, linux, win32] @@ -2681,30 +2436,60 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.32.0: resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.32.0: resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.32.0: resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.32.0: resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} @@ -2712,6 +2497,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} @@ -2719,6 +2511,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} @@ -2726,6 +2525,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} @@ -2733,26 +2539,52 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.32.0: resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.32.0: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -2760,15 +2592,8 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.23: - resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} - - logform@2.7.0: - resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} - engines: {node: '>= 12.0.0'} - - lru-cache@11.2.7: - resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} engines: {node: 20 || >=22} lz-string@1.5.0: @@ -2785,17 +2610,9 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - memoize-weak@1.0.2: resolution: {integrity: sha512-gj39xkrjEw7nCn4nJ1M5ms6+MyMlyiGmttzsqAUsAKn6bYKwuTHh/AO3cKPF8IBrTIYTxb0wWXFs3E//Y8VoWQ==} @@ -2803,14 +2620,6 @@ packages: resolution: {integrity: sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA==} engines: {node: '>=18'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -2823,6 +2632,10 @@ packages: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + mode-watcher@1.1.0: resolution: {integrity: sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==} peerDependencies: @@ -2839,15 +2652,21 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - multer@2.1.1: - resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} - engines: {node: '>= 10.16.0'} - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@3.3.16: + resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -2855,34 +2674,16 @@ packages: resolution: {integrity: sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==} engines: {node: ^18 || ^20 || >= 21} - node-cron@4.2.1: - resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==} - engines: {node: '>=6.0.0'} - - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-cron@4.6.0: + resolution: {integrity: sha512-Si/bzYiKRHOB8/a99T2+SDGN582ONDMSTlJr5oCkT6GtnqPjZ2s10eoQRYkW9ZHwjVxONL+W8Fb+qR0AHMQsdg==} + engines: {node: '>=20'} node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - nodemailer@8.0.3: - resolution: {integrity: sha512-JQNBqvK+bj3NMhUFR3wmCl3SYcOeMotDiwDBvIoCuQdF0PvlIY0BH+FJ2CG7u4cXKPChplE78oowlH/Otsc4ZQ==} + nodemailer@9.0.5: + resolution: {integrity: sha512-wvjiKvjczmsN7U/8006JOdXubgBk2XFAbioDMbT+sM7cPs0QrhJTa6KBRX7P5REGGkDcLUz/EarWidb8G8C1jQ==} engines: {node: '>=6.0.0'} normalize-url@8.1.1: @@ -2892,16 +2693,9 @@ packages: nub@0.0.0: resolution: {integrity: sha512-dK0Ss9C34R/vV0FfYJXuqDAqHlaW9fvWVufq9MmGF2umCuDbd5GRfRD9fpi/LiM0l4ZXf8IBB+RYmZExqCrf0w==} - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - one-time@1.0.0: - resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2914,8 +2708,14 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - parse5@8.0.0: - resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} @@ -2931,13 +2731,23 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pdfkit@0.19.1: + resolution: {integrity: sha512-6Gzk+wDwTs4VSxsR5rCMTnIl5nlmkye1oWB0l2hDB1EX6ZNSIBroKQEv+2+fPPn+stVjyqzmsqRJVDfB9fo5DA==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + png-js@1.1.0: + resolution: {integrity: sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==} + postcss-load-config@3.1.4: resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} engines: {node: '>= 10'} @@ -2970,22 +2780,31 @@ packages: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-svelte@3.5.1: - resolution: {integrity: sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==} + prettier-plugin-svelte@4.1.1: + resolution: {integrity: sha512-wXvbXMjSvb4C9ENWTHXyd+ihakKCsJ6rJhLP6/8HFNj4GkZr48jqL9PoKsl2sk7SyCZRTnJ7O2TTowUpOxP/KA==} + engines: {node: '>=20'} peerDependencies: prettier: ^3.0.0 - svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + svelte: ^5.0.0 - prettier-plugin-tailwindcss@0.7.2: - resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} + prettier-plugin-tailwindcss@0.8.1: + resolution: {integrity: sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw==} engines: {node: '>=20.19'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' @@ -3039,15 +2858,11 @@ packages: prettier-plugin-svelte: optional: true - prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} engines: {node: '>=14'} hasBin: true - pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - promise-limit@2.7.0: resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} @@ -3061,21 +2876,10 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -3088,11 +2892,14 @@ packages: engines: {node: '>= 0.4'} hasBin: true + restructure@3.0.2: + resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==} + robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - rolldown@1.0.0-rc.10: - resolution: {integrity: sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==} + rolldown@1.2.2: + resolution: {integrity: sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -3125,23 +2932,25 @@ packages: '@sveltejs/kit': optional: true + runed@0.37.1: + resolution: {integrity: sha512-MeFY73xBW8IueWBm012nNFIGy19WUGPLtknavyUPMpnyt350M47PhGSGrGoSLbidwn+Zlt/O0cp8/OZE3LASWA==} + peerDependencies: + '@sveltejs/kit': ^2.21.0 + svelte: ^5.7.0 + zod: ^4.1.0 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + zod: + optional: true + rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - rxjs@7.8.2: - resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safe-stable-stringify@2.5.0: - resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} - engines: {node: '>=10'} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -3165,10 +2974,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.3: - resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} - engines: {node: '>= 0.4'} - siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3192,30 +2997,12 @@ packages: peerDependencies: kysely: '*' - stack-trace@0.0.10: - resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} std-env@4.0.0: resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} - streamsearch@1.1.0: - resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} - engines: {node: '>=10.0.0'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} @@ -3227,16 +3014,12 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte-awesome-color-picker@4.1.1: - resolution: {integrity: sha512-k4APFGBFN8EpvUS0UJfdlqj8xDekMbqGBwnDYBK1QCY8kcTXSO8c7SitEk70MkiYOIByOgXlHcjaipy6H7dpAw==} + svelte-awesome-color-picker@4.1.3: + resolution: {integrity: sha512-fYio6XR2vp9WO0ihT6Vlw1Kj278gEZtqfIRde4YBx5OgGjBdQJxFE8jl6OibPQPrmaz625HZPZaSbRflL88/Mg==} engines: {node: '>=24'} peerDependencies: svelte: ^5.0.0 @@ -3246,25 +3029,30 @@ packages: peerDependencies: svelte: ^5.0.0 - svelte-check@4.4.5: - resolution: {integrity: sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==} + svelte-check@4.7.5: + resolution: {integrity: sha512-NnkHGCTPH6k4ka1E9IpTuNv40uLArHnX52kLEuaHSGqRlPYTnkbFs529jSWG+y+wDy+v+jA2PQ0soN1umVK+OA==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: '>=5.0.0' + typescript: ^5.0.0 || ^6.0.0 + + svelte-dnd-action@0.9.78: + resolution: {integrity: sha512-qov+j3A9Umo1Ng2b2PaQ/nTDBo1sogAw19DF1KZ35Pcx/9yJtlmnKYKVHjCz9iqUH+g1sk4XuCstxPZFFPEa/Q==} + peerDependencies: + svelte: '>=3.23.0 || ^5.0.0-next.0' - svelte-eslint-parser@1.6.0: - resolution: {integrity: sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.30.3} + svelte-eslint-parser@1.8.0: + resolution: {integrity: sha512-mikR1qwIVy3t5WthUoAXkMwxkXvabZP9FJgdx35Ei7EbGWmctva1Pih16Koeor/bdNNq8NXHlwKGS6NkYTawLg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.34.1} peerDependencies: svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 peerDependenciesMeta: svelte: optional: true - svelte-sonner@1.1.0: - resolution: {integrity: sha512-3lYM6ZIqWe+p9vwwWHGWP/ZdvHiUtzURsud2quIxivrX4rvpXh6i+geBGn0m3JS6KwW6W8VgbOl3xQMcDuh6gg==} + svelte-sonner@1.1.1: + resolution: {integrity: sha512-5cd3p7wa4cq0NsqslMwdlPb7x1JglEZ/GKrLePWNr5bCxR1nagAVrY01FRFrXfUGs41miLt3C327+8XJo5BzZw==} peerDependencies: svelte: ^5.0.0 @@ -3286,12 +3074,12 @@ packages: peerDependencies: svelte: ^5.0.0 - svelte@5.54.0: - resolution: {integrity: sha512-TTDxwYnHkova6Wsyj1PGt9TByuWqvMoeY1bQiuAf2DM/JeDSMw7FjRKzk8K/5mJ99vGOKhbCqTDpyAKwjp4igg==} + svelte@5.56.8: + resolution: {integrity: sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==} engines: {node: '>=18'} - sveltekit-superforms@2.30.0: - resolution: {integrity: sha512-EzXD7sHbi7yBU/eNtzVm6P6axcrVM8BArkbiT96Vdx48s5m4KXte/tbbp3UULtEW8Nk9wt2hYkGeq7nDBwVceg==} + sveltekit-superforms@2.30.2: + resolution: {integrity: sha512-6sR70ZfjFMAfdNure/Bu26o1rY32+WGxebko1L8jUFS+qZw5fxIHBPkUaTpNBkOIlJZ/UJwoNCYCm5xK2Z7Kqw==} peerDependencies: '@sveltejs/kit': 1.x || 2.x svelte: 3.x || 4.x || >=5.0.0-next.51 @@ -3302,32 +3090,34 @@ packages: tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} - tailwind-merge@3.5.0: - resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} - tailwind-variants@3.2.2: - resolution: {integrity: sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==} - engines: {node: '>=16.x', pnpm: '>=7.x'} + tailwind-variants@3.3.1: + resolution: {integrity: sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw==} + engines: {node: '>=16.9.x', pnpm: '>=7.x'} peerDependencies: tailwind-merge: '>=3.0.0' tailwindcss: '*' peerDependenciesMeta: tailwind-merge: optional: true + tailwindcss: + optional: true - tailwindcss@4.2.2: - resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} - text-hex@1.0.0: - resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} - tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3335,8 +3125,12 @@ packages: resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} tinyrainbow@3.1.0: @@ -3357,25 +3151,14 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tough-cookie@6.0.1: - resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} engines: {node: '>=16'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@6.0.0: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - - triple-beam@1.4.1: - resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} - engines: {node: '>= 14.0.0'} - ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} @@ -3385,15 +3168,15 @@ packages: peerDependencies: typescript: '>=4.8.4' - ts-deepmerge@7.0.3: - resolution: {integrity: sha512-Du/ZW2RfwV/D4cmA5rXafYjBQVuvu4qGiEEla4EmEHVHgRdx68Gftx7i66jn2bzHPwSVZY36Ae6OuDn9el4ZKA==} + ts-deepmerge@8.0.0: + resolution: {integrity: sha512-133O+10nJmVI8w5xeVZPEv5PIrv7iaUae07wv1aH8XJH95Ur6YIhWAPhPyP1YPlbPS9fCVcNIZTu7m8urRVF0A==} engines: {node: '>=14.13.1'} tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -3408,27 +3191,26 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - - typebox@1.1.6: - resolution: {integrity: sha512-O2iWCF+RboQfDqr6n83eOq0dKCjVchMWklKgdwKFeR01MGTskILHYEFi9n3lQvfuua4CtvG/EJEIg3P8H9eBcw==} + typebox@1.3.6: + resolution: {integrity: sha512-Sc8RA0NCMEFmApHNU9ZMzqcpQj46She44J8ffpLM/bdhLNUZKq7DJumcLcsFx1gRmDfQPgCgOmFFJ7rcnfWNyA==} - typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + undici@8.9.0: + resolution: {integrity: sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==} + engines: {node: '>=22.19.0'} - undici@7.24.4: - resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} - engines: {node: '>=20.18.1'} + unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + + unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} unplugin@2.3.11: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} @@ -3443,16 +3225,12 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@10.0.0: - resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} - hasBin: true - - uuid@13.0.0: - resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true - valibot@1.3.1: - resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==} + valibot@1.4.2: + resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} peerDependencies: typescript: '>=5' peerDependenciesMeta: @@ -3463,18 +3241,14 @@ packages: resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} engines: {node: '>= 0.10'} - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - - vite@8.0.1: - resolution: {integrity: sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==} + vite@8.2.1: + resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.0 - esbuild: ^0.27.0 + '@vitejs/devtools': ^0.4.0 + esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 sass: ^1.70.0 @@ -3518,21 +3292,23 @@ packages: vite: optional: true - vitest@4.1.0: - resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.0 - '@vitest/browser-preview': 4.1.0 - '@vitest/browser-webdriverio': 4.1.0 - '@vitest/ui': 4.1.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -3546,6 +3322,10 @@ packages: optional: true '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -3557,13 +3337,6 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@8.0.1: resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} engines: {node: '>=20'} @@ -3579,8 +3352,9 @@ packages: resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@17.1.0: + resolution: {integrity: sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==} + engines: {node: ^22.14.0 || >=24.0.0} which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} @@ -3592,22 +3366,10 @@ packages: engines: {node: '>=8'} hasBin: true - winston-transport@4.9.0: - resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} - engines: {node: '>= 12.0.0'} - - winston@3.19.0: - resolution: {integrity: sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==} - engines: {node: '>= 12.0.0'} - word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -3627,22 +3389,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3658,42 +3408,35 @@ packages: peerDependencies: zod: ^3.25 || ^4.0.14 - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} snapshots: - '@ark/schema@0.56.0': + '@ark/schema@0.56.2': dependencies: - '@ark/util': 0.56.0 + '@ark/util': 0.56.2 optional: true - '@ark/util@0.56.0': + '@ark/util@0.56.2': optional: true - '@asamuzakjp/css-color@5.0.1': + '@asamuzakjp/css-color@6.0.5': dependencies: - '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - lru-cache: 11.2.7 + lru-cache: 11.5.2 + optional: true - '@asamuzakjp/dom-selector@7.0.3': + '@asamuzakjp/dom-selector@8.3.0': dependencies: - '@asamuzakjp/nwsapi': 2.3.9 bidi-js: 1.0.3 css-tree: 3.2.1 is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.7 - - '@asamuzakjp/nwsapi@2.3.9': {} - - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 + lru-cache: 11.5.2 + optional: true '@babel/helper-string-parser@7.27.1': {} @@ -3703,7 +3446,8 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/runtime@7.29.2': {} + '@babel/runtime@7.29.2': + optional: true '@babel/types@7.29.0': dependencies: @@ -3715,63 +3459,46 @@ snapshots: '@bramus/specificity@2.4.2': dependencies: css-tree: 3.2.1 + optional: true - '@colors/colors@1.6.0': {} - - '@csstools/color-helpers@6.0.2': {} + '@csstools/color-helpers@6.1.0': + optional: true - '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 + optional: true - '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/css-color-parser@4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/color-helpers': 6.0.2 - '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 + optional: true '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': dependencies: '@csstools/css-tokenizer': 4.0.0 + optional: true - '@csstools/css-syntax-patches-for-csstree@1.1.1(css-tree@3.2.1)': + '@csstools/css-syntax-patches-for-csstree@1.1.7(css-tree@3.2.1)': optionalDependencies: css-tree: 3.2.1 + optional: true - '@csstools/css-tokenizer@4.0.0': {} - - '@dabh/diagnostics@2.0.8': - dependencies: - '@so-ric/colorspace': 1.1.6 - enabled: 2.0.0 - kuler: 2.0.0 + '@csstools/css-tokenizer@4.0.0': + optional: true - '@dagrejs/dagre@1.1.8': + '@dagrejs/dagre@2.0.4': dependencies: - '@dagrejs/graphlib': 2.2.4 + '@dagrejs/graphlib': 3.0.4 - '@dagrejs/graphlib@2.2.4': {} + '@dagrejs/graphlib@3.0.4': {} '@drizzle-team/brocli@0.10.2': {} - '@emnapi/core@1.9.0': - dependencies: - '@emnapi/wasi-threads': 1.2.0 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.9.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.0': - dependencies: - tslib: 2.8.1 - optional: true - '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 @@ -3785,7 +3512,7 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/aix-ppc64@0.27.4': + '@esbuild/aix-ppc64@0.28.0': optional: true '@esbuild/android-arm64@0.18.20': @@ -3794,7 +3521,7 @@ snapshots: '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm64@0.27.4': + '@esbuild/android-arm64@0.28.0': optional: true '@esbuild/android-arm@0.18.20': @@ -3803,7 +3530,7 @@ snapshots: '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-arm@0.27.4': + '@esbuild/android-arm@0.28.0': optional: true '@esbuild/android-x64@0.18.20': @@ -3812,7 +3539,7 @@ snapshots: '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/android-x64@0.27.4': + '@esbuild/android-x64@0.28.0': optional: true '@esbuild/darwin-arm64@0.18.20': @@ -3821,7 +3548,7 @@ snapshots: '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.27.4': + '@esbuild/darwin-arm64@0.28.0': optional: true '@esbuild/darwin-x64@0.18.20': @@ -3830,7 +3557,7 @@ snapshots: '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/darwin-x64@0.27.4': + '@esbuild/darwin-x64@0.28.0': optional: true '@esbuild/freebsd-arm64@0.18.20': @@ -3839,7 +3566,7 @@ snapshots: '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.27.4': + '@esbuild/freebsd-arm64@0.28.0': optional: true '@esbuild/freebsd-x64@0.18.20': @@ -3848,7 +3575,7 @@ snapshots: '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.27.4': + '@esbuild/freebsd-x64@0.28.0': optional: true '@esbuild/linux-arm64@0.18.20': @@ -3857,7 +3584,7 @@ snapshots: '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm64@0.27.4': + '@esbuild/linux-arm64@0.28.0': optional: true '@esbuild/linux-arm@0.18.20': @@ -3866,7 +3593,7 @@ snapshots: '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-arm@0.27.4': + '@esbuild/linux-arm@0.28.0': optional: true '@esbuild/linux-ia32@0.18.20': @@ -3875,7 +3602,7 @@ snapshots: '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-ia32@0.27.4': + '@esbuild/linux-ia32@0.28.0': optional: true '@esbuild/linux-loong64@0.18.20': @@ -3884,7 +3611,7 @@ snapshots: '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-loong64@0.27.4': + '@esbuild/linux-loong64@0.28.0': optional: true '@esbuild/linux-mips64el@0.18.20': @@ -3893,7 +3620,7 @@ snapshots: '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-mips64el@0.27.4': + '@esbuild/linux-mips64el@0.28.0': optional: true '@esbuild/linux-ppc64@0.18.20': @@ -3902,7 +3629,7 @@ snapshots: '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-ppc64@0.27.4': + '@esbuild/linux-ppc64@0.28.0': optional: true '@esbuild/linux-riscv64@0.18.20': @@ -3911,7 +3638,7 @@ snapshots: '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.27.4': + '@esbuild/linux-riscv64@0.28.0': optional: true '@esbuild/linux-s390x@0.18.20': @@ -3920,7 +3647,7 @@ snapshots: '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-s390x@0.27.4': + '@esbuild/linux-s390x@0.28.0': optional: true '@esbuild/linux-x64@0.18.20': @@ -3929,13 +3656,13 @@ snapshots: '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/linux-x64@0.27.4': + '@esbuild/linux-x64@0.28.0': optional: true '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.27.4': + '@esbuild/netbsd-arm64@0.28.0': optional: true '@esbuild/netbsd-x64@0.18.20': @@ -3944,13 +3671,13 @@ snapshots: '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.27.4': + '@esbuild/netbsd-x64@0.28.0': optional: true '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.27.4': + '@esbuild/openbsd-arm64@0.28.0': optional: true '@esbuild/openbsd-x64@0.18.20': @@ -3959,13 +3686,13 @@ snapshots: '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.27.4': + '@esbuild/openbsd-x64@0.28.0': optional: true '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.27.4': + '@esbuild/openharmony-arm64@0.28.0': optional: true '@esbuild/sunos-x64@0.18.20': @@ -3974,7 +3701,7 @@ snapshots: '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/sunos-x64@0.27.4': + '@esbuild/sunos-x64@0.28.0': optional: true '@esbuild/win32-arm64@0.18.20': @@ -3983,7 +3710,7 @@ snapshots: '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-arm64@0.27.4': + '@esbuild/win32-arm64@0.28.0': optional: true '@esbuild/win32-ia32@0.18.20': @@ -3992,7 +3719,7 @@ snapshots: '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-ia32@0.27.4': + '@esbuild/win32-ia32@0.28.0': optional: true '@esbuild/win32-x64@0.18.20': @@ -4001,49 +3728,52 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true - '@esbuild/win32-x64@0.27.4': + '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.0.3(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.9.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))': dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.3': + '@eslint/config-array@0.23.5(supports-color@7.2.0)': dependencies: - '@eslint/object-schema': 3.0.3 - debug: 4.4.3 + '@eslint/object-schema': 3.0.5 + debug: 4.4.3(supports-color@7.2.0) minimatch: 10.2.4 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.5.3': + '@eslint/config-helpers@0.7.0': dependencies: - '@eslint/core': 1.1.1 + '@eslint/core': 1.2.1 - '@eslint/core@1.1.1': + '@eslint/core@1.2.1': dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.0.3(jiti@2.6.1))': + '@eslint/js@10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))': optionalDependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) - '@eslint/object-schema@3.0.3': {} + '@eslint/object-schema@3.0.5': {} - '@eslint/plugin-kit@0.6.1': + '@eslint/plugin-kit@0.7.2': dependencies: - '@eslint/core': 1.1.1 + '@eslint/core': 1.2.1 levn: 0.4.1 - '@exodus/bytes@1.15.0': {} + '@exodus/bytes@1.15.1(@noble/hashes@1.8.0)': + optionalDependencies: + '@noble/hashes': 1.8.0 + optional: true '@exodus/schemasafe@1.3.0': optional: true - '@faker-js/faker@10.3.0': {} + '@faker-js/faker@10.5.0': {} '@floating-ui/core@1.7.5': dependencies: @@ -4075,15 +3805,18 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@inlang/paraglide-js@2.15.0': + '@inlang/paraglide-js@2.23.2(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1))': dependencies: '@inlang/recommend-sherlock': 0.2.1 - '@inlang/sdk': 2.8.0 + '@inlang/sdk': 2.10.2 commander: 11.1.0 consola: 3.4.0 json5: 2.2.3 unplugin: 2.3.11 urlpattern-polyfill: 10.1.0 + optionalDependencies: + typescript: 6.0.3 + vite: 8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1) transitivePeerDependencies: - babel-plugin-macros @@ -4091,17 +3824,17 @@ snapshots: dependencies: comment-json: 4.6.2 - '@inlang/sdk@2.8.0': + '@inlang/sdk@2.10.2': dependencies: - '@lix-js/sdk': 0.4.7 + '@lix-js/sdk': 0.4.10 '@sinclair/typebox': 0.31.28 - kysely: 0.27.6 - sqlite-wasm-kysely: 0.3.0(kysely@0.27.6) - uuid: 13.0.0 + kysely: 0.28.16 + sqlite-wasm-kysely: 0.3.0(kysely@0.28.16) + uuid: 14.0.0 transitivePeerDependencies: - babel-plugin-macros - '@internationalized/date@3.12.0': + '@internationalized/date@3.12.3': dependencies: '@swc/helpers': 0.5.19 @@ -4124,62 +3857,56 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@layerstack/svelte-actions@1.0.1-next.12': + '@layerstack/svelte-actions@1.0.1-next.18': dependencies: '@floating-ui/dom': 1.7.6 - '@layerstack/utils': 2.0.0-next.12 + '@layerstack/utils': 2.0.0-next.18 d3-scale: 4.0.2 - '@layerstack/svelte-state@0.1.0-next.17': + '@layerstack/svelte-state@0.1.0-next.23': dependencies: - '@layerstack/utils': 2.0.0-next.12 + '@layerstack/utils': 2.0.0-next.18 - '@layerstack/tailwind@2.0.0-next.15': + '@layerstack/tailwind@2.0.0-next.21': dependencies: - '@layerstack/utils': 2.0.0-next.12 + '@layerstack/utils': 2.0.0-next.18 clsx: 2.1.1 d3-array: 3.2.4 - lodash-es: 4.17.23 - tailwind-merge: 3.5.0 + tailwind-merge: 3.6.0 - '@layerstack/utils@2.0.0-next.12': + '@layerstack/utils@2.0.0-next.18': dependencies: d3-array: 3.2.4 d3-time: 3.1.0 d3-time-format: 4.1.0 - lodash-es: 4.17.23 - '@libsql/client@0.17.0': + '@libsql/client@0.17.4': dependencies: - '@libsql/core': 0.17.0 - '@libsql/hrana-client': 0.9.0 + '@libsql/core': 0.17.4 + '@libsql/hrana-client': 0.10.0 js-base64: 3.7.8 - libsql: 0.5.22 + libsql: 0.5.29 promise-limit: 2.7.0 transitivePeerDependencies: - bufferutil - - encoding - utf-8-validate - '@libsql/core@0.17.0': + '@libsql/core@0.17.4': dependencies: js-base64: 3.7.8 - '@libsql/darwin-arm64@0.5.22': + '@libsql/darwin-arm64@0.5.29': optional: true - '@libsql/darwin-x64@0.5.22': + '@libsql/darwin-x64@0.5.29': optional: true - '@libsql/hrana-client@0.9.0': + '@libsql/hrana-client@0.10.0': dependencies: '@libsql/isomorphic-ws': 0.1.5 - cross-fetch: 4.1.0 js-base64: 3.7.8 - node-fetch: 3.3.2 transitivePeerDependencies: - bufferutil - - encoding - utf-8-validate '@libsql/isomorphic-ws@0.1.5': @@ -4190,54 +3917,51 @@ snapshots: - bufferutil - utf-8-validate - '@libsql/linux-arm-gnueabihf@0.5.22': + '@libsql/linux-arm-gnueabihf@0.5.29': optional: true - '@libsql/linux-arm-musleabihf@0.5.22': + '@libsql/linux-arm-musleabihf@0.5.29': optional: true - '@libsql/linux-arm64-gnu@0.5.22': + '@libsql/linux-arm64-gnu@0.5.29': optional: true - '@libsql/linux-arm64-musl@0.5.22': + '@libsql/linux-arm64-musl@0.5.29': optional: true - '@libsql/linux-x64-gnu@0.5.22': + '@libsql/linux-x64-gnu@0.5.29': optional: true - '@libsql/linux-x64-musl@0.5.22': + '@libsql/linux-x64-musl@0.5.29': optional: true - '@libsql/win32-x64-msvc@0.5.22': + '@libsql/win32-x64-msvc@0.5.29': optional: true - '@lix-js/sdk@0.4.7': + '@lix-js/sdk@0.4.10': dependencies: '@lix-js/server-protocol-schema': 0.1.1 dedent: 1.5.1 human-id: 4.1.3 js-sha256: 0.11.1 - kysely: 0.27.6 - sqlite-wasm-kysely: 0.3.0(kysely@0.27.6) - uuid: 10.0.0 + kysely: 0.28.16 + sqlite-wasm-kysely: 0.3.0(kysely@0.28.16) + uuid: 14.0.0 transitivePeerDependencies: - babel-plugin-macros '@lix-js/server-protocol-schema@0.1.1': {} - '@lucide/svelte@0.577.0(svelte@5.54.0)': - dependencies: - svelte: 5.54.0 - - '@napi-rs/wasm-runtime@1.1.1': + '@lucide/svelte@1.30.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))': dependencies: - '@emnapi/core': 1.9.0 - '@emnapi/runtime': 1.9.0 - '@tybys/wasm-util': 0.10.1 - optional: true + svelte: 5.56.8(@typescript-eslint/types@8.66.0) '@neon-rs/load@0.0.4': {} + '@noble/ciphers@1.3.0': {} + + '@noble/hashes@1.8.0': {} + '@oslojs/asn1@1.0.0': dependencies: '@oslojs/binary': 1.0.0 @@ -4251,71 +3975,66 @@ snapshots: '@oslojs/encoding@1.1.0': {} - '@oxc-project/types@0.120.0': {} + '@oxc-project/types@0.142.0': {} '@polka/url@1.0.0-next.29': {} '@poppinss/macroable@1.1.2': optional: true - '@rolldown/binding-android-arm64@1.0.0-rc.10': + '@rolldown/binding-android-arm64@1.2.2': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.10': + '@rolldown/binding-darwin-arm64@1.2.2': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.10': + '@rolldown/binding-darwin-x64@1.2.2': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.10': + '@rolldown/binding-freebsd-x64@1.2.2': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.10': + '@rolldown/binding-linux-arm-gnueabihf@1.2.2': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.10': + '@rolldown/binding-linux-arm64-gnu@1.2.2': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.10': + '@rolldown/binding-linux-arm64-musl@1.2.2': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.10': + '@rolldown/binding-linux-ppc64-gnu@1.2.2': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.10': + '@rolldown/binding-linux-s390x-gnu@1.2.2': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.10': + '@rolldown/binding-linux-x64-gnu@1.2.2': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.10': + '@rolldown/binding-linux-x64-musl@1.2.2': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.10': + '@rolldown/binding-openharmony-arm64@1.2.2': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.10': - dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@rolldown/binding-win32-arm64-msvc@1.2.2': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.10': + '@rolldown/binding-win32-x64-msvc@1.2.2': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.10': - optional: true - - '@rolldown/pluginutils@1.0.0-rc.10': {} + '@rolldown/pluginutils@1.0.1': {} '@rollup/plugin-commonjs@29.0.2(rollup@4.59.0)': dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.59.0) commondir: 1.0.1 estree-walker: 2.0.2 - fdir: 6.5.0(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.5) is-reference: 1.2.1 magic-string: 0.30.21 - picomatch: 4.0.3 + picomatch: 4.0.5 optionalDependencies: rollup: 4.59.0 @@ -4335,11 +4054,18 @@ snapshots: optionalDependencies: rollup: 4.59.0 + '@rollup/plugin-replace@6.0.3(rollup@4.59.0)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + magic-string: 0.30.21 + optionalDependencies: + rollup: 4.59.0 + '@rollup/pluginutils@5.3.0(rollup@4.59.0)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 - picomatch: 4.0.3 + picomatch: 4.0.5 optionalDependencies: rollup: 4.59.0 @@ -4431,198 +4157,155 @@ snapshots: '@sinclair/typebox@0.31.28': {} - '@so-ric/colorspace@1.1.6': - dependencies: - color: 5.0.3 - text-hex: 1.0.0 - '@sqlite.org/sqlite-wasm@3.48.0-build4': {} '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': + '@sveltejs/acorn-typescript@1.0.11(acorn@8.16.0)': dependencies: acorn: 8.16.0 - '@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))': + '@sveltejs/adapter-node@5.5.7(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))': dependencies: '@rollup/plugin-commonjs': 29.0.2(rollup@4.59.0) '@rollup/plugin-json': 6.1.0(rollup@4.59.0) '@rollup/plugin-node-resolve': 16.0.3(rollup@4.59.0) - '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + '@rollup/plugin-replace': 6.0.3(rollup@4.59.0) + '@sveltejs/kit': 2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)) rollup: 4.59.0 - '@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + '@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1))': dependencies: '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + '@sveltejs/acorn-typescript': 1.0.11(acorn@8.16.0) + '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)) '@types/cookie': 0.6.0 acorn: 8.16.0 cookie: 0.6.0 - devalue: 5.6.4 + devalue: 5.8.1 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 mrmime: 2.0.1 set-cookie-parser: 3.0.1 sirv: 3.0.2 - svelte: 5.54.0 - vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + vite: 8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1) optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 - '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + '@sveltejs/load-config@0.2.2': {} + + '@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 - svelte: 5.54.0 - vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) - vitefu: 1.1.2(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + vite: 8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1) + vitefu: 1.1.2(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)) '@swc/helpers@0.5.19': dependencies: tslib: 2.8.1 - '@tailwindcss/forms@0.5.11(tailwindcss@4.2.2)': + '@tailwindcss/forms@0.5.11(tailwindcss@4.3.3)': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 4.2.2 + tailwindcss: 4.3.3 - '@tailwindcss/node@4.2.2': + '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.20.1 - jiti: 2.6.1 + enhanced-resolve: 5.24.3 + jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.2.2 + tailwindcss: 4.3.3 - '@tailwindcss/oxide-android-arm64@4.2.2': + '@tailwindcss/oxide-android-arm64@4.3.3': optional: true - '@tailwindcss/oxide-darwin-arm64@4.2.2': + '@tailwindcss/oxide-darwin-arm64@4.3.3': optional: true - '@tailwindcss/oxide-darwin-x64@4.2.2': + '@tailwindcss/oxide-darwin-x64@4.3.3': optional: true - '@tailwindcss/oxide-freebsd-x64@4.2.2': + '@tailwindcss/oxide-freebsd-x64@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.2.2': + '@tailwindcss/oxide-linux-x64-musl@4.3.3': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.2.2': + '@tailwindcss/oxide-wasm32-wasi@4.3.3': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': optional: true - '@tailwindcss/oxide@4.2.2': + '@tailwindcss/oxide@4.3.3': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.2.2 - '@tailwindcss/oxide-darwin-arm64': 4.2.2 - '@tailwindcss/oxide-darwin-x64': 4.2.2 - '@tailwindcss/oxide-freebsd-x64': 4.2.2 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 - '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 - '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 - '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 - '@tailwindcss/oxide-linux-x64-musl': 4.2.2 - '@tailwindcss/oxide-wasm32-wasi': 4.2.2 - '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 - '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - - '@tailwindcss/typography@0.5.19(tailwindcss@4.2.2)': + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/typography@0.5.20(tailwindcss@4.3.3)': dependencies: postcss-selector-parser: 6.0.10 - tailwindcss: 4.2.2 + tailwindcss: 4.3.3 - '@tailwindcss/vite@4.2.2(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + '@tailwindcss/vite@4.3.3(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1))': dependencies: - '@tailwindcss/node': 4.2.2 - '@tailwindcss/oxide': 4.2.2 - tailwindcss: 4.2.2 - vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1) '@tanstack/table-core@8.21.3': {} - '@testing-library/dom@10.4.1': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/runtime': 7.29.2 - '@types/aria-query': 5.0.4 - aria-query: 5.3.0 - dom-accessibility-api: 0.5.16 - lz-string: 1.5.0 - picocolors: 1.1.1 - pretty-format: 27.5.1 - - '@testing-library/svelte-core@1.0.0(svelte@5.54.0)': - dependencies: - svelte: 5.54.0 - - '@testing-library/svelte@5.3.1(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))(vitest@4.1.0(@types/node@25.5.0)(jsdom@29.0.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))': - dependencies: - '@testing-library/dom': 10.4.1 - '@testing-library/svelte-core': 1.0.0(svelte@5.54.0) - svelte: 5.54.0 - optionalDependencies: - vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) - vitest: 4.1.0(@types/node@25.5.0)(jsdom@29.0.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) - - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@types/aria-query@5.0.4': {} - '@types/bcrypt@6.0.0': dependencies: - '@types/node': 25.5.0 - - '@types/body-parser@1.19.6': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 25.5.0 + '@types/node': 26.1.2 '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/connect@3.4.38': - dependencies: - '@types/node': 25.5.0 - '@types/cookie@0.6.0': {} - '@types/cookiejar@2.1.5': {} + '@types/d3-array@3.2.2': {} - '@types/cors@2.8.19': + '@types/d3-contour@3.0.6': dependencies: - '@types/node': 25.5.0 - - '@types/d3-array@3.2.2': {} + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 '@types/d3-path@3.1.1': {} @@ -4642,67 +4325,25 @@ snapshots: '@types/estree@1.0.8': {} - '@types/express-serve-static-core@5.1.1': - dependencies: - '@types/node': 25.5.0 - '@types/qs': 6.15.0 - '@types/range-parser': 1.2.7 - '@types/send': 1.2.1 - - '@types/express@5.0.6': - dependencies: - '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 5.1.1 - '@types/serve-static': 2.2.0 - - '@types/http-errors@2.0.5': {} + '@types/geojson@7946.0.16': {} '@types/json-schema@7.0.15': {} - '@types/methods@1.1.4': {} - - '@types/multer@2.1.0': - dependencies: - '@types/express': 5.0.6 - '@types/node-cron@3.0.11': {} - '@types/node@25.5.0': - dependencies: - undici-types: 7.18.2 - - '@types/nodemailer@7.0.11': - dependencies: - '@types/node': 25.5.0 - - '@types/qs@6.15.0': {} - - '@types/range-parser@1.2.7': {} - - '@types/resolve@1.20.2': {} - - '@types/send@1.2.1': + '@types/node@26.1.2': dependencies: - '@types/node': 25.5.0 + undici-types: 8.3.0 - '@types/serve-static@2.2.0': + '@types/nodemailer@8.0.1': dependencies: - '@types/http-errors': 2.0.5 - '@types/node': 25.5.0 + '@types/node': 26.1.2 - '@types/superagent@8.1.9': + '@types/pdfkit@0.17.6': dependencies: - '@types/cookiejar': 2.1.5 - '@types/methods': 1.1.4 - '@types/node': 25.5.0 - form-data: 4.0.5 + '@types/node': 26.1.2 - '@types/supertest@7.2.0': - dependencies: - '@types/methods': 1.1.4 - '@types/superagent': 8.1.9 - - '@types/triple-beam@1.3.5': {} + '@types/resolve@1.20.2': {} '@types/trusted-types@2.0.7': {} @@ -4711,7 +4352,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.5.0 + '@types/node': 26.1.2 '@typeschema/class-validator@0.3.0(@types/json-schema@7.0.15)(class-validator@0.14.4)': dependencies: @@ -4727,100 +4368,100 @@ snapshots: '@types/json-schema': 7.0.15 optional: true - '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/type-utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 - eslint: 10.0.3(jiti@2.6.1) + '@typescript-eslint/parser': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/type-utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.66.0 + eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.57.1 - debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) - typescript: 5.9.3 + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.66.0 + debug: 4.4.3(supports-color@7.2.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.57.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.66.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - debug: 4.4.3 - typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3) + '@typescript-eslint/types': 8.66.0 + debug: 4.4.3(supports-color@7.2.0) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.57.1': + '@typescript-eslint/scope-manager@8.66.0': dependencies: - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/visitor-keys': 8.57.1 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/visitor-keys': 8.66.0 - '@typescript-eslint/tsconfig-utils@8.57.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.66.0(typescript@6.0.3)': dependencies: - typescript: 5.9.3 + typescript: 6.0.3 - '@typescript-eslint/type-utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3 - eslint: 10.0.3(jiti@2.6.1) - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@6.0.3) + debug: 4.4.3(supports-color@7.2.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.57.1': {} + '@typescript-eslint/types@8.66.0': {} - '@typescript-eslint/typescript-estree@8.57.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.66.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.57.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.57.1(typescript@5.9.3) - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/visitor-keys': 8.57.1 - debug: 4.4.3 - minimatch: 10.2.4 + '@typescript-eslint/project-service': 8.66.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3) + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/visitor-keys': 8.66.0 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.5 semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.5.0(typescript@5.9.3) - typescript: 5.9.3 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.57.1 - '@typescript-eslint/types': 8.57.1 - '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) - eslint: 10.0.3(jiti@2.6.1) - typescript: 5.9.3 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) + '@typescript-eslint/scope-manager': 8.66.0 + '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.57.1': + '@typescript-eslint/visitor-keys@8.66.0': dependencies: - '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/types': 8.66.0 eslint-visitor-keys: 5.0.1 - '@valibot/to-json-schema@1.6.0(valibot@1.3.1(typescript@5.9.3))': + '@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@6.0.3))': dependencies: - valibot: 1.3.1(typescript@5.9.3) + valibot: 1.4.2(typescript@6.0.3) optional: true '@vinejs/compiler@3.0.0': @@ -4838,10 +4479,10 @@ snapshots: validator: 13.15.26 optional: true - '@vitest/coverage-v8@4.1.0(vitest@4.1.0(@types/node@25.5.0)(jsdom@29.0.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))': + '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.0 + '@vitest/utils': 4.1.10 ast-v8-to-istanbul: 1.0.0 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 @@ -4850,46 +4491,46 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.0(@types/node@25.5.0)(jsdom@29.0.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + vitest: 4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.0(@noble/hashes@1.8.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)) - '@vitest/expect@4.1.0': + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.0 - '@vitest/utils': 4.1.0 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.0(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': + '@vitest/mocker@4.1.10(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1))': dependencies: - '@vitest/spy': 4.1.0 + '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1) - '@vitest/pretty-format@4.1.0': + '@vitest/pretty-format@4.1.10': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.0': + '@vitest/runner@4.1.10': dependencies: - '@vitest/utils': 4.1.0 + '@vitest/utils': 4.1.10 pathe: 2.0.3 - '@vitest/snapshot@4.1.0': + '@vitest/snapshot@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.0 - '@vitest/utils': 4.1.0 + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.0': {} + '@vitest/spy@4.1.10': {} - '@vitest/utils@4.1.0': + '@vitest/utils@4.1.10': dependencies: - '@vitest/pretty-format': 4.1.0 + '@vitest/pretty-format': 4.1.10 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 @@ -4906,32 +4547,18 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ansi-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@5.2.0: {} - - append-field@1.0.0: {} - - aria-query@5.3.0: - dependencies: - dequal: 2.0.3 - aria-query@5.3.1: {} - arkregex@0.0.5: + arkregex@0.0.8: dependencies: - '@ark/util': 0.56.0 + '@ark/util': 0.56.2 optional: true - arktype@2.2.0: + arktype@2.2.3: dependencies: - '@ark/schema': 0.56.0 - '@ark/util': 0.56.0 - arkregex: 0.0.5 + '@ark/schema': 0.56.2 + '@ark/util': 0.56.2 + arkregex: 0.0.8 optional: true array-timsort@1.0.3: {} @@ -4944,14 +4571,14 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 - async@3.2.6: {} - - asynckit@0.4.0: {} - axobject-query@4.1.0: {} balanced-match@4.0.4: {} + base64-js@0.0.8: {} + + base64-js@1.5.1: {} + bcrypt@6.0.0: dependencies: node-addon-api: 8.6.0 @@ -4960,16 +4587,17 @@ snapshots: bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 + optional: true - bits-ui@2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0): + bits-ui@2.18.1(@internationalized/date@3.12.3)(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: '@floating-ui/core': 1.7.5 '@floating-ui/dom': 1.7.6 - '@internationalized/date': 3.12.0 + '@internationalized/date': 3.12.3 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0) - svelte: 5.54.0 - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0) + runed: 0.35.1(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) tabbable: 6.4.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -4978,27 +4606,25 @@ snapshots: dependencies: balanced-match: 4.0.4 - buffer-from@1.1.2: {} + brace-expansion@5.0.8: + dependencies: + balanced-match: 4.0.4 - busboy@1.6.0: + brotli@1.3.3: dependencies: - streamsearch: 1.1.0 + base64-js: 1.5.1 - call-bind-apply-helpers@1.0.2: + browserify-zlib@0.2.0: dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 + pako: 1.0.11 + + buffer-from@1.1.2: {} camelcase@8.0.0: optional: true chai@6.2.2: {} - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -5010,41 +4636,12 @@ snapshots: validator: 13.15.26 optional: true - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 + clone@2.1.2: {} clsx@2.1.1: {} - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-convert@3.1.3: - dependencies: - color-name: 2.1.0 - - color-name@1.1.4: {} - - color-name@2.1.0: {} - - color-string@2.1.4: - dependencies: - color-name: 2.1.0 - - color@5.0.3: - dependencies: - color-convert: 3.1.3 - color-string: 2.1.4 - colord@2.9.3: {} - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - commander@11.1.0: {} commander@7.2.0: {} @@ -5056,39 +4653,12 @@ snapshots: commondir@1.0.1: {} - concat-stream@2.0.0: - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 3.6.2 - typedarray: 0.0.6 - - concurrently@9.2.1: - dependencies: - chalk: 4.1.2 - rxjs: 7.8.2 - shell-quote: 1.8.3 - supports-color: 8.1.1 - tree-kill: 1.2.2 - yargs: 17.7.2 - consola@3.4.0: {} convert-source-map@2.0.0: {} cookie@0.6.0: {} - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - - cross-fetch@4.1.0: - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5099,10 +4669,11 @@ snapshots: dependencies: mdn-data: 2.27.1 source-map-js: 1.2.1 + optional: true cssesc@3.0.0: {} - csv-parse@6.2.0: {} + csv-parse@7.0.2: {} currency-codes@2.2.0: dependencies: @@ -5117,8 +4688,16 @@ snapshots: dependencies: internmap: 2.0.3 + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + d3-color@3.1.0: {} + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-delaunay@6.0.4: dependencies: delaunator: 5.0.1 @@ -5162,6 +4741,8 @@ snapshots: d3-path@3.1.0: {} + d3-polygon@3.0.1: {} + d3-quadtree@3.0.1: {} d3-random@3.0.1: {} @@ -5209,29 +4790,31 @@ snapshots: d3-delaunay: 6.0.4 d3-scale: 4.0.2 - data-uri-to-buffer@4.0.1: {} - - data-urls@7.0.0: + data-urls@7.0.0(@noble/hashes@1.8.0): dependencies: whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 + whatwg-url: 16.0.1(@noble/hashes@1.8.0) transitivePeerDependencies: - '@noble/hashes' + optional: true - date-fns-tz@3.2.0(date-fns@4.1.0): + date-fns-tz@3.2.0(date-fns@4.4.0): dependencies: - date-fns: 4.1.0 + date-fns: 4.4.0 - date-fns@4.1.0: {} + date-fns@4.4.0: {} dayjs@1.11.20: optional: true - debug@4.4.3: + debug@4.4.3(supports-color@7.2.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 - decimal.js@10.6.0: {} + decimal.js@10.6.0: + optional: true dedent@1.5.1: {} @@ -5243,75 +4826,47 @@ snapshots: dependencies: robust-predicates: 3.0.2 - delayed-stream@1.0.0: {} - dequal@2.0.3: {} detect-libc@2.0.2: {} detect-libc@2.1.2: {} - devalue@5.6.4: {} + devalue@5.8.1: {} + + dfa@1.2.0: {} dlv@1.1.3: optional: true - dom-accessibility-api@0.5.16: {} - - dotenv@17.3.1: {} - drizzle-kit@0.31.10: dependencies: '@drizzle-team/brocli': 0.10.2 '@esbuild-kit/esm-loader': 2.6.5 esbuild: 0.25.12 - tsx: 4.21.0 + tsx: 4.23.1 - drizzle-orm@0.45.1(@libsql/client@0.17.0)(kysely@0.27.6): + drizzle-orm@0.45.2(@libsql/client@0.17.4)(kysely@0.28.16): optionalDependencies: - '@libsql/client': 0.17.0 - kysely: 0.27.6 - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 + '@libsql/client': 0.17.4 + kysely: 0.28.16 - effect@3.20.0: + effect@3.21.4: dependencies: '@standard-schema/spec': 1.1.0 fast-check: 3.23.2 optional: true - emoji-regex@8.0.0: {} - - enabled@2.0.0: {} - - enhanced-resolve@5.20.1: + enhanced-resolve@5.24.3: dependencies: graceful-fs: 4.2.11 - tapable: 2.3.0 - - entities@6.0.1: {} + tapable: 2.3.3 - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} + entities@8.0.0: + optional: true es-module-lexer@2.0.0: {} - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - esbuild@0.18.20: optionalDependencies: '@esbuild/android-arm': 0.18.20 @@ -5366,66 +4921,64 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 - esbuild@0.27.4: + esbuild@0.28.0: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.4 - '@esbuild/android-arm': 0.27.4 - '@esbuild/android-arm64': 0.27.4 - '@esbuild/android-x64': 0.27.4 - '@esbuild/darwin-arm64': 0.27.4 - '@esbuild/darwin-x64': 0.27.4 - '@esbuild/freebsd-arm64': 0.27.4 - '@esbuild/freebsd-x64': 0.27.4 - '@esbuild/linux-arm': 0.27.4 - '@esbuild/linux-arm64': 0.27.4 - '@esbuild/linux-ia32': 0.27.4 - '@esbuild/linux-loong64': 0.27.4 - '@esbuild/linux-mips64el': 0.27.4 - '@esbuild/linux-ppc64': 0.27.4 - '@esbuild/linux-riscv64': 0.27.4 - '@esbuild/linux-s390x': 0.27.4 - '@esbuild/linux-x64': 0.27.4 - '@esbuild/netbsd-arm64': 0.27.4 - '@esbuild/netbsd-x64': 0.27.4 - '@esbuild/openbsd-arm64': 0.27.4 - '@esbuild/openbsd-x64': 0.27.4 - '@esbuild/openharmony-arm64': 0.27.4 - '@esbuild/sunos-x64': 0.27.4 - '@esbuild/win32-arm64': 0.27.4 - '@esbuild/win32-ia32': 0.27.4 - '@esbuild/win32-x64': 0.27.4 - - escalade@3.2.0: {} + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) - eslint-plugin-svelte@3.15.2(eslint@10.0.3(jiti@2.6.1))(svelte@5.54.0): + eslint-plugin-svelte@3.22.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) esutils: 2.0.3 globals: 16.5.0 known-css-properties: 0.37.0 - postcss: 8.5.8 - postcss-load-config: 3.1.4(postcss@8.5.8) - postcss-safe-parser: 7.0.1(postcss@8.5.8) + postcss: 8.5.16 + postcss-load-config: 3.1.4(postcss@8.5.16) + postcss-safe-parser: 7.0.1(postcss@8.5.16) semver: 7.7.4 - svelte-eslint-parser: 1.6.0(svelte@5.54.0) + svelte-eslint-parser: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) optionalDependencies: - svelte: 5.54.0 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) transitivePeerDependencies: - ts-node - eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1)): + eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)): dependencies: - eslint: 10.0.3(jiti@2.6.1) + eslint: 10.8.0(jiti@2.7.0)(supports-color@7.2.0) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) eslint-scope@8.4.0: dependencies: @@ -5445,21 +4998,21 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.0.3(jiti@2.6.1): + eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.8.0(jiti@2.7.0)(supports-color@7.2.0)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.3 - '@eslint/config-helpers': 0.5.3 - '@eslint/core': 1.1.1 - '@eslint/plugin-kit': 0.6.1 + '@eslint/config-array': 0.23.5(supports-color@7.2.0) + '@eslint/config-helpers': 0.7.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 ajv: 6.14.0 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 @@ -5474,11 +5027,11 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.4 + minimatch: 10.2.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 2.6.1 + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -5502,10 +5055,11 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.2.4: + esrap@2.2.13(@typescript-eslint/types@8.66.0): dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@typescript-eslint/types': 8.57.1 + optionalDependencies: + '@typescript-eslint/types': 8.66.0 esrecurse@4.3.0: dependencies: @@ -5534,16 +5088,9 @@ snapshots: fast-levenshtein@2.0.6: {} - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: - picomatch: 4.0.3 - - fecha@4.2.3: {} - - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 + picomatch: 4.0.5 file-entry-cache@8.0.0: dependencies: @@ -5563,51 +5110,29 @@ snapshots: flatted@3.4.2: {} - fn.name@1.1.0: {} - - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.2 - mime-types: 2.1.35 - - formdata-polyfill@4.0.10: + fontkit@2.0.4: dependencies: - fetch-blob: 3.2.0 + '@swc/helpers': 0.5.19 + brotli: 1.3.3 + clone: 2.1.2 + dfa: 1.2.0 + fast-deep-equal: 3.1.3 + restructure: 3.0.2 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 - formsnap@2.0.1(svelte@5.54.0)(sveltekit-superforms@2.30.0(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@types/json-schema@7.0.15)(svelte@5.54.0)(typescript@5.9.3)): + formsnap@2.0.1(svelte@5.56.8(@typescript-eslint/types@8.66.0))(sveltekit-superforms@2.30.2(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(@types/json-schema@7.0.15)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)): dependencies: - svelte: 5.54.0 - svelte-toolbelt: 0.5.0(svelte@5.54.0) - sveltekit-superforms: 2.30.0(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@types/json-schema@7.0.15)(svelte@5.54.0)(typescript@5.9.3) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + svelte-toolbelt: 0.5.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + sveltekit-superforms: 2.30.2(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(@types/json-schema@7.0.15)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3) fsevents@2.3.3: optional: true function-bind@1.1.2: {} - get-caller-file@2.0.5: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - get-tsconfig@4.13.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -5618,31 +5143,22 @@ snapshots: globals@16.5.0: {} - globals@17.4.0: {} - - gopd@1.2.0: {} + globals@17.9.0: {} graceful-fs@4.2.11: {} has-flag@4.0.0: {} - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - hasown@2.0.2: dependencies: function-bind: 1.1.2 - helmet@8.1.0: {} - - html-encoding-sniffer@6.0.0: + html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0): dependencies: - '@exodus/bytes': 1.15.0 + '@exodus/bytes': 1.15.1(@noble/hashes@1.8.0) transitivePeerDependencies: - '@noble/hashes' + optional: true html-escaper@2.0.2: {} @@ -5658,8 +5174,6 @@ snapshots: imurmurhash@0.1.4: {} - inherits@2.0.4: {} - inline-style-parser@0.2.7: {} internmap@1.0.1: {} @@ -5672,15 +5186,14 @@ snapshots: is-extglob@2.1.1: {} - is-fullwidth-code-point@3.0.0: {} - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 is-module@1.0.0: {} - is-potential-custom-element-name@1.0.1: {} + is-potential-custom-element-name@1.0.1: + optional: true is-reference@1.2.1: dependencies: @@ -5690,8 +5203,6 @@ snapshots: dependencies: '@types/estree': 1.0.8 - is-stream@2.0.1: {} - isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -5707,9 +5218,9 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jiti@2.6.1: {} + jiti@2.7.0: {} - joi@17.13.3: + joi@17.13.4: dependencies: '@hapi/hoek': 9.3.0 '@hapi/topo': 5.1.0 @@ -5720,37 +5231,38 @@ snapshots: js-base64@3.7.8: {} + js-md5@0.8.3: {} + js-sha256@0.11.1: {} js-tokens@10.0.0: {} - js-tokens@4.0.0: {} - - jsdom@29.0.0: + jsdom@30.0.0(@noble/hashes@1.8.0): dependencies: - '@asamuzakjp/css-color': 5.0.1 - '@asamuzakjp/dom-selector': 7.0.3 + '@asamuzakjp/css-color': 6.0.5 + '@asamuzakjp/dom-selector': 8.3.0 '@bramus/specificity': 2.4.2 - '@csstools/css-syntax-patches-for-csstree': 1.1.1(css-tree@3.2.1) - '@exodus/bytes': 1.15.0 + '@csstools/css-syntax-patches-for-csstree': 1.1.7(css-tree@3.2.1) + '@exodus/bytes': 1.15.1(@noble/hashes@1.8.0) css-tree: 3.2.1 - data-urls: 7.0.0 + data-urls: 7.0.0(@noble/hashes@1.8.0) decimal.js: 10.6.0 - html-encoding-sniffer: 6.0.0 + html-encoding-sniffer: 6.0.0(@noble/hashes@1.8.0) is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.7 - parse5: 8.0.0 + lru-cache: 11.5.2 + parse5: 8.0.1 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 6.0.1 - undici: 7.24.4 + tough-cookie: 6.0.2 + undici: 8.9.0 w3c-xmlserializer: 5.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1 + whatwg-url: 17.1.0(@noble/hashes@1.8.0) xml-name-validator: 5.0.0 transitivePeerDependencies: - '@noble/hashes' + optional: true json-buffer@3.0.1: {} @@ -5774,19 +5286,20 @@ snapshots: known-css-properties@0.37.0: {} - kuler@2.0.0: {} - - kysely@0.27.6: {} + kysely@0.28.16: {} - layerchart@2.0.0-next.27(svelte@5.54.0): + layerchart@2.1.0(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(zod@4.4.3): dependencies: - '@dagrejs/dagre': 1.1.8 - '@layerstack/svelte-actions': 1.0.1-next.12 - '@layerstack/svelte-state': 0.1.0-next.17 - '@layerstack/tailwind': 2.0.0-next.15 - '@layerstack/utils': 2.0.0-next.12 + '@dagrejs/dagre': 2.0.4 + '@layerstack/svelte-actions': 1.0.1-next.18 + '@layerstack/svelte-state': 0.1.0-next.23 + '@layerstack/tailwind': 2.0.0-next.21 + '@layerstack/utils': 2.0.0-next.18 + '@types/d3-contour': 3.0.6 d3-array: 3.2.4 + d3-chord: 3.0.1 d3-color: 3.1.0 + d3-contour: 4.0.2 d3-delaunay: 6.0.4 d3-dsv: 3.0.1 d3-force: 3.0.0 @@ -5796,6 +5309,7 @@ snapshots: d3-interpolate: 3.0.1 d3-interpolate-path: 2.3.0 d3-path: 3.1.0 + d3-polygon: 3.0.1 d3-quadtree: 3.0.1 d3-random: 3.0.1 d3-sankey: 0.12.3 @@ -5804,10 +5318,12 @@ snapshots: d3-shape: 3.2.0 d3-tile: 1.0.0 d3-time: 3.1.0 - lodash-es: 4.17.23 memoize: 10.2.0 - runed: 0.28.0(svelte@5.54.0) - svelte: 5.54.0 + runed: 0.37.1(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(zod@4.4.3) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + transitivePeerDependencies: + - '@sveltejs/kit' + - zod levn@0.4.1: dependencies: @@ -5817,54 +5333,87 @@ snapshots: libphonenumber-js@1.12.40: optional: true - libsql@0.5.22: + libsql@0.5.29: dependencies: '@neon-rs/load': 0.0.4 detect-libc: 2.0.2 optionalDependencies: - '@libsql/darwin-arm64': 0.5.22 - '@libsql/darwin-x64': 0.5.22 - '@libsql/linux-arm-gnueabihf': 0.5.22 - '@libsql/linux-arm-musleabihf': 0.5.22 - '@libsql/linux-arm64-gnu': 0.5.22 - '@libsql/linux-arm64-musl': 0.5.22 - '@libsql/linux-x64-gnu': 0.5.22 - '@libsql/linux-x64-musl': 0.5.22 - '@libsql/win32-x64-msvc': 0.5.22 + '@libsql/darwin-arm64': 0.5.29 + '@libsql/darwin-x64': 0.5.29 + '@libsql/linux-arm-gnueabihf': 0.5.29 + '@libsql/linux-arm-musleabihf': 0.5.29 + '@libsql/linux-arm64-gnu': 0.5.29 + '@libsql/linux-arm64-musl': 0.5.29 + '@libsql/linux-x64-gnu': 0.5.29 + '@libsql/linux-x64-musl': 0.5.29 + '@libsql/win32-x64-msvc': 0.5.29 lightningcss-android-arm64@1.32.0: optional: true + lightningcss-android-arm64@1.33.0: + optional: true + lightningcss-darwin-arm64@1.32.0: optional: true + lightningcss-darwin-arm64@1.33.0: + optional: true + lightningcss-darwin-x64@1.32.0: optional: true + lightningcss-darwin-x64@1.33.0: + optional: true + lightningcss-freebsd-x64@1.32.0: optional: true + lightningcss-freebsd-x64@1.33.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: optional: true + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + lightningcss-linux-arm64-gnu@1.32.0: optional: true + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + lightningcss-linux-arm64-musl@1.32.0: optional: true + lightningcss-linux-arm64-musl@1.33.0: + optional: true + lightningcss-linux-x64-gnu@1.32.0: optional: true + lightningcss-linux-x64-gnu@1.33.0: + optional: true + lightningcss-linux-x64-musl@1.32.0: optional: true + lightningcss-linux-x64-musl@1.33.0: + optional: true + lightningcss-win32-arm64-msvc@1.32.0: optional: true + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + lightningcss-win32-x64-msvc@1.32.0: optional: true + lightningcss-win32-x64-msvc@1.33.0: + optional: true + lightningcss@1.32.0: dependencies: detect-libc: 2.1.2 @@ -5881,26 +5430,37 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + lilconfig@2.1.0: {} + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + locate-character@3.0.0: {} locate-path@6.0.0: dependencies: p-locate: 5.0.0 - lodash-es@4.17.23: {} - - logform@2.7.0: - dependencies: - '@colors/colors': 1.6.0 - '@types/triple-beam': 1.3.5 - fecha: 4.2.3 - ms: 2.1.3 - safe-stable-stringify: 2.5.0 - triple-beam: 1.4.1 - - lru-cache@11.2.7: {} + lru-cache@11.5.2: + optional: true lz-string@1.5.0: {} @@ -5918,11 +5478,8 @@ snapshots: dependencies: semver: 7.7.4 - math-intrinsics@1.1.0: {} - - mdn-data@2.27.1: {} - - media-typer@0.3.0: {} + mdn-data@2.27.1: + optional: true memoize-weak@1.0.2: {} @@ -5930,12 +5487,6 @@ snapshots: dependencies: mimic-function: 5.0.1 - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - mimic-function@5.0.1: {} mini-svg-data-uri@1.4.4: {} @@ -5944,11 +5495,15 @@ snapshots: dependencies: brace-expansion: 5.0.4 - mode-watcher@1.1.0(svelte@5.54.0): + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.8 + + mode-watcher@1.1.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: - runed: 0.25.0(svelte@5.54.0) - svelte: 5.54.0 - svelte-toolbelt: 0.7.1(svelte@5.54.0) + runed: 0.25.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + svelte-toolbelt: 0.7.1(svelte@5.56.8(@typescript-eslint/types@8.66.0)) mri@1.2.0: {} @@ -5956,50 +5511,29 @@ snapshots: ms@2.1.3: {} - multer@2.1.1: - dependencies: - append-field: 1.0.0 - busboy: 1.6.0 - concat-stream: 2.0.0 - type-is: 1.6.18 - nanoid@3.3.11: {} - natural-compare@1.4.0: {} + nanoid@3.3.15: {} - node-addon-api@8.6.0: {} - - node-cron@4.2.1: {} + nanoid@3.3.16: {} - node-domexception@1.0.0: {} + natural-compare@1.4.0: {} - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 + node-addon-api@8.6.0: {} - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 + node-cron@4.6.0: {} node-gyp-build@4.8.4: {} - nodemailer@8.0.3: {} + nodemailer@9.0.5: {} normalize-url@8.1.1: optional: true nub@0.0.0: {} - object-assign@4.1.1: {} - obug@2.1.1: {} - one-time@1.0.0: - dependencies: - fn.name: 1.1.0 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -6017,9 +5551,14 @@ snapshots: dependencies: p-limit: 3.1.0 - parse5@8.0.0: + pako@0.2.9: {} + + pako@1.0.11: {} + + parse5@8.0.1: dependencies: - entities: 6.0.1 + entities: 8.0.0 + optional: true path-exists@4.0.0: {} @@ -6029,24 +5568,39 @@ snapshots: pathe@2.0.3: {} + pdfkit@0.19.1: + dependencies: + '@noble/ciphers': 1.3.0 + '@noble/hashes': 1.8.0 + fontkit: 2.0.4 + js-md5: 0.8.3 + linebreak: 1.1.0 + png-js: 1.1.0 + picocolors@1.1.1: {} - picomatch@4.0.3: {} + picomatch@4.0.4: {} + + picomatch@4.0.5: {} + + png-js@1.1.0: + dependencies: + browserify-zlib: 0.2.0 - postcss-load-config@3.1.4(postcss@8.5.8): + postcss-load-config@3.1.4(postcss@8.5.16): dependencies: lilconfig: 2.1.0 yaml: 1.10.2 optionalDependencies: - postcss: 8.5.8 + postcss: 8.5.16 - postcss-safe-parser@7.0.1(postcss@8.5.8): + postcss-safe-parser@7.0.1(postcss@8.5.16): dependencies: - postcss: 8.5.8 + postcss: 8.5.16 - postcss-scss@4.0.9(postcss@8.5.8): + postcss-scss@4.0.9(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-selector-parser@6.0.10: dependencies: @@ -6058,32 +5612,38 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.5.8: + postcss@8.5.14: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.16: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.25: + dependencies: + nanoid: 3.3.16 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.54.0): + prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: - prettier: 3.8.1 - svelte: 5.54.0 + prettier: 3.9.6 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - prettier-plugin-tailwindcss@0.7.2(prettier-plugin-svelte@3.5.1(prettier@3.8.1)(svelte@5.54.0))(prettier@3.8.1): + prettier-plugin-tailwindcss@0.8.1(prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)))(prettier@3.9.6): dependencies: - prettier: 3.8.1 + prettier: 3.9.6 optionalDependencies: - prettier-plugin-svelte: 3.5.1(prettier@3.8.1)(svelte@5.54.0) - - prettier@3.8.1: {} + prettier-plugin-svelte: 4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.66.0)) - pretty-format@27.5.1: - dependencies: - ansi-regex: 5.0.1 - ansi-styles: 5.2.0 - react-is: 17.0.2 + prettier@3.9.6: {} promise-limit@2.7.0: {} @@ -6095,19 +5655,10 @@ snapshots: pure-rand@6.1.0: optional: true - react-is@17.0.2: {} - - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - readdirp@4.1.2: {} - require-directory@2.1.1: {} - - require-from-string@2.0.2: {} + require-from-string@2.0.2: + optional: true resolve-pkg-maps@1.0.0: {} @@ -6117,28 +5668,29 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + restructure@3.0.2: {} + robust-predicates@3.0.2: {} - rolldown@1.0.0-rc.10: + rolldown@1.2.2: dependencies: - '@oxc-project/types': 0.120.0 - '@rolldown/pluginutils': 1.0.0-rc.10 + '@oxc-project/types': 0.142.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.10 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.10 - '@rolldown/binding-darwin-x64': 1.0.0-rc.10 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.10 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.10 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.10 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.10 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.10 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.10 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.10 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.10 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.10 + '@rolldown/binding-android-arm64': 1.2.2 + '@rolldown/binding-darwin-arm64': 1.2.2 + '@rolldown/binding-darwin-x64': 1.2.2 + '@rolldown/binding-freebsd-x64': 1.2.2 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.2 + '@rolldown/binding-linux-arm64-gnu': 1.2.2 + '@rolldown/binding-linux-arm64-musl': 1.2.2 + '@rolldown/binding-linux-ppc64-gnu': 1.2.2 + '@rolldown/binding-linux-s390x-gnu': 1.2.2 + '@rolldown/binding-linux-x64-gnu': 1.2.2 + '@rolldown/binding-linux-x64-musl': 1.2.2 + '@rolldown/binding-openharmony-arm64': 1.2.2 + '@rolldown/binding-win32-arm64-msvc': 1.2.2 + '@rolldown/binding-win32-x64-msvc': 1.2.2 rollup@4.59.0: dependencies: @@ -6171,49 +5723,52 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 - runed@0.23.4(svelte@5.54.0): + runed@0.23.4(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: esm-env: 1.2.2 - svelte: 5.54.0 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - runed@0.25.0(svelte@5.54.0): + runed@0.25.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: esm-env: 1.2.2 - svelte: 5.54.0 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - runed@0.28.0(svelte@5.54.0): + runed@0.28.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: esm-env: 1.2.2 - svelte: 5.54.0 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - runed@0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0): + runed@0.35.1(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 - svelte: 5.54.0 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) optionalDependencies: - '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) + '@sveltejs/kit': 2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)) - rw@1.3.3: {} - - rxjs@7.8.2: + runed@0.37.1(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(zod@4.4.3): dependencies: - tslib: 2.8.1 + dequal: 2.0.3 + esm-env: 1.2.2 + lz-string: 1.5.0 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + optionalDependencies: + '@sveltejs/kit': 2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)) + zod: 4.4.3 + + rw@1.3.3: {} sade@1.8.1: dependencies: mri: 1.2.0 - safe-buffer@5.2.1: {} - - safe-stable-stringify@2.5.0: {} - safer-buffer@2.1.2: {} saxes@6.0.0: dependencies: xmlchars: 2.2.0 + optional: true semver@7.7.4: {} @@ -6225,8 +5780,6 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.3: {} - siginfo@2.0.0: {} sirv@3.0.2: @@ -6244,33 +5797,15 @@ snapshots: source-map@0.6.1: {} - sqlite-wasm-kysely@0.3.0(kysely@0.27.6): + sqlite-wasm-kysely@0.3.0(kysely@0.28.16): dependencies: '@sqlite.org/sqlite-wasm': 3.48.0-build4 - kysely: 0.27.6 - - stack-trace@0.0.10: {} + kysely: 0.28.16 stackback@0.0.2: {} std-env@4.0.0: {} - streamsearch@1.1.0: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - style-to-object@1.0.14: dependencies: inline-style-parser: 0.2.7 @@ -6282,192 +5817,197 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - supports-preserve-symlinks-flag@1.0.0: {} - svelte-awesome-color-picker@4.1.1(svelte@5.54.0): + svelte-awesome-color-picker@4.1.3(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: colord: 2.9.3 - svelte: 5.54.0 - svelte-awesome-slider: 2.0.0(svelte@5.54.0) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + svelte-awesome-slider: 2.0.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) - svelte-awesome-slider@2.0.0(svelte@5.54.0): + svelte-awesome-slider@2.0.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: - svelte: 5.54.0 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - svelte-check@4.4.5(picomatch@4.0.3)(svelte@5.54.0)(typescript@5.9.3): + svelte-check@4.7.5(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 + '@sveltejs/load-config': 0.2.2 chokidar: 4.0.3 - fdir: 6.5.0(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.5) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.54.0 - typescript: 5.9.3 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + typescript: 6.0.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.6.0(svelte@5.54.0): + svelte-dnd-action@0.9.78(svelte@5.56.8(@typescript-eslint/types@8.66.0)): + dependencies: + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + + svelte-eslint-parser@1.8.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 - postcss: 8.5.8 - postcss-scss: 4.0.9(postcss@8.5.8) + postcss: 8.5.14 + postcss-scss: 4.0.9(postcss@8.5.14) postcss-selector-parser: 7.1.1 semver: 7.7.4 optionalDependencies: - svelte: 5.54.0 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - svelte-sonner@1.1.0(svelte@5.54.0): + svelte-sonner@1.1.1(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: - runed: 0.28.0(svelte@5.54.0) - svelte: 5.54.0 + runed: 0.28.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)) + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - svelte-toolbelt@0.10.6(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0): + svelte-toolbelt@0.10.6(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0) + runed: 0.35.1(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0)) style-to-object: 1.0.14 - svelte: 5.54.0 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) transitivePeerDependencies: - '@sveltejs/kit' - svelte-toolbelt@0.5.0(svelte@5.54.0): + svelte-toolbelt@0.5.0(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: clsx: 2.1.1 style-to-object: 1.0.14 - svelte: 5.54.0 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - svelte-toolbelt@0.7.1(svelte@5.54.0): + svelte-toolbelt@0.7.1(svelte@5.56.8(@typescript-eslint/types@8.66.0)): dependencies: clsx: 2.1.1 - runed: 0.23.4(svelte@5.54.0) + runed: 0.23.4(svelte@5.56.8(@typescript-eslint/types@8.66.0)) style-to-object: 1.0.14 - svelte: 5.54.0 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) - svelte@5.54.0: + svelte@5.56.8(@typescript-eslint/types@8.66.0): dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@sveltejs/acorn-typescript': 1.0.11(acorn@8.16.0) '@types/estree': 1.0.8 '@types/trusted-types': 2.0.7 acorn: 8.16.0 aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.6.4 + devalue: 5.8.1 esm-env: 1.2.2 - esrap: 2.2.4 + esrap: 2.2.13(@typescript-eslint/types@8.66.0) is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' - sveltekit-superforms@2.30.0(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(@types/json-schema@7.0.15)(svelte@5.54.0)(typescript@5.9.3): + sveltekit-superforms@2.30.2(@sveltejs/kit@2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(@types/json-schema@7.0.15)(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3): dependencies: - '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.54.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.54.0)(typescript@5.9.3)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) - devalue: 5.6.4 + '@sveltejs/kit': 2.70.2(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.66.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)))(svelte@5.56.8(@typescript-eslint/types@8.66.0))(typescript@6.0.3)(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)) + devalue: 5.8.1 memoize-weak: 1.0.2 - svelte: 5.54.0 - ts-deepmerge: 7.0.3 + svelte: 5.56.8(@typescript-eslint/types@8.66.0) + ts-deepmerge: 8.0.0 optionalDependencies: '@exodus/schemasafe': 1.3.0 '@standard-schema/spec': 1.1.0 '@typeschema/class-validator': 0.3.0(@types/json-schema@7.0.15)(class-validator@0.14.4) - '@valibot/to-json-schema': 1.6.0(valibot@1.3.1(typescript@5.9.3)) + '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@6.0.3)) '@vinejs/vine': 3.0.1 - arktype: 2.2.0 + arktype: 2.2.3 class-validator: 0.14.4 - effect: 3.20.0 - joi: 17.13.3 + effect: 3.21.4 + joi: 17.13.4 json-schema-to-ts: 3.1.1 superstruct: 2.0.2 - typebox: 1.1.6 - valibot: 1.3.1(typescript@5.9.3) + typebox: 1.3.6 + valibot: 1.4.2(typescript@6.0.3) yup: 1.7.1 - zod: 4.3.6 - zod-v3-to-json-schema: 4.0.0(zod@4.3.6) + zod: 4.4.3 + zod-v3-to-json-schema: 4.0.0(zod@4.4.3) transitivePeerDependencies: - '@types/json-schema' - typescript - symbol-tree@3.2.4: {} + symbol-tree@3.2.4: + optional: true tabbable@6.4.0: {} - tailwind-merge@3.5.0: {} + tailwind-merge@3.6.0: {} - tailwind-variants@3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2): - dependencies: - tailwindcss: 4.2.2 + tailwind-variants@3.3.1(tailwind-merge@3.6.0)(tailwindcss@4.3.3): optionalDependencies: - tailwind-merge: 3.5.0 - - tailwindcss@4.2.2: {} + tailwind-merge: 3.6.0 + tailwindcss: 4.3.3 - tapable@2.3.0: {} + tailwindcss@4.3.3: {} - text-hex@1.0.0: {} + tapable@2.3.3: {} tiny-case@1.0.3: optional: true + tiny-inflate@1.0.3: {} + tinybench@2.9.0: {} tinyexec@1.0.4: {} - tinyglobby@0.2.15: + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinyrainbow@3.1.0: {} - tldts-core@7.0.26: {} + tldts-core@7.0.26: + optional: true tldts@7.0.26: dependencies: tldts-core: 7.0.26 + optional: true toposort@2.0.2: optional: true totalist@3.0.1: {} - tough-cookie@6.0.1: + tough-cookie@6.0.2: dependencies: tldts: 7.0.26 - - tr46@0.0.3: {} + optional: true tr46@6.0.0: dependencies: punycode: 2.3.1 - - tree-kill@1.2.2: {} - - triple-beam@1.4.1: {} + optional: true ts-algebra@2.0.0: optional: true - ts-api-utils@2.5.0(typescript@5.9.3): + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: - typescript: 5.9.3 + typescript: 6.0.3 - ts-deepmerge@7.0.3: {} + ts-deepmerge@8.0.0: {} tslib@2.8.1: {} - tsx@4.21.0: + tsx@4.23.1: dependencies: - esbuild: 0.27.4 - get-tsconfig: 4.13.6 + esbuild: 0.28.0 optionalDependencies: fsevents: 2.3.3 @@ -6480,27 +6020,31 @@ snapshots: type-fest@2.19.0: optional: true - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - - typebox@1.1.6: + typebox@1.3.6: optional: true - typedarray@0.0.6: {} + typescript@6.0.3: {} - typescript@5.9.3: {} + undici-types@8.3.0: {} - undici-types@7.18.2: {} + undici@8.9.0: + optional: true + + unicode-properties@1.4.1: + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 - undici@7.24.4: {} + unicode-trie@2.0.0: + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.16.0 - picomatch: 4.0.3 + picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 uri-js@4.4.1: @@ -6511,92 +6055,93 @@ snapshots: util-deprecate@1.0.2: {} - uuid@10.0.0: {} + uuid@14.0.0: {} - uuid@13.0.0: {} - - valibot@1.3.1(typescript@5.9.3): + valibot@1.4.2(typescript@6.0.3): optionalDependencies: - typescript: 5.9.3 + typescript: 6.0.3 optional: true validator@13.15.26: optional: true - vary@1.1.2: {} - - vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0): + vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1): dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.3 - postcss: 8.5.8 - rolldown: 1.0.0-rc.10 - tinyglobby: 0.2.15 + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.25 + rolldown: 1.2.2 + tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 25.5.0 - esbuild: 0.27.4 + '@types/node': 26.1.2 + esbuild: 0.28.0 fsevents: 2.3.3 - jiti: 2.6.1 - tsx: 4.21.0 + jiti: 2.7.0 + tsx: 4.23.1 - vitefu@1.1.2(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)): + vitefu@1.1.2(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)): optionalDependencies: - vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1) - vitest@4.1.0(@types/node@25.5.0)(jsdom@29.0.0)(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)): + vitest@4.1.10(@types/node@26.1.2)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.0(@noble/hashes@1.8.0))(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)): dependencies: - '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)) - '@vitest/pretty-format': 4.1.0 - '@vitest/runner': 4.1.0 - '@vitest/snapshot': 4.1.0 - '@vitest/spy': 4.1.0 - '@vitest/utils': 4.1.0 + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 + picomatch: 4.0.4 std-env: 4.0.0 tinybench: 2.9.0 tinyexec: 1.0.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.1(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0) + vite: 8.2.1(@types/node@26.1.2)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.23.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.5.0 - jsdom: 29.0.0 + '@types/node': 26.1.2 + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + jsdom: 30.0.0(@noble/hashes@1.8.0) transitivePeerDependencies: - msw w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 + optional: true - web-streams-polyfill@3.3.3: {} - - webidl-conversions@3.0.1: {} - - webidl-conversions@8.0.1: {} + webidl-conversions@8.0.1: + optional: true webpack-virtual-modules@0.6.2: {} - whatwg-mimetype@5.0.0: {} + whatwg-mimetype@5.0.0: + optional: true - whatwg-url@16.0.1: + whatwg-url@16.0.1(@noble/hashes@1.8.0): dependencies: - '@exodus/bytes': 1.15.0 + '@exodus/bytes': 1.15.1(@noble/hashes@1.8.0) tr46: 6.0.0 webidl-conversions: 8.0.1 transitivePeerDependencies: - '@noble/hashes' + optional: true - whatwg-url@5.0.0: + whatwg-url@17.1.0(@noble/hashes@1.8.0): dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 + '@exodus/bytes': 1.15.1(@noble/hashes@1.8.0) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + optional: true which@2.0.2: dependencies: @@ -6607,56 +6152,18 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - winston-transport@4.9.0: - dependencies: - logform: 2.7.0 - readable-stream: 3.6.2 - triple-beam: 1.4.1 - - winston@3.19.0: - dependencies: - '@colors/colors': 1.6.0 - '@dabh/diagnostics': 2.0.8 - async: 3.2.6 - is-stream: 2.0.1 - logform: 2.7.0 - one-time: 1.0.0 - readable-stream: 3.6.2 - safe-stable-stringify: 2.5.0 - stack-trace: 0.0.10 - triple-beam: 1.4.1 - winston-transport: 4.9.0 - word-wrap@1.2.5: {} - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - ws@8.19.0: {} - xml-name-validator@5.0.0: {} - - xmlchars@2.2.0: {} + xml-name-validator@5.0.0: + optional: true - y18n@5.0.8: {} + xmlchars@2.2.0: + optional: true yaml@1.10.2: {} - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - yocto-queue@0.1.0: {} yup@1.7.1: @@ -6669,9 +6176,9 @@ snapshots: zimmerframe@1.1.4: {} - zod-v3-to-json-schema@4.0.0(zod@4.3.6): + zod-v3-to-json-schema@4.0.0(zod@4.4.3): dependencies: - zod: 4.3.6 + zod: 4.4.3 optional: true - zod@4.3.6: {} + zod@4.4.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3f6c3c42..8202a663 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,16 @@ -onlyBuiltDependencies: - - bcrypt - - better-sqlite3 - - esbuild - - sqlite3 +allowBuilds: + bcrypt: true + esbuild: true + better-sqlite3: true + sqlite3: true +minimumReleaseAgeExclude: + - helmet@8.3.0 + - globals@17.8.0 + - jsdom@30.0.0 + - tailwind-variants@3.3.0 + - svelte-dnd-action@0.9.78 + - '@inlang/paraglide-js@2.23.2' + - '@lucide/svelte@1.30.0' + - '@sveltejs/load-config@0.2.2' + - nodemailer@9.0.5 + - svelte-check@4.7.5 diff --git a/project.inlang/project_id b/project.inlang/project_id deleted file mode 100644 index a55a6247..00000000 --- a/project.inlang/project_id +++ /dev/null @@ -1 +0,0 @@ -iYqM0PXxJ6fvRb4qpQ \ No newline at end of file diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 00000000..abe05c72 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "skills": { + "shadcn-svelte": { + "source": "huntabyte/shadcn-svelte", + "sourceType": "github", + "skillPath": "skills/shadcn-svelte/SKILL.md", + "computedHash": "d4f3f983a71466a86649985a8a7fbce47da29f44de3a9d02540bbcf3167134e0" + } + } +} diff --git a/src/__tests__/file-upload-response.test.ts b/src/__tests__/file-upload-response.test.ts new file mode 100644 index 00000000..32cdd7de --- /dev/null +++ b/src/__tests__/file-upload-response.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('fs/promises', () => ({ writeFile: vi.fn().mockResolvedValue(undefined) })); + +import { POST } from '../routes/api/files/+server'; + +describe('POST /api/files', () => { + it('returns the filename where uploadFile() reads it (res.data.filename)', async () => { + const body = new FormData(); + body.append('file', new File(['x'], 'car.png', { type: 'image/png' })); + + const response = await POST({ + request: new Request('http://localhost/api/files', { method: 'POST', body }) + } as Parameters[0]); + + const payload = await response.json(); + expect(payload.data.filename).toMatch(/car\.png$/); + }); +}); diff --git a/src/__tests__/grid-layout.test.ts b/src/__tests__/grid-layout.test.ts new file mode 100644 index 00000000..32dcf22e --- /dev/null +++ b/src/__tests__/grid-layout.test.ts @@ -0,0 +1,122 @@ +import { describe, expect, it } from 'vitest'; +import { + compactLayout, + moveElement, + resizeElement, + type GridItem +} from '$lib/components/dashboard/grid-layout'; +import { WIDGET_REGISTRY } from '$lib/components/dashboard/widget-registry'; +import { DEFAULT_WIDGET_LAYOUT, widgetMinSize } from '$lib/domain/dashboard'; + +function item(id: string, colStart: number, rowStart: number, colSpan = 6, rowSpan = 4): GridItem { + return { id, colStart, rowStart, colSpan, rowSpan }; +} + +function positions(items: GridItem[]): Record { + return Object.fromEntries(items.map((i) => [i.id, [i.colStart, i.rowStart]])); +} + +describe('compactLayout', () => { + it('pulls items up to close vertical gaps', () => { + const items = [item('a', 1, 1), item('b', 1, 20)]; + expect(positions(compactLayout(items))).toEqual({ a: [1, 1], b: [1, 5] }); + }); + + it('leaves items in separate columns at the top', () => { + const items = [item('a', 1, 1), item('b', 7, 9)]; + expect(positions(compactLayout(items))).toEqual({ a: [1, 1], b: [7, 1] }); + }); + + it('separates items that arrive overlapping', () => { + const items = [item('a', 1, 1), item('b', 1, 1)]; + const packed = compactLayout(items); + expect(packed.find((i) => i.id === 'b')!.rowStart).toBe(5); + }); +}); + +describe('moveElement', () => { + it('swaps with the widget below instead of pushing it further down', () => { + // The old compactor pinned the dragged widget then repacked everything else beneath it, so + // dragging 'a' down shoved 'b' — which was already below — down again instead of swapping. + const items = [item('a', 1, 1), item('b', 1, 5)]; + expect(positions(moveElement(items, 'a', 1, 5))).toEqual({ a: [1, 5], b: [1, 1] }); + }); + + it('swaps with the widget above when dragging up', () => { + const items = [item('a', 1, 1), item('b', 1, 5)]; + expect(positions(moveElement(items, 'b', 1, 1))).toEqual({ a: [1, 5], b: [1, 1] }); + }); + + it('ignores a nudge too small to clear the neighbour', () => { + const items = [item('a', 1, 1), item('b', 1, 5)]; + expect(positions(moveElement(items, 'a', 1, 3))).toEqual({ a: [1, 1], b: [1, 5] }); + }); + + it('leaves untouched columns alone', () => { + const items = [item('a', 1, 1), item('b', 7, 1), item('c', 1, 5)]; + expect(positions(moveElement(items, 'a', 1, 5))).toEqual({ + a: [1, 5], + b: [7, 1], + c: [1, 1] + }); + }); + + it('cascades displaced widgets down without dropping any', () => { + const items = [item('a', 1, 1), item('b', 1, 5), item('c', 1, 9), item('d', 1, 13)]; + const moved = moveElement(items, 'd', 1, 1); + const rows = moved.map((i) => i.rowStart).sort((x, y) => x - y); + expect(rows).toEqual([1, 5, 9, 13]); + expect(moved.find((i) => i.id === 'd')!.rowStart).toBe(1); + }); + + it('clamps a widget dragged past the right edge', () => { + const items = [item('a', 1, 1)]; + expect(moveElement(items, 'a', 99, 1)[0].colStart).toBe(7); + }); +}); + +describe('resizeElement', () => { + it('pushes the widget below down rather than lifting it above', () => { + const items = [item('a', 1, 1), item('b', 1, 5)]; + expect(positions(resizeElement(items, 'a', 6, 8))).toEqual({ a: [1, 1], b: [1, 9] }); + }); + + it('pulls neighbours back up when a widget shrinks', () => { + const items = [item('a', 1, 1, 6, 8), item('b', 1, 9)]; + expect(positions(resizeElement(items, 'a', 6, 4))).toEqual({ a: [1, 1], b: [1, 5] }); + }); + + it('clamps growth to the columns remaining to the right', () => { + const items = [item('a', 7, 1)]; + expect(resizeElement(items, 'a', 12, 4)[0].colSpan).toBe(6); + }); + + it('refuses to shrink a widget below its own minimum', () => { + const items = [item('a', 1, 1)]; + const resized = resizeElement(items, 'a', 1, 1, { minColSpan: 3, minRowSpan: 2 })[0]; + expect([resized.colSpan, resized.rowSpan]).toEqual([3, 2]); + }); + + it('lets the grid edge win over a minimum that cannot fit', () => { + const items = [item('a', 11, 1, 2, 4)]; + expect(resizeElement(items, 'a', 1, 4, { minColSpan: 6, minRowSpan: 1 })[0].colSpan).toBe(2); + }); +}); + +describe('widgetMinSize', () => { + it('keeps every registry default at or above its widget minimum', () => { + for (const def of Object.values(WIDGET_REGISTRY)) { + const { minColSpan, minRowSpan } = widgetMinSize(def.type); + expect(def.defaultColSpan).toBeGreaterThanOrEqual(minColSpan); + expect(def.defaultRowSpan).toBeGreaterThanOrEqual(minRowSpan); + } + }); + + it('ships a default layout that already satisfies the minimums', () => { + for (const layoutItem of DEFAULT_WIDGET_LAYOUT) { + const { minColSpan, minRowSpan } = widgetMinSize(layoutItem.type); + expect(layoutItem.colSpan).toBeGreaterThanOrEqual(minColSpan); + expect(layoutItem.rowSpan).toBeGreaterThanOrEqual(minRowSpan); + } + }); +}); diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts deleted file mode 100644 index cf0f0739..00000000 --- a/src/__tests__/index.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -describe('test', () => { - it('test', () => { - expect(true).toBe(true); - }); -}); diff --git a/src/__tests__/mileage.test.ts b/src/__tests__/mileage.test.ts new file mode 100644 index 00000000..6c0a46cd --- /dev/null +++ b/src/__tests__/mileage.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; +import { computeMileagePerWindow, type FuelLogInput } from '../lib/domain/fuel/mileage'; + +function log(overrides: Partial): FuelLogInput { + return { filled: true, missedLast: false, odometer: null, fuelAmount: null, ...overrides }; +} + +describe('computeMileagePerWindow', () => { + it('computes a ratio for a simple two-fill window', () => { + const logs = [log({ odometer: 100, fuelAmount: 10 }), log({ odometer: 200, fuelAmount: 10 })]; + expect(computeMileagePerWindow(logs)).toEqual([null, 10]); + }); + + it('does not crash when a fillable log has no valid preceding start (regression)', () => { + // Index 1 passes the per-log eligibility filter (filled, not missedLast, has + // odometer/fuelAmount) but findMileageWindows drops it anyway because the + // backward walk never finds an earlier filled odometer to pair it with. + // A shared index counter between the two functions used to desync here and + // read past the end of the compacted windows array on the next real window. + const logs = [ + log({ filled: false }), + log({ odometer: 150, fuelAmount: 8 }), + log({ odometer: 250, fuelAmount: 10 }) + ]; + expect(() => computeMileagePerWindow(logs)).not.toThrow(); + expect(computeMileagePerWindow(logs)).toEqual([null, null, 100 / 10]); + }); +}); diff --git a/src/__tests__/vehicle-color.test.ts b/src/__tests__/vehicle-color.test.ts new file mode 100644 index 00000000..331b5c0d --- /dev/null +++ b/src/__tests__/vehicle-color.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; +import { vintageVehicleColor, vehicleTrendColor } from '../lib/domain/vehicle'; + +describe('vintageVehicleColor', () => { + it('desaturates a saturated hex toward a muted tone', () => { + const muted = vintageVehicleColor('#ff0000'); + expect(muted).not.toBeNull(); + expect(muted).not.toBe('#ff0000'); + expect(muted).toMatch(/^#[0-9a-f]{6}$/); + }); + + it('expands 3-digit hex before muting', () => { + expect(vintageVehicleColor('#f00')).toBe(vintageVehicleColor('#ff0000')); + }); + + it('passes through null/invalid input unchanged', () => { + expect(vintageVehicleColor(null)).toBeNull(); + expect(vintageVehicleColor(undefined)).toBeNull(); + expect(vintageVehicleColor('not-a-color')).toBe('not-a-color'); + }); +}); + +describe('vehicleTrendColor', () => { + it('returns null for near-achromatic colors so callers fall back to the theme palette', () => { + // White, black, silver, gray — none of these can stay visible against both a + // light and a dark chart background as a single static hex. + expect(vehicleTrendColor('#f5f5f5')).toBeNull(); + expect(vehicleTrendColor('#1a1a1a')).toBeNull(); + expect(vehicleTrendColor('#c0c0c0')).toBeNull(); + expect(vehicleTrendColor('#808080')).toBeNull(); + }); + + it('mutes a saturated color instead of dropping it', () => { + expect(vehicleTrendColor('#c62828')).toBe(vintageVehicleColor('#c62828')); + expect(vehicleTrendColor('#c62828')).not.toBeNull(); + }); + + it('returns null for missing or invalid input', () => { + expect(vehicleTrendColor(null)).toBeNull(); + expect(vehicleTrendColor(undefined)).toBeNull(); + expect(vehicleTrendColor('not-a-color')).toBeNull(); + }); +}); diff --git a/src/app.d.ts b/src/app.d.ts index 7eb2de4a..2f1d244f 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -7,7 +7,6 @@ declare global { message: string; } interface Locals { - requestBody?: any; user?: { id: string; username: string; @@ -22,7 +21,7 @@ declare global { // Environment variable types for better type safety declare namespace NodeJS { interface ProcessEnv { - NODE_ENV: 'dev' | 'production' | 'test'; + NODE_ENV: 'development' | 'production' | 'test'; SERVER_HOST: string; SERVER_PORT: string; DB_PATH: string; diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 54666e54..3f80bda6 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,24 +1,17 @@ import { sequence } from '@sveltejs/kit/hooks'; import { paraglideMiddleware } from '$lib/paraglide/server'; import type { Handle, HandleServerError } from '@sveltejs/kit'; -import { createErrorResponseBody, logError } from './server/utils/errorHandler'; -import { - CorsMiddleware, - RateLimitMiddleware, - AuthMiddleware, - LoggingMiddleware -} from '$server/middlewares'; - -import { MiddlewareChain } from '$server/middlewares/base'; +import { handleCors } from '$server/middlewares/cors'; +import { handleAuth } from '$server/middlewares/auth'; +import { handleLogging } from '$server/middlewares/logging'; import { initializeDatabase } from '$server/db/init'; import { appAsciiArt, appVersion, logger } from '$server/config'; import { env } from '$lib/config/env.server'; +import { getTextDirection } from '$lib/utils'; import { ensureAppDirectories } from '$server/utils/fs'; import { initializeNotificationScheduler } from '$server/services/notificationSchedulerService'; -const middlewareChain = new MiddlewareChain(); - const envSnapshot = () => ({ APP_VERSION: appVersion, LOG_LEVEL: env.LOG_LEVEL, @@ -83,36 +76,16 @@ const initPromise = (async () => { } })(); -const buildMiddlewares = () => [ - new CorsMiddleware(), - new AuthMiddleware(), - new RateLimitMiddleware(), - new LoggingMiddleware() -]; - export const handleError: HandleServerError = async ({ error, event }) => { - logError(error, event); + logger.error(`Error in ${event.request.method} - ${event.url.pathname}`, error); - const body = createErrorResponseBody(error); - - return { message: body.message || 'Internal server error' }; + return { message: error instanceof Error ? error.message : 'Internal server error' }; }; -const originalHandle: Handle = async ({ event, resolve }) => { +const handleInit: Handle = async ({ event, resolve }) => { await initPromise; - middlewareChain.init(buildMiddlewares()); - - const middlewareResult = await middlewareChain.handle(event); - - if (middlewareResult.response) { - return middlewareResult.response; - } - - const response = await resolve(event); - - CorsMiddleware.addCorsHeaders(response, event.request); - return response; + return resolve(event); }; const handleParaglide: Handle = ({ event, resolve }) => @@ -120,15 +93,11 @@ const handleParaglide: Handle = ({ event, resolve }) => event.request = request; return resolve(event, { - transformPageChunk: ({ html }) => { - // Set language and direction attributes based on locale - const rtlLanguages = ['ar', 'he', 'fa', 'ur', 'yi']; - const direction = rtlLanguages.includes(locale) ? 'rtl' : 'ltr'; - return html + transformPageChunk: ({ html }) => + html .replace('%paraglide.lang%', locale) - .replace('dir="%paraglide.lang%"', `dir="${direction}"`); - } + .replace('dir="%paraglide.lang%"', `dir="${getTextDirection(locale)}"`) }); }); -export const handle = sequence(originalHandle, handleParaglide); +export const handle = sequence(handleInit, handleCors, handleAuth, handleLogging, handleParaglide); diff --git a/src/lib/components/app/AttachmentLink.svelte b/src/lib/components/app/AttachmentLink.svelte index ab848da9..fd1a8762 100644 --- a/src/lib/components/app/AttachmentLink.svelte +++ b/src/lib/components/app/AttachmentLink.svelte @@ -5,6 +5,8 @@ import FilePreviewModal from '$lib/components/app/FilePreviewModal.svelte'; import type { Snippet } from 'svelte'; + import * as m from '$lib/paraglide/messages'; + let { fileName, children }: { fileName: string; children?: Snippet } = $props(); let open = $state(false); @@ -15,7 +17,7 @@ +
+
diff --git a/src/lib/components/app/ChartPoints.svelte b/src/lib/components/app/ChartPoints.svelte new file mode 100644 index 00000000..6bc7e2d4 --- /dev/null +++ b/src/lib/components/app/ChartPoints.svelte @@ -0,0 +1,25 @@ + + + + + diff --git a/src/lib/components/app/CrudActionsMenu.svelte b/src/lib/components/app/CrudActionsMenu.svelte index 8266fd66..c9e5a3d9 100644 --- a/src/lib/components/app/CrudActionsMenu.svelte +++ b/src/lib/components/app/CrudActionsMenu.svelte @@ -53,7 +53,7 @@ ]); -
+
e.stopPropagation()}> removeField(index)} ariaLabel={m.custom_fields_remove_aria()} - buttonStyles="hover:bg-red-100 dark:hover:bg-red-700" - iconStyles="text-red-500" + buttonStyles="hover:bg-destructive/10" + iconStyles="text-destructive" />
{/each} diff --git a/src/lib/components/app/DeleteConfirmation.svelte b/src/lib/components/app/DeleteConfirmation.svelte index 8bce6f8c..7612ef13 100644 --- a/src/lib/components/app/DeleteConfirmation.svelte +++ b/src/lib/components/app/DeleteConfirmation.svelte @@ -1,42 +1,51 @@ -{#if open} -
-
- - + + + + + -

- {m.delete_dialog_title()} -

-
- {m.delete_dialog_message()} -
-
- - -
-
-
-{/if} + {title} + {message} + + + + + + + diff --git a/src/lib/components/app/FeatureRecordCard.svelte b/src/lib/components/app/FeatureRecordCard.svelte index 82c6468a..57d59f87 100644 --- a/src/lib/components/app/FeatureRecordCard.svelte +++ b/src/lib/components/app/FeatureRecordCard.svelte @@ -8,6 +8,8 @@ titleIcon: Component<{ class?: string }>; titleClass?: string; class?: string; + /** Shown as a small pill next to the title — used for the vehicle badge in fleet scope. */ + subtitle?: string; headerExtras?: Snippet; actions?: Snippet; children?: Snippet; @@ -19,6 +21,7 @@ titleIcon: TitleIcon, titleClass = '', class: className = '', + subtitle, headerExtras, actions, children @@ -32,6 +35,13 @@ {title}
+ {#if subtitle} + + {subtitle} + + {/if} {@render headerExtras?.()}
{@render actions?.()} diff --git a/src/lib/components/app/FeatureTabShell.svelte b/src/lib/components/app/FeatureTabShell.svelte index 23e406e3..7e2f5c15 100644 --- a/src/lib/components/app/FeatureTabShell.svelte +++ b/src/lib/components/app/FeatureTabShell.svelte @@ -2,7 +2,6 @@ import type { Component } from 'svelte'; import TabContainer from '$appui/TabContainer.svelte'; import { sheetStore } from '$lib/stores/sheet.svelte'; - import { vehicleStore } from '$lib/stores/vehicle.svelte'; type SheetDataResolver = () => unknown; type AnyComponent = Component; @@ -16,6 +15,8 @@ importSheetTitle?: string; importSheetComponent?: AnyComponent; importSheetData?: unknown | SheetDataResolver; + exportAction?: (() => void) | null; + exportActionDisabled?: boolean; } let { @@ -26,7 +27,9 @@ addSheetData, importSheetTitle, importSheetComponent, - importSheetData + importSheetData, + exportAction = null, + exportActionDisabled = false }: Props = $props(); const resolveData = (data: unknown | SheetDataResolver) => { @@ -52,7 +55,8 @@ resolveData(importSheetData) ) : null} - addActionDisabled={!vehicleStore.selectedId} + {exportAction} + {exportActionDisabled} > diff --git a/src/lib/components/app/LegendInfoGroup.svelte b/src/lib/components/app/LegendInfoGroup.svelte new file mode 100644 index 00000000..63961147 --- /dev/null +++ b/src/lib/components/app/LegendInfoGroup.svelte @@ -0,0 +1,42 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + {#each items as item (item.label)} +
+ + {item.label} + {#if item.detail} + {item.detail} + {/if} +
+ {/each} +
+
diff --git a/src/lib/components/app/SearchableSelect.svelte b/src/lib/components/app/SearchableSelect.svelte index da4449e2..32dd9306 100644 --- a/src/lib/components/app/SearchableSelect.svelte +++ b/src/lib/components/app/SearchableSelect.svelte @@ -6,24 +6,30 @@ import * as Popover from '$ui/popover/index.js'; import { Button } from '$ui/button/index.js'; import { cn } from '$lib/utils.js'; + import VehicleTypeBadge from '$feature/vehicle/VehicleTypeBadge.svelte'; import * as m from '$lib/paraglide/messages'; let { options, name, + label, value = $bindable(), icon: Icon }: { - options: { value: string; label: string }[]; + options: { value: string; label: string; vehicleType?: string | null; color?: string | null }[]; name: string; + label?: string; value: string; icon: Component; } = $props(); + const displayName = $derived(label ?? name); + let open = $state(false); let triggerRef = $state(null!); - const selectedValue = $derived(options.find((f) => f.value === value)?.label); + const selectedOption = $derived(options.find((f) => f.value === value)); + const selectedValue = $derived(selectedOption?.label); function closeAndFocusTrigger() { open = false; @@ -48,8 +54,18 @@ aria-expanded={open} >
- - {selectedValue || value || m.common_select_placeholder({ name })} + {#if selectedOption?.vehicleType !== undefined} + + {:else} + + {/if} + {selectedValue || value || m.common_select_placeholder({ name: displayName })}
@@ -60,7 +76,7 @@ {m.common_no_match_found()} @@ -76,6 +92,13 @@ }} > + {#if option.vehicleType !== undefined} + + {/if} {option.label} {/each} diff --git a/src/lib/components/app/StoreResourceState.svelte b/src/lib/components/app/StoreResourceState.svelte new file mode 100644 index 00000000..f2a33b27 --- /dev/null +++ b/src/lib/components/app/StoreResourceState.svelte @@ -0,0 +1,37 @@ + + +{#if processing} + {#if actions} +
{@render actions()}
+ {/if} + {#if skeleton} + {@render skeleton()} + {/if} +{:else if error} + {#if actions} +
{@render actions()}
+ {/if} + +{:else if !data || data.length === 0} + {#if actions} +
{@render actions()}
+ {/if} + +{:else} + {@render children?.()} +{/if} diff --git a/src/lib/components/app/TabContainer.svelte b/src/lib/components/app/TabContainer.svelte index 85e4f4e7..0ebdf9de 100644 --- a/src/lib/components/app/TabContainer.svelte +++ b/src/lib/components/app/TabContainer.svelte @@ -1,6 +1,7 @@ @@ -26,6 +29,18 @@ {title}
+ {#if exportAction} + + {/if} {#if importAction} +
diff --git a/src/lib/components/dashboard/DashboardGrid.svelte b/src/lib/components/dashboard/DashboardGrid.svelte new file mode 100644 index 00000000..9f69000f --- /dev/null +++ b/src/lib/components/dashboard/DashboardGrid.svelte @@ -0,0 +1,104 @@ + + +
+ + {#if placeholder && placeholderRect && interaction.enabled} +
+
+ {/if} + + {#each interaction.items as item (item.id)} + {@render children(item)} + {/each} +
+ + diff --git a/src/lib/components/dashboard/DonutChart.svelte b/src/lib/components/dashboard/DonutChart.svelte new file mode 100644 index 00000000..b3d8e993 --- /dev/null +++ b/src/lib/components/dashboard/DonutChart.svelte @@ -0,0 +1,189 @@ + + +{#snippet centerContent(hovered?: DonutDataPoint & { percentage: number })} + {#if hovered} +
+ {hovered.name} + {hovered.value.toLocaleString()} + {hovered.percentage}% +
+ {:else if centerLabel} +
+ {centerLabel} + {centerValueFormatter(total)} +
+ {/if} +{/snippet} + +
+ {#if title} +
+ {title} +
+ {/if} + + {#if loading} +
+ +
+ {:else if data.length === 0} +
+ No data available +
+ {:else if data.length === 1} + {@const size = ringSize} + {@const strokeWidth = size * 0.2} + {@const radius = (size - strokeWidth) / 2} + +
+ + (singleHovered = true)} + onmouseleave={() => (singleHovered = false)} + /> + + {@render centerContent(singleHovered ? { ...data[0], percentage: 100 } : undefined)} +
+ {:else} +
+
+ + d.color} + {innerRadius} + padAngle={0.02} + cornerRadius={4} + bind:context={pieContext} + > + {#snippet tooltip()}{/snippet} + + + {@render centerContent( + hoveredDatum ? { ...hoveredDatum, percentage: hoveredPercentage } : undefined + )} +
+
+ {/if} +
diff --git a/src/lib/components/dashboard/FilterTabs.svelte b/src/lib/components/dashboard/FilterTabs.svelte new file mode 100644 index 00000000..1b2551aa --- /dev/null +++ b/src/lib/components/dashboard/FilterTabs.svelte @@ -0,0 +1,18 @@ + + + + + {#each tabs as tab (tab.id)} + {tab.label} + {/each} + + diff --git a/src/lib/components/dashboard/PageHeader.svelte b/src/lib/components/dashboard/PageHeader.svelte new file mode 100644 index 00000000..79b20df4 --- /dev/null +++ b/src/lib/components/dashboard/PageHeader.svelte @@ -0,0 +1,25 @@ + + +
+
+

{title}

+ {#if description} +

{description}

+ {/if} +
+ {#if children} +
+ {@render children()} +
+ {/if} +
diff --git a/src/lib/components/dashboard/StackedAreaChart.svelte b/src/lib/components/dashboard/StackedAreaChart.svelte new file mode 100644 index 00000000..3ba00cee --- /dev/null +++ b/src/lib/components/dashboard/StackedAreaChart.svelte @@ -0,0 +1,142 @@ + + +
+
+ {#if !bare} + {title} + {/if} +
+ ({ color: s.color, label: s.label }))} /> +
+
+ {#if loading} +
+
+ {#each [40, 65, 45, 80, 55, 70, 50, 85, 60, 75] as height, i (i)} + + {/each} +
+ +
+ {:else if hasData} + + ({ key: s.key, label: s.label, color: s.color }))} + seriesLayout="stack" + axis + props={chartProps} + > + {#snippet tooltip()} + + v.toLocaleDateString('en-IN', { month: 'long', year: 'numeric' })} + indicator="dot" + > + {#snippet formatter({ value, name })} + {name} + + {typeof value === 'number' ? `$${value.toFixed(2)}` : value} + + {/snippet} + + {/snippet} + {#snippet marks({ context }: { context: any })} + {#each context.series.visibleSeries as s (s.key)} + + {#snippet children({ gradient })} + + {/snippet} + + + {/each} + {/snippet} + + + {:else} +
+ +
+ {/if} +
diff --git a/src/lib/components/dashboard/StatCard.svelte b/src/lib/components/dashboard/StatCard.svelte new file mode 100644 index 00000000..5f32fa9c --- /dev/null +++ b/src/lib/components/dashboard/StatCard.svelte @@ -0,0 +1,85 @@ + + +{#snippet trendBadge()} + {#if trend} + + {trendIcon} + {trend.value} + + {/if} +{/snippet} + +{#if bare} + +
+ {value} + {@render trendBadge()} +
+{:else} +
+ {#if Icon} +
+ +
+ {/if} + +
+ {label} + {value} + {@render trendBadge()} +
+
+{/if} diff --git a/src/lib/components/dashboard/StatusPill.svelte b/src/lib/components/dashboard/StatusPill.svelte new file mode 100644 index 00000000..87e26efe --- /dev/null +++ b/src/lib/components/dashboard/StatusPill.svelte @@ -0,0 +1,62 @@ + + + + {displayLabel} + diff --git a/src/lib/components/dashboard/VehicleFilterDropdown.svelte b/src/lib/components/dashboard/VehicleFilterDropdown.svelte new file mode 100644 index 00000000..90d6db14 --- /dev/null +++ b/src/lib/components/dashboard/VehicleFilterDropdown.svelte @@ -0,0 +1,83 @@ + + + + + {#snippet child({ props })} + + {/snippet} + + + (selectedIds = [])}> + 0 && 'text-transparent')} /> + All Vehicles + + {#if vehicles.length > 0} + + {#each vehicles as vehicle (vehicle.id)} + {#if vehicle.id} + toggle(vehicle.id!)} + > +
+ + {vehicleLabel(vehicle)} +
+
+ {/if} + {/each} + {/if} +
+
diff --git a/src/lib/components/dashboard/VehicleLeaderboard.svelte b/src/lib/components/dashboard/VehicleLeaderboard.svelte new file mode 100644 index 00000000..0ea73ca4 --- /dev/null +++ b/src/lib/components/dashboard/VehicleLeaderboard.svelte @@ -0,0 +1,71 @@ + + +{#if loading} +
+ {#each [0, 1, 2, 3] as i (i)} +
+ + + +
+ {/each} +
+{:else if entries.length === 0} +
+ {emptyLabel} +
+{:else} +
+ {#each entries as entry, i (entry.id)} +
+ + {i + 1} + +
+

{entry.name}

+ {#if entry.plate} +

{entry.plate}

+ {/if} +
+ {entry.formattedValue} +
+ {/each} +
+{/if} diff --git a/src/lib/components/dashboard/WidgetCard.svelte b/src/lib/components/dashboard/WidgetCard.svelte new file mode 100644 index 00000000..874e5567 --- /dev/null +++ b/src/lib/components/dashboard/WidgetCard.svelte @@ -0,0 +1,244 @@ + + +
+ {#if Icon} + +
(headerHovered = true)} + onmouseleave={() => (headerHovered = false)} + aria-label="Move {title}. Use the arrow keys to reposition it." + class={`flex h-full min-h-0 items-center gap-3 py-2 pr-9 pl-3 ${interactive ? 'cursor-grab active:cursor-grabbing' : ''}`} + > +
+ +
+ +
+

+ {title} +

+ {@render children()} +
+
+ {:else} + +
(headerHovered = true)} + onmouseleave={() => (headerHovered = false)} + aria-label="Move {title}. Use the arrow keys to reposition it." + class={`flex shrink-0 items-center px-4 pt-3 pr-9 pb-2 ${interactive ? 'cursor-grab active:cursor-grabbing' : ''}`} + > +

+ {title} +

+
+ +
+ {@render children()} +
+ {/if} + + + + + +
+ + diff --git a/src/lib/components/dashboard/grid-interaction.svelte.ts b/src/lib/components/dashboard/grid-interaction.svelte.ts new file mode 100644 index 00000000..c20dcd63 --- /dev/null +++ b/src/lib/components/dashboard/grid-interaction.svelte.ts @@ -0,0 +1,319 @@ +import { getContext, setContext } from 'svelte'; +import { + GRID_COLUMNS, + GRID_MAX_ROW_SPAN, + widgetMinSize, + type WidgetLayoutItem, + type WidgetMinSize +} from '$lib/domain/dashboard'; +import { clamp, ROW_UNIT_PX } from './widget-size'; +import { moveElement, resizeElement, type GridRect } from './grid-layout'; + +type GridMode = 'move' | 'resize'; + +/** Pixel geometry of the widget being dragged, relative to the grid's padding box. */ +interface FloatRect { + left: number; + top: number; + width: number; + height: number; +} + +/** Rendered track geometry: `pitch` is one track plus one gap, i.e. the distance between track starts. */ +interface GridMetrics { + colPitch: number; + colGap: number; + rowPitch: number; + rowGap: number; +} + +const EDGE_SCROLL_ZONE_PX = 80; +const EDGE_SCROLL_MAX_PX = 22; + +function readMetrics(grid: HTMLElement): GridMetrics { + const style = getComputedStyle(grid); + const columns = style.gridTemplateColumns + .split(' ') + .map(parseFloat) + .filter((width) => !Number.isNaN(width)); + const colGap = parseFloat(style.columnGap) || 0; + const rowGap = parseFloat(style.rowGap) || 0; + + return { + colGap, + rowGap, + colPitch: (columns[0] ?? grid.clientWidth / GRID_COLUMNS) + colGap, + rowPitch: ROW_UNIT_PX + rowGap + }; +} + +function findScroller(el: HTMLElement | undefined): HTMLElement { + const fallback = (document.scrollingElement as HTMLElement | null) ?? document.documentElement; + for (let node = el?.parentElement; node; node = node.parentElement) { + const overflowY = getComputedStyle(node).overflowY; + if ((overflowY === 'auto' || overflowY === 'scroll') && node.scrollHeight > node.clientHeight) { + return node; + } + } + return fallback; +} + +/** + * Drives pointer-driven move/resize for a widget grid. The widget being manipulated follows the + * pointer in raw pixels while a snapped `draft` layout is recomputed from the committed layout on + * every frame — the dragged item's rect within that draft is the dotted placeholder. Nothing reaches + * the store until the pointer is released, so the live layout never churns mid-gesture. + */ +export class GridInteraction { + #getItems: () => WidgetLayoutItem[]; + #commit: (items: WidgetLayoutItem[]) => void; + + gridEl = $state(); + /** Pointer interaction only applies at the 12-column breakpoint; below it the grid stacks. */ + enabled = $state(false); + + draft = $state(null); + activeId = $state(null); + mode = $state(null); + float = $state(null); + /** Track sizes captured at gesture start; also lets the placeholder be drawn in pixels. */ + metrics = $state(null); + + constructor(getItems: () => WidgetLayoutItem[], commit: (items: WidgetLayoutItem[]) => void) { + this.#getItems = getItems; + this.#commit = commit; + } + + #minSize(item: WidgetLayoutItem): WidgetMinSize { + return widgetMinSize(item.type); + } + + /** Layout to render: the in-flight draft while dragging, otherwise the committed layout. */ + get items(): WidgetLayoutItem[] { + return this.draft ?? this.#getItems(); + } + + get placeholder(): GridRect | null { + if (!this.activeId || !this.draft) return null; + return this.draft.find((item) => item.id === this.activeId) ?? null; + } + + /** + * The placeholder's rect in pixels. Drawing the outline as an absolutely-positioned box instead of + * a grid-placed one keeps a *painted* element from re-flowing grid tracks on every snap step — + * Safari doesn't reliably invalidate the area such an element vacates, which left a dashed ghost + * at every width a shrinking widget passed through. + */ + get placeholderRect(): FloatRect | null { + const rect = this.placeholder; + const metrics = this.metrics; + if (!rect || !metrics) return null; + + return { + left: (rect.colStart - 1) * metrics.colPitch, + top: (rect.rowStart - 1) * metrics.rowPitch, + width: rect.colSpan * metrics.colPitch - metrics.colGap, + height: rect.rowSpan * metrics.rowPitch - metrics.rowGap + }; + } + + isActive(id: string): boolean { + return this.activeId === id; + } + + #snapshot(): WidgetLayoutItem[] { + return this.#getItems().map((item) => ({ ...item })); + } + + start(mode: GridMode, id: string, event: PointerEvent, cardEl: HTMLElement): void { + if (!this.enabled || this.activeId || event.button !== 0) return; + + const grid = this.gridEl; + const source = this.#getItems().find((item) => item.id === id); + if (!grid || !source) return; + + event.preventDefault(); + // The stat layout makes the whole card a drag handle, so the resize corner must not also + // reach it. (The `activeId` guard above already covers this; this keeps it explicit.) + event.stopPropagation(); + + const metrics = readMetrics(grid); + const gridBox = grid.getBoundingClientRect(); + const cardBox = cardEl.getBoundingClientRect(); + const origin: FloatRect = { + left: cardBox.left - gridBox.left, + top: cardBox.top - gridBox.top, + width: cardBox.width, + height: cardBox.height + }; + // Where inside the card the pointer grabbed, so the card doesn't jump to the cursor. + const grab = { x: event.clientX - cardBox.left, y: event.clientY - cardBox.top }; + const scroller = findScroller(grid); + + let pointerX = event.clientX; + let pointerY = event.clientY; + let frame = 0; + + this.activeId = id; + this.mode = mode; + this.float = { ...origin }; + this.metrics = metrics; + this.draft = this.#snapshot(); + const bodyClass = mode === 'move' ? 'grid-moving' : 'grid-resizing'; + document.body.classList.add(bodyClass); + + // Capture on the grid — never the card, which goes `pointer-events: none` while it floats. This + // guarantees the gesture terminates: without it, releasing outside the window drops `pointerup` + // and the draft (and its placeholder) is stranded on screen until the next interaction. + const pointerId = event.pointerId; + try { + grid.setPointerCapture(pointerId); + } catch { + // Capture is best-effort; the window listeners below still cover the common case. + } + + const update = () => { + // Re-read the box every frame so edge auto-scroll doesn't skew the mapping. + const box = grid.getBoundingClientRect(); + + if (mode === 'move') { + const left = pointerX - box.left - grab.x; + const top = pointerY - box.top - grab.y; + this.float = { left, top, width: origin.width, height: origin.height }; + + const colStart = clamp( + Math.round(left / metrics.colPitch) + 1, + 1, + GRID_COLUMNS - source.colSpan + 1 + ); + const rowStart = Math.max(1, Math.round(top / metrics.rowPitch) + 1); + this.draft = moveElement(this.#snapshot(), id, colStart, rowStart); + return; + } + + const min = this.#minSize(source); + const maxColSpan = GRID_COLUMNS - source.colStart + 1; + const minColSpan = Math.min(min.minColSpan, maxColSpan); + const minRowSpan = Math.min(min.minRowSpan, GRID_MAX_ROW_SPAN); + + // The pointer can't drag the ghost below the widget's floor either, so what you see while + // resizing is always a size the widget will actually accept. + const width = clamp( + pointerX - box.left - origin.left, + minColSpan * metrics.colPitch - metrics.colGap, + maxColSpan * metrics.colPitch - metrics.colGap + ); + const height = clamp( + pointerY - box.top - origin.top, + minRowSpan * metrics.rowPitch - metrics.rowGap, + GRID_MAX_ROW_SPAN * metrics.rowPitch - metrics.rowGap + ); + this.float = { left: origin.left, top: origin.top, width, height }; + + const colSpan = Math.round((width + metrics.colGap) / metrics.colPitch); + const rowSpan = Math.round((height + metrics.rowGap) / metrics.rowPitch); + this.draft = resizeElement(this.#snapshot(), id, colSpan, rowSpan, min); + }; + + const autoScroll = () => { + frame = requestAnimationFrame(autoScroll); + + const top = pointerY - EDGE_SCROLL_ZONE_PX; + const bottom = pointerY - (window.innerHeight - EDGE_SCROLL_ZONE_PX); + const delta = + top < 0 + ? (top / EDGE_SCROLL_ZONE_PX) * EDGE_SCROLL_MAX_PX + : bottom > 0 + ? (bottom / EDGE_SCROLL_ZONE_PX) * EDGE_SCROLL_MAX_PX + : 0; + if (delta === 0) return; + + const before = scroller.scrollTop; + scroller.scrollTop = before + delta; + if (scroller.scrollTop !== before) update(); + }; + + const handleMove = (moveEvent: PointerEvent) => { + pointerX = moveEvent.clientX; + pointerY = moveEvent.clientY; + update(); + }; + + const finish = (commit: boolean) => { + cancelAnimationFrame(frame); + window.removeEventListener('pointermove', handleMove); + window.removeEventListener('pointerup', handleUp); + window.removeEventListener('pointercancel', handleCancel); + window.removeEventListener('lostpointercapture', handleUp); + window.removeEventListener('blur', handleCancel); + window.removeEventListener('keydown', handleKey, true); + document.body.classList.remove(bodyClass); + if (grid.hasPointerCapture?.(pointerId)) grid.releasePointerCapture(pointerId); + + const next = this.draft; + this.draft = null; + this.activeId = null; + this.mode = null; + this.float = null; + this.metrics = null; + if (commit && next) this.#commit(next); + }; + + const handleUp = () => finish(true); + const handleCancel = () => finish(false); + const handleKey = (keyEvent: KeyboardEvent) => { + if (keyEvent.key !== 'Escape') return; + keyEvent.preventDefault(); + finish(false); + }; + + window.addEventListener('pointermove', handleMove); + window.addEventListener('pointerup', handleUp); + window.addEventListener('pointercancel', handleCancel); + window.addEventListener('lostpointercapture', handleUp); + window.addEventListener('blur', handleCancel); + window.addEventListener('keydown', handleKey, true); + frame = requestAnimationFrame(autoScroll); + } + + /** Keyboard equivalent of dragging the widget by its handle. */ + nudgeMove(id: string, colDelta: number, rowDelta: number): void { + const source = this.#getItems().find((item) => item.id === id); + if (!source) return; + this.#commit( + moveElement( + this.#snapshot(), + id, + source.colStart + colDelta, + Math.max(1, source.rowStart + rowDelta) + ) + ); + } + + /** Keyboard equivalent of dragging the resize handle. */ + nudgeResize(id: string, colDelta: number, rowDelta: number): void { + const source = this.#getItems().find((item) => item.id === id); + if (!source) return; + this.#commit( + resizeElement( + this.#snapshot(), + id, + source.colSpan + colDelta, + source.rowSpan + rowDelta, + this.#minSize(source) + ) + ); + } +} + +const GRID_INTERACTION_KEY = Symbol('grid-interaction'); + +export function setGridInteraction(interaction: GridInteraction): GridInteraction { + return setContext(GRID_INTERACTION_KEY, interaction); +} + +export function getGridInteraction(): GridInteraction { + const interaction = getContext(GRID_INTERACTION_KEY); + if (!interaction) throw new Error('WidgetCard must be rendered inside DashboardGrid'); + return interaction; +} diff --git a/src/lib/components/dashboard/grid-layout.ts b/src/lib/components/dashboard/grid-layout.ts new file mode 100644 index 00000000..b7b90298 --- /dev/null +++ b/src/lib/components/dashboard/grid-layout.ts @@ -0,0 +1,116 @@ +import { GRID_COLUMNS, GRID_MAX_ROW_SPAN } from '$lib/domain/dashboard'; +import { clamp } from './widget-size'; + +export interface GridRect { + colStart: number; + rowStart: number; + colSpan: number; + rowSpan: number; +} + +export type GridItem = GridRect & { id: string }; + +function overlaps(a: GridItem, b: GridItem): boolean { + return ( + a.id !== b.id && + a.colStart < b.colStart + b.colSpan && + a.colStart + a.colSpan > b.colStart && + a.rowStart < b.rowStart + b.rowSpan && + a.rowStart + a.rowSpan > b.rowStart + ); +} + +function byPosition(a: GridItem, b: GridItem): number { + return a.rowStart - b.rowStart || a.colStart - b.colStart; +} + +function collidesAt(items: GridItem[], item: GridItem, rowStart: number): boolean { + const probe = { ...item, rowStart }; + return items.some((other) => overlaps(other, probe)); +} + +// Vertical gravity: walk items in reading order and settle each one at the highest row it can reach +// without touching anything already settled. Items are only ever compared against items placed +// before them, so a single pass both closes gaps and resolves leftover overlap. +export function compactLayout(items: T[]): T[] { + const settled: T[] = []; + + for (const item of [...items].sort(byPosition)) { + let rowStart = item.rowStart; + while (collidesAt(settled, item, rowStart)) rowStart += 1; + while (rowStart > 1 && !collidesAt(settled, item, rowStart - 1)) rowStart -= 1; + item.rowStart = rowStart; + settled.push(item); + } + + return items; +} + +// Clears space for `target` by displacing whatever it now overlaps. A collider first tries the gap +// the target just vacated (directly above it), which is what makes dragging one widget onto another +// read as a swap; otherwise it drops below the target and cascades into whatever it hits in turn. +function displaceColliders( + items: T[], + target: T, + handled: Set, + options: { movingUp: boolean; allowTuck: boolean } +): void { + const colliders = items.filter((item) => overlaps(item, target)).sort(byPosition); + // Resolve the nearest collider in the direction of travel first so cascades run away from the target. + if (options.movingUp) colliders.reverse(); + + for (const collider of colliders) { + if (handled.has(collider.id)) continue; + handled.add(collider.id); + + const tuckedRow = target.rowStart - collider.rowSpan; + if (options.allowTuck && tuckedRow >= 1 && !collidesAt(items, collider, tuckedRow)) { + collider.rowStart = tuckedRow; + continue; + } + + collider.rowStart = target.rowStart + target.rowSpan; + displaceColliders(items, collider, handled, options); + } +} + +/** Drops the widget at (colStart, rowStart), pushing others out of the way, then applies gravity. */ +export function moveElement( + items: T[], + id: string, + colStart: number, + rowStart: number +): T[] { + const target = items.find((item) => item.id === id); + if (!target) return items; + + const movingUp = rowStart < target.rowStart; + target.colStart = clamp(colStart, 1, GRID_COLUMNS - target.colSpan + 1); + target.rowStart = Math.max(1, rowStart); + + displaceColliders(items, target, new Set([id]), { movingUp, allowTuck: true }); + return compactLayout(items); +} + +/** + * Resizes the widget from its top-left anchor, pushing others down, then applies gravity. + * `min` is the widget's own floor; it wins over the pointer but not over the grid's right edge. + */ +export function resizeElement( + items: T[], + id: string, + colSpan: number, + rowSpan: number, + min: { minColSpan: number; minRowSpan: number } = { minColSpan: 1, minRowSpan: 1 } +): T[] { + const target = items.find((item) => item.id === id); + if (!target) return items; + + const maxColSpan = GRID_COLUMNS - target.colStart + 1; + target.colSpan = clamp(colSpan, Math.min(min.minColSpan, maxColSpan), maxColSpan); + target.rowSpan = clamp(rowSpan, Math.min(min.minRowSpan, GRID_MAX_ROW_SPAN), GRID_MAX_ROW_SPAN); + + // Growing a widget must never lift its neighbours above it, so no tucking here. + displaceColliders(items, target, new Set([id]), { movingUp: false, allowTuck: false }); + return compactLayout(items); +} diff --git a/src/lib/components/dashboard/widget-registry.ts b/src/lib/components/dashboard/widget-registry.ts new file mode 100644 index 00000000..d751f655 --- /dev/null +++ b/src/lib/components/dashboard/widget-registry.ts @@ -0,0 +1,208 @@ +import type { Component } from 'svelte'; +import type { WidgetColSpan, WidgetRowSpan, WidgetType } from '$lib/domain/dashboard'; +import { ACCENT } from '$lib/helper/accent-color.helper'; +import FleetStatWidget from './widgets/FleetStatWidget.svelte'; +import ExpenseBreakdownWidget from './widgets/ExpenseBreakdownWidget.svelte'; +import MonthlyExpenseTrendWidget from './widgets/MonthlyExpenseTrendWidget.svelte'; +import VehicleLeaderboardWidget from './widgets/VehicleLeaderboardWidget.svelte'; +import FleetFuelTrendWidget from './widgets/FleetFuelTrendWidget.svelte'; +import FuelConsumptionTrendWidget from './widgets/FuelConsumptionTrendWidget.svelte'; +import MileageOverviewWidget from './widgets/MileageOverviewWidget.svelte'; +import StatusDonutWidget from './widgets/StatusDonutWidget.svelte'; +import VehicleHealthWidget from './widgets/VehicleHealthWidget.svelte'; +import UpcomingRemindersWidget from './widgets/UpcomingRemindersWidget.svelte'; +import VehicleQuickListWidget from './widgets/VehicleQuickListWidget.svelte'; +import RecentActivityWidget from './widgets/RecentActivityWidget.svelte'; +import CalendarWidget from './widgets/CalendarWidget.svelte'; +import Car from '@lucide/svelte/icons/car'; +import Route from '@lucide/svelte/icons/route'; +import Fuel from '@lucide/svelte/icons/fuel'; +import DollarSign from '@lucide/svelte/icons/dollar-sign'; +import CircleGauge from '@lucide/svelte/icons/circle-gauge'; + +interface WidgetDefinition { + type: WidgetType; + title: string; + description: string; + component: Component; + extraProps?: Record; + defaultColSpan: WidgetColSpan; + defaultRowSpan: WidgetRowSpan; + /** Only stat-style widgets set these — WidgetCard renders the icon in its header when present. */ + icon?: Component<{ class?: string }>; + iconColor?: string; +} + +export const WIDGET_REGISTRY: Record = { + 'stat-vehicle-count': { + type: 'stat-vehicle-count', + title: 'Total Vehicles', + description: 'Number of vehicles in your garage', + component: FleetStatWidget, + extraProps: { metric: 'vehicle-count' }, + defaultColSpan: 3, + defaultRowSpan: 2, + icon: Car, + iconColor: ACCENT.denim.gradient + }, + 'stat-total-distance': { + type: 'stat-total-distance', + title: 'Total Distance', + description: 'Distance driven across the whole fleet', + component: FleetStatWidget, + extraProps: { metric: 'total-distance' }, + defaultColSpan: 3, + defaultRowSpan: 2, + icon: Route, + iconColor: ACCENT.plum.gradient + }, + 'stat-fuel-used': { + type: 'stat-fuel-used', + title: 'Total Fuel Used', + description: 'Fuel consumed across the whole fleet', + component: FleetStatWidget, + extraProps: { metric: 'fuel-used' }, + defaultColSpan: 3, + defaultRowSpan: 2, + icon: Fuel, + iconColor: ACCENT.moss.gradient + }, + 'stat-total-expenses': { + type: 'stat-total-expenses', + title: 'Total Expenses', + description: 'Fuel, maintenance and insurance spend combined', + component: FleetStatWidget, + extraProps: { metric: 'total-expenses' }, + defaultColSpan: 3, + defaultRowSpan: 2, + icon: DollarSign, + iconColor: ACCENT.ochre.gradient + }, + 'stat-cost-per-distance': { + type: 'stat-cost-per-distance', + title: 'Cost / Distance', + description: 'Overall running cost per unit distance', + component: FleetStatWidget, + extraProps: { metric: 'cost-per-distance' }, + defaultColSpan: 3, + defaultRowSpan: 2, + icon: CircleGauge, + iconColor: ACCENT.clay.gradient + }, + 'expense-breakdown-donut': { + type: 'expense-breakdown-donut', + title: 'Expenses by Category', + description: 'Fuel vs. maintenance vs. compliance spend', + component: ExpenseBreakdownWidget, + defaultColSpan: 6, + defaultRowSpan: 8 + }, + 'monthly-expense-trend': { + type: 'monthly-expense-trend', + title: 'Monthly Expense Trend', + description: 'Last 12 months of spend by category', + component: MonthlyExpenseTrendWidget, + defaultColSpan: 9, + defaultRowSpan: 8 + }, + 'cost-by-vehicle-leaderboard': { + type: 'cost-by-vehicle-leaderboard', + title: 'Cost by Vehicle', + description: 'Which vehicles cost the most to run', + component: VehicleLeaderboardWidget, + extraProps: { metric: 'cost' }, + defaultColSpan: 6, + defaultRowSpan: 8 + }, + 'fleet-fuel-trend': { + type: 'fleet-fuel-trend', + title: 'Fleet Fuel Trend', + description: 'Daily fuel usage across the fleet', + component: FleetFuelTrendWidget, + defaultColSpan: 9, + defaultRowSpan: 8 + }, + 'fuel-consumption-trend': { + type: 'fuel-consumption-trend', + title: 'Fuel Consumption Trend', + description: 'Fuel usage over time, one line per vehicle', + component: FuelConsumptionTrendWidget, + defaultColSpan: 9, + defaultRowSpan: 8 + }, + 'mileage-overview-trend': { + type: 'mileage-overview-trend', + title: 'Mileage Overview', + description: 'Mileage over time, one line per vehicle', + component: MileageOverviewWidget, + defaultColSpan: 9, + defaultRowSpan: 8 + }, + 'efficiency-leaderboard': { + type: 'efficiency-leaderboard', + title: 'Efficiency Leaderboard', + description: 'Vehicles ranked by fuel efficiency', + component: VehicleLeaderboardWidget, + extraProps: { metric: 'efficiency' }, + defaultColSpan: 6, + defaultRowSpan: 8 + }, + 'pucc-status-donut': { + type: 'pucc-status-donut', + title: 'Other Compliance Status', + description: 'Emissions, roadworthiness & registration status across the fleet', + component: StatusDonutWidget, + extraProps: { metric: 'other' }, + defaultColSpan: 6, + defaultRowSpan: 6 + }, + 'insurance-status-donut': { + type: 'insurance-status-donut', + title: 'Insurance Status', + description: 'Insurance policy status across the fleet', + component: StatusDonutWidget, + extraProps: { metric: 'insurance' }, + defaultColSpan: 6, + defaultRowSpan: 6 + }, + 'vehicle-health-distribution': { + type: 'vehicle-health-distribution', + title: 'Vehicle Health', + description: 'Overall good/attention/needs-action breakdown', + component: VehicleHealthWidget, + defaultColSpan: 6, + defaultRowSpan: 8 + }, + 'upcoming-reminders-list': { + type: 'upcoming-reminders-list', + title: 'Upcoming Reminders', + description: 'Reminders coming due soon', + component: UpcomingRemindersWidget, + defaultColSpan: 6, + defaultRowSpan: 8 + }, + 'vehicle-quick-list': { + type: 'vehicle-quick-list', + title: 'My Vehicles', + description: 'Quick access to your vehicles', + component: VehicleQuickListWidget, + defaultColSpan: 9, + defaultRowSpan: 8 + }, + 'recent-activity-feed': { + type: 'recent-activity-feed', + title: 'Recent Activity', + description: 'Latest fuel and maintenance logs across the fleet', + component: RecentActivityWidget, + defaultColSpan: 6, + defaultRowSpan: 8 + }, + 'activity-calendar': { + type: 'activity-calendar', + title: 'Activity Calendar', + description: 'Upcoming reminders and past fuel/maintenance activity by date', + component: CalendarWidget, + defaultColSpan: 4, + defaultRowSpan: 10 + } +}; diff --git a/src/lib/components/dashboard/widget-size.ts b/src/lib/components/dashboard/widget-size.ts new file mode 100644 index 00000000..55303a6a --- /dev/null +++ b/src/lib/components/dashboard/widget-size.ts @@ -0,0 +1,39 @@ +import { GRID_COLUMNS, GRID_MAX_ROW_SPAN } from '$lib/domain/dashboard'; + +/** Base pixel height of one row unit; a widget's rowSpan is this many units tall (before gaps). */ +export const ROW_UNIT_PX = 28; + +export function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} + +function clampColSpan(span: number): number { + return clamp(Math.round(span), 1, GRID_COLUMNS); +} + +function clampRowSpan(span: number): number { + return clamp(Math.round(span), 1, GRID_MAX_ROW_SPAN); +} + +function clampColStart(colStart: number, colSpan: number): number { + return clamp(Math.round(colStart), 1, GRID_COLUMNS - clampColSpan(colSpan) + 1); +} + +function clampRowStart(rowStart: number): number { + return Math.max(1, Math.round(rowStart)); +} + +// CSS custom properties for a widget's grid rect; WidgetCard only wires these to grid-column/grid-row at the 12-col breakpoint since colStart/rowStart don't translate to the stacked mobile layout. +export function widgetGridVars(item: { + colStart: number; + rowStart: number; + colSpan: number; + rowSpan: number; +}): string { + return ( + `--wc-col-start: ${clampColStart(item.colStart, item.colSpan)}; ` + + `--wc-col-span: ${clampColSpan(item.colSpan)}; ` + + `--wc-row-start: ${clampRowStart(item.rowStart)}; ` + + `--wc-row-span: ${clampRowSpan(item.rowSpan)};` + ); +} diff --git a/src/lib/components/dashboard/widgets/CalendarWidget.svelte b/src/lib/components/dashboard/widgets/CalendarWidget.svelte new file mode 100644 index 00000000..de5ce4cc --- /dev/null +++ b/src/lib/components/dashboard/widgets/CalendarWidget.svelte @@ -0,0 +1,176 @@ + + +{#if loading && !summary} +
+ Loading calendar... +
+{:else} +
+ + + {#snippet children({ months, weekdays })} + + + + + + {#each months as month, monthIndex (month)} + + + + + + + + {#each weekdays as weekday (weekday)} + + {weekday.slice(0, 2)} + + {/each} + + + + {#each month.weeks as weekDates (weekDates)} + + {#each weekDates as date (date)} + {@const events = eventsByDate.get(date.toString()) ?? []} + + + {#if events.length} +
+ {#each events.slice(0, 3) as event (event.id)} + + {/each} +
+ {/if} +
+ {/each} +
+ {/each} +
+
+
+ {/each} +
+ {/snippet} +
+ +
+

+ {formatDateForCalendar(selected)} +

+ {#if selectedEvents.length === 0} +

No activity or reminders on this date.

+ {:else} +
    + {#each selectedEvents as event (event.id)} + {@const Icon = KIND_ICONS[event.kind]} +
  • + + + + {event.label} +
  • + {/each} +
+ {/if} +
+
+{/if} diff --git a/src/lib/components/dashboard/widgets/ExpenseBreakdownWidget.svelte b/src/lib/components/dashboard/widgets/ExpenseBreakdownWidget.svelte new file mode 100644 index 00000000..af518ddd --- /dev/null +++ b/src/lib/components/dashboard/widgets/ExpenseBreakdownWidget.svelte @@ -0,0 +1,33 @@ + + + formatCurrency(value)} +/> diff --git a/src/lib/components/dashboard/widgets/FleetFuelTrendWidget.svelte b/src/lib/components/dashboard/widgets/FleetFuelTrendWidget.svelte new file mode 100644 index 00000000..234fe9a8 --- /dev/null +++ b/src/lib/components/dashboard/widgets/FleetFuelTrendWidget.svelte @@ -0,0 +1,45 @@ + + + `${value.toFixed(1)} L`} + xFormatter={(v: Date) => + v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })} +> + {#snippet filter()} + + {/snippet} + diff --git a/src/lib/components/dashboard/widgets/FleetStatWidget.svelte b/src/lib/components/dashboard/widgets/FleetStatWidget.svelte new file mode 100644 index 00000000..3d0b149a --- /dev/null +++ b/src/lib/components/dashboard/widgets/FleetStatWidget.svelte @@ -0,0 +1,45 @@ + + + + + diff --git a/src/lib/components/dashboard/widgets/FuelConsumptionTrendWidget.svelte b/src/lib/components/dashboard/widgets/FuelConsumptionTrendWidget.svelte new file mode 100644 index 00000000..2fee81bd --- /dev/null +++ b/src/lib/components/dashboard/widgets/FuelConsumptionTrendWidget.svelte @@ -0,0 +1,39 @@ + + + `${value.toFixed(1)} L`} + xFormatter={(v: Date) => + v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })} +> + {#snippet filter()} + + {/snippet} + diff --git a/src/lib/components/dashboard/widgets/MileageOverviewWidget.svelte b/src/lib/components/dashboard/widgets/MileageOverviewWidget.svelte new file mode 100644 index 00000000..bbe5d082 --- /dev/null +++ b/src/lib/components/dashboard/widgets/MileageOverviewWidget.svelte @@ -0,0 +1,40 @@ + + + formatMileage(value, series.fuelType)} + xFormatter={(v: Date) => + v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })} +> + {#snippet filter()} + + {/snippet} + diff --git a/src/lib/components/dashboard/widgets/MonthlyExpenseTrendWidget.svelte b/src/lib/components/dashboard/widgets/MonthlyExpenseTrendWidget.svelte new file mode 100644 index 00000000..8bd49ba1 --- /dev/null +++ b/src/lib/components/dashboard/widgets/MonthlyExpenseTrendWidget.svelte @@ -0,0 +1,13 @@ + + + diff --git a/src/lib/components/dashboard/widgets/RecentActivityWidget.svelte b/src/lib/components/dashboard/widgets/RecentActivityWidget.svelte new file mode 100644 index 00000000..e1414348 --- /dev/null +++ b/src/lib/components/dashboard/widgets/RecentActivityWidget.svelte @@ -0,0 +1,10 @@ + + +
+ +
diff --git a/src/lib/components/dashboard/widgets/StatusDonutWidget.svelte b/src/lib/components/dashboard/widgets/StatusDonutWidget.svelte new file mode 100644 index 00000000..2dc7743e --- /dev/null +++ b/src/lib/components/dashboard/widgets/StatusDonutWidget.svelte @@ -0,0 +1,30 @@ + + + + + diff --git a/src/lib/components/dashboard/widgets/UpcomingRemindersWidget.svelte b/src/lib/components/dashboard/widgets/UpcomingRemindersWidget.svelte new file mode 100644 index 00000000..bc0f535c --- /dev/null +++ b/src/lib/components/dashboard/widgets/UpcomingRemindersWidget.svelte @@ -0,0 +1,49 @@ + + +{#if summary && summary.compliance.upcomingReminders.length > 0} +
+ {#each summary.compliance.upcomingReminders.slice(0, 6) as reminder (reminder.id)} +
+ + + +
+

+ {reminder.vehicleName} + {#if reminder.vehiclePlate} + ({reminder.vehiclePlate}) + {/if} +

+

+ {reminder.note || reminder.type} +

+
+ +
+ {/each} +
+{:else if loading} +
Loading reminders...
+{:else} +
+ No upcoming reminders +
+{/if} diff --git a/src/lib/components/dashboard/widgets/VehicleHealthWidget.svelte b/src/lib/components/dashboard/widgets/VehicleHealthWidget.svelte new file mode 100644 index 00000000..c107f7a0 --- /dev/null +++ b/src/lib/components/dashboard/widgets/VehicleHealthWidget.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/dashboard/widgets/VehicleLeaderboardWidget.svelte b/src/lib/components/dashboard/widgets/VehicleLeaderboardWidget.svelte new file mode 100644 index 00000000..e18ff21d --- /dev/null +++ b/src/lib/components/dashboard/widgets/VehicleLeaderboardWidget.svelte @@ -0,0 +1,67 @@ + + + + +
+
+ +
+
+ +
+
diff --git a/src/lib/components/dashboard/widgets/VehicleQuickListWidget.svelte b/src/lib/components/dashboard/widgets/VehicleQuickListWidget.svelte new file mode 100644 index 00000000..ed851aff --- /dev/null +++ b/src/lib/components/dashboard/widgets/VehicleQuickListWidget.svelte @@ -0,0 +1,23 @@ + + +{#if vehicleStore.vehicles && vehicleStore.vehicles.length > 0} +
+ {#each vehicleStore.vehicles.slice(0, 5) as vehicle (vehicle.id)} + { + if (vehicle.id) goto(`/garage/${vehicle.id}`); + }} + actions={false} + /> + {/each} +
+{:else} +
+ No vehicles yet +
+{/if} diff --git a/src/lib/components/feature/auth/login-form.svelte b/src/lib/components/feature/auth/login-form.svelte index f8094bc3..f18f9375 100644 --- a/src/lib/components/feature/auth/login-form.svelte +++ b/src/lib/components/feature/auth/login-form.svelte @@ -6,6 +6,7 @@ import UserIcon from '@lucide/svelte/icons/circle-user-round'; import RectangleEllipsis from '@lucide/svelte/icons/rectangle-ellipsis'; import SubmitButton from '$appui/SubmitButton.svelte'; + import * as m from '$lib/paraglide/messages'; let username = $state(''); @@ -33,6 +34,11 @@ }; +
+

{m.auth_login_title()}

+

{m.auth_login_subtitle()}

+
+
diff --git a/src/lib/components/feature/compliance/ComplianceContextMenu.svelte b/src/lib/components/feature/compliance/ComplianceContextMenu.svelte new file mode 100644 index 00000000..a3ba871f --- /dev/null +++ b/src/lib/components/feature/compliance/ComplianceContextMenu.svelte @@ -0,0 +1,36 @@ + + + sheetStore.openSheet(ComplianceForm, m.compliance_menu_sheet_title(), '', document)} + onDelete={deleteDoc} +/> diff --git a/src/lib/components/feature/compliance/ComplianceForm.svelte b/src/lib/components/feature/compliance/ComplianceForm.svelte new file mode 100644 index 00000000..593f5762 --- /dev/null +++ b/src/lib/components/feature/compliance/ComplianceForm.svelte @@ -0,0 +1,263 @@ + + + e.preventDefault()}> +
+ {#if !suppliedVehicleId} + + {/if} + + + {#snippet children({ props })} + {@const TypeIcon = getComplianceTypeIcon($formData.type)} + {m.compliance_form_type_label()} + + +
+ + {getComplianceTypeLabel($formData.type, m)} +
+
+ + {#each Object.keys(COMPLIANCE_TYPES) as value} + {@const ItemIcon = getComplianceTypeIcon(value)} + + + {getComplianceTypeLabel(value, m)} + + {/each} + +
+ {/snippet} +
+ +
+ + {#if $formData.type === 'other'} + + + {#snippet children({ props })} + {m.compliance_form_other_label_label()} + + {/snippet} + + + + {/if} + + + + {m.compliance_form_attachment_label()} + + + + + + + {#snippet children({ props })} + {getComplianceIssuerLabel($formData.type, m)} + + {/snippet} + + + + + + + {#snippet children({ props })} + {getComplianceDocumentNumberLabel($formData.type, m)} + + {/snippet} + + + + + + + {#snippet children({ props })} + {m.compliance_form_start_date_label()} + + {/snippet} + + + + + + + + + {#snippet children({ props })} + {m.compliance_form_cost_label()} + + {/snippet} + + + + + + + {#snippet children({ props })} + {m.compliance_form_notes_label()} + + {...restProps}> diff --git a/src/lib/components/ui/tooltip/index.ts b/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 00000000..36a2f3ab --- /dev/null +++ b/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,19 @@ +import Root from './tooltip.svelte'; +import Trigger from './tooltip-trigger.svelte'; +import Content from './tooltip-content.svelte'; +import Provider from './tooltip-provider.svelte'; +import Portal from './tooltip-portal.svelte'; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal +}; diff --git a/src/lib/components/ui/tooltip/tooltip-content.svelte b/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 00000000..296abeba --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,52 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
+ {/snippet} +
+
+
diff --git a/src/lib/components/ui/tooltip/tooltip-portal.svelte b/src/lib/components/ui/tooltip/tooltip-portal.svelte new file mode 100644 index 00000000..7b9e8f9f --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/tooltip/tooltip-provider.svelte b/src/lib/components/ui/tooltip/tooltip-provider.svelte new file mode 100644 index 00000000..49f8379b --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-provider.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 00000000..f9a3bf41 --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/components/ui/tooltip/tooltip.svelte b/src/lib/components/ui/tooltip/tooltip.svelte new file mode 100644 index 00000000..5a327a71 --- /dev/null +++ b/src/lib/components/ui/tooltip/tooltip.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/lib/composables/sheet-form.svelte.ts b/src/lib/composables/sheet-form.svelte.ts new file mode 100644 index 00000000..8c2a2936 --- /dev/null +++ b/src/lib/composables/sheet-form.svelte.ts @@ -0,0 +1,61 @@ +import { superForm, defaults } from 'sveltekit-superforms'; +import { zod4 } from 'sveltekit-superforms/adapters'; +import type { z } from 'zod'; + +export function createSheetForm(config: { + schema: z.ZodObject; + onUpdated?: (event: { form: any }) => void; + validationMethod?: 'onsubmit' | 'oninput' | 'onblur'; +}) { + let processing = $state(false); + let attachment = $state(); + let removeExistingAttachment = $state(false); + + const form = superForm(defaults(zod4(config.schema)), { + validators: zod4(config.schema), + SPA: true, + resetForm: false, + validationMethod: config.validationMethod || 'onsubmit', + onUpdated: config.onUpdated as any + }); + + const { form: formData, enhance } = form; + + function setVehicleId(vehicleId: string) { + formData.update((fd: any) => ({ + ...fd, + vehicleId + })); + } + + function resetAttachment() { + attachment = undefined; + removeExistingAttachment = false; + } + + return { + get processing() { + return processing; + }, + set processing(v) { + processing = v; + }, + get attachment() { + return attachment; + }, + set attachment(v) { + attachment = v; + }, + get removeExistingAttachment() { + return removeExistingAttachment; + }, + set removeExistingAttachment(v) { + removeExistingAttachment = v; + }, + form, + formData, + enhance, + setVehicleId, + resetAttachment + }; +} diff --git a/src/lib/config/env.server.ts b/src/lib/config/env.server.ts index 9d3a33c8..740f3923 100644 --- a/src/lib/config/env.server.ts +++ b/src/lib/config/env.server.ts @@ -1,15 +1,13 @@ import { env as privateEnv } from '$env/dynamic/private'; -import { env as publicEnv } from '$env/dynamic/public'; +import { clientEnv as publicClientEnv } from './env'; /** - * Client-side environment configuration - * Only includes public environment variables that are safe to expose to the browser + * Client-side environment configuration, plus the server-only way of disabling auth + * (the private env var also works, so it can be set in container deployments). */ export const clientEnv = { - DEMO_MODE: publicEnv.TRACKTOR_DEMO_MODE === 'true', - // Allow disabling auth via either the public or private env var so it works in container deployments - DISABLE_AUTH: - publicEnv.TRACKTOR_DISABLE_AUTH === 'true' || privateEnv.TRACKTOR_DISABLE_AUTH === 'true' + ...publicClientEnv, + DISABLE_AUTH: publicClientEnv.DISABLE_AUTH || privateEnv.TRACKTOR_DISABLE_AUTH === 'true' } as const; function getCorsOrigins(origins?: string): string[] { @@ -25,7 +23,7 @@ function getCorsOrigins(origins?: string): string[] { function getDBPath(): string | undefined { switch (privateEnv.NODE_ENV) { - case 'dev': + case 'development': return './tracktor.dev.db'; case 'test': return './tracktor.test.db'; @@ -40,7 +38,7 @@ function getDBPath(): string | undefined { * Includes all environment variables */ export const serverEnv = { - NODE_ENV: privateEnv.NODE_ENV || 'dev', + NODE_ENV: privateEnv.NODE_ENV || 'development', DB_PATH: privateEnv.DB_PATH || getDBPath(), UPLOADS_DIR: privateEnv.UPLOADS_DIR || './uploads', CORS_ORIGINS: getCorsOrigins(privateEnv.CORS_ORIGINS), @@ -64,6 +62,6 @@ export const env = { } as const; // Environment helpers -export const isDevelopment = env.NODE_ENV === 'dev'; +export const isDevelopment = env.NODE_ENV === 'development'; export const isProduction = env.NODE_ENV === 'production'; export const isTest = env.NODE_ENV === 'test'; diff --git a/src/lib/config/themes.ts b/src/lib/config/themes.ts index b84031c1..c2c801c5 100644 --- a/src/lib/config/themes.ts +++ b/src/lib/config/themes.ts @@ -43,14 +43,14 @@ export const themes: Record = { }, active: false, colors: { - primary: 'oklch(0.577 0.245 27.325)', + primary: 'oklch(0.577 0.085 27.325)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.577 0.245 27.325)' + ring: 'oklch(0.577 0.085 27.325)' }, darkColors: { - primary: 'oklch(0.677 0.245 27.325)', + primary: 'oklch(0.677 0.085 27.325)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.677 0.245 27.325)' + ring: 'oklch(0.677 0.085 27.325)' } }, rose: { @@ -60,14 +60,14 @@ export const themes: Record = { }, active: false, colors: { - primary: 'oklch(0.643 0.181 19.301)', + primary: 'oklch(0.643 0.065 19.301)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.643 0.181 19.301)' + ring: 'oklch(0.643 0.065 19.301)' }, darkColors: { - primary: 'oklch(0.743 0.181 19.301)', + primary: 'oklch(0.743 0.065 19.301)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.743 0.181 19.301)' + ring: 'oklch(0.743 0.065 19.301)' } }, blue: { @@ -77,14 +77,14 @@ export const themes: Record = { }, active: false, colors: { - primary: 'oklch(0.488 0.243 264.376)', + primary: 'oklch(0.488 0.073 264.376)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.488 0.243 264.376)' + ring: 'oklch(0.488 0.073 264.376)' }, darkColors: { - primary: 'oklch(0.588 0.243 264.376)', + primary: 'oklch(0.588 0.073 264.376)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.588 0.243 264.376)' + ring: 'oklch(0.588 0.073 264.376)' } }, green: { @@ -94,14 +94,14 @@ export const themes: Record = { }, active: false, colors: { - primary: 'oklch(0.54 0.194 142.495)', + primary: 'oklch(0.54 0.068 142.495)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.54 0.194 142.495)' + ring: 'oklch(0.54 0.068 142.495)' }, darkColors: { - primary: 'oklch(0.64 0.194 142.495)', + primary: 'oklch(0.64 0.068 142.495)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.64 0.194 142.495)' + ring: 'oklch(0.64 0.068 142.495)' } }, purple: { @@ -111,14 +111,14 @@ export const themes: Record = { }, active: false, colors: { - primary: 'oklch(0.589 0.25 292.514)', + primary: 'oklch(0.589 0.08 292.514)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.589 0.25 292.514)' + ring: 'oklch(0.589 0.08 292.514)' }, darkColors: { - primary: 'oklch(0.689 0.25 292.514)', + primary: 'oklch(0.689 0.08 292.514)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.689 0.25 292.514)' + ring: 'oklch(0.689 0.08 292.514)' } }, orange: { @@ -128,14 +128,14 @@ export const themes: Record = { }, active: false, colors: { - primary: 'oklch(0.662 0.218 46.415)', + primary: 'oklch(0.662 0.076 46.415)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.662 0.218 46.415)' + ring: 'oklch(0.662 0.076 46.415)' }, darkColors: { - primary: 'oklch(0.762 0.218 46.415)', + primary: 'oklch(0.762 0.076 46.415)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.762 0.218 46.415)' + ring: 'oklch(0.762 0.076 46.415)' } }, yellow: { @@ -145,14 +145,14 @@ export const themes: Record = { }, active: false, colors: { - primary: 'oklch(0.776 0.173 91.935)', + primary: 'oklch(0.776 0.069 91.935)', primaryForeground: 'oklch(0.205 0 0)', - ring: 'oklch(0.776 0.173 91.935)' + ring: 'oklch(0.776 0.069 91.935)' }, darkColors: { - primary: 'oklch(0.876 0.173 91.935)', + primary: 'oklch(0.876 0.069 91.935)', primaryForeground: 'oklch(0.145 0 0)', - ring: 'oklch(0.876 0.173 91.935)' + ring: 'oklch(0.876 0.069 91.935)' } }, teal: { @@ -162,14 +162,14 @@ export const themes: Record = { }, active: false, colors: { - primary: 'oklch(0.559 0.151 180.735)', + primary: 'oklch(0.559 0.06 180.735)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.559 0.151 180.735)' + ring: 'oklch(0.559 0.06 180.735)' }, darkColors: { - primary: 'oklch(0.659 0.151 180.735)', + primary: 'oklch(0.659 0.06 180.735)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.659 0.151 180.735)' + ring: 'oklch(0.659 0.06 180.735)' } }, indigo: { @@ -179,14 +179,14 @@ export const themes: Record = { }, active: false, colors: { - primary: 'oklch(0.482 0.198 272.314)', + primary: 'oklch(0.482 0.063 272.314)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.482 0.198 272.314)' + ring: 'oklch(0.482 0.063 272.314)' }, darkColors: { - primary: 'oklch(0.582 0.198 272.314)', + primary: 'oklch(0.582 0.063 272.314)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.582 0.198 272.314)' + ring: 'oklch(0.582 0.063 272.314)' } }, pink: { @@ -196,16 +196,14 @@ export const themes: Record = { }, active: false, colors: { - primary: 'oklch(0.671 0.221 349.761)', + primary: 'oklch(0.671 0.071 349.761)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.671 0.221 349.761)' + ring: 'oklch(0.671 0.071 349.761)' }, darkColors: { - primary: 'oklch(0.771 0.221 349.761)', + primary: 'oklch(0.771 0.071 349.761)', primaryForeground: 'oklch(0.985 0 0)', - ring: 'oklch(0.771 0.221 349.761)' + ring: 'oklch(0.771 0.071 349.761)' } } }; - -export const themesList = Object.values(themes); diff --git a/src/lib/constants/timezones.ts b/src/lib/constants/timezones.ts deleted file mode 100644 index 4fa4c4e5..00000000 --- a/src/lib/constants/timezones.ts +++ /dev/null @@ -1,460 +0,0 @@ -/** - * Canonical IANA Timezone Database - * This provides a consistent list of timezones across all operating systems and Node.js versions. - * Unlike Intl.supportedValuesOf('timeZone'), this list remains constant regardless of the runtime environment. - * - * Source: IANA Time Zone Database (https://www.iana.org/time-zones) - * Last updated: 2024 - */ - -export const CANONICAL_TIMEZONES = [ - // UTC - 'UTC', - - // Africa - 'Africa/Abidjan', - 'Africa/Accra', - 'Africa/Addis_Ababa', - 'Africa/Algiers', - 'Africa/Asmara', - 'Africa/Bamako', - 'Africa/Bangui', - 'Africa/Banjul', - 'Africa/Bissau', - 'Africa/Blantyre', - 'Africa/Brazzaville', - 'Africa/Bujumbura', - 'Africa/Cairo', - 'Africa/Casablanca', - 'Africa/Ceuta', - 'Africa/Conakry', - 'Africa/Dakar', - 'Africa/Dar_es_Salaam', - 'Africa/Djibouti', - 'Africa/Douala', - 'Africa/El_Aaiun', - 'Africa/Freetown', - 'Africa/Gaborone', - 'Africa/Harare', - 'Africa/Johannesburg', - 'Africa/Juba', - 'Africa/Kampala', - 'Africa/Khartoum', - 'Africa/Kigali', - 'Africa/Kinshasa', - 'Africa/Lagos', - 'Africa/Libreville', - 'Africa/Lome', - 'Africa/Luanda', - 'Africa/Lubumbashi', - 'Africa/Lusaka', - 'Africa/Malabo', - 'Africa/Maputo', - 'Africa/Maseru', - 'Africa/Mbabane', - 'Africa/Mogadishu', - 'Africa/Monrovia', - 'Africa/Nairobi', - 'Africa/Ndjamena', - 'Africa/Niamey', - 'Africa/Nouakchott', - 'Africa/Ouagadougou', - 'Africa/Porto-Novo', - 'Africa/Sao_Tome', - 'Africa/Tripoli', - 'Africa/Tunis', - 'Africa/Windhoek', - - // America - North America - 'America/Adak', - 'America/Anchorage', - 'America/Anguilla', - 'America/Antigua', - 'America/Araguaina', - 'America/Argentina/Buenos_Aires', - 'America/Argentina/Catamarca', - 'America/Argentina/Cordoba', - 'America/Argentina/Jujuy', - 'America/Argentina/La_Rioja', - 'America/Argentina/Mendoza', - 'America/Argentina/Rio_Gallegos', - 'America/Argentina/Salta', - 'America/Argentina/San_Juan', - 'America/Argentina/San_Luis', - 'America/Argentina/Tucuman', - 'America/Argentina/Ushuaia', - 'America/Aruba', - 'America/Asuncion', - 'America/Atikokan', - 'America/Bahia', - 'America/Bahia_Banderas', - 'America/Barbados', - 'America/Belem', - 'America/Belize', - 'America/Blanc-Sablon', - 'America/Boa_Vista', - 'America/Bogota', - 'America/Boise', - 'America/Cambridge_Bay', - 'America/Campo_Grande', - 'America/Cancun', - 'America/Caracas', - 'America/Cayenne', - 'America/Cayman', - 'America/Chicago', - 'America/Chihuahua', - 'America/Costa_Rica', - 'America/Creston', - 'America/Cuiaba', - 'America/Curacao', - 'America/Danmarkshavn', - 'America/Dawson', - 'America/Dawson_Creek', - 'America/Denver', - 'America/Detroit', - 'America/Dominica', - 'America/Edmonton', - 'America/Eirunepe', - 'America/El_Salvador', - 'America/Fort_Nelson', - 'America/Fortaleza', - 'America/Glace_Bay', - 'America/Godthab', - 'America/Goose_Bay', - 'America/Grand_Turk', - 'America/Grenada', - 'America/Guadeloupe', - 'America/Guatemala', - 'America/Guayaquil', - 'America/Guyana', - 'America/Halifax', - 'America/Havana', - 'America/Hermosillo', - 'America/Indiana/Indianapolis', - 'America/Indiana/Knox', - 'America/Indiana/Marengo', - 'America/Indiana/Petersburg', - 'America/Indiana/Tell_City', - 'America/Indiana/Vevay', - 'America/Indiana/Vincennes', - 'America/Indiana/Winamac', - 'America/Inuvik', - 'America/Iqaluit', - 'America/Jamaica', - 'America/Juneau', - 'America/Kentucky/Louisville', - 'America/Kentucky/Monticello', - 'America/Kralendijk', - 'America/La_Paz', - 'America/Lima', - 'America/Los_Angeles', - 'America/Lower_Princes', - 'America/Maceio', - 'America/Managua', - 'America/Manaus', - 'America/Marigot', - 'America/Martinique', - 'America/Matamoros', - 'America/Mazatlan', - 'America/Menominee', - 'America/Merida', - 'America/Metlakatla', - 'America/Mexico_City', - 'America/Miquelon', - 'America/Moncton', - 'America/Monterrey', - 'America/Montevideo', - 'America/Montserrat', - 'America/Nassau', - 'America/New_York', - 'America/Nipigon', - 'America/Nome', - 'America/Noronha', - 'America/North_Dakota/Beulah', - 'America/North_Dakota/Center', - 'America/North_Dakota/New_Salem', - 'America/Nuuk', - 'America/Ojinaga', - 'America/Panama', - 'America/Pangnirtung', - 'America/Paramaribo', - 'America/Phoenix', - 'America/Port-au-Prince', - 'America/Port_of_Spain', - 'America/Porto_Velho', - 'America/Puerto_Rico', - 'America/Punta_Arenas', - 'America/Rainy_River', - 'America/Rankin_Inlet', - 'America/Recife', - 'America/Regina', - 'America/Resolute', - 'America/Rio_Branco', - 'America/Santarem', - 'America/Santiago', - 'America/Santo_Domingo', - 'America/Sao_Paulo', - 'America/Scoresbysund', - 'America/Sitka', - 'America/St_Barthelemy', - 'America/St_Johns', - 'America/St_Kitts', - 'America/St_Lucia', - 'America/St_Thomas', - 'America/St_Vincent', - 'America/Swift_Current', - 'America/Tegucigalpa', - 'America/Thule', - 'America/Thunder_Bay', - 'America/Tijuana', - 'America/Toronto', - 'America/Tortola', - 'America/Vancouver', - 'America/Whitehorse', - 'America/Winnipeg', - 'America/Yakutat', - 'America/Yellowknife', - - // Antarctica - 'Antarctica/Casey', - 'Antarctica/Davis', - 'Antarctica/DumontDUrville', - 'Antarctica/Macquarie', - 'Antarctica/Mawson', - 'Antarctica/McMurdo', - 'Antarctica/Palmer', - 'Antarctica/Rothera', - 'Antarctica/Syowa', - 'Antarctica/Troll', - 'Antarctica/Vostok', - - // Arctic - 'Arctic/Longyearbyen', - - // Asia - 'Asia/Aden', - 'Asia/Almaty', - 'Asia/Amman', - 'Asia/Anadyr', - 'Asia/Aqtau', - 'Asia/Aqtobe', - 'Asia/Ashgabat', - 'Asia/Atyrau', - 'Asia/Baghdad', - 'Asia/Bahrain', - 'Asia/Baku', - 'Asia/Bangkok', - 'Asia/Barnaul', - 'Asia/Beirut', - 'Asia/Bishkek', - 'Asia/Brunei', - 'Asia/Chita', - 'Asia/Choibalsan', - 'Asia/Colombo', - 'Asia/Damascus', - 'Asia/Dhaka', - 'Asia/Dili', - 'Asia/Dubai', - 'Asia/Dushanbe', - 'Asia/Famagusta', - 'Asia/Gaza', - 'Asia/Hebron', - 'Asia/Ho_Chi_Minh', - 'Asia/Hong_Kong', - 'Asia/Hovd', - 'Asia/Irkutsk', - 'Asia/Jakarta', - 'Asia/Jayapura', - 'Asia/Jerusalem', - 'Asia/Kabul', - 'Asia/Kamchatka', - 'Asia/Karachi', - 'Asia/Kathmandu', - 'Asia/Khandyga', - 'Asia/Kolkata', - 'Asia/Krasnoyarsk', - 'Asia/Kuala_Lumpur', - 'Asia/Kuching', - 'Asia/Kuwait', - 'Asia/Macau', - 'Asia/Magadan', - 'Asia/Makassar', - 'Asia/Manila', - 'Asia/Muscat', - 'Asia/Nicosia', - 'Asia/Novokuznetsk', - 'Asia/Novosibirsk', - 'Asia/Omsk', - 'Asia/Oral', - 'Asia/Phnom_Penh', - 'Asia/Pontianak', - 'Asia/Pyongyang', - 'Asia/Qatar', - 'Asia/Qostanay', - 'Asia/Qyzylorda', - 'Asia/Riyadh', - 'Asia/Sakhalin', - 'Asia/Samarkand', - 'Asia/Seoul', - 'Asia/Shanghai', - 'Asia/Singapore', - 'Asia/Srednekolymsk', - 'Asia/Taipei', - 'Asia/Tashkent', - 'Asia/Tbilisi', - 'Asia/Tehran', - 'Asia/Thimphu', - 'Asia/Tokyo', - 'Asia/Tomsk', - 'Asia/Ulaanbaatar', - 'Asia/Urumqi', - 'Asia/Ust-Nera', - 'Asia/Vientiane', - 'Asia/Vladivostok', - 'Asia/Yakutsk', - 'Asia/Yangon', - 'Asia/Yekaterinburg', - 'Asia/Yerevan', - - // Atlantic - 'Atlantic/Azores', - 'Atlantic/Bermuda', - 'Atlantic/Canary', - 'Atlantic/Cape_Verde', - 'Atlantic/Faroe', - 'Atlantic/Madeira', - 'Atlantic/Reykjavik', - 'Atlantic/South_Georgia', - 'Atlantic/St_Helena', - 'Atlantic/Stanley', - - // Australia - 'Australia/Adelaide', - 'Australia/Brisbane', - 'Australia/Broken_Hill', - 'Australia/Darwin', - 'Australia/Eucla', - 'Australia/Hobart', - 'Australia/Lindeman', - 'Australia/Lord_Howe', - 'Australia/Melbourne', - 'Australia/Perth', - 'Australia/Sydney', - - // Europe - 'Europe/Amsterdam', - 'Europe/Andorra', - 'Europe/Astrakhan', - 'Europe/Athens', - 'Europe/Belgrade', - 'Europe/Berlin', - 'Europe/Bratislava', - 'Europe/Brussels', - 'Europe/Bucharest', - 'Europe/Budapest', - 'Europe/Busingen', - 'Europe/Chisinau', - 'Europe/Copenhagen', - 'Europe/Dublin', - 'Europe/Gibraltar', - 'Europe/Guernsey', - 'Europe/Helsinki', - 'Europe/Isle_of_Man', - 'Europe/Istanbul', - 'Europe/Jersey', - 'Europe/Kaliningrad', - 'Europe/Kiev', - 'Europe/Kirov', - 'Europe/Lisbon', - 'Europe/Ljubljana', - 'Europe/London', - 'Europe/Luxembourg', - 'Europe/Madrid', - 'Europe/Malta', - 'Europe/Mariehamn', - 'Europe/Minsk', - 'Europe/Monaco', - 'Europe/Moscow', - 'Europe/Oslo', - 'Europe/Paris', - 'Europe/Podgorica', - 'Europe/Prague', - 'Europe/Riga', - 'Europe/Rome', - 'Europe/Samara', - 'Europe/San_Marino', - 'Europe/Sarajevo', - 'Europe/Saratov', - 'Europe/Simferopol', - 'Europe/Skopje', - 'Europe/Sofia', - 'Europe/Stockholm', - 'Europe/Tallinn', - 'Europe/Tirane', - 'Europe/Ulyanovsk', - 'Europe/Uzhgorod', - 'Europe/Vaduz', - 'Europe/Vatican', - 'Europe/Vienna', - 'Europe/Vilnius', - 'Europe/Volgograd', - 'Europe/Warsaw', - 'Europe/Zagreb', - 'Europe/Zaporozhye', - 'Europe/Zurich', - - // Indian Ocean - 'Indian/Antananarivo', - 'Indian/Chagos', - 'Indian/Christmas', - 'Indian/Cocos', - 'Indian/Comoro', - 'Indian/Kerguelen', - 'Indian/Mahe', - 'Indian/Maldives', - 'Indian/Mauritius', - 'Indian/Mayotte', - 'Indian/Reunion', - - // Pacific - 'Pacific/Apia', - 'Pacific/Auckland', - 'Pacific/Bougainville', - 'Pacific/Chatham', - 'Pacific/Chuuk', - 'Pacific/Easter', - 'Pacific/Efate', - 'Pacific/Enderbury', - 'Pacific/Fakaofo', - 'Pacific/Fiji', - 'Pacific/Funafuti', - 'Pacific/Galapagos', - 'Pacific/Gambier', - 'Pacific/Guadalcanal', - 'Pacific/Guam', - 'Pacific/Honolulu', - 'Pacific/Kiritimati', - 'Pacific/Kosrae', - 'Pacific/Kwajalein', - 'Pacific/Majuro', - 'Pacific/Marquesas', - 'Pacific/Midway', - 'Pacific/Nauru', - 'Pacific/Niue', - 'Pacific/Norfolk', - 'Pacific/Noumea', - 'Pacific/Pago_Pago', - 'Pacific/Palau', - 'Pacific/Pitcairn', - 'Pacific/Pohnpei', - 'Pacific/Port_Moresby', - 'Pacific/Rarotonga', - 'Pacific/Saipan', - 'Pacific/Tahiti', - 'Pacific/Tarawa', - 'Pacific/Tongatapu', - 'Pacific/Wake', - 'Pacific/Wallis' -] as const; - -export type CanonicalTimezone = (typeof CANONICAL_TIMEZONES)[number]; diff --git a/src/lib/domain/compliance.ts b/src/lib/domain/compliance.ts new file mode 100644 index 00000000..8492746b --- /dev/null +++ b/src/lib/domain/compliance.ts @@ -0,0 +1,204 @@ +import { z } from 'zod'; +import type { Component } from 'svelte'; +import Shield from '@lucide/svelte/icons/shield'; +import Leaf from '@lucide/svelte/icons/leaf'; +import ClipboardCheck from '@lucide/svelte/icons/clipboard-check'; +import FileText from '@lucide/svelte/icons/file-text'; +import Shapes from '@lucide/svelte/icons/shapes'; +import { apiDateString, optionalApiDateString } from './shared'; +import { getNextDueDate } from '$lib/helper/recurrence.helper'; + +/** + * Generic categories covering compliance schemes worldwide: emissions testing (India PUCC, US + * smog check), roadworthiness/safety inspection (EU MOT, Germany TÜV, Australia roadworthy, + * NZ WoF), plus universal insurance and registration/road-tax renewal. `other` + `otherLabel` + * covers anything not fitting those buckets rather than enumerating every regional scheme. + */ +export const COMPLIANCE_TYPES = { + insurance: 'insurance', + emissions: 'emissions', + roadworthiness: 'roadworthiness', + registration: 'registration', + other: 'other' +} as const; + +export type ComplianceType = keyof typeof COMPLIANCE_TYPES; + +const COMPLIANCE_TYPE_ICONS: Record> = { + insurance: Shield, + emissions: Leaf, + roadworthiness: ClipboardCheck, + registration: FileText, + other: Shapes +}; + +export function getComplianceTypeIcon(type: string): Component<{ class?: string }> { + return COMPLIANCE_TYPE_ICONS[type as ComplianceType] ?? Shapes; +} + +export const COMPLIANCE_RECURRENCE_TYPES = { + none: 'none', + yearly: 'yearly', + monthly: 'monthly', + no_end: 'no_end' +} as const; + +export function getComplianceRecurrenceTypeLabel(type: string, m: any): string { + switch (type) { + case 'none': + return m.compliance_recurrence_type_fixed(); + case 'yearly': + return m.compliance_recurrence_type_yearly(); + case 'monthly': + return m.compliance_recurrence_type_monthly(); + case 'no_end': + return m.compliance_recurrence_type_no_end(); + default: + return m.compliance_recurrence_type_fixed(); + } +} + +export function getComplianceTypeLabel(type: string, m: any): string { + switch (type) { + case 'insurance': + return m.compliance_type_insurance(); + case 'emissions': + return m.compliance_type_emissions(); + case 'roadworthiness': + return m.compliance_type_roadworthiness(); + case 'registration': + return m.compliance_type_registration(); + case 'other': + return m.compliance_type_other(); + default: + return m.compliance_type_other(); + } +} + +/** Label for the document/policy/certificate number field, tailored per type. */ +export function getComplianceDocumentNumberLabel(type: string, m: any): string { + switch (type) { + case 'insurance': + return m.compliance_field_policy_number(); + case 'emissions': + case 'roadworthiness': + return m.compliance_field_certificate_number(); + case 'registration': + return m.compliance_field_registration_number(); + default: + return m.compliance_field_document_number(); + } +} + +/** Label for the issuer field (provider/testing center/authority), tailored per type. */ +export function getComplianceIssuerLabel(type: string, m: any): string { + switch (type) { + case 'insurance': + return m.compliance_field_provider(); + case 'emissions': + return m.compliance_field_testing_center(); + case 'roadworthiness': + return m.compliance_field_inspection_center(); + default: + return m.compliance_field_issuing_authority(); + } +} + +export interface Compliance { + id: string | null; + vehicleId: string; + type: ComplianceType; + otherLabel: string | null; + documentNumber: string; + issuer: string; + startDate: Date; + endDate: Date | null; + recurrenceType: keyof typeof COMPLIANCE_RECURRENCE_TYPES; + recurrenceInterval: number; + cost: number | null; + notes: string | null; + attachment: string | null; + vehicleMake?: string | null; + vehicleModel?: string | null; + vehiclePlate?: string | null; +} + +export type ComplianceStatus = 'valid' | 'expiring_soon' | 'expired'; + +/** Next date this document is due (start/end + recurrence). Null when it never expires. */ +export function getComplianceNextDue(doc: Compliance): Date | null { + const baseDate = doc.endDate ?? doc.startDate; + if (!baseDate) return null; + if (doc.recurrenceType === 'no_end') return null; + if (doc.recurrenceType === 'none') return new Date(baseDate); + return getNextDueDate(new Date(baseDate), doc.recurrenceType, doc.recurrenceInterval); +} + +/** Compliance status derived from the next due date, using the same 30-day "expiring soon" window. */ +export function getComplianceStatus(doc: Compliance, today: Date = new Date()): ComplianceStatus { + const nextDue = getComplianceNextDue(doc); + if (!nextDue) return 'valid'; + const thirtyDaysFromNow = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000); + if (nextDue < today) return 'expired'; + if (nextDue <= thirtyDaysFromNow) return 'expiring_soon'; + return 'valid'; +} + +const complianceTypeOptions = Object.keys(COMPLIANCE_TYPES) as [ + ComplianceType, + ...ComplianceType[] +]; + +const complianceRecurrenceOptions = Object.keys( + COMPLIANCE_RECURRENCE_TYPES +) as (keyof typeof COMPLIANCE_RECURRENCE_TYPES)[]; + +export const complianceSchema = z + .object({ + id: z.string().nullable(), + vehicleId: z.uuid(), + type: z.enum(complianceTypeOptions).default('insurance'), + otherLabel: z.string().max(100, 'It must be less than 100 characters.').nullable().optional(), + documentNumber: z + .string() + .min(2, 'It must be more than 1 character.') + .max(50, 'It must be less than 50 characters.'), + issuer: z + .string() + .min(2, 'It must be more than 1 character.') + .max(100, 'It must be less than 100 characters.'), + startDate: apiDateString, + endDate: optionalApiDateString, + recurrenceType: z + .enum( + complianceRecurrenceOptions as [ + keyof typeof COMPLIANCE_RECURRENCE_TYPES, + ...Array + ] + ) + .default('none'), + recurrenceInterval: z.number().int().positive().default(1), + cost: z.float32().nonnegative().nullable().optional(), + notes: z.string().nullable(), + attachment: z.string().nullable() + }) + .refine( + (data) => { + if (data.type !== 'other') return true; + return !!data.otherLabel && data.otherLabel.trim().length > 0; + }, + { message: 'Please name the compliance type', path: ['otherLabel'] } + ) + .refine( + (data) => { + if (data.recurrenceType === undefined) return true; + if (data.recurrenceType !== 'none') return true; + if (data.endDate === undefined) return true; + if (!data.endDate) return false; + if (!data.startDate) return true; + return new Date(data.endDate) > new Date(data.startDate); + }, + { message: 'End date must be after start date when recurrence is fixed' } + ); + +export type ComplianceSchema = typeof complianceSchema; diff --git a/src/lib/domain/config.ts b/src/lib/domain/config.ts index f09e2014..dbf33b19 100644 --- a/src/lib/domain/config.ts +++ b/src/lib/domain/config.ts @@ -1,3 +1,5 @@ +import type { SettingsConfig } from '$lib/helper/settings-form.helper'; + export interface Config { key: string; value?: string; @@ -8,30 +10,10 @@ export interface Config { export const BOOLEAN_CONFIG_KEYS = new Set([ 'featureFuelLog', 'featureMaintenance', - 'featurePucc', + 'featureCompliance', 'featureReminders', - 'featureInsurance', 'featureOverview', 'notificationProcessingEnabled' ]); -export interface Configs { - dateFormat: string; - currency: string; - unitOfDistance: string; - unitOfVolume: string; - unitOfLpg: string; - unitOfCng: string; - mileageUnitFormat: string; - locale: string; - timezone: string; - customCss?: string; - featureFuelLog?: boolean; - featureMaintenance?: boolean; - featurePucc?: boolean; - featureReminders?: boolean; - featureInsurance?: boolean; - featureOverview?: boolean; - notificationProcessingEnabled?: boolean; - notificationProcessingSchedule?: string; -} +export type Configs = SettingsConfig; diff --git a/src/lib/domain/dashboard.ts b/src/lib/domain/dashboard.ts new file mode 100644 index 00000000..c973443e --- /dev/null +++ b/src/lib/domain/dashboard.ts @@ -0,0 +1,314 @@ +import { z } from 'zod'; +import type { Vehicle } from './vehicle'; + +export interface StatusBucket { + valid: number; + expiringSoon: number; + expired: number; + notAvailable: number; +} + +export interface VehicleSummary { + id: string; + make: string; + model: string; + licensePlate: string | null; + image: string | null; + odometer: number; + fuelType: Vehicle['fuelType']; + totalDistance: number; + totalFuelCost: number; + totalMaintenanceCost: number; + totalComplianceCost: number; + totalExpenses: number; + avgMileage: number | null; + costPerDistance: number | null; + otherComplianceStatus: 'valid' | 'expiring_soon' | 'expired' | 'not_available'; + insuranceStatus: 'valid' | 'expiring_soon' | 'expired' | 'not_available'; + healthStatus: 'good' | 'attention' | 'needs_action'; +} + +export interface UpcomingReminder { + id: string; + vehicleId: string; + vehicleName: string; + vehiclePlate: string | null; + type: string; + note: string | null; + dueDate: string; + daysUntilDue: number; +} + +export interface ActivityEntry { + id: string; + type: 'fuel' | 'maintenance'; + vehicleId: string; + vehicleName: string; + date: string; + description: string; + cost: number; +} + +export interface MonthlyExpensePoint { + month: string; // YYYY-MM + fuel: number; + maintenance: number; + compliance: number; + total: number; +} + +export interface DashboardSummary { + fleet: { + totalVehicles: number; + totalDistance: number; + totalFuelUsed: number; + totalExpenses: number; + costPerDistance: number | null; + }; + expenses: { + breakdown: { fuel: number; maintenance: number; compliance: number }; + monthlyTrend: MonthlyExpensePoint[]; + }; + fuel: { + dailyTrend: Array<{ date: string; fuelAmount: number }>; + }; + compliance: { + other: StatusBucket; + insurance: StatusBucket; + vehicleHealth: { good: number; attention: number; needsAction: number }; + upcomingReminders: UpcomingReminder[]; + }; + vehicles: VehicleSummary[]; + activity: ActivityEntry[]; +} + +export const WIDGET_TYPES = { + 'stat-vehicle-count': 'stat-vehicle-count', + 'stat-total-distance': 'stat-total-distance', + 'stat-fuel-used': 'stat-fuel-used', + 'stat-total-expenses': 'stat-total-expenses', + 'stat-cost-per-distance': 'stat-cost-per-distance', + 'expense-breakdown-donut': 'expense-breakdown-donut', + 'monthly-expense-trend': 'monthly-expense-trend', + 'cost-by-vehicle-leaderboard': 'cost-by-vehicle-leaderboard', + 'fleet-fuel-trend': 'fleet-fuel-trend', + 'fuel-consumption-trend': 'fuel-consumption-trend', + 'mileage-overview-trend': 'mileage-overview-trend', + 'efficiency-leaderboard': 'efficiency-leaderboard', + 'pucc-status-donut': 'pucc-status-donut', + 'insurance-status-donut': 'insurance-status-donut', + 'vehicle-health-distribution': 'vehicle-health-distribution', + 'upcoming-reminders-list': 'upcoming-reminders-list', + 'vehicle-quick-list': 'vehicle-quick-list', + 'recent-activity-feed': 'recent-activity-feed', + 'activity-calendar': 'activity-calendar' +} as const; + +export type WidgetType = keyof typeof WIDGET_TYPES; + +const widgetTypeOptions = Object.keys(WIDGET_TYPES) as [WidgetType, ...WidgetType[]]; + +// Widgets have an explicit (colStart, rowStart) position plus a (colSpan, rowSpan) size in a 12-col grid; every move/resize/remove recompacts the layout (see grid-compaction.ts). +export const GRID_COLUMNS = 12; +export const GRID_MAX_ROW_SPAN = 12; + +export type WidgetColSpan = number; +export type WidgetRowSpan = number; + +export interface WidgetLayoutItem { + id: string; + type: WidgetType; + colStart: number; + rowStart: number; + colSpan: WidgetColSpan; + rowSpan: WidgetRowSpan; +} + +const widgetLayoutItemSchema = z.object({ + id: z.string(), + type: z.enum(widgetTypeOptions), + colStart: z.number().int().min(1).max(GRID_COLUMNS), + rowStart: z.number().int().min(1), + colSpan: z.number().int().min(1).max(GRID_COLUMNS), + rowSpan: z.number().int().min(1).max(GRID_MAX_ROW_SPAN) +}); + +export const widgetLayoutSchema = z.array(widgetLayoutItemSchema).max(32); + +/** + * Smallest rect a widget stays legible in. Enforced when resizing, when adding, and when reading a + * persisted layout back — so a layout saved before a widget's minimum changed heals on load rather + * than rendering clipped. One row unit is 28px plus the 16px grid gap, so N rows ≈ 44N − 16 px. + */ +export interface WidgetMinSize { + minColSpan: number; + minRowSpan: number; +} + +const DEFAULT_MIN_SIZE: WidgetMinSize = { minColSpan: 2, minRowSpan: 3 }; + +const WIDGET_MIN_SIZES: Partial> = { + // Label over a single number, laid out beside the icon. Legible down to 2 rows (72px); 3 columns + // is what the longest label ("Total Fuel Used") needs beside the icon before it starts eliding. + 'stat-vehicle-count': { minColSpan: 3, minRowSpan: 2 }, + 'stat-total-distance': { minColSpan: 3, minRowSpan: 2 }, + 'stat-fuel-used': { minColSpan: 3, minRowSpan: 2 }, + 'stat-total-expenses': { minColSpan: 3, minRowSpan: 2 }, + 'stat-cost-per-distance': { minColSpan: 3, minRowSpan: 2 }, + // Ring plus legend. + 'expense-breakdown-donut': { minColSpan: 3, minRowSpan: 6 }, + 'pucc-status-donut': { minColSpan: 3, minRowSpan: 6 }, + 'insurance-status-donut': { minColSpan: 3, minRowSpan: 6 }, + 'vehicle-health-distribution': { minColSpan: 3, minRowSpan: 6 }, + // Plot area plus a rotated x-axis. + 'monthly-expense-trend': { minColSpan: 4, minRowSpan: 6 }, + 'fleet-fuel-trend': { minColSpan: 4, minRowSpan: 6 }, + 'fuel-consumption-trend': { minColSpan: 4, minRowSpan: 6 }, + 'mileage-overview-trend': { minColSpan: 4, minRowSpan: 6 }, + // Scrolling lists stay usable at a couple of visible rows. + 'cost-by-vehicle-leaderboard': { minColSpan: 3, minRowSpan: 4 }, + 'efficiency-leaderboard': { minColSpan: 3, minRowSpan: 4 }, + 'upcoming-reminders-list': { minColSpan: 3, minRowSpan: 4 }, + 'vehicle-quick-list': { minColSpan: 3, minRowSpan: 4 }, + 'recent-activity-feed': { minColSpan: 3, minRowSpan: 4 }, + // Month grid (7 cols) plus a per-date event list below it. + 'activity-calendar': { minColSpan: 4, minRowSpan: 8 } +}; + +export function widgetMinSize(type: WidgetType): WidgetMinSize { + return WIDGET_MIN_SIZES[type] ?? DEFAULT_MIN_SIZE; +} + +/** + * Ships every registered widget. colSpan/rowSpan/colStart/rowStart here are the + * *default-layout* sizes/positions, independent of each widget's defaultColSpan in + * widget-registry.ts (which only applies when a widget is added later via "Add Widget"). + * `compactLayout` (run on every load) resolves any remaining overlap, so these don't need + * to be perfectly non-overlapping on their own. + */ +export const DEFAULT_WIDGET_LAYOUT: WidgetLayoutItem[] = [ + { id: 'default-1', type: 'stat-vehicle-count', colStart: 6, rowStart: 5, colSpan: 3, rowSpan: 2 }, + { + id: 'default-2', + type: 'stat-total-distance', + colStart: 4, + rowStart: 1, + colSpan: 4, + rowSpan: 2 + }, + { id: 'default-3', type: 'stat-fuel-used', colStart: 6, rowStart: 3, colSpan: 3, rowSpan: 2 }, + { + id: 'default-4', + type: 'stat-total-expenses', + colStart: 8, + rowStart: 1, + colSpan: 5, + rowSpan: 2 + }, + { + id: 'default-5', + type: 'stat-cost-per-distance', + colStart: 1, + rowStart: 1, + colSpan: 3, + rowSpan: 2 + }, + { id: 'default-6', type: 'activity-calendar', colStart: 9, rowStart: 3, colSpan: 4, rowSpan: 10 }, + { + id: 'default-7', + type: 'monthly-expense-trend', + colStart: 1, + rowStart: 7, + colSpan: 8, + rowSpan: 6 + }, + { id: 'default-8', type: 'fleet-fuel-trend', colStart: 1, rowStart: 13, colSpan: 12, rowSpan: 8 }, + { + id: 'default-9', + type: 'fuel-consumption-trend', + colStart: 1, + rowStart: 21, + colSpan: 6, + rowSpan: 8 + }, + { + id: 'default-10', + type: 'mileage-overview-trend', + colStart: 7, + rowStart: 29, + colSpan: 6, + rowSpan: 8 + }, + { + id: 'default-11', + type: 'expense-breakdown-donut', + colStart: 7, + rowStart: 21, + colSpan: 3, + rowSpan: 8 + }, + { + id: 'default-12', + type: 'cost-by-vehicle-leaderboard', + colStart: 7, + rowStart: 37, + colSpan: 6, + rowSpan: 8 + }, + { + id: 'default-13', + type: 'efficiency-leaderboard', + colStart: 1, + rowStart: 29, + colSpan: 6, + rowSpan: 8 + }, + { + id: 'default-14', + type: 'vehicle-health-distribution', + colStart: 10, + rowStart: 21, + colSpan: 3, + rowSpan: 8 + }, + { + id: 'default-15', + type: 'pucc-status-donut', + colStart: 1, + rowStart: 37, + colSpan: 3, + rowSpan: 6 + }, + { + id: 'default-16', + type: 'insurance-status-donut', + colStart: 4, + rowStart: 37, + colSpan: 3, + rowSpan: 6 + }, + { + id: 'default-17', + type: 'vehicle-quick-list', + colStart: 7, + rowStart: 45, + colSpan: 6, + rowSpan: 8 + }, + { + id: 'default-18', + type: 'recent-activity-feed', + colStart: 1, + rowStart: 43, + colSpan: 6, + rowSpan: 10 + }, + { + id: 'default-19', + type: 'upcoming-reminders-list', + colStart: 1, + rowStart: 3, + colSpan: 5, + rowSpan: 4 + } +]; diff --git a/src/lib/domain/fuel.ts b/src/lib/domain/fuel.ts index 35f0c620..0e923b96 100644 --- a/src/lib/domain/fuel.ts +++ b/src/lib/domain/fuel.ts @@ -1,35 +1,34 @@ -import { parseDate } from '$lib/helper/format.helper'; import { z } from 'zod'; +import { apiDateString } from './shared'; export interface FuelLog { id: string | null; vehicleId: string; date: Date; odometer: number | null; + distanceDriven?: number | null; filled: boolean; missedLast: boolean; fuelAmount: number | null; + rate: number | null; cost: number; notes: string | null; attachment: string | null; mileage?: number; + vehicleMake?: string | null; + vehicleModel?: string | null; + vehiclePlate?: string | null; } export const fuelSchema = z.object({ id: z.string().nullable(), vehicleId: z.uuid(), - date: z.string().refine((val) => { - try { - parseDate(val); - return true; - } catch { - return false; - } - }, 'Invalid date format'), + date: apiDateString, odometer: z.number().positive().nullable(), filled: z.boolean().default(true), missedLast: z.boolean(), fuelAmount: z.number().positive().nullable(), + rate: z.float32().positive().nullable(), cost: z.float32().positive(), notes: z.string().nullable(), attachment: z.string().nullable() diff --git a/src/lib/domain/fuel/mileage.ts b/src/lib/domain/fuel/mileage.ts new file mode 100644 index 00000000..f93d16ef --- /dev/null +++ b/src/lib/domain/fuel/mileage.ts @@ -0,0 +1,89 @@ +export type FuelLogInput = { + filled: boolean; + missedLast: boolean; + odometer: number | null; + fuelAmount: number | null; +}; + +export function computeLatestOdometer( + baseOdometer: number | null, + maxFuelOdometer: number | null, + maxMaintenanceOdometer: number | null +): number { + const values = [baseOdometer, maxFuelOdometer, maxMaintenanceOdometer].filter( + (v): v is number => v !== null && v > 0 + ); + return values.length > 0 ? Math.max(...values) : 0; +} + +type MileageResult = { + distance: number; + totalFuel: number; +}; + +function findMileageWindows(fuelLogs: FuelLogInput[]): Map { + const results = new Map(); + + for (let index = 0; index < fuelLogs.length; index++) { + const log = fuelLogs[index]!; + if ( + index === 0 || + !log.filled || + log.missedLast || + log.odometer === null || + log.fuelAmount === null + ) { + continue; + } + + let startIndex = -1; + for (let i = index - 1; i >= 0; i--) { + if (fuelLogs[i]?.filled && fuelLogs[i]?.odometer !== null) { + startIndex = i; + break; + } + if (fuelLogs[i]?.missedLast) { + break; + } + } + + if (startIndex === -1) continue; + + const startLog = fuelLogs[startIndex]!; + const distance = log.odometer - startLog.odometer!; + if (distance <= 0) continue; + + let totalFuel = 0; + for (let i = startIndex + 1; i <= index; i++) { + const amount = fuelLogs[i]!.fuelAmount; + if (amount !== null) totalFuel += amount; + } + if (totalFuel === 0) continue; + + results.set(index, { distance, totalFuel }); + } + + return results; +} + +export function computeAverageMileage(fuelLogs: FuelLogInput[]): number | null { + const windows = [...findMileageWindows(fuelLogs).values()]; + if (windows.length === 0) return null; + + const ratios = windows.map((w) => w.distance / w.totalFuel); + const avg = ratios.reduce((sum, r) => sum + r, 0) / ratios.length; + return parseFloat(avg.toFixed(2)); +} + +export function computeTotalDistance(fuelLogs: FuelLogInput[]): number { + const windows = [...findMileageWindows(fuelLogs).values()]; + return windows.reduce((sum, w) => sum + w.distance, 0); +} + +export function computeMileagePerWindow(fuelLogs: FuelLogInput[]): (number | null)[] { + const windows = findMileageWindows(fuelLogs); + return fuelLogs.map((_, index) => { + const w = windows.get(index); + return w ? parseFloat((w.distance / w.totalFuel).toFixed(2)) : null; + }); +} diff --git a/src/lib/domain/index.ts b/src/lib/domain/index.ts deleted file mode 100644 index 14d77ab2..00000000 --- a/src/lib/domain/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -export type DataPoint = { - x: Date | string; - y: number | null; -}; - -export type Response = { - status: 'OK' | 'ERROR'; - data?: DataType; - error?: string; -}; - -export enum Status { - LOADING, - DONE, - ERROR -} - -// Re-export types from other modules -export type { FuelLog, FuelSchema } from './fuel'; -export type { Vehicle, VehicleSchema } from './vehicle'; -export type { Insurance, InsuranceSchema } from './insurance'; -export type { PollutionCertificate, PollutionCertificateSchema } from './pucc'; -export type { MaintenanceLog, MaintenanceSchema } from './maintenance'; -export type { Config } from './config'; -export type { Reminder, ReminderSchema } from './reminder'; -export type { Notification, NotificationSchema } from './notification'; - -// Re-export constants -export { INSURANCE_RECURRENCE_TYPES, insuranceSchema } from './insurance'; -export { PUCC_RECURRENCE_TYPES, pollutionCertificateSchema } from './pucc'; -export { - REMINDER_TYPES, - REMINDER_SCHEDULES, - REMINDER_RECURRENCE_TYPES, - reminderSchema -} from './reminder'; -export { - NOTIFICATION_CHANNELS, - NOTIFICATION_TYPES, - NOTIFICATION_SOURCES, - notificationSchema -} from './notification'; diff --git a/src/lib/domain/insurance.ts b/src/lib/domain/insurance.ts deleted file mode 100644 index 3018e2c5..00000000 --- a/src/lib/domain/insurance.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { parseDate } from '$lib/helper/format.helper'; -import { z } from 'zod'; - -export const INSURANCE_RECURRENCE_TYPES = { - none: 'none', - yearly: 'yearly', - monthly: 'monthly', - no_end: 'no_end' -} as const; - -// Helper function to get localized insurance recurrence type label -export function getInsuranceRecurrenceTypeLabel(type: string, m: any): string { - switch (type) { - case 'none': - return m.insurance_recurrence_type_fixed(); - case 'yearly': - return m.insurance_recurrence_type_yearly(); - case 'monthly': - return m.insurance_recurrence_type_monthly(); - case 'no_end': - return m.insurance_recurrence_type_no_end(); - default: - return m.insurance_recurrence_type_fixed(); - } -} - -export interface Insurance { - id: string | null; - vehicleId: string; - provider: string; - policyNumber: string; - startDate: Date; - endDate: Date | null; - recurrenceType: keyof typeof INSURANCE_RECURRENCE_TYPES; - recurrenceInterval: number; - cost: number; - notes: string | null; - attachment: string | null; -} - -const insuranceRecurrenceOptions = Object.keys( - INSURANCE_RECURRENCE_TYPES -) as (keyof typeof INSURANCE_RECURRENCE_TYPES)[]; - -export const insuranceSchema = z.object({ - id: z.string().nullable(), - vehicleId: z.uuid(), - provider: z - .string() - .min(2, 'It must be more than 1 character.') - .max(100, 'It must be less than 100 characters.'), - policyNumber: z - .string() - .min(2, 'It must be more than 1 character.') - .max(50, 'It must be less than 50 characters.'), - startDate: z.string().refine((val) => { - try { - parseDate(val); - return true; - } catch { - return false; - } - }, 'Invalid date format'), - endDate: z.string().nullable().optional(), - recurrenceType: z - .enum( - insuranceRecurrenceOptions as [ - keyof typeof INSURANCE_RECURRENCE_TYPES, - ...Array - ] - ) - .default('no_end'), - recurrenceInterval: z.number().int().positive().default(1), - cost: z.float32().positive(), - notes: z.string().nullable(), - attachment: z.string().nullable() -}); - -export type InsuranceSchema = typeof insuranceSchema; diff --git a/src/lib/domain/maintenance.ts b/src/lib/domain/maintenance.ts index 9f0b28d1..f1383893 100644 --- a/src/lib/domain/maintenance.ts +++ b/src/lib/domain/maintenance.ts @@ -1,5 +1,5 @@ -import { parseDate } from '$lib/helper/format.helper'; import { z } from 'zod'; +import { apiDateString } from './shared'; export interface MaintenanceLog { id: string | null; @@ -10,19 +10,15 @@ export interface MaintenanceLog { cost: number; notes: string | null; attachment: string | null; + vehicleMake?: string | null; + vehicleModel?: string | null; + vehiclePlate?: string | null; } export const maintenanceSchema = z.object({ id: z.string().nullable(), vehicleId: z.uuid(), - date: z.string().refine((val) => { - try { - parseDate(val); - return true; - } catch { - return false; - } - }, 'Invalid date format'), + date: apiDateString, odometer: z.number().positive(), serviceCenter: z .string() diff --git a/src/lib/domain/notification-provider.ts b/src/lib/domain/notification-provider.ts index ff43b8b3..4af80b67 100644 --- a/src/lib/domain/notification-provider.ts +++ b/src/lib/domain/notification-provider.ts @@ -22,7 +22,7 @@ export type NotificationProviderType = z.infer; // Webhook Provider Configuration -export const webhookProviderConfigSchema = z.object({ +const webhookProviderConfigSchema = z.object({ url: z.string().url('Valid webhook URL is required'), method: z.enum(['POST', 'PUT', 'PATCH']).default('POST'), headers: z.record(z.string(), z.string()).optional(), @@ -57,7 +57,7 @@ export const webhookProviderConfigSchema = z.object({ export type WebhookProviderConfig = z.infer; // Gotify Provider Configuration -export const gotifyProviderConfigSchema = z.object({ +const gotifyProviderConfigSchema = z.object({ serverUrl: z.string().url('Valid Gotify server URL is required'), appToken: z.string().min(1, 'App token is required'), priority: z.number().int().min(0).max(10).default(5) @@ -83,7 +83,7 @@ export const notificationProviderConfigSchema = z.discriminatedUnion('type', [ export type NotificationProviderConfig = z.infer; -export const notificationProviderSchema = z.object({ +const notificationProviderSchema = z.object({ id: z.string().uuid(), name: z.string().min(1, 'Provider name is required').max(100), type: notificationProviderTypeSchema, @@ -94,7 +94,7 @@ export const notificationProviderSchema = z.object({ updated_at: z.string() }); -export type NotificationProvider = z.infer; +type NotificationProvider = z.infer; export const createNotificationProviderSchema = z.object({ name: z.string().min(1, 'Provider name is required').max(100), diff --git a/src/lib/domain/notification.ts b/src/lib/domain/notification.ts index 9f27787d..4a0dff54 100644 --- a/src/lib/domain/notification.ts +++ b/src/lib/domain/notification.ts @@ -11,8 +11,7 @@ export const NOTIFICATION_TYPES = { alert: 'alert', information: 'information', maintenance: 'maintenance', - insurance: 'insurance', - pollution: 'pollution', + compliance: 'compliance', registration: 'registration' } as const; diff --git a/src/lib/domain/pucc.ts b/src/lib/domain/pucc.ts deleted file mode 100644 index c49b1e79..00000000 --- a/src/lib/domain/pucc.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { parseDate } from '$lib/helper/format.helper'; -import { z } from 'zod'; - -export const PUCC_RECURRENCE_TYPES = { - none: 'none', - yearly: 'yearly', - monthly: 'monthly', - no_end: 'no_end' -} as const; - -// Helper function to get localized PUCC recurrence type label -export function getPuccRecurrenceTypeLabel(type: string, m: any): string { - switch (type) { - case 'none': - return m.pollution_recurrence_type_fixed(); - case 'yearly': - return m.pollution_recurrence_type_yearly(); - case 'monthly': - return m.pollution_recurrence_type_monthly(); - case 'no_end': - return m.pollution_recurrence_type_no_end(); - default: - return m.pollution_recurrence_type_fixed(); - } -} - -export interface PollutionCertificate { - id: string | null; - vehicleId: string; - certificateNumber: string; - issueDate: Date; - expiryDate: Date | null; - recurrenceType: keyof typeof PUCC_RECURRENCE_TYPES; - recurrenceInterval: number; - testingCenter: string; - notes: string | null; - attachment: string | null; -} - -const puccRecurrenceOptions = Object.keys( - PUCC_RECURRENCE_TYPES -) as (keyof typeof PUCC_RECURRENCE_TYPES)[]; - -export const pollutionCertificateSchema = z.object({ - id: z.string().nullable(), - vehicleId: z.uuid(), - certificateNumber: z - .string() - .min(2, 'It must be more than 1 character.') - .max(50, 'It must be less than 50 characters.'), - issueDate: z.string().refine((val) => { - try { - parseDate(val); - return true; - } catch { - return false; - } - }, 'Invalid date format'), - expiryDate: z.string().nullable().optional(), - recurrenceType: z - .enum( - puccRecurrenceOptions as [ - keyof typeof PUCC_RECURRENCE_TYPES, - ...Array - ] - ) - .default('none'), - recurrenceInterval: z.number().int().positive().default(1), - testingCenter: z - .string() - .min(2, 'It must be more than 1 character.') - .max(100, 'It must be less than 100 characters.'), - notes: z.string().nullable(), - attachment: z.string().nullable() -}); -export type PollutionCertificateSchema = typeof pollutionCertificateSchema; diff --git a/src/lib/domain/reminder.ts b/src/lib/domain/reminder.ts index 9be1e7ac..65deaccd 100644 --- a/src/lib/domain/reminder.ts +++ b/src/lib/domain/reminder.ts @@ -1,5 +1,5 @@ -import { parseDate } from '$lib/helper/format.helper'; import { z } from 'zod'; +import { apiDateString, optionalApiDateString } from './shared'; export const REMINDER_TYPES = { maintenance: 'maintenance', @@ -93,6 +93,9 @@ export interface Reminder { recurrenceEndDate: Date | null; note: string | null; isCompleted: boolean; + vehicleMake?: string | null; + vehicleModel?: string | null; + vehiclePlate?: string | null; } const reminderTypeOptions = Object.keys(REMINDER_TYPES) as (keyof typeof REMINDER_TYPES)[]; @@ -111,14 +114,7 @@ export const reminderSchema = z.object({ reminderTypeOptions as [keyof typeof REMINDER_TYPES, ...Array] ) .default('custom'), - dueDate: z.string().refine((val) => { - try { - parseDate(val); - return true; - } catch (err) { - return false; - } - }, 'Invalid date format'), + dueDate: apiDateString, remindSchedule: z .enum( reminderScheduleOptions as [ @@ -136,18 +132,7 @@ export const reminderSchema = z.object({ ) .default('none'), recurrenceInterval: z.number().int().positive().default(1), - recurrenceEndDate: z - .string() - .refine((val) => { - if (!val) return true; - try { - parseDate(val); - return true; - } catch (err) { - return false; - } - }, 'Invalid date format') - .nullable(), + recurrenceEndDate: optionalApiDateString, note: z.string().max(500, 'Notes cannot be longer than 500 characters.').nullable(), isCompleted: z.boolean().default(false) }); diff --git a/src/lib/domain/shared.ts b/src/lib/domain/shared.ts new file mode 100644 index 00000000..502cfd0f --- /dev/null +++ b/src/lib/domain/shared.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +/** + * Validates date strings accepted by the API: anything the Date + * constructor can parse (ISO 8601 payloads from JSON-serialized Dates). + * Display-format validation for forms lives in format.helper (client-only). + */ +export const apiDateString = z + .string() + .refine((val) => !Number.isNaN(new Date(val).getTime()), 'Invalid date format'); + +/** Nullable/optional variant for fields that can be empty or omitted. */ +export const optionalApiDateString = apiDateString.nullable().optional(); + +export type DataPoint = { + x: Date | string; + y: number | null; +}; + +/** Result envelope returned by the client-side `$lib/services/*` layer. */ +export type Response = { + status: 'OK' | 'ERROR'; + data?: DataType; + error?: string; +}; diff --git a/src/lib/domain/status.ts b/src/lib/domain/status.ts deleted file mode 100644 index 0d97e581..00000000 --- a/src/lib/domain/status.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Status = { - message?: string; - type: 'ERROR' | 'SUCCESS' | 'INFO'; -}; diff --git a/src/lib/domain/vehicle.ts b/src/lib/domain/vehicle.ts index b018d345..8fc05185 100644 --- a/src/lib/domain/vehicle.ts +++ b/src/lib/domain/vehicle.ts @@ -1,4 +1,131 @@ import { z } from 'zod'; +import type { Component } from 'svelte'; +import Car from '@lucide/svelte/icons/car'; +import Bike from '@lucide/svelte/icons/bike'; +import Scooter from '@lucide/svelte/icons/scooter'; +import Truck from '@lucide/svelte/icons/truck'; +import Van from '@lucide/svelte/icons/van'; +import BusFront from '@lucide/svelte/icons/bus-front'; +import Tractor from '@lucide/svelte/icons/tractor'; +import Sailboat from '@lucide/svelte/icons/sailboat'; +import Caravan from '@lucide/svelte/icons/caravan'; +import Shapes from '@lucide/svelte/icons/shapes'; + +/** Standard paint colors offered in the vehicle color picker, before vintage muting. */ +export const STANDARD_VEHICLE_COLORS: Array<{ name: string; hex: string }> = [ + { name: 'White', hex: '#f5f5f5' }, + { name: 'Black', hex: '#1a1a1a' }, + { name: 'Silver', hex: '#c0c0c0' }, + { name: 'Gray', hex: '#808080' }, + { name: 'Red', hex: '#c62828' }, + { name: 'Blue', hex: '#1565c0' }, + { name: 'Navy', hex: '#0d3b66' }, + { name: 'Green', hex: '#2e7d32' }, + { name: 'Yellow', hex: '#f9d71c' }, + { name: 'Orange', hex: '#e65100' }, + { name: 'Brown', hex: '#6d4c41' }, + { name: 'Beige', hex: '#d8c39a' }, + { name: 'Gold', hex: '#cba135' }, + { name: 'Maroon', hex: '#7b1e1e' }, + { name: 'Bronze', hex: '#8c6239' }, + { name: 'Purple', hex: '#6a1b9a' } +]; + +const HEX_COLOR_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/; + +function hexToRgb(hex: string): { r: number; g: number; b: number } { + const normalized = hex.length === 4 ? `#${[...hex.slice(1)].map((c) => c + c).join('')}` : hex; + const num = parseInt(normalized.slice(1), 16); + return { r: (num >> 16) & 255, g: (num >> 8) & 255, b: num & 255 }; +} + +function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } { + const rn = r / 255; + const gn = g / 255; + const bn = b / 255; + const max = Math.max(rn, gn, bn); + const min = Math.min(rn, gn, bn); + const l = (max + min) / 2; + const d = max - min; + if (d === 0) return { h: 0, s: 0, l }; + const s = d / (1 - Math.abs(2 * l - 1)); + let h: number; + if (max === rn) h = ((gn - bn) / d) % 6; + else if (max === gn) h = (bn - rn) / d + 2; + else h = (rn - gn) / d + 4; + h *= 60; + if (h < 0) h += 360; + return { h, s, l }; +} + +function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } { + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = l - c / 2; + let r: number; + let g: number; + let b: number; + if (h < 60) [r, g, b] = [c, x, 0]; + else if (h < 120) [r, g, b] = [x, c, 0]; + else if (h < 180) [r, g, b] = [0, c, x]; + else if (h < 240) [r, g, b] = [0, x, c]; + else if (h < 300) [r, g, b] = [x, 0, c]; + else [r, g, b] = [c, 0, x]; + return { + r: Math.round((r + m) * 255), + g: Math.round((g + m) * 255), + b: Math.round((b + m) * 255) + }; +} + +function toHexByte(value: number): string { + return Math.max(0, Math.min(255, value)).toString(16).padStart(2, '0'); +} + +/** + * Renders a vehicle's stored paint color as its muted "vintage" accent — same hue, cut + * saturation and pulled toward a mid lightness — so any custom hex a user enters still + * matches the app's desaturated palette wherever it's used as an accent. + */ +export function vintageVehicleColor(hex: string | null | undefined): string | null { + if (!hex || !HEX_COLOR_RE.test(hex)) return hex ?? null; + const { r, g, b } = hexToRgb(hex); + const { h, s, l } = rgbToHsl(r, g, b); + const mutedS = s * 0.45; + const mutedL = l + (0.5 - l) * 0.3; + const muted = hslToRgb(h, mutedS, mutedL); + return `#${toHexByte(muted.r)}${toHexByte(muted.g)}${toHexByte(muted.b)}`; +} + +/** + * Vintage-mutes a vehicle color for use as a chart line, but bails to `null` for + * white/black/gray/silver — those are near-achromatic, and a single static hex can't + * stay visible against both a light and a dark chart background. Callers should fall + * back to a theme-aware palette color (e.g. `var(--chart-1)`) when this returns null. + */ +export function vehicleTrendColor(hex: string | null | undefined): string | null { + if (!hex || !HEX_COLOR_RE.test(hex)) return null; + const { r, g, b } = hexToRgb(hex); + const { s } = rgbToHsl(r, g, b); + if (s < 0.1) return null; + return vintageVehicleColor(hex); +} + +/** + * A vehicle's vintage-muted color as a badge background, plus a black/white icon class + * picked for contrast against that specific background. Returns null when no color is + * set, so callers can fall back to a neutral theme token instead. + */ +export function vehicleAccent(hex: string | null | undefined): { + background: string; + iconClass: string; +} | null { + const muted = vintageVehicleColor(hex); + if (!muted || !HEX_COLOR_RE.test(muted)) return null; + const { r, g, b } = hexToRgb(muted); + const { l } = rgbToHsl(r, g, b); + return { background: muted, iconClass: l > 0.6 ? 'text-black/70' : 'text-white' }; +} export interface Vehicle { id: string | null; @@ -9,13 +136,46 @@ export interface Vehicle { vin: string | null; color: string | null; odometer: number | null; - insuranceStatus?: string; - puccStatus?: string; - image: string | null; + image?: string | null; fuelType: 'petrol' | 'diesel' | 'electric' | 'lpg' | 'cng'; + vehicleType: + | 'car' + | 'motorcycle' + | 'scooter' + | 'truck' + | 'van' + | 'bus' + | 'farm_vehicle' + | 'yacht' + | 'rv' + | 'other'; customFields?: Record | null; } +export interface VehicleActivityEntry { + id: string; + kind: 'fuel' | 'maintenance' | 'compliance'; + date: string; + cost?: number | null; + fuelAmount?: number | null; + serviceCenter?: string | null; + documentNumber?: string | null; +} + +/** Shape returned by the vehicle hub page's server load (getVehicleSummary) — vehicle plus derived stats. */ +export interface VehicleHubSummary extends Vehicle { + currentOdometer?: number | null; + overallMileage?: number | null; + totalFuelLogs?: number; + totalMaintenanceLogs?: number; + insuranceValidTill?: string | null; + insuranceValidityStatus?: 'valid' | 'expired' | 'not_available'; + otherComplianceValidTill?: string | null; + otherComplianceValidityStatus?: 'valid' | 'expired' | 'not_available'; + upcomingRemindersCount?: number; + recentActivity?: VehicleActivityEntry[]; +} + export const FUEL_TYPES = { petrol: 'petrol', diesel: 'diesel', @@ -42,6 +202,66 @@ export function getFuelTypeLabel(fuelType: string, m: any): string { } } +export const VEHICLE_TYPES = { + car: 'car', + motorcycle: 'motorcycle', + scooter: 'scooter', + truck: 'truck', + van: 'van', + bus: 'bus', + farm_vehicle: 'farm_vehicle', + yacht: 'yacht', + rv: 'rv', + other: 'other' +} as const; + +const VEHICLE_TYPE_ICONS: Record> = { + car: Car, + motorcycle: Bike, + scooter: Scooter, + truck: Truck, + van: Van, + bus: BusFront, + farm_vehicle: Tractor, + yacht: Sailboat, + rv: Caravan, + other: Shapes +}; + +export function getVehicleTypeIcon( + vehicleType: string +): Component<{ class?: string; style?: any }> { + return VEHICLE_TYPE_ICONS[vehicleType] ?? Car; +} + +// Helper function to get localized vehicle type label +export function getVehicleTypeLabel(vehicleType: string, m: any): string { + switch (vehicleType) { + case 'car': + return m.vehicle_type_car(); + case 'motorcycle': + return m.vehicle_type_motorcycle(); + case 'scooter': + return m.vehicle_type_scooter(); + case 'truck': + return m.vehicle_type_truck(); + case 'van': + return m.vehicle_type_van(); + case 'bus': + return m.vehicle_type_bus(); + case 'farm_vehicle': + return m.vehicle_type_farm_vehicle(); + case 'yacht': + return m.vehicle_type_yacht(); + case 'rv': + return m.vehicle_type_rv(); + case 'other': + return m.vehicle_type_other(); + default: + return m.vehicle_type_car(); + } +} + export const vehicleSchema = z.object({ id: z.string().nullable(), make: z @@ -72,11 +292,25 @@ export const vehicleSchema = z.object({ .nullable(), color: z .string() - .regex(/^(#[0-9a-fA-F]{3})|(#[0-9a-fA-F]{6})$/, 'Only hex color codes allowed.') + .regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, 'Only hex color codes allowed.') .nullable(), odometer: z.number().nonnegative().nullable(), - image: z.string().nullable(), + image: z.string().nullable().optional(), fuelType: z.enum(['petrol', 'diesel', 'electric', 'lpg', 'cng']).default('petrol'), + vehicleType: z + .enum([ + 'car', + 'motorcycle', + 'scooter', + 'truck', + 'van', + 'bus', + 'farm_vehicle', + 'yacht', + 'rv', + 'other' + ]) + .default('car'), customFields: z.record(z.string(), z.string()).nullable().optional() }); diff --git a/src/lib/helper/accent-color.helper.ts b/src/lib/helper/accent-color.helper.ts new file mode 100644 index 00000000..4a2f2bfe --- /dev/null +++ b/src/lib/helper/accent-color.helper.ts @@ -0,0 +1,107 @@ +/** + * Shared vintage/muted accent palette for decorative icon chips, dots, and badges + * (StatCard icons, notification types, activity dots, etc). Deliberately desaturated — + * this is the "no bright colors" counterpart to the semantic --success/--warning/--info/ + * --destructive tokens in app.css, which cover status meaning rather than feature branding. + * + * Every value below is a complete, literal Tailwind class string (never built by + * interpolating a hex variable) so Tailwind's static scanner can find and generate it. + */ +export const ACCENT = { + moss: { + gradient: 'bg-gradient-to-br from-[#96a67c] to-[#56643f] shadow-[#74845c]/30', + chip: 'bg-[#74845c]/10 text-[#74845c]', + solid: 'bg-[#74845c]', + soft: 'bg-[#74845c]/10', + text: 'text-[#74845c]', + ring: 'border-[#74845c]/40 bg-[#74845c]/10 text-[#74845c]', + pill: 'bg-[#74845c]/15 text-[#56643f]', + hoverText: 'text-[#74845c] hover:text-[#56643f]', + hoverBg: 'hover:bg-[#74845c]/15' + }, + ochre: { + gradient: 'bg-gradient-to-br from-[#c9a35b] to-[#8a6a2c] shadow-[#b08a3c]/30', + chip: 'bg-[#b08a3c]/10 text-[#b08a3c]', + solid: 'bg-[#b08a3c]', + soft: 'bg-[#b08a3c]/10', + text: 'text-[#b08a3c]', + ring: 'border-[#b08a3c]/40 bg-[#b08a3c]/10 text-[#b08a3c]', + pill: 'bg-[#b08a3c]/15 text-[#8a6a2c]', + hoverText: 'text-[#b08a3c] hover:text-[#8a6a2c]', + hoverBg: 'hover:bg-[#b08a3c]/15', + medal: + 'bg-gradient-to-br from-[#c9a35b] to-[#b08a3c] text-[#8a6a2c] shadow-sm shadow-[#b08a3c]/30' + }, + denim: { + gradient: 'bg-gradient-to-br from-[#7f96a6] to-[#435868] shadow-[#5c7487]/30', + chip: 'bg-[#5c7487]/10 text-[#5c7487]', + solid: 'bg-[#5c7487]', + soft: 'bg-[#5c7487]/10', + text: 'text-[#5c7487]', + ring: 'border-[#5c7487]/40 bg-[#5c7487]/10 text-[#5c7487]', + pill: 'bg-[#5c7487]/15 text-[#435868]', + hoverText: 'text-[#5c7487] hover:text-[#435868]', + hoverBg: 'hover:bg-[#5c7487]/15' + }, + teal: { + gradient: 'bg-gradient-to-br from-[#82a29e] to-[#486863] shadow-[#5f8783]/30', + chip: 'bg-[#5f8783]/10 text-[#5f8783]', + solid: 'bg-[#5f8783]', + soft: 'bg-[#5f8783]/10', + text: 'text-[#5f8783]', + ring: 'border-[#5f8783]/40 bg-[#5f8783]/10 text-[#5f8783]', + pill: 'bg-[#5f8783]/15 text-[#486863]', + hoverText: 'text-[#5f8783] hover:text-[#486863]', + hoverBg: 'hover:bg-[#5f8783]/15' + }, + plum: { + gradient: 'bg-gradient-to-br from-[#9c85a0] to-[#5f4c63] shadow-[#816783]/30', + chip: 'bg-[#816783]/10 text-[#816783]', + solid: 'bg-[#816783]', + soft: 'bg-[#816783]/10', + text: 'text-[#816783]', + ring: 'border-[#816783]/40 bg-[#816783]/10 text-[#816783]', + pill: 'bg-[#816783]/15 text-[#5f4c63]', + hoverText: 'text-[#816783] hover:text-[#5f4c63]', + hoverBg: 'hover:bg-[#816783]/15' + }, + brick: { + gradient: 'bg-gradient-to-br from-[#b47c6e] to-[#784339] shadow-[#9c5a4c]/30', + chip: 'bg-[#9c5a4c]/10 text-[#9c5a4c]', + solid: 'bg-[#9c5a4c]', + soft: 'bg-[#9c5a4c]/10', + text: 'text-[#9c5a4c]', + ring: 'border-[#9c5a4c]/40 bg-[#9c5a4c]/10 text-[#9c5a4c]', + pill: 'bg-[#9c5a4c]/15 text-[#784339]', + hoverText: 'text-[#9c5a4c] hover:text-[#784339]', + hoverBg: 'hover:bg-[#9c5a4c]/15' + }, + fog: { + gradient: 'bg-gradient-to-br from-[#a19e97] to-[#625f5b] shadow-[#83807a]/30', + chip: 'bg-[#83807a]/10 text-[#83807a]', + solid: 'bg-[#83807a]', + soft: 'bg-[#83807a]/10', + text: 'text-[#83807a]', + ring: 'border-[#83807a]/40 bg-[#83807a]/10 text-[#83807a]', + pill: 'bg-[#83807a]/15 text-[#625f5b]', + hoverText: 'text-[#83807a] hover:text-[#625f5b]', + hoverBg: 'hover:bg-[#83807a]/15', + medal: + 'bg-gradient-to-br from-[#a19e97] to-[#83807a] text-[#3f3d3a] shadow-sm shadow-[#83807a]/30' + }, + clay: { + gradient: 'bg-gradient-to-br from-[#cf9b78] to-[#8f5b3d] shadow-[#b97a55]/30', + chip: 'bg-[#b97a55]/10 text-[#b97a55]', + solid: 'bg-[#b97a55]', + soft: 'bg-[#b97a55]/10', + text: 'text-[#b97a55]', + ring: 'border-[#b97a55]/40 bg-[#b97a55]/10 text-[#b97a55]', + pill: 'bg-[#b97a55]/15 text-[#8f5b3d]', + hoverText: 'text-[#b97a55] hover:text-[#8f5b3d]', + hoverBg: 'hover:bg-[#b97a55]/15', + medal: + 'bg-gradient-to-br from-[#cf9b78] to-[#b97a55] text-[#5c3a26] shadow-sm shadow-[#b97a55]/30' + } +} as const; + +export type AccentName = keyof typeof ACCENT; diff --git a/src/lib/helper/alert.helper.ts b/src/lib/helper/alert.helper.ts deleted file mode 100644 index 1a7c68c7..00000000 --- a/src/lib/helper/alert.helper.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { Insurance, PollutionCertificate } from '$lib/domain'; -import { differenceInDays } from 'date-fns'; -import * as m from '$lib/paraglide/messages'; - -export type VehicleAlertType = 'insurance' | 'pucc'; -export type VehicleAlertStatus = 'expired' | 'expiring' | 'valid' | 'missing'; - -export interface VehicleAlert { - type: VehicleAlertType; - status: VehicleAlertStatus; - title: string; - message: string; - daysRemaining: number; - expiryDate: Date | null; - hasRecord: boolean; -} - -const ALERT_THRESHOLD_DAYS = 30; - -const classifyStatus = (daysRemaining: number): VehicleAlertStatus => { - if (daysRemaining < 0) return 'expired'; - if (daysRemaining <= ALERT_THRESHOLD_DAYS) return 'expiring'; - return 'valid'; -}; - -const formatMessage = (type: VehicleAlertType, status: VehicleAlertStatus, days: number) => { - const label = type === 'insurance' ? m.alert_type_insurance() : m.alert_type_pucc(); - if (status === 'expired') { - return m.alert_status_expired_ago({ label, days: Math.abs(days) }); - } - if (status === 'expiring') { - return m.alert_status_expires_in({ label, days }); - } - return m.alert_status_valid_for({ label, days }); -}; - -const buildAlert = (type: VehicleAlertType, expiryDate: Date): VehicleAlert => { - const today = new Date(); - const daysRemaining = differenceInDays(expiryDate, today); - const status = classifyStatus(daysRemaining); - - return { - type, - status, - title: type === 'insurance' ? m.alert_type_insurance() : m.alert_type_pucc(), - message: formatMessage(type, status, daysRemaining), - daysRemaining, - expiryDate, - hasRecord: true - }; -}; - -const buildMissingAlert = (type: VehicleAlertType): VehicleAlert => { - const label = type === 'insurance' ? 'Insurance' : 'PUCC'; - return { - type, - status: 'missing', - title: type === 'insurance' ? m.alert_type_insurance() : m.alert_type_pucc(), - message: m.alert_record_not_found({ label }), - daysRemaining: Number.POSITIVE_INFINITY, - expiryDate: null, - hasRecord: false - }; -}; - -export const calculateInsuranceAlert = (insurances?: Insurance[] | null): VehicleAlert | null => { - if (!insurances || insurances.length === 0) return buildMissingAlert('insurance'); - - const perpetual = insurances.find( - (insurance) => !insurance.endDate || insurance.recurrenceType === 'no_end' - ); - if (perpetual) { - return { - type: 'insurance', - status: 'valid', - title: m.alert_type_insurance(), - message: m.alert_insurance_active_no_end(), - daysRemaining: Number.POSITIVE_INFINITY, - expiryDate: null, - hasRecord: true - }; - } - - const latest = insurances.reduce((latest, current) => { - return new Date(current.endDate!) > new Date(latest.endDate!) ? current : latest; - }); - return buildAlert('insurance', new Date(latest.endDate!)); -}; - -export const calculatePuccAlert = ( - certificates?: PollutionCertificate[] | null -): VehicleAlert | null => { - if (!certificates || certificates.length === 0) return buildMissingAlert('pucc'); - - const perpetual = certificates.find( - (certificate) => !certificate.expiryDate || certificate.recurrenceType === 'no_end' - ); - if (perpetual) { - return { - type: 'pucc', - status: 'valid', - title: m.alert_type_pucc(), - message: m.alert_pucc_active_no_end(), - daysRemaining: Number.POSITIVE_INFINITY, - expiryDate: null, - hasRecord: true - }; - } - - const latest = certificates.reduce((latest, current) => { - return new Date(current.expiryDate!) > new Date(latest.expiryDate!) ? current : latest; - }); - return buildAlert('pucc', new Date(latest.expiryDate!)); -}; - -export const calculateVehicleAlerts = ( - insurances?: Insurance[] | null, - certificates?: PollutionCertificate[] | null -): VehicleAlert[] => { - return [calculateInsuranceAlert(insurances), calculatePuccAlert(certificates)].filter( - (alert): alert is VehicleAlert => Boolean(alert) - ); -}; diff --git a/src/lib/helper/config.helper.ts b/src/lib/helper/config.helper.ts index ca1b802d..d3880831 100644 --- a/src/lib/helper/config.helper.ts +++ b/src/lib/helper/config.helper.ts @@ -31,7 +31,7 @@ export function rawConfigToFormData( /** * Convert form data back into a Config[] array suitable for the save API. * Merges with existing rawConfig to preserve descriptions and any extra keys. - * The `theme` key is excluded since it is stored client-side only. + * The `theme` and `darkVariant` keys are excluded since they are stored client-side only. */ export function formDataToConfigs( formData: Record, @@ -42,7 +42,7 @@ export function formDataToConfigs( configMap.set(item.key, item); } for (const [key, value] of Object.entries(formData)) { - if (key === 'theme') continue; + if (key === 'theme' || key === 'darkVariant') continue; const stringValue = typeof value === 'boolean' ? String(value) : ((value || '') as string); const existing = configMap.get(key); if (existing) { diff --git a/src/lib/helper/csv-export.helper.ts b/src/lib/helper/csv-export.helper.ts new file mode 100644 index 00000000..21650ce9 --- /dev/null +++ b/src/lib/helper/csv-export.helper.ts @@ -0,0 +1,16 @@ +function csvEscape(value: string): string { + return `"${value.replace(/"/g, '""')}"`; +} + +export function downloadCsv(filename: string, header: string[], rows: string[][]): void { + const csv = [header, ...rows].map((row) => row.map(csvEscape).join(',')).join('\r\n'); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} diff --git a/src/lib/helper/csv.helper.ts b/src/lib/helper/csv.helper.ts index debc1588..7bc10535 100644 --- a/src/lib/helper/csv.helper.ts +++ b/src/lib/helper/csv.helper.ts @@ -1,12 +1,12 @@ // Stub helpers for CSV import flows. Implement the real parsing and persistence logic here. -export type CsvParseOptions = { +type CsvParseOptions = { delimiter: string; hasHeaders: boolean; }; export type ParsedCsvRow = Record; -export type ParsedCsv = { +type ParsedCsv = { headers: string[]; rows: ParsedCsvRow[]; }; @@ -64,8 +64,6 @@ export const importFuelLogsFromCsv = async ( vehicleId: string, dateFormat: string ): Promise<{ imported: number; failed: number; errors: string[] }> => { - // Simulate delay for demo purposes - await new Promise((resolve) => setTimeout(resolve, 200000)); const { parseWithFormat } = await import('$lib/helper/format.helper'); const { saveFuelLog } = await import('$lib/services/fuel.service'); const { parseDate } = await import('$lib/helper/format.helper'); @@ -131,6 +129,7 @@ export const importFuelLogsFromCsv = async ( date: parsedDate, odometer, fuelAmount, + rate: null, cost, filled, missedLast, diff --git a/src/lib/helper/date.helper.ts b/src/lib/helper/date.helper.ts new file mode 100644 index 00000000..2e40a465 --- /dev/null +++ b/src/lib/helper/date.helper.ts @@ -0,0 +1,75 @@ +import type { DateValue } from '@internationalized/date'; +import { format, parse } from 'date-fns'; +import { formatInTimeZone, fromZonedTime } from 'date-fns-tz'; +import { es, fr, de, hi } from 'date-fns/locale'; + +export interface FormatConfig { + dateFormat: string; + timezone: string; + locale: string; + currency: string; + unitOfVolume: string; + unitOfLpg: string; + unitOfCng: string; + unitOfDistance: string; + mileageUnitFormat: string; +} + +const getDateFnsLocale = (locale: string) => { + switch (locale) { + case 'es': + return es; + case 'fr': + return fr; + case 'de': + return de; + case 'hi': + return hi; + case 'en': + default: + return undefined; + } +}; + +export const formatDate = (date: Date | string, config: FormatConfig): string => { + const dateObj = typeof date === 'string' ? new Date(date) : date; + try { + const zonedDate = formatInTimeZone(dateObj, config.timezone, 'yyyy-MM-dd HH:mm:ss'); + const parsedDate = parse(zonedDate, 'yyyy-MM-dd HH:mm:ss', new Date()); + return format(parsedDate, config.dateFormat, { + locale: getDateFnsLocale(config.locale) + }); + } catch (e) { + return ''; + } +}; + +export const formatDateForCalendar = (date: DateValue, config: FormatConfig): string => { + const dateObj = date.toDate(config.timezone); + return format(dateObj, config.dateFormat, { locale: getDateFnsLocale(config.locale) }); +}; + +export const parseDate = (date: string, config: FormatConfig) => { + const parsedDate = parse(date, config.dateFormat, new Date()); + return fromZonedTime(parsedDate, config.timezone); +}; + +export const isValidFormat = (fmt: string): { ex?: string; valid: boolean } => { + try { + return { + ex: format(new Date(), fmt), + valid: true + }; + } catch (_) { + return { valid: false }; + } +}; + +export const parseWithFormat = (dateStr: string, fmt: string): Date | null => { + try { + const parsed = parse(dateStr, fmt, new Date()); + return isNaN(parsed.getTime()) ? null : parsed; + } catch (_) { + return null; + } +}; diff --git a/src/lib/helper/dev.helper.ts b/src/lib/helper/dev.helper.ts deleted file mode 100644 index 2b087e9a..00000000 --- a/src/lib/helper/dev.helper.ts +++ /dev/null @@ -1,5 +0,0 @@ -const simulateNetworkDelay = (ms: number) => { - return new Promise((resolve) => setTimeout(resolve, ms)); -}; - -export { simulateNetworkDelay }; diff --git a/src/lib/helper/feature.helper.ts b/src/lib/helper/feature.helper.ts index ae8dd8df..59bc778f 100644 --- a/src/lib/helper/feature.helper.ts +++ b/src/lib/helper/feature.helper.ts @@ -17,29 +17,12 @@ export function isFeatureEnabled(feature: string): boolean { export const Features = { FUEL_LOG: 'fuelLog', MAINTENANCE: 'maintenance', - PUCC: 'pucc', + COMPLIANCE: 'compliance', REMINDERS: 'reminders', - INSURANCE: 'insurance', OVERVIEW: 'overview' } as const; /** - * Get all enabled features - * @returns Array of enabled feature names - */ -export function getEnabledFeatures(): string[] { - const features = []; - if (configStore.configs.featureFuelLog) features.push(Features.FUEL_LOG); - if (configStore.configs.featureMaintenance) features.push(Features.MAINTENANCE); - if (configStore.configs.featurePucc) features.push(Features.PUCC); - if (configStore.configs.featureReminders) features.push(Features.REMINDERS); - if (configStore.configs.featureInsurance) features.push(Features.INSURANCE); - if (configStore.configs.featureOverview) features.push(Features.OVERVIEW); - return features; -} - -/** - * Check if all specified features are enabled * @param features - Array of feature names to check * @returns boolean - true if all features are enabled */ diff --git a/src/lib/helper/format.helper.ts b/src/lib/helper/format.helper.ts index 33467f9f..2b7d653e 100644 --- a/src/lib/helper/format.helper.ts +++ b/src/lib/helper/format.helper.ts @@ -1,292 +1,37 @@ import configs from '$stores/config.svelte'; -import type { DateValue } from '@internationalized/date'; -import { format, parse } from 'date-fns'; -import { formatInTimeZone, fromZonedTime } from 'date-fns-tz'; -import { es, fr, de, hi } from 'date-fns/locale'; -import { CANONICAL_TIMEZONES } from '$lib/constants/timezones'; - -const getDateFnsLocale = () => { - switch (configs.locale) { - case 'es': - return es; - case 'fr': - return fr; - case 'de': - return de; - case 'hi': - return hi; - case 'en': - default: - return undefined; // English is the default locale in date-fns - } -}; - -const formatDate = (date: Date | string): string => { - const dateObj = typeof date === 'string' ? new Date(date) : date; - try { - const zonedDate = formatInTimeZone(dateObj, configs.timezone, 'yyyy-MM-dd HH:mm:ss'); - const parsedDate = parse(zonedDate, 'yyyy-MM-dd HH:mm:ss', new Date()); - return format(parsedDate, configs.dateFormat, { - locale: getDateFnsLocale() - }); - } catch (e) { - return ''; - } -}; - -const formatDateForCalendar = (date: DateValue): string => { - const dateObj = date.toDate(configs.timezone); - return format(dateObj, configs.dateFormat, { locale: getDateFnsLocale() }); -}; - -const parseDate = (date: string) => { - const parsedDate = parse(date, configs.dateFormat, new Date()); - return fromZonedTime(parsedDate, configs.timezone); -}; - -const isValidFormat = (fmt: string): { ex?: string; valid: boolean } => { - try { - return { - ex: format(new Date(), fmt), - valid: true - }; - } catch (_) { - return { valid: false }; - } -}; - -const parseWithFormat = (dateStr: string, fmt: string): Date | null => { - try { - const parsed = parse(dateStr, fmt, new Date()); - return isNaN(parsed.getTime()) ? null : parsed; - } catch (_) { - return null; - } -}; - -const getTimezoneOptions = (): { - value: string; - label: string; - offset: number; -}[] => { - // Use a fixed reference date (Jan 1, 2024 UTC) to ensure consistent timezone offsets - // across all platforms and avoid DST variations - const referenceDate = new Date('2024-01-01T12:00:00Z'); - - // Use canonical timezone list instead of Intl.supportedValuesOf to ensure - // consistency across all operating systems and Node.js versions - return CANONICAL_TIMEZONES.map((zone) => { - try { - const offset = formatInTimeZone(referenceDate, zone, 'xxx'); - return { - value: zone, - label: `[${offset}] ${zone}`, - offset: Number(offset.replace(':', '')) - }; - } catch (e) { - // Fallback for any timezone that might not be supported - return { - value: zone, - label: zone, - offset: 0 - }; - } - }).sort((a, b) => a.offset - b.offset); -}; - -const isValidTimezone = (tz: string) => { - // Check against canonical timezone list for consistency across all platforms - return CANONICAL_TIMEZONES.includes(tz as any); -}; - -const getCurrencySymbol = (currency?: string): string => { - try { - return ( - new Intl.NumberFormat('en-US', { - style: 'currency', - currency: currency || configs.currency - }) - .formatToParts(0) - .find((part) => part.type === 'currency')?.value || '' - ); - } catch (e) { - // console.debug('Unable to find currency Symbol : ', currency); - return ''; - } -}; - -const formatCurrency = (amount: number): string => { - return new Intl.NumberFormat(configs.locale, { - style: 'currency', - currency: configs.currency - }).format(amount); -}; - -const UNIT_LABEL_FALLBACKS: Record = { - liter: 'L', - gallon: 'gal', - kilogram: 'kg', - pound: 'lb', - kilometer: 'km', - mile: 'mi' -}; - -const safeUnitLabel = (unit: string): string => { - try { - return ( - new Intl.NumberFormat(configs.locale, { - style: 'unit', - unit - }) - .formatToParts(0) - .find((part) => part.type === 'unit')?.value || - UNIT_LABEL_FALLBACKS[unit] || - unit - ); - } catch (_) { - return UNIT_LABEL_FALLBACKS[unit] || unit; - } -}; - -const safeUnitFormat = (value: number, unit: string): string | null => { - try { - return new Intl.NumberFormat(configs.locale, { - style: 'unit', - unit - }).format(value); - } catch (_) { - return null; - } -}; - -const getFuelVolumeUnit = (fuelType: string): string => { - switch (fuelType) { - case 'lpg': - return configs.unitOfLpg || configs.unitOfVolume; - case 'cng': - return configs.unitOfCng || configs.unitOfVolume; - default: - return configs.unitOfVolume; - } -}; - -const getDistanceUnit = (): string => { - return safeUnitLabel(configs.unitOfDistance); -}; - -const formatDistance = (distance: number): string => { - return ( - safeUnitFormat(distance, configs.unitOfDistance) || - `${distance} ${safeUnitLabel(configs.unitOfDistance)}` - ); -}; - -const getFuelUnit = (vehicleType: string): string => { - if (vehicleType === 'electric') { - return 'kWh'; - } - return safeUnitLabel(getFuelVolumeUnit(vehicleType)); -}; - -const formatFuel = (amount: number, vehicleType: string): string => { - if (vehicleType === 'electric') { - return `${amount.toFixed(3)} kWh`; - } - - const fuelUnit = getFuelVolumeUnit(vehicleType); - return safeUnitFormat(amount, fuelUnit) || `${amount.toFixed(2)} ${safeUnitLabel(fuelUnit)}`; -}; - -const getMileageUnit = (vehicleType: string): string => { - if (vehicleType === 'electric') { - return 'km/kWh'; - } - const fuelUnit = getFuelVolumeUnit(vehicleType); - const distanceUnit = safeUnitLabel(configs.unitOfDistance); - const fuelLabel = safeUnitLabel(fuelUnit); - - // Support both distance/fuel and fuel/distance formats - if (configs.mileageUnitFormat === 'fuel-per-distance') { - return `${fuelLabel}/100${distanceUnit}`; - } - - // only show uk mpg if miles and liters are used - if ( - configs.mileageUnitFormat === 'uk-mpg' && - configs.unitOfDistance === 'mile' && - fuelUnit === 'liter' - ) { - return 'mpg'; - } - - // Default: distance-per-fuel (e.g., km/L, mpg) - const mileageUnit = `${configs.unitOfDistance}-per-${fuelUnit}`; - const label = safeUnitLabel(mileageUnit); - return label === mileageUnit ? `${distanceUnit}/${fuelLabel}` : label; -}; - -const formatMileage = (mileage: number, vehicleType: string): string => { - if (vehicleType === 'electric') { - return `${mileage.toFixed(3)} km/kWh`; - } - const fuelUnit = getFuelVolumeUnit(vehicleType); - const distanceUnit = safeUnitLabel(configs.unitOfDistance); - const fuelLabel = safeUnitLabel(fuelUnit); - - // Support both distance/fuel and fuel/distance formats - if (configs.mileageUnitFormat === 'fuel-per-distance') { - // For fuel/distance format (e.g., L/100km), display as fuel per 100 distance units - return `${mileage.toFixed(2)} ${fuelLabel}/100${distanceUnit}`; - } - - // only show uk mpg if miles and liters are used - if ( - configs.mileageUnitFormat === 'uk-mpg' && - configs.unitOfDistance === 'mile' && - fuelUnit === 'liter' - ) { - return `${mileage.toFixed(2)} mpg`; - } - - // Default: distance-per-fuel (e.g., km/L, mpg) - const mileageUnit = `${configs.unitOfDistance}-per-${fuelUnit}`; - return ( - safeUnitFormat(mileage, mileageUnit) || `${mileage.toFixed(2)} ${distanceUnit}/${fuelLabel}` - ); -}; - -const roundNumber = (num: number, decimal: number = 2): number => { - return Number(num.toFixed(2)); -}; - -export { - formatDate, - formatDateForCalendar, - parseDate, - isValidFormat, - getTimezoneOptions, - isValidTimezone, - getCurrencySymbol, - formatCurrency, - getDistanceUnit, - formatDistance, - getFuelUnit, - formatFuel, - getMileageUnit, - formatMileage, - roundNumber, - parseWithFormat -}; - -export const cleanup = (obj: Record): Record => { - const result: Record = { ...obj }; - for (const key in result) { - if ( - result.hasOwnProperty(key) && - (String(result[key]).trim() === '' || result[key] === undefined) - ) { - result[key] = null; - } - } - return result; -}; +import type { FormatConfig } from './date.helper'; +import { + formatDate as formatDatePure, + formatDateForCalendar as formatDateForCalendarPure, + parseDate as parseDatePure +} from './date.helper'; +export type { FormatConfig }; +export { isValidFormat, parseWithFormat } from './date.helper'; +export { getTimezoneOptions, isValidTimezone } from './timezone.helper'; +import { + getCurrencySymbol as getCurrencySymbolPure, + formatCurrency as formatCurrencyPure, + formatDistance as formatDistancePure, + getDistanceUnit as getDistanceUnitPure, + getFuelUnit as getFuelUnitPure, + formatFuel as formatFuelPure, + getMileageUnit as getMileageUnitPure, + formatMileage as formatMileagePure +} from './unit.helper'; + +export const formatDate = (date: Date | string) => formatDatePure(date, configs); +export const formatDateForCalendar = (date: import('@internationalized/date').DateValue) => + formatDateForCalendarPure(date, configs); +export const parseDate = (date: string) => parseDatePure(date, configs); +export const getCurrencySymbol = (currency?: string) => + getCurrencySymbolPure(currency || configs.currency); +export const formatCurrency = (amount: number) => formatCurrencyPure(amount, configs); +export const getDistanceUnit = () => getDistanceUnitPure(configs); +export const formatDistance = (distance: number) => formatDistancePure(distance, configs); +export const getFuelUnit = (vehicleType: string) => getFuelUnitPure(vehicleType, configs); +export const formatFuel = (amount: number, vehicleType: string) => + formatFuelPure(amount, vehicleType, configs); +export const getMileageUnit = (vehicleType: string) => getMileageUnitPure(vehicleType, configs); +export const formatMileage = (mileage: number, vehicleType: string) => + formatMileagePure(mileage, vehicleType, configs); +export { roundNumber } from './unit.helper'; diff --git a/src/lib/helper/http.helper.ts b/src/lib/helper/http.helper.ts index f7d1bf21..39ef835c 100644 --- a/src/lib/helper/http.helper.ts +++ b/src/lib/helper/http.helper.ts @@ -1,254 +1,96 @@ +/* global BodyInit */ + interface RequestConfig { method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; headers?: Record; - params?: Record; data?: any; timeout?: number; - baseURL?: string; - skipInterceptors?: boolean; + responseType?: 'json' | 'blob'; } interface Response { data: T; status: number; - statusText: string; - headers: Headers; - config: RequestConfig; } class HttpError extends Error { - response?: Response; - request?: RequestConfig; - status?: number; - - constructor(message: string, config?: RequestConfig, response?: Response) { + constructor( + message: string, + readonly status?: number, + readonly response?: Response + ) { super(message); this.name = 'HttpError'; - this.request = config; - this.response = response; - this.status = response?.status; } } class HttpClient { - private baseURL: string = ''; - private defaultHeaders: Record = { - 'Content-Type': 'application/json' - }; - private timeout: number = 10000; - private requestInterceptors: ((req: RequestConfig) => boolean)[] = []; - - constructor(config?: { baseURL?: string; headers?: Record; timeout?: number }) { - if (config?.baseURL) this.baseURL = config.baseURL; - if (config?.headers) this.defaultHeaders = { ...this.defaultHeaders, ...config.headers }; - if (config?.timeout) this.timeout = config.timeout; - } - - private buildURL(url: string, params?: Record): string { - const fullURL = url.startsWith('http') ? url : `${this.baseURL}${url}`; - - if (!params) return fullURL; - - const searchParams = new URLSearchParams(); - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - searchParams.append(key, String(value)); - } - }); - - const paramString = searchParams.toString(); - return paramString ? `${fullURL}?${paramString}` : fullURL; - } - - private async makeRequest( - url: string, - config: RequestConfig = {} - ): Promise> { - if (!config.skipInterceptors) { - this.requestInterceptors.forEach((intercept) => { - const isSuccessful = intercept(config); - if (!isSuccessful) { - console.warn('Request cancelled by interceptor:', config); - throw new HttpError('Request cancelled by interceptor', config); - } - }); - } - - const { - method = 'GET', - headers = {}, - params, - data, - timeout = this.timeout, - baseURL = this.baseURL - } = config; - - const fullURL = this.buildURL(url, params); - const mergedHeaders = { ...this.defaultHeaders, ...headers }; - - /* global RequestInit */ - const fetchOptions: RequestInit = { - method, - headers: mergedHeaders + constructor( + private config: { baseURL?: string; headers?: Record; timeout?: number } = {} + ) {} + + private async request(url: string, config: RequestConfig): Promise> { + const isFormData = config.data instanceof FormData; + const headers: Record = { + // FormData sets its own Content-Type so it carries the multipart boundary. + ...(isFormData ? {} : { 'Content-Type': 'application/json' }), + ...this.config.headers, + ...config.headers }; + let body: BodyInit | undefined; - if (data && method !== 'GET') { - if (data instanceof FormData) { - fetchOptions.body = data; - // Remove Content-Type to let browser set it with boundary - delete mergedHeaders['Content-Type']; - } else if (typeof data === 'object') { - fetchOptions.body = JSON.stringify(data); - } else { - fetchOptions.body = data; - } + if (config.data !== undefined && config.method !== 'GET') { + body = + isFormData || typeof config.data !== 'object' ? config.data : JSON.stringify(config.data); } - // Timeout handling - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); - fetchOptions.signal = controller.signal; - + let response: globalThis.Response; try { - const response = await fetch(fullURL, fetchOptions); - - let responseData: T; - const contentType = response.headers.get('content-type'); - - if (contentType?.includes('application/json')) { - responseData = await response.json(); - } else { - responseData = (await response.text()) as T; - } - - clearTimeout(timeoutId); - - const result: Response = { - data: responseData, - status: response.status, - statusText: response.statusText, - headers: response.headers, - config - }; - - if (!response.ok) { - throw new HttpError(`Request failed with status ${response.status}`, config, result); - } - - return result; + response = await fetch(url.startsWith('http') ? url : `${this.config.baseURL ?? ''}${url}`, { + method: config.method, + headers, + body, + signal: AbortSignal.timeout(config.timeout ?? this.config.timeout ?? 10000) + }); } catch (error: any) { - clearTimeout(timeoutId); - - if (error instanceof HttpError) { - throw error; - } - - if (error.name === 'AbortError') { - throw new HttpError('Request timeout', config); - } - - throw new HttpError(error.message || 'Network error', config); + throw new HttpError(error?.name === 'TimeoutError' ? 'Request timeout' : 'Network error'); } - } - - // Main HTTP methods - async get( - url: string, - config?: Omit - ): Promise> { - return this.makeRequest(url, { ...config, method: 'GET' }); - } - - async post( - url: string, - data?: any, - config?: Omit - ): Promise> { - return this.makeRequest(url, { ...config, method: 'POST', data }); - } - - async put( - url: string, - data?: any, - config?: Omit - ): Promise> { - return this.makeRequest(url, { ...config, method: 'PUT', data }); - } - - async patch( - url: string, - data?: any, - config?: Omit - ): Promise> { - return this.makeRequest(url, { ...config, method: 'PATCH', data }); - } - async delete( - url: string, - config?: Omit - ): Promise> { - return this.makeRequest(url, { ...config, method: 'DELETE' }); - } - - // Axios-like static methods - static async get(url: string, config?: RequestConfig): Promise> { - const client = new HttpClient(); - return client.get(url, config); - } - - static async post( - url: string, - data?: any, - config?: RequestConfig - ): Promise> { - const client = new HttpClient(); - return client.post(url, data, config); - } - - static async put(url: string, data?: any, config?: RequestConfig): Promise> { - const client = new HttpClient(); - return client.put(url, data, config); + const contentType = response.headers.get('content-type'); + const data = ( + config.responseType === 'blob' + ? await response.blob() + : contentType?.includes('application/json') + ? await response.json() + : await response.text() + ) as T; + + const result = { data, status: response.status }; + if (!response.ok) { + throw new HttpError(`Request failed with status ${response.status}`, response.status, result); + } + return result; } - static async patch( - url: string, - data?: any, - config?: RequestConfig - ): Promise> { - const client = new HttpClient(); - return client.patch(url, data, config); + get(url: string, config: RequestConfig = {}) { + return this.request(url, { ...config, method: 'GET' }); } - static async delete(url: string, config?: RequestConfig): Promise> { - const client = new HttpClient(); - return client.delete(url, config); + post(url: string, data?: any, config: RequestConfig = {}) { + return this.request(url, { ...config, method: 'POST', data }); } - addRequestInterceptor = (interceptor: (req: RequestConfig) => boolean) => { - this.requestInterceptors.push(interceptor); - }; - - // Interceptors (simplified version) - setDefaultHeader(key: string, value: string): void { - this.defaultHeaders[key] = value; + put(url: string, data?: any, config: RequestConfig = {}) { + return this.request(url, { ...config, method: 'PUT', data }); } - removeDefaultHeader(key: string): void { - delete this.defaultHeaders[key]; + patch(url: string, data?: any, config: RequestConfig = {}) { + return this.request(url, { ...config, method: 'PATCH', data }); } - setBaseURL(baseURL: string): void { - this.baseURL = baseURL; - } - - setTimeout(timeout: number): void { - this.timeout = timeout; + delete(url: string, config: RequestConfig = {}) { + return this.request(url, { ...config, method: 'DELETE' }); } } -// Create default instance -const http = new HttpClient(); - -// Export both the class and default instance export { HttpClient, HttpError, type RequestConfig, type Response }; -export default http; diff --git a/src/lib/helper/recurrence.helper.ts b/src/lib/helper/recurrence.helper.ts index 1010de3b..e2bf4135 100644 --- a/src/lib/helper/recurrence.helper.ts +++ b/src/lib/helper/recurrence.helper.ts @@ -9,7 +9,7 @@ * @param interval - The interval (e.g., every 1 year, every 2 months) * @returns The next occurrence date */ -export function calculateNextOccurrence( +function calculateNextOccurrence( currentDate: Date, recurrenceType: string, interval: number = 1 @@ -42,97 +42,10 @@ export function calculateNextOccurrence( * @param recurrenceType - Type of recurrence * @returns true if the date should recur */ -export function shouldRecur(recurrenceType: string): boolean { +function shouldRecur(recurrenceType: string): boolean { return ['yearly', 'monthly', 'weekly', 'daily'].includes(recurrenceType); } -/** - * Check if recurrence has ended - * @param currentDate - The current date - * @param recurrenceEndDate - The end date for recurrence (null means no end) - * @returns true if recurrence has ended - */ -export function hasRecurrenceEnded(currentDate: Date, recurrenceEndDate: Date | null): boolean { - if (!recurrenceEndDate) { - return false; - } - return currentDate > recurrenceEndDate; -} - -/** - * Calculate all occurrences between start and end date - * @param startDate - The start date - * @param endDate - The end date (can be null for no end) - * @param recurrenceType - Type of recurrence - * @param interval - The interval - * @param maxOccurrences - Maximum number of occurrences to calculate (default 100) - * @returns Array of occurrence dates - */ -export function calculateOccurrences( - startDate: Date, - endDate: Date | null, - recurrenceType: string, - interval: number = 1, - maxOccurrences: number = 100 -): Date[] { - if (!shouldRecur(recurrenceType)) { - return [startDate]; - } - - const occurrences: Date[] = [startDate]; - let currentDate = new Date(startDate); - const today = new Date(); - - // If no end date, calculate future occurrences up to maxOccurrences or 2 years ahead - const calculationLimit = - endDate || new Date(today.getFullYear() + 2, today.getMonth(), today.getDate()); - - while (occurrences.length < maxOccurrences) { - currentDate = calculateNextOccurrence(currentDate, recurrenceType, interval); - - if (currentDate > calculationLimit) { - break; - } - - occurrences.push(new Date(currentDate)); - } - - return occurrences; -} - -/** - * Get the effective end date based on recurrence type - * @param endDate - The original end date - * @param recurrenceType - Type of recurrence - * @returns The effective end date or null for no end - */ -export function getEffectiveEndDate(endDate: Date, recurrenceType: string): Date | null { - if (recurrenceType === 'no_end') { - return null; - } - return endDate; -} - -/** - * Format recurrence description for display - * @param recurrenceType - Type of recurrence - * @param interval - The interval - * @returns Human-readable recurrence description - */ -export function formatRecurrenceDescription(recurrenceType: string, interval: number = 1): string { - if (recurrenceType === 'none') { - return 'Fixed end date'; - } - if (recurrenceType === 'no_end') { - return 'No end date'; - } - - const intervalText = interval === 1 ? '' : `every ${interval} `; - const periodText = interval === 1 ? recurrenceType.slice(0, -2) : recurrenceType; - - return `Renews ${intervalText}${periodText}`; -} - /** * Calculate the next due date for a recurring item. * Returns null when there is no further scheduled occurrence. diff --git a/src/lib/helper/settings-form.helper.ts b/src/lib/helper/settings-form.helper.ts index a2e4c4f8..194d1ae6 100644 --- a/src/lib/helper/settings-form.helper.ts +++ b/src/lib/helper/settings-form.helper.ts @@ -4,9 +4,31 @@ import { data as currencies } from 'currency-codes'; import { getCurrencySymbol } from '$lib/helper/format.helper'; import { z } from 'zod/v4'; -interface SettingsSchemaOptions { - includeNotificationProcessingSchedule?: boolean; -} +const settingsConfigSchema = z.object({ + dateFormat: z.string(), + locale: z.string().min(2), + timezone: z.string().min(3), + currency: z.string().min(1, 'Currency is required'), + unitOfDistance: z.enum(['kilometer', 'mile']), + unitOfVolume: z.enum(['liter', 'gallon']), + unitOfLpg: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('liter'), + unitOfCng: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('kilogram'), + mileageUnitFormat: z + .enum(['distance-per-fuel', 'fuel-per-distance', 'uk-mpg']) + .default('distance-per-fuel'), + theme: z.string().default('light'), + darkVariant: z.string().default('default'), + customCss: z.string().optional(), + featureFuelLog: z.boolean().default(true), + featureMaintenance: z.boolean().default(true), + featureCompliance: z.boolean().default(true), + featureReminders: z.boolean().default(true), + featureOverview: z.boolean().default(true), + notificationProcessingEnabled: z.boolean().default(true), + notificationProcessingSchedule: z.string().default('0 9 * * *') +}); + +export type SettingsConfig = z.infer; export function createSettingsConfigSchema( isValidFormat: (value: string) => { valid: boolean }, @@ -17,52 +39,28 @@ export function createSettingsConfigSchema( export function createSettingsConfigSchema( isValidFormat: (value: string) => { valid: boolean }, isValidTimezone: (value: string) => boolean, - options?: SettingsSchemaOptions + options?: { includeNotificationProcessingSchedule?: boolean } ): ReturnType; export function createSettingsConfigSchema( isValidFormat: (value: string) => { valid: boolean }, isValidTimezone: (value: string) => boolean, - options: SettingsSchemaOptions = {} + options: { includeNotificationProcessingSchedule?: boolean } = {} ) { - const baseSchema = z - .object({ + const schema = settingsConfigSchema + .extend({ dateFormat: z.string().refine((fmt) => isValidFormat(fmt).valid, 'Format not valid'), - locale: z.string().min(2), - timezone: z.string().min(3).refine(isValidTimezone, 'Invalid timzone value.'), - currency: z.string().min(1, 'Currency is required'), - unitOfDistance: z.enum(['kilometer', 'mile']), - unitOfVolume: z.enum(['liter', 'gallon']), - unitOfLpg: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('liter'), - unitOfCng: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('kilogram'), - mileageUnitFormat: z - .enum(['distance-per-fuel', 'fuel-per-distance', 'uk-mpg']) - .default('distance-per-fuel'), - theme: z.string().default('light'), - customCss: z.string().optional(), - featureFuelLog: z.boolean().default(true), - featureMaintenance: z.boolean().default(true), - featurePucc: z.boolean().default(true), - featureReminders: z.boolean().default(true), - featureInsurance: z.boolean().default(true), - featureOverview: z.boolean().default(true), - notificationProcessingEnabled: z.boolean().default(true) + timezone: z.string().min(3).refine(isValidTimezone, 'Invalid timzone value.') }) .refine((obj) => { if (obj.mileageUnitFormat !== 'uk-mpg') return true; return obj.unitOfDistance === 'mile' && obj.unitOfVolume === 'liter'; }, 'UK MPG calculation requires unit of distance to be miles and unit of volume to be litres.'); - if (!options.includeNotificationProcessingSchedule) { - return baseSchema; + if (options.includeNotificationProcessingSchedule) { + return schema; } - - return baseSchema.extend({ - notificationProcessingSchedule: z - .string() - .refine((expr) => expr.trim().split(/\s+/).length === 5, 'Invalid cron expression') - .default('0 9 * * *') - }); + return schema.omit({ notificationProcessingSchedule: true }); } export function createSettingsOptions( @@ -78,7 +76,8 @@ export function createSettingsOptions( de: 'Deutsch', it: 'Italiano', hu: 'Magyar', - fi: 'Suomi' + fi: 'Suomi', + ro: 'Română' }; return { @@ -87,6 +86,11 @@ export function createSettingsOptions( label: theme.label, colorPreview: theme.colors?.primary || '#000' })), + darkVariantOptions: [ + { value: 'default', label: m.dark_variant_default() }, + { value: 'dim', label: m.dark_variant_dim() }, + { value: 'oled', label: m.dark_variant_oled() } + ], currencyOptions: currencies.map((currency) => ({ value: currency.code, label: `${getCurrencySymbol(currency.code)} - ${currency.currency} ` @@ -125,34 +129,3 @@ export function createSettingsOptions( })) }; } - -export function createSettingsFieldSectionMap( - includeNotifications = false -): Record { - const fieldMap: Record = { - dateFormat: 'personalization', - locale: 'personalization', - unitOfDistance: 'units', - unitOfVolume: 'units', - unitOfLpg: 'units', - unitOfCng: 'units', - mileageUnitFormat: 'units', - timezone: 'personalization', - currency: 'personalization', - theme: 'personalization', - customCss: 'personalization', - featureFuelLog: 'features', - featureMaintenance: 'features', - featurePucc: 'features', - featureReminders: 'features', - featureInsurance: 'features', - featureOverview: 'features', - notificationProcessingEnabled: 'notifications' - }; - - if (includeNotifications) { - fieldMap.notificationProcessingSchedule = 'notifications'; - } - - return fieldMap; -} diff --git a/src/lib/helper/timezone.helper.ts b/src/lib/helper/timezone.helper.ts new file mode 100644 index 00000000..3eba8d16 --- /dev/null +++ b/src/lib/helper/timezone.helper.ts @@ -0,0 +1,23 @@ +import { formatInTimeZone } from 'date-fns-tz'; + +// Intl omits UTC itself from the zone list, but it's the app's default timezone. +const TIMEZONES = ['UTC', ...Intl.supportedValuesOf('timeZone')]; + +export const getTimezoneOptions = (): { + value: string; + label: string; + offset: number; +}[] => { + const referenceDate = new Date('2024-01-01T12:00:00Z'); + + return TIMEZONES.map((zone) => { + const offset = formatInTimeZone(referenceDate, zone, 'xxx'); + return { + value: zone, + label: `[${offset}] ${zone}`, + offset: Number(offset.replace(':', '')) + }; + }).sort((a, b) => a.offset - b.offset); +}; + +export const isValidTimezone = (tz: string) => TIMEZONES.includes(tz); diff --git a/src/lib/helper/unit.helper.ts b/src/lib/helper/unit.helper.ts new file mode 100644 index 00000000..23c606c8 --- /dev/null +++ b/src/lib/helper/unit.helper.ts @@ -0,0 +1,161 @@ +import type { FormatConfig } from './date.helper'; + +const UNIT_LABEL_FALLBACKS: Record = { + liter: 'L', + gallon: 'gal', + kilogram: 'kg', + pound: 'lb', + kilometer: 'km', + mile: 'mi' +}; + +const safeUnitLabel = (unit: string, locale: string): string => { + try { + return ( + new Intl.NumberFormat(locale, { + style: 'unit', + unit + }) + .formatToParts(0) + .find((part) => part.type === 'unit')?.value || + UNIT_LABEL_FALLBACKS[unit] || + unit + ); + } catch (_) { + return UNIT_LABEL_FALLBACKS[unit] || unit; + } +}; + +const safeUnitFormat = (value: number, unit: string, locale: string): string | null => { + try { + return new Intl.NumberFormat(locale, { + style: 'unit', + unit + }).format(value); + } catch (_) { + return null; + } +}; + +export const getCurrencySymbol = (currency: string): string => { + try { + return ( + new Intl.NumberFormat('en-US', { + style: 'currency', + currency + }) + .formatToParts(0) + .find((part) => part.type === 'currency')?.value || '' + ); + } catch (e) { + return ''; + } +}; + +export const formatCurrency = (amount: number, config: FormatConfig): string => { + return new Intl.NumberFormat(config.locale, { + style: 'currency', + currency: config.currency + }).format(amount); +}; + +const getFuelVolumeUnit = (fuelType: string, config: FormatConfig): string => { + switch (fuelType) { + case 'lpg': + return config.unitOfLpg || config.unitOfVolume; + case 'cng': + return config.unitOfCng || config.unitOfVolume; + default: + return config.unitOfVolume; + } +}; + +export const getDistanceUnit = (config: FormatConfig): string => { + return safeUnitLabel(config.unitOfDistance, config.locale); +}; + +export const formatDistance = (distance: number, config: FormatConfig): string => { + return ( + safeUnitFormat(distance, config.unitOfDistance, config.locale) || + `${distance} ${safeUnitLabel(config.unitOfDistance, config.locale)}` + ); +}; + +export const getFuelUnit = (vehicleType: string, config: FormatConfig): string => { + if (vehicleType === 'electric') { + return 'kWh'; + } + return safeUnitLabel(getFuelVolumeUnit(vehicleType, config), config.locale); +}; + +export const formatFuel = (amount: number, vehicleType: string, config: FormatConfig): string => { + if (vehicleType === 'electric') { + return `${amount.toFixed(3)} kWh`; + } + + const fuelUnit = getFuelVolumeUnit(vehicleType, config); + return ( + safeUnitFormat(amount, fuelUnit, config.locale) || + `${amount.toFixed(2)} ${safeUnitLabel(fuelUnit, config.locale)}` + ); +}; + +export const getMileageUnit = (vehicleType: string, config: FormatConfig): string => { + if (vehicleType === 'electric') { + return 'km/kWh'; + } + const fuelUnit = getFuelVolumeUnit(vehicleType, config); + const distanceUnit = safeUnitLabel(config.unitOfDistance, config.locale); + const fuelLabel = safeUnitLabel(fuelUnit, config.locale); + + if (config.mileageUnitFormat === 'fuel-per-distance') { + return `${fuelLabel}/100${distanceUnit}`; + } + + if ( + config.mileageUnitFormat === 'uk-mpg' && + config.unitOfDistance === 'mile' && + fuelUnit === 'liter' + ) { + return 'mpg'; + } + + const mileageUnit = `${config.unitOfDistance}-per-${fuelUnit}`; + const label = safeUnitLabel(mileageUnit, config.locale); + return label === mileageUnit ? `${distanceUnit}/${fuelLabel}` : label; +}; + +export const formatMileage = ( + mileage: number, + vehicleType: string, + config: FormatConfig +): string => { + if (vehicleType === 'electric') { + return `${mileage.toFixed(3)} km/kWh`; + } + const fuelUnit = getFuelVolumeUnit(vehicleType, config); + const distanceUnit = safeUnitLabel(config.unitOfDistance, config.locale); + const fuelLabel = safeUnitLabel(fuelUnit, config.locale); + + if (config.mileageUnitFormat === 'fuel-per-distance') { + return `${mileage.toFixed(2)} ${fuelLabel}/100${distanceUnit}`; + } + + if ( + config.mileageUnitFormat === 'uk-mpg' && + config.unitOfDistance === 'mile' && + fuelUnit === 'liter' + ) { + return `${mileage.toFixed(2)} mpg`; + } + + const mileageUnit = `${config.unitOfDistance}-per-${fuelUnit}`; + return ( + safeUnitFormat(mileage, mileageUnit, config.locale) || + `${mileage.toFixed(2)} ${distanceUnit}/${fuelLabel}` + ); +}; + +export const roundNumber = (num: number, decimal: number = 2): number => { + return Number(num.toFixed(decimal)); +}; diff --git a/src/lib/hooks/is-mobile.svelte.ts b/src/lib/hooks/is-mobile.svelte.ts new file mode 100644 index 00000000..e3802d38 --- /dev/null +++ b/src/lib/hooks/is-mobile.svelte.ts @@ -0,0 +1,9 @@ +import { MediaQuery } from 'svelte/reactivity'; + +const DEFAULT_MOBILE_BREAKPOINT = 768; + +export class IsMobile extends MediaQuery { + constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { + super(`max-width: ${breakpoint - 1}px`); + } +} diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index 723d001f..00000000 --- a/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { type ApiResponse } from './response'; diff --git a/src/lib/scope/vehicle-scope.svelte.ts b/src/lib/scope/vehicle-scope.svelte.ts new file mode 100644 index 00000000..c1220d8d --- /dev/null +++ b/src/lib/scope/vehicle-scope.svelte.ts @@ -0,0 +1,53 @@ +import { goto } from '$app/navigation'; +import { page } from '$app/state'; +import type { Vehicle } from '$lib/domain/vehicle'; + +/** Query param used to express the current vehicle scope in the URL. */ +const SCOPE_PARAM = 'vehicle'; + +export interface VehicleScope { + vehicleId?: string; + vehicle?: Vehicle; + isFleet: boolean; +} + +/** + * Read the current vehicle scope from the URL. + * + * Falls back to fleet (`isFleet: true`, no vehicleId) when the param is + * absent, or when the id doesn't match any known vehicle (stale/deleted + * vehicle, or a hand-typed URL). While `vehicles` is still loading + * (`undefined`), the param is trusted as-is with `vehicle` left undefined. + * + * Plain function — call sites wrap it in `$derived(...)` using `page` from + * `$app/state`. + */ +export function readVehicleScope(url: URL, vehicles: Vehicle[] | undefined): VehicleScope { + const vehicleId = url.searchParams.get(SCOPE_PARAM); + + if (!vehicleId) { + return { isFleet: true }; + } + + if (vehicles === undefined) { + return { vehicleId, isFleet: false }; + } + + const vehicle = vehicles.find((v) => v.id === vehicleId); + if (!vehicle) { + return { isFleet: true }; + } + + return { vehicleId, vehicle, isFleet: false }; +} + +/** Navigate to the current path with the scope param set (or removed when omitted). */ +export function setVehicleScope(vehicleId?: string): void { + const url = new URL(page.url); + if (vehicleId) { + url.searchParams.set(SCOPE_PARAM, vehicleId); + } else { + url.searchParams.delete(SCOPE_PARAM); + } + goto(`${url.pathname}${url.search}`, { keepFocus: true, noScroll: true }); +} diff --git a/src/lib/services/autocomplete.service.ts b/src/lib/services/autocomplete.service.ts index c919965d..2a54b27b 100644 --- a/src/lib/services/autocomplete.service.ts +++ b/src/lib/services/autocomplete.service.ts @@ -33,17 +33,11 @@ export async function getServiceCenterSuggestions(): Promise { } /** - * Get unique insurance provider names + * Get unique issuer names for compliance documents (insurance providers, testing centers, + * inspection centers, registration authorities, ...), optionally narrowed to one type. */ -export async function getInsuranceProviderSuggestions(): Promise { - return fetchAutocompleteSuggestions('insuranceProvider'); -} - -/** - * Get unique testing center names from pollution certificates - */ -export async function getTestingCenterSuggestions(): Promise { - return fetchAutocompleteSuggestions('testingCenter'); +export async function getComplianceIssuerSuggestions(type?: string): Promise { + return fetchAutocompleteSuggestions('complianceIssuer', type ? { type } : undefined); } /** diff --git a/src/lib/services/compliance.service.ts b/src/lib/services/compliance.service.ts new file mode 100644 index 00000000..649dbf23 --- /dev/null +++ b/src/lib/services/compliance.service.ts @@ -0,0 +1,9 @@ +import type { Compliance } from '$lib/domain/compliance'; +import { createEntityService } from './entity-service'; + +const { saveWithAttachment, delete: remove } = createEntityService({ + basePath: 'compliance' +}); + +export const saveComplianceWithAttachment = saveWithAttachment; +export const deleteComplianceDocument = remove; diff --git a/src/lib/services/config.service.ts b/src/lib/services/config.service.ts index 358a6fe9..c4afbe32 100644 --- a/src/lib/services/config.service.ts +++ b/src/lib/services/config.service.ts @@ -1,4 +1,5 @@ -import type { Config, Response } from '$lib/domain'; +import type { Config } from '$lib/domain/config'; +import type { Response } from '$lib/domain/shared'; import { apiClient } from '$lib/helper/api.helper'; export const saveConfig = async (configs: Config[]): Promise> => { diff --git a/src/lib/services/entity-service.ts b/src/lib/services/entity-service.ts new file mode 100644 index 00000000..3ef0e875 --- /dev/null +++ b/src/lib/services/entity-service.ts @@ -0,0 +1,88 @@ +import type { Response } from '$lib/domain/shared'; +import { apiClient } from '$lib/helper/api.helper'; +import { uploadFile } from './file.service'; + +function extractApiError(e: unknown, fallback: string): string { + const err = e as { response?: { data?: { message?: string } } }; + return err.response?.data?.message || fallback; +} + +interface EntityServiceOptions { + basePath: string; + fileField?: 'attachment' | 'image'; + serialize?: (entity: T) => Record; +} + +export function createEntityService( + options: EntityServiceOptions +) { + const { basePath, fileField = 'attachment', serialize } = options; + + function buildUrl(entity: T): string { + const suffix = entity.id || ''; + return `/vehicles/${entity.vehicleId}/${basePath}/${suffix}`; + } + + function getMethod(entity: T): 'post' | 'put' { + return entity.id ? 'put' : 'post'; + } + + function preparePayload(entity: T): Record { + return serialize ? serialize(entity) : (entity as Record); + } + + async function save(entity: T): Promise> { + const res: Response = { status: 'OK' }; + try { + const method = getMethod(entity); + const response = await apiClient[method](buildUrl(entity), preparePayload(entity)); + res.data = response.data; + } catch (e: unknown) { + res.status = 'ERROR'; + res.error = extractApiError(e, `Failed to save.`); + } + return res; + } + + async function saveWithAttachment( + entity: T, + attachment: File | undefined, + removeExisting: boolean = false + ): Promise> { + if (attachment) { + try { + const res = await uploadFile(attachment); + (entity as Record)[fileField] = res.data.filename || null; + } catch (e: unknown) { + return { + status: 'ERROR' as const, + error: extractApiError(e, 'Failed to upload attachment') + }; + } + } + + if (removeExisting) { + (entity as Record)[fileField] = null; + } else if (!attachment && entity.id) { + const payload = { ...entity }; + delete (payload as Record)[fileField]; + return save(payload as T); + } + + return save(entity); + } + + async function deleteEntity(entity: T): Promise> { + const res: Response = { status: 'OK' }; + try { + await apiClient.delete(buildUrl(entity)); + res.data = entity.id as string; + } catch (e: unknown) { + res.status = 'ERROR'; + res.error = extractApiError(e, `Failed to delete.`); + } + return res; + } + + return { save, saveWithAttachment, delete: deleteEntity }; +} diff --git a/src/lib/services/file.service.ts b/src/lib/services/file.service.ts index e609c8c0..27100380 100644 --- a/src/lib/services/file.service.ts +++ b/src/lib/services/file.service.ts @@ -1,5 +1,5 @@ import { HttpClient } from '$lib/helper/http.helper'; -import type { Response } from '$lib/domain'; +import type { Response } from '$lib/domain/shared'; import type { ApiResponse } from '$lib/response'; import { withBase } from '$lib/utils'; diff --git a/src/lib/services/fuel.service.ts b/src/lib/services/fuel.service.ts index 168a7004..95659596 100644 --- a/src/lib/services/fuel.service.ts +++ b/src/lib/services/fuel.service.ts @@ -1,62 +1,14 @@ -import type { FuelLog, Response } from '$lib/domain'; -import { apiClient } from '$lib/helper/api.helper'; -import { uploadFile } from './file.service'; - -export const saveFuelLogWithAttachment = async ( - fuelLog: FuelLog, - attachment: File | undefined, - removeExisting: boolean = false -): Promise> => { - if (attachment) { - try { - const res = await uploadFile(attachment); - fuelLog.attachment = res.data.filename || null; - } catch (e: any) { - return { - status: 'ERROR', - error: e.response?.data?.message || 'Failed to upload attachment' - }; - } - } - // Handle existing attachment removal - if (removeExisting) { - fuelLog.attachment = null; - } - // If no new attachment and this is an update (has id) and not removing existing, don't modify attachment field - // This preserves existing attachment when editing without uploading new file - else if (!attachment && fuelLog.id) { - // Remove attachment from the payload to avoid overwriting existing value - const { attachment: _, ...fuelLogWithoutAttachment } = fuelLog; - return saveFuelLog(fuelLogWithoutAttachment as FuelLog); - } - return saveFuelLog(fuelLog); -}; - -export const saveFuelLog = async (fuelLog: FuelLog): Promise> => { - const res: Response = { status: 'OK' }; - try { - const method = fuelLog.id ? 'PUT' : 'POST'; - const url = `/vehicles/${fuelLog.vehicleId}/fuel-logs/${fuelLog.id || ''}`; - - const response = await apiClient[method.toLowerCase() as 'put' | 'post'](url, fuelLog); - res.data = response.data; - } catch (e: any) { - res.status = 'ERROR'; - res.error = e.response?.data?.message || 'Failed to save fuel log.'; - } - return res; -}; - -export const deleteFuelLog = async (fuelLog: FuelLog): Promise> => { - const res: Response = { status: 'OK' }; - try { - const response = await apiClient.delete( - `/vehicles/${fuelLog.vehicleId}/fuel-logs/${fuelLog.id}` - ); - res.data = response.data; - } catch (e: any) { - res.status = 'ERROR'; - res.error = e.response?.data?.message || 'Failed to delete fuel log.'; - } - return res; -}; +import type { FuelLog } from '$lib/domain/fuel'; +import { createEntityService } from './entity-service'; + +const { + save, + saveWithAttachment, + delete: remove +} = createEntityService({ + basePath: 'fuel-logs' +}); + +export const saveFuelLog = save; +export const saveFuelLogWithAttachment = saveWithAttachment; +export const deleteFuelLog = remove; diff --git a/src/lib/services/insurance.service.ts b/src/lib/services/insurance.service.ts deleted file mode 100644 index 1fe1be56..00000000 --- a/src/lib/services/insurance.service.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { Response, Insurance } from '$lib/domain'; -import { apiClient } from '$lib/helper/api.helper'; -import { uploadFile } from './file.service'; - -export const saveInsuranceWithAttachment = async ( - insurance: Insurance, - attachment: File | undefined, - removeExisting: boolean = false -): Promise> => { - if (attachment) { - try { - const res = await uploadFile(attachment); - insurance.attachment = res.data.filename || null; - } catch (e: any) { - return { - status: 'ERROR', - error: e.response?.data?.message || 'Failed to upload attachment' - }; - } - } - // Handle existing attachment removal - if (removeExisting) { - insurance.attachment = null; - } - // If no new attachment and this is an update (has id) and not removing existing, don't modify attachment field - // This preserves existing attachment when editing without uploading new file - else if (!attachment && insurance.id) { - // Remove attachment from the payload to avoid overwriting existing value - const { attachment: _, ...insuranceWithoutAttachment } = insurance; - return saveInsurance(insuranceWithoutAttachment as Insurance); - } - return saveInsurance(insurance); -}; - -export const saveInsurance = async (insurance: Insurance): Promise> => { - const res: Response = { status: 'OK' }; - try { - const method = insurance.id ? 'PUT' : 'POST'; - const url = `/vehicles/${insurance.vehicleId}/insurance/${insurance.id || ''}`; - - const response = await apiClient[method.toLowerCase() as 'put' | 'post'](url, insurance); - res.data = response.data; - } catch (e: any) { - res.status = 'ERROR'; - res.error = e.response?.data?.message || 'Failed to save insurance.'; - } - return res; -}; - -export const deleteInsurance = async (insurance: Insurance): Promise> => { - const res: Response = { status: 'OK' }; - try { - const response = await apiClient.delete( - `/vehicles/${insurance.vehicleId}/insurance/${insurance.id}` - ); - res.data = response.data; - } catch (e: any) { - res.status = 'ERROR'; - res.error = e.response?.data?.message || 'Failed to delete insurance.'; - } - return res; -}; diff --git a/src/lib/services/maintenance.service.ts b/src/lib/services/maintenance.service.ts index aaf4f09a..657898fe 100644 --- a/src/lib/services/maintenance.service.ts +++ b/src/lib/services/maintenance.service.ts @@ -1,66 +1,26 @@ -import type { Response, MaintenanceLog } from '$lib/domain'; +import type { MaintenanceLog } from '$lib/domain/maintenance'; +import { createEntityService } from './entity-service'; import { apiClient } from '$lib/helper/api.helper'; -import { uploadFile } from './file.service'; -export const saveMaintenanceLogWithAttachment = async ( - maintenanceLog: MaintenanceLog, - attachment: File | undefined, - removeExisting: boolean = false -): Promise> => { - if (attachment) { - try { - const res = await uploadFile(attachment); - maintenanceLog.attachment = res.data.filename || null; - } catch (e: any) { - return { - status: 'ERROR', - error: e.response?.data?.message || 'Failed to upload attachment' - }; - } - } - // Handle existing attachment removal - if (removeExisting) { - maintenanceLog.attachment = null; - } - // If no new attachment and this is an update (has id) and not removing existing, don't modify attachment field - // This preserves existing attachment when editing without uploading new file - else if (!attachment && maintenanceLog.id) { - // Remove attachment from the payload to avoid overwriting existing value - const { attachment: _, ...maintenanceLogWithoutAttachment } = maintenanceLog; - return saveMaintenanceLog(maintenanceLogWithoutAttachment as MaintenanceLog); - } - return saveMaintenanceLog(maintenanceLog); -}; +const { saveWithAttachment, delete: remove } = createEntityService({ + basePath: 'maintenance-logs' +}); -export const saveMaintenanceLog = async ( - maintenanceLog: MaintenanceLog -): Promise> => { - const res: Response = { status: 'OK' }; - try { - const method = maintenanceLog.id ? 'PUT' : 'POST'; - const url = `/vehicles/${maintenanceLog.vehicleId}/maintenance-logs/${maintenanceLog.id || ''}`; +export const saveMaintenanceLogWithAttachment = saveWithAttachment; +export const deleteMaintenanceLog = remove; - const response = await apiClient[method.toLowerCase() as 'put' | 'post'](url, maintenanceLog); - res.data = response.data; - } catch (e: any) { - res.status = 'ERROR'; - res.error = e.response?.data?.message || 'Failed to save maintenance log.'; - } - return res; -}; +export const exportMaintenanceLogsPdf = async (vehicleId: string): Promise => { + const response = await apiClient.get(`/vehicles/${vehicleId}/maintenance-logs/export-pdf`, { + responseType: 'blob' + }); -export const deleteMaintenanceLog = async ( - maintenanceLog: MaintenanceLog -): Promise> => { - const res: Response = { status: 'OK' }; - try { - const response = await apiClient.delete( - `/vehicles/${maintenanceLog.vehicleId}/maintenance-logs/${maintenanceLog.id}` - ); - res.data = response.data; - } catch (e: any) { - res.status = 'ERROR'; - res.error = e.response?.data?.message || 'Failed to delete maintenance log.'; - } - return res; + const blob = new Blob([response.data], { type: 'application/pdf' }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `maintenance-log-${vehicleId}.pdf`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); }; diff --git a/src/lib/services/notification-provider.service.ts b/src/lib/services/notification-provider.service.ts index 945f02fa..81a17a25 100644 --- a/src/lib/services/notification-provider.service.ts +++ b/src/lib/services/notification-provider.service.ts @@ -17,16 +17,6 @@ export const getProviders = async (): Promise => { - try { - const response = await apiClient.get(`/notification-providers/${id}`); - return response.data.data; - } catch (error) { - console.error('Failed to fetch notification provider:', error); - throw error; - } -}; - export const createProvider = async ( provider: CreateNotificationProvider ): Promise => { diff --git a/src/lib/services/notification.service.ts b/src/lib/services/notification.service.ts index 2a9fd470..448bc12c 100644 --- a/src/lib/services/notification.service.ts +++ b/src/lib/services/notification.service.ts @@ -1,4 +1,5 @@ -import type { Notification, Response } from '$lib/domain'; +import type { Notification } from '$lib/domain/notification'; +import type { Response } from '$lib/domain/shared'; import { apiClient } from '$lib/helper/api.helper'; export const getNotifications = async (vehicleId: string): Promise> => { @@ -14,6 +15,18 @@ export const getNotifications = async (vehicleId: string): Promise> => { + const res: Response = { status: 'OK' }; + try { + const response = await apiClient.get('/notifications'); + res.data = response.data.data || response.data; + } catch (e: any) { + res.status = 'ERROR'; + res.error = e.response?.data?.message || 'Failed to fetch notifications.'; + } + return res; +}; + export const markNotificationAsRead = async ( vehicleId: string, notificationId: string diff --git a/src/lib/services/pucc.service.ts b/src/lib/services/pucc.service.ts deleted file mode 100644 index c2add9c7..00000000 --- a/src/lib/services/pucc.service.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { Response, PollutionCertificate } from '$lib/domain'; -import { apiClient } from '$lib/helper/api.helper'; -import { uploadFile } from './file.service'; - -export const savePuccWithAttachment = async ( - certificate: PollutionCertificate, - attachment: File | undefined, - removeExisting: boolean = false -): Promise> => { - if (attachment) { - try { - const res = await uploadFile(attachment); - certificate.attachment = res.data.filename || null; - } catch (e: any) { - return { - status: 'ERROR', - error: e.response?.data?.message || 'Failed to upload attachment' - }; - } - } - // Handle existing attachment removal - if (removeExisting) { - certificate.attachment = null; - } - // If no new attachment and this is an update (has id) and not removing existing, don't modify attachment field - // This preserves existing attachment when editing without uploading new file - else if (!attachment && certificate.id) { - // Remove attachment from the payload to avoid overwriting existing value - const { attachment: _, ...certificateWithoutAttachment } = certificate; - return savePucc(certificateWithoutAttachment as PollutionCertificate); - } - return savePucc(certificate); -}; - -export const savePucc = async ( - certificate: PollutionCertificate -): Promise> => { - const res: Response = { status: 'OK' }; - try { - const method = certificate.id ? 'PUT' : 'POST'; - const url = `/vehicles/${certificate.vehicleId}/pucc/${certificate.id || ''}`; - - const response = await apiClient[method.toLowerCase() as 'put' | 'post'](url, certificate); - res.data = response.data; - } catch (e: any) { - res.status = 'ERROR'; - res.error = e.response?.data?.message || 'Failed to save pollution certificate.'; - } - return res; -}; - -export const deletePucc = async (pucc: PollutionCertificate): Promise> => { - const res: Response = { status: 'OK' }; - try { - const response = await apiClient.delete(`/vehicles/${pucc.vehicleId}/pucc/${pucc.id}`); - res.data = response.data; - } catch (e: any) { - res.status = 'ERROR'; - res.error = e.response?.data?.message || 'Failed to delete PUCC.'; - } - return res; -}; -// Backward compatibility aliases -export const savePollutionCertificate = savePucc; -export const savePollutionCertificateWithAttachment = savePuccWithAttachment; -export const deletePollutionCertificate = deletePucc; diff --git a/src/lib/services/reminder.service.ts b/src/lib/services/reminder.service.ts index ad9fd6cc..610d88dc 100644 --- a/src/lib/services/reminder.service.ts +++ b/src/lib/services/reminder.service.ts @@ -1,46 +1,14 @@ -import type { Reminder, Response } from '$lib/domain'; -import { apiClient } from '$lib/helper/api.helper'; - -const serializeReminder = (reminder: Reminder) => ({ - ...reminder, - dueDate: - reminder.dueDate instanceof Date ? reminder.dueDate.toISOString() : (reminder.dueDate ?? null) +import type { Reminder } from '$lib/domain/reminder'; +import { createEntityService } from './entity-service'; + +const { save, delete: remove } = createEntityService({ + basePath: 'reminders', + serialize: (reminder) => ({ + ...reminder, + dueDate: + reminder.dueDate instanceof Date ? reminder.dueDate.toISOString() : (reminder.dueDate ?? null) + }) }); -export const saveReminder = async (reminder: Reminder): Promise> => { - const res: Response = { status: 'OK' }; - try { - const method = reminder.id ? 'PUT' : 'POST'; - const url = `/vehicles/${reminder.vehicleId}/reminders/${reminder.id || ''}`; - const response = await apiClient[method.toLowerCase() as 'post' | 'put']( - url, - serializeReminder(reminder) - ); - res.data = response.data; - } catch (e: any) { - res.status = 'ERROR'; - res.error = e.response?.data?.message || 'Failed to save reminder.'; - } - return res; -}; - -export const deleteReminder = async (reminder: Reminder): Promise> => { - if (!reminder.id) { - return { - status: 'ERROR', - error: 'Reminder id is required to delete.' - }; - } - - const res: Response = { status: 'OK' }; - try { - const response = await apiClient.delete( - `/vehicles/${reminder.vehicleId}/reminders/${reminder.id}` - ); - res.data = response.data; - } catch (e: any) { - res.status = 'ERROR'; - res.error = e.response?.data?.message || 'Failed to delete reminder.'; - } - return res; -}; +export const saveReminder = save; +export const deleteReminder = remove; diff --git a/src/lib/services/vehicle.service.ts b/src/lib/services/vehicle.service.ts index 21b8f135..2884501c 100644 --- a/src/lib/services/vehicle.service.ts +++ b/src/lib/services/vehicle.service.ts @@ -1,51 +1,8 @@ -import type { DataPoint, Response, Vehicle } from '$lib/domain'; +import type { Response } from '$lib/domain/shared'; +import type { Vehicle } from '$lib/domain/vehicle'; import { apiClient } from '$lib/helper/api.helper'; import { uploadFile } from './file.service'; -export const fetchMileageData = async (vehicleId: string): Promise => { - try { - const response = await apiClient.get(`/vehicles/${vehicleId}/fuel-logs`); - const data: { - date: string; - mileage: number | null; - }[] = response.data; - return data - .filter((log) => log.mileage != null) - .map((log) => { - return { - x: new Date(log.date), - y: log.mileage - }; - }) - .sort((a, b) => a.x.getTime() - b.x.getTime()); - } catch (e) { - console.error('Failed to fetch mileage data:', e); - return []; - } -}; - -export const fetchCostData = async (vehicleId: string): Promise => { - try { - const response = await apiClient.get(`/vehicles/${vehicleId}/fuel-logs`); - const data: { - date: string; - cost: number | null; - }[] = response.data; - return data - .filter((log) => log.cost != null) - .map((log) => { - return { - x: new Date(log.date), - y: log.cost - }; - }) - .sort((a, b) => a.x.getTime() - b.x.getTime()); - } catch (e) { - console.error('Failed to fetch cost data:', e); - return []; - } -}; - export const saveVehicleWithImage = async ( vehicle: Vehicle, image: File | undefined, diff --git a/src/lib/stores/auth.svelte.ts b/src/lib/stores/auth.svelte.ts index c0d6b0a4..11d59bb5 100644 --- a/src/lib/stores/auth.svelte.ts +++ b/src/lib/stores/auth.svelte.ts @@ -28,9 +28,7 @@ class AuthStore { } try { - const { data: res } = await apiClient.get('/auth', { - skipInterceptors: true - }); + const { data: res } = await apiClient.get('/auth'); this.isAuthDisabled = !!res.data?.isAuthDisabled; this.hasUsers = res.data?.hasUsers ?? false; @@ -57,11 +55,7 @@ class AuthStore { login = async (username: string, password: string) => { try { - const { data: res } = await apiClient.post( - '/auth', - { username, password }, - { skipInterceptors: true } - ); + const { data: res } = await apiClient.post('/auth', { username, password }); if (res.success && res.data) { this.user = res.data.user; @@ -81,11 +75,10 @@ class AuthStore { register = async (username: string, password: string) => { try { - const { data: res } = await apiClient.post( - '/auth/register', - { username, password }, - { skipInterceptors: true } - ); + const { data: res } = await apiClient.post('/auth/register', { + username, + password + }); if (res.success) { toast.success('User created successfully. Please login.'); diff --git a/src/lib/stores/chart.svelte.ts b/src/lib/stores/chart.svelte.ts index 59a214f6..9f638c3c 100644 --- a/src/lib/stores/chart.svelte.ts +++ b/src/lib/stores/chart.svelte.ts @@ -1,5 +1,9 @@ -import type { DataPoint, FuelLog } from '$lib/domain'; +import type { FuelLog } from '$lib/domain/fuel'; +import type { DataPoint } from '$lib/domain/shared'; +import type { Vehicle } from '$lib/domain/vehicle'; +import { vehicleTrendColor } from '$lib/domain/vehicle'; import { fuelLogStore } from './fuel-log.svelte'; +import { vehicleStore } from './vehicle.svelte'; const calculateCostData = (logs: FuelLog[]) => { return logs @@ -25,9 +29,68 @@ const calculateMileageData = (logs: FuelLog[]) => { .sort((a, b) => a.x.getTime() - b.x.getTime()); }; +export const calculateFuelAmountData = (logs: FuelLog[]) => { + return logs + .filter((log) => log.fuelAmount) + .map((log) => { + return { + x: new Date(log.date), + y: log.fuelAmount + }; + }) + .sort((a, b) => a.x.getTime() - b.x.getTime()); +}; + +export interface VehicleTrendSeries { + vehicleId: string; + label: string; + fuelType: Vehicle['fuelType']; + color: string | null; + data: DataPoint[]; +} + +const groupByVehicle = ( + logs: FuelLog[], + calculate: (logs: FuelLog[]) => DataPoint[] +): VehicleTrendSeries[] => { + const byVehicle = new Map(); + for (const log of logs) { + const group = byVehicle.get(log.vehicleId); + if (group) { + group.push(log); + } else { + byVehicle.set(log.vehicleId, [log]); + } + } + + return Array.from(byVehicle.entries()).map(([vehicleId, vehicleLogs]) => { + const vehicle = vehicleStore.vehicles?.find((v) => v.id === vehicleId); + const label = vehicle + ? `${vehicle.make} ${vehicle.model}` + : `${vehicleLogs[0].vehicleMake ?? ''} ${vehicleLogs[0].vehicleModel ?? ''}`.trim() || + vehicleLogs[0].vehiclePlate || + vehicleId; + return { + vehicleId, + label, + fuelType: vehicle?.fuelType ?? 'petrol', + color: vehicleTrendColor(vehicle?.color), + data: calculate(vehicleLogs) + }; + }); +}; + class ChartStore { mileageData? = $derived(calculateMileageData(fuelLogStore.fuelLogs || [])); costData? = $derived(calculateCostData(fuelLogStore.fuelLogs || [])); + fuelAmountData? = $derived(calculateFuelAmountData(fuelLogStore.fuelLogs || [])); + + mileageByVehicle = $derived( + groupByVehicle(fuelLogStore.fuelLogs || [], calculateMileageData) + ); + fuelAmountByVehicle = $derived( + groupByVehicle(fuelLogStore.fuelLogs || [], calculateFuelAmountData) + ); } export const chartStore = new ChartStore(); diff --git a/src/lib/stores/compliance.svelte.ts b/src/lib/stores/compliance.svelte.ts new file mode 100644 index 00000000..8a7b9ef5 --- /dev/null +++ b/src/lib/stores/compliance.svelte.ts @@ -0,0 +1,21 @@ +import type { Compliance } from '$lib/domain/compliance'; +import { createEntityStore } from './entity-store.svelte'; + +const entityStore = createEntityStore({ + buildPath: (vehicleId) => (vehicleId ? `/compliance?vehicleId=${vehicleId}` : '/compliance'), + errorMessage: 'Failed to fetch compliance documents' +}); + +export const complianceStore = { + get documents() { + return entityStore.items; + }, + get processing() { + return entityStore.processing; + }, + get error() { + return entityStore.error; + }, + refreshDocuments: entityStore.refresh, + reloadDocuments: entityStore.reload +}; diff --git a/src/lib/stores/config.svelte.ts b/src/lib/stores/config.svelte.ts index 93ef4866..c9a5a57b 100644 --- a/src/lib/stores/config.svelte.ts +++ b/src/lib/stores/config.svelte.ts @@ -17,12 +17,13 @@ const DEFAULT_CONFIGS: Configs = { timezone: 'UTC', featureFuelLog: true, featureMaintenance: true, - featurePucc: true, + featureCompliance: true, featureReminders: true, - featureInsurance: true, featureOverview: true, notificationProcessingEnabled: true, - notificationProcessingSchedule: '0 9 * * *' + notificationProcessingSchedule: '0 9 * * *', + theme: 'light', + darkVariant: 'default' }; /** @@ -49,6 +50,20 @@ class ConfigStore { processing = $state(false); error = $state(); + setConfigs = (configData: Config[]) => { + this.rawConfig = configData; + applyRawConfigs(this.configs, configData); + + // Update paraglide locale without reloading + try { + if (this.configs.locale) { + setLocale(this.configs.locale as any, { reload: false }); + } + } catch (_) { + /* noop */ + } + }; + getCustomCss = async (): Promise => { this.processing = true; return apiClient diff --git a/src/lib/stores/dashboard-layout.svelte.ts b/src/lib/stores/dashboard-layout.svelte.ts new file mode 100644 index 00000000..dbad666d --- /dev/null +++ b/src/lib/stores/dashboard-layout.svelte.ts @@ -0,0 +1,116 @@ +import { apiClient } from '$lib/helper/api.helper'; +import { + DEFAULT_WIDGET_LAYOUT, + GRID_COLUMNS, + widgetMinSize, + WIDGET_TYPES, + type WidgetColSpan, + type WidgetLayoutItem, + type WidgetRowSpan, + type WidgetType +} from '$lib/domain/dashboard'; +import { compactLayout } from '$lib/components/dashboard/grid-layout'; + +const SAVE_DEBOUNCE_MS = 600; + +/** Grows anything below its widget's minimum, so old layouts don't render clipped. */ +function applyMinSizes(items: WidgetLayoutItem[]): WidgetLayoutItem[] { + return items.map((item) => { + const { minColSpan, minRowSpan } = widgetMinSize(item.type); + const colSpan = Math.max(item.colSpan, minColSpan); + return { + ...item, + colSpan, + rowSpan: Math.max(item.rowSpan, minRowSpan), + colStart: Math.min(item.colStart, GRID_COLUMNS - colSpan + 1) + }; + }); +} + +class DashboardLayoutStore { + items = $state([]); + loading = $state(false); + saving = $state(false); + error = $state(); + + private saveTimer: ReturnType | undefined; + + async fetchLayout() { + this.loading = true; + this.error = undefined; + try { + const { data: res } = await apiClient.get<{ success: boolean; data: WidgetLayoutItem[] }>( + '/dashboard/layout' + ); + const layout = res.success && res.data ? res.data : DEFAULT_WIDGET_LAYOUT; + this.items = compactLayout(applyMinSizes(layout)); + } catch (err) { + this.error = 'Failed to load dashboard layout'; + this.items = compactLayout(applyMinSizes(DEFAULT_WIDGET_LAYOUT)); + console.error(err); + } finally { + this.loading = false; + } + } + + private scheduleSave() { + this.saving = true; + clearTimeout(this.saveTimer); + this.saveTimer = setTimeout(() => this.persist(), SAVE_DEBOUNCE_MS); + } + + private async persist() { + try { + await apiClient.put('/dashboard/layout', $state.snapshot(this.items)); + } catch (err) { + console.error('Failed to save dashboard layout', err); + } finally { + this.saving = false; + } + } + + get availableWidgetTypes(): WidgetType[] { + const used = new Set(this.items.map((w) => w.type)); + return (Object.keys(WIDGET_TYPES) as WidgetType[]).filter((t) => !used.has(t)); + } + + addWidget(type: WidgetType, colSpan: WidgetColSpan = 6, rowSpan: WidgetRowSpan = 8) { + const { minColSpan, minRowSpan } = widgetMinSize(type); + const bottomRow = this.items.reduce((max, w) => Math.max(max, w.rowStart + w.rowSpan), 1); + const items = [ + ...this.items.map((w) => ({ ...w })), + { + id: crypto.randomUUID(), + type, + colStart: 1, + rowStart: bottomRow, + colSpan: Math.max(colSpan, minColSpan), + rowSpan: Math.max(rowSpan, minRowSpan) + } + ]; + this.items = compactLayout(items); + this.scheduleSave(); + } + + removeWidget(id: string) { + const items = this.items.filter((w) => w.id !== id).map((w) => ({ ...w })); + this.items = compactLayout(items); + this.scheduleSave(); + } + + resetToDefault() { + this.items = compactLayout(applyMinSizes(DEFAULT_WIDGET_LAYOUT)); + this.scheduleSave(); + } + + /** + * Accepts a layout already resolved by the grid engine (see `grid-layout.ts`). Called once per + * drag/resize gesture, on drop — the grid keeps its own draft while the pointer is down. + */ + commitLayout(items: WidgetLayoutItem[]) { + this.items = items; + this.scheduleSave(); + } +} + +export const dashboardLayoutStore = new DashboardLayoutStore(); diff --git a/src/lib/stores/dashboard.svelte.ts b/src/lib/stores/dashboard.svelte.ts new file mode 100644 index 00000000..6199543b --- /dev/null +++ b/src/lib/stores/dashboard.svelte.ts @@ -0,0 +1,28 @@ +import { apiClient } from '$lib/helper/api.helper'; +import type { DashboardSummary } from '$lib/domain/dashboard'; + +class DashboardStore { + summary = $state(null); + loading = $state(false); + error = $state(); + + async fetchSummary() { + this.loading = true; + this.error = undefined; + try { + const { data: res } = await apiClient.get<{ success: boolean; data: DashboardSummary }>( + '/dashboard/summary' + ); + if (res.success && res.data) { + this.summary = res.data; + } + } catch (err) { + this.error = 'Failed to load dashboard summary'; + console.error(err); + } finally { + this.loading = false; + } + } +} + +export const dashboardStore = new DashboardStore(); diff --git a/src/lib/stores/entity-store.svelte.ts b/src/lib/stores/entity-store.svelte.ts new file mode 100644 index 00000000..a7964aae --- /dev/null +++ b/src/lib/stores/entity-store.svelte.ts @@ -0,0 +1,77 @@ +import { apiClient } from '$lib/helper/api.helper'; +import type { ApiResponse } from '$lib/response'; + +interface EntityStoreOptions { + buildPath: (vehicleId?: string) => string; + sort?: (a: T, b: T) => number; + map?: (raw: unknown) => T; + errorMessage?: string; +} + +export function createEntityStore(options: EntityStoreOptions) { + let items = $state() as T[] | undefined; + let processing = $state(false); + let error = $state(); + // Remembered so post-mutation reloads stay in the scope the user is viewing + // instead of silently widening the list back to the whole fleet. + let currentVehicleId: string | undefined; + // A page and the list it renders often derive the same scope independently, + // so both ask for it on mount. Share the in-flight request instead of + // firing duplicate round trips. + let inFlight: { path: string; promise: Promise } | undefined; + + function refresh(vehicleId?: string): Promise { + currentVehicleId = vehicleId; + const urlPath = options.buildPath(vehicleId); + if (inFlight?.path === urlPath) return inFlight.promise; + + const promise = (async () => { + processing = true; + try { + const { data: res } = await apiClient.get(urlPath); + let result: T[] = options.map + ? (res.data as unknown[]).map(options.map) + : (res.data as T[]); + if (options.sort) result = [...result].sort(options.sort); + items = result; + error = undefined; + } catch { + error = options.errorMessage || 'Failed to fetch data'; + } finally { + processing = false; + if (inFlight?.path === urlPath) inFlight = undefined; + } + })(); + + inFlight = { path: urlPath, promise }; + return promise; + } + + /** + * Re-fetch the current scope. Use after a create/edit/delete — it bypasses + * the in-flight dedupe so it never resolves with pre-mutation data. + */ + function reload(): Promise { + inFlight = undefined; + return refresh(currentVehicleId); + } + + function clear(): void { + items = undefined; + } + + return { + get items() { + return items; + }, + get processing() { + return processing; + }, + get error() { + return error; + }, + refresh, + reload, + clear + }; +} diff --git a/src/lib/stores/fuel-log.svelte.ts b/src/lib/stores/fuel-log.svelte.ts index 02261873..580c1538 100644 --- a/src/lib/stores/fuel-log.svelte.ts +++ b/src/lib/stores/fuel-log.svelte.ts @@ -1,32 +1,27 @@ -import type { FuelLog } from '$lib/domain'; -import { apiClient } from '$lib/helper/api.helper'; -import type { ApiResponse } from '$lib/response'; +import type { FuelLog } from '$lib/domain/fuel'; import { compareDesc } from 'date-fns'; -import { vehicleStore } from './vehicle.svelte'; +import { createEntityStore } from './entity-store.svelte'; -class FuelLogStore { - fuelLogs = $state(); - processing = $state(false); - error = $state(); +const entityStore = createEntityStore({ + buildPath: (vehicleId) => (vehicleId ? `/fuel-logs?vehicleId=${vehicleId}` : '/fuel-logs'), + sort: (a, b) => { + const dateDiff = compareDesc(a.date, b.date); + if (dateDiff !== 0) return dateDiff; + return (b.odometer ?? 0) - (a.odometer ?? 0); + }, + errorMessage: 'Failed to fetch Fuel Logs' +}); - refreshFuelLogs = () => { - if (!vehicleStore.selectedId) return; - this.processing = true; - apiClient - .get(`/vehicles/${vehicleStore.selectedId}/fuel-logs`) - .then(({ data: res }) => { - const logs: FuelLog[] = res.data; - logs.sort((a, b) => { - const dateDiff = compareDesc(a.date, b.date); - if (dateDiff !== 0) return dateDiff; - return (b.odometer ?? 0) - (a.odometer ?? 0); - }); - this.fuelLogs = logs; - this.error = undefined; - }) - .catch((err) => (this.error = 'Failed to fetch Fuel Logs')) - .finally(() => (this.processing = false)); - }; -} - -export const fuelLogStore = new FuelLogStore(); +export const fuelLogStore = { + get fuelLogs() { + return entityStore.items; + }, + get processing() { + return entityStore.processing; + }, + get error() { + return entityStore.error; + }, + refreshFuelLogs: entityStore.refresh, + reloadFuelLogs: entityStore.reload +}; diff --git a/src/lib/stores/insurance.svelte.ts b/src/lib/stores/insurance.svelte.ts deleted file mode 100644 index d9b9b52c..00000000 --- a/src/lib/stores/insurance.svelte.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Insurance } from '$lib/domain'; -import { apiClient } from '$lib/helper/api.helper'; -import type { ApiResponse } from '$lib/response'; -import { vehicleStore } from './vehicle.svelte'; - -class InsuranceStore { - insurances = $state(); - processing = $state(false); - error = $state(); - - refreshInsurances = () => { - if (!vehicleStore.selectedId) return; - this.processing = true; - apiClient - .get(`/vehicles/${vehicleStore.selectedId}/insurance`) - .then(({ data: res }) => { - this.insurances = res.data; - this.error = undefined; - }) - .catch((err) => (this.error = 'Failed to fetch Insurances')) - .finally(() => (this.processing = false)); - }; -} - -export const insuranceStore = new InsuranceStore(); diff --git a/src/lib/stores/maintenance.svelte.ts b/src/lib/stores/maintenance.svelte.ts index 0217cd5c..45e73653 100644 --- a/src/lib/stores/maintenance.svelte.ts +++ b/src/lib/stores/maintenance.svelte.ts @@ -1,32 +1,28 @@ -import type { FuelLog, MaintenanceLog } from '$lib/domain'; -import { apiClient } from '$lib/helper/api.helper'; -import type { ApiResponse } from '$lib/response'; +import type { MaintenanceLog } from '$lib/domain/maintenance'; import { compareDesc } from 'date-fns'; -import { vehicleStore } from './vehicle.svelte'; +import { createEntityStore } from './entity-store.svelte'; -class MaintenanceStore { - maintenanceLogs = $state(); - processing = $state(false); - error = $state(); +const entityStore = createEntityStore({ + buildPath: (vehicleId) => + vehicleId ? `/maintenance-logs?vehicleId=${vehicleId}` : '/maintenance-logs', + sort: (a, b) => { + const dateDiff = compareDesc(a.date, b.date); + if (dateDiff !== 0) return dateDiff; + return b.odometer - a.odometer; + }, + errorMessage: 'Failed to fetch Maintenance Logs' +}); - refreshMaintenanceLogs = () => { - if (!vehicleStore.selectedId) return; - this.processing = true; - apiClient - .get(`/vehicles/${vehicleStore.selectedId}/maintenance-logs`) - .then(({ data: res }) => { - const logs: MaintenanceLog[] = res.data; - this.maintenanceLogs = logs; - logs.sort((a, b) => { - const dateDiff = compareDesc(a.date, b.date); - if (dateDiff !== 0) return dateDiff; - return b.odometer - a.odometer; - }); - this.error = undefined; - }) - .catch((err) => (this.error = 'Failed to fetch Maintenance Logs')) - .finally(() => (this.processing = false)); - }; -} - -export const maintenanceStore = new MaintenanceStore(); +export const maintenanceStore = { + get maintenanceLogs() { + return entityStore.items; + }, + get processing() { + return entityStore.processing; + }, + get error() { + return entityStore.error; + }, + refreshMaintenanceLogs: entityStore.refresh, + reloadMaintenanceLogs: entityStore.reload +}; diff --git a/src/lib/stores/notification.svelte.ts b/src/lib/stores/notification.svelte.ts new file mode 100644 index 00000000..bc310a3d --- /dev/null +++ b/src/lib/stores/notification.svelte.ts @@ -0,0 +1,135 @@ +import { goto } from '$app/navigation'; +import type { Notification } from '$lib/domain/notification'; +import { + getAllNotifications, + markNotificationAsRead, + markAllNotificationsAsRead, + clearNotification +} from '$lib/services/notification.service'; +import { toast } from 'svelte-sonner'; +import * as m from '$lib/paraglide/messages/_index.js'; + +type NotificationType = Notification['type']; + +const NAVIGATION_MAP: Record = { + information: '/dashboard', + compliance: '/compliance', + reminder: '/reminders', + maintenance: '/maintenance', + registration: '/compliance', + alert: '/dashboard' +}; + +class NotificationStore { + apiNotifications = $state([]); + isLoadingNotifications = $state(false); + isClearingAll = $state(false); + markingAsReadIds = $state>({}); + + unreadCount = $derived(this.apiNotifications.filter((n) => !n.isRead).length); + clearableReadCount = $derived( + this.apiNotifications.filter((n) => n.isRead && n.channel !== 'alert').length + ); + + async fetch() { + this.isLoadingNotifications = true; + try { + const response = await getAllNotifications(); + this.apiNotifications = response.status === 'OK' && response.data ? response.data : []; + } catch (err) { + console.error('Failed to fetch notifications:', err); + this.apiNotifications = []; + } finally { + this.isLoadingNotifications = false; + } + } + + async markAsRead(notification: Notification) { + if (notification.isRead) return; + if (!notification.vehicleId || !notification.id) return; + if (this.markingAsReadIds[notification.id]) return; + this.markingAsReadIds = { ...this.markingAsReadIds, [notification.id]: true }; + try { + const response = await markNotificationAsRead(notification.vehicleId, notification.id); + if (response.status === 'OK') { + this.apiNotifications = this.apiNotifications.map((n) => + n.id === notification.id ? { ...n, isRead: true } : n + ); + } + } catch (err) { + console.error('Failed to mark as read:', err); + } finally { + const next = { ...this.markingAsReadIds }; + delete next[notification.id]; + this.markingAsReadIds = next; + } + } + + async navigate(notification: Notification) { + const targetPath = NAVIGATION_MAP[notification.type]; + if (targetPath) await goto(targetPath); + } + + async clearAllRead() { + if (this.isClearingAll) return; + const readNotifications = this.apiNotifications.filter( + (n) => n.isRead && n.channel !== 'alert' + ); + if (readNotifications.length === 0) return; + this.isClearingAll = true; + try { + const results = await Promise.allSettled( + readNotifications.map((n) => clearNotification(n.vehicleId, n.id!)) + ); + const successCount = results.filter((r) => r.status === 'fulfilled').length; + const failCount = results.filter((r) => r.status === 'rejected').length; + const clearedIds = readNotifications + .map((n) => n.id) + .filter((_, i) => results[i].status === 'fulfilled'); + this.apiNotifications = this.apiNotifications.filter((n) => !clearedIds.includes(n.id)); + if (failCount === 0) { + toast.success( + successCount === 1 + ? m.notif_cleared_success_one() + : m.notif_cleared_success_other({ count: String(successCount) }) + ); + } else if (successCount > 0) { + toast.warning( + m.notif_cleared_partial({ success: String(successCount), failed: String(failCount) }) + ); + } else { + toast.error(m.notif_clear_failed()); + } + } catch (err) { + toast.error(err instanceof Error ? err.message : m.notif_clear_failed()); + } finally { + this.isClearingAll = false; + } + } + + async markAllAsRead() { + if (this.unreadCount === 0 || this.isClearingAll) return; + this.isClearingAll = true; + try { + const vehicleIds = Array.from( + new Set(this.apiNotifications.filter((n) => !n.isRead).map((n) => n.vehicleId)) + ); + const results = await Promise.allSettled( + vehicleIds.map((id) => markAllNotificationsAsRead(id)) + ); + const anySucceeded = results.some((r) => r.status === 'fulfilled' && r.value.status === 'OK'); + if (anySucceeded) { + this.apiNotifications = this.apiNotifications.map((n) => ({ ...n, isRead: true })); + toast.success(m.notif_all_marked_read()); + } else { + toast.error(m.notif_mark_read_failed()); + } + } catch (err) { + toast.error(err instanceof Error ? err.message : m.notif_mark_read_failed()); + } finally { + this.isClearingAll = false; + } + } +} + +export const notificationStore = new NotificationStore(); diff --git a/src/lib/stores/pucc.svelte.ts b/src/lib/stores/pucc.svelte.ts deleted file mode 100644 index 7cbb6d5a..00000000 --- a/src/lib/stores/pucc.svelte.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { PollutionCertificate } from '$lib/domain'; -import { apiClient } from '$lib/helper/api.helper'; -import type { ApiResponse } from '$lib/response'; -import { vehicleStore } from './vehicle.svelte'; -import { onMount } from 'svelte'; - -class PuccStore { - pollutionCerts = $state(); - vehicleId = $derived(vehicleStore.selectedId); - selectedId = $state(); - processing = $state(false); - openSheet = $state(false); - editMode = $state(false); - error = $state(); - - refreshPuccs = () => { - if (!this.vehicleId) return; - this.processing = true; - apiClient - .get(`/vehicles/${this.vehicleId}/pucc`) - .then(({ data: res }) => { - this.pollutionCerts = res.data; - this.error = undefined; - }) - .catch((err) => (this.error = 'Failed to fetch PUCCs')) - .finally(() => (this.processing = false)); - }; - - openForm = (state: boolean, id?: string | null, vehicleId?: string) => { - this.openSheet = state; - if (id) { - this.selectedId = id; - this.vehicleId = vehicleId; - this.editMode = true; - } - }; -} - -export const puccStore = new PuccStore(); diff --git a/src/lib/stores/reminder.svelte.ts b/src/lib/stores/reminder.svelte.ts index f3a47b5e..4ad00bd9 100644 --- a/src/lib/stores/reminder.svelte.ts +++ b/src/lib/stores/reminder.svelte.ts @@ -1,34 +1,29 @@ -import type { Reminder } from '$lib/domain'; -import { apiClient } from '$lib/helper/api.helper'; -import type { ApiResponse } from '$lib/response'; -import { vehicleStore } from './vehicle.svelte'; +import type { Reminder } from '$lib/domain/reminder'; +import { createEntityStore } from './entity-store.svelte'; -class ReminderStore { - reminders = $state(); - processing = $state(false); - error = $state(); - - refreshReminders = async () => { - if (!vehicleStore.selectedId) return; - this.processing = true; - try { - const { data } = await apiClient.get( - `/vehicles/${vehicleStore.selectedId}/reminders` - ); - const reminders = - (data.data as Reminder[] | undefined)?.map((reminder) => ({ - ...reminder, - dueDate: new Date(reminder.dueDate) - })) || []; - reminders.sort((a, b) => a.dueDate.getTime() - b.dueDate.getTime()); - this.reminders = reminders; - this.error = undefined; - } catch (err) { - this.error = 'Failed to fetch reminders'; - } finally { - this.processing = false; - } - }; +function parseReminder(raw: unknown): Reminder { + const r = raw as Reminder & { dueDate: string }; + return { ...r, dueDate: new Date(r.dueDate) }; } -export const reminderStore = new ReminderStore(); +const entityStore = createEntityStore({ + buildPath: (vehicleId) => (vehicleId ? `/reminders?vehicleId=${vehicleId}` : '/reminders'), + map: parseReminder, + sort: (a, b) => a.dueDate.getTime() - b.dueDate.getTime(), + errorMessage: 'Failed to fetch reminders' +}); + +export const reminderStore = { + get reminders() { + return entityStore.items; + }, + get processing() { + return entityStore.processing; + }, + get error() { + return entityStore.error; + }, + refreshReminders: entityStore.refresh, + reloadReminders: entityStore.reload, + clear: entityStore.clear +}; diff --git a/src/lib/stores/theme.svelte.ts b/src/lib/stores/theme.svelte.ts index 7d816604..d7b92f6b 100644 --- a/src/lib/stores/theme.svelte.ts +++ b/src/lib/stores/theme.svelte.ts @@ -1,13 +1,22 @@ -import { getContext, setContext } from 'svelte'; -import { type ThemeName, type ThemeConfig } from '$lib/types/theme'; +import { type ThemeName, type ThemeConfig, type DarkVariant } from '$lib/types/theme'; import { themes } from '$lib/config/themes'; -import { getStoredTheme, saveTheme, applyThemeColors, setThemeClass } from '$lib/utils/theme'; +import { + getStoredTheme, + saveTheme, + applyThemeColors, + setThemeClass, + getStoredDarkVariant, + saveDarkVariant, + setDarkVariantAttr +} from '$lib/utils/theme'; -const THEME_KEY = Symbol('theme'); +const DARK_VARIANTS: DarkVariant[] = ['default', 'dim', 'oled']; interface ThemeStore { theme: ThemeName; + darkVariant: DarkVariant; setTheme: (name: ThemeName) => void; + setDarkVariant: (variant: DarkVariant) => void; getThemes: () => ThemeConfig[]; getActiveTheme: () => ThemeConfig | undefined; initializeTheme: () => void; @@ -15,6 +24,7 @@ interface ThemeStore { function createThemeStore(): ThemeStore { let theme = $state('slate'); + let darkVariant = $state('default'); let initialized = false; function applyTheme(name: ThemeName) { @@ -29,13 +39,14 @@ function createThemeStore(): ThemeStore { if (initialized || typeof window === 'undefined') return; const storedTheme = getStoredTheme(); - if (storedTheme && storedTheme in themes) { - theme = storedTheme; - } else { - theme = 'slate'; - } - + theme = storedTheme && storedTheme in themes ? storedTheme : 'slate'; applyTheme(theme); + + const storedVariant = getStoredDarkVariant(); + darkVariant = + storedVariant && DARK_VARIANTS.includes(storedVariant) ? storedVariant : 'default'; + setDarkVariantAttr(darkVariant); + initialized = true; } @@ -47,6 +58,14 @@ function createThemeStore(): ThemeStore { } } + function setDarkVariant(variant: DarkVariant) { + if (DARK_VARIANTS.includes(variant)) { + darkVariant = variant; + setDarkVariantAttr(variant); + saveDarkVariant(variant); + } + } + function getThemes(): ThemeConfig[] { return Object.values(themes); } @@ -62,7 +81,14 @@ function createThemeStore(): ThemeStore { set theme(value: ThemeName) { setTheme(value); }, + get darkVariant() { + return darkVariant; + }, + set darkVariant(value: DarkVariant) { + setDarkVariant(value); + }, setTheme, + setDarkVariant, getThemes, getActiveTheme, initializeTheme @@ -70,12 +96,3 @@ function createThemeStore(): ThemeStore { } export const themeStore = createThemeStore(); - -// Context helpers for components -export function setThemeContext(store: ThemeStore) { - return setContext(THEME_KEY, store); -} - -export function getThemeContext(): ThemeStore { - return getContext(THEME_KEY); -} diff --git a/src/lib/stores/vehicle.svelte.ts b/src/lib/stores/vehicle.svelte.ts index 6bf18945..3000ff3f 100644 --- a/src/lib/stores/vehicle.svelte.ts +++ b/src/lib/stores/vehicle.svelte.ts @@ -1,49 +1,27 @@ -import type { Vehicle } from '$lib/domain'; +import type { Vehicle } from '$lib/domain/vehicle'; import { apiClient } from '$lib/helper/api.helper'; import type { ApiResponse } from '$lib/response'; -import { toast } from 'svelte-sonner'; class VehicleStore { vehicles = $state(); - selectedId = $state(); processing = $state(false); - openSheet = $state(false); - editMode = $state(false); error = $state(); + setVehicles = (vehicles: Vehicle[]) => { + this.vehicles = vehicles; + }; + refreshVehicles = () => { this.processing = true; apiClient .get('/vehicles') .then(({ data: res }) => { this.vehicles = res.data; - if (this.vehicles && this.vehicles.length > 0) { - this.selectedId = this.vehicles[0].id || undefined; - } else { - this.selectedId = undefined; - } this.error = undefined; }) - .catch((err) => (this.error = 'Failed to fetch vehicles')) + .catch(() => (this.error = 'Failed to fetch vehicles')) .finally(() => (this.processing = false)); }; - - openForm = (id?: string) => { - this.openSheet = true; - if (id) { - const vehicle = this.vehicles?.find((v) => v.id == id); - if (vehicle) { - this.selectedId = id; - this.editMode = true; - } - } - }; - - closeForm = () => { - this.openSheet = false; - this.editMode = false; - this.selectedId = undefined; - }; } export const vehicleStore = new VehicleStore(); diff --git a/src/lib/types/settings.ts b/src/lib/types/settings.ts index 862db3b7..2f568516 100644 --- a/src/lib/types/settings.ts +++ b/src/lib/types/settings.ts @@ -1,25 +1,3 @@ -export interface SettingsFormShape extends Record { - dateFormat: string; - locale: string; - timezone: string; - currency: string; - unitOfDistance: 'kilometer' | 'mile'; - unitOfVolume: 'liter' | 'gallon'; - unitOfLpg: 'liter' | 'gallon' | 'kilogram' | 'pound'; - unitOfCng: 'liter' | 'gallon' | 'kilogram' | 'pound'; - mileageUnitFormat: 'distance-per-fuel' | 'fuel-per-distance' | 'uk-mpg'; - theme: string; - customCss?: string; - featureFuelLog: boolean; - featureMaintenance: boolean; - featurePucc: boolean; - featureReminders: boolean; - featureInsurance: boolean; - featureOverview: boolean; - notificationProcessingEnabled?: boolean; - notificationProcessingSchedule?: string; -} - export interface SettingsOption { value: string; label: string; diff --git a/src/lib/types/theme.ts b/src/lib/types/theme.ts index 8afccfc0..461bcd57 100644 --- a/src/lib/types/theme.ts +++ b/src/lib/types/theme.ts @@ -12,6 +12,9 @@ export type ThemeName = | 'indigo' | 'pink'; +/** Dark-mode contrast style, independent of the accent color chosen via ThemeName. */ +export type DarkVariant = 'default' | 'dim' | 'oled'; + export interface ThemeColors { background?: string; foreground?: string; @@ -41,9 +44,3 @@ export interface ThemeConfig { colors?: ThemeColors; darkColors?: ThemeColors; } - -export interface ThemeContextValue { - theme: ThemeName; - themes: ThemeConfig[]; - setTheme: (theme: ThemeName) => void; -} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d89395e4..9a02d407 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -36,7 +36,7 @@ const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur', 'yi']; * @param locale - Language/locale code (e.g., 'ar', 'en') * @returns true if the language is RTL */ -export function isRtlLanguage(locale: string): boolean { +function isRtlLanguage(locale: string): boolean { return RTL_LANGUAGES.includes(locale.toLowerCase().split('-')[0]); } diff --git a/src/lib/utils/theme.ts b/src/lib/utils/theme.ts index 43d49ed6..366145e1 100644 --- a/src/lib/utils/theme.ts +++ b/src/lib/utils/theme.ts @@ -1,6 +1,7 @@ -import type { ThemeName, ThemeConfig } from '$lib/types/theme'; +import type { ThemeName, ThemeConfig, DarkVariant } from '$lib/types/theme'; const THEME_STORAGE_KEY = 'tracktor-theme'; +const DARK_VARIANT_STORAGE_KEY = 'tracktor-dark-variant'; /** * Get the theme from localStorage @@ -22,7 +23,7 @@ export function saveTheme(theme: ThemeName): void { /** * Check if dark mode is currently active */ -export function isDarkMode(): boolean { +function isDarkMode(): boolean { if (typeof document === 'undefined') return false; return document.documentElement.classList.contains('dark'); } @@ -59,31 +60,34 @@ export function applyThemeColors( } /** - * Reset theme to default (remove all custom theme variables) + * Add or remove theme class from HTML element */ -export function resetThemeColors(): void { +export function setThemeClass(theme: ThemeName): void { if (typeof document === 'undefined') return; const root = document.documentElement; - const customProps = ['primary', 'primary-foreground', 'ring']; + root.setAttribute('data-theme', theme); +} - customProps.forEach((prop) => { - root.style.removeProperty(`--${prop}`); - }); +/** + * Get the dark mode variant from localStorage + */ +export function getStoredDarkVariant(): DarkVariant | null { + if (typeof window === 'undefined') return null; + return (localStorage.getItem(DARK_VARIANT_STORAGE_KEY) as DarkVariant) || null; } /** - * Add or remove theme class from HTML element + * Save the dark mode variant to localStorage */ -export function setThemeClass(theme: ThemeName): void { - if (typeof document === 'undefined') return; - const root = document.documentElement; - root.setAttribute('data-theme', theme); +export function saveDarkVariant(variant: DarkVariant): void { + if (typeof window === 'undefined') return; + localStorage.setItem(DARK_VARIANT_STORAGE_KEY, variant); } /** - * Get the current system theme preference + * Apply the dark mode variant attribute, read by the `.dark[data-dark-variant=...]` CSS overrides */ -export function getSystemTheme(): 'light' | 'dark' { - if (typeof window === 'undefined') return 'light'; - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +export function setDarkVariantAttr(variant: DarkVariant): void { + if (typeof document === 'undefined') return; + document.documentElement.setAttribute('data-dark-variant', variant); } diff --git a/src/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts new file mode 100644 index 00000000..44cbdb17 --- /dev/null +++ b/src/routes/(app)/+layout.server.ts @@ -0,0 +1,50 @@ +import { redirect } from '@sveltejs/kit'; +import type { LayoutServerLoad } from './$types'; +import { getAppConfigs } from '$server/services/configService'; +import { getAllVehicles } from '$server/services/vehicleService'; +import { validateSession } from '$server/services/authService'; +import { env } from '$lib/config/env.server'; + +export const load: LayoutServerLoad = async ({ cookies }) => { + const isAuthDisabled = env.DISABLE_AUTH; + let user = null; + + if (!isAuthDisabled) { + const sessionToken = cookies.get('session'); + if (sessionToken) { + try { + const sessionResult = await validateSession(sessionToken); + user = sessionResult.user; + } catch { + // Ignore invalid session + } + } + + if (!user) { + throw redirect(307, '/login'); + } + } + + // Fetch configs and vehicles + const [configs, vehicles] = await Promise.all([getAppConfigs(), getAllVehicles()]); + + const rawConfigs = (configs || []).map((c) => ({ + ...c, + description: c.description ?? undefined + })); + const configsMap: Record = {}; + if (Array.isArray(rawConfigs)) { + rawConfigs.forEach((item: { key: string; value?: string }) => { + if (item.key.startsWith('feature')) { + configsMap[item.key] = item.value === 'true'; + } + }); + } + + return { + user, + rawConfigs, + configs: configsMap, + vehicles: vehicles || [] + }; +}; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte new file mode 100644 index 00000000..3fba12af --- /dev/null +++ b/src/routes/(app)/+layout.svelte @@ -0,0 +1,58 @@ + + + + + +
+ + {#if demoMode} +
+ + {demo_banner()} +
+ {/if} +
+ +
+
+
+ {@render children()} +
+
+
+ + diff --git a/src/routes/(app)/compliance/+page.svelte b/src/routes/(app)/compliance/+page.svelte new file mode 100644 index 00000000..6981215e --- /dev/null +++ b/src/routes/(app)/compliance/+page.svelte @@ -0,0 +1,177 @@ + + + + {#snippet actions()} + + {/snippet} + + {#if isEmpty} +
+ + + +
+

{m.compliance_cta_heading()}

+

{m.compliance_list_empty()}

+
+ +
+ {:else} +
+ + + + +
+ +
+ + + {typeOptionLabel} + + {#each typeOptions as option (option.id)} + {option.label} + {/each} + + + + {statusOptionLabel} + + {#each statusOptions as option (option.id)} + {option.label} + {/each} + + +
+ + + {/if} +
diff --git a/src/routes/(app)/dashboard/+page.svelte b/src/routes/(app)/dashboard/+page.svelte new file mode 100644 index 00000000..e84be8fc --- /dev/null +++ b/src/routes/(app)/dashboard/+page.svelte @@ -0,0 +1,96 @@ + + +
+ + + + + + + { + dashboardLayoutStore.resetToDefault(); + resetDialogOpen = false; + }} + /> + + dashboardLayoutStore.commitLayout(items)} + > + {#snippet children(item)} + {@const def = WIDGET_REGISTRY[item.type]} + dashboardLayoutStore.removeWidget(item.id)} + > + + + {/snippet} + + + + goto('/reports')} + /> +
diff --git a/src/routes/(app)/dashboard/+page.ts b/src/routes/(app)/dashboard/+page.ts new file mode 100644 index 00000000..c9a9cf41 --- /dev/null +++ b/src/routes/(app)/dashboard/+page.ts @@ -0,0 +1,5 @@ +import type { PageLoad } from './$types'; + +export const load: PageLoad = async () => { + return {}; +}; diff --git a/src/routes/(app)/expenses/+page.server.ts b/src/routes/(app)/expenses/+page.server.ts new file mode 100644 index 00000000..ebcc876b --- /dev/null +++ b/src/routes/(app)/expenses/+page.server.ts @@ -0,0 +1,6 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + throw redirect(308, '/reports'); +}; diff --git a/src/routes/(app)/fuel/+page.svelte b/src/routes/(app)/fuel/+page.svelte new file mode 100644 index 00000000..a4365f31 --- /dev/null +++ b/src/routes/(app)/fuel/+page.svelte @@ -0,0 +1,93 @@ + + + + {#snippet children(_scope)} +
+ 0 ? `${totalFuelUsed.toFixed(1)} L` : '--'} + color={ACCENT.moss.gradient} + /> + 0 ? `$${totalCost.toFixed(2)}` : '--'} + color={ACCENT.ochre.gradient} + /> + 0 ? avgMileage.toFixed(1) : '--'} + color={ACCENT.denim.gradient} + /> + +
+ +
+ `${value.toFixed(1)} L`} + xFormatter={(v: Date) => + v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })} + /> + formatMileage(value, series.fuelType)} + xFormatter={(v: Date) => + v.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' })} + /> +
+ + + {/snippet} +
diff --git a/src/routes/(app)/garage/+page.svelte b/src/routes/(app)/garage/+page.svelte new file mode 100644 index 00000000..78f8a337 --- /dev/null +++ b/src/routes/(app)/garage/+page.svelte @@ -0,0 +1,49 @@ + + +
+ + + {#if vehicleStore.processing} + + {:else if vehicleStore.vehicles && vehicleStore.vehicles.length > 0} +
+ {#each vehicleStore.vehicles as vehicle (vehicle.id)} + { + if (vehicle.id) goto(`/garage/${vehicle.id}`); + }} + onkeydown={() => {}} + /> + {/each} + +
+ {:else} + + {/if} +
diff --git a/src/routes/(app)/garage/[id]/+page.server.ts b/src/routes/(app)/garage/[id]/+page.server.ts new file mode 100644 index 00000000..f42e6ea8 --- /dev/null +++ b/src/routes/(app)/garage/[id]/+page.server.ts @@ -0,0 +1,16 @@ +import { error } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { getVehicleSummary } from '$server/services/vehicleService'; +import { AppError } from '$server/exceptions/AppError'; + +export const load: PageServerLoad = async ({ params }) => { + try { + const vehicle = await getVehicleSummary(params.id); + return { vehicle }; + } catch (err) { + if (err instanceof AppError && err.status === 404) { + throw error(404, 'Vehicle not found'); + } + throw err; + } +}; diff --git a/src/routes/(app)/garage/[id]/+page.svelte b/src/routes/(app)/garage/[id]/+page.svelte new file mode 100644 index 00000000..f4d8f310 --- /dev/null +++ b/src/routes/(app)/garage/[id]/+page.svelte @@ -0,0 +1,82 @@ + + +
+
+ + +
+ + +
+
+ + + +
+
+ + +
+ + +
+
+ + diff --git a/src/routes/(app)/maintenance/+page.svelte b/src/routes/(app)/maintenance/+page.svelte new file mode 100644 index 00000000..fd028663 --- /dev/null +++ b/src/routes/(app)/maintenance/+page.svelte @@ -0,0 +1,149 @@ + + + + {#if scope.isFleet} +
+ 0 ? totalServices : '--'} + color={ACCENT.moss.gradient} + /> + 0 ? formatCurrency(totalSpent) : '--'} + color={ACCENT.ochre.gradient} + /> + +
+ {:else} +
+ + + 0 ? `${currentOdometer.toLocaleString()} km` : '--'} + color={ACCENT.denim.gradient} + /> +
+ {/if} + + + + {maintenance_tab_overview()} + {maintenance_tab_history()} + + + +
+ +
+
+ + + + +
+
diff --git a/src/routes/(app)/reminders/+page.svelte b/src/routes/(app)/reminders/+page.svelte new file mode 100644 index 00000000..e1fbb042 --- /dev/null +++ b/src/routes/(app)/reminders/+page.svelte @@ -0,0 +1,424 @@ + + + + {#snippet actions()} + + {/snippet} + + {#if isEmpty} +
+ + + +
+

{m.reminder_empty_title()}

+

{m.reminder_list_empty()}

+
+ +
+ {:else} +
+ + + + +
+ +
+
+
+

+ {selectedDateLabel + ? m.reminder_list_title_for_date({ date: selectedDateLabel }) + : m.reminder_list_title()} +

+
+ + + + + {typeOptionLabel} + + + + {#each typeOptions as option (option.id)} + {option.label} + {/each} + + + {#if selectedDateKey} + + {/if} +
+
+ + {#if reminderStore.processing && upcomingReminders.length === 0 && doneReminders.length === 0} +

{m.common_loading()}

+ {:else if upcomingReminders.length === 0 && doneReminders.length === 0} +

+ {selectedDateKey ? m.reminder_calendar_empty_day() : m.reminder_list_empty()} +

+ {:else} +
+ {#if upcomingReminders.length > 0} +
+

+ {m.reminder_section_upcoming()} ({upcomingReminders.length}) +

+
    + {#each upcomingReminders as reminder (reminderKey(reminder))} + {@render reminderRow(reminder, false)} + {/each} +
+
+ {/if} + {#if doneReminders.length > 0} +
+

+ {m.reminder_section_marked_done()} ({doneReminders.length}) +

+
    + {#each doneReminders as reminder (reminderKey(reminder))} + {@render reminderRow(reminder, true)} + {/each} +
+
+ {/if} +
+ {/if} +
+ +
+
+

{m.reminder_calendar_title()}

+ +
+ +
+

{m.reminder_quick_actions_title()}

+
+ + + +
+
+
+
+ {/if} +
+ +{#snippet reminderRow(reminder: Reminder, done: boolean)} + {@const typeStyle = TYPE_STYLES[reminder.type]} + {@const info = statusFor(reminder)} +
  • +
    + + + +
    +
    +

    {getReminderTypeLabel(reminder.type, m)}

    + +
    +

    {metaFor(reminder)}

    +
    +
    +

    + {formatDate(reminder.dueDate)} + {#if info.sub} + ({info.sub}) + {/if} +

    + +
    + {#if !done} + + {/if} + +
    +
    +
    +
  • +{/snippet} + + diff --git a/src/routes/(app)/reports/+page.svelte b/src/routes/(app)/reports/+page.svelte new file mode 100644 index 00000000..3558457a --- /dev/null +++ b/src/routes/(app)/reports/+page.svelte @@ -0,0 +1,124 @@ + + + + {#snippet children(scope)} + {@const costs = scopedCosts(scope)} +
    +

    {reports_section_costs()}

    + +
    + + + +
    + +
    +
    + +
    +
    + {#if scope.isFleet} + + {:else} + + {/if} +
    +
    +
    + +
    +

    {reports_section_details()}

    + +
    + {/snippet} +
    diff --git a/src/routes/(app)/settings/+page.svelte b/src/routes/(app)/settings/+page.svelte new file mode 100644 index 00000000..11f47771 --- /dev/null +++ b/src/routes/(app)/settings/+page.svelte @@ -0,0 +1,427 @@ + + +
    + + + +
    + + + + +
    + e.preventDefault()}> +
    + + + + + + + + + + + + + + + + + + + + + + + + + + {#if hasErrors} +
    +

    + {m.settings_error_fix_errors()} +

    +
      + {#each errorEntries as [field, errorArray]} +
    • + {field.replace(/([A-Z])/g, ' $1').trim()}: + {errorArray.join(', ')} +
    • + {/each} +
    +
    + {/if} +
    + + +
    +
    + +
    + + + {m.settings_update_button()} + +
    +
    +

    + + {m.settings_secure_note()} +

    +
    + +
    +
    +
    diff --git a/src/routes/(auth)/+layout.svelte b/src/routes/(auth)/+layout.svelte index c09c502d..c1dd137a 100644 --- a/src/routes/(auth)/+layout.svelte +++ b/src/routes/(auth)/+layout.svelte @@ -1,24 +1,36 @@
    + {#if env.DEMO_MODE} +
    + + {m.demo_banner()} {m.default_login()} +
    + {/if}