ui-next poc

This commit is contained in:
lovebird 2026-04-09 13:27:16 +02:00
parent 6dad7aafbf
commit 3e3efbe4d4
18 changed files with 3057 additions and 0 deletions

35
packages/ui-next/.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# Deno
.deno
deno.lock
deno.json._decorators
# Dependencies
node_modules/
# Build output
build/
dist/
out/
# Environment files
.env*
!src/.env/
!src/.env/*md
# Generated files
.dts
types/
.D_Store
.vscode/!settings.json
# Logs
*.Log
*.Log.*
docs-internal
systems/code-server-defaults
systems/workspace/kbot-docs
systems/.code-server/code-server-ipc.sock
systems/.code-server/User/workspaceStorage/
systems/code-server-defaults
systems/.code-server
tests/assets/
packages/kbot/systems/gptr/gpt-researcher

View File

@ -0,0 +1,48 @@
# Ignore node_modules directory
node_modules/
# Ignore log files
*.log
# Ignore temporary files
*.tmp
# Ignore coverage reports
coverage/
.kbot
docs
docs_
.env
report
.vscode
config
systems
tools.json
commit.json
docker.sh
package-lock.json
scripts
todos.md
tests
tmp
dist/node_modules
dist/data
dist/.kbot
dist/package-lock.json
# Logs
*.Log
*.Log.*
docs
docs-internal
systems/code-server-defaults
kbot-extensions
systems/workspace/kbot-docs
systems/.code-server/code-server-ipc.sock
systems/.code-server/User/workspaceStorage/
systems/code-server-defaults
systems/.code-server
kbot-tests
kbot-extensions

View File

@ -0,0 +1,69 @@
# `@polymech/ui-next`
A focused **proof-of-concept** for the next Polymech web shell: **React 18**, **TypeScript**, **native ESM**, and **TanStack Router** with patterns we intend to use when migrating away from React Router v6.
This package is a sandbox, not a published library. It exists so we can **try routing, typing, and UX behaviors in isolation** before rolling them into production apps such as `pm-pics` or shared bundles like `@polymech/ecommerce`.
---
## Why this stack
| Capability | What we gain |
|------------|----------------|
| **Static route tree + typed links** | Invalid routes and broken `to` values surface at compile time instead of in QA. |
| **`validateSearch` + Zod** | Query strings for tabs, filters, and view modes are parsed and typed once on the route. |
| **Loaders** | Data can load with the route, reducing `useEffect` + client fetch waterfalls on big pages. |
| **Scroll restoration** | First-class support via `createRouter({ scrollRestoration: true })`, with demos that prove behavior—including **lazy-split** route chunks. |
| **Router subscriptions** | `router.subscribe('onResolved', …)` gives a single place for analytics and instrumentation. |
The detailed mapping from React Router habits to TanStack APIs lives in [`src/examples/migration/MIGRATION_EXAMPLES.md`](src/examples/migration/MIGRATION_EXAMPLES.md).
---
## Quick start
This workspace uses **npm** for this package (no pnpm required here).
```bash
cd packages/ui-next
npm install
npm run dev
```
Open the app and use the header or **`/examples`** to walk through interactive migration examples (dynamic params, splat, typed search, loaders, redirects, 404, scroll proofs, lazy routes).
```bash
npm run build # tsc --noEmit && vite build
npm run preview # production build preview
```
---
## Project layout
| Path | Purpose |
|------|---------|
| [`src/router.tsx`](src/router.tsx) | Root layout, `createRouter`, scroll restoration, global 404 |
| [`src/main.tsx`](src/main.tsx) | `RouterProvider`, dev-only `router.subscribe` stub for analytics |
| [`src/examples/migration/`](src/examples/migration/) | Runnable demos + [`MIGRATION_EXAMPLES.md`](src/examples/migration/MIGRATION_EXAMPLES.md) |
| [`vite.config.ts`](vite.config.ts) | Vite + React; comment shows where `TanStackRouterVite` plugs in for file-based routes later |
| [`tsconfig.json`](tsconfig.json) | Extends `../typescript-config/base.json`, tuned for Vite + `bundler` resolution |
---
## What “compelling” looks like here
- **Scroll restoration**: `/examples/scroll-restoration` uses a live `window.scrollY` HUD. Scroll, navigate away, hit **Back**—position is restored, not reset to zero.
- **Lazy route + same restoration**: `/examples/lazy-scroll-restoration` loads UI from a **separate JS chunk** (`lazyRouteComponent` + dynamic `import`). Production builds list a dedicated `LazyScrollRestorationChunk-*.js` asset; scroll behavior matches the non-lazy demo.
---
## Relationship to the monorepo
`@polymech/ui-next` is a **`packages/*`** workspace member. Host applications may stay on their current router until migration phases are scheduled; this package does not replace them—it **informs** the migration plan with working code and checklists.
---
## License
Private to the Polymech monorepo; not published to npm.

View File

@ -0,0 +1,11 @@
{
"folders": [
{
"path": "."
},
{
"path": "../ui"
}
],
"settings": {}
}

View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ui-next POC</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

1897
packages/ui-next/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
{
"name": "@polymech/ui-next",
"private": true,
"version": "0.0.0",
"packageManager": "npm@10.9.2",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && vite build",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-router": "^1.114.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/node": "^22.10.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.6.3",
"vite": "^5.4.11"
}
}

View File

@ -0,0 +1,72 @@
# React Router v6 → TanStack Router: mapping for Polymech
This folder is a **runnable** slice of the migration plan. It shows *why* TanStack Router helps Polymech: **typed routes**, **loaders instead of effect waterfalls**, **first-class search validation**, **scroll restoration**, and **router-level subscriptions** for analytics.
## Why switch (tied to product goals)
| Goal | TanStack Router lever | Where it lives here |
|------|------------------------|---------------------|
| Fewer broken links at compile time | Route tree + typed `Link`/`navigate` | `src/router.tsx`, `routeTree.tsx` |
| Replace ad-hoc URL parsing | `validateSearch` + Zod | `searchSchema.ts`, `/examples/search` |
| Fewer data waterfalls | `loader` + `useLoaderData` | `/examples/loader-demo` |
| Replace custom scroll restoration | `createRouter({ scrollRestoration: true })` | `src/router.tsx` + proof: `/examples/scroll-restoration` and lazy + scroll: `/examples/lazy-scroll-restoration` |
| Centralize analytics on navigation | `router.subscribe('onResolved', …)` | `src/main.tsx` |
| Global 404 | `defaultNotFoundComponent` | `src/router.tsx` |
| Deep links / nested apps | Nested layout + splat `_splat` | `/examples/categories/...` |
## Phase checklist → code
### Phase 1: Setup and basic configuration
- **Dependencies**: `package.json` lists `@tanstack/react-router` and `zod`. In the host app, also add `@tanstack/router-vite-plugin` when you move to file-based routes under `src/routes/`.
- **Vite**: see the comment in `vite.config.ts` for `TanStackRouterVite` (plugin is optional while this POC uses a **static** route tree).
- **Root layout**: global shell is the root route component in `src/router.tsx` (analogous to moving `AppWrapper` into `__root.tsx`).
- **Router init**: `createRouter` + `RouterProvider``src/router.tsx` + `src/main.tsx`.
### Phase 2: Route declaration and mapping
| React Router v6 idea | TanStack pattern in this POC |
|----------------------|------------------------------|
| `/post/:id` | `/examples/post/$postId` |
| `/user/:userId` | `/examples/user/$userId` |
| `/tags/:tag` | `/examples/tags/$tag` |
| `/collections/:userId/:slug` | `/examples/collections/$userId/$slug` |
| Wildcard / nested app | `/examples/categories/$``_splat` in `Route.useParams()` |
| Not found (global) | `defaultNotFoundComponent` in `createRouter` — try link “trigger 404” |
### Phase 3: Component refactoring (hooks)
| React Router | TanStack (this POC) |
|--------------|---------------------|
| `useParams()` | `myRoute.useParams()` on the route object |
| `useNavigate()` | `useNavigate()` from `@tanstack/react-router` |
| `useLocation()` | `useLocation()` + `useRouterState` |
| `useSearchParams` | `validateSearch` + `Route.useSearch()` |
| `<Navigate />` | `<Navigate />` from `@tanstack/react-router` or `redirect()` in `beforeLoad` |
### Phase 4: Enhancements
- **Loaders**: `/examples/loader-demo`.
- **Scroll restoration** (prove it yourself):
1. Open **`/examples/scroll-restoration`**. A fixed HUD shows live **`window.scrollY`**.
2. Scroll well past the first panel (e.g. until **Section 25** is on screen) and note the pixel value.
3. Navigate away with an in-app link (e.g. **loader demo** or **Home** in the header).
4. Use the browser **Back** button (not a fresh reload). The HUD should return to about the **same `scrollY`**, not `0` — that is TanStacks scroll cache with `scrollRestoration: true` (the browsers native position is overridden via `history.scrollRestoration = 'manual'` inside the router).
5. **Contrast**: a full page reload on this URL always starts at the top; restoration is tied to **history navigation** for the same entries, which is what you want when replacing a custom `ScrollRestoration` tied to route changes.
- **Lazy route + same scroll behavior**: **`/examples/lazy-scroll-restoration`** uses `lazyRouteComponent(() => import('…/LazyScrollRestorationChunk'))` with `pendingComponent` while the chunk loads. The page content is a **separate Vite chunk** (verify in Network). Repeat the scroll → navigate away → **Back** test: restoration still applies; splitting the route module does not turn off scroll caching.
- **Analytics**: `router.subscribe('onResolved', …)` in `src/main.tsx` (dev-only `console.debug` — swap for your analytics client).
## `@polymech/ecommerce` / standalone bundles
Host apps should inject navigation via props or context, or migrate the bundle to TanStack Router APIs so `useLocation` / `matchPath` / `Navigate` are not hard-coded to `react-router-dom`. This POC does not include that package; use the hook table above when refactoring `EcommerceBundle.tsx`.
## File map
| File | Role |
|------|------|
| `src/router.tsx` | Root route, app shell, `createRouter`, `scrollRestoration`, `defaultNotFoundComponent` |
| `src/main.tsx` | `RouterProvider`, `router.subscribe` analytics stub |
| `src/examples/migration/routeTree.tsx` | All migration demos under `/examples/*` |
| `src/examples/migration/ScrollRestorationDemo.tsx` | Long page + HUD for scroll restoration verification |
| `src/examples/migration/lazy/LazyScrollRestorationChunk.tsx` | Default export loaded via `lazyRouteComponent` + dynamic `import()` (code-split) |
| `src/examples/migration/searchSchema.ts` | Zod search schema shared with `/examples/search` |

View File

@ -0,0 +1,71 @@
import { useEffect, useState } from "react";
import { Link } from "@tanstack/react-router";
/**
* Proof harness for `createRouter({ scrollRestoration: true })` in `src/router.tsx`.
* TanStack restores `window` scroll for this route when you leave and return via history.
*/
export function ScrollRestorationDemo() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const onScroll = () => setScrollY(Math.round(window.scrollY));
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
<>
<div className="scroll-hud" aria-live="polite">
<div>
<strong>window.scrollY</strong> = {scrollY}px
</div>
<div className="scroll-hud-hint">
Scroll down, note the value, open another example, then use the browser{" "}
<strong>Back</strong> button the pixel value should return (TanStack scroll
cache + <code>history.scrollRestoration = &apos;manual&apos;</code>).
</div>
</div>
<article className="panel scroll-demo-intro">
<h2>Scroll restoration (evidence)</h2>
<p>
Router option: <code>scrollRestoration: true</code> in{" "}
<code>src/router.tsx</code> (replaces a bespoke <code>ScrollRestoration.tsx</code>{" "}
in many apps).
</p>
<ol className="scroll-demo-steps">
<li>Scroll until <strong>Section 25</strong> is visible and remember the HUD value.</li>
<li>
Click{" "}
<Link to="/examples/loader-demo" className="link">
loader demo
</Link>{" "}
(or Home in the site header).
</li>
<li>
Press the browser <strong>Back</strong> button to return here.
</li>
<li>
The HUD should show the same ballpark <code>scrollY</code> as before not{" "}
<code>0</code>.
</li>
</ol>
<p className="muted">
Forward navigation to a new URL still scrolls to top by default; restoration is
for same-route revisit via history (Back/Forward).
</p>
</article>
<div className="scroll-demo-blocks" aria-hidden>
{Array.from({ length: 48 }, (_, i) => (
<div key={i} className="scroll-demo-block" id={`section-${i + 1}`}>
Section {i + 1}
</div>
))}
</div>
</>
);
}

View File

@ -0,0 +1,63 @@
import { useEffect, useState } from "react";
import { Link } from "@tanstack/react-router";
/**
* Default export is loaded only when the user hits `/examples/lazy-scroll-restoration`
* (`lazyRouteComponent` + dynamic `import()`). Vite emits a separate JS chunk check Network.
* Scroll restoration for this route is unchanged: use Back after visiting another page.
*/
export default function LazyScrollRestorationChunk() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const onScroll = () => setScrollY(Math.round(window.scrollY));
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
<>
<div className="scroll-hud" aria-live="polite">
<div>
<strong>window.scrollY</strong> = {scrollY}px
</div>
<div className="scroll-hud-hint">
Lazy route: scroll, go to{" "}
<Link to="/examples/loader-demo" className="link">
loader demo
</Link>
, then <strong>Back</strong> position should restore like the non-lazy scroll
demo.
</div>
</div>
<article className="panel scroll-demo-intro lazy-chunk-intro">
<h2>Lazy route + scroll restoration</h2>
<p>
This screen is the <strong>default export</strong> of a dynamically imported
module (
<code>
{`lazyRouteComponent(() => import("…/LazyScrollRestorationChunk"))`}
</code>
). It is not in the main bundle; DevTools Network should show a separate script
when you first open this URL.
</p>
<p>
<code>{`createRouter({ scrollRestoration: true })`}</code> in{" "}
<code>src/router.tsx</code> still caches scroll for this entry code splitting
does not disable restoration.
</p>
</article>
<div className="scroll-demo-blocks" aria-hidden>
{Array.from({ length: 40 }, (_, i) => (
<div key={i} className="scroll-demo-block" id={`lazy-section-${i + 1}`}>
Lazy chunk · block {i + 1}
</div>
))}
</div>
</>
);
}

View File

@ -0,0 +1,372 @@
/**
* Runnable patterns for migrating from React Router v6 TanStack Router.
* See `MIGRATION_EXAMPLES.md` in this folder for the phase checklist mapping.
*/
import {
Link,
Navigate,
Outlet,
createRoute,
lazyRouteComponent,
redirect,
useLocation,
useNavigate,
useRouterState,
} from "@tanstack/react-router";
import type { AnyRoute } from "@tanstack/react-router";
import {
migrationSearchSchema,
type MigrationSearch,
} from "@/examples/migration/searchSchema";
import { ScrollRestorationDemo } from "@/examples/migration/ScrollRestorationDemo";
export function buildMigrationExamplesBranch(rootRoute: AnyRoute) {
function ExamplesIndex() {
return (
<article className="panel">
<h2>Index</h2>
<p>
Pick a link above. This section mirrors the Polymech migration phases (setup,
route mapping, hooks, loaders, scroll restoration, analytics).
</p>
</article>
);
}
function ExamplesLayout() {
return (
<section className="nested">
<h1>Migration examples</h1>
<p className="muted">
Static route tree (same ideas as file-based <code>post.$postId.tsx</code> +
Vite plugin).
</p>
<nav className="examples-nav">
<Link to="/examples" className="link">
Index
</Link>
<Link
to="/examples/post/$postId"
params={{ postId: "abc123" }}
className="link"
>
/post/:id
</Link>
<Link
to="/examples/user/$userId"
params={{ userId: "u-42" }}
className="link"
>
/user/:userId
</Link>
<Link to="/examples/tags/$tag" params={{ tag: "react" }} className="link">
/tags/:tag
</Link>
<Link
to="/examples/collections/$userId/$slug"
params={{ userId: "jane", slug: "summer-2024" }}
className="link"
>
/collections/:userId/:slug
</Link>
<Link
to="/examples/categories/$"
params={{ _splat: "a/b/c" }}
className="link"
>
categories/* (splat)
</Link>
<Link
to="/examples/search"
search={{ view: "grid", q: "demo" }}
className="link"
>
/search?view=
</Link>
<Link to="/examples/scroll-restoration" className="link">
scroll proof
</Link>
<Link to="/examples/lazy-scroll-restoration" className="link">
lazy + scroll
</Link>
<Link to="/examples/loader-demo" className="link">
loader
</Link>
<Link to="/examples/navigate-demo" className="link">
navigate / location
</Link>
<Link to="/examples/redirect-me" className="link">
redirect (beforeLoad)
</Link>
<Link to="/examples/login-redirect" className="link">
Navigate component
</Link>
<a href="/this-path-is-not-defined" className="link">
trigger 404
</a>
</nav>
<Outlet />
</section>
);
}
const examplesRoute = createRoute({
getParentRoute: () => rootRoute,
path: "examples",
component: ExamplesLayout,
});
const examplesIndexRoute = createRoute({
getParentRoute: () => examplesRoute,
path: "/",
component: ExamplesIndex,
});
const postRoute = createRoute({
getParentRoute: () => examplesRoute,
path: "post/$postId",
component: function PostById() {
const { postId } = postRoute.useParams();
return (
<article className="panel">
<h2>Dynamic: /post/:id</h2>
<p>
React Router: <code>useParams()</code> TanStack:{" "}
<code>postRoute.useParams()</code>
</p>
<p>
<code>postId</code>: <strong>{postId}</strong>
</p>
</article>
);
},
});
const userRoute = createRoute({
getParentRoute: () => examplesRoute,
path: "user/$userId",
component: function UserById() {
const { userId } = userRoute.useParams();
return (
<article className="panel">
<h2>Dynamic: /user/:userId</h2>
<p>
<code>userId</code>: <strong>{userId}</strong>
</p>
</article>
);
},
});
const tagRoute = createRoute({
getParentRoute: () => examplesRoute,
path: "tags/$tag",
component: function TagPage() {
const { tag } = tagRoute.useParams();
return (
<article className="panel">
<h2>Dynamic: /tags/:tag</h2>
<p>
<code>tag</code>: <strong>{tag}</strong>
</p>
</article>
);
},
});
const collectionRoute = createRoute({
getParentRoute: () => examplesRoute,
path: "collections/$userId/$slug",
component: function CollectionPage() {
const { userId, slug } = collectionRoute.useParams();
return (
<article className="panel">
<h2>Multi-segment: /collections/:userId/:slug</h2>
<p>
<code>userId</code>: <strong>{userId}</strong>
</p>
<p>
<code>slug</code>: <strong>{slug}</strong>
</p>
</article>
);
},
});
const categoriesSplatRoute = createRoute({
getParentRoute: () => examplesRoute,
path: "categories/$",
component: function CategoriesSplat() {
const params = categoriesSplatRoute.useParams();
const splat = params._splat ?? "";
return (
<article className="panel">
<h2>Catch-all / splat</h2>
<p>
Remainder path (React Router <code>{"*"}</code> style):{" "}
<strong>{splat || "(empty)"}</strong>
</p>
<p>
<code>Route.useParams()</code> exposes <code>_splat</code> (see TanStack path
docs).
</p>
</article>
);
},
});
const searchRoute = createRoute({
getParentRoute: () => examplesRoute,
path: "search",
validateSearch: migrationSearchSchema,
component: function SearchDemo() {
const search = searchRoute.useSearch();
const navigate = useNavigate({ from: "/examples/search" });
return (
<article className="panel">
<h2>Typed search params (replaces useSearchParams)</h2>
<p>
Current URL search: <code>{JSON.stringify(search)}</code>
</p>
<p>
<button
type="button"
className="btn"
onClick={() =>
navigate({
search: (prev: MigrationSearch) => ({
...prev,
view: prev.view === "grid" ? "list" : "grid",
}),
})
}
>
Toggle view
</button>{" "}
<button
type="button"
className="btn"
onClick={() =>
navigate({
search: (prev: MigrationSearch) => ({
...prev,
q: "polymech",
}),
})
}
>
Set q=polymech
</button>
</p>
</article>
);
},
});
const scrollRestorationDemoRoute = createRoute({
getParentRoute: () => examplesRoute,
path: "scroll-restoration",
component: ScrollRestorationDemo,
});
const lazyScrollRestorationRoute = createRoute({
getParentRoute: () => examplesRoute,
path: "lazy-scroll-restoration",
pendingComponent: () => (
<article className="panel">
<p>Loading lazy route chunk</p>
</article>
),
component: lazyRouteComponent(
() => import("@/examples/migration/lazy/LazyScrollRestorationChunk"),
),
});
const loaderDemoRoute = createRoute({
getParentRoute: () => examplesRoute,
path: "loader-demo",
loader: async () => {
await new Promise((r) => setTimeout(r, 120));
return { loadedAt: new Date().toISOString() };
},
component: function LoaderDemo() {
const data = loaderDemoRoute.useLoaderData();
return (
<article className="panel">
<h2>Loader (prefetch / data before render)</h2>
<p>
Data from <code>loader</code>: <strong>{data.loadedAt}</strong>
</p>
<p>
Move feed/API calls here instead of only <code>useEffect</code>+useQuery to
avoid waterfalls (Phase 4).
</p>
</article>
);
},
});
const navigateDemoRoute = createRoute({
getParentRoute: () => examplesRoute,
path: "navigate-demo",
component: function NavigateDemo() {
const navigate = useNavigate();
const location = useLocation();
const pathname = useRouterState({ select: (s) => s.location.pathname });
return (
<article className="panel">
<h2>useNavigate / useLocation / useRouterState</h2>
<p>
<code>useLocation().pathname</code>: <strong>{location.pathname}</strong>
</p>
<p>
<code>useRouterState(select)</code>: <strong>{pathname}</strong>
</p>
<p>
<button
type="button"
className="btn"
onClick={() => void navigate({ to: "/examples" })}
>
Programmatic navigate to /examples
</button>
</p>
</article>
);
},
});
const redirectRoute = createRoute({
getParentRoute: () => examplesRoute,
path: "redirect-me",
beforeLoad: () => {
throw redirect({ to: "/examples", replace: true });
},
component: () => null,
});
const declarativeRedirectRoute = createRoute({
getParentRoute: () => examplesRoute,
path: "login-redirect",
component: () => <Navigate to="/examples" replace />,
});
return examplesRoute.addChildren([
examplesIndexRoute,
postRoute,
userRoute,
tagRoute,
collectionRoute,
categoriesSplatRoute,
searchRoute,
scrollRestorationDemoRoute,
lazyScrollRestorationRoute,
loaderDemoRoute,
navigateDemoRoute,
redirectRoute,
declarativeRedirectRoute,
]);
}

View File

@ -0,0 +1,9 @@
import { z } from "zod";
/** Matches the kind of validation you get on a Route with TanStack + Zod (Phase 3 / 4). */
export const migrationSearchSchema = z.object({
view: z.enum(["list", "grid"]).optional().default("list"),
q: z.string().optional(),
});
export type MigrationSearch = z.infer<typeof migrationSearchSchema>;

View File

@ -0,0 +1,24 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider } from "@tanstack/react-router";
import { router } from "@/router";
import "@/styles.css";
if (import.meta.env.DEV) {
router.subscribe("onResolved", (event) => {
// Phase 4: replace custom route-change listeners / ScrollRestoration onRouteChange
console.debug("[analytics]", event.toLocation.pathname);
});
}
const rootEl = document.getElementById("root");
if (!rootEl) {
throw new Error("Root element #root not found");
}
createRoot(rootEl).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
);

View File

@ -0,0 +1,123 @@
import {
Link,
Outlet,
createRootRoute,
createRoute,
createRouter,
} from "@tanstack/react-router";
import { buildMigrationExamplesBranch } from "@/examples/migration/routeTree";
const rootRoute = createRootRoute({
component: () => (
<div className="layout">
<header className="header">
<strong>ui-next POC</strong>
<nav className="nav">
<Link to="/" className="link">
Home
</Link>
<Link to="/app" className="link">
App
</Link>
<Link to="/app/settings" className="link">
App Settings
</Link>
<Link to="/examples" className="link">
Migration examples
</Link>
</nav>
</header>
<main className="main">
<Outlet />
</main>
</div>
),
});
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/",
component: () => (
<section>
<h1>Home</h1>
<p>
Nested routes live under <code>/app</code> (layout + child segments). See{" "}
<Link to="/examples" className="link">
/examples
</Link>{" "}
for React Router TanStack migration patterns.
</p>
</section>
),
});
const appRoute = createRoute({
getParentRoute: () => rootRoute,
path: "app",
component: () => (
<section className="nested">
<h1>App shell</h1>
<p>
Shared layout for everything under <code>/app/*</code>.
</p>
<Outlet />
</section>
),
});
const appIndexRoute = createRoute({
getParentRoute: () => appRoute,
path: "/",
component: () => (
<article className="panel">
<h2>App index</h2>
<p>
Route: <code>/app</code>
</p>
</article>
),
});
const appSettingsRoute = createRoute({
getParentRoute: () => appRoute,
path: "settings",
component: () => (
<article className="panel">
<h2>Settings</h2>
<p>
Nested route: <code>/app/settings</code>
</p>
</article>
),
});
const migrationExamplesRoute = buildMigrationExamplesBranch(rootRoute);
const routeTree = rootRoute.addChildren([
indexRoute,
appRoute.addChildren([appIndexRoute, appSettingsRoute]),
migrationExamplesRoute,
]);
export const router = createRouter({
routeTree,
scrollRestoration: true,
defaultNotFoundComponent: () => (
<article className="panel">
<h1>404</h1>
<p>Global not found use TanStacks not-found handling instead of a React Router catch-all route.</p>
<p>
<Link to="/" className="link">
Home
</Link>
</p>
</article>
),
});
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}

View File

@ -0,0 +1,168 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
system-ui,
-apple-system,
Segoe UI,
Roboto,
sans-serif;
line-height: 1.5;
color: #0f172a;
background: #f8fafc;
}
.layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
padding: 0.75rem 1.25rem;
background: #0f172a;
color: #f8fafc;
}
.nav {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.link {
color: #93c5fd;
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
.main {
flex: 1;
padding: 1.5rem 1.25rem;
max-width: 56rem;
}
.nested {
margin-top: 1rem;
}
.panel {
margin-top: 1rem;
padding: 1rem 1.25rem;
border-radius: 0.5rem;
background: #fff;
border: 1px solid #e2e8f0;
}
code {
font-size: 0.95em;
padding: 0.1em 0.35em;
border-radius: 0.25rem;
background: #e2e8f0;
}
.muted {
color: #64748b;
font-size: 0.95rem;
}
.examples-nav {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 0.75rem;
margin: 1rem 0;
padding-bottom: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.btn {
font: inherit;
cursor: pointer;
padding: 0.35rem 0.75rem;
border-radius: 0.375rem;
border: 1px solid #cbd5e1;
background: #f1f5f9;
color: #0f172a;
}
.btn:hover {
background: #e2e8f0;
}
/* Scroll restoration demo (see /examples/scroll-restoration) */
.scroll-hud {
position: fixed;
bottom: 1rem;
right: 1rem;
z-index: 100;
max-width: min(22rem, calc(100vw - 2rem));
padding: 0.65rem 0.85rem;
background: #0f172a;
color: #e2e8f0;
font-size: 0.8rem;
line-height: 1.4;
border-radius: 0.5rem;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.35);
}
.scroll-hud code {
background: rgba(148, 163, 184, 0.25);
color: #f8fafc;
}
.scroll-hud-hint {
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid rgba(148, 163, 184, 0.35);
font-size: 0.78rem;
color: #94a3b8;
}
.scroll-demo-intro {
position: relative;
z-index: 1;
}
.scroll-demo-steps {
margin: 0.5rem 0 0;
padding-left: 1.25rem;
}
.scroll-demo-steps li {
margin-bottom: 0.35rem;
}
.scroll-demo-blocks {
margin-top: 1.5rem;
}
.scroll-demo-block {
min-height: 5.5rem;
margin-bottom: 0.75rem;
padding: 1rem 1.25rem;
border-radius: 0.5rem;
border: 1px dashed #cbd5e1;
background: linear-gradient(180deg, #fff 0%, #f1f5f9 100%);
color: #475569;
font-weight: 600;
}
.scroll-demo-block:nth-child(12n + 1) {
background: linear-gradient(180deg, #eff6ff 0%, #dbeafe 100%);
}
.lazy-chunk-intro {
border-left: 4px solid #6366f1;
}

1
packages/ui-next/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,26 @@
{
"extends": "../typescript-config/base.json",
"compilerOptions": {
"baseUrl": ".",
"allowJs": true,
"esModuleInterop": true,
"composite": false,
"importHelpers": false,
"inlineSourceMap": true,
"jsx": "react-jsx",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
"isolatedModules": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ESNext",
"types": ["vite/client", "node"],
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"]
}

View File

@ -0,0 +1,30 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
// Phase 1 (host app): add file-based route generation alongside React:
// import { TanStackRouterVite } from "@tanstack/router-vite-plugin"
// plugins: [TanStackRouterVite({ routesDirectory: "./src/routes" }), react()],
// This POC uses a static route tree under `src/router.tsx` + `src/examples/migration/`.
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
plugins: [react()],
build: {
target: "esnext",
modulePreload: { polyfill: false },
rollupOptions: {
output: {
format: "es",
},
},
},
});