diff --git a/apps/web/package.json b/apps/web/package.json index 191906c..a62e343 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,6 +19,7 @@ }, "devDependencies": { "@forgekit/typescript-config": "workspace:*", + "@tailwindcss/vite": "^4.3.3", "@tanstack/router-cli": "^1.167.19", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", @@ -26,6 +27,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.3", "jsdom": "^29.1.1", + "tailwindcss": "^4.3.3", "vite": "^8.1.5" } } diff --git a/apps/web/src/__tests__/home.test.tsx b/apps/web/src/__tests__/home.test.tsx index 312f934..10132c8 100644 --- a/apps/web/src/__tests__/home.test.tsx +++ b/apps/web/src/__tests__/home.test.tsx @@ -7,10 +7,11 @@ import { describe, expect, it } from "vitest"; import { HomePage } from "../components/home-page"; describe("HomePage", () => { - it("renders the placeholder and UI package version", () => { + it("renders the placeholder, UI action, and package version", () => { render(); expect(screen.getByText("ForgeKit web is running.")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Get started" })).toBeInTheDocument(); expect(screen.getByText(`UI package version: ${UI_VERSION}`)).toBeInTheDocument(); }); }); diff --git a/apps/web/src/components/home-page.tsx b/apps/web/src/components/home-page.tsx index 4f0da0c..d80eb50 100644 --- a/apps/web/src/components/home-page.tsx +++ b/apps/web/src/components/home-page.tsx @@ -1,6 +1,6 @@ // Defines the placeholder home page component. -import { UI_VERSION } from "@forgekit/ui"; +import { Button, UI_VERSION } from "@forgekit/ui"; import type React from "react"; @@ -9,8 +9,9 @@ import type React from "react"; */ export function HomePage(): React.JSX.Element { return ( -
+

ForgeKit web is running.

+

UI package version: {UI_VERSION}

); diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 4d47b69..2087974 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -2,6 +2,8 @@ import { Outlet, createRootRoute } from "@tanstack/react-router"; +import "../styles.css"; + import type React from "react"; import { Document } from "../components/document"; diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css new file mode 100644 index 0000000..dc5e310 --- /dev/null +++ b/apps/web/src/styles.css @@ -0,0 +1,6 @@ +/* App stylesheet: Tailwind base plus the ForgeKit theme, scanning the compiled UI package. */ + +@import "tailwindcss"; +@import "@forgekit/ui/theme.css"; + +@source "../node_modules/@forgekit/ui/dist"; diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts new file mode 100644 index 0000000..e4b6b42 --- /dev/null +++ b/apps/web/src/vite-env.d.ts @@ -0,0 +1,2 @@ +// Ambient Vite client types: import.meta.env, and asset and CSS module imports. +/// diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index cbf2023..8143106 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,6 +1,7 @@ // Configures Vite and TanStack Start for the web app. import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import tailwindcss from "@tailwindcss/vite"; import viteReact from "@vitejs/plugin-react"; import { defineConfig } from "vite"; @@ -11,6 +12,7 @@ const config = defineConfig({ plugins: [ // Start injects the framework entries that the React plugin then compiles. tanstackStart(), + tailwindcss(), viteReact() ] }); diff --git a/docs/ui/component-library.md b/docs/ui/component-library.md index 0672461..5ab151c 100644 --- a/docs/ui/component-library.md +++ b/docs/ui/component-library.md @@ -22,10 +22,10 @@ An app stylesheet must import Tailwind before the ForgeKit theme and source the @import "tailwindcss"; @import "@forgekit/ui/theme.css"; -@source "../../../packages/ui/dist"; +@source "../node_modules/@forgekit/ui/dist"; ``` -The `@source` path is application-stylesheet-relative. The example fits a stylesheet under `apps/web/src`; use the equivalent path to the compiled `@forgekit/ui/dist` in another app. This wiring belongs to the later app integration chunk. +The `@source` path is application-stylesheet-relative. The example fits a stylesheet under `apps/web/src`; use the equivalent path to the compiled `@forgekit/ui/dist` in another app. This wiring belongs in each application stylesheet that consumes UI components. `src/theme.css` is the source of truth for semantic tokens and radii. It defines light values on `:root` and dark values under `.dark`. A `.dark` ancestor flips the tokens for all descendants. The later `ThemeProvider` applies or removes that class on ``. diff --git a/docs/web/app-skeleton.md b/docs/web/app-skeleton.md index bdd212f..1453fb6 100644 --- a/docs/web/app-skeleton.md +++ b/docs/web/app-skeleton.md @@ -2,7 +2,7 @@ `apps/web` is a TanStack Start React app. It server-renders the placeholder home page and hydrates the same route tree on the client. -This is the booting frontend shell. Later feature chunks attach UI, a typed API client, theming, and auth to the same app boundary. +This is the booting frontend shell. Later feature chunks attach a typed API client, a `ThemeProvider`, and auth to the same app boundary. ## Routing and the document shell @@ -24,6 +24,16 @@ tsr generate && tsc -p tsconfig.typecheck.json --noEmit A clean checkout rebuilds the generated route tree during build or before type checking. No separate manual step is needed for those gates. +## Styling + +`apps/web/vite.config.ts` compiles Tailwind v4 through the first-party `@tailwindcss/vite` plugin. `apps/web/src/styles.css` imports `tailwindcss` first, then `@forgekit/ui/theme.css`, so the application receives Tailwind utilities and the ForgeKit semantic theme in one global stylesheet. The stylesheet enters the app through a side-effect import in `apps/web/src/routes/__root.tsx`; TanStack Start injects it during server rendering and hydration, so every route receives it. + +The stylesheet includes `@source "../node_modules/@forgekit/ui/dist"`. Tailwind does not scan `node_modules` by default, so this points at pnpm's symlinked compiled UI package and generates the utility classes used inside UI components. + +The `.dark` class flips the semantic theme tokens for its descendants. A later chunk adds the `ThemeProvider` that toggles that class on the document root. + +The UI package must be built before the app can import it and before `@source` can scan its component output, so every task that consumes UI depends on Turborepo's `^build`, including `dev`. `turbo dev` therefore builds UI once before starting the watchers, then runs the UI package's `tsc -p tsconfig.json --watch --preserveWatchOutput` script so its `dist` stays current while the web development server runs. + ## Gates for a Vite app `apps/web/package.json` defines the web workspace gates: @@ -47,7 +57,7 @@ A clean checkout rebuilds the generated route tree during build or before type c ## Dependency edge -`apps/web/src/components/home-page.tsx` imports `UI_VERSION` from `@forgekit/ui` and renders it in the placeholder. The `@forgekit/web` package declares `@forgekit/ui` as a workspace dependency, and `@forgekit/dependency-flow` allows that as the only internal runtime edge from web. Because the import is compiled into the app, Turborepo's `^build` dependency ordering has a real package edge to follow. +`apps/web/src/components/home-page.tsx` imports `Button` and `UI_VERSION` from `@forgekit/ui` and renders them in the placeholder. The `@forgekit/web` package declares `@forgekit/ui` as a workspace dependency, and `@forgekit/dependency-flow` allows that as the only internal runtime edge from web. Because the import is compiled into the app, Turborepo's `^build` dependency ordering has a real package edge to follow. ## Running it diff --git a/packages/ui/package.json b/packages/ui/package.json index 1baa8ab..39a02b6 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -16,6 +16,7 @@ "sideEffects": ["./src/theme.css"], "scripts": { "build": "tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", "lint": "eslint . --max-warnings 0", "typecheck": "tsc -p tsconfig.typecheck.json --noEmit", "test": "vitest run" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fbe0eac..0d67a72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,6 +88,9 @@ importers: '@forgekit/typescript-config': specifier: workspace:* version: link:../../tooling/typescript-config + '@tailwindcss/vite': + specifier: ^4.3.3 + version: 4.3.3(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)) '@tanstack/router-cli': specifier: ^1.167.19 version: 1.167.21 @@ -109,6 +112,9 @@ importers: jsdom: specifier: ^29.1.1 version: 29.1.1 + tailwindcss: + specifier: ^4.3.3 + version: 4.3.3 vite: specifier: ^8.1.5 version: 8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0) @@ -1439,6 +1445,100 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tanstack/history@1.162.0': resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==} engines: {node: '>=20.19'} @@ -2045,6 +2145,10 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + enhanced-resolve@5.24.2: + resolution: {integrity: sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==} + engines: {node: '>=10.13.0'} + entities@8.0.0: resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} engines: {node: '>=20.19.0'} @@ -2187,6 +2291,9 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + h3@2.0.1-rc.20: resolution: {integrity: sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==} engines: {node: '>=20.11.1'} @@ -2689,6 +2796,13 @@ packages: tailwind-merge@3.6.0: resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3897,6 +4011,74 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.2 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.1.5(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0) + '@tanstack/history@1.162.0': {} '@tanstack/react-router@1.170.17(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': @@ -4524,6 +4706,11 @@ snapshots: emoji-regex@8.0.0: {} + enhanced-resolve@5.24.2: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + entities@8.0.0: {} es-module-lexer@2.3.0: {} @@ -4712,6 +4899,8 @@ snapshots: dependencies: is-glob: 4.0.3 + graceful-fs@4.2.11: {} + h3@2.0.1-rc.20: dependencies: rou3: 0.8.1 @@ -5154,6 +5343,10 @@ snapshots: tailwind-merge@3.6.0: {} + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + tinybench@2.9.0: {} tinyexec@1.2.4: {} diff --git a/turbo.json b/turbo.json index 16a5b07..0c849b3 100644 --- a/turbo.json +++ b/turbo.json @@ -2,6 +2,7 @@ "$schema": "https://turbo.build/schema.json", "tasks": { "dev": { + "dependsOn": ["^build"], "cache": false, "persistent": true },