diff --git a/.github/workflows/ci-web.yml b/.github/workflows/ci-web.yml new file mode 100644 index 000000000..642524d1a --- /dev/null +++ b/.github/workflows/ci-web.yml @@ -0,0 +1,68 @@ +name: CI Web + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened] + branches: [main] + paths: + - 'tsworkspace/**' + - '.mise.toml' + - '.github/workflows/ci-web.yml' + +concurrency: + group: ci-web-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + with: + package_json_file: tsworkspace/package.json + + - name: Setup Node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 26 + cache: pnpm + cache-dependency-path: tsworkspace/pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + working-directory: tsworkspace + + - name: Format check + run: pnpm fmt:check + working-directory: tsworkspace + + - name: Lint + run: pnpm lint + working-directory: tsworkspace + + - name: Typecheck + run: pnpm typecheck + working-directory: tsworkspace + + - name: Build + run: pnpm build + working-directory: tsworkspace + + - name: Test + run: pnpm test + working-directory: tsworkspace + + - name: Primitive layer policy + run: pnpm check:primitives + working-directory: tsworkspace diff --git a/.mise.toml b/.mise.toml index 7506ecbd7..837018d85 100644 --- a/.mise.toml +++ b/.mise.toml @@ -20,19 +20,27 @@ run = "docker compose -f devops/docker/compose/compose.yml up -d" [tasks.build] description = "Build all" -depends = ["build:rust", "build:docs"] +depends = ["build:rust", "build:docs", "build:web"] [tasks."build:docs"] dir = "docs" run = "pnpm docs:build" +[tasks."build:web"] +dir = "tsworkspace" +run = "pnpm build" + +[tasks."check:web"] +dir = "tsworkspace" +run = "pnpm fmt:check && pnpm lint && pnpm typecheck" + [tasks."build:rust"] dir = "rsworkspace" run = "cargo build" [tasks.check] -description = "Check Rust" -depends = ["check:rust"] +description = "Check workspaces" +depends = ["check:rust", "check:web"] [tasks."check:rust"] dir = "rsworkspace" diff --git a/README.md b/README.md index 42d523ca5..200f2c2df 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,4 @@ - [`docs/`](docs/) - Documentation website and contribution workflow. - [`rsworkspace/`](rsworkspace/) - Rust components and crate documentation. +- [`tsworkspace/`](tsworkspace/) - TypeScript workspace, including the console webapp. diff --git a/docs/adr/0018-connectrpc-gateway-for-browser-product-surfaces.md b/docs/adr/0018-connectrpc-gateway-for-browser-product-surfaces.md new file mode 100644 index 000000000..e606bf052 --- /dev/null +++ b/docs/adr/0018-connectrpc-gateway-for-browser-product-surfaces.md @@ -0,0 +1,172 @@ +--- +number: "0018" +slug: connectrpc-gateway-for-browser-product-surfaces +status: accepted +date: 2026-07-08 +--- + +# ADR 0018: ConnectRPC Gateway for Browser Product Surfaces + +## Context + +The platform is getting its first product-facing web application, the operator +console ([ADR 0019](./0019-console-webapp-stack.md)). A browser has to call +first-party platform services, and every existing first-party RPC path lives on +the NATS backbone: protobuf services bind to NATS micro +([ADR 0016](./0016-protobuf-rpc-over-nats-micro-binding.md)), the JSON-RPC +family binds to NATS subjects +([ADR 0011](./0011-jsonrpc-over-nats-binding.md)), and every signed request on +the mesh carries the AAuth NATS PoP envelope +([ADR 0017](./0017-aauth-agent-authentication.md)). + +NATS itself does not keep a browser off the backbone. `nats-server` ships a +WebSocket listener, `nats.ws` runs in browsers, and NATS has real +connection-level authentication: passwords, tokens, NKey challenge-response, +decentralized user JWTs, and auth callout, which can mint an ephemeral, +subject-scoped NATS user from an external credential such as a web session. A +browser connection to the backbone is technically achievable. + +Connection authentication is not the boundary that matters here. First-party +requests on the mesh carry the AAuth PoP envelope: each request is signed with +a `cnf.jwk`-bound private key, with nonce and content-digest bookkeeping. A +browser tab cannot provide that custody, because any key the page's scripts +can use, an injected script can use and exfiltrate. Putting browsers on the +bus would also make the internal subject namespace an internet-facing surface +whose authorization story is NATS subject permissions rather than a reviewed +method allowlist, and it would require a JavaScript reimplementation of the +ADR 0011/0016/0017 bindings that must track the Rust implementations forever. + +[ADR 0003](./0003-ai-protocol-transport-taxonomy.md) already selects the API +style for this situation. Its boundary selection order names +"browser-compatible HTTP access" as a trigger for a first-party service API and +prefers ConnectRPC for that surface. What ADR 0003 does not decide is where the +boundary lives, who holds which credentials, and how a ConnectRPC method +reaches a NATS micro endpoint. Without one rule, each product surface would +invent its own bridge, its own token custody, and its own error mapping, the +same per-call-site drift ADR 0011 and ADR 0016 exist to prevent. + +## Decision + +### 1. Product web surfaces reach the mesh only through a gateway + +A browser product surface talks to a first-party gateway service exposing +ConnectRPC over HTTPS. The gateway is a gateway in the ADR 0003 sense: a +production edge component that accepts external traffic and routes it inward, +containing a bridge onto the backbone. + +The ConnectRPC surface is generated from the same `.proto` sources that define +the backbone services ([ADR 0009](./0009-protocol-buffers-wire-contracts.md)). +Browser clients are generated with `protobuf-es` and `connect-es` from the +same Buf pipeline that generates the Rust code. There is no hand-written HTTP +client, no parallel OpenAPI document, and no GraphQL layer; the exceptions ADR +0003 allows for OpenAPI remain exceptions and are not triggered by a +first-party browser surface. + +Browsers do not connect to NATS directly, even though the WebSocket listener +and auth callout would make a scoped connection possible. Connection-level +NATS auth cannot substitute for the per-request AAuth PoP envelope, and the +envelope is exactly what a page cannot sign safely. Browser NATS access, if a +narrow read-only case ever justifies it, requires its own decision and does +not weaken this default. + +### 2. The gateway is the credential boundary + +The human operator authenticates to the gateway with an OAuth 2.0 +Authorization Code + PKCE flow. The gateway completes the code exchange, holds +the resulting tokens server-side, and issues an HttpOnly, SameSite session +cookie to the browser. The browser never receives an AAuth token, a signing +key, or a NATS credential of any kind. + +Browser auth is cookie-based, and token-in-page patterns are prohibited: no +access, refresh, ID, or session token in `localStorage`, `sessionStorage`, +IndexedDB, JavaScript-readable cookies, or long-lived JavaScript memory, and +no `Authorization: Bearer` header minted by page code. The session cookie is +an opaque identifier, not a JWT; session state lives in the gateway. Anything +JavaScript can read, an injected script can exfiltrate; an HttpOnly cookie +limits an XSS to riding the live session, which is the strictly smaller +failure. Cookie-based auth carries CSRF obligations, which the gateway owns: +`SameSite` on the cookie, strict `Origin` checking on every state-changing +request, and CORS locked to the product surface's origin. + +On the mesh, the gateway is an agent under ADR 0017: it holds its own +`aa-agent+jwt` and signing key, signs every backbone request with the Trogon +NATS PoP binding, and, when acting on behalf of an authenticated operator, +presents the operator-linked `aa-auth+jwt` in `AAuth-Auth-Token` alongside its +own agent token. Person-linked authorization therefore rides the same +mechanism every other agent on the mesh uses; the gateway adds no parallel +identity scheme. + +Auth layering is explicit. NATS auth callout (`a2a-auth-callout`) is +connection admission: it decides which clients may attach to the backbone and +scopes their subject permissions, and the gateway attaches under that +admission like any other mesh client. The AAuth PoP envelope is request +authentication, and the policy tiers are authorization. Connection admission +does not substitute for either layer above it, which is why a browser +admitted through auth callout would still be unable to make signed +first-party calls. + +### 3. The bridge is mechanical and holds no business logic + +ADR 0016 binds protobuf method names to NATS subjects deterministically, so +the gateway maps traffic without per-method invention: + +- A unary Connect RPC becomes one NATS request-reply on the bound subject. +- A server-streaming Connect RPC bridges a NATS subscription into one Connect + stream, scoped to the operator's session. +- Error mapping is canonical: the NATS micro error channel carries the + gRPC-idiom status semantics ADR 0016 defines, and Connect uses the same + canonical status codes, so the gateway translates status and message without + inventing an error vocabulary. + +The gateway performs translation, session handling, authorization screening, +and streaming fan-out. It does not aggregate, decide, or own domain rules. +When a handler needs business logic, that logic belongs in a platform service +behind the backbone, and the gateway exposes that service's method instead. + +### 4. Exposure is explicit and default-closed + +Being an ADR 0016 service does not make a method browser-reachable. The +gateway exposes an explicit allowlist of services and methods; everything else +on the backbone is unreachable from the browser surface. Adding a method to a +product surface is a reviewed gateway change, not a side effect of deploying a +backbone service. + +### 5. One gateway workload per product surface family + +A gateway is one operated workload in the ADR 0003 combined-binary sense: one +deployment unit, one telemetry identity, one security boundary. It may also +serve the static assets of its product surface when the assets share +ownership and release cadence, keeping a product surface deployable as one +service. Distinct product surfaces with different audiences, trust levels, or +release cadences get their own gateway workload rather than sharing one +allowlist and session model. + +## Consequences + +- The repository gains a new workload class, the product surface gateway. The + console gateway is its first instance. +- Browser code consumes generated Connect clients, so the `.proto` sources + remain the single wire contract from browser to backbone service. +- Trace context propagates end to end under + [ADR 0008](./0008-opentelemetry-observability.md): the browser sends + `traceparent` on every Connect call and the gateway continues that trace + onto its NATS micro requests. +- Live updates in a browser surface are Connect server streams fed by NATS + subscriptions in the gateway; product surfaces do not open their own NATS + connections. +- The gateway's session store and PoP signing make it stateful in the same + ways the A2A gateway already is; replay-store and multi-node caveats from + ADR 0017 apply to it equally. +- A future non-browser consumer that needs an explicit API surface (partner + integration, external tooling) can reuse the same ConnectRPC surface + without a new decision, because ADR 0003 already prefers ConnectRPC there. + +## References + +- [ADR 0003: AI Protocol Transport Taxonomy](./0003-ai-protocol-transport-taxonomy.md) +- [ADR 0009: Protocol Buffers Wire Contracts](./0009-protocol-buffers-wire-contracts.md) +- [ADR 0011: JSON-RPC over NATS Binding](./0011-jsonrpc-over-nats-binding.md) +- [ADR 0016: Protocol Buffers RPC over NATS micro Binding](./0016-protobuf-rpc-over-nats-micro-binding.md) +- [ADR 0017: AAuth Agent Authentication over a Trogon NATS PoP Binding](./0017-aauth-agent-authentication.md) +- [ConnectRPC protocol reference](https://connectrpc.com/docs/protocol/) +- [Protobuf-ES](https://github.com/bufbuild/protobuf-es) diff --git a/docs/adr/0019-console-webapp-stack.md b/docs/adr/0019-console-webapp-stack.md new file mode 100644 index 000000000..c1e5aa2bd --- /dev/null +++ b/docs/adr/0019-console-webapp-stack.md @@ -0,0 +1,191 @@ +--- +number: "0019" +slug: console-webapp-stack +status: accepted +date: 2026-07-08 +--- + +# ADR 0019: Console Webapp Stack + +## Context + +The platform needs an operator console: a product-facing web application for +managing agents and schedules, browsing the discovery catalog +([ADR 0012](./0012-ard-compatible-discovery-catalog.md)), and observing live +execution. It is the repository's first product web application, so its stack +choices set the default for every later TypeScript product surface. + +The console's data path is decided by +[ADR 0018](./0018-connectrpc-gateway-for-browser-product-surfaces.md): the +browser speaks ConnectRPC to a gateway that bridges onto the NATS backbone and +owns all credentials. This ADR decides everything above that boundary: the +application framework, build tooling, component system, data and form +libraries, testing, and how the application scales to more product areas +without a runtime composition scheme. + +An operator console is an authenticated, information-dense internal surface. +It has no anonymous visitors, no search-engine surface, and no +first-paint-for-logged-out-users requirement. + +## Decision + +### 1. Placement follows ADR 0005 + +The console lives at `tsworkspace/apps/console/`, with reusable TypeScript +packages under `tsworkspace/packages/`, per the layout and App Surface Rules +in [ADR 0005](./0005-polyglot-workspace-layout.md). There is no top-level +`webapps/` directory. If the console later grows desktop or mobile surfaces, +it regroups as `tsworkspace/apps/console/{web,desktop,shared}/` as ADR 0005 +already prescribes. The workspace uses pnpm, with Turborepo as the task +runner so build, test, and typecheck tasks are dependency-ordered and cached +as the package graph grows. + +Package names use the `@trogonai/` scope per ADR 0005, and the scope is +protected against public-registry collision deliberately. An npm scope is an +owned namespace, so the `@trogonai` organization must be registered on the +public npm registry (unclaimed and empty as of 2026-07-08) before these names +are treated as authoritative; once claimed, no third party can publish under +them. Independently of scope ownership, internal cross-package dependencies +use the pnpm `workspace:` protocol, which resolves only inside the workspace +and never falls back to the registry, closing the dependency-confusion path +even for an unclaimed name, and every package not deliberately published +carries `"private": true`. Directory-derived scopes such as `@tsworkspace/*` +are not used: the scope names the owning identity, not the repository layout, +and it must remain a namespace the project can actually own on the registry. + +### 2. The console is a Vite single-page application, not a meta-framework app + +The console is a client-rendered SPA built with Vite. Next.js, Nuxt, and +similar server-rendering meta-frameworks are not used. Their value is a +server tier for SEO and anonymous first paint, which this surface does not +have, and that tier would duplicate session and credential responsibilities +that ADR 0018 assigns to the gateway. The build output is static assets served +by the console gateway, keeping the product surface one deployable workload. + +If server rendering ever becomes a real requirement, the migration path is +TanStack Start, which builds on the same router and Vite toolchain. + +### 3. React with a TanStack-first library rule + +The console is a React application. When a needed capability has a viable +TanStack option, the console uses it before considering alternatives: + +- Routing: TanStack Router with file-based route trees. +- Server state: TanStack Query over the generated Connect clients. Query owns + every server-derived value; no server data is copied into client stores. +- Tables: TanStack Table, with TanStack Virtual when row counts require + virtualization. +- Forms: TanStack Form. Form owns edit state; submission goes through a + Connect mutation and Query invalidation. +- Client-only state: component-local state by default. If shared client state + accumulates, TanStack Store before any other state library. + +Deviating from a viable TanStack option requires a stated reason in the code +review that introduces the deviation. + +### 4. UI components are shadcn on Base UI, and Radix is prohibited + +Styling is Tailwind CSS. Components follow the shadcn model (source-owned +components generated into the repository, not a packaged component +dependency) built on Base UI primitives. Exactly one headless primitive layer +is allowed in the dependency tree: `@radix-ui/*` packages must not appear, +directly or transitively. Community shadcn components that import Radix are +ported to Base UI rather than adopted as-is. The workspace enforces the ban +with tooling rather than review vigilance. + +### 5. Validation is zod at the boundary, per the ADR 0009 conversion rule + +Runtime validation uses zod, wired into TanStack Form through Standard +Schema. Generated protobuf types do not leak primitive obsession into +application code: screens and forms convert generated messages into richer +edit models at the boundary when validation, units, identity, or invariants +matter, the same conversion rule +[ADR 0009](./0009-protocol-buffers-wire-contracts.md) sets for domain code. + +### 6. Generated Connect clients come from the existing Buf pipeline + +`protobuf-es` and `connect-es` code generation is added to the repository's +`buf.gen.yaml`, emitting a generated protocol package under +`tsworkspace/packages/`. The `.proto` sources under `proto/` remain the only +wire contract; the console defines no hand-written request or response +shapes. + +### 7. Testing is unit-first, per ADR 0010's posture + +- Vitest with Testing Library for unit and component tests. +- Connect's router transport for API mocking, so tests exercise the same + generated service interfaces the app uses instead of intercepting HTTP. +- Playwright for end-to-end coverage of the flows that cannot be trusted to + unit tests, primarily authentication and live streaming. + +### 8. Lint and format run on the Oxc toolchain + +Linting is oxlint and formatting is oxfmt, run workspace-wide, replacing the +ESLint + Prettier combination. One Rust toolchain covers both jobs with near +instant runs, which matches the repository's tooling posture, and oxlint's +default rule set applies without a config sprawl. Generated files +(`routeTree.gen.ts`) are excluded from formatting. Adopting ESLint later is +warranted only if a needed rule class (for example exhaustive React hooks +checks) is missing from oxlint, and that adoption is additive rather than a +replacement. + +### 9. Telemetry follows ADR 0008 + +The console uses the OpenTelemetry Web SDK, exports OTLP to the same +collector path the platform uses, and propagates `traceparent` on every +Connect call through a client interceptor so one operator action traces from +the browser through the gateway onto the backbone +([ADR 0008](./0008-opentelemetry-observability.md)). + +### 10. The console scales by vertical slices, not microfrontends + +Each product area (agents, schedules, catalog, execution) is a vertical +slice: its own package or route subtree, lazily loaded through the router, +with screens, queries, and forms owned by the slice. Slices do not import +each other's internals; contact happens through shared packages or route +navigation. + +Runtime composition schemes (module federation, microfrontends) are not +used. They solve independent deploy cadence across many teams, which this +surface does not have, at the permanent cost of duplicated dependencies, +singleton version skew, and design drift. The reopening conditions are: + +- Multiple teams needing independent deploy cadence into one surface, which + would justify revisiting composition. +- Untrusted third-party UI, which gets sandboxed iframes with a message + bridge, not module federation, because the problem is isolation. + +A different audience, trust level, or release cadence is a new app under +`tsworkspace/apps/` with its own gateway workload (ADR 0018), sharing code +through workspace packages at build time, never through runtime federation. + +## Consequences + +- The first product surface establishes `tsworkspace/` with `apps/console/` + and the shared `packages/` split, so later TypeScript surfaces inherit a + working layout instead of a greenfield decision. +- End-to-end typing holds from `.proto` to screen: generated Connect clients, + typed routes, typed forms, and zod-validated edit models. +- The Radix prohibition keeps one primitive layer's focus, portal, and + dismissal semantics across the console, at the cost of occasionally porting + a community component to Base UI. +- The TanStack-first rule removes a per-feature library debate and keeps one + vendor's conventions across routing, data, tables, and forms. +- Chart tooling is deliberately not pinned here; it is a reversible choice + to make when the first chart lands. +- A future schema-driven UI (rendering service-declared config and status + surfaces from the ADR 0012 discovery catalog) is the preferred path for + third-party extensibility, keeping extension a matter of publishing + schemas rather than shipping JavaScript into the console. + +## References + +- [ADR 0005: Polyglot Workspace Layout](./0005-polyglot-workspace-layout.md) +- [ADR 0008: OpenTelemetry Observability](./0008-opentelemetry-observability.md) +- [ADR 0009: Protocol Buffers Wire Contracts](./0009-protocol-buffers-wire-contracts.md) +- [ADR 0010: Unit Tests First, Testcontainers Only When Necessary](./0010-testcontainers-for-infrastructure-tests.md) +- [ADR 0012: ARD-Compatible Discovery Catalog](./0012-ard-compatible-discovery-catalog.md) +- [ADR 0018: ConnectRPC Gateway for Browser Product Surfaces](./0018-connectrpc-gateway-for-browser-product-surfaces.md) +- [TanStack](https://tanstack.com/) +- [Base UI](https://base-ui.com/) +- [shadcn/ui](https://ui.shadcn.com/) diff --git a/docs/adr/index.md b/docs/adr/index.md index a106725fe..9450ff49e 100644 --- a/docs/adr/index.md +++ b/docs/adr/index.md @@ -22,3 +22,5 @@ future implementation work. - [ADR 0015: Rust TLS Library](./0015-rust-tls-library.md) - [ADR 0016: Protocol Buffers RPC over NATS micro Binding](./0016-protobuf-rpc-over-nats-micro-binding.md) - [ADR 0017: AAuth Agent Authentication over a Trogon NATS PoP Binding](./0017-aauth-agent-authentication.md) +- [ADR 0018: ConnectRPC Gateway for Browser Product Surfaces](./0018-connectrpc-gateway-for-browser-product-surfaces.md) +- [ADR 0019: Console Webapp Stack](./0019-console-webapp-stack.md) diff --git a/tsworkspace/.gitignore b/tsworkspace/.gitignore new file mode 100644 index 000000000..300176e7e --- /dev/null +++ b/tsworkspace/.gitignore @@ -0,0 +1,6 @@ +coverage +playwright-report +test-results +.tanstack +*.local +**/routeTree.gen.ts diff --git a/tsworkspace/.npmrc b/tsworkspace/.npmrc new file mode 100644 index 000000000..cffe8cdef --- /dev/null +++ b/tsworkspace/.npmrc @@ -0,0 +1 @@ +save-exact=true diff --git a/tsworkspace/.oxfmtrc.json b/tsworkspace/.oxfmtrc.json new file mode 100644 index 000000000..1fef4afd8 --- /dev/null +++ b/tsworkspace/.oxfmtrc.json @@ -0,0 +1,26 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "sortImports": {}, + "sortTailwindcss": { + "functions": ["clsx"] + }, + "ignorePatterns": [ + ".next/**", + "dist/**", + "node_modules/**", + "coverage/**", + "storybook-static/**", + "playwright-report/**", + "test-results/**", + "*.min.js", + "pnpm-lock.yaml", + "*.hbs", + "**/routeTree.gen.ts" + ] +} diff --git a/tsworkspace/apps/console/components.json b/tsworkspace/apps/console/components.json new file mode 100644 index 000000000..7f1011e4f --- /dev/null +++ b/tsworkspace/apps/console/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-nova", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide", + "rtl": false, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/tsworkspace/apps/console/index.html b/tsworkspace/apps/console/index.html new file mode 100644 index 000000000..fd0a05a27 --- /dev/null +++ b/tsworkspace/apps/console/index.html @@ -0,0 +1,12 @@ + + + + + + @trogonai/console + + +
+ + + diff --git a/tsworkspace/apps/console/package.json b/tsworkspace/apps/console/package.json new file mode 100644 index 000000000..30c065f65 --- /dev/null +++ b/tsworkspace/apps/console/package.json @@ -0,0 +1,57 @@ +{ + "name": "@trogonai/console", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "generate-routes": "tsr generate", + "build": "vite build", + "preview": "vite preview", + "test": "vitest run", + "typecheck": "tsr generate && tsc --noEmit", + "check:primitives": "sh -c '! grep -q \"@radix-ui/\" ../../pnpm-lock.yaml'", + "lint": "oxlint ." + }, + "dependencies": { + "@base-ui/react": "1.6.0", + "@tailwindcss/vite": "4.3.2", + "@tanstack/react-devtools": "0.10.8", + "@tanstack/react-form": "1.33.0", + "@tanstack/react-query": "5.101.2", + "@tanstack/react-router": "1.170.17", + "@tanstack/react-router-devtools": "1.167.0", + "@tanstack/react-table": "8.21.3", + "@tanstack/router-plugin": "1.168.19", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "lucide-react": "0.545.0", + "react": "19.2.7", + "react-dom": "19.2.7", + "tailwind-merge": "3.6.0", + "tailwindcss": "4.3.2", + "tw-animate-css": "1.4.0", + "zod": "4.4.3" + }, + "devDependencies": { + "@tanstack/devtools-vite": "0.8.1", + "@tanstack/router-cli": "1.167.18", + "@testing-library/dom": "10.4.1", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/react": "16.3.2", + "@types/node": "22.20.0", + "@types/react": "19.2.17", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "6.0.3", + "jsdom": "28.1.0", + "shadcn": "4.13.0", + "typescript": "6.0.3", + "vite": "8.1.3", + "vitest": "4.1.10" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild", + "lightningcss" + ] + } +} diff --git a/tsworkspace/apps/console/public/favicon.ico b/tsworkspace/apps/console/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/tsworkspace/apps/console/public/favicon.ico differ diff --git a/tsworkspace/apps/console/src/components/ui/button.tsx b/tsworkspace/apps/console/src/components/ui/button.tsx new file mode 100644 index 000000000..25aecb672 --- /dev/null +++ b/tsworkspace/apps/console/src/components/ui/button.tsx @@ -0,0 +1,50 @@ +import { Button as ButtonPrimitive } from '@base-ui/react/button'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +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-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_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 dark:border-input dark:bg-input/30 dark:hover:bg-input/50', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground', + ghost: + 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50', + destructive: + 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40', + 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, buttonVariants }; diff --git a/tsworkspace/apps/console/src/components/ui/card.tsx b/tsworkspace/apps/console/src/components/ui/card.tsx new file mode 100644 index 000000000..a7e96f7c9 --- /dev/null +++ b/tsworkspace/apps/console/src/components/ui/card.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Card({ className, size = 'default', ...props }: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) { + return ( +
img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl', + className, + )} + {...props} + /> + ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +function CardAction({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<'div'>) { + return
; +} + +function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ); +} + +export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }; diff --git a/tsworkspace/apps/console/src/components/ui/input.tsx b/tsworkspace/apps/console/src/components/ui/input.tsx new file mode 100644 index 000000000..f88cf7a9c --- /dev/null +++ b/tsworkspace/apps/console/src/components/ui/input.tsx @@ -0,0 +1,20 @@ +import { Input as InputPrimitive } from '@base-ui/react/input'; +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Input({ className, type, ...props }: React.ComponentProps<'input'>) { + return ( + + ); +} + +export { Input }; diff --git a/tsworkspace/apps/console/src/components/ui/label.tsx b/tsworkspace/apps/console/src/components/ui/label.tsx new file mode 100644 index 000000000..b0776c37c --- /dev/null +++ b/tsworkspace/apps/console/src/components/ui/label.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +function Label({ className, ...props }: React.ComponentProps<'label'>) { + return ( +