@botiverse/raft-ui 0.83.0 → 0.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/cn.d.mts +2 -0
  3. package/dist/cn.mjs +21 -1
  4. package/dist/components/alert-dialog/alert-dialog.mjs +2 -2
  5. package/dist/components/app-link-card/app-link-card.mjs +19 -37
  6. package/dist/components/app-shell/app-shell.mjs +2 -2
  7. package/dist/components/avatar/avatar.mjs +1 -1
  8. package/dist/components/banner/banner.mjs +15 -2
  9. package/dist/components/button/button-split.mjs +2 -2
  10. package/dist/components/button/button.mjs +2 -2
  11. package/dist/components/choice-list/choice-list.recipe.mjs +1 -1
  12. package/dist/components/code-block/code-block.recipe.mjs +2 -2
  13. package/dist/components/combobox/combobox.mjs +1 -1
  14. package/dist/components/combobox-select/combobox-select.css +1 -1
  15. package/dist/components/comment/comment.recipe.mjs +2 -2
  16. package/dist/components/composer/composer-attachment.recipe.mjs +5 -5
  17. package/dist/components/composer/composer-drop-overlay.mjs +2 -2
  18. package/dist/components/composer/composer-drop-overlay.recipe.mjs +1 -1
  19. package/dist/components/composer/sortable-composer-attachments.mjs +1 -1
  20. package/dist/components/context-menu/context-menu.mjs +1 -1
  21. package/dist/components/conversation-panel/conversation-panel-empty-state.mjs +1 -1
  22. package/dist/components/dialog/dialog.mjs +4 -4
  23. package/dist/components/drag/drag-lift.d.mts +3 -2
  24. package/dist/components/drag/drag-lift.mjs +3 -2
  25. package/dist/components/drag/drag.recipe.mjs +1 -1
  26. package/dist/components/drawer/drawer.recipe.mjs +4 -4
  27. package/dist/components/dropdown-menu/dropdown-menu.mjs +1 -1
  28. package/dist/components/file-preview/file-preview.recipe.mjs +1 -1
  29. package/dist/components/fit-toolbar/fit-toolbar.d.mts +63 -8
  30. package/dist/components/fit-toolbar/fit-toolbar.mjs +310 -43
  31. package/dist/components/fit-toolbar/fit-toolbar.recipe.mjs +21 -7
  32. package/dist/components/fit-toolbar/index.d.mts +2 -2
  33. package/dist/components/inbox-item/inbox-item.recipe.mjs +7 -7
  34. package/dist/components/lightbox/index.d.mts +2 -2
  35. package/dist/components/lightbox/lightbox.d.mts +70 -4
  36. package/dist/components/lightbox/lightbox.mjs +285 -91
  37. package/dist/components/list-row/list-row.d.mts +14 -1
  38. package/dist/components/list-row/list-row.mjs +4 -2
  39. package/dist/components/list-row/list-row.recipe.d.mts +12 -0
  40. package/dist/components/list-row/list-row.recipe.mjs +19 -7
  41. package/dist/components/message-action-card/message-action-card-content.recipe.mjs +3 -3
  42. package/dist/components/message-action-card/message-action-card.mjs +7 -3
  43. package/dist/components/message-action-card/message-action-card.recipe.mjs +2 -6
  44. package/dist/components/message-attachment/message-image-gallery.recipe.mjs +1 -1
  45. package/dist/components/message-embed/index.d.mts +2 -1
  46. package/dist/components/message-embed/message-embed.d.mts +8 -1
  47. package/dist/components/message-embed/message-embed.mjs +2 -2
  48. package/dist/components/message-embed/message-embed.recipe.d.mts +160 -0
  49. package/dist/components/message-embed/message-embed.recipe.mjs +49 -10
  50. package/dist/components/message-forwarded-bundle/message-forwarded-bundle.mjs +1 -0
  51. package/dist/components/message-forwarded-bundle/message-forwarded-bundle.recipe.mjs +1 -1
  52. package/dist/components/message-item/message-item.recipe.mjs +2 -2
  53. package/dist/components/message-list/message-list.recipe.mjs +2 -2
  54. package/dist/components/message-quoted-preview/message-quoted-preview.mjs +2 -0
  55. package/dist/components/message-replies/message-replies.mjs +1 -1
  56. package/dist/components/message-replies/message-replies.recipe.mjs +1 -1
  57. package/dist/components/mobile-nav/mobile-nav.mjs +2 -2
  58. package/dist/components/notification-center/notification-center.mjs +1 -1
  59. package/dist/components/panel/panel-header.recipe.mjs +1 -1
  60. package/dist/components/panel/panel-split-action.recipe.mjs +2 -2
  61. package/dist/components/popover/popover.d.mts +11 -2
  62. package/dist/components/popover/popover.mjs +51 -38
  63. package/dist/components/preview-card/preview-card.d.mts +5 -1
  64. package/dist/components/preview-card/preview-card.mjs +25 -28
  65. package/dist/components/pull-to-refresh/pull-to-refresh-indicator.mjs +1 -1
  66. package/dist/components/resizable/resizable.recipe.mjs +1 -1
  67. package/dist/components/save-status/save-status.d.mts +16 -9
  68. package/dist/components/save-status/save-status.mjs +19 -30
  69. package/dist/components/scroll-area/scroll-area.css +1 -0
  70. package/dist/components/segmented-control/segmented-control.mjs +45 -8
  71. package/dist/components/select/select.mjs +2 -2
  72. package/dist/components/selection-toolbar/selection-toolbar.recipe.mjs +1 -1
  73. package/dist/components/sidebar/sidebar.recipe.mjs +1 -1
  74. package/dist/components/tabs/tabs.mjs +4 -4
  75. package/dist/components/task-board/task-board.d.mts +5 -0
  76. package/dist/components/task-board/task-board.mjs +8 -1
  77. package/dist/components/task-dnd/task-dnd.d.mts +10 -1
  78. package/dist/components/task-dnd/task-dnd.mjs +2 -1
  79. package/dist/components/task-dnd/task-dnd.recipe.mjs +2 -2
  80. package/dist/components/thread-panel/thread-panel.recipe.mjs +1 -1
  81. package/dist/components/toast/toast.mjs +5 -5
  82. package/dist/components/tooltip/tooltip.mjs +2 -2
  83. package/dist/index.d.mts +2 -1
  84. package/dist/index.mjs +2 -1
  85. package/dist/lib/laid-out-children.mjs +32 -0
  86. package/dist/lib/list-cell.mjs +1 -1
  87. package/dist/lib/stacked-actions.mjs +7 -4
  88. package/dist/lint/no-raw-z-index.mjs +142 -0
  89. package/dist/lint/prefer-scroll-area.mjs +1 -1
  90. package/dist/lint/recommended.json +2 -1
  91. package/dist/lint.d.mts +8 -2
  92. package/dist/lint.mjs +16 -3
  93. package/dist/styles.css +28 -0
  94. package/dist/wip.d.mts +4 -2
  95. package/dist/wip.mjs +3 -2
  96. package/package.json +1 -1
  97. package/skills/raft-ui-guide/SKILL.md +1 -0
