@vvedantb/ui 0.5.3 → 0.6.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.
- package/README.md +165 -7
- package/package.json +1 -1
- package/shell/src/AppShell.tsx +130 -43
- package/shell/src/AppSidebar.tsx +27 -119
- package/shell/src/Sidebar.tsx +78 -0
- package/shell/src/SidebarAccountMenu.tsx +169 -77
- package/shell/src/SidebarChrome.tsx +295 -0
- package/shell/src/SidebarHeader.tsx +6 -7
- package/shell/src/SidebarPanel.tsx +95 -33
- package/shell/src/SidebarRail.tsx +74 -28
- package/shell/src/SignOutDialog.tsx +24 -51
- package/shell/src/app-shell-classes.ts +6 -0
- package/shell/src/index.ts +9 -1
- package/src/composer/input-area/InputAreaEditor.tsx +52 -13
- package/src/index.ts +9 -1
- package/src/ui/confirm-dialog-parts.tsx +318 -0
- package/src/ui/confirm-dialog.tsx +65 -106
- package/src/ui/overlay-a11y.ts +5 -3
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
2
|
import { IconX } from "@tabler/icons-react";
|
|
3
|
-
import { Button } from "../../src/ui/button";
|
|
3
|
+
import { Button, type ButtonProps } from "../../src/ui/button";
|
|
4
4
|
import { cn } from "../../src/utils/cn";
|
|
5
|
-
import { panelCloseButtonClass } from "./app-shell-classes";
|
|
6
|
-
import {
|
|
5
|
+
import { panelCloseButtonClass, sidebarHeaderRowClass } from "./app-shell-classes";
|
|
6
|
+
import { useOptionalSidebar } from "./SidebarChrome";
|
|
7
7
|
|
|
8
8
|
export type SidebarPanelProps = {
|
|
9
9
|
title: string;
|
|
10
|
-
/**
|
|
10
|
+
/** Legacy (still supported): rendered in the header, before the close button. Compose `Sidebar.PanelActions`. */
|
|
11
11
|
headerTrailing?: ReactNode;
|
|
12
|
-
/**
|
|
12
|
+
/** Legacy (still supported): when set, a close button follows `headerTrailing`. Pass it on the mobile drawer only. */
|
|
13
13
|
onClose?: () => void;
|
|
14
|
-
/**
|
|
14
|
+
/** Legacy (still supported): close button label. Default `"Close navigation"`. */
|
|
15
15
|
closeLabel?: string;
|
|
16
|
-
/**
|
|
16
|
+
/** Legacy (still supported): merged onto the close button. */
|
|
17
17
|
closeButtonClassName?: string;
|
|
18
18
|
/** Desktop: hides the panel (`md:hidden`) and leaves the rail. */
|
|
19
19
|
collapsed?: boolean;
|
|
@@ -22,50 +22,112 @@ export type SidebarPanelProps = {
|
|
|
22
22
|
children?: ReactNode;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
/** Sidebar panel column beside the rail: a
|
|
25
|
+
/** Sidebar panel column beside the rail: a header and the app's body. Composed from `Sidebar.Panel` parts. */
|
|
26
26
|
export function SidebarPanel({
|
|
27
27
|
title,
|
|
28
28
|
headerTrailing,
|
|
29
29
|
onClose,
|
|
30
|
-
closeLabel
|
|
30
|
+
closeLabel,
|
|
31
31
|
closeButtonClassName,
|
|
32
32
|
collapsed = false,
|
|
33
33
|
className,
|
|
34
34
|
children,
|
|
35
35
|
}: SidebarPanelProps) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
{onClose ? (
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
className={cn(panelCloseButtonClass, closeButtonClassName)}
|
|
48
|
-
>
|
|
49
|
-
<IconX className="h-4 w-4" />
|
|
50
|
-
</Button>
|
|
36
|
+
return (
|
|
37
|
+
<Panel collapsed={collapsed} panelHidden={false} className={className}>
|
|
38
|
+
<PanelHeader>
|
|
39
|
+
<PanelTitle>{title}</PanelTitle>
|
|
40
|
+
{headerTrailing || onClose ? (
|
|
41
|
+
<PanelActions>
|
|
42
|
+
{headerTrailing}
|
|
43
|
+
{onClose ? (
|
|
44
|
+
<PanelClose onClick={onClose} label={closeLabel} className={closeButtonClassName} />
|
|
45
|
+
) : null}
|
|
46
|
+
</PanelActions>
|
|
51
47
|
) : null}
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
</PanelHeader>
|
|
49
|
+
{children}
|
|
50
|
+
</Panel>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
54
53
|
|
|
54
|
+
export type SidebarPanelRootProps = ComponentPropsWithoutRef<"div"> & {
|
|
55
|
+
/** Desktop: hides the panel (`md:hidden`). Default the `Sidebar.Root` `collapsed`, else `false`. */
|
|
56
|
+
collapsed?: boolean;
|
|
57
|
+
/** Renders nothing. Default the `Sidebar.Root` `panelHidden`, else `false`. */
|
|
58
|
+
panelHidden?: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Panel column `<div>` beside the rail. */
|
|
62
|
+
export function Panel({ collapsed, panelHidden, className, ...props }: SidebarPanelRootProps) {
|
|
63
|
+
const sidebar = useOptionalSidebar();
|
|
64
|
+
if (panelHidden ?? sidebar?.panelHidden ?? false) return null;
|
|
55
65
|
return (
|
|
56
66
|
<div
|
|
67
|
+
{...props}
|
|
57
68
|
className={cn(
|
|
58
69
|
// No border-r: the main content card floats beside this panel, so a
|
|
59
70
|
// right edge line reads as a hard divider.
|
|
60
71
|
"relative flex h-full min-w-0 flex-1 flex-col overflow-hidden bg-background",
|
|
61
|
-
collapsed && "md:hidden",
|
|
72
|
+
(collapsed ?? sidebar?.collapsed ?? false) && "md:hidden",
|
|
62
73
|
className,
|
|
63
74
|
)}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type SidebarPanelHeaderProps = ComponentPropsWithoutRef<"div">;
|
|
80
|
+
|
|
81
|
+
/** Padded header row of the panel. Holds `PanelTitle` and `PanelActions`. `className` goes to the outer padding `<div>`. */
|
|
82
|
+
export function PanelHeader({ className, children, ...props }: SidebarPanelHeaderProps) {
|
|
83
|
+
return (
|
|
84
|
+
<div {...props} className={cn("px-2 pt-3", className)}>
|
|
85
|
+
<div className={sidebarHeaderRowClass}>{children}</div>
|
|
69
86
|
</div>
|
|
70
87
|
);
|
|
71
88
|
}
|
|
89
|
+
|
|
90
|
+
export type SidebarPanelTitleProps = ComponentPropsWithoutRef<"h2">;
|
|
91
|
+
|
|
92
|
+
/** Panel title `<h2>`, truncated. */
|
|
93
|
+
export function PanelTitle({ className, ...props }: SidebarPanelTitleProps) {
|
|
94
|
+
return (
|
|
95
|
+
<h2
|
|
96
|
+
{...props}
|
|
97
|
+
className={cn(
|
|
98
|
+
"min-w-0 flex-1 truncate text-left text-base font-semibold leading-none tracking-tight text-foreground",
|
|
99
|
+
className,
|
|
100
|
+
)}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type SidebarPanelActionsProps = ComponentPropsWithoutRef<"div">;
|
|
106
|
+
|
|
107
|
+
/** Trailing actions in the panel header. */
|
|
108
|
+
export function PanelActions({ className, ...props }: SidebarPanelActionsProps) {
|
|
109
|
+
return <div {...props} className={cn("flex items-center justify-end gap-0.5", className)} />;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type SidebarPanelCloseProps = ButtonProps & {
|
|
113
|
+
/** `aria-label`. Default `"Close navigation"`. */
|
|
114
|
+
label?: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** Ghost close button. Without `onClick` it closes the `Sidebar.Root` drawer. */
|
|
118
|
+
export function PanelClose({ label = "Close navigation", onClick, className, children, ...props }: SidebarPanelCloseProps) {
|
|
119
|
+
const sidebar = useOptionalSidebar();
|
|
120
|
+
return (
|
|
121
|
+
<Button
|
|
122
|
+
type="button"
|
|
123
|
+
variant="ghost"
|
|
124
|
+
size="icon-sm"
|
|
125
|
+
{...props}
|
|
126
|
+
onClick={onClick ?? (() => sidebar?.onMobileOpenChange(false))}
|
|
127
|
+
aria-label={label}
|
|
128
|
+
className={cn(panelCloseButtonClass, className)}
|
|
129
|
+
>
|
|
130
|
+
{children ?? <IconX className="h-4 w-4" />}
|
|
131
|
+
</Button>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -28,7 +28,13 @@ export type SidebarRailProps = Omit<ComponentPropsWithoutRef<"div">, "children"
|
|
|
28
28
|
footerClassName?: string;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Icon rail column. Width reads `--app-shell-rail-width`; override with `className`. Other props go to the root `<div>`.
|
|
33
|
+
*
|
|
34
|
+
* Legacy (still supported): the `top`, `footer`, `bodyAsChild`, `tabs` and
|
|
35
|
+
* `*ClassName` props. Compose `Sidebar.Rail`, `.RailHeader`, `.RailBody` and
|
|
36
|
+
* `.RailFooter` instead.
|
|
37
|
+
*/
|
|
32
38
|
export function SidebarRail({
|
|
33
39
|
top,
|
|
34
40
|
children,
|
|
@@ -42,11 +48,24 @@ export function SidebarRail({
|
|
|
42
48
|
footerClassName,
|
|
43
49
|
...props
|
|
44
50
|
}: SidebarRailProps) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
return (
|
|
52
|
+
<Rail {...props} showDivider={showDivider} className={className}>
|
|
53
|
+
{top == null ? null : <RailHeader className={topClassName}>{top}</RailHeader>}
|
|
54
|
+
<RailBody asChild={bodyAsChild} tabs={tabs} className={bodyClassName}>
|
|
55
|
+
{children}
|
|
56
|
+
</RailBody>
|
|
57
|
+
{footer == null ? null : <RailFooter className={footerClassName}>{footer}</RailFooter>}
|
|
58
|
+
</Rail>
|
|
49
59
|
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type SidebarRailRootProps = ComponentPropsWithoutRef<"div"> & {
|
|
63
|
+
/** Adds a right border between the rail and the panel. Default `false`. */
|
|
64
|
+
showDivider?: boolean;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Rail column `<div>`. Width reads `--app-shell-rail-width`. */
|
|
68
|
+
export function Rail({ showDivider = false, className, ...props }: SidebarRailRootProps) {
|
|
50
69
|
return (
|
|
51
70
|
<div
|
|
52
71
|
{...props}
|
|
@@ -55,29 +74,43 @@ export function SidebarRail({
|
|
|
55
74
|
showDivider && "border-r border-separator",
|
|
56
75
|
className,
|
|
57
76
|
)}
|
|
58
|
-
|
|
59
|
-
{top == null ? null : (
|
|
60
|
-
<div className={cn("flex w-full flex-col items-center gap-1.5 px-0 pt-3", topClassName)}>
|
|
61
|
-
{top}
|
|
62
|
-
</div>
|
|
63
|
-
)}
|
|
64
|
-
{tabs ? (
|
|
65
|
-
<TabsPrimitive.List asChild={bodyAsChild} className={bodyClass}>
|
|
66
|
-
{children}
|
|
67
|
-
</TabsPrimitive.List>
|
|
68
|
-
) : (
|
|
69
|
-
<Body className={bodyClass}>{children}</Body>
|
|
70
|
-
)}
|
|
71
|
-
{footer == null ? null : (
|
|
72
|
-
<div className={cn("flex w-full flex-col items-center gap-1.5 py-3", footerClassName)}>
|
|
73
|
-
{footer}
|
|
74
|
-
</div>
|
|
75
|
-
)}
|
|
76
|
-
</div>
|
|
77
|
+
/>
|
|
77
78
|
);
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
export type
|
|
81
|
+
export type SidebarRailHeaderProps = ComponentPropsWithoutRef<"div">;
|
|
82
|
+
|
|
83
|
+
/** Block pinned at the top of the rail, above the scroll body. */
|
|
84
|
+
export function RailHeader({ className, ...props }: SidebarRailHeaderProps) {
|
|
85
|
+
return <div {...props} className={cn("flex w-full flex-col items-center gap-1.5 px-0 pt-3", className)} />;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type SidebarRailBodyProps = ComponentPropsWithoutRef<"div"> & {
|
|
89
|
+
/** Render the body as its single child element (for example a `<nav>`), with the body classes merged. */
|
|
90
|
+
asChild?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Render a Radix tabs list (arrow-key roving focus). Use it inside a Tabs
|
|
93
|
+
* root with `orientation="vertical"` and give each item a `tabValue`.
|
|
94
|
+
*/
|
|
95
|
+
tabs?: boolean;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Scroll body of the rail. */
|
|
99
|
+
export function RailBody({ asChild = false, tabs = false, className, ...props }: SidebarRailBodyProps) {
|
|
100
|
+
const bodyClass = cn("flex w-full flex-1 flex-col items-center gap-1.5 overflow-y-auto py-2", className);
|
|
101
|
+
if (tabs) return <TabsPrimitive.List {...props} asChild={asChild} className={bodyClass} />;
|
|
102
|
+
const Body = asChild ? Slot : "div";
|
|
103
|
+
return <Body {...props} className={bodyClass} />;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type SidebarRailFooterProps = ComponentPropsWithoutRef<"div">;
|
|
107
|
+
|
|
108
|
+
/** Block pinned at the bottom of the rail. */
|
|
109
|
+
export function RailFooter({ className, ...props }: SidebarRailFooterProps) {
|
|
110
|
+
return <div {...props} className={cn("flex w-full flex-col items-center gap-1.5 py-3", className)} />;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type SidebarRailItemProps = Omit<ComponentPropsWithoutRef<"button">, "children"> & {
|
|
81
114
|
/** `aria-label`, and the tooltip unless `tooltip` is set. */
|
|
82
115
|
label: string;
|
|
83
116
|
/** Active state. With `asChild`, also sets `aria-current="page"`. */
|
|
@@ -99,7 +132,7 @@ export type RailTileProps = Omit<ComponentPropsWithoutRef<"button">, "children">
|
|
|
99
132
|
};
|
|
100
133
|
|
|
101
134
|
/** Rail tile with a right-side tooltip. Renders a native `<button>`, its child with `asChild`, or a tabs trigger with `tabValue`. */
|
|
102
|
-
export function
|
|
135
|
+
export function RailItem({
|
|
103
136
|
label,
|
|
104
137
|
active = false,
|
|
105
138
|
tooltip,
|
|
@@ -110,7 +143,7 @@ export function RailTile({
|
|
|
110
143
|
className,
|
|
111
144
|
children,
|
|
112
145
|
...props
|
|
113
|
-
}:
|
|
146
|
+
}: SidebarRailItemProps) {
|
|
114
147
|
return (
|
|
115
148
|
<SidebarIconTooltip label={tooltip ?? label} enabled={showTooltip}>
|
|
116
149
|
{tabValue !== undefined ? (
|
|
@@ -148,8 +181,21 @@ export function RailTile({
|
|
|
148
181
|
);
|
|
149
182
|
}
|
|
150
183
|
|
|
184
|
+
export type RailTileProps = SidebarRailItemProps;
|
|
185
|
+
|
|
186
|
+
/** Legacy name of `Sidebar.RailItem`. */
|
|
187
|
+
export const RailTile = RailItem;
|
|
188
|
+
|
|
189
|
+
export type SidebarRailSeparatorProps = ComponentPropsWithoutRef<"div">;
|
|
190
|
+
|
|
191
|
+
/** Short horizontal rule between rail items. */
|
|
192
|
+
export function RailSeparator({ className, ...props }: SidebarRailSeparatorProps) {
|
|
193
|
+
return <div {...props} className={cn("h-px w-8 bg-separator", className)} aria-hidden />;
|
|
194
|
+
}
|
|
195
|
+
|
|
151
196
|
export type RailDividerProps = { className?: string };
|
|
152
197
|
|
|
198
|
+
/** Legacy name of `Sidebar.RailSeparator`. */
|
|
153
199
|
export function RailDivider({ className }: RailDividerProps) {
|
|
154
|
-
return <
|
|
200
|
+
return <RailSeparator className={className} />;
|
|
155
201
|
}
|
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
import { IconAlertTriangle
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
Dialog,
|
|
5
|
-
DialogContent,
|
|
6
|
-
DialogDescription,
|
|
7
|
-
DialogFooter,
|
|
8
|
-
DialogHeader,
|
|
9
|
-
DialogTitle,
|
|
10
|
-
} from "../../src/ui/dialog";
|
|
1
|
+
import { IconAlertTriangle } from "@tabler/icons-react";
|
|
2
|
+
import { ConfirmDialog } from "../../src/ui/confirm-dialog";
|
|
11
3
|
import { cn } from "../../src/utils/cn";
|
|
12
4
|
import { signOutCancelClass, signOutDescriptionClass } from "./app-shell-classes";
|
|
13
5
|
|
|
@@ -22,13 +14,13 @@ export type SignOutDialogProps = {
|
|
|
22
14
|
description?: string;
|
|
23
15
|
/** Merged onto the dialog content after `sm:max-w-sm`. */
|
|
24
16
|
contentClassName?: string;
|
|
25
|
-
/**
|
|
17
|
+
/** Legacy (still supported): dialog title and confirm button label. Default `"Sign out"`. */
|
|
26
18
|
title?: string;
|
|
27
|
-
/**
|
|
19
|
+
/** Legacy (still supported): default `"Sign out of your account?"`. */
|
|
28
20
|
question?: string;
|
|
29
21
|
};
|
|
30
22
|
|
|
31
|
-
/** Sign-out confirmation. The app owns the state and the sign-out call. */
|
|
23
|
+
/** Sign-out confirmation. The app owns the state and the sign-out call. Composed from `ConfirmDialog` parts. */
|
|
32
24
|
export function SignOutDialog({
|
|
33
25
|
open,
|
|
34
26
|
onOpenChange,
|
|
@@ -40,21 +32,15 @@ export function SignOutDialog({
|
|
|
40
32
|
question = "Sign out of your account?",
|
|
41
33
|
}: SignOutDialogProps) {
|
|
42
34
|
return (
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}}
|
|
49
|
-
>
|
|
50
|
-
<DialogContent className={cn("sm:max-w-sm", contentClassName)}>
|
|
51
|
-
<DialogHeader>
|
|
52
|
-
<DialogTitle className="text-foreground">{title}</DialogTitle>
|
|
53
|
-
<DialogDescription className="sr-only">
|
|
35
|
+
<ConfirmDialog.Root open={open} onOpenChange={onOpenChange} alert={false} pending={pending}>
|
|
36
|
+
<ConfirmDialog.Content className={cn("sm:max-w-sm", contentClassName)}>
|
|
37
|
+
<ConfirmDialog.Header>
|
|
38
|
+
<ConfirmDialog.Title className="text-foreground">{title}</ConfirmDialog.Title>
|
|
39
|
+
<ConfirmDialog.Description className="sr-only">
|
|
54
40
|
Confirm signing out of your account
|
|
55
|
-
</
|
|
56
|
-
</
|
|
57
|
-
<
|
|
41
|
+
</ConfirmDialog.Description>
|
|
42
|
+
</ConfirmDialog.Header>
|
|
43
|
+
<ConfirmDialog.Body className="flex items-start gap-3 py-4">
|
|
58
44
|
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-danger/10">
|
|
59
45
|
<IconAlertTriangle size={20} className="text-danger" />
|
|
60
46
|
</div>
|
|
@@ -62,32 +48,19 @@ export function SignOutDialog({
|
|
|
62
48
|
<p className="text-foreground">{question}</p>
|
|
63
49
|
{description ? <p className={signOutDescriptionClass}>{description}</p> : null}
|
|
64
50
|
</div>
|
|
65
|
-
</
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
onClick={() => onOpenChange(false)}
|
|
70
|
-
disabled={pending}
|
|
71
|
-
className={signOutCancelClass}
|
|
72
|
-
>
|
|
73
|
-
Cancel
|
|
74
|
-
</Button>
|
|
75
|
-
<Button
|
|
51
|
+
</ConfirmDialog.Body>
|
|
52
|
+
<ConfirmDialog.Footer>
|
|
53
|
+
<ConfirmDialog.Cancel className={signOutCancelClass} />
|
|
54
|
+
<ConfirmDialog.Action
|
|
76
55
|
onClick={() => void onConfirm()}
|
|
77
|
-
|
|
56
|
+
pendingLabel="Signing out..."
|
|
57
|
+
spinnerSize={16}
|
|
78
58
|
className="bg-danger text-danger-foreground"
|
|
79
59
|
>
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
) : (
|
|
86
|
-
title
|
|
87
|
-
)}
|
|
88
|
-
</Button>
|
|
89
|
-
</DialogFooter>
|
|
90
|
-
</DialogContent>
|
|
91
|
-
</Dialog>
|
|
60
|
+
{title}
|
|
61
|
+
</ConfirmDialog.Action>
|
|
62
|
+
</ConfirmDialog.Footer>
|
|
63
|
+
</ConfirmDialog.Content>
|
|
64
|
+
</ConfirmDialog.Root>
|
|
92
65
|
);
|
|
93
66
|
}
|
|
@@ -33,6 +33,9 @@ export function sidebarNavRowClass(isIconOnly: boolean): string {
|
|
|
33
33
|
return cn(isIconOnly ? "justify-center px-0 py-1.5" : "gap-3 px-3 py-1.5");
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** Sidebar header row: the panel title and its trailing actions. */
|
|
37
|
+
export const sidebarHeaderRowClass = "flex h-11 items-center gap-1 pl-3 pr-1";
|
|
38
|
+
|
|
36
39
|
export const mobileMenuButtonClass = "-ml-1 h-11 w-11 shrink-0 rounded-lg text-muted hover:bg-surface-tertiary/50 hover:text-foreground"; // check-tokens-allow: text-muted
|
|
37
40
|
|
|
38
41
|
export const panelCloseButtonClass = "rounded-lg text-muted hover:bg-surface-tertiary/50 hover:text-foreground"; // check-tokens-allow: text-muted
|
|
@@ -42,3 +45,6 @@ export const accountEmailClass = "block truncate text-xs font-normal text-muted"
|
|
|
42
45
|
export const signOutDescriptionClass = "mt-1 text-sm text-muted"; // check-tokens-allow: text-muted
|
|
43
46
|
|
|
44
47
|
export const signOutCancelClass = "text-muted"; // check-tokens-allow: text-muted
|
|
48
|
+
|
|
49
|
+
/** Label of the type-to-confirm field (`ConfirmDialog.Field`). */
|
|
50
|
+
export const confirmFieldLabelClass = "text-sm text-muted"; // check-tokens-allow: text-muted
|
package/shell/src/index.ts
CHANGED
|
@@ -11,9 +11,17 @@ export type {
|
|
|
11
11
|
SessionsHoverCardSurface,
|
|
12
12
|
} from "./SessionsHoverCard.js";
|
|
13
13
|
export { AppShell } from "./AppShell.js";
|
|
14
|
-
export type {
|
|
14
|
+
export type {
|
|
15
|
+
AppShellContentProps,
|
|
16
|
+
AppShellMainProps,
|
|
17
|
+
AppShellProps,
|
|
18
|
+
AppShellRootProps,
|
|
19
|
+
AppShellSkipLinkProps,
|
|
20
|
+
} from "./AppShell.js";
|
|
15
21
|
export { AppSidebar } from "./AppSidebar.js";
|
|
16
22
|
export type { AppSidebarProps } from "./AppSidebar.js";
|
|
23
|
+
// Composable sidebar parts (`Sidebar.Root`, `Sidebar.Rail`, ...) and their prop types.
|
|
24
|
+
export * from "./Sidebar.js";
|
|
17
25
|
export { SidebarRail, RailTile, RailDivider } from "./SidebarRail.js";
|
|
18
26
|
export type { SidebarRailProps, RailTileProps, RailDividerProps } from "./SidebarRail.js";
|
|
19
27
|
export { SidebarResizeHandle, useAppShellResize } from "./SidebarResize.js";
|
|
@@ -30,16 +30,33 @@ const CHIPS_HOST_CLASS = "contents empty:hidden [&>*]:mr-1.5";
|
|
|
30
30
|
|
|
31
31
|
const ZERO_WIDTH_SPACE = "\u200b";
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* A value of "\n" is an empty draft: Shift+Enter on an empty editor, or a
|
|
35
|
+
* stray line holder that apps stored before 0.5.4. Mirrors `isComposerCompact`.
|
|
36
|
+
*/
|
|
37
|
+
function isEmptyDraft(value: string): boolean {
|
|
38
|
+
return value === "" || value === "\n";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Reads the plain text of the editor. Skips the chips host. Under `pre-wrap` a
|
|
43
|
+
* single trailing "\n" renders no line, so Chromium ends the text with an extra
|
|
44
|
+
* "\n" that only holds the last line open. That holder, or a trailing `<br>`,
|
|
45
|
+
* is dropped once.
|
|
46
|
+
*/
|
|
34
47
|
export function extractEditorText(root: HTMLElement): string {
|
|
35
48
|
let out = "";
|
|
49
|
+
let heldByBr = false;
|
|
36
50
|
const walk = (node: Node, isRoot: boolean) => {
|
|
37
51
|
node.childNodes.forEach((child, index) => {
|
|
38
52
|
if (child instanceof HTMLElement) {
|
|
39
53
|
if (child.hasAttribute(CHIPS_HOST_ATTR)) return;
|
|
40
54
|
if (child.tagName === "BR") {
|
|
41
55
|
// A trailing <br> only holds the line open.
|
|
42
|
-
if (isRoot && index === node.childNodes.length - 1)
|
|
56
|
+
if (isRoot && index === node.childNodes.length - 1) {
|
|
57
|
+
heldByBr = true;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
43
60
|
out += "\n";
|
|
44
61
|
return;
|
|
45
62
|
}
|
|
@@ -57,7 +74,29 @@ export function extractEditorText(root: HTMLElement): string {
|
|
|
57
74
|
});
|
|
58
75
|
};
|
|
59
76
|
walk(root, true);
|
|
60
|
-
return out;
|
|
77
|
+
return !heldByBr && out.endsWith("\n") ? out.slice(0, -1) : out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The DOM text for a value: a trailing "\n" gets its line holder. */
|
|
81
|
+
function editorTextFor(value: string): string {
|
|
82
|
+
return value.endsWith("\n") ? `${value}\n` : value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Every editor node except the chips host: the anchor, text and line breaks. */
|
|
86
|
+
function textNodes(root: HTMLElement): ChildNode[] {
|
|
87
|
+
return Array.from(root.childNodes).filter(
|
|
88
|
+
(child) => !(child instanceof HTMLElement && child.hasAttribute(CHIPS_HOST_ATTR)),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* True when the DOM shows `shown`. An empty draft must be the bare U+200B
|
|
94
|
+
* anchor: a stray "\n" or `<br>` reads as "" but pushes the placeholder down.
|
|
95
|
+
*/
|
|
96
|
+
function editorShows(root: HTMLElement, shown: string): boolean {
|
|
97
|
+
if (shown !== "") return extractEditorText(root) === shown;
|
|
98
|
+
const [only, ...rest] = textNodes(root);
|
|
99
|
+
return rest.length === 0 && only instanceof Text && only.data === ZERO_WIDTH_SPACE;
|
|
61
100
|
}
|
|
62
101
|
|
|
63
102
|
function chipsHost(root: HTMLElement): HTMLElement | null {
|
|
@@ -96,11 +135,14 @@ function setSelection(
|
|
|
96
135
|
window.getSelection()?.setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
|
|
97
136
|
}
|
|
98
137
|
|
|
99
|
-
/**
|
|
138
|
+
/**
|
|
139
|
+
* End of the text: the last text node before its trailing line holder, or the
|
|
140
|
+
* end of the editor after a line break.
|
|
141
|
+
*/
|
|
100
142
|
function textEnd(root: HTMLElement): [Node, number] {
|
|
101
143
|
ensureCaretAnchor(root);
|
|
102
144
|
const last = root.lastChild;
|
|
103
|
-
if (last instanceof Text) return [last, last.length];
|
|
145
|
+
if (last instanceof Text) return [last, last.data.endsWith("\n") ? last.length - 1 : last.length];
|
|
104
146
|
return [root, root.childNodes.length];
|
|
105
147
|
}
|
|
106
148
|
|
|
@@ -276,13 +318,10 @@ export function InputAreaEditor({
|
|
|
276
318
|
useLayoutEffect(() => {
|
|
277
319
|
const root = rootRef.current;
|
|
278
320
|
if (!root || composingRef.current) return;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
root.appendChild(document.createTextNode(ZERO_WIDTH_SPACE + value));
|
|
321
|
+
const shown = isEmptyDraft(value) ? "" : value;
|
|
322
|
+
if (!editorShows(root, shown)) {
|
|
323
|
+
textNodes(root).forEach((child) => root.removeChild(child));
|
|
324
|
+
root.appendChild(document.createTextNode(ZERO_WIDTH_SPACE + editorTextFor(shown)));
|
|
286
325
|
if (document.activeElement === root) placeCaretAtEnd(root);
|
|
287
326
|
}
|
|
288
327
|
ensureCaretAnchor(root);
|
|
@@ -361,7 +400,7 @@ export function InputAreaEditor({
|
|
|
361
400
|
aria-multiline
|
|
362
401
|
{...ariaProps}
|
|
363
402
|
role="combobox"
|
|
364
|
-
data-empty={value
|
|
403
|
+
data-empty={isEmptyDraft(value) ? "" : undefined}
|
|
365
404
|
data-placeholder={placeholder}
|
|
366
405
|
data-suggestion={suggestion || undefined}
|
|
367
406
|
className={cn(INLINE_EDITOR_CLASS, className)}
|
package/src/index.ts
CHANGED
|
@@ -123,9 +123,17 @@ export type {
|
|
|
123
123
|
// App shell: sidebar and main page layout. Widths come from the
|
|
124
124
|
// `--app-shell-*` CSS variables that `AppShell` sets.
|
|
125
125
|
export { AppShell } from "../shell/src/AppShell";
|
|
126
|
-
export type {
|
|
126
|
+
export type {
|
|
127
|
+
AppShellContentProps,
|
|
128
|
+
AppShellMainProps,
|
|
129
|
+
AppShellProps,
|
|
130
|
+
AppShellRootProps,
|
|
131
|
+
AppShellSkipLinkProps,
|
|
132
|
+
} from "../shell/src/AppShell";
|
|
127
133
|
export { AppSidebar } from "../shell/src/AppSidebar";
|
|
128
134
|
export type { AppSidebarProps } from "../shell/src/AppSidebar";
|
|
135
|
+
// Composable sidebar parts (`Sidebar.Root`, `Sidebar.Rail`, ...) and their prop types.
|
|
136
|
+
export * from "../shell/src/Sidebar";
|
|
129
137
|
export { SidebarRail, RailTile, RailDivider } from "../shell/src/SidebarRail";
|
|
130
138
|
export type { SidebarRailProps, RailTileProps, RailDividerProps } from "../shell/src/SidebarRail";
|
|
131
139
|
export { SidebarResizeHandle, useAppShellResize } from "../shell/src/SidebarResize";
|