@marktiderman/genesis-ui 2.3.1 → 2.5.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 (39) hide show
  1. package/SKILL.md +137 -0
  2. package/dist/{AppShell-D_E_Hk4g.d.ts → AppShell-CX0M-dfc.d.ts} +24 -6
  3. package/dist/chunk-CDYZRZ6U.js +141 -0
  4. package/dist/chunk-CDYZRZ6U.js.map +1 -0
  5. package/dist/{chunk-UBCYXOXT.js → chunk-DWM7ELFI.js} +28 -7
  6. package/dist/chunk-DWM7ELFI.js.map +1 -0
  7. package/dist/{chunk-GDFNAHI4.js → chunk-IXGAEECT.js} +4 -2
  8. package/dist/chunk-IXGAEECT.js.map +1 -0
  9. package/dist/{chunk-D4JTQYP5.js → chunk-JA3ZKULS.js} +3 -3
  10. package/dist/{chunk-D4JTQYP5.js.map → chunk-JA3ZKULS.js.map} +1 -1
  11. package/dist/chunk-VFO3I7PV.js +24 -0
  12. package/dist/chunk-VFO3I7PV.js.map +1 -0
  13. package/dist/components/data/index.d.ts +42 -9
  14. package/dist/components/data/index.js +208 -76
  15. package/dist/components/data/index.js.map +1 -1
  16. package/dist/components/layout/index.d.ts +2 -2
  17. package/dist/components/layout/index.js +6 -6
  18. package/dist/components/patterns/index.js +3 -3
  19. package/dist/hooks/index.d.ts +2 -2
  20. package/dist/hooks/index.js +4 -4
  21. package/dist/hooks/use-data-filters.d.ts +1 -1
  22. package/dist/hooks/use-data-filters.js +2 -2
  23. package/dist/hooks/use-mobile.d.ts +1 -0
  24. package/dist/hooks/use-mobile.js +1 -1
  25. package/dist/hooks/use-resource-page.d.ts +1 -1
  26. package/dist/hooks/use-resource-page.js +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.js +12 -12
  29. package/dist/navigation-DBwcZ6vb.d.ts +163 -0
  30. package/dist/styles.css +11 -0
  31. package/dist/{use-resource-page-DTQS011Q.d.ts → use-resource-page-Dd2VUv30.d.ts} +15 -1
  32. package/package.json +8 -6
  33. package/dist/chunk-6NPEMLBA.js +0 -51
  34. package/dist/chunk-6NPEMLBA.js.map +0 -1
  35. package/dist/chunk-GDFNAHI4.js.map +0 -1
  36. package/dist/chunk-M7AU5DDM.js +0 -26
  37. package/dist/chunk-M7AU5DDM.js.map +0 -1
  38. package/dist/chunk-UBCYXOXT.js.map +0 -1
  39. package/dist/navigation-CpOEyUSb.d.ts +0 -94
@@ -1,4 +1,4 @@
1
- export { A as AppShell, a as AppShellOptions, b as AppShellProps, N as NavItem, P as PageHeader, c as PageHeaderOptions, d as PageHeaderProps } from '../../AppShell-D_E_Hk4g.js';
1
+ export { A as AppShell, a as AppShellOptions, b as AppShellProps, N as NavItem, P as PageHeader, c as PageHeaderOptions, d as PageHeaderProps } from '../../AppShell-CX0M-dfc.js';
2
2
  export { DetailPage, DetailPageProps } from './detail-page.js';
3
3
  export { FormPage, FormPageProps } from './form-page.js';
4
4
  export { D as DashboardPage, a as DashboardPageProps, G as Grid, b as GridProps, g as gridVariants } from '../../dashboard-page-C0TihRl5.js';
@@ -9,6 +9,6 @@ import 'react';
9
9
  import 'lucide-react';
10
10
  import '../../view-toggle-CahQ87PW.js';
11
11
  import '../../hooks/use-view-settings.js';
12
- import '../../navigation-CpOEyUSb.js';
12
+ import '../../navigation-DBwcZ6vb.js';
13
13
  import 'class-variance-authority/types';
14
14
  import 'class-variance-authority';
@@ -1,22 +1,22 @@
1
- export { AppShell, PageHeader } from '../../chunk-UBCYXOXT.js';
1
+ export { AppShell, PageHeader } from '../../chunk-DWM7ELFI.js';
2
2
  import '../../chunk-46TZGMFW.js';
3
3
  export { Container, Stack, containerVariants, stackVariants } from '../../chunk-4CQHPWJD.js';
4
4
  export { FormPage } from '../../chunk-PLIYF6KI.js';
5
- export { Section, SettingsPage, sectionHeadingVariants } from '../../chunk-P64MFQXZ.js';
6
5
  import '../../chunk-OZZTPE7F.js';
7
- export { DashboardPage, Grid, gridVariants } from '../../chunk-ARJI2JUV.js';
6
+ export { Section, SettingsPage, sectionHeadingVariants } from '../../chunk-P64MFQXZ.js';
8
7
  import '../../chunk-QJ6P2DW6.js';
9
8
  import '../../chunk-B2GIBUQD.js';
10
9
  import '../../chunk-NPNADZHM.js';
11
10
  import '../../chunk-FK42ZGGV.js';
12
11
  import '../../chunk-22XB3Q6H.js';
13
12
  import '../../chunk-27ELYTGE.js';
13
+ import '../../chunk-NWSCYLPT.js';
14
+ import '../../chunk-F47U5KNW.js';
15
+ export { DashboardPage, Grid, gridVariants } from '../../chunk-ARJI2JUV.js';
14
16
  export { DetailPage } from '../../chunk-AFZ76Q26.js';
15
17
  export { Split, splitVariants } from '../../chunk-D2LIXA2Y.js';
16
18
  export { GAP_CLASSES, spaceScale } from '../../chunk-AVFZ4P4B.js';
17
- import '../../chunk-NWSCYLPT.js';
18
- import '../../chunk-F47U5KNW.js';
19
19
  import '../../chunk-NZ5WBA4R.js';
20
- import '../../chunk-6NPEMLBA.js';
20
+ import '../../chunk-CDYZRZ6U.js';
21
21
  //# sourceMappingURL=index.js.map
22
22
  //# sourceMappingURL=index.js.map
@@ -1,16 +1,16 @@
1
1
  export { ViewSettings, ViewToggle } from '../../chunk-46TZGMFW.js';
2
2
  export { StatusBadge, statusBadgeVariants } from '../../chunk-XMK3T4TA.js';
3
3
  export { ToggleRow } from '../../chunk-6M4NHHKG.js';
4
- export { SettingsRow } from '../../chunk-A7AHVDG7.js';
5
4
  export { UserAvatar, userAvatarVariants } from '../../chunk-XBX6QMR5.js';
6
5
  import '../../chunk-LNFF2RBD.js';
6
+ export { SettingsRow } from '../../chunk-A7AHVDG7.js';
7
7
  import '../../chunk-FK42ZGGV.js';
8
8
  import '../../chunk-22XB3Q6H.js';
9
- export { PageLoading, pageLoadingVariants } from '../../chunk-YRYQXU3G.js';
10
- import '../../chunk-OVU3Z2MT.js';
11
9
  import '../../chunk-27ELYTGE.js';
