@lateralus-ai/shipping-ui 2.0.0-dev.20 → 2.0.0-dev.22

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 (71) hide show
  1. package/dist/domain/Filters/FilterDropdown.d.ts +93 -0
  2. package/dist/domain/Filters/FilterPill.d.ts +12 -5
  3. package/dist/domain/Filters/FilterPills.d.ts +18 -0
  4. package/dist/domain/Filters/FilteredPill.d.ts +15 -5
  5. package/dist/domain/Filters/formatActiveFilterChipLabel.d.ts +11 -0
  6. package/dist/domain/Filters/index.d.ts +8 -3
  7. package/dist/index.cjs +33 -33
  8. package/dist/index.esm.js +7300 -5990
  9. package/package.json +2 -1
  10. package/src/components/EmptyState.tsx +44 -44
  11. package/src/components/Modal.tsx +141 -141
  12. package/src/components/PageHeader.tsx +132 -132
  13. package/src/components/ScrollableList.tsx +61 -61
  14. package/src/components/Tabs.tsx +146 -146
  15. package/src/components/index.ts +56 -56
  16. package/src/domain/Filters/FilterDropdown.tsx +438 -0
  17. package/src/domain/Filters/FilterPill.tsx +54 -18
  18. package/src/domain/Filters/FilterPills.tsx +34 -0
  19. package/src/domain/Filters/FilteredPill.tsx +48 -28
  20. package/src/domain/Filters/FiltersBar.tsx +15 -19
  21. package/src/domain/Filters/formatActiveFilterChipLabel.ts +23 -0
  22. package/src/domain/Filters/index.ts +35 -3
  23. package/src/icons/arrow-paths.ts +8 -8
  24. package/src/icons/chevron-paths.ts +17 -17
  25. package/src/icons/icons-data.ts +656 -656
  26. package/src/index.ts +85 -85
  27. package/src/patterns/Search/ResultRow.tsx +44 -44
  28. package/src/patterns/Search/SearchModal.tsx +310 -310
  29. package/src/patterns/Search/index.ts +31 -31
  30. package/src/patterns/Sidebar/assets/logo-compliance-mark.png +5 -5
  31. package/src/patterns/Sidebar/assets/logo-compliance-mask.png +13 -13
  32. package/src/patterns/Sidebar/assets/logo-compliance.svg +17 -17
  33. package/src/patterns/Sidebar/assets/logo-technical-avatar.svg +17 -17
  34. package/src/patterns/Sidebar/assets/logo-technical-mark.png +5 -5
  35. package/src/patterns/Sidebar/assets/logo-technical.svg +16 -16
  36. package/src/patterns/Skeleton/Skeleton.tsx +56 -56
  37. package/src/primitives/button-styles.ts +92 -92
  38. package/src/stories/canvases/ButtonsCanvas.tsx +107 -107
  39. package/src/stories/canvases/ButtonsMatrixCanvas.tsx +65 -65
  40. package/src/stories/canvases/ContentCanvas.tsx +353 -353
  41. package/src/stories/canvases/FiltersCanvas.tsx +156 -30
  42. package/src/stories/canvases/SearchCanvas.tsx +150 -150
  43. package/src/stories/canvases/figma-buttons-layout.ts +83 -83
  44. package/src/stories/canvases/figma-widths.ts +28 -28
  45. package/src/stories/canvases/helpers.tsx +146 -146
  46. package/src/stories/components/Buttons.stories.tsx +11 -11
  47. package/src/stories/components/Chat.stories.tsx +11 -11
  48. package/src/stories/components/Content.stories.tsx +11 -11
  49. package/src/stories/components/Core.stories.tsx +11 -11
  50. package/src/stories/components/DomainForms.stories.tsx +11 -11
  51. package/src/stories/components/Filters.stories.tsx +11 -11
  52. package/src/stories/components/Forms.stories.tsx +11 -11
  53. package/src/stories/components/Icons.stories.tsx +11 -11
  54. package/src/stories/components/Illustrations.stories.tsx +11 -11
  55. package/src/stories/components/Library.stories.tsx +11 -11
  56. package/src/stories/components/Modals.stories.tsx +11 -11
  57. package/src/stories/components/Report.stories.tsx +11 -11
  58. package/src/stories/components/ReportLayout.stories.tsx +11 -11
  59. package/src/stories/components/Search.stories.tsx +11 -11
  60. package/src/stories/components/Settings.stories.tsx +11 -11
  61. package/src/stories/components/Ships.stories.tsx +11 -11
  62. package/src/stories/components/SidebarLayouts.stories.tsx +11 -11
  63. package/src/stories/components/Skeletons.stories.tsx +11 -11
  64. package/src/stories/components/Workflows.stories.tsx +11 -11
  65. package/src/stories/style-guide/Buttons.stories.tsx +11 -11
  66. package/src/stories/style-guide/ColorTokens.stories.tsx +11 -11
  67. package/src/stories/style-guide/Colors.stories.tsx +11 -11
  68. package/src/stories/style-guide/RaiseLevels.stories.tsx +11 -11
  69. package/src/stories/style-guide/Typography.stories.tsx +11 -11
  70. package/src/tailwind-theme.ts +186 -186
  71. package/src/theme-entry.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lateralus-ai/shipping-ui",
