diff --git a/docs-site/src/content/docs/getting-started/quickstart.md b/docs-site/src/content/docs/getting-started/quickstart.md index 11a9a7a1..a3b8553f 100644 --- a/docs-site/src/content/docs/getting-started/quickstart.md +++ b/docs-site/src/content/docs/getting-started/quickstart.md @@ -63,6 +63,16 @@ curl -sS "http://127.0.0.1:/healthz" The proxy exposes `/healthz` and `/v1/*` for any Responses-compatible client. You do not need Codex installed for this step. +The pending dashboard redesign keeps the same routes and API. Its desktop navigation +uses a side rail; smaller screens show all six destinations in a labelled two-row grid. +Overview, Providers & accounts, Models, Traffic, Usage and System remain available, including +existing bookmarked hashes. Navigation motion respects reduced-motion preferences. This +interface work does not change provider credentials or the proxy's deployment state. +Settings opens a keyboard-accessible panel for language and light/dark/system theme. +The legacy skin selector is removed; stored skin preferences are retained for compatibility. +Unavailable dashboard readings display a dash rather than an invented zero; failures remain +visible alongside any previously fetched readings. + ## 4. Use Codex (optional) If you enabled Codex injection in step 1, Codex talks to opencodex transparently: diff --git a/docs-site/src/content/docs/guides/sub-agent-surface.md b/docs-site/src/content/docs/guides/sub-agent-surface.md index afb53480..f0583436 100644 --- a/docs-site/src/content/docs/guides/sub-agent-surface.md +++ b/docs-site/src/content/docs/guides/sub-agent-surface.md @@ -11,11 +11,11 @@ On the v2 surface (`multi_agent_v2`), a spawned sub-agent inherits the parent mo ## Modes -| Mode | Surface | Behavior | -| --- | --- | --- | -| **v1** | `multi_agent_v1` | Classic namespaced agent tools with `send_input` / `close_agent` / `resume_agent`. A `spawn_agent` model override can start a sub-agent on a different model. | -| **base** (default) | Upstream pins | Restores upstream model pins: gpt-5.6-sol and gpt-5.6-terra use v2, gpt-5.6-luna uses v1, and unpinned models follow the Codex `multi_agent_v2` feature flag. Spawn behavior follows the surface that resolves for that model. | -| **v2** | `multi_agent_v2` | Flat `spawn_agent` tools with concurrent sessions and `send_message` / `followup_task` / `wait_agent` / `interrupt_agent`. Children inherit the parent model on full-history forks; `fork_turns: "none"` (or a partial fork) accepts `model` / `reasoning_effort` overrides. If a native→routed child receives only backend-encrypted task content, external routes return `unreadable_encrypted_agent_task`; mixed combos prefer a decrypt-capable native target ([#92](https://github.com/GroepOnline/opencodex/issues/92)). | +| Mode | Surface | Behavior | +| ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **v1** | `multi_agent_v1` | Classic namespaced agent tools with `send_input` / `close_agent` / `resume_agent`. A `spawn_agent` model override can start a sub-agent on a different model. | +| **base** (default) | Upstream pins | Restores upstream model pins: gpt-5.6-sol and gpt-5.6-terra use v2, gpt-5.6-luna uses v1, and unpinned models follow the Codex `multi_agent_v2` feature flag. Spawn behavior follows the surface that resolves for that model. | +| **v2** | `multi_agent_v2` | Flat `spawn_agent` tools with concurrent sessions and `send_message` / `followup_task` / `wait_agent` / `interrupt_agent`. Children inherit the parent model on full-history forks; `fork_turns: "none"` (or a partial fork) accepts `model` / `reasoning_effort` overrides. If a native→routed child receives only backend-encrypted task content, external routes return `unreadable_encrypted_agent_task`; mixed combos prefer a decrypt-capable native target ([#92](https://github.com/GroepOnline/opencodex/issues/92)). | ### Encrypted v2 task delivery @@ -118,12 +118,12 @@ The optional sub-agent effort setting is stored as `injectionEffort` and is mean `ultra` ranks above `max` in the Codex catalog and adds automatic-delegation semantics, but it never reaches a provider as a literal wire value. Codex converts `ultra` to `max` at the client boundary. opencodex then keeps the provider request valid: -| Model | `max` on wire | `ultra` selection on wire | -| --- | --- | --- | -| gpt-5.5, gpt-5.4, gpt-5.4-mini | xhigh | xhigh (via max, then `nativeEffortClamp`) | -| gpt-5.6-sol, gpt-5.6-terra | max | max | -| gpt-5.6-luna | max | Not advertised by its exact upstream ladder | -| Routed models | Mapped or clamped by the adapter | Converted to max, then mapped or clamped by the adapter | +| Model | `max` on wire | `ultra` selection on wire | +| ------------------------------ | -------------------------------- | ------------------------------------------------------- | +| gpt-5.5, gpt-5.4, gpt-5.4-mini | xhigh | xhigh (via max, then `nativeEffortClamp`) | +| gpt-5.6-sol, gpt-5.6-terra | max | max | +| gpt-5.6-luna | max | Not advertised by its exact upstream ladder | +| Routed models | Mapped or clamped by the adapter | Converted to max, then mapped or clamped by the adapter | Catalog availability is independent of the v1/v2 mode. Reasoning-capable generated entries advertise `max` so direct sub-agent effort overrides validate; current generated routed entries also advertise `ultra`. Exact upstream model ladders are preserved, which is why gpt-5.6-luna stops at `max`. @@ -132,3 +132,7 @@ Catalog availability is independent of the v1/v2 mode. Reasoning-capable generat The global context cap value defaults to 350k and limits the advertised `context_window` only for routed providers whose cap is enabled. Native OpenAI models keep their real context windows. Change the value or the all-provider setting in the Models page, or toggle the cap next to an individual provider group header. + +In the pending catalog redesign, global controls are under **Model settings & +advanced controls**, and each group's cap toggle is under **Provider settings**. +Only their placement changes; the cap and multi-agent behavior above is unchanged. diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index 4066199e..e0627559 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -33,24 +33,24 @@ auto-generated `~/.opencodex/admin-api-token` file). ## What you can do -| Area | What it does | -| --- | --- | -| **Dashboard summary** | Multi-agent mode, online state, version, uptime, provider count, 30-day token total, estimated cost, usage coverage, 429/502 ratios, active providers, and available native/routed models. | -| **Sub-agent delegation** | Choose a native or routed model and optional reasoning effort shared by OpenCodex delegation guidance and the separate native-default opt-in. This is not a proxy-side per-spawn router; see below. | -| **Sidecars** | Choose the web-search model and effort plus the vision-description model. Changes apply on the next request. | -| **Maintenance** | Resync the Codex model catalog, inspect project-local config bypass warnings, check the latest or preview release, and run an update with optional proxy restart. | -| **Startup safety** | Show whether injected Codex routing survives a restart, with separate service and launcher-shim health plus exact repair commands. | -| **Windows tray** | Install a per-user login tray for one-click proxy start, stop, restart, dashboard access, and status. The tray is a controller, not a proxy restart service. | -| **Codex autostart** | Allow an already-installed Codex launcher shim to run `ocx ensure`. This toggle does not install a shim or background service. | -| **Providers** | Add, edit, enable/disable, and remove providers; manage OAuth account pools and API-key pools where supported. Provider Settings can disable live model discovery for endpoints with missing, slow, or oversized `/models` catalogs. Claude, Cursor, and Google Antigravity OAuth surfaces each have an experimental account-pool card. Claude and Cursor show per-account usage bars (usage is per credential); a failed probe keeps the last-known bars and marks them unavailable until the next successful refresh. Cursor accepts a second account via browser login or a pasted user API key (JWT) on the same login surface. The pool turns on automatically when a second account is added; you can still turn it off. Cooldown rows have a clear-cooldown control. | -| **Add provider** | Search registry-backed presets for account login, API-key services, local servers, or a custom endpoint. | -| **Codex Auth** | Add ChatGPT/Codex pool accounts, select the next-session account, refresh 5h / weekly / 30d quotas, enable or disable quota auto-switch, set its 1–100% threshold, and configure transient-failure failover. | -| **Subagents** | Feature up to five bare native or namespaced routed models in the `spawn_agent` override list. | -| **Models** | Toggle native GPT and routed models, set provider allowlists and context caps, choose v1/base/v2, and configure the v2 thread limit. Configured providers stay visible as zero-model groups when discovery is off or returns no rows. | -| **Logs** | Auto-refresh recent requests with tokens, requested effort and (when available) effective outbound effort, resolved model, provider, status, request id, duration, and error details. The detail view includes the exact reasoning wire field when the adapter emits one. Filter by opaque conversation/session id (when the client sends one) to total tokens and estimated list-price cost for the currently loaded Logs ring. | -| **Usage / Debug** | Inspect token-usage coverage and trends, or enable opt-in provider transport and usage-extraction diagnostics. | -| **Storage** | Read-only CODEX_HOME disk breakdown (sessions, archives, DBs, attachments). Optional archived cleanup: preview the oldest N%, then quarantine to `CODEX_HOME/.trash` (default) or permanently delete behind an explicit checkbox. **Auto-cleanup policy** is opt-in and **default OFF** (`storageCleanupPolicy.enabled`); configure threshold/target/schedule/mode on the Storage page, or trigger **Run now**. Quarantined entries can be restored from the Storage page (JSONL + threads). Active sessions stay read-only. Cleanup and restore are refused while Codex holds the newest/active `state_*.sqlite` locked. | -| **Stop** | Gracefully stop the proxy and installed background service, restore native Codex, and exit (`POST /api/stop`). | +| Area | What it does | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Dashboard summary** | Multi-agent mode, online state, version, uptime, provider count, 30-day token total, estimated cost, usage coverage, 429/502 ratios, active providers, and available native/routed models. | +| **Sub-agent delegation** | Choose a native or routed model and optional reasoning effort shared by OpenCodex delegation guidance and the separate native-default opt-in. This is not a proxy-side per-spawn router; see below. | +| **Sidecars** | Choose the web-search model and effort plus the vision-description model. Changes apply on the next request. | +| **Maintenance** | Resync the Codex model catalog, inspect project-local config bypass warnings, check the latest or preview release, and run an update with optional proxy restart. | +| **Startup safety** | Show whether injected Codex routing survives a restart, with separate service and launcher-shim health plus exact repair commands. | +| **Windows tray** | Install a per-user login tray for one-click proxy start, stop, restart, dashboard access, and status. The tray is a controller, not a proxy restart service. | +| **Codex autostart** | Allow an already-installed Codex launcher shim to run `ocx ensure`. This toggle does not install a shim or background service. | +| **Providers** | Add, edit, enable/disable, and remove providers; manage OAuth account pools and API-key pools where supported. Provider Settings can disable live model discovery for endpoints with missing, slow, or oversized `/models` catalogs. Claude, Cursor, and Google Antigravity OAuth surfaces each have an experimental account-pool card. Claude and Cursor show per-account usage bars (usage is per credential); a failed probe keeps the last-known bars and marks them unavailable until the next successful refresh. Cursor accepts a second account via browser login or a pasted user API key (JWT) on the same login surface. The pool turns on automatically when a second account is added; you can still turn it off. Cooldown rows have a clear-cooldown control. | +| **Add provider** | Search registry-backed presets for account login, API-key services, local servers, or a custom endpoint. | +| **Codex Auth** | Add ChatGPT/Codex pool accounts, select the next-session account, refresh 5h / weekly / 30d quotas, enable or disable quota auto-switch, set its 1–100% threshold, and configure transient-failure failover. | +| **Subagents** | Feature up to five bare native or namespaced routed models in the `spawn_agent` override list. | +| **Models** | Toggle native GPT and routed models, set provider allowlists and context caps, choose v1/base/v2, and configure the v2 thread limit. Configured providers stay visible as zero-model groups when discovery is off or returns no rows. | +| **Logs** | Auto-refresh recent requests with tokens, requested effort and (when available) effective outbound effort, resolved model, provider, status, request id, duration, and error details. The detail view includes the exact reasoning wire field when the adapter emits one. Filter by opaque conversation/session id (when the client sends one) to total tokens and estimated list-price cost for the currently loaded Logs ring. | +| **Usage / Debug** | Inspect token-usage coverage and trends, or enable opt-in provider transport and usage-extraction diagnostics. | +| **Storage** | Read-only CODEX_HOME disk breakdown (sessions, archives, DBs, attachments). Optional archived cleanup: preview the oldest N%, then quarantine to `CODEX_HOME/.trash` (default) or permanently delete behind an explicit checkbox. **Auto-cleanup policy** is opt-in and **default OFF** (`storageCleanupPolicy.enabled`); configure threshold/target/schedule/mode on the Storage page, or trigger **Run now**. Quarantined entries can be restored from the Storage page (JSONL + threads). Active sessions stay read-only. Cleanup and restore are refused while Codex holds the newest/active `state_*.sqlite` locked. | +| **Stop** | Gracefully stop the proxy and installed background service, restore native Codex, and exit (`POST /api/stop`). | ### Linking to a section @@ -70,6 +70,34 @@ may apply instead. ## Model visibility +### Pending catalog workspace redesign + +The redesign branch replaces the provider side rail and model hover cards with a +searchable catalog and persistent model inspector. This interface is pending +release; an installed version may still have the earlier layout. + +- Search matches model IDs, display names and provider names. The provider filter + narrows the same catalog. Search also finds models in collapsed groups. +- Select a model row to inspect its metadata without changing configuration. The + separate visibility switch and **In model picker** control use the same final + visibility rules below. Catalog metadata does not prove live account access or + which model was actually served; missing metadata is shown as **Not reported**. +- On narrow screens, details replace the list. **Back to models** restores the + list and focus. Custom-model edit and delete actions are in the inspector. +- **Copy ID** copies the routed provider-qualified ID, or the bare ID for a native + model. Clipboard failures are reported rather than shown as successful copies. + Wide catalogs include a modalities column; details retain this data on mobile. +- Open **Provider settings** in a group for refresh, custom-model creation, bulk + visibility and its context-cap toggle. Bulk actions affect that entire provider, + not just search results. Empty providers cannot send an empty bulk operation. +- Global caps, shadow-call settings, v1/base/v2 controls and combo summaries remain + under **Model settings & advanced controls**. Existing route bookmarks still work. +- New users see expanded provider groups. Saved collapse preferences are retained. +- The pending catalog uses shared library controls, including a provider dropdown, + a search field with a clear action, and collapsible provider/global settings. + Refresh and save controls show a spinner only while their real request is pending; + reduced-motion mode keeps the loading text without rotation. + The **Models** switches show final Codex visibility: a routed model is on only when its provider allowlist includes it (or no allowlist is set) and it is not disabled. Turning a model on reconciles both filters atomically; **All on** clears the provider allowlist so newly discovered models are also on. ## Delegation picker vs spawn routing @@ -124,29 +152,29 @@ The **Codex Auth** page manages the native ChatGPT/Codex route: The GUI is a thin client over the proxy's JSON management API. Useful endpoints include: -| Endpoint | Purpose | -| --- | --- | -| `GET` / `PUT /api/settings` | Read settings or toggle Codex autostart. | -| `GET /api/startup-health` | Read secret-free routing, service, shim, and restart-safety diagnostics. | -| `POST /api/startup-action` | Install the background service or Codex launcher shim through fixed, allowlisted actions. | -| `GET` / `POST /api/windows-tray` | Read or change the Windows tray installation and visible-process state. POST accepts `install`, `start`, `stop`, or `uninstall`. | -| `POST /api/sync` | Rebuild the shared model catalog and stale the Codex model cache. | -| `POST /api/providers/models/refresh?name=...` | Force-fetch one provider's live `/models` list (ignores cache and failure cooldown) and resync the Codex catalog. Dashboard **Fetch models** uses this. | -| `GET /api/update/check` · `POST /api/update/run` · `GET /api/update/status` | Check, run, and monitor self-update jobs. Worker PIDs are persisted so a crashed job recovers automatically; legacy no-PID jobs recover after ten minutes. | -| `GET` / `PUT /api/sidecar-settings` | Read or set search/vision sidecar model settings. | -| `GET` / `PUT /api/injection-model` | Read or set the shared sub-agent model/effort selection and the independent guidance/native-default switches. | -| `GET` / `PUT /api/v2` | Read or set the surface mode, Codex feature flag, and v2 thread limit. | +| Endpoint | Purpose | +| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GET` / `PUT /api/settings` | Read settings or toggle Codex autostart. | +| `GET /api/startup-health` | Read secret-free routing, service, shim, and restart-safety diagnostics. | +| `POST /api/startup-action` | Install the background service or Codex launcher shim through fixed, allowlisted actions. | +| `GET` / `POST /api/windows-tray` | Read or change the Windows tray installation and visible-process state. POST accepts `install`, `start`, `stop`, or `uninstall`. | +| `POST /api/sync` | Rebuild the shared model catalog and stale the Codex model cache. | +| `POST /api/providers/models/refresh?name=...` | Force-fetch one provider's live `/models` list (ignores cache and failure cooldown) and resync the Codex catalog. Dashboard **Fetch models** uses this. | +| `GET /api/update/check` · `POST /api/update/run` · `GET /api/update/status` | Check, run, and monitor self-update jobs. Worker PIDs are persisted so a crashed job recovers automatically; legacy no-PID jobs recover after ten minutes. | +| `GET` / `PUT /api/sidecar-settings` | Read or set search/vision sidecar model settings. | +| `GET` / `PUT /api/injection-model` | Read or set the shared sub-agent model/effort selection and the independent guidance/native-default switches. | +| `GET` / `PUT /api/v2` | Read or set the surface mode, Codex feature flag, and v2 thread limit. | | `GET /api/providers` · `POST /api/providers` · `PATCH /api/providers?name=...` · `DELETE /api/providers?name=...` | List, add/replace, partially edit, or remove providers. PATCH accepts `noReasoningModels` as an array of non-empty model strings; `[]` clears the opt-out, while omitted fields and stored credentials are preserved. API keys must use the dedicated key endpoints. Never POST a masked GET response back as provider configuration. | -| `GET /api/models` · `PUT /api/disabled-models` | List native/routed model rows and update the shared disabled-model set. | -| `GET /api/selected-models` · `PUT /api/model-visibility` | Read provider allowlists and atomically change the final visibility of one model or provider group. | -| `GET /api/key-providers` · `GET /api/oauth/providers` | Read the API-key and OAuth provider catalogs. | -| `POST /api/oauth/login` · `GET /api/oauth/status` | Start a provider OAuth flow and poll for completion. | -| `GET /api/codex-auth/accounts?refresh=1` | List main and pool accounts, force quota refresh, and report main-account `hasCredential` / terminal `needsReauth` state. | -| `PUT /api/codex-auth/active` · `PUT /api/codex-auth/auto-switch` · `PUT /api/codex-auth/failover` | Select the account for the next request and configure pool routing. | -| `POST /api/codex-auth/login` · `GET /api/codex-auth/login-status` | Add a pool account through browser login. | -| `GET /api/logs?tail=50&provider=...&status=5xx` | Read recent request metadata with optional tail, provider, and exact/class status filters. | -| `GET` / `PUT /api/subagent-models` | Read or set the five featured `spawn_agent` override models. | -| `POST /api/stop` | Stop the proxy/service, restore native Codex, and exit. | +| `GET /api/models` · `PUT /api/disabled-models` | List native/routed model rows and update the shared disabled-model set. | +| `GET /api/selected-models` · `PUT /api/model-visibility` | Read provider allowlists and atomically change the final visibility of one model or provider group. | +| `GET /api/key-providers` · `GET /api/oauth/providers` | Read the API-key and OAuth provider catalogs. | +| `POST /api/oauth/login` · `GET /api/oauth/status` | Start a provider OAuth flow and poll for completion. | +| `GET /api/codex-auth/accounts?refresh=1` | List main and pool accounts, force quota refresh, and report main-account `hasCredential` / terminal `needsReauth` state. | +| `PUT /api/codex-auth/active` · `PUT /api/codex-auth/auto-switch` · `PUT /api/codex-auth/failover` | Select the account for the next request and configure pool routing. | +| `POST /api/codex-auth/login` · `GET /api/codex-auth/login-status` | Add a pool account through browser login. | +| `GET /api/logs?tail=50&provider=...&status=5xx` | Read recent request metadata with optional tail, provider, and exact/class status filters. | +| `GET` / `PUT /api/subagent-models` | Read or set the five featured `spawn_agent` override models. | +| `POST /api/stop` | Stop the proxy/service, restore native Codex, and exit. | :::tip Adding **Ollama Cloud** or another catalog provider from the dashboard copies its text-versus-vision diff --git a/gui/AGENTS.md b/gui/AGENTS.md index 9f413539..3b8db2b9 100644 --- a/gui/AGENTS.md +++ b/gui/AGENTS.md @@ -45,7 +45,7 @@ Run all of the following for every functional `gui/` change: ```bash cd gui -bun test tests +bun run test bun run lint bun run build ``` @@ -58,3 +58,8 @@ bun run lint:i18n ``` Run the repository-level checks required by any non-GUI files changed in the same work. + +Use the package test script, including its existing `--isolate` option. DOM and +server-rendering tests need separate module caches: Base UI chooses its layout +effect implementation at import time. Do not remove isolation or mock that +behavior to make shared-cache test runs pass. diff --git a/gui/DESIGN.md b/gui/DESIGN.md index cde54a9b..0de0481f 100644 --- a/gui/DESIGN.md +++ b/gui/DESIGN.md @@ -1,8 +1,193 @@ -# gui/DESIGN.md — de ontwerptaal van het dashboard +# OCX — productinterface in herontwerp + +## Actuele autoriteit: expliciete redesignopdracht 2026-09-06 + +Joep heeft voor OCX expliciet een volledig nieuwe identiteit gevraagd: niet de +bestaande ChefGroep/Signaal-stijl, maar moderner, componentgedreven, met betere +verhoudingen en meer motion. Deze sectie vervangt daarom de visuele voorschriften +hieronder voor de nieuwe interface. De historische Signaal-beschrijving blijft +alleen migratiecontext, niet een reden om nieuw werk terug te draaien. Andere +ChefGroep-producten en het gedeelde design-system worden hiermee niet gewijzigd. + +### Richting en componenten + +Zowel de eerste inkt/mint-richting (werknaam Orbit) als de daaropvolgende blauwe +dashboardcorrectie zijn door Joep afgewezen als generiek. De bestaande runtime is +dus geen visueel geaccepteerd ontwerp. Blauw en paars zijn uitgesloten als merk-, +actie-, selectie- en focusaccent voor het herontwerp; een andere accentkleur op +dezelfde dashboardcompositie is geen oplossing. Het product blijft opencodex. + +De catalogusslice vertrekt vanuit het dagelijkse werk: een model vinden, +de bijbehorende provider begrijpen en de bestaande instellingen aanpassen. Een +doorzoekbare modellenlijst met contextueel detail krijgt voorrang boven een +overzicht van zeven even grote getallen. Bestaande verkeer- en gebruiksschermen +blijven bereikbaar; metrics verdwijnen niet, maar bepalen niet automatisch de +hele werkinterface. Geen verzonnen routinggrafiek of nieuwe backend hiervoor. + +De voorgestelde basis is monochroom: wit `#ffffff`, lichtgrijs `#f1f1f1`, lijn +`#d4d4d4`, secundaire inkt `#656565`, donker oppervlak `#262626`, inkt `#161616`. +Dit zijn de basisrollen van de branch, geen uitgerolde productie-identiteit. Primaire acties +gebruiken tekst/achtergrond-inversie; selectie gebruikt neutraal vlak plus gewicht +of contour. Groen, amber en rood blijven betekenisvolle statussen met tekst, +niet een nieuw merkpalet. Toets focus en tekstcontrast in beide thema's. +Manrope blijft voorlopig voor interfacecopy, JetBrains Mono voor machinedata: +een nieuwe fontdependency is niet de oplossing voor gebrekkige hiërarchie. + +### Onderzoeksbasis en wat we niet kopiëren + +- [Raycast, A fresh look and feel (2022)](https://www.raycast.com/blog/a-fresh-look-and-feel): + zoekveld en contextacties krijgen prioriteit; de actiebalk maakt verborgen + handelingen vindbaar. [Raycast 2.0 (2026)](https://www.raycast.com/blog/the-new-raycast) + behoudt deze actiegerichte opbouw. Geen glaslaag of wallpaper overnemen. +- [Resend, domeinverificatie (2023)](https://resend.com/blog/new-domain-verification-experience): + uitleg en feedback sluiten aan op de gekozen provider en individuele records. + [Productrebranding (2025)](https://resend.com/blog/rebranding-resend) toont + objectlijsten als hoofdinhoud. Niet hun logo, serifkoppen of statuskleuren kopiëren. +- [GitButler 0.15 (2025)](https://blog.gitbutler.com/gitbutler-15-quirky-quinceanera): + werkobjecten en bewerkingen bepalen de ruimte; herstel en selectie zijn onderdeel + van de interface. Geen commit-lanes nabouwen voor modellen zonder zo'n workflow. + +Deze bronnen zijn gedateerde primaire ontwerpvoorbeelden, geen bewijs dat ieder +detail vandaag ongewijzigd is of dat hun vormgeving automatisch bij OCX past. +Ook zwart/neon, crème/terracotta en een krantachtig raster kunnen templates zijn. +Componentlibraries leveren gedrag en consistentie, niet de productcompositie. + +Aanvullende referenties van Joep voor de polishlaag: + +- [Kinetics](https://kinetics.colorion.co/#library): retargetbare selectie en + beperkte microfeedback. Geen magneetknoppen, springende cijfers of gekopieerde + height/left-animaties; de bestaande Motion-laag blijft eigenaar van navigatie. +- [sv-matrix](https://sv-matrix.vercel.app/): een 5×5 midden-naar-buiten motief in + het lege detailpaneel. `MatrixMark` is een eigen React/SVG-implementatie van het + motief, geen Svelte-runtime of overgenomen upstreamcomponent. Eén opacity-reveal + per lege staat, drie ringgroepen, klaar binnen 820ms; geen loader of live-status. +- [Libraries.dev Beam](https://libraries.dev/beam): gerichte randfeedback bij een + actie. Hier een monochrome lijn langs het model-ID na bewezen kopieersucces, + met transform/opacity en zonder loop, shader, gradient of extra dependency. +- `GroepOnline/design-system` op `2a8da31`: `DESIGN.md` §15–16, + `taste/taste-rules.md` en `surfaces/auth-landing.md` bieden relevante + product-/eerste-indrukdiscipline. Hun historische blauwe accent, loginontwerp + en branding worden niet naar OCX gekopieerd. Die repo blijft ongewijzigd. + +De matrix is verborgen voor assistieve technologie. Reduced-motion toont de +statische eindstaat; toetsenbordgestuurde kopieerfeedback heeft geen lijnbeweging. +Kopieersucces blijft daarnaast leesbare tekst met een check-icoon, nooit alleen motion. + +- `WorkspaceNavigation`: één getypeerde bestemmingencatalogus, iconen, labels, + actieve pagina en een gedeelde Motion-selectie. Bestaande hashes blijven werken. +- Shadcn `base-nova` op Base UI levert de gedeelde Button, Sheet, Empty en + ToggleGroup in `src/components/primitives/`. Geen volledig preset of tweede + palet. Bestaande controls, waaronder de authored Select in `src/ui.tsx`, blijven + eigenaar totdat hun workflow bewust wordt gemigreerd. Zie `UX-CONTRACT.md`. +- Motion for React (`LazyMotion`) verzorgt gedeelde layoutovergangen. CSS behandelt + eenvoudige hover/press-feedback. Geen React-state-updates per animatieframe. +- Nieuwe dependencies zijn exact gepind en vereisen de bestaande security-review. +- ThreeUI blijft voorlopig eigenaar van de bestaande publieke landing. Die landing + is nog niet hetzelfde als een volledig herontworpen productpagina. + +### Library-componentmigratie — 2026-09-07 + +Joep vroeg expliciet om daadwerkelijke library-buttons, controls, motion en +spinners, niet alleen inspiratie of losse effecten. De catalogusslice gebruikt +nu officiële shadcn `base-nova`-broncomponenten: Button, InputGroup, Select, Badge, +Switch, Accordion, Field, Alert, Empty, Separator en Spinner. Input/Label/Textarea +zijn hun gedeelde onderbouw. Bestaande Sheet/ToggleGroup blijven in gebruik. + +De nieuwe controls vervangen markup in Models en ModelInspector; ze zijn niet +alleen geïnstalleerd. De overige legacy-schermen zijn nog geen volledige migratie. +De pagina houdt state, API-mutaties, selectie en polling; providerkaart/rij/settings, +geavanceerde controls, combo-overzicht en modals zijn afzonderlijke gecontroleerde +componenten. Inspectorvelden zijn eveneens los samengesteld, terwijl focus en +kopieerfeedback hun bestaande lifecycle-eigenaar behouden. +Modelkeuze en API-mutaties blijven eigendom van de bestaande handlers. Catalogus- +visibility gebruikt Base UI `role="switch"`/`aria-checked`; selectie houdt +`aria-pressed`. De providerfilter krijgt een portaled Base UI Select. Legacy +Selects binnen geavanceerde controls en Settings worden niet stilzwijgend vervangen. + +Spinners zijn door deze expliciete opdracht toegestaan bij echte pending acties: +initiële catalogusload, providerrefresh en custom-model opslaan. Geen loader in +rust, geen kunstmatige minimumduur; tekst blijft aanwezig en reduced-motion stopt +de rotatie. Accordion-motion volgt de Base UI-paneelhoogte, 180ms, geen max-height +hack; keyboard/reduced-motion opent zonder beweging. Dit verfijnt de eerdere +spinner-ban uitsluitend voor deze gevraagde OCX-opdracht. + +Registrybron gelezen via CLI/docs. Aanpassingen blijven in de componentlaag: +geen `transition-all`, geen tweede dark-palet, gelokaliseerde spinner, een expliciete +44×44 touchvariant voor Switch, en transparante Accordion-knoppen zonder afhankelijkheid +van Tailwind preflight. Bronlicentie wordt meegebouwd uit `public/third-party-notices.txt`. +Lucide React is exact gepind op 1.41.0; geen nieuwe transitieve runtimepackages. + +### Geometrie en responsiviteit (actueel) + +De catalogusslice heeft een werkvlak tot 1800px en een desktopheader van minimaal +64px met merk, globale navigatie en status op één regel. Onder 1280px krijgt de +navigatie een eigen regel. De modellenwerkplek gebruikt een vergelijkbare lijst +en een detailpaneel van 320–400px met een vaste scheidingslijn. Geen drie smalle +zijbalken of een mini-tabel in een grote kaart. Links uitlijnen, model-id's leesbaar +houden en numerieke kolommen uitlijnen. Begin bij 14px interfacecopy, 12px metadata +en 24px paginatitel; toets lange namen en EN/NL vóór verdere verfijning. +Controls mogen compact zijn op desktop, maar primaire touch-acties blijven minimaal +44px. Mobiel toont lijst of detail met een expliciete terugactie en focusherstel, +niet een verkleinde desktop. Bestaande hashes en bestemmingen blijven bereikbaar. +Geen paginabrede horizontale overflow of acties die alleen op hover bestaan. + +### Toestanden en motion + +Echte API-status blijft leidend: laden, leeg, gedeeltelijk, fout en offline mogen +niet worden vervangen door mooie voorbeeldcijfers. Catalogusvermelding is geen +bewijs van bereikbaarheid, beschikbaar account of daadwerkelijk geserveerd model. +Toon alleen relaties/statussen waarvoor de bestaande API bewijs levert. +Selectiefeedback is direct; een detailovergang mag kort en onderbreekbaar zijn, +zonder de lijst te laten verspringen. Toetsenbordnavigatie en reduced-motion slaan +verplaatsing over. Geen automatische page-reveal of pressed-translate in nieuwe controls. +Sheet opent maximaal 200ms via transform/opacity; toetsenbordopening zonder +verplaatsing, reduced-motion alleen korte opacity-feedback. Focus, contrast en +labels blijven zichtbaar in licht én donker. Onbekende waarden zijn geen nullen. + +### Token-eigendom en adapters + +Runtime CSS is canoniek, niet een gegenereerde tweede tokenbron: + +| Eigenaar | Verantwoordelijkheid | +| -------------------------------- | --------------------------------------------------------------------------------- | +| `src/styles.css` | Bestaande reset, spacing, semantische status en legacy controls | +| `src/styles/workspace-orbit.css` | Actuele kleur-, type-, geometrie- en scrollbarrollen; bestandsnaam is geen merk | +| `src/styles/app-base.css` | Cascadevolgorde; legacy CSS in eigen laag | +| `src/styles/primitives.css` | Tailwind-semantieken verwijzen naar runtime-rollen; geen preflight of eigen palet | +| `components.json` | Officiële shadcn-registry, Base UI-style en gedeelde componentpaden | + +Tailwind `text-xs/sm/base` verwijzen naar `--text-caption/control/body` met hun +line-height. Geen rem-afhankelijke tweede letterladder. Historische `devin/strak` +waarden blijven leesbaar voor compatibiliteit, maar de settings-skinselector is +verwijderd: de nieuwe tokens oversturen beide skins en twee gelijke keuzes zijn +misleidend. Licht/donker/systeem en EN/NL blijven beschikbaar. + +### Implementatiestatus en acceptatie + +De eerste dashboardcompositie is visueel afgewezen. De daaropvolgende +modellenwerkplek is geïmplementeerd in de redesignbranch, nog niet door Joep +visueel geaccepteerd of uitgerold. `ModelInspector.tsx` vervangt de hoverkaart; +`styles/model-catalog.css` is eigenaar van lijst/detail-geometrie. Zoekfilter en +providerkeuze staan boven de lijst; geavanceerde bediening blijft in disclosures. +Brede schermen tonen modaliteiten als vergelijkingskolom; smallere schermen houden +die informatie in het detailpaneel. Model-ID kopiëren gebruikt de bestaande +clipboardlaag, met succes- en foutfeedback die bij de geselecteerde identiteit horen. +De bestaande API-, locale- en visibilitylogica blijven eigenaar. Verwijderde +selecties worden bij verversen gewist; focus wordt hersteld als die verloren raakt. +Detailpanelen, volledige componentmigratie, landing en volledige visuele acceptatie +blijven expliciete vervolgstappen. Brave-browserchecks van deze slice zijn geen +productie- of brede toegankelijkheidscertificering. Niet als volledig +redesign of live release presenteren zolang die niet aantoonbaar zijn afgerond. +Tokens: `src/styles/workspace-orbit.css`; die worden na de bestaande laag geladen. +Geen nieuwe parallelle app, backend, routing- of authenticatielaag. + +--- + +# Historisch: Signaal-binding vóór de redesignopdracht > De levende ontwerp- en smaakgids voor het opencodex-dashboard (`gui/`). > Dit is de ChefGroep-taal (v3 "Signaal"): een stil, warm, mat instrument. -> Bron van waarheid voor de *taal*: [`GroepOnline/design-system`](https://github.com/GroepOnline/design-system) +> Bron van waarheid voor de _taal_: [`GroepOnline/design-system`](https://github.com/GroepOnline/design-system) > (`tokens.css`, `DESIGN.md`, `motion-spec.md`). Dit bestand legt vast hoe die > taal in dít dashboard leeft, en — belangrijker — **hoe je 'm uitbreidt zonder > 'm te breken**. @@ -31,14 +216,14 @@ aan" zegt is blauw: links, focus-ring, actieve nav, geselecteerde tab, toggles, selectie. De **primaire knop** blijft juist monochroom (tekst↔achtergrond omgekeerd) — dat is de shadcn-conventie, geen tweede accent. -| Rol | Token | -|---|---| -| Achtergrond / rail / kaart | `--bg` · `--rail` · `--surface` · `--raised` | -| Lijnen | `--border` (sterk) · `--border-soft` (hairline) | -| Tekst | `--text` · `--muted` · `--faint` | -| Primaire actie | `--accent` (+ `--accent-ink`) | -| Het accent | `--accent-blue` · `--accent-blue-ink` · `--accent-soft` (ring/tint) | -| Semantiek | `--green` (git/PR/toestemming) · `--amber` (wacht-op-jou) · `--red` (destructief) | +| Rol | Token | +| -------------------------- | --------------------------------------------------------------------------------- | +| Achtergrond / rail / kaart | `--bg` · `--rail` · `--surface` · `--raised` | +| Lijnen | `--border` (sterk) · `--border-soft` (hairline) | +| Tekst | `--text` · `--muted` · `--faint` | +| Primaire actie | `--accent` (+ `--accent-ink`) | +| Het accent | `--accent-blue` · `--accent-blue-ink` · `--accent-soft` (ring/tint) | +| Semantiek | `--green` (git/PR/toestemming) · `--amber` (wacht-op-jou) · `--red` (destructief) | Regels: groen/amber/rood zijn **gereserveerd**, nooit decoratie. Neutraal is warm, nooit koudgrijs. Dark mode is basalt-warm, geen zuiver zwart. Elke token @@ -53,16 +238,16 @@ is `light-dark(licht, donker)` — schrijf beide kanten, altijd. diffs, tellers). Geen Archivo/Inter/Geist — design-system v3 §11. - Eén type-ladder — gebruik de tokens, nooit losse px: -| Token | px | Gebruik | -|---|---|---| -| `--text-micro` | 10.5 | meta, tellers, caps-labels | -| `--text-caption` | 11.5 | labels, captions | -| `--text-label` | 12.5 | secundair / beschrijvingen | -| `--text-control` | 13.5 | **UI-standaard** (body van de app) | -| `--text-body` | 15 | leestekst | -| `--text-subtitle` | 15 | kleine titels | -| `--text-title` | 24 | paginatitels | -| `--text-display` | 28 | hero-getallen (product-extensie, geen §11-trede) | +| Token | px | Gebruik | +| ----------------- | ---- | ------------------------------------------------ | +| `--text-micro` | 10.5 | meta, tellers, caps-labels | +| `--text-caption` | 11.5 | labels, captions | +| `--text-label` | 12.5 | secundair / beschrijvingen | +| `--text-control` | 13.5 | **UI-standaard** (body van de app) | +| `--text-body` | 15 | leestekst | +| `--text-subtitle` | 15 | kleine titels | +| `--text-title` | 24 | paginatitels | +| `--text-display` | 28 | hero-getallen (product-extensie, geen §11-trede) | - Koppen: gewicht 500, `letter-spacing: var(--tracking-tight)` (−0.02em), `text-wrap: balance`. Leading via `--leading-*` (tight 1.2 / ui 1.45 / diff --git a/gui/UX-CONTRACT.md b/gui/UX-CONTRACT.md new file mode 100644 index 00000000..76b808d1 --- /dev/null +++ b/gui/UX-CONTRACT.md @@ -0,0 +1,111 @@ +# OCX workspace UX contract + +Scope: the pending workspace navigation, Models catalog, Overview and Settings migration. This +records ownership, not approval of the unfinished visual redesign. Existing +provider/authentication, deletion, billing and persistence policy stays with its +runtime owner; this document does not invent replacement business rules. + +## Product and authority + +- Audience: operators routing model clients through the existing OCX proxy. +- Jobs: inspect real runtime/provider readings, navigate configuration, adjust + language and theme without changing provider credentials. +- Domain: repository `CONTEXT.md`; management API: `src/api.ts` in this GUI; + route catalogue and health state: `src/App.tsx`. +- EN/NL copy and locale persistence: `src/i18n/`; no new markets, timezone policy + or provider-family claims in this migration. +- Visual authority: `DESIGN.md`; runtime CSS remains canonical. The Tailwind + adapter maps existing roles, never a second palette or reset. +- Accessibility target: WCAG 2.2 AA. Automated/component checks and bounded Brave + checks are evidence for specific behaviors, not a conformance certification. + +## Canonical UI Map + +| Capability | Canonical owner | Source of truth | Allowed variants | Verification | +| ---------------------- | ----------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------ | +| Select/Listbox | Shared Select | `src/ui.tsx`, `src/select-position.ts` | Authored, portaled by default; inline inside Settings Sheet | Existing select tests; nested Escape and popup browser check | +| Settings overlay | Shared Sheet / Base UI Dialog | `src/components/primitives/sheet.tsx` | Controlled modal; translated title/close; external return-focus ref | `tests/settings-sheet.test.tsx`, native Tab/Shift+Tab/Escape | +| Theme selection | Shared ToggleGroup | `src/components/primitives/toggle-group.tsx` | Light/dark/system, exactly one active value | Settings behavioral tests | +| Buttons / empty states | Shared Button / Empty | `src/components/primitives/button.tsx`, `empty.tsx` | Existing `.btn` callers remain legacy; touched Overview uses shared primitives | Overview data-state tests and browser | +| Readings | MetricList | `src/components/MetricList.tsx` | Semantic definition list, numbers supplied by caller | Dashboard data-state tests | +| Scrollbar | Application stylesheet | `src/styles/workspace-orbit.css` | Global semantic roles; forced-colors defers to system | Computed styles / browser; platform rendering may differ | +| Navigation | WorkspaceNavigation | `src/components/WorkspaceNavigation.tsx`, `src/route.ts` | Existing hashes; current page via aria-current | Workspace navigation tests | + +Model selection belongs to `src/pages/Models.tsx`; the persistent detail surface +is `src/pages/ModelInspector.tsx`. Selection uses provider-qualified identity and +does not mutate configuration. The existing visibility and custom-model API +handlers retain ownership of writes. No new auth, billing or CRUD backend. + +The 2026-09-07 catalog migration uses actual shadcn Base UI Button, InputGroup, +Select, Badge, Switch, Accordion, Field, Alert, Empty, Separator and Spinner source +components. The provider filter uses the library Select; other existing authored +Select consumers retain their owner above. No global replacement in `ui.tsx`. +Catalog visibility is `role="switch"` / `aria-checked`, independent of selection's +`aria-pressed`. InputGroup forwards the real search-input ref. Closed Accordion +panels unmount; bulk operations still use full-provider data, not filtered rows. +Success alerts are polite status; failures use alerts. Spinners are mounted only +during actual pending work, accompanied by text, and static with reduced motion. +The shared source attribution is packaged in `public/third-party-notices.txt`. + +## Behavior and resilience + +- Dashboard: pending is not empty; successful zero stays zero; unsuccessful first + load displays unknown readings. Failed refresh preserves fetched readings and + shows the error in the affected section. Polling remains owned by Dashboard. +- Empty-success providers/traffic has a real link to the relevant configuration + or traffic route. A failure never simultaneously claims successful emptiness. +- Settings: title labels the dialog; opening focuses Close, Tab wraps inside, + closing restores the external opener. Escape first dismisses an open Select, + then the Sheet. The locale popup stays inside the dialog and matches its trigger. +- Language/theme selections reuse current persistence. Removed skin choices were + visually identical under the new token override; saved values are not deleted. +- Buttons retain native disabled behavior and visible focus. Icon-only Close has + localized accessible copy. No decorative pressed translation or transition-all. +- Overlay uses existing `--z-modal`; the inline locale popup uses `--z-popover` + inside it. No unrelated body-level popup may escape modal focus containment. + +## Navigation, layout and motion + +- Document title is localized route name plus opencodex; public landing keeps the + product name. Single-destination views do not repeat an Overview subtab. +- Desktop navigation is a compact horizontal row; six labelled mobile destinations + remain visible in two rows. Primary + navigation and Settings controls keep 44px targets; no page-wide overflow. +- Mobile readings become compact label/value rows rather than empty KPI cards. +- Pointer selection has a scoped, interruptible shared-layout spring; keyboard + navigation and reduced motion are instant. No page entrance animation. +- Settings movement is at most 200ms, transform/opacity only. Keyboard opening is + instant; reduced motion suppresses displacement. No ambient animation added. +- Models uses global search plus provider filtering, preserving empty-provider and + discovery-failure states. Search opens matching groups without overwriting saved + collapse preferences; collapse controls are disabled while search is active. +- Selecting a model focuses its inspector heading. Back restores the originating + row, or search if that row is no longer rendered. On narrow screens, the detail + pane replaces list/header/filter chrome; none of the hidden controls stays tabbable. +- Visibility switches have 44px targets. Missing context/modalities remain unknown; + catalog visibility is explicitly not served-model or live-auth proof. +- Copy ID copies the provider-qualified ID for routed models and the bare native + ID for native models. Success/failure feedback is scoped to the selected identity; + clipboard failure must never appear as success. Catalog provenance stays explicit + in the inspector disclosure summary even when the explanation is collapsed. +- Provider bulk actions retain full-provider scope under search. Advanced controls + are disclosed separately, not removed. Existing custom-model confirmation stays. + +## Migration and verification + +- Source owners above precede premium defaults. This is incremental migration, + not permission to replace established controls with screen-local copies. +- Legacy provider textareas remain vertically resizable. A stricter premium + resize-none migration requires usable autosizing/expansion, not lost editing + capacity. The static audit reports this debt; it is not suppressed. +- Run `bun run test` (existing isolated GUI script), `bun run lint`, + `bun run lint:i18n`, `bun run build`; root checks and docs build per AGENTS. +- Behavioral evidence: `tests/dashboard-data-states.test.tsx`, + `tests/settings-sheet.test.tsx`, `tests/workspace-navigation.test.tsx`. +- Browser matrix: wide 1920×1080, desktop 1440, intermediate 1024, mobile 390; EN/NL, light/dark, + reduced motion, focus return and nested popup. Any unobserved combination remains + unverified. Synthetic UI fixtures are explicitly not live runtime evidence. +- Full provider CRUD, screen-reader audit, all detail panels, public landing, + exact-head CI/security approval and deployment acceptance remain separate gates. +- Rollout is through the existing OCX PR/release path. No production cutover is + authorized by a local preview or static audit alone. diff --git a/gui/bun.lock b/gui/bun.lock index cb18ca20..e50b5289 100644 --- a/gui/bun.lock +++ b/gui/bun.lock @@ -5,10 +5,16 @@ "": { "name": "gui", "dependencies": { + "@base-ui/react": "1.8.0", "@designcodeio/threeui": "^1.2.0", "@fontsource-variable/jetbrains-mono": "^5.3.0", + "@fontsource-variable/manrope": "5.3.0", "@fontsource/instrument-serif": "^5.3.0", "@tanstack/react-virtual": "^3.14.5", + "class-variance-authority": "0.7.1", + "cn": "0.2.6", + "lucide-react": "1.41.0", + "motion": "13.2.0", "posthog-js": "^1.422.5", "react": "^19.2.7", "react-dom": "^19.2.7", @@ -16,6 +22,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "4.3.3", "@types/node": "^26.4.1", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.7", @@ -25,6 +32,7 @@ "eslint-plugin-react-refresh": "^0.5.6", "globals": "^17.12.0", "happy-dom": "20.14.0", + "tailwindcss": "4.3.3", "typescript": "~6.0.2", "typescript-eslint": "^8.69.0", "vite": "^8.1.0", @@ -63,12 +71,18 @@ "@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/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.8.0", "", { "dependencies": { "@babel/runtime": "^7.29.7", "@base-ui/utils": "0.4.0", "@floating-ui/react-dom": "^2.1.9", "@floating-ui/utils": "^0.2.12", "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-P0/1sxo6SBVZOklKMIedvTWqw2s2IQzi9x5bIVsXu980cuSOD4NeuRSs+/L7LZQfDkZP/uRZyGPyfFl/B1oH+Q=="], + + "@base-ui/utils": ["@base-ui/utils@0.4.0", "", { "dependencies": { "@babel/runtime": "^7.29.7", "@floating-ui/utils": "^0.2.12", "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-bO9fz25kKtPf+aZVyfQrC0PDmJdmVni31W2hCS5/Owb+inwdIL3XU26pCPRPlt4LSxZrBgLwubXQXQlKaFEZzw=="], + "@designcodeio/threeui": ["@designcodeio/threeui@1.2.0", "", { "dependencies": { "three128": "npm:three@0.128.0", "three165": "npm:three@0.165.0" }, "peerDependencies": { "react": ">=18 <20", "react-dom": ">=18 <20", "three": ">=0.149 <1" } }, "sha512-E6F/+IPfLKYatB5W62H3gK1bp9WYvfO6xuWBaBh8oLVR7fbTeeEwgLqqy7ZPMA0zmDpAy03nKYOIswBXsstL2g=="], "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], @@ -93,8 +107,18 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.2", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A=="], + "@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=="], + "@fontsource-variable/jetbrains-mono": ["@fontsource-variable/jetbrains-mono@5.3.0", "", {}, "sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw=="], + "@fontsource-variable/manrope": ["@fontsource-variable/manrope@5.3.0", "", {}, "sha512-6D5dgokHsWDDMtmXHznKa0hK229NN+1a4BLPmUCLqcO1Pw5EEhWY5RFt0AcXnVRAljFFPfRtLkJePQj6LSsV6g=="], + "@fontsource/instrument-serif": ["@fontsource/instrument-serif@5.3.0", "", {}, "sha512-mDiaIg0u67sYV59fie92Wz4sM8UiVlbL7fLxnFPCKkX15DMASEnQTREbaP5S5/3DCcsoAOcQ0sV9E4AeY4QqYQ=="], "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], @@ -159,6 +183,36 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + "@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/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.5", "", { "dependencies": { "@tanstack/virtual-core": "3.17.3" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4EKRXh7zBLkbKbFmG3AUVkircuHd+7OdT1pocJSepxtfBd3qnrJgJ5rtPkRYyo9fmyVb2+pI2xPy5oYvMLQy6A=="], "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.3", "", {}, "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw=="], @@ -223,6 +277,12 @@ "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cn": ["cn@0.2.6", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "core-js": ["core-js@3.49.0", "", {}, "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg=="], @@ -241,6 +301,8 @@ "electron-to-chromium": ["electron-to-chromium@1.5.420", "", {}, "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA=="], + "enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="], + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -285,6 +347,8 @@ "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + "framer-motion": ["framer-motion@13.2.0", "", { "dependencies": { "motion-dom": "^13.2.0", "motion-utils": "^13.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-9E33ebgMaO33w1nN/jEdW8z3/GO483fMi4rqbMG9rt83XgW9QLKRe4NcmJ8s+fQ3O34++UHrIQwlIWGIWTITjA=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], @@ -293,6 +357,8 @@ "globals": ["globals@17.12.0", "", {}, "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "happy-dom": ["happy-dom@20.14.0", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-4bRh1KzRvKDnFNTlLhzT1RZTpkKhQbQDl9j+7GXszWsvuspYdo29k6OHRf4PwiM6oLb8r/pMWeYiJjkfod5AvQ=="], "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], @@ -309,6 +375,8 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -353,8 +421,18 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "lucide-react": ["lucide-react@1.41.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6lksP35l6KszDKUeRTi4LV7i6DEe0Yzl2ALJm9j4c5xEYN91GdW1xGsawGMOg2mgjF5GHBVX8pKX9kP+cWsP3Q=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "motion": ["motion@13.2.0", "", { "dependencies": { "framer-motion": "^13.2.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-4Hrb5vD6HhjFstLUiCmWvtpsw+WTpP4R+QXfSDYZBz7+uxE/LrRg3aV0ReJxHrTRffHhbIE6svEqnotngXvesQ=="], + + "motion-dom": ["motion-dom@13.2.0", "", { "dependencies": { "motion-utils": "^13.0.0" } }, "sha512-N6gdSoWRDk0Rh/fVtlqUtLs+fEN3ELFZI3cn3IQE9Mnf3E+Mh8wjO6MstzCOPFh4Yf0L1as5m2eUyYWj8ylVSQ=="], + + "motion-utils": ["motion-utils@13.0.0", "", {}, "sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], @@ -393,6 +471,8 @@ "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + "reselect": ["reselect@5.3.0", "", {}, "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg=="], + "rolldown": ["rolldown@1.1.3", "", { "dependencies": { "@oxc-project/types": "=0.137.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.3", "@rolldown/binding-darwin-arm64": "1.1.3", "@rolldown/binding-darwin-x64": "1.1.3", "@rolldown/binding-freebsd-x64": "1.1.3", "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", "@rolldown/binding-linux-arm64-gnu": "1.1.3", "@rolldown/binding-linux-arm64-musl": "1.1.3", "@rolldown/binding-linux-ppc64-gnu": "1.1.3", "@rolldown/binding-linux-s390x-gnu": "1.1.3", "@rolldown/binding-linux-x64-gnu": "1.1.3", "@rolldown/binding-linux-x64-musl": "1.1.3", "@rolldown/binding-openharmony-arm64": "1.1.3", "@rolldown/binding-wasm32-wasi": "1.1.3", "@rolldown/binding-win32-arm64-msvc": "1.1.3", "@rolldown/binding-win32-x64-msvc": "1.1.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -405,6 +485,10 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + "three": ["three@0.185.1", "", {}, "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg=="], "three128": ["three@0.128.0", "", {}, "sha512-i0ap/E+OaSfzw7bD1TtYnPo3VEplkl70WX5fZqZnfZsE3k3aSFudqrrC9ldFZfYFkn1zwDmBcdGfiIm/hnbyZA=="], @@ -429,6 +513,8 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "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=="], + "vite": ["vite@8.1.0", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "~1.1.2", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q=="], "web-vitals": ["web-vitals@6.2.1", "", {}, "sha512-rLcLXA2sx6+9dE88NHFubwTtGxpK4yYBLj6qHPdFoCaLr0cXGb4efOqtKLlm4loGA4OEKHIQKMKzZkKyOh5ctw=="], @@ -453,6 +539,18 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + + "@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=="], + "@types/ws/@types/node": ["@types/node@24.13.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], diff --git a/gui/components.json b/gui/components.json new file mode 100644 index 00000000..97fd2b8a --- /dev/null +++ b/gui/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-nova", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/primitives.css", + "baseColor": "neutral", + "cssVariables": true + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "ui": "@/components/primitives", + "utils": "@/lib/utils", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/gui/package.json b/gui/package.json index 22dc2e75..8f7a0919 100644 --- a/gui/package.json +++ b/gui/package.json @@ -14,10 +14,16 @@ "preview": "vite preview" }, "dependencies": { + "@base-ui/react": "1.8.0", "@designcodeio/threeui": "^1.2.0", "@fontsource-variable/jetbrains-mono": "^5.3.0", + "@fontsource-variable/manrope": "5.3.0", "@fontsource/instrument-serif": "^5.3.0", "@tanstack/react-virtual": "^3.14.5", + "class-variance-authority": "0.7.1", + "cn": "0.2.6", + "lucide-react": "1.41.0", + "motion": "13.2.0", "posthog-js": "^1.422.5", "react": "^19.2.7", "react-dom": "^19.2.7", @@ -25,6 +31,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@tailwindcss/vite": "4.3.3", "@types/node": "^26.4.1", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.7", @@ -34,6 +41,7 @@ "eslint-plugin-react-refresh": "^0.5.6", "globals": "^17.12.0", "happy-dom": "20.14.0", + "tailwindcss": "4.3.3", "typescript": "~6.0.2", "typescript-eslint": "^8.69.0", "vite": "^8.1.0" diff --git a/gui/premium-ui.json b/gui/premium-ui.json new file mode 100644 index 00000000..13508d54 --- /dev/null +++ b/gui/premium-ui.json @@ -0,0 +1,15 @@ +{ + "profile": "product-admin", + "sourceRoots": ["src"], + "canonicalMap": "UX-CONTRACT.md", + "requiredCapabilities": ["Select/Listbox", "Scrollbar"], + "ownership": { "Select/Listbox": "authored" }, + "requiredCommands": ["unit", "lint", "i18n", "build"], + "commands": { + "unit": "bun run test", + "lint": "bun run lint", + "i18n": "bun run lint:i18n", + "build": "bun run build" + }, + "evidence": { "failurePaths": "tests/dashboard-data-states.test.tsx" } +} diff --git a/gui/public/third-party-notices.txt b/gui/public/third-party-notices.txt new file mode 100644 index 00000000..391452bd --- /dev/null +++ b/gui/public/third-party-notices.txt @@ -0,0 +1,84 @@ +# GUI source attribution + +## shadcn/ui + +The components under `src/components/primitives/` originate from the official +shadcn/ui `base-nova` registry, imported with the shadcn CLI and adapted for this +application's semantic tokens, accessibility, localization and motion policy. +Source: . + +MIT License + +Copyright (c) 2023 shadcn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## Lucide React 1.41.0 + +Source: https://github.com/lucide-icons/lucide +The following upstream license is included for the icons bundled into this GUI. + +ISC License + +Copyright (c) 2026 Lucide Icons and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +--- + +The following Lucide icons are derived from the Feather project: + +airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out + +The MIT License (MIT) (for the icons listed above) + +Copyright (c) 2013-present Cole Bemis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## Other package dependencies + +Base UI and the other installed packages retain their own package licenses. +No Svelte/sv-matrix or Libraries.dev Beam source is vendored. diff --git a/gui/src/App.tsx b/gui/src/App.tsx index ece1d65e..2f020da4 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -8,6 +8,18 @@ import { installApiAuthFetch } from "./api"; import { canonicalHashFor, type View } from "./app-routing"; import { useAppRouteState } from "./use-app-route-state"; import { requestProxyStop } from "./stop-proxy"; +import { domAnimation, LazyMotion, MotionConfig } from "motion/react"; +import WorkspaceNavigation, { + type WorkspaceDestination, +} from "./components/WorkspaceNavigation"; +import { + IconActivity, + IconBoxes, + IconGrid, + IconList, + IconMonitor, + IconServer, +} from "./icons"; const Providers = lazy(() => import("./pages/Providers")); const Models = lazy(() => import("./pages/Models")); @@ -31,24 +43,19 @@ type Theme = "light" | "dark" | "system"; const API_BASE = import.meta.env.VITE_API_BASE || ""; const THEME_KEY = "ocx-theme"; -/** Five views (design-system v2 IA, 2026-08). Dashboard is home. */ -const VIEW_TABS: { view: View; tkey: TKey }[] = [ - { view: "dashboard", tkey: "nav.dashboard" }, - { view: "leveranciers", tkey: "nav.providers" }, - { view: "modellen", tkey: "nav.models" }, - { view: "verkeer", tkey: "nav.verkeer" }, - { view: "verbruik", tkey: "nav.usage" }, - { view: "systeem", tkey: "nav.systeem" }, +const VIEW_TABS: WorkspaceDestination[] = [ + { view: "dashboard", tkey: "nav.dashboard", icon: IconGrid }, + { view: "leveranciers", tkey: "nav.providers", icon: IconServer }, + { view: "modellen", tkey: "nav.models", icon: IconBoxes }, + { view: "verkeer", tkey: "nav.verkeer", icon: IconList }, + { view: "verbruik", tkey: "nav.usage", icon: IconActivity }, + { view: "systeem", tkey: "nav.systeem", icon: IconMonitor }, ]; /** Sub-tabs per view; `null` is the view's home target. */ const SUB_TABS: Record = { - landing: [ - { sub: null, tkey: "nav.dashboard" }, - ], - dashboard: [ - { sub: null, tkey: "nav.dashboard" }, - ], + landing: [{ sub: null, tkey: "nav.dashboard" }], + dashboard: [{ sub: null, tkey: "nav.dashboard" }], leveranciers: [ { sub: null, tkey: "sub.overview" }, { sub: "claude", tkey: "nav.claude" }, @@ -63,9 +70,7 @@ const SUB_TABS: Record = { { sub: null, tkey: "sub.logs" }, { sub: "debug", tkey: "sub.debug" }, ], - verbruik: [ - { sub: null, tkey: "nav.usage" }, - ], + verbruik: [{ sub: null, tkey: "nav.usage" }], systeem: [ { sub: null, tkey: "sub.status" }, { sub: "storage", tkey: "nav.storage" }, @@ -93,6 +98,16 @@ export default function App() { const { route, navigateTo } = useAppRouteState(); const [theme, setTheme] = useState(readStoredTheme); const t = useT(); + useEffect(() => { + const title = + route.sub === null + ? VIEW_TABS.find((entry) => entry.view === route.view)?.tkey + : SUB_TABS[route.view].find((entry) => entry.sub === route.sub)?.tkey; + document.title = + title && route.view !== "landing" + ? t("app.pageTitle", { page: t(title) }) + : "opencodex"; + }, [route, t]); useEffect(() => { const el = document.documentElement; if (theme === "system") el.removeAttribute("data-theme"); @@ -116,14 +131,27 @@ export default function App() { detailsLabel={t("errorBoundary.details")} reloadLabel={t("errorBoundary.reload")} > - {t("common.loading")}}> + + {t("common.loading")} + + } + > ); } - return ; + return ( + + ); } function DashboardShell({ @@ -139,6 +167,8 @@ function DashboardShell({ }) { const t = useT(); const [settingsOpen, setSettingsOpen] = useState(false); + const [settingsInstant, setSettingsInstant] = useState(false); + const settingsTriggerRef = useRef(null); const healthPoll = useKeyedClientResource( `app-healthz:${API_BASE}`, [], @@ -153,10 +183,15 @@ function DashboardShell({ const displayedVersion: string = healthPoll.data?.version ?? __APP_VERSION__; // null = first poll still in flight: no stamp until the first verdict. - const proxyOnline: boolean | null = healthPoll.error ? false : healthPoll.data ? true : null; + const proxyOnline: boolean | null = healthPoll.error + ? false + : healthPoll.data + ? true + : null; const activeTkey = - SUB_TABS[route.view].find(s => s.sub === route.sub)?.tkey ?? "nav.providers"; + SUB_TABS[route.view].find((s) => s.sub === route.sub)?.tkey ?? + "nav.providers"; const brand = (
@@ -167,89 +202,148 @@ function DashboardShell({ ); return ( -
-
- {brand} -
- {proxyOnline !== null && ( - - {proxyOnline ? : } - {t(proxyOnline ? "proxy.online" : "proxy.offline")} - - )} - -
-
+ + +
+
+ {brand} + navigateTo({ view, sub: null })} + /> +
+ {proxyOnline !== null && ( + + {proxyOnline ? ( + + ) : ( + + )} + {t(proxyOnline ? "proxy.online" : "proxy.offline")} + + )} + +
+
- {proxyOnline === false && ( -
- - {t("offline.banner")} - -
- )} + {proxyOnline === false && ( +
+ + {t("offline.banner")} + +
+ )} - +
+
+ {SUB_TABS[route.view].length > 1 && ( + + )} +
+ + + {t("common.loading")} +
+ } + > + {route.view === "dashboard" && ( + + )} + {route.view === "leveranciers" && route.sub === null && ( + + )} + {route.view === "leveranciers" && + route.sub === "claude" && } + {route.view === "leveranciers" && route.sub === "grok" && ( + + )} + {route.view === "modellen" && route.sub === null && ( + + )} + {route.view === "modellen" && route.sub === "combos" && ( + + )} + {route.view === "modellen" && route.sub === "subagents" && ( + + )} + {route.view === "verkeer" && route.sub === "debug" && ( + + )} + {route.view === "verkeer" && route.sub !== "debug" && ( + + )} + {route.view === "verbruik" && } + {route.view === "systeem" && route.sub === null && ( + + )} + {route.view === "systeem" && route.sub === "storage" && ( + + )} + {route.view === "systeem" && route.sub === "api" && ( + + )} + + +
+ {route.view === "systeem" && route.sub === null && } +
+ -
-
- -
- - {t("common.loading")}
}> - {route.view === "dashboard" && } - {route.view === "leveranciers" && route.sub === null && } - {route.view === "leveranciers" && route.sub === "claude" && } - {route.view === "leveranciers" && route.sub === "grok" && } - {route.view === "modellen" && route.sub === null && } - {route.view === "modellen" && route.sub === "combos" && } - {route.view === "modellen" && route.sub === "subagents" && } - {route.view === "verkeer" && route.sub === "debug" && } - {route.view === "verkeer" && route.sub !== "debug" && } - {route.view === "verbruik" && } - {route.view === "systeem" && route.sub === null && } - {route.view === "systeem" && route.sub === "storage" && } - {route.view === "systeem" && route.sub === "api" && } - - -
- {route.view === "systeem" && route.sub === null && } + setSettingsOpen(false)} + />
- - - {settingsOpen && ( - setSettingsOpen(false)} /> - )} -
+ + ); } @@ -264,7 +358,9 @@ function DangerZone() { useEffect(() => { if (!confirming) return; cancelRef.current?.focus(); - const onKey = (e: KeyboardEvent) => { if (e.key === "Escape" && !stopping) setConfirming(false); }; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape" && !stopping) setConfirming(false); + }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [confirming, stopping]); @@ -272,7 +368,8 @@ function DangerZone() { const handleStop = async () => { setStopping(true); const outcome = await requestProxyStop(API_BASE, { - formatFailure: status => t("dash.stopFailed", { status: String(status) }), + formatFailure: (status) => + t("dash.stopFailed", { status: String(status) }), }); // Refusals and restore failures return normally instead of dropping the connection. // In both cases the proxy did not reach a clean-stop result, so surface the server's @@ -286,32 +383,59 @@ function DangerZone() { return (
-

{t("danger.title")}

+

+ {t("danger.title")} +

{t("danger.stopAction")}
{t("danger.stopBody")}
-
{confirming && ( -
{ if (e.target === e.currentTarget && !stopping) setConfirming(false); }}> +
{ + if (e.target === e.currentTarget && !stopping) setConfirming(false); + }} + >

{t("danger.stopTitle")}

-

{t("danger.stopBody")}

+

+ {t("danger.stopBody")} +

- -
diff --git a/gui/src/components/MatrixMark.tsx b/gui/src/components/MatrixMark.tsx new file mode 100644 index 00000000..969a8391 --- /dev/null +++ b/gui/src/components/MatrixMark.tsx @@ -0,0 +1,28 @@ +/** A finite, decorative center-out motif inspired by sv-matrix, not a loading state. */ +export default function MatrixMark() { + return ( + + ); +} diff --git a/gui/src/components/MetricList.tsx b/gui/src/components/MetricList.tsx new file mode 100644 index 00000000..823a1d82 --- /dev/null +++ b/gui/src/components/MetricList.tsx @@ -0,0 +1,26 @@ +import type { ReactNode } from "react"; + +interface Metric { + label: string; + value: ReactNode; +} + +/** Related readings share one definition list, not a collection of action cards. */ +export default function MetricList({ + label, + metrics, +}: { + label: string; + metrics: readonly Metric[]; +}) { + return ( +
+ {metrics.map((metric) => ( +
+
{metric.label}
+
{metric.value}
+
+ ))} +
+ ); +} diff --git a/gui/src/components/SettingsSheet.tsx b/gui/src/components/SettingsSheet.tsx index e8fb6359..2e9f2fbd 100644 --- a/gui/src/components/SettingsSheet.tsx +++ b/gui/src/components/SettingsSheet.tsx @@ -1,20 +1,19 @@ -/* Settings sheet — design-system v2 Motion 5 (pane slide). Config lives here, - not on the dashboard: language, theme, skin. Destructive actions do NOT live - here (those sit in the Systeem danger zone). */ -import { useEffect, useRef, useState } from "react"; +/* Application preferences; destructive actions remain in System. */ +import { useRef, type RefObject } from "react"; import { IconGithub, IconMonitor, IconMoon, IconSun, IconX } from "../icons"; import { useI18n, useT, LOCALES, type Locale, type TKey } from "../i18n/shared"; import { Select } from "../ui"; +import { + Sheet, + SheetClose, + SheetContent, + SheetHeader, + SheetTitle, +} from "./primitives/sheet"; +import { Button } from "./primitives/button"; +import { ToggleGroup, ToggleGroupItem } from "./primitives/toggle-group"; type Theme = "light" | "dark" | "system"; -type Skin = "devin" | "strak"; - -const STYLE_KEY = "ocx-style"; - -function readSkin(): Skin { - return document.documentElement.getAttribute("data-style") === "strak" ? "strak" : "devin"; -} - const THEME_OPTIONS: { id: Theme; tkey: TKey; Icon: typeof IconSun }[] = [ { id: "light", tkey: "theme.light", Icon: IconSun }, { id: "dark", tkey: "theme.dark", Icon: IconMoon }, @@ -22,102 +21,95 @@ const THEME_OPTIONS: { id: Theme; tkey: TKey; Icon: typeof IconSun }[] = [ ]; export default function SettingsSheet({ + open, + returnFocusRef, + instant, theme, onTheme, onClose, }: { + open: boolean; + returnFocusRef: RefObject; + instant: boolean; theme: Theme; onTheme: (next: Theme) => void; onClose: () => void; }) { const t = useT(); const { locale, setLocale } = useI18n(); - const [skin, setSkin] = useState(readSkin); const closeRef = useRef(null); - useEffect(() => { - closeRef.current?.focus(); - const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; - window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); - }, [onClose]); - - const applySkin = (next: Skin) => { - setSkin(next); - document.documentElement.setAttribute("data-style", next); - try { localStorage.setItem(STYLE_KEY, next); } catch { /* private mode */ } - }; - return ( -
-
-
-
{t("settings.skin")}
-
- - +
+
{t("theme.label")}
+ { + const next = values[0]; + if (next === "light" || next === "dark" || next === "system") + onTheme(next); + }} + > + {THEME_OPTIONS.map(({ id, tkey, Icon }) => ( + + {t(tkey)} + + ))} +
-
- - -
+ + ); } diff --git a/gui/src/components/WorkspaceNavigation.tsx b/gui/src/components/WorkspaceNavigation.tsx new file mode 100644 index 00000000..da75b7d6 --- /dev/null +++ b/gui/src/components/WorkspaceNavigation.tsx @@ -0,0 +1,60 @@ +import { LayoutGroup, m, useReducedMotion } from "motion/react"; +import { useId, useState, type ComponentType } from "react"; +import type { View } from "../app-routing"; +import { useT, type TKey } from "../i18n/shared"; +import { Button } from "./primitives/button"; + +export interface WorkspaceDestination { + view: View; + tkey: TKey; + icon: ComponentType<{ size?: 13 | 15 | 18 | 24; "aria-hidden"?: boolean }>; +} + +export default function WorkspaceNavigation({ + destinations, + active, + onNavigate, +}: { + destinations: readonly WorkspaceDestination[]; + active: View; + onNavigate: (view: View) => void; +}) { + const t = useT(); + const reduceMotion = useReducedMotion(); + const groupId = useId(); + const [keyboardNavigation, setKeyboardNavigation] = useState(false); + return ( + + ); +} diff --git a/gui/src/components/primitives/accordion.tsx b/gui/src/components/primitives/accordion.tsx new file mode 100644 index 00000000..036631bc --- /dev/null +++ b/gui/src/components/primitives/accordion.tsx @@ -0,0 +1,77 @@ +import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"; +import { cn } from "cn"; +import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; + +function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) { + return ( + + ); +} + +function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) { + return ( + + ); +} + +function AccordionTrigger({ + className, + children, + ...props +}: AccordionPrimitive.Trigger.Props) { + return ( + + + {children} + + + + + ); +} + +function AccordionContent({ + className, + children, + ...props +}: AccordionPrimitive.Panel.Props) { + return ( + +
+ {children} +
+
+ ); +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/gui/src/components/primitives/alert.tsx b/gui/src/components/primitives/alert.tsx new file mode 100644 index 00000000..753782a8 --- /dev/null +++ b/gui/src/components/primitives/alert.tsx @@ -0,0 +1,75 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "cn"; + +const alertVariants = cva( + "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground", + className, + )} + {...props} + /> + ); +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { Alert, AlertTitle, AlertDescription, AlertAction }; diff --git a/gui/src/components/primitives/badge.tsx b/gui/src/components/primitives/badge.tsx new file mode 100644 index 00000000..1166df2b --- /dev/null +++ b/gui/src/components/primitives/badge.tsx @@ -0,0 +1,50 @@ +import { mergeProps } from "@base-ui/react/merge-props"; +import { useRender } from "@base-ui/react/use-render"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "cn"; + +const badgeVariants = cva( + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-colors focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 [&>svg]:pointer-events-none [&>svg]:size-3!", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: + "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 [a]:hover:bg-destructive/20", + outline: + "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: "hover:bg-muted hover:text-muted-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function Badge({ + className, + variant = "default", + render, + ...props +}: useRender.ComponentProps<"span"> & VariantProps) { + return useRender({ + defaultTagName: "span", + props: mergeProps<"span">( + { + className: cn(badgeVariants({ variant }), className), + }, + props, + ), + render, + state: { + slot: "badge", + variant, + }, + }); +} + +export { Badge }; diff --git a/gui/src/components/primitives/button.tsx b/gui/src/components/primitives/button.tsx new file mode 100644 index 00000000..8525214b --- /dev/null +++ b/gui/src/components/primitives/button.tsx @@ -0,0 +1,57 @@ +import { Button as ButtonPrimitive } from "@base-ui/react/button"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "cn"; + +const buttonVariants = cva( + "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-colors duration-150 outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/80", + outline: + "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-accent aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: + "bg-transparent hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground", + destructive: + "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: + "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", + lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + icon: "size-8", + "icon-xs": + "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", + "icon-sm": + "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", + "icon-lg": "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +function Button({ + className, + variant = "default", + size = "default", + ...props +}: ButtonPrimitive.Props & VariantProps) { + return ( + + ); +} + +export { Button }; diff --git a/gui/src/components/primitives/empty.tsx b/gui/src/components/primitives/empty.tsx new file mode 100644 index 00000000..363b01f7 --- /dev/null +++ b/gui/src/components/primitives/empty.tsx @@ -0,0 +1,103 @@ +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "cn"; + +function Empty({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +const emptyMediaVariants = cva( + "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-transparent", + icon: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function EmptyMedia({ + className, + variant = "default", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function EmptyDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", + className, + )} + {...props} + /> + ); +} + +function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { + Empty, + EmptyHeader, + EmptyTitle, + EmptyDescription, + EmptyContent, + EmptyMedia, +}; diff --git a/gui/src/components/primitives/field.tsx b/gui/src/components/primitives/field.tsx new file mode 100644 index 00000000..ca80335c --- /dev/null +++ b/gui/src/components/primitives/field.tsx @@ -0,0 +1,238 @@ +"use client"; + +import { useMemo } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "cn"; + +import { Label } from "@/components/primitives/label"; +import { Separator } from "@/components/primitives/separator"; + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className, + )} + {...props} + /> + ); +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ); +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +const fieldVariants = cva( + "group/field flex w-full gap-2 data-[invalid=true]:text-destructive", + { + variants: { + orientation: { + vertical: "flex-col *:w-full [&>.sr-only]:w-auto", + horizontal: + "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + responsive: + "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + }, + }, + defaultVariants: { + orientation: "vertical", + }, + }, +); + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps) { + return ( +