@kode4/react-foundation 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/DESIGN-SYSTEM.md +190 -0
  2. package/INSTRUCTIONS.md +694 -0
  3. package/LICENSE +21 -0
  4. package/README.md +53 -0
  5. package/errors.d.ts +24 -0
  6. package/forms/Form/Form.d.ts +30 -0
  7. package/forms/Form/index.d.ts +1 -0
  8. package/forms/index.d.ts +1 -0
  9. package/hooks/index.d.ts +1 -0
  10. package/hooks/useMediaQuery.d.ts +8 -0
  11. package/index.d.ts +24 -0
  12. package/index.js +1679 -0
  13. package/index.js.map +1 -0
  14. package/internal/env.d.ts +1 -0
  15. package/internal/registry.d.ts +3 -0
  16. package/internal/type-utils.d.ts +6 -0
  17. package/layout/AppLayout/AppLayout.d.ts +18 -0
  18. package/layout/AppLayout/index.d.ts +1 -0
  19. package/layout/CenteredLayout/CenteredLayout.d.ts +12 -0
  20. package/layout/CenteredLayout/index.d.ts +1 -0
  21. package/layout/Columns/Columns.d.ts +35 -0
  22. package/layout/Columns/index.d.ts +1 -0
  23. package/layout/Container/Container.d.ts +5 -0
  24. package/layout/Container/index.d.ts +1 -0
  25. package/layout/DashboardGrid/DashboardGrid.d.ts +37 -0
  26. package/layout/DashboardGrid/index.d.ts +1 -0
  27. package/layout/FillHeight/FillHeight.d.ts +16 -0
  28. package/layout/FillHeight/index.d.ts +1 -0
  29. package/layout/Footer/Footer.d.ts +5 -0
  30. package/layout/Footer/index.d.ts +1 -0
  31. package/layout/Header/Header.d.ts +13 -0
  32. package/layout/Header/index.d.ts +1 -0
  33. package/layout/SideBar/SideBar.d.ts +6 -0
  34. package/layout/SideBar/index.d.ts +1 -0
  35. package/layout/SiteLayout/SiteLayout.d.ts +27 -0
  36. package/layout/SiteLayout/index.d.ts +1 -0
  37. package/layout/SkipLink/SkipLink.d.ts +9 -0
  38. package/layout/SkipLink/index.d.ts +1 -0
  39. package/layout/TopBar/TopBar.d.ts +32 -0
  40. package/layout/TopBar/index.d.ts +1 -0
  41. package/layout/index.d.ts +12 -0
  42. package/package.json +52 -0
  43. package/query/index.d.ts +1 -0
  44. package/query/sort.d.ts +19 -0
  45. package/router/DefaultErrorBoundary.d.ts +5 -0
  46. package/router/TypedLink/TypedLink.d.ts +22 -0
  47. package/router/TypedLink/index.d.ts +1 -0
  48. package/router/adapter.d.ts +27 -0
  49. package/router/chain.d.ts +17 -0
  50. package/router/context.d.ts +24 -0
  51. package/router/createAppRouter.d.ts +15 -0
  52. package/router/defineRoute.d.ts +11 -0
  53. package/router/index.d.ts +11 -0
  54. package/router/menu.d.ts +51 -0
  55. package/router/pathTo.d.ts +21 -0
  56. package/router/types.d.ts +72 -0
  57. package/router/useRoute.d.ts +22 -0
  58. package/style.css +3 -0
  59. package/theme/ModeToggle/ModeToggle.d.ts +6 -0
  60. package/theme/ModeToggle/index.d.ts +1 -0
  61. package/theme/ThemeProvider/ThemeProvider.d.ts +25 -0
  62. package/theme/ThemeProvider/index.d.ts +1 -0
  63. package/theme/Toaster/Toaster.d.ts +9 -0
  64. package/theme/Toaster/index.d.ts +2 -0
  65. package/theme/Toaster/toast.d.ts +38 -0
  66. package/theme/index.d.ts +3 -0
  67. package/ui/Accordion/Accordion.d.ts +7 -0
  68. package/ui/Accordion/index.d.ts +1 -0
  69. package/ui/Alert/Alert.d.ts +11 -0
  70. package/ui/Alert/index.d.ts +1 -0
  71. package/ui/AlertDialog/AlertDialog.d.ts +19 -0
  72. package/ui/AlertDialog/index.d.ts +1 -0
  73. package/ui/Avatar/Avatar.d.ts +7 -0
  74. package/ui/Avatar/index.d.ts +1 -0
  75. package/ui/AwaitErrorBlock/AwaitErrorBlock.d.ts +11 -0
  76. package/ui/AwaitErrorBlock/index.d.ts +1 -0
  77. package/ui/Badge/Badge.d.ts +10 -0
  78. package/ui/Badge/index.d.ts +1 -0
  79. package/ui/Button/Button.d.ts +13 -0
  80. package/ui/Button/index.d.ts +1 -0
  81. package/ui/Card/Card.d.ts +11 -0
  82. package/ui/Card/index.d.ts +1 -0
  83. package/ui/Checkbox/Checkbox.d.ts +5 -0
  84. package/ui/Checkbox/index.d.ts +1 -0
  85. package/ui/Command/Command.d.ts +19 -0
  86. package/ui/Command/index.d.ts +1 -0
  87. package/ui/Dialog/Dialog.d.ts +17 -0
  88. package/ui/Dialog/index.d.ts +1 -0
  89. package/ui/DropdownMenu/DropdownMenu.d.ts +28 -0
  90. package/ui/DropdownMenu/index.d.ts +1 -0
  91. package/ui/ErrorBlock/ErrorBlock.d.ts +21 -0
  92. package/ui/ErrorBlock/index.d.ts +1 -0
  93. package/ui/Input/Input.d.ts +4 -0
  94. package/ui/Input/index.d.ts +1 -0
  95. package/ui/Label/Label.d.ts +5 -0
  96. package/ui/Label/index.d.ts +1 -0
  97. package/ui/Pagination/Pagination.d.ts +17 -0
  98. package/ui/Pagination/index.d.ts +1 -0
  99. package/ui/Popover/Popover.d.ts +7 -0
  100. package/ui/Popover/index.d.ts +1 -0
  101. package/ui/Progress/Progress.d.ts +16 -0
  102. package/ui/Progress/index.d.ts +1 -0
  103. package/ui/RadioGroup/RadioGroup.d.ts +7 -0
  104. package/ui/RadioGroup/index.d.ts +1 -0
  105. package/ui/Select/Select.d.ts +16 -0
  106. package/ui/Select/index.d.ts +1 -0
  107. package/ui/Separator/Separator.d.ts +5 -0
  108. package/ui/Separator/index.d.ts +1 -0
  109. package/ui/Sheet/Sheet.d.ts +17 -0
  110. package/ui/Sheet/index.d.ts +1 -0
  111. package/ui/Skeleton/Skeleton.d.ts +4 -0
  112. package/ui/Skeleton/index.d.ts +1 -0
  113. package/ui/Spinner/Spinner.d.ts +35 -0
  114. package/ui/Spinner/index.d.ts +1 -0
  115. package/ui/Switch/Switch.d.ts +5 -0
  116. package/ui/Switch/index.d.ts +1 -0
  117. package/ui/Table/Table.d.ts +11 -0
  118. package/ui/Table/index.d.ts +1 -0
  119. package/ui/Tabs/Tabs.d.ts +8 -0
  120. package/ui/Tabs/index.d.ts +1 -0
  121. package/ui/Textarea/Textarea.d.ts +4 -0
  122. package/ui/Textarea/index.d.ts +1 -0
  123. package/ui/Tooltip/Tooltip.d.ts +7 -0
  124. package/ui/Tooltip/index.d.ts +1 -0
  125. package/ui/index.d.ts +29 -0
  126. package/utils.d.ts +8 -0
