@calliopelabs/ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +332 -2
  3. package/dist/appearance/AppearancePicker.d.ts +10 -0
  4. package/dist/appearance/AppearancePicker.js +75 -0
  5. package/dist/appearance/AppearanceProvider.d.ts +53 -0
  6. package/dist/appearance/AppearanceProvider.js +113 -0
  7. package/dist/appearance/AppearanceScript.d.ts +16 -0
  8. package/dist/appearance/AppearanceScript.js +14 -0
  9. package/dist/appearance/appearance.d.ts +90 -0
  10. package/dist/appearance/appearance.js +151 -0
  11. package/dist/appearance/boot.d.ts +30 -0
  12. package/dist/appearance/boot.js +91 -0
  13. package/dist/appearance/brands.d.ts +94 -0
  14. package/dist/appearance/brands.js +61 -0
  15. package/dist/appearance/product.d.ts +89 -0
  16. package/dist/appearance/product.js +96 -0
  17. package/dist/data/DataTable.d.ts +66 -0
  18. package/dist/data/DataTable.js +148 -0
  19. package/dist/data/List.d.ts +55 -0
  20. package/dist/data/List.js +41 -0
  21. package/dist/data/types.d.ts +92 -0
  22. package/dist/data/types.js +2 -0
  23. package/dist/data/use-row-selection.d.ts +17 -0
  24. package/dist/data/use-row-selection.js +51 -0
  25. package/dist/index.d.ts +50 -0
  26. package/dist/index.js +61 -0
  27. package/dist/lib/cn.d.ts +3 -0
  28. package/dist/lib/cn.js +6 -0
  29. package/dist/lib/local-store.d.ts +12 -0
  30. package/dist/lib/local-store.js +54 -0
  31. package/dist/nav/Nav.d.ts +21 -0
  32. package/dist/nav/Nav.js +97 -0
  33. package/dist/nav/model.d.ts +77 -0
  34. package/dist/nav/model.js +72 -0
  35. package/dist/overlays/ConfirmDialog.d.ts +37 -0
  36. package/dist/overlays/ConfirmDialog.js +70 -0
  37. package/dist/overlays/Dialog.d.ts +18 -0
  38. package/dist/overlays/Dialog.js +39 -0
  39. package/dist/overlays/Drawer.d.ts +24 -0
  40. package/dist/overlays/Drawer.js +42 -0
  41. package/dist/page/Breadcrumbs.d.ts +20 -0
  42. package/dist/page/Breadcrumbs.js +25 -0
  43. package/dist/page/PageFrame.d.ts +17 -0
  44. package/dist/page/PageFrame.js +14 -0
  45. package/dist/page/PageHeader.d.ts +28 -0
  46. package/dist/page/PageHeader.js +13 -0
  47. package/dist/page/Section.d.ts +18 -0
  48. package/dist/page/Section.js +12 -0
  49. package/dist/page/TabNav.d.ts +19 -0
  50. package/dist/page/TabNav.js +16 -0
  51. package/dist/primitives/Button.d.ts +16 -0
  52. package/dist/primitives/Button.js +36 -0
  53. package/dist/primitives/Checkbox.d.ts +8 -0
  54. package/dist/primitives/Checkbox.js +13 -0
  55. package/dist/primitives/DropdownMenu.d.ts +11 -0
  56. package/dist/primitives/DropdownMenu.js +25 -0
  57. package/dist/primitives/Input.d.ts +2 -0
  58. package/dist/primitives/Input.js +5 -0
  59. package/dist/primitives/Popover.d.ts +5 -0
  60. package/dist/primitives/Popover.js +13 -0
  61. package/dist/primitives/Skeleton.d.ts +3 -0
  62. package/dist/primitives/Skeleton.js +6 -0
  63. package/dist/primitives/Tabs.d.ts +12 -0
  64. package/dist/primitives/Tabs.js +22 -0
  65. package/dist/provider/UIProvider.d.ts +29 -0
  66. package/dist/provider/UIProvider.js +21 -0
  67. package/dist/provider/messages.d.ts +106 -0
  68. package/dist/provider/messages.js +104 -0
  69. package/dist/shell/AppShell.d.ts +43 -0
  70. package/dist/shell/AppShell.js +62 -0
  71. package/dist/shell/BrandMark.d.ts +14 -0
  72. package/dist/shell/BrandMark.js +14 -0
  73. package/dist/shell/RailPanel.d.ts +39 -0
  74. package/dist/shell/RailPanel.js +62 -0
  75. package/dist/shell/RailTree.d.ts +35 -0
  76. package/dist/shell/RailTree.js +32 -0
  77. package/dist/shell/Sidebar.d.ts +26 -0
  78. package/dist/shell/Sidebar.js +21 -0
  79. package/dist/shell/UserMenu.d.ts +22 -0
  80. package/dist/shell/UserMenu.js +31 -0
  81. package/dist/shell/placement.d.ts +10 -0
  82. package/dist/shell/placement.js +6 -0
  83. package/dist/shell/rail-state.d.ts +13 -0
  84. package/dist/shell/rail-state.js +28 -0
  85. package/dist/states/EmptyState.d.ts +20 -0
  86. package/dist/states/EmptyState.js +15 -0
  87. package/dist/states/ErrorState.d.ts +23 -0
  88. package/dist/states/ErrorState.js +16 -0
  89. package/dist/states/LoadingState.d.ts +14 -0
  90. package/dist/states/LoadingState.js +14 -0
  91. package/dist/status/StatusBadge.d.ts +19 -0
  92. package/dist/status/StatusBadge.js +13 -0
  93. package/dist/status/StatusDot.d.ts +14 -0
  94. package/dist/status/StatusDot.js +7 -0
  95. package/dist/status/tones.d.ts +17 -0
  96. package/dist/status/tones.js +29 -0
  97. package/dist/styles/preset.css +175 -0
  98. package/dist/styles/theme.css +339 -0
  99. package/dist/theme/contrast.d.ts +8 -0
  100. package/dist/theme/contrast.js +18 -0
  101. package/dist/theme/css.d.ts +23 -0
  102. package/dist/theme/css.js +116 -0
  103. package/dist/theme/tokens.d.ts +195 -0
  104. package/dist/theme/tokens.js +294 -0
  105. package/package.json +112 -4