3
- "version": "2.0.0-dev.20",
3
+ "version": "2.0.0-dev.22",
4
4
  "description": "Shared UI theme and components for Lateralus shipping applications",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.esm.js",
@@ -112,6 +112,7 @@
112
112
  "dependencies": {
113
113
  "@radix-ui/react-dialog": "^1.1.19",
114
114
  "@radix-ui/react-dropdown-menu": "^2.1.20",
115
+ "@radix-ui/react-popover": "^1.1.15",
115
116
  "@radix-ui/react-tabs": "^1.1.17",
116
117
  "@radix-ui/react-tooltip": "^1.2.8",
117
118
  "@react-hooks-library/core": "^0.6.2",
@@ -1,44 +1,44 @@
1
- import { type ReactNode } from "react";
2
- import { Button, type ButtonProps } from "../primitives";
3
- import { cn } from "../utils/cn";
4
-
5
- export type EmptyStateProps = {
6
- illustration?: ReactNode;
7
- title: string;
8
- description?: string;
9
- action?: Pick<ButtonProps, "children" | "onClick" | "hierarchy" | "icon" | "startIcon">;
10
- className?: string;
11
- };
12
-
13
- export const EmptyState = ({
14
- illustration,
15
- title,
16
- description,
17
- action,
18
- className,
19
- }: EmptyStateProps) => (
20
- <div
21
- className={cn(
22
- "flex max-w-sm flex-col items-center gap-4 px-6 py-10 text-center",
23
- className,
24
- )}
25
- >
26
- {illustration && <div className="shrink-0">{illustration}</div>}
27
- <div className="flex flex-col gap-1">
28
- <h3 className="text-subheader-em text-display-on-light-primary">{title}</h3>
29
- {description && (
30
- <p className="text-body text-display-on-light-secondary">{description}</p>
31
- )}
32
- </div>
33
- {action && (
34
- <Button
35
- hierarchy={action.hierarchy ?? "primary"}
36
- onClick={action.onClick}
37
- icon={action.icon}
38
- startIcon={action.startIcon}
39
- >
40
- {action.children}
41
- </Button>
42
- )}
43
- </div>
44
- );
1
+ import { type ReactNode } from "react";
2
+ import { Button, type ButtonProps } from "../primitives";
3
+ import { cn } from "../utils/cn";
4
+
5
+ export type EmptyStateProps = {
6
+ illustration?: ReactNode;
7
+ title: string;
8
+ description?: string;
9
+ action?: Pick<ButtonProps, "children" | "onClick" | "hierarchy" | "icon" | "startIcon">;
10
+ className?: string;
11
+ };
12
+
13
+ export const EmptyState = ({
14
+ illustration,
15
+ title,
16
+ description,
17
+ action,
18
+ className,
19
+ }: EmptyStateProps) => (
20
+ <div
21
+ className={cn(
22
+ "flex max-w-sm flex-col items-center gap-4 px-6 py-10 text-center",
23
+ className,
24
+ )}
25
+ >
26
+ {illustration && <div className="shrink-0">{illustration}</div>}
27
+ <div className="flex flex-col gap-1">
28
+ <h3 className="text-subheader-em text-display-on-light-primary">{title}</h3>
29
+ {description && (
30
+ <p className="text-body text-display-on-light-secondary">{description}</p>
31
+ )}
32
+ </div>
33
+ {action && (
34
+ <Button
35
+ hierarchy={action.hierarchy ?? "primary"}
36
+ onClick={action.onClick}
37
+ icon={action.icon}
38
+ startIcon={action.startIcon}
39
+ >
40
+ {action.children}
41
+ </Button>
42
+ )}
43
+ </div>
44
+ );
@@ -1,141 +1,141 @@
1
- import * as DialogPrimitive from "@radix-ui/react-dialog";
2
- import {
3
- forwardRef,
4
- type ComponentPropsWithoutRef,
5
- type ElementRef,
6
- type ReactNode,
7
- } from "react";
8
- import { cn } from "../utils/cn";
9
-
10
- export type ModalProps = ComponentPropsWithoutRef<typeof DialogPrimitive.Root>;
11
-
12
- /** Generic dialog shell — portal, backdrop, focus trap, Escape. */
13
- export const Modal = DialogPrimitive.Root;
14
-
15
- export const ModalTrigger = DialogPrimitive.Trigger;
16
-
17
- export const ModalClose = DialogPrimitive.Close;
18
-
19
- export type ModalPortalProps = ComponentPropsWithoutRef<
20
- typeof DialogPrimitive.Portal
21
- >;
22
-
23
- export const ModalPortal = DialogPrimitive.Portal;
24
-
25
- export type ModalOverlayProps = ComponentPropsWithoutRef<
26
- typeof DialogPrimitive.Overlay
27
- >;
28
-
29
- /** Dark gray backdrop (app convention; Figma search frames omit overlay). */
30
- export const ModalOverlay = forwardRef<
31
- ElementRef<typeof DialogPrimitive.Overlay>,
32
- ModalOverlayProps
33
- >(({ className, ...props }, ref) => (
34
- <DialogPrimitive.Overlay
35
- ref={ref}
36
- className={cn(
37
- "fixed inset-0 z-[999] bg-grey-900/65 transition-opacity",
38
- "data-[state=open]:animate-in data-[state=open]:fade-in",
39
- "data-[state=closed]:animate-out data-[state=closed]:fade-out",
40
- className,
41
- )}
42
- {...props}
43
- />
44
- ));
45
- ModalOverlay.displayName = DialogPrimitive.Overlay.displayName;
46
-
47
- export type ModalContentProps = ComponentPropsWithoutRef<
48
- typeof DialogPrimitive.Content
49
- > & {
50
- /** When false, backdrop is omitted. Defaults to true. */
51
- showOverlay?: boolean;
52
- overlayClassName?: string;
53
- };
54
-
55
- export const ModalContent = forwardRef<
56
- ElementRef<typeof DialogPrimitive.Content>,
57
- ModalContentProps
58
- >(
59
- (
60
- {
61
- className,
62
- children,
63
- showOverlay = true,
64
- overlayClassName,
65
- ...props
66
- },
67
- ref,
68
- ) => (
69
- <ModalPortal>
70
- {showOverlay && <ModalOverlay className={overlayClassName} />}
71
- <DialogPrimitive.Content
72
- ref={ref}
73
- className={cn(
74
- "fixed left-1/2 top-1/2 z-[999] w-full max-w-xl -translate-x-1/2 -translate-y-1/2",
75
- "overflow-hidden rounded-xl border border-divider-primary bg-background-primary shadow-raise3",
76
- "outline-none",
77
- "data-[state=open]:animate-in data-[state=open]:fade-in-90",
78
- "data-[state=closed]:animate-out data-[state=closed]:fade-out",
79
- className,
80
- )}
81
- {...props}
82
- >
83
- {children}
84
- </DialogPrimitive.Content>
85
- </ModalPortal>
86
- ),
87
- );
88
- ModalContent.displayName = DialogPrimitive.Content.displayName;
89
-
90
- export type ModalTitleProps = ComponentPropsWithoutRef<
91
- typeof DialogPrimitive.Title
92
- >;
93
-
94
- export const ModalTitle = forwardRef<
95
- ElementRef<typeof DialogPrimitive.Title>,
96
- ModalTitleProps
97
- >(({ className, ...props }, ref) => (
98
- <DialogPrimitive.Title
99
- ref={ref}
100
- className={cn("text-subheader-em text-display-on-light-primary", className)}
101
- {...props}
102
- />
103
- ));
104
- ModalTitle.displayName = DialogPrimitive.Title.displayName;
105
-
106
- export type ModalDescriptionProps = ComponentPropsWithoutRef<
107
- typeof DialogPrimitive.Description
108
- >;
109
-
110
- export const ModalDescription = forwardRef<
111
- ElementRef<typeof DialogPrimitive.Description>,
112
- ModalDescriptionProps
113
- >(({ className, ...props }, ref) => (
114
- <DialogPrimitive.Description
115
- ref={ref}
116
- className={cn("text-caption-2 text-display-on-light-secondary", className)}
117
- {...props}
118
- />
119
- ));
120
- ModalDescription.displayName = DialogPrimitive.Description.displayName;
121
-
122
- export type ModalHeaderSlotProps = {
123
- children: ReactNode;
124
- className?: string;
125
- };
126
-
127
- export const ModalHeaderSlot = ({
128
- children,
129
- className,
130
- }: ModalHeaderSlotProps) => (
131
- <div className={cn("shrink-0", className)}>{children}</div>
132
- );
133
-
134
- export type ModalBodyProps = {
135
- children: ReactNode;
136
- className?: string;
137
- };
138
-
139
- export const ModalBody = ({ children, className }: ModalBodyProps) => (
140
- <div className={cn("min-h-0 overflow-auto", className)}>{children}</div>
141
- );
1
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
2
+ import {
3
+ forwardRef,
4
+ type ComponentPropsWithoutRef,
5
+ type ElementRef,
6
+ type ReactNode,
7
+ } from "react";
8
+ import { cn } from "../utils/cn";
9
+
10
+ export type ModalProps = ComponentPropsWithoutRef<typeof DialogPrimitive.Root>;
11
+
12
+ /** Generic dialog shell — portal, backdrop, focus trap, Escape. */
13
+ export const Modal = DialogPrimitive.Root;
14
+
15
+ export const ModalTrigger = DialogPrimitive.Trigger;
16
+
17
+ export const ModalClose = DialogPrimitive.Close;
18
+
19
+ export type ModalPortalProps = ComponentPropsWithoutRef<
20
+ typeof DialogPrimitive.Portal
21
+ >;
22
+
23
+ export const ModalPortal = DialogPrimitive.Portal;
24
+
25
+ export type ModalOverlayProps = ComponentPropsWithoutRef<
26
+ typeof DialogPrimitive.Overlay
27
+ >;
28
+
29
+ /** Dark gray backdrop (app convention; Figma search frames omit overlay). */
30
+ export const ModalOverlay = forwardRef<
31
+ ElementRef<typeof DialogPrimitive.Overlay>,
32
+ ModalOverlayProps
33
+ >(({ className, ...props }, ref) => (
34
+ <DialogPrimitive.Overlay
35
+ ref={ref}
36
+ className={cn(
37
+ "fixed inset-0 z-[999] bg-grey-900/65 transition-opacity",
38
+ "data-[state=open]:animate-in data-[state=open]:fade-in",
39
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out",
40
+ className,
41
+ )}
42
+ {...props}
43
+ />
44
+ ));
45
+ ModalOverlay.displayName = DialogPrimitive.Overlay.displayName;
46
+
47
+ export type ModalContentProps = ComponentPropsWithoutRef<
48
+ typeof DialogPrimitive.Content
49
+ > & {
50
+ /** When false, backdrop is omitted. Defaults to true. */
51
+ showOverlay?: boolean;
52
+ overlayClassName?: string;
53
+ };
54
+
55
+ export const ModalContent = forwardRef<
56
+ ElementRef<typeof DialogPrimitive.Content>,
57
+ ModalContentProps
58
+ >(
59
+ (
60
+ {
61
+ className,
62
+ children,
63
+ showOverlay = true,
64
+ overlayClassName,
65
+ ...props
66
+ },
67
+ ref,
68
+ ) => (
69
+ <ModalPortal>
70
+ {showOverlay && <ModalOverlay className={overlayClassName} />}
71
+ <DialogPrimitive.Content
72
+ ref={ref}
73
+ className={cn(
74
+ "fixed left-1/2 top-1/2 z-[999] w-full max-w-xl -translate-x-1/2 -translate-y-1/2",
75
+ "overflow-hidden rounded-xl border border-divider-primary bg-background-primary shadow-raise3",
76
+ "outline-none",
77
+ "data-[state=open]:animate-in data-[state=open]:fade-in-90",
78
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out",
79
+ className,
80
+ )}
81
+ {...props}
82
+ >
83
+ {children}
84
+ </DialogPrimitive.Content>
85
+ </ModalPortal>
86
+ ),
87
+ );
88
+ ModalContent.displayName = DialogPrimitive.Content.displayName;
89
+
90
+ export type ModalTitleProps = ComponentPropsWithoutRef<
91
+ typeof DialogPrimitive.Title
92
+ >;
93
+
94
+ export const ModalTitle = forwardRef<
95
+ ElementRef<typeof DialogPrimitive.Title>,
96
+ ModalTitleProps
97
+ >(({ className, ...props }, ref) => (
98
+ <DialogPrimitive.Title
99
+ ref={ref}
100
+ className={cn("text-subheader-em text-display-on-light-primary", className)}
101
+ {...props}
102
+ />
103
+ ));
104
+ ModalTitle.displayName = DialogPrimitive.Title.displayName;
105
+
106
+ export type ModalDescriptionProps = ComponentPropsWithoutRef<
107
+ typeof DialogPrimitive.Description
108
+ >;
109
+
110
+ export const ModalDescription = forwardRef<
111
+ ElementRef<typeof DialogPrimitive.Description>,
112
+ ModalDescriptionProps
113
+ >(({ className, ...props }, ref) => (
114
+ <DialogPrimitive.Description
115
+ ref={ref}
116
+ className={cn("text-caption-2 text-display-on-light-secondary", className)}
117
+ {...props}
118
+ />
119
+ ));
120
+ ModalDescription.displayName = DialogPrimitive.Description.displayName;
121
+
122
+ export type ModalHeaderSlotProps = {
123
+ children: ReactNode;
124
+ className?: string;
125
+ };
126
+
127
+ export const ModalHeaderSlot = ({
128
+ children,
129
+ className,
130
+ }: ModalHeaderSlotProps) => (
131
+ <div className={cn("shrink-0", className)}>{children}</div>
132
+ );
133
+
134
+ export type ModalBodyProps = {
135
+ children: ReactNode;
136
+ className?: string;
137
+ };
138
+
139
+ export const ModalBody = ({ children, className }: ModalBodyProps) => (
140
+ <div className={cn("min-h-0 overflow-auto", className)}>{children}</div>
141
+ );
@@ -1,132 +1,132 @@
1
- import {
2
- type ComponentPropsWithoutRef,
3
- type MouseEvent,
4
- type ReactNode,
5
- } from "react";
6
- import { cn } from "../utils/cn";
7
-
8
- export type PageHeaderCrumb = {
9
- label: string;
10
- /** Renders a real `<a href>` so native browser controls work. */
11
- href: string;
12
- /**
13
- * Optional SPA handler. Callers should `preventDefault` only for unmodified
14
- * primary clicks; modifier / middle-click keep native anchor behavior.
15
- */
16
- onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
17
- };
18
-
19
- export type PageHeaderProps = {
20
- /** Leading icon (Figma 24×24). */
21
- icon?: ReactNode;
22
- /** Standard title, or current (last) segment when `crumbs` is set. */
23
- title: ReactNode;
24
- /**
25
- * Parent breadcrumb links. When present, renders Nested: icon + parents + `/`
26
- * + current `title` (non-link).
27
- */
28
- crumbs?: PageHeaderCrumb[];
29
- /** Right-hand actions slot (buttons, menus, etc.). */
30
- actions?: ReactNode;
31
- className?: string;
32
- };
33
-
34
- export type PageHeaderShellProps = ComponentPropsWithoutRef<"div">;
35
-
36
- export type PageHeaderBodyProps = ComponentPropsWithoutRef<"div">;
37
-
38
- /**
39
- * Page title row — Figma `Header` (389:8850).
40
- * Variants: Standard (icon + title) | Nested (icon + crumb trail).
41
- */
42
- function PageHeaderRoot({
43
- icon,
44
- title,
45
- crumbs,
46
- actions,
47
- className,
48
- }: PageHeaderProps) {
49
- const isNested = Boolean(crumbs?.length);
50
-
51
- return (
52
- <div
53
- data-variant={isNested ? "nested" : "standard"}
54
- className={cn(
55
- "flex w-full shrink-0 items-center justify-between gap-4",
56
- "min-h-10",
57
- className,
58
- )}
59
- >
60
- <div className="flex min-w-0 flex-1 items-center gap-2">
61
- {icon != null && (
62
- <span className="inline-flex size-6 shrink-0 items-center justify-center [&>svg]:size-6">
63
- {icon}
64
- </span>
65
- )}
66
-
67
- {isNested ? (
68
- <nav aria-label="Breadcrumb" className="min-w-0">
69
- <ol className="flex min-w-0 flex-wrap items-center gap-2">
70
- {crumbs!.map((crumb) => (
71
- <li key={`${crumb.href}-${crumb.label}`} className="contents">
72
- <a
73
- href={crumb.href}
74
- onClick={crumb.onClick}
75
- className="truncate text-caption-1-em text-display-on-light-secondary transition-colors hover:text-display-on-light-primary"
76
- >
77
- {crumb.label}
78
- </a>
79
- <span
80
- aria-hidden
81
- className="text-caption-1-em text-display-on-light-secondary"
82
- >
83
- /
84
- </span>
85
- </li>
86
- ))}
87
- <li
88
- aria-current="page"
89
- className="min-w-0 truncate text-caption-1-em text-display-on-light-primary"
90
- >
91
- {title}
92
- </li>
93
- </ol>
94
- </nav>
95
- ) : (
96
- <h1 className="min-w-0 truncate text-caption-1-em text-display-on-light-primary">
97
- {title}
98
- </h1>
99
- )}
100
- </div>
101
-
102
- {actions != null && (
103
- <div className="flex shrink-0 items-center gap-2">{actions}</div>
104
- )}
105
- </div>
106
- );
107
- }
108
-
109
- /** Flex column shell — pin header, scroll body (avoids brittle CSS sticky). */
110
- function PageHeaderShell({ className, ...props }: PageHeaderShellProps) {
111
- return (
112
- <div
113
- className={cn("flex h-full min-h-0 flex-col", className)}
114
- {...props}
115
- />
116
- );
117
- }
118
-
119
- /** Scrollable page content under a pinned PageHeader. */
120
- function PageHeaderBody({ className, ...props }: PageHeaderBodyProps) {
121
- return (
122
- <div
123
- className={cn("min-h-0 flex-1 overflow-y-auto", className)}
124
- {...props}
125
- />
126
- );
127
- }
128
-
129
- export const PageHeader = Object.assign(PageHeaderRoot, {
130
- Shell: PageHeaderShell,
131
- Body: PageHeaderBody,
132
- });
1
+ import {
2
+ type ComponentPropsWithoutRef,
3
+ type MouseEvent,
4
+ type ReactNode,
5
+ } from "react";
6
+ import { cn } from "../utils/cn";
7
+
8
+ export type PageHeaderCrumb = {
9
+ label: string;
10
+ /** Renders a real `<a href>` so native browser controls work. */
11
+ href: string;
12
+ /**
13
+ * Optional SPA handler. Callers should `preventDefault` only for unmodified
14
+ * primary clicks; modifier / middle-click keep native anchor behavior.
15
+ */
16
+ onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
17
+ };
18
+
19
+ export type PageHeaderProps = {
20
+ /** Leading icon (Figma 24×24). */
21
+ icon?: ReactNode;
22
+ /** Standard title, or current (last) segment when `crumbs` is set. */
23
+ title: ReactNode;
24
+ /**
25
+ * Parent breadcrumb links. When present, renders Nested: icon + parents + `/`
26
+ * + current `title` (non-link).
27
+ */
28
+ crumbs?: PageHeaderCrumb[];
29
+ /** Right-hand actions slot (buttons, menus, etc.). */
30
+ actions?: ReactNode;
31
+ className?: string;
32
+ };
33
+
34
+ export type PageHeaderShellProps = ComponentPropsWithoutRef<"div">;
35
+
36
+ export type PageHeaderBodyProps = ComponentPropsWithoutRef<"div">;
37
+
38
+ /**
39
+ * Page title row — Figma `Header` (389:8850).
40
+ * Variants: Standard (icon + title) | Nested (icon + crumb trail).
41
+ */
42
+ function PageHeaderRoot({
43
+ icon,
44
+ title,
45
+ crumbs,
46
+ actions,
47
+ className,
48
+ }: PageHeaderProps) {
49
+ const isNested = Boolean(crumbs?.length);
50
+
51
+ return (
52
+ <div
53
+ data-variant={isNested ? "nested" : "standard"}
54
+ className={cn(
55
+ "flex w-full shrink-0 items-center justify-between gap-4",
56
+ "min-h-10",
57
+ className,
58
+ )}
59
+ >
60
+ <div className="flex min-w-0 flex-1 items-center gap-2">
61
+ {icon != null && (
62
+ <span className="inline-flex size-6 shrink-0 items-center justify-center [&>svg]:size-6">
63
+ {icon}
64
+ </span>
65
+ )}
66
+
67
+ {isNested ? (
68
+ <nav aria-label="Breadcrumb" className="min-w-0">
69
+ <ol className="flex min-w-0 flex-wrap items-center gap-2">
70
+ {crumbs!.map((crumb) => (
71
+ <li key={`${crumb.href}-${crumb.label}`} className="contents">
72
+ <a
73
+ href={crumb.href}
74
+ onClick={crumb.onClick}
75
+ className="truncate text-caption-1-em text-display-on-light-secondary transition-colors hover:text-display-on-light-primary"
76
+ >
77
+ {crumb.label}
78
+ </a>
79
+ <span
80
+ aria-hidden
81
+ className="text-caption-1-em text-display-on-light-secondary"
82
+ >
83
+ /
84
+ </span>
85
+ </li>
86
+ ))}
87
+ <li
88
+ aria-current="page"
89
+ className="min-w-0 truncate text-caption-1-em text-display-on-light-primary"
90
+ >
91
+ {title}
92
+ </li>
93
+ </ol>
94
+ </nav>
95
+ ) : (
96
+ <h1 className="min-w-0 truncate text-caption-1-em text-display-on-light-primary">
97
+ {title}
98
+ </h1>
99
+ )}
100
+ </div>
101
+
102
+ {actions != null && (
103
+ <div className="flex shrink-0 items-center gap-2">{actions}</div>
104
+ )}
105
+ </div>
106
+ );
107
+ }
108
+
109
+ /** Flex column shell — pin header, scroll body (avoids brittle CSS sticky). */
110
+ function PageHeaderShell({ className, ...props }: PageHeaderShellProps) {
111
+ return (
112
+ <div
113
+ className={cn("flex h-full min-h-0 flex-col", className)}
114
+ {...props}
115
+ />
116
+ );
117
+ }
118
+
119
+ /** Scrollable page content under a pinned PageHeader. */
120
+ function PageHeaderBody({ className, ...props }: PageHeaderBodyProps) {
121
+ return (
122
+ <div
123
+ className={cn("min-h-0 flex-1 overflow-y-auto", className)}
124
+ {...props}
125
+ />
126
+ );
127
+ }
128
+
129
+ export const PageHeader = Object.assign(PageHeaderRoot, {
130
+ Shell: PageHeaderShell,
131
+ Body: PageHeaderBody,
132
+ });