diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 6ea65da..fb9156b 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -47,14 +47,6 @@ jobs: version: 11.1.3 run_install: false - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown - - - name: Install Trunk - run: cargo install trunk --version 0.22.0-beta.1 --locked - - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/CLAUDE.md b/CLAUDE.md index 83ee5f9..8733bbe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ Built with **pnpm workspace monorepo** with 3 services and 1 shared library: |---------|------|-------------| | `discord-moderation-backend` | `services/backend` | Express HTTP/WS server, REST API, Redis bridge | | `@bete/discord-gateway` | `services/discord-gateway` | Discord client, voice recording, message capture, AI moderation | -| `frontend` | `services/frontend/frontend` | Leptos 0.7 CSR WASM dashboard | +| `frontend` | `services/frontend` | Next.js 16 (React 19) static dashboard, Tailwind v4, shadcn/ui | | `@bete/shared` | `packages/shared` | Shared types, errors, logger, utilities | **Database:** PostgreSQL (Drizzle ORM) — NOT SQLite. @@ -28,7 +28,7 @@ Discord | v discord-gateway ---- Redis ---- backend ---- WebSocket ---- frontend - | pub/sub (broadcast) (Leptos WASM) + | pub/sub (broadcast) (Next.js static) | | | | <------------------+ @@ -194,43 +194,22 @@ The core service that connects to Discord using `discord.js-selfbot-v13`. - `src/shared/database/voiceRecordingRepo.ts` — Voice recording queries - `src/shared/discord/clientOptions.ts` — Discord client configuration -### frontend (`services/frontend/frontend`) +### frontend (`services/frontend`) -Leptos 0.7 CSR WASM + TypeScript + CSS dashboard. +Next.js 16 (React 19) static export dashboard, built with TypeScript + Tailwind v4 + shadcn/ui + base-ui. **Tech stack:** -- Leptos 0.7 CSR (WASM via `#[wasm_bindgen(start)]`) -- Trunk for bundling -- Plain CSS with design tokens -- Canvas 2D via `web-sys` for audio visualization -- CSS animations (GSAP/Framer Motion dihapus — unused) -- `web-sys` primitives (WebSocket, AudioContext, IntersectionObserver) -- `lucide-leptos` 3 for icons +- Next.js 16 (App Router, static export) +- React 19 with React Compiler +- TypeScript strict +- Tailwind v4 + shadcn/ui + base-ui components +- lucide-react icons -**Feature structure (entity + feature slices):** -- `entities/` — Type exports re-exported from shared API client - - `guild/types.ts` — Guild, Channel - - `message/types.ts` — MessageRecord, PageResult - - `voice/types.ts` — ActiveSpeaker, VoiceStatus - - `media/types.ts` — MediaItem, MediaMode, MediaState - - `ui/types.ts` — UIState, DashboardTab -- `features/` - - `live/` — Voice connection, music player, screenshare, recordings - - Components: ActiveSpeakers, AudioVisualizer, MusicSubPanel, NowPlaying, RecordingsSubPanel, ScreenSubPanel, VoiceConnectionCard - - Hooks: `useVoiceControl`, `useMediaControl` - - `messages/` — Message list with filters - - Hooks: `useMessages` -- `shared/` - - `api/client.ts` — All HTTP API calls + types - - `ws/socket.ts` — WebSocket singleton with `useDashboardSocket` hook - - `ws/events.ts` — Typed event map - - `hooks/` — useAudioPlayback, useAudioTransmit, useUIState, useMascotChat, useMascotSummary, useFramerStagger, useGsapTransition, useLocalStorage - - `ui/` — Reusable UI components (Badge, Button, Card, Input, Select, Skeleton, Tabs, Toast, ScrollArea) - - `lib/utils.ts` — `cn()` and other utilities - -**WebSocket protocol:** -- Binary: PCM audio data (24kHz mono s16le) -- JSON events: message_*, voice_*, attachment_*, user_state, ui_state, media_state, heartbeat +**Feature structure:** +- `src/app/` — App Router pages (login, dashboard with tabs) +- `src/features/` — Feature components (dashboard, messages, live, mascot) +- `src/lib/` — Shared utilities (types, API client, WebSocket, hooks) +- `src/components/` — Shared UI components (layout, ui) ### shared (`packages/shared`) @@ -388,16 +367,16 @@ pnpm install # Run each service in development mode (separate terminal each) pnpm run dev:backend # Backend on port 3001 pnpm run dev:discord-gateway # Discord client + all features -pnpm run dev:web # Frontend via trunk serve +pnpm run dev:web # Frontend via next dev (port 3000) # Build pnpm run build:backend pnpm run build:discord-gateway -pnpm run build:web # trunk build --release +pnpm run build:web # next build (static export) # Type checking pnpm run typecheck # Node services (pnpm -r) -pnpm run typecheck:web # Leptos frontend (cargo check) +pnpm run typecheck:web # Frontend typecheck (next build) # Lint (Biome) pnpm run lint diff --git a/deploy.sh b/deploy.sh index 7bb1fd7..bb86cbf 100755 --- a/deploy.sh +++ b/deploy.sh @@ -9,7 +9,7 @@ # # Usage: # ./deploy.sh # build + deploy all services -# ./deploy.sh --frontend # frontend WASM only +# ./deploy.sh --frontend # frontend (Next.js) only # ./deploy.sh --backend # backend JS only # ./deploy.sh --gateway # discord-gateway JS only # ./deploy.sh --all # same as no-flag (default) @@ -32,7 +32,7 @@ APP_DIR="/opt/imphenbot" COMPOSE_FILE="infra/docker/docker-compose.yml" # Local build output directories (relative to repo root) -FRONTEND_DIST="services/frontend/frontend/dist" +FRONTEND_DIST="services/frontend/out" BACKEND_DIST="services/backend/dist" GATEWAY_DIST="services/discord-gateway/dist" @@ -118,9 +118,9 @@ if $DO_BUILD; then fi if $DO_FRONTEND; then - log "Building frontend (WASM)..." - cd services/frontend/frontend - trunk build --release 2>&1 | tail -5 || die "Frontend build failed" + log "Building frontend (Next.js)..." + cd services/frontend + bun run build 2>&1 | tail -5 || die "Frontend build failed" cd "$REPO_ROOT" ok "Frontend built" fi diff --git a/package.json b/package.json index 034ea7b..1464c69 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "scripts": { "dev:backend": "pnpm --filter './services/backend' run dev", "dev:discord-gateway": "pnpm --filter './services/discord-gateway' run dev", - "dev:web": "cd services/frontend/frontend && trunk serve --address 0.0.0.0", - "build:web": "cd services/frontend/frontend && trunk build --release", - "typecheck:web": "cd services/frontend/frontend && cargo check", + "dev:web": "pnpm --filter './services/frontend' run dev", + "build:web": "pnpm --filter './services/frontend' run build", + "typecheck:web": "cd services/frontend && bun run build", "build:backend": "pnpm --filter './services/backend' run build", "build:discord-gateway": "pnpm --filter './services/discord-gateway' run build", "typecheck": "pnpm -r run typecheck", diff --git a/services/frontend/.env.example b/services/frontend/.env.example deleted file mode 100644 index 3634e80..0000000 --- a/services/frontend/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -# API/WS endpoints — set these before building -VITE_BE_API_URL=http://localhost:3001 -VITE_BE_WS_URL=ws://localhost:3001/ws diff --git a/services/frontend/.gitignore b/services/frontend/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/services/frontend/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/services/frontend/AGENTS.md b/services/frontend/AGENTS.md new file mode 100644 index 0000000..8bd0e39 --- /dev/null +++ b/services/frontend/AGENTS.md @@ -0,0 +1,5 @@ + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices. + diff --git a/services/frontend/CLAUDE.md b/services/frontend/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/services/frontend/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/services/frontend/Cargo.lock b/services/frontend/Cargo.lock deleted file mode 100644 index 3407567..0000000 --- a/services/frontend/Cargo.lock +++ /dev/null @@ -1,2557 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "any_spawner" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1384d3fe1eecb464229fcf6eebb72306591c56bf27b373561489458a7c73027d" -dependencies = [ - "futures", - "thiserror 2.0.18", - "wasm-bindgen-futures", -] - -[[package]] -name = "anyhow" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-once-cell" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "attribute-derive" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" -dependencies = [ - "attribute-derive-macro", - "derive-where", - "manyhow", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "attribute-derive-macro" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" -dependencies = [ - "collection_literals", - "interpolator", - "manyhow", - "proc-macro-utils", - "proc-macro2", - "quote", - "quote-use", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base16" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytes" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" - -[[package]] -name = "camino" -version = "1.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0" - -[[package]] -name = "cc" -version = "1.2.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "codee" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9dbbdc4b4d349732bc6690de10a9de952bd39ba6a065c586e26600b6b0b91f5" -dependencies = [ - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "collection_literals" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "config" -version = "0.15.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b85f248a4de22d204ceabc6299d89d2c70fbd7f09fea53c06c852369652d8139" -dependencies = [ - "convert_case 0.6.0", - "pathdiff", - "serde_core", - "toml", - "winnow", -] - -[[package]] -name = "console_error_panic_hook" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" -dependencies = [ - "cfg-if", - "wasm-bindgen", -] - -[[package]] -name = "const-str" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" - -[[package]] -name = "const_format" -version = "0.2.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" -dependencies = [ - "const_format_proc_macros", - "konst", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "const_str_slice_concat" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67855af358fcb20fac58f9d714c94e2b228fe5694c1c9b4ead4a366343eda1b" - -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "convert_case" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "convert_case_extras" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589c70f0faf8aa9d17787557d5eae854d7755cac50f5c3d12c81d3d57661cebb" -dependencies = [ - "convert_case 0.11.0", -] - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "default-struct-builder" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0df63c21a4383f94bd5388564829423f35c316aed85dc4f8427aded372c7c0d" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" - -[[package]] -name = "derive-where" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "displaydoc" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "drain_filter_polyfill" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" - -[[package]] -name = "either" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" - -[[package]] -name = "either_of" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5060e0a4cbf26a87550792688ade88e6b8aec9208613631a7a363bda7bc2d4cd" -dependencies = [ - "paste", - "pin-project-lite", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1731451909bde27714eacba19c2566362a7f35224f52b153d3f42cf60f72472" - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "frontend" -version = "0.1.0" -dependencies = [ - "console_error_panic_hook", - "gloo-net", - "gloo-timers 0.3.0", - "js-sys", - "leptos 0.9.0-alpha", - "leptos-use", - "lucide-leptos", - "regex", - "serde", - "serde-wasm-bindgen", - "serde_json", - "shared-types", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-logger", - "web-sys", -] - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasm-bindgen", -] - -[[package]] -name = "gloo-net" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" -dependencies = [ - "futures-channel", - "futures-core", - "futures-sink", - "gloo-utils", - "http", - "js-sys", - "pin-project", - "serde", - "serde_json", - "thiserror 1.0.69", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "gloo-timers" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "482ce8a491a501da4cd806bd190275363d674f2845005c6ddbd5d3e1dd54495d" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "gloo-utils" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" -dependencies = [ - "js-sys", - "serde", - "serde_json", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "guardian" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f" - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "html-escape" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" -dependencies = [ - "utf8-width", -] - -[[package]] -name = "http" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "hydration_context" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bbbeb23ee808258cef2c5585ff0dc8e41da21a8dde943f6b290da153a042a96" -dependencies = [ - "futures", - "or_poisoned", - "pin-project-lite", - "serde", - "throw_error", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "interpolator" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "konst" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" -dependencies = [ - "konst_macro_rules", -] - -[[package]] -name = "konst_macro_rules" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leptos" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "705e2951f3688e0c4f66bbb7a2702282782dcee716971dbd6209c2619d272479" -dependencies = [ - "any_spawner", - "cfg-if", - "either_of", - "futures", - "hydration_context", - "leptos_config 0.8.10", - "leptos_dom 0.8.8", - "leptos_hot_reload 0.8.6", - "leptos_macro 0.8.17", - "leptos_server 0.8.7", - "oco_ref", - "or_poisoned", - "paste", - "reactive_graph 0.2.14", - "rustc-hash", - "rustc_version", - "send_wrapper", - "serde", - "serde_json", - "serde_qs 0.15.0", - "server_fn 0.8.13", - "slotmap", - "tachys 0.2.18", - "thiserror 2.0.18", - "throw_error", - "typed-builder", - "typed-builder-macro", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm_split_helpers", - "web-sys", -] - -[[package]] -name = "leptos" -version = "0.9.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e1a74bae96dc8bedda6bab9dbf3c88219c1620d98e185b3fd8a9234669d2d4" -dependencies = [ - "any_spawner", - "cfg-if", - "either_of", - "futures", - "getrandom", - "hydration_context", - "leptos_config 0.9.0-alpha", - "leptos_dom 0.9.0-alpha", - "leptos_hot_reload 0.9.0-alpha", - "leptos_macro 0.9.0-alpha", - "leptos_server 0.9.0-alpha", - "oco_ref", - "or_poisoned", - "paste", - "reactive_graph 0.3.0-alpha", - "rustc-hash", - "rustc_version", - "send_wrapper", - "serde", - "serde_json", - "serde_qs 1.1.2", - "server_fn 0.9.0-alpha", - "slotmap", - "tachys 0.3.0-alpha", - "thiserror 2.0.18", - "throw_error", - "typed-builder", - "typed-builder-macro", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm_split_helpers", - "web-sys", -] - -[[package]] -name = "leptos-use" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc07f2657dcc366ef2d80045354143f6e0a86919763a1a5753cd3871f8f95e2d" -dependencies = [ - "cfg-if", - "chrono", - "codee", - "cookie", - "default-struct-builder", - "futures-util", - "gloo-timers 0.4.0", - "js-sys", - "lazy_static", - "leptos 0.8.20", - "paste", - "send_wrapper", - "thiserror 2.0.18", - "unic-langid", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "leptos_config" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c06f751315bccc0d193fab302ac01d25bcfcd97474d4676440e7e3250dc3fc3" -dependencies = [ - "config", - "regex", - "serde", - "thiserror 2.0.18", - "typed-builder", -] - -[[package]] -name = "leptos_config" -version = "0.9.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c772a8935361c148107777535dfc1d5000c37648cbac94460f96b29d843d4b8" -dependencies = [ - "config", - "serde", - "thiserror 2.0.18", - "typed-builder", -] - -[[package]] -name = "leptos_dom" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35742e9ed8f8aaf9e549b454c68a7ac0992536e06856365639b111f72ab07884" -dependencies = [ - "js-sys", - "or_poisoned", - "reactive_graph 0.2.14", - "send_wrapper", - "tachys 0.2.18", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "leptos_dom" -version = "0.9.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe77de5164845764352e66a5fcb259abba75fbfa50c77519c35242f0bf3f2ad" -dependencies = [ - "js-sys", - "or_poisoned", - "reactive_graph 0.3.0-alpha", - "send_wrapper", - "tachys 0.3.0-alpha", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "leptos_hot_reload" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d2a0f220c8a5ef3c51199dfb9cdd702bc0eb80d52fbe70c7890adfaaae8a4b1" -dependencies = [ - "anyhow", - "camino", - "indexmap", - "or_poisoned", - "proc-macro2", - "quote", - "rstml", - "serde", - "syn", - "walkdir", -] - -[[package]] -name = "leptos_hot_reload" -version = "0.9.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "466b340e7447a30373f5aa81b1f2f926e385a3d2d6e073644ae6f24c6eba9661" -dependencies = [ - "anyhow", - "camino", - "indexmap", - "or_poisoned", - "proc-macro2", - "quote", - "rstml", - "serde", - "syn", - "walkdir", -] - -[[package]] -name = "leptos_macro" -version = "0.8.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de6e8da9d4f1a7b74b447b317d590ebabb38709f588f7ee20564b773ccbcce" -dependencies = [ - "attribute-derive", - "cfg-if", - "convert_case 0.11.0", - "convert_case_extras", - "html-escape", - "itertools", - "leptos_hot_reload 0.8.6", - "prettyplease", - "proc-macro-error2", - "proc-macro2", - "quote", - "rstml", - "rustc_version", - "server_fn_macro 0.8.10", - "syn", - "uuid", -] - -[[package]] -name = "leptos_macro" -version = "0.9.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51cac3715644ebff86b2f48a07e865e7bb1b80acfb15fa2977779110e2234779" -dependencies = [ - "attribute-derive", - "cfg-if", - "convert_case 0.11.0", - "convert_case_extras", - "html-escape", - "itertools", - "leptos_hot_reload 0.9.0-alpha", - "prettyplease", - "proc-macro-error2", - "proc-macro2", - "quote", - "rstml", - "rustc_version", - "server_fn_macro 0.9.0-alpha", - "syn", - "uuid", -] - -[[package]] -name = "leptos_server" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da974775c5ccbb6bd64be7f53f75e8321542e28f21563a416574dbe4d5447eae" -dependencies = [ - "any_spawner", - "base64", - "codee", - "futures", - "hydration_context", - "or_poisoned", - "reactive_graph 0.2.14", - "send_wrapper", - "serde", - "serde_json", - "server_fn 0.8.13", - "tachys 0.2.18", -] - -[[package]] -name = "leptos_server" -version = "0.9.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "481a884e7faaaee5f67f46585606de63337b3cbe0425e41428035c6d422b3459" -dependencies = [ - "any_spawner", - "base64", - "codee", - "futures", - "hydration_context", - "or_poisoned", - "reactive_graph 0.3.0-alpha", - "send_wrapper", - "serde", - "serde_json", - "server_fn 0.9.0-alpha", - "tachys 0.3.0-alpha", -] - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "lucide-leptos" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9cd884208fcbdffa621eb2b867e51a1ef9d016c599ccb195195aedf42fb29c1" -dependencies = [ - "leptos 0.8.20", -] - -[[package]] -name = "manyhow" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" -dependencies = [ - "manyhow-macros", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "manyhow-macros" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" -dependencies = [ - "proc-macro-utils", - "proc-macro2", - "quote", -] - -[[package]] -name = "memchr" -version = "2.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" - -[[package]] -name = "next_tuple" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60993920e071b0c9b66f14e2b32740a4e27ffc82854dcd72035887f336a09a28" - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "oco_ref" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0423ff9973dea4d6bd075934fdda86ebb8c05bdf9d6b0507067d4a1226371d" -dependencies = [ - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "or_poisoned" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c04f5d74368e4d0dfe06c45c8627c81bd7c317d52762d118fb9b3076f6420fd" - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "proc-macro-utils" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" -dependencies = [ - "proc-macro2", - "quote", - "smallvec", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "version_check", - "yansi", -] - -[[package]] -name = "quote" -version = "1.0.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "quote-use" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" -dependencies = [ - "quote", - "quote-use-macros", -] - -[[package]] -name = "quote-use-macros" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" -dependencies = [ - "proc-macro-utils", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "reactive_graph" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c5a025366836190c7030e883cc2bcd9e384ff555336e3c7954741ca411b177" -dependencies = [ - "any_spawner", - "async-lock", - "futures", - "guardian", - "hydration_context", - "indexmap", - "or_poisoned", - "paste", - "pin-project-lite", - "rustc-hash", - "rustc_version", - "send_wrapper", - "serde", - "slotmap", - "thiserror 2.0.18", - "web-sys", -] - -[[package]] -name = "reactive_graph" -version = "0.3.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a021bc1b2f273fa8d9b56e290b13b571232fc1322c757245b0e72690f4415791" -dependencies = [ - "any_spawner", - "async-lock", - "futures", - "guardian", - "hydration_context", - "indexmap", - "or_poisoned", - "paste", - "pin-project-lite", - "rustc-hash", - "rustc_version", - "send_wrapper", - "serde", - "slotmap", - "thiserror 2.0.18", - "web-sys", -] - -[[package]] -name = "reactive_stores" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c30fd35b7d299c591293bb69fed47a703eb2703b1cff0493e78b16ed007e5382" -dependencies = [ - "guardian", - "indexmap", - "itertools", - "or_poisoned", - "paste", - "reactive_graph 0.2.14", - "reactive_stores_macro 0.4.3", - "rustc-hash", - "send_wrapper", -] - -[[package]] -name = "reactive_stores" -version = "0.5.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28faff28ee5386b0334fa7b6bcf90bf14cfdc4763e0f6cc750cc77f48d7d20dc" -dependencies = [ - "guardian", - "indexmap", - "itertools", - "or_poisoned", - "paste", - "reactive_graph 0.3.0-alpha", - "reactive_stores_macro 0.5.0-alpha", - "rustc-hash", - "send_wrapper", -] - -[[package]] -name = "reactive_stores_macro" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68072edd607edd30b9ebf57d984ba45d8ab8809e598d0f6046278373fb76a5a0" -dependencies = [ - "convert_case 0.11.0", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "reactive_stores_macro" -version = "0.5.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d620949472121407c8e56321ce65c9af8756df1229dea2b4584b3ea2e0d014e" -dependencies = [ - "convert_case 0.11.0", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "regex" -version = "1.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "rstml" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61cf4616de7499fc5164570d40ca4e1b24d231c6833a88bff0fe00725080fd56" -dependencies = [ - "derive-where", - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn", - "syn_derive", - "thiserror 2.0.18", -] - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "send_wrapper" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" -dependencies = [ - "futures-core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-wasm-bindgen" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" -dependencies = [ - "js-sys", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_qs" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" -dependencies = [ - "percent-encoding", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "serde_qs" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67d525c8ff68aa99e5818302259bdd02d86d0303710616f39c0f44846ff6d332" -dependencies = [ - "itoa", - "percent-encoding", - "ryu", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "server_fn" -version = "0.8.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8559dd05af1b5b7e363a150616589d5a88af5187273f7f331ba0dae8922812" -dependencies = [ - "base64", - "bytes", - "const-str", - "const_format", - "futures", - "gloo-net", - "http", - "js-sys", - "or_poisoned", - "pin-project-lite", - "rustc_version", - "rustversion", - "send_wrapper", - "serde", - "serde_json", - "serde_qs 0.15.0", - "server_fn_macro_default 0.8.5", - "thiserror 2.0.18", - "throw_error", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "xxhash-rust", -] - -[[package]] -name = "server_fn" -version = "0.9.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc14077e0509ce01cb22040451442fcc049b6639103b4a1aeccbc1be754b48f3" -dependencies = [ - "base64", - "bytes", - "const-str", - "const_format", - "futures", - "gloo-net", - "http", - "js-sys", - "or_poisoned", - "pin-project-lite", - "rustc_version", - "rustversion", - "send_wrapper", - "serde", - "serde_json", - "serde_qs 1.1.2", - "server_fn_macro_default 0.9.0-alpha", - "thiserror 2.0.18", - "throw_error", - "typed-builder", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "xxhash-rust", -] - -[[package]] -name = "server_fn_macro" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1295b54815397d30d986b63f93cfd515fa86d5e528e0bb589ce9d530502f9e0f" -dependencies = [ - "const_format", - "convert_case 0.11.0", - "proc-macro2", - "quote", - "rustc_version", - "syn", - "xxhash-rust", -] - -[[package]] -name = "server_fn_macro" -version = "0.9.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78472a390a5824c6e12c1d76dbbd2e4812b482aea22e04260a387a48a318a93b" -dependencies = [ - "const_format", - "convert_case 0.11.0", - "proc-macro2", - "quote", - "rustc_version", - "syn", - "xxhash-rust", -] - -[[package]] -name = "server_fn_macro_default" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63eb08f80db903d3c42f64e60ebb3875e0305be502bdc064ec0a0eab42207f00" -dependencies = [ - "server_fn_macro 0.8.10", - "syn", -] - -[[package]] -name = "server_fn_macro_default" -version = "0.9.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee84c961bd807f906092eb26e690f49b84ea7f320ab03751f6f2e0ad67598658" -dependencies = [ - "server_fn_macro 0.9.0-alpha", - "syn", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shared-types" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "syn" -version = "2.0.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb066a04799e45f5d582e8fc6ec8e6d6896040d00898eb4e6a835196815b219" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tachys" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92ba81187437cc5df4281f2326a2e13cc81e8f96448292d1112388e2025ca66" -dependencies = [ - "any_spawner", - "async-trait", - "const_str_slice_concat", - "drain_filter_polyfill", - "either_of", - "erased", - "futures", - "html-escape", - "indexmap", - "itertools", - "js-sys", - "next_tuple", - "oco_ref", - "or_poisoned", - "paste", - "reactive_graph 0.2.14", - "reactive_stores 0.4.3", - "rustc-hash", - "rustc_version", - "send_wrapper", - "slotmap", - "throw_error", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "tachys" -version = "0.3.0-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a619d080e7916f4708022ed226712278babb1922d36e6f9adeb29eb91f2af7" -dependencies = [ - "any_spawner", - "async-trait", - "const_str_slice_concat", - "drain_filter_polyfill", - "either_of", - "erased", - "futures", - "html-escape", - "indexmap", - "itertools", - "js-sys", - "next_tuple", - "oco_ref", - "or_poisoned", - "paste", - "reactive_graph 0.3.0-alpha", - "reactive_stores 0.5.0-alpha", - "rustc-hash", - "rustc_version", - "send_wrapper", - "slotmap", - "throw_error", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "throw_error" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0ed6038fcbc0795aca7c92963ddda636573b956679204e044492d2b13c8f64" -dependencies = [ - "pin-project-lite", -] - -[[package]] -name = "time" -version = "0.3.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "serde_core", - "zerovec", -] - -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow", -] - -[[package]] -name = "typed-builder" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31aa81521b70f94402501d848ccc0ecaa8f93c8eb6999eb9747e72287757ffda" -dependencies = [ - "typed-builder-macro", -] - -[[package]] -name = "typed-builder-macro" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076a02dc54dd46795c2e9c8282ed40bcfb1e22747e955de9389a1de28190fb26" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "unic-langid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" -dependencies = [ - "unic-langid-impl", -] - -[[package]] -name = "unic-langid-impl" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" -dependencies = [ - "tinystr", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" -dependencies = [ - "getrandom", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-logger" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "074649a66bb306c8f2068c9016395fa65d8e08d2affcbf95acf3c24c3ab19718" -dependencies = [ - "log", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-streams" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasm_split_helpers" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab578aae2fe2916edaea06843187d50f87b0965622da0ceef648edca27b385ba" -dependencies = [ - "async-once-cell", - "wasm_split_macros", -] - -[[package]] -name = "wasm_split_macros" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e653af7ee4a9ef0fce481a9ec6f43cb78de20d0cdb4f4f5862e1dc6e407e6c8" -dependencies = [ - "base16", - "quote", - "sha2", - "syn", -] - -[[package]] -name = "web-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" -dependencies = [ - "memchr", -] - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "xxhash-rust" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d93c89cdc2d3a63c3ec48ffe926931bdc069eafa8e4402fe6d8f790c9d1e576" - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "yoke" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "serde", - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/services/frontend/Cargo.toml b/services/frontend/Cargo.toml deleted file mode 100644 index 3ff0eb7..0000000 --- a/services/frontend/Cargo.toml +++ /dev/null @@ -1,3 +0,0 @@ -[workspace] -resolver = "2" -members = ["shared-types", "frontend"] diff --git a/services/frontend/README.md b/services/frontend/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/services/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/services/frontend/biome.json b/services/frontend/biome.json new file mode 100644 index 0000000..35c6cec --- /dev/null +++ b/services/frontend/biome.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "includes": ["**", "!node_modules", "!.next", "!dist", "!build"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noUnknownAtRules": "off", + "useIterableCallbackReturn": "off", + "noArrayIndexKey": "warn" + }, + "a11y": { + "useButtonType": "off", + "noAutofocus": "off" + }, + "performance": { + "noImgElement": "warn" + }, + "security": { + "noDangerouslySetInnerHtml": "warn" + }, + "correctness": { + "noInvalidUseBeforeDeclaration": "off", + "noUnusedFunctionParameters": "warn" + } + }, + "domains": { + "next": "recommended", + "react": "recommended" + } + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/services/frontend/bun.lock b/services/frontend/bun.lock new file mode 100644 index 0000000..ce7234a --- /dev/null +++ b/services/frontend/bun.lock @@ -0,0 +1,881 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "frontend", + "dependencies": { + "@base-ui/react": "^1.6.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.27.0", + "next": "16.2.12", + "react": "19.2.4", + "react-dom": "19.2.4", + "shadcn": "^4.15.0", + "tailwind-merge": "^3.6.0", + "tw-animate-css": "^1.4.0", + }, + "devDependencies": { + "@biomejs/biome": "2.2.0", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "babel-plugin-react-compiler": "1.0.0", + "tailwindcss": "^4", + "typescript": "^5", + }, + }, + }, + "trustedDependencies": [ + "sharp", + ], + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="], + + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@base-ui/react": ["@base-ui/react@1.6.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.3.1", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw=="], + + "@base-ui/utils": ["@base-ui/utils@0.3.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg=="], + + "@biomejs/biome": ["@biomejs/biome@2.2.0", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.0", "@biomejs/cli-darwin-x64": "2.2.0", "@biomejs/cli-linux-arm64": "2.2.0", "@biomejs/cli-linux-arm64-musl": "2.2.0", "@biomejs/cli-linux-x64": "2.2.0", "@biomejs/cli-linux-x64-musl": "2.2.0", "@biomejs/cli-win32-arm64": "2.2.0", "@biomejs/cli-win32-x64": "2.2.0" }, "bin": { "biome": "bin/biome" } }, "sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMN1yAAvHtzIw=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zKbwUUh+9uFmWfS8IFxmVD6XwqFcENjZvEyfOxHs1epjdH3wyyMQG80FGDsmauPwS2r5kXdEM0v/+dTIA9FXAg=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-+OmT4dsX2eTfhD5crUOPw3RPhaR+SKVspvGVmSdZ9y9O/AgL8pla6T4hOn1q+VAFBHuHhsdxDRJgFCSC7RaMOw=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-6eoRdF2yW5FnW9Lpeivh7Mayhq0KDdaDMYOJnH9aT02KuSIX5V1HmWJCQQPwIQbhDh68Zrcpl8inRlTEan0SXw=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-egKpOa+4FL9YO+SMUMLUvf543cprjevNc3CAgDNFLcjknuNMcZ0GLJYa3EGTCR2xIkIUJDVneBV3O9OcIlCEZQ=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5UmQx/OZAfJfi25zAnAGHUMuOd+LOsliIt119x2soA2gLggQYrVPA+2kMUxR6Mw5M1deUF/AWWP2qpxgH7Nyfw=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-I5J85yWwUWpgJyC1CcytNSGusu2p9HjDnOPAFG4Y515hwRD0jpR9sT9/T1cKHtuCvEQ/sBvx+6zhz9l9wEJGAg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-n9a1/f2CwIDmNMNkFs+JI0ZjFnMO0jdOyGNtihgUNFnlmd84yIYY2KMTBmMV58ZlVHjgmY5Y6E1hVTnSRieggA=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww=="], + + "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.75.1", "", { "dependencies": { "@dotenvx/primitives": "^0.8.0", "commander": "^11.1.0", "conf": "^10.2.0", "dotenv": "^17.2.1", "enquirer": "^2.4.1", "env-paths": "^2.2.1", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "open": "^8.4.2", "picomatch": "^4.0.4", "systeminformation": "^5.22.11", "undici": "^7.11.0", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ=="], + + "@dotenvx/primitives": ["@dotenvx/primitives@0.8.0", "", {}, "sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.9", "", { "dependencies": { "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], + + "@hono/node-server": ["@hono/node-server@1.19.15", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Za2ai6TLdKjUvnur+eenO6nuYYipVAEhyCAdaV8IRvmU9kK8crOZUSYvIXn72E4f8fJqyAbpcJuTsYYmZp9Deg=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + + "@next/env": ["@next/env@16.2.12", "", {}, "sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.12", "", { "os": "linux", "cpu": "x64" }, "sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.12", "", { "os": "linux", "cpu": "x64" }, "sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.12", "", { "os": "win32", "cpu": "x64" }, "sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@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" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="], + + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.3", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "postcss": "^8.5.16", "tailwindcss": "4.3.3" } }, "sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg=="], + + "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], + + "@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="], + + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], + + "atomically": ["atomically@1.7.0", "", {}, "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="], + + "babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.3", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-sbT0Ui/CZwyAyy7icT1Gw5P1LKRlFaHwaF6tDCW5YHq2X5SeeZFphBuIagopSfwSSZq3sQcbmEL072yphxm7ew=="], + + "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], + + "brace-expansion": ["brace-expansion@5.0.8", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], + + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], + + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "conf": ["conf@10.2.0", "", { "dependencies": { "ajv": "^8.6.3", "ajv-formats": "^2.1.1", "atomically": "^1.7.0", "debounce-fn": "^4.0.0", "dot-prop": "^6.0.1", "env-paths": "^2.2.1", "json-schema-typed": "^7.0.3", "onetime": "^5.1.2", "pkg-up": "^3.1.0", "semver": "^7.3.5" } }, "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg=="], + + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debounce-fn": ["debounce-fn@4.0.0", "", { "dependencies": { "mimic-fn": "^3.0.0" } }, "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + + "dot-prop": ["dot-prop@6.0.1", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="], + + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.396", "", {}, "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "enhanced-resolve": ["enhanced-resolve@5.24.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ=="], + + "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.6.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-uri": ["fast-uri@3.1.4", "", {}, "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "get-intrinsic": ["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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-own-enumerable-keys": ["get-own-enumerable-keys@1.0.0", "", {}, "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "hono": ["hono@4.12.32", "", {}, "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ip-address": ["ip-address@10.3.1", "", {}, "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-obj": ["is-obj@3.0.0", "", {}, "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "is-regexp": ["is-regexp@3.1.0", "", {}, "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="], + + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], + + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "jose": ["jose@6.2.4", "", {}, "sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + + "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lucide-react": ["lucide-react@1.27.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-rJicGl/3Fly/E0rOH1YmPZ6e49JCnKknh1ox1vpHnkfjujAkKA6sqUZvH3MTAaXXjgexyUwgNwTJzTtYuAFYJw=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "mimic-fn": ["mimic-fn@3.1.0", "", {}, "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "next": ["next@16.2.12", "", { "dependencies": { "@next/env": "16.2.12", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.12", "@next/swc-darwin-x64": "16.2.12", "@next/swc-linux-arm64-gnu": "16.2.12", "@next/swc-linux-arm64-musl": "16.2.12", "@next/swc-linux-x64-gnu": "16.2.12", "@next/swc-linux-x64-musl": "16.2.12", "@next/swc-win32-arm64-msvc": "16.2.12", "@next/swc-win32-x64-msvc": "16.2.12", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw=="], + + "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], + + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], + + "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], + + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + + "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], + + "postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="], + + "postcss-selector-parser": ["postcss-selector-parser@7.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg=="], + + "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "recast": ["recast@0.23.12", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "reselect": ["reselect@5.2.0", "", {}, "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shadcn": ["shadcn@4.15.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "kleur": "^4.1.5", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "undici": "^7.27.2", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-fFTpfOuRwqjpXGp/sKpAxJkjdgv1jf8bDrW1xi0cVn2k7WJ5ijV/gjkAlkAidGDjhEkgcUuxVdm4oRB9r8BivA=="], + + "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="], + + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "@babel/core": "*", "babel-plugin-macros": "*", "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, "optionalPeers": ["@babel/core", "babel-plugin-macros"] }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + + "systeminformation": ["systeminformation@5.33.1", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w=="], + + "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], + + "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="], + + "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yocto-spinner": ["yocto-spinner@1.2.2", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng=="], + + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + + "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "@dotenvx/dotenvx/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + + "conf/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + + "conf/json-schema-typed": ["json-schema-typed@7.0.3", "", {}, "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A=="], + + "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + + "enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + + "wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], + + "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "@dotenvx/dotenvx/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "@dotenvx/dotenvx/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "@dotenvx/dotenvx/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@dotenvx/dotenvx/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "@dotenvx/dotenvx/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + } +} diff --git a/services/frontend/components.json b/services/frontend/components.json new file mode 100644 index 0000000..8d886db --- /dev/null +++ b/services/frontend/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-nova", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/services/frontend/frontend/Cargo.toml b/services/frontend/frontend/Cargo.toml deleted file mode 100644 index 7a580cc..0000000 --- a/services/frontend/frontend/Cargo.toml +++ /dev/null @@ -1,63 +0,0 @@ -[package] -name = "frontend" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -leptos = { version = "=0.9.0-alpha", features = ["csr"] } -leptos-use = "0.19" -lucide-leptos = "3.23" -wasm-bindgen = "0.2" -wasm-bindgen-futures = "0.4" -js-sys = "0.3" -web-sys = { version = "0.3", features = [ - "WebSocket", - "MessageEvent", - "CloseEvent", - "ErrorEvent", - "CanvasRenderingContext2d", - "AudioContext", - "AudioBuffer", - "AudioBufferSourceNode", - "AudioDestinationNode", - "AudioNode", - "AudioProcessingEvent", - "MediaStreamAudioSourceNode", - "ScriptProcessorNode", - "Window", - "Document", - "Element", - "HtmlElement", - "HtmlSelectElement", - "KeyboardEvent", - "Storage", - "IntersectionObserver", - "ResizeObserver", - "Url", - "Headers", - "Request", - "RequestInit", - "RequestMode", - "Response", - "HtmlInputElement", - "HtmlAudioElement", - "HtmlCanvasElement", - "MediaDevices", - "MediaStream", - "MediaStreamConstraints", - "MediaStreamTrack", - "Navigator", - "console", -] } -gloo-net = "0.6" -gloo-timers = { version = "0.3", features = ["futures"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -serde-wasm-bindgen = "0.6" -wasm-logger = "0.2" -console_error_panic_hook = "0.1" -regex = "1" -shared-types = { path = "../shared-types" } diff --git a/services/frontend/frontend/Trunk.toml b/services/frontend/frontend/Trunk.toml deleted file mode 100644 index 1ee94b8..0000000 --- a/services/frontend/frontend/Trunk.toml +++ /dev/null @@ -1,7 +0,0 @@ -[build] -target = "index.html" -dist = "dist" - -[serve] -port = 8080 -open = false diff --git a/services/frontend/frontend/index.html b/services/frontend/frontend/index.html deleted file mode 100644 index bc04cd6..0000000 --- a/services/frontend/frontend/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - IMPHNEN -- Discord Moderation - - - - - - - - - - - - - diff --git a/services/frontend/frontend/public/.gitkeep b/services/frontend/frontend/public/.gitkeep deleted file mode 100644 index 2f48bcb..0000000 --- a/services/frontend/frontend/public/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# Trunk copies this directory to dist/ diff --git a/services/frontend/frontend/src/api/auth.rs b/services/frontend/frontend/src/api/auth.rs deleted file mode 100644 index e52090f..0000000 --- a/services/frontend/frontend/src/api/auth.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::api::client::{request, ApiError}; -use serde::{Deserialize, Serialize}; -use crate::{log_error, log_info, log_warn, make_logger}; - -make_logger!(); - -#[derive(Serialize)] -struct LoginPayload { - password: String, -} - -#[derive(Deserialize)] -struct LoginResponse { - ok: bool, -} - -pub async fn login(password: &str) -> Result { - let payload = LoginPayload { - password: password.to_string(), - }; - let body = serde_json::to_string(&payload).unwrap(); - let resp: LoginResponse = request("POST", "/api/auth/login", Some(&body)).await?; - Ok(resp.ok) -} diff --git a/services/frontend/frontend/src/api/client.rs b/services/frontend/frontend/src/api/client.rs deleted file mode 100644 index 7138509..0000000 --- a/services/frontend/frontend/src/api/client.rs +++ /dev/null @@ -1,184 +0,0 @@ -use serde::de::DeserializeOwned; -use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::JsFuture; -use web_sys::{Headers, Request, RequestInit, RequestMode, Response}; -use crate::{log_debug, log_error, make_logger}; - -make_logger!(); - -#[derive(Debug)] -pub struct ApiError { - pub message: String, - pub status_code: u16, -} - -impl std::fmt::Display for ApiError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "API error {}: {}", self.status_code, self.message) - } -} - -impl std::error::Error for ApiError {} - -fn get_base_url() -> String { - if let Some(window) = web_sys::window() { - let location = window.location(); - let protocol = location.protocol().unwrap_or_else(|_| "http:".to_string()); - let protocol = protocol.trim_end_matches(':'); - let host = location - .host() - .unwrap_or_else(|_| "localhost:3001".to_string()); - format!("{}://{}", protocol, host) - } else { - "http://localhost:3001".to_string() - } -} - -fn get_auth_header() -> Option { - // Read password from sessionStorage - let storage = web_sys::window()?.local_storage().ok()??; - storage.get_item("admin-password").ok()? -} - -pub async fn request( - method: &str, - path: &str, - body: Option<&str>, -) -> Result { - let url = format!("{}{}", get_base_url(), path); - - let headers = Headers::new().map_err(|_| { - let msg = "Failed to create headers"; - log_error!("API {} {} failed: {}", method, path, msg); - ApiError { - message: msg.to_string(), - status_code: 0, - } - })?; - - if let Some(password) = get_auth_header() { - headers.set("X-Admin-Password", &password).ok(); - } - - if body.is_some() { - headers.set("Content-Type", "application/json").ok(); - } - - log_debug!("{} {} ->", method, path); - - let opts = RequestInit::new(); - opts.set_method(method); - opts.set_headers(&headers); - opts.set_mode(RequestMode::Cors); - - if let Some(json_body) = body { - opts.set_body(&JsValue::from_str(json_body)); - } - - let request = Request::new_with_str_and_init(&url, &opts).map_err(|e| { - let msg = format!("Failed to create request: {:?}", e); - log_error!("API {} {} failed: {}", method, path, msg); - ApiError { - message: msg, - status_code: 0, - } - })?; - - let window = web_sys::window().ok_or_else(|| { - let msg = "No window"; - log_error!("API {} {} failed: {}", method, path, msg); - ApiError { - message: msg.to_string(), - status_code: 0, - } - })?; - - let resp_value = JsFuture::from(window.fetch_with_request(&request)) - .await - .map_err(|e| { - let msg = format!("Fetch failed: {:?}", e); - log_error!("API {} {} failed: {}", method, path, msg); - ApiError { - message: msg, - status_code: 0, - } - })?; - - let response: Response = resp_value.dyn_into().map_err(|_| { - let msg = "Invalid response"; - log_error!("API {} {} failed: {}", method, path, msg); - ApiError { - message: msg.to_string(), - status_code: 0, - } - })?; - - let status = response.status(); - if status >= 400 { - let text = JsFuture::from(response.text().map_err(|_| { - let msg = "Failed to read error body"; - log_error!("API {} {} failed: {}", method, path, msg); - ApiError { - message: msg.to_string(), - status_code: status, - } - })?) - .await - .ok() - .and_then(|v| v.as_string()) - .unwrap_or_default(); - - log_error!("API {} {} failed: status={} {}", method, path, status, text); - return Err(ApiError { - message: text, - status_code: status, - }); - } - - let text = JsFuture::from(response.text().map_err(|_| { - let msg = "Failed to read response body"; - log_error!("API {} {} failed: {}", method, path, msg); - ApiError { - message: msg.to_string(), - status_code: status, - } - })?) - .await - .map_err(|_| { - let msg = "Failed to await response"; - log_error!("API {} {} failed: {}", method, path, msg); - ApiError { - message: msg.to_string(), - status_code: status, - } - })? - .as_string() - .ok_or_else(|| { - let msg = "Response is not text"; - log_error!("API {} {} failed: {}", method, path, msg); - ApiError { - message: msg.to_string(), - status_code: status, - } - })?; - - log_debug!("{} {} <- {}", method, path, status); - - serde_json::from_str(&text).map_err(|e| { - let msg = format!( - "JSON parse error: {} — body: {}", - e, - &text[..text.len().min(200)] - ); - log_error!("API {} {} failed: {}", method, path, msg); - ApiError { - message: msg, - status_code: status, - } - }) -} - -pub async fn request_no_body(method: &str, path: &str) -> Result<(), ApiError> { - request::(method, path, None).await?; - Ok(()) -} diff --git a/services/frontend/frontend/src/api/config.rs b/services/frontend/frontend/src/api/config.rs deleted file mode 100644 index 61dab4e..0000000 --- a/services/frontend/frontend/src/api/config.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::api::client::{request, ApiError}; -use serde::Deserialize; -#[derive(Debug, Clone, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AppConfigResponse { - pub monitor_guild_id: Option, -} - -/// GET /api/config -pub async fn get_config() -> Result { - request("GET", "/api/config", None).await -} diff --git a/services/frontend/frontend/src/api/dashboard.rs b/services/frontend/frontend/src/api/dashboard.rs deleted file mode 100644 index 5a046c8..0000000 --- a/services/frontend/frontend/src/api/dashboard.rs +++ /dev/null @@ -1,96 +0,0 @@ -use crate::api::client::{request, ApiError}; -use shared_types::dashboard::*; -use crate::{log_debug, make_logger}; - -make_logger!(); - -/// GET /api/dashboard/stats -pub async fn get_dashboard_stats() -> Result { - log_debug!("get_dashboard_stats"); - request("GET", "/api/dashboard/stats", None).await -} - -/// GET /api/dashboard/users?limit=&cursor=&search= -pub async fn get_dashboard_users( - limit: Option, - cursor: Option<&str>, - search: Option<&str>, -) -> Result { - log_debug!("get_dashboard_users: limit={:?}, cursor={:?}, search={:?}", limit, cursor, search); - let mut path = "/api/dashboard/users".to_string(); - let mut params = vec![]; - if let Some(l) = limit { - params.push(format!("limit={}", l)); - } - if let Some(c) = cursor { - params.push(format!("cursor={}", c)); - } - if let Some(s) = search { - params.push(format!("search={}", s)); - } - if !params.is_empty() { - path.push_str(&format!("?{}", params.join("&"))); - } - request("GET", &path, None).await -} - -#[derive(serde::Deserialize)] -pub struct PaginatedUsers { - pub data: Vec, - #[serde(rename = "nextCursor")] - pub next_cursor: Option, -} - -/// GET /api/dashboard/users/{userId} -pub async fn get_dashboard_user_detail(user_id: &str) -> Result { - log_debug!("get_dashboard_user_detail: user_id={}", user_id); - request("GET", &format!("/api/dashboard/users/{}", user_id), None).await -} - -/// GET /api/dashboard/channels?limit=&cursor=&search=&guild_id= -pub async fn get_dashboard_channels( - limit: Option, - cursor: Option<&str>, - search: Option<&str>, - guild_id: Option<&str>, -) -> Result { - log_debug!("get_dashboard_channels: limit={:?}, cursor={:?}, search={:?}, guild_id={:?}", limit, cursor, search, guild_id); - let mut path = "/api/dashboard/channels".to_string(); - let mut params = vec![]; - if let Some(l) = limit { - params.push(format!("limit={}", l)); - } - if let Some(c) = cursor { - params.push(format!("cursor={}", c)); - } - if let Some(s) = search { - params.push(format!("search={}", s)); - } - if let Some(g) = guild_id { - params.push(format!("guild_id={}", g)); - } - if !params.is_empty() { - path.push_str(&format!("?{}", params.join("&"))); - } - request("GET", &path, None).await -} - -#[derive(serde::Deserialize)] -pub struct PaginatedChannels { - pub data: Vec, - #[serde(rename = "nextCursor")] - pub next_cursor: Option, -} - -/// GET /api/dashboard/channels/{channelId} -pub async fn get_dashboard_channel_detail( - channel_id: &str, -) -> Result { - log_debug!("get_dashboard_channel_detail: channel_id={}", channel_id); - request( - "GET", - &format!("/api/dashboard/channels/{}", channel_id), - None, - ) - .await -} diff --git a/services/frontend/frontend/src/api/mascot.rs b/services/frontend/frontend/src/api/mascot.rs deleted file mode 100644 index 2df4bf5..0000000 --- a/services/frontend/frontend/src/api/mascot.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::api::client::{request, ApiError}; -use serde::{Deserialize, Serialize}; -#[derive(Debug, Serialize)] -struct MascotChatRequest<'a> { - message: &'a str, -} - -#[derive(Debug, Deserialize)] -pub struct MascotChatResponse { - pub response: String, - pub timestamp: String, -} - -#[derive(Debug, Deserialize)] -pub struct ChatHistoryMessage { - pub role: String, - pub content: String, - pub timestamp: String, -} - -pub async fn send_mascot_message(message: &str) -> Result { - let body = serde_json::to_string(&MascotChatRequest { message }).map_err(|err| ApiError { - message: format!("Failed to serialize mascot request: {}", err), - status_code: 0, - })?; - request("POST", "/api/mascot/chat", Some(&body)).await -} - -/// GET /api/mascot/chat/history -pub async fn get_chat_history() -> Result, ApiError> { - request("GET", "/api/mascot/chat/history", None).await -} diff --git a/services/frontend/frontend/src/api/messages.rs b/services/frontend/frontend/src/api/messages.rs deleted file mode 100644 index 403ec1c..0000000 --- a/services/frontend/frontend/src/api/messages.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::api::client::{request, ApiError}; -use shared_types::message::{MessageRecord, PageResult}; -use crate::{log_debug, make_logger}; - -make_logger!(); - -/// GET /api/messages?guildId=&limit=&channelId=&cursor= -pub async fn get_messages( - guild_id: &str, - limit: Option, - channel_id: Option<&str>, - cursor: Option<&str>, -) -> Result, ApiError> { - log_debug!("get_messages: guild_id={}, limit={:?}, channel_id={:?}, cursor={:?}", guild_id, limit, channel_id, cursor); - let mut path = format!("/api/messages?guildId={}", guild_id); - if let Some(l) = limit { - path.push_str(&format!("&limit={}", l)); - } - if let Some(c) = channel_id { - path.push_str(&format!("&channelId={}", c)); - } - if let Some(c) = cursor { - path.push_str(&format!("&cursor={}", c)); - } - request("GET", &path, None).await -} - -/// GET /api/review?params -/// Backend `GET /review` accepts `channelId` and `limit` (not guildId). -pub async fn get_review_messages( - limit: Option, - channel_id: Option<&str>, -) -> Result, ApiError> { - log_debug!("get_review_messages: limit={:?}, channel_id={:?}", limit, channel_id); - let mut path = "/api/review".to_string(); - let mut params = vec![]; - if let Some(l) = limit { - params.push(format!("limit={}", l)); - } - if let Some(c) = channel_id { - params.push(format!("channelId={}", c)); - } - if !params.is_empty() { - path.push_str(&format!("?{}", params.join("&"))); - } - request("GET", &path, None).await -} - -/// GET /api/messages/images?guildId=&limit= -pub async fn get_images( - guild_id: &str, - limit: Option, -) -> Result, ApiError> { - log_debug!("get_images: guild_id={}, limit={:?}", guild_id, limit); - let mut path = format!("/api/messages/images?guildId={}", guild_id); - if let Some(l) = limit { - path.push_str(&format!("&limit={}", l)); - } - request("GET", &path, None).await -} - -/// GET /api/messages/detail/{id} -pub async fn get_message_detail(id: &str) -> Result, ApiError> { - log_debug!("get_message_detail: id={}", id); - request("GET", &format!("/api/messages/detail/{}", id), None).await -} - -/// POST /api/messages/{id}/reanalyze -pub async fn reanalyze_message(id: &str) -> Result<(), ApiError> { - log_debug!("reanalyze_message: id={}", id); - let _: serde_json::Value = request( - "POST", - &format!("/api/messages/{}/reanalyze", id), - Some("{}"), - ) - .await?; - Ok(()) -} - -/// POST /api/messages/reanalyze-batch -pub async fn reanalyze_batch() -> Result { - log_debug!("reanalyze_batch"); - #[derive(serde::Deserialize)] - #[allow(dead_code)] - struct BatchResp { - ok: bool, - count: u64, - } - let resp: BatchResp = request("POST", "/api/messages/reanalyze-batch", Some("{}")).await?; - Ok(resp.count) -} - -/// GET /api/analysis/search?q=&limit= -pub async fn search_messages( - query: &str, - limit: Option, -) -> Result, ApiError> { - log_debug!("search_messages: query={}, limit={:?}", query, limit); - #[derive(serde::Deserialize)] - struct SearchResult { - results: Vec, - } - let mut path = format!("/api/analysis/search?q={}", query); - if let Some(l) = limit { - path.push_str(&format!("&limit={}", l)); - } - let resp: SearchResult = request("GET", &path, None).await?; - Ok(resp.results) -} diff --git a/services/frontend/frontend/src/api/mod.rs b/services/frontend/frontend/src/api/mod.rs deleted file mode 100644 index dfe2f33..0000000 --- a/services/frontend/frontend/src/api/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod auth; -pub mod client; -pub mod config; -pub mod dashboard; -pub mod mascot; -pub mod messages; -pub mod recordings; -pub mod voice; diff --git a/services/frontend/frontend/src/api/recordings.rs b/services/frontend/frontend/src/api/recordings.rs deleted file mode 100644 index b7dd738..0000000 --- a/services/frontend/frontend/src/api/recordings.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::api::client::{request, request_no_body, ApiError}; -use shared_types::recording::VoiceRecordingListResponse; -/// GET /api/recordings?limit=&cursor= -pub async fn get_recordings( - limit: Option, - cursor: Option<&str>, -) -> Result { - let mut path = "/api/recordings".to_string(); - let mut params = vec![]; - if let Some(l) = limit { - params.push(format!("limit={}", l)); - } - if let Some(c) = cursor { - params.push(format!("cursor={}", c)); - } - if !params.is_empty() { - path.push_str(&format!("?{}", params.join("&"))); - } - request("GET", &path, None).await -} - -/// DELETE /api/recordings/{id} -pub async fn delete_recording(id: &str) -> Result<(), ApiError> { - request_no_body("DELETE", &format!("/api/recordings/{}", id)).await -} diff --git a/services/frontend/frontend/src/api/voice.rs b/services/frontend/frontend/src/api/voice.rs deleted file mode 100644 index 04a85c8..0000000 --- a/services/frontend/frontend/src/api/voice.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::api::client::{request, ApiError}; -use serde::Serialize; -use shared_types::guild::{Channel, Guild}; -use shared_types::media::MediaState; -use shared_types::voice::VoiceStatus; -/// GET /api/guilds -pub async fn get_guilds() -> Result, ApiError> { - request("GET", "/api/guilds", None).await -} - -/// GET /api/guilds/{guildId}/voice-channels -pub async fn get_voice_channels(guild_id: &str) -> Result, ApiError> { - request( - "GET", - &format!("/api/guilds/{}/voice-channels", guild_id), - None, - ) - .await -} - -/// GET /api/guilds/{guildId}/channels -pub async fn get_text_channels(guild_id: &str) -> Result, ApiError> { - request("GET", &format!("/api/guilds/{}/channels", guild_id), None).await -} - -/// GET /api/voice/status -pub async fn get_voice_status() -> Result { - request("GET", "/api/voice/status", None).await -} - -/// POST /api/voice/connect { guildId, channelId } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct ConnectPayload { - guild_id: String, - channel_id: String, -} -pub async fn connect_voice(guild_id: &str, channel_id: &str) -> Result { - let body = serde_json::to_string(&ConnectPayload { - guild_id: guild_id.to_string(), - channel_id: channel_id.to_string(), - }) - .unwrap(); - request("POST", "/api/voice/connect", Some(&body)).await -} - -/// POST /api/voice/disconnect -pub async fn disconnect_voice() -> Result { - request("POST", "/api/voice/disconnect", Some("{}")).await -} - -/// GET /api/media/status -pub async fn get_media_status() -> Result { - request("GET", "/api/media/status", None).await -} - -/// POST /api/media/queue { source, mode } -#[derive(Serialize)] -struct MediaQueuePayload { - source: String, - mode: String, -} -pub async fn media_queue(source: &str, mode: &str) -> Result { - let body = serde_json::to_string(&MediaQueuePayload { - source: source.to_string(), - mode: mode.to_string(), - }) - .unwrap(); - request("POST", "/api/media/queue", Some(&body)).await -} - -/// POST /api/media/skip -pub async fn media_skip() -> Result { - request("POST", "/api/media/skip", Some("{}")).await -} - -/// POST /api/media/stop -pub async fn media_stop() -> Result { - request("POST", "/api/media/stop", Some("{}")).await -} - -/// POST /api/media/volume { volume } -#[derive(Serialize)] -struct VolumePayload { - volume: f64, -} -pub async fn media_volume(volume: f64) -> Result { - let body = serde_json::to_string(&VolumePayload { volume }).unwrap(); - request("POST", "/api/media/volume", Some(&body)).await -} diff --git a/services/frontend/frontend/src/app.rs b/services/frontend/frontend/src/app.rs deleted file mode 100644 index 9949445..0000000 --- a/services/frontend/frontend/src/app.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::api::config as config_api; -use crate::features::dashboard::DashboardPanel; -use crate::features::live::LivePanel; -use crate::features::messages::MessagesPanel; -use crate::features::polish::components::{MascotChatbot, ParticleBackground}; -use crate::features::polish::{initial_theme, ThemeContext}; -use crate::layout::sidebar::Sidebar; -use crate::ws::context::WsContext; -use leptos::prelude::*; -use shared_types::ui_state::Tab; -use wasm_bindgen_futures::spawn_local; -use crate::{log_info, log_warn, make_logger}; - -make_logger!(); - -fn get_ws_url() -> String { - web_sys::window() - .map(|w| { - let loc = w.location(); - let protocol = loc.protocol().unwrap_or_else(|_| "http:".to_string()); - let host = loc.host().unwrap_or_else(|_| "localhost:3001".to_string()); - let ws_proto = if protocol.starts_with("https") { - "wss" - } else { - "ws" - }; - format!("{}://{}/ws", ws_proto, host) - }) - .unwrap_or_else(|| "ws://localhost:3001/ws".to_string()) -} - -#[derive(Clone)] -pub struct AppConfig { - pub monitor_guild_id: RwSignal>, -} - -#[derive(Clone)] -pub struct AuthContext { - pub authenticated: RwSignal, - pub password: RwSignal, -} - -#[derive(Clone)] -pub struct UiContext { - pub active_tab: RwSignal, - pub selected_guild: RwSignal>, -} - -#[component] -pub fn App() -> impl IntoView { - let auth = AuthContext { - authenticated: RwSignal::new(false), - password: RwSignal::new(String::new()), - }; - let ui = UiContext { - active_tab: RwSignal::new(Tab::Messages), - selected_guild: RwSignal::new(None), - }; - let theme = ThemeContext { - theme: RwSignal::new(initial_theme()), - }; - - provide_context(auth.clone()); - provide_context(ui.clone()); - provide_context(theme.clone()); - - let config = AppConfig { - monitor_guild_id: RwSignal::new(None), - }; - provide_context(config.clone()); - - let ws = WsContext::new(&get_ws_url()); - provide_context(ws.clone()); - - ws.connect(); - log_info!("App mounted, WS connecting to {}", get_ws_url()); - - spawn_local({ - let config = config.clone(); - async move { - match config_api::get_config().await { - Ok(cfg) => { - log_info!("[config] fetched OK — monitorGuildId={:?}", cfg.monitor_guild_id); - config.monitor_guild_id.set(cfg.monitor_guild_id); - } - Err(e) => { - log_warn!("[config] failed to fetch: {}", e); - } - } - } - }); - - Effect::new(move |_| { - if auth.authenticated.get() { - spawn_local({ - let config = config.clone(); - async move { - match config_api::get_config().await { - Ok(cfg) => { - config.monitor_guild_id.set(cfg.monitor_guild_id); - } - Err(e) => { - log_info!("[config] fetch after auth failed: {}", e); - } - } - } - }); - } - }); - - view! { -
- - -
- - -
- {move || match ui.active_tab.get() { - Tab::Messages => view! { }.into_any(), - Tab::Live => view! { }.into_any(), - Tab::Dashboard => view! { }.into_any(), - }} -
-
- - {move || auth.authenticated.get().then(|| view! { })} -
- } -} diff --git a/services/frontend/frontend/src/auth.rs b/services/frontend/frontend/src/auth.rs deleted file mode 100644 index fae0481..0000000 --- a/services/frontend/frontend/src/auth.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::api::auth as auth_api; -use crate::app::AuthContext; -use crate::app::UiContext; -use leptos::prelude::*; -use shared_types::ui_state::Tab; -use wasm_bindgen_futures::spawn_local; -use crate::{log_error, log_info, log_warn, make_logger}; - -make_logger!(); - -#[component] -pub fn AuthOverlay() -> impl IntoView { - let auth = use_context::().expect("AuthContext not provided"); - let ui = use_context::(); - let (password, set_password) = signal(String::new()); - let (error, set_error) = signal(Option::::None); - let (loading, set_loading) = signal(false); - - let handle_submit = move |ev: leptos::ev::SubmitEvent| { - ev.prevent_default(); - let pwd = password.get(); - if pwd.is_empty() { - set_error.set(Some("Password diperlukan".to_string())); - return; - } - set_loading.set(true); - set_error.set(None); - - let auth_clone = auth.clone(); - let pwd_clone = pwd.clone(); - let set_loading_clone = set_loading; - let set_error_clone = set_error; - - spawn_local(async move { - match auth_api::login(&pwd_clone).await { - Ok(true) => { - log_info!("Auth login successful"); - if let Some(storage) = web_sys::window() - .and_then(|w| w.local_storage().ok()) - .flatten() - { - let _ = storage.set_item("admin-password", &pwd_clone); - } - auth_clone.authenticated.set(true); - auth_clone.password.set(pwd_clone); - } - Ok(false) => { - log_warn!("Auth login failed - wrong password"); - set_error_clone.set(Some("Login gagal — password salah".to_string())); - } - Err(e) => { - log_error!("Auth login error: {}", e.message); - set_error_clone.set(Some(format!("Error: {}", e.message))); - } - } - set_loading_clone.set(false); - }); - }; - - let tab_messages = ui.as_ref().map(|u| u.active_tab); - let skip_dismiss = move |_| { - if let Some(ref t) = tab_messages { - t.set(Tab::Messages); - } - }; - - view! { - - } -} diff --git a/services/frontend/frontend/src/features/dashboard/components/channel_summary_list.rs b/services/frontend/frontend/src/features/dashboard/components/channel_summary_list.rs deleted file mode 100644 index 6b3a99d..0000000 --- a/services/frontend/frontend/src/features/dashboard/components/channel_summary_list.rs +++ /dev/null @@ -1,143 +0,0 @@ -use leptos::prelude::*; -use shared_types::dashboard::DashboardChannel; - -#[component] -pub fn ChannelSummaryList( - channels: Vec, - loading: bool, - error: Option, - search: String, - has_more: bool, - on_search_change: Box, - on_load_more: Box, - on_retry: Box, -) -> impl IntoView { - let search_cb = StoredValue::new(on_search_change); - let load_more_cb = StoredValue::new(on_load_more); - let retry_cb = StoredValue::new(on_retry); - - view! { -
-
-
"Kanal"
-

"Ringkasan aktivitas, flagged message, dan budaya kanal."

-
-
-
- -
- - {move || { - if loading && channels.is_empty() { - view! { }.into_any() - } else if let Some(err) = error.clone() { - view! { -
-
"⚠"
-

{err}

- -
- }.into_any() - } else if channels.is_empty() { - view! { -
-
"#"
-

"No channels found."

-
- }.into_any() - } else { - view! { -
- {channels.clone().into_iter().map(|channel| view! { - - }).collect::>()} -
- }.into_any() - } - }} - - {move || { - (has_more && !loading).then(|| view! { -
- -
- }) - }} -
-
- } -} - -#[component] -fn ChannelRow(channel: DashboardChannel) -> impl IntoView { - let name = channel - .channel_name - .clone() - .unwrap_or_else(|| channel.channel_id.clone()); - let summary = channel - .culture_summary - .clone() - .unwrap_or_else(|| format!("{} messages", format_number(channel.total_messages))); - let last_seen = channel.last_message_at.map(format_timestamp); - - view! { -
-
- "#" -
-
-
{format!("#{}", name)}
-
{summary}
-
- {format!("{} messages", format_number(channel.total_messages))} - {format!("{} flagged", format_number(channel.flagged_count))} - {last_seen.map(|t| view! { {format!("Last: {}", t)} })} -
-
-
- } -} - -#[component] -fn ListSkeleton() -> impl IntoView { - view! { -
- {(0..5).map(|_| view! { -
-
-
-
-
-
-
- }).collect::>()} -
- } -} - -fn format_number(value: u64) -> String { - let raw = value.to_string(); - let mut out = String::new(); - for (idx, ch) in raw.chars().rev().enumerate() { - if idx > 0 && idx % 3 == 0 { - out.push(','); - } - out.push(ch); - } - out.chars().rev().collect() -} - -fn format_timestamp(ts: i64) -> String { - let d = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64((ts as f64) * 1000.0)); - d.to_locale_date_string("en-US", &wasm_bindgen::JsValue::UNDEFINED) - .into() -} diff --git a/services/frontend/frontend/src/features/dashboard/components/mod.rs b/services/frontend/frontend/src/features/dashboard/components/mod.rs deleted file mode 100644 index 9461194..0000000 --- a/services/frontend/frontend/src/features/dashboard/components/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod channel_summary_list; -pub mod stats_overview; -pub mod user_summary_list; - -pub use channel_summary_list::ChannelSummaryList; -pub use stats_overview::StatsOverview; -pub use user_summary_list::UserSummaryList; diff --git a/services/frontend/frontend/src/features/dashboard/components/stats_overview.rs b/services/frontend/frontend/src/features/dashboard/components/stats_overview.rs deleted file mode 100644 index 1bb9d08..0000000 --- a/services/frontend/frontend/src/features/dashboard/components/stats_overview.rs +++ /dev/null @@ -1,150 +0,0 @@ -use leptos::prelude::*; -use shared_types::dashboard::{DashboardStats, TopChannel}; -#[component] -pub fn StatsOverview( - stats: Option, - loading: bool, - error: Option, - on_retry: Box, -) -> impl IntoView { - let retry = StoredValue::new(on_retry); - - view! { -
- {move || { - if loading { - view! { }.into_any() - } else if let Some(err) = error.clone() { - view! { -
-
"⚠"
-

{err}

- -
- }.into_any() - } else if let Some(stats) = stats.clone() { - view! { -
- - - - - - - - - -
-
-
"Top Channels"
-
-
- -
-
- -
-
-
"Moderation Queue"
-
-
-
- - - -
-
-
-
- }.into_any() - } else { - view! { -
-

"No dashboard data available yet."

-
- }.into_any() - } - }} -
- } -} - -#[component] -fn MetricCard( - label: &'static str, - value: u64, - icon: &'static str, - tone: &'static str, -) -> impl IntoView { - view! { -
-
-
-
{label}
-
{format_number(value)}
-
-
{icon}
-
-
- } -} - -#[component] -fn QueueMetric(label: &'static str, value: u64, tone: &'static str) -> impl IntoView { - view! { -
-
{format_number(value)}
-
{label}
-
- } -} - -#[component] -fn TopChannels(channels: Vec) -> impl IntoView { - if channels.is_empty() { - return view! {

"No channel data yet."

}.into_any(); - } - - view! { -
- {channels.into_iter().map(|ch| { - let name = ch.channel_name.unwrap_or_else(|| ch.channel_id.clone()); - view! { -
- {format!("#{}", name)} - {format_number(ch.message_count)} -
- } - }).collect::>()} -
- } - .into_any() -} - -#[component] -fn StatsSkeleton() -> impl IntoView { - view! { -
- {(0..8).map(|_| view! { -
-
-
-
- }).collect::>()} -
- } -} - -fn format_number(value: u64) -> String { - let raw = value.to_string(); - let mut out = String::new(); - for (idx, ch) in raw.chars().rev().enumerate() { - if idx > 0 && idx % 3 == 0 { - out.push(','); - } - out.push(ch); - } - out.chars().rev().collect() -} diff --git a/services/frontend/frontend/src/features/dashboard/components/user_summary_list.rs b/services/frontend/frontend/src/features/dashboard/components/user_summary_list.rs deleted file mode 100644 index 8543391..0000000 --- a/services/frontend/frontend/src/features/dashboard/components/user_summary_list.rs +++ /dev/null @@ -1,148 +0,0 @@ -use leptos::prelude::*; -use shared_types::dashboard::DashboardUser; - -#[component] -pub fn UserSummaryList( - users: Vec, - loading: bool, - error: Option, - search: String, - has_more: bool, - on_search_change: Box, - on_load_more: Box, - on_retry: Box, -) -> impl IntoView { - let search_cb = StoredValue::new(on_search_change); - let load_more_cb = StoredValue::new(on_load_more); - let retry_cb = StoredValue::new(on_retry); - - view! { -
-
-
"Pengguna"
-

"Ringkasan aktivitas dan trust score pengguna."

-
-
-
- -
- - {move || { - if loading && users.is_empty() { - view! { }.into_any() - } else if let Some(err) = error.clone() { - view! { -
-
"⚠"
-

{err}

- -
- }.into_any() - } else if users.is_empty() { - view! { -
-
"👤"
-

"No users found."

-
- }.into_any() - } else { - view! { -
- {users.clone().into_iter().map(|user| view! { - - }).collect::>()} -
- }.into_any() - } - }} - - {move || { - (has_more && !loading).then(|| view! { -
- -
- }) - }} -
-
- } -} - -#[component] -fn UserRow(user: DashboardUser) -> impl IntoView { - let name = user - .username - .clone() - .unwrap_or_else(|| user.user_id.clone()); - let summary = user - .profile_summary - .clone() - .unwrap_or_else(|| format!("{} messages", format_number(user.total_messages))); - let trust = user.trust_score.map(|score| format!("Trust: {:.2}", score)); - let last_seen = user.last_message_at.map(format_timestamp); - - view! { -
-
- {if let Some(url) = user.avatar_url.clone() { - view! { }.into_any() - } else { - view! { "👤" }.into_any() - }} -
-
-
{name}
-
{summary}
-
- {format!("{} flagged", format_number(user.flagged_count))} - {trust.map(|t| view! { {t} })} - {last_seen.map(|t| view! { {format!("Last: {}", t)} })} -
-
-
- } -} - -#[component] -fn ListSkeleton() -> impl IntoView { - view! { -
- {(0..5).map(|_| view! { -
-
-
-
-
-
-
- }).collect::>()} -
- } -} - -fn format_number(value: u64) -> String { - let raw = value.to_string(); - let mut out = String::new(); - for (idx, ch) in raw.chars().rev().enumerate() { - if idx > 0 && idx % 3 == 0 { - out.push(','); - } - out.push(ch); - } - out.chars().rev().collect() -} - -fn format_timestamp(ts: i64) -> String { - let d = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64((ts as f64) * 1000.0)); - d.to_locale_date_string("en-US", &wasm_bindgen::JsValue::UNDEFINED) - .into() -} diff --git a/services/frontend/frontend/src/features/dashboard/mod.rs b/services/frontend/frontend/src/features/dashboard/mod.rs deleted file mode 100644 index 52f0da7..0000000 --- a/services/frontend/frontend/src/features/dashboard/mod.rs +++ /dev/null @@ -1,276 +0,0 @@ -pub mod components; - -use components::{ChannelSummaryList, StatsOverview, UserSummaryList}; -use leptos::prelude::*; -use shared_types::dashboard::{DashboardChannel, DashboardStats, DashboardUser}; -use std::sync::Arc; -use wasm_bindgen_futures::spawn_local; -use crate::{log_error, log_info, make_logger}; - -make_logger!(); - -#[derive(Clone, PartialEq)] -enum DashboardTab { - Stats, - Users, - Channels, -} - -#[component] -pub fn DashboardPanel() -> impl IntoView { - let active_tab = RwSignal::new(DashboardTab::Stats); - - let stats = RwSignal::new(None::); - let stats_loading = RwSignal::new(false); - let stats_error = RwSignal::new(None::); - - let users = RwSignal::new(Vec::::new()); - let users_loading = RwSignal::new(false); - let users_error = RwSignal::new(None::); - let users_search = RwSignal::new(String::new()); - let users_cursor = RwSignal::new(None::); - - let channels = RwSignal::new(Vec::::new()); - let channels_loading = RwSignal::new(false); - let channels_error = RwSignal::new(None::); - let channels_search = RwSignal::new(String::new()); - let channels_cursor = RwSignal::new(None::); - - let fetch_stats: Arc = Arc::new(move || { - stats_loading.set(true); - stats_error.set(None); - log_info!("Dashboard fetching stats..."); - spawn_local(async move { - match crate::api::dashboard::get_dashboard_stats().await { - Ok(data) => { - log_info!("Dashboard stats loaded: {} messages", data.total_messages); - stats.set(Some(data)); - } - Err(err) => { - log_error!("Dashboard stats error: {}", err); - stats_error.set(Some(format!("Failed to load stats: {}", err))); - } - } - stats_loading.set(false); - }); - }); - - let fetch_users: Arc = Arc::new(move |reset: bool| { - if users_loading.get() { - return; - } - users_loading.set(true); - users_error.set(None); - log_info!("Dashboard fetching users..."); - - let cursor = if reset { None } else { users_cursor.get() }; - let search = users_search.get(); - spawn_local(async move { - let search_ref = (!search.trim().is_empty()).then_some(search.trim()); - match crate::api::dashboard::get_dashboard_users( - Some(20), - cursor.as_deref(), - search_ref, - ) - .await - { - Ok(page) => { - log_info!("Dashboard users loaded: {} users", page.data.len()); - if reset { - users.set(page.data); - } else { - let mut current = users.get(); - current.extend(page.data); - users.set(current); - } - users_cursor.set(page.next_cursor); - } - Err(err) => { - log_error!("Dashboard users error: {}", err); - users_error.set(Some(format!("Failed to load users: {}", err))); - } - } - users_loading.set(false); - }); - }); - - let fetch_channels: Arc = Arc::new(move |reset: bool| { - if channels_loading.get() { - return; - } - channels_loading.set(true); - channels_error.set(None); - log_info!("Dashboard fetching channels..."); - - let cursor = if reset { None } else { channels_cursor.get() }; - let search = channels_search.get(); - spawn_local(async move { - let search_ref = (!search.trim().is_empty()).then_some(search.trim()); - match crate::api::dashboard::get_dashboard_channels( - Some(20), - cursor.as_deref(), - search_ref, - None, - ) - .await - { - Ok(page) => { - log_info!("Dashboard channels loaded: {} channels", page.data.len()); - if reset { - channels.set(page.data); - } else { - let mut current = channels.get(); - current.extend(page.data); - channels.set(current); - } - channels_cursor.set(page.next_cursor); - } - Err(err) => { - log_error!("Dashboard channels error: {}", err); - channels_error.set(Some(format!("Failed to load channels: {}", err))); - } - } - channels_loading.set(false); - }); - }); - - // Initial fetch on mount (use spawn_local to avoid reactive dependency tracking) - { - let fetch_stats = fetch_stats.clone(); - let fetch_users = fetch_users.clone(); - let fetch_channels = fetch_channels.clone(); - spawn_local(async move { - fetch_stats(); - fetch_users(true); - fetch_channels(true); - }); - } - - view! { -
-
-
-

"Dashboard Guild"

-

- "Pantau statistik, profil pengguna, dan aktivitas kanal komunitas IMPHNEN." -

-
-
- -
-
- - - -
- -
- {move || { - let on_retry = { - let fetch_stats = fetch_stats.clone(); - Box::new(move || fetch_stats()) - }; - view! { - - } - }} -
- -
- {move || { - let on_search_change = { - let fetch_users = fetch_users.clone(); - Box::new(move |value| { - users_search.set(value); - users_cursor.set(None); - fetch_users(true); - }) - }; - let on_load_more = { - let fetch_users = fetch_users.clone(); - Box::new(move || fetch_users(false)) - }; - let on_retry = { - let fetch_users = fetch_users.clone(); - Box::new(move || fetch_users(true)) - }; - view! { - - } - }} -
- -
- {move || { - let on_search_change = { - let fetch_channels = fetch_channels.clone(); - Box::new(move |value| { - channels_search.set(value); - channels_cursor.set(None); - fetch_channels(true); - }) - }; - let on_load_more = { - let fetch_channels = fetch_channels.clone(); - Box::new(move || fetch_channels(false)) - }; - let on_retry = { - let fetch_channels = fetch_channels.clone(); - Box::new(move || fetch_channels(true)) - }; - view! { - - } - }} -
-
-
- } -} - -#[component] -fn DashboardTabButton( - tab: DashboardTab, - active_tab: RwSignal, - label: &'static str, - icon: &'static str, -) -> impl IntoView { - let tab_for_class = tab.clone(); - let tab_for_aria = tab.clone(); - let tab_for_click = tab; - - view! { - - } -} diff --git a/services/frontend/frontend/src/features/live/audio/mod.rs b/services/frontend/frontend/src/features/live/audio/mod.rs deleted file mode 100644 index a3c78c4..0000000 --- a/services/frontend/frontend/src/features/live/audio/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod pcm_decoder; -pub mod ring_buffer; diff --git a/services/frontend/frontend/src/features/live/audio/pcm_decoder.rs b/services/frontend/frontend/src/features/live/audio/pcm_decoder.rs deleted file mode 100644 index 0a6b271..0000000 --- a/services/frontend/frontend/src/features/live/audio/pcm_decoder.rs +++ /dev/null @@ -1,70 +0,0 @@ -use wasm_bindgen::prelude::*; - -/// PCM Frame decoded from binary WebSocket data -/// Format: [u32 userId (4 bytes)][i16 samples (N bytes)] -pub struct PcmFrame { - pub user_id: u32, - pub samples: Vec, // Normalized to [-1.0, 1.0] -} - -/// Decode a binary WebSocket message into PCM frames -/// Returns None if data is too short or malformed -pub fn decode_pcm_frame(data: &[u8]) -> Option { - if data.len() < 4 { - return None; - } - - let user_id = u32::from_le_bytes([data[0], data[1], data[2], data[3]]); - let sample_bytes = &data[4..]; - let sample_count = sample_bytes.len() / 2; - - if sample_count == 0 { - return None; - } - - let samples = decode_i16_samples(sample_bytes); - Some(PcmFrame { user_id, samples }) -} - -/// Decode raw i16 PCM bytes to normalized f32 samples [-1.0, 1.0] -pub fn decode_i16_samples(data: &[u8]) -> Vec { - let count = data.len() / 2; - let mut out = Vec::with_capacity(count); - - for i in 0..count { - let offset = i * 2; - if offset + 1 < data.len() { - let sample = i16::from_le_bytes([data[offset], data[offset + 1]]); - out.push((sample as f32) / 32768.0); - } - } - - out -} - -/// Encode f32 samples [-1.0, 1.0] to base64 for WebSocket transmission -/// Uses JavaScript btoa for encoding -pub fn encode_samples_to_base64(samples: &[f32]) -> String { - // Convert f32 samples to i16 bytes - let mut bytes = Vec::with_capacity(samples.len() * 2); - for &sample in samples { - let clamped = sample.clamp(-1.0, 1.0); - let int_sample = (clamped * 32767.0) as i16; - bytes.extend_from_slice(&int_sample.to_le_bytes()); - } - encode_bytes_base64(&bytes) -} - -/// Encode raw bytes to base64 using JavaScript's btoa via wasm-bindgen -fn encode_bytes_base64(data: &[u8]) -> String { - // Convert bytes 0-255 to a Latin-1 string (each byte → char with same codepoint) - let latin1: String = data.iter().map(|&b| b as char).collect(); - js_btoa(&latin1) -} - -/// Direct wasm-bindgen binding to the browser's btoa function -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(js_name = btoa)] - fn js_btoa(input: &str) -> String; -} diff --git a/services/frontend/frontend/src/features/live/audio/ring_buffer.rs b/services/frontend/frontend/src/features/live/audio/ring_buffer.rs deleted file mode 100644 index a806b47..0000000 --- a/services/frontend/frontend/src/features/live/audio/ring_buffer.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::sync::{Arc, Mutex}; - -/// AudioRingBuffer — Fixed-size circular buffer for real-time PCM streaming -/// Provides thread-safe write/read with automatic overwrite protection -pub struct AudioRingBuffer { - buffer: Vec, - capacity: usize, - write_pos: usize, - read_pos: usize, - available: usize, -} - -impl AudioRingBuffer { - /// Create a new ring buffer with given capacity (in samples) - pub fn new(capacity: usize) -> Self { - Self { - buffer: vec![0.0; capacity], - capacity, - write_pos: 0, - read_pos: 0, - available: 0, - } - } - - /// Write samples to the ring buffer. Overwrites oldest data if full. - pub fn write(&mut self, samples: &[f32]) { - let mut written = 0; - while written < samples.len() { - let chunk = (samples.len() - written).min(self.capacity - self.write_pos); - let src = &samples[written..written + chunk]; - let dest = &mut self.buffer[self.write_pos..self.write_pos + chunk]; - dest.copy_from_slice(src); - written += chunk; - self.write_pos = (self.write_pos + chunk) % self.capacity; - self.available = (self.available + chunk).min(self.capacity); - // If we overwrote unread data, advance read_pos - if self.available == self.capacity { - self.read_pos = self.write_pos; - } - } - } - - /// Read up to `max_samples` from the buffer. Returns the samples read. - pub fn read(&mut self, max_samples: usize) -> Vec { - let to_read = max_samples.min(self.available); - let mut out = Vec::with_capacity(to_read); - let mut remaining = to_read; - - while remaining > 0 { - let chunk = remaining.min(self.capacity - self.read_pos); - out.extend_from_slice(&self.buffer[self.read_pos..self.read_pos + chunk]); - remaining -= chunk; - self.read_pos = (self.read_pos + chunk) % self.capacity; - } - - self.available -= to_read; - out - } - - /// Number of samples available to read - pub fn available_samples(&self) -> usize { - self.available - } - - /// Clear all buffered data - pub fn clear(&mut self) { - self.write_pos = 0; - self.read_pos = 0; - self.available = 0; - } -} - -/// Thread-safe wrapper around AudioRingBuffer -pub struct SharedRingBuffer { - inner: Arc>, -} - -impl SharedRingBuffer { - pub fn new(capacity: usize) -> Self { - Self { - inner: Arc::new(Mutex::new(AudioRingBuffer::new(capacity))), - } - } - - pub fn write(&self, samples: &[f32]) { - if let Ok(mut guard) = self.inner.lock() { - guard.write(samples); - } - } - - pub fn read(&self, max_samples: usize) -> Vec { - if let Ok(mut guard) = self.inner.lock() { - guard.read(max_samples) - } else { - Vec::new() - } - } - - pub fn available_samples(&self) -> usize { - if let Ok(guard) = self.inner.lock() { - guard.available_samples() - } else { - 0 - } - } - - pub fn clear(&self) { - if let Ok(mut guard) = self.inner.lock() { - guard.clear(); - } - } - - pub fn clone_inner(&self) -> Arc> { - self.inner.clone() - } -} - -impl Clone for SharedRingBuffer { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - } - } -} diff --git a/services/frontend/frontend/src/features/live/components/active_speakers.rs b/services/frontend/frontend/src/features/live/components/active_speakers.rs deleted file mode 100644 index bda43fa..0000000 --- a/services/frontend/frontend/src/features/live/components/active_speakers.rs +++ /dev/null @@ -1,80 +0,0 @@ -use leptos::prelude::*; -use shared_types::voice::ActiveSpeaker; - -/// ActiveSpeakers component for Leptos -/// Displays a real-time list of speaking users with avatar and status indicator -#[component] -pub fn ActiveSpeakers( - #[prop(optional)] speakers: RwSignal>, - #[prop(optional)] class: &'static str, -) -> impl IntoView { - let empty_state = move || speakers.get().is_empty(); - - view! { -
- - -
-
- {speaker.avatar.as_ref().map(|avatar_url| { - let url = avatar_url.clone(); - view! { - - } - })} -
-
-
- {speaker.username.clone()} -
-
- - - {move || if speaker.speaking { "Speaking" } else { "Silent" }} - -
-
-
-
-
- } - } - > -
-
-
- "🎤" -
-

- "No active speakers" -

-
-
- - - } -} diff --git a/services/frontend/frontend/src/features/live/components/audio_visualizer.rs b/services/frontend/frontend/src/features/live/components/audio_visualizer.rs deleted file mode 100644 index cd3e557..0000000 --- a/services/frontend/frontend/src/features/live/components/audio_visualizer.rs +++ /dev/null @@ -1,79 +0,0 @@ -use leptos::prelude::*; -use std::sync::{Arc, Mutex}; - -/// AudioVisualizer — Real-time 32-bar frequency spectrum display -/// Simplified implementation using CSS bars updated via signals -#[component] -pub fn AudioVisualizer( - #[prop(default = true)] _active: bool, - #[prop(optional)] pcm_data: Option>>>, -) -> impl IntoView { - let bars = RwSignal::new(vec![0.0; 32]); - let (tick, set_tick) = signal(0u32); - - // Drive periodic updates: increment tick every 100ms - wasm_bindgen_futures::spawn_local(async move { - loop { - gloo_timers::future::TimeoutFuture::new(100).await; - set_tick.update(|t| *t = t.wrapping_add(1)); - } - }); - - // Effect reacts to tick changes, updating bars from PCM data each frame - Effect::new(move |_| { - tick.get(); // Track — Effect re-runs on each tick (every 100ms) - if let Some(ref pcm_arc) = pcm_data { - if let Ok(pcm_vec) = pcm_arc.lock() { - let computed = compute_frequency_bands(&pcm_vec); - bars.update(|b| { - for (i, band) in b.iter_mut().enumerate() { - let target = computed.get(i).copied().unwrap_or(0.0).clamp(0.0, 1.0); - *band = *band * 0.7 + target * 0.3; // Smooth decay - } - }); - } - } - }); - - view! { -
-
- {(0..32).map(|i| { - view! { -
- } - }).collect::>()} -
-
- } -} - -/// Compute 32-band frequency spectrum from PCM samples -fn compute_frequency_bands(pcm_samples: &[f32]) -> Vec { - let mut bands = vec![0.0; 32]; - - if pcm_samples.is_empty() { - return bands; - } - - let samples_per_band = (pcm_samples.len() / 32).max(1); - - for (band_idx, band) in bands.iter_mut().enumerate() { - let start = band_idx * samples_per_band; - let end = ((band_idx + 1) * samples_per_band).min(pcm_samples.len()); - - if start < pcm_samples.len() { - let slice = &pcm_samples[start..end]; - let rms = (slice.iter().map(|s| s * s).sum::() / slice.len() as f32).sqrt(); - *band = rms.min(1.0); - } - } - - bands -} diff --git a/services/frontend/frontend/src/features/live/components/mic_level_meter.rs b/services/frontend/frontend/src/features/live/components/mic_level_meter.rs deleted file mode 100644 index 50c2ee6..0000000 --- a/services/frontend/frontend/src/features/live/components/mic_level_meter.rs +++ /dev/null @@ -1,94 +0,0 @@ -use leptos::prelude::*; -use std::sync::{Arc, Mutex}; - -/// MicLevelMeter — Horizontal level indicator for microphone input -/// Displays 0-100% amplitude as a filling bar with smooth decay -#[component] -pub fn MicLevelMeter( - #[prop(default = true)] active: bool, - #[prop(optional)] pcm_data: Option>>>, - #[prop(optional)] label: Option<&'static str>, -) -> impl IntoView { - let level = RwSignal::new(0.0f32); - let peak = RwSignal::new(0.0f32); - let (tick, set_tick) = signal(0u32); - - // Drive periodic updates: increment tick every 100ms - wasm_bindgen_futures::spawn_local(async move { - loop { - gloo_timers::future::TimeoutFuture::new(100).await; - set_tick.update(|t| *t = t.wrapping_add(1)); - } - }); - - // Effect reacts to tick changes, updating level from PCM data each frame - Effect::new(move |_| { - tick.get(); // Track — Effect re-runs on each tick - if !active { - return; - } - - if let Some(ref pcm_arc) = pcm_data { - if let Ok(pcm_vec) = pcm_arc.lock() { - let current_level = compute_rms(&pcm_vec); - level.update(|l| { - *l = *l * 0.8 + current_level * 0.2; // Smooth decay - }); - peak.update(|p| { - *p = (*p * 0.95).max(current_level); // Peak hold with decay - }); - } - } - }); - - let level_percent = move || (level.get() * 100.0).min(100.0); - let peak_percent = move || (peak.get() * 100.0).min(100.0); - - // Determine color based on level - let level_color = move || { - let l = level.get(); - if l < 0.5 { - "bg-green-500" - } else if l < 0.75 { - "bg-yellow-500" - } else { - "bg-red-500" - } - }; - - view! { -
- {label.map(|l| view! { - - })} -
- {/* Main level bar */} -
-
- {/* Peak indicator */} -
-
- {/* Percentage display */} - - {move || format!("{}%", (level_percent() as u8))} - -
-
- } -} - -/// Compute RMS (Root Mean Square) amplitude from PCM samples -/// Returns normalized value 0.0-1.0 -fn compute_rms(samples: &[f32]) -> f32 { - if samples.is_empty() { - return 0.0; - } - let mean_square = samples.iter().map(|s| s * s).sum::() / samples.len() as f32; - mean_square.sqrt() -} diff --git a/services/frontend/frontend/src/features/live/components/mod.rs b/services/frontend/frontend/src/features/live/components/mod.rs deleted file mode 100644 index abc5cdd..0000000 --- a/services/frontend/frontend/src/features/live/components/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -pub mod active_speakers; -pub mod audio_visualizer; -pub mod mic_level_meter; -pub mod music_sub_panel; -pub mod now_playing; -pub mod recordings_sub_panel; -pub mod screen_sub_panel; -pub mod voice_connection_card; -pub mod waveform_player; - -pub use active_speakers::ActiveSpeakers; -pub use audio_visualizer::AudioVisualizer; -pub use mic_level_meter::MicLevelMeter; -pub use music_sub_panel::MusicSubPanel; -pub use now_playing::NowPlaying; -pub use recordings_sub_panel::RecordingsSubPanel; -pub use screen_sub_panel::ScreenSubPanel; -pub use voice_connection_card::VoiceConnectionCard; -pub use waveform_player::WaveformPlayer; diff --git a/services/frontend/frontend/src/features/live/components/music_sub_panel.rs b/services/frontend/frontend/src/features/live/components/music_sub_panel.rs deleted file mode 100644 index 8e9e526..0000000 --- a/services/frontend/frontend/src/features/live/components/music_sub_panel.rs +++ /dev/null @@ -1,53 +0,0 @@ -use leptos::prelude::*; - -/// MusicSubPanel — Music playlist controls and URL input -#[component] -pub fn MusicSubPanel( - #[prop(optional)] on_queue: Option>, -) -> impl IntoView { - let (url_input, set_url_input) = signal::(String::new()); - - let handle_queue_click = move |_| { - let url = url_input.get_untracked().trim().to_string(); - if !url.is_empty() { - if let Some(ref cb) = on_queue { - cb(url.clone()); - set_url_input.set(String::new()); - } - } - }; - - view! { -
-
-
- - - - - "Music" -
-
-
-
- - -
- -
-
- } -} diff --git a/services/frontend/frontend/src/features/live/components/now_playing.rs b/services/frontend/frontend/src/features/live/components/now_playing.rs deleted file mode 100644 index 1ddc888..0000000 --- a/services/frontend/frontend/src/features/live/components/now_playing.rs +++ /dev/null @@ -1,106 +0,0 @@ -use leptos::prelude::*; -use shared_types::media::MediaState; - -/// NowPlaying — Displays current media item and queue info -/// Accepts an optional RwSignal to enable real-time updates from WebSocket events. -#[component] -pub fn NowPlaying( - #[prop(optional)] media_rw: Option>>, - #[prop(optional)] on_skip: Option>, - #[prop(optional)] on_stop: Option>, -) -> impl IntoView { - // Use provided signal, or fall back to a local one for static usage - let media_state = media_rw.unwrap_or_else(|| RwSignal::new(None)); - - // Wrap callbacks in StoredValue for shareable non-Clone ownership in Leptos context - let skip_cb = StoredValue::new(on_skip); - let stop_cb = StoredValue::new(on_stop); - let has_skip = skip_cb.with_value(|v| v.is_some()); - let has_stop = stop_cb.with_value(|v| v.is_some()); - - view! { -
-
-
"Now Playing"
-
-
- {move || { - media_state.get().map(|ms| { - let current = ms.current.as_ref().cloned(); - let queue_len = ms.queue.len(); - - view! { - <> - {current.map(|item| { - let title = item.title.clone().unwrap_or_else(|| "Unknown".to_string()); - let duration_ms = item.duration_ms.unwrap_or(0); - let duration_sec = duration_ms / 1000; - view! { -
-
- {title} -
-
- {format!("{}s", duration_sec)} -
-
- {has_skip.then(|| { - view! { - - } - })} - {has_stop.then(|| { - view! { - - } - })} -
-
- } - })} - - {(queue_len > 0).then(|| { - view! { -
-
- {format!("Queue: {} item{}", queue_len, if queue_len == 1 { "" } else { "s" })} -
-
- } - })} - - {(queue_len == 0).then(|| { - view! { -
- "Queue is empty" -
- } - })} - - } - }) - }} - - {move || { - media_state.get().is_none().then(|| { - view! { -
- "No media connected" -
- } - }) - }} -
-
- } -} diff --git a/services/frontend/frontend/src/features/live/components/recordings_sub_panel.rs b/services/frontend/frontend/src/features/live/components/recordings_sub_panel.rs deleted file mode 100644 index 34375bb..0000000 --- a/services/frontend/frontend/src/features/live/components/recordings_sub_panel.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::api::recordings::{delete_recording, get_recordings}; -use leptos::prelude::*; -use shared_types::recording::VoiceRecording; - -/// RecordingsSubPanel — Paginated list of voice recordings -/// Accepts an optional refresh_trigger signal to reload when a new recording is uploaded. -#[component] -pub fn RecordingsSubPanel( - #[prop(optional)] refresh_trigger: Option>, -) -> impl IntoView { - let recordings = RwSignal::new(Vec::::new()); - let loading = RwSignal::new(false); - let has_more = RwSignal::new(true); - let next_cursor = RwSignal::new(None::); - - // Load recordings - let load = move |reset: bool| { - if loading.get_untracked() { - return; - } - loading.set(true); - - let cursor_val = if reset { - None - } else { - next_cursor.get_untracked() - }; - wasm_bindgen_futures::spawn_local({ - async move { - match get_recordings(Some(20), cursor_val.as_deref()).await { - Ok(resp) => { - if reset { - recordings.set(resp.items); - } else { - let mut current = recordings.get_untracked(); - current.extend(resp.items); - recordings.set(current); - } - has_more.set(resp.has_more); - next_cursor.set(resp.next_cursor); - } - Err(_) => { - if reset { - recordings.set(Vec::new()); - } - } - } - loading.set(false); - } - }); - }; - - // Load on mount, and reload when refresh_trigger changes (e.g., new recording uploaded) - Effect::new(move |_| { - if let Some(trigger) = refresh_trigger { - trigger.get(); // Track — re-run when WS signals a new recording - } - load(true); - }); - - // Delete recording handler - let do_delete = move |id: String| { - wasm_bindgen_futures::spawn_local({ - let id = id.clone(); - async move { - let _ = delete_recording(&id).await; - recordings.update(|r| r.retain(|rec| rec.id != id)); - } - }); - }; - - view! { -
-
-
- - - - - - - "Recordings" -
-

"Voice channel recordings from all sessions."

-
-
- {move || { - let recs = recordings.get(); - if recs.is_empty() && !loading.get() { - view! { -
-

"No recordings yet."

-

"Join a voice channel to start recording."

-
- }.into_any() - } else { - view! { -
- {recs.iter().map(|rec| { - let id = rec.id.clone(); - let username = rec.username.clone(); - let channel_name = rec.channel_name.clone().unwrap_or_default(); - let created_at = format_timestamp(rec.created_at); - let has_url = rec.download_url.is_some(); - let url = rec.download_url.clone().unwrap_or_default(); - - view! { -
-
-
{username}
-
- {channel_name} - "·" - {format_size(rec.size_bytes)} - "·" - {created_at} -
-
-
- {has_url.then(|| { - view! { - - "Download" - - } - })} - -
-
- } - }).collect::>()} -
- }.into_any() - } - }} - - {move || { - (has_more.get() && !loading.get()).then(|| { - view! { - - } - }) - }} -
-
- } -} - -/// Format file size bytes to human readable -fn format_size(bytes: u64) -> String { - if bytes < 1024 { - format!("{} B", bytes) - } else if bytes < 1024 * 1024 { - format!("{:.1} KB", bytes as f64 / 1024.0) - } else { - format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0)) - } -} - -/// Format timestamp i64 to readable date -fn format_timestamp(ts: i64) -> String { - let d = js_sys::Date::new(&wasm_bindgen::JsValue::from_f64((ts as f64) * 1000.0)); - d.to_locale_date_string("en-US", &wasm_bindgen::JsValue::UNDEFINED) - .into() -} diff --git a/services/frontend/frontend/src/features/live/components/screen_sub_panel.rs b/services/frontend/frontend/src/features/live/components/screen_sub_panel.rs deleted file mode 100644 index 302d4e1..0000000 --- a/services/frontend/frontend/src/features/live/components/screen_sub_panel.rs +++ /dev/null @@ -1,83 +0,0 @@ -use leptos::prelude::*; - -/// ScreenSubPanel — Screenshare controls -#[component] -pub fn ScreenSubPanel( - #[prop(optional)] on_start_stream: Option>, - #[prop(optional)] on_stop_stream: Option>, -) -> impl IntoView { - let (is_streaming, set_is_streaming) = signal::(false); - - let has_start = on_start_stream.is_some(); - let has_stop = on_stop_stream.is_some(); - - view! { -
-
-
- - - - - - "Screenshare" -
-
-
-

- "Stream your screen to the voice channel for everyone to see." -

- -
- {has_start.then(|| { - view! { - - } - })} - - {has_stop.then(|| { - view! { - - } - })} -
- - {move || { - is_streaming.get().then(|| { - view! { -
- "🔴 Live streaming..." -
- } - }) - }} -
-
- } -} diff --git a/services/frontend/frontend/src/features/live/components/voice_connection_card.rs b/services/frontend/frontend/src/features/live/components/voice_connection_card.rs deleted file mode 100644 index ef2ba53..0000000 --- a/services/frontend/frontend/src/features/live/components/voice_connection_card.rs +++ /dev/null @@ -1,220 +0,0 @@ -use crate::features::live::hooks::use_voice_control::{use_voice_control, VoiceControlState}; -use leptos::prelude::*; -use wasm_bindgen::JsCast; - -/// VoiceConnectionCard component for Leptos -/// Renders guild and voice channel selectors with connect/disconnect controls -#[component] -pub fn VoiceConnectionCard( - #[prop(optional)] voice_state: Option, - #[prop(optional)] class: &'static str, -) -> impl IntoView { - let default_state = use_voice_control(); - let state = voice_state.unwrap_or(default_state); - - // Reactive signal for selected guild - let (selected_guild, set_selected_guild) = signal::(String::new()); - // Reactive signal for selected channel - let (selected_channel, set_selected_channel) = signal::(String::new()); - - // When guild is selected, load voice channels - Effect::new(move |_| { - let guild_id = selected_guild.get(); - if !guild_id.is_empty() { - (state.load_voice_channels)(guild_id); - } - }); - - // Load guilds on mount - Effect::new(move |_| { - (state.load_guilds)(); - }); - - let on_guild_change = move |ev: leptos::ev::Event| { - if let Some(target) = ev.target() { - if let Ok(select_el) = target.dyn_into::() { - set_selected_guild.set(select_el.value()); - } - } - }; - - let on_channel_change = move |ev: leptos::ev::Event| { - if let Some(target) = ev.target() { - if let Ok(select_el) = target.dyn_into::() { - set_selected_channel.set(select_el.value()); - } - } - }; - - let on_join_click = move |_| { - let guild_id = selected_guild.get(); - let channel_id = selected_channel.get(); - if !guild_id.is_empty() && !channel_id.is_empty() { - (state.join_voice)(guild_id, channel_id); - } - }; - - let on_disconnect_click = move |_| { - (state.leave_voice)(); - }; - - // Read signals for reactive rendering - let guilds = state.guilds; - let voice_channels = state.voice_channels; - let loading = state.loading; - let error = state.error; - let voice_status = state.voice_status; - - let is_connected = move || voice_status.get().map(|s| s.connected).unwrap_or(false); - - let can_join = move || { - !selected_guild.get().is_empty() && !selected_channel.get().is_empty() && !loading.get() - }; - - let can_disconnect = move || is_connected() && !loading.get(); - - view! { -
-
-
- - - -

"Voice Bridge"

-
-

- "Join a Discord voice channel, listen, and transmit audio." -

- - {/* Guild and Channel Selectors */} -
-
- - -
- -
- - -
-
- - {/* Error Display */} - {move || { - error.get().map(|err| { - view! { -
- {err} -
- } - }) - }} - - {/* Status Display */} - {move || { - voice_status.get().map(|status| { - let connected = status.connected; - let active_channel = status.active_channel_name.clone(); - view! { -
-
- - {if connected { "Connected" } else { "Disconnected" }} - - {active_channel.map(|name| { - view! { - - {format!(" - {}", name)} - - } - })} -
- } - }) - }} - - {/* Control Buttons */} -
- - - - - {move || { - if loading.get() { - view! { - - "Loading..." - - }.into_any() - } else { - let _: () = view! { <> }; - ().into_any() - } - }} -
-
-
- } -} diff --git a/services/frontend/frontend/src/features/live/components/waveform_player.rs b/services/frontend/frontend/src/features/live/components/waveform_player.rs deleted file mode 100644 index d154d01..0000000 --- a/services/frontend/frontend/src/features/live/components/waveform_player.rs +++ /dev/null @@ -1,103 +0,0 @@ -use leptos::prelude::*; -use wasm_bindgen::JsCast; - -/// WaveformPlayer — Audio player with waveform progress bar -#[component] -pub fn WaveformPlayer( - audio_url: String, - #[prop(default = "Recording".to_string())] title: String, -) -> impl IntoView { - let is_playing = RwSignal::new(false); - let current_time = RwSignal::new(0.0); - let duration = RwSignal::new(0.0); - let audio_id = format!("audio_{}", audio_url); - - // Clone audio_url for the audio element - let audio_src = audio_url.clone(); - let audio_src_for_id = audio_src.clone(); - - let toggle_play = move |_| { - let doc = web_sys::window().unwrap().document().unwrap(); - let audio_opt = doc.get_element_by_id(&format!("audio_{}", audio_src_for_id)); - if let Some(audio_el) = audio_opt { - if let Ok(audio) = audio_el.dyn_into::() { - if is_playing.get_untracked() { - let _ = audio.pause(); - is_playing.set(false); - } else { - if audio.ended() { - audio.set_current_time(0.0); - } - if audio.play().is_ok() { - is_playing.set(true); - } - } - } - } - }; - - let _ = audio_url; // Mark as used for the audio_id - - view! { -
- - -
-
-
- -
- - -
- {move || format_time(current_time.get())} - {title.clone()} -
-
-
- } -} - -fn progress_pct(current: f64, dur: f64) -> f64 { - if dur > 0.0 { - (current / dur * 100.0).min(100.0) - } else { - 0.0 - } -} - -fn format_time(secs: f64) -> String { - if !secs.is_finite() || secs < 0.0 { - return "00:00".to_string(); - } - let total = secs as u32; - format!("{:02}:{:02}", total / 60, total % 60) -} diff --git a/services/frontend/frontend/src/features/live/hooks/mod.rs b/services/frontend/frontend/src/features/live/hooks/mod.rs deleted file mode 100644 index e907635..0000000 --- a/services/frontend/frontend/src/features/live/hooks/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod use_audio_playback; -pub mod use_audio_transmit; -pub mod use_media_control; -pub mod use_voice_control; diff --git a/services/frontend/frontend/src/features/live/hooks/use_audio_playback.rs b/services/frontend/frontend/src/features/live/hooks/use_audio_playback.rs deleted file mode 100644 index ad846b6..0000000 --- a/services/frontend/frontend/src/features/live/hooks/use_audio_playback.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::features::live::audio::pcm_decoder::decode_pcm_frame; -use crate::features::live::audio::ring_buffer::SharedRingBuffer; -use leptos::prelude::*; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; - -/// AudioPlaybackState — Manages PCM audio playback from WebSocket binary frames -#[derive(Clone)] -pub struct AudioPlaybackState { - /// Ring buffer for incoming PCM data - pub buffer: SharedRingBuffer, - /// Whether playback is active - pub active: RwSignal, - /// Volume level (0.0-1.0) - pub volume: RwSignal, - /// Abort flag to stop the playback loop (prevents leak on teardown) - pub abort: Arc, -} - -/// Create and initialize audio playback state -pub fn use_audio_playback() -> AudioPlaybackState { - let buffer = SharedRingBuffer::new(44100 * 5); // 5 seconds at 44.1kHz - let active = RwSignal::new(false); - let volume = RwSignal::new(0.5); - - AudioPlaybackState { - buffer, - active, - volume, - abort: Arc::new(AtomicBool::new(false)), - } -} - -/// Process incoming binary data from WebSocket (PCM audio frame) -/// Format: [u32 userId (4 bytes)][i16 samples (N bytes)] -pub fn process_pcm_data(state: &AudioPlaybackState, data: Vec) { - if let Some(frame) = decode_pcm_frame(&data) { - state.buffer.write(&frame.samples); - } -} - -/// Start consuming the ring buffer and playing through AudioContext -/// The loop respects the abort flag in `AudioPlaybackState` for clean teardown. -pub fn start_playback(state: &AudioPlaybackState) { - if state.active.get_untracked() { - return; - } - state.active.set(true); - // Reset abort flag for a fresh start - state.abort.store(false, Ordering::Relaxed); - - let buffer = state.buffer.clone(); - let active = state.active; - let abort = state.abort.clone(); - - wasm_bindgen_futures::spawn_local(async move { - let ctx = match web_sys::AudioContext::new() { - Ok(ctx) => ctx, - Err(_) => { - active.set(false); - return; - } - }; - - let ctx_ref = &ctx; - let _ = ctx_ref.resume(); - - while active.get_untracked() && !abort.load(Ordering::Relaxed) { - let available = buffer.available_samples(); - if available >= 4410 { - // ~100ms worth at 44.1kHz - let samples = buffer.read(4410); - if !samples.is_empty() { - play_samples(&ctx, &samples); - } - } - let _ = gloo_timers::future::TimeoutFuture::new(50).await; - } - - let _ = ctx.close(); - }); -} - -/// Stop playback and clear buffer -pub fn stop_playback(state: &AudioPlaybackState) { - state.abort.store(true, Ordering::Relaxed); - state.active.set(false); - state.buffer.clear(); -} - -/// Play a chunk of PCM samples through AudioContext using AudioBufferSourceNode -fn play_samples(ctx: &web_sys::AudioContext, samples: &[f32]) { - let frame_count = samples.len() as u32; - let Ok(audio_buffer) = ctx.create_buffer(1, frame_count, ctx.sample_rate()) else { - return; - }; - - // Write samples into the buffer channel - let Ok(channel_data) = audio_buffer.get_channel_data(0) else { - return; - }; - - let len = samples.len().min(channel_data.len()); - if len == 0 { - return; - } - - // Copy samples directly to audio buffer channel - let _ = audio_buffer.copy_to_channel(&samples[..len], 0); - - // Create source and play - if let Ok(source) = ctx.create_buffer_source() { - source.set_buffer(Some(&audio_buffer)); - source.set_loop(false); - let _ = source.start(); - } -} diff --git a/services/frontend/frontend/src/features/live/hooks/use_audio_transmit.rs b/services/frontend/frontend/src/features/live/hooks/use_audio_transmit.rs deleted file mode 100644 index ba5007c..0000000 --- a/services/frontend/frontend/src/features/live/hooks/use_audio_transmit.rs +++ /dev/null @@ -1,79 +0,0 @@ -use leptos::prelude::*; -use wasm_bindgen::{JsCast, JsValue}; -use wasm_bindgen_futures::spawn_local; -use web_sys::{MediaStream, MediaStreamConstraints, MediaStreamTrack}; - -/// AudioTransmitState — Manages microphone capture state -pub struct AudioTransmitState { - pub active: RwSignal, - pub stream: StoredValue>, -} - -/// Create microphone transmit state -pub fn use_audio_transmit() -> AudioTransmitState { - let active = RwSignal::new(false); - let stream = StoredValue::new(None::); - AudioTransmitState { active, stream } -} - -/// Start microphone capture - requests getUserMedia and stores the stream -pub fn start_transmit(state: &AudioTransmitState) { - if state.active.get_untracked() { - return; - } - state.active.set(true); - - let constraints = MediaStreamConstraints::new(); - let _ = js_sys::Reflect::set( - &constraints, - &JsValue::from_str("audio"), - &JsValue::from_bool(true), - ); - - let window = match web_sys::window() { - Some(w) => w, - None => return, - }; - let media_devices = match window.navigator().media_devices() { - Ok(md) => md, - Err(_) => return, - }; - - let promise = match media_devices.get_user_media_with_constraints(&constraints) { - Ok(p) => p, - Err(_) => return, - }; - - // Clone signals before spawning async task to avoid reference escaping - let active_signal = state.active; - let stream_signal = state.stream; - - spawn_local(async move { - match wasm_bindgen_futures::JsFuture::from(promise).await { - Ok(val) => { - if let Ok(s) = val.dyn_into::() { - stream_signal.set_value(Some(s)); - } - } - Err(_) => { - active_signal.set(false); - } - } - }); -} - -/// Stop microphone transmission -pub fn stop_transmit(state: &AudioTransmitState) { - state.active.set(false); - state.stream.update_value(|s| { - if let Some(stream) = s.take() { - let tracks = stream.get_tracks(); - for i in 0..tracks.length() { - let track_val = tracks.get(i); - if let Ok(track) = track_val.dyn_into::() { - track.stop(); - } - } - } - }); -} diff --git a/services/frontend/frontend/src/features/live/hooks/use_media_control.rs b/services/frontend/frontend/src/features/live/hooks/use_media_control.rs deleted file mode 100644 index 165f14d..0000000 --- a/services/frontend/frontend/src/features/live/hooks/use_media_control.rs +++ /dev/null @@ -1,155 +0,0 @@ -use crate::api::voice::{get_media_status, media_queue, media_skip, media_stop, media_volume}; -use leptos::prelude::*; -use shared_types::media::MediaState; -use std::sync::Arc; -use wasm_bindgen_futures::spawn_local; - -/// Callback type for enqueue -pub type EnqueueCallback = Arc; -/// Callback type for skip_track -pub type SkipTrackCallback = Arc; -/// Callback type for stop_playback -pub type StopPlaybackCallback = Arc; -/// Callback type for set_volume -pub type SetVolumeCallback = Arc; -/// Callback type for refresh -pub type RefreshCallback = Arc; - -/// State returned by use_media_control hook -#[derive(Clone)] -pub struct MediaControlState { - /// Current media playback state - pub media_state: RwSignal>, - /// Whether we're currently loading data - pub loading: RwSignal, - /// Last error message if any - pub error: RwSignal>, - /// Enqueue media (source URL, mode: "music" or "screen") - pub enqueue: EnqueueCallback, - /// Skip to next track - pub skip_track: SkipTrackCallback, - /// Stop all playback - pub stop_playback: StopPlaybackCallback, - /// Set volume level (0.0 - 1.0) - pub set_volume: SetVolumeCallback, - /// Refresh media status from server - pub refresh: RefreshCallback, -} - -/// Hook to manage media playback state and controls -pub fn use_media_control() -> MediaControlState { - // Core signals - let media_state_signal = RwSignal::new(None::); - let loading_signal = RwSignal::new(false); - let error_signal = RwSignal::new(None::); - - // Enqueue media - let enqueue_impl = Arc::new(move |source: String, mode: String| { - spawn_local({ - let source = source.clone(); - let mode = mode.clone(); - async move { - error_signal.set(None); - loading_signal.set(true); - - match media_queue(&source, &mode).await { - Ok(state) => { - media_state_signal.set(Some(state)); - loading_signal.set(false); - } - Err(e) => { - error_signal.set(Some(format!("Failed to enqueue media: {}", e))); - loading_signal.set(false); - } - } - } - }); - }); - - // Skip to next track - let skip_track_impl = Arc::new(move || { - spawn_local(async move { - error_signal.set(None); - loading_signal.set(true); - - match media_skip().await { - Ok(state) => { - media_state_signal.set(Some(state)); - loading_signal.set(false); - } - Err(e) => { - error_signal.set(Some(format!("Failed to skip track: {}", e))); - loading_signal.set(false); - } - } - }); - }); - - // Stop all playback - let stop_playback_impl = Arc::new(move || { - spawn_local(async move { - error_signal.set(None); - loading_signal.set(true); - - match media_stop().await { - Ok(state) => { - media_state_signal.set(Some(state)); - loading_signal.set(false); - } - Err(e) => { - error_signal.set(Some(format!("Failed to stop playback: {}", e))); - loading_signal.set(false); - } - } - }); - }); - - // Set volume level - let set_volume_impl = Arc::new(move |volume: f64| { - spawn_local(async move { - error_signal.set(None); - loading_signal.set(true); - - match media_volume(volume).await { - Ok(state) => { - media_state_signal.set(Some(state)); - loading_signal.set(false); - } - Err(e) => { - error_signal.set(Some(format!("Failed to set volume: {}", e))); - loading_signal.set(false); - } - } - }); - }); - - // Refresh media status - let refresh_impl = Arc::new(move || { - spawn_local(async move { - error_signal.set(None); - loading_signal.set(true); - - match get_media_status().await { - Ok(state) => { - media_state_signal.set(Some(state)); - loading_signal.set(false); - } - Err(e) => { - error_signal.set(Some(format!("Failed to refresh media status: {}", e))); - loading_signal.set(false); - } - } - }); - }); - - MediaControlState { - media_state: media_state_signal, - loading: loading_signal, - error: error_signal, - enqueue: enqueue_impl, - skip_track: skip_track_impl, - stop_playback: stop_playback_impl, - set_volume: set_volume_impl, - refresh: refresh_impl, - } -} diff --git a/services/frontend/frontend/src/features/live/hooks/use_voice_control.rs b/services/frontend/frontend/src/features/live/hooks/use_voice_control.rs deleted file mode 100644 index a11c92e..0000000 --- a/services/frontend/frontend/src/features/live/hooks/use_voice_control.rs +++ /dev/null @@ -1,176 +0,0 @@ -use crate::api::voice::{ - connect_voice, disconnect_voice, get_guilds, get_text_channels, get_voice_channels, -}; -use leptos::prelude::*; -use shared_types::guild::{Channel, Guild}; -use shared_types::voice::VoiceStatus; -use std::sync::Arc; -use wasm_bindgen_futures::spawn_local; - -/// Callback type for join_voice -pub type JoinVoiceCallback = Arc; -/// Callback type for leave_voice -pub type LeaveVoiceCallback = Arc; -/// Callback type for load_guilds -pub type LoadGuildsCallback = Arc; -/// Callback type for load_voice_channels -pub type LoadVoiceChannelsCallback = Arc; -/// Callback type for load_text_channels -pub type LoadTextChannelsCallback = Arc; - -/// State returned by use_voice_control hook -#[derive(Clone)] -pub struct VoiceControlState { - /// List of available guilds - pub guilds: RwSignal>, - /// List of voice channels for current guild - pub voice_channels: RwSignal>, - /// List of text channels for current guild - pub text_channels: RwSignal>, - /// Current voice connection status - pub voice_status: RwSignal>, - /// Whether we're currently loading data - pub loading: RwSignal, - /// Last error message if any - pub error: RwSignal>, - /// Join a voice channel - pub join_voice: JoinVoiceCallback, - /// Leave the current voice channel - pub leave_voice: LeaveVoiceCallback, - /// Fetch list of guilds - pub load_guilds: LoadGuildsCallback, - /// Fetch voice channels for a guild - pub load_voice_channels: LoadVoiceChannelsCallback, - /// Fetch text channels for a guild - pub load_text_channels: LoadTextChannelsCallback, -} - -/// Hook to manage voice connection state and controls -pub fn use_voice_control() -> VoiceControlState { - // Core signals - let guilds_signal = RwSignal::new(Vec::::new()); - let voice_channels_signal = RwSignal::new(Vec::::new()); - let text_channels_signal = RwSignal::new(Vec::::new()); - let voice_status_signal = RwSignal::new(None::); - let loading_signal = RwSignal::new(false); - let error_signal = RwSignal::new(None::); - - // Join a voice channel - let join_voice_impl = Arc::new(move |guild_id: String, channel_id: String| { - spawn_local({ - let guild_id = guild_id.clone(); - let channel_id = channel_id.clone(); - async move { - error_signal.set(None); - loading_signal.set(true); - - match connect_voice(&guild_id, &channel_id).await { - Ok(status) => { - voice_status_signal.set(Some(status)); - loading_signal.set(false); - } - Err(e) => { - error_signal.set(Some(format!("Failed to join voice: {}", e))); - loading_signal.set(false); - } - } - } - }); - }); - - // Leave the current voice channel - let leave_voice_impl = Arc::new(move || { - spawn_local(async move { - error_signal.set(None); - loading_signal.set(true); - - match disconnect_voice().await { - Ok(status) => { - voice_status_signal.set(Some(status)); - loading_signal.set(false); - } - Err(e) => { - error_signal.set(Some(format!("Failed to leave voice: {}", e))); - loading_signal.set(false); - } - } - }); - }); - - // Fetch list of guilds - let load_guilds_impl = Arc::new(move || { - spawn_local(async move { - error_signal.set(None); - loading_signal.set(true); - - match get_guilds().await { - Ok(guilds) => { - guilds_signal.set(guilds); - loading_signal.set(false); - } - Err(e) => { - error_signal.set(Some(format!("Failed to load guilds: {}", e))); - loading_signal.set(false); - } - } - }); - }); - - // Fetch voice channels for a guild - let load_voice_channels_impl = Arc::new(move |guild_id: String| { - spawn_local({ - let guild_id = guild_id.clone(); - async move { - error_signal.set(None); - loading_signal.set(true); - - match get_voice_channels(&guild_id).await { - Ok(channels) => { - voice_channels_signal.set(channels); - loading_signal.set(false); - } - Err(e) => { - error_signal.set(Some(format!("Failed to load voice channels: {}", e))); - loading_signal.set(false); - } - } - } - }); - }); - - // Fetch text channels for a guild - let load_text_channels_impl = Arc::new(move |guild_id: String| { - spawn_local({ - let guild_id = guild_id.clone(); - async move { - error_signal.set(None); - loading_signal.set(true); - - match get_text_channels(&guild_id).await { - Ok(channels) => { - text_channels_signal.set(channels); - loading_signal.set(false); - } - Err(e) => { - error_signal.set(Some(format!("Failed to load text channels: {}", e))); - loading_signal.set(false); - } - } - } - }); - }); - - VoiceControlState { - guilds: guilds_signal, - voice_channels: voice_channels_signal, - text_channels: text_channels_signal, - voice_status: voice_status_signal, - loading: loading_signal, - error: error_signal, - join_voice: join_voice_impl, - leave_voice: leave_voice_impl, - load_guilds: load_guilds_impl, - load_voice_channels: load_voice_channels_impl, - load_text_channels: load_text_channels_impl, - } -} diff --git a/services/frontend/frontend/src/features/live/mod.rs b/services/frontend/frontend/src/features/live/mod.rs deleted file mode 100644 index 1a5ebd2..0000000 --- a/services/frontend/frontend/src/features/live/mod.rs +++ /dev/null @@ -1,140 +0,0 @@ -pub mod audio; -pub mod components; -pub mod hooks; - -use crate::app::AuthContext; -use crate::auth::AuthOverlay; -use crate::ws::context::WsContext; -use components::{ - ActiveSpeakers, AudioVisualizer, MusicSubPanel, NowPlaying, RecordingsSubPanel, ScreenSubPanel, - VoiceConnectionCard, -}; -use leptos::prelude::*; -use shared_types::media::MediaState; -use shared_types::voice::ActiveSpeaker; -use crate::{log_debug, log_info, make_logger}; - -make_logger!(); - -/// LivePanel — Composition shell for all voice and media components. -/// Shows an auth overlay if not authenticated, otherwise shows voice controls. -#[component] -pub fn LivePanel() -> impl IntoView { - let auth = use_context::().expect("AuthContext not provided"); - let ws = use_context::(); - - // ── Shared state for WS-driven components ────────────── - let speakers = RwSignal::new(Vec::::new()); - let media_state = RwSignal::new(None::); - let (recordings_refresh, set_recordings_refresh) = signal(0u64); - let audio_playback = hooks::use_audio_playback::use_audio_playback(); - - // ── Wire WS events (runs on mount, persists while LivePanel is active) ── - if let Some(ref ws) = ws { - log_info!("LivePanel wiring WS handlers"); - // Voice active user — update speakers list - *ws.on_voice_active_user.borrow_mut() = Some(Box::new({ - let speakers = speakers.clone(); - move |speaker: ActiveSpeaker| { - log_debug!("LivePanel voice_active_user: {}", speaker.user_id); - speakers.update(|list| { - if let Some(pos) = list.iter().position(|s| s.user_id == speaker.user_id) { - list[pos] = speaker; - } else { - list.push(speaker); - } - }); - } - })); - - // Media state — update NowPlaying - *ws.on_media_state.borrow_mut() = Some(Box::new({ - let ms = media_state.clone(); - move |state: MediaState| { - log_debug!("LivePanel media_state received"); - ms.set(Some(state)); - } - })); - - // Recording uploaded — trigger recordings list refresh - *ws.on_voice_recording_uploaded.borrow_mut() = Some(Box::new({ - let set_refresh = set_recordings_refresh; - move |_recording| { - log_debug!("LivePanel recording_uploaded received"); - set_refresh.update(|v| *v = v.wrapping_add(1)); - } - })); - - // Binary PCM data — process and play audio - *ws.on_binary.borrow_mut() = Some(Box::new({ - let playback = audio_playback.clone(); - move |data: Vec| { - log_debug!("LivePanel binary PCM data received: {} bytes", data.len()); - hooks::use_audio_playback::process_pcm_data(&playback, data); - // Auto-start playback on first PCM data - if !playback.active.get_untracked() { - hooks::use_audio_playback::start_playback(&playback); - } - } - })); - } - - view! { -
- {move || { - if auth.authenticated.get() { - view! { -
-
-
-

"Voice & Media"

-

- "Monitor voice channels, play music, share your screen, and browse recordings." -

-
-
- - {/* Top row: Voice connection + speakers + visualizer */} -
-
- -
-
- -
-
- - {/* Audio visualization */} -
-
-
"Audio Visualization"
-
-
- -
-
- - {/* Media controls: Now Playing + Music + Screen */} -
-
- -
-
- -
-
- -
-
- - {/* Recordings */} - -
- }.into_any() - } else { - view! { }.into_any() - } - }} -
- } -} diff --git a/services/frontend/frontend/src/features/messages/components/image_grid.rs b/services/frontend/frontend/src/features/messages/components/image_grid.rs deleted file mode 100644 index 09fbd39..0000000 --- a/services/frontend/frontend/src/features/messages/components/image_grid.rs +++ /dev/null @@ -1,79 +0,0 @@ -use leptos::prelude::*; -use shared_types::message::MessageRecord; - -#[component] -pub fn ImageGrid(messages: Vec) -> impl IntoView { - let mut seen_urls = std::collections::HashSet::new(); - let mut urls = Vec::new(); - - for msg in &messages { - if let Some(meta) = &msg.metadata { - // attachments with image MIME - if let Some(atts) = &meta.attachments { - for att in atts { - let is_img = att - .content_type - .as_deref() - .map(|ct| ct.starts_with("image/")) - .unwrap_or(false) - || att.name.to_lowercase().ends_with(".png") - || att.name.to_lowercase().ends_with(".jpg") - || att.name.to_lowercase().ends_with(".jpeg") - || att.name.to_lowercase().ends_with(".gif") - || att.name.to_lowercase().ends_with(".webp"); - if is_img && seen_urls.insert(att.url.clone()) { - urls.push(att.url.clone()); - } - } - } - // stickers - if let Some(stickers) = &meta.stickers { - for s in stickers { - if let Some(ref url) = s.url { - if seen_urls.insert(url.clone()) { - urls.push(url.clone()); - } - } - } - } - // embed images - if let Some(embeds) = &meta.embeds { - for e in embeds { - if let Some(ref img) = e.image { - if seen_urls.insert(img.url.clone()) { - urls.push(img.url.clone()); - } - } - if let Some(ref thumb) = e.thumbnail { - if seen_urls.insert(thumb.url.clone()) { - urls.push(thumb.url.clone()); - } - } - } - } - } - } - - if urls.is_empty() { - return view! { -
- "No images found" -
- } - .into_any(); - } - - view! { -
- {urls.into_iter().map(|url| { - let url_clone = url.clone(); - view! { - - attachment - - } - }).collect::>()} -
- } - .into_any() -} diff --git a/services/frontend/frontend/src/features/messages/components/message_actions.rs b/services/frontend/frontend/src/features/messages/components/message_actions.rs deleted file mode 100644 index ab08a32..0000000 --- a/services/frontend/frontend/src/features/messages/components/message_actions.rs +++ /dev/null @@ -1,28 +0,0 @@ -use leptos::prelude::*; -use shared_types::message::AiStatus; -use std::sync::Arc; - -// ─── Reanalyze Button ──────────────────────────────────── -#[component] -pub fn ReanalyzeButton( - message_id: String, - ai_status: AiStatus, - on_reanalyze: Arc, -) -> impl IntoView { - let on_click_re = move |_| on_reanalyze(message_id.clone()); - view! { -
- - {(ai_status == AiStatus::Error).then(|| view! { - "Click to retry" - })} -
- } -} diff --git a/services/frontend/frontend/src/features/messages/components/message_card.rs b/services/frontend/frontend/src/features/messages/components/message_card.rs deleted file mode 100644 index 9a77dc3..0000000 --- a/services/frontend/frontend/src/features/messages/components/message_card.rs +++ /dev/null @@ -1,374 +0,0 @@ -use leptos::prelude::*; -use shared_types::message::{AiSeverity, AiStatus, AttachmentRef, MessageRecord, ReferenceInfo}; -use std::sync::Arc; - -use super::message_actions::ReanalyzeButton; -use super::message_embed::{MessageAnalysis, MessageError}; -use super::message_meta::{fmt_time, get_cats, is_fallback, render_emojis, severity_class, StatusBadgeInline, time_ago}; - -// ─── MessageRow ─────────────────────────────────────────── -#[component] -pub fn MessageRow( - message: MessageRecord, - on_reanalyze: Arc, -) -> impl IntoView { - let cats = get_cats(&message.ai_categories); - let conf = message.ai_confidence.or(message.ai_moderation_score); - let display = message - .edited_content - .as_deref() - .unwrap_or(&message.content); - let show = !display.is_empty() && !is_fallback(display); - let ai_st = message.ai_status.clone().unwrap_or(AiStatus::Pending); - - let analysis_summary = { - let mut p = cats.iter().take(3).cloned().collect::>().join(", "); - if cats.len() > 3 { - p = format!("{} +{} more", p, cats.len() - 3); - } - if !p.is_empty() { - p.push_str(" · "); - } - p.push_str(&format!( - "{}% conf", - conf.map(|c| (c * 100.0) as u8).unwrap_or(0) - )); - p - }; - - // Attachments - let all_atts = message - .metadata - .as_ref() - .and_then(|m| m.attachments.as_ref()) - .cloned() - .unwrap_or_default(); - let imgs: Vec = all_atts - .iter() - .filter(|a| { - a.content_type - .as_deref() - .map(|ct| ct.starts_with("image/")) - .unwrap_or(false) - || a.name.to_lowercase().ends_with(".png") - || a.name.to_lowercase().ends_with(".jpg") - || a.name.to_lowercase().ends_with(".jpeg") - || a.name.to_lowercase().ends_with(".gif") - || a.name.to_lowercase().ends_with(".webp") - }) - .cloned() - .collect(); - let vids: Vec = all_atts - .iter() - .filter(|a| { - a.content_type - .as_deref() - .map(|ct| ct.starts_with("video/")) - .unwrap_or(false) - || a.name.to_lowercase().ends_with(".mp4") - || a.name.to_lowercase().ends_with(".webm") - || a.name.to_lowercase().ends_with(".mov") - }) - .cloned() - .collect(); - - let stickers = message - .metadata - .as_ref() - .and_then(|m| m.stickers.as_ref()) - .cloned() - .unwrap_or_default(); - - // Extract reply info before view! to avoid closure capture issues - let reply_el = message.is_reply.unwrap_or(false).then(|| { - message - .metadata - .as_ref() - .and_then(|m| m.reference.as_ref()) - .map(|ref_info| { - let r_user = ref_info.replied_username.as_deref().unwrap_or("unknown").to_string(); - let r_content = ref_info - .content - .as_deref() - .unwrap_or("") - .to_string(); - (r_user, r_content) - }) - }); - let reply_user = reply_el.as_ref().map(|r| r.as_ref().map(|(u, _)| u.clone())); - let reply_user = reply_user.flatten(); - let reply_content = reply_el.as_ref().map(|r| r.as_ref().map(|(_, c)| c.clone())); - let reply_content = reply_content.flatten(); - let reply_content_snippet = reply_content.as_ref().map(|c| { - if c.len() > 48 { - format!("{}…", &c[..48]) - } else { - c.clone() - } - }); - let reply_initial = reply_user - .as_ref() - .and_then(|u| u.chars().next()) - .map(|c| c.to_uppercase().to_string()) - .unwrap_or_else(|| "?".to_string()); - - view! { -
- {/* Header */} -
- - {fmt_time(message.created_at)} - - {message.edited_at.is_some().then(|| view! { - - "✎ edited" - - })} - {message.deleted_at.is_some().then(|| view! { - - "🗑 deleted" - - })} -
- - {message.ai_severity.as_ref().filter(|s| **s != AiSeverity::None).map(|sev| view! { - {format!("{:?}", sev)} - })} -
-
- - {/* Reply indicator — Discord-style reference block */} - {reply_user.as_ref().map(|r_user| { - let r_user_cloned = r_user.clone(); - let snippet = reply_content_snippet.clone(); - let initial = reply_initial.clone(); - view! { -
-
-
- {initial} - "Replying to" - "@" {r_user_cloned} - {(!snippet.as_deref().unwrap_or("").is_empty()).then(|| { - let s = snippet.unwrap_or_default(); - view! { - {s} - } - })} -
-
- } - })} - - {/* Content */} - {show.then(|| { - let rendered = render_emojis(display); - view! { -

- {rendered.into_iter().collect::>()} -

- } - })} - - {/* Stickers */} - {(!stickers.is_empty()).then(|| view! { -
- {stickers.iter().map(|s| { - let url_owned = s.url.clone().unwrap_or_default(); - let name_owned = s.name.clone().unwrap_or_default(); - let has_url = !url_owned.is_empty(); - view! { -
- {if has_url { - view! { - name_owned - }.into_any() - } else { - view! { -
- "😊" -
- }.into_any() - }} -
- } - }).collect::>()} -
- })} - - {/* Images */} - {if !imgs.is_empty() { - let imgs_local = imgs.clone(); - let images_view = imgs_local.iter().take(4).map(|a| { - let url1 = a.url.clone(); - let url2 = a.url.clone(); - let name1 = a.name.clone(); - view! { - - name1 - - } - }).collect::>(); - let overflow = if imgs.len() > 4 { - let extra = imgs.len() - 4; - view! { -
- {"+"} {extra} "🖼" -
- }.into_any() - } else { - ().into_any() - }; - view! { -
- {images_view} - {overflow} -
- }.into_any() - } else { - ().into_any() - }} - - {/* Videos */} - {if !vids.is_empty() { - let vids_local = vids.clone(); - let videos_view = vids_local.iter().take(4).map(|a| { - let url = a.url.clone(); - view! { - - } - }).collect::>(); - let overflow = if vids.len() > 4 { - let extra = vids.len() - 4; - view! { -
- {"+"} {extra} "▶" -
- }.into_any() - } else { - ().into_any() - }; - view! { -
- {videos_view} - {overflow} -
- }.into_any() - } else { - ().into_any() - }} - - {/* Categories */} - {if !cats.is_empty() { - let cats_local = cats.clone(); - view! { -
- {cats_local.iter().map(|c| view! { - {c.clone()} - }).collect::>()} -
- }.into_any() - } else { - ().into_any() - }} - - {/* AI Analysis */} - {message.ai_analysis.as_ref().map(|analysis| { - let analysis_summary_str = analysis_summary.clone(); - let analysis_str = analysis.clone(); - view! { - - } - })} - - {/* Error */} - {message.ai_error.as_ref().map(|e| { - let error_str = e.clone(); - view! { - - } - })} - - {/* Re-analyze */} - -
- } -} - -// ─── MessageCard ────────────────────────────────────────── -#[component] -pub fn MessageCard( - messages: Vec, - on_reanalyze: Arc, -) -> impl IntoView { - let first = &messages[0]; - let has_multi = messages.len() > 1; - let deleted = first.deleted_at.is_some(); - let avatar = first - .avatar_url - .clone() - .unwrap_or_else(|| "https://cdn.discordapp.com/embed/avatars/0.png".into()); - let loc_label = first - .metadata - .as_ref() - .and_then(|m| m.channel.as_ref()) - .map(|c| { - if let Some(ref tn) = c.thread_name { - format!("# {} › {}", c.channel_name.as_deref().unwrap_or("?"), tn) - } else { - format!("# {}", c.channel_name.as_deref().unwrap_or("?")) - } - }); - let card_cls = if deleted { " is-deleted" } else { "" }; - - view! { -
-
- -
-
- {first.username.clone()} - {loc_label.as_ref().map(|l| { - let label_str = l.clone(); - view! { - {label_str} - } - })} - - {time_ago(first.created_at)} - {has_multi.then(|| format!(" · {} msgs", messages.len()))} - -
-
- {messages.into_iter().enumerate().map(|(_i, msg)| { - view! { - - } - }).collect::>()} -
-
-
-
- } -} - -// ─── Skeleton ───────────────────────────────────────────── -#[component] -pub fn MessageCardSkeleton() -> impl IntoView { - view! { -
-
-
-
-
-
-
-
-
-
-
-
-
-
- } -} diff --git a/services/frontend/frontend/src/features/messages/components/message_embed.rs b/services/frontend/frontend/src/features/messages/components/message_embed.rs deleted file mode 100644 index 696d28e..0000000 --- a/services/frontend/frontend/src/features/messages/components/message_embed.rs +++ /dev/null @@ -1,36 +0,0 @@ -use leptos::prelude::*; -use shared_types::message::AiStatus; - -// ─── Message Analysis ───────────────────────────────────── -#[component] -pub fn MessageAnalysis( - analysis: String, - ai_status: AiStatus, - analysis_summary: String, -) -> impl IntoView { - let f_cls = if ai_status == AiStatus::Flagged { "flagged" } else { "clean" }; - let icon = if ai_status == AiStatus::Flagged { "🚨" } else { "ℹ️" }; - view! { -
-
- {icon} -
- {analysis_summary} -
{analysis}
-
-
-
- } -} - -// ─── Message Error ──────────────────────────────────────── -#[component] -pub fn MessageError( - error: String, -) -> impl IntoView { - view! { -
- "AI error: "{error} -
- } -} diff --git a/services/frontend/frontend/src/features/messages/components/message_feed.rs b/services/frontend/frontend/src/features/messages/components/message_feed.rs deleted file mode 100644 index d772a85..0000000 --- a/services/frontend/frontend/src/features/messages/components/message_feed.rs +++ /dev/null @@ -1,174 +0,0 @@ -use leptos::html; -use leptos::prelude::*; -use shared_types::message::MessageRecord; -use std::sync::Arc; -use wasm_bindgen::prelude::*; -use wasm_bindgen::JsCast; -use web_sys::IntersectionObserver; - - -const GROUP_WINDOW_MS: i64 = 5 * 60 * 1000; - -fn group_messages(messages: Vec) -> Vec> { - let mut groups: Vec> = Vec::new(); - for msg in messages { - if let Some(last_group) = groups.last_mut() { - let same_user = last_group - .first() - .map(|m| m.user_id == msg.user_id) - .unwrap_or(false); - let same_window = last_group - .last() - .map(|m| (m.created_at - msg.created_at).abs() < GROUP_WINDOW_MS) - .unwrap_or(false); - if same_user && same_window { - last_group.push(msg); - continue; - } - } - groups.push(vec![msg]); - } - groups -} - -#[component] -pub fn MessageFeed( - messages: Vec, - #[prop(optional)] empty_text: &'static str, - #[prop(optional)] loading: bool, - #[prop(optional)] has_more: bool, - #[prop(optional)] loading_more: bool, - #[prop(optional)] on_load_more: Option>, - on_reanalyze: Arc, -) -> impl IntoView { - let sentinel_ref = NodeRef::::new(); - let (observer_ready, set_observer_ready) = signal(false); - - // Schedule observer setup to run AFTER the DOM is mounted (next microtask). - // With has_more, !loading, and messages present the sentinel div will be in the DOM. - if !loading && !messages.is_empty() && has_more { - wasm_bindgen_futures::spawn_local({ - let setter = set_observer_ready; - async move { - setter.set(true); - } - }); - } - - // Clone before move into Effect closure so it's still available for the view - let on_load_more_io = on_load_more.clone(); - Effect::new(move |_| { - let _ready = observer_ready.get(); - if !_ready { - return; - } - if let Some(node) = sentinel_ref.get() { - let cb = on_load_more_io.clone(); - let observer_cb = Closure::, IntersectionObserver)>::new( - move |entries: Vec, _observer: IntersectionObserver| { - for entry in entries { - if let Some(entry) = - entry.dyn_ref::() - { - if entry.is_intersecting() { - if let Some(ref cb) = cb { - cb(); - } - } - } - } - }, - ); - let observer = - IntersectionObserver::new(observer_cb.as_ref().unchecked_ref()) - .expect("IntersectionObserver failed"); - observer.observe(&node); - // Keep closure alive — forget rather than cleanup since observer owns it - observer_cb.forget(); - on_cleanup(move || { - observer.disconnect(); - }); - } - }); - - // Loading state - if loading { - return view! { -
- {std::iter::repeat_with(|| { - use super::message_card::MessageCardSkeleton; - view! { } - }).take(3).collect::>()} -
- } - .into_any(); - } - - if messages.is_empty() { - return view! { -
-
- {if empty_text.is_empty() { "No messages" } else { empty_text }} -
-
- } - .into_any(); - } - - let groups = group_messages(messages); - let has_more_val = has_more; - let loading_more_val = loading_more; - - view! { -
- {groups.into_iter().map(|group| { - let cb = on_reanalyze.clone(); - view! { - - } - }).collect::>()} - - {/* Infinite scroll sentinel + fallback load more button */} - {has_more_val.then(|| { - view! { - <> -
- {loading_more_val.then(|| { - use super::message_card::MessageCardSkeleton; - view! { } - })} -
- {/* Fallback: visible button in case IntersectionObserver doesn't fire */} - {(!loading_more_val).then(|| { - let load_more_cb = on_load_more.clone(); - view! { -
- -
- } - })} - - } - })} -
- } - .into_any() -} - -#[component] -fn MessageCardGroup( - messages: Vec, - on_reanalyze: Arc, -) -> impl IntoView { - use super::message_card::MessageCard; - view! { - - } -} diff --git a/services/frontend/frontend/src/features/messages/components/message_meta.rs b/services/frontend/frontend/src/features/messages/components/message_meta.rs deleted file mode 100644 index 0749fe7..0000000 --- a/services/frontend/frontend/src/features/messages/components/message_meta.rs +++ /dev/null @@ -1,114 +0,0 @@ -use leptos::prelude::*; -use regex::Regex; -use shared_types::message::{AiSeverity, AiStatus}; -use std::sync::OnceLock; -use wasm_bindgen::prelude::*; - -// ─── Helpers ────────────────────────────────────────────── - -pub fn custom_emoji_regex() -> &'static Regex { - static RE: OnceLock = OnceLock::new(); - RE.get_or_init(|| Regex::new(r"<(a)?:([a-zA-Z0-9_]+):(\d+)>").unwrap()) -} - -pub fn render_emojis(content: &str) -> Vec { - let re = custom_emoji_regex(); - let mut parts: Vec = Vec::new(); - let mut last = 0; - let content_owned = content.to_string(); - for cap in re.captures_iter(&content_owned) { - let m = cap.get(0).unwrap(); - if m.start() > last { - let text = content_owned[last..m.start()].to_string(); - parts.push(view! { {text} }.into_any()); - } - let animated = cap.get(1).is_some(); - let name = cap.get(2).map(|c| c.as_str()).unwrap_or("").to_string(); - let id = cap.get(3).map(|c| c.as_str()).unwrap_or("0").to_string(); - let ext = if animated { "gif" } else { "png" }; - let url = format!("https://cdn.discordapp.com/emojis/{}.{}?size=128", id, ext); - let title = format!(":{}:", name); - parts.push( - view! { - name - } - .into_any(), - ); - last = m.end(); - } - if last < content_owned.len() { - let text = content_owned[last..].to_string(); - parts.push(view! { {text} }.into_any()); - } - parts -} - -pub fn time_ago(ts: i64) -> String { - let now = js_sys::Date::now() as i64; - // created_at is in milliseconds (from Discord's message.createdTimestamp) - let secs = if now > ts { (now - ts) / 1000 } else { 0 }; - if secs < 60 { - format!("{}s ago", secs) - } else if secs < 3600 { - format!("{}m ago", secs / 60) - } else if secs < 86400 { - format!("{}h ago", secs / 3600) - } else { - let d = js_sys::Date::new(&JsValue::from_f64(ts as f64)); - format!("{}", d.to_locale_date_string("en-US", &JsValue::UNDEFINED)) - } -} - -pub fn fmt_time(ts: i64) -> String { - let d = js_sys::Date::new(&JsValue::from_f64(ts as f64)); - format!("{:02}:{:02}", d.get_hours(), d.get_minutes()) -} - -pub fn severity_class(s: &AiSeverity) -> &'static str { - match s { - AiSeverity::Critical | AiSeverity::High => "badge-destructive", - AiSeverity::Medium => "badge-warning", - AiSeverity::Low => "badge-info", - AiSeverity::None => "badge-outline", - } -} - -pub fn is_fallback(t: &str) -> bool { - t.starts_with("[Attachment:") || t.starts_with("[Sticker:") || t.starts_with("[Embed]") -} - -pub fn get_cats(raw: &Option>) -> Vec { - raw.as_ref() - .map(|v| { - v.iter() - .filter(|c| *c != "analysis_incomplete") - .cloned() - .collect() - }) - .unwrap_or_default() -} - -// ─── StatusBadgeInline ──────────────────────────────────── -#[component] -pub fn StatusBadgeInline(status: AiStatus) -> impl IntoView { - let (cl, icon_svg): (&'static str, AnyView) = match &status { - AiStatus::Clean => ("status-badge-clean", view! { }.into_any()), - AiStatus::Flagged => ("status-badge-flagged", view! { }.into_any()), - AiStatus::Error => ("status-badge-error", view! { }.into_any()), - AiStatus::Pending => { - ("status-badge-pending", ().into_any()) - }, - AiStatus::Processing => { - ("status-badge-processing", ().into_any()) - }, - AiStatus::Warn => { - ("status-badge-warn", ().into_any()) - }, - }; - view! { - - {icon_svg} - {format!("{:?}", status)} - - } -} diff --git a/services/frontend/frontend/src/features/messages/components/mod.rs b/services/frontend/frontend/src/features/messages/components/mod.rs deleted file mode 100644 index a068dc9..0000000 --- a/services/frontend/frontend/src/features/messages/components/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod image_grid; -pub mod message_card; -pub mod message_embed; -pub mod message_meta; -pub mod message_actions; -pub mod message_feed; diff --git a/services/frontend/frontend/src/features/messages/filter_bar.rs b/services/frontend/frontend/src/features/messages/filter_bar.rs deleted file mode 100644 index 46a358d..0000000 --- a/services/frontend/frontend/src/features/messages/filter_bar.rs +++ /dev/null @@ -1,135 +0,0 @@ -use leptos::prelude::*; -use shared_types::message::MessageRecord; -use std::sync::Arc; -use wasm_bindgen_futures::spawn_local; -use crate::{log_info, log_warn, make_logger}; - -make_logger!(); - -type AiFilter = &'static str; -const FILTERS: &[AiFilter] = &["all", "analyzed", "clean", "flagged", "error", "pending"]; - -#[component] -pub fn FilterBar( - search_query: ReadSignal, - set_search_query: WriteSignal, - show_search: ReadSignal, - set_show_search: WriteSignal, - is_searching: ReadSignal, - set_is_searching: WriteSignal, - set_search_results: WriteSignal>, - ai_filter: RwSignal, - error_count: Memo, - retrying_all: ReadSignal, - set_retrying_all: WriteSignal, - reanalyze_all_errors: Arc, -) -> impl IntoView { - // Search handler - takes any event type and triggers the search - let do_search = { - let q = search_query; - move || { - let query = q.get(); - if query.trim().is_empty() { - set_show_search.set(false); - set_search_results.set(Vec::new()); - return; - } - set_is_searching.set(true); - let q_clone = query.trim().to_string(); - log_info!("Messages searching for: {}", q_clone); - spawn_local(async move { - match crate::api::messages::search_messages(&q_clone, Some(50)).await { - Ok(results) => { - log_info!("Messages search found {} results", results.len()); - set_search_results.set(results); - set_show_search.set(true); - } - Err(_) => { - log_warn!("Messages search failed"); - set_search_results.set(Vec::new()); - } - } - set_is_searching.set(false); - }); - } - }; - // Separate closures for different event types so on:click/on:keydown type-check - let handle_search_click = move |_: web_sys::MouseEvent| do_search(); - let handle_search_keydown = move |_: web_sys::KeyboardEvent| do_search(); - - // Clear search - let clear_search = move |_| { - set_show_search.set(false); - set_search_results.set(Vec::new()); - set_search_query.set(String::new()); - }; - - // Filter chip click - let set_filter = { - let af = ai_filter; - move |f: &'static str| af.set(f.to_string()) - }; - - view! { - - } -} diff --git a/services/frontend/frontend/src/features/messages/hooks/mod.rs b/services/frontend/frontend/src/features/messages/hooks/mod.rs deleted file mode 100644 index 4533575..0000000 --- a/services/frontend/frontend/src/features/messages/hooks/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod use_messages; diff --git a/services/frontend/frontend/src/features/messages/hooks/use_messages.rs b/services/frontend/frontend/src/features/messages/hooks/use_messages.rs deleted file mode 100644 index b792048..0000000 --- a/services/frontend/frontend/src/features/messages/hooks/use_messages.rs +++ /dev/null @@ -1,229 +0,0 @@ -use crate::api::messages::{get_messages, reanalyze_batch, reanalyze_message}; -use leptos::prelude::*; -use shared_types::message::{MessageRecord, PageResult}; -use std::collections::HashMap; -use std::sync::Arc; -use wasm_bindgen_futures::spawn_local; -use crate::{log_debug, log_error, log_info, log_trace, log_warn, make_logger}; - -make_logger!(); - -/// Merges current messages with incoming messages, deduplicating by ID and sorting -pub fn merge_messages(current: &[MessageRecord], incoming: &[MessageRecord]) -> Vec { - let mut by_id: HashMap = - current.iter().map(|m| (m.id.clone(), m.clone())).collect(); - for msg in incoming { - by_id.insert(msg.id.clone(), msg.clone()); - } - let mut merged: Vec = by_id.into_values().collect(); - merged.sort_by(|a, b| { - b.created_at - .cmp(&a.created_at) - .then_with(|| b.id.cmp(&a.id)) - }); - merged -} - -/// Callback type for fetch_messages -pub type FetchMessagesCallback = Arc; -/// Callback type for load_more -pub type LoadMoreCallback = Arc; -/// Callback type for reanalyze -pub type ReanalyzeCallback = Arc; -/// Callback type for reanalyze_all_errors -pub type ReanalyzeAllErrorsCallback = Arc; - -/// State returned by use_messages hook -#[derive(Clone)] -pub struct MessagesState { - /// Current list of messages - pub messages: RwSignal>, - /// Whether the initial fetch is in progress - pub loading: ReadSignal, - /// Whether we're loading more messages - pub loading_more: RwSignal, - /// Pagination cursor for next page - pub cursor: RwSignal>, - /// Derived: whether there are more messages to load - pub has_more: Memo, - /// Last error message if any - pub error: RwSignal>, - /// Current guild ID - pub current_guild: RwSignal>, - /// Fetch initial messages for a guild - pub fetch_messages: FetchMessagesCallback, - /// Load next page of messages - pub load_more: LoadMoreCallback, - /// Reanalyze a single message - pub reanalyze: ReanalyzeCallback, - /// Reanalyze all error messages in current batch - pub reanalyze_all_errors: ReanalyzeAllErrorsCallback, -} - -/// Hook to manage message data fetching and state -pub fn use_messages() -> MessagesState { - // Core signals - let messages_signal = RwSignal::new(Vec::::new()); - let (loading, set_loading) = signal(false); - let loading_more_signal = RwSignal::new(false); - let cursor_signal = RwSignal::new(None::); - let error_signal = RwSignal::new(None::); - let current_guild_signal = RwSignal::new(None::); - - // Derived signal: has_more is true if cursor is Some - let has_more_signal = Memo::new(move |_| cursor_signal.get().is_some()); - - // Fetch initial messages for a guild - let fetch_messages_impl = Arc::new(move |guild_id: String| { - spawn_local({ - let guild_id = guild_id.clone(); - async move { - error_signal.set(None); - set_loading.set(true); - log_info!("Messages fetch start for guild {}", guild_id); - - match get_messages(&guild_id, Some(30), None, None).await { - Ok(PageResult { data, next_cursor }) => { - log_info!("Messages fetch OK: count={}, cursor={:?}", data.len(), next_cursor); - web_sys::console::log_3( - &"[messages] fetch OK".into(), - &format!("count={}", data.len()).into(), - &format!("cursor={:?}", next_cursor).into(), - ); - messages_signal.set(data); - cursor_signal.set(next_cursor); - current_guild_signal.set(Some(guild_id)); - set_loading.set(false); - } - Err(e) => { - log_warn!("Messages fetch error: {}", e); - web_sys::console::log_2( - &"[messages] fetch ERROR".into(), - &format!("{}", e).into(), - ); - error_signal.set(Some(format!("Failed to fetch messages: {}", e))); - set_loading.set(false); - } - } - } - }); - }); - - // Load more messages (append next page) - let load_more_impl = Arc::new(move || { - spawn_local(async move { - let guild_id = match current_guild_signal.get() { - Some(id) => id, - None => { - error_signal.set(Some("No guild selected".to_string())); - return; - } - }; - - let cursor = match cursor_signal.get() { - Some(c) => c, - None => { - error_signal.set(Some("No more messages to load".to_string())); - return; - } - }; - - loading_more_signal.set(true); - error_signal.set(None); - log_info!("Messages load more for guild {}", guild_id); - - match get_messages(&guild_id, Some(30), None, Some(&cursor)).await { - Ok(PageResult { data, next_cursor }) => { - log_info!("Messages load more OK: count={}, cursor={:?}", data.len(), next_cursor); - let current = messages_signal.get(); - messages_signal.set(merge_messages(¤t, &data)); - cursor_signal.set(next_cursor); - loading_more_signal.set(false); - } - Err(e) => { - log_warn!("Messages load more error: {}", e); - error_signal.set(Some(format!("Failed to load more: {}", e))); - loading_more_signal.set(false); - } - } - }); - }); - - // Reanalyze single message with optimistic update - let reanalyze_impl = Arc::new(move |message_id: String| { - spawn_local({ - let message_id = message_id.clone(); - async move { - log_info!("Messages reanalyze start for message {}", message_id); - // Optimistic: flip status to Processing - let mut msgs = messages_signal.get(); - if let Some(pos) = msgs.iter().position(|m| m.id == message_id) { - if let Some(ref mut msg) = msgs.get_mut(pos) { - msg.ai_status = Some(shared_types::message::AiStatus::Processing); - } - } - messages_signal.set(msgs); - - // Call API - match reanalyze_message(&message_id).await { - Ok(_) => { - log_info!("Messages reanalyze OK for message {}", message_id); - // Success: keep the Processing status (will be updated via WS) - } - Err(e) => { - log_warn!("Messages reanalyze error for message {}: {}", message_id, e); - // Revert to Error status on failure - let mut msgs = messages_signal.get(); - if let Some(pos) = msgs.iter().position(|m| m.id == message_id) { - if let Some(ref mut msg) = msgs.get_mut(pos) { - msg.ai_status = Some(shared_types::message::AiStatus::Error); - msg.ai_error = Some(e.to_string()); - } - } - messages_signal.set(msgs); - error_signal.set(Some(format!("Reanalyze failed: {}", e))); - } - } - } - }); - }); - - // Reanalyze all error messages - let reanalyze_all_errors_impl = Arc::new(move || { - spawn_local(async move { - log_info!("Messages reanalyze all errors start"); - match reanalyze_batch().await { - Ok(_count) => { - log_info!("Messages reanalyze all errors OK: count={}", _count); - error_signal.set(None); - // Optimistically mark all error messages as Processing - let mut msgs = messages_signal.get(); - for msg in msgs.iter_mut() { - if msg.ai_status == Some(shared_types::message::AiStatus::Error) { - msg.ai_status = Some(shared_types::message::AiStatus::Processing); - } - } - messages_signal.set(msgs); - } - Err(e) => { - log_info!("Messages reanalyze all errors failed: {}", e); - error_signal.set(Some(format!("Batch reanalyze failed: {}", e))); - } - } - }); - }); - - MessagesState { - messages: messages_signal, - loading, - loading_more: loading_more_signal, - cursor: cursor_signal, - has_more: has_more_signal, - error: error_signal, - current_guild: current_guild_signal, - fetch_messages: fetch_messages_impl, - load_more: load_more_impl, - reanalyze: reanalyze_impl, - reanalyze_all_errors: reanalyze_all_errors_impl, - } -} diff --git a/services/frontend/frontend/src/features/messages/message_list.rs b/services/frontend/frontend/src/features/messages/message_list.rs deleted file mode 100644 index dc3dfe3..0000000 --- a/services/frontend/frontend/src/features/messages/message_list.rs +++ /dev/null @@ -1,87 +0,0 @@ -use leptos::prelude::*; -use shared_types::message::MessageRecord; -use std::sync::Arc; -use super::components::message_feed::MessageFeed; -use super::components::image_grid::ImageGrid; - -#[derive(Clone, PartialEq)] -pub enum ViewTab { - All, - Images, -} - -#[component] -pub fn MessageListView( - view_tab: RwSignal, - show_search: ReadSignal, - search_results: ReadSignal>, - filtered_messages: Memo>, - image_messages: ReadSignal>, - loading: ReadSignal, - has_more: Memo, - loading_more: ReadSignal, - on_load_more: Arc, - on_reanalyze: Arc, -) -> impl IntoView { - view! { - {/* Search results count */} - {move || show_search.get().then(|| { - let n = search_results.get().len(); - view! { -
- "Found " {n} " result" {if n != 1 { "s" } else { "" }} -
- } - })} - - {/* View tabs + content */} -
-
- - -
- -
- {move || { - let effective_has_more = if show_search.get() { false } else { has_more.get() }; - let load_more_cb = on_load_more.clone(); - let empty_text: &'static str = if show_search.get() { "No messages found matching your search." } else { "No captures yet." }; - let on_load_more_clone: Arc = Arc::new(move || load_more_cb()); - view! { - - } - }} -
-
- {move || view! { - - }} -
-
- } -} diff --git a/services/frontend/frontend/src/features/messages/mod.rs b/services/frontend/frontend/src/features/messages/mod.rs deleted file mode 100644 index 9daea54..0000000 --- a/services/frontend/frontend/src/features/messages/mod.rs +++ /dev/null @@ -1,397 +0,0 @@ -use leptos::prelude::*; -use shared_types::message::{AiStatus, MessageRecord, PageResult}; -use std::sync::Arc; -use wasm_bindgen_futures::spawn_local; -use crate::{log_debug, log_error, log_info, log_trace, log_warn, make_logger}; - -make_logger!(); - -/// Threads whose messages should be hidden from both the feed and the -/// Images tab. A bot or selfbot may be spamming in a thread, polluting -/// the dashboard — add its thread ID here to keep the view clean. -const EXCLUDED_THREAD_IDS: &[&str] = &["1522077685508083893"]; - -fn is_excluded_thread(m: &MessageRecord) -> bool { - m.thread_id - .as_deref() - .is_some_and(|tid| EXCLUDED_THREAD_IDS.contains(&tid)) -} - -pub mod components; -pub mod hooks; - -use components::image_grid::ImageGrid; -use components::message_feed::MessageFeed; -use hooks::use_messages::{merge_messages, use_messages}; - -type AiFilter = &'static str; -const FILTERS: &[AiFilter] = &["all", "analyzed", "clean", "flagged", "error", "pending"]; - -#[derive(Clone, PartialEq)] -enum ViewTab { - All, - Images, -} - -#[component] -pub fn MessagesPanel() -> impl IntoView { - let state = use_messages(); - let (search_query, set_search_query) = signal(String::new()); - let (search_results, set_search_results) = signal::>(Vec::new()); - let (show_search, set_show_search) = signal(false); - let (is_searching, set_is_searching) = signal(false); - let ai_filter = RwSignal::new("analyzed".to_string()); - let view_tab = RwSignal::new(ViewTab::All); - let image_messages = RwSignal::new(Vec::::new()); - let (retrying_all, set_retrying_all) = signal(false); - - // Stats derived from filtered messages - let stats = Memo::new(move |_| { - let base = if show_search.get() { - search_results.get() - } else { - state.messages.get() - }; - let total = base.len(); - let clean = base - .iter() - .filter(|m| m.ai_status == Some(AiStatus::Clean)) - .count(); - let flagged = base - .iter() - .filter(|m| m.ai_status == Some(AiStatus::Flagged)) - .count(); - let error = base - .iter() - .filter(|m| m.ai_status == Some(AiStatus::Error)) - .count(); - let pending = base - .iter() - .filter(|m| m.ai_status.is_none() || m.ai_status == Some(AiStatus::Pending)) - .count(); - let deleted = base.iter().filter(|m| m.deleted_at.is_some()).count(); - let edited = base.iter().filter(|m| m.edited_at.is_some()).count(); - (total, clean, flagged, error, pending, deleted, edited) - }); - - // Filter messages based on active filter - let filtered_messages = Memo::new(move |_| { - let base = if show_search.get() { - search_results.get() - } else { - state.messages.get() - }; - let filter = ai_filter.get(); - if filter == "all" { - return base; - } - base.into_iter() - .filter(|m| { - let status = m.ai_status.clone().unwrap_or(AiStatus::Pending); - if filter == "analyzed" { - return status != AiStatus::Pending; - } - if filter == "pending" { - return status == AiStatus::Pending; - } - format!("{:?}", status).to_lowercase() == filter - }) - .filter(|m| !is_excluded_thread(m)) - .collect() - }); - - // Search handler - takes any event type and triggers the search - let do_search = { - let q = search_query; - move || { - let query = q.get(); - if query.trim().is_empty() { - set_show_search.set(false); - set_search_results.set(Vec::new()); - return; - } - set_is_searching.set(true); - let q_clone = query.trim().to_string(); - log_info!("Messages searching for: {}", q_clone); - spawn_local(async move { - match crate::api::messages::search_messages(&q_clone, Some(50)).await { - Ok(results) => { - log_info!("Messages search found {} results", results.len()); - set_search_results.set(results); - set_show_search.set(true); - } - Err(_) => { - log_warn!("Messages search failed"); - set_search_results.set(Vec::new()); - } - } - set_is_searching.set(false); - }); - } - }; - // Separate closures for different event types so on:click/on:keydown type-check - let handle_search_click = move |_: web_sys::MouseEvent| do_search(); - let handle_search_keydown = move |_: web_sys::KeyboardEvent| do_search(); - - // Clear search - let clear_search = move |_| { - set_show_search.set(false); - set_search_results.set(Vec::new()); - set_search_query.set(String::new()); - }; - - // Filter chip click - let set_filter = { - let af = ai_filter; - move |f: &'static str| af.set(f.to_string()) - }; - - // WS event handlers (wire once on mount) - let ws = use_context::(); - if let Some(ref ws) = ws { - log_info!("MessagesPanel wiring WS handlers"); - // Subscribe to real-time message events - { - let msgs = state.messages; - *ws.on_message_created.borrow_mut() = Some(Box::new(move |msg| { - log_debug!("WS message_created received: {}", msg.id); - let current = msgs.get(); - msgs.set(merge_messages(¤t, &[msg])); - })); - } - { - let msgs = state.messages; - *ws.on_message_updated.borrow_mut() = Some(Box::new(move |msg| { - log_debug!("WS message_updated received: {}", msg.id); - let current = msgs.get(); - msgs.set(merge_messages(¤t, &[msg])); - })); - } - { - let msgs = state.messages; - *ws.on_message_deleted.borrow_mut() = Some(Box::new(move |id| { - log_debug!("WS message_deleted received: {}", id); - let current = msgs.get(); - msgs.set(current.into_iter().filter(|m| m.id != id).collect()); - })); - } - { - let msgs = state.messages; - *ws.on_message_analyzed.borrow_mut() = Some(Box::new(move |msg| { - log_debug!("WS message_analyzed received: {}", msg.id); - let current = msgs.get(); - msgs.set(merge_messages(¤t, &[msg])); - })); - } - } - - // Fetch messages on mount if guild is configured - Effect::new(move |_| { - if let Some(config) = use_context::() { - if let Some(ref guild_id) = config.monitor_guild_id.get() { - let gid = guild_id.clone(); - (state.fetch_messages)(gid); - } - } - }); - - // Fetch image messages when Images tab is selected - let fetch_images = { - let image_messages = image_messages.clone(); - move || { - let guild_id = use_context::() - .and_then(|c| c.monitor_guild_id.get()); - if let Some(gid) = guild_id { - log_info!("Messages fetching images for guild {}", gid); - spawn_local({ - let image_messages = image_messages.clone(); - async move { - match crate::api::messages::get_images(&gid, Some(100)).await { - Ok(PageResult { data, .. }) => { - log_info!("Messages images loaded: count={}", data.len()); - image_messages.set( - data.into_iter() - .filter(|m| !is_excluded_thread(m)) - .collect(), - ); - } - Err(e) => { - log_error!("Messages images error: {}", e); - } - } - } - }); - } - } - }; - // Fetch images when tab changes to Images - Effect::new(move |_| { - if view_tab.get() == ViewTab::Images { - fetch_images(); - } - }); - - // ─── View ──────────────────────────────────────────────── - let get_stats = move || stats.get(); - let (total, clean, flagged, error, pending, deleted, edited) = ( - move || get_stats().0, - move || get_stats().1, - move || get_stats().2, - move || get_stats().3, - move || get_stats().4, - move || get_stats().5, - move || get_stats().6, - ); - - view! { -
- {/* Header card */} -
-
-
"Messages"
-

- "Messages are automatically captured from all text channels. Real-time updates arrive via WebSocket." -

-
-
- - {/* Stats badges */} - {move || (total() > 0).then(|| view! { -
- {total()} " total" {state.has_more.get().then_some("+")} - {clean()} " clean" - {flagged()} " flagged" - {error()} " error" - {pending()} " pending" - {(deleted() > 0).then(|| view! { - {deleted()} " deleted" - })} - {(edited() > 0).then(|| view! { - {edited()} " edited" - })} -
- })} - - {/* Search + filters row */} - - - {/* Search results count */} - {move || show_search.get().then(|| { - let n = search_results.get().len(); - view! { -
- "Found " {n} " result" {if n != 1 { "s" } else { "" }} -
- } - })} - - {/* View tabs + content */} -
-
- - -
- -
- {move || { - let load_more_cb = state.load_more.clone(); - let empty_text: &'static str = if show_search.get() { "No messages found matching your search." } else { "No captures yet." }; - let has_more = if show_search.get() { false } else { state.has_more.get() }; - let on_load_more_clone: Arc = Arc::new(move || load_more_cb()); - view! { - - } - }} -
-
- {move || view! { - - }} -
-
-
- } -} diff --git a/services/frontend/frontend/src/features/mod.rs b/services/frontend/frontend/src/features/mod.rs deleted file mode 100644 index ff99f81..0000000 --- a/services/frontend/frontend/src/features/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod dashboard; -pub mod live; -pub mod messages; -pub mod polish; diff --git a/services/frontend/frontend/src/features/polish/components/mascot_chatbot.rs b/services/frontend/frontend/src/features/polish/components/mascot_chatbot.rs deleted file mode 100644 index b9008be..0000000 --- a/services/frontend/frontend/src/features/polish/components/mascot_chatbot.rs +++ /dev/null @@ -1,191 +0,0 @@ -use leptos::prelude::*; -use wasm_bindgen_futures::spawn_local; - -#[derive(Clone, PartialEq)] -enum ChatRole { - User, - Mascot, -} - -#[derive(Clone)] -struct ChatMessage { - #[allow(dead_code)] - id: String, - role: ChatRole, - content: String, -} - -#[component] -pub fn MascotChatbot() -> impl IntoView { - let open = RwSignal::new(false); - let minimized = RwSignal::new(false); - let loading = RwSignal::new(false); - let input = RwSignal::new(String::new()); - let messages = RwSignal::new(vec![ChatMessage { - id: "init-1".to_string(), - role: ChatRole::Mascot, - content: - "Halo! 👋 Aku mascot IMPHNEN. Tanya aku soal analytics, pesan, atau moderation queue." - .to_string(), - }]); - - // Fetch chat history when the panel opens - Effect::new(move |_| { - if open.get() { - spawn_local(async move { - if let Ok(history) = crate::api::mascot::get_chat_history().await { - messages.update(|list| { - // Keep the initial greeting, then append history messages - let greeting = list.first().cloned(); - list.clear(); - if let Some(g) = greeting { - list.push(g); - } - for msg in history { - let role = if msg.role == "user" { - ChatRole::User - } else { - ChatRole::Mascot - }; - list.push(ChatMessage { - id: format!("hist-{}", list.len()), - role, - content: msg.content, - }); - } - }); - } - }); - } - }); - - let send_message = move || { - let text = input.get_untracked().trim().to_string(); - if text.is_empty() || loading.get_untracked() { - return; - } - - let now = js_sys::Date::now() as u64; - messages.update(|list| { - list.push(ChatMessage { - id: format!("user-{}", now), - role: ChatRole::User, - content: text.clone(), - }) - }); - input.set(String::new()); - loading.set(true); - - spawn_local(async move { - let response = match crate::api::mascot::send_mascot_message(&text).await { - Ok(resp) => resp.response, - Err(_) => fallback_response(&text), - }; - - messages.update(|list| { - list.push(ChatMessage { - id: format!("mascot-{}", js_sys::Date::now() as u64), - role: ChatRole::Mascot, - content: response, - }) - }); - loading.set(false); - }); - }; - - view! { -
- {move || if open.get() { - view! { -
-
-
-
"💬"
-
-
"Mascot IMPHNEN"
-
{move || if loading.get() { "Mengetik..." } else { "Online" }}
-
-
-
- - -
-
- - {move || (!minimized.get()).then(|| view! { - <> -
- {messages.get().into_iter().map(|msg| { - let is_user = msg.role == ChatRole::User; - view! { -
- {(!is_user).then(|| view! {
"🤖"
})} -
- {msg.content} -
-
- } - }).collect::>()} - - {loading.get().then(|| view! { -
-
"🤖"
-
- -
-
- })} -
- -
- - -
- - })} -
- }.into_any() - } else { - view! { - - }.into_any() - }} -
- } -} - -fn fallback_response(input: &str) -> String { - let lower = input.to_lowercase(); - if lower.contains("halo") || lower.contains("hai") { - "Halo juga! 👋 Aku siap bantu baca kondisi server.".to_string() - } else if lower.contains("pesan") || lower.contains("message") { - "Cek tab Messages untuk live capture dan hasil AI moderation terbaru.".to_string() - } else if lower.contains("voice") || lower.contains("audio") { - "Tab Voice & Media punya voice bridge, speakers, media controls, dan recordings." - .to_string() - } else if lower.contains("dashboard") || lower.contains("stat") { - "Dashboard Guild merangkum total pesan, user aktif, channel teratas, dan moderation queue." - .to_string() - } else { - format!("Menarik: \"{}\". Kalau backend mascot offline, aku tetap bisa bantu arahkan ke Messages, Voice, atau Dashboard. 😊", input) - } -} diff --git a/services/frontend/frontend/src/features/polish/components/mod.rs b/services/frontend/frontend/src/features/polish/components/mod.rs deleted file mode 100644 index 2bd9b1f..0000000 --- a/services/frontend/frontend/src/features/polish/components/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod mascot_chatbot; -pub mod particle_background; -pub mod theme_toggle; - -pub use mascot_chatbot::MascotChatbot; -pub use particle_background::ParticleBackground; -pub use theme_toggle::ThemeToggle; diff --git a/services/frontend/frontend/src/features/polish/components/particle_background.rs b/services/frontend/frontend/src/features/polish/components/particle_background.rs deleted file mode 100644 index 6d9de95..0000000 --- a/services/frontend/frontend/src/features/polish/components/particle_background.rs +++ /dev/null @@ -1,12 +0,0 @@ -use leptos::prelude::*; - -#[component] -pub fn ParticleBackground() -> impl IntoView { - view! { - - } -} diff --git a/services/frontend/frontend/src/features/polish/components/theme_toggle.rs b/services/frontend/frontend/src/features/polish/components/theme_toggle.rs deleted file mode 100644 index b92ecd9..0000000 --- a/services/frontend/frontend/src/features/polish/components/theme_toggle.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::features::polish::{persist_theme, ThemeContext}; -use leptos::prelude::*; -use crate::{log_info, make_logger}; - -make_logger!(); - -#[component] -pub fn ThemeToggle() -> impl IntoView { - let theme_ctx = use_context::(); - let theme_for_label = theme_ctx.clone(); - let theme_for_toggle = theme_ctx.clone(); - - let is_dark = move || { - theme_for_label - .as_ref() - .map(|ctx| ctx.theme.get() == "dark") - .unwrap_or(false) - }; - - let toggle = move |_| { - if let Some(ctx) = theme_for_toggle.as_ref() { - let next = if ctx.theme.get() == "dark" { - "light" - } else { - "dark" - }; - log_info!("Theme toggled to {}", next); - ctx.theme.set(next.to_string()); - persist_theme(next); - } - }; - - view! { - - } -} diff --git a/services/frontend/frontend/src/features/polish/mod.rs b/services/frontend/frontend/src/features/polish/mod.rs deleted file mode 100644 index 961d03c..0000000 --- a/services/frontend/frontend/src/features/polish/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -pub mod components; - -use leptos::prelude::*; -use crate::{log_info, make_logger}; - -make_logger!(); - -#[derive(Clone)] -pub struct ThemeContext { - pub theme: RwSignal, -} - -pub fn initial_theme() -> String { - let theme = web_sys::window() - .and_then(|window| window.local_storage().ok().flatten()) - .and_then(|storage| storage.get_item("imphnen-theme").ok().flatten()) - .filter(|value| value == "dark" || value == "light") - .unwrap_or_else(|| "dark".to_string()); - log_info!("Initial theme resolved: {}", theme); - theme -} - -pub fn persist_theme(theme: &str) { - log_info!("Persisting theme: {}", theme); - if let Some(storage) = - web_sys::window().and_then(|window| window.local_storage().ok().flatten()) - { - let _ = storage.set_item("imphnen-theme", theme); - } -} diff --git a/services/frontend/frontend/src/layout/dashboard_layout.rs b/services/frontend/frontend/src/layout/dashboard_layout.rs deleted file mode 100644 index 1172436..0000000 --- a/services/frontend/frontend/src/layout/dashboard_layout.rs +++ /dev/null @@ -1,18 +0,0 @@ -// services/frontend-leptos/frontend/src/layout/dashboard_layout.rs -use super::mobile_tab_bar::MobileTabBar; -use super::sidebar::Sidebar; -use leptos::children::Children; -use leptos::prelude::*; - -#[component] -pub fn DashboardLayout(children: Children) -> impl IntoView { - view! { -
- -
- {children()} -
- -
- } -} diff --git a/services/frontend/frontend/src/layout/header.rs b/services/frontend/frontend/src/layout/header.rs deleted file mode 100644 index b61d927..0000000 --- a/services/frontend/frontend/src/layout/header.rs +++ /dev/null @@ -1,59 +0,0 @@ -// services/frontend-leptos/frontend/src/layout/header.rs -use crate::ws::context::WsContext; -use crate::ws::handlers::WsStatus; -use leptos::prelude::*; - - -#[component] -pub fn Header() -> impl IntoView { - let ws = use_context::().expect("WsContext not provided"); - let ws_status = ws.status; - - let indicator_text_memo = Memo::new(move |_| match ws_status.get() { - WsStatus::Connected => "Online", - WsStatus::Connecting => "Menghubungkan...", - WsStatus::Disconnected => "Offline", - WsStatus::Error(_) => "Error", - }); - let indicator_color_memo = Memo::new(move |_| match ws_status.get() { - WsStatus::Connected => "var(--color-success)", - WsStatus::Connecting => "var(--color-warning)", - WsStatus::Disconnected => "var(--text-tertiary)", - WsStatus::Error(_) => "var(--color-error)", - }); - let is_connecting = Memo::new(move |_| matches!(ws_status.get(), WsStatus::Connecting)); - - view! { -
-
- - "IMPHNEN" - - - "Guild Watcher" - -
- -
-
- - {move || indicator_text_memo.get()} -
-
-
- } -} diff --git a/services/frontend/frontend/src/layout/mobile_tab_bar.rs b/services/frontend/frontend/src/layout/mobile_tab_bar.rs deleted file mode 100644 index 3ec29f8..0000000 --- a/services/frontend/frontend/src/layout/mobile_tab_bar.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::app::UiContext; -use leptos::prelude::*; -use shared_types::ui_state::Tab; - -#[component] -pub fn MobileTabBar() -> impl IntoView { - let ui = use_context::().expect("UiContext not provided"); - - view! { -
- - - -
- } -} - -#[component] -fn MobileTabItem(label: &'static str, tab: Tab, ui: UiContext) -> impl IntoView { - let tab_active = tab.clone(); - let tab_click = tab; - - view! { - - } -} diff --git a/services/frontend/frontend/src/layout/mod.rs b/services/frontend/frontend/src/layout/mod.rs deleted file mode 100644 index 87dcf77..0000000 --- a/services/frontend/frontend/src/layout/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -// services/frontend-leptos/frontend/src/layout/mod.rs -pub mod dashboard_layout; -pub mod mobile_tab_bar; -pub mod sidebar; -pub mod tab_strip; diff --git a/services/frontend/frontend/src/layout/sidebar.rs b/services/frontend/frontend/src/layout/sidebar.rs deleted file mode 100644 index 91ed77d..0000000 --- a/services/frontend/frontend/src/layout/sidebar.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::app::UiContext; -use crate::features::polish::{persist_theme, ThemeContext}; -use crate::ws::context::WsContext; -use crate::ws::handlers::WsStatus; -use leptos::prelude::*; -use shared_types::ui_state::Tab; - -#[component] -pub fn Sidebar() -> impl IntoView { - let ui = use_context::().expect("UiContext not provided"); - let ws = use_context::(); - let theme_ctx = use_context::(); - - // WS status - let ws_status = ws.as_ref().map(|w| w.status); - let status_text = Memo::new(move |_| match ws_status.map(|s| s.get()) { - Some(WsStatus::Connected) => "Online", - Some(WsStatus::Connecting) => "Menghubungkan...", - Some(WsStatus::Disconnected) => "Offline", - Some(WsStatus::Error(_)) => "Error", - None => "Offline", - }); - let status_color = Memo::new(move |_| match ws_status.map(|s| s.get()) { - Some(WsStatus::Connected) => "var(--color-success)", - Some(WsStatus::Connecting) => "var(--color-warning)", - Some(WsStatus::Disconnected) => "var(--text-tertiary)", - Some(WsStatus::Error(_)) => "var(--color-error)", - None => "var(--text-tertiary)", - }); - let is_connecting = Memo::new(move |_| matches!(ws_status.map(|s| s.get()), Some(WsStatus::Connecting))); - - // Theme toggle - let theme_ctx_for_dark = theme_ctx.clone(); - let is_dark = move || { - theme_ctx_for_dark - .as_ref() - .map(|ctx| ctx.theme.get() == "dark") - .unwrap_or(false) - }; - let toggle_theme = move |_| { - if let Some(ctx) = theme_ctx.as_ref() { - let next = if ctx.theme.get() == "dark" { "light" } else { "dark" }; - ctx.theme.set(next.to_string()); - persist_theme(next); - } - }; - - view! { - - } -} - -#[component] -fn SidebarNavItem(icon: &'static str, label: &'static str, tab: Tab, ui: UiContext) -> impl IntoView { - let tab_for_active = tab.clone(); - let tab_for_click = tab; - - view! { - - } -} diff --git a/services/frontend/frontend/src/layout/tab_strip.rs b/services/frontend/frontend/src/layout/tab_strip.rs deleted file mode 100644 index f438a74..0000000 --- a/services/frontend/frontend/src/layout/tab_strip.rs +++ /dev/null @@ -1,51 +0,0 @@ -// services/frontend-leptos/frontend/src/layout/tab_strip.rs -use crate::app::UiContext; -use leptos::prelude::*; -use shared_types::ui_state::Tab; - - -#[component] -pub fn TabStrip() -> impl IntoView { - let ui = use_context::().expect("UiContext not provided"); - - view! { -
- - - -
- } -} - -#[component] -fn TabItem(label: &'static str, tab: Tab, ui: UiContext) -> impl IntoView { - let tab_color = tab.clone(); - let tab_border = tab.clone(); - let tab_click = tab; - view! { - - } -} diff --git a/services/frontend/frontend/src/lib.rs b/services/frontend/frontend/src/lib.rs deleted file mode 100644 index 94147b8..0000000 --- a/services/frontend/frontend/src/lib.rs +++ /dev/null @@ -1,21 +0,0 @@ -pub mod api; -pub mod app; -pub mod auth; -pub mod features; -pub mod layout; -pub mod logger; -pub mod ui; -pub mod ws; - - -use wasm_bindgen::prelude::*; - -make_logger!(); - -#[wasm_bindgen(start)] -pub fn start() { - console_error_panic_hook::set_once(); - wasm_logger::init(wasm_logger::Config::default()); - log_info!("IMPHNEN frontend starting..."); - leptos::mount::mount_to_body(app::App); -} diff --git a/services/frontend/frontend/src/logger.rs b/services/frontend/frontend/src/logger.rs deleted file mode 100644 index e7472c8..0000000 --- a/services/frontend/frontend/src/logger.rs +++ /dev/null @@ -1,177 +0,0 @@ -// services/frontend/frontend/src/logger.rs -// Structured logging for WASM browser console with levels, timestamps, and styled output. - -/// Log level with numeric priority (lower = more verbose). -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum LogLevel { - Trace = 0, - Debug = 1, - Info = 2, - Warn = 3, - Error = 4, -} - -impl LogLevel { - fn as_str(&self) -> &'static str { - match self { - LogLevel::Trace => "TRACE", - LogLevel::Debug => "DEBUG", - LogLevel::Info => "INFO", - LogLevel::Warn => "WARN", - LogLevel::Error => "ERROR", - } - } - - /// CSS color for the browser console label. - fn console_style(&self) -> &'static str { - match self { - LogLevel::Trace => "color:#888", - LogLevel::Debug => "color:#54a2ff", - LogLevel::Info => "color:#23a1eb;font-weight:bold", - LogLevel::Warn => "color:#f59e0b;font-weight:bold", - LogLevel::Error => "color:#e4405f;font-weight:bold", - } - } -} - -/// A per-module logger that produces styled, timestamped console output. -#[derive(Clone)] -pub struct Logger { - module: &'static str, - min_level: LogLevel, -} - -impl Logger { - /// Create a logger for a given module path (call with `module_path!()`). - pub const fn new(module: &'static str, min_level: LogLevel) -> Self { - Self { module, min_level } - } - - /// Create a logger that shows everything (min_level = Trace). - pub const fn verbose(module: &'static str) -> Self { - Self::new(module, LogLevel::Trace) - } - - /// Format an ISO-like timestamp from `Date.now()`. - fn timestamp() -> String { - let d = js_sys::Date::new_0(); - // HH:MM:SS.mmm - format!( - "{:02}:{:02}:{:02}.{:03}", - d.get_hours(), - d.get_minutes(), - d.get_seconds(), - d.get_milliseconds() - ) - } - - fn should_log(&self, level: LogLevel) -> bool { - level >= self.min_level - } - - fn log_inner(&self, level: LogLevel, message: &str) { - if !self.should_log(level) { - return; - } - let ts = Self::timestamp(); - let lvl_str = level.as_str(); - let style = level.console_style(); - let styled = format!("%c{:.7} [{}] {}", ts, self.module, message); - match level { - LogLevel::Error => { - web_sys::console::error_3( - &styled.into(), - &style.into(), - &"".into(), - ); - } - LogLevel::Warn => { - web_sys::console::warn_3( - &styled.into(), - &style.into(), - &"".into(), - ); - } - _ => { - web_sys::console::log_3( - &styled.into(), - &style.into(), - &"".into(), - ); - } - } - } - - pub fn trace(&self, msg: &str) { - self.log_inner(LogLevel::Trace, msg); - } - - pub fn debug(&self, msg: &str) { - self.log_inner(LogLevel::Debug, msg); - } - - pub fn info(&self, msg: &str) { - self.log_inner(LogLevel::Info, msg); - } - - pub fn warn(&self, msg: &str) { - self.log_inner(LogLevel::Warn, msg); - } - - pub fn error(&self, msg: &str) { - self.log_inner(LogLevel::Error, msg); - } - - /// Log with a dynamic format string. - pub fn info_fmt(&self, fmt: &str, args: &[&dyn std::fmt::Display]) { - let msg = if args.is_empty() { - fmt.to_string() - } else { - let mut s = String::new(); - let mut iter = args.iter(); - for part in fmt.split("{}") { - s.push_str(part); - if let Some(arg) = iter.next() { - s.push_str(&arg.to_string()); - } - } - s - }; - self.log_inner(LogLevel::Info, &msg); - } -} - -/// Macro to create a module-level logger at `Info` level. -/// Usage: `log::module!()` at the top of a source file (after imports). -#[macro_export] -macro_rules! make_logger { - () => { - static LOGGER: std::sync::LazyLock<$crate::logger::Logger> = - std::sync::LazyLock::new(|| { - $crate::logger::Logger::new(module_path!(), $crate::logger::LogLevel::Trace) - }); - }; -} - -/// Convenience macros that log through the module's static LOGGER. -/// Usage: `log_info!("something happened")`. -#[macro_export] -macro_rules! log_trace { - ($($arg:tt)*) => { LOGGER.trace(&format!($($arg)*)); }; -} -#[macro_export] -macro_rules! log_debug { - ($($arg:tt)*) => { LOGGER.debug(&format!($($arg)*)); }; -} -#[macro_export] -macro_rules! log_info { - ($($arg:tt)*) => { LOGGER.info(&format!($($arg)*)); }; -} -#[macro_export] -macro_rules! log_warn { - ($($arg:tt)*) => { LOGGER.warn(&format!($($arg)*)); }; -} -#[macro_export] -macro_rules! log_error { - ($($arg:tt)*) => { LOGGER.error(&format!($($arg)*)); }; -} diff --git a/services/frontend/frontend/src/styles/animations.css b/services/frontend/frontend/src/styles/animations.css deleted file mode 100644 index a33f36e..0000000 --- a/services/frontend/frontend/src/styles/animations.css +++ /dev/null @@ -1,543 +0,0 @@ -/* ── Animations — Live Dashboard Effects ──────────────── * - * Keyframes, applied animations, and utility classes. * - * Respects prefers-reduced-motion. * - * ──────────────────────────────────────────────────────── */ - -/* ════════════════════════════════════════════════════════ - 1. KEYFRAMES - ════════════════════════════════════════════════════════ */ - -/* ── Entry / Reveal ─────────────────────────────────── */ -@keyframes slide-in-left { - from { opacity: 0; transform: translateX(-20px); } - to { opacity: 1; transform: translateX(0); } -} - -@keyframes slide-in-down { - from { opacity: 0; transform: translateY(-12px); } - to { opacity: 1; transform: translateY(0); } -} - -@keyframes scale-bounce { - 0% { opacity: 0; transform: scale(0.85); } - 60% { opacity: 1; transform: scale(1.04); } - 100% { opacity: 1; transform: scale(1); } -} - -@keyframes pop-in { - 0% { opacity: 0; transform: scale(0.8); } - 70% { opacity: 1; transform: scale(1.08); } - 100% { opacity: 1; transform: scale(1); } -} - -/* ── Status / Alive ─────────────────────────────────── */ -@keyframes breathe { - 0%, 100% { opacity: 1; transform: scale(1); box-shadow: 0 0 4px currentColor; } - 50% { opacity: 0.6; transform: scale(1.2); box-shadow: 0 0 14px currentColor; } -} - -@keyframes breathe-soft { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -@keyframes ring-pulse { - 0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); } - 70% { box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); } - 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); } -} - -@keyframes ring-pulse-success { - 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); } - 70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); } - 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); } -} - -@keyframes ring-pulse-error { - 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); } - 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } - 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } -} - -@keyframes processing-sweep { - 0% { background-position: -200% center; } - 100% { background-position: 200% center; } -} - -@keyframes live-indicator { - 0%, 100% { opacity: 1; box-shadow: 0 0 6px rgba(239, 68, 68, 0.4); } - 50% { opacity: 0.7; box-shadow: 0 0 16px rgba(239, 68, 68, 0.7); } -} - -/* ── Ambient / Decorative ───────────────────────────── */ -/* Slow breathing for background glow layers */ -@keyframes ambient-glow { - 0%, 100% { opacity: 0.3; } - 50% { opacity: 0.7; } -} - -/* Dynamic mesh drift — shifts gradient center positions */ -@keyframes bg-mesh-drift { - 0% { background-position: 0% 0%, 100% 100%, 50% 50%; } - 25% { background-position: 30% 20%, 70% 80%, 20% 60%; } - 50% { background-position: 60% 10%, 40% 60%, 80% 30%; } - 75% { background-position: 20% 60%, 80% 20%, 40% 80%; } - 100% { background-position: 0% 0%, 100% 100%, 50% 50%; } -} - -/* Slow hue rotation for orb glow */ -@keyframes hue-rotate-slow { - 0% { filter: hue-rotate(0deg); } - 50% { filter: hue-rotate(30deg); } - 100% { filter: hue-rotate(0deg); } -} - -/* Orb size pulsing */ -@keyframes orb-pulse { - 0%, 100% { transform: scale(1); opacity: 0.4; } - 50% { transform: scale(1.08); opacity: 0.6; } -} - -/* Expanded orb drift — wider, slower, organic */ -@keyframes orb-drift-enhanced { - 0% { transform: translate(0, 0) scale(1); } - 20% { transform: translate(80px, -50px) scale(1.1); } - 40% { transform: translate(-50px, 70px) scale(0.9); } - 60% { transform: translate(100px, 30px) scale(1.05); } - 80% { transform: translate(-70px, -60px) scale(0.95); } - 100% { transform: translate(0, 0) scale(1); } -} - -/* Grain/noise texture shifting */ -@keyframes grain-shift { - 0%, 100% { transform: translate(0, 0) rotate(0deg); } - 10% { transform: translate(-5%, -5%) rotate(0.5deg); } - 20% { transform: translate(-10%, 0%) rotate(-0.5deg); } - 30% { transform: translate(0%, 5%) rotate(1deg); } - 40% { transform: translate(5%, -3%) rotate(-0.3deg); } - 50% { transform: translate(-3%, -8%) rotate(0.8deg); } - 60% { transform: translate(8%, 3%) rotate(-0.6deg); } - 70% { transform: translate(-6%, 6%) rotate(0.4deg); } - 80% { transform: translate(4%, -6%) rotate(-0.7deg); } - 90% { transform: translate(-2%, 2%) rotate(0.2deg); } -} - -@keyframes gradient-shimmer { - 0% { background-position: 0% center; } - 100% { background-position: 200% center; } -} - -@keyframes float-variation { - 0%, 100% { transform: translateY(0) rotate(0deg); } - 33% { transform: translateY(-8px) rotate(1.5deg); } - 66% { transform: translateY(-4px) rotate(-1deg); } -} - -@keyframes fade-in-down { - from { opacity: 0; transform: translateY(-10px); } - to { opacity: 1; transform: translateY(0); } -} - -/* ── Interactive ────────────────────────────────────── */ -@keyframes glow-border { - 0%, 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); } - 50% { box-shadow: 0 0 12px 1px rgba(59, 130, 246, 0.15); } -} - -@keyframes ripple { - 0% { transform: scale(0); opacity: 0.6; } - 100% { transform: scale(4); opacity: 0; } -} - -/* ── Bar pulses (audio / visualizer) ────────────────── */ -@keyframes bar-pulse-1 { - 0%, 100% { transform: scaleY(1); } - 50% { transform: scaleY(0.4); } -} -@keyframes bar-pulse-2 { - 0%, 100% { transform: scaleY(0.5); } - 50% { transform: scaleY(1.2); } -} -@keyframes bar-pulse-3 { - 0%, 100% { transform: scaleY(0.7); } - 50% { transform: scaleY(1); } -} - -@keyframes count-up { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } -} - - -/* ════════════════════════════════════════════════════════ - 2. UTILITY ANIMATION CLASSES - Use these in Rust components: class="animate-breathe" - ════════════════════════════════════════════════════════ */ - -.animate-breathe { - animation: breathe 3s ease-in-out infinite; -} - -.animate-breathe-soft { - animation: breathe-soft 2s ease-in-out infinite; -} - -.animate-ring-pulse { - animation: ring-pulse 2s ease-in-out infinite; -} - -.animate-live-indicator { - animation: live-indicator 1.5s ease-in-out infinite; -} - -.animate-float { - animation: float-variation 5s ease-in-out infinite; -} - -.animate-gradient-shimmer { - background-size: 200% 100%; - animation: gradient-shimmer 4s ease-in-out infinite alternate; -} - -.animate-spin-slow { - animation: spin 3s linear infinite; -} - -.animate-pulse-connecting { - animation: pulse-connecting 1s ease-in-out infinite; -} - -.animate-scale-bounce { - animation: scale-bounce 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) both; -} - -.animate-pop-in { - animation: pop-in 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) both; -} - -.animate-fade-in-down { - animation: fade-in-down var(--transition-normal) ease-out; -} - -.animate-slide-in-left { - animation: slide-in-left var(--transition-normal) ease-out; -} - -.animate-count-up { - animation: count-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; -} - -.animate-shimmer-sweep { - background: linear-gradient( - 110deg, - transparent 30%, - rgba(59, 130, 246, 0.08) 50%, - transparent 70% - ); - background-size: 200% 100%; - animation: processing-sweep 1.5s ease-in-out infinite; -} - - -/* ════════════════════════════════════════════════════════ - 3. APPLIED ANIMATIONS — auto-wired to selectors - ════════════════════════════════════════════════════════ */ - -/* ── Ambient animated mesh background ────────────────── */ -.app-shell::before { - content: ''; - position: fixed; - inset: -50%; - pointer-events: none; - z-index: -1; - background: - radial-gradient(ellipse at 20% 30%, rgba(59, 130, 246, 0.06) 0%, transparent 40%), - radial-gradient(ellipse at 80% 70%, rgba(99, 102, 241, 0.05) 0%, transparent 40%), - radial-gradient(ellipse at 40% 80%, rgba(16, 185, 129, 0.03) 0%, transparent 40%), - radial-gradient(ellipse at 60% 20%, rgba(245, 158, 11, 0.02) 0%, transparent 40%); - background-size: 200% 200%, 200% 200%, 200% 200%, 200% 200%; - animation: bg-mesh-drift 20s ease-in-out infinite alternate; - will-change: background-position; -} - -/* ── Subtle noise/grain texture overlay ─────────────── */ -.app-shell::after { - content: ''; - position: fixed; - inset: -50%; - pointer-events: none; - z-index: 0; - opacity: 0.015; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); - background-repeat: repeat; - background-size: 256px 256px; - animation: grain-shift 0.5s steps(4) infinite; - will-change: transform; -} - -/* ── Light theme mesh adjustment ────────────────────── */ -[data-theme="light"] .app-shell::before { - background: - radial-gradient(ellipse at 20% 30%, rgba(37, 99, 235, 0.04) 0%, transparent 40%), - radial-gradient(ellipse at 80% 70%, rgba(99, 102, 241, 0.04) 0%, transparent 40%), - radial-gradient(ellipse at 40% 80%, rgba(16, 185, 129, 0.03) 0%, transparent 40%), - radial-gradient(ellipse at 60% 20%, rgba(245, 158, 11, 0.02) 0%, transparent 40%); -} -[data-theme="light"] .app-shell::after { - opacity: 0.008; -} - -/* ── Gradient text shimmer (brand elements) ─────────── */ -.sidebar-brand-name, -.live-title, -.auth-title { - background-size: 200% 100%; - animation: gradient-shimmer 6s ease-in-out infinite alternate; -} - -/* ── Connected status dot: alive breath ─────────────── */ -.status-dot, -.voice-connection-dot.connected { - animation: breathe 3s ease-in-out infinite; -} - -/* Keep existing connecting animation (overrides above) */ -.status-dot.is-connecting, -.voice-connection-dot.connecting { - animation: pulse-connecting 1s ease-in-out infinite !important; -} - -.status-dot.disconnected, -.voice-connection-dot.disconnected { - animation: none !important; -} - -/* ── Live indicator (red recording dot) ─────────────── */ -.msg-card-ai-badge.flagged::before, -.msg-analysis.flagged::before { - content: ''; - display: inline-block; - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--color-error); - margin-right: 4px; - vertical-align: middle; - animation: live-indicator 1.5s ease-in-out infinite; -} - -/* ── Particle orbs: ambient breathing overlay ───────── */ -.particle-bg { - animation: ambient-glow 6s ease-in-out infinite alternate; -} - -/* ── Live bento grid: staggered entry ───────────────── */ -.live-bento > * { - animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) both; -} -.live-bento > :nth-child(1) { animation-delay: 0.03s; } -.live-bento > :nth-child(2) { animation-delay: 0.06s; } -.live-bento > :nth-child(3) { animation-delay: 0.09s; } -.live-bento > :nth-child(4) { animation-delay: 0.12s; } -.live-bento > :nth-child(5) { animation-delay: 0.15s; } -.live-bento > :nth-child(6) { animation-delay: 0.18s; } - -/* ── Dashboard metric cards: staggered entry ────────── */ -.dashboard-metric-card { - animation: fade-in-up 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; -} -.dashboard-metric-card:nth-child(1) { animation-delay: 0.04s; } -.dashboard-metric-card:nth-child(2) { animation-delay: 0.08s; } -.dashboard-metric-card:nth-child(3) { animation-delay: 0.12s; } -.dashboard-metric-card:nth-child(4) { animation-delay: 0.16s; } -.dashboard-metric-card:nth-child(5) { animation-delay: 0.20s; } -.dashboard-metric-card:nth-child(6) { animation-delay: 0.24s; } -.dashboard-metric-card:nth-child(7) { animation-delay: 0.28s; } -.dashboard-metric-card:nth-child(8) { animation-delay: 0.32s; } - -/* ── Dashboard rows: staggered fade-in ──────────────── */ -.dashboard-summary-row, -.dashboard-top-channel-row { - animation: fade-in 0.4s ease-out both; -} -.dashboard-summary-row:nth-child(1), .dashboard-top-channel-row:nth-child(1) { animation-delay: 0.02s; } -.dashboard-summary-row:nth-child(2), .dashboard-top-channel-row:nth-child(2) { animation-delay: 0.04s; } -.dashboard-summary-row:nth-child(3), .dashboard-top-channel-row:nth-child(3) { animation-delay: 0.06s; } -.dashboard-summary-row:nth-child(4), .dashboard-top-channel-row:nth-child(4) { animation-delay: 0.08s; } -.dashboard-summary-row:nth-child(5), .dashboard-top-channel-row:nth-child(5) { animation-delay: 0.10s; } -.dashboard-summary-row:nth-child(6), .dashboard-top-channel-row:nth-child(6) { animation-delay: 0.12s; } -.dashboard-summary-row:nth-child(7), .dashboard-top-channel-row:nth-child(7) { animation-delay: 0.14s; } -.dashboard-summary-row:nth-child(8), .dashboard-top-channel-row:nth-child(8) { animation-delay: 0.16s; } -.dashboard-summary-row:nth-child(9), .dashboard-top-channel-row:nth-child(9) { animation-delay: 0.18s; } -.dashboard-summary-row:nth-child(10),.dashboard-top-channel-row:nth-child(10) { animation-delay: 0.20s; } - -/* ── Message cards: staggered entry ─────────────────── */ -.msg-card { - animation: fade-in-up 0.4s cubic-bezier(0.16, 1, 0.3, 1) both; -} -.msg-card:nth-child(1) { animation-delay: 0.02s; } -.msg-card:nth-child(2) { animation-delay: 0.05s; } -.msg-card:nth-child(3) { animation-delay: 0.08s; } -.msg-card:nth-child(4) { animation-delay: 0.11s; } -.msg-card:nth-child(5) { animation-delay: 0.14s; } -.msg-card:nth-child(6) { animation-delay: 0.17s; } -.msg-card:nth-child(7) { animation-delay: 0.20s; } -.msg-card:nth-child(8) { animation-delay: 0.23s; } -.msg-card:nth-child(9) { animation-delay: 0.26s; } -.msg-card:nth-child(10) { animation-delay: 0.29s; } - -/* ── Recording items: slide in from left ────────────── */ -.rec-item { - animation: slide-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; -} -.rec-item:nth-child(1) { animation-delay: 0.02s; } -.rec-item:nth-child(2) { animation-delay: 0.06s; } -.rec-item:nth-child(3) { animation-delay: 0.10s; } -.rec-item:nth-child(4) { animation-delay: 0.14s; } -.rec-item:nth-child(5) { animation-delay: 0.18s; } - -/* ── Speaker items: pop in ──────────────────────────── */ -.speak-item { - animation: fade-in 0.3s ease-out both; -} -.speak-item:nth-child(1) { animation-delay: 0.02s; } -.speak-item:nth-child(2) { animation-delay: 0.05s; } -.speak-item:nth-child(3) { animation-delay: 0.08s; } -.speak-item:nth-child(4) { animation-delay: 0.11s; } -.speak-item:nth-child(5) { animation-delay: 0.14s; } - -/* ── Audio visualizer bars: individual timing ───────── */ -.audio-bar { - animation: bar-pulse-2 1.2s ease-in-out infinite; -} -.audio-bar:nth-child(odd) { - animation-name: bar-pulse-1; - animation-duration: 0.9s; -} -.audio-bar:nth-child(3n) { - animation-name: bar-pulse-3; - animation-duration: 1.5s; -} - -/* ── Interactive cards: hover glow effect ────────────── */ -.card-interactive:hover { - animation: glow-border 0.8s ease-in-out; - border-color: var(--color-primary); -} - -/* ── Nav active indicator: continuous subtle glow ───── */ -.sidebar-nav-item.is-active::after { - animation: indicator-grow 250ms cubic-bezier(0.16, 1, 0.3, 1) both, - breathe-soft 3s ease-in-out 0.3s infinite; -} - -/* ── Filter chip active: pop scale ──────────────────── */ -.filter-chip-v2.is-active { - animation: pop-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) both; -} - -/* ── AI badge processing: shimmer sweep ─────────────── */ -.msg-card-ai-badge.processing, -.status-badge-processing { - background: linear-gradient( - 110deg, - var(--color-primary-muted) 25%, - rgba(59, 130, 246, 0.35) 50%, - var(--color-primary-muted) 75% - ) !important; - background-size: 200% 100% !important; - animation: processing-sweep 1.4s ease-in-out infinite; -} -.status-badge-processing::before { - animation: ring-pulse 1.2s ease-in-out infinite !important; -} - -/* ── Image grid items: reveal ───────────────────────── */ -.image-grid-item { - animation: fade-in 0.4s ease-out both; -} -.image-grid-item:nth-child(1) { animation-delay: 0.02s; } -.image-grid-item:nth-child(2) { animation-delay: 0.05s; } -.image-grid-item:nth-child(3) { animation-delay: 0.08s; } -.image-grid-item:nth-child(4) { animation-delay: 0.11s; } -.image-grid-item:nth-child(5) { animation-delay: 0.14s; } -.image-grid-item:nth-child(6) { animation-delay: 0.17s; } -.image-grid-item:nth-child(7) { animation-delay: 0.20s; } -.image-grid-item:nth-child(8) { animation-delay: 0.23s; } - -/* ── Dashboard queue value: count-up feel ───────────── */ -.dashboard-queue-value { - animation: count-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; -} - -/* ── Voice connection panel: entry ──────────────────── */ -.voice-connection { - animation: fade-in-up 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; -} - -/* ── Now playing: slide in ──────────────────────────── */ -.np-body { - animation: slide-in-left 0.4s cubic-bezier(0.16, 1, 0.3, 1) both; -} - -/* ── Skeleton: enhanced shimmer ─────────────────────── */ -.skeleton, -.msg-skel-avatar, -.msg-skel-line, -.msg-skel-badge, -.dashboard-skel-avatar, -.dashboard-skel-line { - background: linear-gradient( - 110deg, - var(--surface-container) 28%, - rgba(59, 130, 246, 0.06) 48%, - var(--surface-container) 68% - ) !important; - background-size: 200% 100% !important; -} - -/* ── Tab content: stronger entry ────────────────────── */ -.tab-content { - animation: fade-in-up 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; -} - -/* ── Mobile tab active: subtle indicator ────────────── */ -.mobile-tab-item.is-active { - animation: pop-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) both; -} - -/* ── Mascot launcher: enhanced float ────────────────── */ -.mascot-launcher { - animation: float-variation 5s ease-in-out infinite; -} -.mascot-launcher:hover { - animation: none !important; -} - -/* ── Empty state icons: subtle float ────────────────── */ -.empty-state-icon { - animation: float-variation 6s ease-in-out infinite; -} - -/* ── Recent message action buttons ──────────────────── */ -.btn-icon-sm:active:not(:disabled), -.btn-icon:active:not(:disabled) { - animation: ripple 0.4s ease-out; -} - -/* ── Small decorative: channel list rows ────────────── */ -.dashboard-top-channel-row:hover { - animation: none; /* Keep the existing hover translateX */ -} - - -/* ════════════════════════════════════════════════════════ - 4. RESPECT REDUCED MOTION - ════════════════════════════════════════════════════════ */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} diff --git a/services/frontend/frontend/src/styles/bundle.css b/services/frontend/frontend/src/styles/bundle.css deleted file mode 100644 index c3eb139..0000000 --- a/services/frontend/frontend/src/styles/bundle.css +++ /dev/null @@ -1,3120 +0,0 @@ -/* ── Design Tokens — IMPHNEN Neuform Dark ────────────── * - * Dark default (no attribute selector) * - * Light: [data-theme="light"] * - * ──────────────────────────────────────────────────────── */ - -:root { - /* ── Surfaces ──────────────────────────────────── */ - --surface-base: #050510; - --surface-raised: #0a0a1a; - --surface-overlay: #12122a; - --surface-container: #1a1a35; - --surface-border: rgba(255, 255, 255, 0.06); - --surface-hover: rgba(59, 130, 246, 0.06); - --surface-glass: rgba(5, 5, 16, 0.78); - - /* ── Text ──────────────────────────────────────── */ - --text-primary: #f1f1f9; - --text-secondary: #9d9db5; - --text-tertiary: #5c5c78; - --text-inverse: #050510; - - /* ── Brand ─────────────────────────────────────── */ - --color-primary: #3b82f6; - --color-primary-hover: #60a5fa; - --color-primary-active: #2563eb; - --color-primary-muted: rgba(59, 130, 246, 0.12); - --gradient-primary: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%); - --gradient-brand: linear-gradient(135deg, #3b82f6 0%, #5865f2 50%, #6366f1 100%); - - /* ── Semantics ─────────────────────────────────── */ - --color-success: #10b981; - --color-warning: #f59e0b; - --color-error: #ef4444; - --color-info: #3b82f6; - --color-destructive: #ef4444; - - /* ── AI Status ─────────────────────────────────── */ - --color-ai-flagged: #ef4444; - --color-ai-clean: #10b981; - --color-ai-warn: #f59e0b; - --color-ai-pending: #5c5c78; - --color-ai-processing: #3b82f6; - --color-ai-error: #dc2626; - --color-ai-deleted: #6b7280; - - /* ── Shadows ───────────────────────────────────── */ - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35); - --shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.4); - - /* ── Radii ─────────────────────────────────────── */ - --radius-sm: 8px; - --radius-md: 12px; - --radius-lg: 16px; - --radius-xl: 23px; - --radius-pill: 9999px; - - /* ── Spacing ───────────────────────────────────── */ - --space-0: 0px; - --space-1: 4px; - --space-2: 8px; - --space-3: 12px; - --space-4: 16px; - --space-5: 20px; - --space-6: 24px; - --space-8: 32px; - --space-10: 40px; - --space-12: 48px; - --space-16: 64px; - - /* ── Layout ────────────────────────────────────── */ - --sidebar-width: 240px; - --header-height: 0px; - - /* ── Transitions ───────────────────────────────── */ - --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-normal: 300ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-slow: 500ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-bounce: 400ms cubic-bezier(0.34, 1.56, 0.64, 1); - - /* ── Z-index ───────────────────────────────────── */ - --z-sidebar: 30; - --z-header: 40; - --z-overlay: 50; - --z-modal: 60; - --z-toast: 70; -} - -/* ── Light Theme ──────────────────────────────────────── */ -[data-theme="light"] { - --surface-base: #f4f6fb; - --surface-raised: #ffffff; - --surface-overlay: #eeeff4; - --surface-container: #dde0e8; - --surface-border: rgba(0, 0, 0, 0.06); - --surface-hover: rgba(37, 99, 235, 0.05); - --surface-glass: rgba(255, 255, 255, 0.72); - - --text-primary: #0f172a; - --text-secondary: #475569; - --text-tertiary: #94a3b8; - --text-inverse: #ffffff; - - --color-primary: #2563eb; - --color-primary-hover: #3b82f6; - --color-primary-active: #1d4ed8; - --color-primary-muted: rgba(37, 99, 235, 0.1); - --gradient-primary: linear-gradient(135deg, #2563eb 0%, #6366f1 100%); - --gradient-brand: linear-gradient(135deg, #2563eb 0%, #5865f2 50%, #6366f1 100%); - - --color-success: #10b981; - --color-warning: #f59e0b; - --color-error: #ef4444; - --color-info: #2563eb; - --color-destructive: #ef4444; - - --color-ai-flagged: #ef4444; - --color-ai-clean: #10b981; - --color-ai-warn: #f59e0b; - --color-ai-pending: #94a3b8; - --color-ai-processing: #2563eb; - --color-ai-error: #dc2626; - --color-ai-deleted: #6b7280; - - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06); - --shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.08); -} -/* ── Reset & Base ─────────────────────────────────────── */ - -*, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - font-size: 16px; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - line-height: 1.6; - min-height: 100vh; - overflow-x: hidden; - background: var(--surface-base); - color: var(--text-primary); -} - -a { - color: var(--color-primary); - text-decoration: none; - transition: color var(--transition-fast); -} -a:hover { color: var(--color-primary-hover); } - -img { max-width: 100%; height: auto; } -svg { display: inline-block; vertical-align: middle; } - -/* ── Scrollbar ────────────────────────────────────────── */ -::-webkit-scrollbar { width: 6px; height: 6px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { - background: var(--color-primary); - border-radius: var(--radius-pill); -} - -::selection { - background: rgba(59, 130, 246, 0.25); - color: var(--text-primary); -} -[data-theme="light"] ::selection { - background: rgba(37, 99, 235, 0.25); -} -/* ── Utility Classes ──────────────────────────────────── */ - -/* Layout */ -.flex { display: flex; } -.inline-flex { display: inline-flex; } -.grid { display: grid; } -.block { display: block; } -.hidden { display: none; } -.flex-col { flex-direction: column; } -.flex-row { flex-direction: row; } -.flex-wrap { flex-wrap: wrap; } -.flex-1 { flex: 1 1 0%; } -.flex-shrink-0, .shrink-0 { flex-shrink: 0; } -.items-center { align-items: center; } -.items-start { align-items: flex-start; } -.items-end { align-items: flex-end; } -.items-baseline { align-items: baseline; } -.justify-center { justify-content: center; } -.justify-between { justify-content: space-between; } -.justify-end { justify-content: flex-end; } -.gap-0 { gap: 0; } -.gap-1 { gap: var(--space-1); } -.gap-1\.5 { gap: 6px; } -.gap-2 { gap: var(--space-2); } -.gap-3 { gap: var(--space-3); } -.gap-4 { gap: var(--space-4); } -.gap-6 { gap: var(--space-6); } -.gap-8 { gap: var(--space-8); } -.grid-cols-2 { grid-template-columns: repeat(2, 1fr); } -.grid-cols-3 { grid-template-columns: repeat(3, 1fr); } - -/* Width / Height */ -.w-full { width: 100%; } -.w-auto { width: auto; } -.h-full { height: 100%; } -.h-auto { height: auto; } -.min-w-0 { min-width: 0; } -.min-h-0 { min-height: 0; } - -/* Sizing helpers */ -.h-3 { height: 12px; } -.h-4 { height: 16px; } -.h-5 { height: 20px; } -.h-6 { height: 24px; } -.h-8 { height: 32px; } -.h-10 { height: 40px; } -.h-12 { height: 48px; } -.h-16 { height: 64px; } -.h-28 { height: 112px; } -.w-3 { width: 12px; } -.w-4 { width: 16px; } -.w-5 { width: 20px; } -.w-6 { width: 24px; } -.w-8 { width: 32px; } -.w-10 { width: 40px; } -.w-12 { width: 48px; } -.w-16 { width: 64px; } -.w-48 { width: 192px; } - -/* Position */ -.relative { position: relative; } -.absolute { position: absolute; } -.fixed { position: fixed; } -.sticky { position: sticky; } -.inset-0 { inset: 0; } -.top-0 { top: 0; } -.right-0 { right: 0; } -.bottom-0 { bottom: 0; } -.left-0 { left: 0; } - -/* Overflow */ -.overflow-auto { overflow: auto; } -.overflow-hidden { overflow: hidden; } -.overflow-y-auto { overflow-y: auto; } -.overflow-x-auto { overflow-x: auto; } - -/* Z-index */ -.z-0 { z-index: 0; } -.z-10 { z-index: 10; } -.z-50 { z-index: 50; } - -/* Margin */ -.m-0 { margin: 0; } -.mx-auto { margin-left: auto; margin-right: auto; } -.ml-auto { margin-left: auto; } -.mr-auto { margin-right: auto; } -.mt-0 { margin-top: 0; } -.mt-1 { margin-top: var(--space-1); } -.mt-2 { margin-top: var(--space-2); } -.mt-3 { margin-top: var(--space-3); } -.mt-4 { margin-top: var(--space-4); } -.mt-6 { margin-top: var(--space-6); } -.mb-0 { margin-bottom: 0; } -.mb-1 { margin-bottom: var(--space-1); } -.mb-2 { margin-bottom: var(--space-2); } -.mb-3 { margin-bottom: var(--space-3); } -.mb-4 { margin-bottom: var(--space-4); } -.mb-6 { margin-bottom: var(--space-6); } -.ml-0 { margin-left: 0; } -.ml-1 { margin-left: var(--space-1); } -.ml-2 { margin-left: var(--space-2); } -.mr-1 { margin-right: var(--space-1); } -.mr-2 { margin-right: var(--space-2); } -.mr-1\.5 { margin-right: 6px; } - -/* Padding */ -.p-0 { padding: 0; } -.p-1 { padding: var(--space-1); } -.p-2 { padding: var(--space-2); } -.p-3 { padding: var(--space-3); } -.p-4 { padding: var(--space-4); } -.p-5 { padding: var(--space-5); } -.p-6 { padding: var(--space-6); } -.px-1 { padding-left: var(--space-1); padding-right: var(--space-1); } -.px-2 { padding-left: var(--space-2); padding-right: var(--space-2); } -.px-3 { padding-left: var(--space-3); padding-right: var(--space-3); } -.px-4 { padding-left: var(--space-4); padding-right: var(--space-4); } -.px-6 { padding-left: var(--space-6); padding-right: var(--space-6); } -.py-0 { padding-top: 0; padding-bottom: 0; } -.py-1 { padding-top: var(--space-1); padding-bottom: var(--space-1); } -.py-2 { padding-top: var(--space-2); padding-bottom: var(--space-2); } -.py-3 { padding-top: var(--space-3); padding-bottom: var(--space-3); } -.py-4 { padding-top: var(--space-4); padding-bottom: var(--space-4); } -.pt-2 { padding-top: var(--space-2); } -.pt-3 { padding-top: var(--space-3); } -.pt-4 { padding-top: var(--space-4); } -.pb-2 { padding-bottom: var(--space-2); } -.pb-3 { padding-bottom: var(--space-3); } -.pb-4 { padding-bottom: var(--space-4); } -.pl-2 { padding-left: var(--space-2); } -.pr-2 { padding-right: var(--space-2); } - -/* Border */ -.border { border: 1px solid var(--surface-border); } -.border-0 { border: none; } -.border-t { border-top: 1px solid var(--surface-border); } -.border-b { border-bottom: 1px solid var(--surface-border); } -.border-l { border-left: 1px solid var(--surface-border); } -.border-r { border-right: 1px solid var(--surface-border); } -.border-l-3 { border-left-width: 3px; } -.border-destructive { border-color: var(--color-error); } -.border-border { border-color: var(--surface-border); } -.border-primary { border-color: var(--color-primary); } -.rounded-sm { border-radius: var(--radius-sm); } -.rounded { border-radius: var(--radius-md); } -.rounded-md { border-radius: var(--radius-md); } -.rounded-lg { border-radius: var(--radius-lg); } -.rounded-xl { border-radius: var(--radius-xl); } -.rounded-full { border-radius: var(--radius-pill); } - -/* Typography */ -.text-left { text-align: left; } -.text-center { text-align: center; } -.text-right { text-align: right; } -.whitespace-nowrap { white-space: nowrap; } -.whitespace-pre-wrap { white-space: pre-wrap; } -.break-words { word-break: break-word; } -.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.text-xs { font-size: 0.75rem; line-height: 1rem; } -.text-sm { font-size: 0.875rem; line-height: 1.25rem; } -.text-base { font-size: 1rem; line-height: 1.5rem; } -.text-lg { font-size: 1.125rem; line-height: 1.75rem; } -.text-xl { font-size: 1.25rem; line-height: 1.75rem; } -.text-2xl { font-size: 1.5rem; line-height: 2rem; } -.text-3xl { font-size: 1.875rem; line-height: 2.25rem; } -.font-normal { font-weight: 400; } -.font-medium { font-weight: 500; } -.font-semibold { font-weight: 600; } -.font-bold { font-weight: 700; } -.font-extrabold { font-weight: 800; } -.text-primary { color: var(--text-primary); } -.text-secondary { color: var(--text-secondary); } -.text-tertiary { color: var(--text-tertiary); } -.text-primary-color { color: var(--color-primary); } -.text-error { color: var(--color-error); } -.text-success { color: var(--color-success); } -.text-warning { color: var(--color-warning); } -.text-inverse { color: var(--text-inverse); } - -/* Background */ -.bg-surface { background: var(--surface-raised); } -.bg-overlay { background: var(--surface-overlay); } -.bg-base { background: var(--surface-base); } -.bg-primary { background: var(--color-primary); } -.bg-transparent { background: transparent; } - -/* Opacity / Misc */ -.opacity-0 { opacity: 0; } -.opacity-50 { opacity: 0.5; } -.opacity-60 { opacity: 0.6; } -.opacity-70 { opacity: 0.7; } -.opacity-80 { opacity: 0.8; } -.opacity-85 { opacity: 0.85; } -.object-contain { object-fit: contain; } -.object-cover { object-fit: cover; } -.cursor-pointer { cursor: pointer; } -.cursor-default { cursor: default; } -.cursor-not-allowed { cursor: not-allowed; } -.select-none { user-select: none; } -.pointer-events-none { pointer-events: none; } -.pointer-events-auto { pointer-events: auto; } - -/* Transitions */ -.transition-all { transition: all var(--transition-fast); } -.transition-transform { transition: transform var(--transition-fast); } -.transition-opacity { transition: opacity var(--transition-fast); } -.transition-colors { transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast); } -.transition-shadow { transition: box-shadow var(--transition-fast); } -.hover\:scale-105:hover { transform: scale(1.05); } -.hover\:opacity-80:hover { opacity: 0.8; } - -/* Animation */ -.animate-spin { animation: spin 1s linear infinite; } -@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } - -/* Responsive */ -@media (max-width: 768px) { - .grid-cols-2 { grid-template-columns: 1fr; } - .grid-cols-3 { grid-template-columns: 1fr; } -} - -/* ── Extra Utilities (component-used aliases) ────────── */ -.active { background: var(--gradient-primary); color: white; } -.is-active { background: var(--gradient-primary); color: white; } -.is-deleted { opacity: 0.6; } -.is-connecting { animation: pulse-connecting 1s ease-in-out infinite; } -.is-scroll { overflow-x: auto; flex-wrap: nowrap; } -.separated > * + * { margin-top: 0.625rem; padding-top: 0.625rem; border-top: 1px solid var(--surface-border); } -.tall { height: 112px; width: 64px; } -.typing { animation: pulse-dot 1.5s ease-in-out infinite; } -.mascot { font-variant-numeric: tabular-nums; } - -.ml-0\.5 { margin-left: 2px; } -.space-y-2 > * + * { margin-top: var(--space-2); } -.tracking-tight { letter-spacing: -0.025em; } -.text-foreground { color: var(--text-primary); } -.text-muted-foreground { color: var(--text-tertiary); } -.text-destructive { color: var(--color-error); } -.text-\[10px\] { font-size: 10px; } -.h-3\.5 { height: 14px; } -.w-3\.5 { width: 14px; } - -.bg-background { background: var(--surface-base); } -.bg-card { background: var(--surface-raised); } -.border-input { border-color: var(--surface-border); } - -.head-left { display: flex; align-items: center; gap: var(--space-3); } -.head-right { display: flex; align-items: center; gap: var(--space-4); margin-left: auto; } -.head-status { display: flex; align-items: center; gap: var(--space-1); font-size: 0.75rem; color: var(--text-secondary); } -.head-status-dot { width: 8px; height: 8px; border-radius: 50%; transition: all var(--transition-fast); } - -.card-bordered { border: 1px solid var(--surface-border); } -.card-elevated { box-shadow: var(--shadow-md); } - -.empty-state-icon { font-size: 3rem; margin-bottom: var(--space-4); opacity: 0.5; } - -.input-error { border-color: var(--color-error); } -.input-soft { background: var(--surface-container); border: 1px solid transparent; } - -.theme-toggle { width: 44px; height: 44px; border: 1px solid var(--surface-border); border-radius: var(--radius-md); background: var(--surface-overlay); color: var(--text-tertiary); cursor: pointer; font-size: 1.25rem; display: flex; align-items: center; justify-content: center; transition: all var(--transition-fast); } -.theme-toggle:hover { color: var(--color-primary); border-color: var(--color-primary); } - -.mascot-controls { display: flex; align-items: center; gap: var(--space-2); } -.mascot-inline { display: flex; align-items: center; gap: var(--space-1); } - -.placeholder-muted-foreground::placeholder { color: var(--text-tertiary); } - -.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } -@media (max-width: 768px) { .md\:grid-cols-2 { grid-template-columns: 1fr; } } - -.bg-destructive\/15 { background: rgba(239, 68, 68, 0.15); } -.border-border\/50 { border-color: rgba(255, 255, 255, 0.03); } -[data-theme="light"] .border-border\/50 { border-color: rgba(0, 0, 0, 0.03); } -/* ── App Shell & Layout ───────────────────────────────── */ - -.app-shell { - position: relative; - z-index: 1; - display: flex; - height: 100vh; - background: var(--surface-base); - color: var(--text-primary); -} - -/* ── Sidebar ─────────────────────────────────────────── */ -.app-sidebar { - width: var(--sidebar-width); - flex-shrink: 0; - height: 100vh; - display: flex; - flex-direction: column; - border-right: 1px solid var(--surface-border); - background: var(--surface-glass); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - position: sticky; - top: 0; - z-index: var(--z-sidebar); -} - -.sidebar-brand { - display: flex; - align-items: center; - gap: var(--space-3); - height: 60px; - padding: 0 var(--space-4); - flex-shrink: 0; - border-bottom: 1px solid var(--surface-border); -} - -.sidebar-brand-icon { - font-size: 1.25rem; - line-height: 1; -} - -.sidebar-brand-text { - display: flex; - flex-direction: column; -} - -.sidebar-brand-name { - font-weight: 800; - font-size: 1rem; - background: var(--gradient-brand); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - letter-spacing: -0.03em; - line-height: 1.2; -} - -.sidebar-brand-subtitle { - color: var(--text-tertiary); - font-size: 0.6875rem; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.08em; - line-height: 1.3; -} - -/* ── Navigation ──────────────────────────────────────── */ -.sidebar-nav { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - gap: var(--space-1); - padding: var(--space-3); - overflow-y: auto; -} - -.sidebar-nav-item { - display: flex; - align-items: center; - gap: var(--space-3); - height: 40px; - padding: 0 var(--space-3); - border: none; - border-radius: 10px; - background: transparent; - color: var(--text-secondary); - font-family: inherit; - font-size: 14px; - font-weight: 500; - line-height: 20px; - cursor: pointer; - transition: all var(--transition-fast); - text-align: left; - width: 100%; - position: relative; - -webkit-tap-highlight-color: transparent; -} - -.sidebar-nav-item:hover { - background: var(--surface-hover); - color: var(--color-primary); -} - -.sidebar-nav-item.is-active { - background: var(--surface-overlay); - color: var(--color-primary); - font-weight: 600; -} - -.sidebar-nav-item.is-active::after { - content: ''; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - width: 3px; - height: 24px; - border-radius: var(--radius-pill); - background: var(--gradient-primary); - box-shadow: 0 0 12px rgba(59, 130, 246, 0.4); - animation: indicator-grow 250ms cubic-bezier(0.16, 1, 0.3, 1) both; -} - -.sidebar-nav-icon { - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - flex-shrink: 0; - font-size: 1rem; -} - -/* ── Sidebar Footer ──────────────────────────────────── */ -.sidebar-footer { - flex-shrink: 0; - padding: var(--space-3); - border-top: 1px solid var(--surface-border); - display: flex; - align-items: center; - gap: var(--space-2); -} - -.sidebar-footer-status { - display: flex; - align-items: center; - gap: var(--space-1); - flex: 1; - min-width: 0; -} - -.status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - flex-shrink: 0; - transition: background var(--transition-fast); -} - -.status-dot.is-connecting { - animation: pulse-connecting 1s ease-in-out infinite; -} - -.status-text { - font-size: 0.75rem; - color: var(--text-tertiary); - white-space: nowrap; -} - -/* ── Content Area ────────────────────────────────────── */ -.app-content { - flex: 1; - min-width: 0; - overflow-y: auto; - padding: var(--space-6); - max-width: 1400px; - animation: content-enter 500ms cubic-bezier(0.16, 1, 0.3, 1) both; -} - -.app-content > * { - animation: card-in 500ms cubic-bezier(0.16, 1, 0.3, 1) both; -} -.app-content > :nth-child(1) { animation-delay: 30ms; } -.app-content > :nth-child(2) { animation-delay: 60ms; } -.app-content > :nth-child(3) { animation-delay: 90ms; } - -/* ── Theme Toggle in Sidebar ──────────────────────────── */ -.sidebar-theme-btn { - width: 32px; - height: 32px; - border: 1px solid var(--surface-border); - border-radius: var(--radius-sm); - background: var(--surface-overlay); - color: var(--text-tertiary); - cursor: pointer; - font-size: 0.875rem; - display: flex; - align-items: center; - justify-content: center; - transition: all var(--transition-fast); - flex-shrink: 0; -} -.sidebar-theme-btn:hover { - color: var(--color-primary); - border-color: var(--color-primary); -} - -/* ── Mobile Tab Bar ──────────────────────────────────── */ -.mobile-tab-bar { - display: none; -} - -@media (max-width: 768px) { - .app-main { flex-direction: column; } - .app-sidebar { display: none; } - .app-content { padding: var(--space-4); } - - .mobile-tab-bar { - display: flex; - position: fixed; - bottom: 0; - left: 0; - right: 0; - background: var(--surface-base); - border-top: 1px solid var(--surface-border); - z-index: var(--z-overlay); - } - - .mobile-tab-item { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - gap: 0.25rem; - padding: 0.5rem; - background: none; - border: none; - font-family: inherit; - font-size: 0.625rem; - cursor: pointer; - color: var(--text-tertiary); - transition: color var(--transition-fast); - -webkit-tap-highlight-color: transparent; - } - - .mobile-tab-item.is-active { - color: var(--color-primary); - } - - .mobile-tab-item-icon { - font-size: 1.25rem; - line-height: 1; - } -} - -/* ── Animations ──────────────────────────────────────── */ -@keyframes content-enter { - from { opacity: 0; transform: translateY(16px); } - to { opacity: 1; transform: translateY(0); } -} -@keyframes card-in { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } -} -@keyframes indicator-grow { - from { height: 0; opacity: 0; } - to { height: 24px; opacity: 1; } -} -@keyframes pulse-connecting { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.4; transform: scale(0.7); } -} -/* ── UI Primitives ────────────────────────────────────── */ - -/* ── Button ──────────────────────────────────────────── */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--space-2); - padding: 12px 24px; - height: 44px; - border: 1px solid transparent; - border-radius: var(--radius-pill); - font-family: inherit; - font-size: 14px; - font-weight: 600; - line-height: 20px; - letter-spacing: 0.02em; - cursor: pointer; - transition: all var(--transition-fast); - white-space: nowrap; - user-select: none; - -webkit-tap-highlight-color: transparent; - text-decoration: none; -} -.btn:active:not(:disabled) { transform: scale(0.97); } -.btn:disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; } - -.btn-primary { - background: var(--gradient-primary); - color: white; - border: none; -} -.btn-primary:hover:not(:disabled) { - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.25); - transform: translateY(-1px); -} - -.btn-secondary { - background: transparent; - color: var(--color-primary); - border: 1.5px solid var(--color-primary); -} -.btn-secondary:hover:not(:disabled) { - background: var(--color-primary-muted); - transform: translateY(-1px); -} - -.btn-destructive { - background: var(--color-error); - color: white; - border: none; -} -.btn-destructive:hover:not(:disabled) { - filter: brightness(1.1); - transform: translateY(-1px); -} - -.btn-outline { - background: var(--surface-glass); - border: 1px solid var(--surface-border); - color: var(--text-secondary); -} -.btn-outline:hover:not(:disabled) { - background: var(--surface-raised); - border-color: var(--color-primary); - color: var(--color-primary); -} - -.btn-ghost { - background: transparent; - color: var(--text-secondary); - border: none; - height: auto; - padding: 0.5rem 0.75rem; - border-radius: var(--radius-md); -} -.btn-ghost:hover:not(:disabled) { - background: var(--surface-hover); - color: var(--text-primary); -} - -.btn-link { - background: transparent; - color: var(--color-primary); - border: none; - height: auto; - padding: 0; - font-weight: 500; -} -.btn-link:hover:not(:disabled) { text-decoration: underline; } - -/* Sizes */ -.btn-sm { padding: 8px 16px; height: 36px; font-size: 12px; border-radius: var(--radius-md); } -.btn-lg { padding: 14px 28px; height: 48px; font-size: 16px; } -.btn-icon { width: 44px; height: 44px; padding: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 1.25rem; border-radius: var(--radius-pill); } -.btn-icon-sm { width: 36px; height: 36px; padding: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 1rem; border-radius: var(--radius-md); } - -/* ── Card ────────────────────────────────────────────── */ -.card { - background: var(--surface-raised); - border: 1px solid var(--surface-border); - border-radius: var(--radius-md); - overflow: hidden; - box-shadow: var(--shadow-sm); - transition: all var(--transition-normal); -} -.card:hover { - box-shadow: var(--shadow-md); -} -.card-interactive:hover { - border-color: var(--color-primary); - box-shadow: var(--shadow-md); - transform: translateY(-1px); -} - -.card-header { - padding: var(--space-4) var(--space-6); - border-bottom: 1px solid var(--surface-border); -} -.card-title { - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); -} -.card-description { - font-size: 0.875rem; - color: var(--text-secondary); - margin-top: var(--space-1); -} -.card-content { padding: var(--space-6); } -.card-footer { - padding: var(--space-4) var(--space-6); - border-top: 1px solid var(--surface-border); -} - -/* ── Badge ───────────────────────────────────────────── */ -.badge { - display: inline-flex; - align-items: center; - padding: 4px 8px; - border-radius: var(--radius-sm); - font-size: 12px; - font-weight: 500; - line-height: 16px; - letter-spacing: 0.03em; - background: var(--surface-container); - color: var(--text-secondary); - border: none; - transition: all var(--transition-fast); -} -.badge-primary { background: var(--color-primary-muted); color: var(--color-primary); } -.badge-success { background: rgba(16, 185, 129, 0.12); color: var(--color-success); } -.badge-warning { background: rgba(245, 158, 11, 0.12); color: var(--color-warning); } -.badge-destructive { background: rgba(239, 68, 68, 0.12); color: var(--color-error); } -.badge-outline { background: transparent; border: 1px solid var(--surface-border); color: var(--text-tertiary); } -.badge-info { background: var(--color-primary-muted); color: var(--color-primary); } - -/* ── Status Badge ────────────────────────────────────── */ -.status-badge { - display: inline-flex; - align-items: center; - gap: var(--space-1); - padding: 3px 10px; - border-radius: var(--radius-pill); - font-size: 11px; - font-weight: 600; - letter-spacing: 0.02em; - transition: all var(--transition-fast); -} -.status-badge::before { - content: ''; - width: 6px; - height: 6px; - border-radius: 50%; - flex-shrink: 0; -} -.status-badge-flagged { background: rgba(239, 68, 68, 0.12); color: var(--color-ai-flagged); } -.status-badge-flagged::before { background: var(--color-ai-flagged); box-shadow: 0 0 8px rgba(239, 68, 68, 0.6); } -.status-badge-clean { background: rgba(16, 185, 129, 0.12); color: var(--color-ai-clean); } -.status-badge-clean::before { background: var(--color-ai-clean); box-shadow: 0 0 8px rgba(16, 185, 129, 0.4); } -.status-badge-warn { background: rgba(245, 158, 11, 0.12); color: var(--color-ai-warn); } -.status-badge-warn::before { background: var(--color-ai-warn); box-shadow: 0 0 8px rgba(245, 158, 11, 0.4); } -.status-badge-pending { background: rgba(92, 92, 120, 0.12); color: var(--color-ai-pending); } -.status-badge-pending::before { background: var(--color-ai-pending); } -.status-badge-processing { background: var(--color-primary-muted); color: var(--color-ai-processing); } -.status-badge-processing::before { background: var(--color-ai-processing); animation: pulse-dot 1.5s ease-in-out infinite; } -.status-badge-error { background: rgba(220, 38, 38, 0.12); color: var(--color-ai-error); } -.status-badge-error::before { background: var(--color-ai-error); box-shadow: 0 0 8px rgba(220, 38, 38, 0.4); } - -@keyframes pulse-dot { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.3; } -} - -/* ── Input ───────────────────────────────────────────── */ -.input { - display: block; - width: 100%; - padding: var(--space-3); - background: var(--surface-container); - border: 1.5px solid transparent; - border-radius: var(--radius-sm); - color: var(--text-primary); - font-family: inherit; - font-size: 16px; - font-weight: 400; - line-height: 24px; - transition: all var(--transition-fast); - outline: none; -} -.input::placeholder { color: var(--text-tertiary); } -.input:focus { - border-color: var(--color-primary); - box-shadow: 0 0 0 2px var(--color-primary-muted), 0 0 16px rgba(59, 130, 246, 0.08); - background: var(--surface-raised); -} -.input[aria-invalid="true"] { - border-color: var(--color-error); - box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.12); -} - -/* ── Select ──────────────────────────────────────────── */ -.select { - display: block; - width: 100%; - padding: var(--space-3) 2rem var(--space-3) var(--space-3); - background: var(--surface-container); - border: 1.5px solid transparent; - border-radius: var(--radius-sm); - color: var(--text-primary); - font-family: inherit; - font-size: 0.875rem; - cursor: pointer; - transition: all var(--transition-fast); - outline: none; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%235c5c78' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 0.75rem center; -} -.select:focus { - border-color: var(--color-primary); - box-shadow: 0 0 0 2px var(--color-primary-muted); - background: var(--surface-raised); -} - -/* ── Skeleton ────────────────────────────────────────── */ -.skeleton { - background: linear-gradient(110deg, var(--surface-container) 30%, rgba(59, 130, 246, 0.04) 50%, var(--surface-container) 70%); - background-size: 200% 100%; - animation: shimmer 1.8s ease-in-out infinite; - border-radius: var(--radius-sm); -} -.skeleton-circular { border-radius: 50%; } -@keyframes shimmer { - 0% { background-position: 200% 0; } - 100% { background-position: -200% 0; } -} - -/* ── Empty State ─────────────────────────────────────── */ -.empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: var(--space-16) var(--space-4); - text-align: center; -} -.empty-state-title { - font-size: 0.9375rem; - font-weight: 600; - color: var(--text-secondary); - margin-bottom: var(--space-2); -} -.empty-state-description { - font-size: 0.8125rem; - color: var(--text-tertiary); - max-width: 280px; - line-height: 1.5; -} - -/* ── Tabs ────────────────────────────────────────────── */ -.tabs { display: flex; flex-direction: column; } -.tab-list { - display: flex; - gap: var(--space-2); - border-bottom: 2px solid var(--surface-border); -} -.tab-trigger { - padding: 8px 16px; - background: transparent; - border: none; - border-bottom: 2px solid transparent; - margin-bottom: -2px; - color: var(--text-secondary); - font-family: inherit; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all var(--transition-fast); -} -.tab-trigger:hover { color: var(--text-primary); } -.tab-trigger.active, -.tab-trigger[aria-selected="true"] { - color: var(--color-primary); - font-weight: 600; - border-bottom-color: var(--color-primary); -} -.tab-content { - padding-top: var(--space-4); - animation: fade-in-up var(--transition-normal) ease-out; -} - -/* ── Modal ───────────────────────────────────────────── */ -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.55); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - display: flex; - align-items: center; - justify-content: center; - z-index: var(--z-modal); - animation: fade-in var(--transition-fast) ease-out; -} -.modal-content { - background: var(--surface-raised); - border-radius: var(--radius-lg); - border: 1px solid var(--surface-border); - box-shadow: var(--shadow-lg); - max-width: 90vw; - max-height: 85vh; - overflow: auto; - animation: scale-in var(--transition-bounce); -} -.modal-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-4) var(--space-6); - border-bottom: 1px solid var(--surface-border); -} -.modal-body { padding: var(--space-6); } -.modal-footer { - display: flex; - justify-content: flex-end; - gap: var(--space-2); - padding: var(--space-4) var(--space-6); - border-top: 1px solid var(--surface-border); -} - -/* ── Toast ───────────────────────────────────────────── */ -.toast-container { - position: fixed; - bottom: var(--space-4); - right: var(--space-4); - z-index: var(--z-toast); - display: flex; - flex-direction: column; - gap: var(--space-2); - pointer-events: none; -} -.toast { - display: flex; - align-items: center; - gap: var(--space-3); - padding: 0.75rem 1rem; - background: var(--surface-raised); - border: 1px solid var(--surface-border); - border-radius: var(--radius-md); - box-shadow: var(--shadow-lg); - min-width: 300px; - max-width: 420px; - pointer-events: auto; - animation: slide-in-right var(--transition-bounce); -} -.toast-success { border-left: 3px solid var(--color-success); } -.toast-error { border-left: 3px solid var(--color-error); } -.toast-warning { border-left: 3px solid var(--color-warning); } -.toast-info { border-left: 3px solid var(--color-primary); } -.toast-close { - margin-left: auto; - background: none; - border: none; - color: var(--text-tertiary); - cursor: pointer; - padding: 0.25rem; - transition: color var(--transition-fast); -} -.toast-close:hover { color: var(--text-primary); } - -/* ── Animations ──────────────────────────────────────── */ -@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } -@keyframes fade-in-up { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } -@keyframes scale-in { from { opacity: 0; transform: scale(0.92); } to { opacity: 1; transform: scale(1); } } -@keyframes slide-in-right { from { opacity: 0; transform: translateX(120%); } to { opacity: 1; transform: translateX(0); } } - -.animate-fade-in { animation: fade-in var(--transition-normal) ease-out; } -.animate-fade-in-up { animation: fade-in-up var(--transition-normal) ease-out; } -.animate-scale-in { animation: scale-in var(--transition-normal) ease-out; } -.animate-slide-in-right { animation: slide-in-right var(--transition-normal) ease-out; } - -/* Reduced motion */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} -/* ── Messages Panel ───────────────────────────────────── */ - -.messages-panel { display: flex; flex-direction: column; gap: var(--space-5); } - -/* ── Filter Bar ──────────────────────────────────────── */ -.filter-bar { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--space-3); -} - -.filter-bar-search { - position: relative; - flex: 1; - min-width: 200px; -} - -.filter-bar-search-icon { - position: absolute; - left: 0.75rem; - top: 50%; - transform: translateY(-50%); - color: var(--text-tertiary); - pointer-events: none; -} - -.filter-bar-input { - width: 100%; - padding: 0.5rem 0.75rem 0.5rem 2.25rem; - height: 36px; - background: var(--surface-container); - border: 1px solid transparent; - border-radius: var(--radius-pill); - color: var(--text-primary); - font-family: inherit; - font-size: 0.875rem; - transition: all var(--transition-fast); - outline: none; -} -.filter-bar-input::placeholder { color: var(--text-tertiary); } -.filter-bar-input:focus { - border-color: var(--color-primary); - box-shadow: 0 0 0 2px var(--color-primary-muted); - background: var(--surface-raised); -} - -.filter-bar-chips { - display: flex; - align-items: center; - gap: 0.375rem; - flex-wrap: wrap; -} - -.filter-chip { - padding: 0.25rem 0.75rem; - border-radius: var(--radius-pill); - font-size: 0.6875rem; - font-weight: 500; - border: 1px solid transparent; - background: var(--surface-hover); - color: var(--text-tertiary); - cursor: pointer; - transition: all var(--transition-fast); - font-family: inherit; -} -.filter-chip:hover { - color: var(--text-secondary); - background: var(--surface-container); -} -.filter-chip.is-active { - background: var(--gradient-primary); - color: white; -} - -.filter-bar-live-count { - font-size: 0.875rem; - color: var(--text-tertiary); - white-space: nowrap; -} - -/* ── Stats Bar ───────────────────────────────────────── */ -.stats-bar { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--space-1); -} - -/* ── Message Card ────────────────────────────────────── */ -.msg-card { - background: var(--surface-raised); - border: 1px solid var(--surface-border); - border-radius: var(--radius-md); - overflow: hidden; - transition: all var(--transition-fast); - box-shadow: var(--shadow-sm); -} -.msg-card:hover { - border-color: rgba(59, 130, 246, 0.20); - box-shadow: var(--shadow-md); - transform: translateY(-1px); -} -.msg-card.is-deleted { - opacity: 0.6; - border-color: var(--color-error); -} - -.msg-card-inner { - display: flex; - gap: var(--space-3); - padding: var(--space-4); -} - -.msg-card-avatar { - width: 40px; - height: 40px; - flex-shrink: 0; - border-radius: 50%; - object-fit: cover; -} - -.msg-card-body { min-width: 0; flex: 1; } - -.msg-card-meta { - display: flex; - align-items: baseline; - gap: var(--space-2); - margin-bottom: var(--space-2); - font-size: 0.75rem; - color: var(--text-secondary); - font-family: 'JetBrains Mono', monospace; -} - -.msg-card-username { - font-size: 0.8125rem; - font-weight: 600; - color: var(--text-primary); -} - -.msg-card-channel { - display: inline-flex; - align-items: center; - gap: var(--space-1); - font-size: 0.6875rem; - color: var(--text-tertiary); - background: var(--surface-container); - padding: 1px 8px; - border-radius: var(--radius-pill); -} -.msg-card-channel::before { content: '#'; opacity: 0.5; } - -.msg-card-time { - font-size: 0.6875rem; - color: var(--text-tertiary); - font-variant-numeric: tabular-nums; - margin-left: auto; -} - -/* ── AI Badge ────────────────────────────────────────── */ -.msg-card-ai-badge { - display: inline-flex; - align-items: center; - gap: 0.25rem; - padding: 2px 8px; - border-radius: 6px; - font-size: 0.625rem; - font-weight: 600; - margin-left: var(--space-1); -} -.msg-card-ai-badge.clean { - background: rgba(16, 185, 129, 0.10); - color: var(--color-ai-clean); -} -.msg-card-ai-badge.warn { - background: rgba(245, 158, 11, 0.10); - color: var(--color-ai-warn); -} -.msg-card-ai-badge.flagged { - background: rgba(239, 68, 68, 0.10); - color: var(--color-ai-flagged); -} -.msg-card-ai-badge.processing { - background: var(--color-primary-muted); - color: var(--color-ai-processing); -} -.msg-card-ai-badge.pending { - background: rgba(92, 92, 120, 0.10); - color: var(--color-ai-pending); -} - -/* ── Reply indicator (Discord-style reference block) ──── */ -.msg-row-reply { - display: flex; - align-items: stretch; - margin: 0.375rem 0 0.625rem; - border-radius: 6px; - overflow: hidden; - cursor: default; - transition: background 150ms ease; -} -.msg-row-reply:hover { - background: rgba(59, 130, 246, 0.04); -} -.msg-row-reply-line { - width: 3px; - flex-shrink: 0; - border-radius: 2px; - background: linear-gradient(180deg, #3b82f6, #8b5cf6); - opacity: 0.6; -} -.msg-row-reply-main { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.375rem 0.625rem; - min-width: 0; - flex: 1; - font-size: 0.75rem; - line-height: 1.4; -} -.msg-row-reply-avatar { - width: 20px; - height: 20px; - border-radius: 50%; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.625rem; - font-weight: 700; - color: white; - background: linear-gradient(135deg, #3b82f6, #6366f1); - text-transform: uppercase; - box-shadow: 0 1px 3px rgba(59, 130, 246, 0.25); -} -.msg-row-reply-label { - flex-shrink: 0; - color: var(--text-tertiary); - font-weight: 450; - opacity: 0.8; -} -.msg-row-reply-user { - flex-shrink: 0; - font-weight: 700; - color: #3b82f6; - letter-spacing: -0.01em; -} -.msg-row-reply-snippet { - color: var(--text-tertiary); - font-style: italic; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; - min-width: 0; - opacity: 0.9; - display: inline-flex; - align-items: center; - gap: 0; -} -.msg-row-reply-snippet::before { - content: '"'; - opacity: 0.5; - margin-right: 1px; -} -.msg-row-reply-snippet::after { - content: '"'; - opacity: 0.5; - margin-left: 1px; -} - -/* ── Message Content ─────────────────────────────────── */ -.msg-card-content { - font-size: 0.875rem; - line-height: 1.5; - white-space: pre-wrap; - word-break: break-word; - color: var(--text-primary); -} -.msg-card-content.is-deleted { - opacity: 0.6; - color: var(--text-secondary); -} - -.msg-card-messages.separated > * + * { - margin-top: 0.625rem; - padding-top: 0.625rem; - border-top: 1px solid var(--surface-border); -} - -/* ── Actions Bar ─────────────────────────────────────── */ -.msg-card-actions { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-3) var(--space-4); - border-top: 1px solid var(--surface-border); - background: var(--surface-base); -} - -.msg-card-actions .btn-icon-sm { - color: var(--text-tertiary); -} -.msg-card-actions .btn-icon-sm:hover { - color: var(--color-primary); -} - -/* ── Embed ───────────────────────────────────────────── */ -.msg-embed { - margin-top: var(--space-2); - padding: var(--space-3); - border-left: 3px solid var(--color-primary); - border-radius: var(--radius-sm); - background: var(--surface-overlay); -} -.msg-embed-title { - font-weight: 600; - font-size: 0.875rem; - color: var(--text-primary); - margin-bottom: var(--space-1); -} -.msg-embed-description { - font-size: 0.8125rem; - color: var(--text-secondary); - line-height: 1.5; -} -.msg-embed-fields { - display: flex; - flex-direction: column; - gap: var(--space-1); - margin-top: var(--space-2); -} -.msg-embed-field { - display: flex; - flex-direction: column; -} -.msg-embed-field-name { - font-size: 0.75rem; - font-weight: 600; - color: var(--text-secondary); - margin-bottom: 0.125rem; -} -.msg-embed-field-value { - font-size: 0.8125rem; - color: var(--text-primary); -} -.msg-embed-footer { - margin-top: var(--space-2); - font-size: 0.6875rem; - color: var(--text-tertiary); -} - -/* ── Image Grid ──────────────────────────────────────── */ -.image-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); - gap: var(--space-2); -} -.image-grid-item { - aspect-ratio: 1; - border-radius: var(--radius-md); - overflow: hidden; - cursor: pointer; - transition: all var(--transition-fast); - border: 1px solid var(--surface-border); -} -.image-grid-item:hover { - opacity: 0.85; - transform: scale(1.02); -} -.image-grid-item img { - width: 100%; - height: 100%; - object-fit: cover; -} - -/* ── Message Media ───────────────────────────────────── */ -.msg-media-row { - display: flex; - flex-wrap: wrap; - gap: var(--space-2); - margin-top: var(--space-2); -} -.msg-media-row.is-scroll { - overflow-x: auto; - flex-wrap: nowrap; -} -.msg-thumb { - width: 64px; - height: 64px; - object-fit: cover; - border-radius: var(--radius-sm); - border: 1px solid var(--surface-border); - transition: all var(--transition-fast); -} -.msg-thumb:hover { transform: scale(1.08); } -.msg-thumb-link { - display: block; - flex-shrink: 0; - overflow: hidden; - border-radius: var(--radius-sm); - border: 1px solid var(--surface-border); - transition: all var(--transition-fast); -} -.msg-thumb-link:hover { border-color: var(--color-primary); } -.msg-sticker { - width: 48px; - height: 48px; - object-fit: contain; -} -.msg-video { - height: 112px; - width: 192px; - flex-shrink: 0; - border-radius: var(--radius-sm); - border: 1px solid var(--surface-border); - object-fit: cover; - background: #000; -} -.msg-media-overflow { - display: flex; - width: 64px; - height: 64px; - align-items: center; - justify-content: center; - border-radius: var(--radius-sm); - border: 1px solid var(--surface-border); - font-size: 0.75rem; - color: var(--text-secondary); - background: var(--surface-container); -} -.msg-media-overflow.tall { height: 112px; width: 64px; } - -/* ── Message Categories ──────────────────────────────── */ -.msg-cats { - display: flex; - flex-wrap: wrap; - gap: var(--space-1); - margin-top: var(--space-2); -} - -/* ── AI Analysis ─────────────────────────────────────── */ -.msg-analysis { - margin-top: var(--space-2); - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-sm); - font-size: 0.75rem; - border-left: 3px solid; -} -.msg-analysis.flagged { - background: rgba(239, 68, 68, 0.06); - border-color: var(--color-ai-flagged); -} -.msg-analysis.clean { - background: rgba(16, 185, 129, 0.06); - border-color: var(--color-ai-clean); -} -.msg-analysis-row { - display: flex; - align-items: flex-start; - gap: var(--space-2); -} -.msg-analysis-body { min-width: 0; flex: 1; } -.msg-analysis-summary { - display: block; - font-weight: 500; - margin-bottom: var(--space-1); -} -.msg-analysis-text { - font-size: 0.75rem; - line-height: 1.5; - white-space: pre-wrap; -} - -/* ── Message Error ───────────────────────────────────── */ -.msg-error { - margin-top: var(--space-2); - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-sm); - font-size: 0.75rem; - color: var(--color-warning); - background: rgba(245, 158, 11, 0.06); -} - -/* ── Message Skeleton ────────────────────────────────── */ -.msg-skel { - display: flex; - gap: var(--space-3); - padding: var(--space-4); -} -.msg-skel-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - background: linear-gradient(90deg, var(--surface-container) 25%, rgba(59, 130, 246, 0.04) 50%, var(--surface-container) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s ease-in-out infinite; - flex-shrink: 0; -} -.msg-skel-lines { - min-width: 0; - flex: 1; - display: flex; - flex-direction: column; - gap: var(--space-3); -} -.msg-skel-line { - height: 20px; - background: linear-gradient(90deg, var(--surface-container) 25%, rgba(59, 130, 246, 0.04) 50%, var(--surface-container) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s ease-in-out infinite; - border-radius: var(--radius-sm); -} -.msg-skel-badges { - display: flex; - gap: var(--space-2); -} -.msg-skel-badge { - height: 24px; - border-radius: var(--radius-pill); - background: linear-gradient(90deg, var(--surface-container) 25%, rgba(59, 130, 246, 0.04) 50%, var(--surface-container) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s ease-in-out infinite; -} - -/* ── Feed ────────────────────────────────────────────── */ -.feed-wrap { - display: flex; - flex-direction: column; - gap: var(--space-4); -} -.feed-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: var(--space-16) var(--space-4); - text-align: center; -} -.feed-empty-title { - font-size: 0.9375rem; - font-weight: 600; - color: var(--text-secondary); - margin-bottom: var(--space-2); -} -.feed-empty-desc { - font-size: 0.8125rem; - color: var(--text-tertiary); - max-width: 280px; - line-height: 1.5; -} -.feed-sentinel { height: var(--space-4); } -.feed-loader { margin-top: var(--space-4); text-align: center; } - -/* ── Misc ────────────────────────────────────────────── */ -.search-count { - font-size: 0.875rem; - color: var(--text-secondary); -} -.icon-spin { animation: spin 1s linear infinite; } -.custom-emoji { - display: inline-block; - height: 20px; - width: 20px; - vertical-align: middle; - object-fit: contain; -} - -/* ── Legacy aliases (components still use these) ────── */ -.panel-card { background: var(--surface-raised); border: 1px solid var(--surface-border); border-radius: var(--radius-md); overflow: hidden; box-shadow: var(--shadow-sm); transition: all var(--transition-normal); } -.panel-card-head { padding: var(--space-5) var(--space-6); border-bottom: 1px solid var(--surface-border); } -.panel-card-title { font-size: 1rem; font-weight: 600; color: var(--text-primary); } -.panel-card-desc { font-size: 0.875rem; color: var(--text-secondary); margin-top: var(--space-1); } - -.search-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); } -.search-wrap { position: relative; flex: 1; min-width: 200px; } -.search-icon { position: absolute; left: 0.75rem; top: 50%; transform: translateY(-50%); color: var(--text-tertiary); pointer-events: none; } -.search-input { width: 100%; padding: 0.5rem 0.75rem 0.5rem 2.25rem; height: 36px; background: var(--surface-container); border: 1px solid transparent; border-radius: var(--radius-pill); color: var(--text-primary); font-family: inherit; font-size: 0.875rem; transition: all var(--transition-fast); outline: none; } -.search-input::placeholder { color: var(--text-tertiary); } -.search-input:focus { border-color: var(--color-primary); box-shadow: 0 0 0 2px var(--color-primary-muted); background: var(--surface-raised); } - -.filter-group { display: flex; align-items: center; gap: 0.375rem; margin-left: auto; } -.filter-chip-v2 { padding: 0.25rem 0.75rem; border-radius: var(--radius-pill); font-size: 0.6875rem; font-weight: 500; border: 1px solid transparent; background: var(--surface-hover); color: var(--text-tertiary); cursor: pointer; transition: all var(--transition-fast); font-family: inherit; } -.filter-chip-v2:hover { color: var(--text-secondary); background: var(--surface-container); } -.filter-chip-v2.is-active { background: var(--gradient-primary); color: white; } - -.msg-row { padding: var(--space-3) 0; border-bottom: 1px solid var(--surface-border); transition: all var(--transition-fast); } -.msg-row:hover { background: var(--surface-hover); margin: 0 calc(-1 * var(--space-4)); padding-left: var(--space-4); padding-right: var(--space-4); border-radius: var(--radius-sm); } -.msg-row:last-child { border-bottom: none; } -.msg-row-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.5rem; } -.msg-row-time { font-size: 0.6875rem; color: var(--text-tertiary); font-variant-numeric: tabular-nums; min-width: 48px; } -.msg-row-badge { display: inline-flex; align-items: center; gap: 0.125rem; font-size: 0.75rem; } -.msg-row-badge.edited { color: var(--text-secondary); } -.msg-row-badge.deleted { color: var(--color-error); } -.msg-row-status { margin-left: auto; display: flex; align-items: center; gap: var(--space-1); } -.msg-row-body { font-size: 0.875rem; line-height: 1.5rem; white-space: pre-wrap; word-break: break-word; } -.msg-row-body.is-deleted { opacity: 0.6; color: var(--text-secondary); } - -.msg-card-head { display: flex; align-items: baseline; gap: var(--space-2); margin-bottom: var(--space-2); } -.msg-card-name { font-size: 0.875rem; font-weight: 600; color: var(--text-primary); } -.msg-card-messages > * + * { margin-top: 0.625rem; padding-top: 0.625rem; border-top: 1px solid var(--surface-border); } -.msg-card-messages.separated > * + * { margin-top: 0.625rem; padding-top: 0.625rem; border-top: 1px solid var(--surface-border); } - -.msg-actions { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-2); } -.msg-retry-hint { font-size: 0.75rem; color: var(--text-tertiary); opacity: 0.7; } - -.msg-sticker-placeholder { display: flex; width: 48px; height: 48px; align-items: center; justify-content: center; border-radius: var(--radius-sm); border: 1px solid var(--surface-border); } - -.msg-analysis-icon { margin-top: 0.125rem; flex-shrink: 0; } - -.img-empty { display: flex; align-items: center; justify-content: center; height: 128px; color: var(--text-secondary); font-style: italic; } -/* ── Dashboard Panel ──────────────────────────────────── */ - -.dashboard-panel { - display: flex; - flex-direction: column; - gap: var(--space-6); -} - -.dashboard-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--space-4); -} - -/* ── Stats Overview (4-column grid) ──────────────────── */ -.dashboard-stats-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: var(--space-4); -} - -.dashboard-metric-card { - padding: var(--space-5); - overflow: hidden; - height: 140px; - display: flex; - flex-direction: column; - justify-content: center; -} -.dashboard-metric-card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-md); - border-color: rgba(59, 130, 246, 0.15); -} - -.dashboard-metric-content { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-3); -} - -.dashboard-metric-label { - color: var(--text-secondary); - font-size: 0.75rem; - font-weight: 500; - letter-spacing: 0.02em; - text-transform: uppercase; -} - -.dashboard-metric-value { - margin-top: var(--space-1); - font-size: 1.75rem; - font-weight: 800; - letter-spacing: -0.03em; - color: var(--text-primary); - font-family: 'JetBrains Mono', monospace; -} - -.dashboard-metric-trend { - font-size: 0.6875rem; - font-weight: 500; - margin-top: var(--space-1); -} -.dashboard-metric-trend.positive { color: var(--color-success); } -.dashboard-metric-trend.negative { color: var(--color-error); } - -.dashboard-metric-icon { - display: flex; - align-items: center; - justify-content: center; - width: 2.75rem; - height: 2.75rem; - border-radius: var(--radius-lg); - font-size: 1.25rem; -} - -/* ── Summary Lists ───────────────────────────────────── */ -.dashboard-wide-card { grid-column: span 2; } - -.dashboard-summary-list { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.dashboard-summary-row { - display: flex; - align-items: flex-start; - gap: var(--space-3); - padding: var(--space-3); - border-bottom: 1px solid var(--surface-border); - transition: all var(--transition-fast); -} -.dashboard-summary-row:hover { - background: var(--surface-hover); -} - -.dashboard-summary-avatar { - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - width: 2.5rem; - height: 2.5rem; - border-radius: var(--radius-full); - background: var(--surface-container); - color: var(--text-secondary); - font-weight: 700; - overflow: hidden; -} -.dashboard-summary-avatar-img { - width: 100%; - height: 100%; - object-fit: cover; -} -.dashboard-channel-avatar { - color: var(--color-primary); - background: var(--color-primary-muted); -} - -.dashboard-summary-main { min-width: 0; flex: 1; } -.dashboard-summary-title { - color: var(--text-primary); - font-size: 0.875rem; - font-weight: 600; -} -.dashboard-summary-text { - margin-top: var(--space-1); - color: var(--text-secondary); - font-size: 0.8125rem; - line-height: 1.45; -} -.dashboard-summary-meta { - display: flex; - flex-wrap: wrap; - gap: var(--space-2); - margin-top: var(--space-2); - color: var(--text-tertiary); - font-size: 0.75rem; -} - -.dashboard-list-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--space-3); - padding: var(--space-10) var(--space-4); - text-align: center; -} - -/* ── Skeleton cells ──────────────────────────────────── */ -.dashboard-skel-row { - display: flex; - align-items: center; - gap: var(--space-3); - padding: var(--space-3); -} -.dashboard-skel-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - flex-shrink: 0; - background: linear-gradient(90deg, var(--surface-container) 25%, rgba(59, 130, 246, 0.04) 50%, var(--surface-container) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s ease-in-out infinite; -} -.dashboard-skel-lines { - flex: 1; - display: flex; - flex-direction: column; - gap: var(--space-2); -} -.dashboard-skel-line { - height: 16px; - border-radius: var(--radius-sm); - background: linear-gradient(90deg, var(--surface-container) 25%, rgba(59, 130, 246, 0.04) 50%, var(--surface-container) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s ease-in-out infinite; -} - -/* ── Responsive ──────────────────────────────────────── */ -@media (max-width: 1024px) { - .dashboard-stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } -} -@media (max-width: 768px) { - .dashboard-stats-grid { grid-template-columns: 1fr; } - .dashboard-wide-card { grid-column: span 1; } -} - -/* ── Legacy aliases ──────────────────────────────────── */ -.dashboard-stats { display: flex; flex-wrap: wrap; gap: var(--space-4); } -.dashboard-top-channels, .dashboard-summary-list { display: flex; flex-direction: column; gap: var(--space-2); } -.dashboard-top-channel-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); background: var(--surface-container); color: var(--text-secondary); font-size: 0.875rem; transition: all var(--transition-fast); } -.dashboard-top-channel-row:hover { background: var(--surface-overlay); transform: translateX(2px); } -.dashboard-moderation-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-3); } -.dashboard-queue-value { font-size: 1.75rem; font-weight: 800; color: var(--text-primary); letter-spacing: -0.03em; } -.dashboard-queue-label { margin-top: var(--space-1); color: var(--text-secondary); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; } -.dashboard-list-card { overflow: hidden; } -.dashboard-list-toolbar { margin-bottom: var(--space-4); } - -.badge-secondary { background: var(--color-primary-muted); color: var(--color-primary); } -/* ── Live Panel (Bento Grid) ──────────────────────────── */ - -.live-body { - display: flex; - flex-direction: column; - gap: var(--space-6); -} - -.live-head { - display: flex; - align-items: center; - justify-content: space-between; -} - -.live-title { - font-size: 1.5rem; - font-weight: 700; - letter-spacing: -0.025em; - background: var(--gradient-brand); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} -.live-desc { - font-size: 0.875rem; - color: var(--text-secondary); - margin-top: var(--space-1); -} - -/* ── Bento Grid ──────────────────────────────────────── */ -.live-grid { - display: grid; - gap: var(--space-4); -} - -/* 2-column bento layout */ -.live-bento { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--space-4); -} - -.live-bento > .live-span-2 { - grid-column: 1 / -1; -} - -@media (max-width: 768px) { - .live-bento { grid-template-columns: 1fr; } -} - -/* ── Voice Connection Card ───────────────────────────── */ -.voice-connection { - display: flex; - flex-direction: column; - gap: var(--space-3); -} - -.voice-connection-header { - display: flex; - align-items: center; - justify-content: space-between; -} - -.voice-connection-status { - display: flex; - align-items: center; - gap: var(--space-2); - font-size: 0.875rem; -} - -.voice-connection-dot { - width: 10px; - height: 10px; - border-radius: 50%; -} -.voice-connection-dot.connected { background: var(--color-success); box-shadow: 0 0 8px rgba(16, 185, 129, 0.4); } -.voice-connection-dot.connecting { background: var(--color-warning); animation: pulse-connecting 1s ease-in-out infinite; } -.voice-connection-dot.disconnected { background: var(--text-tertiary); } - -/* ── Active Speakers ─────────────────────────────────── */ -.speak-list { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.speak-item { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-sm); - transition: background var(--transition-fast); -} -.speak-item:hover { background: var(--surface-hover); } - -.speak-avatar { - width: 32px; - height: 32px; - border-radius: 50%; - background: var(--surface-container); - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - font-size: 0.75rem; - color: var(--text-secondary); -} - -.speak-info { flex: 1; min-width: 0; } -.speak-name { font-size: 0.8125rem; font-weight: 500; color: var(--text-primary); } -.speak-status { font-size: 0.6875rem; color: var(--text-tertiary); } - -.speak-indicator { - display: flex; - align-items: center; - gap: 2px; -} -.speak-bar { - width: 3px; - height: 12px; - border-radius: 1px; - background: var(--color-primary); - animation: bar-pulse 0.8s ease-in-out infinite; -} -.speak-bar:nth-child(2) { animation-delay: 0.1s; } -.speak-bar:nth-child(3) { animation-delay: 0.2s; } -.speak-bar:nth-child(4) { animation-delay: 0.3s; } -.speak-bar:nth-child(5) { animation-delay: 0.4s; } - -@keyframes bar-pulse { - 0%, 100% { transform: scaleY(1); } - 50% { transform: scaleY(0.5); } -} - -/* ── Audio Visualizer ────────────────────────────────── */ -.audio-visualizer-canvas { - width: 100%; - height: 80px; - border-radius: var(--radius-sm); - background: var(--surface-container); -} - -/* ── Mic Level Meter ─────────────────────────────────── */ -.mic-level { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.mic-track { - position: relative; - height: var(--space-2); - border-radius: var(--radius-pill); - overflow: hidden; - background: var(--surface-container); -} -.mic-fill { - height: 100%; - border-radius: var(--radius-pill); - background: var(--gradient-primary); - transition: width 100ms ease; -} -.mic-clip { - position: absolute; - height: 100%; - width: 2px; - background: white; - right: 0; -} - -/* ── Music Player / Now Playing ──────────────────────── */ -.np-body { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.np-meta { - display: flex; - align-items: center; - justify-content: space-between; - font-size: 0.75rem; - color: var(--text-secondary); -} - -.np-tags { - display: flex; - align-items: center; - gap: var(--space-2); -} - -.np-sep { - border-top: 1px solid var(--surface-border); - padding-top: var(--space-3); -} - -.wave-wrap { - border: 1px solid var(--surface-border); - border-radius: var(--radius-md); - padding: var(--space-3); -} -.wave-progress { - height: var(--space-2); - border-radius: var(--radius-pill); - overflow: hidden; - background: var(--surface-container); - margin-bottom: var(--space-2); -} -.wave-bar { - height: 100%; - border-radius: var(--radius-pill); - background: var(--gradient-primary); - transition: width 200ms ease; -} -.wave-info { - display: flex; - align-items: center; - justify-content: space-between; -} -.wave-title { - font-size: 0.875rem; - font-weight: 500; - max-width: 192px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.wave-time { - display: flex; - align-items: center; - gap: var(--space-2); - font-size: 0.75rem; - font-family: monospace; -} - -/* ── Screen Share Panel ──────────────────────────────── */ -.scrn-actions { - display: flex; - gap: var(--space-2); -} -.scrn-status { - display: inline-block; - padding: 2px 8px; - border-radius: var(--radius-sm); - font-size: 0.75rem; - color: var(--color-success); -} - -/* ── Recordings Panel ────────────────────────────────── */ -.rec-list { - display: flex; - flex-direction: column; - gap: var(--space-2); -} -.rec-item { - display: flex; - align-items: center; - gap: var(--space-3); - padding: var(--space-3); - border-radius: var(--radius-md); - border: 1px solid var(--surface-border); - transition: all var(--transition-fast); -} -.rec-item:hover { - background: var(--surface-hover); - border-color: var(--color-primary); - transform: translateX(4px); -} -.rec-info { flex: 1; min-width: 0; } -.rec-name { font-size: 0.875rem; font-weight: 500; } -.rec-meta { - display: flex; - align-items: center; - gap: var(--space-2); - font-size: 0.75rem; - color: var(--text-secondary); - margin-top: var(--space-1); -} -.rec-actions { - display: flex; - align-items: center; - gap: 0.375rem; - flex-shrink: 0; -} -.rec-footer { margin-top: var(--space-3); text-align: center; } -.rec-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: var(--space-8); - gap: var(--space-2); -} - -.music-body { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -/* ── Legacy aliases ──────────────────────────────────── */ -.live-panel { display: flex; flex-direction: column; gap: var(--space-6); } -.live-grid-3 { grid-template-columns: repeat(3, 1fr); } - -.recordings-sub-panel { overflow: hidden; } - -.audio-visualizer { display: flex; align-items: flex-end; justify-content: center; height: 80px; gap: 2px; padding: var(--space-2); } -.audio-visualizer-bars { display: flex; align-items: flex-end; gap: 2px; height: 100%; width: 100%; } -.audio-bar { width: 8px; background: var(--gradient-primary); border-radius: var(--radius-sm) var(--radius-sm) 0 0; transition: height 100ms ease; } - -.mic-level-meter { display: flex; flex-direction: column; gap: var(--space-2); } -.mic-row { display: flex; align-items: center; gap: var(--space-2); } - -@media (max-width: 1024px) { .live-grid-3 { grid-template-columns: repeat(2, 1fr); } } -@media (max-width: 768px) { .live-grid-3 { grid-template-columns: 1fr; } } -/* ── Polish: Particles, Theme Toggle, Mascot ──────────── */ - -/* ── Particle Background ─────────────────────────────── */ -.particle-bg { - position: fixed; - inset: 0; - overflow: hidden; - pointer-events: none; - z-index: 0; -} -.particle-orb { - position: absolute; - border-radius: 50%; - will-change: transform, filter; -} -.particle-orb:nth-child(1) { - width: 600px; height: 600px; - top: -200px; right: -150px; - background: radial-gradient(circle, rgba(59, 130, 246, 0.5) 0%, transparent 70%); - filter: blur(100px); - animation: orb-drift-enhanced 30s ease-in-out infinite, - hue-rotate-slow 12s ease-in-out infinite, - orb-pulse 8s ease-in-out infinite; -} -.particle-orb:nth-child(2) { - width: 500px; height: 500px; - bottom: -150px; left: -150px; - background: radial-gradient(circle, rgba(99, 102, 241, 0.5) 0%, transparent 70%); - filter: blur(100px); - animation: orb-drift-enhanced 35s ease-in-out infinite reverse, - hue-rotate-slow 14s ease-in-out infinite reverse, - orb-pulse 10s ease-in-out infinite; - animation-delay: -5s; -} -.particle-orb:nth-child(3) { - width: 350px; height: 350px; - top: 40%; left: 60%; - background: radial-gradient(circle, rgba(99, 102, 241, 0.4) 0%, transparent 70%); - filter: blur(100px); - animation: orb-drift-enhanced 40s ease-in-out infinite, - hue-rotate-slow 16s ease-in-out infinite, - orb-pulse 6s ease-in-out infinite; - animation-delay: -10s; -} -.particle-orb:nth-child(4) { - width: 250px; height: 250px; - top: 10%; left: 20%; - background: radial-gradient(circle, rgba(16, 185, 129, 0.4) 0%, transparent 70%); - filter: blur(100px); - animation: orb-drift-enhanced 25s ease-in-out infinite reverse, - hue-rotate-slow 18s ease-in-out infinite, - orb-pulse 12s ease-in-out infinite; - animation-delay: -3s; -} - -.particle-orb:nth-child(5) { - width: 180px; height: 180px; - top: 70%; left: 30%; - background: radial-gradient(circle, rgba(59, 130, 246, 0.35) 0%, transparent 70%); - filter: blur(80px); - animation: orb-drift-enhanced 20s ease-in-out infinite, - hue-rotate-slow 10s ease-in-out infinite, - orb-pulse 5s ease-in-out infinite; - animation-delay: -7s; -} - -@media (max-width: 768px) { - .particle-bg { display: none; } - .app-shell::after { display: none; } -} - -/* ── Theme Toggle ────────────────────────────────────── */ -.theme-toggle-btn { - width: 32px; - height: 32px; - border: 1px solid var(--surface-border); - border-radius: var(--radius-sm); - background: var(--surface-overlay); - color: var(--text-tertiary); - cursor: pointer; - font-size: 0.875rem; - display: flex; - align-items: center; - justify-content: center; - transition: all var(--transition-fast); - flex-shrink: 0; - -webkit-tap-highlight-color: transparent; -} -.theme-toggle-btn:hover { - color: var(--color-primary); - border-color: var(--color-primary); -} - -/* ── Mascot Chatbot ──────────────────────────────────── */ -.mascot-widget { - position: fixed; - right: var(--space-6); - bottom: var(--space-6); - z-index: var(--z-toast); -} -.mascot-launcher { - width: 3.75rem; - height: 3.75rem; - border: none; - border-radius: 50%; - background: var(--gradient-primary); - color: white; - box-shadow: var(--shadow-lg), 0 0 24px rgba(59, 130, 246, 0.25); - cursor: pointer; - font-size: 1.5rem; - transition: all var(--transition-fast); - animation: float 4s ease-in-out infinite; -} -.mascot-launcher:hover { - transform: translateY(-3px) scale(1.05); - animation: none; -} -.mascot-panel { - width: min(24rem, calc(100vw - 2rem)); - height: min(32.5rem, calc(100vh - 7rem)); - display: flex; - flex-direction: column; - overflow: hidden; - border: 1px solid var(--surface-border); - border-radius: var(--radius-lg); - background: var(--surface-raised); - box-shadow: var(--shadow-lg); - animation: fade-in-up var(--transition-bounce); -} -.mascot-panel.minimized { height: 3.75rem; } -.mascot-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-3); - padding: var(--space-3); - background: var(--gradient-primary); - color: white; - flex-shrink: 0; -} -.mascot-header-icon { - display: flex; - align-items: center; - justify-content: center; - width: 2rem; - height: 2rem; - border-radius: var(--radius-sm); - background: rgba(255,255,255,0.15); -} -.mascot-title { - font-size: 0.875rem; - font-weight: 700; - line-height: 1.15; - letter-spacing: 0.02em; -} -.mascot-subtitle { margin-top: 0.125rem; font-size: 0.6875rem; opacity: 0.7; } -.mascot-icon-button { - width: 2rem; - height: 2rem; - border: none; - border-radius: var(--radius-sm); - background: transparent; - color: white; - cursor: pointer; - transition: background var(--transition-fast); -} -.mascot-icon-button:hover { background: rgba(255,255,255,0.20); } -.mascot-messages { - flex: 1; - min-height: 0; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: var(--space-3); - padding: var(--space-4); -} -.mascot-message-row { - display: flex; - gap: var(--space-2); - animation: fade-in-up var(--transition-fast) both; -} -.mascot-message-row.user { justify-content: flex-end; } -.mascot-avatar { - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - width: 1.5rem; - height: 1.5rem; - border-radius: 50%; - background: var(--surface-container); - font-size: 0.875rem; -} -.mascot-bubble { - max-width: 17.5rem; - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-lg); - font-size: 0.875rem; - line-height: 1.5; - overflow-wrap: anywhere; -} -.mascot-bubble.user { - color: white; - background: var(--gradient-primary); - border-bottom-right-radius: var(--radius-sm); -} -.mascot-bubble.mascot { - color: var(--text-primary); - background: var(--surface-container); - border: 1px solid var(--surface-border); - border-bottom-left-radius: var(--radius-sm); -} -.mascot-bubble.typing { - display: flex; - gap: 0.25rem; - padding-top: 0.7rem; - padding-bottom: 0.7rem; -} -.mascot-bubble.typing span { - width: 0.5rem; - height: 0.5rem; - border-radius: 50%; - background: var(--text-tertiary); - animation: notification-pulse 0.8s infinite; -} -.mascot-bubble.typing span:nth-child(2) { animation-delay: 0.1s; } -.mascot-bubble.typing span:nth-child(3) { animation-delay: 0.2s; } -@keyframes notification-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.3; } -} -.mascot-form { - display: flex; - gap: var(--space-2); - padding: var(--space-3); - border-top: 1px solid var(--surface-border); -} -.mascot-input { - flex: 1; - min-width: 0; - border: 1.5px solid transparent; - border-radius: var(--radius-sm); - padding: var(--space-2) var(--space-3); - background: var(--surface-container); - color: var(--text-primary); - font-size: 0.875rem; - transition: all var(--transition-fast); -} -.mascot-input:focus { - outline: none; - border-color: var(--color-primary); - box-shadow: 0 0 0 2px var(--color-primary-muted); - background: var(--surface-raised); -} -.mascot-send { - width: 2.25rem; - border: none; - border-radius: var(--radius-sm); - background: var(--gradient-primary); - color: white; - cursor: pointer; - transition: all var(--transition-fast); -} -.mascot-send:hover { box-shadow: 0 0 12px rgba(59, 130, 246, 0.25); } -.mascot-send:disabled, .mascot-input:disabled { cursor: not-allowed; opacity: 0.55; } - -/* ── Float animation ─────────────────────────────────── */ -@keyframes float { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-8px); } -} - -/* ── Auth ────────────────────────────────────────────── */ -.auth-box { width: 400px; text-align: center; } -.auth-lock { font-size: 3rem; margin-bottom: var(--space-4); } -.auth-title { - font-size: 1.375rem; - font-weight: 700; - margin-bottom: var(--space-2); - background: var(--gradient-brand); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} -.auth-desc { - font-size: 0.875rem; - color: var(--text-secondary); - margin-bottom: var(--space-6); -} -.auth-form { display: flex; flex-direction: column; gap: var(--space-3); } -.auth-error { - font-size: 0.75rem; - color: var(--color-error); - padding: var(--space-2) var(--space-3); - background: rgba(239, 68, 68, 0.08); - border-radius: var(--radius-sm); -} -.auth-close-btn { - position: absolute; - top: 0; - right: 0; - background: none; - border: none; - color: var(--text-tertiary); - font-size: 1.25rem; - cursor: pointer; - padding: 0.25rem; - line-height: 1; - transition: color var(--transition-fast); -} -.auth-close-btn:hover { color: var(--text-primary); } -/* ── Animations — Live Dashboard Effects ──────────────── * - * Keyframes, applied animations, and utility classes. * - * Respects prefers-reduced-motion. * - * ──────────────────────────────────────────────────────── */ - -/* ════════════════════════════════════════════════════════ - 1. KEYFRAMES - ════════════════════════════════════════════════════════ */ - -/* ── Entry / Reveal ─────────────────────────────────── */ -@keyframes slide-in-left { - from { opacity: 0; transform: translateX(-20px); } - to { opacity: 1; transform: translateX(0); } -} - -@keyframes slide-in-down { - from { opacity: 0; transform: translateY(-12px); } - to { opacity: 1; transform: translateY(0); } -} - -@keyframes scale-bounce { - 0% { opacity: 0; transform: scale(0.85); } - 60% { opacity: 1; transform: scale(1.04); } - 100% { opacity: 1; transform: scale(1); } -} - -@keyframes pop-in { - 0% { opacity: 0; transform: scale(0.8); } - 70% { opacity: 1; transform: scale(1.08); } - 100% { opacity: 1; transform: scale(1); } -} - -/* ── Status / Alive ─────────────────────────────────── */ -@keyframes breathe { - 0%, 100% { opacity: 1; transform: scale(1); box-shadow: 0 0 4px currentColor; } - 50% { opacity: 0.6; transform: scale(1.2); box-shadow: 0 0 14px currentColor; } -} - -@keyframes breathe-soft { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -@keyframes ring-pulse { - 0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); } - 70% { box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); } - 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); } -} - -@keyframes ring-pulse-success { - 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); } - 70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); } - 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); } -} - -@keyframes ring-pulse-error { - 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); } - 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } - 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } -} - -@keyframes processing-sweep { - 0% { background-position: -200% center; } - 100% { background-position: 200% center; } -} - -@keyframes live-indicator { - 0%, 100% { opacity: 1; box-shadow: 0 0 6px rgba(239, 68, 68, 0.4); } - 50% { opacity: 0.7; box-shadow: 0 0 16px rgba(239, 68, 68, 0.7); } -} - -/* ── Ambient / Decorative ───────────────────────────── */ -/* Slow breathing for background glow layers */ -@keyframes ambient-glow { - 0%, 100% { opacity: 0.3; } - 50% { opacity: 0.7; } -} - -/* Dynamic mesh drift — shifts gradient center positions */ -@keyframes bg-mesh-drift { - 0% { background-position: 0% 0%, 100% 100%, 50% 50%; } - 25% { background-position: 30% 20%, 70% 80%, 20% 60%; } - 50% { background-position: 60% 10%, 40% 60%, 80% 30%; } - 75% { background-position: 20% 60%, 80% 20%, 40% 80%; } - 100% { background-position: 0% 0%, 100% 100%, 50% 50%; } -} - -/* Slow hue rotation for orb glow */ -@keyframes hue-rotate-slow { - 0% { filter: hue-rotate(0deg); } - 50% { filter: hue-rotate(30deg); } - 100% { filter: hue-rotate(0deg); } -} - -/* Orb size pulsing */ -@keyframes orb-pulse { - 0%, 100% { transform: scale(1); opacity: 0.4; } - 50% { transform: scale(1.08); opacity: 0.6; } -} - -/* Expanded orb drift — wider, slower, organic */ -@keyframes orb-drift-enhanced { - 0% { transform: translate(0, 0) scale(1); } - 20% { transform: translate(80px, -50px) scale(1.1); } - 40% { transform: translate(-50px, 70px) scale(0.9); } - 60% { transform: translate(100px, 30px) scale(1.05); } - 80% { transform: translate(-70px, -60px) scale(0.95); } - 100% { transform: translate(0, 0) scale(1); } -} - -/* Grain/noise texture shifting */ -@keyframes grain-shift { - 0%, 100% { transform: translate(0, 0) rotate(0deg); } - 10% { transform: translate(-5%, -5%) rotate(0.5deg); } - 20% { transform: translate(-10%, 0%) rotate(-0.5deg); } - 30% { transform: translate(0%, 5%) rotate(1deg); } - 40% { transform: translate(5%, -3%) rotate(-0.3deg); } - 50% { transform: translate(-3%, -8%) rotate(0.8deg); } - 60% { transform: translate(8%, 3%) rotate(-0.6deg); } - 70% { transform: translate(-6%, 6%) rotate(0.4deg); } - 80% { transform: translate(4%, -6%) rotate(-0.7deg); } - 90% { transform: translate(-2%, 2%) rotate(0.2deg); } -} - -@keyframes gradient-shimmer { - 0% { background-position: 0% center; } - 100% { background-position: 200% center; } -} - -@keyframes float-variation { - 0%, 100% { transform: translateY(0) rotate(0deg); } - 33% { transform: translateY(-8px) rotate(1.5deg); } - 66% { transform: translateY(-4px) rotate(-1deg); } -} - -@keyframes fade-in-down { - from { opacity: 0; transform: translateY(-10px); } - to { opacity: 1; transform: translateY(0); } -} - -/* ── Interactive ────────────────────────────────────── */ -@keyframes glow-border { - 0%, 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); } - 50% { box-shadow: 0 0 12px 1px rgba(59, 130, 246, 0.15); } -} - -@keyframes ripple { - 0% { transform: scale(0); opacity: 0.6; } - 100% { transform: scale(4); opacity: 0; } -} - -/* ── Bar pulses (audio / visualizer) ────────────────── */ -@keyframes bar-pulse-1 { - 0%, 100% { transform: scaleY(1); } - 50% { transform: scaleY(0.4); } -} -@keyframes bar-pulse-2 { - 0%, 100% { transform: scaleY(0.5); } - 50% { transform: scaleY(1.2); } -} -@keyframes bar-pulse-3 { - 0%, 100% { transform: scaleY(0.7); } - 50% { transform: scaleY(1); } -} - -@keyframes count-up { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } -} - - -/* ════════════════════════════════════════════════════════ - 2. UTILITY ANIMATION CLASSES - Use these in Rust components: class="animate-breathe" - ════════════════════════════════════════════════════════ */ - -.animate-breathe { - animation: breathe 3s ease-in-out infinite; -} - -.animate-breathe-soft { - animation: breathe-soft 2s ease-in-out infinite; -} - -.animate-ring-pulse { - animation: ring-pulse 2s ease-in-out infinite; -} - -.animate-live-indicator { - animation: live-indicator 1.5s ease-in-out infinite; -} - -.animate-float { - animation: float-variation 5s ease-in-out infinite; -} - -.animate-gradient-shimmer { - background-size: 200% 100%; - animation: gradient-shimmer 4s ease-in-out infinite alternate; -} - -.animate-spin-slow { - animation: spin 3s linear infinite; -} - -.animate-pulse-connecting { - animation: pulse-connecting 1s ease-in-out infinite; -} - -.animate-scale-bounce { - animation: scale-bounce 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) both; -} - -.animate-pop-in { - animation: pop-in 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) both; -} - -.animate-fade-in-down { - animation: fade-in-down var(--transition-normal) ease-out; -} - -.animate-slide-in-left { - animation: slide-in-left var(--transition-normal) ease-out; -} - -.animate-count-up { - animation: count-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; -} - -.animate-shimmer-sweep { - background: linear-gradient( - 110deg, - transparent 30%, - rgba(59, 130, 246, 0.08) 50%, - transparent 70% - ); - background-size: 200% 100%; - animation: processing-sweep 1.5s ease-in-out infinite; -} - - -/* ════════════════════════════════════════════════════════ - 3. APPLIED ANIMATIONS — auto-wired to selectors - ════════════════════════════════════════════════════════ */ - -/* ── Ambient animated mesh background ────────────────── */ -.app-shell::before { - content: ''; - position: fixed; - inset: -50%; - pointer-events: none; - z-index: -1; - background: - radial-gradient(ellipse at 20% 30%, rgba(59, 130, 246, 0.06) 0%, transparent 40%), - radial-gradient(ellipse at 80% 70%, rgba(99, 102, 241, 0.05) 0%, transparent 40%), - radial-gradient(ellipse at 40% 80%, rgba(16, 185, 129, 0.03) 0%, transparent 40%), - radial-gradient(ellipse at 60% 20%, rgba(245, 158, 11, 0.02) 0%, transparent 40%); - background-size: 200% 200%, 200% 200%, 200% 200%, 200% 200%; - animation: bg-mesh-drift 20s ease-in-out infinite alternate; - will-change: background-position; -} - -/* ── Subtle noise/grain texture overlay ─────────────── */ -.app-shell::after { - content: ''; - position: fixed; - inset: -50%; - pointer-events: none; - z-index: 0; - opacity: 0.015; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); - background-repeat: repeat; - background-size: 256px 256px; - animation: grain-shift 0.5s steps(4) infinite; - will-change: transform; -} - -/* ── Light theme mesh adjustment ────────────────────── */ -[data-theme="light"] .app-shell::before { - background: - radial-gradient(ellipse at 20% 30%, rgba(37, 99, 235, 0.04) 0%, transparent 40%), - radial-gradient(ellipse at 80% 70%, rgba(99, 102, 241, 0.04) 0%, transparent 40%), - radial-gradient(ellipse at 40% 80%, rgba(16, 185, 129, 0.03) 0%, transparent 40%), - radial-gradient(ellipse at 60% 20%, rgba(245, 158, 11, 0.02) 0%, transparent 40%); -} -[data-theme="light"] .app-shell::after { - opacity: 0.008; -} - -/* ── Gradient text shimmer (brand elements) ─────────── */ -.sidebar-brand-name, -.live-title, -.auth-title { - background-size: 200% 100%; - animation: gradient-shimmer 6s ease-in-out infinite alternate; -} - -/* ── Connected status dot: alive breath ─────────────── */ -.status-dot, -.voice-connection-dot.connected { - animation: breathe 3s ease-in-out infinite; -} - -/* Keep existing connecting animation (overrides above) */ -.status-dot.is-connecting, -.voice-connection-dot.connecting { - animation: pulse-connecting 1s ease-in-out infinite !important; -} - -.status-dot.disconnected, -.voice-connection-dot.disconnected { - animation: none !important; -} - -/* ── Live indicator (red recording dot) ─────────────── */ -.msg-card-ai-badge.flagged::before, -.msg-analysis.flagged::before { - content: ''; - display: inline-block; - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--color-error); - margin-right: 4px; - vertical-align: middle; - animation: live-indicator 1.5s ease-in-out infinite; -} - -/* ── Particle orbs: ambient breathing overlay ───────── */ -.particle-bg { - animation: ambient-glow 6s ease-in-out infinite alternate; -} - -/* ── Live bento grid: staggered entry ───────────────── */ -.live-bento > * { - animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) both; -} -.live-bento > :nth-child(1) { animation-delay: 0.03s; } -.live-bento > :nth-child(2) { animation-delay: 0.06s; } -.live-bento > :nth-child(3) { animation-delay: 0.09s; } -.live-bento > :nth-child(4) { animation-delay: 0.12s; } -.live-bento > :nth-child(5) { animation-delay: 0.15s; } -.live-bento > :nth-child(6) { animation-delay: 0.18s; } - -/* ── Dashboard metric cards: staggered entry ────────── */ -.dashboard-metric-card { - animation: fade-in-up 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; -} -.dashboard-metric-card:nth-child(1) { animation-delay: 0.04s; } -.dashboard-metric-card:nth-child(2) { animation-delay: 0.08s; } -.dashboard-metric-card:nth-child(3) { animation-delay: 0.12s; } -.dashboard-metric-card:nth-child(4) { animation-delay: 0.16s; } -.dashboard-metric-card:nth-child(5) { animation-delay: 0.20s; } -.dashboard-metric-card:nth-child(6) { animation-delay: 0.24s; } -.dashboard-metric-card:nth-child(7) { animation-delay: 0.28s; } -.dashboard-metric-card:nth-child(8) { animation-delay: 0.32s; } - -/* ── Dashboard rows: staggered fade-in ──────────────── */ -.dashboard-summary-row, -.dashboard-top-channel-row { - animation: fade-in 0.4s ease-out both; -} -.dashboard-summary-row:nth-child(1), .dashboard-top-channel-row:nth-child(1) { animation-delay: 0.02s; } -.dashboard-summary-row:nth-child(2), .dashboard-top-channel-row:nth-child(2) { animation-delay: 0.04s; } -.dashboard-summary-row:nth-child(3), .dashboard-top-channel-row:nth-child(3) { animation-delay: 0.06s; } -.dashboard-summary-row:nth-child(4), .dashboard-top-channel-row:nth-child(4) { animation-delay: 0.08s; } -.dashboard-summary-row:nth-child(5), .dashboard-top-channel-row:nth-child(5) { animation-delay: 0.10s; } -.dashboard-summary-row:nth-child(6), .dashboard-top-channel-row:nth-child(6) { animation-delay: 0.12s; } -.dashboard-summary-row:nth-child(7), .dashboard-top-channel-row:nth-child(7) { animation-delay: 0.14s; } -.dashboard-summary-row:nth-child(8), .dashboard-top-channel-row:nth-child(8) { animation-delay: 0.16s; } -.dashboard-summary-row:nth-child(9), .dashboard-top-channel-row:nth-child(9) { animation-delay: 0.18s; } -.dashboard-summary-row:nth-child(10),.dashboard-top-channel-row:nth-child(10) { animation-delay: 0.20s; } - -/* ── Message cards: staggered entry ─────────────────── */ -.msg-card { - animation: fade-in-up 0.4s cubic-bezier(0.16, 1, 0.3, 1) both; -} -.msg-card:nth-child(1) { animation-delay: 0.02s; } -.msg-card:nth-child(2) { animation-delay: 0.05s; } -.msg-card:nth-child(3) { animation-delay: 0.08s; } -.msg-card:nth-child(4) { animation-delay: 0.11s; } -.msg-card:nth-child(5) { animation-delay: 0.14s; } -.msg-card:nth-child(6) { animation-delay: 0.17s; } -.msg-card:nth-child(7) { animation-delay: 0.20s; } -.msg-card:nth-child(8) { animation-delay: 0.23s; } -.msg-card:nth-child(9) { animation-delay: 0.26s; } -.msg-card:nth-child(10) { animation-delay: 0.29s; } - -/* ── Recording items: slide in from left ────────────── */ -.rec-item { - animation: slide-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; -} -.rec-item:nth-child(1) { animation-delay: 0.02s; } -.rec-item:nth-child(2) { animation-delay: 0.06s; } -.rec-item:nth-child(3) { animation-delay: 0.10s; } -.rec-item:nth-child(4) { animation-delay: 0.14s; } -.rec-item:nth-child(5) { animation-delay: 0.18s; } - -/* ── Speaker items: pop in ──────────────────────────── */ -.speak-item { - animation: fade-in 0.3s ease-out both; -} -.speak-item:nth-child(1) { animation-delay: 0.02s; } -.speak-item:nth-child(2) { animation-delay: 0.05s; } -.speak-item:nth-child(3) { animation-delay: 0.08s; } -.speak-item:nth-child(4) { animation-delay: 0.11s; } -.speak-item:nth-child(5) { animation-delay: 0.14s; } - -/* ── Audio visualizer bars: individual timing ───────── */ -.audio-bar { - animation: bar-pulse-2 1.2s ease-in-out infinite; -} -.audio-bar:nth-child(odd) { - animation-name: bar-pulse-1; - animation-duration: 0.9s; -} -.audio-bar:nth-child(3n) { - animation-name: bar-pulse-3; - animation-duration: 1.5s; -} - -/* ── Interactive cards: hover glow effect ────────────── */ -.card-interactive:hover { - animation: glow-border 0.8s ease-in-out; - border-color: var(--color-primary); -} - -/* ── Nav active indicator: continuous subtle glow ───── */ -.sidebar-nav-item.is-active::after { - animation: indicator-grow 250ms cubic-bezier(0.16, 1, 0.3, 1) both, - breathe-soft 3s ease-in-out 0.3s infinite; -} - -/* ── Filter chip active: pop scale ──────────────────── */ -.filter-chip-v2.is-active { - animation: pop-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) both; -} - -/* ── AI badge processing: shimmer sweep ─────────────── */ -.msg-card-ai-badge.processing, -.status-badge-processing { - background: linear-gradient( - 110deg, - var(--color-primary-muted) 25%, - rgba(59, 130, 246, 0.35) 50%, - var(--color-primary-muted) 75% - ) !important; - background-size: 200% 100% !important; - animation: processing-sweep 1.4s ease-in-out infinite; -} -.status-badge-processing::before { - animation: ring-pulse 1.2s ease-in-out infinite !important; -} - -/* ── Image grid items: reveal ───────────────────────── */ -.image-grid-item { - animation: fade-in 0.4s ease-out both; -} -.image-grid-item:nth-child(1) { animation-delay: 0.02s; } -.image-grid-item:nth-child(2) { animation-delay: 0.05s; } -.image-grid-item:nth-child(3) { animation-delay: 0.08s; } -.image-grid-item:nth-child(4) { animation-delay: 0.11s; } -.image-grid-item:nth-child(5) { animation-delay: 0.14s; } -.image-grid-item:nth-child(6) { animation-delay: 0.17s; } -.image-grid-item:nth-child(7) { animation-delay: 0.20s; } -.image-grid-item:nth-child(8) { animation-delay: 0.23s; } - -/* ── Dashboard queue value: count-up feel ───────────── */ -.dashboard-queue-value { - animation: count-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; -} - -/* ── Voice connection panel: entry ──────────────────── */ -.voice-connection { - animation: fade-in-up 0.5s cubic-bezier(0.16, 1, 0.3, 1) both; -} - -/* ── Now playing: slide in ──────────────────────────── */ -.np-body { - animation: slide-in-left 0.4s cubic-bezier(0.16, 1, 0.3, 1) both; -} - -/* ── Skeleton: enhanced shimmer ─────────────────────── */ -.skeleton, -.msg-skel-avatar, -.msg-skel-line, -.msg-skel-badge, -.dashboard-skel-avatar, -.dashboard-skel-line { - background: linear-gradient( - 110deg, - var(--surface-container) 28%, - rgba(59, 130, 246, 0.06) 48%, - var(--surface-container) 68% - ) !important; - background-size: 200% 100% !important; -} - -/* ── Tab content: stronger entry ────────────────────── */ -.tab-content { - animation: fade-in-up 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; -} - -/* ── Mobile tab active: subtle indicator ────────────── */ -.mobile-tab-item.is-active { - animation: pop-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) both; -} - -/* ── Mascot launcher: enhanced float ────────────────── */ -.mascot-launcher { - animation: float-variation 5s ease-in-out infinite; -} -.mascot-launcher:hover { - animation: none !important; -} - -/* ── Empty state icons: subtle float ────────────────── */ -.empty-state-icon { - animation: float-variation 6s ease-in-out infinite; -} - -/* ── Recent message action buttons ──────────────────── */ -.btn-icon-sm:active:not(:disabled), -.btn-icon:active:not(:disabled) { - animation: ripple 0.4s ease-out; -} - -/* ── Small decorative: channel list rows ────────────── */ -.dashboard-top-channel-row:hover { - animation: none; /* Keep the existing hover translateX */ -} - - -/* ════════════════════════════════════════════════════════ - 4. RESPECT REDUCED MOTION - ════════════════════════════════════════════════════════ */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} diff --git a/services/frontend/frontend/src/styles/dashboard.css b/services/frontend/frontend/src/styles/dashboard.css deleted file mode 100644 index 6b31224..0000000 --- a/services/frontend/frontend/src/styles/dashboard.css +++ /dev/null @@ -1,204 +0,0 @@ -/* ── Dashboard Panel ──────────────────────────────────── */ - -.dashboard-panel { - display: flex; - flex-direction: column; - gap: var(--space-6); -} - -.dashboard-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--space-4); -} - -/* ── Stats Overview (4-column grid) ──────────────────── */ -.dashboard-stats-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: var(--space-4); -} - -.dashboard-metric-card { - padding: var(--space-5); - overflow: hidden; - height: 140px; - display: flex; - flex-direction: column; - justify-content: center; -} -.dashboard-metric-card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-md); - border-color: rgba(59, 130, 246, 0.15); -} - -.dashboard-metric-content { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-3); -} - -.dashboard-metric-label { - color: var(--text-secondary); - font-size: 0.75rem; - font-weight: 500; - letter-spacing: 0.02em; - text-transform: uppercase; -} - -.dashboard-metric-value { - margin-top: var(--space-1); - font-size: 1.75rem; - font-weight: 800; - letter-spacing: -0.03em; - color: var(--text-primary); - font-family: 'JetBrains Mono', monospace; -} - -.dashboard-metric-trend { - font-size: 0.6875rem; - font-weight: 500; - margin-top: var(--space-1); -} -.dashboard-metric-trend.positive { color: var(--color-success); } -.dashboard-metric-trend.negative { color: var(--color-error); } - -.dashboard-metric-icon { - display: flex; - align-items: center; - justify-content: center; - width: 2.75rem; - height: 2.75rem; - border-radius: var(--radius-lg); - font-size: 1.25rem; -} - -/* ── Summary Lists ───────────────────────────────────── */ -.dashboard-wide-card { grid-column: span 2; } - -.dashboard-summary-list { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.dashboard-summary-row { - display: flex; - align-items: flex-start; - gap: var(--space-3); - padding: var(--space-3); - border-bottom: 1px solid var(--surface-border); - transition: all var(--transition-fast); -} -.dashboard-summary-row:hover { - background: var(--surface-hover); -} - -.dashboard-summary-avatar { - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - width: 2.5rem; - height: 2.5rem; - border-radius: var(--radius-full); - background: var(--surface-container); - color: var(--text-secondary); - font-weight: 700; - overflow: hidden; -} -.dashboard-summary-avatar-img { - width: 100%; - height: 100%; - object-fit: cover; -} -.dashboard-channel-avatar { - color: var(--color-primary); - background: var(--color-primary-muted); -} - -.dashboard-summary-main { min-width: 0; flex: 1; } -.dashboard-summary-title { - color: var(--text-primary); - font-size: 0.875rem; - font-weight: 600; -} -.dashboard-summary-text { - margin-top: var(--space-1); - color: var(--text-secondary); - font-size: 0.8125rem; - line-height: 1.45; -} -.dashboard-summary-meta { - display: flex; - flex-wrap: wrap; - gap: var(--space-2); - margin-top: var(--space-2); - color: var(--text-tertiary); - font-size: 0.75rem; -} - -.dashboard-list-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--space-3); - padding: var(--space-10) var(--space-4); - text-align: center; -} - -/* ── Skeleton cells ──────────────────────────────────── */ -.dashboard-skel-row { - display: flex; - align-items: center; - gap: var(--space-3); - padding: var(--space-3); -} -.dashboard-skel-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - flex-shrink: 0; - background: linear-gradient(90deg, var(--surface-container) 25%, rgba(59, 130, 246, 0.04) 50%, var(--surface-container) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s ease-in-out infinite; -} -.dashboard-skel-lines { - flex: 1; - display: flex; - flex-direction: column; - gap: var(--space-2); -} -.dashboard-skel-line { - height: 16px; - border-radius: var(--radius-sm); - background: linear-gradient(90deg, var(--surface-container) 25%, rgba(59, 130, 246, 0.04) 50%, var(--surface-container) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s ease-in-out infinite; -} - -/* ── Responsive ──────────────────────────────────────── */ -@media (max-width: 1024px) { - .dashboard-stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } -} -@media (max-width: 768px) { - .dashboard-stats-grid { grid-template-columns: 1fr; } - .dashboard-wide-card { grid-column: span 1; } -} - -/* ── Legacy aliases ──────────────────────────────────── */ -.dashboard-stats { display: flex; flex-wrap: wrap; gap: var(--space-4); } -.dashboard-top-channels, .dashboard-summary-list { display: flex; flex-direction: column; gap: var(--space-2); } -.dashboard-top-channel-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); padding: var(--space-2) var(--space-3); border-radius: var(--radius-sm); background: var(--surface-container); color: var(--text-secondary); font-size: 0.875rem; transition: all var(--transition-fast); } -.dashboard-top-channel-row:hover { background: var(--surface-overlay); transform: translateX(2px); } -.dashboard-moderation-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-3); } -.dashboard-queue-value { font-size: 1.75rem; font-weight: 800; color: var(--text-primary); letter-spacing: -0.03em; } -.dashboard-queue-label { margin-top: var(--space-1); color: var(--text-secondary); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; } -.dashboard-list-card { overflow: hidden; } -.dashboard-list-toolbar { margin-bottom: var(--space-4); } - -.badge-secondary { background: var(--color-primary-muted); color: var(--color-primary); } diff --git a/services/frontend/frontend/src/styles/layout.css b/services/frontend/frontend/src/styles/layout.css deleted file mode 100644 index d448c2b..0000000 --- a/services/frontend/frontend/src/styles/layout.css +++ /dev/null @@ -1,274 +0,0 @@ -/* ── App Shell & Layout ───────────────────────────────── */ - -.app-shell { - position: relative; - z-index: 1; - display: flex; - height: 100vh; - background: var(--surface-base); - color: var(--text-primary); -} - -/* ── Sidebar ─────────────────────────────────────────── */ -.app-sidebar { - width: var(--sidebar-width); - flex-shrink: 0; - height: 100vh; - display: flex; - flex-direction: column; - border-right: 1px solid var(--surface-border); - background: var(--surface-glass); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - position: sticky; - top: 0; - z-index: var(--z-sidebar); -} - -.sidebar-brand { - display: flex; - align-items: center; - gap: var(--space-3); - height: 60px; - padding: 0 var(--space-4); - flex-shrink: 0; - border-bottom: 1px solid var(--surface-border); -} - -.sidebar-brand-icon { - font-size: 1.25rem; - line-height: 1; -} - -.sidebar-brand-text { - display: flex; - flex-direction: column; -} - -.sidebar-brand-name { - font-weight: 800; - font-size: 1rem; - background: var(--gradient-brand); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - letter-spacing: -0.03em; - line-height: 1.2; -} - -.sidebar-brand-subtitle { - color: var(--text-tertiary); - font-size: 0.6875rem; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.08em; - line-height: 1.3; -} - -/* ── Navigation ──────────────────────────────────────── */ -.sidebar-nav { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - gap: var(--space-1); - padding: var(--space-3); - overflow-y: auto; -} - -.sidebar-nav-item { - display: flex; - align-items: center; - gap: var(--space-3); - height: 40px; - padding: 0 var(--space-3); - border: none; - border-radius: 10px; - background: transparent; - color: var(--text-secondary); - font-family: inherit; - font-size: 14px; - font-weight: 500; - line-height: 20px; - cursor: pointer; - transition: all var(--transition-fast); - text-align: left; - width: 100%; - position: relative; - -webkit-tap-highlight-color: transparent; -} - -.sidebar-nav-item:hover { - background: var(--surface-hover); - color: var(--color-primary); -} - -.sidebar-nav-item.is-active { - background: var(--surface-overlay); - color: var(--color-primary); - font-weight: 600; -} - -.sidebar-nav-item.is-active::after { - content: ''; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - width: 3px; - height: 24px; - border-radius: var(--radius-pill); - background: var(--gradient-primary); - box-shadow: 0 0 12px rgba(59, 130, 246, 0.4); - animation: indicator-grow 250ms cubic-bezier(0.16, 1, 0.3, 1) both; -} - -.sidebar-nav-icon { - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - flex-shrink: 0; - font-size: 1rem; -} - -/* ── Sidebar Footer ──────────────────────────────────── */ -.sidebar-footer { - flex-shrink: 0; - padding: var(--space-3); - border-top: 1px solid var(--surface-border); - display: flex; - align-items: center; - gap: var(--space-2); -} - -.sidebar-footer-status { - display: flex; - align-items: center; - gap: var(--space-1); - flex: 1; - min-width: 0; -} - -.status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - flex-shrink: 0; - transition: background var(--transition-fast); -} - -.status-dot.is-connecting { - animation: pulse-connecting 1s ease-in-out infinite; -} - -.status-text { - font-size: 0.75rem; - color: var(--text-tertiary); - white-space: nowrap; -} - -/* ── Content Area ────────────────────────────────────── */ -.app-content { - flex: 1; - min-width: 0; - overflow-y: auto; - padding: var(--space-6); - max-width: 1400px; - animation: content-enter 500ms cubic-bezier(0.16, 1, 0.3, 1) both; -} - -.app-content > * { - animation: card-in 500ms cubic-bezier(0.16, 1, 0.3, 1) both; -} -.app-content > :nth-child(1) { animation-delay: 30ms; } -.app-content > :nth-child(2) { animation-delay: 60ms; } -.app-content > :nth-child(3) { animation-delay: 90ms; } - -/* ── Theme Toggle in Sidebar ──────────────────────────── */ -.sidebar-theme-btn { - width: 32px; - height: 32px; - border: 1px solid var(--surface-border); - border-radius: var(--radius-sm); - background: var(--surface-overlay); - color: var(--text-tertiary); - cursor: pointer; - font-size: 0.875rem; - display: flex; - align-items: center; - justify-content: center; - transition: all var(--transition-fast); - flex-shrink: 0; -} -.sidebar-theme-btn:hover { - color: var(--color-primary); - border-color: var(--color-primary); -} - -/* ── Mobile Tab Bar ──────────────────────────────────── */ -.mobile-tab-bar { - display: none; -} - -@media (max-width: 768px) { - .app-main { flex-direction: column; } - .app-sidebar { display: none; } - .app-content { padding: var(--space-4); } - - .mobile-tab-bar { - display: flex; - position: fixed; - bottom: 0; - left: 0; - right: 0; - background: var(--surface-base); - border-top: 1px solid var(--surface-border); - z-index: var(--z-overlay); - } - - .mobile-tab-item { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - gap: 0.25rem; - padding: 0.5rem; - background: none; - border: none; - font-family: inherit; - font-size: 0.625rem; - cursor: pointer; - color: var(--text-tertiary); - transition: color var(--transition-fast); - -webkit-tap-highlight-color: transparent; - } - - .mobile-tab-item.is-active { - color: var(--color-primary); - } - - .mobile-tab-item-icon { - font-size: 1.25rem; - line-height: 1; - } -} - -/* ── Animations ──────────────────────────────────────── */ -@keyframes content-enter { - from { opacity: 0; transform: translateY(16px); } - to { opacity: 1; transform: translateY(0); } -} -@keyframes card-in { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } -} -@keyframes indicator-grow { - from { height: 0; opacity: 0; } - to { height: 24px; opacity: 1; } -} -@keyframes pulse-connecting { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.4; transform: scale(0.7); } -} diff --git a/services/frontend/frontend/src/styles/live.css b/services/frontend/frontend/src/styles/live.css deleted file mode 100644 index bafd3f0..0000000 --- a/services/frontend/frontend/src/styles/live.css +++ /dev/null @@ -1,316 +0,0 @@ -/* ── Live Panel (Bento Grid) ──────────────────────────── */ - -.live-body { - display: flex; - flex-direction: column; - gap: var(--space-6); -} - -.live-head { - display: flex; - align-items: center; - justify-content: space-between; -} - -.live-title { - font-size: 1.5rem; - font-weight: 700; - letter-spacing: -0.025em; - background: var(--gradient-brand); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} -.live-desc { - font-size: 0.875rem; - color: var(--text-secondary); - margin-top: var(--space-1); -} - -/* ── Bento Grid ──────────────────────────────────────── */ -.live-grid { - display: grid; - gap: var(--space-4); -} - -/* 2-column bento layout */ -.live-bento { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--space-4); -} - -.live-bento > .live-span-2 { - grid-column: 1 / -1; -} - -@media (max-width: 768px) { - .live-bento { grid-template-columns: 1fr; } -} - -/* ── Voice Connection Card ───────────────────────────── */ -.voice-connection { - display: flex; - flex-direction: column; - gap: var(--space-3); -} - -.voice-connection-header { - display: flex; - align-items: center; - justify-content: space-between; -} - -.voice-connection-status { - display: flex; - align-items: center; - gap: var(--space-2); - font-size: 0.875rem; -} - -.voice-connection-dot { - width: 10px; - height: 10px; - border-radius: 50%; -} -.voice-connection-dot.connected { background: var(--color-success); box-shadow: 0 0 8px rgba(16, 185, 129, 0.4); } -.voice-connection-dot.connecting { background: var(--color-warning); animation: pulse-connecting 1s ease-in-out infinite; } -.voice-connection-dot.disconnected { background: var(--text-tertiary); } - -/* ── Active Speakers ─────────────────────────────────── */ -.speak-list { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.speak-item { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-sm); - transition: background var(--transition-fast); -} -.speak-item:hover { background: var(--surface-hover); } - -.speak-avatar { - width: 32px; - height: 32px; - border-radius: 50%; - background: var(--surface-container); - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - font-size: 0.75rem; - color: var(--text-secondary); -} - -.speak-info { flex: 1; min-width: 0; } -.speak-name { font-size: 0.8125rem; font-weight: 500; color: var(--text-primary); } -.speak-status { font-size: 0.6875rem; color: var(--text-tertiary); } - -.speak-indicator { - display: flex; - align-items: center; - gap: 2px; -} -.speak-bar { - width: 3px; - height: 12px; - border-radius: 1px; - background: var(--color-primary); - animation: bar-pulse 0.8s ease-in-out infinite; -} -.speak-bar:nth-child(2) { animation-delay: 0.1s; } -.speak-bar:nth-child(3) { animation-delay: 0.2s; } -.speak-bar:nth-child(4) { animation-delay: 0.3s; } -.speak-bar:nth-child(5) { animation-delay: 0.4s; } - -@keyframes bar-pulse { - 0%, 100% { transform: scaleY(1); } - 50% { transform: scaleY(0.5); } -} - -/* ── Audio Visualizer ────────────────────────────────── */ -.audio-visualizer-canvas { - width: 100%; - height: 80px; - border-radius: var(--radius-sm); - background: var(--surface-container); -} - -/* ── Mic Level Meter ─────────────────────────────────── */ -.mic-level { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.mic-track { - position: relative; - height: var(--space-2); - border-radius: var(--radius-pill); - overflow: hidden; - background: var(--surface-container); -} -.mic-fill { - height: 100%; - border-radius: var(--radius-pill); - background: var(--gradient-primary); - transition: width 100ms ease; -} -.mic-clip { - position: absolute; - height: 100%; - width: 2px; - background: white; - right: 0; -} - -/* ── Music Player / Now Playing ──────────────────────── */ -.np-body { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -.np-meta { - display: flex; - align-items: center; - justify-content: space-between; - font-size: 0.75rem; - color: var(--text-secondary); -} - -.np-tags { - display: flex; - align-items: center; - gap: var(--space-2); -} - -.np-sep { - border-top: 1px solid var(--surface-border); - padding-top: var(--space-3); -} - -.wave-wrap { - border: 1px solid var(--surface-border); - border-radius: var(--radius-md); - padding: var(--space-3); -} -.wave-progress { - height: var(--space-2); - border-radius: var(--radius-pill); - overflow: hidden; - background: var(--surface-container); - margin-bottom: var(--space-2); -} -.wave-bar { - height: 100%; - border-radius: var(--radius-pill); - background: var(--gradient-primary); - transition: width 200ms ease; -} -.wave-info { - display: flex; - align-items: center; - justify-content: space-between; -} -.wave-title { - font-size: 0.875rem; - font-weight: 500; - max-width: 192px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.wave-time { - display: flex; - align-items: center; - gap: var(--space-2); - font-size: 0.75rem; - font-family: monospace; -} - -/* ── Screen Share Panel ──────────────────────────────── */ -.scrn-actions { - display: flex; - gap: var(--space-2); -} -.scrn-status { - display: inline-block; - padding: 2px 8px; - border-radius: var(--radius-sm); - font-size: 0.75rem; - color: var(--color-success); -} - -/* ── Recordings Panel ────────────────────────────────── */ -.rec-list { - display: flex; - flex-direction: column; - gap: var(--space-2); -} -.rec-item { - display: flex; - align-items: center; - gap: var(--space-3); - padding: var(--space-3); - border-radius: var(--radius-md); - border: 1px solid var(--surface-border); - transition: all var(--transition-fast); -} -.rec-item:hover { - background: var(--surface-hover); - border-color: var(--color-primary); - transform: translateX(4px); -} -.rec-info { flex: 1; min-width: 0; } -.rec-name { font-size: 0.875rem; font-weight: 500; } -.rec-meta { - display: flex; - align-items: center; - gap: var(--space-2); - font-size: 0.75rem; - color: var(--text-secondary); - margin-top: var(--space-1); -} -.rec-actions { - display: flex; - align-items: center; - gap: 0.375rem; - flex-shrink: 0; -} -.rec-footer { margin-top: var(--space-3); text-align: center; } -.rec-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: var(--space-8); - gap: var(--space-2); -} - -.music-body { - display: flex; - flex-direction: column; - gap: var(--space-2); -} - -/* ── Legacy aliases ──────────────────────────────────── */ -.live-panel { display: flex; flex-direction: column; gap: var(--space-6); } -.live-grid-3 { grid-template-columns: repeat(3, 1fr); } - -.recordings-sub-panel { overflow: hidden; } - -.audio-visualizer { display: flex; align-items: flex-end; justify-content: center; height: 80px; gap: 2px; padding: var(--space-2); } -.audio-visualizer-bars { display: flex; align-items: flex-end; gap: 2px; height: 100%; width: 100%; } -.audio-bar { width: 8px; background: var(--gradient-primary); border-radius: var(--radius-sm) var(--radius-sm) 0 0; transition: height 100ms ease; } - -.mic-level-meter { display: flex; flex-direction: column; gap: var(--space-2); } -.mic-row { display: flex; align-items: center; gap: var(--space-2); } - -@media (max-width: 1024px) { .live-grid-3 { grid-template-columns: repeat(2, 1fr); } } -@media (max-width: 768px) { .live-grid-3 { grid-template-columns: 1fr; } } diff --git a/services/frontend/frontend/src/styles/main.css b/services/frontend/frontend/src/styles/main.css deleted file mode 100644 index d37fc45..0000000 --- a/services/frontend/frontend/src/styles/main.css +++ /dev/null @@ -1,13 +0,0 @@ -/* ── IMPHNEN Design System — Main Import Hub ─────────── * - * Order is guaranteed by @import sequence. * - * ─────────────────────────────────────────────────────── */ - -@import './tokens.css'; -@import './reset.css'; -@import './utilities.css'; -@import './layout.css'; -@import './ui.css'; -@import './messages.css'; -@import './dashboard.css'; -@import './live.css'; -@import './polish.css'; diff --git a/services/frontend/frontend/src/styles/messages.css b/services/frontend/frontend/src/styles/messages.css deleted file mode 100644 index fce873f..0000000 --- a/services/frontend/frontend/src/styles/messages.css +++ /dev/null @@ -1,612 +0,0 @@ -/* ── Messages Panel ───────────────────────────────────── */ - -.messages-panel { display: flex; flex-direction: column; gap: var(--space-5); } - -/* ── Filter Bar ──────────────────────────────────────── */ -.filter-bar { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--space-3); -} - -.filter-bar-search { - position: relative; - flex: 1; - min-width: 200px; -} - -.filter-bar-search-icon { - position: absolute; - left: 0.75rem; - top: 50%; - transform: translateY(-50%); - color: var(--text-tertiary); - pointer-events: none; -} - -.filter-bar-input { - width: 100%; - padding: 0.5rem 0.75rem 0.5rem 2.25rem; - height: 36px; - background: var(--surface-container); - border: 1px solid transparent; - border-radius: var(--radius-pill); - color: var(--text-primary); - font-family: inherit; - font-size: 0.875rem; - transition: all var(--transition-fast); - outline: none; -} -.filter-bar-input::placeholder { color: var(--text-tertiary); } -.filter-bar-input:focus { - border-color: var(--color-primary); - box-shadow: 0 0 0 2px var(--color-primary-muted); - background: var(--surface-raised); -} - -.filter-bar-chips { - display: flex; - align-items: center; - gap: 0.375rem; - flex-wrap: wrap; -} - -.filter-chip { - padding: 0.25rem 0.75rem; - border-radius: var(--radius-pill); - font-size: 0.6875rem; - font-weight: 500; - border: 1px solid transparent; - background: var(--surface-hover); - color: var(--text-tertiary); - cursor: pointer; - transition: all var(--transition-fast); - font-family: inherit; -} -.filter-chip:hover { - color: var(--text-secondary); - background: var(--surface-container); -} -.filter-chip.is-active { - background: var(--gradient-primary); - color: white; -} - -.filter-bar-live-count { - font-size: 0.875rem; - color: var(--text-tertiary); - white-space: nowrap; -} - -/* ── Stats Bar ───────────────────────────────────────── */ -.stats-bar { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--space-1); -} - -/* ── Message Card ────────────────────────────────────── */ -.msg-card { - background: var(--surface-raised); - border: 1px solid var(--surface-border); - border-radius: var(--radius-md); - overflow: hidden; - transition: all var(--transition-fast); - box-shadow: var(--shadow-sm); -} -.msg-card:hover { - border-color: rgba(59, 130, 246, 0.20); - box-shadow: var(--shadow-md); - transform: translateY(-1px); -} -.msg-card.is-deleted { - opacity: 0.6; - border-color: var(--color-error); -} - -.msg-card-inner { - display: flex; - gap: var(--space-3); - padding: var(--space-4); -} - -.msg-card-avatar { - width: 40px; - height: 40px; - flex-shrink: 0; - border-radius: 50%; - object-fit: cover; -} - -.msg-card-body { min-width: 0; flex: 1; } - -.msg-card-meta { - display: flex; - align-items: baseline; - gap: var(--space-2); - margin-bottom: var(--space-2); - font-size: 0.75rem; - color: var(--text-secondary); - font-family: 'JetBrains Mono', monospace; -} - -.msg-card-username { - font-size: 0.8125rem; - font-weight: 600; - color: var(--text-primary); -} - -.msg-card-channel { - display: inline-flex; - align-items: center; - gap: var(--space-1); - font-size: 0.6875rem; - color: var(--text-tertiary); - background: var(--surface-container); - padding: 1px 8px; - border-radius: var(--radius-pill); -} -.msg-card-channel::before { content: '#'; opacity: 0.5; } - -.msg-card-time { - font-size: 0.6875rem; - color: var(--text-tertiary); - font-variant-numeric: tabular-nums; - margin-left: auto; -} - -/* ── AI Badge ────────────────────────────────────────── */ -.msg-card-ai-badge { - display: inline-flex; - align-items: center; - gap: 0.25rem; - padding: 2px 8px; - border-radius: 6px; - font-size: 0.625rem; - font-weight: 600; - margin-left: var(--space-1); -} -.msg-card-ai-badge.clean { - background: rgba(16, 185, 129, 0.10); - color: var(--color-ai-clean); -} -.msg-card-ai-badge.warn { - background: rgba(245, 158, 11, 0.10); - color: var(--color-ai-warn); -} -.msg-card-ai-badge.flagged { - background: rgba(239, 68, 68, 0.10); - color: var(--color-ai-flagged); -} -.msg-card-ai-badge.processing { - background: var(--color-primary-muted); - color: var(--color-ai-processing); -} -.msg-card-ai-badge.pending { - background: rgba(92, 92, 120, 0.10); - color: var(--color-ai-pending); -} - -/* ── Reply indicator (Discord-style reference block) ──── */ -.msg-row-reply { - display: flex; - align-items: stretch; - margin: 0.375rem 0 0.625rem; - border-radius: 6px; - overflow: hidden; - cursor: default; - transition: background 150ms ease; -} -.msg-row-reply:hover { - background: rgba(59, 130, 246, 0.04); -} -.msg-row-reply-line { - width: 3px; - flex-shrink: 0; - border-radius: 2px; - background: linear-gradient(180deg, #3b82f6, #8b5cf6); - opacity: 0.6; -} -.msg-row-reply-main { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.375rem 0.625rem; - min-width: 0; - flex: 1; - font-size: 0.75rem; - line-height: 1.4; -} -.msg-row-reply-avatar { - width: 20px; - height: 20px; - border-radius: 50%; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.625rem; - font-weight: 700; - color: white; - background: linear-gradient(135deg, #3b82f6, #6366f1); - text-transform: uppercase; - box-shadow: 0 1px 3px rgba(59, 130, 246, 0.25); -} -.msg-row-reply-label { - flex-shrink: 0; - color: var(--text-tertiary); - font-weight: 450; - opacity: 0.8; -} -.msg-row-reply-user { - flex-shrink: 0; - font-weight: 700; - color: #3b82f6; - letter-spacing: -0.01em; -} -.msg-row-reply-snippet { - color: var(--text-tertiary); - font-style: italic; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; - min-width: 0; - opacity: 0.9; - display: inline-flex; - align-items: center; - gap: 0; -} -.msg-row-reply-snippet::before { - content: '"'; - opacity: 0.5; - margin-right: 1px; -} -.msg-row-reply-snippet::after { - content: '"'; - opacity: 0.5; - margin-left: 1px; -} - -/* ── Message Content ─────────────────────────────────── */ -.msg-card-content { - font-size: 0.875rem; - line-height: 1.5; - white-space: pre-wrap; - word-break: break-word; - color: var(--text-primary); -} -.msg-card-content.is-deleted { - opacity: 0.6; - color: var(--text-secondary); -} - -.msg-card-messages.separated > * + * { - margin-top: 0.625rem; - padding-top: 0.625rem; - border-top: 1px solid var(--surface-border); -} - -/* ── Actions Bar ─────────────────────────────────────── */ -.msg-card-actions { - display: flex; - align-items: center; - gap: var(--space-2); - padding: var(--space-3) var(--space-4); - border-top: 1px solid var(--surface-border); - background: var(--surface-base); -} - -.msg-card-actions .btn-icon-sm { - color: var(--text-tertiary); -} -.msg-card-actions .btn-icon-sm:hover { - color: var(--color-primary); -} - -/* ── Embed ───────────────────────────────────────────── */ -.msg-embed { - margin-top: var(--space-2); - padding: var(--space-3); - border-left: 3px solid var(--color-primary); - border-radius: var(--radius-sm); - background: var(--surface-overlay); -} -.msg-embed-title { - font-weight: 600; - font-size: 0.875rem; - color: var(--text-primary); - margin-bottom: var(--space-1); -} -.msg-embed-description { - font-size: 0.8125rem; - color: var(--text-secondary); - line-height: 1.5; -} -.msg-embed-fields { - display: flex; - flex-direction: column; - gap: var(--space-1); - margin-top: var(--space-2); -} -.msg-embed-field { - display: flex; - flex-direction: column; -} -.msg-embed-field-name { - font-size: 0.75rem; - font-weight: 600; - color: var(--text-secondary); - margin-bottom: 0.125rem; -} -.msg-embed-field-value { - font-size: 0.8125rem; - color: var(--text-primary); -} -.msg-embed-footer { - margin-top: var(--space-2); - font-size: 0.6875rem; - color: var(--text-tertiary); -} - -/* ── Image Grid ──────────────────────────────────────── */ -.image-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); - gap: var(--space-2); -} -.image-grid-item { - aspect-ratio: 1; - border-radius: var(--radius-md); - overflow: hidden; - cursor: pointer; - transition: all var(--transition-fast); - border: 1px solid var(--surface-border); -} -.image-grid-item:hover { - opacity: 0.85; - transform: scale(1.02); -} -.image-grid-item img { - width: 100%; - height: 100%; - object-fit: cover; -} - -/* ── Message Media ───────────────────────────────────── */ -.msg-media-row { - display: flex; - flex-wrap: wrap; - gap: var(--space-2); - margin-top: var(--space-2); -} -.msg-media-row.is-scroll { - overflow-x: auto; - flex-wrap: nowrap; -} -.msg-thumb { - width: 64px; - height: 64px; - object-fit: cover; - border-radius: var(--radius-sm); - border: 1px solid var(--surface-border); - transition: all var(--transition-fast); -} -.msg-thumb:hover { transform: scale(1.08); } -.msg-thumb-link { - display: block; - flex-shrink: 0; - overflow: hidden; - border-radius: var(--radius-sm); - border: 1px solid var(--surface-border); - transition: all var(--transition-fast); -} -.msg-thumb-link:hover { border-color: var(--color-primary); } -.msg-sticker { - width: 48px; - height: 48px; - object-fit: contain; -} -.msg-video { - height: 112px; - width: 192px; - flex-shrink: 0; - border-radius: var(--radius-sm); - border: 1px solid var(--surface-border); - object-fit: cover; - background: #000; -} -.msg-media-overflow { - display: flex; - width: 64px; - height: 64px; - align-items: center; - justify-content: center; - border-radius: var(--radius-sm); - border: 1px solid var(--surface-border); - font-size: 0.75rem; - color: var(--text-secondary); - background: var(--surface-container); -} -.msg-media-overflow.tall { height: 112px; width: 64px; } - -/* ── Message Categories ──────────────────────────────── */ -.msg-cats { - display: flex; - flex-wrap: wrap; - gap: var(--space-1); - margin-top: var(--space-2); -} - -/* ── AI Analysis ─────────────────────────────────────── */ -.msg-analysis { - margin-top: var(--space-2); - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-sm); - font-size: 0.75rem; - border-left: 3px solid; -} -.msg-analysis.flagged { - background: rgba(239, 68, 68, 0.06); - border-color: var(--color-ai-flagged); -} -.msg-analysis.clean { - background: rgba(16, 185, 129, 0.06); - border-color: var(--color-ai-clean); -} -.msg-analysis-row { - display: flex; - align-items: flex-start; - gap: var(--space-2); -} -.msg-analysis-body { min-width: 0; flex: 1; } -.msg-analysis-summary { - display: block; - font-weight: 500; - margin-bottom: var(--space-1); -} -.msg-analysis-text { - font-size: 0.75rem; - line-height: 1.5; - white-space: pre-wrap; -} - -/* ── Message Error ───────────────────────────────────── */ -.msg-error { - margin-top: var(--space-2); - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-sm); - font-size: 0.75rem; - color: var(--color-warning); - background: rgba(245, 158, 11, 0.06); -} - -/* ── Message Skeleton ────────────────────────────────── */ -.msg-skel { - display: flex; - gap: var(--space-3); - padding: var(--space-4); -} -.msg-skel-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - background: linear-gradient(90deg, var(--surface-container) 25%, rgba(59, 130, 246, 0.04) 50%, var(--surface-container) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s ease-in-out infinite; - flex-shrink: 0; -} -.msg-skel-lines { - min-width: 0; - flex: 1; - display: flex; - flex-direction: column; - gap: var(--space-3); -} -.msg-skel-line { - height: 20px; - background: linear-gradient(90deg, var(--surface-container) 25%, rgba(59, 130, 246, 0.04) 50%, var(--surface-container) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s ease-in-out infinite; - border-radius: var(--radius-sm); -} -.msg-skel-badges { - display: flex; - gap: var(--space-2); -} -.msg-skel-badge { - height: 24px; - border-radius: var(--radius-pill); - background: linear-gradient(90deg, var(--surface-container) 25%, rgba(59, 130, 246, 0.04) 50%, var(--surface-container) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s ease-in-out infinite; -} - -/* ── Feed ────────────────────────────────────────────── */ -.feed-wrap { - display: flex; - flex-direction: column; - gap: var(--space-4); -} -.feed-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: var(--space-16) var(--space-4); - text-align: center; -} -.feed-empty-title { - font-size: 0.9375rem; - font-weight: 600; - color: var(--text-secondary); - margin-bottom: var(--space-2); -} -.feed-empty-desc { - font-size: 0.8125rem; - color: var(--text-tertiary); - max-width: 280px; - line-height: 1.5; -} -.feed-sentinel { height: var(--space-4); } -.feed-loader { margin-top: var(--space-4); text-align: center; } - -/* ── Misc ────────────────────────────────────────────── */ -.search-count { - font-size: 0.875rem; - color: var(--text-secondary); -} -.icon-spin { animation: spin 1s linear infinite; } -.custom-emoji { - display: inline-block; - height: 20px; - width: 20px; - vertical-align: middle; - object-fit: contain; -} - -/* ── Legacy aliases (components still use these) ────── */ -.panel-card { background: var(--surface-raised); border: 1px solid var(--surface-border); border-radius: var(--radius-md); overflow: hidden; box-shadow: var(--shadow-sm); transition: all var(--transition-normal); } -.panel-card-head { padding: var(--space-5) var(--space-6); border-bottom: 1px solid var(--surface-border); } -.panel-card-title { font-size: 1rem; font-weight: 600; color: var(--text-primary); } -.panel-card-desc { font-size: 0.875rem; color: var(--text-secondary); margin-top: var(--space-1); } - -.search-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); } -.search-wrap { position: relative; flex: 1; min-width: 200px; } -.search-icon { position: absolute; left: 0.75rem; top: 50%; transform: translateY(-50%); color: var(--text-tertiary); pointer-events: none; } -.search-input { width: 100%; padding: 0.5rem 0.75rem 0.5rem 2.25rem; height: 36px; background: var(--surface-container); border: 1px solid transparent; border-radius: var(--radius-pill); color: var(--text-primary); font-family: inherit; font-size: 0.875rem; transition: all var(--transition-fast); outline: none; } -.search-input::placeholder { color: var(--text-tertiary); } -.search-input:focus { border-color: var(--color-primary); box-shadow: 0 0 0 2px var(--color-primary-muted); background: var(--surface-raised); } - -.filter-group { display: flex; align-items: center; gap: 0.375rem; margin-left: auto; } -.filter-chip-v2 { padding: 0.25rem 0.75rem; border-radius: var(--radius-pill); font-size: 0.6875rem; font-weight: 500; border: 1px solid transparent; background: var(--surface-hover); color: var(--text-tertiary); cursor: pointer; transition: all var(--transition-fast); font-family: inherit; } -.filter-chip-v2:hover { color: var(--text-secondary); background: var(--surface-container); } -.filter-chip-v2.is-active { background: var(--gradient-primary); color: white; } - -.msg-row { padding: var(--space-3) 0; border-bottom: 1px solid var(--surface-border); transition: all var(--transition-fast); } -.msg-row:hover { background: var(--surface-hover); margin: 0 calc(-1 * var(--space-4)); padding-left: var(--space-4); padding-right: var(--space-4); border-radius: var(--radius-sm); } -.msg-row:last-child { border-bottom: none; } -.msg-row-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem 0.5rem; } -.msg-row-time { font-size: 0.6875rem; color: var(--text-tertiary); font-variant-numeric: tabular-nums; min-width: 48px; } -.msg-row-badge { display: inline-flex; align-items: center; gap: 0.125rem; font-size: 0.75rem; } -.msg-row-badge.edited { color: var(--text-secondary); } -.msg-row-badge.deleted { color: var(--color-error); } -.msg-row-status { margin-left: auto; display: flex; align-items: center; gap: var(--space-1); } -.msg-row-body { font-size: 0.875rem; line-height: 1.5rem; white-space: pre-wrap; word-break: break-word; } -.msg-row-body.is-deleted { opacity: 0.6; color: var(--text-secondary); } - -.msg-card-head { display: flex; align-items: baseline; gap: var(--space-2); margin-bottom: var(--space-2); } -.msg-card-name { font-size: 0.875rem; font-weight: 600; color: var(--text-primary); } -.msg-card-messages > * + * { margin-top: 0.625rem; padding-top: 0.625rem; border-top: 1px solid var(--surface-border); } -.msg-card-messages.separated > * + * { margin-top: 0.625rem; padding-top: 0.625rem; border-top: 1px solid var(--surface-border); } - -.msg-actions { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-2); } -.msg-retry-hint { font-size: 0.75rem; color: var(--text-tertiary); opacity: 0.7; } - -.msg-sticker-placeholder { display: flex; width: 48px; height: 48px; align-items: center; justify-content: center; border-radius: var(--radius-sm); border: 1px solid var(--surface-border); } - -.msg-analysis-icon { margin-top: 0.125rem; flex-shrink: 0; } - -.img-empty { display: flex; align-items: center; justify-content: center; height: 128px; color: var(--text-secondary); font-style: italic; } diff --git a/services/frontend/frontend/src/styles/polish.css b/services/frontend/frontend/src/styles/polish.css deleted file mode 100644 index 64095fc..0000000 --- a/services/frontend/frontend/src/styles/polish.css +++ /dev/null @@ -1,311 +0,0 @@ -/* ── Polish: Particles, Theme Toggle, Mascot ──────────── */ - -/* ── Particle Background ─────────────────────────────── */ -.particle-bg { - position: fixed; - inset: 0; - overflow: hidden; - pointer-events: none; - z-index: 0; -} -.particle-orb { - position: absolute; - border-radius: 50%; - will-change: transform, filter; -} -.particle-orb:nth-child(1) { - width: 600px; height: 600px; - top: -200px; right: -150px; - background: radial-gradient(circle, rgba(59, 130, 246, 0.5) 0%, transparent 70%); - filter: blur(100px); - animation: orb-drift-enhanced 30s ease-in-out infinite, - hue-rotate-slow 12s ease-in-out infinite, - orb-pulse 8s ease-in-out infinite; -} -.particle-orb:nth-child(2) { - width: 500px; height: 500px; - bottom: -150px; left: -150px; - background: radial-gradient(circle, rgba(99, 102, 241, 0.5) 0%, transparent 70%); - filter: blur(100px); - animation: orb-drift-enhanced 35s ease-in-out infinite reverse, - hue-rotate-slow 14s ease-in-out infinite reverse, - orb-pulse 10s ease-in-out infinite; - animation-delay: -5s; -} -.particle-orb:nth-child(3) { - width: 350px; height: 350px; - top: 40%; left: 60%; - background: radial-gradient(circle, rgba(99, 102, 241, 0.4) 0%, transparent 70%); - filter: blur(100px); - animation: orb-drift-enhanced 40s ease-in-out infinite, - hue-rotate-slow 16s ease-in-out infinite, - orb-pulse 6s ease-in-out infinite; - animation-delay: -10s; -} -.particle-orb:nth-child(4) { - width: 250px; height: 250px; - top: 10%; left: 20%; - background: radial-gradient(circle, rgba(16, 185, 129, 0.4) 0%, transparent 70%); - filter: blur(100px); - animation: orb-drift-enhanced 25s ease-in-out infinite reverse, - hue-rotate-slow 18s ease-in-out infinite, - orb-pulse 12s ease-in-out infinite; - animation-delay: -3s; -} - -.particle-orb:nth-child(5) { - width: 180px; height: 180px; - top: 70%; left: 30%; - background: radial-gradient(circle, rgba(59, 130, 246, 0.35) 0%, transparent 70%); - filter: blur(80px); - animation: orb-drift-enhanced 20s ease-in-out infinite, - hue-rotate-slow 10s ease-in-out infinite, - orb-pulse 5s ease-in-out infinite; - animation-delay: -7s; -} - -@media (max-width: 768px) { - .particle-bg { display: none; } - .app-shell::after { display: none; } -} - -/* ── Theme Toggle ────────────────────────────────────── */ -.theme-toggle-btn { - width: 32px; - height: 32px; - border: 1px solid var(--surface-border); - border-radius: var(--radius-sm); - background: var(--surface-overlay); - color: var(--text-tertiary); - cursor: pointer; - font-size: 0.875rem; - display: flex; - align-items: center; - justify-content: center; - transition: all var(--transition-fast); - flex-shrink: 0; - -webkit-tap-highlight-color: transparent; -} -.theme-toggle-btn:hover { - color: var(--color-primary); - border-color: var(--color-primary); -} - -/* ── Mascot Chatbot ──────────────────────────────────── */ -.mascot-widget { - position: fixed; - right: var(--space-6); - bottom: var(--space-6); - z-index: var(--z-toast); -} -.mascot-launcher { - width: 3.75rem; - height: 3.75rem; - border: none; - border-radius: 50%; - background: var(--gradient-primary); - color: white; - box-shadow: var(--shadow-lg), 0 0 24px rgba(59, 130, 246, 0.25); - cursor: pointer; - font-size: 1.5rem; - transition: all var(--transition-fast); - animation: float 4s ease-in-out infinite; -} -.mascot-launcher:hover { - transform: translateY(-3px) scale(1.05); - animation: none; -} -.mascot-panel { - width: min(24rem, calc(100vw - 2rem)); - height: min(32.5rem, calc(100vh - 7rem)); - display: flex; - flex-direction: column; - overflow: hidden; - border: 1px solid var(--surface-border); - border-radius: var(--radius-lg); - background: var(--surface-raised); - box-shadow: var(--shadow-lg); - animation: fade-in-up var(--transition-bounce); -} -.mascot-panel.minimized { height: 3.75rem; } -.mascot-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-3); - padding: var(--space-3); - background: var(--gradient-primary); - color: white; - flex-shrink: 0; -} -.mascot-header-icon { - display: flex; - align-items: center; - justify-content: center; - width: 2rem; - height: 2rem; - border-radius: var(--radius-sm); - background: rgba(255,255,255,0.15); -} -.mascot-title { - font-size: 0.875rem; - font-weight: 700; - line-height: 1.15; - letter-spacing: 0.02em; -} -.mascot-subtitle { margin-top: 0.125rem; font-size: 0.6875rem; opacity: 0.7; } -.mascot-icon-button { - width: 2rem; - height: 2rem; - border: none; - border-radius: var(--radius-sm); - background: transparent; - color: white; - cursor: pointer; - transition: background var(--transition-fast); -} -.mascot-icon-button:hover { background: rgba(255,255,255,0.20); } -.mascot-messages { - flex: 1; - min-height: 0; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: var(--space-3); - padding: var(--space-4); -} -.mascot-message-row { - display: flex; - gap: var(--space-2); - animation: fade-in-up var(--transition-fast) both; -} -.mascot-message-row.user { justify-content: flex-end; } -.mascot-avatar { - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - width: 1.5rem; - height: 1.5rem; - border-radius: 50%; - background: var(--surface-container); - font-size: 0.875rem; -} -.mascot-bubble { - max-width: 17.5rem; - padding: var(--space-2) var(--space-3); - border-radius: var(--radius-lg); - font-size: 0.875rem; - line-height: 1.5; - overflow-wrap: anywhere; -} -.mascot-bubble.user { - color: white; - background: var(--gradient-primary); - border-bottom-right-radius: var(--radius-sm); -} -.mascot-bubble.mascot { - color: var(--text-primary); - background: var(--surface-container); - border: 1px solid var(--surface-border); - border-bottom-left-radius: var(--radius-sm); -} -.mascot-bubble.typing { - display: flex; - gap: 0.25rem; - padding-top: 0.7rem; - padding-bottom: 0.7rem; -} -.mascot-bubble.typing span { - width: 0.5rem; - height: 0.5rem; - border-radius: 50%; - background: var(--text-tertiary); - animation: notification-pulse 0.8s infinite; -} -.mascot-bubble.typing span:nth-child(2) { animation-delay: 0.1s; } -.mascot-bubble.typing span:nth-child(3) { animation-delay: 0.2s; } -@keyframes notification-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.3; } -} -.mascot-form { - display: flex; - gap: var(--space-2); - padding: var(--space-3); - border-top: 1px solid var(--surface-border); -} -.mascot-input { - flex: 1; - min-width: 0; - border: 1.5px solid transparent; - border-radius: var(--radius-sm); - padding: var(--space-2) var(--space-3); - background: var(--surface-container); - color: var(--text-primary); - font-size: 0.875rem; - transition: all var(--transition-fast); -} -.mascot-input:focus { - outline: none; - border-color: var(--color-primary); - box-shadow: 0 0 0 2px var(--color-primary-muted); - background: var(--surface-raised); -} -.mascot-send { - width: 2.25rem; - border: none; - border-radius: var(--radius-sm); - background: var(--gradient-primary); - color: white; - cursor: pointer; - transition: all var(--transition-fast); -} -.mascot-send:hover { box-shadow: 0 0 12px rgba(59, 130, 246, 0.25); } -.mascot-send:disabled, .mascot-input:disabled { cursor: not-allowed; opacity: 0.55; } - -/* ── Float animation ─────────────────────────────────── */ -@keyframes float { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-8px); } -} - -/* ── Auth ────────────────────────────────────────────── */ -.auth-box { width: 400px; text-align: center; } -.auth-lock { font-size: 3rem; margin-bottom: var(--space-4); } -.auth-title { - font-size: 1.375rem; - font-weight: 700; - margin-bottom: var(--space-2); - background: var(--gradient-brand); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} -.auth-desc { - font-size: 0.875rem; - color: var(--text-secondary); - margin-bottom: var(--space-6); -} -.auth-form { display: flex; flex-direction: column; gap: var(--space-3); } -.auth-error { - font-size: 0.75rem; - color: var(--color-error); - padding: var(--space-2) var(--space-3); - background: rgba(239, 68, 68, 0.08); - border-radius: var(--radius-sm); -} -.auth-close-btn { - position: absolute; - top: 0; - right: 0; - background: none; - border: none; - color: var(--text-tertiary); - font-size: 1.25rem; - cursor: pointer; - padding: 0.25rem; - line-height: 1; - transition: color var(--transition-fast); -} -.auth-close-btn:hover { color: var(--text-primary); } diff --git a/services/frontend/frontend/src/styles/reset.css b/services/frontend/frontend/src/styles/reset.css deleted file mode 100644 index 0a5f669..0000000 --- a/services/frontend/frontend/src/styles/reset.css +++ /dev/null @@ -1,48 +0,0 @@ -/* ── Reset & Base ─────────────────────────────────────── */ - -*, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - font-size: 16px; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - line-height: 1.6; - min-height: 100vh; - overflow-x: hidden; - background: var(--surface-base); - color: var(--text-primary); -} - -a { - color: var(--color-primary); - text-decoration: none; - transition: color var(--transition-fast); -} -a:hover { color: var(--color-primary-hover); } - -img { max-width: 100%; height: auto; } -svg { display: inline-block; vertical-align: middle; } - -/* ── Scrollbar ────────────────────────────────────────── */ -::-webkit-scrollbar { width: 6px; height: 6px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { - background: var(--color-primary); - border-radius: var(--radius-pill); -} - -::selection { - background: rgba(59, 130, 246, 0.25); - color: var(--text-primary); -} -[data-theme="light"] ::selection { - background: rgba(37, 99, 235, 0.25); -} diff --git a/services/frontend/frontend/src/styles/tokens.css b/services/frontend/frontend/src/styles/tokens.css deleted file mode 100644 index 73f7a3a..0000000 --- a/services/frontend/frontend/src/styles/tokens.css +++ /dev/null @@ -1,128 +0,0 @@ -/* ── Design Tokens — IMPHNEN Neuform Dark ────────────── * - * Dark default (no attribute selector) * - * Light: [data-theme="light"] * - * ──────────────────────────────────────────────────────── */ - -:root { - /* ── Surfaces ──────────────────────────────────── */ - --surface-base: #050510; - --surface-raised: #0a0a1a; - --surface-overlay: #12122a; - --surface-container: #1a1a35; - --surface-border: rgba(255, 255, 255, 0.06); - --surface-hover: rgba(59, 130, 246, 0.06); - --surface-glass: rgba(5, 5, 16, 0.78); - - /* ── Text ──────────────────────────────────────── */ - --text-primary: #f1f1f9; - --text-secondary: #9d9db5; - --text-tertiary: #5c5c78; - --text-inverse: #050510; - - /* ── Brand ─────────────────────────────────────── */ - --color-primary: #3b82f6; - --color-primary-hover: #60a5fa; - --color-primary-active: #2563eb; - --color-primary-muted: rgba(59, 130, 246, 0.12); - --gradient-primary: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%); - --gradient-brand: linear-gradient(135deg, #3b82f6 0%, #5865f2 50%, #6366f1 100%); - - /* ── Semantics ─────────────────────────────────── */ - --color-success: #10b981; - --color-warning: #f59e0b; - --color-error: #ef4444; - --color-info: #3b82f6; - --color-destructive: #ef4444; - - /* ── AI Status ─────────────────────────────────── */ - --color-ai-flagged: #ef4444; - --color-ai-clean: #10b981; - --color-ai-warn: #f59e0b; - --color-ai-pending: #5c5c78; - --color-ai-processing: #3b82f6; - --color-ai-error: #dc2626; - --color-ai-deleted: #6b7280; - - /* ── Shadows ───────────────────────────────────── */ - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35); - --shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.4); - - /* ── Radii ─────────────────────────────────────── */ - --radius-sm: 8px; - --radius-md: 12px; - --radius-lg: 16px; - --radius-xl: 23px; - --radius-pill: 9999px; - - /* ── Spacing ───────────────────────────────────── */ - --space-0: 0px; - --space-1: 4px; - --space-2: 8px; - --space-3: 12px; - --space-4: 16px; - --space-5: 20px; - --space-6: 24px; - --space-8: 32px; - --space-10: 40px; - --space-12: 48px; - --space-16: 64px; - - /* ── Layout ────────────────────────────────────── */ - --sidebar-width: 240px; - --header-height: 0px; - - /* ── Transitions ───────────────────────────────── */ - --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-normal: 300ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-slow: 500ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-bounce: 400ms cubic-bezier(0.34, 1.56, 0.64, 1); - - /* ── Z-index ───────────────────────────────────── */ - --z-sidebar: 30; - --z-header: 40; - --z-overlay: 50; - --z-modal: 60; - --z-toast: 70; -} - -/* ── Light Theme ──────────────────────────────────────── */ -[data-theme="light"] { - --surface-base: #f4f6fb; - --surface-raised: #ffffff; - --surface-overlay: #eeeff4; - --surface-container: #dde0e8; - --surface-border: rgba(0, 0, 0, 0.06); - --surface-hover: rgba(37, 99, 235, 0.05); - --surface-glass: rgba(255, 255, 255, 0.72); - - --text-primary: #0f172a; - --text-secondary: #475569; - --text-tertiary: #94a3b8; - --text-inverse: #ffffff; - - --color-primary: #2563eb; - --color-primary-hover: #3b82f6; - --color-primary-active: #1d4ed8; - --color-primary-muted: rgba(37, 99, 235, 0.1); - --gradient-primary: linear-gradient(135deg, #2563eb 0%, #6366f1 100%); - --gradient-brand: linear-gradient(135deg, #2563eb 0%, #5865f2 50%, #6366f1 100%); - - --color-success: #10b981; - --color-warning: #f59e0b; - --color-error: #ef4444; - --color-info: #2563eb; - --color-destructive: #ef4444; - - --color-ai-flagged: #ef4444; - --color-ai-clean: #10b981; - --color-ai-warn: #f59e0b; - --color-ai-pending: #94a3b8; - --color-ai-processing: #2563eb; - --color-ai-error: #dc2626; - --color-ai-deleted: #6b7280; - - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06); - --shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.08); -} diff --git a/services/frontend/frontend/src/styles/ui.css b/services/frontend/frontend/src/styles/ui.css deleted file mode 100644 index 4130ca8..0000000 --- a/services/frontend/frontend/src/styles/ui.css +++ /dev/null @@ -1,411 +0,0 @@ -/* ── UI Primitives ────────────────────────────────────── */ - -/* ── Button ──────────────────────────────────────────── */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--space-2); - padding: 12px 24px; - height: 44px; - border: 1px solid transparent; - border-radius: var(--radius-pill); - font-family: inherit; - font-size: 14px; - font-weight: 600; - line-height: 20px; - letter-spacing: 0.02em; - cursor: pointer; - transition: all var(--transition-fast); - white-space: nowrap; - user-select: none; - -webkit-tap-highlight-color: transparent; - text-decoration: none; -} -.btn:active:not(:disabled) { transform: scale(0.97); } -.btn:disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; } - -.btn-primary { - background: var(--gradient-primary); - color: white; - border: none; -} -.btn-primary:hover:not(:disabled) { - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.25); - transform: translateY(-1px); -} - -.btn-secondary { - background: transparent; - color: var(--color-primary); - border: 1.5px solid var(--color-primary); -} -.btn-secondary:hover:not(:disabled) { - background: var(--color-primary-muted); - transform: translateY(-1px); -} - -.btn-destructive { - background: var(--color-error); - color: white; - border: none; -} -.btn-destructive:hover:not(:disabled) { - filter: brightness(1.1); - transform: translateY(-1px); -} - -.btn-outline { - background: var(--surface-glass); - border: 1px solid var(--surface-border); - color: var(--text-secondary); -} -.btn-outline:hover:not(:disabled) { - background: var(--surface-raised); - border-color: var(--color-primary); - color: var(--color-primary); -} - -.btn-ghost { - background: transparent; - color: var(--text-secondary); - border: none; - height: auto; - padding: 0.5rem 0.75rem; - border-radius: var(--radius-md); -} -.btn-ghost:hover:not(:disabled) { - background: var(--surface-hover); - color: var(--text-primary); -} - -.btn-link { - background: transparent; - color: var(--color-primary); - border: none; - height: auto; - padding: 0; - font-weight: 500; -} -.btn-link:hover:not(:disabled) { text-decoration: underline; } - -/* Sizes */ -.btn-sm { padding: 8px 16px; height: 36px; font-size: 12px; border-radius: var(--radius-md); } -.btn-lg { padding: 14px 28px; height: 48px; font-size: 16px; } -.btn-icon { width: 44px; height: 44px; padding: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 1.25rem; border-radius: var(--radius-pill); } -.btn-icon-sm { width: 36px; height: 36px; padding: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 1rem; border-radius: var(--radius-md); } - -/* ── Card ────────────────────────────────────────────── */ -.card { - background: var(--surface-raised); - border: 1px solid var(--surface-border); - border-radius: var(--radius-md); - overflow: hidden; - box-shadow: var(--shadow-sm); - transition: all var(--transition-normal); -} -.card:hover { - box-shadow: var(--shadow-md); -} -.card-interactive:hover { - border-color: var(--color-primary); - box-shadow: var(--shadow-md); - transform: translateY(-1px); -} - -.card-header { - padding: var(--space-4) var(--space-6); - border-bottom: 1px solid var(--surface-border); -} -.card-title { - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); -} -.card-description { - font-size: 0.875rem; - color: var(--text-secondary); - margin-top: var(--space-1); -} -.card-content { padding: var(--space-6); } -.card-footer { - padding: var(--space-4) var(--space-6); - border-top: 1px solid var(--surface-border); -} - -/* ── Badge ───────────────────────────────────────────── */ -.badge { - display: inline-flex; - align-items: center; - padding: 4px 8px; - border-radius: var(--radius-sm); - font-size: 12px; - font-weight: 500; - line-height: 16px; - letter-spacing: 0.03em; - background: var(--surface-container); - color: var(--text-secondary); - border: none; - transition: all var(--transition-fast); -} -.badge-primary { background: var(--color-primary-muted); color: var(--color-primary); } -.badge-success { background: rgba(16, 185, 129, 0.12); color: var(--color-success); } -.badge-warning { background: rgba(245, 158, 11, 0.12); color: var(--color-warning); } -.badge-destructive { background: rgba(239, 68, 68, 0.12); color: var(--color-error); } -.badge-outline { background: transparent; border: 1px solid var(--surface-border); color: var(--text-tertiary); } -.badge-info { background: var(--color-primary-muted); color: var(--color-primary); } - -/* ── Status Badge ────────────────────────────────────── */ -.status-badge { - display: inline-flex; - align-items: center; - gap: var(--space-1); - padding: 3px 10px; - border-radius: var(--radius-pill); - font-size: 11px; - font-weight: 600; - letter-spacing: 0.02em; - transition: all var(--transition-fast); -} -.status-badge::before { - content: ''; - width: 6px; - height: 6px; - border-radius: 50%; - flex-shrink: 0; -} -.status-badge-flagged { background: rgba(239, 68, 68, 0.12); color: var(--color-ai-flagged); } -.status-badge-flagged::before { background: var(--color-ai-flagged); box-shadow: 0 0 8px rgba(239, 68, 68, 0.6); } -.status-badge-clean { background: rgba(16, 185, 129, 0.12); color: var(--color-ai-clean); } -.status-badge-clean::before { background: var(--color-ai-clean); box-shadow: 0 0 8px rgba(16, 185, 129, 0.4); } -.status-badge-warn { background: rgba(245, 158, 11, 0.12); color: var(--color-ai-warn); } -.status-badge-warn::before { background: var(--color-ai-warn); box-shadow: 0 0 8px rgba(245, 158, 11, 0.4); } -.status-badge-pending { background: rgba(92, 92, 120, 0.12); color: var(--color-ai-pending); } -.status-badge-pending::before { background: var(--color-ai-pending); } -.status-badge-processing { background: var(--color-primary-muted); color: var(--color-ai-processing); } -.status-badge-processing::before { background: var(--color-ai-processing); animation: pulse-dot 1.5s ease-in-out infinite; } -.status-badge-error { background: rgba(220, 38, 38, 0.12); color: var(--color-ai-error); } -.status-badge-error::before { background: var(--color-ai-error); box-shadow: 0 0 8px rgba(220, 38, 38, 0.4); } - -@keyframes pulse-dot { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.3; } -} - -/* ── Input ───────────────────────────────────────────── */ -.input { - display: block; - width: 100%; - padding: var(--space-3); - background: var(--surface-container); - border: 1.5px solid transparent; - border-radius: var(--radius-sm); - color: var(--text-primary); - font-family: inherit; - font-size: 16px; - font-weight: 400; - line-height: 24px; - transition: all var(--transition-fast); - outline: none; -} -.input::placeholder { color: var(--text-tertiary); } -.input:focus { - border-color: var(--color-primary); - box-shadow: 0 0 0 2px var(--color-primary-muted), 0 0 16px rgba(59, 130, 246, 0.08); - background: var(--surface-raised); -} -.input[aria-invalid="true"] { - border-color: var(--color-error); - box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.12); -} - -/* ── Select ──────────────────────────────────────────── */ -.select { - display: block; - width: 100%; - padding: var(--space-3) 2rem var(--space-3) var(--space-3); - background: var(--surface-container); - border: 1.5px solid transparent; - border-radius: var(--radius-sm); - color: var(--text-primary); - font-family: inherit; - font-size: 0.875rem; - cursor: pointer; - transition: all var(--transition-fast); - outline: none; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%235c5c78' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 0.75rem center; -} -.select:focus { - border-color: var(--color-primary); - box-shadow: 0 0 0 2px var(--color-primary-muted); - background: var(--surface-raised); -} - -/* ── Skeleton ────────────────────────────────────────── */ -.skeleton { - background: linear-gradient(110deg, var(--surface-container) 30%, rgba(59, 130, 246, 0.04) 50%, var(--surface-container) 70%); - background-size: 200% 100%; - animation: shimmer 1.8s ease-in-out infinite; - border-radius: var(--radius-sm); -} -.skeleton-circular { border-radius: 50%; } -@keyframes shimmer { - 0% { background-position: 200% 0; } - 100% { background-position: -200% 0; } -} - -/* ── Empty State ─────────────────────────────────────── */ -.empty-state { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: var(--space-16) var(--space-4); - text-align: center; -} -.empty-state-title { - font-size: 0.9375rem; - font-weight: 600; - color: var(--text-secondary); - margin-bottom: var(--space-2); -} -.empty-state-description { - font-size: 0.8125rem; - color: var(--text-tertiary); - max-width: 280px; - line-height: 1.5; -} - -/* ── Tabs ────────────────────────────────────────────── */ -.tabs { display: flex; flex-direction: column; } -.tab-list { - display: flex; - gap: var(--space-2); - border-bottom: 2px solid var(--surface-border); -} -.tab-trigger { - padding: 8px 16px; - background: transparent; - border: none; - border-bottom: 2px solid transparent; - margin-bottom: -2px; - color: var(--text-secondary); - font-family: inherit; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all var(--transition-fast); -} -.tab-trigger:hover { color: var(--text-primary); } -.tab-trigger.active, -.tab-trigger[aria-selected="true"] { - color: var(--color-primary); - font-weight: 600; - border-bottom-color: var(--color-primary); -} -.tab-content { - padding-top: var(--space-4); - animation: fade-in-up var(--transition-normal) ease-out; -} - -/* ── Modal ───────────────────────────────────────────── */ -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.55); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - display: flex; - align-items: center; - justify-content: center; - z-index: var(--z-modal); - animation: fade-in var(--transition-fast) ease-out; -} -.modal-content { - background: var(--surface-raised); - border-radius: var(--radius-lg); - border: 1px solid var(--surface-border); - box-shadow: var(--shadow-lg); - max-width: 90vw; - max-height: 85vh; - overflow: auto; - animation: scale-in var(--transition-bounce); -} -.modal-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--space-4) var(--space-6); - border-bottom: 1px solid var(--surface-border); -} -.modal-body { padding: var(--space-6); } -.modal-footer { - display: flex; - justify-content: flex-end; - gap: var(--space-2); - padding: var(--space-4) var(--space-6); - border-top: 1px solid var(--surface-border); -} - -/* ── Toast ───────────────────────────────────────────── */ -.toast-container { - position: fixed; - bottom: var(--space-4); - right: var(--space-4); - z-index: var(--z-toast); - display: flex; - flex-direction: column; - gap: var(--space-2); - pointer-events: none; -} -.toast { - display: flex; - align-items: center; - gap: var(--space-3); - padding: 0.75rem 1rem; - background: var(--surface-raised); - border: 1px solid var(--surface-border); - border-radius: var(--radius-md); - box-shadow: var(--shadow-lg); - min-width: 300px; - max-width: 420px; - pointer-events: auto; - animation: slide-in-right var(--transition-bounce); -} -.toast-success { border-left: 3px solid var(--color-success); } -.toast-error { border-left: 3px solid var(--color-error); } -.toast-warning { border-left: 3px solid var(--color-warning); } -.toast-info { border-left: 3px solid var(--color-primary); } -.toast-close { - margin-left: auto; - background: none; - border: none; - color: var(--text-tertiary); - cursor: pointer; - padding: 0.25rem; - transition: color var(--transition-fast); -} -.toast-close:hover { color: var(--text-primary); } - -/* ── Animations ──────────────────────────────────────── */ -@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } -@keyframes fade-in-up { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } -@keyframes scale-in { from { opacity: 0; transform: scale(0.92); } to { opacity: 1; transform: scale(1); } } -@keyframes slide-in-right { from { opacity: 0; transform: translateX(120%); } to { opacity: 1; transform: translateX(0); } } - -.animate-fade-in { animation: fade-in var(--transition-normal) ease-out; } -.animate-fade-in-up { animation: fade-in-up var(--transition-normal) ease-out; } -.animate-scale-in { animation: scale-in var(--transition-normal) ease-out; } -.animate-slide-in-right { animation: slide-in-right var(--transition-normal) ease-out; } - -/* Reduced motion */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} diff --git a/services/frontend/frontend/src/styles/utilities.css b/services/frontend/frontend/src/styles/utilities.css deleted file mode 100644 index ec02ea9..0000000 --- a/services/frontend/frontend/src/styles/utilities.css +++ /dev/null @@ -1,273 +0,0 @@ -/* ── Utility Classes ──────────────────────────────────── */ - -/* Layout */ -.flex { display: flex; } -.inline-flex { display: inline-flex; } -.grid { display: grid; } -.block { display: block; } -.hidden { display: none; } -.flex-col { flex-direction: column; } -.flex-row { flex-direction: row; } -.flex-wrap { flex-wrap: wrap; } -.flex-1 { flex: 1 1 0%; } -.flex-shrink-0, .shrink-0 { flex-shrink: 0; } -.items-center { align-items: center; } -.items-start { align-items: flex-start; } -.items-end { align-items: flex-end; } -.items-baseline { align-items: baseline; } -.justify-center { justify-content: center; } -.justify-between { justify-content: space-between; } -.justify-end { justify-content: flex-end; } -.gap-0 { gap: 0; } -.gap-1 { gap: var(--space-1); } -.gap-1\.5 { gap: 6px; } -.gap-2 { gap: var(--space-2); } -.gap-3 { gap: var(--space-3); } -.gap-4 { gap: var(--space-4); } -.gap-6 { gap: var(--space-6); } -.gap-8 { gap: var(--space-8); } -.grid-cols-2 { grid-template-columns: repeat(2, 1fr); } -.grid-cols-3 { grid-template-columns: repeat(3, 1fr); } - -/* Width / Height */ -.w-full { width: 100%; } -.w-auto { width: auto; } -.h-full { height: 100%; } -.h-auto { height: auto; } -.min-w-0 { min-width: 0; } -.min-h-0 { min-height: 0; } - -/* Sizing helpers */ -.h-3 { height: 12px; } -.h-4 { height: 16px; } -.h-5 { height: 20px; } -.h-6 { height: 24px; } -.h-8 { height: 32px; } -.h-10 { height: 40px; } -.h-12 { height: 48px; } -.h-16 { height: 64px; } -.h-28 { height: 112px; } -.w-3 { width: 12px; } -.w-4 { width: 16px; } -.w-5 { width: 20px; } -.w-6 { width: 24px; } -.w-8 { width: 32px; } -.w-10 { width: 40px; } -.w-12 { width: 48px; } -.w-16 { width: 64px; } -.w-48 { width: 192px; } - -/* Position */ -.relative { position: relative; } -.absolute { position: absolute; } -.fixed { position: fixed; } -.sticky { position: sticky; } -.inset-0 { inset: 0; } -.top-0 { top: 0; } -.right-0 { right: 0; } -.bottom-0 { bottom: 0; } -.left-0 { left: 0; } - -/* Overflow */ -.overflow-auto { overflow: auto; } -.overflow-hidden { overflow: hidden; } -.overflow-y-auto { overflow-y: auto; } -.overflow-x-auto { overflow-x: auto; } - -/* Z-index */ -.z-0 { z-index: 0; } -.z-10 { z-index: 10; } -.z-50 { z-index: 50; } - -/* Margin */ -.m-0 { margin: 0; } -.mx-auto { margin-left: auto; margin-right: auto; } -.ml-auto { margin-left: auto; } -.mr-auto { margin-right: auto; } -.mt-0 { margin-top: 0; } -.mt-1 { margin-top: var(--space-1); } -.mt-2 { margin-top: var(--space-2); } -.mt-3 { margin-top: var(--space-3); } -.mt-4 { margin-top: var(--space-4); } -.mt-6 { margin-top: var(--space-6); } -.mb-0 { margin-bottom: 0; } -.mb-1 { margin-bottom: var(--space-1); } -.mb-2 { margin-bottom: var(--space-2); } -.mb-3 { margin-bottom: var(--space-3); } -.mb-4 { margin-bottom: var(--space-4); } -.mb-6 { margin-bottom: var(--space-6); } -.ml-0 { margin-left: 0; } -.ml-1 { margin-left: var(--space-1); } -.ml-2 { margin-left: var(--space-2); } -.mr-1 { margin-right: var(--space-1); } -.mr-2 { margin-right: var(--space-2); } -.mr-1\.5 { margin-right: 6px; } - -/* Padding */ -.p-0 { padding: 0; } -.p-1 { padding: var(--space-1); } -.p-2 { padding: var(--space-2); } -.p-3 { padding: var(--space-3); } -.p-4 { padding: var(--space-4); } -.p-5 { padding: var(--space-5); } -.p-6 { padding: var(--space-6); } -.px-1 { padding-left: var(--space-1); padding-right: var(--space-1); } -.px-2 { padding-left: var(--space-2); padding-right: var(--space-2); } -.px-3 { padding-left: var(--space-3); padding-right: var(--space-3); } -.px-4 { padding-left: var(--space-4); padding-right: var(--space-4); } -.px-6 { padding-left: var(--space-6); padding-right: var(--space-6); } -.py-0 { padding-top: 0; padding-bottom: 0; } -.py-1 { padding-top: var(--space-1); padding-bottom: var(--space-1); } -.py-2 { padding-top: var(--space-2); padding-bottom: var(--space-2); } -.py-3 { padding-top: var(--space-3); padding-bottom: var(--space-3); } -.py-4 { padding-top: var(--space-4); padding-bottom: var(--space-4); } -.pt-2 { padding-top: var(--space-2); } -.pt-3 { padding-top: var(--space-3); } -.pt-4 { padding-top: var(--space-4); } -.pb-2 { padding-bottom: var(--space-2); } -.pb-3 { padding-bottom: var(--space-3); } -.pb-4 { padding-bottom: var(--space-4); } -.pl-2 { padding-left: var(--space-2); } -.pr-2 { padding-right: var(--space-2); } - -/* Border */ -.border { border: 1px solid var(--surface-border); } -.border-0 { border: none; } -.border-t { border-top: 1px solid var(--surface-border); } -.border-b { border-bottom: 1px solid var(--surface-border); } -.border-l { border-left: 1px solid var(--surface-border); } -.border-r { border-right: 1px solid var(--surface-border); } -.border-l-3 { border-left-width: 3px; } -.border-destructive { border-color: var(--color-error); } -.border-border { border-color: var(--surface-border); } -.border-primary { border-color: var(--color-primary); } -.rounded-sm { border-radius: var(--radius-sm); } -.rounded { border-radius: var(--radius-md); } -.rounded-md { border-radius: var(--radius-md); } -.rounded-lg { border-radius: var(--radius-lg); } -.rounded-xl { border-radius: var(--radius-xl); } -.rounded-full { border-radius: var(--radius-pill); } - -/* Typography */ -.text-left { text-align: left; } -.text-center { text-align: center; } -.text-right { text-align: right; } -.whitespace-nowrap { white-space: nowrap; } -.whitespace-pre-wrap { white-space: pre-wrap; } -.break-words { word-break: break-word; } -.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.text-xs { font-size: 0.75rem; line-height: 1rem; } -.text-sm { font-size: 0.875rem; line-height: 1.25rem; } -.text-base { font-size: 1rem; line-height: 1.5rem; } -.text-lg { font-size: 1.125rem; line-height: 1.75rem; } -.text-xl { font-size: 1.25rem; line-height: 1.75rem; } -.text-2xl { font-size: 1.5rem; line-height: 2rem; } -.text-3xl { font-size: 1.875rem; line-height: 2.25rem; } -.font-normal { font-weight: 400; } -.font-medium { font-weight: 500; } -.font-semibold { font-weight: 600; } -.font-bold { font-weight: 700; } -.font-extrabold { font-weight: 800; } -.text-primary { color: var(--text-primary); } -.text-secondary { color: var(--text-secondary); } -.text-tertiary { color: var(--text-tertiary); } -.text-primary-color { color: var(--color-primary); } -.text-error { color: var(--color-error); } -.text-success { color: var(--color-success); } -.text-warning { color: var(--color-warning); } -.text-inverse { color: var(--text-inverse); } - -/* Background */ -.bg-surface { background: var(--surface-raised); } -.bg-overlay { background: var(--surface-overlay); } -.bg-base { background: var(--surface-base); } -.bg-primary { background: var(--color-primary); } -.bg-transparent { background: transparent; } - -/* Opacity / Misc */ -.opacity-0 { opacity: 0; } -.opacity-50 { opacity: 0.5; } -.opacity-60 { opacity: 0.6; } -.opacity-70 { opacity: 0.7; } -.opacity-80 { opacity: 0.8; } -.opacity-85 { opacity: 0.85; } -.object-contain { object-fit: contain; } -.object-cover { object-fit: cover; } -.cursor-pointer { cursor: pointer; } -.cursor-default { cursor: default; } -.cursor-not-allowed { cursor: not-allowed; } -.select-none { user-select: none; } -.pointer-events-none { pointer-events: none; } -.pointer-events-auto { pointer-events: auto; } - -/* Transitions */ -.transition-all { transition: all var(--transition-fast); } -.transition-transform { transition: transform var(--transition-fast); } -.transition-opacity { transition: opacity var(--transition-fast); } -.transition-colors { transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast); } -.transition-shadow { transition: box-shadow var(--transition-fast); } -.hover\:scale-105:hover { transform: scale(1.05); } -.hover\:opacity-80:hover { opacity: 0.8; } - -/* Animation */ -.animate-spin { animation: spin 1s linear infinite; } -@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } - -/* Responsive */ -@media (max-width: 768px) { - .grid-cols-2 { grid-template-columns: 1fr; } - .grid-cols-3 { grid-template-columns: 1fr; } -} - -/* ── Extra Utilities (component-used aliases) ────────── */ -.active { background: var(--gradient-primary); color: white; } -.is-active { background: var(--gradient-primary); color: white; } -.is-deleted { opacity: 0.6; } -.is-connecting { animation: pulse-connecting 1s ease-in-out infinite; } -.is-scroll { overflow-x: auto; flex-wrap: nowrap; } -.separated > * + * { margin-top: 0.625rem; padding-top: 0.625rem; border-top: 1px solid var(--surface-border); } -.tall { height: 112px; width: 64px; } -.typing { animation: pulse-dot 1.5s ease-in-out infinite; } -.mascot { font-variant-numeric: tabular-nums; } - -.ml-0\.5 { margin-left: 2px; } -.space-y-2 > * + * { margin-top: var(--space-2); } -.tracking-tight { letter-spacing: -0.025em; } -.text-foreground { color: var(--text-primary); } -.text-muted-foreground { color: var(--text-tertiary); } -.text-destructive { color: var(--color-error); } -.text-\[10px\] { font-size: 10px; } -.h-3\.5 { height: 14px; } -.w-3\.5 { width: 14px; } - -.bg-background { background: var(--surface-base); } -.bg-card { background: var(--surface-raised); } -.border-input { border-color: var(--surface-border); } - -.head-left { display: flex; align-items: center; gap: var(--space-3); } -.head-right { display: flex; align-items: center; gap: var(--space-4); margin-left: auto; } -.head-status { display: flex; align-items: center; gap: var(--space-1); font-size: 0.75rem; color: var(--text-secondary); } -.head-status-dot { width: 8px; height: 8px; border-radius: 50%; transition: all var(--transition-fast); } - -.card-bordered { border: 1px solid var(--surface-border); } -.card-elevated { box-shadow: var(--shadow-md); } - -.empty-state-icon { font-size: 3rem; margin-bottom: var(--space-4); opacity: 0.5; } - -.input-error { border-color: var(--color-error); } -.input-soft { background: var(--surface-container); border: 1px solid transparent; } - -.theme-toggle { width: 44px; height: 44px; border: 1px solid var(--surface-border); border-radius: var(--radius-md); background: var(--surface-overlay); color: var(--text-tertiary); cursor: pointer; font-size: 1.25rem; display: flex; align-items: center; justify-content: center; transition: all var(--transition-fast); } -.theme-toggle:hover { color: var(--color-primary); border-color: var(--color-primary); } - -.mascot-controls { display: flex; align-items: center; gap: var(--space-2); } -.mascot-inline { display: flex; align-items: center; gap: var(--space-1); } - -.placeholder-muted-foreground::placeholder { color: var(--text-tertiary); } - -.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } -@media (max-width: 768px) { .md\:grid-cols-2 { grid-template-columns: 1fr; } } - -.bg-destructive\/15 { background: rgba(239, 68, 68, 0.15); } -.border-border\/50 { border-color: rgba(255, 255, 255, 0.03); } -[data-theme="light"] .border-border\/50 { border-color: rgba(0, 0, 0, 0.03); } diff --git a/services/frontend/frontend/src/ui/badge.rs b/services/frontend/frontend/src/ui/badge.rs deleted file mode 100644 index 9807f96..0000000 --- a/services/frontend/frontend/src/ui/badge.rs +++ /dev/null @@ -1,34 +0,0 @@ -use leptos::prelude::*; - -#[derive(Clone, Default)] -pub enum BadgeVariant { - #[default] - Default, - Primary, - Success, - Warning, - Destructive, - Outline, - Info, -} - -#[component] -pub fn Badge(#[prop(optional)] variant: BadgeVariant, children: Children) -> impl IntoView { - let variant_class = match variant { - BadgeVariant::Default => "", - BadgeVariant::Primary => "badge-primary", - BadgeVariant::Success => "badge-success", - BadgeVariant::Warning => "badge-warning", - BadgeVariant::Destructive => "badge-destructive", - BadgeVariant::Outline => "badge-outline", - BadgeVariant::Info => "badge-info", - }; - - let combined = format!("badge {}", variant_class); - - view! { - - {children()} - - } -} diff --git a/services/frontend/frontend/src/ui/button.rs b/services/frontend/frontend/src/ui/button.rs deleted file mode 100644 index 468cacf..0000000 --- a/services/frontend/frontend/src/ui/button.rs +++ /dev/null @@ -1,63 +0,0 @@ -use leptos::prelude::*; - - -#[derive(Clone, Default)] -pub enum ButtonVariant { - #[default] - Primary, - Secondary, - Tertiary, - Destructive, - Outline, - Ghost, - Link, -} - -#[derive(Clone, Default)] -pub enum ButtonSize { - #[default] - Default, - Sm, - Lg, - Icon, - IconSm, -} - -#[component] -pub fn Button( - #[prop(optional)] variant: ButtonVariant, - #[prop(optional)] size: ButtonSize, - #[prop(optional)] disabled: bool, - #[prop(optional)] class: &'static str, - #[prop(optional)] on_click: Option>, - children: Children, -) -> impl IntoView { - let variant_class = match variant { - ButtonVariant::Primary => "btn-primary", - ButtonVariant::Secondary => "btn-secondary", - ButtonVariant::Tertiary => "btn-tertiary", - ButtonVariant::Destructive => "btn-destructive", - ButtonVariant::Outline => "btn-outline", - ButtonVariant::Ghost => "btn-ghost", - ButtonVariant::Link => "btn-link", - }; - let size_class = match size { - ButtonSize::Default => "", - ButtonSize::Sm => "btn-sm", - ButtonSize::Lg => "btn-lg", - ButtonSize::Icon => "btn-icon", - ButtonSize::IconSm => "btn-icon-sm", - }; - - let combined = format!("btn {} {} {}", variant_class, size_class, class); - - view! { - - } -} diff --git a/services/frontend/frontend/src/ui/card.rs b/services/frontend/frontend/src/ui/card.rs deleted file mode 100644 index a272452..0000000 --- a/services/frontend/frontend/src/ui/card.rs +++ /dev/null @@ -1,47 +0,0 @@ -use leptos::prelude::*; - - -#[component] -pub fn Card( - #[prop(optional)] elevated: bool, - #[prop(optional)] bordered: bool, - #[prop(optional)] class: &'static str, - children: Children, -) -> impl IntoView { - let combined = format!("card {}", class); - - view! { -
- {children()} -
- } -} - -#[component] -pub fn CardHeader(children: Children) -> impl IntoView { - view! {
{children()}
} -} - -#[component] -pub fn CardTitle(children: Children) -> impl IntoView { - view! {

{children()}

} -} - -#[component] -pub fn CardDescription(children: Children) -> impl IntoView { - view! {

{children()}

} -} - -#[component] -pub fn CardContent(children: Children) -> impl IntoView { - view! {
{children()}
} -} - -#[component] -pub fn CardFooter(children: Children) -> impl IntoView { - view! { } -} diff --git a/services/frontend/frontend/src/ui/empty_state.rs b/services/frontend/frontend/src/ui/empty_state.rs deleted file mode 100644 index 47072a0..0000000 --- a/services/frontend/frontend/src/ui/empty_state.rs +++ /dev/null @@ -1,20 +0,0 @@ -// services/frontend-leptos/frontend/src/ui/empty_state.rs -use leptos::prelude::*; - - -#[component] -pub fn EmptyState( - #[prop(optional)] icon: Option, - title: &'static str, - #[prop(optional)] description: Option<&'static str>, - #[prop(optional)] children: Option, -) -> impl IntoView { - view! { -
- {icon.map(|i| view! {
{i}
})} -
{title}
- {description.map(|d| view! {

{d}

})} - {children.map(|c| c())} -
- } -} diff --git a/services/frontend/frontend/src/ui/input.rs b/services/frontend/frontend/src/ui/input.rs deleted file mode 100644 index 809aa61..0000000 --- a/services/frontend/frontend/src/ui/input.rs +++ /dev/null @@ -1,47 +0,0 @@ -// services/frontend-leptos/frontend/src/ui/input.rs -use leptos::prelude::*; - -#[component] -pub fn Input( - #[prop(optional)] input_type: &'static str, - #[prop(optional)] placeholder: &'static str, - #[prop(optional)] value: RwSignal, - #[prop(optional)] soft: bool, - #[prop(optional)] error: bool, - #[prop(optional)] class: &'static str, - #[prop(optional)] on_input: Option>, -) -> impl IntoView { - view! { - - } -} - -#[component] -pub fn TextArea( - #[prop(optional)] placeholder: &'static str, - #[prop(optional)] value: RwSignal, - #[prop(optional)] rows: u32, - #[prop(optional)] class: &'static str, -) -> impl IntoView { - view! { - - } -} diff --git a/services/frontend/frontend/src/ui/mod.rs b/services/frontend/frontend/src/ui/mod.rs deleted file mode 100644 index 6d81960..0000000 --- a/services/frontend/frontend/src/ui/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -// services/frontend-leptos/frontend/src/ui/mod.rs -pub mod badge; -pub mod button; -pub mod card; -pub mod empty_state; -pub mod input; -pub mod modal; -pub mod scroll_area; -pub mod select; -pub mod skeleton; -pub mod status_badge; -pub mod tabs; -pub mod toast; diff --git a/services/frontend/frontend/src/ui/modal.rs b/services/frontend/frontend/src/ui/modal.rs deleted file mode 100644 index f23167f..0000000 --- a/services/frontend/frontend/src/ui/modal.rs +++ /dev/null @@ -1,47 +0,0 @@ -// services/frontend-leptos/frontend/src/ui/modal.rs -use leptos::prelude::*; -use std::sync::Arc; - - -#[component] -pub fn Modal( - is_open: RwSignal, - #[prop(optional)] title: Option<&'static str>, - #[prop(optional)] on_close: Option>, - children: Children, -) -> impl IntoView { - let oc1 = on_close.clone(); - let oc2 = on_close; - - view! { - - } -} diff --git a/services/frontend/frontend/src/ui/scroll_area.rs b/services/frontend/frontend/src/ui/scroll_area.rs deleted file mode 100644 index 22f8f30..0000000 --- a/services/frontend/frontend/src/ui/scroll_area.rs +++ /dev/null @@ -1,15 +0,0 @@ -// services/frontend-leptos/frontend/src/ui/scroll_area.rs -use leptos::prelude::*; - -#[component] -pub fn ScrollArea( - #[prop(optional)] class: &'static str, - #[prop(optional)] style: &'static str, - children: Children, -) -> impl IntoView { - view! { -
- {children()} -
- } -} diff --git a/services/frontend/frontend/src/ui/select.rs b/services/frontend/frontend/src/ui/select.rs deleted file mode 100644 index 24119b8..0000000 --- a/services/frontend/frontend/src/ui/select.rs +++ /dev/null @@ -1,30 +0,0 @@ -// services/frontend-leptos/frontend/src/ui/select.rs -use leptos::prelude::*; - -/// Simple select — values and labels are the same -/// For options with different value/label, use `SelectOptions` -#[component] -pub fn Select( - #[prop(optional)] value: RwSignal, - options: Vec<(&'static str, &'static str)>, // (value, label) - #[prop(optional)] placeholder: &'static str, - #[prop(optional)] class: &'static str, - #[prop(optional)] on_change: Option>, -) -> impl IntoView { - view! { - - } -} diff --git a/services/frontend/frontend/src/ui/skeleton.rs b/services/frontend/frontend/src/ui/skeleton.rs deleted file mode 100644 index 5cb9f49..0000000 --- a/services/frontend/frontend/src/ui/skeleton.rs +++ /dev/null @@ -1,30 +0,0 @@ -// services/frontend-leptos/frontend/src/ui/skeleton.rs -use leptos::prelude::*; - -#[derive(Clone, Default)] -pub enum SkeletonShape { - #[default] - Rounded, - Circular, - Rectangular, -} - -#[component] -pub fn Skeleton( - #[prop(optional)] width: &'static str, - #[prop(optional)] height: &'static str, - #[prop(optional)] shape: SkeletonShape, -) -> impl IntoView { - let shape_class = match shape { - SkeletonShape::Rounded => "", - SkeletonShape::Circular => "skeleton-circular", - SkeletonShape::Rectangular => "skeleton-rectangular", - }; - let combined = format!("skeleton {}", shape_class); - view! { -
- } -} diff --git a/services/frontend/frontend/src/ui/status_badge.rs b/services/frontend/frontend/src/ui/status_badge.rs deleted file mode 100644 index 5dfb59d..0000000 --- a/services/frontend/frontend/src/ui/status_badge.rs +++ /dev/null @@ -1,21 +0,0 @@ -// services/frontend-leptos/frontend/src/ui/status_badge.rs -use leptos::prelude::*; -use shared_types::message::AiStatus; - -#[component] -pub fn StatusBadge(status: AiStatus) -> impl IntoView { - let (class, label) = match status { - AiStatus::Flagged => ("status-badge-flagged", "Flagged"), - AiStatus::Clean => ("status-badge-clean", "Clean"), - AiStatus::Warn => ("status-badge-warn", "Warned"), - AiStatus::Pending => ("status-badge-pending", "Pending"), - AiStatus::Processing => ("status-badge-processing", "Processing"), - AiStatus::Error => ("status-badge-error", "Error"), - }; - let combined = format!("status-badge {}", class); - view! { - - {label} - - } -} diff --git a/services/frontend/frontend/src/ui/tabs.rs b/services/frontend/frontend/src/ui/tabs.rs deleted file mode 100644 index c2ddda6..0000000 --- a/services/frontend/frontend/src/ui/tabs.rs +++ /dev/null @@ -1,56 +0,0 @@ -// services/frontend-leptos/frontend/src/ui/tabs.rs -use leptos::prelude::*; - -#[component] -pub fn Tabs( - active: RwSignal, - #[prop(optional)] class: &'static str, - children: Children, -) -> impl IntoView { - let _ = active; - view! { -
- {children()} -
- } -} - -#[component] -pub fn TabList(#[prop(optional)] class: &'static str, children: Children) -> impl IntoView { - view! { -
- {children()} -
- } -} - -#[component] -pub fn TabTrigger(value: String, active: RwSignal, children: Children) -> impl IntoView { - let v1 = value.clone(); - let v2 = value.clone(); - view! { - - } -} - -#[component] -pub fn TabContent(value: String, active: RwSignal, children: Children) -> impl IntoView { - let is_selected = move || active.get() == value; - view! { -
- {children()} -
- } -} diff --git a/services/frontend/frontend/src/ui/toast.rs b/services/frontend/frontend/src/ui/toast.rs deleted file mode 100644 index a6c5d13..0000000 --- a/services/frontend/frontend/src/ui/toast.rs +++ /dev/null @@ -1,107 +0,0 @@ -// services/frontend-leptos/frontend/src/ui/toast.rs -use leptos::prelude::*; -use std::sync::{Arc, Mutex}; -use std::fmt; -use crate::{log_info, make_logger}; - -make_logger!(); - -#[derive(Clone)] -pub enum ToastType { - Info, - Success, - Error, - Warning, -} - -impl fmt::Display for ToastType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ToastType::Info => write!(f, "info"), - ToastType::Success => write!(f, "success"), - ToastType::Error => write!(f, "error"), - ToastType::Warning => write!(f, "warning"), - } - } -} - -#[derive(Clone)] -pub struct ToastMessage { - pub id: u64, - pub message: String, - pub toast_type: ToastType, -} - -#[derive(Clone)] -pub struct ToastContext { - pub toasts: RwSignal>, - next_id: Arc>, -} - -impl Default for ToastContext { - fn default() -> Self { - Self::new() - } -} - -impl ToastContext { - pub fn new() -> Self { - Self { - toasts: RwSignal::new(vec![]), - next_id: Arc::new(Mutex::new(0)), - } - } - - pub fn show(&self, message: &str, toast_type: ToastType) { - log_info!("Toast: {} ({})", message, toast_type); - let id = { - let mut n = self.next_id.lock().unwrap(); - *n += 1; - *n - }; - let msg = ToastMessage { - id, - message: message.to_string(), - toast_type, - }; - self.toasts.update(|t| t.push(msg)); - - // Auto-dismiss after 4 seconds - let toasts = self.toasts; - let _ = leptos::prelude::set_timeout( - move || { - toasts.update(|t| t.retain(|m| m.id != id)); - }, - std::time::Duration::from_secs(4), - ); - } -} - -#[component] -pub fn ToastProvider(children: Children) -> impl IntoView { - let ctx = ToastContext::new(); - provide_context(ctx.clone()); - - view! { - {children()} -
- {move || ctx.toasts.get().into_iter().map(|msg| { - let type_class = match msg.toast_type { - ToastType::Info => "toast-info", - ToastType::Success => "toast-success", - ToastType::Error => "toast-error", - ToastType::Warning => "toast-warning", - }; - let toasts = ctx.toasts; - view! { -
- {msg.message} - -
- } - }).collect::>()} -
- } -} diff --git a/services/frontend/frontend/src/ws/connection.rs b/services/frontend/frontend/src/ws/connection.rs deleted file mode 100644 index d8eaa72..0000000 --- a/services/frontend/frontend/src/ws/connection.rs +++ /dev/null @@ -1,214 +0,0 @@ -// services/frontend-leptos/frontend/src/ws/connection.rs -use crate::ws::handlers::{WsEvent, WsStatus}; -use leptos::prelude::*; -use wasm_bindgen::prelude::*; -use wasm_bindgen::JsCast; -use web_sys::{CloseEvent, ErrorEvent, MessageEvent, WebSocket}; -use crate::{log_debug, log_error, log_info, log_trace, log_warn, make_logger}; - -make_logger!(); - -#[allow(clippy::type_complexity)] -pub struct WsHandle { - pub status: ReadSignal, - set_status: WriteSignal, - ws: std::cell::RefCell>, - on_event: std::rc::Rc>>>, - url: String, - reconnect_attempt: std::cell::Cell, -} - -impl WsHandle { - pub fn new(url: &str) -> Self { - let (status, set_status) = signal(WsStatus::Disconnected); - Self { - status, - set_status, - ws: std::cell::RefCell::new(None), - on_event: std::rc::Rc::new(std::cell::RefCell::new(None)), - url: url.to_string(), - reconnect_attempt: std::cell::Cell::new(0), - } - } - - pub fn on_event(&self, callback: F) - where - F: Fn(WsEvent) + 'static, - { - *self.on_event.borrow_mut() = Some(Box::new(callback)); - } - - pub fn connect(&self) { - if self.status.get_untracked() == WsStatus::Connected - || self.status.get_untracked() == WsStatus::Connecting - { - return; - } - self.set_status.set(WsStatus::Connecting); - - let url = self.url.clone(); - let status_clone = self.set_status; - #[allow(clippy::type_complexity)] - let event_clone: std::rc::Rc>>> = - self.on_event.clone(); - let ws_holder = &self.ws as *const std::cell::RefCell>; - let reconnect_attempt = &self.reconnect_attempt as *const std::cell::Cell; - - Self::perform_connect( - &url, - status_clone, - event_clone, - ws_holder, - reconnect_attempt, - ); - } - - /// Shared connection setup used for both initial connect and reconnection. - /// Takes raw pointers because it must be callable from `wasm_bindgen` closures - /// that cannot borrow `self`. - #[allow(unsafe_code, clippy::type_complexity)] - fn perform_connect( - url: &str, - set_status: WriteSignal, - on_event: std::rc::Rc>>>, - ws_holder: *const std::cell::RefCell>, - reconnect_attempt: *const std::cell::Cell, - ) { - let url_owned = url.to_string(); - let url_close = url_owned.clone(); - let status1 = set_status; - let status2 = set_status; - let status3 = set_status; - let event_clone = on_event.clone(); - - match WebSocket::new(&url_owned) { - Ok(ws) => { - // Store reference - unsafe { *(*ws_holder).borrow_mut() = Some(ws.clone()) }; - - // onopen - let onopen_cb = Closure::::new(move |_| { - status1.set(WsStatus::Connected); - log_info!("WS connected to {}", url_owned); - unsafe { (*reconnect_attempt).set(0) }; - }); - ws.set_onopen(Some(onopen_cb.as_ref().unchecked_ref())); - onopen_cb.forget(); - - // onclose — schedule reconnect with exponential backoff - let event_for_close = event_clone.clone(); - let onclose_cb = Closure::::new(move |_| { - status2.set(WsStatus::Disconnected); - log_info!("WS disconnected from {}", url_close); - unsafe { *(*ws_holder).borrow_mut() = None }; - - let attempt = unsafe { (*reconnect_attempt).get() }; - if attempt >= 20 { - status2.set(WsStatus::Error( - "Max reconnect attempts reached".to_string(), - )); - log_error!("WS reconnect max attempts reached for {}", url_close); - return; - } - // Full-jitter exponential backoff: min(1000 * 2^attempt, 30000) * (0.5 + random * 0.5) - let base = core::cmp::min(1000u32 * (1u32 << attempt), 30000u32); - let jitter = 0.5 + js_sys::Math::random() * 0.5; - let delay_ms = (base as f64 * jitter) as u32; - unsafe { (*reconnect_attempt).set(attempt + 1) }; - - log_info!("WS reconnecting to {} in {}ms (attempt {})", url_close, delay_ms, attempt + 1); - - let url_reconnect = url_close.clone(); - let status_rc = status2; - let event_rc = event_for_close.clone(); - let reconnect_fn = Closure::::new(move || { - Self::perform_connect( - &url_reconnect, - status_rc, - event_rc.clone(), - ws_holder, - reconnect_attempt, - ); - }); - web_sys::window().and_then(|w| { - w.set_timeout_with_callback_and_timeout_and_arguments_0( - reconnect_fn.as_ref().unchecked_ref(), - delay_ms as i32, - ) - .ok() - }); - reconnect_fn.forget(); - }); - ws.set_onclose(Some(onclose_cb.as_ref().unchecked_ref())); - onclose_cb.forget(); - - // onerror - let onerror_cb = Closure::::new(move |e: ErrorEvent| { - log_error!("WS error: {}", e.message()); - status3.set(WsStatus::Error(e.message())); - }); - ws.set_onerror(Some(onerror_cb.as_ref().unchecked_ref())); - onerror_cb.forget(); - - // onmessage - let onmsg_cb = Closure::::new(move |e: MessageEvent| { - if let Some(cb) = &*event_clone.borrow() { - if let Some(text) = e.data().as_string() { - cb(WsEvent::Text(text)); - } else if let Some(abuf) = e.data().dyn_ref::() { - let len = abuf.byte_length() as usize; - let u8view = js_sys::Uint8Array::new(abuf); - let mut bytes = vec![0u8; len]; - u8view.copy_to(&mut bytes); - cb(WsEvent::Binary(bytes)); - } else { - // Blob — would need async FileReader, skip for now - } - } - }); - ws.set_onmessage(Some(onmsg_cb.as_ref().unchecked_ref())); - onmsg_cb.forget(); - } - Err(e) => { - let msg = js_sys::Error::from(e) - .to_string() - .as_string() - .unwrap_or_default(); - log_error!("WS connect failed: {}", msg); - set_status.set(WsStatus::Error(msg)); - } - } - } - - pub fn disconnect(&self) { - if let Some(ws) = self.ws.borrow_mut().take() { - ws.close().ok(); - } - self.set_status.set(WsStatus::Disconnected); - } - - pub fn send_text(&self, text: &str) -> Result<(), JsValue> { - if let Some(ws) = self.ws.borrow().as_ref() { - ws.send_with_str(text) - } else { - Err(JsValue::from_str("WebSocket not connected")) - } - } - - pub fn send_binary(&self, data: &[u8]) -> Result<(), JsValue> { - if let Some(ws) = self.ws.borrow().as_ref() { - let array = js_sys::Uint8Array::from(data); - let buffer = array.buffer(); - ws.send_with_array_buffer(&buffer) - } else { - Err(JsValue::from_str("WebSocket not connected")) - } - } -} - -// SAFETY: WsHandle uses Rc/RefCell for cheap cloning in a single-threaded WASM -// environment. The leptos reactive system requires provided contexts to be Send+Sync. -#[allow(unsafe_code)] -unsafe impl Send for WsHandle {} -#[allow(unsafe_code)] -unsafe impl Sync for WsHandle {} diff --git a/services/frontend/frontend/src/ws/context.rs b/services/frontend/frontend/src/ws/context.rs deleted file mode 100644 index 8457c60..0000000 --- a/services/frontend/frontend/src/ws/context.rs +++ /dev/null @@ -1,176 +0,0 @@ -// services/frontend-leptos/frontend/src/ws/context.rs -use crate::ws::connection::WsHandle; -use crate::ws::handlers::{WsEvent, WsStatus}; -use leptos::prelude::*; -use shared_types::media::MediaState; -use shared_types::message::MessageRecord; -use shared_types::recording::VoiceRecording; -use shared_types::voice::ActiveSpeaker; -use crate::{log_debug, log_error, log_info, log_trace, log_warn, make_logger}; - -make_logger!(); - -#[derive(Clone)] -#[allow(clippy::type_complexity)] -pub struct WsContext { - pub handle: std::rc::Rc, - pub status: ReadSignal, - // Per-event callbacks (set externally by feature components) - // Wrapped in Rc so cloning shares the same callback slots - pub on_message_created: std::rc::Rc>>>, - pub on_message_updated: std::rc::Rc>>>, - pub on_message_deleted: std::rc::Rc>>>, - pub on_message_analyzed: std::rc::Rc>>>, - pub on_voice_active_user: std::rc::Rc>>>, - pub on_voice_recording_uploaded: - std::rc::Rc>>>, - pub on_media_state: std::rc::Rc>>>, - pub on_binary: std::rc::Rc)>>>>, -} - -impl WsContext { - pub fn new(url: &str) -> Self { - let ws_handle = std::rc::Rc::new(WsHandle::new(url)); - let status = ws_handle.status; - - let ctx = Self { - status, - handle: ws_handle, - on_message_created: std::rc::Rc::new(std::cell::RefCell::new(None)), - on_message_updated: std::rc::Rc::new(std::cell::RefCell::new(None)), - on_message_deleted: std::rc::Rc::new(std::cell::RefCell::new(None)), - on_message_analyzed: std::rc::Rc::new(std::cell::RefCell::new(None)), - on_voice_active_user: std::rc::Rc::new(std::cell::RefCell::new(None)), - on_voice_recording_uploaded: std::rc::Rc::new(std::cell::RefCell::new(None)), - on_media_state: std::rc::Rc::new(std::cell::RefCell::new(None)), - on_binary: std::rc::Rc::new(std::cell::RefCell::new(None)), - }; - - // Wire up the main event dispatcher - let ctx_clone = ctx.clone(); - ctx.handle.on_event(move |event| { - ctx_clone.dispatch_event(event); - }); - - ctx - } - - fn dispatch_event(&self, event: WsEvent) { - match event { - WsEvent::Text(text) => { - // Parse JSON envelope: { type: string, data?: any } - if let Ok(parsed) = serde_json::from_str::(&text) { - let event_type = parsed["type"].as_str().unwrap_or("").to_string(); - let data = parsed.get("data"); - - match event_type.as_str() { - "message_created" => { - log_debug!("WS event: message_created"); - if let Some(d) = data.and_then(|v| { - serde_json::from_value::(v.clone()).ok() - }) { - if let Some(cb) = self.on_message_created.borrow().as_ref() { - cb(d); - } - } - } - "message_updated" => { - log_debug!("WS event: message_updated"); - if let Some(d) = data.and_then(|v| { - serde_json::from_value::(v.clone()).ok() - }) { - if let Some(cb) = self.on_message_updated.borrow().as_ref() { - cb(d); - } - } - } - "message_deleted" => { - log_debug!("WS event: message_deleted"); - if let Some(d) = data.and_then(|v| v.as_str().map(String::from)) { - if let Some(cb) = self.on_message_deleted.borrow().as_ref() { - cb(d); - } - } - } - "message_analyzed" => { - log_debug!("WS event: message_analyzed"); - if let Some(d) = data.and_then(|v| { - serde_json::from_value::(v.clone()).ok() - }) { - if let Some(cb) = self.on_message_analyzed.borrow().as_ref() { - cb(d); - } - } - } - "voice_active_user" => { - log_debug!("WS event: voice_active_user"); - if let Some(d) = data.and_then(|v| { - serde_json::from_value::(v.clone()).ok() - }) { - if let Some(cb) = self.on_voice_active_user.borrow().as_ref() { - cb(d); - } - } - } - "voice_recording_uploaded" => { - log_debug!("WS event: voice_recording_uploaded"); - if let Some(d) = data.and_then(|v| { - serde_json::from_value::(v.clone()).ok() - }) { - if let Some(cb) = self.on_voice_recording_uploaded.borrow().as_ref() - { - cb(d); - } - } - } - "media_state" => { - log_debug!("WS event: media_state"); - // Backend sends initial state with "state" key, live updates with "data" - let raw = data.or_else(|| parsed.get("state")).cloned(); - if let Some(d) = - raw.and_then(|v| serde_json::from_value::(v).ok()) - { - if let Some(cb) = self.on_media_state.borrow().as_ref() { - cb(d); - } - } - } - _ => { - // Unknown event type — log and ignore - log_warn!("WS unhandled event type: {}", event_type); - } - } - } - } - WsEvent::Binary(data) => { - log_debug!("WS event: binary ({} bytes)", data.len()); - if let Some(cb) = self.on_binary.borrow().as_ref() { - cb(data); - } - } - } - } - - pub fn connect(&self) { - self.handle.connect(); - } - - pub fn disconnect(&self) { - self.handle.disconnect(); - } - - pub fn send_text(&self, text: &str) { - let _ = self.handle.send_text(text); - } - - pub fn send_binary(&self, data: &[u8]) { - let _ = self.handle.send_binary(data); - } -} - -// SAFETY: WsContext uses Rc/RefCell for cheap cloning in a single-threaded WASM -// environment. The leptos reactive system requires provided contexts to be Send+Sync. -#[allow(unsafe_code)] -unsafe impl Send for WsContext {} -#[allow(unsafe_code)] -unsafe impl Sync for WsContext {} diff --git a/services/frontend/frontend/src/ws/handlers.rs b/services/frontend/frontend/src/ws/handlers.rs deleted file mode 100644 index 3e87b63..0000000 --- a/services/frontend/frontend/src/ws/handlers.rs +++ /dev/null @@ -1,16 +0,0 @@ -// services/frontend-leptos/frontend/src/ws/handlers.rs -use leptos::prelude::*; - -#[derive(Debug, Clone, PartialEq)] -pub enum WsStatus { - Disconnected, - Connecting, - Connected, - Error(String), -} - -#[derive(Debug, Clone)] -pub enum WsEvent { - Text(String), - Binary(Vec), -} diff --git a/services/frontend/frontend/src/ws/mod.rs b/services/frontend/frontend/src/ws/mod.rs deleted file mode 100644 index 0bfdab8..0000000 --- a/services/frontend/frontend/src/ws/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// services/frontend-leptos/frontend/src/ws/mod.rs -pub mod connection; -pub mod context; -pub mod handlers; diff --git a/services/frontend/next.config.ts b/services/frontend/next.config.ts new file mode 100644 index 0000000..bba8180 --- /dev/null +++ b/services/frontend/next.config.ts @@ -0,0 +1,10 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + reactCompiler: true, + output: "export", + trailingSlash: true, + images: { unoptimized: true }, +}; + +export default nextConfig; diff --git a/services/frontend/package.json b/services/frontend/package.json new file mode 100644 index 0000000..329d419 --- /dev/null +++ b/services/frontend/package.json @@ -0,0 +1,42 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "biome check", + "format": "biome format --write" + }, + "dependencies": { + "@base-ui/react": "^1.6.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.27.0", + "next": "16.2.12", + "react": "19.2.4", + "react-dom": "19.2.4", + "shadcn": "^4.15.0", + "tailwind-merge": "^3.6.0", + "tw-animate-css": "^1.4.0" + }, + "devDependencies": { + "@biomejs/biome": "2.2.0", + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "babel-plugin-react-compiler": "1.0.0", + "tailwindcss": "^4", + "typescript": "^5" + }, + "ignoreScripts": [ + "sharp", + "unrs-resolver" + ], + "trustedDependencies": [ + "sharp", + "unrs-resolver" + ] +} diff --git a/services/frontend/postcss.config.mjs b/services/frontend/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/services/frontend/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/services/frontend/public/file.svg b/services/frontend/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/services/frontend/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/services/frontend/public/globe.svg b/services/frontend/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/services/frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/services/frontend/public/next.svg b/services/frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/services/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/services/frontend/public/vercel.svg b/services/frontend/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/services/frontend/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/services/frontend/public/window.svg b/services/frontend/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/services/frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/services/frontend/rust-toolchain.toml b/services/frontend/rust-toolchain.toml deleted file mode 100644 index 0c59078..0000000 --- a/services/frontend/rust-toolchain.toml +++ /dev/null @@ -1,4 +0,0 @@ -[toolchain] -channel = "nightly-2026-07-03" -components = ["rust-src", "rustc-dev"] -targets = ["wasm32-unknown-unknown"] diff --git a/services/frontend/shared-types/Cargo.toml b/services/frontend/shared-types/Cargo.toml deleted file mode 100644 index 8df0da8..0000000 --- a/services/frontend/shared-types/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "shared-types" -version = "0.1.0" -edition = "2021" - -[dependencies] -serde = { version = "1", features = ["derive"] } -serde_json = "1" diff --git a/services/frontend/shared-types/src/dashboard.rs b/services/frontend/shared-types/src/dashboard.rs deleted file mode 100644 index e2be098..0000000 --- a/services/frontend/shared-types/src/dashboard.rs +++ /dev/null @@ -1,87 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DashboardStats { - pub total_messages: u64, - pub total_users: u64, - pub total_flagged: u64, - pub total_clean: u64, - pub total_warned: u64, - pub total_error: u64, - pub total_voice_recordings: u64, - pub total_profiles: u64, - pub today_messages: u64, - pub today_flagged: u64, - pub active_users_24h: u64, - pub top_channels: Vec, - pub moderation_overview: ModerationOverview, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TopChannel { - pub channel_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub channel_name: Option, - pub message_count: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ModerationOverview { - pub pending: u64, - pub processing: u64, - pub error: u64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DashboardUser { - pub user_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub username: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub avatar_url: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub profile_summary: Option, - pub total_messages: u64, - pub flagged_count: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_message_at: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub trust_score: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DashboardUserDetail { - #[serde(flatten)] - pub user: DashboardUser, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_analyzed_at: Option, - pub clean_message_streak: u64, - pub total_infractions: u64, - pub clean_count: u64, - pub recent_messages: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DashboardChannel { - pub channel_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub channel_name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub guild_id: Option, - pub total_messages: u64, - pub flagged_count: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_message_at: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub culture_summary: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_analyzed_at: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DashboardChannelDetail { - #[serde(flatten)] - pub channel: DashboardChannel, - pub clean_count: u64, - pub recent_messages: Vec, -} diff --git a/services/frontend/shared-types/src/guild.rs b/services/frontend/shared-types/src/guild.rs deleted file mode 100644 index 85367b9..0000000 --- a/services/frontend/shared-types/src/guild.rs +++ /dev/null @@ -1,36 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Guild { - pub id: String, - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub icon: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Channel { - pub id: String, - pub name: String, - #[serde(rename = "type")] - #[serde(skip_serializing_if = "Option::is_none")] - pub channel_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub parent_id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GuildVoiceEntry { - pub guild_id: String, - pub channel_id: String, - pub channel_name: String, - pub connected_at: i64, -} - -// ── Config ──────────────────────────────────────────────── -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AppConfig { - #[serde(skip_serializing_if = "Option::is_none")] - pub monitor_guild_id: Option, -} diff --git a/services/frontend/shared-types/src/lib.rs b/services/frontend/shared-types/src/lib.rs deleted file mode 100644 index 475e67a..0000000 --- a/services/frontend/shared-types/src/lib.rs +++ /dev/null @@ -1,81 +0,0 @@ -pub mod message; -pub mod guild; -pub mod voice; -pub mod media; -pub mod dashboard; -pub mod recording; -pub mod ui_state; - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_embed_media_string() { - // Simulate what Discord sends: thumbnail as a plain URL string - let json = r#"{"title":"Test","thumbnail":"https://cdn.example.com/image.gif"}"#; - let embed: message::EmbedInfo = serde_json::from_str(json).unwrap(); - assert_eq!(embed.title, Some("Test".into())); - let thumb = embed.thumbnail.unwrap(); - assert_eq!(thumb.url, "https://cdn.example.com/image.gif"); - assert_eq!(thumb.width, None); - assert_eq!(thumb.height, None); - } - - #[test] - fn test_embed_media_object() { - // Standard embed media object - let json = r#"{"thumbnail":{"url":"https://cdn.example.com/img.png","width":128,"height":128}}"#; - let embed: message::EmbedInfo = serde_json::from_str(json).unwrap(); - let thumb = embed.thumbnail.unwrap(); - assert_eq!(thumb.url, "https://cdn.example.com/img.png"); - assert_eq!(thumb.width, Some(128)); - assert_eq!(thumb.height, Some(128)); - } - - #[test] - fn test_embed_media_null() { - let json = r#"{"title":"No Media Here"}"#; - let embed: message::EmbedInfo = serde_json::from_str(json).unwrap(); - assert_eq!(embed.thumbnail, None); - assert_eq!(embed.image, None); - } - - #[test] - fn test_embed_media_both_strings() { - // Discord might send both image and thumbnail as strings - let json = r#"{"image":"https://cdn.example.com/banner.gif","thumbnail":"https://cdn.example.com/thumb.gif"}"#; - let embed: message::EmbedInfo = serde_json::from_str(json).unwrap(); - assert_eq!(embed.image.as_ref().unwrap().url, "https://cdn.example.com/banner.gif"); - assert_eq!(embed.thumbnail.as_ref().unwrap().url, "https://cdn.example.com/thumb.gif"); - } - - #[test] - fn test_full_message_metadata_with_string_thumbnail() { - // Full realistic metadata with string thumbnail (the actual bug) - let json = r#"{ - "stickers": [], - "embeds": [{ - "title": "Meisho Doto Tm Opera O", - "description": null, - "url": "https://klipy.com/gifs/test", - "color": null, - "image": null, - "thumbnail": "https://static.klipy.com/ii/test.webp", - "author": null, - "footer": null, - "fields": [] - }] - }"#; - let meta: message::MessageMetadata = serde_json::from_str(json).unwrap(); - let embeds = meta.embeds.unwrap(); - assert_eq!(embeds.len(), 1); - let embed = &embeds[0]; - assert_eq!(embed.title.as_deref(), Some("Meisho Doto Tm Opera O")); - assert!(embed.image.is_none()); - let thumb = embed.thumbnail.as_ref().unwrap(); - assert_eq!(thumb.url, "https://static.klipy.com/ii/test.webp"); - assert_eq!(thumb.width, None); - assert_eq!(thumb.height, None); - } -} diff --git a/services/frontend/shared-types/src/media.rs b/services/frontend/shared-types/src/media.rs deleted file mode 100644 index c175dd1..0000000 --- a/services/frontend/shared-types/src/media.rs +++ /dev/null @@ -1,30 +0,0 @@ -use serde::{Deserialize, Serialize}; - -pub type MediaMode = String; // "music" | "screen" - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MediaItem { - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, - pub source: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub mode: Option, - #[serde(rename = "durationMs")] - #[serde(skip_serializing_if = "Option::is_none")] - pub duration_ms: Option, - #[serde(rename = "thumbnailUrl")] - #[serde(skip_serializing_if = "Option::is_none")] - pub thumbnail_url: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MediaState { - pub playing: bool, - #[serde(rename = "musicVolume")] - pub music_volume: f64, - #[serde(skip_serializing_if = "Option::is_none")] - pub current: Option, - pub queue: Vec, -} diff --git a/services/frontend/shared-types/src/message.rs b/services/frontend/shared-types/src/message.rs deleted file mode 100644 index d6f47d7..0000000 --- a/services/frontend/shared-types/src/message.rs +++ /dev/null @@ -1,304 +0,0 @@ -use serde::de::{self, DeserializeOwned, Deserializer}; -use serde::{Deserialize, Serialize}; - -// ── AI Status ───────────────────────────────────────────── -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum AiStatus { - Pending, - Processing, - Clean, - Warn, - Flagged, - Error, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum AiSeverity { - None, - Low, - Medium, - High, - Critical, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum AiRecommendedAction { - None, - Monitor, - Warn, - Review, - Delete, - Escalate, -} - -// ── Message Metadata ────────────────────────────────────── -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct MessageMetadata { - #[serde(skip_serializing_if = "Option::is_none")] - pub stickers: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub attachments: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub embeds: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub channel: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub reference: Option, -} - -/// Information about a referenced (replied-to) message. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct ReferenceInfo { - #[serde(skip_serializing_if = "Option::is_none")] - pub message_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub channel_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub guild_id: Option, - #[serde(rename = "type")] - #[serde(skip_serializing_if = "Option::is_none")] - pub ref_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub content: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub replied_username: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub replied_user_id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct StickerInfo { - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct AttachmentRef { - pub name: String, - pub url: String, - #[serde(rename = "contentType")] - #[serde(skip_serializing_if = "Option::is_none")] - pub content_type: Option, -} - -/// Deserialize `EmbedMedia` from either: -/// - `null` → `None` -/// - a JSON string → `Some(EmbedMedia { url: , width: None, height: None })` -/// - a JSON object → standard struct deserialization -fn deser_embed_media<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { - let v = Option::::deserialize(d)?; - match v { - None => Ok(None), - Some(serde_json::Value::String(s)) => Ok(Some(EmbedMedia { - url: s, - width: None, - height: None, - })), - Some(obj) => serde_json::from_value(obj).map(Some).map_err(de::Error::custom), - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct EmbedInfo { - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - #[serde( - default, - deserialize_with = "deser_embed_media", - skip_serializing_if = "Option::is_none" - )] - pub image: Option, - #[serde( - default, - deserialize_with = "deser_embed_media", - skip_serializing_if = "Option::is_none" - )] - pub thumbnail: Option, -} - -#[derive(Debug, Clone, Serialize, PartialEq)] -pub struct EmbedMedia { - pub url: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub width: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub height: Option, -} - -impl<'de> Deserialize<'de> for EmbedMedia { - fn deserialize>(d: D) -> Result { - // When EmbedMedia appears as a struct member (inside the object branch - // of deser_embed_media), serde calls this directly. We delegate to a - // derived deserializer on the struct fields. - #[derive(serde::Deserialize)] - struct Inner { - url: String, - #[serde(default)] - width: Option, - #[serde(default)] - height: Option, - } - let inner = Inner::deserialize(d)?; - Ok(EmbedMedia { - url: inner.url, - width: inner.width, - height: inner.height, - }) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct ChannelRef { - pub channel_id: String, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] - pub channel_name: Option, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] - pub thread_id: Option, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] - pub thread_name: Option, -} - -// ── Helpers: deserialize JSON-string fields ──────────────── -// -// The backend stores certain fields as raw JSON strings in PostgreSQL. -// The JSON response therefore contains *stringified* JSON for these fields -// (e.g. `"metadata":"{\"stickers\":[]}"`) instead of the actual JSON value. -// These helpers transparently parse the string when present, so the frontend -// works with the native Rust type regardless of whether the backend ships -// a parsed value or a stringified one. - -/// Deserialize a `T` from a JSON value that may be: -/// - `null` → `None` -/// - a plain JSON value → `Some(T)` (direct serde) -/// - a JSON *string* whose *contents* are JSON for `T` -fn from_json_string_or_value<'de, T, D>(d: D) -> Result, D::Error> -where - T: DeserializeOwned, - D: Deserializer<'de>, -{ - // Intermediate Value to distinguish null / object / array / string - let v = Option::::deserialize(d)?; - match v { - None => Ok(None), - Some(serde_json::Value::String(s)) => { - serde_json::from_str(&s).map(Some).map_err(de::Error::custom) - } - Some(json) => serde_json::from_value(json).map(Some).map_err(de::Error::custom), - } -} - -/// Concrete wrapper for `metadata: Option`. -pub(crate) fn deser_msg_meta<'de, D: Deserializer<'de>>( - d: D, -) -> Result, D::Error> { - from_json_string_or_value(d) -} - -/// Concrete wrapper for `Option>` fields -/// (ai_moderation_flags, ai_categories, etc.). -pub(crate) fn deser_str_vec<'de, D: Deserializer<'de>>( - d: D, -) -> Result>, D::Error> { - from_json_string_or_value(d) -} - -// ── Message Record ──────────────────────────────────────── -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct MessageRecord { - pub id: String, - pub guild_id: String, - pub channel_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub thread_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub reference_message_id: Option, - pub user_id: String, - pub username: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub avatar_url: Option, - pub content: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub edited_content: Option, - #[serde(rename = "type")] - pub msg_type: String, // "text" | "edited" | "deleted" - #[serde(skip_serializing_if = "Option::is_none")] - pub is_reply: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub is_forward: Option, - pub created_at: i64, - #[serde(skip_serializing_if = "Option::is_none")] - pub edited_at: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub deleted_at: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ai_status: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ai_severity: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ai_confidence: Option, - #[serde( - deserialize_with = "deser_str_vec", - skip_serializing_if = "Option::is_none" - )] - pub ai_moderation_flags: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub ai_moderation_score: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ai_analysis: Option, - #[serde( - deserialize_with = "deser_str_vec", - skip_serializing_if = "Option::is_none" - )] - pub ai_categories: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub ai_recommended_action: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ai_error: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ai_analyzed_at: Option, - #[serde( - deserialize_with = "deser_msg_meta", - skip_serializing_if = "Option::is_none" - )] - pub metadata: Option, -} - -// ── Pagination ──────────────────────────────────────────── -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct PageResult { - pub data: Vec, - #[serde(rename = "nextCursor")] - pub next_cursor: Option, -} - -// ── Attachment ──────────────────────────────────────────── -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct AttachmentRecord { - pub id: String, - pub message_id: String, - pub guild_id: String, - pub channel_id: String, - pub filename: String, - pub size: u64, - #[serde(rename = "type")] - pub mime_type: String, - pub discord_url: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub uploaded_url: Option, - pub upload_status: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub upload_error: Option, - pub created_at: i64, - #[serde(skip_serializing_if = "Option::is_none")] - pub uploaded_at: Option, -} diff --git a/services/frontend/shared-types/src/recording.rs b/services/frontend/shared-types/src/recording.rs deleted file mode 100644 index 5058069..0000000 --- a/services/frontend/shared-types/src/recording.rs +++ /dev/null @@ -1,37 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct VoiceRecording { - pub id: String, - pub user_id: String, - pub username: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub avatar_url: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub guild_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub channel_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub channel_name: Option, - pub filename: String, - pub size_bytes: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub download_url: Option, - pub upload_status: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub upload_error: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub transcription: Option, - pub created_at: i64, - #[serde(skip_serializing_if = "Option::is_none")] - pub uploaded_at: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct VoiceRecordingListResponse { - pub items: Vec, - #[serde(rename = "nextCursor")] - pub next_cursor: Option, - #[serde(rename = "hasMore")] - pub has_more: bool, -} diff --git a/services/frontend/shared-types/src/ui_state.rs b/services/frontend/shared-types/src/ui_state.rs deleted file mode 100644 index f0eb826..0000000 --- a/services/frontend/shared-types/src/ui_state.rs +++ /dev/null @@ -1,29 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum Tab { - Messages, - Live, - Dashboard, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UiState { - #[serde(skip_serializing_if = "Option::is_none")] - pub selected_guild: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub selected_voice_guild: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub selected_voice_channel: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub selected_text_guild: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub selected_text_channel: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub active_tab: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub is_listening: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub is_streaming: Option, -} diff --git a/services/frontend/shared-types/src/voice.rs b/services/frontend/shared-types/src/voice.rs deleted file mode 100644 index 5d0329e..0000000 --- a/services/frontend/shared-types/src/voice.rs +++ /dev/null @@ -1,26 +0,0 @@ -use serde::{Deserialize, Serialize}; -use crate::guild::GuildVoiceEntry; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct VoiceStatus { - pub connected: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub active_guild_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub active_channel_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub active_channel_name: Option, - pub connections: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ActiveSpeaker { - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, - pub user_id: String, - pub username: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub avatar: Option, - pub speaking: bool, -} diff --git a/services/frontend/src/app/dashboard/layout.tsx b/services/frontend/src/app/dashboard/layout.tsx new file mode 100644 index 0000000..16ae6a2 --- /dev/null +++ b/services/frontend/src/app/dashboard/layout.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; +import { Suspense, useEffect, useRef } from "react"; +import { uiStateApi } from "@/lib/api"; +import { Header } from "@/components/layout/header"; +import { MobileTabBar } from "@/components/layout/mobile-tab-bar"; +import { Sidebar } from "@/components/layout/sidebar"; +import { MascotChatbot } from "@/features/mascot/mascot-chatbot"; +import { AuthProvider, useAuth } from "@/lib/hooks/use-auth"; +import { WsProvider } from "@/lib/ws/context"; + +function DashboardGuard({ children }: { children: React.ReactNode }) { + const { authenticated, loading } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (!loading && !authenticated) { + router.push("/"); + } + }, [authenticated, loading, router]); + + if (loading) { + return ( +
+
+
+ ); + } + + if (!authenticated) return null; + + return <>{children}; +} + +function DashboardShell({ children }: { children: React.ReactNode }) { + const searchParams = useSearchParams(); + const router = useRouter(); + const restored = useRef(false); + + const activeTab = (searchParams.get("tab") ?? "messages") as + | "messages" + | "live" + | "dashboard"; + + // Restore persisted tab on mount (only if no explicit tab in URL) + useEffect(() => { + if (restored.current) return; + const tabParam = searchParams.get("tab"); + if (tabParam) { + restored.current = true; + return; // explicit tab in URL — don't override + } + uiStateApi + .get() + .then((state) => { + restored.current = true; + const savedTab = state.active_tab; + if (savedTab && savedTab !== activeTab) { + router.replace(`/dashboard?tab=${savedTab}`); + } + }) + .catch(() => { + restored.current = true; + }); + }, [searchParams, activeTab, router]); + + // Persist tab changes + useEffect(() => { + if (!restored.current) return; + uiStateApi.save({ active_tab: activeTab }).catch(() => {}); + }, [activeTab]); + + return ( +
+ +
+
+
{children}
+
+ +
+ ); +} + +export default function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + +
+
+ } + > + {children} +
+ +
+
+
+ ); +} diff --git a/services/frontend/src/app/dashboard/page.tsx b/services/frontend/src/app/dashboard/page.tsx new file mode 100644 index 0000000..9e0ea5a --- /dev/null +++ b/services/frontend/src/app/dashboard/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import { DashboardPanel } from "@/features/dashboard/dashboard-panel"; +import { LivePanel } from "@/features/live/live-panel"; +import { MessagesPanel } from "@/features/messages/messages-panel"; + +export default function DashboardPage() { + const searchParams = useSearchParams(); + const tab = searchParams.get("tab") ?? "messages"; + + switch (tab) { + case "live": + return ; + case "dashboard": + return ; + default: + return ; + } +} diff --git a/services/frontend/src/app/favicon.ico b/services/frontend/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/services/frontend/src/app/favicon.ico differ diff --git a/services/frontend/src/app/globals.css b/services/frontend/src/app/globals.css new file mode 100644 index 0000000..6a25b1b --- /dev/null +++ b/services/frontend/src/app/globals.css @@ -0,0 +1,130 @@ +@import "tailwindcss"; +@import "tw-animate-css"; +@import "shadcn/tailwind.css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-sans); + --font-mono: var(--font-geist-mono); + --font-heading: var(--font-sans); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); +} + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + html { + @apply font-sans; + } +} diff --git a/services/frontend/src/app/layout.tsx b/services/frontend/src/app/layout.tsx new file mode 100644 index 0000000..ba7b85f --- /dev/null +++ b/services/frontend/src/app/layout.tsx @@ -0,0 +1,46 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Bete — Discord Moderation Dashboard", + description: "Live Discord monitoring and AI moderation dashboard", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + +