@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.
@@ -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 { SidebarHeader } from "./SidebarHeader";
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
- /** Rendered in the header, before the close button. */
10
+ /** Legacy (still supported): rendered in the header, before the close button. Compose `Sidebar.PanelActions`. */
11
11
  headerTrailing?: ReactNode;
12
- /** When set, a close button follows `headerTrailing`. Pass it on the mobile drawer only. */
12
+ /** Legacy (still supported): when set, a close button follows `headerTrailing`. Pass it on the mobile drawer only. */
13
13
  onClose?: () => void;
14
- /** Close button label. Default `"Close navigation"`. */
14
+ /** Legacy (still supported): close button label. Default `"Close navigation"`. */
15
15
  closeLabel?: string;
16
- /** Merged onto the close button. */
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 `SidebarHeader` and the app's body. */
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 = "Close navigation",
30
+ closeLabel,
31
31
  closeButtonClassName,
32
32
  collapsed = false,
33
33
  className,
34
34
  children,
35
35
  }: SidebarPanelProps) {
36
- const trailing =
37
- headerTrailing || onClose ? (
38
- <>
39
- {headerTrailing}
40
- {onClose ? (
41
- <Button
42
- type="button"
43
- variant="ghost"
44
- size="icon-sm"
45
- onClick={onClose}
46
- aria-label={closeLabel}
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
- ) : null;
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
- <div className="px-2 pt-3">
66
- <SidebarHeader title={title} trailing={trailing} />
67
- </div>
68
- {children}
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
- /** Icon rail column. Width reads `--app-shell-rail-width`; override with `className`. Other props go to the root `<div>`. */
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
- const Body = bodyAsChild ? Slot : "div";
46
- const bodyClass = cn(
47
- "flex w-full flex-1 flex-col items-center gap-1.5 overflow-y-auto py-2",
48
- bodyClassName,
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 RailTileProps = Omit<ComponentPropsWithoutRef<"button">, "children"> & {
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 RailTile({
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
- }: RailTileProps) {
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 <div className={cn("h-px w-8 bg-separator", className)} aria-hidden />;
200
+ return <RailSeparator className={className} />;
155
201
  }
@@ -1,13 +1,5 @@
1
- import { IconAlertTriangle, IconLoader2 } from "@tabler/icons-react";
2
- import { Button } from "../../src/ui/button";
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
- /** Dialog title and confirm button label. Default `"Sign out"`. */
17
+ /** Legacy (still supported): dialog title and confirm button label. Default `"Sign out"`. */
26
18
  title?: string;
27
- /** Default `"Sign out of your account?"`. */
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
- <Dialog
44
- open={open}
45
- onOpenChange={(next) => {
46
- if (!next && pending) return;
47
- onOpenChange(next);
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
- </DialogDescription>
56
- </DialogHeader>
57
- <div className="flex items-start gap-3 py-4">
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
- </div>
66
- <DialogFooter>
67
- <Button
68
- variant="ghost"
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
- disabled={pending}
56
+ pendingLabel="Signing out..."
57
+ spinnerSize={16}
78
58
  className="bg-danger text-danger-foreground"
79
59
  >
80
- {pending ? (
81
- <>
82
- <IconLoader2 size={16} className="animate-spin" />
83
- Signing out...
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
@@ -11,9 +11,17 @@ export type {
11
11
  SessionsHoverCardSurface,
12
12
  } from "./SessionsHoverCard.js";
13
13
  export { AppShell } from "./AppShell.js";
14
- export type { AppShellProps } from "./AppShell.js";
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
- /** Reads the plain text of the editor. Skips the chips host. */
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) return;
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
- /** End of the text: the last text node, or the end of the editor after a line break. */
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
- if (extractEditorText(root) !== value) {
280
- Array.from(root.childNodes).forEach((child) => {
281
- if (!(child instanceof HTMLElement && child.hasAttribute(CHIPS_HOST_ATTR))) {
282
- root.removeChild(child);
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 === "" ? "" : undefined}
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 { AppShellProps } from "../shell/src/AppShell";
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";