@columenlabs/lumen-ui 0.1.293 → 0.1.295

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,8 +1,11 @@
1
1
  import * as React from 'react';
2
- /** A snap point: a share of the screen's height (0 to 1) or a height in px ("76px"). */
2
+ /**
3
+ * A snap point: a share of the screen's height (0 to 1), or a height in px ("64px") measured above the
4
+ * phone's home bar (the sheet adds the bar's safe area to it).
5
+ */
3
6
  export type SheetSnap = number | string;
4
7
  export interface LumenBottomSheetProps {
5
- /** The heights it rests at, smallest first: `['76px', 0.5, 0.92]` is peek, half and full. */
8
+ /** The heights it rests at, smallest first: `['64px', 0.5, 0.92]` is peek, half and full. */
6
9
  snaps: SheetSnap[];
7
10
  /** Where it rests now (one of `snaps`). */
8
11
  snap: SheetSnap;
@@ -8,6 +8,24 @@ function px(s, h) {
8
8
  return s * h;
9
9
  return Number.parseFloat(s);
10
10
  }
11
+ /** The phone's bottom safe area (its home bar), px: 0 where there is none. */
12
+ function useSafeBottom() {
13
+ const [inset, setInset] = React.useState(0);
14
+ React.useEffect(() => {
15
+ const probe = document.createElement('div');
16
+ probe.style.cssText =
17
+ 'position:fixed;visibility:hidden;padding-bottom:env(safe-area-inset-bottom)';
18
+ document.body.appendChild(probe);
19
+ const read = () => setInset(Number.parseFloat(getComputedStyle(probe).paddingBottom) || 0);
20
+ read();
21
+ window.addEventListener('resize', read);
22
+ return () => {
23
+ window.removeEventListener('resize', read);
24
+ probe.remove();
25
+ };
26
+ }, []);
27
+ return inset;
28
+ }
11
29
  /** The screen's height, kept up to date. */
12
30
  function useScreenHeight() {
13
31
  const [h, setH] = React.useState(() => typeof window === 'undefined' ? 800 : window.innerHeight);
@@ -25,10 +43,20 @@ function useScreenHeight() {
25
43
  */
26
44
  export function LumenBottomSheet({ snaps, snap, onSnap, title, children, className, }) {
27
45
  const h = useScreenHeight();
46
+ const inset = useSafeBottom();
47
+ // A px snap rests above the home bar: vaul gets it with the bar's height added.
48
+ const rest = (s) => (typeof s === 'string' ? `${px(s, h) + inset}px` : s);
49
+ const rests = snaps.map(rest);
50
+ const at = snaps.indexOf(snap);
28
51
  // The sheet is as tall as the screen and slides down to its snap; what it holds fills only the
29
52
  // part on screen, so a composer or footer at its bottom stays in view at every snap.
30
- const shown = Math.min(h, Math.max(0, px(snap, h)));
31
- return (_jsx(Drawer.Root, { open: true, modal: false, dismissible: false, handleOnly: true, snapPoints: snaps, activeSnapPoint: snap, setActiveSnapPoint: (s) => s != null && onSnap(s), children: _jsx(Drawer.Portal, { children: _jsxs(Drawer.Content, { "data-slot": "lumen-bottom-sheet", "aria-describedby": undefined,
53
+ const shown = Math.min(h, Math.max(0, px(rest(snap), h)));
54
+ return (_jsx(Drawer.Root, { open: true, modal: false, dismissible: false, handleOnly: true, snapPoints: rests, activeSnapPoint: rests[at] ?? rest(snap), setActiveSnapPoint: (s) => {
55
+ const i = s == null ? -1 : rests.indexOf(s);
56
+ const next = snaps[i];
57
+ if (next !== undefined)
58
+ onSnap(next);
59
+ }, children: _jsx(Drawer.Portal, { children: _jsxs(Drawer.Content, { "data-slot": "lumen-bottom-sheet", "aria-describedby": undefined,
32
60
  // Focus stays where the person was: the sheet is always there, opening it is not a move.
33
- onOpenAutoFocus: (e) => e.preventDefault(), className: cn('fixed inset-x-0 bottom-0 z-40 flex h-dvh flex-col rounded-t-2xl border-t bg-popover text-popover-foreground shadow-[0_-4px_16px_rgb(0_0_0/0.15)] outline-none', className), children: [_jsx(Drawer.Title, { className: "sr-only", children: title }), _jsxs("div", { className: "flex min-h-0 flex-col", style: { height: shown }, children: [_jsx(Drawer.Handle, { "aria-label": "Drag to resize", className: "mx-auto! my-2! h-1! w-9! shrink-0 rounded-full! bg-border! opacity-100!" }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col pb-[env(safe-area-inset-bottom)]", children: children })] })] }) }) }));
61
+ onOpenAutoFocus: (e) => e.preventDefault(), className: cn('fixed inset-x-0 bottom-0 z-40 flex h-dvh flex-col rounded-t-2xl border-t bg-popover text-popover-foreground shadow-[0_-4px_16px_rgb(0_0_0/0.15)] outline-none', className), children: [_jsx(Drawer.Title, { className: "sr-only", children: title }), _jsxs("div", { className: "flex min-h-0 flex-col overflow-hidden", style: { height: shown }, children: [_jsx(Drawer.Handle, { "aria-label": "Drag to resize", className: "mx-auto! my-2! h-1! w-9! shrink-0 rounded-full! bg-border! opacity-100!" }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col pb-[env(safe-area-inset-bottom)]", children: children })] })] }) }) }));
34
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-ui",
3
- "version": "0.1.293",
3
+ "version": "0.1.295",
4
4
  "description": "Lumen: shared React components (frame, views, widgets, charts, drawers).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,7 +49,7 @@
49
49
  "@codemirror/lang-javascript": "6.2.5",
50
50
  "@codemirror/lang-json": "6.0.2",
51
51
  "@codemirror/lang-sql": "6.10.0",
52
- "@columenlabs/lumen-ui-types": "0.1.293",
52
+ "@columenlabs/lumen-ui-types": "0.1.295",
53
53
  "@dnd-kit/core": "6.3.1",
54
54
  "@dnd-kit/sortable": "10.0.0",
55
55
  "@dnd-kit/utilities": "3.2.2",