@@ -0,0 +1,104 @@
1
+ const lower = (label) => label.toLocaleLowerCase("en");
2
+ const count = (n) => n.toLocaleString("en");
3
+ export const defaultMessages = {
4
+ shell: {
5
+ skipToContent: "Skip to content",
6
+ navigation: "Navigation",
7
+ close: "Close",
8
+ },
9
+ nav: {
10
+ label: "Main",
11
+ collapse: "Collapse sidebar",
12
+ expand: "Expand sidebar",
13
+ collapseTitle: "Collapse sidebar ([)",
14
+ expandTitle: "Expand sidebar ([)",
15
+ },
16
+ rail: {
17
+ show: (label) => `Show ${lower(label)}`,
18
+ hide: (label) => `Hide ${lower(label)}`,
19
+ resize: (label) => `Resize ${lower(label)}`,
20
+ expand: (label) => `Expand ${lower(label)} to full width`,
21
+ restore: (label) => `Restore ${lower(label)} to the side`,
22
+ },
23
+ breadcrumb: {
24
+ label: "Breadcrumb",
25
+ switchFrom: (label) => `Switch from ${label}`,
26
+ },
27
+ table: {
28
+ search: "Search",
29
+ clearSearch: "Clear search",
30
+ results: (n) => (n === 1 ? "1 result" : `${count(n)} results`),
31
+ loading: "Loading",
32
+ loadFailed: (label) => `Could not load ${lower(label)}`,
33
+ requestFailed: "The request failed.",
34
+ retry: "Retry",
35
+ noMatches: "No matches",
36
+ noMatchesDescription: (label) => `No ${lower(label)} match the search.`,
37
+ selected: (n) => `${count(n)} selected`,
38
+ clearSelection: "Clear",
39
+ selectRow: (id) => `Select ${id}`,
40
+ selectPage: (label) => `Select all ${lower(label)} on this page`,
41
+ actions: "Actions",
42
+ rowActions: (label) => `Actions for ${label}`,
43
+ rowsPerPage: "Rows per page",
44
+ page: (n) => `Page ${count(n)}`,
45
+ previousPage: "Previous page",
46
+ nextPage: "Next page",
47
+ sortKey: (index) => `sort key ${index}`,
48
+ },
49
+ states: {
50
+ errorTitle: "Something went wrong",
51
+ unexpected: "An unexpected error occurred.",
52
+ retry: "Try again",
53
+ errorId: (digest) => `Error ID ${digest}`,
54
+ loading: "Loading",
55
+ learnMore: "Learn more",
56
+ },
57
+ dialog: {
58
+ close: "Close",
59
+ },
60
+ confirm: {
61
+ confirm: "Confirm",
62
+ cancel: "Cancel",
63
+ working: "Working",
64
+ failed: "That did not work. Try again.",
65
+ reasonRequired: "A reason is required.",
66
+ },
67
+ user: {
68
+ accountMenu: (name, initials) => `${initials}, account menu for ${name}`,
69
+ signOut: "Sign out",
70
+ },
71
+ appearance: {
72
+ locked: "Your organization has locked the theme. These settings are read-only.",
73
+ themes: "Theme",
74
+ accent: "Accent",
75
+ ground: "Ground",
76
+ density: "Density",
77
+ corners: "Corners",
78
+ reset: "Use the default",
79
+ mode: (mode) => mode,
80
+ groundLabel: (_ground, label) => label,
81
+ accentLabel: (_accent, label) => label,
82
+ densityLabel: (density) => (density === "compact" ? "Compact" : "Cards"),
83
+ densityHint: (density) => density === "compact"
84
+ ? "Dense rows with hairline dividers"
85
+ : "Boxed items that flow into columns",
86
+ cornersLabel: (corners) => corners === 0 ? "Square" : corners === 10 ? "Rounded" : `${corners}px`,
87
+ customAccent: "Custom accent",
88
+ customAccentPick: "Pick a colour",
89
+ customAccentHelp: "Any colour that stands 3:1 off the ground. Status colours never change.",
90
+ customAccentInvalid: "Use a hex colour such as #2f9e52.",
91
+ customAccentInUse: "In use",
92
+ customAccentOk: (ratio) => `${ratio}:1 against this ground.`,
93
+ customAccentFaint: (ratio, min, mode) => `${ratio}:1 against this ground; it needs ${min}:1. Try a ${mode === "dark" ? "lighter" : "darker"} colour.`,
94
+ },
95
+ };
96
+ export function mergeMessages(overrides) {
97
+ if (!overrides)
98
+ return defaultMessages;
99
+ const merged = { ...defaultMessages };
100
+ for (const group of Object.keys(overrides)) {
101
+ merged[group] = { ...defaultMessages[group], ...overrides[group] };
102
+ }
103
+ return merged;
104
+ }
@@ -0,0 +1,43 @@
1
+ import * as React from "react";
2
+ interface AppShellBaseProps {
3
+ /** The main rail, on the left: a Sidebar. */
4
+ sidebar: React.ReactNode;
5
+ children: React.ReactNode;
6
+ /** A bar above the content, between the rails: global search, an assistant toggle. */
7
+ header?: React.ReactNode;
8
+ /** Above everything, full width: an incident banner. */
9
+ banner?: React.ReactNode;
10
+ /** `[`: collapse or expand the sidebar. */
11
+ onToggleSidebar?: () => void;
12
+ /** `]`: show or hide the right rail. */
13
+ onToggleRightRail?: () => void;
14
+ className?: string;
15
+ }
16
+ type RightRailProps = {
17
+ rightRail?: undefined;
18
+ rightRailLabel?: undefined;
19
+ rightRailExpanded?: undefined;
20
+ } | {
21
+ /** The right rail: Astrolift's projects, Zentinelle's assistant dock. Usually a RailPanel. */
22
+ rightRail: React.ReactNode;
23
+ /** Names the right rail on narrow screens: its button and its drawer's title. */
24
+ rightRailLabel: string;
25
+ /** The right rail takes the whole content column (the assistant's full-width mode). */
26
+ rightRailExpanded?: boolean;
27
+ };
28
+ export type AppShellProps = AppShellBaseProps & RightRailProps;
29
+ /**
30
+ * The app frame (Astrolift layout D, Zentinelle §5): the sidebar on the
31
+ * left, the content between, the right rail on the right, each scrolling on
32
+ * its own. Pure: it lays them out and owns only the shortcuts (`[` and `]`,
33
+ * handed to the caller's collapse state) and, below `md`, the drawers the
34
+ * rails open in.
35
+ *
36
+ * On narrow screens the top bar holds a button for each rail's drawer, so
37
+ * hide header controls that only make sense beside the content (a dock
38
+ * toggle). A rail then renders inside its drawer as well as in its hidden
39
+ * desktop place: keep anything that must survive (an assistant's
40
+ * conversation) in state above the shell, not inside the rail.
41
+ */
42
+ export declare function AppShell({ sidebar, children, header, rightRail, rightRailLabel, rightRailExpanded, banner, onToggleSidebar, onToggleRightRail, className, }: AppShellProps): React.JSX.Element;
43
+ export {};
@@ -0,0 +1,62 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "../overlays/Drawer.js";
6
+ import { Button } from "../primitives/Button.js";
7
+ import { useUI } from "../provider/UIProvider.js";
8
+ import { RailPlacementContext } from "./placement.js";
9
+ function ignoresShortcut(event) {
10
+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey || event.isComposing)
11
+ return true;
12
+ const target = event.target;
13
+ if (!(target instanceof HTMLElement))
14
+ return false;
15
+ if (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName))
16
+ return true;
17
+ // A dialog owns the keyboard while it is open.
18
+ return Boolean(target.closest("[role='dialog'], [role='alertdialog']"));
19
+ }
20
+ /**
21
+ * The app frame (Astrolift layout D, Zentinelle §5): the sidebar on the
22
+ * left, the content between, the right rail on the right, each scrolling on
23
+ * its own. Pure: it lays them out and owns only the shortcuts (`[` and `]`,
24
+ * handed to the caller's collapse state) and, below `md`, the drawers the
25
+ * rails open in.
26
+ *
27
+ * On narrow screens the top bar holds a button for each rail's drawer, so
28
+ * hide header controls that only make sense beside the content (a dock
29
+ * toggle). A rail then renders inside its drawer as well as in its hidden
30
+ * desktop place: keep anything that must survive (an assistant's
31
+ * conversation) in state above the shell, not inside the rail.
32
+ */
33
+ export function AppShell({ sidebar, children, header, rightRail, rightRailLabel, rightRailExpanded = false, banner, onToggleSidebar, onToggleRightRail, className, }) {
34
+ const { messages } = useUI();
35
+ const [drawer, setDrawer] = React.useState(null);
36
+ React.useEffect(() => {
37
+ function onKey(event) {
38
+ if (ignoresShortcut(event))
39
+ return;
40
+ if (event.key === "[" && onToggleSidebar) {
41
+ event.preventDefault();
42
+ onToggleSidebar();
43
+ }
44
+ else if (event.key === "]" && onToggleRightRail) {
45
+ event.preventDefault();
46
+ onToggleRightRail();
47
+ }
48
+ }
49
+ window.addEventListener("keydown", onKey);
50
+ return () => window.removeEventListener("keydown", onKey);
51
+ }, [onToggleSidebar, onToggleRightRail]);
52
+ const railLabel = rightRailLabel ?? "";
53
+ const drawerTitle = drawer === "rightRail" ? railLabel : messages.shell.navigation;
54
+ return (_jsxs("div", { "data-slot": "app-shell", className: cn("flex h-dvh min-h-0 w-full flex-col bg-background text-foreground", className), children: [_jsx("a", { href: "#main-content", className: "sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-100 focus:rounded-lg focus:bg-primary focus:px-3 focus:py-2 focus:text-primary-foreground focus:shadow-md focus:ring-2 focus:ring-ring focus:outline-none", children: messages.shell.skipToContent }), banner, _jsx(Drawer, { open: drawer !== null, onOpenChange: (open) => {
55
+ if (!open)
56
+ setDrawer(null);
57
+ }, children: _jsxs(DrawerContent, { side: drawer === "rightRail" ? "right" : "left", "aria-describedby": undefined, className: cn("gap-0 p-0", drawer === "rightRail" ? "w-full sm:max-w-md" : "w-72 max-w-[85vw]"), children: [_jsx(DrawerHeader, { className: "border-b", children: _jsx(DrawerTitle, { children: drawerTitle }) }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col", onClick: (event) => {
58
+ // Following a link closes the drawer, as navigating would on a wide screen.
59
+ if (event.target.closest("a[href]"))
60
+ setDrawer(null);
61
+ }, children: _jsx(RailPlacementContext.Provider, { value: "drawer", children: drawer === "rightRail" ? rightRail : sidebar }) })] }) }), _jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsx("div", { className: "hidden min-h-0 shrink-0 md:flex", children: sidebar }), _jsxs("div", { className: cn("flex min-w-0 flex-1 flex-col", rightRailExpanded && rightRail && "md:hidden"), children: [_jsxs("div", { className: cn("flex shrink-0 items-center gap-2 border-b px-3 py-2", !header && "md:hidden"), children: [_jsxs("div", { className: "flex shrink-0 gap-2 md:hidden", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setDrawer("sidebar"), children: messages.shell.navigation }), rightRail && (_jsx(Button, { variant: "outline", size: "sm", onClick: () => setDrawer("rightRail"), children: railLabel }))] }), header && _jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: header })] }), _jsx("main", { id: "main-content", tabIndex: -1, className: "min-w-0 flex-1 overflow-y-auto px-3 py-5 outline-none md:px-6", children: children })] }), rightRail && (_jsx("div", { className: cn("hidden min-h-0 md:flex", rightRailExpanded ? "min-w-0 flex-1" : "shrink-0"), children: rightRail }))] })] }));
62
+ }
@@ -0,0 +1,14 @@
1
+ import type * as React from "react";
2
+ export interface BrandMarkProps {
3
+ /** Where the mark leads: the product's home. */
4
+ href: string;
5
+ /** The product's logo mark (an <img> or SVG). Decorative: the name or the label carries the meaning. */
6
+ mark?: React.ReactNode;
7
+ /** Defaults to the product's name from AppearanceProvider. */
8
+ name?: string;
9
+ /** The mark alone, for the collapsed sidebar. */
10
+ collapsed?: boolean;
11
+ className?: string;
12
+ }
13
+ /** The product's mark and name, heading the sidebar. */
14
+ export declare function BrandMark({ href, mark, name, collapsed, className }: BrandMarkProps): React.JSX.Element;
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useProduct } from "../appearance/AppearanceProvider.js";
4
+ import { cn } from "../lib/cn.js";
5
+ import { useUI } from "../provider/UIProvider.js";
6
+ /** The product's mark and name, heading the sidebar. */
7
+ export function BrandMark({ href, mark, name, collapsed = false, className }) {
8
+ const { Link } = useUI();
9
+ const product = useProduct();
10
+ const title = name ?? product?.name;
11
+ if (!title)
12
+ throw new Error("BrandMark needs a name, or an AppearanceProvider with a product");
13
+ return (_jsxs(Link, { href: href, "aria-label": collapsed ? title : undefined, className: cn("flex min-w-0 items-center gap-2 rounded-lg focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", collapsed && "justify-center", className), children: [mark && (_jsx("span", { "aria-hidden": true, className: "flex size-7 shrink-0 items-center justify-center rounded-lg bg-sidebar-primary/10 ring-1 ring-sidebar-primary/30 [&_img]:size-4.5 [&_svg]:size-4.5", children: mark })), !collapsed && (_jsx("span", { className: "truncate font-head text-sm font-semibold tracking-tight", children: title }))] }));
14
+ }
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+ export interface RailPanelProps {
3
+ /** Names the rail: its landmark, and its show, hide, resize and expand controls. */
4
+ label: string;
5
+ /** The rail's heading row: an organization switcher, a title. Defaults to the label. */
6
+ title?: React.ReactNode;
7
+ /** Controls beside the title. */
8
+ actions?: React.ReactNode;
9
+ footer?: React.ReactNode;
10
+ children: React.ReactNode;
11
+ collapsed: boolean;
12
+ onCollapsedChange: (collapsed: boolean) => void;
13
+ /**
14
+ * Collapsed, a rail is a thin strip with a show button (Astrolift's projects
15
+ * rail), or nothing at all, reopened from elsewhere (Zentinelle's assistant,
16
+ * from a header button or `]`).
17
+ */
18
+ collapsedMode?: "strip" | "hidden";
19
+ /** Under the show button, in the strip. */
20
+ collapsedIcon?: React.ReactNode;
21
+ /** px. Fixed unless `onWidthChange` is given. */
22
+ width?: number;
23
+ minWidth?: number;
24
+ maxWidth?: number;
25
+ /** Makes the rail resizable from its inner edge, by pointer or by arrow keys. */
26
+ onWidthChange?: (width: number) => void;
27
+ /** The rail takes the content column (pair with AppShell's `rightRailExpanded`). */
28
+ expanded?: boolean;
29
+ /** Shows the expand and restore control. */
30
+ onExpandedChange?: (expanded: boolean) => void;
31
+ className?: string;
32
+ }
33
+ /**
34
+ * The right rail's frame: the slot where Astrolift keeps its projects and
35
+ * Zentinelle docks its assistant. A rail pushes the content aside rather
36
+ * than covering it. In the shell's drawer (narrow screens) it fills the
37
+ * drawer and drops the controls that only make sense beside the content.
38
+ */
39
+ export declare function RailPanel({ label, title, actions, footer, children, collapsed, onCollapsedChange, collapsedMode, collapsedIcon, width, minWidth, maxWidth, onWidthChange, expanded, onExpandedChange, className, }: RailPanelProps): React.JSX.Element | null;
@@ -0,0 +1,62 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronsLeftIcon, ChevronsRightIcon, Maximize2Icon, Minimize2Icon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { useUI } from "../provider/UIProvider.js";
7
+ import { useRailPlacement } from "./placement.js";
8
+ const STEP = 16;
9
+ /**
10
+ * The right rail's frame: the slot where Astrolift keeps its projects and
11
+ * Zentinelle docks its assistant. A rail pushes the content aside rather
12
+ * than covering it. In the shell's drawer (narrow screens) it fills the
13
+ * drawer and drops the controls that only make sense beside the content.
14
+ */
15
+ export function RailPanel({ label, title, actions, footer, children, collapsed, onCollapsedChange, collapsedMode = "strip", collapsedIcon, width = 260, minWidth = 200, maxWidth = 640, onWidthChange, expanded = false, onExpandedChange, className, }) {
16
+ const { messages } = useUI();
17
+ const inDrawer = useRailPlacement() === "drawer";
18
+ const clamp = (value) => Math.min(maxWidth, Math.max(minWidth, value));
19
+ if (collapsed && !inDrawer) {
20
+ if (collapsedMode === "hidden")
21
+ return null;
22
+ return (_jsxs("aside", { "aria-label": label, "data-slot": "rail-panel", "data-collapsed": true, className: cn("flex h-full w-8 shrink-0 flex-col items-center gap-2 border-l border-sidebar-border bg-sidebar py-2 text-muted-foreground", className), children: [_jsx("button", { type: "button", onClick: () => onCollapsedChange(false), "aria-label": messages.rail.show(label), title: `${messages.rail.show(label)} (])`, className: "flex size-7 items-center justify-center rounded-lg hover:bg-sidebar-accent hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", children: _jsx(ChevronsLeftIcon, { className: "size-4", "aria-hidden": true }) }), collapsedIcon && (_jsx("span", { "aria-hidden": true, className: "[&_svg]:size-4", children: collapsedIcon }))] }));
23
+ }
24
+ const resizable = Boolean(onWidthChange) && !inDrawer && !expanded;
25
+ const iconButton = "flex size-7 shrink-0 items-center justify-center rounded-lg text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none";
26
+ return (_jsxs("aside", { "aria-label": label, "data-slot": "rail-panel", "data-expanded": expanded || undefined, style: inDrawer || expanded ? undefined : { width }, className: cn("relative flex h-full min-h-0 shrink-0 flex-col border-l border-sidebar-border bg-sidebar text-sidebar-foreground", (inDrawer || expanded) && "w-full min-w-0 flex-1", inDrawer && "border-l-0", className), children: [resizable && (_jsx(ResizeHandle, { label: messages.rail.resize(label), width: width, minWidth: minWidth, maxWidth: maxWidth, onWidthChange: (next) => onWidthChange?.(clamp(next)) })), _jsxs("div", { className: "flex min-w-0 items-center gap-2 border-b border-sidebar-border px-3 py-2", children: [_jsx("div", { className: "min-w-0 flex-1 truncate text-sm font-semibold", children: title ?? label }), actions, onExpandedChange && !inDrawer && (_jsx("button", { type: "button", onClick: () => onExpandedChange(!expanded), "aria-label": expanded ? messages.rail.restore(label) : messages.rail.expand(label), title: expanded ? messages.rail.restore(label) : messages.rail.expand(label), className: iconButton, children: expanded ? (_jsx(Minimize2Icon, { className: "size-4", "aria-hidden": true })) : (_jsx(Maximize2Icon, { className: "size-4", "aria-hidden": true })) })), !inDrawer && (_jsx("button", { type: "button", onClick: () => onCollapsedChange(true), "aria-label": messages.rail.hide(label), title: `${messages.rail.hide(label)} (])`, className: iconButton, children: _jsx(ChevronsRightIcon, { className: "size-4", "aria-hidden": true }) }))] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", children: children }), footer && _jsx("div", { className: "border-t border-sidebar-border px-3 py-2", children: footer })] }));
27
+ }
28
+ /* A focusable separator is the WAI-ARIA window splitter, an interactive
29
+ widget; jsx-a11y counts every separator as static. */
30
+ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/no-noninteractive-tabindex */
31
+ function ResizeHandle({ label, width, minWidth, maxWidth, onWidthChange, }) {
32
+ const drag = React.useRef(null);
33
+ return (_jsx("div", { role: "separator", "aria-orientation": "vertical", "aria-label": label, "aria-valuenow": width, "aria-valuemin": minWidth, "aria-valuemax": maxWidth, tabIndex: 0, onKeyDown: (event) => {
34
+ // The rail is on the right, so moving the edge left widens it.
35
+ const next = event.key === "ArrowLeft"
36
+ ? width + STEP
37
+ : event.key === "ArrowRight"
38
+ ? width - STEP
39
+ : event.key === "Home"
40
+ ? minWidth
41
+ : event.key === "End"
42
+ ? maxWidth
43
+ : null;
44
+ if (next === null)
45
+ return;
46
+ event.preventDefault();
47
+ onWidthChange(next);
48
+ }, onPointerDown: (event) => {
49
+ if (event.button !== 0)
50
+ return;
51
+ event.preventDefault();
52
+ event.currentTarget.setPointerCapture?.(event.pointerId);
53
+ drag.current = { x: event.clientX, width };
54
+ }, onPointerMove: (event) => {
55
+ const start = drag.current;
56
+ if (start)
57
+ onWidthChange(start.width + (start.x - event.clientX));
58
+ }, onPointerUp: (event) => {
59
+ drag.current = null;
60
+ event.currentTarget.releasePointerCapture?.(event.pointerId);
61
+ }, className: "absolute inset-y-0 left-0 z-10 w-1.5 -translate-x-1/2 cursor-col-resize touch-none outline-none hover:bg-primary/40 focus-visible:bg-primary/60" }));
62
+ }
@@ -0,0 +1,35 @@
1
+ import * as React from "react";
2
+ import type { NavIcon } from "../nav/model.js";
3
+ import type { StatusTone } from "../status/tones.js";
4
+ export interface RailTreeItem {
5
+ key: string;
6
+ label: string;
7
+ href: string;
8
+ icon?: NavIcon;
9
+ status?: StatusTone;
10
+ /** Read with the status dot, which carries no text of its own ("failing"). */
11
+ statusLabel?: string;
12
+ }
13
+ export interface RailTreeGroup {
14
+ key: string;
15
+ label: string;
16
+ items: readonly RailTreeItem[];
17
+ }
18
+ export interface RailTreeProps {
19
+ /** The tree's heading ("My projects"). */
20
+ label: string;
21
+ groups: readonly RailTreeGroup[];
22
+ /** The page on screen: its row is lit and its group opens. */
23
+ activeHref?: string;
24
+ loading?: boolean;
25
+ /** Shown when there are no groups. */
26
+ emptyText: string;
27
+ className?: string;
28
+ }
29
+ /**
30
+ * A tree of linked groups for a rail: Astrolift's projects and the apps,
31
+ * agents and workflows in each. It navigates; it never filters the lists on
32
+ * the page. Each group shows its count and folds; the group holding the
33
+ * current page starts open.
34
+ */
35
+ export declare function RailTree({ label, groups, activeHref, loading, emptyText, className, }: RailTreeProps): React.JSX.Element;
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
4
+ import * as React from "react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { Skeleton } from "../primitives/Skeleton.js";
7
+ import { useUI } from "../provider/UIProvider.js";
8
+ import { StatusDot } from "../status/StatusDot.js";
9
+ /**
10
+ * A tree of linked groups for a rail: Astrolift's projects and the apps,
11
+ * agents and workflows in each. It navigates; it never filters the lists on
12
+ * the page. Each group shows its count and folds; the group holding the
13
+ * current page starts open.
14
+ */
15
+ export function RailTree({ label, groups, activeHref, loading = false, emptyText, className, }) {
16
+ const headingId = React.useId();
17
+ return (_jsxs("div", { className: cn("flex min-w-0 flex-col", className), children: [_jsx("div", { id: headingId, className: "px-3 pt-3 pb-1 text-2xs font-semibold tracking-wider text-muted-foreground uppercase", children: label }), _jsx("div", { className: "px-2 pb-2", children: loading ? (_jsx("div", { className: "flex flex-col gap-2 px-1 pt-1", "aria-busy": true, children: Array.from({ length: 4 }, (_, i) => (_jsx(Skeleton, { className: "h-6 w-full" }, i))) })) : groups.length === 0 ? (_jsx("p", { className: "px-2 py-2 text-xs text-muted-foreground", children: emptyText })) : (_jsx("ul", { "aria-labelledby": headingId, className: "flex flex-col gap-0.5", children: groups.map((group) => (_jsx(Branch, { group: group, activeHref: activeHref }, group.key))) })) })] }));
18
+ }
19
+ function Branch({ group, activeHref }) {
20
+ const { Link } = useUI();
21
+ const holdsActive = group.items.some((item) => item.href === activeHref);
22
+ const [open, setOpen] = React.useState(holdsActive);
23
+ const listId = React.useId();
24
+ const Chevron = open ? ChevronDownIcon : ChevronRightIcon;
25
+ return (_jsxs("li", { children: [_jsxs("button", { type: "button", onClick: () => setOpen((o) => !o), "aria-expanded": open, "aria-controls": listId, className: "flex h-7 w-full min-w-0 items-center gap-1.5 rounded-lg px-1.5 text-left text-sm hover:bg-sidebar-accent/60 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", children: [_jsx(Chevron, { className: "size-3.5 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx("span", { className: "truncate font-medium", title: group.label, children: group.label }), " ", _jsx("span", { className: "ml-auto font-mono text-2xs text-muted-foreground", children: group.items.length })] }), _jsx("ul", { id: listId, hidden: !open, className: "ml-3 flex flex-col gap-0.5 border-l border-current/10 pl-2", children: group.items.map((item) => {
26
+ const Icon = item.icon;
27
+ const isActive = item.href === activeHref;
28
+ return (_jsx("li", { children: _jsxs(Link, { href: item.href, "aria-current": isActive ? "page" : undefined, className: cn("flex h-7 min-w-0 items-center gap-2 rounded-lg px-1.5 text-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", isActive
29
+ ? "bg-primary/15 font-medium text-sidebar-foreground"
30
+ : "text-sidebar-foreground/80 hover:bg-sidebar-accent/60"), children: [Icon && _jsx(Icon, { className: "size-3.5 shrink-0 opacity-70", "aria-hidden": true }), _jsx("span", { className: "truncate", title: item.label, children: item.label }), " ", item.status && (_jsx(StatusDot, { tone: item.status, label: item.statusLabel, className: "ml-auto" }))] }) }, item.key));
31
+ }) })] }));
32
+ }
@@ -0,0 +1,26 @@
1
+ import type * as React from "react";
2
+ import type { NavActive, NavModel } from "../nav/model.js";
3
+ export interface SidebarProps {
4
+ /** The model, filtered to what the viewer may open (`visibleNav`). */
5
+ nav: NavModel;
6
+ /** The current row (`activeFor(nav, pathname)`). */
7
+ active: NavActive | null;
8
+ collapsed: boolean;
9
+ onCollapsedChange: (collapsed: boolean) => void;
10
+ /** Above the areas: the brand mark. */
11
+ header?: React.ReactNode;
12
+ /** The mark alone, for the collapsed rail; nothing when absent (a name cut to one letter reads as a bug). */
13
+ headerCollapsed?: React.ReactNode;
14
+ /** Below the areas: the user menu, notifications, help. Hidden while collapsed. */
15
+ footer?: React.ReactNode;
16
+ /** The navigation landmark's name. Defaults to "Main". */
17
+ label?: string;
18
+ className?: string;
19
+ }
20
+ /**
21
+ * The main rail, on the left (Astrolift layout D): the one brand surface.
22
+ * 220px with labels, 48px with icons. Pure: the filtered model, the active
23
+ * row and the collapsed state are the caller's; `[` toggles it through the
24
+ * AppShell.
25
+ */
26
+ export declare function Sidebar({ nav, active, collapsed: collapsedProp, onCollapsedChange, header, headerCollapsed, footer, label, className, }: SidebarProps): React.JSX.Element;
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ChevronsLeftIcon, ChevronsRightIcon } from "lucide-react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { Nav } from "../nav/Nav.js";
6
+ import { useUI } from "../provider/UIProvider.js";
7
+ import { useRailPlacement } from "./placement.js";
8
+ /**
9
+ * The main rail, on the left (Astrolift layout D): the one brand surface.
10
+ * 220px with labels, 48px with icons. Pure: the filtered model, the active
11
+ * row and the collapsed state are the caller's; `[` toggles it through the
12
+ * AppShell.
13
+ */
14
+ export function Sidebar({ nav, active, collapsed: collapsedProp, onCollapsedChange, header, headerCollapsed, footer, label, className, }) {
15
+ const { messages } = useUI();
16
+ const inDrawer = useRailPlacement() === "drawer";
17
+ const collapsed = collapsedProp && !inDrawer;
18
+ return (_jsxs("div", { "data-slot": "sidebar", "data-collapsed": collapsed || undefined, className: cn("flex h-full min-h-0 shrink-0 flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground", inDrawer ? "w-full border-r-0" : "transition-[width] duration-(--motion-base)", !inDrawer && (collapsed ? "w-12" : "w-55"), className), children: [collapsed
19
+ ? headerCollapsed && _jsx("div", { className: "flex justify-center py-3", children: headerCollapsed })
20
+ : header && _jsx("div", { className: "flex min-w-0 items-center px-3 py-3", children: header }), _jsx("nav", { "aria-label": label ?? messages.nav.label, className: "min-h-0 flex-1 overflow-y-auto px-2 py-1", children: _jsx(Nav, { nav: nav, active: active, collapsed: collapsed }) }), (footer || !inDrawer) && (_jsxs("div", { className: "flex flex-col gap-1 border-t border-sidebar-border px-2 py-2", children: [!collapsed && footer, !inDrawer && (_jsx("button", { type: "button", onClick: () => onCollapsedChange(!collapsed), "aria-label": collapsed ? messages.nav.expand : messages.nav.collapse, title: collapsed ? messages.nav.expandTitle : messages.nav.collapseTitle, className: "flex h-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", children: collapsed ? (_jsx(ChevronsRightIcon, { className: "size-4", "aria-hidden": true })) : (_jsx(ChevronsLeftIcon, { className: "size-4", "aria-hidden": true })) }))] }))] }));
21
+ }
@@ -0,0 +1,22 @@
1
+ import type * as React from "react";
2
+ import type { NavIcon } from "../nav/model.js";
3
+ export interface UserMenuItem {
4
+ label: string;
5
+ icon?: NavIcon;
6
+ /** A page: every item is a link unless it acts in place. */
7
+ href?: string;
8
+ /** An action in place (open the appearance picker, say) when there is no page. */
9
+ onSelect?: () => void;
10
+ }
11
+ export interface UserMenuProps {
12
+ name: string;
13
+ email: string;
14
+ /** Groups of items, separated by rules: account pages, then help. */
15
+ groups?: readonly (readonly UserMenuItem[])[];
16
+ onSignOut: () => void;
17
+ /** The avatar alone, for a collapsed rail. */
18
+ collapsed?: boolean;
19
+ className?: string;
20
+ }
21
+ /** The signed-in person's menu, at the foot of the sidebar. Pure: the person, the items and sign-out are the app's. */
22
+ export declare function UserMenu({ name, email, groups, onSignOut, collapsed, className, }: UserMenuProps): React.JSX.Element;
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { LogOutIcon } from "lucide-react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "../primitives/DropdownMenu.js";
6
+ import { useUI } from "../provider/UIProvider.js";
7
+ function initialsOf(name) {
8
+ return (name
9
+ .split(/\s+/)
10
+ .map((part) => part[0])
11
+ .filter(Boolean)
12
+ .slice(0, 2)
13
+ .join("")
14
+ .toUpperCase() || "?");
15
+ }
16
+ /** The signed-in person's menu, at the foot of the sidebar. Pure: the person, the items and sign-out are the app's. */
17
+ export function UserMenu({ name, email, groups = [], onSignOut, collapsed = false, className, }) {
18
+ const { Link, messages } = useUI();
19
+ return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger
20
+ // Expanded, the visible name and email are the trigger's name (WCAG
21
+ // 2.5.3, label in name); collapsed, only the initials show, so it
22
+ // needs one.
23
+ , { "aria-label": collapsed ? messages.user.accountMenu(name, initialsOf(name)) : undefined, className: cn("flex h-10 w-full min-w-0 items-center gap-2 rounded-lg px-1.5 text-left hover:bg-sidebar-accent/60 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none", collapsed && "justify-center px-0", className), children: [_jsx("span", { "aria-hidden": true, className: "flex size-7 shrink-0 items-center justify-center rounded-full bg-muted font-mono text-xs font-medium text-foreground", children: initialsOf(name) }), !collapsed && (_jsxs("span", { className: "grid min-w-0 flex-1 leading-tight", children: [_jsx("span", { className: "truncate text-sm font-medium", children: name }), " ", _jsx("span", { className: "truncate text-xs text-muted-foreground", children: email })] }))] }), _jsxs(DropdownMenuContent, { side: "right", align: "end", className: "min-w-56", children: [_jsxs(DropdownMenuLabel, { className: "grid font-normal", children: [_jsx("span", { className: "truncate text-sm font-medium text-popover-foreground", children: name }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: email })] }), groups.map((items, i) => (_jsx(MenuGroup, { items: items, Link: Link }, i))), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: onSignOut, children: [_jsx(LogOutIcon, { "aria-hidden": true }), " ", messages.user.signOut] })] })] }));
24
+ }
25
+ function MenuGroup({ items, Link, }) {
26
+ return (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuGroup, { children: items.map((item) => {
27
+ const Icon = item.icon;
28
+ const content = (_jsxs(_Fragment, { children: [Icon && _jsx(Icon, { "aria-hidden": true }), " ", item.label] }));
29
+ return item.href ? (_jsx(DropdownMenuItem, { asChild: true, children: _jsx(Link, { href: item.href, children: content }) }, item.label)) : (_jsx(DropdownMenuItem, { onSelect: item.onSelect, children: content }, item.label));
30
+ }) })] }));
31
+ }
@@ -0,0 +1,10 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Where a rail is drawn: inline beside the content, or inside the drawer the
4
+ * shell opens on narrow screens. In a drawer a rail fills it, ignores its
5
+ * collapsed state and width, and hides the controls that only make sense
6
+ * beside the content.
7
+ */
8
+ export type RailPlacement = "inline" | "drawer";
9
+ export declare const RailPlacementContext: React.Context<RailPlacement>;
10
+ export declare function useRailPlacement(): RailPlacement;
@@ -0,0 +1,6 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ export const RailPlacementContext = React.createContext("inline");
4
+ export function useRailPlacement() {
5
+ return React.useContext(RailPlacementContext);
6
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * One rail's collapsed state, remembered per person in this browser.
3
+ * `defaultCollapsed` applies until they choose: Astrolift's projects rail
4
+ * passes "narrower than 1280px". The server renders the rail expanded and the
5
+ * client applies the saved choice without a hydration mismatch; a change in
6
+ * one tab reaches the others.
7
+ */
8
+ export declare function useRailState(storageKey: string, defaultCollapsed?: () => boolean): readonly [boolean, (collapsed: boolean) => void];
9
+ /** A resizable rail's width in px, remembered like its collapsed state and held to its range. */
10
+ export declare function useRailWidth(storageKey: string, defaultWidth: number, range: {
11
+ min: number;
12
+ max: number;
13
+ }): readonly [number, (width: number) => void];
@@ -0,0 +1,28 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { readLocal, subscribeLocal, writeLocal } from "../lib/local-store.js";
4
+ /**
5
+ * One rail's collapsed state, remembered per person in this browser.
6
+ * `defaultCollapsed` applies until they choose: Astrolift's projects rail
7
+ * passes "narrower than 1280px". The server renders the rail expanded and the
8
+ * client applies the saved choice without a hydration mismatch; a change in
9
+ * one tab reaches the others.
10
+ */
11
+ export function useRailState(storageKey, defaultCollapsed = () => false) {
12
+ const collapsed = React.useSyncExternalStore(subscribeLocal, () => {
13
+ const saved = readLocal(storageKey);
14
+ return saved === "1" ? true : saved === "0" ? false : defaultCollapsed();
15
+ }, () => false);
16
+ const set = React.useCallback((next) => writeLocal(storageKey, next ? "1" : "0"), [storageKey]);
17
+ return [collapsed, set];
18
+ }
19
+ /** A resizable rail's width in px, remembered like its collapsed state and held to its range. */
20
+ export function useRailWidth(storageKey, defaultWidth, range) {
21
+ const { min, max } = range;
22
+ const width = React.useSyncExternalStore(subscribeLocal, () => {
23
+ const saved = Number(readLocal(storageKey));
24
+ return Number.isFinite(saved) && saved >= min && saved <= max ? saved : defaultWidth;
25
+ }, () => defaultWidth);
26
+ const set = React.useCallback((next) => writeLocal(storageKey, String(Math.round(Math.min(max, Math.max(min, next))))), [storageKey, min, max]);
27
+ return [width, set];
28
+ }
@@ -0,0 +1,20 @@
1
+ import type * as React from "react";
2
+ export interface EmptyStateProps {
3
+ icon?: React.ReactNode;
4
+ title: string;
5
+ description?: React.ReactNode;
6
+ /** The create action: a link, so it can open in a new tab. */
7
+ actionHref?: string;
8
+ actionLabel?: string;
9
+ /**
10
+ * Context help, low-prominence beneath the description (Astrolift #356).
11
+ * External URLs open in a new tab; app paths go through the app's link.
12
+ */
13
+ learnMoreHref?: string;
14
+ learnMoreLabel?: string;
15
+ /** Anything else: a secondary action, a button with a handler. */
16
+ secondary?: React.ReactNode;
17
+ className?: string;
18
+ }
19
+ /** Nothing to show yet, with the way to change that. "No matches" is a different state; see DataTable. */
20
+ export declare function EmptyState({ icon, title, description, actionHref, actionLabel, learnMoreHref, learnMoreLabel, secondary, className, }: EmptyStateProps): React.JSX.Element;
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { ExternalLinkIcon } from "lucide-react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { Button } from "../primitives/Button.js";
6
+ import { useUI } from "../provider/UIProvider.js";
7
+ /** Nothing to show yet, with the way to change that. "No matches" is a different state; see DataTable. */
8
+ export function EmptyState({ icon, title, description, actionHref, actionLabel, learnMoreHref, learnMoreLabel, secondary, className, }) {
9
+ const { Link, messages } = useUI();
10
+ const learnMoreText = learnMoreLabel ?? messages.states.learnMore;
11
+ const external = learnMoreHref ? /^https?:\/\//.test(learnMoreHref) : false;
12
+ const learnMoreClass = "mt-2 inline-flex items-center gap-1 text-xs text-muted-foreground underline-offset-2 hover:text-foreground hover:underline";
13
+ return (_jsxs("div", { "data-slot": "empty-state", className: cn("flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed px-6 py-12 text-center whitespace-normal", className), children: [icon && (_jsx("div", { "aria-hidden": true, className: "rounded-lg bg-muted p-2 text-muted-foreground [&_svg]:size-5", children: icon })), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-medium [overflow-wrap:anywhere]", children: title }), description && (_jsx("p", { className: "mx-auto mt-1 max-w-sm text-sm leading-relaxed text-muted-foreground", children: description })), learnMoreHref &&
14
+ (external ? (_jsxs("a", { href: learnMoreHref, target: "_blank", rel: "noreferrer", className: learnMoreClass, children: [learnMoreText, _jsx(ExternalLinkIcon, { className: "size-3", "aria-hidden": true })] })) : (_jsx(Link, { href: learnMoreHref, className: learnMoreClass, children: learnMoreText })))] }), actionHref && actionLabel && (_jsx(Button, { asChild: true, size: "sm", children: _jsx(Link, { href: actionHref, children: actionLabel }) })), secondary] }));
15
+ }