@botiverse/raft-ui 0.82.0 → 0.83.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/CHANGELOG.md +42 -0
- package/dist/components/dialog/dialog-sheet-context.mjs +7 -2
- package/dist/components/dialog/dialog-sheet.d.mts +3 -2
- package/dist/components/dialog/dialog-sheet.mjs +29 -43
- package/dist/components/dialog/dialog.d.mts +12 -1
- package/dist/components/dialog/dialog.mjs +50 -10
- package/dist/components/fit-toolbar/fit-toolbar.d.mts +110 -0
- package/dist/components/fit-toolbar/fit-toolbar.mjs +469 -0
- package/dist/components/fit-toolbar/fit-toolbar.recipe.mjs +21 -0
- package/dist/components/fit-toolbar/index.d.mts +2 -0
- package/dist/components/list-row/list-row.recipe.d.mts +3 -3
- package/dist/components/list-row/list-row.recipe.mjs +7 -2
- package/dist/components/mobile-nav/mobile-nav-server.d.mts +21 -8
- package/dist/components/mobile-nav/mobile-nav-server.mjs +142 -13
- package/dist/components/segmented-control/segmented-control.d.mts +1 -1
- package/dist/components/segmented-control/segmented-control.mjs +11 -2
- package/dist/components/selection-toolbar/index.d.mts +2 -0
- package/dist/components/selection-toolbar/selection-toolbar.d.mts +100 -0
- package/dist/components/selection-toolbar/selection-toolbar.mjs +217 -0
- package/dist/components/selection-toolbar/selection-toolbar.recipe.mjs +42 -0
- package/dist/index.d.mts +3 -2
- package/dist/index.mjs +3 -2
- package/dist/lib/label-value-fit.mjs +33 -6
- package/dist/lib/use-mobile-viewport.mjs +5 -2
- package/dist/lib/use-soft-keyboard.mjs +72 -0
- package/dist/wip.d.mts +6 -3
- package/dist/wip.mjs +4 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
## [0.83.0](https://github.com/botiverse/raft-design/compare/v0.82.0...v0.83.0) (2026-11-10)
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **`placement="top"` on DialogContent**, on a desktop and a tablet (and on DialogSheetContent there): the dialog stays centered across, its top edge at `var(--dialog-top, 8vh)`, and it is never taller than the window less that top and 16px, so it never runs past the window's bottom. Its width still comes from `size`. The content carries `data-placement="top"`. To anchor the card to something, the app measures it and sets `--dialog-top` in the content's `style`; raft-ui measures nothing. Below 768px it does nothing.
|
|
6
|
+
- Why: search opens from the top bar's field, and its panel's top edge belongs on the field, which a centered dialog can't do (slock drew it on DialogSurface with its own positioning, #triage:#278).
|
|
7
|
+
- Affects: nothing until a dialog passes `placement="top"`: `"center"`, the default, renders the same DOM and classes as before.
|
|
8
|
+
- Docs: https://raft-design.botiverse.build/components/dialog/#at-the-top
|
|
9
|
+
- **On a phone every centered dialog moves up while the on-screen keyboard is open**, whatever its `placement`: its top edge goes where the phone's top bar ends (`--header-height` below the top of what is visible, under the status bar), it ends at least 16px above the keyboard, and it goes back to the middle when the keyboard closes. While lifted the content carries `data-soft-keyboard`. A phone sheet doesn't move; it keeps its own keyboard box.
|
|
10
|
+
- Why: a centered dialog covered by the keyboard is always wrong, its field or its buttons out of sight, so this is platform behavior and no app opts in (stdrc, 2026-10-21: centered unless the keyboard is open). slock did it with its own `narrow="top"`; that maps to the default now.
|
|
11
|
+
- Affects: every centered `DialogContent` below 768px, only while a text field in it has focus and the visible area is over 100px shorter than the window; at rest nothing changes. A DialogSheetContent's keyboard box now reads the keyboard through the same check, once a frame: a focused checkbox, radio or button no longer counts as typing. An app that lifted its dialogs itself (slock's `narrow="top"`) can drop that code.
|
|
12
|
+
- Docs: https://raft-design.botiverse.build/components/dialog/#the-keyboard
|
|
13
|
+
- **SelectionToolbar** (from `@botiverse/raft-ui/wip`): the floating toolbar for selected items (messages, tasks), in the BatchBar style. `SelectionToolbarDock` floats it 12px above the bottom (plus the safe area) and 20px from the sides; `SelectionToolbarCount` (with the number in `SelectionToolbarCountValue`, Space Mono), `SelectionToolbarHint`, `SelectionToolbarActions` with `SelectionToolbarAction` (`secondary`, `menu`, `moreItem`), `SelectionToolbarMore` with `SelectionToolbarMoreItems` in its menu, `SelectionToolbarItem` for another control such as a select-all checkbox, and `SelectionToolbarClose`. A floating panel's frame (`line-seam` ring, `shadow-float`) as tall as a head: 44px with `md` buttons 4px apart with a mouse, 56px with 44px bar buttons side by side showing their icons alone by touch. It is a Base UI Toolbar (one Tab stop, arrow keys inside) and never wraps: when its row doesn't fit the width it floats in, measured, the hint, the secondary actions, the labels (the first action's last) and then the other actions give way, the last first; the count and × stay.
|
|
14
|
+
- Why: the toolbar for selected items is one bar shared by message multi-select and task bulk actions, in Noel's BatchBar style: `md` ghost buttons with a 14px icon and a label, no primary (stdrc, 2026-11-10, #wg-task-v2); by touch it aligns with the head, so a phone fits the tasks' Status, Assignee and Nudge at 360px (Noel, 2026-11-10).
|
|
15
|
+
- Affects: nothing yet. The stable `MessageMultiSelectToolbar` and `MessageMultiSelectToolbarActions` keep their earlier bar until SelectionToolbar leaves wip; move to it from `@botiverse/raft-ui/wip` when you are ready.
|
|
16
|
+
- Docs: https://raft-design.botiverse.build/components/selection-toolbar/, https://raft-design.botiverse.build/components/#toolbar-for-selected-items
|
|
17
|
+
- **FitToolbar** (from `@botiverse/raft-ui/wip`): a Base UI Toolbar whose items give way in named steps as the region it sits in narrows: `label` (to an icon), `more` (into More), `collapse` (a group to one button) and `wrap` (a group to a line of its own, with `wrap`; groups with the same `wrapLine` share one, and a wrapped group gets back what its items gave before it wrapped). Items declare their steps and order with `useFitItem`, or are a `FitToolbarButton`; a `FitToolbarGroup overflow="scroll"` scrolls sideways instead, its `data-active` items kept in view; groups are `FitToolbarGroup` (`firstLine`, `wrapOrder`, `wrapLine`, `collapsed`, `collapseOrder`); More takes `useFitMore` and lists `FitToolbarMoreItems`. It measures its own region's width, never the window's, again when its content changes on its own, and writes each step as `data-fit-<step>`. A SegmentedControl inside it follows its `label` step; its own `iconsOnlyBelow` is off there.
|
|
18
|
+
- Why: one measured engine for every toolbar that has to give way (responsive.md "measure the content rather than guess a width"), used by SelectionToolbar now and meant for the list toolbars of #proj-raft-ui:#225, instead of a breakpoint guess or a hook per app. Its step names and API stay in wip until sindresorhus confirms them for those toolbars.
|
|
19
|
+
- Docs: https://raft-design.botiverse.build/components/fit-toolbar/
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **DialogSheetContent, DialogCommit and DialogSheetBack are stable**: import them, and their props types, from `@botiverse/raft-ui`. `@botiverse/raft-ui/wip` no longer exports them.
|
|
24
|
+
- Why: slock is moving its own dialogs, its phone sheets among them, onto raft-ui (#triage:#278), and a product builds on the stable entry.
|
|
25
|
+
- Affects: an import from `@botiverse/raft-ui/wip` fails; import from `@botiverse/raft-ui` instead. No app imported them from `/wip` (slock draws its sheets itself today), and the docs' examples have moved.
|
|
26
|
+
- Docs: https://raft-design.botiverse.build/components/dialog/#making-a-dialog-a-sheet
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **A label/value list puts its labels above when a chip or a long word of a wrapping value can't fit beside its label**: DescriptionList `direction="horizontal"` and MessageActionCard's properties now also stack when one chip of a value that wraps either way (a row of chips wider than the list) is wider than the value column but no wider than the list, and likewise when a long word in such a value (an ID, a commit SHA, a URL) is: beside the label the chip was cut and the word ran past the value column. A word set to `overflow-wrap: anywhere` breaks where it must, and doesn't decide. There is no new prop.
|
|
31
|
+
- Why: the one-line rule let a row of chips wider than the list stay beside its labels, where one long chip was cut off with no way to read it (mobile #4507, the Computer page's Detected Runtimes chip).
|
|
32
|
+
- Affects: lists whose value wraps either way and holds a chip or a long word wider than the value column now stack instead of staying beside their labels (slock's Computer page among them). Such a word used to run past the value column in DescriptionList and break mid-word in MessageActionCard properties; it now reads whole on its own line. Every other list lays out as before; the extra measurement runs only for values wider than the list.
|
|
33
|
+
- Docs: https://raft-design.botiverse.build/components/description-list/#labels-go-above-only-when-a-value-doesnt-fit
|
|
34
|
+
- **The server tab's popover keeps one whole server row and opens on the current server**: `MobileNavServerPopoverList` is never shorter than one two-line row. Where the footer actions and the pages would leave it less inside 60% of the screen, `MobileNavServerPopoverContent` grows past 60% as far as that row needs; where the screen is too short even for that, the whole popover scrolls, the footer and the pages with it, in a ScrollArea. On open the list scrolls, with no animation, to the row with `aria-current="true"`, and focus goes to that row (a touch still focuses the popover itself), so focus no longer scrolls the list back to the top.
|
|
35
|
+
- Why: with 5 pages in the grid, a 360dp phone squeezed the list until the current server was half hidden, and the list always opened at the top (Lagoon, mobile #4500; Jasper's review of raft-design #1117).
|
|
36
|
+
- Affects: every `MobileNavServerPopoverContent`. A popover that fit before is unchanged except that it opens scrolled to the current server; one whose list was squeezed below a row is now taller than 60%, or scrolls as a whole. Opening from a mouse or keyboard focuses the current server's row instead of the first row; pass `initialFocus` to choose otherwise. The popover's parts now sit in a ScrollArea (`data-slot="mobile-nav-server-popover-scroll"`) inside the popup, so a selector for a direct child of `mobile-nav-server-popover-content` no longer matches them. Mark the current server's row with `aria-current="true"` for the scroll and the focus to find it.
|
|
37
|
+
- Docs: https://raft-design.botiverse.build/components/mobile-nav/#the-server-tab
|
|
38
|
+
- **A settings row's switch centers on its text, not on the SaveStatus line under it**: in a `ListRow variant="plain"` whose `ListRowBody` ends with a `SaveStatus`, both ends (the switch, value, chevron, checkbox, radio or button, and a leading icon, avatar or tile) center on the title and description. The row moves the line's reserved space (4px and one footnote line) into its bottom padding, so its height is unchanged and nothing moves when "Saved" or a one-line error appears. There is no new prop.
|
|
39
|
+
- Why: the ends centered on the whole body, the empty reserved line included, so the switch sat visibly below the text (slock's Administration › Hide #all channel; Jasper and Kirby, 2026-11-09, #proj-raft-ui:#220).
|
|
40
|
+
- Affects: in such rows the ends move up by half the reserved line (11px). A row without a `SaveStatus` is unchanged. An error that wraps still grows the row, and the ends move down by half of each extra line. Beside a title alone on a phone, the 30px switch keeps 12px above it, so that row is 76px instead of 70px. A local wrapper that put an invisible line under the switch to compensate (slock's) now lifts it too far: remove it.
|
|
41
|
+
- Docs: https://raft-design.botiverse.build/components/list-row/#anatomy, https://raft-design.botiverse.build/components/save-status/#behavior
|
|
42
|
+
|
|
1
43
|
## [0.82.0](https://github.com/botiverse/raft-design/compare/v0.81.0...v0.82.0) (2026-11-09)
|
|
2
44
|
|
|
3
45
|
### Changed
|
|
@@ -12,6 +12,11 @@ const DialogSheetContext = createContext(NO_SHEET);
|
|
|
12
12
|
function useDialogSheet() {
|
|
13
13
|
return useContext(DialogSheetContext);
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* True around the DialogContent a `DialogSheetContent` draws as a phone
|
|
17
|
+
* sheet: the sheet has its own keyboard box, so the content doesn't lift.
|
|
18
|
+
*/
|
|
19
|
+
const DialogSheetSurfaceContext = createContext(false);
|
|
15
20
|
/** Inside the sheet's head: its × is a pushed page's leading action. */
|
|
16
21
|
const DialogSheetHeadContext = createContext(false);
|
|
17
22
|
function useInDialogSheetHead() {
|
|
@@ -23,7 +28,7 @@ function useInDialogSheetHead() {
|
|
|
23
28
|
* button instead of taking the sheet's.
|
|
24
29
|
*/
|
|
25
30
|
function DialogSheetReset({ children }) {
|
|
26
|
-
return createElement(DialogSheetContext.Provider, { value: NO_SHEET }, createElement(DialogSheetHeadContext.Provider, { value: false }, children));
|
|
31
|
+
return createElement(DialogSheetSurfaceContext.Provider, { value: false }, createElement(DialogSheetContext.Provider, { value: NO_SHEET }, createElement(DialogSheetHeadContext.Provider, { value: false }, children)));
|
|
27
32
|
}
|
|
28
33
|
/**
|
|
29
34
|
* A dialog drawn as a phone sheet (design: dialog.md "On a phone"). The
|
|
@@ -55,4 +60,4 @@ const dialogSheet = tv({ slots: {
|
|
|
55
60
|
bodyEnd: ["mt-4 empty:hidden", "text-start font-sans text-footnote text-foreground-muted *:data-[slot=text-link]:text-footnote"]
|
|
56
61
|
} });
|
|
57
62
|
//#endregion
|
|
58
|
-
export { DialogSheetContext, DialogSheetHeadContext, DialogSheetReset, NO_SHEET, dialogSheet, useDialogSheet, useInDialogSheetHead };
|
|
63
|
+
export { DialogSheetContext, DialogSheetHeadContext, DialogSheetReset, DialogSheetSurfaceContext, NO_SHEET, dialogSheet, useDialogSheet, useInDialogSheetHead };
|
|
@@ -10,9 +10,10 @@ export type DialogSheetContentProps = DialogContentProps;
|
|
|
10
10
|
* up over its scrim; `DialogHeader` becomes a pushed page's head (× at the
|
|
11
11
|
* start, the title, the `DialogCommit` button at the end), `DialogFooter` is
|
|
12
12
|
* hidden (× is Cancel) and `DialogBody` scrolls on the phone's 16px margin
|
|
13
|
-
* above the home indicator. Base UI keeps Esc, focus and close.
|
|
13
|
+
* above the home indicator. Base UI keeps Esc, focus and close. `placement`
|
|
14
|
+
* applies on a desktop and a tablet only: the sheet is docked to the bottom.
|
|
14
15
|
*/
|
|
15
|
-
export declare function DialogSheetContent({ className, overlay, style, ref, children, ...props }: DialogSheetContentProps): import("react").JSX.Element;
|
|
16
|
+
export declare function DialogSheetContent({ className, overlay, placement, style, ref, children, ...props }: DialogSheetContentProps): import("react").JSX.Element;
|
|
16
17
|
export type DialogCommitProps = ButtonProps;
|
|
17
18
|
/**
|
|
18
19
|
* The dialog's one commit button: a `Button`, written where it goes on a
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { cn } from "../../cn.mjs";
|
|
2
2
|
import { Button } from "../button/button.mjs";
|
|
3
|
-
import { DialogSheetContext, dialogSheet, useDialogSheet } from "./dialog-sheet-context.mjs";
|
|
4
|
-
import { DialogContent } from "./dialog.mjs";
|
|
3
|
+
import { DialogSheetContext, DialogSheetSurfaceContext, dialogSheet, useDialogSheet } from "./dialog-sheet-context.mjs";
|
|
5
4
|
import { useMobileViewport } from "../../lib/use-mobile-viewport.mjs";
|
|
5
|
+
import { useSoftKeyboard } from "../../lib/use-soft-keyboard.mjs";
|
|
6
|
+
import { DialogContent } from "./dialog.mjs";
|
|
6
7
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
7
8
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
import { createPortal } from "react-dom";
|
|
@@ -14,9 +15,10 @@ import { createPortal } from "react-dom";
|
|
|
14
15
|
* up over its scrim; `DialogHeader` becomes a pushed page's head (× at the
|
|
15
16
|
* start, the title, the `DialogCommit` button at the end), `DialogFooter` is
|
|
16
17
|
* hidden (× is Cancel) and `DialogBody` scrolls on the phone's 16px margin
|
|
17
|
-
* above the home indicator. Base UI keeps Esc, focus and close.
|
|
18
|
+
* above the home indicator. Base UI keeps Esc, focus and close. `placement`
|
|
19
|
+
* applies on a desktop and a tablet only: the sheet is docked to the bottom.
|
|
18
20
|
*/
|
|
19
|
-
function DialogSheetContent({ className, overlay, style, ref, children, ...props }) {
|
|
21
|
+
function DialogSheetContent({ className, overlay, placement, style, ref, children, ...props }) {
|
|
20
22
|
const phone = useMobileViewport();
|
|
21
23
|
const [commitSlot, setCommitSlot] = useState(null);
|
|
22
24
|
const [bodyEndSlot, setBodyEndSlot] = useState(null);
|
|
@@ -51,16 +53,20 @@ function DialogSheetContent({ className, overlay, style, ref, children, ...props
|
|
|
51
53
|
...style,
|
|
52
54
|
...keyboardBox
|
|
53
55
|
} : style, [style, keyboardBox]);
|
|
54
|
-
return /* @__PURE__ */ jsx(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
return /* @__PURE__ */ jsx(DialogSheetSurfaceContext.Provider, {
|
|
57
|
+
value: phone,
|
|
58
|
+
children: /* @__PURE__ */ jsx(DialogContent, {
|
|
59
|
+
...props,
|
|
60
|
+
ref: setRef,
|
|
61
|
+
placement: phone ? void 0 : placement,
|
|
62
|
+
overlay: phone ? sheetOverlay : overlay,
|
|
63
|
+
className: phone ? cn(className, sheet.content()) : className,
|
|
64
|
+
style: phone ? sheetStyle : style,
|
|
65
|
+
"data-sheet": phone ? "" : void 0,
|
|
66
|
+
children: /* @__PURE__ */ jsx(DialogSheetContext.Provider, {
|
|
67
|
+
value: state,
|
|
68
|
+
children
|
|
69
|
+
})
|
|
64
70
|
})
|
|
65
71
|
});
|
|
66
72
|
}
|
|
@@ -132,36 +138,16 @@ function DialogCommit({ size, form, onClick, ...props }) {
|
|
|
132
138
|
* keyboard. `undefined` otherwise (the CSS box).
|
|
133
139
|
*/
|
|
134
140
|
function useSheetKeyboardBox(active) {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const top = Math.round(Math.max(viewport.offsetTop, 0) + viewport.height - height);
|
|
144
|
-
setBox((previous) => {
|
|
145
|
-
if (!keyboardUp) return void 0;
|
|
146
|
-
return previous?.top === `${top}px` && previous?.height === `${height}px` ? previous : {
|
|
147
|
-
top: `${top}px`,
|
|
148
|
-
bottom: "auto",
|
|
149
|
-
height: `${height}px`
|
|
150
|
-
};
|
|
151
|
-
});
|
|
152
|
-
};
|
|
153
|
-
viewport.addEventListener("resize", update);
|
|
154
|
-
viewport.addEventListener("scroll", update, { passive: true });
|
|
155
|
-
document.addEventListener("focusin", update);
|
|
156
|
-
document.addEventListener("focusout", update);
|
|
157
|
-
return () => {
|
|
158
|
-
viewport.removeEventListener("resize", update);
|
|
159
|
-
viewport.removeEventListener("scroll", update);
|
|
160
|
-
document.removeEventListener("focusin", update);
|
|
161
|
-
document.removeEventListener("focusout", update);
|
|
141
|
+
const keyboard = useSoftKeyboard(active);
|
|
142
|
+
return useMemo(() => {
|
|
143
|
+
if (!keyboard) return void 0;
|
|
144
|
+
const height = Math.round(Math.min(keyboard.height, keyboard.windowHeight * .9));
|
|
145
|
+
return {
|
|
146
|
+
top: `${keyboard.top + keyboard.height - height}px`,
|
|
147
|
+
bottom: "auto",
|
|
148
|
+
height: `${height}px`
|
|
162
149
|
};
|
|
163
|
-
}, [
|
|
164
|
-
return active ? box : void 0;
|
|
150
|
+
}, [keyboard]);
|
|
165
151
|
}
|
|
166
152
|
/**
|
|
167
153
|
* The commit is at the top of a sheet, so an error a failed commit adds near
|
|
@@ -22,8 +22,19 @@ export type DialogContentProps = StyledProps<Dialog.Popup.Props> & {
|
|
|
22
22
|
overlay?: false | DialogOverlayProps;
|
|
23
23
|
size?: "sm" | "md" | "lg" | "wide";
|
|
24
24
|
layer?: 0 | 1;
|
|
25
|
+
/**
|
|
26
|
+
* Where the card sits in the window. `"center"` (the default): in the
|
|
27
|
+
* middle. `"top"`: centred across, its top edge at `var(--dialog-top, 8vh)`,
|
|
28
|
+
* and never taller than the window less that top and 16px, so it never runs
|
|
29
|
+
* past the window's bottom; its width is still `size`'s. To anchor the card
|
|
30
|
+
* to something on the page (search under the top bar's field), measure it
|
|
31
|
+
* and set `--dialog-top` in `style`. Desktop and tablet only: below 768px
|
|
32
|
+
* every centred dialog sits in the middle and moves up only while the
|
|
33
|
+
* on-screen keyboard is open (dialog.md, "On a phone").
|
|
34
|
+
*/
|
|
35
|
+
placement?: "center" | "top";
|
|
25
36
|
};
|
|
26
|
-
export declare function DialogContent({ className, children, portalProps, overlay, size, layer, ...props }: DialogContentProps): import("react").JSX.Element;
|
|
37
|
+
export declare function DialogContent({ className, children, portalProps, overlay, size, layer, placement, ...props }: DialogContentProps): import("react").JSX.Element;
|
|
27
38
|
export type DialogCloseProps = Dialog.Close.Props & Pick<ButtonProps, "variant" | "size"> & {
|
|
28
39
|
className?: string;
|
|
29
40
|
children?: ReactNode;
|
|
@@ -6,13 +6,15 @@ import { tv } from "../../lib/tv.mjs";
|
|
|
6
6
|
import { useThemeScopeProps } from "../../lib/theme/theme-scope.mjs";
|
|
7
7
|
import { useParentOverlayKind, withOverlayLayer } from "../../lib/overlay-depth.mjs";
|
|
8
8
|
import { Button } from "../button/button.mjs";
|
|
9
|
-
import { DialogSheetHeadContext, DialogSheetReset, dialogSheet, useDialogSheet, useInDialogSheetHead } from "./dialog-sheet-context.mjs";
|
|
9
|
+
import { DialogSheetHeadContext, DialogSheetReset, DialogSheetSurfaceContext, dialogSheet, useDialogSheet, useInDialogSheetHead } from "./dialog-sheet-context.mjs";
|
|
10
10
|
import { ScrollArea } from "../scroll-area/scroll-area.mjs";
|
|
11
|
+
import { useMobileViewport } from "../../lib/use-mobile-viewport.mjs";
|
|
12
|
+
import { useSoftKeyboard } from "../../lib/use-soft-keyboard.mjs";
|
|
11
13
|
import { TextLink } from "../text-link/text-link.mjs";
|
|
12
14
|
import { PanelAction } from "../panel/panel-action.mjs";
|
|
13
15
|
import { Dialog } from "@base-ui/react/dialog";
|
|
14
16
|
import { ArrowLeft, X } from "lucide-react";
|
|
15
|
-
import { Children, createContext, isValidElement, useContext, useEffect } from "react";
|
|
17
|
+
import { Children, createContext, isValidElement, useContext, useEffect, useState } from "react";
|
|
16
18
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
17
19
|
import { mergeProps } from "@base-ui/react/merge-props";
|
|
18
20
|
import { useRender } from "@base-ui/react/use-render";
|
|
@@ -59,12 +61,20 @@ const dialog = tv({
|
|
|
59
61
|
lg: { content: ["max-w-160"] },
|
|
60
62
|
wide: { content: ["max-w-4xl h-[min(760px,calc(100dvh-2rem))]"] }
|
|
61
63
|
},
|
|
64
|
+
placement: {
|
|
65
|
+
center: {},
|
|
66
|
+
top: { content: ["md:top-(--dialog-top,8vh) md:translate-y-0", "md:max-h-[calc(100dvh-var(--dialog-top,8vh)-16px)]"] }
|
|
67
|
+
},
|
|
68
|
+
keyboard: {
|
|
69
|
+
false: {},
|
|
70
|
+
true: { content: ["data-soft-keyboard:top-[calc(var(--dialog-visible-top)+env(safe-area-inset-top,0px)+var(--header-height))] data-soft-keyboard:translate-y-0", "data-soft-keyboard:max-h-[calc(var(--dialog-visible-height)-env(safe-area-inset-top,0px)-var(--header-height)-16px)]"] }
|
|
71
|
+
},
|
|
62
72
|
bar: {
|
|
63
73
|
false: {},
|
|
64
74
|
true: {
|
|
65
|
-
header: ["flex h-(--header-height) shrink-0 items-center gap-2 border-b px-(--surface-inset) py-0", "border-line-seam pt-0
|
|
66
|
-
body: ["flex min-h-0 flex-1 overflow-hidden p-0", "
|
|
67
|
-
footer: ["shrink-0 border-t px-(--surface-inset) py-3", "border-line-seam
|
|
75
|
+
header: ["flex h-(--header-height) shrink-0 items-center gap-2 border-b px-(--surface-inset) py-0", "border-line-seam pt-0 *:data-[slot=dialog-close]:my-0"],
|
|
76
|
+
body: ["flex min-h-0 flex-1 overflow-hidden p-0", "p-0 first:pt-0 text-foreground"],
|
|
77
|
+
footer: ["shrink-0 border-t px-(--surface-inset) py-3", "border-line-seam py-3 first:pt-3 [[data-slot=dialog-header]+&]:pt-3"]
|
|
68
78
|
}
|
|
69
79
|
},
|
|
70
80
|
layer: {
|
|
@@ -125,10 +135,16 @@ function DialogOverlay({ className, layer, ...props }) {
|
|
|
125
135
|
});
|
|
126
136
|
}
|
|
127
137
|
const DialogSizeContext = createContext(void 0);
|
|
128
|
-
function DialogContent({ className, children, portalProps, overlay, size, layer, ...props }) {
|
|
138
|
+
function DialogContent({ className, children, portalProps, overlay, size, layer, placement, ...props }) {
|
|
139
|
+
const top = placement === "top";
|
|
140
|
+
const sheetSurface = useContext(DialogSheetSurfaceContext);
|
|
141
|
+
const [keyboard, setKeyboard] = useState(null);
|
|
142
|
+
const raised = !sheetSurface && keyboard != null;
|
|
129
143
|
const { content } = dialog({
|
|
130
144
|
size,
|
|
131
|
-
layer
|
|
145
|
+
layer,
|
|
146
|
+
placement,
|
|
147
|
+
keyboard: raised || void 0
|
|
132
148
|
});
|
|
133
149
|
const overlayProps = typeof overlay === "object" ? overlay : {};
|
|
134
150
|
return /* @__PURE__ */ jsxs(DialogPortal, {
|
|
@@ -136,17 +152,41 @@ function DialogContent({ className, children, portalProps, overlay, size, layer,
|
|
|
136
152
|
children: [overlay === false ? null : /* @__PURE__ */ jsx(DialogOverlay, {
|
|
137
153
|
layer,
|
|
138
154
|
...overlayProps
|
|
139
|
-
}), /* @__PURE__ */
|
|
155
|
+
}), /* @__PURE__ */ jsxs(Dialog.Popup, {
|
|
140
156
|
"data-slot": "dialog-content",
|
|
157
|
+
"data-placement": top ? "top" : void 0,
|
|
158
|
+
"data-soft-keyboard": raised ? "" : void 0,
|
|
141
159
|
className: content({ className }),
|
|
142
160
|
...props,
|
|
143
|
-
|
|
161
|
+
...raised ? { style: withVisibleArea(props.style, keyboard) } : null,
|
|
162
|
+
children: [sheetSurface ? null : /* @__PURE__ */ jsx(SoftKeyboardWatch, { onChange: setKeyboard }), /* @__PURE__ */ jsx(DialogSizeContext.Provider, {
|
|
144
163
|
value: size ?? (/(^|\s)max-w-/.test(className ?? "") ? void 0 : "md"),
|
|
145
164
|
children: /* @__PURE__ */ jsx(DialogSheetReset, { children })
|
|
146
|
-
})
|
|
165
|
+
})]
|
|
147
166
|
})]
|
|
148
167
|
});
|
|
149
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Reports the on-screen keyboard on a phone to a centred dialog, which moves
|
|
171
|
+
* up while it is open (dialog.md, "On a phone"). Rendered inside the popup,
|
|
172
|
+
* so it listens only while the dialog is open, and only below 768px.
|
|
173
|
+
*/
|
|
174
|
+
function SoftKeyboardWatch({ onChange }) {
|
|
175
|
+
const keyboard = useSoftKeyboard(useMobileViewport());
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
onChange(keyboard);
|
|
178
|
+
}, [keyboard, onChange]);
|
|
179
|
+
useEffect(() => () => onChange(null), [onChange]);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
/** The popup's style with the area the keyboard leaves visible, for its CSS. */
|
|
183
|
+
function withVisibleArea(style, keyboard) {
|
|
184
|
+
return {
|
|
185
|
+
...style,
|
|
186
|
+
"--dialog-visible-top": `${keyboard.top}px`,
|
|
187
|
+
"--dialog-visible-height": `${keyboard.height}px`
|
|
188
|
+
};
|
|
189
|
+
}
|
|
150
190
|
function DialogClose({ className, variant, size, children, render, "aria-label": ariaLabel, ...props }) {
|
|
151
191
|
const inSheetHead = useInDialogSheetHead();
|
|
152
192
|
if (render) return /* @__PURE__ */ jsx(Dialog.Close, {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { StyledProps } from "../../lib/styled-props.mjs";
|
|
2
|
+
import { ButtonProps } from "../button/button.mjs";
|
|
3
|
+
import { ReactNode, Ref } from "react";
|
|
4
|
+
import { Toolbar } from "@base-ui/react/toolbar";
|
|
5
|
+
//#region src/components/fit-toolbar/fit-toolbar.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The ways an item gives way, each one step:
|
|
8
|
+
* - `label`: its label hides and its icon stays (the label stays its accessible name);
|
|
9
|
+
* - `more`: it moves into More;
|
|
10
|
+
* - `collapse`: a group shows its one button instead of its items (a Filter button for its filters);
|
|
11
|
+
* - `wrap`: a group moves to a line of its own, only in a toolbar that allows it.
|
|
12
|
+
*/
|
|
13
|
+
export type FitStep = "label" | "more" | "collapse" | "wrap";
|
|
14
|
+
/** When each of an item's steps comes: lower gives way first; at the same number, the last item first. */
|
|
15
|
+
export type FitSteps = Partial<Record<FitStep, number>>;
|
|
16
|
+
export type FitToolbarProps = StyledProps<Toolbar.Root.Props> & {
|
|
17
|
+
ref?: Ref<HTMLDivElement>;
|
|
18
|
+
/**
|
|
19
|
+
* Groups may move to a line of their own (the `wrap` step) when the row
|
|
20
|
+
* runs out of room. Off for a toolbar that floats over content: a second
|
|
21
|
+
* line would cover it, so its last step is into More.
|
|
22
|
+
*/
|
|
23
|
+
wrap?: boolean;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A toolbar whose items give way, in named steps and an order the items
|
|
27
|
+
* declare, as the region it sits in narrows: it measures its content in its
|
|
28
|
+
* own width, never the window's. The steps are written on each item as
|
|
29
|
+
* `data-fit-<step>` for its styles. A Base UI Toolbar: one Tab stop, the
|
|
30
|
+
* arrow keys inside.
|
|
31
|
+
*/
|
|
32
|
+
export declare function FitToolbar({ className, wrap, ref, ...props }: FitToolbarProps): import("react").JSX.Element;
|
|
33
|
+
/**
|
|
34
|
+
* Inside a FitToolbar: a control that would measure its own fit (a
|
|
35
|
+
* SegmentedControl's `iconsOnlyBelow`) leaves it to the toolbar's steps.
|
|
36
|
+
*/
|
|
37
|
+
export declare function useInFitToolbar(): boolean;
|
|
38
|
+
export type FitItemOptions = FitSteps & {
|
|
39
|
+
/** What the item is in More's menu while it is there (`FitToolbarMoreItems`). */
|
|
40
|
+
moreItem?: ReactNode;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Makes an element an item of the nearest FitToolbar: spread `props` on it.
|
|
44
|
+
* Says which steps it has taken, for what it renders (a tooltip once its
|
|
45
|
+
* label is gone, say).
|
|
46
|
+
*/
|
|
47
|
+
export declare function useFitItem({ moreItem, ...steps }?: FitItemOptions): {
|
|
48
|
+
id: string;
|
|
49
|
+
props: Record<string, string | undefined>;
|
|
50
|
+
/** Steps it has taken. */
|
|
51
|
+
taken: (step: FitStep) => boolean;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Makes an element the toolbar's More: spread `props` on it. It shows only
|
|
55
|
+
* while an item is in More (`data-fit-shown`); hide it otherwise.
|
|
56
|
+
*/
|
|
57
|
+
export declare function useFitMore(): {
|
|
58
|
+
shown: boolean;
|
|
59
|
+
props: Record<string, string | undefined>;
|
|
60
|
+
};
|
|
61
|
+
/** The items in More, in their order, each as its `moreItem`: put it in More's menu. */
|
|
62
|
+
export declare function FitToolbarMoreItems(): import("react").JSX.Element[] | null;
|
|
63
|
+
export type FitToolbarGroupProps = StyledProps<Toolbar.Group.Props> & {
|
|
64
|
+
/** When it moves to a line of its own (the `wrap` step), in a toolbar that allows it. */
|
|
65
|
+
wrapOrder?: number;
|
|
66
|
+
/** Stays on the first line: it never wraps. The page decides which groups do. */
|
|
67
|
+
firstLine?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Groups with the same `wrapLine` wrap onto the same line, in their order, a
|
|
70
|
+
* fixed gap apart; a group without one takes a line of its own.
|
|
71
|
+
*/
|
|
72
|
+
wrapLine?: string;
|
|
73
|
+
/** When it shows `collapsed` instead of its items (the `collapse` step). */
|
|
74
|
+
collapseOrder?: number;
|
|
75
|
+
/** The group as one button (a Filter button for its filters), shown once it collapses. */
|
|
76
|
+
collapsed?: ReactNode;
|
|
77
|
+
/**
|
|
78
|
+
* `scroll`: the group takes the rest of its line and scrolls sideways
|
|
79
|
+
* instead of giving way, its items whole, the side that hides more fading.
|
|
80
|
+
* Mark an applied or chosen item `data-active`: it is kept in view.
|
|
81
|
+
*/
|
|
82
|
+
overflow?: "scroll";
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* A group of items in one row, a fixed gap apart. A toolbar wraps only
|
|
86
|
+
* between groups, the last first; groups with the same `wrapLine` share a
|
|
87
|
+
* line, any other takes one of its own.
|
|
88
|
+
*/
|
|
89
|
+
export declare function FitToolbarGroup({ className, wrapOrder, firstLine, wrapLine, collapseOrder, collapsed, overflow, children, ref, ...props }: FitToolbarGroupProps & {
|
|
90
|
+
ref?: Ref<HTMLDivElement>;
|
|
91
|
+
}): import("react").JSX.Element;
|
|
92
|
+
export type FitToolbarButtonProps = Omit<ButtonProps, "children" | "size"> & {
|
|
93
|
+
/** Its icon, which stays when its label gives way. */
|
|
94
|
+
icon: ReactNode;
|
|
95
|
+
/** Its label: its accessible name and, once it has given way, its tooltip. */
|
|
96
|
+
children: ReactNode;
|
|
97
|
+
/** When its label gives way (the `label` step). */
|
|
98
|
+
labelOrder?: number;
|
|
99
|
+
/** When it moves into More (the `more` step). */
|
|
100
|
+
moreOrder?: number;
|
|
101
|
+
/** What it is in More's menu. */
|
|
102
|
+
moreItem?: ReactNode;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* A toolbar button that can give way: an `md` Button (ghost unless you say)
|
|
106
|
+
* with an icon and a label, an icon square with its label as a tooltip from
|
|
107
|
+
* `labelOrder` on, and hidden from `moreOrder` on.
|
|
108
|
+
*/
|
|
109
|
+
export declare function FitToolbarButton({ icon, children, labelOrder, moreOrder, moreItem, variant, className, ...props }: FitToolbarButtonProps): import("react").JSX.Element;
|
|
110
|
+
//#endregion
|