12
10
  export { EmptyState, NoFeaturesFound, NoFeedbackFound, NoSearchResults } from '../../chunk-65SPT7EA.js';
13
11
  import '../../chunk-NWSCYLPT.js';
12
+ export { PageLoading, pageLoadingVariants } from '../../chunk-YRYQXU3G.js';
13
+ import '../../chunk-OVU3Z2MT.js';
14
14
  export { FormDescription, FormError, FormField, FormLabel } from '../../chunk-R7QFP6EL.js';
15
15
  import '../../chunk-F47U5KNW.js';
16
16
  import '../../chunk-NZ5WBA4R.js';
@@ -6,9 +6,9 @@ export { useViewPreference } from './use-view-preference.js';
6
6
  export { Density, ViewSettings, useViewSettings } from './use-view-settings.js';
7
7
  export { UseSavedViewsReturn, useSavedViews } from './use-saved-views.js';
8
8
  export { RouteMap, createPrefetch } from './use-prefetch.js';
9
- export { R as ResourceActions, a as ResourceColumnDef, U as UseResourcePageProps, b as UseResourcePageReturn, u as useResourcePage } from '../use-resource-page-DTQS011Q.js';
9
+ export { R as ResourceActions, a as ResourceColumnDef, U as UseResourcePageProps, b as UseResourcePageReturn, u as useResourcePage } from '../use-resource-page-Dd2VUv30.js';
10
10
  export { CommandPalette, CommandPaletteItem, CommandPaletteProps, useCommandPaletteHotkey } from '../components/ui/command-palette.js';
11
- import '../navigation-CpOEyUSb.js';
11
+ import '../navigation-DBwcZ6vb.js';
12
12
  import 'react';
13
13
  import '../view-toggle-CahQ87PW.js';
14
14
  import '@marktiderman/genesis-core/provider';
@@ -1,4 +1,4 @@
1
- export { useResourcePage } from '../chunk-GDFNAHI4.js';
1
+ export { useResourcePage } from '../chunk-IXGAEECT.js';
2
2
  export { useSavedViews } from '../chunk-GCQFVMPI.js';
3
3
  export { useViewPreference } from '../chunk-FTTNGGLA.js';
4
4
  export { useViewSettings } from '../chunk-OZ7ZAYCS.js';
@@ -6,10 +6,10 @@ export { CommandPalette, useCommandPaletteHotkey } from '../chunk-CNTWQSUT.js';
6
6
  import '../chunk-SEFK4NZV.js';
7
7
  import '../chunk-7XMZVISM.js';
8
8
  import '../chunk-NZ5WBA4R.js';
9
- export { useDataFilters } from '../chunk-D4JTQYP5.js';
10
- import '../chunk-6NPEMLBA.js';
9
+ export { useDataFilters } from '../chunk-JA3ZKULS.js';
10
+ import '../chunk-CDYZRZ6U.js';
11
11
  export { useKeyboardNavigation } from '../chunk-YJ6MPS6U.js';
12
- export { useIsMobile } from '../chunk-M7AU5DDM.js';
12
+ export { useIsMobile } from '../chunk-VFO3I7PV.js';
13
13
  export { createPrefetch } from '../chunk-HSHIZHYR.js';
14
14
  export { useOne, useResource, useResourceForm, useWizard } from '@marktiderman/genesis-core/hooks';
15
15
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- import { S as SearchParamsAdapter } from '../navigation-CpOEyUSb.js';
1
+ import { S as SearchParamsAdapter } from '../navigation-DBwcZ6vb.js';
2
2
  import 'react';
3
3
 
4
4
  type FilterType = "string" | "multi";
@@ -1,4 +1,4 @@
1
- export { useDataFilters } from '../chunk-D4JTQYP5.js';
2
- import '../chunk-6NPEMLBA.js';
1
+ export { useDataFilters } from '../chunk-JA3ZKULS.js';
2
+ import '../chunk-CDYZRZ6U.js';
3
3
  //# sourceMappingURL=use-data-filters.js.map
4
4
  //# sourceMappingURL=use-data-filters.js.map
@@ -1,3 +1,4 @@
1
+ /** Whether the viewport is narrower than the mobile breakpoint (768px). */
1
2
  declare function useIsMobile(): boolean;
2
3
 
3
4
  export { useIsMobile };
@@ -1,3 +1,3 @@
1
- export { useIsMobile } from '../chunk-M7AU5DDM.js';
1
+ export { useIsMobile } from '../chunk-VFO3I7PV.js';
2
2
  //# sourceMappingURL=use-mobile.js.map
3
3
  //# sourceMappingURL=use-mobile.js.map
@@ -1,5 +1,5 @@
1
1
  import 'react';
2
- export { R as ResourceActions, a as ResourceColumnDef, U as UseResourcePageProps, b as UseResourcePageReturn, t as titleCase, u as useResourcePage } from '../use-resource-page-DTQS011Q.js';
2
+ export { R as ResourceActions, a as ResourceColumnDef, U as UseResourcePageProps, b as UseResourcePageReturn, t as titleCase, u as useResourcePage } from '../use-resource-page-Dd2VUv30.js';
3
3
  import '../view-toggle-CahQ87PW.js';
4
4
  import '@marktiderman/genesis-core/provider';
5
5
  import './use-view-settings.js';
@@ -1,4 +1,4 @@
1
- export { titleCase, useResourcePage } from '../chunk-GDFNAHI4.js';
1
+ export { titleCase, useResourcePage } from '../chunk-IXGAEECT.js';
2
2
  import '../chunk-FTTNGGLA.js';
3
3
  //# sourceMappingURL=use-resource-page.js.map
4
4
  //# sourceMappingURL=use-resource-page.js.map
package/dist/index.d.ts CHANGED
@@ -67,7 +67,7 @@ export { DetailPage, DetailPageProps } from './components/layout/detail-page.js'
67
67
  export { FormPage, FormPageProps } from './components/layout/form-page.js';
68
68
  export { SettingsPage, SettingsPageProps, SettingsPageSection } from './components/layout/settings-page.js';
69
69
  export { cn } from './utils.js';
70
- export { D as DefaultLink, G as GenesisLinkProps, L as LinkComponent, N as NavigateFn, S as SearchParamsAdapter, d as defaultNavigate, i as isPathActive, u as useBrowserSearchParams } from './navigation-CpOEyUSb.js';
70
+ export { D as DefaultLink, G as GenesisLinkProps, I as IsPathActiveOptions, L as LinkComponent, N as NavigateFn, a as NavigationNode, R as ResolvedNavigation, S as SearchParamsAdapter, d as defaultNavigate, i as isPathActive, r as resolveNavigation, u as useBrowserSearchParams } from './navigation-DBwcZ6vb.js';
71
71
  export { S as SlotOverride, a as SlotPropsFor, b as Slots, r as renderSlot } from './slots-tR0wf89f.js';
72
72
  export { toast } from 'sonner';
73
73
  import 'class-variance-authority/types';
