@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +1 -1
  3. package/bin/templates/fbca/src/web/lib/page-router.tsx.template +223 -109
  4. package/dist/combobox.js +119 -0
  5. package/dist/combobox.js.map +1 -0
  6. package/dist/data-table.js +94 -93
  7. package/dist/data-table.js.map +1 -1
  8. package/dist/hooks.js +7 -6
  9. package/dist/index.js +100 -93
  10. package/dist/index.js.map +1 -1
  11. package/dist/llms.txt +146 -1
  12. package/dist/permission-gate.js +28 -0
  13. package/dist/permission-gate.js.map +1 -0
  14. package/dist/styles.css +1 -1
  15. package/dist/types/components/ui/combobox.d.ts +57 -0
  16. package/dist/types/components/ui/combobox.d.ts.map +1 -0
  17. package/dist/types/components/ui/data-table.d.ts +17 -3
  18. package/dist/types/components/ui/data-table.d.ts.map +1 -1
  19. package/dist/types/components/ui/permission-gate.d.ts +70 -0
  20. package/dist/types/components/ui/permission-gate.d.ts.map +1 -0
  21. package/dist/types/hooks/index.d.ts +2 -0
  22. package/dist/types/hooks/index.d.ts.map +1 -1
  23. package/dist/types/hooks/usePagination.d.ts +67 -0
  24. package/dist/types/hooks/usePagination.d.ts.map +1 -0
  25. package/dist/types/index.d.ts +6 -2
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/dist/uikit.css +1 -1
  28. package/dist/usePagination-CmeREbKO.js +294 -0
  29. package/dist/usePagination-CmeREbKO.js.map +1 -0
  30. package/examples/combobox.tsx +37 -0
  31. package/examples/permission-gate.tsx +29 -0
  32. package/examples/use-pagination.tsx +46 -0
  33. package/llms.txt +146 -1
  34. package/package.json +9 -1
  35. package/dist/useDataTable-CPiBpEg-.js +0 -254
  36. 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**: 13 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.
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
- * Auto-discovers routes using Vite glob imports and file-based conventions
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
- // Auto-discover all page files using Vite glob import (including nested) - eager loading
10
- const pageFiles = import.meta.glob('../features/*/pages/**/*.{tsx,jsx}', { eager: true });
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
- routes.push({
81
- path: routePath,
82
- component: (module as any).default
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
- // Sort routes so more specific ones come first
87
- routes.sort((a, b) => {
88
- // Root route should be last for proper matching
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
- console.log('🚀 Auto-discovered routes:');
95
- routes.forEach(route => console.log(` ${route.path}`));
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
- return routes;
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
- // Component to handle scroll to top on route change
101
- const ScrollToTop: React.FC = () => {
102
- const { pathname } = useLocation();
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
- useEffect(() => {
105
- window.scrollTo(0, 0);
106
- }, [pathname]);
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
- export const PageRouter: React.FC = () => {
112
- const routes = generateRoutes();
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
- <Routes>
118
- {routes.map(({ path, component: Component }) => (
119
- <Route key={path} path={path} element={<Component />} />
120
- ))}
121
- {/* Fallback for 404 */}
122
- <Route path="*" element={
123
- <div className="text-center py-12 text-muted-foreground">
124
- <h1 className="text-4xl font-bold mb-4">404</h1>
125
- <p>Page not found - Route not discovered</p>
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
+ };
@@ -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]}