From a4b09eb0ad631bc60c7938416492654faa0b0c57 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:00:45 -0400 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20SHOC-alignment=20Phase=201=20?= =?UTF-8?q?=E2=80=94=20ADRs,=20governance=20files,=20doc=20corrections=20(?= =?UTF-8?q?#220)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ADR 0002 (SHOC merge boundary: separate backend services, shared conventions) and ADR 0003 (adopt SHOC design system + UI/UX layout) - Add CODEOWNERS (internal-dev) and PR template (Summary/Test plan/Jira/ docs-current checklist + contract-table convention) - Fix stale facts in README/CLAUDE.md: MUI v7→v9, RN 0.85→0.86, OpenSearch Serverless/oss-index-creator → Aurora pgvector/ aurora-pgvector-init (ADR 0001), test counts 149→186 (123 xUnit / 26 vitest / 37 pytest), deploy triggers are workflow_dispatch-only, reusable workflow refs float on @main, aws-cdk-lib version claim replaced with Dependabot-maintained note --- .github/CODEOWNERS | 2 + .github/pull_request_template.md | 21 +++++++ CLAUDE.md | 18 ++++-- README.md | 44 +++++++------- docs/adr/0002-shoc-merge-boundary.md | 62 ++++++++++++++++++++ docs/adr/0003-shoc-design-system-adoption.md | 49 ++++++++++++++++ 6 files changed, 167 insertions(+), 29 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/pull_request_template.md create mode 100644 docs/adr/0002-shoc-merge-boundary.md create mode 100644 docs/adr/0003-shoc-design-system-adoption.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..db61049 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Default: all changes require internal-dev review +* @Sea-Haven-Industries/internal-dev diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..1072bdc --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +## Summary + + + +## Test plan + + + +## Jira / tracking + + + +## Checklist + +- [ ] Docs current — README / CLAUDE.md / ADRs / Confluence updated if this changes architecture, endpoints, or behavior +- [ ] If an endpoint shape changed: contract table included below and `shared/api-contracts` updated + + diff --git a/CLAUDE.md b/CLAUDE.md index fa96f5a..5d761bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,10 +6,10 @@ Proposal management platform for Sea Haven Industries. Dispatchers submit servic ## Architecture -- **api/**: .NET 8 API, EF Core, PostgreSQL, Cognito JWT auth -- **web/**: React 19 + MUI v7 SPA, Vite, CloudFront + S3 -- **mobile/**: React Native 0.85 iOS app, offline-capable, Hermes -- **lambdas/**: Python 3.12 Lambdas (ARM64): pdf-extract, pdf-generate, library-ingest, suggestions, oss-index-creator +- **api/**: .NET 8 API, EF Core, PostgreSQL (Aurora Serverless v2), Cognito JWT auth +- **web/**: React 19 + MUI v9 SPA, Vite, CloudFront + S3 +- **mobile/**: React Native 0.86 iOS app, offline-capable, Hermes +- **lambdas/**: Python 3.12 Lambdas (ARM64): pdf-extract, pdf-generate, library-ingest, suggestions, aurora-pgvector-init - **infra/**: CDK TypeScript (foundation-stack, compute-stack, frontend-stack) - **shared/**: Shared TypeScript API contracts - **scripts/**: Local dev helpers @@ -19,6 +19,12 @@ Proposal management platform for Sea Haven Industries. Dispatchers submit servic External: Cognito JWT via API Gateway (web + mobile client IDs, groups: dispatchers/admins/sysadmins) Internal: Lambdas → .NET Function URL with Secrets Manager API key via custom middleware +## Key Decisions (ADRs in docs/adr/) + +- **ADR 0001**: Bedrock KB vector store is Aurora PostgreSQL + pgvector (replaced OpenSearch Serverless; `oss-index-creator` Lambda replaced by `aurora-pgvector-init`) +- **ADR 0002**: SHOC merge boundary — backends stay separate services permanently (PostgreSQL + Cognito here; SQL Server + ASP.NET Identity in SHOC); consolidation converges on conventions/layers/service patterns, never on platform +- **ADR 0003**: SHOC dev's design system + UI/UX layout is canonical (Montserrat/DM Sans, primary #1c75bc, 244px sidebar, CSS-variable single-token-source consumed by MUI via getCssVar); the old Nunito/#0c4f6f canon and the interim "Sea Haven Ops" Inter/#2563EB theme are superseded + ## Request Flow 1. Dispatcher submits proposal (web/mobile) → InReview (no Draft stage) @@ -29,7 +35,7 @@ Internal: Lambdas → .NET Function URL with Secrets Manager API key via custom ## Infrastructure -AWS us-east-1, RDS PostgreSQL 15, S3, SQS+DLQ, Cognito+Google OAuth, OpenSearch Serverless, Bedrock KB, GitHub Actions OIDC, CloudFront+S3 OAC +AWS us-east-1, Aurora PostgreSQL 15 Serverless v2 (RDS Data API + pgvector), S3, SQS+DLQ, Cognito+Google OAuth, Bedrock KB (Aurora pgvector store — ADR 0001), GitHub Actions OIDC, CloudFront+S3 OAC ## Agent Delegation Rules @@ -58,7 +64,7 @@ AWS us-east-1, RDS PostgreSQL 15, S3, SQS+DLQ, Cognito+Google OAuth, OpenSearch ## Audit Status -AUDIT-REPORT.md completed 2026-05-27. 5 Critical, 36 High, 75+ Medium, 60+ Low findings. Phase 1-5 complete: all Critical/High fixed, 17 Medium fixed, CI runs 108 tests. +AUDIT-REPORT.md completed 2026-05-27. 5 Critical, 36 High, 75+ Medium, 60+ Low findings. Phase 1-5 complete: all Critical/High fixed, 17 Medium fixed, CI runs 186 tests (123 xUnit, 26 vitest, 37 pytest). ### Critical Findings (fix first) diff --git a/README.md b/README.md index 879b0cb..2c4dde6 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ Internal proposal management platform for Sea Haven Industries. Dispatchers subm Monorepo with five primary services: - **.NET 8 API** -- Clean Architecture REST API hosted on Lambda behind API Gateway (JWT-authorized) with Function URL (AWS_IAM) for internal access -- **React 19 Web** -- MUI v7 admin/dispatcher workspace served via CloudFront + S3 +- **React 19 Web** -- MUI v9 admin/dispatcher workspace served via CloudFront + S3 - **React Native Mobile** -- iOS-first field app for dispatchers (offline-capable) -- **Python Lambdas** -- PDF extraction, PDF generation, library ingestion, AI suggestions, AOSS index provisioning +- **Python Lambdas** -- PDF extraction, PDF generation, library ingestion, AI suggestions, Aurora pgvector bootstrap - **CDK Infrastructure** -- Three TypeScript stacks managing all AWS resources ## Repository Structure @@ -23,8 +23,8 @@ Monorepo with five primary services: ``` proposal-system/ ├── api/ .NET 8 Web API (Lambda-hosted, EF Core + PostgreSQL) -├── web/ React 19 + MUI v7 + Vite frontend -├── mobile/ React Native 0.85 iOS app +├── web/ React 19 + MUI v9 + Vite frontend +├── mobile/ React Native 0.86 iOS app ├── lambdas/ Python 3.12 processing functions (arm64) ├── infra/ CDK TypeScript (3 stacks) ├── shared/ TypeScript API contracts (shared between web + mobile) @@ -38,11 +38,11 @@ proposal-system/ | Component | Technologies | |---|---| | API | .NET 8, ASP.NET Core, EF Core + Npgsql, FluentValidation, Cognito JWT, Amazon.Lambda.AspNetCoreServer | -| Web | React 19, TypeScript, MUI v7, Vite, Redux Toolkit, TanStack Query, axios | -| Mobile | React Native CLI 0.85, React 19, React Native Paper, React Navigation, react-native-app-auth (PKCE), amazon-cognito-identity-js (SRP), Keychain, offline draft queue | +| Web | React 19, TypeScript, MUI v9, Vite, Redux Toolkit, TanStack Query, axios | +| Mobile | React Native CLI 0.86, React 19, React Native Paper, React Navigation, react-native-app-auth (PKCE), amazon-cognito-identity-js (SRP), Keychain, offline draft queue | | Lambdas | Python 3.12, arm64, pdfplumber, reportlab, httpx, boto3 | -| Infrastructure | CDK TypeScript (aws-cdk-lib 2.253.1) | -| AI/RAG | Bedrock Knowledge Base (Titan Embeddings v2), OpenSearch Serverless (VPC-only), Claude Sonnet via Bedrock cross-region inference | +| Infrastructure | CDK TypeScript (aws-cdk-lib pinned exact, kept current by Dependabot) | +| AI/RAG | Bedrock Knowledge Base (Titan Embeddings v2), Aurora PostgreSQL + pgvector vector store (see ADR 0001), Claude Sonnet via Bedrock cross-region inference | | Auth | Cognito User Pool + Google OAuth IdP (groups: dispatchers, admins, sysadmins) | ## AWS Resources @@ -51,8 +51,8 @@ All resources are in **us-east-1** (account 328440206208). | CDK Stack | Key Resources | |---|---| -| `proposal-system-foundation` | RDS PostgreSQL 15 (t4g.small), S3 buckets, SQS queue + DLQ, Cognito user pool, Secrets Manager | -| `proposal-system-compute` | API Gateway HTTP API (JWT authorizer + access logging), .NET 8 API Lambda + Function URL (AWS_IAM), Python Lambdas (pdf-extract, pdf-generate, library-ingest, suggestions, oss-index-creator), OpenSearch Serverless collection (VPC endpoint), Bedrock KB | +| `proposal-system-foundation` | Aurora PostgreSQL 15 Serverless v2 (RDS Data API, pgvector), S3 buckets, SQS queue + DLQ, Cognito user pool, Secrets Manager | +| `proposal-system-compute` | API Gateway HTTP API (JWT authorizer + access logging), .NET 8 API Lambda + Function URL (AWS_IAM), Python Lambdas (pdf-extract, pdf-generate, library-ingest, suggestions, aurora-pgvector-init bootstrap), Bedrock KB (Aurora pgvector store) | | `proposal-system-frontend` | CloudFront distribution (S3 OAC) | | Resource Type | Names | @@ -118,17 +118,17 @@ Seven parallel jobs calling org reusable workflows: | Job | Workflow | What it checks | |---|---|---| -| .NET Build & Test | `ci-dotnet.yaml` | Restore, build, test the API solution (104 xUnit tests) | +| .NET Build & Test | `ci-dotnet.yaml` | Restore, build, test the API solution (123 xUnit tests) | | Web Frontend Check | `ci-typescript-cdk.yaml` | TypeScript typecheck for web | -| Web Tests | `ci-typescript-cdk.yaml` | vitest suite (26 tests — auth, interceptors, components) | +| Web Tests | inline job | vitest suite (26 tests — auth, interceptors, components) | | Mobile Typecheck | `ci-typescript-cdk.yaml` | TypeScript typecheck for mobile | | Python Lint | `ci-python-sam.yaml` | ruff check + format on lambdas/ | -| Python Tests | `ci-python-sam.yaml` | pytest suite (19 tests — pdf-generate, suggestions handlers) | +| Python Tests | inline job | pytest suite (37 tests — pdf-generate, suggestions, library-ingest, internal API signing) | | CDK Synth | `ci-typescript-cdk.yaml` | Synthesize CDK stacks (includes .NET publish) | -### Deploy (on push to main) +### Deploy (manual) -Calls `cd-cdk.yaml` reusable workflow: +Auto-deploy on push to main is currently disabled (PR #167) — both deploy workflows run via `workflow_dispatch` from the Actions tab. `deploy.yaml` calls the `cd-cdk.yaml` reusable workflow: 1. Publishes .NET 8 API and Python Lambdas 2. Runs `cdk deploy --all` 3. Executes `scripts/post-deploy.sh` (builds web, syncs to S3, invalidates CloudFront) @@ -139,9 +139,7 @@ Deploy uses OIDC role `githubdeploy-proposal-system`. Concurrency group prevents Workflow: `deploy-mobile.yaml` -- builds and uploads to TestFlight via `cd-mobile-ios.yaml` reusable workflow on `macos-26`. -Triggers: -- **Automatic**: push to `main` with changes in `mobile/**` -- **Manual**: `workflow_dispatch` for on-demand builds +Trigger: **manual only** (`workflow_dispatch`) while the app is pre-V1, to save macOS runner cost. ## Mobile iOS @@ -186,13 +184,13 @@ Failed SQS messages are reported via `batchItemFailures` and retried up to 3 tim ## Testing -149 tests across three stacks, all run in CI on every PR: +186 tests across three stacks, all run in CI on every PR: | Suite | Framework | Count | Coverage | |---|---|---|---| -| .NET API | xUnit | 104 | State machine transitions, authorization attributes, middleware, validators, ProposalNumberGenerator, LineItemService state guards | +| .NET API | xUnit | 123 | State machine transitions, authorization attributes, middleware, validators, ProposalNumberGenerator, LineItemService state guards | | Web | vitest | 26 | ProtectedRoute, RoleGuard, API client interceptor (401 logout, token attachment) | -| Python Lambdas | pytest | 19 | pdf-generate and suggestions handler contracts | +| Python Lambdas | pytest | 37 | pdf-generate, suggestions, library-ingest handler contracts, internal API signing | ```bash cd api && dotnet test # .NET tests @@ -207,7 +205,7 @@ Hardening applied across all layers (see AUDIT-REPORT.md for full details): - **Auth:** Cognito JWT validation with audience check, startup fails if auth not configured, DevMode gated to `IsDevelopment()` - **API:** FluentValidation on all DTOs, generic error responses (no stack traces or config leaks), structured audit logging with before/after diffs - **Function URL:** AWS_IAM auth + internal API key (two-layer defense) -- **Infrastructure:** S3 `enforceSSL` + `BLOCK_ALL`, SQS managed encryption, OpenSearch VPC-only, Cognito optional TOTP MFA, API Gateway access logging +- **Infrastructure:** S3 `enforceSSL` + `BLOCK_ALL`, SQS managed encryption, Aurora in private subnets, Cognito optional TOTP MFA, API Gateway access logging - **Lambdas:** Prompt injection sanitization, PDF size limits, numeric validation on AI suggestions, S3 key sanitization, idempotent SQS processing -- **CI/CD:** OIDC (no long-lived credentials), SHA-pinned workflow refs, `--require-approval broadening` on local deploys +- **CI/CD:** OIDC (no long-lived credentials), org reusable workflows (`@main`), `--require-approval broadening` on local deploys - **Web:** sessionStorage for tokens (not localStorage), error boundaries, role guards on all admin routes, 401 interceptor clears auth state diff --git a/docs/adr/0002-shoc-merge-boundary.md b/docs/adr/0002-shoc-merge-boundary.md new file mode 100644 index 0000000..c603636 --- /dev/null +++ b/docs/adr/0002-shoc-merge-boundary.md @@ -0,0 +1,62 @@ +# ADR 0002 — SHOC merge boundary: separate backend services, shared conventions + +- **Status:** Accepted (2026-07-13) +- **Decision owner:** Adam Moussa +- **Scope:** `proposal-system` ↔ SHOC (`shoc-backend` / `shoc-frontend-new`) consolidation strategy + +## Context + +proposal-system was built as a standalone service deliberately mirroring SHOC's +architecture for a clean future merge. As of 2026-07-13 the two projects diverge on +platform fundamentals: + +| Axis | proposal-system | SHOC | +|---|---|---| +| Database | Aurora PostgreSQL (pgvector) via EF Core/Npgsql | SQL Server via EF Core | +| Identity | Cognito (hosted UI, groups, web + mobile clients) | ASP.NET Identity + custom JWT issuance | +| API layout | 4-project Clean Architecture (Api/Application/Infrastructure/Domain) | Single API project, services injecting DbContext | +| Error contract | RFC 7807 ProblemDetails, string enums | Typed-exception codes per controller, numeric enums | +| Hosting | Lambda behind API Gateway (CDK) | Elastic Beanstalk (external-dev account) | + +A full platform alignment (engine migration, auth migration, rehosting) would cost +weeks, carry data-migration risk, and deliver no user value. + +## Decision + +**The backends remain separate services permanently. Consolidation converges on +conventions, layers, and service patterns — never on platform.** + +Concretely: + +1. **No database engine migration** in either direction. PostgreSQL stays here; + SQL Server stays in SHOC. +2. **No identity migration now.** Cognito stays here. At consolidation time, identity + converges on Cognito (or a federation layer in front of both) — not on + ASP.NET Identity, which would regress to self-managed credentials and reintroduce + the remediated WEB-C1 token-theft class (SHOC currently stores JWTs in + localStorage). +3. **Conventions converge** (tracked by the 2026-07 SHOC-alignment plan): + - Frontend: SHOC's design system + UI/UX layout (ADR 0003), domain layering + (`src/domain//{api,schemas,mappers,types,use-cases}`), TanStack Query, + zod, react-hook-form. + - API contracts: shared TypeScript contracts package + zod schemas; ProblemDetails + with machine-readable business `code` extensions (SHOC's error-code vocabulary, + proposal-system's envelope). + - Mutation semantics: optimistic concurrency with SHOC's 409-plus-currentState + response shape; staged audit-trail entries persisted at one SaveChanges boundary. + - Process: org reusable CI callers, CODEOWNERS, PR template, conventional commits. +4. **Layering stays Clean Architecture here.** SHOC's flatter service style is not + adopted; if SHOC restructures at consolidation, this repo's 4-project layout is the + reference shape. + +## Consequences + +- The eventual "merge" is a monorepo consolidation of independently deployable + services sharing a frontend architecture and wire conventions — not a single + backend. +- A merged frontend can treat both APIs identically for errors (ProblemDetails + + `code`) and conflicts (409 + currentState) once alignment phases 3/6 land. +- Anything requiring one database across both domains (cross-domain reporting, + shared entities) must go through APIs, not shared tables. +- SHOC's localStorage JWT + CORS `*` are flagged to the SHOC team as findings; they + are not constraints on this repo. diff --git a/docs/adr/0003-shoc-design-system-adoption.md b/docs/adr/0003-shoc-design-system-adoption.md new file mode 100644 index 0000000..741dade --- /dev/null +++ b/docs/adr/0003-shoc-design-system-adoption.md @@ -0,0 +1,49 @@ +# ADR 0003 — Adopt the SHOC design system and UI/UX layout + +- **Status:** Accepted (2026-07-13) +- **Decision owner:** Adam Moussa +- **Scope:** `web/` theming, layout shell, and all future proposal-system UI work + +## Context + +Three design-token sets existed across Sea Haven web apps as of 2026-07-13: + +1. **Old canon** — Nunito, primary `#0c4f6f`, 220px sidebar, `#f4f6f7` background + (what SHOC's stale `docs/DESIGN_SYSTEM.md` still describes). +2. **SHOC dev's new system** (shoc-frontend-new PR #17) — Montserrat (display) / + DM Sans (body) / JetBrains Mono, primary `#1c75bc`, navy `#262262`, page background + `#f9fafb`, 244px sidebar (76px collapsed), 64px gradient topbar + (`#1b1f52 → #1c4f8f → #1c75bc`), defined in a single CSS-variable token file + (`src/styles/theme.css`) consumed by both MUI (`getCssVar` → `createTheme`) and + Tailwind v4. +3. **proposal-system's "Sea Haven Ops" theme** — Inter, accent `#2563EB`, + navy-900 AppBar, 216px drawer (`web/src/theme.ts`). + +## Decision + +**SHOC dev's new design system and UI/UX layout (set 2) is the canonical Sea Haven +standard.** proposal-system adopts it fully: + +- **Tokens:** mirror shoc-frontend-new dev's `theme.css` values and structure + (fonts, palette, radii, spacing, shadows). +- **Mechanism:** single CSS-variable token file is the one source of truth; + `web/src/theme.ts` becomes a thin `getCssVar` → MUI `createTheme` adapter matching + SHOC's `mui-theme.ts`. Token changes are values-only edits, never theme rewrites. +- **Layout shell:** SHOC's admin shell dimensions and treatment — 244px/76px-collapse + sidebar, 64px gradient topbar, avatar initials. +- **Component library:** MUI only. Tailwind (SHOC runs a MUI + Tailwind v4 hybrid off + the same CSS variables) is **not** adopted here pre-consolidation; the shared + CSS-variable source keeps that door open. + +The reference is shoc-frontend-new's **`dev` branch** at adoption time; subsequent +SHOC token changes should be ported as values-only updates. + +## Consequences + +- The "Sea Haven Ops" theme (Inter/#2563EB) is retired; alignment plan Phase 2 + implements the port. +- Visual verification for Phase 2 compares proposal-system screens side-by-side with + SHOC dev screens, not with the old canon. +- The superseded Nunito/`#0c4f6f` tokens must not be reintroduced anywhere. +- At monorepo consolidation, both frontends already share token structure, so a + single `theme.css` can serve both. From 8ce2a5cd8fbf9ae53cf5c38c1a053adb49f3a078 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:06:51 -0400 Subject: [PATCH 2/2] feat(web): adopt SHOC design system and shell layout (ADR 0003) (#221) Port shoc-frontend-new dev's design system with its CSS-variable single-token-source mechanism: - src/styles/theme.css: SHOC token file ported verbatim (Montserrat/ DM Sans/JetBrains Mono, primary #1c75bc, navy #262262, full radius/ shadow/sidebar/header token layers); fonts self-hosted via @fontsource - src/lib/theme/{css-vars,mui-theme}.ts: getCssVar -> createTheme adapter mirroring SHOC's mui-theme.ts (palette, typography, shadows tuple, component overrides; MUI v9 slot renames expressed as class selectors); theme.ts is now a re-export - Shell: SHOC composition (sidebar column + sticky gradient topbar + scrolling main); sidebar 244px/76px collapse with brand header row, grouped nav, SHOC active treatment (white card + 3px accent bar); topbar 100-degree gradient, surface hamburger, gradient avatar pill - Brand: SeahavenMark + BrandLockup ported (Tailwind re-expressed as sx; wordmark subtitle localized to PROPOSAL SYSTEM) - Login: SHOC auth-card treatment (centered 384px card on #f9fafb) - Old "Sea Haven Ops" Inter/#2563EB theme and Nunito remnants removed; remaining hardcoded hexes replaced with tokens; lucide-react for shell/nav icons per SHOC convention Verify: tsc clean, 26/26 vitest, vite build OK; Playwright screenshots pixel-sampled against the extracted SHOC spec (all hard values exact, no blocking deviations). --- web/index.html | 3 - web/package-lock.json | 40 ++ web/package.json | 4 + web/src/App.tsx | 36 +- web/src/components/Sidebar.tsx | 289 ++++++++------ web/src/components/Topbar.tsx | 129 ++++--- web/src/components/brand/BrandLockup.tsx | 68 ++++ web/src/components/brand/SeahavenMark.tsx | 21 ++ web/src/constants/index.ts | 2 +- web/src/index.css | 13 +- web/src/lib/theme/css-vars.ts | 32 ++ web/src/lib/theme/mui-theme.ts | 295 +++++++++++++++ web/src/main.tsx | 1 + .../pages/admin/dashboard/AdminDashboard.tsx | 2 +- .../pages/admin/workspace/AdminWorkspace.tsx | 2 +- web/src/pages/auth/LoginPage.tsx | 246 ++++++------ web/src/pages/dashboard/Dashboard.tsx | 4 +- web/src/styles/fonts.css | 12 + web/src/styles/globals.css | 5 + web/src/styles/motion.css | 6 + web/src/styles/theme.css | 188 +++++++++ web/src/styles/typography.css | 52 +++ web/src/theme.ts | 357 +----------------- 23 files changed, 1134 insertions(+), 673 deletions(-) create mode 100644 web/src/components/brand/BrandLockup.tsx create mode 100644 web/src/components/brand/SeahavenMark.tsx create mode 100644 web/src/lib/theme/css-vars.ts create mode 100644 web/src/lib/theme/mui-theme.ts create mode 100644 web/src/styles/fonts.css create mode 100644 web/src/styles/globals.css create mode 100644 web/src/styles/motion.css create mode 100644 web/src/styles/theme.css create mode 100644 web/src/styles/typography.css diff --git a/web/index.html b/web/index.html index 62fb501..7b7a1a9 100644 --- a/web/index.html +++ b/web/index.html @@ -4,9 +4,6 @@ Proposal System - Sea Haven Industries - - -
diff --git a/web/package-lock.json b/web/package-lock.json index f0b34a7..60df75c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,11 +10,15 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@fontsource/dm-sans": "^5.2.8", + "@fontsource/jetbrains-mono": "^5.2.8", + "@fontsource/montserrat": "^5.2.8", "@mui/icons-material": "^9.2.0", "@mui/material": "^9.1.1", "@reduxjs/toolkit": "^2.11.2", "@tanstack/react-query": "^5.101.2", "axios": "^1.18.1", + "lucide-react": "^1.24.0", "react": "^19.2.7", "react-dom": "^19.2.7", "react-redux": "^9.2.0", @@ -582,6 +586,33 @@ } } }, + "node_modules/@fontsource/dm-sans": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/dm-sans/-/dm-sans-5.2.8.tgz", + "integrity": "sha512-tlovG42m9ESG28WiHpLq3F5umAlm64rv0RkqTbYowRn70e9OlRr5a3yTJhrhrY+k5lftR/OFJjPzOLQzk8EfCA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/jetbrains-mono": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz", + "integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/montserrat": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/montserrat/-/montserrat-5.2.8.tgz", + "integrity": "sha512-xTjLxSbSfCycDB0pwmNsfNvdfWPaDaRQ2LC6yt/ZI7SdvXG52zHnzNYC/09mzuAuWNJyShkteutfCoDgym56hQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2943,6 +2974,15 @@ "node": "20 || >=22" } }, + "node_modules/lucide-react": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.24.0.tgz", + "integrity": "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", diff --git a/web/package.json b/web/package.json index 245c73e..42707cc 100644 --- a/web/package.json +++ b/web/package.json @@ -13,11 +13,15 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@fontsource/dm-sans": "^5.2.8", + "@fontsource/jetbrains-mono": "^5.2.8", + "@fontsource/montserrat": "^5.2.8", "@mui/icons-material": "^9.2.0", "@mui/material": "^9.1.1", "@reduxjs/toolkit": "^2.11.2", "@tanstack/react-query": "^5.101.2", "axios": "^1.18.1", + "lucide-react": "^1.24.0", "react": "^19.2.7", "react-dom": "^19.2.7", "react-redux": "^9.2.0", diff --git a/web/src/App.tsx b/web/src/App.tsx index 94a14ad..f247d00 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,5 +1,5 @@ import { Routes, Route, Navigate } from 'react-router-dom'; -import { Box, Button, Card, CardContent, Toolbar, Typography } from '@mui/material'; +import { Box, Button, Card, CardContent, Typography } from '@mui/material'; import PeopleIcon from '@mui/icons-material/People'; import ProtectedRoute, { RoleGuard } from './components/ProtectedRoute'; import Topbar from './components/Topbar'; @@ -25,20 +25,21 @@ export default function App() { path="/*" element={ - - + {/* SHOC shell composition (ADR 0003): sidebar column + [topbar / scrolling main] */} + - - - + + + + } /> } /> } /> @@ -55,7 +56,7 @@ export default function App() { - + User Management Users and roles are currently managed in AWS Cognito. Contact the system administrator to update access. @@ -75,8 +76,9 @@ export default function App() { } /> - } /> - + } /> + + diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx index 617277b..191c619 100644 --- a/web/src/components/Sidebar.tsx +++ b/web/src/components/Sidebar.tsx @@ -1,48 +1,45 @@ +// App shell sidebar — SHOC design system port (ADR 0003, mirrors app-sidebar.tsx). +// Expanded 244px / collapsed 76px; brand header row; grouped nav with SHOC +// active treatment (white card bg + shadow + 3px left accent bar). import { useLocation, useNavigate } from 'react-router-dom'; import { useSelector, useDispatch } from 'react-redux'; +import { Drawer, Box, Tooltip, useMediaQuery, useTheme } from '@mui/material'; import { - Drawer, - List, - ListItemButton, - ListItemIcon, - ListItemText, - Toolbar, - Box, - Typography, - Tooltip, - useMediaQuery, - useTheme, -} from '@mui/material'; -import DashboardIcon from '@mui/icons-material/Dashboard'; -import DescriptionIcon from '@mui/icons-material/Description'; -import AddCircleIcon from '@mui/icons-material/AddCircle'; -import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'; -import AssignmentIcon from '@mui/icons-material/Assignment'; -import PeopleIcon from '@mui/icons-material/People'; -import BusinessIcon from '@mui/icons-material/Business'; -import MenuBookIcon from '@mui/icons-material/MenuBook'; + LayoutDashboard, + FileText, + CirclePlus, + ClipboardList, + FolderOpen, + Building2, + BookOpen, + Users, + type LucideIcon, +} from 'lucide-react'; import { selectSidebarOpen, setSidebarOpen } from '../app/slices/uiSlice'; import { selectUser } from '../app/slices/authSlice'; import type { RootState } from '../app/store'; import { DRAWER_WIDTH } from '../constants'; +import BrandLockup, { BrandMark } from './brand/BrandLockup'; -const COLLAPSED_WIDTH = 72; +const COLLAPSED_WIDTH = 76; // --spacing-sidebar-collapsed -const dispatcherNav = [ - { label: 'Dashboard', path: '/', icon: }, - { label: 'My Proposals', path: '/proposals', icon: }, - { label: 'New Proposal', path: '/proposals/new', icon: }, +type NavEntry = { label: string; path: string; icon: LucideIcon }; + +const dispatcherNav: NavEntry[] = [ + { label: 'Dashboard', path: '/', icon: LayoutDashboard }, + { label: 'My Proposals', path: '/proposals', icon: FileText }, + { label: 'New Proposal', path: '/proposals/new', icon: CirclePlus }, ]; -const adminNav = [ - { label: 'Admin Queue', path: '/admin', icon: }, - { label: 'All Proposals', path: '/admin/proposals', icon: }, - { label: 'Customers', path: '/admin/customers', icon: }, - { label: 'Pricing Library', path: '/admin/pricing-library', icon: }, +const adminNav: NavEntry[] = [ + { label: 'Admin Queue', path: '/admin', icon: ClipboardList }, + { label: 'All Proposals', path: '/admin/proposals', icon: FolderOpen }, + { label: 'Customers', path: '/admin/customers', icon: Building2 }, + { label: 'Pricing Library', path: '/admin/pricing-library', icon: BookOpen }, ]; -const sysadminNav = [ - { label: 'Users', path: '/admin/users', icon: }, +const sysadminNav: NavEntry[] = [ + { label: 'Users', path: '/admin/users', icon: Users }, ]; const ALL_NAV_PATHS = new Set( @@ -56,6 +53,88 @@ function isNavActive(pathname: string, itemPath: string): boolean { return pathname.startsWith(itemPath + '/'); } +function NavItem({ + item, + active, + collapsed, + onClick, +}: { + item: NavEntry; + active: boolean; + collapsed: boolean; + onClick: () => void; +}) { + const Icon = item.icon; + const row = ( + + {active && ( + + )} + + {!collapsed && ( + + {item.label} + + )} + + ); + + if (collapsed) { + return ( + + {row} + + ); + } + return row; +} + export default function Sidebar() { const navigate = useNavigate(); const location = useLocation(); @@ -65,14 +144,14 @@ export default function Sidebar() { const user = useSelector((state: RootState) => selectUser(state)); const isMobile = useMediaQuery(theme.breakpoints.down('md')); // <900px - const isCollapsed = useMediaQuery(theme.breakpoints.between('md', 'lg')); // 900–1200px const isAdmin = user?.role === 'Admin' || user?.role === 'SysAdmin'; const isSysAdmin = user?.role === 'SysAdmin'; - const navItems = dispatcherNav; - const drawerVariant = isMobile ? 'temporary' : 'persistent'; - const effectiveWidth = isCollapsed ? COLLAPSED_WIDTH : DRAWER_WIDTH; + // Desktop: hamburger toggles expanded (244) ↔ collapsed (76). + // Mobile: temporary drawer, hamburger opens/closes it. + const collapsed = !isMobile && !open; + const width = collapsed ? COLLAPSED_WIDTH : DRAWER_WIDTH; const handleNavClick = (path: string) => { navigate(path); @@ -81,107 +160,69 @@ export default function Sidebar() { } }; - const handleDrawerClose = () => { - dispatch(setSidebarOpen(false)); - }; + const groupLabel = (label: string) => ( + + {label} + + ); - const renderNavItem = (item: { label: string; path: string; icon: React.ReactNode }) => { - const button = ( - handleNavClick(item.path)} - sx={{ - py: 1, - justifyContent: isCollapsed ? 'center' : 'flex-start', - minHeight: 40, - }} - > - - {item.icon} - - {!isCollapsed && ( - - )} - - ); + const groupDivider = ( + + ); - if (isCollapsed) { - return ( - - {button} - - ); - } - - return button; - }; + const renderGroup = (label: string, items: NavEntry[], first: boolean) => ( + <> + {collapsed ? (first ? null : groupDivider) : groupLabel(label)} + {items.map((item) => ( + handleNavClick(item.path)} + /> + ))} + + ); return ( dispatch(setSidebarOpen(false))} sx={{ - width: open ? effectiveWidth : 0, + width: isMobile ? 0 : width, flexShrink: 0, - transition: 'width 250ms ease', '& .MuiDrawer-paper': { - width: effectiveWidth, + width: isMobile ? DRAWER_WIDTH : width, boxSizing: 'border-box', + backgroundColor: 'var(--color-sidebar-bg)', + borderRight: '1px solid var(--color-sidebar-border)', + borderRadius: 0, overflowX: 'hidden', + transition: 'width 200ms var(--ease-default, ease)', }, }} > - - - {isAdmin && ( - <> - {!isCollapsed && ( - - Admin - - )} - - {adminNav.map((item) => renderNavItem(item))} - {isSysAdmin && sysadminNav.map((item) => renderNavItem(item))} - - - )} - {!isCollapsed && ( - - Proposals - - )} - - {navItems.map((item) => renderNavItem(item))} - + {/* Brand header row — 64px, white, aligns with the topbar */} + + {collapsed ? : } + + + + {isAdmin && renderGroup('Admin', [...adminNav, ...(isSysAdmin ? sysadminNav : [])], true)} + {renderGroup('Proposals', dispatcherNav, !isAdmin)} ); diff --git a/web/src/components/Topbar.tsx b/web/src/components/Topbar.tsx index 9a04831..5ba17f6 100644 --- a/web/src/components/Topbar.tsx +++ b/web/src/components/Topbar.tsx @@ -1,7 +1,7 @@ +// App shell top bar — SHOC design system port (ADR 0003, mirrors app-topbar.tsx). import { useDispatch } from 'react-redux'; -import { AppBar, Toolbar, IconButton, Typography, Box, Avatar } from '@mui/material'; -import MenuIcon from '@mui/icons-material/Menu'; -import LogoutIcon from '@mui/icons-material/Logout'; +import { AppBar, Toolbar, IconButton, Box, Avatar, Typography } from '@mui/material'; +import { Menu as MenuIcon, LogOut } from 'lucide-react'; import { toggleSidebar } from '../app/slices/uiSlice'; import { useAuth } from '../hooks/useAuth'; @@ -11,70 +11,103 @@ export default function Topbar() { const initials = user?.displayName ? user.displayName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) - : ''; + : 'U'; const isDevMode = !import.meta.env.VITE_COGNITO_DOMAIN; return ( - theme.zIndex.drawer + 1, height: 64 }}> - + + dispatch(toggleSidebar())} - sx={{ mr: 2 }} aria-label="Toggle sidebar menu" + sx={{ + width: 38, + height: 38, + borderRadius: 'var(--radius-md)', + backgroundColor: 'var(--color-header-surface)', + '&:hover': { backgroundColor: 'var(--color-header-surface-hover)' }, + }} > - + - - Proposal System - {isDevMode && ( - + DEV + + )} + + + + {user && ( + + - DEV + {initials} + + + + {user.displayName} + + + {user.role} + - )} - - {user && ( - - - - {initials} - - - - {user.displayName} - - - {user.role} - - - - - + )} diff --git a/web/src/components/brand/BrandLockup.tsx b/web/src/components/brand/BrandLockup.tsx new file mode 100644 index 0000000..4472ac9 --- /dev/null +++ b/web/src/components/brand/BrandLockup.tsx @@ -0,0 +1,68 @@ +// Sea Haven brand lockup — ported from shoc-frontend-new dev (brand-lockup.tsx), +// Tailwind classes re-expressed as sx (this app is MUI-only per ADR 0003). +// Wordmark subtitle localized to this app: "PROPOSAL SYSTEM". +import { Box } from '@mui/material'; +import SeahavenMark from './SeahavenMark'; + +type BrandLockupSize = 'default' | 'compact'; + +export function BrandMark({ size = 'default' }: { size?: BrandLockupSize }) { + const compact = size === 'compact'; + return ( + + + + ); +} + +export function BrandWordmark({ size = 'default' }: { size?: BrandLockupSize }) { + const compact = size === 'compact'; + return ( + + + SEAHAVEN + + + PROPOSAL SYSTEM + + + ); +} + +export default function BrandLockup({ size = 'default' }: { size?: BrandLockupSize }) { + return ( + + + + + ); +} diff --git a/web/src/components/brand/SeahavenMark.tsx b/web/src/components/brand/SeahavenMark.tsx new file mode 100644 index 0000000..f60a4b1 --- /dev/null +++ b/web/src/components/brand/SeahavenMark.tsx @@ -0,0 +1,21 @@ +// Sea Haven brand mark — ported from shoc-frontend-new dev (seahaven-mark.tsx). +export default function SeahavenMark({ size = 18 }: { size?: number }) { + return ( + + ); +} diff --git a/web/src/constants/index.ts b/web/src/constants/index.ts index 36ebcb3..89b2b60 100644 --- a/web/src/constants/index.ts +++ b/web/src/constants/index.ts @@ -38,7 +38,7 @@ export const STATUS_CHIP_STYLES: Record = { Standard: 'Standard', diff --git a/web/src/index.css b/web/src/index.css index 6ad3d12..9acafa1 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,5 +1,4 @@ -@import url("https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,600;0,700;1,400&display=swap"); - +/* App-level base styles. Fonts/tokens/typography live in src/styles/globals.css. */ * { box-sizing: border-box; } @@ -9,16 +8,12 @@ html, body, #root { } body { - margin: 0; - font-family: "Nunito", "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 13px; - background: #f4f6f7; - color: #333; - line-height: 1.5; + font-size: var(--text-base); + line-height: var(--leading-normal); } a { - color: #007bff; + color: var(--primary); text-decoration: none; } diff --git a/web/src/lib/theme/css-vars.ts b/web/src/lib/theme/css-vars.ts new file mode 100644 index 0000000..7f73b3d --- /dev/null +++ b/web/src/lib/theme/css-vars.ts @@ -0,0 +1,32 @@ +/** + * CSS custom-property bridge — mirrors shoc-frontend-new dev src/lib/theme/css-vars.ts. + * Reads live tokens from :root at theme-creation time; falls back to the static + * map when document is unavailable (tests, pre-paint). + */ +export function getCssVar(name: string, fallback = ''): string { + if (typeof document === 'undefined') return fallback; + const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); + return value || fallback; +} + +export const CSS_VAR_FALLBACKS = { + primary: '#1c75bc', + primaryHover: '#155a92', + primaryForeground: '#ffffff', + destructive: '#dc2626', + success: '#00a79d', + warning: '#d97706', + background: '#ffffff', + foreground: '#262262', + border: '#dfe3ea', + muted: '#e6e7e8', + mutedForeground: '#58595b', + inputBackground: '#f9fafb', + ring: '#1c75bc', + bgPage: '#f9fafb', + radius: '8', + headerStart: '#1b1f52', + headerMid: '#1c4f8f', + headerEnd: '#1c75bc', + headerText: '#ffffff', +} as const; diff --git a/web/src/lib/theme/mui-theme.ts b/web/src/lib/theme/mui-theme.ts new file mode 100644 index 0000000..28be2c5 --- /dev/null +++ b/web/src/lib/theme/mui-theme.ts @@ -0,0 +1,295 @@ +import { createTheme } from '@mui/material/styles'; +import type { Shadows } from '@mui/material/styles'; +import { getCssVar, CSS_VAR_FALLBACKS } from './css-vars'; + +/** + * MUI adapter over the CSS-variable token file (src/styles/theme.css). + * Mirrors shoc-frontend-new dev src/lib/theme/mui-theme.ts (ADR 0003). + * Token changes are values-only edits in theme.css — never here. + */ + +declare module '@mui/material/styles' { + interface Palette { + header: Palette['primary']; + } + interface PaletteOptions { + header?: PaletteOptions['primary']; + } +} + +const v = (name: string, fallback: string) => getCssVar(name, fallback); + +const primary = v('--primary', CSS_VAR_FALLBACKS.primary); +const primaryHover = v('--primary-hover', CSS_VAR_FALLBACKS.primaryHover); +const primaryForeground = v('--primary-foreground', CSS_VAR_FALLBACKS.primaryForeground); +const destructive = v('--destructive', CSS_VAR_FALLBACKS.destructive); +const success = v('--success', CSS_VAR_FALLBACKS.success); +const warning = v('--warning', CSS_VAR_FALLBACKS.warning); +const background = v('--background', CSS_VAR_FALLBACKS.background); +const foreground = v('--foreground', CSS_VAR_FALLBACKS.foreground); +const border = v('--border', CSS_VAR_FALLBACKS.border); +const muted = v('--muted', CSS_VAR_FALLBACKS.muted); +const mutedForeground = v('--muted-foreground', CSS_VAR_FALLBACKS.mutedForeground); +const inputBackground = v('--input-background', CSS_VAR_FALLBACKS.inputBackground); +const ring = v('--ring', CSS_VAR_FALLBACKS.ring); +const bgPage = v('--color-bg-page', CSS_VAR_FALLBACKS.bgPage); +const radius = parseInt(v('--radius', `${CSS_VAR_FALLBACKS.radius}px`), 10); +const headerEnd = v('--color-header-bg-end', CSS_VAR_FALLBACKS.headerEnd); +const headerText = v('--color-header-text', CSS_VAR_FALLBACKS.headerText); + +const shadowSm = v('--shadow-sm', '0 1px 3px rgba(22, 30, 56, 0.06), 0 1px 2px rgba(22, 30, 56, 0.04)'); +const shadowMd = v('--shadow-md', '0 4px 6px -1px rgba(22, 30, 56, 0.08), 0 2px 4px -2px rgba(22, 30, 56, 0.06)'); +const shadowLg = v('--shadow-lg', '0 10px 15px -3px rgba(22, 30, 56, 0.1), 0 4px 6px -4px rgba(22, 30, 56, 0.08)'); + +const fontSans = '"DM Sans", ui-sans-serif, system-ui, sans-serif'; +const fontDisplay = '"Montserrat", ui-sans-serif, system-ui, sans-serif'; + +// MUI's 25-slot elevation tuple, fully overridden: 0=none, 1–2=sm, 3–5=md, 6+=lg. +const shadows = [ + 'none', + shadowSm, + shadowSm, + shadowMd, + shadowMd, + shadowMd, + ...Array(19).fill(shadowLg), +] as Shadows; + +export const muiTheme = createTheme({ + palette: { + mode: 'light', + primary: { + main: primary, + dark: primaryHover, + contrastText: primaryForeground, + }, + // Repurposed as the muted/neutral swatch (SHOC convention), not a brand color. + secondary: { + main: muted, + contrastText: foreground, + }, + error: { + main: destructive, + contrastText: '#ffffff', + }, + success: { + main: success, + contrastText: '#ffffff', + }, + warning: { + main: warning, + contrastText: '#ffffff', + }, + background: { + default: bgPage, + paper: background, + }, + text: { + primary: foreground, + secondary: mutedForeground, + }, + divider: border, + header: { + main: headerEnd, + contrastText: headerText, + }, + }, + + typography: { + fontFamily: fontSans, + fontSize: 14, + fontWeightRegular: 400, + fontWeightMedium: 500, + fontWeightBold: 600, + h1: { fontFamily: fontDisplay, fontWeight: 800, fontSize: '1.75rem', lineHeight: 1.25 }, + h2: { fontFamily: fontDisplay, fontWeight: 700, fontSize: '1.375rem', lineHeight: 1.25 }, + h3: { fontFamily: fontDisplay, fontWeight: 600, fontSize: '1.125rem', lineHeight: 1.25 }, + h4: { fontFamily: fontDisplay, fontWeight: 600, fontSize: '1rem', lineHeight: 1.25 }, + h5: { fontFamily: fontDisplay, fontWeight: 600, fontSize: '0.875rem', lineHeight: 1.25 }, + h6: { fontFamily: fontDisplay, fontWeight: 600, fontSize: '0.8125rem', lineHeight: 1.25 }, + subtitle1: { fontWeight: 600, fontSize: '1rem' }, + subtitle2: { fontWeight: 500, fontSize: '0.875rem', color: mutedForeground }, + body1: { fontSize: '0.875rem', lineHeight: 1.5 }, + body2: { fontSize: '0.8125rem', lineHeight: 1.5, color: mutedForeground }, + caption: { fontSize: '0.75rem', color: mutedForeground }, + button: { textTransform: 'none' as const, fontWeight: 600, fontSize: '0.875rem' }, + overline: { + fontSize: '0.6875rem', + fontWeight: 600, + letterSpacing: '0.08em', + textTransform: 'uppercase' as const, + color: mutedForeground, + }, + }, + + shape: { + borderRadius: radius, + }, + + shadows, + + components: { + MuiCssBaseline: { + styleOverrides: { + body: { + fontSize: 14, + backgroundColor: bgPage, + color: foreground, + }, + }, + }, + + MuiButton: { + defaultProps: { + disableElevation: true, + }, + styleOverrides: { + root: { + borderRadius: 'var(--radius-md)', + textTransform: 'none' as const, + fontWeight: 600, + fontSize: '0.875rem', + transition: 'background-color var(--duration-fast) var(--ease-default), box-shadow var(--duration-fast) var(--ease-default)', + '&:focus-visible': { + outline: `2px solid ${ring}`, + outlineOffset: 2, + }, + // MUI v9: containedPrimary slot removed — class selector instead + '&.MuiButton-containedPrimary:hover': { + backgroundColor: primaryHover, + }, + }, + outlined: { + borderColor: border, + '&:hover': { + borderColor: 'var(--border-hover)', + backgroundColor: 'var(--secondary)', + }, + }, + }, + }, + + MuiTextField: { + defaultProps: { + size: 'small' as const, + }, + }, + + MuiOutlinedInput: { + styleOverrides: { + root: { + backgroundColor: inputBackground, + borderRadius: 'var(--radius-md)', + '& .MuiOutlinedInput-notchedOutline': { + borderColor: border, + }, + '&:hover .MuiOutlinedInput-notchedOutline': { + borderColor: 'var(--border-hover)', + }, + '&.Mui-focused': { + boxShadow: `0 0 0 3px color-mix(in srgb, ${ring} 25%, transparent)`, + }, + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: ring, + borderWidth: 2, + }, + }, + input: { + padding: '8.5px 13px', + fontSize: '0.875rem', + '&.MuiOutlinedInput-inputAdornedEnd': { + paddingRight: 40, + }, + }, + }, + }, + + MuiTableCell: { + styleOverrides: { + root: { + fontSize: '0.875rem', + borderColor: border, + padding: '10px 16px', + }, + head: { + fontWeight: 600, + backgroundColor: 'var(--color-bg-muted)', + color: foreground, + }, + }, + }, + + MuiTableRow: { + styleOverrides: { + root: { + '&:last-child .MuiTableCell-root': { + borderBottom: 0, + }, + }, + }, + }, + + MuiChip: { + styleOverrides: { + root: { + borderRadius: 'var(--radius-sm)', + fontWeight: 500, + fontSize: '0.75rem', + }, + outlined: { + borderColor: border, + }, + }, + }, + + MuiPaper: { + defaultProps: { + elevation: 0, + }, + styleOverrides: { + root: { + backgroundImage: 'none', + borderRadius: radius, + }, + outlined: { + borderColor: border, + }, + }, + }, + + MuiDialog: { + styleOverrides: { + paper: { + borderRadius: 'var(--radius-xl)', + boxShadow: shadowLg, + }, + }, + }, + + MuiAlert: { + styleOverrides: { + root: { + borderRadius: 'var(--radius-md)', + fontSize: '0.875rem', + // MUI v9: standard* slots removed — class selectors instead + '&.MuiAlert-standardSuccess': { + backgroundColor: 'var(--status-completed-bg)', + color: 'var(--status-completed-text)', + }, + '&.MuiAlert-standardError': { + backgroundColor: 'var(--doc-no-bg)', + color: 'var(--doc-no-text)', + }, + '&.MuiAlert-standardWarning': { + backgroundColor: 'var(--status-pending-bg)', + color: 'var(--status-pending-text)', + }, + '&.MuiAlert-standardInfo': { + backgroundColor: 'var(--color-primary-tint)', + color: 'var(--sidebar-accent-foreground)', + }, + }, + }, + }, + }, +}); diff --git a/web/src/main.tsx b/web/src/main.tsx index f56fab5..0d0ee59 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -6,6 +6,7 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { ThemeProvider, CssBaseline } from '@mui/material'; import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; +import './styles/globals.css'; import './index.css'; import { store } from './app/store'; import { queryClient } from './lib/queryClient'; diff --git a/web/src/pages/admin/dashboard/AdminDashboard.tsx b/web/src/pages/admin/dashboard/AdminDashboard.tsx index bdf4578..b664728 100644 --- a/web/src/pages/admin/dashboard/AdminDashboard.tsx +++ b/web/src/pages/admin/dashboard/AdminDashboard.tsx @@ -56,7 +56,7 @@ function StatCard({ icon, label, value, color, subtitle }: { icon: React.ReactNo {label} - + {value} {subtitle && ( diff --git a/web/src/pages/admin/workspace/AdminWorkspace.tsx b/web/src/pages/admin/workspace/AdminWorkspace.tsx index ff32e7c..87729e8 100644 --- a/web/src/pages/admin/workspace/AdminWorkspace.tsx +++ b/web/src/pages/admin/workspace/AdminWorkspace.tsx @@ -310,7 +310,7 @@ export default function AdminWorkspace() { component="button" underline="hover" onClick={() => guardedNavigate('/admin')} - sx={{ fontSize: 14, color: '#2563EB', cursor: 'pointer' }} + sx={{ fontSize: 14, color: 'var(--primary)', cursor: 'pointer' }} > Admin Queue diff --git a/web/src/pages/auth/LoginPage.tsx b/web/src/pages/auth/LoginPage.tsx index 9f2e7a2..8694088 100644 --- a/web/src/pages/auth/LoginPage.tsx +++ b/web/src/pages/auth/LoginPage.tsx @@ -1,7 +1,9 @@ +// Login — SHOC auth-card treatment (ADR 0003, mirrors AuthPageShell/AuthCardHeader): +// centered 384px card on --color-bg-page, brand lockup, Montserrat heading. import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useDispatch } from 'react-redux'; -import { Alert, Box, Button, Card, CardContent, Typography, Divider, Stack } from '@mui/material'; +import { Alert, Box, Button, Typography, Divider, Stack } from '@mui/material'; import GoogleIcon from '@mui/icons-material/Google'; import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings'; import EngineeringIcon from '@mui/icons-material/Engineering'; @@ -9,6 +11,7 @@ import PersonIcon from '@mui/icons-material/Person'; import { useAuth } from '../../hooks/useAuth'; import { authApi } from '../../lib/api/auth'; import { setUser, setError } from '../../app/slices/authSlice'; +import BrandLockup from '../../components/brand/BrandLockup'; const COGNITO_DOMAIN = import.meta.env.VITE_COGNITO_DOMAIN || ''; const CLIENT_ID = import.meta.env.VITE_COGNITO_CLIENT_ID || ''; @@ -24,6 +27,12 @@ function buildLoginUrl(): string { return `https://${COGNITO_DOMAIN}/oauth2/authorize?${params.toString()}`; } +const devButtonSx = { + py: 1.5, + height: 56, + justifyContent: 'flex-start', +} as const; + export default function LoginPage() { const navigate = useNavigate(); const dispatch = useDispatch(); @@ -62,126 +71,135 @@ export default function LoginPage() { - - - + + + + + + Welcome back + + + Sign in to your account + + + {error && ( + + {error} + + )} + + {isDevMode ? ( + <> + + + + + + + + Development sign-in — only shown when Cognito is not configured. + + + ) : ( + - - - - - - Dev login is only shown when Cognito is not configured. - - - ) : ( - <> - - Sign in with your Sea Haven Google account to continue. - - - - )} - - - - Sea Haven Industries © {new Date().getFullYear()} + Sign in with Google + + )} + + + Having trouble? Contact your administrator. diff --git a/web/src/pages/dashboard/Dashboard.tsx b/web/src/pages/dashboard/Dashboard.tsx index 3728186..56931a9 100644 --- a/web/src/pages/dashboard/Dashboard.tsx +++ b/web/src/pages/dashboard/Dashboard.tsx @@ -34,11 +34,11 @@ function KpiCard({ icon, label, value, subtitle }: { icon: React.ReactNode; labe return ( - {icon} + {icon} {label} - + {value} {subtitle && ( diff --git a/web/src/styles/fonts.css b/web/src/styles/fonts.css new file mode 100644 index 0000000..bf759c9 --- /dev/null +++ b/web/src/styles/fonts.css @@ -0,0 +1,12 @@ +/* Self-hosted fonts (@fontsource) — same weights as shoc-frontend-new dev. */ +@import "@fontsource/montserrat/600.css"; +@import "@fontsource/montserrat/700.css"; +@import "@fontsource/montserrat/800.css"; + +@import "@fontsource/dm-sans/400.css"; +@import "@fontsource/dm-sans/500.css"; +@import "@fontsource/dm-sans/600.css"; + +@import "@fontsource/jetbrains-mono/400.css"; +@import "@fontsource/jetbrains-mono/500.css"; +@import "@fontsource/jetbrains-mono/600.css"; diff --git a/web/src/styles/globals.css b/web/src/styles/globals.css new file mode 100644 index 0000000..db4a57b --- /dev/null +++ b/web/src/styles/globals.css @@ -0,0 +1,5 @@ +/* Global stylesheet — import order mirrors shoc-frontend-new dev (minus Tailwind). */ +@import "./fonts.css"; +@import "./theme.css"; +@import "./typography.css"; +@import "./motion.css"; diff --git a/web/src/styles/motion.css b/web/src/styles/motion.css new file mode 100644 index 0000000..afba9f4 --- /dev/null +++ b/web/src/styles/motion.css @@ -0,0 +1,6 @@ +/* Motion tokens — mirrors shoc-frontend-new dev src/styles/motion.css. */ +:root { + --duration-fast: 150ms; + --duration-normal: 250ms; + --ease-default: cubic-bezier(0.4, 0, 0.2, 1); +} diff --git a/web/src/styles/theme.css b/web/src/styles/theme.css new file mode 100644 index 0000000..bb1ce7d --- /dev/null +++ b/web/src/styles/theme.css @@ -0,0 +1,188 @@ +/* + * Sea Haven design tokens — single source of truth (ADR 0003). + * Ported verbatim from shoc-frontend-new dev src/styles/theme.css (values-only + * changes belong here; never rewrite the MUI adapter to change a color). + * The SHOC @theme inline Tailwind block is intentionally omitted — this app is MUI-only. + */ +:root { + /* Body typography reference — do not set on html (rem uses 16px root) */ + --font-size: 14px; + + /* ── TYPOGRAPHY ─────────────────────────────────────────────────── */ + --font-family-display: "Montserrat", ui-sans-serif, system-ui, sans-serif; + --font-family-sans: "DM Sans", ui-sans-serif, system-ui, sans-serif; + --font-family-mono: "JetBrains Mono", ui-monospace, monospace; + --font-display: var(--font-family-display); + --font-sans: var(--font-family-sans); + --font-mono: var(--font-family-mono); + --font-heading: var(--font-family-display); + + /* ── TYPOGRAPHY SCALE ───────────────────────────────────────────── */ + --text-xs: 11px; + --text-sm: 12px; + --text-base-sm: 13px; + --text-base: 14px; + --text-md: 16px; + --text-lg: 18px; + --text-xl: 22px; + --text-2xl: 28px; + --leading-tight: 1.25; + --leading-normal: 1.5; + --leading-relaxed: 1.625; + --tracking-tight: -0.01em; + --tracking-normal: 0; + --tracking-wide: 0.05em; + --tracking-wider: 0.08em; + + /* ── LAYOUT SPACING ─────────────────────────────────────────────── */ + --spacing-sidebar: 244px; + --spacing-topbar: 64px; + --spacing-sidebar-collapsed: 76px; + + /* ── RADIUS SCALE (scale resolves to 4 / 6 / 8 / 12) ────────────── */ + --radius: 8px; + --radius-md: 6px; + --radius-sm: 4px; + --radius-xl: 12px; + + /* ── ELEVATION ──────────────────────────────────────────────────── */ + --shadow-sm: 0 1px 3px rgba(22, 30, 56, 0.06), 0 1px 2px rgba(22, 30, 56, 0.04); + --shadow-md: 0 4px 6px -1px rgba(22, 30, 56, 0.08), 0 2px 4px -2px rgba(22, 30, 56, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(22, 30, 56, 0.1), 0 4px 6px -4px rgba(22, 30, 56, 0.08); + --shadow-brand-mark: 0 2px 6px -1px rgba(28, 117, 188, 0.45); + + /* ── SHADCN BASE (Seahaven DS) ──────────────────────────────────── */ + --background: #ffffff; + --foreground: #262262; + --placeholder-foreground: color-mix(in srgb, var(--foreground) 50%, transparent); + --card: #ffffff; + --card-foreground: #262262; + --popover: #ffffff; + --popover-foreground: #262262; + --primary: #1c75bc; + --primary-hover: #155a92; + --primary-foreground: #ffffff; + --secondary: #f1f4f9; + --secondary-foreground: #262262; + --muted: #e6e7e8; + --muted-foreground: #58595b; + --accent: #27aae1; + --accent-foreground: #ffffff; + --success: #00a79d; + --warning: #d97706; + --destructive: #dc2626; + --destructive-foreground: #ffffff; + --border: #dfe3ea; + --border-hover: #c8d0e0; + --input: transparent; + --input-background: #f9fafb; + --switch-background: #c8d0e0; + --ring: #1c75bc; + + --chart-1: #1c75bc; + --chart-2: #00a79d; + --chart-3: #d97706; + --chart-4: #7c3aed; + --chart-5: #dc2626; + + /* ── SIDEBAR (light surface — Seahaven DS) ──────────────────────── */ + --sidebar: #f6f8fb; + --sidebar-bg: #f6f8fb; + --sidebar-foreground: #262262; + --sidebar-primary: #1c75bc; + --sidebar-primary-foreground: #ffffff; + --sidebar-accent: #e6f4fb; + --sidebar-accent-foreground: #155a92; + --sidebar-border: #dfe3ea; + --sidebar-ring: #1c75bc; + + /* ── SEAHAVEN SEMANTIC ALIASES (consumed across the app) ────────── */ + --color-primary: var(--primary); + --color-primary-hover: var(--primary-hover); + --color-primary-foreground: #ffffff; + --color-primary-tint: #e6f4fb; + --color-accent: var(--accent); + + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--foreground); + --color-ring: var(--ring); + + --color-bg-page: #f9fafb; + --color-bg-surface: var(--card); + --color-bg-muted: #f1f4f9; + --color-border: var(--border); + --color-border-hover: #c8d0e0; + --color-text-primary: var(--foreground); + --color-text-secondary: #58595b; + --color-text-muted: #6b7a96; + --color-text-inverse: #ffffff; + + /* ── SIDEBAR ALIASES (light) ────────────────────────────────────── */ + --color-sidebar-bg: var(--sidebar-bg); + --color-sidebar-text: #262262; + --color-sidebar-text-hover: #155a92; + --color-sidebar-active-bg: var(--sidebar-accent); + --color-sidebar-active-text: var(--sidebar-accent-foreground); + --color-sidebar-hover-bg: #eef2f7; + --color-sidebar-label: #6b7a96; + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-badge-bg: #e6f4fb; + --color-sidebar-badge-text: #155a92; + --color-sidebar-item-active-bg: var(--card); + --color-sidebar-item-active-shadow: 0 1px 3px rgba(38, 34, 98, 0.08); + + /* ── SEMANTIC (universal meaning) ───────────────────────────────── */ + --color-destructive: var(--destructive); + --color-destructive-fg: #ffffff; + --color-warning: var(--warning); + --color-success: var(--success); + + /* ── STATUS TOKENS (immutable — mirror SHOC; consumed by MuiAlert + mapping and proposal status badges) ──────────────────────────── */ + --status-incomplete-bg: #f3f4f6; + --status-incomplete-text: #374151; + --status-pending-bg: #fff7ed; + --status-pending-text: #92400e; + --status-scheduled-bg: #eff6ff; + --status-scheduled-text: #1e40af; + --status-inprogress-bg: #eff6ff; + --status-inprogress-text: #1d4ed8; + --status-completed-bg: #f0fdf4; + --status-completed-text: #166534; + --status-canceled-bg: #f9fafb; + --status-canceled-text: #6b7280; + --status-pastdue-bg: #fff1f2; + --status-pastdue-text: #9f1239; + + /* ── COMPLETION DOCUMENT (immutable — mirror SHOC) ──────────────── */ + --doc-yes-bg: #f0fdf4; + --doc-yes-text: #166534; + --doc-no-bg: #fff1f2; + --doc-no-text: #be123c; + --doc-nn-bg: #f3f4f6; + --doc-nn-text: #6b7280; + + /* ── SLA (immutable — mirror SHOC) ──────────────────────────────── */ + --sla-safe: #16a34a; + --sla-warning: #d97706; + --sla-critical: #dc2626; + + /* ── HEADER (app shell top bar) ─────────────────────────────────── */ + --color-header-bg-start: #1b1f52; + --color-header-bg-mid: #1c4f8f; + --color-header-bg-end: var(--primary); + --color-header-gradient: linear-gradient( + 100deg, + var(--color-header-bg-start) 0%, + var(--color-header-bg-mid) 58%, + var(--color-header-bg-end) 100% + ); + --color-header-shadow: 0 1px 0 rgba(38, 34, 98, 0.18), 0 6px 18px -10px rgba(28, 117, 188, 0.5); + --color-header-surface: rgba(255, 255, 255, 0.08); + --color-header-surface-hover: rgba(255, 255, 255, 0.18); + --color-header-chip-hover: rgba(255, 255, 255, 0.12); + --color-header-icon: #eaf3fb; + --color-header-text: #ffffff; + --color-header-text-muted: #bdd8f0; + --color-header-avatar-ring: rgba(255, 255, 255, 0.25); +} diff --git a/web/src/styles/typography.css b/web/src/styles/typography.css new file mode 100644 index 0000000..c9fb518 --- /dev/null +++ b/web/src/styles/typography.css @@ -0,0 +1,52 @@ +/* Base element typography — mirrors shoc-frontend-new dev src/styles/typography.css. */ +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; + font-family: var(--font-sans); + color: var(--foreground); + background-color: var(--color-bg-page); +} + +h1, h2, h3, h4, h5, h6 { + font-family: var(--font-heading); + color: var(--foreground); + margin: 0; +} + +h1 { font-size: var(--text-2xl); font-weight: 800; } +h2 { font-size: var(--text-xl); font-weight: 700; } +h3 { font-size: var(--text-lg); font-weight: 600; } +h4 { font-size: var(--text-md); font-weight: 600; } +h5 { font-size: var(--text-base); font-weight: 600; } +h6 { font-size: var(--text-base-sm); font-weight: 600; } + +.text-caption { + font-family: var(--font-sans); + font-size: var(--text-sm); + line-height: var(--leading-normal); + color: var(--muted-foreground); +} + +.text-label { + font-family: var(--font-sans); + font-size: var(--text-xs); + font-weight: 600; + line-height: 1.5; + letter-spacing: var(--tracking-wider); + text-transform: uppercase; + color: var(--color-sidebar-label); +} + +.text-nav-group { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--muted-foreground); +} diff --git a/web/src/theme.ts b/web/src/theme.ts index 353e284..abba78e 100644 --- a/web/src/theme.ts +++ b/web/src/theme.ts @@ -1,353 +1,4 @@ -import { createTheme } from '@mui/material/styles'; - -// Sea Haven Ops design system — flat, dense, border-driven enterprise UI. -// Neutral navy/slate/gray structure; action-blue #2563EB is the only brand accent. -// Tokens mirror the Sea Haven Ops design bundle (tokens/colors.css, spacing.css, typography.css). -export const theme = createTheme({ - palette: { - primary: { - main: '#2563EB', // action blue — primary buttons, links, selected - light: '#3B82F6', - dark: '#1D4ED8', // contained-button hover/pressed - contrastText: '#FFFFFF', - }, - secondary: { - main: '#4B5563', // slate-600 - light: '#6B7280', - dark: '#374151', - contrastText: '#FFFFFF', - }, - success: { - main: '#16A34A', - light: '#DCFCE7', - dark: '#166534', - contrastText: '#FFFFFF', - }, - warning: { - main: '#D97706', // orange — Urgent - light: '#FFEDD5', - dark: '#9A3412', - contrastText: '#FFFFFF', - }, - error: { - main: '#DC2626', // red — Emergency / overdue / destructive - light: '#FEE2E2', - dark: '#991B1B', - contrastText: '#FFFFFF', - }, - info: { - main: '#2563EB', - light: '#DBEAFE', - dark: '#1E40AF', - contrastText: '#FFFFFF', - }, - background: { - default: '#F3F4F6', // gray-100 page background - paper: '#FFFFFF', - }, - text: { - primary: '#111827', // navy-900 - secondary: '#4B5563', // slate-600 - disabled: '#9CA3AF', // gray-400 - }, - divider: '#E5E7EB', // gray-200 - }, - - typography: { - fontFamily: '"Inter", "Segoe UI", Roboto, Helvetica, Arial, sans-serif', - fontSize: 14, - h4: { - fontWeight: 700, - fontSize: '24px', - lineHeight: 1.25, - letterSpacing: '-0.02em', - }, - h5: { - fontWeight: 600, - fontSize: '18px', - lineHeight: 1.3, - }, - h6: { - fontWeight: 600, - fontSize: '16px', - lineHeight: 1.4, - }, - subtitle1: { - fontWeight: 600, - fontSize: '14px', - lineHeight: 1.4, - }, - body1: { - fontSize: '14px', - lineHeight: 1.5, - }, - body2: { - fontSize: '13px', - lineHeight: 1.45, - }, - caption: { - fontSize: '12px', - lineHeight: 1.35, - color: '#6B7280', - }, - button: { - fontSize: '14px', - fontWeight: 600, - textTransform: 'none' as const, - }, - overline: { - fontSize: '11px', - lineHeight: '16px', - fontWeight: 700, - letterSpacing: '0.08em', - textTransform: 'uppercase' as const, - color: '#6B7280', - }, - }, - - shape: { - borderRadius: 6, - }, - - components: { - MuiCssBaseline: { - styleOverrides: { - body: { - fontFeatureSettings: '"tnum"', - backgroundColor: '#F3F4F6', - }, - }, - }, - - MuiAppBar: { - defaultProps: { - elevation: 0, - }, - styleOverrides: { - root: { - backgroundColor: '#111827', // navy-900 - boxShadow: 'none', - borderRadius: 0, - borderBottom: '1px solid rgba(255,255,255,0.08)', - }, - }, - }, - - MuiDrawer: { - styleOverrides: { - paper: { - backgroundColor: '#FFFFFF', - borderRight: '1px solid #E5E7EB', // border-subtle - width: 216, - }, - }, - }, - - MuiCard: { - defaultProps: { - elevation: 0, - }, - styleOverrides: { - // Cards are border-driven — no shadow (overlays only). - root: { - border: '1px solid #E5E7EB', - boxShadow: 'none', - borderRadius: 8, - }, - }, - }, - - MuiPaper: { - defaultProps: { - elevation: 0, - }, - styleOverrides: { - root: { - backgroundImage: 'none', - borderRadius: 6, - }, - }, - }, - - MuiButton: { - styleOverrides: { - root: { - borderRadius: 6, - boxShadow: 'none', - textTransform: 'none' as const, - fontWeight: 600, - height: 36, - paddingLeft: 14, - paddingRight: 14, - }, - contained: { - boxShadow: 'none', - '&:hover': { - boxShadow: 'none', - }, - }, - outlined: { - borderColor: '#D1D5DB', // border-default - color: '#111827', - '&:hover': { - borderColor: '#9CA3AF', // border-strong - backgroundColor: '#F9FAFB', // gray-50 - }, - }, - }, - }, - - MuiTextField: { - defaultProps: { - size: 'small' as const, - }, - styleOverrides: { - root: { - '& .MuiOutlinedInput-root': { - borderRadius: 6, - }, - }, - }, - }, - - MuiOutlinedInput: { - styleOverrides: { - root: { - borderRadius: 6, - minHeight: 38, - '& .MuiOutlinedInput-notchedOutline': { - borderColor: '#D1D5DB', - }, - '&:hover .MuiOutlinedInput-notchedOutline': { - borderColor: '#9CA3AF', - }, - '&.Mui-focused .MuiOutlinedInput-notchedOutline': { - borderColor: '#2563EB', - borderWidth: 2, - }, - }, - input: { - fontSize: 14, - padding: '9px 12px', - }, - }, - }, - - MuiInputLabel: { - styleOverrides: { - root: { - fontSize: 13, - color: '#6B7280', - '&.Mui-focused': { - color: '#2563EB', - }, - }, - }, - }, - - MuiTableHead: { - styleOverrides: { - root: { - '& .MuiTableCell-head': { - color: '#374151', // slate-700 - fontWeight: 700, - fontSize: 13, - paddingTop: 12, - paddingBottom: 12, - borderBottom: '1px solid #E5E7EB', - backgroundColor: '#F9FAFB', // gray-50 - whiteSpace: 'nowrap', - }, - }, - }, - }, - - MuiTableCell: { - styleOverrides: { - root: { - color: '#111827', - fontSize: 14, - padding: '12px 14px', - borderBottom: '1px solid #E5E7EB', - }, - }, - }, - - MuiTableRow: { - styleOverrides: { - root: { - '&:hover': { - backgroundColor: '#F9FAFB', - }, - }, - }, - }, - - MuiChip: { - styleOverrides: { - root: { - height: 24, - borderRadius: 6, - fontSize: 12, - fontWeight: 700, - }, - sizeSmall: { - height: 22, - fontSize: 12, - }, - label: { - paddingLeft: 8, - paddingRight: 8, - }, - icon: { - fontSize: 14, - marginLeft: 6, - }, - }, - }, - - MuiListItemButton: { - styleOverrides: { - root: { - borderRadius: 6, - marginLeft: 8, - marginRight: 8, - marginBottom: 4, - color: '#4B5563', - '&.Mui-selected': { - backgroundColor: '#EFF6FF', // blue-50 - borderLeft: '3px solid #2563EB', - color: '#111827', - '& .MuiListItemIcon-root': { - color: '#2563EB', - }, - '&:hover': { - backgroundColor: '#DBEAFE', // blue-100 - }, - }, - '&:hover': { - backgroundColor: '#F3F4F6', // gray-100 - }, - }, - }, - }, - - MuiListItemIcon: { - styleOverrides: { - root: { - color: '#9CA3AF', // gray-400 — inactive nav icon - minWidth: 36, - }, - }, - }, - - MuiTooltip: { - styleOverrides: { - tooltip: { - fontSize: 12, - backgroundColor: '#1F2937', // navy-800 - }, - }, - }, - }, -}); +// Sea Haven design system (ADR 0003) — SHOC dev token port. +// Tokens live in src/styles/theme.css (single source of truth); +// src/lib/theme/mui-theme.ts is the getCssVar → createTheme adapter. +export { muiTheme as theme } from './lib/theme/mui-theme';