From db572f002bba8205ce2782bf315bbda9211a8307 Mon Sep 17 00:00:00 2001 From: Vinit Khandal Date: Wed, 8 Apr 2026 11:24:22 +0530 Subject: [PATCH 01/82] feat: add Blend Token Studio implementation and deployment setup --- TOKEN_STUDIO_IMPLEMENTATION.md | 1736 +++++++++++++++++ .../.dockerignore | 0 .../.env.example | 0 .../.eslintrc.json | 0 .../.gitignore | 0 .../DEPLOYMENT.md | 0 .../Dockerfile | 0 .../LOCAL_SETUP.md | 0 .../{blend-monitor => blend-studio}/README.md | 0 .../app/api/activity/recent/route.ts | 0 .../app/api/components/components-pg/route.ts | 0 .../app/api/components/route.ts | 0 .../app/api/health/route.ts | 0 .../app/api/npm/route.ts | 0 .../app/api/npm/stats/route.ts | 0 .../app/api/npm/sync/route.ts | 0 .../app/api/npm/trends/route.ts | 0 .../app/api/npm/versions/route.ts | 0 .../app/api/users/[userId]/role/route.ts | 0 .../app/api/users/[userId]/route.ts | 0 .../app/api/users/activity/route.ts | 0 .../app/api/users/route.ts | 0 .../app/code-connect/health/page.tsx | 0 .../app/code-connect/page.tsx | 0 .../app/favicon.ico | Bin .../app/globals.css | 0 .../app/layout.tsx | 0 .../app/login/page.tsx | 0 .../app/not-found.tsx | 0 .../app/npm/page.tsx | 0 .../app/page.tsx | 0 .../app/users/[userId]/activity/page.tsx | 0 .../app/users/page.tsx | 0 .../cloudbuild.yaml | 0 .../database/schema.sql | 0 .../{blend-monitor => blend-studio}/deploy.sh | 0 .../eslint.config.mjs | 0 .../extract-firebase-key.js | 0 .../middleware.ts | 0 .../next.config.ts | 0 .../package.json | 3 +- .../polyfills.ts | 0 .../postcss.config.mjs | 0 .../public/file.svg | 0 .../public/globe.svg | 0 .../public/next.svg | 0 .../public/vercel.svg | 0 .../public/window.svg | 0 .../src/README.md | 0 .../src/backend/api/activity/recent/route.ts | 0 .../api/components/components-pg/route.ts | 0 .../src/backend/api/components/route.ts | 0 .../src/backend/api/health/route.ts | 0 .../src/backend/api/npm/route.ts | 0 .../src/backend/api/npm/stats/route.ts | 0 .../src/backend/api/npm/sync/route.ts | 0 .../src/backend/api/npm/trends/route.ts | 0 .../src/backend/api/npm/versions/route.ts | 0 .../backend/api/users/[userId]/role/route.ts | 0 .../src/backend/api/users/[userId]/route.ts | 0 .../src/backend/api/users/activity/route.ts | 0 .../src/backend/api/users/route.ts | 0 .../src/backend/external/npm-client.ts | 0 .../src/backend/lib/auth-middleware.ts | 0 .../src/backend/lib/database-service.ts | 0 .../src/backend/lib/database.ts | 0 .../src/backend/lib/firebase-admin.ts | 0 .../src/backend/lib/role-service.ts | 0 .../src/backend/scanners/component-scanner.ts | 0 .../frontend/app/code-connect/health/page.tsx | 0 .../src/frontend/app/code-connect/page.tsx | 0 .../src/frontend/app/login/page.tsx | 0 .../src/frontend/app/npm/page.tsx | 0 .../src/frontend/app/page.tsx | 0 .../app/users/[userId]/activity/page.tsx | 0 .../src/frontend/app/users/page.tsx | 0 .../components/auth/PermissionGuard.tsx | 0 .../components/auth/ProtectedRoute.tsx | 0 .../dashboard/CodeConnectContent.tsx | 0 .../components/dashboard/MetricCard.tsx | 0 .../frontend/components/shared/AppShell.tsx | 0 .../components/shared/ClientLayout.tsx | 0 .../frontend/components/shared/EmptyState.tsx | 0 .../src/frontend/components/shared/Loader.tsx | 0 .../components/shared/SidebarConfig.tsx | 51 +- .../frontend/components/shared/UserAvatar.tsx | 0 .../src/frontend/contexts/AuthContext.tsx | 0 .../src/frontend/hooks/usePostgreSQLData.ts | 0 .../src/frontend/lib/api-client.ts | 0 .../src/frontend/lib/firebase.ts | 0 .../src/shared/types/index.ts | 0 .../tsconfig.json | 0 packages/blend/lib/main.ts | 34 + packages/blend/package.json | 3 +- packages/cli/package.json | 38 + packages/cli/src/commands/brand.ts | 202 ++ packages/cli/src/commands/diff.ts | 69 + packages/cli/src/commands/generate.ts | 91 + packages/cli/src/commands/init.ts | 121 ++ packages/cli/src/commands/validate.ts | 76 + .../cli/src/generators/config-generator.ts | 25 + .../cli/src/generators/provider-generator.ts | 38 + .../cli/src/generators/tokens-generator.ts | 74 + packages/cli/src/index.ts | 104 + packages/cli/src/utils/detect-project.ts | 101 + packages/cli/src/utils/logger.ts | 52 + packages/cli/tsconfig.json | 15 + packages/token-engine/package.json | 29 + .../src/build-brand-foundation.ts | 120 ++ packages/token-engine/src/color-scale.ts | 186 ++ packages/token-engine/src/diff.ts | 124 ++ packages/token-engine/src/index.ts | 102 + packages/token-engine/src/presets.ts | 120 ++ .../token-engine/src/resolve-all-tokens.ts | 102 + packages/token-engine/src/types.ts | 130 ++ packages/token-engine/src/validate.ts | 201 ++ packages/token-engine/tsconfig.json | 18 + 117 files changed, 3953 insertions(+), 12 deletions(-) create mode 100644 TOKEN_STUDIO_IMPLEMENTATION.md rename apps/{blend-monitor => blend-studio}/.dockerignore (100%) rename apps/{blend-monitor => blend-studio}/.env.example (100%) rename apps/{blend-monitor => blend-studio}/.eslintrc.json (100%) rename apps/{blend-monitor => blend-studio}/.gitignore (100%) rename apps/{blend-monitor => blend-studio}/DEPLOYMENT.md (100%) rename apps/{blend-monitor => blend-studio}/Dockerfile (100%) rename apps/{blend-monitor => blend-studio}/LOCAL_SETUP.md (100%) rename apps/{blend-monitor => blend-studio}/README.md (100%) rename apps/{blend-monitor => blend-studio}/app/api/activity/recent/route.ts (100%) rename apps/{blend-monitor => blend-studio}/app/api/components/components-pg/route.ts (100%) rename apps/{blend-monitor => blend-studio}/app/api/components/route.ts (100%) rename apps/{blend-monitor => blend-studio}/app/api/health/route.ts (100%) rename apps/{blend-monitor => blend-studio}/app/api/npm/route.ts (100%) rename apps/{blend-monitor => blend-studio}/app/api/npm/stats/route.ts (100%) rename apps/{blend-monitor => blend-studio}/app/api/npm/sync/route.ts (100%) rename apps/{blend-monitor => blend-studio}/app/api/npm/trends/route.ts (100%) rename apps/{blend-monitor => blend-studio}/app/api/npm/versions/route.ts (100%) rename apps/{blend-monitor => blend-studio}/app/api/users/[userId]/role/route.ts (100%) rename apps/{blend-monitor => blend-studio}/app/api/users/[userId]/route.ts (100%) rename apps/{blend-monitor => blend-studio}/app/api/users/activity/route.ts (100%) rename apps/{blend-monitor => blend-studio}/app/api/users/route.ts (100%) rename apps/{blend-monitor => blend-studio}/app/code-connect/health/page.tsx (100%) rename apps/{blend-monitor => blend-studio}/app/code-connect/page.tsx (100%) rename apps/{blend-monitor => blend-studio}/app/favicon.ico (100%) rename apps/{blend-monitor => blend-studio}/app/globals.css (100%) rename apps/{blend-monitor => blend-studio}/app/layout.tsx (100%) rename apps/{blend-monitor => blend-studio}/app/login/page.tsx (100%) rename apps/{blend-monitor => blend-studio}/app/not-found.tsx (100%) rename apps/{blend-monitor => blend-studio}/app/npm/page.tsx (100%) rename apps/{blend-monitor => blend-studio}/app/page.tsx (100%) rename apps/{blend-monitor => blend-studio}/app/users/[userId]/activity/page.tsx (100%) rename apps/{blend-monitor => blend-studio}/app/users/page.tsx (100%) rename apps/{blend-monitor => blend-studio}/cloudbuild.yaml (100%) rename apps/{blend-monitor => blend-studio}/database/schema.sql (100%) rename apps/{blend-monitor => blend-studio}/deploy.sh (100%) rename apps/{blend-monitor => blend-studio}/eslint.config.mjs (100%) rename apps/{blend-monitor => blend-studio}/extract-firebase-key.js (100%) rename apps/{blend-monitor => blend-studio}/middleware.ts (100%) rename apps/{blend-monitor => blend-studio}/next.config.ts (100%) rename apps/{blend-monitor => blend-studio}/package.json (93%) rename apps/{blend-monitor => blend-studio}/polyfills.ts (100%) rename apps/{blend-monitor => blend-studio}/postcss.config.mjs (100%) rename apps/{blend-monitor => blend-studio}/public/file.svg (100%) rename apps/{blend-monitor => blend-studio}/public/globe.svg (100%) rename apps/{blend-monitor => blend-studio}/public/next.svg (100%) rename apps/{blend-monitor => blend-studio}/public/vercel.svg (100%) rename apps/{blend-monitor => blend-studio}/public/window.svg (100%) rename apps/{blend-monitor => blend-studio}/src/README.md (100%) rename apps/{blend-monitor => blend-studio}/src/backend/api/activity/recent/route.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/api/components/components-pg/route.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/api/components/route.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/api/health/route.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/api/npm/route.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/api/npm/stats/route.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/api/npm/sync/route.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/api/npm/trends/route.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/api/npm/versions/route.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/api/users/[userId]/role/route.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/api/users/[userId]/route.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/api/users/activity/route.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/api/users/route.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/external/npm-client.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/lib/auth-middleware.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/lib/database-service.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/lib/database.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/lib/firebase-admin.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/lib/role-service.ts (100%) rename apps/{blend-monitor => blend-studio}/src/backend/scanners/component-scanner.ts (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/app/code-connect/health/page.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/app/code-connect/page.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/app/login/page.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/app/npm/page.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/app/page.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/app/users/[userId]/activity/page.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/app/users/page.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/components/auth/PermissionGuard.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/components/auth/ProtectedRoute.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/components/dashboard/CodeConnectContent.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/components/dashboard/MetricCard.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/components/shared/AppShell.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/components/shared/ClientLayout.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/components/shared/EmptyState.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/components/shared/Loader.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/components/shared/SidebarConfig.tsx (55%) rename apps/{blend-monitor => blend-studio}/src/frontend/components/shared/UserAvatar.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/contexts/AuthContext.tsx (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/hooks/usePostgreSQLData.ts (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/lib/api-client.ts (100%) rename apps/{blend-monitor => blend-studio}/src/frontend/lib/firebase.ts (100%) rename apps/{blend-monitor => blend-studio}/src/shared/types/index.ts (100%) rename apps/{blend-monitor => blend-studio}/tsconfig.json (100%) create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/commands/brand.ts create mode 100644 packages/cli/src/commands/diff.ts create mode 100644 packages/cli/src/commands/generate.ts create mode 100644 packages/cli/src/commands/init.ts create mode 100644 packages/cli/src/commands/validate.ts create mode 100644 packages/cli/src/generators/config-generator.ts create mode 100644 packages/cli/src/generators/provider-generator.ts create mode 100644 packages/cli/src/generators/tokens-generator.ts create mode 100644 packages/cli/src/index.ts create mode 100644 packages/cli/src/utils/detect-project.ts create mode 100644 packages/cli/src/utils/logger.ts create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/token-engine/package.json create mode 100644 packages/token-engine/src/build-brand-foundation.ts create mode 100644 packages/token-engine/src/color-scale.ts create mode 100644 packages/token-engine/src/diff.ts create mode 100644 packages/token-engine/src/index.ts create mode 100644 packages/token-engine/src/presets.ts create mode 100644 packages/token-engine/src/resolve-all-tokens.ts create mode 100644 packages/token-engine/src/types.ts create mode 100644 packages/token-engine/src/validate.ts create mode 100644 packages/token-engine/tsconfig.json diff --git a/TOKEN_STUDIO_IMPLEMENTATION.md b/TOKEN_STUDIO_IMPLEMENTATION.md new file mode 100644 index 000000000..80d98b20c --- /dev/null +++ b/TOKEN_STUDIO_IMPLEMENTATION.md @@ -0,0 +1,1736 @@ +# Blend Token Studio -- Full Implementation Plan + +> **Target**: Production-grade MVP +> **App**: Rename `apps/blend-monitor` to `apps/blend-studio` (keeps monitor as admin-only section) +> **Backend**: Firestore + Firebase Auth (already set up in blend-monitor) +> **Hosting**: GCP (Firebase Hosting / Cloud Run -- already configured) +> **No semantic token layer** -- work directly with existing V2 light/dark token files + +--- + +## The DX Problem Today (Why Token Studio Must Exist) + +### Current developer journey: 7 painful steps + +``` +Step 1: npm install @juspay/blend-design-system styled-components +Step 2: import '@juspay/blend-design-system/style.css' +Step 3: Wrap app in +Step 4: Want custom brand colors? Clone FOUNDATION_THEME manually: + const custom = { ...FOUNDATION_THEME, colors: { ...FOUNDATION_THEME.colors, primary: { ...spread 11 shades... } } } +Step 5: Pass foundationTokens={custom} -- BUT this only changes foundation, + components still use defaults unless you ALSO pass componentTokens +Step 6: To pass componentTokens, you need to call getButtonV2Tokens(custom, 'light') + for EVERY SINGLE V2 component (26 calls) and build the full object +Step 7: None of this is documented. getXXXTokens functions aren't even exported. +``` + +**Result**: Nobody customizes. They either use Blend defaults or give up and write CSS overrides. + +### What shadcn/ui gets right + +shadcn's DX is one command: + +```bash +npx shadcn@latest init # scaffolds config, CSS vars, utils +npx shadcn@latest add button # copies component into your project +``` + +Then you edit `globals.css` to change `--primary` and every component updates. Zero ThemeProvider boilerplate, zero token wiring. + +### What Blend Token Studio should deliver + +**Path A -- CLI (shadcn-style, for developers)**: + +```bash +npx blend-token-studio init # 1 command to scaffold +npx blend-token-studio brand --preset hdfc # 1 command to brand +# Done. App renders with HDFC colors. +``` + +**Path B -- Dashboard (for designers / non-developers)**: + +``` +Open studio.blend.juspay.design + -> Pick colors with a color picker + -> See live preview of all components + -> Click "Publish" + -> Developer runs: npx blend-token-studio pull hdfc/retail + -> Done. +``` + +--- + +## CLI Design: The shadcn-Inspired Developer Experience + +### `npx blend-token-studio init` + +**What it does** (single command, zero config): + +1. Detects project type (Next.js / Vite / CRA) by reading package.json +2. Checks if `@juspay/blend-design-system` is installed, installs if not +3. Checks if `styled-components` is installed, installs if not +4. Creates `blend.config.json` at project root +5. Creates `src/blend/provider.tsx` -- the pre-wired ThemeProvider wrapper +6. Creates `src/blend/tokens.ts` -- default token export (Blend defaults) +7. Prints "Done. Wrap your app with ``" + +**Generated `blend.config.json`**: + +```json +{ + "$schema": "https://studio.blend.juspay.design/schema.json", + "brand": "blend/default", + "theme": "light", + "output": "src/blend", + "components": "all" +} +``` + +**Generated `src/blend/provider.tsx`**: + +```tsx +// Auto-generated by Blend Token Studio -- safe to edit +import { ThemeProvider, Theme } from '@juspay/blend-design-system' +import '@juspay/blend-design-system/style.css' +import { componentTokens } from './tokens' + +export function BlendProvider({ + children, + theme = 'light', +}: { + children: React.ReactNode + theme?: 'light' | 'dark' +}) { + return ( + + {children} + + ) +} +``` + +**Generated `src/blend/tokens.ts`**: + +```tsx +// Auto-generated by Blend Token Studio -- do not edit manually +// Brand: Blend Default +// Run `npx blend-token-studio brand` to customize + +import type { ComponentTokenType } from '@juspay/blend-design-system' + +export const componentTokens: ComponentTokenType = {} +// Empty = use all Blend defaults +``` + +**Developer's app.tsx after init**: + +```tsx +import { BlendProvider } from './blend/provider' +import { ButtonV2 } from '@juspay/blend-design-system' + +export function App() { + return ( + + + + ) +} +``` + +**That's 3 lines of setup.** Compare to the 7-step manual process above. + +--- + +### `npx blend-token-studio brand` + +**Interactive mode** (no args): + +``` +$ npx blend-token-studio brand + +? Choose a brand preset or customize: + > Blend Default (blue) + HDFC Bank (red, sharp corners) + NeoBank (purple, rounded) + FinTech (green) + Custom (enter hex color) + +? Primary brand color: #E31837 +? Border radius style: + > Sharp (4px) + Default (10px) + Rounded (20px) + Pill (9999px) + +Generating tokens for 26 V2 components... + BUTTONV2 done + TEXT_INPUTV2 done + MULTI_SELECT_V2 done + ... (26 total) + +Written: + src/blend/tokens.ts (component tokens) + src/blend/brand.json (brand config -- commit this) + +Done! Your app now renders with HDFC Bank branding. +``` + +**One-liner mode**: + +```bash +npx blend-token-studio brand --preset hdfc +npx blend-token-studio brand --primary "#E31837" --radius sharp +``` + +**What changes**: Only `src/blend/tokens.ts` and `src/blend/brand.json`. The provider stays the same. The component usage code stays the same. Just the token values change. + +--- + +### `npx blend-token-studio pull ` + +**For teams using the dashboard**: + +```bash +$ npx blend-token-studio pull hdfc/retail + +Fetching brand config from Blend Token Studio... +Branch: hdfc/retail (v2.1.0, published 2h ago) +Resolving tokens for 26 components... + +Written: + src/blend/tokens.ts + src/blend/brand.json + +Tip: Commit brand.json to version control. +``` + +--- + +### `npx blend-token-studio diff` + +```bash +$ npx blend-token-studio diff + +Comparing local brand.json vs Blend defaults: + + colors.primary.500 #2B7FFF -> #E31837 + colors.primary.600 #0561E2 -> #C01530 + border.radius.10 10px -> 4px + border.radius.12 12px -> 6px + +4 overrides from Blend defaults. +``` + +--- + +### Full CLI Command Reference + +```bash +# Setup +blend-token-studio init # Scaffold project (provider + tokens) +blend-token-studio brand # Interactive brand customization +blend-token-studio brand --preset hdfc # Use a preset +blend-token-studio brand --primary "#E31837" --radius sharp + +# Studio integration +blend-token-studio pull # Pull from dashboard +blend-token-studio push # Push local brand.json to dashboard +blend-token-studio list # List available branches + +# Utilities +blend-token-studio diff # Diff local vs defaults +blend-token-studio preview # Open local preview server +blend-token-studio validate # Validate brand.json + +# Offline +blend-token-studio generate ./brand.json # Generate tokens from any JSON file +``` + +--- + +## White-Label Dashboard: The Designer Experience + +The dashboard at `studio.blend.juspay.design` is a **white-label-ready** Next.js app. It serves two audiences: + +### For Blend team (admin): Monitor + Studio + +Admins see everything -- the existing blend-monitor pages (Dashboard, Code Connect, NPM Stats, Users) plus the Token Studio. + +### For clients (editor/viewer): Studio only + +External users (bank partners, fintech teams) only see the Token Studio section. They log in with Google, see their branches, edit tokens, preview, publish. They never see the monitor. + +**Access control** (already built in blend-monitor): + +``` +Admin: Monitor + Studio (full access) +Editor: Studio only (create/edit/publish branches) +Viewer: Studio only (view branches, preview, no edit) +``` + +### Dashboard page structure + +``` +/login # Google auth (existing) +/studio # Branch list (all users) +/studio/editor/[branchId] # Token editor + live preview +/studio/preview/[branchId] # Shareable full-page preview +/studio/diff/[branchId] # Visual diff view +/ # Monitor dashboard (admin only) +/code-connect # Code Connect (admin only) +/npm # NPM stats (admin only) +/users # User management (admin only) +``` + +### Key dashboard components + +``` +src/frontend/components/studio/ +├── BranchList.tsx # DataTable of branches + create/fork +├── BranchCard.tsx # Branch status card +├── TokenEditor.tsx # Main editor layout (left panel) +│ ├── ColorSection.tsx # Color group with pickers +│ │ ├── ColorShadeRow.tsx # Single shade: label + hex input + picker +│ │ └── ColorPaletteGenerator.tsx # Enter 1 hex -> generate full 50-950 scale +│ ├── RadiusSection.tsx # Radius sliders with visual preview +│ ├── ShadowSection.tsx # Shadow editor with visual preview +│ └── FontSection.tsx # Font family + weight overrides +├── ComponentShowcase.tsx # Right panel: all V2 components rendered +│ ├── ButtonShowcase.tsx # All button variants/sizes/states +│ ├── InputShowcase.tsx # TextInput, Select, MultiSelect +│ ├── FeedbackShowcase.tsx # Alert, Snackbar, Tooltip +│ ├── NavigationShowcase.tsx # Tabs, Breadcrumb, Sidebar +│ └── DataShowcase.tsx # StatCard, Charts, Table +├── DiffViewer.tsx # Side-by-side diff (color swatches + values) +├── ExportPanel.tsx # JSON export, CLI command, copy-paste +├── BrandPresetSelector.tsx # Preset cards (Blend, HDFC, NeoBank, etc.) +└── PublishDialog.tsx # Version + notes + publish confirmation +``` + +### ColorPaletteGenerator (key UX feature) + +The biggest DX win in the dashboard. Designer enters ONE hex color, the system generates the full 50-950 scale: + +``` +Input: #E31837 (brand primary) + +Output: + 50: #FEF2F2 (lightest tint) + 100: #FFE2E2 + 200: #FFC9C9 + 300: #FFA2A2 + 400: #FF6467 + 500: #E31837 (the input color, placed at 500) + 600: #C01530 (auto-darkened) + 700: #A01228 + 800: #801020 + 900: #600D18 + 950: #400810 (darkest shade) +``` + +Uses oklch color space to generate perceptually uniform scales. This is how shadcn does it -- one `--primary` value derives the entire palette. + +--- + +## 0. What Already Exists (No Work Needed) + +These are **done** and will not be touched: + +| What | Where | Status | +| --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ------ | +| Foundation tokens (colors, units, border, shadow, font, opacity, zIndex) | `packages/blend/lib/tokens/` | Done | +| 25+ V2 component token files (light + dark, each with `getXXXTokens(foundationToken, theme)`) | `packages/blend/lib/components/*V2/*.tokens.ts` | Done | +| `ComponentTokenType` -- union type of all component token keys | `packages/blend/lib/context/ThemeContext.tsx:202-271` | Done | +| `ThemeProvider` accepts `componentTokens?: ComponentTokenType` prop | `packages/blend/lib/context/ThemeProvider.tsx:11` | Done | +| `initComponentTokens` -- merges user overrides with defaults via `??` | `packages/blend/lib/context/initComponentTokens.ts` | Done | +| `useResponsiveTokens(componentKey)` -- breakpoint-aware hook | `packages/blend/lib/hooks/useResponsiveTokens.ts` | Done | +| `ComponentTokenType` exported publicly | `packages/blend/lib/context/index.ts:4` | Done | +| Firebase Auth (Google login, roles, permissions) | `apps/blend-monitor/src/frontend/contexts/AuthContext.tsx` | Done | +| Firebase Admin SDK | `apps/blend-monitor/src/backend/lib/firebase-admin.ts` | Done | +| Role-based access control (admin/editor/viewer) | `apps/blend-monitor/src/backend/lib/role-service.ts` | Done | +| Auth middleware + permission guards | `apps/blend-monitor/src/backend/lib/auth-middleware.ts` | Done | +| PermissionGuard component + `usePermissions` hook | `apps/blend-monitor/src/frontend/components/auth/PermissionGuard.tsx` | Done | +| ProtectedRoute wrapper | `apps/blend-monitor/src/frontend/components/auth/ProtectedRoute.tsx` | Done | +| AppShell with Sidebar navigation | `apps/blend-monitor/src/frontend/components/shared/AppShell.tsx` | Done | +| ClientLayout with login/protected routing | `apps/blend-monitor/src/frontend/components/shared/ClientLayout.tsx` | Done | +| PostgreSQL schema (users, roles, components, activity, audit) | `apps/blend-monitor/database/schema.sql` | Done | +| Next.js 15 + Tailwind 4 + standalone output | `apps/blend-monitor/next.config.ts` | Done | +| Turbo monorepo build pipeline | Root `turbo.json` + `package.json` | Done | + +--- + +## 1. The Core Insight (Why No Semantic Layer Needed for MVP) + +Every V2 component already has this pattern: + +```typescript +// buttonV2.tokens.ts +export const getButtonV2Tokens = ( + foundationToken: FoundationTokenType, + theme: Theme | string = Theme.LIGHT +): ResponsiveButtonV2Tokens => { + if (theme === Theme.DARK) return getButtonV2DarkTokens(foundationToken) + return getButtonV2LightTokens(foundationToken) +} +``` + +And light tokens directly reference foundation values: + +```typescript +// buttonV2.light.tokens.ts +backgroundColor: { + primary: { + default: { + default: `linear-gradient(180deg, ${foundationToken.colors.primary[600]} -5%, ${foundationToken.colors.primary[500]} 107.5%)`, + hover: foundationToken.colors.primary[600], + disabled: foundationToken.colors.primary[300], + } + } +} +borderRadius: { + sm: { + primary: { + default: foundationToken.border.radius[10], + } + } +} +``` + +**The token engine can intercept at the foundation level.** Instead of creating a new semantic layer, we: + +1. Take a `BrandConfig` JSON (small: ~20 values) +2. Build a **modified `FoundationTokenType`** by cloning `FOUNDATION_THEME` and overriding specific color/radius/shadow entries +3. Pass that modified foundation into the existing `getXXXTokens(modifiedFoundation, theme)` functions +4. Get back fully resolved component tokens -- zero changes to any existing component code + +This is the **fastest path to production** because: + +- Zero changes to `packages/blend` (the library) +- Every existing component works automatically +- All 25+ V2 components get brand support for free +- Light + dark themes both work because the foundation is the input to both + +--- + +## 2. Architecture Overview + +``` + +-----------------------+ + | Brand Config JSON | <-- small, ~20 values + | { colors, radius } | + +----------+------------+ + | + +----------v------------+ + | Token Engine | + | buildBrandFoundation()| <-- clones FOUNDATION_THEME + overrides + | resolveAllTokens() | <-- calls every getXXXTokens() + +----------+------------+ + | + +----------------+----------------+ + | | | + +---------v------+ +------v-------+ +-----v--------+ + | Dashboard | | CLI | | Firestore API| + | (Next.js app) | | npx blend-ts | | (branches, | + | Live preview | | pull/push | | versions) | + +----------------+ +--------------+ +--------------+ + | + +---------v-------------------+ + | ThemeProvider | + | componentTokens={...} | + | All V2 components render | + | with brand colors/radius | + +-----------------------------+ +``` + +--- + +## 3. V2 Component Token Inventory + +Every V2 component that the token engine must resolve: + +| # | Component | Token Key in ComponentTokenType | getTokens Function | Light File | Dark File | Foundation Colors Used | Has Composite CSS Strings | +| --- | -------------- | ------------------------------- | ----------------------------- | -------------------------------- | ------------------------------- | ------------------------------------------------- | ------------------------------ | +| 1 | ButtonV2 | `BUTTONV2` | `getButtonV2Tokens` | `buttonV2.light.tokens.ts` | `buttonV2.dark.tokens.ts` | primary, gray, red, green | Yes (gradient, border, shadow) | +| 2 | AccordionV2 | `ACCORDIONV2` | `getAccordionV2Tokens` | `accordionV2.light.tokens.ts` | `accordionV2.dark.tokens.ts` | gray | Yes (border) | +| 3 | AlertV2 | `ALERTV2` | `getAlertV2Tokens` | `alertV2.light.tokens.ts` | `alertV2.dark.tokens.ts` | primary, gray, yellow, green, purple, red, orange | Yes (border) | +| 4 | AvatarV2 | `AVATARV2` | `getAvatarV2Tokens` | `avatarV2.light.tokens.ts` | `avatarV2.dark.tokens.ts` | gray, green, yellow, red | Yes (border, shadow) | +| 5 | BreadcrumbV2 | `BREADCRUMBV2` | `getBreadcrumbV2Tokens` | `breadcrumbV2.light.tokens.ts` | `breadcrumbV2.dark.tokens.ts` | gray | No | +| 6 | ChartsV2 | `CHARTSV2` | `getChartV2Tokens` | `chartV2.light.tokens.ts` | `chartV2.dark.tokens.ts` | gray | Yes (border) | +| 7 | CheckboxV2 | `CHECKBOXV2` | `getCheckboxV2Tokens` | `checkboxV2.light.tokens.ts` | `checkboxV2.dark.tokens.ts` | primary, gray, red | Yes (border) | +| 8 | CodeEditorV2 | `CODEEDITORV2` | `getCodeEditorV2Tokens` | `codeEditorV2.light.token.ts` | `codeEditorV2.dark.tokens.ts` | gray, red, green, purple, orange, primary | Yes (border, shadow) | +| 9 | KeyValuePairV2 | `KEYVALUEPAIRV2` | `getKeyValuePairV2Tokens` | `keyValuePairV2.light.tokens.ts` | `keyValuePairV2.dark.tokens.ts` | gray | No | +| 10 | MenuV2 | `MENU_V2` | `getMenuV2Tokens` | `menuV2.light.tokens.ts` | `menuV2.dark.tokens.ts` | gray, primary, red | Yes (border) | +| 11 | MultiSelectV2 | `MULTI_SELECT_V2` | `getMultiSelectV2Tokens` | `multiSelectV2.light.tokens.ts` | `multiSelectV2.dark.tokens.ts` | gray, primary, red | Yes (border, outline) | +| 12 | PopoverV2 | `POPOVERV2` | `getPopoverV2Tokens` | `popoverV2.light.token.ts` | `popoverV2.dark.token.ts` | gray, primary | Yes (border, shadow) | +| 13 | ProgressBarV2 | `PROGRESS_BARV2` | `getProgressBarV2Tokens` | `progressBarV2.light.tokens.ts` | `progressBarV2.dark.tokens.ts` | primary, gray | Yes (gradient) | +| 14 | RadioV2 | `RADIOV2` | `getRadioV2Tokens` | `radioV2.light.tokens.ts` | `radioV2.dark.tokens.ts` | primary, gray, red | Yes (border) | +| 15 | SingleSelectV2 | `SINGLE_SELECT_V2` | `getSingleSelectV2Tokens` | `singleSelectV2.light.tokens.ts` | `singleSelectV2.dark.tokens.ts` | gray, primary, red | Yes (border) | +| 16 | SwitchV2 | `SWITCHV2` | `getSwitchV2Tokens` | `switchV2.light.tokens.ts` | `switchV2.dark.tokens.ts` | primary, gray | Yes (border, shadow) | +| 17 | SnackbarV2 | `SNACKBARV2` | `getSnackbarV2Tokens` | `snackbarV2.light.tokens.ts` | `snackbarV2.dark.tokens.ts` | gray, primary, green, yellow, red | No | +| 18 | StatCardV2 | `STATCARDV2` | `getStatCardV2Tokens` | `statcardV2.light.tokens.ts` | `statcardV2.dark.tokens.ts` | gray, green, red | Yes (border) | +| 19 | TabsV2 | `TABSV2` | `getTabsV2Tokens` | `tabsV2.light.tokens.ts` | `tabsV2.dark.tokens.ts` | gray, primary | Yes (border, shadow) | +| 20 | TagV2 | `TAGV2` | `getTagV2Tokens` | `tagV2.light.tokens.ts` | `tagV2.dark.tokens.ts` | gray, primary, green, red, orange, purple | Yes (border) | +| 21 | TextInputV2 | `TEXT_INPUTV2` | `getTextInputV2Tokens` | `TextInputV2.light.tokens.ts` | `TextInputV2.dark.tokens.ts` | gray, primary, red | Yes (border) | +| 22 | Timeline | `TIMELINE` | `getTimelineTokens` | `timeline.light.token.ts` | `timeline.dark.token.ts` | gray, primary, green, red | Yes (border) | +| 23 | TooltipV2 | `TOOLTIPV2` | `getTooltipV2Tokens` | `tooltipV2.light.tokens.ts` | `tooltipV2.dark.tokens.ts` | gray | No | +| 24 | TopbarV2 | `TOPBARV2` | `getTopbarV2Tokens` | `topbarV2.light.tokens.ts` | `topbarV2.dark.tokens.ts` | gray, primary | Yes (border, shadow) | +| 25 | SidebarV2 | `SIDEBARV2` | `getSidebarV2Tokens` | `sidebarV2.light.tokens.ts` | `sidebarV2.dark.tokens.ts` | gray, primary | Yes (border) | +| 26 | MobileNavV2 | `MOBILE_NAVIGATION_V2` | `getMobileNavigationV2Tokens` | `mobile.light.tokens.ts` | `mobile.dark.tokens.ts` | gray, primary | Yes (border) | + +--- + +## 4. BrandConfig JSON Schema + +This is what the dashboard edits and Firestore stores: + +```typescript +// packages/token-engine/src/types.ts + +export interface BrandConfig { + brandId: string // "hdfc/retail" + name: string // "HDFC Bank Retail" + version: string // "1.0.0" + + colors: { + // Override FOUNDATION_THEME.colors entries + primary?: Partial> // { 500: "#E31837", 600: "#C01530", ... } + gray?: Partial> + red?: Partial> + green?: Partial> + yellow?: Partial> + orange?: Partial> + purple?: Partial> + } + + radius?: { + // Override FOUNDATION_THEME.border.radius entries + [key: string]: string // { 10: "4px", 12: "4px" } + } + + shadows?: { + // Override FOUNDATION_THEME.shadows entries + [key: string]: string // { xs: "0 1px 2px rgba(227,24,55,0.1)" } + } + + font?: { + // Override FOUNDATION_THEME.font entries + family?: string + weight?: Partial> + } +} +``` + +**Example -- HDFC Bank brand:** + +```json +{ + "brandId": "hdfc/retail", + "name": "HDFC Bank Retail", + "version": "1.0.0", + "colors": { + "primary": { + "50": "#FEF2F2", + "100": "#FFE2E2", + "200": "#FFC9C9", + "300": "#FFA2A2", + "400": "#FF6467", + "500": "#E31837", + "600": "#C01530", + "700": "#A01228", + "800": "#801020", + "900": "#600D18", + "950": "#400810" + } + }, + "radius": { + "6": "4px", + "8": "4px", + "10": "4px", + "12": "6px" + } +} +``` + +This is **~20 lines** vs the **1500+ lines** of a single component's token file. The token engine expands it into the full component token set automatically. + +--- + +## 5. Implementation Steps + +### Step 1: Token Engine Package + +**Location**: `packages/token-engine/` + +**What it does**: Takes a BrandConfig JSON + theme, produces a complete `ComponentTokenType` object. + +**Files to create**: + +``` +packages/token-engine/ +├── package.json +├── tsconfig.json +├── src/ +│ ├── index.ts # Public API +│ ├── types.ts # BrandConfig type, ValidationResult +│ ├── buildBrandFoundation.ts # Clone FOUNDATION_THEME + apply overrides +│ ├── resolveAllTokens.ts # Call all 26 getXXXTokens() functions +│ ├── validateBrandConfig.ts # Validate JSON structure + color formats +│ └── diffBrandConfigs.ts # Diff two brand configs +``` + +**Core logic** (`buildBrandFoundation.ts`): + +```typescript +import { + FOUNDATION_THEME, + type FoundationTokenType, +} from '@juspay/blend-design-system' +import type { BrandConfig } from './types' + +export function buildBrandFoundation(brand: BrandConfig): FoundationTokenType { + // Deep clone the foundation + const foundation = structuredClone(FOUNDATION_THEME) + + // Override colors + if (brand.colors) { + for (const [colorGroup, overrides] of Object.entries(brand.colors)) { + if (overrides && foundation.colors[colorGroup]) { + for (const [shade, value] of Object.entries(overrides)) { + foundation.colors[colorGroup][shade] = value + } + } + } + } + + // Override radii + if (brand.radius) { + for (const [key, value] of Object.entries(brand.radius)) { + if (key in foundation.border.radius) { + ;(foundation.border.radius as Record)[key] = + value + } + } + } + + // Override shadows + if (brand.shadows) { + for (const [key, value] of Object.entries(brand.shadows)) { + if (key in foundation.shadows) { + ;(foundation.shadows as Record)[key] = value + } + } + } + + return foundation +} +``` + +**Core logic** (`resolveAllTokens.ts`): + +```typescript +import type { ComponentTokenType } from '@juspay/blend-design-system' +import type { FoundationTokenType } from '@juspay/blend-design-system' +import { Theme } from '@juspay/blend-design-system' + +// Import all V2 getXXXTokens functions +import { getButtonV2Tokens } from '@juspay/blend-design-system/lib/components/ButtonV2/buttonV2.tokens' +import { getAccordionV2Tokens } from '@juspay/blend-design-system/lib/components/AccordionV2/accordionV2.tokens' +// ... all 26 imports + +export function resolveAllTokens( + foundation: FoundationTokenType, + theme: Theme | string = Theme.LIGHT +): ComponentTokenType { + return { + BUTTONV2: getButtonV2Tokens(foundation, theme), + ACCORDIONV2: getAccordionV2Tokens(foundation, theme), + ALERTV2: getAlertV2Tokens(foundation, theme), + AVATARV2: getAvatarV2Tokens(foundation, theme), + BREADCRUMBV2: getBreadcrumbV2Tokens(foundation, theme), + CHARTSV2: getChartV2Tokens(foundation, theme), + CHECKBOXV2: getCheckboxV2Tokens(foundation, theme), + CODEEDITORV2: getCodeEditorV2Tokens(foundation, theme), + KEYVALUEPAIRV2: getKeyValuePairV2Tokens(foundation, theme), + MENU_V2: getMenuV2Tokens(foundation, theme), + MULTI_SELECT_V2: getMultiSelectV2Tokens(foundation, theme), + POPOVERV2: getPopoverV2Tokens(foundation, theme), + PROGRESS_BARV2: getProgressBarV2Tokens(foundation, theme), + RADIOV2: getRadioV2Tokens(foundation, theme), + SINGLE_SELECT_V2: getSingleSelectV2Tokens(foundation, theme), + SWITCHV2: getSwitchV2Tokens(foundation, theme), + SNACKBARV2: getSnackbarV2Tokens(foundation, theme), + STATCARDV2: getStatCardV2Tokens(foundation, theme), + TABSV2: getTabsV2Tokens(foundation, theme), + TAGV2: getTagV2Tokens(foundation, theme), + TEXT_INPUTV2: getTextInputV2Tokens(foundation, theme), + TIMELINE: getTimelineTokens(foundation, theme), + TOOLTIPV2: getTooltipV2Tokens(foundation, theme), + TOPBARV2: getTopbarV2Tokens(foundation, theme), + SIDEBARV2: getSidebarV2Tokens(foundation, theme), + MOBILE_NAVIGATION_V2: getMobileNavigationV2Tokens(foundation, theme), + } +} +``` + +**Public API** (`index.ts`): + +```typescript +import type { BrandConfig } from './types' +import type { ComponentTokenType } from '@juspay/blend-design-system' +import { buildBrandFoundation } from './buildBrandFoundation' +import { resolveAllTokens } from './resolveAllTokens' +import { validateBrandConfig } from './validateBrandConfig' +import { diffBrandConfigs } from './diffBrandConfigs' +import { Theme } from '@juspay/blend-design-system' + +export function resolveBrandTokens( + brandConfig: BrandConfig, + theme: Theme | string = Theme.LIGHT +): ComponentTokenType { + const foundation = buildBrandFoundation(brandConfig) + return resolveAllTokens(foundation, theme) +} + +export { validateBrandConfig, diffBrandConfigs, buildBrandFoundation } +export type { BrandConfig } +``` + +**Dependency**: `packages/token-engine/package.json`: + +```json +{ + "name": "@blend-design/token-engine", + "version": "0.1.0", + "private": true, + "main": "src/index.ts", + "dependencies": { + "@juspay/blend-design-system": "workspace:*" + } +} +``` + +**Why this works**: The existing `getButtonV2LightTokens(foundationToken)` function already takes `foundationToken` as an argument. If we pass a modified foundation where `colors.primary[600]` is `"#E31837"` instead of `"#0561E2"`, every place that references `foundationToken.colors.primary[600]` automatically gets the HDFC red. The component code, token type definitions, and ThemeProvider contract are all unchanged. + +--- + +### Step 2: Export getXXXTokens Functions from Blend Package + +**Problem**: Currently the V2 `getXXXTokens` functions are not exported from `packages/blend/lib/main.ts`. The token engine needs to import them. + +**What to do**: Add exports to `packages/blend/lib/main.ts`: + +```typescript +// Token factory functions (for Token Studio engine) +export { getButtonV2Tokens } from './components/ButtonV2/buttonV2.tokens' +export { getAccordionV2Tokens } from './components/AccordionV2/accordionV2.tokens' +export { getAlertV2Tokens } from './components/AlertV2/alertV2.tokens' +export { getAvatarV2Tokens } from './components/AvatarV2/avatarV2.tokens' +export { getBreadcrumbV2Tokens } from './components/BreadcrumbV2/breadcrumbV2.tokens' +export { getChartV2Tokens } from './components/ChartsV2/chartV2.tokens' +export { getCheckboxV2Tokens } from './components/SelectorV2/CheckboxV2/checkboxV2.tokens' +export { getCodeEditorV2Tokens } from './components/CodeEditorV2/codeEditorV2.tokens' +export { getKeyValuePairV2Tokens } from './components/KeyValuePairV2/keyValuePairV2.tokens' +export { getMenuV2Tokens } from './components/MenuV2/menuV2.tokens' +export { getMultiSelectV2Tokens } from './components/MultiSelectV2/multiSelectV2.tokens' +export { getPopoverV2Tokens } from './components/PopoverV2/popoverV2.token' +export { getProgressBarV2Tokens } from './components/ProgressBarV2/progressBarV2.tokens' +export { getRadioV2Tokens } from './components/SelectorV2/RadioV2/radioV2.tokens' +export { getSingleSelectV2Tokens } from './components/SingleSelectV2/singleSelectV2.tokens' +export { getSwitchV2Tokens } from './components/SelectorV2/SwitchV2/switchV2.tokens' +export { getSnackbarV2Tokens } from './components/SnackbarV2/snackbarV2.tokens' +export { getStatCardV2Tokens } from './components/StatCardV2/statcardV2.tokens' +export { getTabsV2Tokens } from './components/TabsV2/tabsV2.tokens' +export { getTagV2Tokens } from './components/TagV2/tagV2.tokens' +export { getTextInputV2Tokens } from './components/InputsV2/TextInputV2/TextInputV2.tokens' +export { getTimelineTokens } from './components/Timeline/timeline.token' +export { getTooltipV2Tokens } from './components/TooltipV2/tooltipV2.tokens' +export { getTopbarV2Tokens } from './components/TopbarV2/topbarV2.tokens' +export { getSidebarV2Tokens } from './components/SidebarV2/sidebarV2.tokens' +export { getMobileNavigationV2Tokens } from './components/SidebarV2/SidebarV2MobileNavigation/mobile.tokens' + +// Token type exports (for Token Studio engine) +export type { ResponsiveButtonV2Tokens } from './components/ButtonV2/buttonV2.tokens' +export type { FoundationTokenType } from './tokens/theme.token' +export { default as FOUNDATION_THEME } from './tokens/theme.token' +``` + +**Files changed**: 1 file (`packages/blend/lib/main.ts`) +**Risk**: None -- only adds exports, no behavior change. + +--- + +### Step 3: Rename blend-monitor to blend-studio + Add Token Studio Navigation + +**What to do**: + +1. Rename the app directory from `apps/blend-monitor` to `apps/blend-studio` +2. Update `package.json` name to `"blend-studio"` +3. Update sidebar navigation to add Token Studio section +4. Keep existing Monitor pages (Dashboard, Code Connect, NPM, Users) as admin-only +5. Add new Token Studio pages accessible to all authenticated users + +**Updated SidebarConfig.tsx** -- adds Token Studio navigation: + +```typescript +export const getNavigationData = (router, pathname) => [ + { + label: 'Token Studio', + isCollapsible: false, + items: [ + { + label: 'Branches', + leftSlot: , + onClick: () => router.push('/studio'), + isActive: pathname === '/studio', + }, + { + label: 'Editor', + leftSlot: , + onClick: () => router.push('/studio/editor'), + isActive: pathname.startsWith('/studio/editor'), + }, + { + label: 'Preview', + leftSlot: , + onClick: () => router.push('/studio/preview'), + isActive: pathname.startsWith('/studio/preview'), + }, + ], + }, + { + label: 'Monitor (Admin)', + isCollapsible: true, + items: [ + // Existing Dashboard, Code Connect, NPM, Users items + // Wrapped with PermissionGuard for admin-only + ], + }, +] +``` + +**Admin-only gating**: The existing `PermissionGuard` component and `usePermissions` hook already support this: + +```tsx + + {/* Monitor section items */} + +``` + +--- + +### Step 4: Firestore Data Model for Token Studio + +**Collections to add** (alongside existing PostgreSQL tables -- Firestore for token data, PostgreSQL for monitor data): + +``` +Firestore Collections: +====================== + +/branches/{branchId} + ├── brandId: string // "hdfc/retail" + ├── name: string // "HDFC Bank Retail" + ├── parentBranch: string | null // "main" or another branchId + ├── status: "draft" | "published" + ├── brandConfig: BrandConfig // The full JSON config + ├── createdBy: string // Firebase UID + ├── createdAt: Timestamp + ├── updatedAt: Timestamp + └── publishedVersions: number // Count + +/branches/{branchId}/versions/{versionId} + ├── version: string // "1.0.0" + ├── brandConfig: BrandConfig // Immutable snapshot + ├── publishedBy: string // Firebase UID + ├── publishedAt: Timestamp + └── notes: string + +/branches/{branchId}/snapshots/{snapshotId} + ├── brandConfig: BrandConfig // Auto-saved draft + ├── savedAt: Timestamp + └── savedBy: string +``` + +**Why Firestore (not PostgreSQL)**: + +- Brand configs are JSON documents -- Firestore is a natural fit +- Real-time subscriptions for live preview updates +- No schema migration needed when BrandConfig shape evolves +- Existing PostgreSQL stays for monitor data (components, npm, deployments) + +--- + +### Step 5: API Routes for Token Studio + +**Location**: `apps/blend-studio/app/api/studio/` + +``` +app/api/studio/ +├── branches/ +│ ├── route.ts # GET (list), POST (create) +│ └── [branchId]/ +│ ├── route.ts # GET (detail), PATCH (update config), DELETE +│ ├── publish/route.ts # POST (publish version) +│ ├── fork/route.ts # POST (fork from this branch) +│ ├── diff/route.ts # GET (diff vs parent) +│ ├── resolve/route.ts # POST (resolve tokens -- returns ComponentTokenType) +│ └── versions/ +│ ├── route.ts # GET (list versions) +│ └── [versionId]/ +│ └── route.ts # GET (specific version) +``` + +**Key endpoint -- `/api/studio/branches/[branchId]/resolve`**: + +```typescript +// POST /api/studio/branches/[branchId]/resolve +// Body: { theme: "light" | "dark" } +// Returns: ComponentTokenType (the full resolved tokens) + +import { resolveBrandTokens } from '@blend-design/token-engine' + +export async function POST(request, { params }) { + const { branchId } = params + const { theme = 'light' } = await request.json() + + // Fetch brand config from Firestore + const doc = await firestore.doc(`branches/${branchId}`).get() + const brandConfig = doc.data().brandConfig + + // Resolve tokens using the engine + const componentTokens = resolveBrandTokens(brandConfig, theme) + + return Response.json({ componentTokens }) +} +``` + +--- + +### Step 6: Dashboard Pages + +**6a. Branch List Page** (`/studio`) + +``` +app/studio/ +├── page.tsx # Branch list + create new +├── layout.tsx # Studio layout wrapper +├── editor/ +│ └── [branchId]/ +│ └── page.tsx # Token editor + live preview +└── preview/ + └── [branchId]/ + └── page.tsx # Full-page preview (shareable URL) +``` + +**Branch List UI**: + +- DataTable showing all branches (name, status, last edited, version count) +- "Create Branch" button -- opens modal with name + optional parent branch +- "Fork" action on each row +- Status badges: Draft (yellow), Published (green) + +**6b. Token Editor Page** (`/studio/editor/[branchId]`) + +This is the main Token Studio screen. Two panels side-by-side: + +``` ++----------------------------------+----------------------------------+ +| LEFT: Editor | RIGHT: Live Preview | +| | | +| Brand: HDFC Retail | +----------------------------+ | +| Status: Draft | | ButtonV2 (all variants) | | +| | | Primary | Secondary | ... | | +| [Colors] | +----------------------------+ | +| Primary: | | +| 500: [#E31837] (picker) | +----------------------------+ | +| 600: [#C01530] (picker) | | TextInputV2 | | +| ... | | Default | Focus | Error | | +| | +----------------------------+ | +| [Border Radius] | | +| 10: [4px] (slider) | +----------------------------+ | +| 12: [6px] (slider) | | MultiSelectV2 | | +| | | Closed | Open | Selected | | +| [Shadows] | +----------------------------+ | +| xs: [...] (input) | | +| | +----------------------------+ | +| [Actions] | | SingleSelectV2 | | +| [Save Draft] [Publish] [Reset] | +----------------------------+ | +| [Export JSON] [Diff vs Parent] | | ++----------------------------------+----------------------------------+ +``` + +**How the live preview works**: + +```tsx +// In the editor page +import { resolveBrandTokens } from '@blend-design/token-engine' +import { ThemeProvider, ButtonV2, TextInputV2, ... } from '@juspay/blend-design-system' + +function EditorPage({ branchId }) { + const [brandConfig, setBrandConfig] = useState(/* from Firestore */) + const [theme, setTheme] = useState<'light' | 'dark'>('light') + + // Resolve tokens on every config change + const componentTokens = useMemo( + () => resolveBrandTokens(brandConfig, theme), + [brandConfig, theme] + ) + + return ( +
+ {/* Left: Editor controls */} +
+ setBrandConfig(prev => ({ ...prev, colors }))} + /> + setBrandConfig(prev => ({ ...prev, radius }))} + /> +
+ + {/* Right: Live preview -- THIS IS THE KEY */} +
+ + + +
+
+ ) +} +``` + +The `` wrapping is the **entire integration**. Every Blend V2 component inside it automatically renders with the brand's colors and radii. No component code changes needed. + +**6c. Component Showcase** (preview panel content): + +```tsx +function ComponentShowcase() { + return ( +
+
+

Buttons

+
+ + + + +
+
+ + + +
+
+ +
+

Text Input

+ + +
+ +
+

Multi Select

+ +
+ +
+

Tabs

+ +
+ + {/* ... All other V2 components */} +
+ ) +} +``` + +--- + +### Step 7: CLI Tool (shadcn-Inspired) + +**Location**: `packages/cli/` +**Published as**: `blend-token-studio` on npm +**Invoked as**: `npx blend-token-studio ` + +``` +packages/cli/ +├── package.json # bin: { "blend-token-studio": "./dist/index.js" } +├── tsconfig.json +├── src/ +│ ├── index.ts # CLI entry (Commander.js) +│ ├── commands/ +│ │ ├── init.ts # Detect project -> scaffold provider + tokens +│ │ ├── brand.ts # Interactive brand customization or --preset +│ │ ├── pull.ts # Fetch from studio API -> write tokens +│ │ ├── push.ts # Upload local brand.json to studio API +│ │ ├── diff.ts # Diff local vs defaults or vs remote +│ │ ├── generate.ts # Offline: JSON -> tokens.ts +│ │ ├── list.ts # List available branches from API +│ │ ├── preview.ts # Open local dev preview of branded components +│ │ └── validate.ts # Validate brand.json +│ ├── generators/ +│ │ ├── providerGenerator.ts # Generate src/blend/provider.tsx +│ │ ├── tokensGenerator.ts # Generate src/blend/tokens.ts +│ │ └── configGenerator.ts # Generate blend.config.json +│ ├── utils/ +│ │ ├── detectProject.ts # Detect Next.js / Vite / CRA from package.json +│ │ ├── installDeps.ts # Auto-install missing deps (blend + styled-components) +│ │ ├── colorScale.ts # Generate 50-950 color scale from 1 hex +│ │ ├── api-client.ts # HTTP client for studio API +│ │ └── logger.ts # Pretty CLI output with spinner + colors +│ └── presets/ +│ ├── index.ts # Preset registry +│ ├── blend-default.json +│ ├── hdfc.json +│ ├── neobank.json +│ └── fintech.json +``` + +**Key command -- `init`**: + +```typescript +async function init() { + const spinner = ora('Detecting project...').start() + + // 1. Detect project type + const project = detectProject(process.cwd()) + spinner.succeed(`Detected: ${project.type} (${project.framework})`) + + // 2. Check/install dependencies + const missing = checkDeps([ + '@juspay/blend-design-system', + 'styled-components', + ]) + if (missing.length) { + spinner.start(`Installing ${missing.join(', ')}...`) + await installDeps(missing, project.packageManager) + spinner.succeed('Dependencies installed') + } + + // 3. Create blend.config.json + writeFileSync( + 'blend.config.json', + JSON.stringify( + { + $schema: 'https://studio.blend.juspay.design/schema.json', + brand: 'blend/default', + theme: 'light', + output: 'src/blend', + }, + null, + 4 + ) + ) + + // 4. Generate provider + tokens + const outputDir = 'src/blend' + mkdirSync(outputDir, { recursive: true }) + writeFileSync(`${outputDir}/provider.tsx`, generateProvider()) + writeFileSync(`${outputDir}/tokens.ts`, generateDefaultTokens()) + + console.log('\n Done! Add to your app:\n') + console.log(" import { BlendProvider } from './blend/provider'") + console.log(' {children}\n') + console.log(' Then run: npx blend-token-studio brand\n') +} +``` + +**Key command -- `brand`**: + +```typescript +async function brand(options: { + preset?: string + primary?: string + radius?: string +}) { + // Load blend.config.json + const config = readConfig() + + let brandConfig: BrandConfig + + if (options.preset) { + // Use preset + brandConfig = loadPreset(options.preset) + } else if (options.primary) { + // Generate from single color + const scale = generateColorScale(options.primary) + brandConfig = { + brandId: 'custom', + name: 'Custom Brand', + version: '1.0.0', + colors: { primary: scale }, + radius: options.radius + ? getRadiusPreset(options.radius) + : undefined, + } + } else { + // Interactive prompts + brandConfig = await interactiveBrandPrompt() + } + + // Resolve tokens using token-engine + const spinner = ora('Resolving tokens for 26 components...').start() + const lightTokens = resolveBrandTokens(brandConfig, 'light') + const darkTokens = resolveBrandTokens(brandConfig, 'dark') + spinner.succeed('Tokens resolved') + + // Write files + const outputDir = config.output + writeFileSync( + `${outputDir}/tokens.ts`, + generateTokensFile(lightTokens, darkTokens, brandConfig) + ) + writeFileSync( + `${outputDir}/brand.json`, + JSON.stringify(brandConfig, null, 4) + ) + + console.log(`\n Brand "${brandConfig.name}" applied!`) + console.log(` Commit ${outputDir}/brand.json to version control.\n`) +} +``` + +**Key command -- `pull`**: + +```typescript +async function pull(branchId: string) { + const config = readConfig() + + // 1. Fetch brand config from studio API + const spinner = ora(`Fetching ${branchId}...`).start() + const brandConfig = await apiClient.getBranch(branchId) + spinner.succeed(`Fetched: ${brandConfig.name} v${brandConfig.version}`) + + // 2. Resolve tokens locally + spinner.start('Resolving tokens...') + const lightTokens = resolveBrandTokens(brandConfig, 'light') + const darkTokens = resolveBrandTokens(brandConfig, 'dark') + spinner.succeed('Tokens resolved') + + // 3. Write files + const outputDir = config.output + writeFileSync( + `${outputDir}/tokens.ts`, + generateTokensFile(lightTokens, darkTokens, brandConfig) + ) + writeFileSync( + `${outputDir}/brand.json`, + JSON.stringify(brandConfig, null, 4) + ) + + // 4. Update blend.config.json + config.brand = branchId + writeFileSync('blend.config.json', JSON.stringify(config, null, 4)) +} +``` + +**Generated `src/blend/tokens.ts`** (what the user's app actually imports): + +```typescript +// Auto-generated by Blend Token Studio -- do not edit manually +// Brand: HDFC Bank Retail (hdfc/retail) v1.0.0 +// Generated: 2026-04-07T12:00:00Z +// Regenerate: npx blend-token-studio brand --preset hdfc + +import type { ComponentTokenType } from '@juspay/blend-design-system' + +export const componentTokens: ComponentTokenType = { + BUTTONV2: { + sm: { + /* 200+ resolved token values */ + }, + lg: { + /* ... */ + }, + }, + MULTI_SELECT_V2: { + sm: { + /* ... */ + }, + lg: { + /* ... */ + }, + }, + // ... all 26 V2 components pre-resolved +} + +export const darkComponentTokens: ComponentTokenType = { + BUTTONV2: { + sm: { + /* dark values */ + }, + lg: { + /* ... */ + }, + }, + // ... +} +``` + +**Generated `src/blend/provider.tsx`** (user wraps their app with this): + +```tsx +// Auto-generated by Blend Token Studio -- safe to edit +'use client' + +import { ThemeProvider, Theme } from '@juspay/blend-design-system' +import '@juspay/blend-design-system/style.css' +import { componentTokens, darkComponentTokens } from './tokens' + +type BlendProviderProps = { + children: React.ReactNode + theme?: 'light' | 'dark' +} + +export function BlendProvider({ + children, + theme = 'light', +}: BlendProviderProps) { + const tokens = theme === 'dark' ? darkComponentTokens : componentTokens + + return ( + + {children} + + ) +} +``` + +--- + +### Step 8: Diff Engine + +**Location**: `packages/token-engine/src/diffBrandConfigs.ts` + +```typescript +export interface TokenDiff { + path: string // "colors.primary.600" + oldValue: string // "#0561E2" + newValue: string // "#E31837" +} + +export function diffBrandConfigs( + configA: BrandConfig, + configB: BrandConfig +): TokenDiff[] { + const diffs: TokenDiff[] = [] + + // Diff colors + for (const group of [ + 'primary', + 'gray', + 'red', + 'green', + 'yellow', + 'orange', + 'purple', + ]) { + const aColors = configA.colors?.[group] ?? {} + const bColors = configB.colors?.[group] ?? {} + const allShades = new Set([ + ...Object.keys(aColors), + ...Object.keys(bColors), + ]) + + for (const shade of allShades) { + if (aColors[shade] !== bColors[shade]) { + diffs.push({ + path: `colors.${group}.${shade}`, + oldValue: aColors[shade] ?? '(default)', + newValue: bColors[shade] ?? '(default)', + }) + } + } + } + + // Diff radius + // Diff shadows + // Diff font + + return diffs +} +``` + +--- + +### Step 9: Validation + +**Location**: `packages/token-engine/src/validateBrandConfig.ts` + +```typescript +export interface ValidationResult { + valid: boolean + errors: ValidationError[] + warnings: ValidationWarning[] +} + +export function validateBrandConfig(config: unknown): ValidationResult { + const errors: ValidationError[] = [] + const warnings: ValidationWarning[] = [] + + // 1. Structure validation + if (!config || typeof config !== 'object') { + errors.push({ path: '', message: 'Config must be an object' }) + return { valid: false, errors, warnings } + } + + const c = config as Record + + // 2. Required fields + if (!c.brandId || typeof c.brandId !== 'string') { + errors.push({ path: 'brandId', message: 'brandId is required' }) + } + + // 3. Color format validation + if (c.colors && typeof c.colors === 'object') { + for (const [group, shades] of Object.entries(c.colors)) { + if (shades && typeof shades === 'object') { + for (const [shade, value] of Object.entries(shades)) { + if (typeof value === 'string' && !isValidColor(value)) { + errors.push({ + path: `colors.${group}.${shade}`, + message: `Invalid color: ${value}`, + }) + } + } + } + } + } + + // 4. Radius format validation + // 5. Contrast ratio warnings (WCAG AA) + + return { valid: errors.length === 0, errors, warnings } +} +``` + +--- + +## 6. Full File Tree (What to Create) + +``` +blend-design-system/ +├── packages/ +│ ├── blend/ # EXISTING -- minimal changes +│ │ └── lib/ +│ │ └── main.ts # ADD: export getXXXTokens functions +│ │ +│ ├── token-engine/ # NEW -- core brain +│ │ ├── package.json +│ │ ├── tsconfig.json +│ │ └── src/ +│ │ ├── index.ts # Public API: resolveBrandTokens() +│ │ ├── types.ts # BrandConfig, ValidationResult +│ │ ├── buildBrandFoundation.ts # Clone FOUNDATION_THEME + apply overrides +│ │ ├── resolveAllTokens.ts # Call all 26 getXXXTokens() +│ │ ├── validateBrandConfig.ts # Validate JSON + color formats +│ │ ├── diffBrandConfigs.ts # Diff two configs +│ │ └── colorScale.ts # Generate 50-950 scale from 1 hex +│ │ +│ └── cli/ # NEW -- shadcn-style CLI +│ ├── package.json # bin: "blend-token-studio" +│ ├── tsconfig.json +│ └── src/ +│ ├── index.ts # Commander.js entry +│ ├── commands/ +│ │ ├── init.ts # Scaffold project (provider + tokens) +│ │ ├── brand.ts # Interactive brand customization +│ │ ├── pull.ts # Fetch from studio API +│ │ ├── push.ts # Upload to studio API +│ │ ├── diff.ts # Diff local vs defaults/remote +│ │ ├── generate.ts # Offline: JSON -> tokens.ts +│ │ ├── list.ts # List branches from API +│ │ ├── preview.ts # Local preview server +│ │ └── validate.ts # Validate brand.json +│ ├── generators/ +│ │ ├── providerGenerator.ts # Generate src/blend/provider.tsx +│ │ ├── tokensGenerator.ts # Generate src/blend/tokens.ts +│ │ └── configGenerator.ts # Generate blend.config.json +│ ├── utils/ +│ │ ├── detectProject.ts # Detect Next.js / Vite / CRA +│ │ ├── installDeps.ts # Auto-install missing deps +│ │ ├── api-client.ts # HTTP client for studio API +│ │ └── logger.ts # Pretty CLI output +│ └── presets/ +│ ├── index.ts # Preset registry +│ ├── blend-default.json +│ ├── hdfc.json +│ ├── neobank.json +│ └── fintech.json +│ +├── apps/ +│ └── blend-studio/ # RENAMED from blend-monitor +│ ├── app/ +│ │ ├── studio/ # NEW -- Token Studio pages +│ │ │ ├── page.tsx # Branch list +│ │ │ ├── layout.tsx # Studio layout +│ │ │ ├── editor/ +│ │ │ │ └── [branchId]/ +│ │ │ │ └── page.tsx # Token editor + live preview +│ │ │ ├── preview/ +│ │ │ │ └── [branchId]/ +│ │ │ │ └── page.tsx # Shareable full-page preview +│ │ │ └── diff/ +│ │ │ └── [branchId]/ +│ │ │ └── page.tsx # Visual diff view +│ │ │ +│ │ ├── api/studio/ # NEW -- API routes +│ │ │ └── branches/ +│ │ │ ├── route.ts # GET list, POST create +│ │ │ └── [branchId]/ +│ │ │ ├── route.ts # GET detail, PATCH update, DELETE +│ │ │ ├── publish/route.ts +│ │ │ ├── fork/route.ts +│ │ │ ├── diff/route.ts +│ │ │ ├── resolve/route.ts +│ │ │ └── versions/ +│ │ │ └── route.ts +│ │ │ +│ │ └── ...existing pages (/, /npm, /code-connect, /users) -- admin only +│ │ +│ └── src/ +│ ├── frontend/ +│ │ ├── components/ +│ │ │ ├── studio/ # NEW -- Token Studio components +│ │ │ │ ├── BranchList.tsx +│ │ │ │ ├── BranchCard.tsx +│ │ │ │ ├── TokenEditor.tsx +│ │ │ │ ├── ColorSection.tsx +│ │ │ │ ├── ColorShadeRow.tsx +│ │ │ │ ├── ColorPaletteGenerator.tsx +│ │ │ │ ├── RadiusSection.tsx +│ │ │ │ ├── ShadowSection.tsx +│ │ │ │ ├── ComponentShowcase.tsx +│ │ │ │ │ ├── ButtonShowcase.tsx +│ │ │ │ │ ├── InputShowcase.tsx +│ │ │ │ │ ├── FeedbackShowcase.tsx +│ │ │ │ │ ├── NavigationShowcase.tsx +│ │ │ │ │ └── DataShowcase.tsx +│ │ │ │ ├── DiffViewer.tsx +│ │ │ │ ├── ExportPanel.tsx +│ │ │ │ ├── BrandPresetSelector.tsx +│ │ │ │ └── PublishDialog.tsx +│ │ │ │ +│ │ │ └── shared/ +│ │ │ └── SidebarConfig.tsx # MODIFIED -- Studio + Admin nav +│ │ │ +│ │ ├── hooks/ +│ │ │ ├── useBranch.ts # Firestore branch CRUD +│ │ │ ├── useBrandConfig.ts # Live config state + auto-save +│ │ │ └── useTokenPreview.ts # Resolve + memoize tokens +│ │ │ +│ │ └── lib/ +│ │ └── firestore.ts # Firestore client for branches +│ │ +│ └── backend/ +│ └── lib/ +│ └── firestore-admin.ts # Admin Firestore access +│ +└── Consumer project (what gets generated in user's codebase): + ├── blend.config.json # Created by `init` + └── src/blend/ + ├── provider.tsx # Created by `init`, safe to edit + ├── tokens.ts # Created by `brand` or `pull`, DO NOT edit + └── brand.json # Created by `brand` or `pull`, commit this +``` + +--- + +## 7. Execution Order + +See **Section 11** for the updated execution order with effort estimates. + +--- + +## 8. What NOT to Do + +1. **Do NOT create a semantic token layer** -- the foundation-level override approach gives us brand support for all 26 V2 components with zero component code changes +2. **Do NOT modify any existing V2 component files** -- they already accept `foundationToken` as input +3. **Do NOT create separate brand factory functions per component** -- `resolveAllTokens` calls the existing `getXXXTokens` functions directly +4. **Do NOT use a separate database for token data** -- Firestore is already configured and ideal for JSON documents +5. **Do NOT build the Figma plugin in MVP** -- it's Phase 5 in the roadmap +6. **Do NOT build multi-tenant isolation in MVP** -- single Firebase project, role-based access is sufficient + +--- + +## 9. Pre-Built Brand Presets (for demo/testing) + +Include 4 brand presets in the dashboard: + +**Blend Default** (no overrides): + +```json +{ + "brandId": "blend/default", + "name": "Blend Default", + "version": "1.0.0", + "colors": {} +} +``` + +**HDFC Bank**: + +```json +{ + "brandId": "hdfc/retail", + "name": "HDFC Bank", + "version": "1.0.0", + "colors": { + "primary": { + "300": "#FF8A8A", + "400": "#FF5252", + "500": "#E31837", + "600": "#C01530", + "700": "#A01228", + "800": "#801020" + } + }, + "radius": { "6": "4px", "8": "4px", "10": "4px", "12": "6px" } +} +``` + +**NeoBank (Purple)**: + +```json +{ + "brandId": "neobank/light", + "name": "NeoBank", + "version": "1.0.0", + "colors": { + "primary": { + "300": "#DAB2FF", + "400": "#C27AFF", + "500": "#AD46FF", + "600": "#9810FA", + "700": "#8200DB", + "800": "#6E11B0" + } + }, + "radius": { "10": "20px", "12": "24px" } +} +``` + +**FinTech (Green)**: + +```json +{ + "brandId": "fintech/app", + "name": "FinTech Green", + "version": "1.0.0", + "colors": { + "primary": { + "300": "#7BF1A8", + "400": "#00D492", + "500": "#00C951", + "600": "#00A63E", + "700": "#008236", + "800": "#016630" + } + } +} +``` + +--- + +## 10. Consumer App Integration (End Result) + +### Path A: Developer using CLI (zero-dashboard flow) + +```bash +# One-time setup (30 seconds) +npx blend-token-studio init + +# Apply HDFC branding (10 seconds) +npx blend-token-studio brand --preset hdfc +``` + +```tsx +// src/App.tsx -- this is ALL the code the developer writes +import { BlendProvider } from './blend/provider' +import { + ButtonV2, + TextInputV2, + MultiSelectV2, +} from '@juspay/blend-design-system' + +export function App() { + return ( + + + + + + ) +} +``` + +**3 lines of setup. 1 CLI command to brand. Every V2 component renders with HDFC colors.** + +### Path B: Designer uses dashboard, developer pulls + +```bash +# Designer publishes "hdfc/retail" from the dashboard +# Developer runs: +npx blend-token-studio pull hdfc/retail + +# Same App.tsx as above -- no code changes needed +``` + +### Path C: Custom hex color (no preset, no dashboard) + +```bash +npx blend-token-studio brand --primary "#E31837" --radius sharp +# Generates full 50-950 color scale from the single hex +# Applies 4px radius to all components +# Done. +``` + +### Comparison: Before vs After + +| | Before Token Studio | After Token Studio | +| ------------ | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | +| Install | `npm install` + manual CSS import | `npx blend-token-studio init` (auto-installs everything) | +| Setup | Write ThemeProvider manually, import FOUNDATION_THEME, understand token architecture | `{children}` | +| Brand colors | Clone FOUNDATION_THEME, spread 11 color shades, call 26 getXXXTokens functions, build ComponentTokenType manually | `npx blend-token-studio brand --preset hdfc` | +| Dark mode | Repeat the above for dark theme | Automatic -- `` | +| Update brand | Re-do all manual steps | `npx blend-token-studio pull hdfc/retail` | +| Documented? | Partially (getting-started.mdx shows old Button, not ButtonV2) | CLI guides you with prompts + generates files | + +**Zero changes to component usage code.** The `componentTokens` prop does all the work through the existing `initComponentTokens` -> `useResponsiveTokens` pipeline. + +--- + +## 11. Execution Order (Updated) + +| Step | What | Depends On | Risk | Effort | +| ------ | -------------------------------------------------------------------------------- | ---------- | ------ | ------ | +| **1** | Create `packages/token-engine/` with `buildBrandFoundation` + `resolveAllTokens` | Nothing | Low | 1 day | +| **2** | Add `getXXXTokens` exports to `packages/blend/lib/main.ts` | Nothing | None | 30 min | +| **3** | Build CLI: `init` + `brand` commands (the shadcn-style DX) | Step 1 | Low | 2 days | +| **4** | Build CLI: `pull` + `push` + `diff` + `generate` + `validate` + `list` | Steps 1, 3 | Low | 2 days | +| **5** | Rename `blend-monitor` -> `blend-studio`, add Token Studio navigation | Nothing | Low | 1 hour | +| **6** | Add Firestore collections + client code for branches | Step 5 | Low | 1 day | +| **7** | Build API routes (`/api/studio/branches/*`) | Steps 1, 6 | Low | 1 day | +| **8** | Build Branch List page (`/studio`) | Steps 6, 7 | Low | 1 day | +| **9** | Build Token Editor page with live preview | Steps 1, 7 | Medium | 3 days | +| **10** | Build Component Showcase (preview panel) | Step 1 | Low | 1 day | +| **11** | Build diff engine + validation | Step 1 | Low | 1 day | +| **12** | E2E test: init -> brand -> preview -> pull -> render | All | Low | 1 day | + +**Total estimated effort: ~15 days** + +**Priority**: Steps 1-4 (token engine + CLI) deliver the most value fastest. A developer can `init` + `brand` without the dashboard existing. The dashboard (Steps 5-10) is for designers and team workflows. diff --git a/apps/blend-monitor/.dockerignore b/apps/blend-studio/.dockerignore similarity index 100% rename from apps/blend-monitor/.dockerignore rename to apps/blend-studio/.dockerignore diff --git a/apps/blend-monitor/.env.example b/apps/blend-studio/.env.example similarity index 100% rename from apps/blend-monitor/.env.example rename to apps/blend-studio/.env.example diff --git a/apps/blend-monitor/.eslintrc.json b/apps/blend-studio/.eslintrc.json similarity index 100% rename from apps/blend-monitor/.eslintrc.json rename to apps/blend-studio/.eslintrc.json diff --git a/apps/blend-monitor/.gitignore b/apps/blend-studio/.gitignore similarity index 100% rename from apps/blend-monitor/.gitignore rename to apps/blend-studio/.gitignore diff --git a/apps/blend-monitor/DEPLOYMENT.md b/apps/blend-studio/DEPLOYMENT.md similarity index 100% rename from apps/blend-monitor/DEPLOYMENT.md rename to apps/blend-studio/DEPLOYMENT.md diff --git a/apps/blend-monitor/Dockerfile b/apps/blend-studio/Dockerfile similarity index 100% rename from apps/blend-monitor/Dockerfile rename to apps/blend-studio/Dockerfile diff --git a/apps/blend-monitor/LOCAL_SETUP.md b/apps/blend-studio/LOCAL_SETUP.md similarity index 100% rename from apps/blend-monitor/LOCAL_SETUP.md rename to apps/blend-studio/LOCAL_SETUP.md diff --git a/apps/blend-monitor/README.md b/apps/blend-studio/README.md similarity index 100% rename from apps/blend-monitor/README.md rename to apps/blend-studio/README.md diff --git a/apps/blend-monitor/app/api/activity/recent/route.ts b/apps/blend-studio/app/api/activity/recent/route.ts similarity index 100% rename from apps/blend-monitor/app/api/activity/recent/route.ts rename to apps/blend-studio/app/api/activity/recent/route.ts diff --git a/apps/blend-monitor/app/api/components/components-pg/route.ts b/apps/blend-studio/app/api/components/components-pg/route.ts similarity index 100% rename from apps/blend-monitor/app/api/components/components-pg/route.ts rename to apps/blend-studio/app/api/components/components-pg/route.ts diff --git a/apps/blend-monitor/app/api/components/route.ts b/apps/blend-studio/app/api/components/route.ts similarity index 100% rename from apps/blend-monitor/app/api/components/route.ts rename to apps/blend-studio/app/api/components/route.ts diff --git a/apps/blend-monitor/app/api/health/route.ts b/apps/blend-studio/app/api/health/route.ts similarity index 100% rename from apps/blend-monitor/app/api/health/route.ts rename to apps/blend-studio/app/api/health/route.ts diff --git a/apps/blend-monitor/app/api/npm/route.ts b/apps/blend-studio/app/api/npm/route.ts similarity index 100% rename from apps/blend-monitor/app/api/npm/route.ts rename to apps/blend-studio/app/api/npm/route.ts diff --git a/apps/blend-monitor/app/api/npm/stats/route.ts b/apps/blend-studio/app/api/npm/stats/route.ts similarity index 100% rename from apps/blend-monitor/app/api/npm/stats/route.ts rename to apps/blend-studio/app/api/npm/stats/route.ts diff --git a/apps/blend-monitor/app/api/npm/sync/route.ts b/apps/blend-studio/app/api/npm/sync/route.ts similarity index 100% rename from apps/blend-monitor/app/api/npm/sync/route.ts rename to apps/blend-studio/app/api/npm/sync/route.ts diff --git a/apps/blend-monitor/app/api/npm/trends/route.ts b/apps/blend-studio/app/api/npm/trends/route.ts similarity index 100% rename from apps/blend-monitor/app/api/npm/trends/route.ts rename to apps/blend-studio/app/api/npm/trends/route.ts diff --git a/apps/blend-monitor/app/api/npm/versions/route.ts b/apps/blend-studio/app/api/npm/versions/route.ts similarity index 100% rename from apps/blend-monitor/app/api/npm/versions/route.ts rename to apps/blend-studio/app/api/npm/versions/route.ts diff --git a/apps/blend-monitor/app/api/users/[userId]/role/route.ts b/apps/blend-studio/app/api/users/[userId]/role/route.ts similarity index 100% rename from apps/blend-monitor/app/api/users/[userId]/role/route.ts rename to apps/blend-studio/app/api/users/[userId]/role/route.ts diff --git a/apps/blend-monitor/app/api/users/[userId]/route.ts b/apps/blend-studio/app/api/users/[userId]/route.ts similarity index 100% rename from apps/blend-monitor/app/api/users/[userId]/route.ts rename to apps/blend-studio/app/api/users/[userId]/route.ts diff --git a/apps/blend-monitor/app/api/users/activity/route.ts b/apps/blend-studio/app/api/users/activity/route.ts similarity index 100% rename from apps/blend-monitor/app/api/users/activity/route.ts rename to apps/blend-studio/app/api/users/activity/route.ts diff --git a/apps/blend-monitor/app/api/users/route.ts b/apps/blend-studio/app/api/users/route.ts similarity index 100% rename from apps/blend-monitor/app/api/users/route.ts rename to apps/blend-studio/app/api/users/route.ts diff --git a/apps/blend-monitor/app/code-connect/health/page.tsx b/apps/blend-studio/app/code-connect/health/page.tsx similarity index 100% rename from apps/blend-monitor/app/code-connect/health/page.tsx rename to apps/blend-studio/app/code-connect/health/page.tsx diff --git a/apps/blend-monitor/app/code-connect/page.tsx b/apps/blend-studio/app/code-connect/page.tsx similarity index 100% rename from apps/blend-monitor/app/code-connect/page.tsx rename to apps/blend-studio/app/code-connect/page.tsx diff --git a/apps/blend-monitor/app/favicon.ico b/apps/blend-studio/app/favicon.ico similarity index 100% rename from apps/blend-monitor/app/favicon.ico rename to apps/blend-studio/app/favicon.ico diff --git a/apps/blend-monitor/app/globals.css b/apps/blend-studio/app/globals.css similarity index 100% rename from apps/blend-monitor/app/globals.css rename to apps/blend-studio/app/globals.css diff --git a/apps/blend-monitor/app/layout.tsx b/apps/blend-studio/app/layout.tsx similarity index 100% rename from apps/blend-monitor/app/layout.tsx rename to apps/blend-studio/app/layout.tsx diff --git a/apps/blend-monitor/app/login/page.tsx b/apps/blend-studio/app/login/page.tsx similarity index 100% rename from apps/blend-monitor/app/login/page.tsx rename to apps/blend-studio/app/login/page.tsx diff --git a/apps/blend-monitor/app/not-found.tsx b/apps/blend-studio/app/not-found.tsx similarity index 100% rename from apps/blend-monitor/app/not-found.tsx rename to apps/blend-studio/app/not-found.tsx diff --git a/apps/blend-monitor/app/npm/page.tsx b/apps/blend-studio/app/npm/page.tsx similarity index 100% rename from apps/blend-monitor/app/npm/page.tsx rename to apps/blend-studio/app/npm/page.tsx diff --git a/apps/blend-monitor/app/page.tsx b/apps/blend-studio/app/page.tsx similarity index 100% rename from apps/blend-monitor/app/page.tsx rename to apps/blend-studio/app/page.tsx diff --git a/apps/blend-monitor/app/users/[userId]/activity/page.tsx b/apps/blend-studio/app/users/[userId]/activity/page.tsx similarity index 100% rename from apps/blend-monitor/app/users/[userId]/activity/page.tsx rename to apps/blend-studio/app/users/[userId]/activity/page.tsx diff --git a/apps/blend-monitor/app/users/page.tsx b/apps/blend-studio/app/users/page.tsx similarity index 100% rename from apps/blend-monitor/app/users/page.tsx rename to apps/blend-studio/app/users/page.tsx diff --git a/apps/blend-monitor/cloudbuild.yaml b/apps/blend-studio/cloudbuild.yaml similarity index 100% rename from apps/blend-monitor/cloudbuild.yaml rename to apps/blend-studio/cloudbuild.yaml diff --git a/apps/blend-monitor/database/schema.sql b/apps/blend-studio/database/schema.sql similarity index 100% rename from apps/blend-monitor/database/schema.sql rename to apps/blend-studio/database/schema.sql diff --git a/apps/blend-monitor/deploy.sh b/apps/blend-studio/deploy.sh similarity index 100% rename from apps/blend-monitor/deploy.sh rename to apps/blend-studio/deploy.sh diff --git a/apps/blend-monitor/eslint.config.mjs b/apps/blend-studio/eslint.config.mjs similarity index 100% rename from apps/blend-monitor/eslint.config.mjs rename to apps/blend-studio/eslint.config.mjs diff --git a/apps/blend-monitor/extract-firebase-key.js b/apps/blend-studio/extract-firebase-key.js similarity index 100% rename from apps/blend-monitor/extract-firebase-key.js rename to apps/blend-studio/extract-firebase-key.js diff --git a/apps/blend-monitor/middleware.ts b/apps/blend-studio/middleware.ts similarity index 100% rename from apps/blend-monitor/middleware.ts rename to apps/blend-studio/middleware.ts diff --git a/apps/blend-monitor/next.config.ts b/apps/blend-studio/next.config.ts similarity index 100% rename from apps/blend-monitor/next.config.ts rename to apps/blend-studio/next.config.ts diff --git a/apps/blend-monitor/package.json b/apps/blend-studio/package.json similarity index 93% rename from apps/blend-monitor/package.json rename to apps/blend-studio/package.json index 1184f493a..83d9ca18a 100644 --- a/apps/blend-monitor/package.json +++ b/apps/blend-studio/package.json @@ -1,5 +1,5 @@ { - "name": "blend-monitor", + "name": "blend-studio", "version": "0.1.0", "private": true, "scripts": { @@ -12,6 +12,7 @@ "populate-data": "node scripts/populate-all-deployment-data.js" }, "dependencies": { + "@blend-design/token-engine": "workspace:*", "@types/pg": "^8.15.4", "@juspay/blend-design-system": "workspace:*", "dotenv": "^17.2.1", diff --git a/apps/blend-monitor/polyfills.ts b/apps/blend-studio/polyfills.ts similarity index 100% rename from apps/blend-monitor/polyfills.ts rename to apps/blend-studio/polyfills.ts diff --git a/apps/blend-monitor/postcss.config.mjs b/apps/blend-studio/postcss.config.mjs similarity index 100% rename from apps/blend-monitor/postcss.config.mjs rename to apps/blend-studio/postcss.config.mjs diff --git a/apps/blend-monitor/public/file.svg b/apps/blend-studio/public/file.svg similarity index 100% rename from apps/blend-monitor/public/file.svg rename to apps/blend-studio/public/file.svg diff --git a/apps/blend-monitor/public/globe.svg b/apps/blend-studio/public/globe.svg similarity index 100% rename from apps/blend-monitor/public/globe.svg rename to apps/blend-studio/public/globe.svg diff --git a/apps/blend-monitor/public/next.svg b/apps/blend-studio/public/next.svg similarity index 100% rename from apps/blend-monitor/public/next.svg rename to apps/blend-studio/public/next.svg diff --git a/apps/blend-monitor/public/vercel.svg b/apps/blend-studio/public/vercel.svg similarity index 100% rename from apps/blend-monitor/public/vercel.svg rename to apps/blend-studio/public/vercel.svg diff --git a/apps/blend-monitor/public/window.svg b/apps/blend-studio/public/window.svg similarity index 100% rename from apps/blend-monitor/public/window.svg rename to apps/blend-studio/public/window.svg diff --git a/apps/blend-monitor/src/README.md b/apps/blend-studio/src/README.md similarity index 100% rename from apps/blend-monitor/src/README.md rename to apps/blend-studio/src/README.md diff --git a/apps/blend-monitor/src/backend/api/activity/recent/route.ts b/apps/blend-studio/src/backend/api/activity/recent/route.ts similarity index 100% rename from apps/blend-monitor/src/backend/api/activity/recent/route.ts rename to apps/blend-studio/src/backend/api/activity/recent/route.ts diff --git a/apps/blend-monitor/src/backend/api/components/components-pg/route.ts b/apps/blend-studio/src/backend/api/components/components-pg/route.ts similarity index 100% rename from apps/blend-monitor/src/backend/api/components/components-pg/route.ts rename to apps/blend-studio/src/backend/api/components/components-pg/route.ts diff --git a/apps/blend-monitor/src/backend/api/components/route.ts b/apps/blend-studio/src/backend/api/components/route.ts similarity index 100% rename from apps/blend-monitor/src/backend/api/components/route.ts rename to apps/blend-studio/src/backend/api/components/route.ts diff --git a/apps/blend-monitor/src/backend/api/health/route.ts b/apps/blend-studio/src/backend/api/health/route.ts similarity index 100% rename from apps/blend-monitor/src/backend/api/health/route.ts rename to apps/blend-studio/src/backend/api/health/route.ts diff --git a/apps/blend-monitor/src/backend/api/npm/route.ts b/apps/blend-studio/src/backend/api/npm/route.ts similarity index 100% rename from apps/blend-monitor/src/backend/api/npm/route.ts rename to apps/blend-studio/src/backend/api/npm/route.ts diff --git a/apps/blend-monitor/src/backend/api/npm/stats/route.ts b/apps/blend-studio/src/backend/api/npm/stats/route.ts similarity index 100% rename from apps/blend-monitor/src/backend/api/npm/stats/route.ts rename to apps/blend-studio/src/backend/api/npm/stats/route.ts diff --git a/apps/blend-monitor/src/backend/api/npm/sync/route.ts b/apps/blend-studio/src/backend/api/npm/sync/route.ts similarity index 100% rename from apps/blend-monitor/src/backend/api/npm/sync/route.ts rename to apps/blend-studio/src/backend/api/npm/sync/route.ts diff --git a/apps/blend-monitor/src/backend/api/npm/trends/route.ts b/apps/blend-studio/src/backend/api/npm/trends/route.ts similarity index 100% rename from apps/blend-monitor/src/backend/api/npm/trends/route.ts rename to apps/blend-studio/src/backend/api/npm/trends/route.ts diff --git a/apps/blend-monitor/src/backend/api/npm/versions/route.ts b/apps/blend-studio/src/backend/api/npm/versions/route.ts similarity index 100% rename from apps/blend-monitor/src/backend/api/npm/versions/route.ts rename to apps/blend-studio/src/backend/api/npm/versions/route.ts diff --git a/apps/blend-monitor/src/backend/api/users/[userId]/role/route.ts b/apps/blend-studio/src/backend/api/users/[userId]/role/route.ts similarity index 100% rename from apps/blend-monitor/src/backend/api/users/[userId]/role/route.ts rename to apps/blend-studio/src/backend/api/users/[userId]/role/route.ts diff --git a/apps/blend-monitor/src/backend/api/users/[userId]/route.ts b/apps/blend-studio/src/backend/api/users/[userId]/route.ts similarity index 100% rename from apps/blend-monitor/src/backend/api/users/[userId]/route.ts rename to apps/blend-studio/src/backend/api/users/[userId]/route.ts diff --git a/apps/blend-monitor/src/backend/api/users/activity/route.ts b/apps/blend-studio/src/backend/api/users/activity/route.ts similarity index 100% rename from apps/blend-monitor/src/backend/api/users/activity/route.ts rename to apps/blend-studio/src/backend/api/users/activity/route.ts diff --git a/apps/blend-monitor/src/backend/api/users/route.ts b/apps/blend-studio/src/backend/api/users/route.ts similarity index 100% rename from apps/blend-monitor/src/backend/api/users/route.ts rename to apps/blend-studio/src/backend/api/users/route.ts diff --git a/apps/blend-monitor/src/backend/external/npm-client.ts b/apps/blend-studio/src/backend/external/npm-client.ts similarity index 100% rename from apps/blend-monitor/src/backend/external/npm-client.ts rename to apps/blend-studio/src/backend/external/npm-client.ts diff --git a/apps/blend-monitor/src/backend/lib/auth-middleware.ts b/apps/blend-studio/src/backend/lib/auth-middleware.ts similarity index 100% rename from apps/blend-monitor/src/backend/lib/auth-middleware.ts rename to apps/blend-studio/src/backend/lib/auth-middleware.ts diff --git a/apps/blend-monitor/src/backend/lib/database-service.ts b/apps/blend-studio/src/backend/lib/database-service.ts similarity index 100% rename from apps/blend-monitor/src/backend/lib/database-service.ts rename to apps/blend-studio/src/backend/lib/database-service.ts diff --git a/apps/blend-monitor/src/backend/lib/database.ts b/apps/blend-studio/src/backend/lib/database.ts similarity index 100% rename from apps/blend-monitor/src/backend/lib/database.ts rename to apps/blend-studio/src/backend/lib/database.ts diff --git a/apps/blend-monitor/src/backend/lib/firebase-admin.ts b/apps/blend-studio/src/backend/lib/firebase-admin.ts similarity index 100% rename from apps/blend-monitor/src/backend/lib/firebase-admin.ts rename to apps/blend-studio/src/backend/lib/firebase-admin.ts diff --git a/apps/blend-monitor/src/backend/lib/role-service.ts b/apps/blend-studio/src/backend/lib/role-service.ts similarity index 100% rename from apps/blend-monitor/src/backend/lib/role-service.ts rename to apps/blend-studio/src/backend/lib/role-service.ts diff --git a/apps/blend-monitor/src/backend/scanners/component-scanner.ts b/apps/blend-studio/src/backend/scanners/component-scanner.ts similarity index 100% rename from apps/blend-monitor/src/backend/scanners/component-scanner.ts rename to apps/blend-studio/src/backend/scanners/component-scanner.ts diff --git a/apps/blend-monitor/src/frontend/app/code-connect/health/page.tsx b/apps/blend-studio/src/frontend/app/code-connect/health/page.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/app/code-connect/health/page.tsx rename to apps/blend-studio/src/frontend/app/code-connect/health/page.tsx diff --git a/apps/blend-monitor/src/frontend/app/code-connect/page.tsx b/apps/blend-studio/src/frontend/app/code-connect/page.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/app/code-connect/page.tsx rename to apps/blend-studio/src/frontend/app/code-connect/page.tsx diff --git a/apps/blend-monitor/src/frontend/app/login/page.tsx b/apps/blend-studio/src/frontend/app/login/page.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/app/login/page.tsx rename to apps/blend-studio/src/frontend/app/login/page.tsx diff --git a/apps/blend-monitor/src/frontend/app/npm/page.tsx b/apps/blend-studio/src/frontend/app/npm/page.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/app/npm/page.tsx rename to apps/blend-studio/src/frontend/app/npm/page.tsx diff --git a/apps/blend-monitor/src/frontend/app/page.tsx b/apps/blend-studio/src/frontend/app/page.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/app/page.tsx rename to apps/blend-studio/src/frontend/app/page.tsx diff --git a/apps/blend-monitor/src/frontend/app/users/[userId]/activity/page.tsx b/apps/blend-studio/src/frontend/app/users/[userId]/activity/page.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/app/users/[userId]/activity/page.tsx rename to apps/blend-studio/src/frontend/app/users/[userId]/activity/page.tsx diff --git a/apps/blend-monitor/src/frontend/app/users/page.tsx b/apps/blend-studio/src/frontend/app/users/page.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/app/users/page.tsx rename to apps/blend-studio/src/frontend/app/users/page.tsx diff --git a/apps/blend-monitor/src/frontend/components/auth/PermissionGuard.tsx b/apps/blend-studio/src/frontend/components/auth/PermissionGuard.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/components/auth/PermissionGuard.tsx rename to apps/blend-studio/src/frontend/components/auth/PermissionGuard.tsx diff --git a/apps/blend-monitor/src/frontend/components/auth/ProtectedRoute.tsx b/apps/blend-studio/src/frontend/components/auth/ProtectedRoute.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/components/auth/ProtectedRoute.tsx rename to apps/blend-studio/src/frontend/components/auth/ProtectedRoute.tsx diff --git a/apps/blend-monitor/src/frontend/components/dashboard/CodeConnectContent.tsx b/apps/blend-studio/src/frontend/components/dashboard/CodeConnectContent.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/components/dashboard/CodeConnectContent.tsx rename to apps/blend-studio/src/frontend/components/dashboard/CodeConnectContent.tsx diff --git a/apps/blend-monitor/src/frontend/components/dashboard/MetricCard.tsx b/apps/blend-studio/src/frontend/components/dashboard/MetricCard.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/components/dashboard/MetricCard.tsx rename to apps/blend-studio/src/frontend/components/dashboard/MetricCard.tsx diff --git a/apps/blend-monitor/src/frontend/components/shared/AppShell.tsx b/apps/blend-studio/src/frontend/components/shared/AppShell.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/components/shared/AppShell.tsx rename to apps/blend-studio/src/frontend/components/shared/AppShell.tsx diff --git a/apps/blend-monitor/src/frontend/components/shared/ClientLayout.tsx b/apps/blend-studio/src/frontend/components/shared/ClientLayout.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/components/shared/ClientLayout.tsx rename to apps/blend-studio/src/frontend/components/shared/ClientLayout.tsx diff --git a/apps/blend-monitor/src/frontend/components/shared/EmptyState.tsx b/apps/blend-studio/src/frontend/components/shared/EmptyState.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/components/shared/EmptyState.tsx rename to apps/blend-studio/src/frontend/components/shared/EmptyState.tsx diff --git a/apps/blend-monitor/src/frontend/components/shared/Loader.tsx b/apps/blend-studio/src/frontend/components/shared/Loader.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/components/shared/Loader.tsx rename to apps/blend-studio/src/frontend/components/shared/Loader.tsx diff --git a/apps/blend-monitor/src/frontend/components/shared/SidebarConfig.tsx b/apps/blend-studio/src/frontend/components/shared/SidebarConfig.tsx similarity index 55% rename from apps/blend-monitor/src/frontend/components/shared/SidebarConfig.tsx rename to apps/blend-studio/src/frontend/components/shared/SidebarConfig.tsx index 1241efac5..e79b89f14 100644 --- a/apps/blend-monitor/src/frontend/components/shared/SidebarConfig.tsx +++ b/apps/blend-studio/src/frontend/components/shared/SidebarConfig.tsx @@ -1,18 +1,27 @@ 'use client' import React from 'react' -import { Home, Link, Package, Zap, Users } from 'lucide-react' +import { + Home, + Link, + Package, + Palette, + Zap, + Users, + GitBranch, + Eye, +} from 'lucide-react' import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime' export const tenants = [ { - label: 'Blend Monitor', + label: 'Blend Studio', icon: (
- +
), - id: 'blend-monitor', + id: 'blend-studio', }, ] @@ -22,21 +31,43 @@ export const getNavigationData = ( router: AppRouterInstance, pathname: string ) => [ + // --------------------------------------------------------------- + // Token Studio — accessible to all authenticated users + // --------------------------------------------------------------- { - label: 'Main', + label: 'Token Studio', isCollapsible: false, items: [ { - label: 'Dashboard', - leftSlot: , - onClick: () => router.push('/'), - isActive: pathname === '/', + label: 'Branches', + leftSlot: , + onClick: () => router.push('/studio'), + isActive: + pathname === '/studio' || + pathname.startsWith('/studio/editor'), + }, + { + label: 'Preview', + leftSlot: , + onClick: () => router.push('/studio/preview'), + isActive: pathname.startsWith('/studio/preview'), }, ], }, + + // --------------------------------------------------------------- + // Monitor — admin-only section (existing functionality) + // --------------------------------------------------------------- { - label: 'Monitoring', + label: 'Monitor', + isCollapsible: true, items: [ + { + label: 'Dashboard', + leftSlot: , + onClick: () => router.push('/'), + isActive: pathname === '/', + }, { label: 'Code Connect', leftSlot: , diff --git a/apps/blend-monitor/src/frontend/components/shared/UserAvatar.tsx b/apps/blend-studio/src/frontend/components/shared/UserAvatar.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/components/shared/UserAvatar.tsx rename to apps/blend-studio/src/frontend/components/shared/UserAvatar.tsx diff --git a/apps/blend-monitor/src/frontend/contexts/AuthContext.tsx b/apps/blend-studio/src/frontend/contexts/AuthContext.tsx similarity index 100% rename from apps/blend-monitor/src/frontend/contexts/AuthContext.tsx rename to apps/blend-studio/src/frontend/contexts/AuthContext.tsx diff --git a/apps/blend-monitor/src/frontend/hooks/usePostgreSQLData.ts b/apps/blend-studio/src/frontend/hooks/usePostgreSQLData.ts similarity index 100% rename from apps/blend-monitor/src/frontend/hooks/usePostgreSQLData.ts rename to apps/blend-studio/src/frontend/hooks/usePostgreSQLData.ts diff --git a/apps/blend-monitor/src/frontend/lib/api-client.ts b/apps/blend-studio/src/frontend/lib/api-client.ts similarity index 100% rename from apps/blend-monitor/src/frontend/lib/api-client.ts rename to apps/blend-studio/src/frontend/lib/api-client.ts diff --git a/apps/blend-monitor/src/frontend/lib/firebase.ts b/apps/blend-studio/src/frontend/lib/firebase.ts similarity index 100% rename from apps/blend-monitor/src/frontend/lib/firebase.ts rename to apps/blend-studio/src/frontend/lib/firebase.ts diff --git a/apps/blend-monitor/src/shared/types/index.ts b/apps/blend-studio/src/shared/types/index.ts similarity index 100% rename from apps/blend-monitor/src/shared/types/index.ts rename to apps/blend-studio/src/shared/types/index.ts diff --git a/apps/blend-monitor/tsconfig.json b/apps/blend-studio/tsconfig.json similarity index 100% rename from apps/blend-monitor/tsconfig.json rename to apps/blend-studio/tsconfig.json diff --git a/packages/blend/lib/main.ts b/packages/blend/lib/main.ts index a061a6ef3..8d13fe1e5 100644 --- a/packages/blend/lib/main.ts +++ b/packages/blend/lib/main.ts @@ -55,3 +55,37 @@ export * from './components/ChartsV2' export * from './components/Timeline' export * from './components/AlertV2' export * from './components/PopoverV2' + +// --------------------------------------------------------------------------- +// Token Factory Exports (for Token Studio engine) +// --------------------------------------------------------------------------- +// These functions are the bridge between foundation tokens and component tokens. +// The token engine calls them with a brand-modified FoundationTokenType to produce +// branded component tokens for every V2 component. + +export { getButtonV2Tokens } from './components/ButtonV2/buttonV2.tokens' +export { getAccordionV2Tokens } from './components/AccordionV2/accordionV2.tokens' +export { getAlertV2Tokens } from './components/AlertV2/alertV2.tokens' +export { getAvatarV2Tokens } from './components/AvatarV2/avatarV2.tokens' +export { getBreadcrumbV2Tokens } from './components/BreadcrumbV2/breadcrumbV2.tokens' +export { getChartV2Tokens } from './components/ChartsV2/chartV2.tokens' +export { getCheckboxV2Tokens } from './components/SelectorV2/CheckboxV2/checkboxV2.tokens' +export { getCodeEditorV2Tokens } from './components/CodeEditorV2/codeEditorV2.tokens' +export { getKeyValuePairV2Tokens } from './components/KeyValuePairV2/keyValuePairV2.tokens' +export { getMenuV2Tokens } from './components/MenuV2/menuV2.tokens' +export { getMultiSelectV2Tokens } from './components/MultiSelectV2/multiSelectV2.tokens' +export { getPopoverV2Tokens } from './components/PopoverV2/popoverV2.token' +export { getProgressBarV2Tokens } from './components/ProgressBarV2/progressBarV2.tokens' +export { getRadioV2Tokens } from './components/SelectorV2/RadioV2/radioV2.tokens' +export { getSingleSelectV2Tokens } from './components/SingleSelectV2/singleSelectV2.tokens' +export { getSwitchV2Tokens } from './components/SelectorV2/SwitchV2/switchV2.tokens' +export { getSnackbarV2Tokens } from './components/SnackbarV2/snackbarV2.tokens' +export { getStatCardV2Tokens } from './components/StatCardV2/statcardV2.tokens' +export { getTabsV2Tokens } from './components/TabsV2/tabsV2.tokens' +export { getTagV2Tokens } from './components/TagV2/tagV2.tokens' +export { getTextInputV2Tokens } from './components/InputsV2/TextInputV2/TextInputV2.tokens' +export { getTimelineTokens } from './components/Timeline/timeline.token' +export { getTooltipV2Tokens } from './components/TooltipV2/tooltipV2.tokens' + +// Foundation token type export (for token engine) +export type { FoundationTokenType } from './tokens/theme.token' diff --git a/packages/blend/package.json b/packages/blend/package.json index 11b8a3ad6..1966675e3 100644 --- a/packages/blend/package.json +++ b/packages/blend/package.json @@ -17,7 +17,8 @@ "import": "./dist/main.js", "types": "./dist/main.d.ts" }, - "./style.css": "./dist/style.css" + "./style.css": "./dist/style.css", + "./lib/*": "./lib/*" }, "scripts": { "lint": "eslint .", diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 000000000..b1ccc998b --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,38 @@ +{ + "name": "blend-token-studio", + "version": "0.1.0", + "description": "CLI for Blend Token Studio — scaffold, brand, and sync design tokens", + "type": "module", + "bin": { + "blend-token-studio": "./dist/index.js" + }, + "main": "./dist/index.js", + "scripts": { + "build": "tsup src/index.ts --format esm --target node18 --clean --shims", + "dev": "tsup src/index.ts --format esm --target node18 --watch", + "typecheck": "tsc --noEmit", + "start": "node dist/index.js" + }, + "dependencies": { + "@blend-design/token-engine": "workspace:*", + "@juspay/blend-design-system": "workspace:*", + "commander": "^12.0.0", + "ora": "^8.0.0", + "prompts": "^2.4.2", + "picocolors": "^1.1.0" + }, + "devDependencies": { + "@types/prompts": "^2.4.9", + "typescript": "~5.8.3", + "tsup": "^8.0.0" + }, + "engines": { + "node": ">=18" + }, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/packages/cli/src/commands/brand.ts b/packages/cli/src/commands/brand.ts new file mode 100644 index 000000000..6fa23a59b --- /dev/null +++ b/packages/cli/src/commands/brand.ts @@ -0,0 +1,202 @@ +/** + * brand command + * + * Apply a brand to the project — either from a preset, a hex color, + * or interactive prompts. + * + * Usage: + * blend-token-studio brand # interactive + * blend-token-studio brand --preset hdfc # preset + * blend-token-studio brand --primary "#E31837" # custom color + * blend-token-studio brand --primary "#E31837" --radius sharp + */ + +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import prompts from 'prompts' +import ora from 'ora' +import { logger } from '../utils/logger' +import { + resolveBrandTokens, + generateColorScale, + getPreset, + listPresets, + RADIUS_PRESETS, + type BrandConfig, + type RadiusPreset, +} from '@blend-design/token-engine' +import { generateBrandTokensCode } from '../generators/tokens-generator' +import type { BlendConfig } from '../generators/config-generator' + +interface BrandOptions { + preset?: string + primary?: string + radius?: string +} + +export async function brandCommand(options: BrandOptions = {}): Promise { + const cwd = process.cwd() + + // Read blend.config.json + const configPath = join(cwd, 'blend.config.json') + if (!existsSync(configPath)) { + logger.error( + 'blend.config.json not found. Run `npx blend-token-studio init` first.' + ) + return + } + + const config: BlendConfig = JSON.parse(readFileSync(configPath, 'utf-8')) + + logger.header('Blend Token Studio — Brand') + + let brandConfig: BrandConfig + + if (options.preset) { + // Preset mode + const preset = getPreset(options.preset) + if (!preset) { + logger.error( + `Unknown preset "${options.preset}". Available: ${listPresets() + .map((p) => p.name) + .join(', ')}` + ) + return + } + brandConfig = { ...preset } + logger.success(`Using preset: ${brandConfig.name}`) + } else if (options.primary) { + // Custom color mode + const scale = generateColorScale(options.primary) + const radiusPreset = (options.radius as RadiusPreset) || 'default' + const radiusOverrides = RADIUS_PRESETS[radiusPreset] ?? {} + + brandConfig = { + brandId: 'custom/brand', + name: 'Custom Brand', + version: '1.0.0', + colors: { primary: scale }, + radius: + Object.keys(radiusOverrides).length > 0 + ? radiusOverrides + : undefined, + } + logger.success(`Custom brand from ${options.primary}`) + } else { + // Interactive mode + const result = await runInteractivePrompt() + if (!result) return + brandConfig = result + } + + // Resolve tokens + const spinner = ora('Resolving tokens for all V2 components...').start() + + const lightTokens = resolveBrandTokens(brandConfig, 'light') + const darkTokens = resolveBrandTokens(brandConfig, 'dark') + + spinner.succeed('Tokens resolved') + + // Write files + const outputDir = join(cwd, config.output) + + const tokensPath = join(outputDir, 'tokens.ts') + writeFileSync( + tokensPath, + generateBrandTokensCode( + brandConfig, + lightTokens as unknown as Record, + darkTokens as unknown as Record + ) + ) + logger.fileWritten(`${config.output}/tokens.ts`) + + const brandPath = join(outputDir, 'brand.json') + writeFileSync(brandPath, JSON.stringify(brandConfig, null, 4) + '\n') + logger.fileWritten(`${config.output}/brand.json`) + + // Update blend.config.json + config.brand = brandConfig.brandId + writeFileSync(configPath, JSON.stringify(config, null, 4) + '\n') + + logger.newline() + logger.success(`Brand "${brandConfig.name}" applied!`) + logger.detail(`Commit ${config.output}/brand.json to version control.`) + logger.newline() +} + +// --------------------------------------------------------------------------- +// Interactive prompt +// --------------------------------------------------------------------------- + +async function runInteractivePrompt(): Promise { + const presets = listPresets() + + const { choice } = await prompts({ + type: 'select', + name: 'choice', + message: 'Choose a brand preset or customize:', + choices: [ + ...presets.map((p) => ({ + title: p.displayName, + value: p.name, + description: p.brandId, + })), + { title: 'Custom (enter hex color)', value: 'custom' }, + ], + }) + + if (!choice) return null + + if (choice !== 'custom') { + return getPreset(choice) ?? null + } + + // Custom flow + const { primary } = await prompts({ + type: 'text', + name: 'primary', + message: 'Primary brand color (hex):', + initial: '#2B7FFF', + validate: (v: string) => + /^#[0-9A-Fa-f]{6}$/.test(v) || + 'Enter a valid hex color (e.g. #E31837)', + }) + + if (!primary) return null + + const { radius } = await prompts({ + type: 'select', + name: 'radius', + message: 'Border radius style:', + choices: [ + { title: 'Default (10px)', value: 'default' }, + { title: 'Sharp (4px)', value: 'sharp' }, + { title: 'Rounded (20px)', value: 'rounded' }, + { title: 'Pill (full)', value: 'pill' }, + ], + }) + + if (!radius) return null + + const { name } = await prompts({ + type: 'text', + name: 'name', + message: 'Brand name:', + initial: 'My Brand', + }) + + const scale = generateColorScale(primary) + const radiusOverrides = RADIUS_PRESETS[radius as RadiusPreset] ?? {} + + return { + brandId: `custom/${name?.toLowerCase().replace(/\s+/g, '-') ?? 'brand'}`, + name: name ?? 'My Brand', + version: '1.0.0', + colors: { primary: scale }, + radius: + Object.keys(radiusOverrides).length > 0 + ? radiusOverrides + : undefined, + } +} diff --git a/packages/cli/src/commands/diff.ts b/packages/cli/src/commands/diff.ts new file mode 100644 index 000000000..aec859b69 --- /dev/null +++ b/packages/cli/src/commands/diff.ts @@ -0,0 +1,69 @@ +/** + * diff command + * + * Show what the current brand config overrides from Blend defaults. + * + * Usage: + * blend-token-studio diff + */ + +import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' +import pc from 'picocolors' +import { logger } from '../utils/logger' +import { + diffBrandConfigs, + PRESET_BLEND_DEFAULT, + type BrandConfig, +} from '@blend-design/token-engine' + +export async function diffCommand(): Promise { + const cwd = process.cwd() + + // Find brand.json + const configPath = join(cwd, 'blend.config.json') + if (!existsSync(configPath)) { + logger.error( + 'blend.config.json not found. Run `npx blend-token-studio init` first.' + ) + return + } + + const config = JSON.parse(readFileSync(configPath, 'utf-8')) + const brandPath = join(cwd, config.output, 'brand.json') + + if (!existsSync(brandPath)) { + logger.info( + 'No brand.json found — using Blend defaults (no overrides).' + ) + return + } + + const brandConfig: BrandConfig = JSON.parse( + readFileSync(brandPath, 'utf-8') + ) + + logger.header(`Diff: ${brandConfig.name} vs Blend Defaults`) + + const diffs = diffBrandConfigs(PRESET_BLEND_DEFAULT, brandConfig) + + if (diffs.length === 0) { + logger.success('No overrides — using Blend defaults.') + return + } + + logger.newline() + + for (const diff of diffs) { + const path = pc.bold(diff.path.padEnd(28)) + const oldVal = pc.red(diff.oldValue.padEnd(16)) + const newVal = pc.green(diff.newValue) + console.log(` ${path} ${oldVal} → ${newVal}`) + } + + logger.newline() + logger.info( + `${diffs.length} override${diffs.length === 1 ? '' : 's'} from Blend defaults.` + ) + logger.newline() +} diff --git a/packages/cli/src/commands/generate.ts b/packages/cli/src/commands/generate.ts new file mode 100644 index 000000000..4e96e0eec --- /dev/null +++ b/packages/cli/src/commands/generate.ts @@ -0,0 +1,91 @@ +/** + * generate command + * + * Offline token generation — takes a local brand.json file + * and produces the tokens.ts output without connecting to the API. + * + * Usage: + * blend-token-studio generate ./my-brand.json --output ./src/blend + */ + +import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import ora from 'ora' +import { logger } from '../utils/logger' +import { + resolveBrandTokens, + validateBrandConfig, + type BrandConfig, +} from '@blend-design/token-engine' +import { generateBrandTokensCode } from '../generators/tokens-generator' + +interface GenerateOptions { + output?: string +} + +export async function generateCommand( + inputPath: string, + options: GenerateOptions = {} +): Promise { + const cwd = process.cwd() + const fullPath = resolve(cwd, inputPath) + + logger.header('Blend Token Studio — Generate') + + // 1. Read input file + if (!existsSync(fullPath)) { + logger.error(`File not found: ${inputPath}`) + return + } + + let brandConfig: BrandConfig + try { + brandConfig = JSON.parse(readFileSync(fullPath, 'utf-8')) + } catch { + logger.error(`Failed to parse ${inputPath} as JSON.`) + return + } + + // 2. Validate + const validation = validateBrandConfig(brandConfig) + if (!validation.valid) { + logger.error('Invalid brand config:') + for (const err of validation.errors) { + logger.detail(`${err.path}: ${err.message}`) + } + return + } + + logger.success(`Brand: ${brandConfig.name} (${brandConfig.brandId})`) + + // 3. Resolve + const spinner = ora('Resolving tokens...').start() + + const lightTokens = resolveBrandTokens(brandConfig, 'light') + const darkTokens = resolveBrandTokens(brandConfig, 'dark') + + spinner.succeed('Tokens resolved') + + // 4. Write output + const outputDir = resolve(cwd, options.output ?? 'src/blend') + mkdirSync(outputDir, { recursive: true }) + + const tokensPath = join(outputDir, 'tokens.ts') + writeFileSync( + tokensPath, + generateBrandTokensCode( + brandConfig, + lightTokens as unknown as Record, + darkTokens as unknown as Record + ) + ) + logger.fileWritten(tokensPath) + + const brandPath = join(outputDir, 'brand.json') + writeFileSync(brandPath, JSON.stringify(brandConfig, null, 4) + '\n') + logger.fileWritten(brandPath) + + logger.newline() + logger.success('Done!') + logger.newline() +} diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts new file mode 100644 index 000000000..cbd17526d --- /dev/null +++ b/packages/cli/src/commands/init.ts @@ -0,0 +1,121 @@ +/** + * init command + * + * Scaffolds a Blend Token Studio project: + * 1. Detects project type (Next.js / Vite / CRA) + * 2. Checks & installs missing dependencies + * 3. Creates blend.config.json + * 4. Creates src/blend/provider.tsx + * 5. Creates src/blend/tokens.ts + * + * Inspired by `npx shadcn@latest init` — one command, zero config. + */ + +import { existsSync, mkdirSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { execSync } from 'node:child_process' +import { logger } from '../utils/logger' +import { detectProject, getInstallCommand } from '../utils/detect-project' +import { generateProviderCode } from '../generators/provider-generator' +import { generateDefaultTokensCode } from '../generators/tokens-generator' +import { + generateConfig, + generateConfigCode, +} from '../generators/config-generator' + +interface InitOptions { + defaults?: boolean + force?: boolean +} + +export async function initCommand(options: InitOptions = {}): Promise { + const cwd = process.cwd() + + logger.header('Blend Token Studio — Init') + + // 1. Detect project + const project = detectProject(cwd) + logger.success(`Detected: ${project.type} (${project.packageManager})`) + + if (project.typescript) { + logger.detail('TypeScript enabled') + } + + // 2. Check for blend.config.json + const configPath = join(cwd, 'blend.config.json') + if (existsSync(configPath) && !options.force) { + logger.warn( + 'blend.config.json already exists. Use --force to overwrite.' + ) + return + } + + // 3. Check & install dependencies + const missingDeps: string[] = [] + if (!project.hasBlend) missingDeps.push('@juspay/blend-design-system') + if (!project.hasStyledComponents) missingDeps.push('styled-components') + + if (missingDeps.length > 0) { + const installCmd = getInstallCommand( + project.packageManager, + missingDeps + ) + logger.info(`Installing: ${missingDeps.join(', ')}`) + logger.detail(installCmd) + + try { + execSync(installCmd, { cwd, stdio: 'pipe' }) + logger.success('Dependencies installed') + } catch { + logger.error( + `Failed to install dependencies. Run manually:\n ${installCmd}` + ) + return + } + } else { + logger.success('Dependencies already installed') + } + + // 4. Create blend.config.json + const config = generateConfig() + writeFileSync(configPath, generateConfigCode(config)) + logger.fileWritten('blend.config.json') + + // 5. Create output directory + const outputDir = join(cwd, config.output) + mkdirSync(outputDir, { recursive: true }) + + // 6. Generate provider.tsx + const providerPath = join(outputDir, 'provider.tsx') + if (!existsSync(providerPath) || options.force) { + const isNextJs = project.type === 'nextjs' + writeFileSync(providerPath, generateProviderCode(isNextJs)) + logger.fileWritten(`${config.output}/provider.tsx`) + } else { + logger.fileSkipped(`${config.output}/provider.tsx`) + } + + // 7. Generate tokens.ts (default — empty, Blend defaults) + const tokensPath = join(outputDir, 'tokens.ts') + writeFileSync(tokensPath, generateDefaultTokensCode()) + logger.fileWritten(`${config.output}/tokens.ts`) + + // 8. Print next steps + logger.newline() + logger.header('Next steps') + logger.newline() + console.log(' 1. Wrap your app with BlendProvider:') + logger.newline() + console.log( + ` import { BlendProvider } from './${config.output.replace('src/', '')}/provider'` + ) + logger.newline() + console.log(' ') + console.log(' ') + console.log(' ') + logger.newline() + console.log(' 2. Apply a brand:') + logger.newline() + console.log(' npx blend-token-studio brand') + logger.newline() +} diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts new file mode 100644 index 000000000..701379aee --- /dev/null +++ b/packages/cli/src/commands/validate.ts @@ -0,0 +1,76 @@ +/** + * validate command + * + * Validate the current brand.json for correctness. + * + * Usage: + * blend-token-studio validate + */ + +import { existsSync, readFileSync } from 'node:fs' +import { join } from 'node:path' +import pc from 'picocolors' +import { logger } from '../utils/logger' +import { validateBrandConfig } from '@blend-design/token-engine' + +export async function validateCommand(): Promise { + const cwd = process.cwd() + + const configPath = join(cwd, 'blend.config.json') + if (!existsSync(configPath)) { + logger.error( + 'blend.config.json not found. Run `npx blend-token-studio init` first.' + ) + return + } + + const config = JSON.parse(readFileSync(configPath, 'utf-8')) + const brandPath = join(cwd, config.output, 'brand.json') + + if (!existsSync(brandPath)) { + logger.info('No brand.json found — nothing to validate.') + return + } + + logger.header('Validate brand.json') + + let brandConfig: unknown + try { + brandConfig = JSON.parse(readFileSync(brandPath, 'utf-8')) + } catch { + logger.error('brand.json is not valid JSON.') + return + } + + const result = validateBrandConfig(brandConfig) + + if (result.errors.length > 0) { + logger.newline() + for (const error of result.errors) { + const path = error.path ? pc.bold(error.path) + ': ' : '' + console.log(` ${pc.red('✗')} ${path}${error.message}`) + } + } + + if (result.warnings.length > 0) { + logger.newline() + for (const warning of result.warnings) { + const path = warning.path ? pc.bold(warning.path) + ': ' : '' + console.log(` ${pc.yellow('!')} ${path}${warning.message}`) + } + } + + logger.newline() + + if (result.valid) { + logger.success( + `brand.json is valid. ${result.warnings.length} warning${result.warnings.length === 1 ? '' : 's'}.` + ) + } else { + logger.error( + `brand.json has ${result.errors.length} error${result.errors.length === 1 ? '' : 's'}.` + ) + } + + logger.newline() +} diff --git a/packages/cli/src/generators/config-generator.ts b/packages/cli/src/generators/config-generator.ts new file mode 100644 index 000000000..0afdbfa2c --- /dev/null +++ b/packages/cli/src/generators/config-generator.ts @@ -0,0 +1,25 @@ +/** + * Config Generator + * + * Generates the blend.config.json project configuration file. + */ + +export interface BlendConfig { + $schema: string + brand: string + theme: string + output: string +} + +export function generateConfig(brand: string = 'blend/default'): BlendConfig { + return { + $schema: 'https://studio.blend.juspay.design/schema.json', + brand, + theme: 'light', + output: 'src/blend', + } +} + +export function generateConfigCode(config: BlendConfig): string { + return JSON.stringify(config, null, 4) + '\n' +} diff --git a/packages/cli/src/generators/provider-generator.ts b/packages/cli/src/generators/provider-generator.ts new file mode 100644 index 000000000..ce189cbc8 --- /dev/null +++ b/packages/cli/src/generators/provider-generator.ts @@ -0,0 +1,38 @@ +/** + * Provider Generator + * + * Generates the BlendProvider wrapper component that consumers + * put in their app. This file is safe to edit — the CLI won't + * overwrite it once created. + */ + +export function generateProviderCode(isNextJs: boolean): string { + const useClientDirective = isNextJs ? "'use client'\n\n" : '' + + return `${useClientDirective}/** + * BlendProvider — wraps your app with the Blend Design System theme. + * + * Generated by Blend Token Studio. Safe to edit. + * Docs: https://blend.juspay.design/docs/getting-started + */ + +import { ThemeProvider, Theme } from '@juspay/blend-design-system' +import '@juspay/blend-design-system/style.css' +import { componentTokens, darkComponentTokens } from './tokens' + +type BlendProviderProps = { + children: React.ReactNode + theme?: 'light' | 'dark' +} + +export function BlendProvider({ children, theme = 'light' }: BlendProviderProps) { + const tokens = theme === 'dark' ? darkComponentTokens : componentTokens + + return ( + + {children} + + ) +} +` +} diff --git a/packages/cli/src/generators/tokens-generator.ts b/packages/cli/src/generators/tokens-generator.ts new file mode 100644 index 000000000..56fa25735 --- /dev/null +++ b/packages/cli/src/generators/tokens-generator.ts @@ -0,0 +1,74 @@ +/** + * Tokens Generator + * + * Generates the tokens.ts file that contains pre-resolved + * component tokens for a brand config. + * + * This file is auto-generated and should NOT be edited manually. + * Re-run `blend-token-studio brand` or `blend-token-studio pull` to update. + */ + +import type { BrandConfig } from '@blend-design/token-engine' + +/** + * Generate the tokens.ts file content. + * + * For the default (no-brand) case, exports empty objects so ThemeProvider + * falls back to Blend defaults. + */ +export function generateDefaultTokensCode(): string { + return `/** + * Blend Design System — Component Tokens + * + * Auto-generated by Blend Token Studio. + * DO NOT EDIT — run \`npx blend-token-studio brand\` to customize. + * + * Brand: Blend Default + */ + +import type { ComponentTokenType } from '@juspay/blend-design-system' + +/** Light theme tokens (empty = Blend defaults) */ +export const componentTokens: ComponentTokenType = {} + +/** Dark theme tokens (empty = Blend defaults) */ +export const darkComponentTokens: ComponentTokenType = {} +` +} + +/** + * Generate the tokens.ts file with pre-resolved brand tokens. + * + * The resolved tokens are serialized as a JSON literal embedded + * in the TypeScript file. This means no runtime resolution cost — + * tokens are statically available at import time. + */ +export function generateBrandTokensCode( + brandConfig: BrandConfig, + lightTokens: Record, + darkTokens: Record +): string { + const timestamp = new Date().toISOString() + const lightJson = JSON.stringify(lightTokens, null, 4) + const darkJson = JSON.stringify(darkTokens, null, 4) + + return `/** + * Blend Design System — Component Tokens + * + * Auto-generated by Blend Token Studio. + * DO NOT EDIT — run \`npx blend-token-studio brand\` to regenerate. + * + * Brand: ${brandConfig.name} (${brandConfig.brandId}) + * Version: ${brandConfig.version} + * Generated: ${timestamp} + */ + +import type { ComponentTokenType } from '@juspay/blend-design-system' + +/** Light theme tokens for ${brandConfig.name} */ +export const componentTokens: ComponentTokenType = ${lightJson} as ComponentTokenType + +/** Dark theme tokens for ${brandConfig.name} */ +export const darkComponentTokens: ComponentTokenType = ${darkJson} as ComponentTokenType +` +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 000000000..87f3c470a --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,104 @@ +#!/usr/bin/env node + +/** + * blend-token-studio CLI + * + * The developer-facing tool for Blend Token Studio. + * Inspired by shadcn/ui CLI — one command to scaffold, + * one command to brand. + * + * Usage: + * npx blend-token-studio init # scaffold project + * npx blend-token-studio brand # interactive branding + * npx blend-token-studio brand --preset hdfc # use a preset + * npx blend-token-studio diff # see overrides + * npx blend-token-studio validate # validate brand.json + * npx blend-token-studio generate ./brand.json # offline generation + */ + +import { Command } from 'commander' +import { initCommand } from './commands/init' +import { brandCommand } from './commands/brand' +import { diffCommand } from './commands/diff' +import { validateCommand } from './commands/validate' +import { generateCommand } from './commands/generate' + +const program = new Command() + +program + .name('blend-token-studio') + .description('Blend Token Studio — scaffold, brand, and sync design tokens') + .version('0.1.0') + +// --------------------------------------------------------------------------- +// init — scaffold a new project +// --------------------------------------------------------------------------- + +program + .command('init') + .description('Scaffold Blend Token Studio in your project') + .option('-d, --defaults', 'Skip prompts, use defaults') + .option('-f, --force', 'Overwrite existing files') + .action(async (options) => { + await initCommand(options) + }) + +// --------------------------------------------------------------------------- +// brand — apply a brand +// --------------------------------------------------------------------------- + +program + .command('brand') + .description('Apply a brand to your project') + .option( + '-p, --preset ', + 'Use a built-in preset (blend, hdfc, neobank, fintech)' + ) + .option('--primary ', 'Primary brand color (hex)') + .option( + '--radius