@ramme-io/ui 4.1.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/component-registry.js +55 -52
- package/dist/components/ai/AIChatThread/AIChatThread.js +3 -3
- package/dist/components/ai/NewsSummaryCard/NewsSummaryCard.js +17 -17
- package/dist/components/ai/StockChart/StockChart.js +4 -4
- package/dist/components/auth/LoginPage.d.ts +22 -0
- package/dist/components/auth/LoginPage.js +50 -42
- package/dist/components/forms/ComboBox.js +2 -2
- package/dist/components/forms/DatePicker.js +37 -32
- package/dist/components/forms/FileUpload.js +39 -37
- package/dist/components/forms/Input.js +19 -18
- package/dist/components/forms/MultiSelect.js +9 -8
- package/dist/components/forms/Select.js +9 -8
- package/dist/components/forms/Textarea.d.ts +13 -11
- package/dist/components/forms/Textarea.js +36 -16
- package/dist/components/layout/Accordion.js +25 -32
- package/dist/components/layout/Card.js +38 -21
- package/dist/components/layout/DetailDrawer.js +36 -37
- package/dist/components/layout/SettingsContainer.js +23 -13
- package/dist/components/layout/Sheet.d.ts +58 -0
- package/dist/components/layout/Sheet.js +144 -0
- package/dist/components/navigation/MobileNav.d.ts +44 -1
- package/dist/components/navigation/MobileNav.js +96 -70
- package/dist/components/navigation/Tabs.js +19 -12
- package/dist/components/ui/icon-loader.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +218 -211
- package/dist/lib/hooks/useSheetGesture.d.ts +81 -0
- package/dist/lib/hooks/useSheetGesture.js +70 -0
- package/dist/setupTests.d.ts +1 -0
- package/dist/wizardManifest.js +130 -10
- package/package.json +9 -2
- package/src/wizardManifest.ts +130 -10
|
@@ -1,56 +1,55 @@
|
|
|
1
1
|
import { jsxs as b, jsx as r, Fragment as k } from "react/jsx-runtime";
|
|
2
|
-
import { useRef as E, useEffect as
|
|
3
|
-
import { createPortal as
|
|
4
|
-
import { Icon as
|
|
2
|
+
import { useRef as E, useEffect as y } from "react";
|
|
3
|
+
import { createPortal as L } from "react-dom";
|
|
4
|
+
import { Icon as D } from "../ui/Icon.js";
|
|
5
5
|
import { cn as h } from "../../utils/cn.js";
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { useBodyScrollLock as M } from "../../lib/hooks/useBodyScrollLock.js";
|
|
7
|
+
const B = ({
|
|
8
|
+
isOpen: t,
|
|
8
9
|
onClose: n,
|
|
9
|
-
children:
|
|
10
|
-
variant:
|
|
11
|
-
resizable:
|
|
12
|
-
width:
|
|
10
|
+
children: p,
|
|
11
|
+
variant: u = "overlay",
|
|
12
|
+
resizable: d = !1,
|
|
13
|
+
width: g = 420,
|
|
13
14
|
onWidthChange: a,
|
|
14
|
-
minWidth:
|
|
15
|
-
maxWidth:
|
|
16
|
-
side:
|
|
15
|
+
minWidth: c = 320,
|
|
16
|
+
maxWidth: m = typeof window < "u" ? window.innerWidth * 0.6 : 800,
|
|
17
|
+
side: o = "right"
|
|
17
18
|
}) => {
|
|
18
19
|
const i = E(!1);
|
|
19
|
-
if (
|
|
20
|
-
if (!
|
|
21
|
-
const e = (
|
|
20
|
+
if (y(() => {
|
|
21
|
+
if (!d || !a) return;
|
|
22
|
+
const e = (v) => {
|
|
22
23
|
if (!i.current) return;
|
|
23
|
-
const x =
|
|
24
|
-
a(Math.max(
|
|
25
|
-
},
|
|
24
|
+
const x = o === "right" ? window.innerWidth - v.clientX : v.clientX;
|
|
25
|
+
a(Math.max(c, Math.min(m, x)));
|
|
26
|
+
}, s = () => {
|
|
26
27
|
i.current && (i.current = !1, document.body.style.userSelect = "", document.body.style.cursor = "");
|
|
27
28
|
};
|
|
28
|
-
return window.addEventListener("mousemove", e), window.addEventListener("mouseup",
|
|
29
|
-
window.removeEventListener("mousemove", e), window.removeEventListener("mouseup",
|
|
29
|
+
return window.addEventListener("mousemove", e), window.addEventListener("mouseup", s), () => {
|
|
30
|
+
window.removeEventListener("mousemove", e), window.removeEventListener("mouseup", s);
|
|
30
31
|
};
|
|
31
|
-
}, [
|
|
32
|
-
const e = (
|
|
33
|
-
|
|
32
|
+
}, [d, a, c, m, o]), y(() => {
|
|
33
|
+
const e = (s) => {
|
|
34
|
+
s.key === "Escape" && t && n();
|
|
34
35
|
};
|
|
35
36
|
return window.addEventListener("keydown", e), () => window.removeEventListener("keydown", e);
|
|
36
|
-
}, [
|
|
37
|
-
|
|
38
|
-
}), [o, s]), !o) return null;
|
|
39
|
-
const l = s === "overlay", w = t === "right" ? "right-0 border-l" : "left-0 border-r", v = /* @__PURE__ */ b(
|
|
37
|
+
}, [t, n]), M(t && u === "overlay"), !t) return null;
|
|
38
|
+
const l = u === "overlay", f = o === "right" ? "right-0 border-l" : "left-0 border-r", w = /* @__PURE__ */ b(
|
|
40
39
|
"div",
|
|
41
40
|
{
|
|
42
41
|
className: h(
|
|
43
42
|
"flex flex-col border-border bg-card shadow-2xl transition-transform duration-300",
|
|
44
|
-
l ? `fixed inset-y-0 ${
|
|
43
|
+
l ? `fixed inset-y-0 ${f} z-drawer w-full sm:w-[420px] animate-in ${o === "right" ? "slide-in-from-right" : "slide-in-from-left"}` : `relative h-full flex-shrink-0 ${f}`
|
|
45
44
|
),
|
|
46
|
-
style: l ? void 0 : { width: `min(100vw, ${
|
|
45
|
+
style: l ? void 0 : { width: `min(100vw, ${g}px)` },
|
|
47
46
|
children: [
|
|
48
|
-
|
|
47
|
+
d && !l && /* @__PURE__ */ r(
|
|
49
48
|
"div",
|
|
50
49
|
{
|
|
51
50
|
className: h(
|
|
52
51
|
"absolute top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-primary/50 active:bg-primary z-10",
|
|
53
|
-
|
|
52
|
+
o === "right" ? "left-0 -ml-0.5" : "right-0 -mr-0.5"
|
|
54
53
|
),
|
|
55
54
|
onMouseDown: (e) => {
|
|
56
55
|
e.preventDefault(), i.current = !0, document.body.style.userSelect = "none", document.body.style.cursor = "col-resize";
|
|
@@ -63,14 +62,14 @@ const S = ({
|
|
|
63
62
|
onClick: n,
|
|
64
63
|
className: "rounded-full p-1.5 bg-background/50 backdrop-blur hover:bg-muted text-muted-foreground hover:text-foreground transition-all shadow-sm border border-border",
|
|
65
64
|
"aria-label": "Close",
|
|
66
|
-
children: /* @__PURE__ */ r(
|
|
65
|
+
children: /* @__PURE__ */ r(D, { name: "x", size: 16 })
|
|
67
66
|
}
|
|
68
67
|
) }),
|
|
69
|
-
/* @__PURE__ */ r("div", { className: "flex-1 overflow-y-auto custom-scrollbar relative h-full", children:
|
|
68
|
+
/* @__PURE__ */ r("div", { className: "flex-1 overflow-y-auto custom-scrollbar relative h-full", children: p })
|
|
70
69
|
]
|
|
71
70
|
}
|
|
72
71
|
);
|
|
73
|
-
return l ? typeof document > "u" ? null :
|
|
72
|
+
return l ? typeof document > "u" ? null : L(
|
|
74
73
|
/* @__PURE__ */ b(k, { children: [
|
|
75
74
|
/* @__PURE__ */ r(
|
|
76
75
|
"div",
|
|
@@ -79,11 +78,11 @@ const S = ({
|
|
|
79
78
|
onClick: n
|
|
80
79
|
}
|
|
81
80
|
),
|
|
82
|
-
|
|
81
|
+
w
|
|
83
82
|
] }),
|
|
84
83
|
document.body
|
|
85
|
-
) :
|
|
84
|
+
) : w;
|
|
86
85
|
};
|
|
87
86
|
export {
|
|
88
|
-
|
|
87
|
+
B as DetailDrawer
|
|
89
88
|
};
|
|
@@ -3,22 +3,32 @@ import { Tabs as b, TabPanel as l } from "../navigation/Tabs.js";
|
|
|
3
3
|
import { cn as c } from "../../utils/cn.js";
|
|
4
4
|
import { AboutRamme as s } from "./AboutRamme.js";
|
|
5
5
|
const p = ({
|
|
6
|
-
generalSlot:
|
|
7
|
-
aiGatewaySlot:
|
|
6
|
+
generalSlot: i,
|
|
7
|
+
aiGatewaySlot: r,
|
|
8
8
|
themeLabSlot: n,
|
|
9
|
-
aboutSlot:
|
|
10
|
-
defaultTab:
|
|
9
|
+
aboutSlot: d = /* @__PURE__ */ e(s, {}),
|
|
10
|
+
defaultTab: o = "general",
|
|
11
11
|
title: a = "Settings",
|
|
12
|
-
className:
|
|
13
|
-
}) =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
className: m
|
|
13
|
+
}) => (
|
|
14
|
+
/* @container so the padding answers to the space this panel was GIVEN,
|
|
15
|
+
not the size of the screen it happens to be on.
|
|
16
|
+
|
|
17
|
+
No horizontal padding when narrow. Nested in a Sheet or a drawer, the
|
|
18
|
+
host already supplies the gutter, and adding a second one is how the
|
|
19
|
+
Theme Studio lost 21% of a 390px screen to three stacked insets before a
|
|
20
|
+
single label was drawn. Vertical padding is kept: it separates, it does
|
|
21
|
+
not compete. */
|
|
22
|
+
/* @__PURE__ */ t("div", { className: c("@container w-full py-fluid-md px-0 @md:p-fluid-lg", m), children: [
|
|
23
|
+
a !== null && /* @__PURE__ */ e("h2", { className: "text-2xl font-bold tracking-tight text-foreground mb-4", children: a }),
|
|
24
|
+
/* @__PURE__ */ t(b, { defaultActiveTab: o, size: "sm", children: [
|
|
25
|
+
/* @__PURE__ */ e(l, { id: "general", label: "General", children: i }),
|
|
26
|
+
/* @__PURE__ */ e(l, { id: "ai-gateway", label: "AI Gateway", children: r }),
|
|
27
|
+
/* @__PURE__ */ e(l, { id: "theme-lab", label: "Theme Lab", children: n }),
|
|
28
|
+
/* @__PURE__ */ e(l, { id: "about", label: "About", children: d })
|
|
29
|
+
] })
|
|
20
30
|
] })
|
|
21
|
-
|
|
31
|
+
);
|
|
22
32
|
export {
|
|
23
33
|
p as SettingsContainer
|
|
24
34
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* @wizard
|
|
4
|
+
* @name Sheet
|
|
5
|
+
* @description A bottom sheet that opens at a partial height and grows to full screen as you drag or scroll it up. Strategic purpose: the native-feeling surface for mobile navigation, settings and pickers — reachable by thumb, dismissed by flicking down. Use instead of Drawer when the surface is primarily touched.
|
|
6
|
+
* @tags layout, overlay, sheet, mobile, touch, drawer, ui
|
|
7
|
+
* @category layout
|
|
8
|
+
* @id sheet
|
|
9
|
+
* @param {boolean} isOpen - Controls the open/closed state. Required.
|
|
10
|
+
* @param {function} onClose - Fired when the sheet requests dismissal (flick down, drag below the smallest detent, Escape, overlay tap, or the close button). Required.
|
|
11
|
+
* @param {number[]} detents - Resting heights as fractions of the viewport, ascending. Defaults to [0.6, 1] — opens at 60%, expands to full.
|
|
12
|
+
* @param {number} initialDetent - Index into detents to open at. Defaults to 0.
|
|
13
|
+
* @param {string} title - Optional title in the sticky header, beside the close button.
|
|
14
|
+
* @param {ReactNode} footer - Optional sticky footer, kept clear of the home indicator.
|
|
15
|
+
* @param {boolean} overlayDismiss - Tap the dimmed backdrop to close. Defaults to true.
|
|
16
|
+
* @param {string} className - Extra classes for the sheet panel.
|
|
17
|
+
*/
|
|
18
|
+
export interface SheetProps {
|
|
19
|
+
isOpen: boolean;
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
detents?: number[];
|
|
23
|
+
initialDetent?: number;
|
|
24
|
+
title?: string;
|
|
25
|
+
footer?: React.ReactNode;
|
|
26
|
+
overlayDismiss?: boolean;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* WHY THIS EXISTS ALONGSIDE Drawer
|
|
31
|
+
*
|
|
32
|
+
* `Drawer` already slides in from any edge, including the bottom, and it is
|
|
33
|
+
* used in 18 places across five products. It is a *static* panel: it animates
|
|
34
|
+
* to one size and stays there. The sheet pattern is a different interaction —
|
|
35
|
+
* detents, a drag that tracks your finger, and a body whose scroll is coupled
|
|
36
|
+
* to the sheet's height — and bolting that onto Drawer would put a gesture
|
|
37
|
+
* state machine into a component whose other three positions can never use it,
|
|
38
|
+
* destabilising every existing call site for a behaviour only `bottom` has.
|
|
39
|
+
*
|
|
40
|
+
* So Drawer stays as it is. Reach for Sheet when the surface is primarily
|
|
41
|
+
* touched; reach for Drawer when it is a desktop side panel.
|
|
42
|
+
*
|
|
43
|
+
* ZERO JANK
|
|
44
|
+
*
|
|
45
|
+
* - Height is driven by `transform: translateY`, never by animating `height`,
|
|
46
|
+
* so no frame triggers layout (rule 3). The panel is laid out at full
|
|
47
|
+
* viewport height once and slid down to reveal only the part in use.
|
|
48
|
+
* - `100dvh`, never `vh` (rule 5) — a sheet measured against `vh` is wrong by
|
|
49
|
+
* the height of the toolbar on every mobile browser that retracts one.
|
|
50
|
+
* - The transition is dropped entirely while a finger is down, so the sheet
|
|
51
|
+
* tracks the pointer exactly instead of lagging behind it by the duration.
|
|
52
|
+
* - `prefers-reduced-motion` disables the drag and the animation; the sheet
|
|
53
|
+
* opens at its largest detent and closes, which is the same functionality
|
|
54
|
+
* without the movement (rule 4).
|
|
55
|
+
* - Safe-area insets are honoured so the footer clears the home indicator.
|
|
56
|
+
*/
|
|
57
|
+
export declare const Sheet: React.FC<SheetProps>;
|
|
58
|
+
export default Sheet;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { jsxs as u, jsx as e } from "react/jsx-runtime";
|
|
2
|
+
import { useState as m, useRef as E, useEffect as w, useCallback as S } from "react";
|
|
3
|
+
import { createPortal as j } from "react-dom";
|
|
4
|
+
import { Icon as A } from "../ui/Icon.js";
|
|
5
|
+
import { cn as x } from "../../utils/cn.js";
|
|
6
|
+
import { useBodyScrollLock as R } from "../../lib/hooks/useBodyScrollLock.js";
|
|
7
|
+
import { useSheetGesture as B } from "../../lib/hooks/useSheetGesture.js";
|
|
8
|
+
const G = ({
|
|
9
|
+
isOpen: o,
|
|
10
|
+
onClose: r,
|
|
11
|
+
children: y,
|
|
12
|
+
detents: v = [0.6, 1],
|
|
13
|
+
initialDetent: f = 0,
|
|
14
|
+
title: s,
|
|
15
|
+
footer: h,
|
|
16
|
+
overlayDismiss: k = !0,
|
|
17
|
+
className: N
|
|
18
|
+
}) => {
|
|
19
|
+
const [d, b] = m(!1), [g, p] = m(!1), D = E(null), [l] = m(
|
|
20
|
+
() => typeof window < "u" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
21
|
+
), i = [...v].sort((n, a) => n - a), t = B({
|
|
22
|
+
detents: i,
|
|
23
|
+
// Under reduced motion there is no drag, so a partial height would be a
|
|
24
|
+
// height the reader can never change. Open fully instead.
|
|
25
|
+
initialDetent: l ? i.length - 1 : f,
|
|
26
|
+
onDismiss: r,
|
|
27
|
+
reducedMotion: l
|
|
28
|
+
});
|
|
29
|
+
R(o), w(() => {
|
|
30
|
+
let n, a;
|
|
31
|
+
return o ? (b(!0), t.setHeight(i[l ? i.length - 1 : f] ?? i[0]), n = requestAnimationFrame(() => {
|
|
32
|
+
a = window.setTimeout(() => p(!0), 10);
|
|
33
|
+
})) : (p(!1), a = window.setTimeout(() => b(!1), 320)), () => {
|
|
34
|
+
cancelAnimationFrame(n), clearTimeout(a);
|
|
35
|
+
};
|
|
36
|
+
}, [o]);
|
|
37
|
+
const c = S(
|
|
38
|
+
(n) => {
|
|
39
|
+
n.key === "Escape" && o && r();
|
|
40
|
+
},
|
|
41
|
+
[o, r]
|
|
42
|
+
);
|
|
43
|
+
if (w(() => {
|
|
44
|
+
if (d)
|
|
45
|
+
return document.addEventListener("keydown", c), () => document.removeEventListener("keydown", c);
|
|
46
|
+
}, [d, c]), !d) return null;
|
|
47
|
+
const P = g ? 1 - t.height : 1;
|
|
48
|
+
return j(
|
|
49
|
+
/* @__PURE__ */ u(
|
|
50
|
+
"div",
|
|
51
|
+
{
|
|
52
|
+
className: x(
|
|
53
|
+
"fixed inset-0 z-drawer",
|
|
54
|
+
"transition-opacity duration-300 ease-out",
|
|
55
|
+
g ? "opacity-100" : "opacity-0"
|
|
56
|
+
),
|
|
57
|
+
children: [
|
|
58
|
+
/* @__PURE__ */ e(
|
|
59
|
+
"div",
|
|
60
|
+
{
|
|
61
|
+
className: "absolute inset-0 bg-black/50",
|
|
62
|
+
onClick: k ? r : void 0,
|
|
63
|
+
"aria-hidden": "true"
|
|
64
|
+
}
|
|
65
|
+
),
|
|
66
|
+
/* @__PURE__ */ u(
|
|
67
|
+
"div",
|
|
68
|
+
{
|
|
69
|
+
ref: D,
|
|
70
|
+
role: "dialog",
|
|
71
|
+
"aria-modal": "true",
|
|
72
|
+
"aria-label": s ?? "Sheet",
|
|
73
|
+
style: {
|
|
74
|
+
transform: `translateY(${P * 100}%)`,
|
|
75
|
+
// Laid out at full height and slid down; only transform animates.
|
|
76
|
+
height: "100dvh",
|
|
77
|
+
paddingBottom: "env(safe-area-inset-bottom)"
|
|
78
|
+
},
|
|
79
|
+
className: x(
|
|
80
|
+
"absolute inset-x-0 bottom-0 flex flex-col",
|
|
81
|
+
"bg-card text-foreground border-t border-border shadow-xl",
|
|
82
|
+
// The corners round while the sheet is partial and square off once it
|
|
83
|
+
// owns the whole screen — the cue that tells you it is fully open.
|
|
84
|
+
t.expanded ? "rounded-t-none" : "rounded-t-2xl",
|
|
85
|
+
// No transition while dragging: the sheet must sit exactly under the
|
|
86
|
+
// finger, not chase it.
|
|
87
|
+
t.dragging ? "transition-none" : "transition-transform duration-300 ease-out",
|
|
88
|
+
"motion-reduce:transition-none",
|
|
89
|
+
N
|
|
90
|
+
),
|
|
91
|
+
children: [
|
|
92
|
+
/* @__PURE__ */ e(
|
|
93
|
+
"div",
|
|
94
|
+
{
|
|
95
|
+
onPointerDown: t.onPointerDown,
|
|
96
|
+
style: { touchAction: "none" },
|
|
97
|
+
className: "flex h-11 w-full shrink-0 cursor-grab items-center justify-center active:cursor-grabbing",
|
|
98
|
+
"aria-hidden": "true",
|
|
99
|
+
children: /* @__PURE__ */ e("div", { className: "h-1 w-10 rounded-full bg-muted-foreground/40" })
|
|
100
|
+
}
|
|
101
|
+
),
|
|
102
|
+
/* @__PURE__ */ u(
|
|
103
|
+
"div",
|
|
104
|
+
{
|
|
105
|
+
onPointerDown: t.onPointerDown,
|
|
106
|
+
style: { touchAction: "none" },
|
|
107
|
+
className: "flex shrink-0 items-center justify-between gap-3 px-4 pb-3",
|
|
108
|
+
children: [
|
|
109
|
+
s ? /* @__PURE__ */ e("h2", { className: "text-lg font-semibold leading-tight", children: s }) : /* @__PURE__ */ e("span", {}),
|
|
110
|
+
/* @__PURE__ */ e(
|
|
111
|
+
"button",
|
|
112
|
+
{
|
|
113
|
+
type: "button",
|
|
114
|
+
onClick: r,
|
|
115
|
+
"aria-label": "Close",
|
|
116
|
+
className: "-mr-2 inline-flex min-h-11 min-w-11 items-center justify-center rounded-full text-muted-foreground transition-colors hover:text-foreground",
|
|
117
|
+
children: /* @__PURE__ */ e(A, { name: "x", size: 22 })
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
),
|
|
123
|
+
/* @__PURE__ */ e(
|
|
124
|
+
"div",
|
|
125
|
+
{
|
|
126
|
+
onPointerDown: t.onBodyPointerDown,
|
|
127
|
+
className: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-4",
|
|
128
|
+
children: y
|
|
129
|
+
}
|
|
130
|
+
),
|
|
131
|
+
h && /* @__PURE__ */ e("div", { className: "shrink-0 border-t border-border bg-card px-4 py-3", children: h })
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
),
|
|
138
|
+
document.body
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
export {
|
|
142
|
+
G as Sheet,
|
|
143
|
+
G as default
|
|
144
|
+
};
|
|
@@ -24,11 +24,50 @@ export interface MobileNavProps {
|
|
|
24
24
|
drawerFooter?: React.ReactNode;
|
|
25
25
|
/** Additional CSS classes on the root bar. */
|
|
26
26
|
className?: string;
|
|
27
|
+
/**
|
|
28
|
+
* How navigation is presented.
|
|
29
|
+
*
|
|
30
|
+
* - `'bar'` (default) — the fixed bottom tab bar, with a "More" drawer for
|
|
31
|
+
* `secondaryItems`.
|
|
32
|
+
* - `'sheet'` — no fixed bar at all. Every destination (tabs AND
|
|
33
|
+
* secondaryItems) lives in a `Sheet`, which the host opens via `isOpen`.
|
|
34
|
+
* Use this when the app already has a top header to hang a trigger from:
|
|
35
|
+
* a persistent bottom bar stacks directly on top of a mobile browser's
|
|
36
|
+
* own toolbar, and the two together eat ~120px of a phone screen before
|
|
37
|
+
* any content is drawn.
|
|
38
|
+
*
|
|
39
|
+
* `Sheet`, not `Drawer` — this surface is thumb-driven, which is the line
|
|
40
|
+
* Sheet's own docs draw between the two. It opens at 60% and grows to
|
|
41
|
+
* full as you drag or scroll it up, keeping the page visible behind it so
|
|
42
|
+
* the menu reads as a layer over where you are rather than a departure
|
|
43
|
+
* from it. ramme-marketing's AppHeader uses the same component the same
|
|
44
|
+
* way; this is that pattern, with the item rendering shared rather than
|
|
45
|
+
* hand-rolled per app.
|
|
46
|
+
*/
|
|
47
|
+
variant?: 'bar' | 'sheet';
|
|
48
|
+
/**
|
|
49
|
+
* Controlled drawer state. Required for `'sheet'`, which has no trigger of
|
|
50
|
+
* its own. Ignored by `'bar'`, whose "More" button owns its own state.
|
|
51
|
+
*/
|
|
52
|
+
isOpen?: boolean;
|
|
53
|
+
/** Fired when the sheet wants to close (backdrop, Escape, item tapped). */
|
|
54
|
+
onOpenChange?: (open: boolean) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Extra content rendered inside the panel body, after the destinations and
|
|
57
|
+
* scrolling with them.
|
|
58
|
+
*
|
|
59
|
+
* The mirror of `drawerHeader` at the other end, and deliberately NOT
|
|
60
|
+
* `drawerFooter`: that one maps to the surface's sticky footer, pinned above
|
|
61
|
+
* the home indicator and always visible. Use this for content that belongs
|
|
62
|
+
* *with* the list rather than under it — a control that the header hides at
|
|
63
|
+
* narrow widths and would otherwise be unreachable, say.
|
|
64
|
+
*/
|
|
65
|
+
children?: React.ReactNode;
|
|
27
66
|
}
|
|
28
67
|
/**
|
|
29
68
|
* @wizard
|
|
30
69
|
* @name MobileNav
|
|
31
|
-
* @description
|
|
70
|
+
* @description Mobile navigation, in two shapes. 'bar' is a fixed bottom tab bar with icon+label tabs and a "More" drawer for secondary links. 'sheet' drops the bar entirely and puts every destination in a bottom drawer the host opens from its own trigger — the right choice when a mobile browser's toolbar already owns the bottom edge. Hidden on md: and up via Tailwind. The mobile counterpart to the desktop Sidebar.
|
|
32
71
|
* @tags navigation, mobile, bottom-bar, shell, ui
|
|
33
72
|
* @category navigation
|
|
34
73
|
* @id mobile-nav
|
|
@@ -40,5 +79,9 @@ export interface MobileNavProps {
|
|
|
40
79
|
* @param {ReactNode} drawerHeader - Optional slot rendered at the top of the drawer body.
|
|
41
80
|
* @param {ReactNode} drawerFooter - Optional slot rendered in the sticky drawer footer (e.g. Logout button).
|
|
42
81
|
* @param {string} className - Additional CSS classes on the root nav bar.
|
|
82
|
+
* @param {string} variant - 'bar' (default) renders the fixed bottom tab bar with a "More" Drawer. 'sheet' renders no bar at all and puts every destination in a Sheet — opening at 60% and growing to full as you drag or scroll it up — which the host opens itself — use it when a top header can hold the trigger, since a fixed bar and a mobile browser's own toolbar stack into ~120px of lost screen.
|
|
83
|
+
* @param {boolean} isOpen - Controlled drawer state. Required by 'sheet', which has no trigger of its own; ignored by 'bar'.
|
|
84
|
+
* @param {function} onOpenChange - Fired when the sheet wants to close (backdrop, Escape, or an item tapped).
|
|
85
|
+
* @param {ReactNode} children - Extra content rendered in the panel body after the destinations, scrolling with them. The mirror of drawerHeader; use it instead of drawerFooter when the content belongs with the list rather than pinned under it.
|
|
43
86
|
*/
|
|
44
87
|
export declare const MobileNav: React.FC<MobileNavProps>;
|
|
@@ -1,98 +1,126 @@
|
|
|
1
|
-
import { jsxs as
|
|
2
|
-
import { useState as
|
|
3
|
-
import { cn as
|
|
4
|
-
import { Icon as
|
|
5
|
-
import { Drawer as
|
|
6
|
-
|
|
1
|
+
import { jsxs as i, Fragment as w, jsx as r } from "react/jsx-runtime";
|
|
2
|
+
import { useState as S, useCallback as p } from "react";
|
|
3
|
+
import { cn as m } from "../../utils/cn.js";
|
|
4
|
+
import { Icon as u } from "../ui/Icon.js";
|
|
5
|
+
import { Drawer as z } from "../layout/Drawer.js";
|
|
6
|
+
import { Sheet as B } from "../layout/Sheet.js";
|
|
7
|
+
const v = ({ tab: t, isActive: s, onTap: o }) => /* @__PURE__ */ i(
|
|
7
8
|
"button",
|
|
8
9
|
{
|
|
9
|
-
onClick:
|
|
10
|
-
className:
|
|
10
|
+
onClick: o,
|
|
11
|
+
className: m(
|
|
11
12
|
"flex flex-1 flex-col items-center justify-center gap-0.5 py-1.5 transition-colors duration-150",
|
|
12
|
-
|
|
13
|
+
s ? "text-primary" : "text-muted-foreground active:text-foreground"
|
|
13
14
|
),
|
|
14
15
|
children: [
|
|
15
16
|
/* @__PURE__ */ r(
|
|
16
|
-
|
|
17
|
+
u,
|
|
17
18
|
{
|
|
18
|
-
name:
|
|
19
|
-
className:
|
|
19
|
+
name: t.icon,
|
|
20
|
+
className: m(
|
|
20
21
|
"h-5 w-5 transition-transform duration-150",
|
|
21
|
-
|
|
22
|
+
s && "scale-110"
|
|
22
23
|
)
|
|
23
24
|
}
|
|
24
25
|
),
|
|
25
|
-
/* @__PURE__ */ r("span", { className: "text-[10px] font-medium leading-tight", children:
|
|
26
|
+
/* @__PURE__ */ r("span", { className: "text-[10px] font-medium leading-tight", children: t.label })
|
|
26
27
|
]
|
|
27
28
|
}
|
|
28
|
-
),
|
|
29
|
+
), A = ({ item: t, isActive: s, onTap: o }) => /* @__PURE__ */ i(
|
|
29
30
|
"button",
|
|
30
31
|
{
|
|
31
|
-
onClick:
|
|
32
|
-
className:
|
|
32
|
+
onClick: o,
|
|
33
|
+
className: m(
|
|
33
34
|
"flex w-full items-center gap-3 rounded-lg p-3 text-left transition-colors duration-150",
|
|
34
|
-
|
|
35
|
+
s ? "bg-primary/10 text-primary" : "text-foreground hover:bg-muted active:bg-muted"
|
|
35
36
|
),
|
|
36
37
|
children: [
|
|
37
|
-
/* @__PURE__ */ r(
|
|
38
|
-
/* @__PURE__ */ r("span", { className: "text-fluid-base font-medium", children:
|
|
38
|
+
/* @__PURE__ */ r(u, { name: t.icon, className: "h-5 w-5 shrink-0" }),
|
|
39
|
+
/* @__PURE__ */ r("span", { className: "text-fluid-base font-medium", children: t.label })
|
|
39
40
|
]
|
|
40
41
|
}
|
|
41
|
-
),
|
|
42
|
-
tabs:
|
|
43
|
-
secondaryItems:
|
|
44
|
-
activeTabId:
|
|
45
|
-
onNavigate:
|
|
46
|
-
drawerTitle:
|
|
47
|
-
drawerHeader:
|
|
48
|
-
drawerFooter:
|
|
49
|
-
className:
|
|
42
|
+
), J = ({
|
|
43
|
+
tabs: t,
|
|
44
|
+
secondaryItems: s = [],
|
|
45
|
+
activeTabId: o,
|
|
46
|
+
onNavigate: l,
|
|
47
|
+
drawerTitle: x = "Menu",
|
|
48
|
+
drawerHeader: h,
|
|
49
|
+
drawerFooter: b,
|
|
50
|
+
className: g,
|
|
51
|
+
variant: D = "bar",
|
|
52
|
+
isOpen: j,
|
|
53
|
+
onOpenChange: a,
|
|
54
|
+
children: T
|
|
50
55
|
}) => {
|
|
51
|
-
const [
|
|
56
|
+
const [C, I] = S(!1), n = D === "sheet", f = n ? !!j : C, c = p(
|
|
52
57
|
(e) => {
|
|
53
|
-
|
|
54
|
-
(i = e.onClick) == null || i.call(e), t == null || t(e);
|
|
58
|
+
n ? a == null || a(e) : I(e);
|
|
55
59
|
},
|
|
56
|
-
[
|
|
57
|
-
), k =
|
|
60
|
+
[n, a]
|
|
61
|
+
), k = n ? [...t, ...s] : s, M = p(
|
|
58
62
|
(e) => {
|
|
59
|
-
var
|
|
60
|
-
(
|
|
63
|
+
var d;
|
|
64
|
+
(d = e.onClick) == null || d.call(e), l == null || l(e);
|
|
61
65
|
},
|
|
62
|
-
[
|
|
66
|
+
[l]
|
|
67
|
+
), N = p(
|
|
68
|
+
(e) => {
|
|
69
|
+
var d;
|
|
70
|
+
(d = e.onClick) == null || d.call(e), l == null || l(e), c(!1);
|
|
71
|
+
},
|
|
72
|
+
// `setIsDrawerOpen` closes over `onOpenChange`, so it belongs here. Without
|
|
73
|
+
// it, a host that memoises `onNavigate` but passes `onOpenChange` inline
|
|
74
|
+
// keeps the first render's close handler for the life of the component and
|
|
75
|
+
// the sheet stops closing on tap.
|
|
76
|
+
[l, c]
|
|
63
77
|
);
|
|
64
|
-
|
|
65
|
-
|
|
78
|
+
if (n && k.length === 0) return null;
|
|
79
|
+
const y = /* @__PURE__ */ i(w, { children: [
|
|
80
|
+
h && /* @__PURE__ */ r("div", { className: "mb-fluid-md pb-fluid-sm border-b border-border", children: h }),
|
|
81
|
+
/* @__PURE__ */ r("div", { className: "space-y-1", children: k.map((e) => /* @__PURE__ */ r(
|
|
82
|
+
A,
|
|
83
|
+
{
|
|
84
|
+
item: e,
|
|
85
|
+
isActive: o === e.id,
|
|
86
|
+
onTap: () => N(e)
|
|
87
|
+
},
|
|
88
|
+
e.id
|
|
89
|
+
)) }),
|
|
90
|
+
T
|
|
91
|
+
] });
|
|
92
|
+
return /* @__PURE__ */ i(w, { children: [
|
|
93
|
+
!n && /* @__PURE__ */ r(
|
|
66
94
|
"nav",
|
|
67
95
|
{
|
|
68
|
-
className:
|
|
96
|
+
className: m(
|
|
69
97
|
"fixed inset-x-0 bottom-0 z-nav md:hidden",
|
|
70
98
|
"border-t border-border bg-card/95 backdrop-blur-md",
|
|
71
99
|
"supports-[backdrop-filter]:bg-card/80",
|
|
72
100
|
// Safe area for notched phones (iPhone etc.)
|
|
73
101
|
"pb-[env(safe-area-inset-bottom,0px)]",
|
|
74
|
-
|
|
102
|
+
g
|
|
75
103
|
),
|
|
76
|
-
children: /* @__PURE__ */
|
|
77
|
-
|
|
78
|
-
|
|
104
|
+
children: /* @__PURE__ */ i("div", { className: "flex items-stretch", children: [
|
|
105
|
+
t.map((e) => /* @__PURE__ */ r(
|
|
106
|
+
v,
|
|
79
107
|
{
|
|
80
108
|
tab: e,
|
|
81
|
-
isActive:
|
|
82
|
-
onTap: () =>
|
|
109
|
+
isActive: o === e.id,
|
|
110
|
+
onTap: () => M(e)
|
|
83
111
|
},
|
|
84
112
|
e.id
|
|
85
113
|
)),
|
|
86
|
-
|
|
114
|
+
s.length > 0 && /* @__PURE__ */ i(
|
|
87
115
|
"button",
|
|
88
116
|
{
|
|
89
|
-
onClick: () =>
|
|
90
|
-
className:
|
|
117
|
+
onClick: () => c(!0),
|
|
118
|
+
className: m(
|
|
91
119
|
"flex flex-1 flex-col items-center justify-center gap-0.5 py-1.5 transition-colors duration-150",
|
|
92
|
-
|
|
120
|
+
f ? "text-primary" : "text-muted-foreground active:text-foreground"
|
|
93
121
|
),
|
|
94
122
|
children: [
|
|
95
|
-
/* @__PURE__ */ r(
|
|
123
|
+
/* @__PURE__ */ r(u, { name: "menu", className: "h-5 w-5" }),
|
|
96
124
|
/* @__PURE__ */ r("span", { className: "text-[10px] font-medium leading-tight", children: "More" })
|
|
97
125
|
]
|
|
98
126
|
}
|
|
@@ -100,31 +128,29 @@ const C = ({ tab: n, isActive: o, onTap: s }) => /* @__PURE__ */ l(
|
|
|
100
128
|
] })
|
|
101
129
|
}
|
|
102
130
|
),
|
|
103
|
-
/* @__PURE__ */
|
|
104
|
-
|
|
131
|
+
n ? /* @__PURE__ */ r(
|
|
132
|
+
B,
|
|
105
133
|
{
|
|
106
|
-
isOpen:
|
|
107
|
-
onClose: () =>
|
|
134
|
+
isOpen: f,
|
|
135
|
+
onClose: () => c(!1),
|
|
136
|
+
title: x,
|
|
137
|
+
footer: b,
|
|
138
|
+
children: y
|
|
139
|
+
}
|
|
140
|
+
) : /* @__PURE__ */ r(
|
|
141
|
+
z,
|
|
142
|
+
{
|
|
143
|
+
isOpen: f,
|
|
144
|
+
onClose: () => c(!1),
|
|
108
145
|
position: "bottom",
|
|
109
146
|
size: "85dvh",
|
|
110
|
-
title:
|
|
111
|
-
footer:
|
|
112
|
-
children:
|
|
113
|
-
m && /* @__PURE__ */ r("div", { className: "mb-fluid-md pb-fluid-sm border-b border-border", children: m }),
|
|
114
|
-
/* @__PURE__ */ r("div", { className: "space-y-1", children: o.map((e) => /* @__PURE__ */ r(
|
|
115
|
-
D,
|
|
116
|
-
{
|
|
117
|
-
item: e,
|
|
118
|
-
isActive: s === e.id,
|
|
119
|
-
onTap: () => k(e)
|
|
120
|
-
},
|
|
121
|
-
e.id
|
|
122
|
-
)) })
|
|
123
|
-
]
|
|
147
|
+
title: x,
|
|
148
|
+
footer: b,
|
|
149
|
+
children: y
|
|
124
150
|
}
|
|
125
151
|
)
|
|
126
152
|
] });
|
|
127
153
|
};
|
|
128
154
|
export {
|
|
129
|
-
|
|
155
|
+
J as MobileNav
|
|
130
156
|
};
|