package/dist/index.js CHANGED
@@ -26,20 +26,19 @@ export { Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, Breadcr
26
26
  export { ButtonGroup } from './chunk-K4YLKVMX.js';
27
27
  export { Calendar, CalendarDayButton } from './chunk-J6FBA7PH.js';
28
28
  export { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from './chunk-AOZGQM6W.js';
29
- export { Container, Stack, containerVariants, stackVariants } from './chunk-4CQHPWJD.js';
30
- export { FormPage } from './chunk-PLIYF6KI.js';
31
- export { Section, SettingsPage, sectionHeadingVariants } from './chunk-P64MFQXZ.js';
32
29
  export { AccessibleIcon } from './chunk-JAMLFMDS.js';
33
30
  export { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './chunk-YWFPMND7.js';
34
31
  export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger } from './chunk-M62CHO3N.js';
35
- export { Alert, AlertDescription, AlertTitle, alertVariants } from './chunk-OZZTPE7F.js';
36
32
  export { AspectRatio } from './chunk-6Q6CLLJ4.js';
33
+ export { Container, Stack, containerVariants, stackVariants } from './chunk-4CQHPWJD.js';
34
+ export { FormPage } from './chunk-PLIYF6KI.js';
35
+ export { Alert, AlertDescription, AlertTitle, alertVariants } from './chunk-OZZTPE7F.js';
36
+ export { Section, SettingsPage, sectionHeadingVariants } from './chunk-P64MFQXZ.js';
37
37
  export { StatusBadge, statusBadgeVariants } from './chunk-XMK3T4TA.js';
38
38
  export { ToggleRow } from './chunk-6M4NHHKG.js';
39
- export { SettingsRow } from './chunk-A7AHVDG7.js';
40
39
  export { UserAvatar, userAvatarVariants } from './chunk-XBX6QMR5.js';
41
40
  export { Avatar, AvatarFallback, AvatarImage } from './chunk-LNFF2RBD.js';
42
- export { DashboardPage, Grid, gridVariants } from './chunk-ARJI2JUV.js';
41
+ export { SettingsRow } from './chunk-A7AHVDG7.js';
43
42
  export { renderSlot } from './chunk-7S7LKQBD.js';
44
43
  export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } from './chunk-N2ZFNOOT.js';
45
44
  export { Textarea } from './chunk-YETMISCE.js';
@@ -55,20 +54,21 @@ export { Collapsible, CollapsibleContent, CollapsibleTrigger } from './chunk-B2G
55
54
  export { Badge } from './chunk-NPNADZHM.js';
56
55
  export { Select, SelectContent, SelectGroup, SelectItem, SelectScrollDownButton, SelectScrollUpButton, SelectTrigger, SelectValue } from './chunk-FK42ZGGV.js';
57
56
  export { Popover, PopoverContent, PopoverTrigger } from './chunk-22XB3Q6H.js';
58
- export { PageLoading, pageLoadingVariants } from './chunk-YRYQXU3G.js';
59
- export { Spinner } from './chunk-OVU3Z2MT.js';
60
57
  export { Switch } from './chunk-27ELYTGE.js';
61
- export { DetailPage } from './chunk-AFZ76Q26.js';
62
- export { Split, splitVariants } from './chunk-D2LIXA2Y.js';
63
- export { GAP_CLASSES, spaceScale } from './chunk-AVFZ4P4B.js';
64
58
  export { EmptyState, NoFeaturesFound, NoFeedbackFound, NoSearchResults } from './chunk-65SPT7EA.js';
65
59
  export { Button } from './chunk-NWSCYLPT.js';
60
+ export { PageLoading, pageLoadingVariants } from './chunk-YRYQXU3G.js';
61
+ export { Spinner } from './chunk-OVU3Z2MT.js';
66
62
  export { FormDescription, FormError, FormField, FormLabel } from './chunk-R7QFP6EL.js';
67
63
  export { Label } from './chunk-F47U5KNW.js';
64
+ export { DashboardPage, Grid, gridVariants } from './chunk-ARJI2JUV.js';
65
+ export { DetailPage } from './chunk-AFZ76Q26.js';
66
+ export { Split, splitVariants } from './chunk-D2LIXA2Y.js';
67
+ export { GAP_CLASSES, spaceScale } from './chunk-AVFZ4P4B.js';
68
68
  export { CommandPalette, useCommandPaletteHotkey } from './chunk-CNTWQSUT.js';
69
69
  export { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut } from './chunk-SEFK4NZV.js';
70
70
  export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from './chunk-7XMZVISM.js';
71
71
  export { cn } from './chunk-NZ5WBA4R.js';
72
- export { DefaultLink, defaultNavigate, isPathActive, useBrowserSearchParams } from './chunk-6NPEMLBA.js';
72
+ export { DefaultLink, defaultNavigate, isPathActive, resolveNavigation, useBrowserSearchParams } from './chunk-CDYZRZ6U.js';
73
73
  //# sourceMappingURL=index.js.map
74
74
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,163 @@
1
+ import { ComponentType, ReactNode, AnchorHTMLAttributes } from 'react';
2
+
3
+ /**
4
+ * Router-agnostic navigation primitives.
5
+ *
6
+ * genesis-ui does NOT depend on any router. Components that need to render a
7
+ * link or navigate imperatively accept an injectable abstraction defined here,
8
+ * and default to plain browser behavior (`<a>` / `window.location`). Consumers
9
+ * on React Router or Next.js inject a thin adapter that maps these props to
10
+ * their own `<Link>` / router.
11
+ */
12
+
13
+ /**
14
+ * Props passed to an injectable link component. The contract is a plain anchor
15
+ * (`href`), so the default works with NO router. A React Router consumer maps
16
+ * `href` → `to`; a Next.js consumer passes `next/link` directly (it already
17
+ * accepts `href`, `className`, `children`).
18
+ *
19
+ * @stability Beta
20
+ */
21
+ interface GenesisLinkProps {
22
+ /** Destination path or URL. */
23
+ href: string;
24
+ className?: string;
25
+ onClick?: () => void;
26
+ children?: ReactNode;
27
+ /** Set to "page" when the link points at the active route. */
28
+ "aria-current"?: AnchorHTMLAttributes<HTMLAnchorElement>["aria-current"];
29
+ /** Accessible name — useful when the visible label is hidden (icon rail). */
30
+ "aria-label"?: string;
31
+ /** Native tooltip / title attribute. */
32
+ title?: string;
33
+ /**
34
+ * Stable test/automation selector. {@link DefaultLink} renders it as
35
+ * `data-testid`; injected router adapters (`next/link`, RR `Link`) forward it
36
+ * to the underlying `<a>` too.
37
+ */
38
+ testID?: string;
39
+ }
40
+ /**
41
+ * A component that renders a navigable link from {@link GenesisLinkProps}.
42
+ *
43
+ * @stability Beta
44
+ */
45
+ type LinkComponent = ComponentType<GenesisLinkProps>;
46
+ /**
47
+ * Default link — a plain `<a>`. No router required.
48
+ *
49
+ * @stability Beta
50
+ */
51
+ declare const DefaultLink: LinkComponent;
52
+ /**
53
+ * Options for {@link isPathActive}.
54
+ *
55
+ * @stability Beta
56
+ */
57
+ interface IsPathActiveOptions {
58
+ /** Require an exact path match instead of a prefix match (like `NavLink end`). */
59
+ end?: boolean;
60
+ }
61
+ /**
62
+ * Default active-path matcher, mirroring React Router's `NavLink` (prefix
63
+ * match, with `/` treated as exact). Consumers can override via an `isActive`
64
+ * prop where components accept one.
65
+ *
66
+ * `activePath` may carry a query and hash (`pathname + search + hash`). An
67
+ * item whose `to` has neither compares against the path part only, exactly as
68
+ * before. An item that opts in by putting a query or hash in its `to`
69
+ * (`/admin?tab=users`, `/settings#profile`) is active only when the path
70
+ * matches exactly AND every query param it names has the same value in
71
+ * `activePath` (extra params such as `&page=2` are ignored) AND, if it names a
72
+ * hash, the hash is equal.
73
+ *
74
+ * @stability Beta
75
+ */
76
+ declare function isPathActive(to: string, activePath?: string, options?: IsPathActiveOptions): boolean;
77
+ /**
78
+ * One node of a logical navigation hierarchy. `path` is a route pattern whose
79
+ * segments are either static (`clients`) or named params (`$clientId`). A
80
+ * node's place in the tree is its LOGICAL parent, which need not share its URL
81
+ * prefix — `/clients/$clientId` can sit under `/dashboard`.
82
+ *
83
+ * @stability Beta
84
+ */
85
+ interface NavigationNode<T> {
86
+ id: string;
87
+ path: string;
88
+ label: string;
89
+ /** Contextual nav items to show while this node is current. */
90
+ items: T[];
91
+ children?: NavigationNode<T>[];
92
+ }
93
+ /**
94
+ * Result of {@link resolveNavigation}. `current`, `parent` and every `trail`
95
+ * entry are shallow copies of their tree nodes with `path` interpolated from
96
+ * `params` (a param the URL did not supply stays as `$name`).
97
+ *
98
+ * @stability Beta
99
+ */
100
+ interface ResolvedNavigation<T> {
101
+ /** The best-matching node — the root when nothing else matches. */
102
+ current: NavigationNode<T>;
103
+ /** `current`'s logical parent, for an "up" / back-to-parent link. */
104
+ parent?: NavigationNode<T>;
105
+ /** `current.items`. */
106
+ items: T[];
107
+ /** Root → `current`, inclusive. */
108
+ trail: NavigationNode<T>[];
109
+ /** Params captured by `current.path`, URL-decoded. */
110
+ params: Record<string, string>;
111
+ }
112
+ /**
113
+ * Resolve where `pathname` sits in a logical navigation tree. Pure: it never
114
+ * reads browser history or `window`.
115
+ *
116
+ * A node matches when its pattern matches a leading run of WHOLE path
117
+ * segments (`/my` matches `/my/tasks`, never `/my-other`). Among matches the
118
+ * winner is decided by positional specificity: at the first position where
119
+ * one pattern is static and the other a `$param`, the static one wins (so
120
+ * `/clients/new` beats `/clients/$clientId` whatever the sibling order, and
121
+ * `/my` beats `/$space/tasks` for `/my/tasks`). Otherwise the pattern matching
122
+ * more segments wins, then the deeper tree node.
123
+ * Non-matching nodes are still searched, so a match keeps its logical
124
+ * ancestors even when their URLs differ.
125
+ *
126
+ * @stability Beta
127
+ */
128
+ declare function resolveNavigation<T>(tree: NavigationNode<T>, pathname: string): ResolvedNavigation<T>;
129
+ /**
130
+ * Injectable imperative navigation callback.
131
+ *
132
+ * @stability Beta
133
+ */
134
+ type NavigateFn = (path: string) => void;
135
+ /**
136
+ * Default navigate — a full-page navigation. No router required.
137
+ *
138
+ * @stability Beta
139
+ */
140
+ declare const defaultNavigate: NavigateFn;
141
+ /**
142
+ * Minimal, router-agnostic replacement for React Router's `useSearchParams`
143
+ * return shape. Consumers may inject their router's implementation for full
144
+ * SPA integration; otherwise the browser History API is used.
145
+ *
146
+ * @stability Beta
147
+ */
148
+ interface SearchParamsAdapter {
149
+ searchParams: URLSearchParams;
150
+ setSearchParams: (next: URLSearchParams, options?: {
151
+ replace?: boolean;
152
+ }) => void;
153
+ }
154
+ /**
155
+ * Browser-native {@link SearchParamsAdapter} built on the History API. Reads
156
+ * the current query string and writes updates via `pushState`/`replaceState`,
157
+ * staying reactive to `popstate` events. No router required.
158
+ *
159
+ * @stability Beta
160
+ */
161
+ declare function useBrowserSearchParams(): SearchParamsAdapter;
162
+
163
+ export { DefaultLink as D, type GenesisLinkProps as G, type IsPathActiveOptions as I, type LinkComponent as L, type NavigateFn as N, type ResolvedNavigation as R, type SearchParamsAdapter as S, type NavigationNode as a, defaultNavigate as d, isPathActive as i, resolveNavigation as r, useBrowserSearchParams as u };
@@ -0,0 +1,11 @@
1
+ /*
2
+ * Tailwind v4 source entry for @marktiderman/genesis-ui.
3
+ *
4
+ * Copied to dist/styles.css at build time, so "./" is the published dist/
5
+ * folder: importing this file makes Tailwind scan genesis-ui's compiled
6
+ * components for the utility classes they use.
7
+ *
8
+ * @import "tailwindcss";
9
+ * @import "@marktiderman/genesis-ui/styles.css";
10
+ */
11
+ @source "./";
@@ -15,7 +15,14 @@ interface DataTableColumn<T> {
15
15
  editable?: {
16
16
  type: "text" | "select";
17
17
  options?: string[];
18
- onSave: (item: T, newValue: string) => void;
18
+ /**
19
+ * Called with the new value. Return a promise to keep the cell in a
20
+ * saving state until it settles: a fulfilled save closes the editor, and
21
+ * a rejected one keeps it open with the error's message beneath it, so
22
+ * nothing the user typed is lost and the reason is on screen. A plain
23
+ * `void` return closes the editor at once, as before.
24
+ */
25
+ onSave: (item: T, newValue: string) => void | Promise<void>;
19
26
  };
20
27
  /** Whether column is visible by default (default true) */
21
28
  defaultVisible?: boolean;
@@ -82,6 +89,13 @@ interface ResourceColumnDef<T> {
82
89
  sortable?: boolean;
83
90
  hideBelow?: "sm" | "md" | "lg" | "xl";
84
91
  render?: (item: T, value: unknown, actions: ResourceActions<T>) => ReactNode;
92
+ /** In-place cell editing, passed straight through to `DataTable`. */
93
+ editable?: DataTableColumn<T>["editable"];
94
+ /**
95
+ * The cell's value, when it isn't `item[key]`. Passed straight through to
96
+ * `DataTable`, which also starts an edit from `String(accessor(item))`.
97
+ */
98
+ accessor?: (item: T) => unknown;
85
99
  }
86
100
  interface UseResourcePageProps<T> {
87
101
  /** Resource name — used for view-preference storage key. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marktiderman/genesis-ui",
3
- "version": "2.3.1",
3
+ "version": "2.5.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -22,7 +22,8 @@
22
22
  "dist",
23
23
  "!dist/metafile-*.json",
24
24
  "README.md",
25
- "EXTENDING.md"
25
+ "EXTENDING.md",
26
+ "SKILL.md"
26
27
  ],
27
28
  "exports": {
28
29
  ".": {
@@ -61,6 +62,7 @@
61
62
  "types": "./dist/provider/index.d.ts",
62
63
  "import": "./dist/provider/index.js"
63
64
  },
65
+ "./styles.css": "./dist/styles.css",
64
66
  "./accessible-icon": {
65
67
  "types": "./dist/components/ui/accessible-icon.d.ts",
66
68
  "import": "./dist/components/ui/accessible-icon.js"
@@ -327,7 +329,7 @@
327
329
  }
328
330
  },
329
331
  "peerDependencies": {
330
- "@marktiderman/genesis-design-system": "^1.2.1",
332
+ "@marktiderman/genesis-design-system": "^1.3.1",
331
333
  "cmdk": "^1.0.0",
332
334
  "date-fns": "^4.0.0",
333
335
  "embla-carousel-react": "^8.0.0",
@@ -345,14 +347,14 @@
345
347
  "vaul": "^1.0.0"
346
348
  },
347
349
  "dependencies": {
348
- "@marktiderman/genesis-core": "^1.1.2",
350
+ "@marktiderman/genesis-core": "^1.2.0",
349
351
  "class-variance-authority": "^0.7.1",
350
352
  "clsx": "^2.1.1",
351
353
  "tailwind-merge": "^3.5.0"
352
354
  },
353
355
  "devDependencies": {
354
356
  "@hookform/resolvers": "^5.2.2",
355
- "@marktiderman/genesis-design-system": "^1.2.1",
357
+ "@marktiderman/genesis-design-system": "^1.3.1",
356
358
  "@testing-library/react": "^16.3.2",
357
359
  "@radix-ui/react-accordion": "^1.2.0",
358
360
  "@radix-ui/react-aspect-ratio": "^1.1.0",
@@ -392,7 +394,7 @@
392
394
  "zod": "^4.3.6"
393
395
  },
394
396
  "scripts": {
395
- "build": "tsup && node scripts/stamp-use-client.mjs && node ../../scripts/check-exports-resolve.mjs",
397
+ "build": "tsup && node scripts/stamp-use-client.mjs && cp src/styles.css dist/styles.css && node ../../scripts/check-exports-resolve.mjs",
396
398
  "clean": "rm -rf dist",
397
399
  "check:exports": "node ../../scripts/check-exports-resolve.mjs",
398
400
  "test": "vitest run"
@@ -1,51 +0,0 @@
1
- "use client";
2
- import { useState, useEffect, useCallback } from 'react';
3
- import { jsx } from 'react/jsx-runtime';
4
-
5
- // src/navigation.tsx
6
- var DefaultLink = ({
7
- href,
8
- children,
9
- testID,
10
- ...rest
11
- }) => /* @__PURE__ */ jsx("a", { href, "data-testid": testID, ...rest, children });
12
- function isPathActive(to, activePath) {
13
- if (activePath == null) return false;
14
- if (to === activePath) return true;
15
- if (to !== "/" && activePath.startsWith(to + "/")) return true;
16
- return false;
17
- }
18
- var defaultNavigate = (path) => {
19
- if (typeof window !== "undefined") window.location.assign(path);
20
- };
21
- function useBrowserSearchParams() {
22
- const read = () => new URLSearchParams(
23
- typeof window !== "undefined" ? window.location.search : ""
24
- );
25
- const [searchParams, setLocal] = useState(read);
26
- useEffect(() => {
27
- if (typeof window === "undefined") return;
28
- const onPop = () => setLocal(read());
29
- window.addEventListener("popstate", onPop);
30
- return () => window.removeEventListener("popstate", onPop);
31
- }, []);
32
- const setSearchParams = useCallback(
33
- (next, options) => {
34
- if (typeof window === "undefined") return;
35
- const qs = next.toString();
36
- const url = window.location.pathname + (qs ? `?${qs}` : "") + window.location.hash;
37
- if (options?.replace) {
38
- window.history.replaceState(window.history.state, "", url);
39
- } else {
40
- window.history.pushState(window.history.state, "", url);
41
- }
42
- setLocal(new URLSearchParams(next));
43
- },
44
- []
45
- );
46
- return { searchParams, setSearchParams };
47
- }
48
-
49
- export { DefaultLink, defaultNavigate, isPathActive, useBrowserSearchParams };
50
- //# sourceMappingURL=chunk-6NPEMLBA.js.map
51
- //# sourceMappingURL=chunk-6NPEMLBA.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/navigation.tsx"],"names":[],"mappings":";;;;AAgEO,IAAM,cAA6B,CAAC;AAAA,EACzC,IAAA;AAAA,EACA,QAAA;AAAA,EACA,MAAA;AAAA,EACA,GAAG;AACL,CAAA,yBACG,GAAA,EAAA,EAAE,IAAA,EAAY,eAAa,MAAA,EAAS,GAAG,MACrC,QAAA,EACH;AAUK,SAAS,YAAA,CAAa,IAAY,UAAA,EAA8B;AACrE,EAAA,IAAI,UAAA,IAAc,MAAM,OAAO,KAAA;AAC/B,EAAA,IAAI,EAAA,KAAO,YAAY,OAAO,IAAA;AAC9B,EAAA,IAAI,OAAO,GAAA,IAAO,UAAA,CAAW,WAAW,EAAA,GAAK,GAAG,GAAG,OAAO,IAAA;AAC1D,EAAA,OAAO,KAAA;AACT;AAkBO,IAAM,eAAA,GAA8B,CAAC,IAAA,KAAS;AACnD,EAAA,IAAI,OAAO,MAAA,KAAW,WAAA,EAAa,MAAA,CAAO,QAAA,CAAS,OAAO,IAAI,CAAA;AAChE;AA4BO,SAAS,sBAAA,GAA8C;AAC5D,EAAA,MAAM,IAAA,GAAO,MACX,IAAI,eAAA;AAAA,IACF,OAAO,MAAA,KAAW,WAAA,GAAc,MAAA,CAAO,SAAS,MAAA,GAAS;AAAA,GAC3D;AAEF,EAAA,MAAM,CAAC,YAAA,EAAc,QAAQ,CAAA,GAAI,SAA0B,IAAI,CAAA;AAE/D,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACnC,IAAA,MAAM,KAAA,GAAQ,MAAM,QAAA,CAAS,IAAA,EAAM,CAAA;AACnC,IAAA,MAAA,CAAO,gBAAA,CAAiB,YAAY,KAAK,CAAA;AACzC,IAAA,OAAO,MAAM,MAAA,CAAO,mBAAA,CAAoB,UAAA,EAAY,KAAK,CAAA;AAAA,EAC3D,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,eAAA,GAAkB,WAAA;AAAA,IACtB,CAAC,MAAM,OAAA,KAAY;AACjB,MAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AACnC,MAAA,MAAM,EAAA,GAAK,KAAK,QAAA,EAAS;AACzB,MAAA,MAAM,GAAA,GACJ,MAAA,CAAO,QAAA,CAAS,QAAA,IAAY,EAAA,GAAK,IAAI,EAAE,CAAA,CAAA,GAAK,EAAA,CAAA,GAAM,MAAA,CAAO,QAAA,CAAS,IAAA;AACpE,MAAA,IAAI,SAAS,OAAA,EAAS;AACpB,QAAA,MAAA,CAAO,QAAQ,YAAA,CAAa,MAAA,CAAO,OAAA,CAAQ,KAAA,EAAO,IAAI,GAAG,CAAA;AAAA,MAC3D,CAAA,MAAO;AACL,QAAA,MAAA,CAAO,QAAQ,SAAA,CAAU,MAAA,CAAO,OAAA,CAAQ,KAAA,EAAO,IAAI,GAAG,CAAA;AAAA,MACxD;AACA,MAAA,QAAA,CAAS,IAAI,eAAA,CAAgB,IAAI,CAAC,CAAA;AAAA,IACpC,CAAA;AAAA,IACA;AAAC,GACH;AAEA,EAAA,OAAO,EAAE,cAAc,eAAA,EAAgB;AACzC","file":"chunk-6NPEMLBA.js","sourcesContent":["\"use client\";\n\n/**\n * Router-agnostic navigation primitives.\n *\n * genesis-ui does NOT depend on any router. Components that need to render a\n * link or navigate imperatively accept an injectable abstraction defined here,\n * and default to plain browser behavior (`<a>` / `window.location`). Consumers\n * on React Router or Next.js inject a thin adapter that maps these props to\n * their own `<Link>` / router.\n */\nimport {\n useCallback,\n useEffect,\n useState,\n type AnchorHTMLAttributes,\n type ComponentType,\n type ReactNode,\n} from \"react\";\n\n// ---------------------------------------------------------------------------\n// Link abstraction\n// ---------------------------------------------------------------------------\n\n/**\n * Props passed to an injectable link component. The contract is a plain anchor\n * (`href`), so the default works with NO router. A React Router consumer maps\n * `href` → `to`; a Next.js consumer passes `next/link` directly (it already\n * accepts `href`, `className`, `children`).\n *\n * @stability Beta\n */\nexport interface GenesisLinkProps {\n /** Destination path or URL. */\n href: string;\n className?: string;\n onClick?: () => void;\n children?: ReactNode;\n /** Set to \"page\" when the link points at the active route. */\n \"aria-current\"?: AnchorHTMLAttributes<HTMLAnchorElement>[\"aria-current\"];\n /** Accessible name — useful when the visible label is hidden (icon rail). */\n \"aria-label\"?: string;\n /** Native tooltip / title attribute. */\n title?: string;\n /**\n * Stable test/automation selector. {@link DefaultLink} renders it as\n * `data-testid`; injected router adapters (`next/link`, RR `Link`) forward it\n * to the underlying `<a>` too.\n */\n testID?: string;\n}\n\n/**\n * A component that renders a navigable link from {@link GenesisLinkProps}.\n *\n * @stability Beta\n */\nexport type LinkComponent = ComponentType<GenesisLinkProps>;\n\n/**\n * Default link — a plain `<a>`. No router required.\n *\n * @stability Beta\n */\nexport const DefaultLink: LinkComponent = ({\n href,\n children,\n testID,\n ...rest\n}) => (\n <a href={href} data-testid={testID} {...rest}>\n {children}\n </a>\n);\n\n/**\n * Default active-path matcher, mirroring React Router's `NavLink` (prefix\n * match, with `/` treated as exact). Consumers can override via an `isActive`\n * prop where components accept one.\n *\n * @stability Beta\n */\nexport function isPathActive(to: string, activePath?: string): boolean {\n if (activePath == null) return false;\n if (to === activePath) return true;\n if (to !== \"/\" && activePath.startsWith(to + \"/\")) return true;\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Imperative navigation abstraction\n// ---------------------------------------------------------------------------\n\n/**\n * Injectable imperative navigation callback.\n *\n * @stability Beta\n */\nexport type NavigateFn = (path: string) => void;\n\n/**\n * Default navigate — a full-page navigation. No router required.\n *\n * @stability Beta\n */\nexport const defaultNavigate: NavigateFn = (path) => {\n if (typeof window !== \"undefined\") window.location.assign(path);\n};\n\n// ---------------------------------------------------------------------------\n// Search-params abstraction (for URL-synced hooks)\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal, router-agnostic replacement for React Router's `useSearchParams`\n * return shape. Consumers may inject their router's implementation for full\n * SPA integration; otherwise the browser History API is used.\n *\n * @stability Beta\n */\nexport interface SearchParamsAdapter {\n searchParams: URLSearchParams;\n setSearchParams: (\n next: URLSearchParams,\n options?: { replace?: boolean },\n ) => void;\n}\n\n/**\n * Browser-native {@link SearchParamsAdapter} built on the History API. Reads\n * the current query string and writes updates via `pushState`/`replaceState`,\n * staying reactive to `popstate` events. No router required.\n *\n * @stability Beta\n */\nexport function useBrowserSearchParams(): SearchParamsAdapter {\n const read = () =>\n new URLSearchParams(\n typeof window !== \"undefined\" ? window.location.search : \"\",\n );\n\n const [searchParams, setLocal] = useState<URLSearchParams>(read);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const onPop = () => setLocal(read());\n window.addEventListener(\"popstate\", onPop);\n return () => window.removeEventListener(\"popstate\", onPop);\n }, []);\n\n const setSearchParams = useCallback<SearchParamsAdapter[\"setSearchParams\"]>(\n (next, options) => {\n if (typeof window === \"undefined\") return;\n const qs = next.toString();\n const url =\n window.location.pathname + (qs ? `?${qs}` : \"\") + window.location.hash;\n if (options?.replace) {\n window.history.replaceState(window.history.state, \"\", url);\n } else {\n window.history.pushState(window.history.state, \"\", url);\n }\n setLocal(new URLSearchParams(next));\n },\n [],\n );\n\n return { searchParams, setSearchParams };\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/hooks/use-resource-page.ts"],"names":[],"mappings":"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","file":"chunk-GDFNAHI4.js","sourcesContent":["\"use client\";\n\nimport { useState, useMemo, useCallback, useEffect, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\nimport type { DataTableColumn } from \"../components/data/DataTable\";\nimport type { DataFilterConfig } from \"../components/data/DataFilters\";\nimport type { ViewMode } from \"../components/patterns/view-toggle\";\nimport type { Identifier, FilterParam, SortParam } from \"@marktiderman/genesis-core/provider\";\nimport { useViewPreference } from \"./use-view-preference\";\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/** Actions available inside column render functions. */\nexport interface ResourceActions<T> {\n update: (id: Identifier, data: Partial<T>) => Promise<T>;\n remove: (id: Identifier) => Promise<void>;\n refetch: () => void;\n}\n\n/** Column definition — full config form. */\nexport interface ResourceColumnDef<T> {\n key: string;\n header?: string;\n sortable?: boolean;\n hideBelow?: \"sm\" | \"md\" | \"lg\" | \"xl\";\n render?: (\n item: T,\n value: unknown,\n actions: ResourceActions<T>,\n ) => ReactNode;\n}\n\nexport interface UseResourcePageProps<T> {\n /** Resource name — used for view-preference storage key. */\n resource?: string;\n /** The data to display (pre-fetched or from useResource). */\n data: T[];\n /** Total count of records (before client-side filtering). */\n total: number;\n /** Column definitions. Strings are expanded to `{ key, header, sortable }`. Omit to auto-generate from data keys. */\n columns?: Array<string | ResourceColumnDef<T>>;\n /** Status filter config. String = field name (options auto-derived). Object = explicit field + options. */\n statusFilter?: string | { field: string; options?: string[] };\n /** Fields to search across. Defaults to all string-valued fields in data. */\n searchFields?: string[];\n /** Initial filter params applied on mount. */\n initialFilters?: FilterParam[];\n /** Callback when filters change. */\n onFiltersChange?: (filters: FilterParam[]) => void;\n /** Default view mode. */\n defaultView?: ViewMode;\n /** Detail interaction mode. */\n detail?: \"panel\" | \"modal\" | \"route\" | \"none\";\n /** Field used for the detail title. Defaults to first column key. */\n titleField?: string;\n /** Field used for the detail subtitle. Defaults to second column key. */\n subtitleField?: string;\n /**\n * Server-controlled mode. The `data` handed in has ALREADY been searched,\n * sorted and paginated by the server, so this hook must not do any of it a\n * second time — `data` is returned untouched and `total` is the server's\n * `total` rather than the length of a client-filtered slice.\n *\n * Search / sort / status state is still tracked (the filter UI stays\n * controlled and `onFiltersChange` still fires) — only the client-side\n * transform of `data` is skipped. Defaults to `false`, so every existing\n * caller keeps its current client-side behavior.\n */\n controlled?: boolean;\n}\n\nexport interface UseResourcePageReturn<T> {\n /** Filtered and sorted data for display. */\n data: T[];\n /** Total count (of the filtered set; the server's `total` when `controlled`). */\n total: number;\n /** Resolved column definitions. */\n columns: ResourceColumnDef<T>[];\n /** Columns mapped to DataTable format. */\n tableColumns: DataTableColumn<T>[];\n /** Ready-to-spread filter config for DataFilters. */\n filterConfig: {\n search: string;\n onSearchChange: (value: string) => void;\n sort: string;\n onSortChange: (sort: string) => void;\n sortOptions: Array<{ value: string; label: string }>;\n statusChips?: {\n options: string[];\n selected: string[];\n onChange: (values: string[]) => void;\n };\n };\n viewMode: ViewMode;\n setViewMode: (mode: ViewMode) => void;\n search: string;\n setSearch: (value: string) => void;\n sort: SortParam | null;\n setSort: (sort: SortParam | null) => void;\n statusValue: string[];\n setStatusValue: (values: string[]) => void;\n selectedItem: T | null;\n setSelectedItem: (item: T | null) => void;\n detailOpen: boolean;\n setDetailOpen: (open: boolean) => void;\n handleRowClick: (item: T) => void;\n titleField: string;\n subtitleField: string;\n statusOptions: string[];\n hasActiveFilters: boolean;\n clearFilters: () => void;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Convert a key string to title case.\n * \"created_at\" → \"Created At\"\n * \"firstName\" → \"First Name\"\n */\nexport function titleCase(str: string): string {\n // Split on underscores, hyphens, and camelCase boundaries\n const words = str\n .replace(/([a-z])([A-Z])/g, \"$1 $2\") // camelCase → camel Case\n .replace(/[_-]/g, \" \") // snake_case / kebab-case → spaces\n .split(/\\s+/)\n .filter(Boolean);\n\n return words\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())\n .join(\" \");\n}\n\n/**\n * Resolve column shorthand into full ResourceColumnDef objects.\n * - undefined + data present → auto-generate from Object.keys(data[0]), skip \"id\"\n * - undefined + empty data → empty array\n * - string items → { key, header: titleCase(key), sortable: true }\n * - object items → defaults applied\n */\nfunction resolveColumns<T>(\n columns: Array<string | ResourceColumnDef<T>> | undefined,\n data: T[],\n): ResourceColumnDef<T>[] {\n if (columns === undefined) {\n if (data.length === 0) return [];\n const keys = Object.keys(data[0] as Record<string, unknown>).filter(\n (k) => k !== \"id\",\n );\n return keys.map((key) => ({\n key,\n header: titleCase(key),\n sortable: true,\n }));\n }\n\n return columns.map((col) => {\n if (typeof col === \"string\") {\n return { key: col, header: titleCase(col), sortable: true };\n }\n return {\n ...col,\n header: col.header ?? titleCase(col.key),\n sortable: col.sortable ?? true,\n };\n });\n}\n\n/**\n * Derive the list of string fields in a data set (for default search).\n */\nfunction deriveStringFields<T>(data: T[]): string[] {\n if (data.length === 0) return [];\n const first = data[0] as Record<string, unknown>;\n return Object.keys(first).filter((k) => typeof first[k] === \"string\");\n}\n\n/**\n * Derive unique status options from a data array.\n */\nfunction deriveStatusOptions<T>(data: T[], field: string): string[] {\n const values = new Set<string>();\n for (const item of data) {\n const val = (item as Record<string, unknown>)[field];\n if (val != null) values.add(String(val));\n }\n return Array.from(values).sort();\n}\n\n/**\n * Client-side text search across multiple fields.\n */\nfunction matchesSearch<T>(\n item: T,\n query: string,\n fields: string[],\n): boolean {\n if (!query) return true;\n const lower = query.toLowerCase();\n const record = item as Record<string, unknown>;\n return fields.some((f) => {\n const val = record[f];\n return val != null && String(val).toLowerCase().includes(lower);\n });\n}\n\n/**\n * Client-side sort comparator.\n */\nfunction compareItems<T>(a: T, b: T, sort: SortParam): number {\n const ra = a as Record<string, unknown>;\n const rb = b as Record<string, unknown>;\n const va = ra[sort.field];\n const vb = rb[sort.field];\n const sa = va == null ? \"\" : String(va);\n const sb = vb == null ? \"\" : String(vb);\n const cmp = sa.localeCompare(sb, undefined, {\n numeric: true,\n sensitivity: \"base\",\n });\n return sort.order === \"asc\" ? cmp : -cmp;\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useResourcePage<T = Record<string, unknown>>(\n props: UseResourcePageProps<T>,\n): UseResourcePageReturn<T> {\n const {\n resource,\n data: rawData,\n total: rawTotal,\n columns: columnsProp,\n statusFilter: statusFilterProp,\n searchFields: searchFieldsProp,\n defaultView = \"table\",\n detail = \"panel\",\n titleField: titleFieldProp,\n subtitleField: subtitleFieldProp,\n initialFilters,\n onFiltersChange,\n controlled = false,\n } = props;\n\n // -- View mode --\n const [viewMode, setViewMode] = useViewPreference(\n resource ?? \"resource-page\",\n defaultView,\n );\n\n // -- Derive initial values from initialFilters --\n const initialSearch = useMemo(() => {\n if (!initialFilters) return \"\";\n const searchFilter = initialFilters.find(\n (f) => f.operator === \"contains\" && f.field === \"__search__\",\n );\n return searchFilter && \"value\" in searchFilter ? String(searchFilter.value ?? \"\") : \"\";\n }, []); // eslint-disable-line react-hooks/exhaustive-deps -- only on mount\n\n const initialSort = useMemo(() => {\n if (!initialFilters) return null;\n const sortFilter = initialFilters.find(\n (f) => f.field === \"__sort__\",\n );\n if (!sortFilter || !(\"value\" in sortFilter) || typeof sortFilter.value !== \"string\") return null;\n const [field, order] = sortFilter.value.split(\":\");\n if (!field || (order !== \"asc\" && order !== \"desc\")) return null;\n return { field, order } as SortParam;\n }, []); // eslint-disable-line react-hooks/exhaustive-deps -- only on mount\n\n const initialStatus = useMemo(() => {\n if (!initialFilters) return [];\n const statusFilter = initialFilters.find(\n (f) => f.operator === \"in\" && f.field === \"__status__\",\n );\n if (!statusFilter || statusFilter.operator !== \"in\") return [];\n return statusFilter.value.map((v) => String(v));\n }, []); // eslint-disable-line react-hooks/exhaustive-deps -- only on mount\n\n // -- Search --\n const [search, setSearch] = useState(initialSearch);\n\n // -- Sort --\n const [sort, setSort] = useState<SortParam | null>(initialSort);\n\n // -- Status filter --\n const [statusValue, setStatusValue] = useState<string[]>(initialStatus);\n\n // -- Detail panel --\n const [selectedItem, setSelectedItem] = useState<T | null>(null);\n const [detailOpen, setDetailOpen] = useState(false);\n\n // -- Resolve columns --\n const resolvedColumns = useMemo(\n () => resolveColumns(columnsProp, rawData),\n [columnsProp, rawData],\n );\n\n // -- Title / subtitle fields --\n const titleField =\n titleFieldProp ?? resolvedColumns[0]?.key ?? \"name\";\n const subtitleField =\n subtitleFieldProp ?? resolvedColumns[1]?.key ?? \"\";\n\n // -- Status filter config --\n const statusFieldName = useMemo(() => {\n if (!statusFilterProp) return null;\n return typeof statusFilterProp === \"string\"\n ? statusFilterProp\n : statusFilterProp.field;\n }, [statusFilterProp]);\n\n const statusOptions = useMemo(() => {\n if (!statusFilterProp) return [];\n if (typeof statusFilterProp === \"object\" && statusFilterProp.options) {\n return statusFilterProp.options;\n }\n if (!statusFieldName) return [];\n return deriveStatusOptions(rawData, statusFieldName);\n }, [statusFilterProp, statusFieldName, rawData]);\n\n // -- Search fields --\n const searchFields = useMemo(\n () => searchFieldsProp ?? deriveStringFields(rawData),\n [searchFieldsProp, rawData],\n );\n\n // -- Client-side filtering + sorting --\n // Skipped entirely in controlled mode: the server already applied the\n // search, the sort and the page window, so re-running them here would\n // filter the current page against a query the server has already honoured\n // (and re-sort a slice of rows that is not the full result set).\n const filteredData = useMemo(() => {\n if (controlled) return rawData;\n\n let result = rawData;\n\n // Text search\n if (search) {\n result = result.filter((item) =>\n matchesSearch(item, search, searchFields),\n );\n }\n\n // Status filter\n if (statusFieldName && statusValue.length > 0) {\n result = result.filter((item) => {\n const val = (item as Record<string, unknown>)[statusFieldName];\n return val != null && statusValue.includes(String(val));\n });\n }\n\n // Sort\n if (sort) {\n result = [...result].sort((a, b) => compareItems(a, b, sort));\n }\n\n return result;\n }, [controlled, rawData, search, searchFields, statusFieldName, statusValue, sort]);\n\n // -- Sort options (built from resolved columns) --\n const sortOptions = useMemo(() => {\n const opts: Array<{ value: string; label: string }> = [];\n for (const col of resolvedColumns) {\n if (col.sortable !== false) {\n opts.push({\n value: `${col.key}:asc`,\n label: `${col.header ?? titleCase(col.key)} (A-Z)`,\n });\n opts.push({\n value: `${col.key}:desc`,\n label: `${col.header ?? titleCase(col.key)} (Z-A)`,\n });\n }\n }\n return opts;\n }, [resolvedColumns]);\n\n // -- Sort string for DataFilters select --\n const sortString = sort ? `${sort.field}:${sort.order}` : \"\";\n\n const handleSortChange = useCallback((value: string) => {\n if (!value) {\n setSort(null);\n return;\n }\n const [field, order] = value.split(\":\");\n setSort({ field, order: order as \"asc\" | \"desc\" });\n }, []);\n\n // -- Map to DataTableColumn --\n const tableColumns: DataTableColumn<T>[] = useMemo(\n () =>\n resolvedColumns.map((col) => ({\n key: col.key,\n header: col.header ?? titleCase(col.key),\n sortable: col.sortable ?? true,\n hideBelow: col.hideBelow,\n render: col.render\n ? (item: T, value: unknown) =>\n col.render!(item, value, {\n update: async () => {\n throw new Error(\n \"ResourceActions.update not available in server-data mode\",\n );\n },\n remove: async () => {\n throw new Error(\n \"ResourceActions.remove not available in server-data mode\",\n );\n },\n refetch: () => {\n /* no-op in server-data mode */\n },\n })\n : undefined,\n })),\n [resolvedColumns],\n );\n\n // -- Active filters check --\n const hasActiveFilters =\n search !== \"\" || statusValue.length > 0 || sort !== null;\n\n const clearFilters = useCallback(() => {\n setSearch(\"\");\n setStatusValue([]);\n setSort(null);\n }, []);\n\n // -- Notify parent of filter changes --\n const onFiltersChangeRef = useRef(onFiltersChange);\n onFiltersChangeRef.current = onFiltersChange;\n\n useEffect(() => {\n if (!onFiltersChangeRef.current) return;\n const filters: FilterParam[] = [];\n if (search) {\n filters.push({ field: \"__search__\", operator: \"contains\", value: search });\n }\n if (sort) {\n filters.push({ field: \"__sort__\", operator: \"eq\", value: `${sort.field}:${sort.order}` });\n }\n if (statusValue.length > 0) {\n filters.push({ field: \"__status__\", operator: \"in\", value: statusValue });\n }\n onFiltersChangeRef.current(filters);\n }, [search, sort, statusValue]);\n\n // -- Row click --\n const handleRowClick = useCallback(\n (item: T) => {\n if (detail === \"none\") return;\n setSelectedItem(item);\n setDetailOpen(true);\n },\n [detail],\n );\n\n // -- Filter config (ready to spread into DataFilters) --\n const filterConfig = useMemo(\n () => ({\n search,\n onSearchChange: setSearch,\n sort: sortString,\n onSortChange: handleSortChange,\n sortOptions,\n ...(statusFieldName && statusOptions.length > 0\n ? {\n statusChips: {\n options: statusOptions,\n selected: statusValue,\n onChange: setStatusValue,\n },\n }\n : {}),\n }),\n [\n search,\n sortString,\n handleSortChange,\n sortOptions,\n statusFieldName,\n statusOptions,\n statusValue,\n ],\n );\n\n return {\n data: filteredData,\n // Controlled mode: `filteredData` is one server page, so its length is\n // not the record count — the server's total is.\n total: controlled ? rawTotal : filteredData.length,\n columns: resolvedColumns,\n tableColumns,\n filterConfig,\n viewMode,\n setViewMode,\n search,\n setSearch,\n sort,\n setSort,\n statusValue,\n setStatusValue,\n selectedItem,\n setSelectedItem,\n detailOpen,\n setDetailOpen,\n handleRowClick,\n titleField,\n subtitleField,\n statusOptions,\n hasActiveFilters,\n clearFilters,\n };\n}\n"]}