react-floating-dock 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +152 -2
- package/dist/index.cjs +171 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +67 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +168 -0
- package/dist/index.js.map +1 -0
- package/dist/style.css +223 -0
- package/package.json +70 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lemon Oluwatobi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,153 @@
|
|
|
1
|
-
#
|
|
1
|
+
# react-floating-dock
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A floating, fully-rounded bottom navigation bar for React, in the style of WhatsApp's mobile tab
|
|
4
|
+
bar: an animated pill slides behind the active tab, and an optional "More" button opens the rest
|
|
5
|
+
of your destinations in a bottom sheet.
|
|
6
|
+
|
|
7
|
+
Framework-agnostic — it doesn't assume Next.js, React Router, or Tailwind. Bring your own router's
|
|
8
|
+
`Link` component and your own colors.
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install react-floating-dock framer-motion
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`react`, `react-dom` and `framer-motion` are peer dependencies — install them if your project
|
|
19
|
+
doesn't already have them.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { FloatingDock, type DockItem } from "react-floating-dock";
|
|
25
|
+
import "react-floating-dock/style.css";
|
|
26
|
+
import { Home, Wallet, ArrowLeftRight, User, Settings } from "lucide-react";
|
|
27
|
+
import Link from "next/link"; // or your router's Link
|
|
28
|
+
import { usePathname } from "next/navigation";
|
|
29
|
+
|
|
30
|
+
const items: DockItem[] = [
|
|
31
|
+
{ key: "home", href: "/", label: "Home", icon: Home },
|
|
32
|
+
{ key: "wallet", href: "/wallet", label: "Wallet", icon: Wallet },
|
|
33
|
+
{ key: "activity", href: "/activity", label: "Activity", icon: ArrowLeftRight },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const moreItems: DockItem[] = [
|
|
37
|
+
{ key: "profile", href: "/profile", label: "Profile", icon: User },
|
|
38
|
+
{ key: "settings", href: "/settings", label: "Settings", icon: Settings },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export function AppNav() {
|
|
42
|
+
const pathname = usePathname();
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<FloatingDock
|
|
46
|
+
items={items}
|
|
47
|
+
moreItems={moreItems}
|
|
48
|
+
isActive={(href) => pathname === href}
|
|
49
|
+
LinkComponent={Link}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That's it — four or five primary tabs, an overflow sheet for the rest, and a theme you can
|
|
56
|
+
override with CSS custom properties.
|
|
57
|
+
|
|
58
|
+
See [`example/`](./example) for a complete, runnable demo (`pnpm example`).
|
|
59
|
+
|
|
60
|
+
## Why a `LinkComponent` prop instead of just using `<a>`?
|
|
61
|
+
|
|
62
|
+
So client-side navigation works with whatever router you're on. Most router `Link` components
|
|
63
|
+
accept `href`, `className` and `children` — the same shape this package expects — so passing
|
|
64
|
+
`next/link`'s `Link`, React Router's `Link`, or TanStack Router's `Link` directly just works. If
|
|
65
|
+
you don't pass one, it falls back to a plain `<a href>`.
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
### `<FloatingDock />`
|
|
70
|
+
|
|
71
|
+
| Prop | Type | Default | Description |
|
|
72
|
+
| ----------------- | -------------------------------------- | -------------- | ----------------------------------------------------------------------------- |
|
|
73
|
+
| `items` | `DockItem[]` | — | Destinations shown directly in the bar. Four or five is the sweet spot. |
|
|
74
|
+
| `moreItems` | `DockItem[]` | `[]` | Extra destinations shown in the overflow sheet. Omit to hide the "More" button entirely. |
|
|
75
|
+
| `isActive` | `(href: string) => boolean` | — | Tells the dock which item (if any) is the current page. |
|
|
76
|
+
| `LinkComponent` | `ComponentType<DockLinkProps>` | plain `<a>` | Your router's Link component. |
|
|
77
|
+
| `moreLabel` | `string` | `"More"` | Label under the overflow button. |
|
|
78
|
+
| `moreSheetTitle` | `string` | `"Menu"` | Heading inside the overflow sheet. |
|
|
79
|
+
| `closeLabel` | `string` | `"Close"` | `aria-label` for the sheet's close button. |
|
|
80
|
+
| `extraContent` | `ReactNode` | — | Rendered below the overflow grid — a natural slot for a theme toggle, sign-out button, language switcher, etc. |
|
|
81
|
+
| `ariaLabel` | `string` | `"Main"` | `aria-label` on the `<nav>` element. |
|
|
82
|
+
| `className` | `string` | — | Extra class on the `<nav>`, alongside the built-in one. |
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
interface DockItem {
|
|
86
|
+
key: string; // React key, usually the same as href
|
|
87
|
+
href: string;
|
|
88
|
+
label: string;
|
|
89
|
+
icon: ComponentType<{ className?: string }>; // any icon component — lucide-react, heroicons, your own SVGs
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `<DockDrawer />`
|
|
94
|
+
|
|
95
|
+
The bottom sheet `FloatingDock` uses internally for its overflow menu. Exported separately because
|
|
96
|
+
it's useful on its own for any mobile-style sheet (filters, a confirmation prompt, etc.).
|
|
97
|
+
|
|
98
|
+
| Prop | Type | Default | Description |
|
|
99
|
+
| ------------ | ------------ | --------- | ----------------------------------- |
|
|
100
|
+
| `open` | `boolean` | — | Whether the sheet is visible. |
|
|
101
|
+
| `onClose` | `() => void` | — | Called on backdrop click, Escape, or the close button. |
|
|
102
|
+
| `title` | `string` | — | Heading shown in the sheet's header. |
|
|
103
|
+
| `children` | `ReactNode` | — | Sheet body. |
|
|
104
|
+
| `closeLabel` | `string` | `"Close"` | `aria-label` for the close button. |
|
|
105
|
+
|
|
106
|
+
## Styling
|
|
107
|
+
|
|
108
|
+
Import `react-floating-dock/style.css` once, anywhere in your app. It's plain CSS driven entirely
|
|
109
|
+
by custom properties, so no Tailwind (or any other build-time CSS tooling) is required.
|
|
110
|
+
|
|
111
|
+
To theme it, redefine the variables wherever makes sense for your app — `:root` for a global
|
|
112
|
+
theme, or a wrapper element for something scoped:
|
|
113
|
+
|
|
114
|
+
```css
|
|
115
|
+
:root {
|
|
116
|
+
--rfd-bg: rgba(255, 255, 255, 0.9); /* bar + sheet background */
|
|
117
|
+
--rfd-bg-solid: #ffffff; /* sheet background (no backdrop-filter) */
|
|
118
|
+
--rfd-border: #e2e8f0;
|
|
119
|
+
--rfd-text: #0f172a;
|
|
120
|
+
--rfd-text-muted: #64748b;
|
|
121
|
+
--rfd-accent: #3b82f6; /* active tab color */
|
|
122
|
+
--rfd-accent-bg: rgba(59, 130, 246, 0.12); /* active tab's pill background */
|
|
123
|
+
--rfd-accent-contrast: #ffffff; /* text/icon color on top of --rfd-accent */
|
|
124
|
+
--rfd-hover: rgba(0, 0, 0, 0.05);
|
|
125
|
+
--rfd-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15);
|
|
126
|
+
--rfd-radius: 9999px; /* fully rounded by default — set a smaller value for softer corners */
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
All of the dock's classes are prefixed `rfd-` to avoid collisions with the rest of your app's CSS.
|
|
131
|
+
|
|
132
|
+
## Accessibility
|
|
133
|
+
|
|
134
|
+
- The active tab gets `aria-current="page"`.
|
|
135
|
+
- The overflow sheet is a `role="dialog"` with `aria-modal="true"`, traps body scroll while open,
|
|
136
|
+
and closes on Escape.
|
|
137
|
+
- Every interactive element is a real `<button>` or your `LinkComponent`, so it's reachable and
|
|
138
|
+
operable by keyboard out of the box.
|
|
139
|
+
|
|
140
|
+
## Development
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pnpm install
|
|
144
|
+
pnpm test # vitest
|
|
145
|
+
pnpm typecheck
|
|
146
|
+
pnpm lint
|
|
147
|
+
pnpm build # outputs dist/ (ESM + CJS + types + style.css)
|
|
148
|
+
pnpm example # runs the example app against the live source, at localhost:5173
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var framerMotion = require('framer-motion');
|
|
5
|
+
var reactDom = require('react-dom');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
|
|
8
|
+
// src/FloatingDock.tsx
|
|
9
|
+
function MoreHorizontalIcon(props) {
|
|
10
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
11
|
+
"svg",
|
|
12
|
+
{
|
|
13
|
+
viewBox: "0 0 24 24",
|
|
14
|
+
width: "24",
|
|
15
|
+
height: "24",
|
|
16
|
+
fill: "none",
|
|
17
|
+
stroke: "currentColor",
|
|
18
|
+
strokeWidth: "2",
|
|
19
|
+
strokeLinecap: "round",
|
|
20
|
+
strokeLinejoin: "round",
|
|
21
|
+
"aria-hidden": "true",
|
|
22
|
+
...props,
|
|
23
|
+
children: [
|
|
24
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "1" }),
|
|
25
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "19", cy: "12", r: "1" }),
|
|
26
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "5", cy: "12", r: "1" })
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
function CloseIcon(props) {
|
|
32
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
33
|
+
"svg",
|
|
34
|
+
{
|
|
35
|
+
viewBox: "0 0 24 24",
|
|
36
|
+
width: "24",
|
|
37
|
+
height: "24",
|
|
38
|
+
fill: "none",
|
|
39
|
+
stroke: "currentColor",
|
|
40
|
+
strokeWidth: "2",
|
|
41
|
+
strokeLinecap: "round",
|
|
42
|
+
strokeLinejoin: "round",
|
|
43
|
+
"aria-hidden": "true",
|
|
44
|
+
...props,
|
|
45
|
+
children: [
|
|
46
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 6 6 18" }),
|
|
47
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "m6 6 12 12" })
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
function DockDrawer({ open, onClose, title, children, closeLabel = "Close" }) {
|
|
53
|
+
const [mounted, setMounted] = react.useState(false);
|
|
54
|
+
react.useEffect(() => setMounted(true), []);
|
|
55
|
+
react.useEffect(() => {
|
|
56
|
+
if (!open) return;
|
|
57
|
+
const previousOverflow = document.body.style.overflow;
|
|
58
|
+
document.body.style.overflow = "hidden";
|
|
59
|
+
return () => {
|
|
60
|
+
document.body.style.overflow = previousOverflow;
|
|
61
|
+
};
|
|
62
|
+
}, [open]);
|
|
63
|
+
react.useEffect(() => {
|
|
64
|
+
if (!open) return;
|
|
65
|
+
const onKey = (event) => {
|
|
66
|
+
if (event.key === "Escape") onClose();
|
|
67
|
+
};
|
|
68
|
+
document.addEventListener("keydown", onKey);
|
|
69
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
70
|
+
}, [open, onClose]);
|
|
71
|
+
if (!mounted) return null;
|
|
72
|
+
return reactDom.createPortal(
|
|
73
|
+
/* @__PURE__ */ jsxRuntime.jsx(framerMotion.AnimatePresence, { children: open && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfd-overlay-wrap", children: [
|
|
74
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
75
|
+
framerMotion.motion.div,
|
|
76
|
+
{
|
|
77
|
+
className: "rfd-overlay",
|
|
78
|
+
initial: { opacity: 0 },
|
|
79
|
+
animate: { opacity: 1 },
|
|
80
|
+
exit: { opacity: 0 },
|
|
81
|
+
onClick: onClose
|
|
82
|
+
}
|
|
83
|
+
),
|
|
84
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
85
|
+
framerMotion.motion.div,
|
|
86
|
+
{
|
|
87
|
+
role: "dialog",
|
|
88
|
+
"aria-modal": "true",
|
|
89
|
+
"aria-label": title,
|
|
90
|
+
className: "rfd-sheet",
|
|
91
|
+
initial: { y: "100%" },
|
|
92
|
+
animate: { y: 0 },
|
|
93
|
+
exit: { y: "100%" },
|
|
94
|
+
transition: { type: "spring", damping: 28, stiffness: 300 },
|
|
95
|
+
children: [
|
|
96
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfd-sheet-header", children: [
|
|
97
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "rfd-sheet-title", children: title }),
|
|
98
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: onClose, "aria-label": closeLabel, className: "rfd-close-btn", children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, { className: "rfd-close-icon" }) })
|
|
99
|
+
] }),
|
|
100
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfd-sheet-body", children })
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
] }) }),
|
|
105
|
+
document.body
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
var DefaultLink = ({ href, children, ...rest }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href, ...rest, children });
|
|
109
|
+
function FloatingDock({
|
|
110
|
+
items,
|
|
111
|
+
moreItems = [],
|
|
112
|
+
isActive,
|
|
113
|
+
LinkComponent = DefaultLink,
|
|
114
|
+
moreLabel = "More",
|
|
115
|
+
moreSheetTitle = "Menu",
|
|
116
|
+
closeLabel = "Close",
|
|
117
|
+
extraContent,
|
|
118
|
+
ariaLabel = "Main",
|
|
119
|
+
className
|
|
120
|
+
}) {
|
|
121
|
+
const [moreOpen, setMoreOpen] = react.useState(false);
|
|
122
|
+
const hasMore = moreItems.length > 0;
|
|
123
|
+
const moreActive = hasMore && moreItems.some((item) => isActive(item.href));
|
|
124
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
125
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
126
|
+
"nav",
|
|
127
|
+
{
|
|
128
|
+
"aria-label": ariaLabel,
|
|
129
|
+
className: ["rfd-bar", className].filter(Boolean).join(" "),
|
|
130
|
+
style: { bottom: "max(0.75rem, env(safe-area-inset-bottom))" },
|
|
131
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rfd-track", children: [
|
|
132
|
+
items.map(({ key, href, label, icon: Icon }) => {
|
|
133
|
+
const active = isActive(href);
|
|
134
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(LinkComponent, { href, className: "rfd-tab", "aria-current": active ? "page" : void 0, children: [
|
|
135
|
+
active && /* @__PURE__ */ jsxRuntime.jsx(
|
|
136
|
+
framerMotion.motion.span,
|
|
137
|
+
{
|
|
138
|
+
layoutId: "rfd-pill",
|
|
139
|
+
transition: { type: "spring", stiffness: 260, damping: 26 },
|
|
140
|
+
className: "rfd-pill"
|
|
141
|
+
}
|
|
142
|
+
),
|
|
143
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { className: active ? "rfd-icon rfd-icon--active" : "rfd-icon" }),
|
|
144
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: active ? "rfd-label rfd-label--active" : "rfd-label", children: label })
|
|
145
|
+
] }, key);
|
|
146
|
+
}),
|
|
147
|
+
hasMore && /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", onClick: () => setMoreOpen(true), "aria-label": moreLabel, className: "rfd-tab rfd-more-btn", children: [
|
|
148
|
+
moreActive && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rfd-pill" }),
|
|
149
|
+
/* @__PURE__ */ jsxRuntime.jsx(MoreHorizontalIcon, { className: moreActive ? "rfd-icon rfd-icon--active" : "rfd-icon" }),
|
|
150
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: moreActive ? "rfd-label rfd-label--active" : "rfd-label", children: moreLabel })
|
|
151
|
+
] })
|
|
152
|
+
] })
|
|
153
|
+
}
|
|
154
|
+
),
|
|
155
|
+
hasMore && /* @__PURE__ */ jsxRuntime.jsxs(DockDrawer, { open: moreOpen, onClose: () => setMoreOpen(false), title: moreSheetTitle, closeLabel, children: [
|
|
156
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rfd-more-grid", children: moreItems.map(({ key, href, label, icon: Icon }) => {
|
|
157
|
+
const active = isActive(href);
|
|
158
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(LinkComponent, { href, className: "rfd-more-item", onClick: () => setMoreOpen(false), children: [
|
|
159
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: active ? "rfd-more-icon-circle rfd-more-icon-circle--active" : "rfd-more-icon-circle", children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "rfd-more-icon" }) }),
|
|
160
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "rfd-more-label", children: label })
|
|
161
|
+
] }, key);
|
|
162
|
+
}) }),
|
|
163
|
+
extraContent
|
|
164
|
+
] })
|
|
165
|
+
] });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
exports.DockDrawer = DockDrawer;
|
|
169
|
+
exports.FloatingDock = FloatingDock;
|
|
170
|
+
//# sourceMappingURL=index.cjs.map
|
|
171
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/icons.tsx","../src/DockDrawer.tsx","../src/FloatingDock.tsx"],"names":["jsxs","jsx","useState","useEffect","createPortal","AnimatePresence","motion","Fragment"],"mappings":";;;;;;;;AAIO,SAAS,mBAAmB,KAAA,EAAgC;AACjE,EAAA,uBACEA,eAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,OAAA,EAAQ,WAAA;AAAA,MACR,KAAA,EAAM,IAAA;AAAA,MACN,MAAA,EAAO,IAAA;AAAA,MACP,IAAA,EAAK,MAAA;AAAA,MACL,MAAA,EAAO,cAAA;AAAA,MACP,WAAA,EAAY,GAAA;AAAA,MACZ,aAAA,EAAc,OAAA;AAAA,MACd,cAAA,EAAe,OAAA;AAAA,MACf,aAAA,EAAY,MAAA;AAAA,MACX,GAAG,KAAA;AAAA,MAEJ,QAAA,EAAA;AAAA,wBAAAC,cAAA,CAAC,YAAO,EAAA,EAAG,IAAA,EAAK,EAAA,EAAG,IAAA,EAAK,GAAE,GAAA,EAAI,CAAA;AAAA,uCAC7B,QAAA,EAAA,EAAO,EAAA,EAAG,MAAK,EAAA,EAAG,IAAA,EAAK,GAAE,GAAA,EAAI,CAAA;AAAA,uCAC7B,QAAA,EAAA,EAAO,EAAA,EAAG,KAAI,EAAA,EAAG,IAAA,EAAK,GAAE,GAAA,EAAI;AAAA;AAAA;AAAA,GAC/B;AAEJ;AAEO,SAAS,UAAU,KAAA,EAAgC;AACxD,EAAA,uBACED,eAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,OAAA,EAAQ,WAAA;AAAA,MACR,KAAA,EAAM,IAAA;AAAA,MACN,MAAA,EAAO,IAAA;AAAA,MACP,IAAA,EAAK,MAAA;AAAA,MACL,MAAA,EAAO,cAAA;AAAA,MACP,WAAA,EAAY,GAAA;AAAA,MACZ,aAAA,EAAc,OAAA;AAAA,MACd,cAAA,EAAe,OAAA;AAAA,MACf,aAAA,EAAY,MAAA;AAAA,MACX,GAAG,KAAA;AAAA,MAEJ,QAAA,EAAA;AAAA,wBAAAC,cAAA,CAAC,MAAA,EAAA,EAAK,GAAE,YAAA,EAAa,CAAA;AAAA,wBACrBA,cAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,YAAA,EAAa;AAAA;AAAA;AAAA,GACvB;AAEJ;ACvBO,SAAS,UAAA,CAAW,EAAE,IAAA,EAAM,OAAA,EAAS,OAAO,QAAA,EAAU,UAAA,GAAa,SAAQ,EAAoB;AAEpG,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAIC,eAAS,KAAK,CAAA;AAC5C,EAAAC,eAAA,CAAU,MAAM,UAAA,CAAW,IAAI,CAAA,EAAG,EAAE,CAAA;AAEpC,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,IAAA,EAAM;AACX,IAAA,MAAM,gBAAA,GAAmB,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,QAAA;AAC7C,IAAA,QAAA,CAAS,IAAA,CAAK,MAAM,QAAA,GAAW,QAAA;AAC/B,IAAA,OAAO,MAAM;AACX,MAAA,QAAA,CAAS,IAAA,CAAK,MAAM,QAAA,GAAW,gBAAA;AAAA,IACjC,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA;AAET,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,IAAA,EAAM;AACX,IAAA,MAAM,KAAA,GAAQ,CAAC,KAAA,KAAyB;AACtC,MAAA,IAAI,KAAA,CAAM,GAAA,KAAQ,QAAA,EAAU,OAAA,EAAQ;AAAA,IACtC,CAAA;AACA,IAAA,QAAA,CAAS,gBAAA,CAAiB,WAAW,KAAK,CAAA;AAC1C,IAAA,OAAO,MAAM,QAAA,CAAS,mBAAA,CAAoB,SAAA,EAAW,KAAK,CAAA;AAAA,EAC5D,CAAA,EAAG,CAAC,IAAA,EAAM,OAAO,CAAC,CAAA;AAElB,EAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AAErB,EAAA,OAAOC,qBAAA;AAAA,oBACLH,eAACI,4BAAA,EAAA,EACE,QAAA,EAAA,IAAA,oBACCL,eAAAA,CAAC,KAAA,EAAA,EAAI,WAAU,kBAAA,EACb,QAAA,EAAA;AAAA,sBAAAC,cAAAA;AAAA,QAACK,mBAAA,CAAO,GAAA;AAAA,QAAP;AAAA,UACC,SAAA,EAAU,aAAA;AAAA,UACV,OAAA,EAAS,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,UACtB,OAAA,EAAS,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,UACtB,IAAA,EAAM,EAAE,OAAA,EAAS,CAAA,EAAE;AAAA,UACnB,OAAA,EAAS;AAAA;AAAA,OACX;AAAA,sBACAN,eAAAA;AAAA,QAACM,mBAAA,CAAO,GAAA;AAAA,QAAP;AAAA,UACC,IAAA,EAAK,QAAA;AAAA,UACL,YAAA,EAAW,MAAA;AAAA,UACX,YAAA,EAAY,KAAA;AAAA,UACZ,SAAA,EAAU,WAAA;AAAA,UACV,OAAA,EAAS,EAAE,CAAA,EAAG,MAAA,EAAO;AAAA,UACrB,OAAA,EAAS,EAAE,CAAA,EAAG,CAAA,EAAE;AAAA,UAChB,IAAA,EAAM,EAAE,CAAA,EAAG,MAAA,EAAO;AAAA,UAClB,YAAY,EAAE,IAAA,EAAM,UAAU,OAAA,EAAS,EAAA,EAAI,WAAW,GAAA,EAAI;AAAA,UAE1D,QAAA,EAAA;AAAA,4BAAAN,eAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,kBAAA,EACb,QAAA,EAAA;AAAA,8BAAAC,cAAAA,CAAC,IAAA,EAAA,EAAG,SAAA,EAAU,iBAAA,EAAmB,QAAA,EAAA,KAAA,EAAM,CAAA;AAAA,8BACvCA,cAAAA,CAAC,QAAA,EAAA,EAAO,IAAA,EAAK,QAAA,EAAS,SAAS,OAAA,EAAS,YAAA,EAAY,UAAA,EAAY,SAAA,EAAU,iBACxE,QAAA,kBAAAA,cAAAA,CAAC,SAAA,EAAA,EAAU,SAAA,EAAU,kBAAiB,CAAA,EACxC;AAAA,aAAA,EACF,CAAA;AAAA,4BACAA,cAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,kBAAkB,QAAA,EAAS;AAAA;AAAA;AAAA;AAC5C,KAAA,EACF,CAAA,EAEJ,CAAA;AAAA,IACA,QAAA,CAAS;AAAA,GACX;AACF;AC9BA,IAAM,WAAA,GAA4C,CAAC,EAAE,IAAA,EAAM,UAAU,GAAG,IAAA,EAAK,qBAC3EA,cAAAA,CAAC,GAAA,EAAA,EAAE,IAAA,EAAa,GAAG,MAChB,QAAA,EACH,CAAA;AAQK,SAAS,YAAA,CAAa;AAAA,EAC3B,KAAA;AAAA,EACA,YAAY,EAAC;AAAA,EACb,QAAA;AAAA,EACA,aAAA,GAAgB,WAAA;AAAA,EAChB,SAAA,GAAY,MAAA;AAAA,EACZ,cAAA,GAAiB,MAAA;AAAA,EACjB,UAAA,GAAa,OAAA;AAAA,EACb,YAAA;AAAA,EACA,SAAA,GAAY,MAAA;AAAA,EACZ;AACF,CAAA,EAAsB;AACpB,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAIC,eAAS,KAAK,CAAA;AAC9C,EAAA,MAAM,OAAA,GAAU,UAAU,MAAA,GAAS,CAAA;AACnC,EAAA,MAAM,UAAA,GAAa,WAAW,SAAA,CAAU,IAAA,CAAK,CAAC,IAAA,KAAS,QAAA,CAAS,IAAA,CAAK,IAAI,CAAC,CAAA;AAE1E,EAAA,uBACEF,gBAAAO,mBAAA,EAAA,EACE,QAAA,EAAA;AAAA,oBAAAN,cAAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,YAAA,EAAY,SAAA;AAAA,QACZ,SAAA,EAAW,CAAC,SAAA,EAAW,SAAS,EAAE,MAAA,CAAO,OAAO,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAAA,QAC1D,KAAA,EAAO,EAAE,MAAA,EAAQ,2CAAA,EAA4C;AAAA,QAE7D,QAAA,kBAAAD,eAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,WAAA,EACZ,QAAA,EAAA;AAAA,UAAA,KAAA,CAAM,GAAA,CAAI,CAAC,EAAE,GAAA,EAAK,MAAM,KAAA,EAAO,IAAA,EAAM,MAAK,KAAM;AAC/C,YAAA,MAAM,MAAA,GAAS,SAAS,IAAI,CAAA;AAC5B,YAAA,uBACEA,gBAAC,aAAA,EAAA,EAAwB,IAAA,EAAY,WAAU,SAAA,EAAU,cAAA,EAAc,MAAA,GAAS,MAAA,GAAS,MAAA,EACtF,QAAA,EAAA;AAAA,cAAA,MAAA,oBACCC,cAAAA;AAAA,gBAACK,mBAAAA,CAAO,IAAA;AAAA,gBAAP;AAAA,kBACC,QAAA,EAAS,UAAA;AAAA,kBACT,YAAY,EAAE,IAAA,EAAM,UAAU,SAAA,EAAW,GAAA,EAAK,SAAS,EAAA,EAAG;AAAA,kBAC1D,SAAA,EAAU;AAAA;AAAA,eACZ;AAAA,8BAEFL,cAAAA,CAAC,IAAA,EAAA,EAAK,SAAA,EAAW,MAAA,GAAS,8BAA8B,UAAA,EAAY,CAAA;AAAA,8BACpEA,cAAAA,CAAC,MAAA,EAAA,EAAK,WAAW,MAAA,GAAS,6BAAA,GAAgC,aAAc,QAAA,EAAA,KAAA,EAAM;AAAA,aAAA,EAAA,EAT5D,GAUpB,CAAA;AAAA,UAEJ,CAAC,CAAA;AAAA,UAEA,OAAA,oBACCD,eAAAA,CAAC,QAAA,EAAA,EAAO,MAAK,QAAA,EAAS,OAAA,EAAS,MAAM,WAAA,CAAY,IAAI,CAAA,EAAG,YAAA,EAAY,SAAA,EAAW,WAAU,sBAAA,EACtF,QAAA,EAAA;AAAA,YAAA,UAAA,oBAAcC,cAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,UAAA,EAAW,CAAA;AAAA,4BAC1CA,cAAAA,CAAC,kBAAA,EAAA,EAAmB,SAAA,EAAW,UAAA,GAAa,8BAA8B,UAAA,EAAY,CAAA;AAAA,4BACtFA,cAAAA,CAAC,MAAA,EAAA,EAAK,WAAW,UAAA,GAAa,6BAAA,GAAgC,aAAc,QAAA,EAAA,SAAA,EAAU;AAAA,WAAA,EACxF;AAAA,SAAA,EAEJ;AAAA;AAAA,KACF;AAAA,IAEC,OAAA,oBACCD,eAAAA,CAAC,UAAA,EAAA,EAAW,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,MAAM,WAAA,CAAY,KAAK,CAAA,EAAG,KAAA,EAAO,gBAAgB,UAAA,EACpF,QAAA,EAAA;AAAA,sBAAAC,cAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,eAAA,EACZ,QAAA,EAAA,SAAA,CAAU,GAAA,CAAI,CAAC,EAAE,GAAA,EAAK,IAAA,EAAM,KAAA,EAAO,IAAA,EAAM,MAAK,KAAM;AACnD,QAAA,MAAM,MAAA,GAAS,SAAS,IAAI,CAAA;AAC5B,QAAA,uBACED,eAAAA,CAAC,aAAA,EAAA,EAAwB,IAAA,EAAY,SAAA,EAAU,iBAAgB,OAAA,EAAS,MAAM,WAAA,CAAY,KAAK,CAAA,EAC7F,QAAA,EAAA;AAAA,0BAAAC,cAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAW,MAAA,GAAS,mDAAA,GAAsD,sBAAA,EAC9E,QAAA,kBAAAA,cAAAA,CAAC,IAAA,EAAA,EAAK,SAAA,EAAU,eAAA,EAAgB,CAAA,EAClC,CAAA;AAAA,0BACAA,cAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,kBAAkB,QAAA,EAAA,KAAA,EAAM;AAAA,SAAA,EAAA,EAJtB,GAKpB,CAAA;AAAA,MAEJ,CAAC,CAAA,EACH,CAAA;AAAA,MACC;AAAA,KAAA,EACH;AAAA,GAAA,EAEJ,CAAA;AAEJ","file":"index.cjs","sourcesContent":["import type { SVGProps } from \"react\";\n\n/** Tiny built-in icons so the package has zero icon-library dependency. */\n\nexport function MoreHorizontalIcon(props: SVGProps<SVGSVGElement>) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n width=\"24\"\n height=\"24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n {...props}\n >\n <circle cx=\"12\" cy=\"12\" r=\"1\" />\n <circle cx=\"19\" cy=\"12\" r=\"1\" />\n <circle cx=\"5\" cy=\"12\" r=\"1\" />\n </svg>\n );\n}\n\nexport function CloseIcon(props: SVGProps<SVGSVGElement>) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n width=\"24\"\n height=\"24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n {...props}\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n );\n}\n","\"use client\";\n\nimport { useEffect, useState, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { CloseIcon } from \"./icons\";\n\nexport interface DockDrawerProps {\n open: boolean;\n onClose: () => void;\n title: string;\n children: ReactNode;\n /** aria-label for the close button. @default \"Close\" */\n closeLabel?: string;\n}\n\n/**\n * A bottom sheet on narrow viewports, a centered modal from `sm` up. Used by {@link FloatingDock}\n * for its overflow menu, but works standalone for any mobile-style sheet.\n */\nexport function DockDrawer({ open, onClose, title, children, closeLabel = \"Close\" }: DockDrawerProps) {\n // Portals need a DOM node, which doesn't exist during SSR — render nothing until mounted client-side.\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n\n useEffect(() => {\n if (!open) return;\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, onClose]);\n\n if (!mounted) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <div className=\"rfd-overlay-wrap\">\n <motion.div\n className=\"rfd-overlay\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n onClick={onClose}\n />\n <motion.div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n className=\"rfd-sheet\"\n initial={{ y: \"100%\" }}\n animate={{ y: 0 }}\n exit={{ y: \"100%\" }}\n transition={{ type: \"spring\", damping: 28, stiffness: 300 }}\n >\n <div className=\"rfd-sheet-header\">\n <h2 className=\"rfd-sheet-title\">{title}</h2>\n <button type=\"button\" onClick={onClose} aria-label={closeLabel} className=\"rfd-close-btn\">\n <CloseIcon className=\"rfd-close-icon\" />\n </button>\n </div>\n <div className=\"rfd-sheet-body\">{children}</div>\n </motion.div>\n </div>\n )}\n </AnimatePresence>,\n document.body,\n );\n}\n","\"use client\";\n\nimport { useState, type ComponentType, type ReactNode } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { DockDrawer } from \"./DockDrawer\";\nimport { MoreHorizontalIcon } from \"./icons\";\n\nexport interface DockItem {\n /** Unique React key. Usually the same as `href`. */\n key: string;\n href: string;\n label: string;\n icon: ComponentType<{ className?: string }>;\n}\n\nexport interface DockLinkProps {\n href: string;\n className?: string;\n children?: ReactNode;\n onClick?: () => void;\n \"aria-current\"?: \"page\";\n}\n\nexport interface FloatingDockProps {\n /** Destinations shown directly in the bar. Four or five is the sweet spot. */\n items: DockItem[];\n /** Extra destinations shown in the overflow sheet when the \"More\" button is pressed. */\n moreItems?: DockItem[];\n /** Tells the dock which item (if any) is the current page. */\n isActive: (href: string) => boolean;\n /**\n * The component used to render each navigable item — swap in your router's Link\n * (e.g. `next/link`, `react-router`'s `Link`) for client-side navigation.\n * @default a plain anchor tag\n */\n LinkComponent?: ComponentType<DockLinkProps>;\n /** @default \"More\" */\n moreLabel?: string;\n /** @default \"Menu\" */\n moreSheetTitle?: string;\n /** Passed through to the overflow sheet's close button. @default \"Close\" */\n closeLabel?: string;\n /** Rendered at the bottom of the overflow sheet — a natural slot for a theme toggle, sign-out, etc. */\n extraContent?: ReactNode;\n /** @default \"Main\" */\n ariaLabel?: string;\n className?: string;\n}\n\nconst DefaultLink: ComponentType<DockLinkProps> = ({ href, children, ...rest }) => (\n <a href={href} {...rest}>\n {children}\n </a>\n);\n\n/**\n * A floating, fully-rounded bottom navigation bar in the style of WhatsApp's mobile tab bar: an\n * animated pill highlights the active tab, and a \"More\" button opens the rest of your destinations\n * in a bottom sheet. Framework-agnostic — bring your own router via `LinkComponent`.\n */\nexport function FloatingDock({\n items,\n moreItems = [],\n isActive,\n LinkComponent = DefaultLink,\n moreLabel = \"More\",\n moreSheetTitle = \"Menu\",\n closeLabel = \"Close\",\n extraContent,\n ariaLabel = \"Main\",\n className,\n}: FloatingDockProps) {\n const [moreOpen, setMoreOpen] = useState(false);\n const hasMore = moreItems.length > 0;\n const moreActive = hasMore && moreItems.some((item) => isActive(item.href));\n\n return (\n <>\n <nav\n aria-label={ariaLabel}\n className={[\"rfd-bar\", className].filter(Boolean).join(\" \")}\n style={{ bottom: \"max(0.75rem, env(safe-area-inset-bottom))\" }}\n >\n <div className=\"rfd-track\">\n {items.map(({ key, href, label, icon: Icon }) => {\n const active = isActive(href);\n return (\n <LinkComponent key={key} href={href} className=\"rfd-tab\" aria-current={active ? \"page\" : undefined}>\n {active && (\n <motion.span\n layoutId=\"rfd-pill\"\n transition={{ type: \"spring\", stiffness: 260, damping: 26 }}\n className=\"rfd-pill\"\n />\n )}\n <Icon className={active ? \"rfd-icon rfd-icon--active\" : \"rfd-icon\"} />\n <span className={active ? \"rfd-label rfd-label--active\" : \"rfd-label\"}>{label}</span>\n </LinkComponent>\n );\n })}\n\n {hasMore && (\n <button type=\"button\" onClick={() => setMoreOpen(true)} aria-label={moreLabel} className=\"rfd-tab rfd-more-btn\">\n {moreActive && <span className=\"rfd-pill\" />}\n <MoreHorizontalIcon className={moreActive ? \"rfd-icon rfd-icon--active\" : \"rfd-icon\"} />\n <span className={moreActive ? \"rfd-label rfd-label--active\" : \"rfd-label\"}>{moreLabel}</span>\n </button>\n )}\n </div>\n </nav>\n\n {hasMore && (\n <DockDrawer open={moreOpen} onClose={() => setMoreOpen(false)} title={moreSheetTitle} closeLabel={closeLabel}>\n <div className=\"rfd-more-grid\">\n {moreItems.map(({ key, href, label, icon: Icon }) => {\n const active = isActive(href);\n return (\n <LinkComponent key={key} href={href} className=\"rfd-more-item\" onClick={() => setMoreOpen(false)}>\n <span className={active ? \"rfd-more-icon-circle rfd-more-icon-circle--active\" : \"rfd-more-icon-circle\"}>\n <Icon className=\"rfd-more-icon\" />\n </span>\n <span className=\"rfd-more-label\">{label}</span>\n </LinkComponent>\n );\n })}\n </div>\n {extraContent}\n </DockDrawer>\n )}\n </>\n );\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentType, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface DockItem {
|
|
5
|
+
/** Unique React key. Usually the same as `href`. */
|
|
6
|
+
key: string;
|
|
7
|
+
href: string;
|
|
8
|
+
label: string;
|
|
9
|
+
icon: ComponentType<{
|
|
10
|
+
className?: string;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
interface DockLinkProps {
|
|
14
|
+
href: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
onClick?: () => void;
|
|
18
|
+
"aria-current"?: "page";
|
|
19
|
+
}
|
|
20
|
+
interface FloatingDockProps {
|
|
21
|
+
/** Destinations shown directly in the bar. Four or five is the sweet spot. */
|
|
22
|
+
items: DockItem[];
|
|
23
|
+
/** Extra destinations shown in the overflow sheet when the "More" button is pressed. */
|
|
24
|
+
moreItems?: DockItem[];
|
|
25
|
+
/** Tells the dock which item (if any) is the current page. */
|
|
26
|
+
isActive: (href: string) => boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The component used to render each navigable item — swap in your router's Link
|
|
29
|
+
* (e.g. `next/link`, `react-router`'s `Link`) for client-side navigation.
|
|
30
|
+
* @default a plain anchor tag
|
|
31
|
+
*/
|
|
32
|
+
LinkComponent?: ComponentType<DockLinkProps>;
|
|
33
|
+
/** @default "More" */
|
|
34
|
+
moreLabel?: string;
|
|
35
|
+
/** @default "Menu" */
|
|
36
|
+
moreSheetTitle?: string;
|
|
37
|
+
/** Passed through to the overflow sheet's close button. @default "Close" */
|
|
38
|
+
closeLabel?: string;
|
|
39
|
+
/** Rendered at the bottom of the overflow sheet — a natural slot for a theme toggle, sign-out, etc. */
|
|
40
|
+
extraContent?: ReactNode;
|
|
41
|
+
/** @default "Main" */
|
|
42
|
+
ariaLabel?: string;
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A floating, fully-rounded bottom navigation bar in the style of WhatsApp's mobile tab bar: an
|
|
47
|
+
* animated pill highlights the active tab, and a "More" button opens the rest of your destinations
|
|
48
|
+
* in a bottom sheet. Framework-agnostic — bring your own router via `LinkComponent`.
|
|
49
|
+
*/
|
|
50
|
+
declare function FloatingDock({ items, moreItems, isActive, LinkComponent, moreLabel, moreSheetTitle, closeLabel, extraContent, ariaLabel, className, }: FloatingDockProps): react.JSX.Element;
|
|
51
|
+
|
|
52
|
+
interface DockDrawerProps {
|
|
53
|
+
open: boolean;
|
|
54
|
+
onClose: () => void;
|
|
55
|
+
title: string;
|
|
56
|
+
children: ReactNode;
|
|
57
|
+
/** aria-label for the close button. @default "Close" */
|
|
58
|
+
closeLabel?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A bottom sheet on narrow viewports, a centered modal from `sm` up. Used by {@link FloatingDock}
|
|
62
|
+
* for its overflow menu, but works standalone for any mobile-style sheet.
|
|
63
|
+
*/
|
|
64
|
+
declare function DockDrawer({ open, onClose, title, children, closeLabel }: DockDrawerProps): react.ReactPortal | null;
|
|
65
|
+
|
|
66
|
+
export { DockDrawer, FloatingDock };
|
|
67
|
+
export type { DockDrawerProps, DockItem, DockLinkProps, FloatingDockProps };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentType, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface DockItem {
|
|
5
|
+
/** Unique React key. Usually the same as `href`. */
|
|
6
|
+
key: string;
|
|
7
|
+
href: string;
|
|
8
|
+
label: string;
|
|
9
|
+
icon: ComponentType<{
|
|
10
|
+
className?: string;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
interface DockLinkProps {
|
|
14
|
+
href: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
onClick?: () => void;
|
|
18
|
+
"aria-current"?: "page";
|
|
19
|
+
}
|
|
20
|
+
interface FloatingDockProps {
|
|
21
|
+
/** Destinations shown directly in the bar. Four or five is the sweet spot. */
|
|
22
|
+
items: DockItem[];
|
|
23
|
+
/** Extra destinations shown in the overflow sheet when the "More" button is pressed. */
|
|
24
|
+
moreItems?: DockItem[];
|
|
25
|
+
/** Tells the dock which item (if any) is the current page. */
|
|
26
|
+
isActive: (href: string) => boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The component used to render each navigable item — swap in your router's Link
|
|
29
|
+
* (e.g. `next/link`, `react-router`'s `Link`) for client-side navigation.
|
|
30
|
+
* @default a plain anchor tag
|
|
31
|
+
*/
|
|
32
|
+
LinkComponent?: ComponentType<DockLinkProps>;
|
|
33
|
+
/** @default "More" */
|
|
34
|
+
moreLabel?: string;
|
|
35
|
+
/** @default "Menu" */
|
|
36
|
+
moreSheetTitle?: string;
|
|
37
|
+
/** Passed through to the overflow sheet's close button. @default "Close" */
|
|
38
|
+
closeLabel?: string;
|
|
39
|
+
/** Rendered at the bottom of the overflow sheet — a natural slot for a theme toggle, sign-out, etc. */
|
|
40
|
+
extraContent?: ReactNode;
|
|
41
|
+
/** @default "Main" */
|
|
42
|
+
ariaLabel?: string;
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A floating, fully-rounded bottom navigation bar in the style of WhatsApp's mobile tab bar: an
|
|
47
|
+
* animated pill highlights the active tab, and a "More" button opens the rest of your destinations
|
|
48
|
+
* in a bottom sheet. Framework-agnostic — bring your own router via `LinkComponent`.
|
|
49
|
+
*/
|
|
50
|
+
declare function FloatingDock({ items, moreItems, isActive, LinkComponent, moreLabel, moreSheetTitle, closeLabel, extraContent, ariaLabel, className, }: FloatingDockProps): react.JSX.Element;
|
|
51
|
+
|
|
52
|
+
interface DockDrawerProps {
|
|
53
|
+
open: boolean;
|
|
54
|
+
onClose: () => void;
|
|
55
|
+
title: string;
|
|
56
|
+
children: ReactNode;
|
|
57
|
+
/** aria-label for the close button. @default "Close" */
|
|
58
|
+
closeLabel?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A bottom sheet on narrow viewports, a centered modal from `sm` up. Used by {@link FloatingDock}
|
|
62
|
+
* for its overflow menu, but works standalone for any mobile-style sheet.
|
|
63
|
+
*/
|
|
64
|
+
declare function DockDrawer({ open, onClose, title, children, closeLabel }: DockDrawerProps): react.ReactPortal | null;
|
|
65
|
+
|
|
66
|
+
export { DockDrawer, FloatingDock };
|
|
67
|
+
export type { DockDrawerProps, DockItem, DockLinkProps, FloatingDockProps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/FloatingDock.tsx
|
|
7
|
+
function MoreHorizontalIcon(props) {
|
|
8
|
+
return /* @__PURE__ */ jsxs(
|
|
9
|
+
"svg",
|
|
10
|
+
{
|
|
11
|
+
viewBox: "0 0 24 24",
|
|
12
|
+
width: "24",
|
|
13
|
+
height: "24",
|
|
14
|
+
fill: "none",
|
|
15
|
+
stroke: "currentColor",
|
|
16
|
+
strokeWidth: "2",
|
|
17
|
+
strokeLinecap: "round",
|
|
18
|
+
strokeLinejoin: "round",
|
|
19
|
+
"aria-hidden": "true",
|
|
20
|
+
...props,
|
|
21
|
+
children: [
|
|
22
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1" }),
|
|
23
|
+
/* @__PURE__ */ jsx("circle", { cx: "19", cy: "12", r: "1" }),
|
|
24
|
+
/* @__PURE__ */ jsx("circle", { cx: "5", cy: "12", r: "1" })
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
function CloseIcon(props) {
|
|
30
|
+
return /* @__PURE__ */ jsxs(
|
|
31
|
+
"svg",
|
|
32
|
+
{
|
|
33
|
+
viewBox: "0 0 24 24",
|
|
34
|
+
width: "24",
|
|
35
|
+
height: "24",
|
|
36
|
+
fill: "none",
|
|
37
|
+
stroke: "currentColor",
|
|
38
|
+
strokeWidth: "2",
|
|
39
|
+
strokeLinecap: "round",
|
|
40
|
+
strokeLinejoin: "round",
|
|
41
|
+
"aria-hidden": "true",
|
|
42
|
+
...props,
|
|
43
|
+
children: [
|
|
44
|
+
/* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
|
|
45
|
+
/* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
function DockDrawer({ open, onClose, title, children, closeLabel = "Close" }) {
|
|
51
|
+
const [mounted, setMounted] = useState(false);
|
|
52
|
+
useEffect(() => setMounted(true), []);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!open) return;
|
|
55
|
+
const previousOverflow = document.body.style.overflow;
|
|
56
|
+
document.body.style.overflow = "hidden";
|
|
57
|
+
return () => {
|
|
58
|
+
document.body.style.overflow = previousOverflow;
|
|
59
|
+
};
|
|
60
|
+
}, [open]);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!open) return;
|
|
63
|
+
const onKey = (event) => {
|
|
64
|
+
if (event.key === "Escape") onClose();
|
|
65
|
+
};
|
|
66
|
+
document.addEventListener("keydown", onKey);
|
|
67
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
68
|
+
}, [open, onClose]);
|
|
69
|
+
if (!mounted) return null;
|
|
70
|
+
return createPortal(
|
|
71
|
+
/* @__PURE__ */ jsx(AnimatePresence, { children: open && /* @__PURE__ */ jsxs("div", { className: "rfd-overlay-wrap", children: [
|
|
72
|
+
/* @__PURE__ */ jsx(
|
|
73
|
+
motion.div,
|
|
74
|
+
{
|
|
75
|
+
className: "rfd-overlay",
|
|
76
|
+
initial: { opacity: 0 },
|
|
77
|
+
animate: { opacity: 1 },
|
|
78
|
+
exit: { opacity: 0 },
|
|
79
|
+
onClick: onClose
|
|
80
|
+
}
|
|
81
|
+
),
|
|
82
|
+
/* @__PURE__ */ jsxs(
|
|
83
|
+
motion.div,
|
|
84
|
+
{
|
|
85
|
+
role: "dialog",
|
|
86
|
+
"aria-modal": "true",
|
|
87
|
+
"aria-label": title,
|
|
88
|
+
className: "rfd-sheet",
|
|
89
|
+
initial: { y: "100%" },
|
|
90
|
+
animate: { y: 0 },
|
|
91
|
+
exit: { y: "100%" },
|
|
92
|
+
transition: { type: "spring", damping: 28, stiffness: 300 },
|
|
93
|
+
children: [
|
|
94
|
+
/* @__PURE__ */ jsxs("div", { className: "rfd-sheet-header", children: [
|
|
95
|
+
/* @__PURE__ */ jsx("h2", { className: "rfd-sheet-title", children: title }),
|
|
96
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: onClose, "aria-label": closeLabel, className: "rfd-close-btn", children: /* @__PURE__ */ jsx(CloseIcon, { className: "rfd-close-icon" }) })
|
|
97
|
+
] }),
|
|
98
|
+
/* @__PURE__ */ jsx("div", { className: "rfd-sheet-body", children })
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
] }) }),
|
|
103
|
+
document.body
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
var DefaultLink = ({ href, children, ...rest }) => /* @__PURE__ */ jsx("a", { href, ...rest, children });
|
|
107
|
+
function FloatingDock({
|
|
108
|
+
items,
|
|
109
|
+
moreItems = [],
|
|
110
|
+
isActive,
|
|
111
|
+
LinkComponent = DefaultLink,
|
|
112
|
+
moreLabel = "More",
|
|
113
|
+
moreSheetTitle = "Menu",
|
|
114
|
+
closeLabel = "Close",
|
|
115
|
+
extraContent,
|
|
116
|
+
ariaLabel = "Main",
|
|
117
|
+
className
|
|
118
|
+
}) {
|
|
119
|
+
const [moreOpen, setMoreOpen] = useState(false);
|
|
120
|
+
const hasMore = moreItems.length > 0;
|
|
121
|
+
const moreActive = hasMore && moreItems.some((item) => isActive(item.href));
|
|
122
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
123
|
+
/* @__PURE__ */ jsx(
|
|
124
|
+
"nav",
|
|
125
|
+
{
|
|
126
|
+
"aria-label": ariaLabel,
|
|
127
|
+
className: ["rfd-bar", className].filter(Boolean).join(" "),
|
|
128
|
+
style: { bottom: "max(0.75rem, env(safe-area-inset-bottom))" },
|
|
129
|
+
children: /* @__PURE__ */ jsxs("div", { className: "rfd-track", children: [
|
|
130
|
+
items.map(({ key, href, label, icon: Icon }) => {
|
|
131
|
+
const active = isActive(href);
|
|
132
|
+
return /* @__PURE__ */ jsxs(LinkComponent, { href, className: "rfd-tab", "aria-current": active ? "page" : void 0, children: [
|
|
133
|
+
active && /* @__PURE__ */ jsx(
|
|
134
|
+
motion.span,
|
|
135
|
+
{
|
|
136
|
+
layoutId: "rfd-pill",
|
|
137
|
+
transition: { type: "spring", stiffness: 260, damping: 26 },
|
|
138
|
+
className: "rfd-pill"
|
|
139
|
+
}
|
|
140
|
+
),
|
|
141
|
+
/* @__PURE__ */ jsx(Icon, { className: active ? "rfd-icon rfd-icon--active" : "rfd-icon" }),
|
|
142
|
+
/* @__PURE__ */ jsx("span", { className: active ? "rfd-label rfd-label--active" : "rfd-label", children: label })
|
|
143
|
+
] }, key);
|
|
144
|
+
}),
|
|
145
|
+
hasMore && /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => setMoreOpen(true), "aria-label": moreLabel, className: "rfd-tab rfd-more-btn", children: [
|
|
146
|
+
moreActive && /* @__PURE__ */ jsx("span", { className: "rfd-pill" }),
|
|
147
|
+
/* @__PURE__ */ jsx(MoreHorizontalIcon, { className: moreActive ? "rfd-icon rfd-icon--active" : "rfd-icon" }),
|
|
148
|
+
/* @__PURE__ */ jsx("span", { className: moreActive ? "rfd-label rfd-label--active" : "rfd-label", children: moreLabel })
|
|
149
|
+
] })
|
|
150
|
+
] })
|
|
151
|
+
}
|
|
152
|
+
),
|
|
153
|
+
hasMore && /* @__PURE__ */ jsxs(DockDrawer, { open: moreOpen, onClose: () => setMoreOpen(false), title: moreSheetTitle, closeLabel, children: [
|
|
154
|
+
/* @__PURE__ */ jsx("div", { className: "rfd-more-grid", children: moreItems.map(({ key, href, label, icon: Icon }) => {
|
|
155
|
+
const active = isActive(href);
|
|
156
|
+
return /* @__PURE__ */ jsxs(LinkComponent, { href, className: "rfd-more-item", onClick: () => setMoreOpen(false), children: [
|
|
157
|
+
/* @__PURE__ */ jsx("span", { className: active ? "rfd-more-icon-circle rfd-more-icon-circle--active" : "rfd-more-icon-circle", children: /* @__PURE__ */ jsx(Icon, { className: "rfd-more-icon" }) }),
|
|
158
|
+
/* @__PURE__ */ jsx("span", { className: "rfd-more-label", children: label })
|
|
159
|
+
] }, key);
|
|
160
|
+
}) }),
|
|
161
|
+
extraContent
|
|
162
|
+
] })
|
|
163
|
+
] });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export { DockDrawer, FloatingDock };
|
|
167
|
+
//# sourceMappingURL=index.js.map
|
|
168
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/icons.tsx","../src/DockDrawer.tsx","../src/FloatingDock.tsx"],"names":["jsx","jsxs","useState","motion"],"mappings":"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","file":"index.js","sourcesContent":["import type { SVGProps } from \"react\";\n\n/** Tiny built-in icons so the package has zero icon-library dependency. */\n\nexport function MoreHorizontalIcon(props: SVGProps<SVGSVGElement>) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n width=\"24\"\n height=\"24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n {...props}\n >\n <circle cx=\"12\" cy=\"12\" r=\"1\" />\n <circle cx=\"19\" cy=\"12\" r=\"1\" />\n <circle cx=\"5\" cy=\"12\" r=\"1\" />\n </svg>\n );\n}\n\nexport function CloseIcon(props: SVGProps<SVGSVGElement>) {\n return (\n <svg\n viewBox=\"0 0 24 24\"\n width=\"24\"\n height=\"24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n {...props}\n >\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </svg>\n );\n}\n","\"use client\";\n\nimport { useEffect, useState, type ReactNode } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { CloseIcon } from \"./icons\";\n\nexport interface DockDrawerProps {\n open: boolean;\n onClose: () => void;\n title: string;\n children: ReactNode;\n /** aria-label for the close button. @default \"Close\" */\n closeLabel?: string;\n}\n\n/**\n * A bottom sheet on narrow viewports, a centered modal from `sm` up. Used by {@link FloatingDock}\n * for its overflow menu, but works standalone for any mobile-style sheet.\n */\nexport function DockDrawer({ open, onClose, title, children, closeLabel = \"Close\" }: DockDrawerProps) {\n // Portals need a DOM node, which doesn't exist during SSR — render nothing until mounted client-side.\n const [mounted, setMounted] = useState(false);\n useEffect(() => setMounted(true), []);\n\n useEffect(() => {\n if (!open) return;\n const previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = previousOverflow;\n };\n }, [open]);\n\n useEffect(() => {\n if (!open) return;\n const onKey = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [open, onClose]);\n\n if (!mounted) return null;\n\n return createPortal(\n <AnimatePresence>\n {open && (\n <div className=\"rfd-overlay-wrap\">\n <motion.div\n className=\"rfd-overlay\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n onClick={onClose}\n />\n <motion.div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n className=\"rfd-sheet\"\n initial={{ y: \"100%\" }}\n animate={{ y: 0 }}\n exit={{ y: \"100%\" }}\n transition={{ type: \"spring\", damping: 28, stiffness: 300 }}\n >\n <div className=\"rfd-sheet-header\">\n <h2 className=\"rfd-sheet-title\">{title}</h2>\n <button type=\"button\" onClick={onClose} aria-label={closeLabel} className=\"rfd-close-btn\">\n <CloseIcon className=\"rfd-close-icon\" />\n </button>\n </div>\n <div className=\"rfd-sheet-body\">{children}</div>\n </motion.div>\n </div>\n )}\n </AnimatePresence>,\n document.body,\n );\n}\n","\"use client\";\n\nimport { useState, type ComponentType, type ReactNode } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { DockDrawer } from \"./DockDrawer\";\nimport { MoreHorizontalIcon } from \"./icons\";\n\nexport interface DockItem {\n /** Unique React key. Usually the same as `href`. */\n key: string;\n href: string;\n label: string;\n icon: ComponentType<{ className?: string }>;\n}\n\nexport interface DockLinkProps {\n href: string;\n className?: string;\n children?: ReactNode;\n onClick?: () => void;\n \"aria-current\"?: \"page\";\n}\n\nexport interface FloatingDockProps {\n /** Destinations shown directly in the bar. Four or five is the sweet spot. */\n items: DockItem[];\n /** Extra destinations shown in the overflow sheet when the \"More\" button is pressed. */\n moreItems?: DockItem[];\n /** Tells the dock which item (if any) is the current page. */\n isActive: (href: string) => boolean;\n /**\n * The component used to render each navigable item — swap in your router's Link\n * (e.g. `next/link`, `react-router`'s `Link`) for client-side navigation.\n * @default a plain anchor tag\n */\n LinkComponent?: ComponentType<DockLinkProps>;\n /** @default \"More\" */\n moreLabel?: string;\n /** @default \"Menu\" */\n moreSheetTitle?: string;\n /** Passed through to the overflow sheet's close button. @default \"Close\" */\n closeLabel?: string;\n /** Rendered at the bottom of the overflow sheet — a natural slot for a theme toggle, sign-out, etc. */\n extraContent?: ReactNode;\n /** @default \"Main\" */\n ariaLabel?: string;\n className?: string;\n}\n\nconst DefaultLink: ComponentType<DockLinkProps> = ({ href, children, ...rest }) => (\n <a href={href} {...rest}>\n {children}\n </a>\n);\n\n/**\n * A floating, fully-rounded bottom navigation bar in the style of WhatsApp's mobile tab bar: an\n * animated pill highlights the active tab, and a \"More\" button opens the rest of your destinations\n * in a bottom sheet. Framework-agnostic — bring your own router via `LinkComponent`.\n */\nexport function FloatingDock({\n items,\n moreItems = [],\n isActive,\n LinkComponent = DefaultLink,\n moreLabel = \"More\",\n moreSheetTitle = \"Menu\",\n closeLabel = \"Close\",\n extraContent,\n ariaLabel = \"Main\",\n className,\n}: FloatingDockProps) {\n const [moreOpen, setMoreOpen] = useState(false);\n const hasMore = moreItems.length > 0;\n const moreActive = hasMore && moreItems.some((item) => isActive(item.href));\n\n return (\n <>\n <nav\n aria-label={ariaLabel}\n className={[\"rfd-bar\", className].filter(Boolean).join(\" \")}\n style={{ bottom: \"max(0.75rem, env(safe-area-inset-bottom))\" }}\n >\n <div className=\"rfd-track\">\n {items.map(({ key, href, label, icon: Icon }) => {\n const active = isActive(href);\n return (\n <LinkComponent key={key} href={href} className=\"rfd-tab\" aria-current={active ? \"page\" : undefined}>\n {active && (\n <motion.span\n layoutId=\"rfd-pill\"\n transition={{ type: \"spring\", stiffness: 260, damping: 26 }}\n className=\"rfd-pill\"\n />\n )}\n <Icon className={active ? \"rfd-icon rfd-icon--active\" : \"rfd-icon\"} />\n <span className={active ? \"rfd-label rfd-label--active\" : \"rfd-label\"}>{label}</span>\n </LinkComponent>\n );\n })}\n\n {hasMore && (\n <button type=\"button\" onClick={() => setMoreOpen(true)} aria-label={moreLabel} className=\"rfd-tab rfd-more-btn\">\n {moreActive && <span className=\"rfd-pill\" />}\n <MoreHorizontalIcon className={moreActive ? \"rfd-icon rfd-icon--active\" : \"rfd-icon\"} />\n <span className={moreActive ? \"rfd-label rfd-label--active\" : \"rfd-label\"}>{moreLabel}</span>\n </button>\n )}\n </div>\n </nav>\n\n {hasMore && (\n <DockDrawer open={moreOpen} onClose={() => setMoreOpen(false)} title={moreSheetTitle} closeLabel={closeLabel}>\n <div className=\"rfd-more-grid\">\n {moreItems.map(({ key, href, label, icon: Icon }) => {\n const active = isActive(href);\n return (\n <LinkComponent key={key} href={href} className=\"rfd-more-item\" onClick={() => setMoreOpen(false)}>\n <span className={active ? \"rfd-more-icon-circle rfd-more-icon-circle--active\" : \"rfd-more-icon-circle\"}>\n <Icon className=\"rfd-more-icon\" />\n </span>\n <span className=\"rfd-more-label\">{label}</span>\n </LinkComponent>\n );\n })}\n </div>\n {extraContent}\n </DockDrawer>\n )}\n </>\n );\n}\n"]}
|
package/dist/style.css
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* react-floating-dock default theme.
|
|
3
|
+
* Everything is driven by CSS custom properties — redefine any of these on `:root`
|
|
4
|
+
* (or scoped to a wrapper element) to theme the dock without touching these rules.
|
|
5
|
+
*/
|
|
6
|
+
:root {
|
|
7
|
+
--rfd-bg: rgba(15, 23, 42, 0.9);
|
|
8
|
+
--rfd-bg-solid: #0f172a;
|
|
9
|
+
--rfd-border: #1e293b;
|
|
10
|
+
--rfd-text: #e2e8f0;
|
|
11
|
+
--rfd-text-muted: #94a3b8;
|
|
12
|
+
--rfd-accent: #3b82f6;
|
|
13
|
+
--rfd-accent-bg: rgba(59, 130, 246, 0.15);
|
|
14
|
+
--rfd-accent-contrast: #ffffff;
|
|
15
|
+
--rfd-hover: rgba(255, 255, 255, 0.06);
|
|
16
|
+
--rfd-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.35);
|
|
17
|
+
--rfd-radius: 9999px;
|
|
18
|
+
--rfd-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.rfd-bar {
|
|
22
|
+
position: fixed;
|
|
23
|
+
left: 0.75rem;
|
|
24
|
+
right: 0.75rem;
|
|
25
|
+
z-index: 40;
|
|
26
|
+
font-family: var(--rfd-font);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.rfd-track {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
gap: 0.25rem;
|
|
34
|
+
border-radius: var(--rfd-radius);
|
|
35
|
+
border: 1px solid var(--rfd-border);
|
|
36
|
+
background: var(--rfd-bg);
|
|
37
|
+
backdrop-filter: blur(16px);
|
|
38
|
+
-webkit-backdrop-filter: blur(16px);
|
|
39
|
+
padding: 0.375rem;
|
|
40
|
+
box-shadow: var(--rfd-shadow);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.rfd-tab {
|
|
44
|
+
position: relative;
|
|
45
|
+
display: flex;
|
|
46
|
+
flex: 1 1 0%;
|
|
47
|
+
height: 3.5rem;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: center;
|
|
51
|
+
gap: 0.125rem;
|
|
52
|
+
border-radius: var(--rfd-radius);
|
|
53
|
+
background: transparent;
|
|
54
|
+
border: none;
|
|
55
|
+
padding: 0;
|
|
56
|
+
margin: 0;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
text-decoration: none;
|
|
59
|
+
color: inherit;
|
|
60
|
+
font: inherit;
|
|
61
|
+
-webkit-tap-highlight-color: transparent;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.rfd-pill {
|
|
65
|
+
position: absolute;
|
|
66
|
+
inset: 0;
|
|
67
|
+
border-radius: var(--rfd-radius);
|
|
68
|
+
background: var(--rfd-accent-bg);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.rfd-icon {
|
|
72
|
+
position: relative;
|
|
73
|
+
width: 1.25rem;
|
|
74
|
+
height: 1.25rem;
|
|
75
|
+
color: var(--rfd-text-muted);
|
|
76
|
+
flex-shrink: 0;
|
|
77
|
+
}
|
|
78
|
+
.rfd-icon--active {
|
|
79
|
+
color: var(--rfd-accent);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.rfd-label {
|
|
83
|
+
position: relative;
|
|
84
|
+
font-size: 11px;
|
|
85
|
+
line-height: 1;
|
|
86
|
+
font-weight: 500;
|
|
87
|
+
color: var(--rfd-text-muted);
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
}
|
|
90
|
+
.rfd-label--active {
|
|
91
|
+
color: var(--rfd-accent);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* --- Overflow sheet --- */
|
|
95
|
+
|
|
96
|
+
.rfd-overlay-wrap {
|
|
97
|
+
position: fixed;
|
|
98
|
+
inset: 0;
|
|
99
|
+
z-index: 50;
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: flex-end;
|
|
102
|
+
justify-content: center;
|
|
103
|
+
font-family: var(--rfd-font);
|
|
104
|
+
}
|
|
105
|
+
@media (min-width: 640px) {
|
|
106
|
+
.rfd-overlay-wrap {
|
|
107
|
+
align-items: center;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.rfd-overlay {
|
|
112
|
+
position: absolute;
|
|
113
|
+
inset: 0;
|
|
114
|
+
background: rgba(0, 0, 0, 0.6);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.rfd-sheet {
|
|
118
|
+
position: relative;
|
|
119
|
+
z-index: 10;
|
|
120
|
+
width: 100%;
|
|
121
|
+
max-width: 32rem;
|
|
122
|
+
border-radius: 1.5rem 1.5rem 0 0;
|
|
123
|
+
border: 1px solid var(--rfd-border);
|
|
124
|
+
background: var(--rfd-bg-solid);
|
|
125
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
126
|
+
}
|
|
127
|
+
@media (min-width: 640px) {
|
|
128
|
+
.rfd-sheet {
|
|
129
|
+
border-radius: 1.5rem;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.rfd-sheet-header {
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: space-between;
|
|
137
|
+
border-bottom: 1px solid var(--rfd-border);
|
|
138
|
+
padding: 1rem 1.25rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.rfd-sheet-title {
|
|
142
|
+
font-size: 1rem;
|
|
143
|
+
font-weight: 600;
|
|
144
|
+
color: var(--rfd-text);
|
|
145
|
+
margin: 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.rfd-close-btn {
|
|
149
|
+
display: flex;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: center;
|
|
152
|
+
width: 2.75rem;
|
|
153
|
+
height: 2.75rem;
|
|
154
|
+
border-radius: var(--rfd-radius);
|
|
155
|
+
border: none;
|
|
156
|
+
background: transparent;
|
|
157
|
+
color: var(--rfd-text-muted);
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
padding: 0;
|
|
160
|
+
-webkit-tap-highlight-color: transparent;
|
|
161
|
+
}
|
|
162
|
+
.rfd-close-btn:hover {
|
|
163
|
+
background: var(--rfd-hover);
|
|
164
|
+
color: var(--rfd-text);
|
|
165
|
+
}
|
|
166
|
+
.rfd-close-icon {
|
|
167
|
+
width: 1.25rem;
|
|
168
|
+
height: 1.25rem;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.rfd-sheet-body {
|
|
172
|
+
max-height: 75vh;
|
|
173
|
+
overflow-y: auto;
|
|
174
|
+
padding: 1.25rem;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.rfd-more-grid {
|
|
178
|
+
display: grid;
|
|
179
|
+
grid-template-columns: repeat(3, 1fr);
|
|
180
|
+
gap: 1rem;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.rfd-more-item {
|
|
184
|
+
display: flex;
|
|
185
|
+
flex-direction: column;
|
|
186
|
+
align-items: center;
|
|
187
|
+
gap: 0.5rem;
|
|
188
|
+
text-align: center;
|
|
189
|
+
text-decoration: none;
|
|
190
|
+
color: inherit;
|
|
191
|
+
background: none;
|
|
192
|
+
border: none;
|
|
193
|
+
padding: 0;
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
font: inherit;
|
|
196
|
+
-webkit-tap-highlight-color: transparent;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.rfd-more-icon-circle {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
justify-content: center;
|
|
203
|
+
width: 4rem;
|
|
204
|
+
height: 4rem;
|
|
205
|
+
border-radius: var(--rfd-radius);
|
|
206
|
+
background: var(--rfd-hover);
|
|
207
|
+
color: var(--rfd-text);
|
|
208
|
+
}
|
|
209
|
+
.rfd-more-icon-circle--active {
|
|
210
|
+
background: var(--rfd-accent);
|
|
211
|
+
color: var(--rfd-accent-contrast);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.rfd-more-icon {
|
|
215
|
+
width: 1.5rem;
|
|
216
|
+
height: 1.5rem;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.rfd-more-label {
|
|
220
|
+
font-size: 12px;
|
|
221
|
+
font-weight: 500;
|
|
222
|
+
color: var(--rfd-text);
|
|
223
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-floating-dock",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A WhatsApp-style floating bottom navigation dock for React, with an animated active-tab pill and an overflow sheet for extra destinations. Framework-agnostic — bring your own router.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./style.css": "./dist/style.css"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": [
|
|
21
|
+
"**/*.css"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup && node scripts/copy-css.mjs",
|
|
25
|
+
"dev": "tsup --watch",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "eslint .",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"example": "vite --config example/vite.config.ts",
|
|
31
|
+
"prepublishOnly": "pnpm typecheck && pnpm lint && pnpm test && pnpm build"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"react",
|
|
35
|
+
"navigation",
|
|
36
|
+
"mobile",
|
|
37
|
+
"bottom-nav",
|
|
38
|
+
"tab-bar",
|
|
39
|
+
"pwa",
|
|
40
|
+
"dock",
|
|
41
|
+
"bottom-sheet"
|
|
42
|
+
],
|
|
43
|
+
"author": "Lemon Oluwatobi",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"framer-motion": ">=11",
|
|
47
|
+
"react": ">=18",
|
|
48
|
+
"react-dom": ">=18"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@eslint/js": "^10.0.1",
|
|
52
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
53
|
+
"@testing-library/react": "^16.3.3",
|
|
54
|
+
"@testing-library/user-event": "^14.6.8",
|
|
55
|
+
"@types/react": "^19.3.0",
|
|
56
|
+
"@types/react-dom": "^19.3.0",
|
|
57
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
58
|
+
"eslint": "^10.13.0",
|
|
59
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
60
|
+
"framer-motion": "^14.3.0",
|
|
61
|
+
"globals": "^17.13.0",
|
|
62
|
+
"jsdom": "^30.1.3",
|
|
63
|
+
"lucide-react": "^1.59.0",
|
|
64
|
+
"react": "^19.3.1",
|
|
65
|
+
"react-dom": "^19.3.1",
|
|
66
|
+
"tsup": "^9.0.0",
|
|
67
|
+
"typescript": "^5.9.3",
|
|
68
|
+
"typescript-eslint": "^8.71.2",
|
|
69
|
+
"vite": "^8.3.4",
|
|
70
|
+
"vitest": "^5.0.5"
|
|
71
|
+
}
|
|
72
|
+
}
|