@@ -0,0 +1,32 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { MenuItem } from '../../router/menu';
3
+ import './TopBar.css';
4
+ export type TopBarProps = {
5
+ items: MenuItem[];
6
+ /** Rendered at the far left — typically the app's brand link. */
7
+ brand?: ReactNode;
8
+ /** Rendered at the far right — typically an auth/user widget. */
9
+ end?: ReactNode;
10
+ /**
11
+ * Menu rendered inside the mobile drawer. Defaults to `items`; pass the full
12
+ * nested menu tree here so phone/tablet users reach every page (including the
13
+ * sub-navigation that a SideBar would show on desktop).
14
+ */
15
+ mobileMenu?: MenuItem[];
16
+ /**
17
+ * Hard floor: at/below this query the inline nav ALWAYS collapses to the
18
+ * drawer, even if it would fit. Above it, the nav collapses automatically the
19
+ * moment its items no longer fit the available width (overflow detection), so
20
+ * it adapts to the menu length and device — e.g. an iPad in portrait. Pass
21
+ * `null` to rely purely on overflow detection.
22
+ */
23
+ mobileQuery?: string | null;
24
+ };
25
+ /**
26
+ * Responsive top navigation. The inline nav is shown while it fits; when its
27
+ * items would overflow the available width it collapses into a hamburger that
28
+ * opens a left-side drawer. Collapsing is content-aware (measured, not a fixed
29
+ * breakpoint), so it adapts to the number/length of menu items and to the
30
+ * device — phones and iPads in portrait get the drawer out of the box.
31
+ */
32
+ export declare function TopBar({ items, brand, end, mobileMenu, mobileQuery, }: TopBarProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ export * from './TopBar';
@@ -0,0 +1,12 @@
1
+ export * from './AppLayout';
2
+ export * from './CenteredLayout';
3
+ export * from './Columns';
4
+ export * from './Container';
5
+ export * from './DashboardGrid';
6
+ export * from './FillHeight';
7
+ export * from './Footer';
8
+ export * from './Header';
9
+ export * from './SideBar';
10
+ export * from './SiteLayout';
11
+ export * from './SkipLink';
12
+ export * from './TopBar';
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@kode4/react-foundation",
3
+ "version": "0.1.0",
4
+ "description": "Typed router, themed UI component library, and design system for kode4 projects.",
5
+ "keywords": [
6
+ "react",
7
+ "react-router",
8
+ "typed-router",
9
+ "component-library",
10
+ "design-system",
11
+ "radix-ui",
12
+ "tailwind",
13
+ "shadcn"
14
+ ],
15
+ "license": "MIT",
16
+ "type": "module",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "sideEffects": [
21
+ "*.css"
22
+ ],
23
+ "main": "./index.js",
24
+ "module": "./index.js",
25
+ "types": "./index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./index.d.ts",
29
+ "import": "./index.js"
30
+ },
31
+ "./router/context": {
32
+ "types": "./router/context.d.ts"
33
+ },
34
+ "./styles.css": "./style.css"
35
+ },
36
+ "peerDependencies": {
37
+ "react": "^19.2.7",
38
+ "react-dom": "^19.2.7",
39
+ "react-router-dom": "^7.17.0",
40
+ "zod": "^4.4.3",
41
+ "@tanstack/react-query": "^5.101.0"
42
+ },
43
+ "dependencies": {
44
+ "radix-ui": "^1.5.0",
45
+ "class-variance-authority": "^0.7.1",
46
+ "clsx": "^2.1.1",
47
+ "lucide-react": "^1.18.0",
48
+ "cmdk": "^1.1.1",
49
+ "sonner": "^2.0.7",
50
+ "react-hook-form": "^7.78.0"
51
+ }
52
+ }
@@ -0,0 +1 @@
1
+ export * from './sort';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Generic sort descriptors, shared across data-fetching code.
3
+ *
4
+ * The string values (`'ASC'` / `'DESC'`) match the common backend convention,
5
+ * so a `SortOrder` can be sent straight to an API as a query/body param. Use
6
+ * `SortOrderInput` wherever an endpoint accepts either a single sort or a
7
+ * prioritized list of sorts.
8
+ */
9
+ export declare enum SortDirection {
10
+ Asc = "ASC",
11
+ Desc = "DESC"
12
+ }
13
+ export type SortOrder = {
14
+ /** The field/property to sort by. */
15
+ property: string;
16
+ direction: SortDirection;
17
+ };
18
+ /** One sort, or a prioritized list of sorts (first has highest priority). */
19
+ export type SortOrderInput = SortOrder | SortOrder[];
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Default route error boundary. Delegates rendering to ErrorBlock.
3
+ * Used by the router adapter as the fallback for every route.
4
+ */
5
+ export declare function DefaultErrorBoundary(): import("react").JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { type LinkProps } from 'react-router-dom';
2
+ import type { IsEmpty } from '../../internal/type-utils';
3
+ import type { RouteDef } from '../types';
4
+ export type TypedLinkProps<TParams, TSearch> = Omit<LinkProps, 'to'> & {
5
+ route: RouteDef<TParams, TSearch, any>;
6
+ } & (IsEmpty<TParams> extends true ? {
7
+ params?: TParams;
8
+ search?: Partial<TSearch>;
9
+ } : {
10
+ params: TParams;
11
+ search?: Partial<TSearch>;
12
+ });
13
+ /**
14
+ * Type-safe link to a route. The route's params/search shape is enforced.
15
+ *
16
+ * @example
17
+ * <TypedLink route={userDetailRoute} params={{ id: 5 }}>Profile</TypedLink>
18
+ * <TypedLink route={projectDetailRoute} params={{ id: 1 }} search={{ tab: 'tasks' }}>
19
+ * Tasks
20
+ * </TypedLink>
21
+ */
22
+ export declare function TypedLink<TParams, TSearch>(props: TypedLinkProps<TParams, TSearch>): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ export * from './TypedLink';
@@ -0,0 +1,27 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { RouteObject } from 'react-router-dom';
3
+ import type { AppContext } from './context';
4
+ import type { AnyRouteDef } from './types';
5
+ /**
6
+ * Converts a tree of `RouteDef`s into the `RouteObject[]` shape that
7
+ * React Router's `createBrowserRouter` expects, while populating the
8
+ * route registry so `pathTo()` works.
9
+ *
10
+ * Wraps the entire tree in a pathless root route with an error boundary,
11
+ * so errors that escape all route-level boundaries (or unmatched URLs)
12
+ * are always caught.
13
+ *
14
+ * Note: every route gets `DefaultErrorBoundary` unless it declares its own.
15
+ * Errors therefore render inside the nearest layout's outlet rather than
16
+ * bubbling to a parent route's boundary.
17
+ *
18
+ * The `context` is captured in a closure and passed to every loader,
19
+ * so guards and loaders can reach the auth state, query client, etc.
20
+ */
21
+ export type BuildRouterOptions = {
22
+ /** Shown only while the data router boots (not for later suspenses). Defaults to DefaultHydrateFallback. */
23
+ hydrateFallback?: ComponentType;
24
+ /** Root error boundary for errors that escape all routes (incl. unmatched URLs). Defaults to DefaultErrorBoundary. */
25
+ errorBoundary?: ComponentType;
26
+ };
27
+ export declare function buildRouterConfig(routes: AnyRouteDef[], context: AppContext, options?: BuildRouterOptions): RouteObject[];
@@ -0,0 +1,17 @@
1
+ import type { LoaderArgs, LoaderMiddlewareCtx } from './types';
2
+ type Ctx<P, S, D extends Record<string, unknown>> = LoaderMiddlewareCtx<D> & {
3
+ params: P;
4
+ search: S;
5
+ };
6
+ type AnyStepFn = (ctx: any) => any;
7
+ declare class Chain<P, S, D extends Record<string, unknown> = Record<string, never>> {
8
+ private steps;
9
+ constructor(steps?: AnyStepFn[]);
10
+ /** Side-effect step (guard, prefetch). Does not contribute to data. */
11
+ use(fn: (ctx: Ctx<P, S, D>) => void | Promise<void>): Chain<P, S, D>;
12
+ /** Data step. Returned object is merged into the accumulator. */
13
+ use<T extends Record<string, unknown>>(fn: (ctx: Ctx<P, S, D>) => T | Promise<T>): Chain<P, S, D & T>;
14
+ build(): (args: LoaderArgs<P, S>) => Promise<D>;
15
+ }
16
+ export declare function chain<P = unknown, S = unknown>(): Chain<P, S>;
17
+ export {};
@@ -0,0 +1,24 @@
1
+ import type { QueryClient } from '@tanstack/react-query';
2
+ /**
3
+ * Application-level context passed into every loader.
4
+ *
5
+ * The library only knows about the query client. Apps inject their own
6
+ * services (auth, feature flags, …) via TypeScript module augmentation
7
+ * targeting THIS module (the one that declares the interface):
8
+ *
9
+ * // app code, e.g. src/demo/app/context.ts
10
+ * declare module '@/lib/router/context' {
11
+ * interface AppContext {
12
+ * auth: AuthState;
13
+ * }
14
+ * }
15
+ *
16
+ * (After extraction to a package the target becomes
17
+ * '@kode4/react-foundation/router/context'.)
18
+ *
19
+ * The object passed to `createAppRouter`/`buildRouterConfig` must satisfy
20
+ * the augmented shape, and loaders/guards see the full type on `context`.
21
+ */
22
+ export interface AppContext {
23
+ queryClient: QueryClient;
24
+ }
@@ -0,0 +1,15 @@
1
+ import { type BuildRouterOptions } from './adapter';
2
+ import type { AppContext } from './context';
3
+ import type { AnyRouteDef } from './types';
4
+ /**
5
+ * One-step router setup: converts the RouteDef tree and creates the
6
+ * browser router. Equivalent to
7
+ * `createBrowserRouter(buildRouterConfig(routes, context, options))`.
8
+ *
9
+ * @example
10
+ * export const router = createAppRouter([shellRoute, loginRoute], {
11
+ * queryClient,
12
+ * auth: authState,
13
+ * });
14
+ */
15
+ export declare function createAppRouter(routes: AnyRouteDef[], context: AppContext, options?: BuildRouterOptions): import("react-router").DataRouter;
@@ -0,0 +1,11 @@
1
+ import type { RouteDef } from './types';
2
+ /**
3
+ * Identity function that exists purely for TypeScript inference.
4
+ *
5
+ * When you write `defineRoute({ params: z.object({ id: z.coerce.number() }), ... })`,
6
+ * TS infers TParams as { id: number } from the Zod schema and threads it through
7
+ * the loader and component types.
8
+ *
9
+ * Use `typeof someRoute` to refer to the inferred type elsewhere.
10
+ */
11
+ export declare function defineRoute<TParams, TSearch, TData>(def: RouteDef<TParams, TSearch, TData>): RouteDef<TParams, TSearch, TData>;
@@ -0,0 +1,11 @@
1
+ export * from './adapter';
2
+ export * from './chain';
3
+ export * from './context';
4
+ export * from './createAppRouter';
5
+ export * from './DefaultErrorBoundary';
6
+ export * from './defineRoute';
7
+ export * from './menu';
8
+ export * from './pathTo';
9
+ export * from './TypedLink';
10
+ export * from './types';
11
+ export * from './useRoute';
@@ -0,0 +1,51 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { AnyRouteDef } from './types';
3
+ /**
4
+ * Authored by the developer in the manual menu tree.
5
+ */
6
+ export type MenuRouteNode = {
7
+ route: AnyRouteDef;
8
+ label?: string;
9
+ icon?: ReactNode;
10
+ children?: MenuNode[];
11
+ };
12
+ export type MenuGroupNode = {
13
+ group: string;
14
+ children: MenuNode[];
15
+ };
16
+ export type MenuContentNode = {
17
+ content: ReactNode;
18
+ };
19
+ export type MenuNode = MenuRouteNode | MenuGroupNode | 'separator' | MenuContentNode;
20
+ /**
21
+ * Resolved menu items ready for rendering.
22
+ */
23
+ export type MenuItemRoute = {
24
+ kind: 'item';
25
+ label: string;
26
+ icon?: ReactNode;
27
+ path: string;
28
+ children?: MenuItem[];
29
+ };
30
+ export type MenuItemGroup = {
31
+ kind: 'group';
32
+ label: string;
33
+ items: MenuItem[];
34
+ };
35
+ export type MenuItemSeparator = {
36
+ kind: 'separator';
37
+ };
38
+ export type MenuItemContent = {
39
+ kind: 'content';
40
+ content: ReactNode;
41
+ };
42
+ export type MenuItem = MenuItemRoute | MenuItemGroup | MenuItemSeparator | MenuItemContent;
43
+ /**
44
+ * Resolves a manually-authored `MenuNode[]` tree into renderable `MenuItem[]`.
45
+ */
46
+ export declare function resolveMenu(nodes: MenuNode[]): MenuItem[];
47
+ /**
48
+ * Finds the top-level menu item that "owns" the given pathname, matching
49
+ * the longest prefix first. Only route items are considered.
50
+ */
51
+ export declare function findActiveTopItem(items: MenuItem[], pathname: string): MenuItemRoute | undefined;
@@ -0,0 +1,21 @@
1
+ import type { RouteDef } from './types';
2
+ import type { IsEmpty } from '../internal/type-utils';
3
+ type PathToArgs<TParams, TSearch> = IsEmpty<TParams> extends true ? [] | [args: {
4
+ search?: Partial<TSearch>;
5
+ }] : [args: {
6
+ params: TParams;
7
+ search?: Partial<TSearch>;
8
+ }];
9
+ /**
10
+ * Produces a fully-qualified URL string for a route, with type-safe params and search.
11
+ *
12
+ * @example
13
+ * pathTo(userDetailRoute, { params: { id: 5 } })
14
+ * pathTo(projectDetailRoute, { params: { id: 5 }, search: { tab: 'tasks' } })
15
+ * pathTo(homeRoute) // no params, no search
16
+ *
17
+ * Search values are filtered through the route's Zod schema so defaults are
18
+ * applied and unknown keys are dropped.
19
+ */
20
+ export declare function pathTo<TParams, TSearch, TData>(route: RouteDef<TParams, TSearch, TData>, ...args: PathToArgs<TParams, TSearch>): string;
21
+ export {};
@@ -0,0 +1,72 @@
1
+ import type { ComponentType, ReactNode } from 'react';
2
+ import type { z } from 'zod';
3
+ import type { AppContext } from './context';
4
+ /**
5
+ * The argument shape passed to a route's typed loader function.
6
+ * `params` and `search` are validated against the route's Zod schemas.
7
+ */
8
+ export type LoaderArgs<TParams, TSearch> = {
9
+ params: TParams;
10
+ search: TSearch;
11
+ request: Request;
12
+ context: AppContext;
13
+ };
14
+ /**
15
+ * Context available to every middleware step.
16
+ * Includes loader args (params, search, request, context) plus the
17
+ * accumulated `data` from prior steps in the chain.
18
+ */
19
+ export type LoaderMiddlewareCtx<TDeps extends Record<string, unknown> = Record<string, unknown>> = LoaderArgs<unknown, unknown> & {
20
+ data: TDeps;
21
+ };
22
+ /**
23
+ * A middleware function for use with `chain().use(...)`.
24
+ *
25
+ * - `TResult`: the object this step contributes (merged into data), or `void` for guards/side effects.
26
+ * - `TDeps`: the accumulated data shape this step requires from prior steps.
27
+ *
28
+ * @example
29
+ * // Guard that contributes { user } — no deps on prior data
30
+ * const requireAuth: LoaderMiddleware<{ user: User }> = ({ context, request }) => { ... };
31
+ *
32
+ * // Guard that reads data.user — depends on requireAuth running first
33
+ * function requireRole(role: Role): LoaderMiddleware<void, { user: User }> { ... }
34
+ */
35
+ export type LoaderMiddleware<TResult extends Record<string, unknown> | void = void, TDeps extends Record<string, unknown> = Record<string, unknown>> = (ctx: LoaderMiddlewareCtx<TDeps>) => TResult | Promise<TResult>;
36
+ export type MenuMeta = {
37
+ label: string;
38
+ icon?: ReactNode;
39
+ };
40
+ export type TabDef = {
41
+ id: string;
42
+ label: string;
43
+ };
44
+ /**
45
+ * The single source of truth for a route.
46
+ *
47
+ * - Path is the SEGMENT (e.g. "users/:id"), not the full path.
48
+ * - Params and search are validated by Zod at loader time.
49
+ * - Loaders are plain typed functions; use guard functions at the top
50
+ * for auth checks and similar cross-cutting concerns.
51
+ * - Components read params/search/data via `useRoute(thisRoute)`.
52
+ * - Tabs/accordions live in `tabs` metadata + the search schema (URL-as-state),
53
+ * not as nested routes.
54
+ */
55
+ export type RouteDef<TParams = any, TSearch = any, TData = any> = {
56
+ /** Path segment relative to the parent. Omit for pathless layouts and index routes. */
57
+ path?: string;
58
+ params?: z.ZodSchema<TParams>;
59
+ search?: z.ZodSchema<TSearch>;
60
+ loader?: (args: LoaderArgs<TParams, TSearch>) => Promise<TData> | TData;
61
+ component: ComponentType;
62
+ /** Custom error boundary. Uses useRouteError() internally. Falls back to the default if omitted. */
63
+ errorBoundary?: ComponentType;
64
+ /** Menu integration metadata. Routes without this are not auto-rendered in menus. */
65
+ menu?: MenuMeta;
66
+ /** Tab metadata for routes whose tabs are state-in-URL (not sub-routes). */
67
+ tabs?: TabDef[];
68
+ /** Whether this route is the parent's index route (renders at the parent's path). */
69
+ index?: boolean;
70
+ children?: RouteDef<any, any, any>[];
71
+ };
72
+ export type AnyRouteDef = RouteDef<any, any, any>;
@@ -0,0 +1,22 @@
1
+ import type { RouteDef } from './types';
2
+ /**
3
+ * Type-safe access to a route's params, search, and loader data.
4
+ * This is THE way page components read route state — call it with the
5
+ * route definition co-located in the same file.
6
+ *
7
+ * The schemas on the route are used to parse and coerce values so that the
8
+ * returned types match what you declared in `defineRoute`. Components never
9
+ * see invalid data — validation failures throw at the loader boundary and
10
+ * are caught by the error element.
11
+ *
12
+ * @example
13
+ * const { params, search, data } = useRoute(userDetailRoute);
14
+ * // ^ { id: number }
15
+ * // ^ { tab: 'profile' | 'posts' }
16
+ * // ^ { user: User }
17
+ */
18
+ export declare function useRoute<TParams, TSearch, TData>(route: RouteDef<TParams, TSearch, TData>): {
19
+ params: TParams;
20
+ search: TSearch;
21
+ data: Awaited<TData>;
22
+ };