@@ -1,20 +1,68 @@
1
1
  import { StyledProps } from "../../lib/styled-props.mjs";
2
2
  import { ButtonProps } from "../button/button.mjs";
3
+ import { ScrollAreaProps } from "../scroll-area/types.mjs";
3
4
  import { Dialog } from "@base-ui/react/dialog";
4
5
  import { ReactNode } from "react";
5
6
  import { useRender } from "@base-ui/react/use-render";
6
7
  //#region src/components/lightbox/lightbox.d.ts
7
- export type LightboxProps<Payload = unknown> = Omit<Dialog.Root.Props<Payload>, "actionsRef" | "children" | "handle"> & {
8
+ export type LightboxProps<Payload = unknown> = Omit<Dialog.Root.Props<Payload>, "actionsRef" | "children" | "handle" | "modal"> & {
8
9
  children?: ReactNode;
10
+ /**
11
+ * Whether Cmd+W (Ctrl+W) closes the lightbox instead of the browser tab.
12
+ * @default true
13
+ */
9
14
  closeOnModW?: boolean;
15
+ /**
16
+ * How the lightbox holds the page while it is open.
17
+ * - `true`: focus is trapped in it and the page's scroll is locked. For an
18
+ * image or a preview that fits the window.
19
+ * - `"trap-focus"`: document mode, for a file read top to bottom (a PDF, a
20
+ * document, a diagram). Focus is still trapped, but the page's scroll is
21
+ * not locked: the document's own body scrolls, and Base UI's lock would
22
+ * only rewrite the page's `<html>` and `<body>` styles under it, again on
23
+ * every resize. The scrim still covers the page.
24
+ * - `false`: nothing is held; focus can leave the lightbox.
25
+ *
26
+ * In every mode the content is neither `inert` nor hidden, so the browser's
27
+ * find in page reaches it.
28
+ * @default true
29
+ */
30
+ modal?: boolean | "trap-focus";
10
31
  };
32
+ /**
33
+ * A full-screen viewer over the scrim for images and attachment previews,
34
+ * with previous and next, thumbnails and one button to close. Pass
35
+ * `modal="trap-focus"` for a file read top to bottom (document mode).
36
+ */
11
37
  export declare function Lightbox<Payload = unknown>({ closeOnModW, defaultOpen, modal, onOpenChange, open, children, ...props }: LightboxProps<Payload>): import("react").JSX.Element;
12
38
  export type LightboxTriggerProps = Dialog.Trigger.Props;
13
39
  export declare function LightboxTrigger(props: LightboxTriggerProps): import("react").JSX.Element;
14
- export type LightboxPortalProps = Dialog.Portal.Props;
15
- export declare function LightboxPortal(props: LightboxPortalProps): import("react").JSX.Element;
40
+ export type LightboxPortalProps = StyledProps<Dialog.Portal.Props>;
41
+ /**
42
+ * The lightbox's own stacking context, at `z-lightbox` (70) above the page's dialogs
43
+ * and popups: the scrim, the content, and every Base UI popup opened from
44
+ * inside, which Base UI portals into this element by default, in DOM order.
45
+ * A popup's own `z-popup` then counts inside the lightbox, so it is drawn above
46
+ * the content instead of under the scrim, and a press on it is a press inside
47
+ * the lightbox.
48
+ */
49
+ export declare function LightboxPortal({ className, ...props }: LightboxPortalProps): import("react").JSX.Element;
16
50
  export type LightboxContentProps = StyledProps<Dialog.Popup.Props>;
17
- export declare function LightboxContent({ className, initialFocus, finalFocus, ...props }: LightboxContentProps): import("react").JSX.Element;
51
+ /**
52
+ * The full-screen layer, with the scrim behind it. Popups opened inside it
53
+ * are drawn above it (`LightboxPortal`). `initialFocus` and `finalFocus` are
54
+ * `false` by default, so focus doesn't move when it opens or closes, except
55
+ * in document mode (`modal="trap-focus"`): focus moves into the lightbox when
56
+ * it opens and back to `LightboxTrigger` when it closes. It lands on
57
+ * `LightboxBody`, so the keys that scroll scroll the document at once; on the
58
+ * first control when there is no body; and on the lightbox itself after a
59
+ * tap, so a phone shows no ring and opens no keyboard. A key that would
60
+ * scroll the page under the scrim scrolls the body instead.
61
+ *
62
+ * A lightbox opened from state passes `finalFocus={openerRef}` so focus goes
63
+ * back to what opened it.
64
+ */
65
+ export declare function LightboxContent({ className, initialFocus, finalFocus, onKeyDown, ref, ...props }: LightboxContentProps): import("react").JSX.Element;
18
66
  export type LightboxHeaderProps = useRender.ComponentProps<"div">;
19
67
  export declare function LightboxHeader({ render, className, ...props }: LightboxHeaderProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
20
68
  export type LightboxToolbarProps = useRender.ComponentProps<"div">;
@@ -27,6 +75,24 @@ export type LightboxTitleProps = useRender.ComponentProps<"div">;
27
75
  export declare function LightboxTitle({ render, className, ...props }: LightboxTitleProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
28
76
  export type LightboxActionsProps = useRender.ComponentProps<"div">;
29
77
  export declare function LightboxActions({ render, className, ...props }: LightboxActionsProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
78
+ export type LightboxBodyProps = Omit<ScrollAreaProps, "overscroll">;
79
+ /**
80
+ * The scrolling body of a document-mode lightbox (`modal="trap-focus"`),
81
+ * under `LightboxToolbar`: a ScrollArea that fills the rest of the content
82
+ * (`min-h-0 flex-1`) on `layer-canvas`, the toolbar's surface. Pass a
83
+ * `className` for another surface (`bg-layer-sidebar`). It takes the pointer
84
+ * back from `LightboxContent`, which lets it through to the scrim, and never
85
+ * passes a scroll on to the page under the scrim (`overscroll-contain`, also
86
+ * while it has nothing to scroll; `overscroll-none` in `className` also stops
87
+ * the bounce). Every other ScrollArea prop, `render` included, is the area's.
88
+ *
89
+ * It is a tab stop even when it has nothing to scroll (`tabIndex={0}`), as it
90
+ * takes focus when the lightbox opens, and draws its focus ring inside its
91
+ * edge, which is the window's. Its content keeps clear of a phone's safe
92
+ * areas at the bottom and the sides, around the caller's own content box
93
+ * (`contentClassName`), so the scrollbar stays at the edge.
94
+ */
95
+ export declare function LightboxBody({ children, className, contentClassName, contentStyle, ...props }: LightboxBodyProps): import("react").JSX.Element;
30
96
  export type LightboxStageProps = useRender.ComponentProps<"div">;
31
97
  export declare function LightboxStage({ render, className, ...props }: LightboxStageProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
32
98
  export type LightboxMediaProps = useRender.ComponentProps<"div">;
@@ -2,96 +2,115 @@ import { cn } from "../../cn.mjs";
2
2
  import { headEdgePull } from "../../lib/head-edges.mjs";
3
3
  import { tv } from "../../lib/tv.mjs";
4
4
  import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
5
+ import { OverlayLayerProvider, useParentOverlayKind } from "../../lib/overlay-depth.mjs";
5
6
  import { Button } from "../button/button.mjs";
7
+ import { useMergedRef } from "../../lib/use-merged-ref.mjs";
8
+ import { ScrollArea } from "../scroll-area/scroll-area.mjs";
6
9
  import { Dialog } from "@base-ui/react/dialog";
7
- import { useCallback, useRef, useState } from "react";
10
+ import { createContext, useCallback, useContext, useRef, useState } from "react";
8
11
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
9
12
  import { mergeProps } from "@base-ui/react/merge-props";
10
13
  import { useRender } from "@base-ui/react/use-render";
11
14
  import { useHotkey } from "@tanstack/react-hotkeys";
12
15
  //#region src/components/lightbox/lightbox.tsx
13
- const lightbox = tv({ slots: {
14
- backdrop: ["fixed inset-0 z-70 bg-layer-backdrop data-closed:hidden"],
15
- content: [
16
- "pointer-events-none fixed inset-0 z-71 flex flex-col data-closed:hidden",
17
- "*:data-lightbox-close:pointer-events-auto *:data-lightbox-close:absolute *:data-lightbox-close:z-20",
18
- "*:data-lightbox-close:top-[calc(16px+env(safe-area-inset-top,0px))] *:data-lightbox-close:right-[calc(16px+env(safe-area-inset-right,0px))]"
19
- ],
20
- header: [
21
- "pointer-events-auto shrink-0",
22
- "flex items-center gap-3 pb-3",
23
- "pt-[calc(env(safe-area-inset-top,0px)+--spacing(3))]",
24
- "pl-[calc(env(safe-area-inset-left,0px)+--spacing(4))]",
25
- "pr-[calc(env(safe-area-inset-right,0px)+--spacing(4))]",
26
- "bg-transparent shadow-none",
27
- headEdgePull
28
- ],
29
- toolbar: [
30
- "pointer-events-auto shrink-0 flex items-center gap-3",
31
- "h-[calc(var(--header-height)+env(safe-area-inset-top,0px))] pt-[env(safe-area-inset-top,0px)]",
32
- "pl-[calc(16px+env(safe-area-inset-left,0px))] pr-[calc(16px+env(safe-area-inset-right,0px))]",
33
- "**:data-lightbox-close:static **:data-lightbox-close:inset-auto",
34
- "**:data-lightbox-close:top-auto **:data-lightbox-close:right-auto",
35
- "border-b border-line-seam bg-layer-canvas shadow-none",
36
- headEdgePull
37
- ],
38
- headerMeta: ["flex min-w-0 flex-1 items-center gap-1.5"],
39
- headerLabel: [
40
- "inline-flex shrink-0 items-center px-1.5",
41
- "h-[22px] rounded-chip bg-fill-muted py-0",
42
- "font-sans text-caption font-semibold normal-case tracking-normal text-foreground-muted"
43
- ],
44
- title: ["min-w-0 flex-1 truncate", "py-0 font-sans text-headline font-bold text-foreground"],
45
- actions: [
46
- "flex shrink-0 items-center",
47
- "gap-1",
48
- "[&>[data-icon-inset]:nth-last-child(1_of_:not([data-base-ui-focus-guard],[aria-owns]))]:-mr-icon-inset"
49
- ],
50
- stage: ["pointer-events-none relative flex min-h-0 flex-1 items-center justify-center overflow-hidden"],
51
- media: ["pointer-events-auto max-h-[80vh] max-w-[86vw] border-0 bg-transparent shadow-none"],
52
- navigationButton: [
53
- "pointer-events-auto absolute z-10 inline-flex items-center justify-center",
54
- "transition-[background-color,color,opacity,box-shadow,transform] duration-150",
55
- "outline-[1px] outline-offset-2 outline-transparent",
56
- "data-[disabled=true]:cursor-default",
57
- "[&_svg]:pointer-events-none [&_svg]:shrink-0",
58
- "size-11 rounded-control border-[1.5px] border-line-strong bg-layer-card text-foreground shadow-none",
59
- "hover:overlay-medium active:overlay-medium",
60
- "focus-visible:outline-transparent focus-visible:shadow-focus",
61
- "[&_svg]:size-5.5"
62
- ],
63
- infoDock: ["pointer-events-none absolute right-6 bottom-20 left-6 z-10 flex justify-center"],
64
- infoBar: [
65
- "pointer-events-auto flex items-center",
66
- "w-[clamp(144px,20vw,224px)] max-w-[calc(100vw-48px)] min-w-36",
67
- "gap-2 rounded-container py-1 pr-1 pl-2.5",
68
- "bg-layer-popover text-foreground shadow-float ring-1 ring-line-seam"
69
- ],
70
- infoTitle: ["min-w-0 flex-1 truncate", "font-sans text-footnote font-semibold text-foreground"],
71
- infoActions: [
72
- "flex shrink-0 items-center",
73
- "gap-1",
74
- "*:data-[slot=button]:h-7 *:data-[slot=button]:w-auto *:data-[slot=button]:min-w-7",
75
- "*:data-[slot=button]:rounded-chip",
76
- "*:data-[slot=button]:after:inset-x-[-3px] *:data-[slot=button]:first:after:left-[-9.5px] *:data-[slot=button]:last:after:right-[-9.5px]",
77
- "pointer-coarse:*:data-[slot=button]:after:inset-y-[-9.5px]"
78
- ],
79
- thumbnailStrip: ["pointer-events-none absolute right-0 bottom-4 left-0 z-10", "flex items-center justify-center gap-1.5"],
80
- thumbnail: [
81
- "group/lightbox-thumb pointer-events-auto relative size-12 overflow-hidden",
82
- "transition-[border-color,opacity,box-shadow] duration-150",
83
- "[&_img]:pointer-events-none [&_img]:size-full [&_img]:object-cover",
84
- "rounded-control border-[1.5px] opacity-100 shadow-none",
85
- "border-sticker-paper/40 hover:border-sticker-paper/80",
86
- "data-active:border-sticker-paper"
87
- ],
88
- thumbnailOverlay: [
89
- "pointer-events-none absolute inset-0 block transition-colors duration-150",
90
- "bg-layer-backdrop-nested group-hover/lightbox-thumb:bg-layer-backdrop-nested/70",
91
- "group-data-active/lightbox-thumb:bg-transparent"
92
- ],
93
- close: ["pointer-events-auto"]
94
- } });
16
+ const lightbox = tv({
17
+ slots: {
18
+ portal: ["relative isolate z-lightbox"],
19
+ backdrop: ["fixed inset-0 bg-layer-backdrop data-closed:hidden"],
20
+ content: [
21
+ "pointer-events-none fixed inset-0 flex flex-col data-closed:hidden",
22
+ "*:data-lightbox-close:pointer-events-auto *:data-lightbox-close:absolute *:data-lightbox-close:z-raised",
23
+ "*:data-lightbox-close:top-[calc(16px+env(safe-area-inset-top,0px))] *:data-lightbox-close:right-[calc(16px+env(safe-area-inset-right,0px))]"
24
+ ],
25
+ header: [
26
+ "pointer-events-auto shrink-0",
27
+ "flex items-center gap-3 pb-3",
28
+ "pt-[calc(env(safe-area-inset-top,0px)+--spacing(3))]",
29
+ "pl-[calc(env(safe-area-inset-left,0px)+--spacing(4))]",
30
+ "pr-[calc(env(safe-area-inset-right,0px)+--spacing(4))]",
31
+ "bg-transparent shadow-none",
32
+ headEdgePull
33
+ ],
34
+ toolbar: [
35
+ "pointer-events-auto shrink-0 flex items-center gap-3",
36
+ "h-[calc(var(--header-height)+env(safe-area-inset-top,0px))] pt-[env(safe-area-inset-top,0px)]",
37
+ "pl-[calc(16px+env(safe-area-inset-left,0px))] pr-[calc(16px+env(safe-area-inset-right,0px))]",
38
+ "**:data-lightbox-close:static **:data-lightbox-close:inset-auto",
39
+ "**:data-lightbox-close:top-auto **:data-lightbox-close:right-auto",
40
+ "border-b border-line-seam bg-layer-canvas shadow-none",
41
+ headEdgePull
42
+ ],
43
+ headerMeta: ["flex min-w-0 flex-1 items-center gap-1.5"],
44
+ headerLabel: [
45
+ "inline-flex shrink-0 items-center px-1.5",
46
+ "h-[22px] rounded-chip bg-fill-muted py-0",
47
+ "font-sans text-caption font-semibold normal-case tracking-normal text-foreground-muted"
48
+ ],
49
+ title: ["min-w-0 flex-1 truncate", "py-0 font-sans text-headline font-bold text-foreground"],
50
+ actions: [
51
+ "flex shrink-0 items-center",
52
+ "gap-1",
53
+ "[&>[data-icon-inset]:nth-last-child(1_of_:not([data-base-ui-focus-guard],[aria-owns]))]:-mr-icon-inset"
54
+ ],
55
+ body: [
56
+ "pointer-events-auto min-h-0 flex-1 overscroll-contain",
57
+ "bg-layer-canvas",
58
+ "focus-visible:shadow-none focus-visible:keyboard:outline-2 focus-visible:keyboard:-outline-offset-2 focus-visible:keyboard:outline-foreground"
59
+ ],
60
+ bodyContent: ["pr-[env(safe-area-inset-right,0px)] pb-[env(safe-area-inset-bottom,0px)] pl-[env(safe-area-inset-left,0px)]"],
61
+ stage: ["pointer-events-none relative flex min-h-0 flex-1 items-center justify-center overflow-hidden"],
62
+ media: ["pointer-events-auto max-h-[80vh] max-w-[86vw] border-0 bg-transparent shadow-none"],
63
+ navigationButton: [
64
+ "pointer-events-auto absolute z-raised inline-flex items-center justify-center",
65
+ "transition-[background-color,color,opacity,box-shadow,transform] duration-150",
66
+ "outline-[1px] outline-offset-2 outline-transparent",
67
+ "data-[disabled=true]:cursor-default",
68
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0",
69
+ "size-11 rounded-control border-[1.5px] border-line-strong bg-layer-card text-foreground shadow-none",
70
+ "hover:overlay-medium active:overlay-medium",
71
+ "focus-visible:outline-transparent focus-visible:shadow-focus",
72
+ "[&_svg]:size-5.5"
73
+ ],
74
+ infoDock: ["pointer-events-none absolute right-6 bottom-20 left-6 z-raised flex justify-center"],
75
+ infoBar: [
76
+ "pointer-events-auto flex items-center",
77
+ "w-[clamp(144px,20vw,224px)] max-w-[calc(100vw-48px)] min-w-36",
78
+ "gap-2 rounded-container py-1 pr-1 pl-2.5",
79
+ "bg-layer-popover text-foreground shadow-float ring-1 ring-line-seam"
80
+ ],
81
+ infoTitle: ["min-w-0 flex-1 truncate", "font-sans text-footnote font-semibold text-foreground"],
82
+ infoActions: [
83
+ "flex shrink-0 items-center",
84
+ "gap-1",
85
+ "*:data-[slot=button]:h-7 *:data-[slot=button]:w-auto *:data-[slot=button]:min-w-7",
86
+ "*:data-[slot=button]:rounded-chip",
87
+ "*:data-[slot=button]:after:inset-x-[-3px] *:data-[slot=button]:first:after:left-[-9.5px] *:data-[slot=button]:last:after:right-[-9.5px]",
88
+ "pointer-coarse:*:data-[slot=button]:after:inset-y-[-9.5px]"
89
+ ],
90
+ thumbnailStrip: ["pointer-events-none absolute right-0 bottom-4 left-0 z-raised", "flex items-center justify-center gap-1.5"],
91
+ thumbnail: [
92
+ "group/lightbox-thumb pointer-events-auto relative size-12 overflow-hidden",
93
+ "transition-[border-color,opacity,box-shadow] duration-150",
94
+ "[&_img]:pointer-events-none [&_img]:size-full [&_img]:object-cover",
95
+ "rounded-control border-[1.5px] opacity-100 shadow-none",
96
+ "border-sticker-paper/40 hover:border-sticker-paper/80",
97
+ "data-active:border-sticker-paper"
98
+ ],
99
+ thumbnailOverlay: [
100
+ "pointer-events-none absolute inset-0 block transition-colors duration-150",
101
+ "bg-layer-backdrop-nested group-hover/lightbox-thumb:bg-layer-backdrop-nested/70",
102
+ "group-data-active/lightbox-thumb:bg-transparent"
103
+ ],
104
+ close: ["pointer-events-auto"]
105
+ },
106
+ variants: { nested: { true: { backdrop: ["bg-layer-backdrop-nested"] } } }
107
+ });
108
+ const LightboxModalContext = createContext(true);
109
+ /**
110
+ * A full-screen viewer over the scrim for images and attachment previews,
111
+ * with previous and next, thumbnails and one button to close. Pass
112
+ * `modal="trap-focus"` for a file read top to bottom (document mode).
113
+ */
95
114
  function Lightbox({ closeOnModW = true, defaultOpen = false, modal = true, onOpenChange, open, children, ...props }) {
96
115
  const dialogRef = useRef(null);
97
116
  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
@@ -108,7 +127,14 @@ function Lightbox({ closeOnModW = true, defaultOpen = false, modal = true, onOpe
108
127
  onOpenChange: handleOpenChange,
109
128
  open,
110
129
  ...props,
111
- children: [modWClose, children]
130
+ children: [modWClose, /* @__PURE__ */ jsx(OverlayLayerProvider, {
131
+ kind: "dialog",
132
+ modal: true,
133
+ children: /* @__PURE__ */ jsx(LightboxModalContext.Provider, {
134
+ value: modal,
135
+ children
136
+ })
137
+ })]
112
138
  });
113
139
  }
114
140
  function LightboxModWClose({ dialogRef }) {
@@ -128,22 +154,158 @@ function LightboxTrigger(props) {
128
154
  ...props
129
155
  });
130
156
  }
131
- function LightboxPortal(props) {
157
+ /**
158
+ * The lightbox's own stacking context, at `z-lightbox` (70) above the page's dialogs
159
+ * and popups: the scrim, the content, and every Base UI popup opened from
160
+ * inside, which Base UI portals into this element by default, in DOM order.
161
+ * A popup's own `z-popup` then counts inside the lightbox, so it is drawn above
162
+ * the content instead of under the scrim, and a press on it is a press inside
163
+ * the lightbox.
164
+ */
165
+ function LightboxPortal({ className, ...props }) {
166
+ const { portal } = lightbox();
132
167
  return /* @__PURE__ */ jsx(Dialog.Portal, {
133
168
  "data-slot": "lightbox-portal",
134
- ...useThemeScopeProps(props)
169
+ ...useThemeScopeProps({
170
+ ...props,
171
+ className: portal({ className })
172
+ })
135
173
  });
136
174
  }
137
- function LightboxContent({ className, initialFocus = false, finalFocus = false, ...props }) {
138
- const { backdrop, content } = lightbox();
175
+ const BODY_SELECTOR = "[data-slot=\"lightbox-body\"]";
176
+ /** The browser's own steps: an arrow key's line, and the share of the view a page keeps. */
177
+ const LINE_STEP = 40;
178
+ const PAGE_FRACTION = .875;
179
+ /**
180
+ * Where a key the browser scrolls with takes the body, or `null` for a key
181
+ * that doesn't scroll (or is the browser's own shortcut, such as Ctrl+Page Down).
182
+ */
183
+ function scrollKeyTarget(event, body) {
184
+ const { altKey, ctrlKey, key, metaKey, shiftKey } = event;
185
+ if (altKey) return null;
186
+ const page = body.clientHeight * PAGE_FRACTION;
187
+ const { scrollLeft: left, scrollTop: top } = body;
188
+ const start = {
189
+ left,
190
+ top: 0
191
+ };
192
+ const end = {
193
+ left,
194
+ top: body.scrollHeight
195
+ };
196
+ if (ctrlKey || metaKey) {
197
+ if (key === "ArrowUp" || key === "Home") return start;
198
+ if (key === "ArrowDown" || key === "End") return end;
199
+ return null;
200
+ }
201
+ switch (key) {
202
+ case "ArrowUp": return {
203
+ left,
204
+ top: top - LINE_STEP
205
+ };
206
+ case "ArrowDown": return {
207
+ left,
208
+ top: top + LINE_STEP
209
+ };
210
+ case "ArrowLeft": return {
211
+ left: left - LINE_STEP,
212
+ top
213
+ };
214
+ case "ArrowRight": return {
215
+ left: left + LINE_STEP,
216
+ top
217
+ };
218
+ case "PageUp": return {
219
+ left,
220
+ top: top - page
221
+ };
222
+ case "PageDown": return {
223
+ left,
224
+ top: top + page
225
+ };
226
+ case " ": return {
227
+ left,
228
+ top: shiftKey ? top - page : top + page
229
+ };
230
+ case "Home": return start;
231
+ case "End": return end;
232
+ default: return null;
233
+ }
234
+ }
235
+ /** Whether `element` takes the key itself: a text field edits with it, a button presses with Space. */
236
+ function takesKey(element, key) {
237
+ if (element instanceof HTMLElement && element.isContentEditable) return true;
238
+ if (element.closest("input, textarea, select")) return true;
239
+ return key === " " && element.closest("button, summary, [role=\"button\"]") !== null;
240
+ }
241
+ /** Whether `element`, or a parent of it inside `popup`, scrolls: the browser scrolls that one. */
242
+ function inScroller(element, popup) {
243
+ for (let node = element; node && node !== popup; node = node.parentElement) {
244
+ const { overflowX, overflowY } = getComputedStyle(node);
245
+ if (/auto|scroll/.test(overflowY) && node.scrollHeight > node.clientHeight) return true;
246
+ if (/auto|scroll/.test(overflowX) && node.scrollWidth > node.clientWidth) return true;
247
+ }
248
+ return false;
249
+ }
250
+ /**
251
+ * Document mode doesn't lock the page, so a key that would scroll it from
252
+ * outside the body (focus on a toolbar button, or on the content after a tap)
253
+ * scrolls `LightboxBody` instead, and never the page under the scrim. A key
254
+ * from inside a scroller is that scroller's; a key a control uses, or one a
255
+ * handler already took (a menu's arrows), is left alone. A popup opened
256
+ * inside is portaled beside the content, so its keys never come here.
257
+ */
258
+ function routeScrollKey(event) {
259
+ const popup = event.currentTarget;
260
+ const target = event.target;
261
+ if (event.defaultPrevented || !popup.contains(target)) return;
262
+ if (takesKey(target, event.key) || inScroller(target, popup)) return;
263
+ const body = popup.querySelector(BODY_SELECTOR);
264
+ const to = scrollKeyTarget(event, body ?? popup);
265
+ if (to === null) return;
266
+ event.preventDefault();
267
+ body?.scrollTo(to);
268
+ }
269
+ /**
270
+ * The full-screen layer, with the scrim behind it. Popups opened inside it
271
+ * are drawn above it (`LightboxPortal`). `initialFocus` and `finalFocus` are
272
+ * `false` by default, so focus doesn't move when it opens or closes, except
273
+ * in document mode (`modal="trap-focus"`): focus moves into the lightbox when
274
+ * it opens and back to `LightboxTrigger` when it closes. It lands on
275
+ * `LightboxBody`, so the keys that scroll scroll the document at once; on the
276
+ * first control when there is no body; and on the lightbox itself after a
277
+ * tap, so a phone shows no ring and opens no keyboard. A key that would
278
+ * scroll the page under the scrim scrolls the body instead.
279
+ *
280
+ * A lightbox opened from state passes `finalFocus={openerRef}` so focus goes
281
+ * back to what opened it.
282
+ */
283
+ function LightboxContent({ className, initialFocus, finalFocus, onKeyDown, ref, ...props }) {
284
+ const documentMode = useContext(LightboxModalContext) === "trap-focus";
285
+ const nested = useParentOverlayKind() != null;
286
+ const { backdrop, content } = lightbox({ nested });
287
+ const [setRef, getRef] = useMergedRef(ref);
288
+ const documentInitialFocus = useCallback((openType) => {
289
+ const popup = getRef();
290
+ if (openType === "touch") return popup;
291
+ return popup?.querySelector(BODY_SELECTOR) ?? true;
292
+ }, [getRef]);
293
+ const handleKeyDown = useCallback((event) => {
294
+ onKeyDown?.(event);
295
+ if (documentMode) routeScrollKey(event);
296
+ }, [documentMode, onKeyDown]);
139
297
  return /* @__PURE__ */ jsxs(LightboxPortal, { children: [/* @__PURE__ */ jsx(Dialog.Backdrop, {
140
298
  "data-slot": "lightbox-backdrop",
299
+ "data-nested": nested ? "" : void 0,
300
+ forceRender: true,
141
301
  className: backdrop()
142
302
  }), /* @__PURE__ */ jsx(Dialog.Popup, {
143
303
  "data-slot": "lightbox-content",
144
- initialFocus,
145
- finalFocus,
304
+ ref: setRef,
305
+ initialFocus: initialFocus ?? (documentMode ? documentInitialFocus : false),
306
+ finalFocus: finalFocus ?? documentMode,
146
307
  className: content({ className }),
308
+ onKeyDown: handleKeyDown,
147
309
  ...props
148
310
  })] });
149
311
  }
@@ -207,6 +369,38 @@ function LightboxActions({ render, className, ...props }) {
207
369
  state: { slot: "lightbox-actions" }
208
370
  });
209
371
  }
372
+ /**
373
+ * The scrolling body of a document-mode lightbox (`modal="trap-focus"`),
374
+ * under `LightboxToolbar`: a ScrollArea that fills the rest of the content
375
+ * (`min-h-0 flex-1`) on `layer-canvas`, the toolbar's surface. Pass a
376
+ * `className` for another surface (`bg-layer-sidebar`). It takes the pointer
377
+ * back from `LightboxContent`, which lets it through to the scrim, and never
378
+ * passes a scroll on to the page under the scrim (`overscroll-contain`, also
379
+ * while it has nothing to scroll; `overscroll-none` in `className` also stops
380
+ * the bounce). Every other ScrollArea prop, `render` included, is the area's.
381
+ *
382
+ * It is a tab stop even when it has nothing to scroll (`tabIndex={0}`), as it
383
+ * takes focus when the lightbox opens, and draws its focus ring inside its
384
+ * edge, which is the window's. Its content keeps clear of a phone's safe
385
+ * areas at the bottom and the sides, around the caller's own content box
386
+ * (`contentClassName`), so the scrollbar stays at the edge.
387
+ */
388
+ function LightboxBody({ children, className, contentClassName, contentStyle, ...props }) {
389
+ const { body, bodyContent } = lightbox();
390
+ return /* @__PURE__ */ jsx(ScrollArea, {
391
+ "data-slot": "lightbox-body",
392
+ tabIndex: 0,
393
+ ...props,
394
+ className: body({ className }),
395
+ contentClassName: bodyContent(),
396
+ children: contentClassName !== void 0 || contentStyle !== void 0 ? /* @__PURE__ */ jsx("div", {
397
+ "data-slot": "lightbox-body-content",
398
+ className: contentClassName,
399
+ style: contentStyle,
400
+ children
401
+ }) : children
402
+ });
403
+ }
210
404
  function LightboxStage({ render, className, ...props }) {
211
405
  const { stage } = lightbox();
212
406
  const defaultProps = { className: stage({ className }) };
@@ -333,4 +527,4 @@ function LightboxClose({ className, render, ...props }) {
333
527
  });
334
528
  }
335
529
  //#endregion
336
- export { Lightbox, LightboxActions, LightboxClose, LightboxContent, LightboxHeader, LightboxHeaderLabel, LightboxHeaderMeta, LightboxInfoActions, LightboxInfoBar, LightboxInfoDock, LightboxInfoTitle, LightboxMedia, LightboxNavigationButton, LightboxPortal, LightboxStage, LightboxThumbnail, LightboxThumbnailStrip, LightboxTitle, LightboxToolbar, LightboxTrigger };
530
+ export { Lightbox, LightboxActions, LightboxBody, LightboxClose, LightboxContent, LightboxHeader, LightboxHeaderLabel, LightboxHeaderMeta, LightboxInfoActions, LightboxInfoBar, LightboxInfoDock, LightboxInfoTitle, LightboxMedia, LightboxNavigationButton, LightboxPortal, LightboxStage, LightboxThumbnail, LightboxThumbnailStrip, LightboxTitle, LightboxToolbar, LightboxTrigger };
@@ -24,6 +24,19 @@ export type ListRowProps = useRender.ComponentProps<"button"> & Pick<VariantProp
24
24
  * `flex flex-col gap-0.5 py-2` column. A settings card keeps the default
25
25
  * look, with its lines. */
26
26
  cell?: boolean;
27
+ /** The row belongs to the setting in the row above it, in the same
28
+ * `ListRowGroup`: quiet hours' start and end times under Quiet hours, a
29
+ * server's channel overrides under its notification level. It draws no
30
+ * hairline above it (a hairline says "another setting"), and a `plain`
31
+ * row has no top padding, so it follows the row it belongs to. The row
32
+ * above keeps its own height and its switch, centred on its own title
33
+ * and description, doesn't move. The row after it draws its hairline as
34
+ * usual. An interactive one keeps the control height as its least, so
35
+ * it stays a 44px target on a phone and a touch screen. Carries
36
+ * `data-attached`. Tie it to its setting for assistive technology: a
37
+ * display-only one takes `role="group"` and `aria-labelledby` the
38
+ * setting's title id; an interactive one, `aria-describedby` that id. */
39
+ attached?: boolean;
27
40
  };
28
41
  /**
29
42
  * One row of a list. It renders a `button` by default; pass `render={<a />}`
@@ -31,7 +44,7 @@ export type ListRowProps = useRender.ComponentProps<"button"> & Pick<VariantProp
31
44
  * block (an avatar, a channel or icon tile, `ListRowIcon`), `ListRowBody`, and
32
45
  * anything trailing (a value, a badge, a switch, `ListRowChevron`).
33
46
  */
34
- export declare function ListRow({ variant, tone, selected, interactive, cell, className, render, ...props }: ListRowProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
47
+ export declare function ListRow({ variant, tone, selected, interactive, cell, attached, className, render, ...props }: ListRowProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
35
48
  export type ListRowIconProps = useRender.ComponentProps<"span">;
36
49
  /** A 20px leading icon in ink-3: entry rows and settings. */
37
50
  export declare function ListRowIcon({ className, render, ...props }: ListRowIconProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -23,17 +23,19 @@ function ListRowGroup({ className, render, ...props }) {
23
23
  * block (an avatar, a channel or icon tile, `ListRowIcon`), `ListRowBody`, and
24
24
  * anything trailing (a value, a badge, a switch, `ListRowChevron`).
25
25
  */
26
- function ListRow({ variant = "default", tone = "default", selected = false, interactive = true, cell = false, className, render, ...props }) {
26
+ function ListRow({ variant = "default", tone = "default", selected = false, interactive = true, cell = false, attached = false, className, render, ...props }) {
27
27
  const { root } = listRow({
28
28
  variant,
29
29
  interactive,
30
- cell
30
+ cell,
31
+ attached
31
32
  });
32
33
  const defaultProps = {
33
34
  "data-slot": "list-row",
34
35
  "data-variant": variant,
35
36
  "data-tone": tone === "default" ? void 0 : tone,
36
37
  "data-selected": selected ? "" : void 0,
38
+ "data-attached": attached ? "" : void 0,
37
39
  className: root({ className }),
38
40
  ...interactive && !render ? { type: "button" } : {}
39
41
  };
@@ -15,6 +15,10 @@ export declare const listRow: import("tailwind-variants/lite").TVReturnType<{
15
15
  true: {};
16
16
  false: {};
17
17
  };
18
+ attached: {
19
+ true: {};
20
+ false: {};
21
+ };
18
22
  cell: {
19
23
  false: {
20
24
  root: string[];
@@ -57,6 +61,10 @@ export declare const listRow: import("tailwind-variants/lite").TVReturnType<{
57
61
  true: {};
58
62
  false: {};
59
63
  };
64
+ attached: {
65
+ true: {};
66
+ false: {};
67
+ };
60
68
  cell: {
61
69
  false: {
62
70
  root: string[];
@@ -99,6 +107,10 @@ export declare const listRow: import("tailwind-variants/lite").TVReturnType<{
99
107
  true: {};
100
108
  false: {};
101
109
  };
110
+ attached: {
111
+ true: {};
112
+ false: {};
113
+ };
102
114
  cell: {
103
115
  false: {
104
116
  root: string[];