@calliopelabs/ui 0.0.0-stage → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +334 -2
- package/dist/appearance/AppearancePicker.d.ts +10 -0
- package/dist/appearance/AppearancePicker.js +75 -0
- package/dist/appearance/AppearanceProvider.d.ts +53 -0
- package/dist/appearance/AppearanceProvider.js +113 -0
- package/dist/appearance/AppearanceScript.d.ts +16 -0
- package/dist/appearance/AppearanceScript.js +14 -0
- package/dist/appearance/appearance.d.ts +90 -0
- package/dist/appearance/appearance.js +151 -0
- package/dist/appearance/boot.d.ts +30 -0
- package/dist/appearance/boot.js +91 -0
- package/dist/appearance/brands.d.ts +187 -0
- package/dist/appearance/brands.js +150 -0
- package/dist/appearance/product.d.ts +89 -0
- package/dist/appearance/product.js +96 -0
- package/dist/data/DataTable.d.ts +66 -0
- package/dist/data/DataTable.js +148 -0
- package/dist/data/List.d.ts +55 -0
- package/dist/data/List.js +41 -0
- package/dist/data/types.d.ts +92 -0
- package/dist/data/types.js +2 -0
- package/dist/data/use-row-selection.d.ts +17 -0
- package/dist/data/use-row-selection.js +51 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +61 -0
- package/dist/lib/cn.d.ts +3 -0
- package/dist/lib/cn.js +6 -0
- package/dist/lib/local-store.d.ts +12 -0
- package/dist/lib/local-store.js +54 -0
- package/dist/nav/Nav.d.ts +21 -0
- package/dist/nav/Nav.js +97 -0
- package/dist/nav/model.d.ts +77 -0
- package/dist/nav/model.js +72 -0
- package/dist/overlays/ConfirmDialog.d.ts +37 -0
- package/dist/overlays/ConfirmDialog.js +70 -0
- package/dist/overlays/Dialog.d.ts +18 -0
- package/dist/overlays/Dialog.js +39 -0
- package/dist/overlays/Drawer.d.ts +24 -0
- package/dist/overlays/Drawer.js +42 -0
- package/dist/page/Breadcrumbs.d.ts +20 -0
- package/dist/page/Breadcrumbs.js +25 -0
- package/dist/page/PageFrame.d.ts +17 -0
- package/dist/page/PageFrame.js +14 -0
- package/dist/page/PageHeader.d.ts +28 -0
- package/dist/page/PageHeader.js +13 -0
- package/dist/page/Section.d.ts +18 -0
- package/dist/page/Section.js +12 -0
- package/dist/page/TabNav.d.ts +19 -0
- package/dist/page/TabNav.js +16 -0
- package/dist/primitives/Button.d.ts +16 -0
- package/dist/primitives/Button.js +36 -0
- package/dist/primitives/Checkbox.d.ts +8 -0
- package/dist/primitives/Checkbox.js +13 -0
- package/dist/primitives/DropdownMenu.d.ts +11 -0
- package/dist/primitives/DropdownMenu.js +25 -0
- package/dist/primitives/Input.d.ts +2 -0
- package/dist/primitives/Input.js +5 -0
- package/dist/primitives/Popover.d.ts +5 -0
- package/dist/primitives/Popover.js +13 -0
- package/dist/primitives/Skeleton.d.ts +3 -0
- package/dist/primitives/Skeleton.js +6 -0
- package/dist/primitives/Tabs.d.ts +12 -0
- package/dist/primitives/Tabs.js +22 -0
- package/dist/provider/UIProvider.d.ts +29 -0
- package/dist/provider/UIProvider.js +21 -0
- package/dist/provider/messages.d.ts +106 -0
- package/dist/provider/messages.js +104 -0
- package/dist/shell/AppShell.d.ts +43 -0
- package/dist/shell/AppShell.js +62 -0
- package/dist/shell/BrandMark.d.ts +14 -0
- package/dist/shell/BrandMark.js +14 -0
- package/dist/shell/RailPanel.d.ts +39 -0
- package/dist/shell/RailPanel.js +62 -0
- package/dist/shell/RailTree.d.ts +35 -0
- package/dist/shell/RailTree.js +32 -0
- package/dist/shell/Sidebar.d.ts +26 -0
- package/dist/shell/Sidebar.js +21 -0
- package/dist/shell/UserMenu.d.ts +22 -0
- package/dist/shell/UserMenu.js +31 -0
- package/dist/shell/placement.d.ts +10 -0
- package/dist/shell/placement.js +6 -0
- package/dist/shell/rail-state.d.ts +13 -0
- package/dist/shell/rail-state.js +28 -0
- package/dist/states/EmptyState.d.ts +20 -0
- package/dist/states/EmptyState.js +15 -0
- package/dist/states/ErrorState.d.ts +23 -0
- package/dist/states/ErrorState.js +16 -0
- package/dist/states/LoadingState.d.ts +14 -0
- package/dist/states/LoadingState.js +14 -0
- package/dist/status/StatusBadge.d.ts +19 -0
- package/dist/status/StatusBadge.js +13 -0
- package/dist/status/StatusDot.d.ts +14 -0
- package/dist/status/StatusDot.js +7 -0
- package/dist/status/tones.d.ts +17 -0
- package/dist/status/tones.js +29 -0
- package/dist/styles/preset.css +175 -0
- package/dist/styles/theme.css +339 -0
- package/dist/theme/contrast.d.ts +8 -0
- package/dist/theme/contrast.js +18 -0
- package/dist/theme/css.d.ts +23 -0
- package/dist/theme/css.js +116 -0
- package/dist/theme/tokens.d.ts +195 -0
- package/dist/theme/tokens.js +294 -0
- package/package.json +112 -4
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
export function DropdownMenu(props) {
|
|
6
|
+
return _jsx(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
|
|
7
|
+
}
|
|
8
|
+
export function DropdownMenuTrigger(props) {
|
|
9
|
+
return _jsx(DropdownMenuPrimitive.Trigger, { "data-slot": "dropdown-menu-trigger", ...props });
|
|
10
|
+
}
|
|
11
|
+
export function DropdownMenuContent({ className, align = "start", sideOffset = 4, ...props }) {
|
|
12
|
+
return (_jsx(DropdownMenuPrimitive.Portal, { children: _jsx(DropdownMenuPrimitive.Content, { "data-slot": "dropdown-menu-content", align: align, sideOffset: sideOffset, className: cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 data-[state=open]:animate-cui-fade-in", className), ...props }) }));
|
|
13
|
+
}
|
|
14
|
+
export function DropdownMenuGroup(props) {
|
|
15
|
+
return _jsx(DropdownMenuPrimitive.Group, { "data-slot": "dropdown-menu-group", ...props });
|
|
16
|
+
}
|
|
17
|
+
export function DropdownMenuItem({ className, variant = "default", ...props }) {
|
|
18
|
+
return (_jsx(DropdownMenuPrimitive.Item, { "data-slot": "dropdown-menu-item", "data-variant": variant, className: cn("relative flex cursor-default items-center gap-1.5 rounded-lg px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-[variant=destructive]:text-danger-fg data-[variant=destructive]:focus:bg-danger/10 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props }));
|
|
19
|
+
}
|
|
20
|
+
export function DropdownMenuLabel({ className, ...props }) {
|
|
21
|
+
return (_jsx(DropdownMenuPrimitive.Label, { "data-slot": "dropdown-menu-label", className: cn("px-1.5 py-1 text-xs font-medium text-muted-foreground", className), ...props }));
|
|
22
|
+
}
|
|
23
|
+
export function DropdownMenuSeparator({ className, ...props }) {
|
|
24
|
+
return (_jsx(DropdownMenuPrimitive.Separator, { "data-slot": "dropdown-menu-separator", className: cn("-mx-1 my-1 h-px bg-border", className), ...props }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
export function Input({ className, type, ...props }) {
|
|
4
|
+
return (_jsx("input", { type: type, "data-slot": "input", className: cn("h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm", className), ...props }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Popover as PopoverPrimitive } from "radix-ui";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
export declare function Popover(props: React.ComponentProps<typeof PopoverPrimitive.Root>): React.JSX.Element;
|
|
4
|
+
export declare function PopoverTrigger(props: React.ComponentProps<typeof PopoverPrimitive.Trigger>): React.JSX.Element;
|
|
5
|
+
export declare function PopoverContent({ className, align, sideOffset, ...props }: React.ComponentProps<typeof PopoverPrimitive.Content>): React.JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Popover as PopoverPrimitive } from "radix-ui";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
export function Popover(props) {
|
|
6
|
+
return _jsx(PopoverPrimitive.Root, { "data-slot": "popover", ...props });
|
|
7
|
+
}
|
|
8
|
+
export function PopoverTrigger(props) {
|
|
9
|
+
return _jsx(PopoverPrimitive.Trigger, { "data-slot": "popover-trigger", ...props });
|
|
10
|
+
}
|
|
11
|
+
export function PopoverContent({ className, align = "center", sideOffset = 4, ...props }) {
|
|
12
|
+
return (_jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Content, { "data-slot": "popover-content", align: align, sideOffset: sideOffset, className: cn("z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden data-[state=open]:animate-cui-fade-in", className), ...props }) }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn.js";
|
|
3
|
+
/** A placeholder block with the geometry of what it stands in for. It pulses; with reduced motion it holds still. */
|
|
4
|
+
export function Skeleton({ className, ...props }) {
|
|
5
|
+
return (_jsx("div", { "data-slot": "skeleton", "aria-hidden": true, className: cn("animate-pulse rounded-lg bg-muted motion-reduce:animate-none", className), ...props }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Tabs as TabsPrimitive } from "radix-ui";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
/**
|
|
4
|
+
* In-page tabs: panels on one URL, with the ARIA tablist pattern (arrow keys
|
|
5
|
+
* move between tabs). Tabs that are pages (an entity's Overview, Logs,
|
|
6
|
+
* Settings) are links: use TabNav. The underline matches TabNav's, so the two
|
|
7
|
+
* read as one family.
|
|
8
|
+
*/
|
|
9
|
+
export declare function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>): React.JSX.Element;
|
|
10
|
+
export declare function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>): React.JSX.Element;
|
|
11
|
+
export declare function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>): React.JSX.Element;
|
|
12
|
+
export declare function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>): React.JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Tabs as TabsPrimitive } from "radix-ui";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
/**
|
|
6
|
+
* In-page tabs: panels on one URL, with the ARIA tablist pattern (arrow keys
|
|
7
|
+
* move between tabs). Tabs that are pages (an entity's Overview, Logs,
|
|
8
|
+
* Settings) are links: use TabNav. The underline matches TabNav's, so the two
|
|
9
|
+
* read as one family.
|
|
10
|
+
*/
|
|
11
|
+
export function Tabs({ className, ...props }) {
|
|
12
|
+
return (_jsx(TabsPrimitive.Root, { "data-slot": "tabs", className: cn("flex min-w-0 flex-col gap-4", className), ...props }));
|
|
13
|
+
}
|
|
14
|
+
export function TabsList({ className, ...props }) {
|
|
15
|
+
return (_jsx(TabsPrimitive.List, { "data-slot": "tabs-list", className: cn("flex min-w-0 gap-1 overflow-x-auto border-b border-border", className), ...props }));
|
|
16
|
+
}
|
|
17
|
+
export function TabsTrigger({ className, ...props }) {
|
|
18
|
+
return (_jsx(TabsPrimitive.Trigger, { "data-slot": "tabs-trigger", className: cn("relative shrink-0 px-3 py-2.5 text-sm text-muted-foreground transition-colors outline-none hover:text-foreground focus-visible:rounded-lg focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=active]:font-medium data-[state=active]:text-foreground data-[state=active]:after:absolute data-[state=active]:after:inset-x-1 data-[state=active]:after:-bottom-px data-[state=active]:after:h-0.5 data-[state=active]:after:rounded-full data-[state=active]:after:bg-primary", className), ...props }));
|
|
19
|
+
}
|
|
20
|
+
export function TabsContent({ className, ...props }) {
|
|
21
|
+
return (_jsx(TabsPrimitive.Content, { "data-slot": "tabs-content", className: cn("min-w-0 outline-none focus-visible:ring-2 focus-visible:ring-ring", className), ...props }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type MessageOverrides, type Messages } from "./messages.js";
|
|
3
|
+
/** What the package hands a link: anchor props with a string `href`. */
|
|
4
|
+
export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
5
|
+
href: string;
|
|
6
|
+
ref?: React.Ref<HTMLAnchorElement>;
|
|
7
|
+
}
|
|
8
|
+
/** The app's link component: next/link, a router's Link, or a plain anchor. */
|
|
9
|
+
export type LinkComponent = React.ComponentType<LinkProps>;
|
|
10
|
+
interface UIContextValue {
|
|
11
|
+
Link: LinkComponent;
|
|
12
|
+
messages: Messages;
|
|
13
|
+
}
|
|
14
|
+
export interface UIProviderProps {
|
|
15
|
+
/** Rendered for every link the package draws. Defaults to a plain `<a>`. */
|
|
16
|
+
link?: LinkComponent;
|
|
17
|
+
/** Copy overrides, one level deep (`{ table: { retry: "Reintentar" } }`). */
|
|
18
|
+
messages?: MessageOverrides;
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The app's environment: its link component and its copy. The package never
|
|
23
|
+
* imports a router, so this is how navigation stays the app's: pass next/link
|
|
24
|
+
* in a Next app, your router's Link elsewhere. Without a provider every link
|
|
25
|
+
* is a plain anchor and every string is English.
|
|
26
|
+
*/
|
|
27
|
+
export declare function UIProvider({ link, messages, children }: UIProviderProps): React.JSX.Element;
|
|
28
|
+
export declare function useUI(): UIContextValue;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { defaultMessages, mergeMessages, } from "./messages.js";
|
|
5
|
+
function Anchor({ ref, children, ...props }) {
|
|
6
|
+
return (_jsx("a", { ref: ref, ...props, children: children }));
|
|
7
|
+
}
|
|
8
|
+
const UIContext = React.createContext({ Link: Anchor, messages: defaultMessages });
|
|
9
|
+
/**
|
|
10
|
+
* The app's environment: its link component and its copy. The package never
|
|
11
|
+
* imports a router, so this is how navigation stays the app's: pass next/link
|
|
12
|
+
* in a Next app, your router's Link elsewhere. Without a provider every link
|
|
13
|
+
* is a plain anchor and every string is English.
|
|
14
|
+
*/
|
|
15
|
+
export function UIProvider({ link, messages, children }) {
|
|
16
|
+
const value = React.useMemo(() => ({ Link: link ?? Anchor, messages: mergeMessages(messages) }), [link, messages]);
|
|
17
|
+
return _jsx(UIContext.Provider, { value: value, children: children });
|
|
18
|
+
}
|
|
19
|
+
export function useUI() {
|
|
20
|
+
return React.useContext(UIContext);
|
|
21
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every string the package renders, in English. An app with a message
|
|
3
|
+
* catalog overrides any of them through `UIProvider messages={...}`; a
|
|
4
|
+
* message that takes a value is a function, so the app's own i18n formats
|
|
5
|
+
* it (plurals, numbers, word order).
|
|
6
|
+
*/
|
|
7
|
+
export interface Messages {
|
|
8
|
+
shell: {
|
|
9
|
+
skipToContent: string;
|
|
10
|
+
/** The mobile button and drawer title for the sidebar. */
|
|
11
|
+
navigation: string;
|
|
12
|
+
close: string;
|
|
13
|
+
};
|
|
14
|
+
nav: {
|
|
15
|
+
/** The sidebar's landmark name. */
|
|
16
|
+
label: string;
|
|
17
|
+
collapse: string;
|
|
18
|
+
expand: string;
|
|
19
|
+
collapseTitle: string;
|
|
20
|
+
expandTitle: string;
|
|
21
|
+
};
|
|
22
|
+
rail: {
|
|
23
|
+
show: (label: string) => string;
|
|
24
|
+
hide: (label: string) => string;
|
|
25
|
+
resize: (label: string) => string;
|
|
26
|
+
expand: (label: string) => string;
|
|
27
|
+
restore: (label: string) => string;
|
|
28
|
+
};
|
|
29
|
+
breadcrumb: {
|
|
30
|
+
label: string;
|
|
31
|
+
switchFrom: (label: string) => string;
|
|
32
|
+
};
|
|
33
|
+
table: {
|
|
34
|
+
search: string;
|
|
35
|
+
clearSearch: string;
|
|
36
|
+
results: (count: number) => string;
|
|
37
|
+
loading: string;
|
|
38
|
+
loadFailed: (label: string) => string;
|
|
39
|
+
requestFailed: string;
|
|
40
|
+
retry: string;
|
|
41
|
+
noMatches: string;
|
|
42
|
+
noMatchesDescription: (label: string) => string;
|
|
43
|
+
selected: (count: number) => string;
|
|
44
|
+
clearSelection: string;
|
|
45
|
+
selectRow: (id: string) => string;
|
|
46
|
+
selectPage: (label: string) => string;
|
|
47
|
+
actions: string;
|
|
48
|
+
rowActions: (label: string) => string;
|
|
49
|
+
rowsPerPage: string;
|
|
50
|
+
page: (page: number) => string;
|
|
51
|
+
previousPage: string;
|
|
52
|
+
nextPage: string;
|
|
53
|
+
sortKey: (index: number) => string;
|
|
54
|
+
};
|
|
55
|
+
states: {
|
|
56
|
+
errorTitle: string;
|
|
57
|
+
unexpected: string;
|
|
58
|
+
retry: string;
|
|
59
|
+
errorId: (digest: string) => string;
|
|
60
|
+
loading: string;
|
|
61
|
+
learnMore: string;
|
|
62
|
+
};
|
|
63
|
+
dialog: {
|
|
64
|
+
close: string;
|
|
65
|
+
};
|
|
66
|
+
confirm: {
|
|
67
|
+
confirm: string;
|
|
68
|
+
cancel: string;
|
|
69
|
+
working: string;
|
|
70
|
+
failed: string;
|
|
71
|
+
reasonRequired: string;
|
|
72
|
+
};
|
|
73
|
+
user: {
|
|
74
|
+
/** The collapsed trigger's name: it shows only the initials, so the name starts with them (WCAG 2.5.3). */
|
|
75
|
+
accountMenu: (name: string, initials: string) => string;
|
|
76
|
+
signOut: string;
|
|
77
|
+
};
|
|
78
|
+
appearance: {
|
|
79
|
+
locked: string;
|
|
80
|
+
themes: string;
|
|
81
|
+
accent: string;
|
|
82
|
+
ground: string;
|
|
83
|
+
density: string;
|
|
84
|
+
corners: string;
|
|
85
|
+
reset: string;
|
|
86
|
+
mode: (mode: "dark" | "light") => string;
|
|
87
|
+
groundLabel: (ground: string, label: string) => string;
|
|
88
|
+
accentLabel: (accent: string, label: string) => string;
|
|
89
|
+
densityLabel: (density: "compact" | "cards") => string;
|
|
90
|
+
densityHint: (density: "compact" | "cards") => string;
|
|
91
|
+
cornersLabel: (corners: number) => string;
|
|
92
|
+
customAccent: string;
|
|
93
|
+
customAccentPick: string;
|
|
94
|
+
customAccentHelp: string;
|
|
95
|
+
customAccentInvalid: string;
|
|
96
|
+
customAccentInUse: string;
|
|
97
|
+
customAccentOk: (ratio: string) => string;
|
|
98
|
+
customAccentFaint: (ratio: string, min: number, mode: "dark" | "light") => string;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export declare const defaultMessages: Messages;
|
|
102
|
+
/** Any subset of the messages, one level deep. */
|
|
103
|
+
export type MessageOverrides = {
|
|
104
|
+
[Group in keyof Messages]?: Partial<Messages[Group]>;
|
|
105
|
+
};
|
|
106
|
+
export declare function mergeMessages(overrides: MessageOverrides | undefined): Messages;
|
|
@@ -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
|
+
}
|