@bloomneo/uikit 1.5.0 → 1.5.1
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.
- package/CHANGELOG.md +85 -0
- package/README.md +1 -1
- package/bin/templates/fbca/src/web/lib/page-router.tsx.template +223 -109
- package/dist/combobox.js +119 -0
- package/dist/combobox.js.map +1 -0
- package/dist/data-table.js +94 -93
- package/dist/data-table.js.map +1 -1
- package/dist/hooks.js +7 -6
- package/dist/index.js +100 -93
- package/dist/index.js.map +1 -1
- package/dist/llms.txt +146 -1
- package/dist/permission-gate.js +28 -0
- package/dist/permission-gate.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/types/components/ui/combobox.d.ts +57 -0
- package/dist/types/components/ui/combobox.d.ts.map +1 -0
- package/dist/types/components/ui/data-table.d.ts +17 -3
- package/dist/types/components/ui/data-table.d.ts.map +1 -1
- package/dist/types/components/ui/permission-gate.d.ts +70 -0
- package/dist/types/components/ui/permission-gate.d.ts.map +1 -0
- package/dist/types/hooks/index.d.ts +2 -0
- package/dist/types/hooks/index.d.ts.map +1 -1
- package/dist/types/hooks/usePagination.d.ts +67 -0
- package/dist/types/hooks/usePagination.d.ts.map +1 -0
- package/dist/types/index.d.ts +6 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/uikit.css +1 -1
- package/dist/usePagination-CmeREbKO.js +294 -0
- package/dist/usePagination-CmeREbKO.js.map +1 -0
- package/examples/combobox.tsx +37 -0
- package/examples/permission-gate.tsx +29 -0
- package/examples/use-pagination.tsx +46 -0
- package/llms.txt +146 -1
- package/package.json +9 -1
- package/dist/useDataTable-CPiBpEg-.js +0 -254
- package/dist/useDataTable-CPiBpEg-.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,91 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to UIKit will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.5.1] - 2026-04-11
|
|
6
|
+
|
|
7
|
+
A focused follow-up to 1.5.0. Fixes the one publicly-documented type bug from
|
|
8
|
+
the AI-ready release, plus lands four small "production reliability + missing
|
|
9
|
+
primitives" items from real-world feedback. Pure additive — no breaking
|
|
10
|
+
changes, no migration needed.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`<DataTable<T>>` generic erasure** — `DataTable` was exported via
|
|
15
|
+
`forwardRef<HTMLTableElement, DataTableProps>`, which erases the row
|
|
16
|
+
generic at the value level. Consumers writing `<DataTable<User> data={...} />`
|
|
17
|
+
(the exact pattern from `examples/data-table.tsx`) hit
|
|
18
|
+
`TS2558: Expected 0 type arguments, but got 1`. The fix is the canonical
|
|
19
|
+
generic-forwardRef recipe: cast the forwardRef result to a generic call
|
|
20
|
+
signature so `T` propagates from the JSX type argument back into `data`,
|
|
21
|
+
`columns`, `actions`, `getRowId`, and the cell renderers. Same runtime
|
|
22
|
+
behavior, type-safe consumer API.
|
|
23
|
+
- **README example count** — README claimed 13 example files; `examples/`
|
|
24
|
+
actually shipped 12. Updated to match reality (now 15 with the new files
|
|
25
|
+
added below).
|
|
26
|
+
|
|
27
|
+
### Added — production reliability (FBCA template)
|
|
28
|
+
|
|
29
|
+
These three fixes apply to the `bin/templates/fbca/.../page-router.tsx`
|
|
30
|
+
template that gets copied into newly-scaffolded projects. Existing scaffolded
|
|
31
|
+
projects need to either re-scaffold or apply the diff manually.
|
|
32
|
+
|
|
33
|
+
- **Default branded 404 page** — the old fallback rendered a debug message
|
|
34
|
+
that **leaked the full route map to end users** (a security smell on every
|
|
35
|
+
scaffolded site). Replaced with a theme-aware 404 page (logo-friendly, "back
|
|
36
|
+
to home" CTA, uses CSS variables so it follows the active theme). Pass
|
|
37
|
+
`<PageRouter notFound={<Custom404 />}>` to override.
|
|
38
|
+
- **Default error boundary** — a single page throwing a runtime error used
|
|
39
|
+
to white-screen the entire SPA. The router now wraps every route in a
|
|
40
|
+
default error boundary that shows a branded "Something went wrong / reload"
|
|
41
|
+
page. Override with `<PageRouter errorBoundary={<MyError />} onError={(err) => sendToSentry(err)} />`.
|
|
42
|
+
- **Code splitting per route by default** — `import.meta.glob` now uses
|
|
43
|
+
lazy mode (`{ eager: false }`), each page is wrapped in `React.lazy()` +
|
|
44
|
+
`Suspense`, and the router supplies a default loading fallback. Tiny apps
|
|
45
|
+
that prefer the old eager behavior can opt out with `<PageRouter eager />`.
|
|
46
|
+
|
|
47
|
+
### Added — runtime primitives
|
|
48
|
+
|
|
49
|
+
- **`<PermissionGate>` + `<PermissionProvider>` + `usePermission()`** —
|
|
50
|
+
unopinionated role-gating primitive for multi-role apps. Bring your own
|
|
51
|
+
auth source via a `check(permission: string) => boolean` function on the
|
|
52
|
+
provider; `<PermissionGate when="admin">`, `when={['admin', 'mod']}` (OR),
|
|
53
|
+
or `when={() => predicate}` are all supported. Replaces the
|
|
54
|
+
`{user.roles.includes('admin') && <Button />}` pattern that gets repeated
|
|
55
|
+
dozens of times in any admin app. See `examples/permission-gate.tsx`.
|
|
56
|
+
- **`usePagination()` hook** — pagination state machine. The
|
|
57
|
+
`<Pagination>` UI component already shipped, but the state (current page,
|
|
58
|
+
total, hasNext, hasPrev, ellipsis logic, page-link compression) was DIY in
|
|
59
|
+
every list view. Returns `page`, `pageCount`, `startIndex`, `endIndex`,
|
|
60
|
+
`hasNext`, `hasPrev`, `pages` (with `'ellipsis-start'` / `'ellipsis-end'`
|
|
61
|
+
markers), and `goTo` / `next` / `prev` / `first` / `last` callbacks.
|
|
62
|
+
See `examples/use-pagination.tsx`.
|
|
63
|
+
- **`<Combobox>` searchable Select** — for dropdowns with more than ~20
|
|
64
|
+
options where typing-to-filter beats scrolling. Built on the existing
|
|
65
|
+
`Command` (cmdk) + `Popover` primitives so no new dependencies. API is
|
|
66
|
+
intentionally close to `<Select>`: `value` / `onChange` / `options` with
|
|
67
|
+
`{ value, label }` shape. Supports `clearable`, `disabled`, custom
|
|
68
|
+
`renderOption`, and configurable popover width. See `examples/combobox.tsx`.
|
|
69
|
+
|
|
70
|
+
### Verified, not changed
|
|
71
|
+
|
|
72
|
+
- **HeaderNav mobile hamburger** (1.5 feedback #17) — verified the existing
|
|
73
|
+
v1.5.0 implementation already ships a working hamburger menu with body
|
|
74
|
+
scroll lock, dropdown handling, and a `<md` breakpoint. The original
|
|
75
|
+
feedback was based on outdated source. A `collapseAt` prop for
|
|
76
|
+
configurable breakpoints is a nice-to-have for a future release; it's
|
|
77
|
+
not closing a real production gap today.
|
|
78
|
+
|
|
79
|
+
### Numbers
|
|
80
|
+
|
|
81
|
+
| Metric | 1.5.0 | 1.5.1 |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| Public exports | 236 | ~248 |
|
|
84
|
+
| Example files | 12 | 15 |
|
|
85
|
+
| New UI components | — | 2 (PermissionGate, Combobox) |
|
|
86
|
+
| New hooks | — | 1 (usePagination) |
|
|
87
|
+
| Template fixes | — | 3 (404, error boundary, code splitting) |
|
|
88
|
+
| Type bugs fixed | — | 1 (DataTable generic erasure) |
|
|
89
|
+
|
|
5
90
|
## [1.5.0] - 2026-04-11
|
|
6
91
|
|
|
7
92
|
> **🔁 Scope change.** Starting with this release the package lives at
|
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ theme classes are on `<html>` before React mounts.
|
|
|
57
57
|
|
|
58
58
|
- **Generated `llms.txt`**: One canonical, machine-readable index of every export, every example, every cookbook recipe — regenerated on every build from `src/index.ts`, `examples/`, and `cookbook/`. Agents read one file and know everything.
|
|
59
59
|
- **Zero `any` in public types**: Full generic inference for `DataTable<User>`, `RowAction<User>`, formatters, hooks. Agent autocomplete actually works.
|
|
60
|
-
- **One copy-pasteable example per primitive**:
|
|
60
|
+
- **One copy-pasteable example per primitive**: 15 minimal `.tsx` files in `examples/` plus 5 composed page recipes in `cookbook/` (CRUD, dashboard, settings, login, delete-flow). Agents pattern-match instead of inventing prop shapes.
|
|
61
61
|
- **Educational runtime errors**: Misuse a component and you get `[@bloomneo/uikit] <DataTable> expects \`data\` to be an array …` linking to the docs entry. Agents read errors and self-correct.
|
|
62
62
|
|
|
63
63
|
**🚀 For rapid development**
|
|
@@ -1,134 +1,248 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Page Router
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Auto-discovers routes using Vite glob imports and file-based conventions.
|
|
5
|
+
*
|
|
6
|
+
* v1.5.1 production-reliability fixes:
|
|
7
|
+
* • Code-splitting per route via React.lazy + Suspense (was eager).
|
|
8
|
+
* Pass <PageRouter eager /> to opt back into the eager behavior for tiny apps.
|
|
9
|
+
* • Default branded 404 page that uses theme tokens. The old debug message
|
|
10
|
+
* leaked the full route map to end users — gone.
|
|
11
|
+
* • Default error boundary so a single page throwing no longer white-screens
|
|
12
|
+
* the whole app. Override with <PageRouter errorBoundary={<MyError/>} />.
|
|
13
|
+
*
|
|
14
|
+
* Override props:
|
|
15
|
+
* <PageRouter
|
|
16
|
+
* notFound={<Custom404 />} // override the default 404 element
|
|
17
|
+
* errorBoundary={<CustomError />} // override the default error UI
|
|
18
|
+
* onError={(err, info) => report(err)} // hook into errors
|
|
19
|
+
* fallback={<MySpinner />} // override the lazy-loading fallback
|
|
20
|
+
* eager // disable code-splitting
|
|
21
|
+
* />
|
|
4
22
|
*/
|
|
5
23
|
|
|
6
|
-
import React, { useEffect } from 'react';
|
|
24
|
+
import React, { Component, Suspense, type ErrorInfo, type ReactNode, useEffect } from 'react';
|
|
7
25
|
import { Routes, Route, useLocation } from 'react-router-dom';
|
|
8
26
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
function generateRoutes() {
|
|
13
|
-
const routes: Array<{ path: string; component: React.ComponentType<any> }> = [];
|
|
14
|
-
|
|
15
|
-
// Process each discovered page file
|
|
16
|
-
Object.entries(pageFiles).forEach(([filePath, module]) => {
|
|
17
|
-
// Extract feature and nested path from file path
|
|
18
|
-
// Examples:
|
|
19
|
-
// ../features/main/pages/index.tsx -> feature: main, nested: ['index']
|
|
20
|
-
// ../features/gallery/pages/new/cat.tsx -> feature: gallery, nested: ['new', 'cat']
|
|
21
|
-
// ../features/gallery/pages/[animal].tsx -> feature: gallery, nested: ['[animal]']
|
|
22
|
-
// ../features/blog/pages/[...slug].tsx -> feature: blog, nested: ['[...slug]']
|
|
23
|
-
const match = filePath.match(/\.\.\/features\/([^/]+)\/pages\/(.+)\.tsx?$/);
|
|
24
|
-
|
|
25
|
-
if (!match) return;
|
|
26
|
-
|
|
27
|
-
const [, feature, nestedPath] = match;
|
|
28
|
-
const pathSegments = nestedPath.split('/');
|
|
29
|
-
|
|
30
|
-
// Convention-based routing logic
|
|
31
|
-
let routePath: string;
|
|
32
|
-
|
|
33
|
-
if (feature === 'main') {
|
|
34
|
-
// Main feature gets priority routes
|
|
35
|
-
if (pathSegments.length === 1 && pathSegments[0] === 'index') {
|
|
36
|
-
routePath = '/';
|
|
37
|
-
} else {
|
|
38
|
-
// Convert nested path: about/details -> /about/details
|
|
39
|
-
routePath = '/' + pathSegments
|
|
40
|
-
.map(segment => segment === 'index' ? '' : segment.toLowerCase())
|
|
41
|
-
.filter(Boolean)
|
|
42
|
-
.map(segment => {
|
|
43
|
-
// Handle catch-all routes: [...slug] -> *
|
|
44
|
-
if (segment.startsWith('[...') && segment.endsWith(']')) {
|
|
45
|
-
return '*';
|
|
46
|
-
}
|
|
47
|
-
// Handle dynamic params: [animal] -> :animal
|
|
48
|
-
if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
49
|
-
return ':' + segment.slice(1, -1);
|
|
50
|
-
}
|
|
51
|
-
return segment;
|
|
52
|
-
})
|
|
53
|
-
.join('/');
|
|
54
|
-
}
|
|
55
|
-
} else {
|
|
56
|
-
// Other features: /feature/nested/path
|
|
57
|
-
const nestedRoute = pathSegments
|
|
58
|
-
.map(segment => segment === 'index' ? '' : segment.toLowerCase())
|
|
59
|
-
.filter(Boolean)
|
|
60
|
-
.map(segment => {
|
|
61
|
-
// Handle catch-all routes: [...slug] -> *
|
|
62
|
-
if (segment.startsWith('[...') && segment.endsWith(']')) {
|
|
63
|
-
return '*';
|
|
64
|
-
}
|
|
65
|
-
// Handle dynamic params: [animal] -> :animal
|
|
66
|
-
if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
67
|
-
return ':' + segment.slice(1, -1);
|
|
68
|
-
}
|
|
69
|
-
return segment;
|
|
70
|
-
})
|
|
71
|
-
.join('/');
|
|
72
|
-
|
|
73
|
-
if (pathSegments.length === 1 && pathSegments[0] === 'index') {
|
|
74
|
-
routePath = `/${feature}`;
|
|
75
|
-
} else {
|
|
76
|
-
routePath = `/${feature}${nestedRoute ? '/' + nestedRoute : ''}`;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
27
|
+
/* -------------------------------------------------------------------------- */
|
|
28
|
+
/* Route discovery */
|
|
29
|
+
/* -------------------------------------------------------------------------- */
|
|
79
30
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
31
|
+
// Eager glob — used when <PageRouter eager />
|
|
32
|
+
const eagerPageFiles = import.meta.glob('../features/*/pages/**/*.{tsx,jsx}', { eager: true });
|
|
33
|
+
|
|
34
|
+
// Lazy glob — default. Each match returns a `() => import('...')` factory.
|
|
35
|
+
const lazyPageFiles = import.meta.glob('../features/*/pages/**/*.{tsx,jsx}');
|
|
85
36
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
37
|
+
type LoadedModule = { default: React.ComponentType<unknown> };
|
|
38
|
+
|
|
39
|
+
interface DiscoveredRoute {
|
|
40
|
+
path: string;
|
|
41
|
+
component: React.ComponentType<unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pathFromFile(filePath: string): string | null {
|
|
45
|
+
const match = filePath.match(/\.\.\/features\/([^/]+)\/pages\/(.+)\.tsx?$/);
|
|
46
|
+
if (!match) return null;
|
|
47
|
+
|
|
48
|
+
const [, feature, nestedPath] = match;
|
|
49
|
+
const pathSegments = nestedPath.split('/');
|
|
50
|
+
|
|
51
|
+
const segmentToRoute = (segment: string) => {
|
|
52
|
+
if (segment.startsWith('[...') && segment.endsWith(']')) return '*';
|
|
53
|
+
if (segment.startsWith('[') && segment.endsWith(']')) return ':' + segment.slice(1, -1);
|
|
54
|
+
return segment;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (feature === 'main') {
|
|
58
|
+
if (pathSegments.length === 1 && pathSegments[0] === 'index') return '/';
|
|
59
|
+
return '/' + pathSegments
|
|
60
|
+
.map(s => s === 'index' ? '' : s.toLowerCase())
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.map(segmentToRoute)
|
|
63
|
+
.join('/');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const nestedRoute = pathSegments
|
|
67
|
+
.map(s => s === 'index' ? '' : s.toLowerCase())
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.map(segmentToRoute)
|
|
70
|
+
.join('/');
|
|
71
|
+
|
|
72
|
+
if (pathSegments.length === 1 && pathSegments[0] === 'index') return `/${feature}`;
|
|
73
|
+
return `/${feature}${nestedRoute ? '/' + nestedRoute : ''}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sortRoutes(routes: DiscoveredRoute[]): DiscoveredRoute[] {
|
|
77
|
+
return routes.sort((a, b) => {
|
|
89
78
|
if (a.path === '/') return 1;
|
|
90
79
|
if (b.path === '/') return -1;
|
|
91
80
|
return b.path.length - a.path.length;
|
|
92
81
|
});
|
|
82
|
+
}
|
|
93
83
|
|
|
94
|
-
|
|
95
|
-
routes
|
|
84
|
+
function generateEagerRoutes(): DiscoveredRoute[] {
|
|
85
|
+
const routes: DiscoveredRoute[] = [];
|
|
86
|
+
Object.entries(eagerPageFiles).forEach(([filePath, mod]) => {
|
|
87
|
+
const path = pathFromFile(filePath);
|
|
88
|
+
if (!path) return;
|
|
89
|
+
routes.push({ path, component: (mod as LoadedModule).default });
|
|
90
|
+
});
|
|
91
|
+
return sortRoutes(routes);
|
|
92
|
+
}
|
|
96
93
|
|
|
97
|
-
|
|
94
|
+
function generateLazyRoutes(): DiscoveredRoute[] {
|
|
95
|
+
const routes: DiscoveredRoute[] = [];
|
|
96
|
+
Object.entries(lazyPageFiles).forEach(([filePath, importer]) => {
|
|
97
|
+
const path = pathFromFile(filePath);
|
|
98
|
+
if (!path) return;
|
|
99
|
+
routes.push({
|
|
100
|
+
path,
|
|
101
|
+
component: React.lazy(importer as () => Promise<LoadedModule>),
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
return sortRoutes(routes);
|
|
98
105
|
}
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
/* -------------------------------------------------------------------------- */
|
|
108
|
+
/* Default 404 */
|
|
109
|
+
/* -------------------------------------------------------------------------- */
|
|
110
|
+
|
|
111
|
+
const DefaultNotFound: React.FC = () => (
|
|
112
|
+
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-4 px-6 py-16 text-center">
|
|
113
|
+
<p className="text-sm font-medium text-muted-foreground">404</p>
|
|
114
|
+
<h1 className="text-3xl font-semibold tracking-tight text-foreground">Page not found</h1>
|
|
115
|
+
<p className="max-w-md text-sm text-muted-foreground">
|
|
116
|
+
The page you’re looking for doesn’t exist or has moved.
|
|
117
|
+
</p>
|
|
118
|
+
<a
|
|
119
|
+
href="/"
|
|
120
|
+
className="mt-2 inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
121
|
+
>
|
|
122
|
+
Back to home
|
|
123
|
+
</a>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
/* -------------------------------------------------------------------------- */
|
|
128
|
+
/* Default error boundary */
|
|
129
|
+
/* -------------------------------------------------------------------------- */
|
|
130
|
+
|
|
131
|
+
interface ErrorBoundaryProps {
|
|
132
|
+
fallback: ReactNode;
|
|
133
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
134
|
+
children: ReactNode;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface ErrorBoundaryState {
|
|
138
|
+
hasError: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
class RouteErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
142
|
+
state: ErrorBoundaryState = { hasError: false };
|
|
103
143
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
144
|
+
static getDerivedStateFromError(): ErrorBoundaryState {
|
|
145
|
+
return { hasError: true };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
149
|
+
this.props.onError?.(error, info);
|
|
150
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
151
|
+
// eslint-disable-next-line no-console
|
|
152
|
+
console.error('[PageRouter] Page threw:', error, info);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
render() {
|
|
157
|
+
if (this.state.hasError) return this.props.fallback;
|
|
158
|
+
return this.props.children;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
107
161
|
|
|
162
|
+
const DefaultErrorElement: React.FC = () => (
|
|
163
|
+
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-4 px-6 py-16 text-center">
|
|
164
|
+
<p className="text-sm font-medium text-destructive">Something went wrong</p>
|
|
165
|
+
<h1 className="text-3xl font-semibold tracking-tight text-foreground">An error occurred</h1>
|
|
166
|
+
<p className="max-w-md text-sm text-muted-foreground">
|
|
167
|
+
Please refresh the page. If the problem persists, contact support.
|
|
168
|
+
</p>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
onClick={() => window.location.reload()}
|
|
172
|
+
className="mt-2 inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
173
|
+
>
|
|
174
|
+
Reload page
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const DefaultLazyFallback: React.FC = () => (
|
|
180
|
+
<div className="flex min-h-[30vh] items-center justify-center" aria-label="Loading">
|
|
181
|
+
<div className="size-6 animate-spin rounded-full border-2 border-muted border-t-primary" />
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
/* -------------------------------------------------------------------------- */
|
|
186
|
+
/* ScrollToTop */
|
|
187
|
+
/* -------------------------------------------------------------------------- */
|
|
188
|
+
|
|
189
|
+
const ScrollToTop: React.FC = () => {
|
|
190
|
+
const { pathname } = useLocation();
|
|
191
|
+
useEffect(() => { window.scrollTo(0, 0); }, [pathname]);
|
|
108
192
|
return null;
|
|
109
193
|
};
|
|
110
194
|
|
|
111
|
-
|
|
112
|
-
|
|
195
|
+
/* -------------------------------------------------------------------------- */
|
|
196
|
+
/* PageRouter */
|
|
197
|
+
/* -------------------------------------------------------------------------- */
|
|
198
|
+
|
|
199
|
+
export interface PageRouterProps {
|
|
200
|
+
/** Custom 404 element. Defaults to a branded theme-aware 404 page. */
|
|
201
|
+
notFound?: ReactNode;
|
|
202
|
+
/** Custom error boundary fallback element. */
|
|
203
|
+
errorBoundary?: ReactNode;
|
|
204
|
+
/** Called when any page throws. Useful for Sentry / observability. */
|
|
205
|
+
onError?: (error: Error, info: ErrorInfo) => void;
|
|
206
|
+
/** Loading fallback shown while a lazy route chunk is fetched. */
|
|
207
|
+
fallback?: ReactNode;
|
|
208
|
+
/** Disable code-splitting and load every page eagerly. Default: false. */
|
|
209
|
+
eager?: boolean;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const PageRouter: React.FC<PageRouterProps> = ({
|
|
213
|
+
notFound,
|
|
214
|
+
errorBoundary,
|
|
215
|
+
onError,
|
|
216
|
+
fallback,
|
|
217
|
+
eager = false,
|
|
218
|
+
}) => {
|
|
219
|
+
const routes = eager ? generateEagerRoutes() : generateLazyRoutes();
|
|
220
|
+
|
|
221
|
+
// In dev, log discovered routes for visibility (unchanged from previous behavior).
|
|
222
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
223
|
+
// eslint-disable-next-line no-console
|
|
224
|
+
console.log('🚀 Auto-discovered routes:');
|
|
225
|
+
routes.forEach(route => {
|
|
226
|
+
// eslint-disable-next-line no-console
|
|
227
|
+
console.log(` ${route.path}`);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const errorElement = errorBoundary ?? <DefaultErrorElement />;
|
|
232
|
+
const notFoundElement = notFound ?? <DefaultNotFound />;
|
|
233
|
+
const lazyFallback = fallback ?? <DefaultLazyFallback />;
|
|
113
234
|
|
|
114
235
|
return (
|
|
115
|
-
|
|
236
|
+
<RouteErrorBoundary fallback={errorElement} onError={onError}>
|
|
116
237
|
<ScrollToTop />
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<div className="mt-4 text-sm">
|
|
127
|
-
Available routes: {routes.map(r => r.path).join(', ')}
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
} />
|
|
131
|
-
</Routes>
|
|
132
|
-
</>
|
|
238
|
+
<Suspense fallback={lazyFallback}>
|
|
239
|
+
<Routes>
|
|
240
|
+
{routes.map(({ path, component: Component }) => (
|
|
241
|
+
<Route key={path} path={path} element={<Component />} />
|
|
242
|
+
))}
|
|
243
|
+
<Route path="*" element={notFoundElement} />
|
|
244
|
+
</Routes>
|
|
245
|
+
</Suspense>
|
|
246
|
+
</RouteErrorBoundary>
|
|
133
247
|
);
|
|
134
|
-
};
|
|
248
|
+
};
|
package/dist/combobox.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { jsxs as a, jsx as r, Fragment as N } from "react/jsx-runtime";
|
|
2
|
+
import * as h from "react";
|
|
3
|
+
import { c as i } from "./utils-CwJPJKOE.js";
|
|
4
|
+
import { Button as w } from "./button.js";
|
|
5
|
+
import { Command as k, CommandInput as S, CommandList as j, CommandEmpty as z, CommandGroup as I, CommandItem as P } from "./command.js";
|
|
6
|
+
import { Popover as D, PopoverTrigger as L, PopoverContent as U } from "./popover.js";
|
|
7
|
+
import { X as B } from "./x-BxwubQiM.js";
|
|
8
|
+
import { c as E } from "./createLucideIcon-B45kRl5r.js";
|
|
9
|
+
import { C as F } from "./check-DXouwtzp.js";
|
|
10
|
+
/**
|
|
11
|
+
* @license lucide-react v0.468.0 - ISC
|
|
12
|
+
*
|
|
13
|
+
* This source code is licensed under the ISC license.
|
|
14
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
15
|
+
*/
|
|
16
|
+
const G = E("ChevronsUpDown", [
|
|
17
|
+
["path", { d: "m7 15 5 5 5-5", key: "1hf1tw" }],
|
|
18
|
+
["path", { d: "m7 9 5-5 5 5", key: "sgt6xg" }]
|
|
19
|
+
]);
|
|
20
|
+
function J({
|
|
21
|
+
value: t,
|
|
22
|
+
onChange: o,
|
|
23
|
+
options: n,
|
|
24
|
+
placeholder: g = "Select…",
|
|
25
|
+
searchPlaceholder: x = "Search…",
|
|
26
|
+
emptyMessage: C = "No results.",
|
|
27
|
+
clearable: m = !1,
|
|
28
|
+
disabled: c = !1,
|
|
29
|
+
renderOption: d,
|
|
30
|
+
className: b,
|
|
31
|
+
contentWidth: s = "trigger"
|
|
32
|
+
}) {
|
|
33
|
+
const [p, u] = h.useState(!1), l = h.useMemo(
|
|
34
|
+
() => n.find((e) => e.value === t),
|
|
35
|
+
[n, t]
|
|
36
|
+
), v = (e) => {
|
|
37
|
+
o?.(e === t && m ? void 0 : e), u(!1);
|
|
38
|
+
}, y = (e) => {
|
|
39
|
+
e.stopPropagation(), o?.(void 0);
|
|
40
|
+
};
|
|
41
|
+
return /* @__PURE__ */ a(D, { open: p, onOpenChange: u, children: [
|
|
42
|
+
/* @__PURE__ */ r(L, { asChild: !0, children: /* @__PURE__ */ a(
|
|
43
|
+
w,
|
|
44
|
+
{
|
|
45
|
+
type: "button",
|
|
46
|
+
variant: "outline",
|
|
47
|
+
role: "combobox",
|
|
48
|
+
"aria-expanded": p,
|
|
49
|
+
"aria-haspopup": "listbox",
|
|
50
|
+
disabled: c,
|
|
51
|
+
className: i(
|
|
52
|
+
"w-full justify-between font-normal",
|
|
53
|
+
!l && "text-muted-foreground",
|
|
54
|
+
b
|
|
55
|
+
),
|
|
56
|
+
children: [
|
|
57
|
+
/* @__PURE__ */ r("span", { className: "truncate", children: l ? l.label : g }),
|
|
58
|
+
/* @__PURE__ */ a("span", { className: "ml-2 flex shrink-0 items-center gap-1", children: [
|
|
59
|
+
m && l && !c && /* @__PURE__ */ r(
|
|
60
|
+
"span",
|
|
61
|
+
{
|
|
62
|
+
role: "button",
|
|
63
|
+
tabIndex: -1,
|
|
64
|
+
"aria-label": "Clear selection",
|
|
65
|
+
onClick: y,
|
|
66
|
+
className: "inline-flex size-4 items-center justify-center rounded text-muted-foreground hover:text-foreground",
|
|
67
|
+
children: /* @__PURE__ */ r(B, { className: "size-3" })
|
|
68
|
+
}
|
|
69
|
+
),
|
|
70
|
+
/* @__PURE__ */ r(G, { className: "size-4 opacity-50" })
|
|
71
|
+
] })
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
) }),
|
|
75
|
+
/* @__PURE__ */ r(
|
|
76
|
+
U,
|
|
77
|
+
{
|
|
78
|
+
className: i("p-0", s === "trigger" && "w-[var(--radix-popover-trigger-width)]"),
|
|
79
|
+
style: s !== "trigger" && s !== "auto" ? { width: s } : void 0,
|
|
80
|
+
align: "start",
|
|
81
|
+
children: /* @__PURE__ */ a(k, { children: [
|
|
82
|
+
/* @__PURE__ */ r(S, { placeholder: x }),
|
|
83
|
+
/* @__PURE__ */ a(j, { children: [
|
|
84
|
+
/* @__PURE__ */ r(z, { children: C }),
|
|
85
|
+
/* @__PURE__ */ r(I, { children: n.map((e) => {
|
|
86
|
+
const f = e.value === t;
|
|
87
|
+
return /* @__PURE__ */ r(
|
|
88
|
+
P,
|
|
89
|
+
{
|
|
90
|
+
value: e.label,
|
|
91
|
+
disabled: e.disabled,
|
|
92
|
+
onSelect: () => v(e.value),
|
|
93
|
+
className: "flex items-center justify-between gap-2",
|
|
94
|
+
children: d ? d(e, f) : /* @__PURE__ */ a(N, { children: [
|
|
95
|
+
/* @__PURE__ */ r("span", { className: "truncate", children: e.label }),
|
|
96
|
+
/* @__PURE__ */ r(
|
|
97
|
+
F,
|
|
98
|
+
{
|
|
99
|
+
className: i(
|
|
100
|
+
"size-4 shrink-0",
|
|
101
|
+
f ? "opacity-100" : "opacity-0"
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
] })
|
|
106
|
+
},
|
|
107
|
+
e.value
|
|
108
|
+
);
|
|
109
|
+
}) })
|
|
110
|
+
] })
|
|
111
|
+
] })
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
] });
|
|
115
|
+
}
|
|
116
|
+
export {
|
|
117
|
+
J as Combobox
|
|
118
|
+
};
|
|
119
|
+
//# sourceMappingURL=combobox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"combobox.js","sources":["../node_modules/lucide-react/dist/esm/icons/chevrons-up-down.js","../src/components/ui/combobox.tsx"],"sourcesContent":["/**\n * @license lucide-react v0.468.0 - ISC\n *\n * This source code is licensed under the ISC license.\n * See the LICENSE file in the root directory of this source tree.\n */\n\nimport createLucideIcon from '../createLucideIcon.js';\n\nconst ChevronsUpDown = createLucideIcon(\"ChevronsUpDown\", [\n [\"path\", { d: \"m7 15 5 5 5-5\", key: \"1hf1tw\" }],\n [\"path\", { d: \"m7 9 5-5 5 5\", key: \"sgt6xg\" }]\n]);\n\nexport { ChevronsUpDown as default };\n//# sourceMappingURL=chevrons-up-down.js.map\n","/**\n * <Combobox> — searchable Select.\n *\n * For dropdowns with more than ~20 options where typing-to-filter beats\n * scrolling. Built on the existing Command (cmdk) + Popover primitives so it\n * doesn't add new dependencies.\n *\n * The API is intentionally close to <Select>:\n *\n * <Combobox\n * value={country}\n * onChange={setCountry}\n * options={[\n * { value: 'us', label: 'United States' },\n * { value: 'in', label: 'India' },\n * { value: 'uk', label: 'United Kingdom' },\n * ]}\n * placeholder=\"Select a country\"\n * searchPlaceholder=\"Search countries…\"\n * />\n *\n * Pass `clearable` to allow setting back to undefined. Pass `disabled` to\n * lock. Pass `renderOption` if you need icons / two-line entries.\n */\n\nimport * as React from 'react';\nimport { Check, ChevronsUpDown, X } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport { Button } from '@/components/ui/button';\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from '@/components/ui/command';\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';\n\nexport interface ComboboxOption {\n value: string;\n label: string;\n /** Disable this specific option. */\n disabled?: boolean;\n}\n\nexport interface ComboboxProps {\n /** Currently-selected value, or undefined for \"no selection\". */\n value?: string;\n /** Called when the user picks an option. Receives undefined when cleared. */\n onChange?: (value: string | undefined) => void;\n /** The full list of options. */\n options: ComboboxOption[];\n /** Placeholder shown in the trigger when no value is selected. */\n placeholder?: string;\n /** Placeholder for the search input inside the popover. */\n searchPlaceholder?: string;\n /** Text shown when the search finds no results. */\n emptyMessage?: string;\n /** Allow the user to clear the selection (shows an X button). */\n clearable?: boolean;\n /** Disable the entire combobox. */\n disabled?: boolean;\n /** Custom option renderer. Receives the option and the selected state. */\n renderOption?: (option: ComboboxOption, isSelected: boolean) => React.ReactNode;\n /** Class name on the trigger button. */\n className?: string;\n /** Width of the popover content. Default: matches the trigger. */\n contentWidth?: 'trigger' | 'auto' | string;\n}\n\nexport function Combobox({\n value,\n onChange,\n options,\n placeholder = 'Select…',\n searchPlaceholder = 'Search…',\n emptyMessage = 'No results.',\n clearable = false,\n disabled = false,\n renderOption,\n className,\n contentWidth = 'trigger',\n}: ComboboxProps): React.JSX.Element {\n const [open, setOpen] = React.useState(false);\n\n const selected = React.useMemo(\n () => options.find((o) => o.value === value),\n [options, value]\n );\n\n const handleSelect = (next: string) => {\n if (next === value && clearable) {\n onChange?.(undefined);\n } else {\n onChange?.(next);\n }\n setOpen(false);\n };\n\n const handleClear = (e: React.MouseEvent) => {\n e.stopPropagation();\n onChange?.(undefined);\n };\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button\n type=\"button\"\n variant=\"outline\"\n role=\"combobox\"\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n disabled={disabled}\n className={cn(\n 'w-full justify-between font-normal',\n !selected && 'text-muted-foreground',\n className\n )}\n >\n <span className=\"truncate\">{selected ? selected.label : placeholder}</span>\n <span className=\"ml-2 flex shrink-0 items-center gap-1\">\n {clearable && selected && !disabled && (\n <span\n role=\"button\"\n tabIndex={-1}\n aria-label=\"Clear selection\"\n onClick={handleClear}\n className=\"inline-flex size-4 items-center justify-center rounded text-muted-foreground hover:text-foreground\"\n >\n <X className=\"size-3\" />\n </span>\n )}\n <ChevronsUpDown className=\"size-4 opacity-50\" />\n </span>\n </Button>\n </PopoverTrigger>\n <PopoverContent\n className={cn('p-0', contentWidth === 'trigger' && 'w-[var(--radix-popover-trigger-width)]')}\n style={contentWidth !== 'trigger' && contentWidth !== 'auto' ? { width: contentWidth } : undefined}\n align=\"start\"\n >\n <Command>\n <CommandInput placeholder={searchPlaceholder} />\n <CommandList>\n <CommandEmpty>{emptyMessage}</CommandEmpty>\n <CommandGroup>\n {options.map((option) => {\n const isSelected = option.value === value;\n return (\n <CommandItem\n key={option.value}\n value={option.label}\n disabled={option.disabled}\n onSelect={() => handleSelect(option.value)}\n className=\"flex items-center justify-between gap-2\"\n >\n {renderOption ? (\n renderOption(option, isSelected)\n ) : (\n <>\n <span className=\"truncate\">{option.label}</span>\n <Check\n className={cn(\n 'size-4 shrink-0',\n isSelected ? 'opacity-100' : 'opacity-0'\n )}\n />\n </>\n )}\n </CommandItem>\n );\n })}\n </CommandGroup>\n </CommandList>\n </Command>\n </PopoverContent>\n </Popover>\n );\n}\n"],"names":["ChevronsUpDown","createLucideIcon","Combobox","value","onChange","options","placeholder","searchPlaceholder","emptyMessage","clearable","disabled","renderOption","className","contentWidth","open","setOpen","React","selected","o","handleSelect","next","handleClear","jsxs","Popover","jsx","PopoverTrigger","Button","cn","X","PopoverContent","Command","CommandInput","CommandList","CommandEmpty","CommandGroup","option","isSelected","CommandItem","Fragment","Check"],"mappings":"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","x_google_ignoreList":[0]}
|