@medialane/ui 0.61.0 → 0.63.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/dist/components/nav-command-menu.cjs +166 -51
- package/dist/components/nav-command-menu.cjs.map +1 -1
- package/dist/components/nav-command-menu.d.cts +2 -0
- package/dist/components/nav-command-menu.d.ts +2 -0
- package/dist/components/nav-command-menu.js +166 -51
- package/dist/components/nav-command-menu.js.map +1 -1
- package/dist/components/nav-shell.cjs +168 -0
- package/dist/components/nav-shell.cjs.map +1 -0
- package/dist/components/nav-shell.d.cts +51 -0
- package/dist/components/nav-shell.d.ts +51 -0
- package/dist/components/nav-shell.js +131 -0
- package/dist/components/nav-shell.js.map +1 -0
- package/dist/index.cjs +9 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -14,12 +14,106 @@ function useNavCommandMenu() {
|
|
|
14
14
|
close: () => document.dispatchEvent(new CustomEvent(ML_NAV_CLOSE))
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
+
const RECENT_KEY = "ml.nav.recent";
|
|
18
|
+
const RECENT_MAX = 3;
|
|
19
|
+
function readRecents() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = localStorage.getItem(RECENT_KEY);
|
|
22
|
+
const ids = raw ? JSON.parse(raw) : [];
|
|
23
|
+
return Array.isArray(ids) ? ids.filter((x) => typeof x === "string") : [];
|
|
24
|
+
} catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function pushRecent(id) {
|
|
29
|
+
try {
|
|
30
|
+
const next = [id, ...readRecents().filter((x) => x !== id)].slice(0, RECENT_MAX);
|
|
31
|
+
localStorage.setItem(RECENT_KEY, JSON.stringify(next));
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function Kbd({ children, className }) {
|
|
36
|
+
return /* @__PURE__ */ jsx(
|
|
37
|
+
"kbd",
|
|
38
|
+
{
|
|
39
|
+
className: cn(
|
|
40
|
+
"inline-flex min-w-[18px] items-center justify-center rounded-md bg-muted/60 px-1.5 py-0.5",
|
|
41
|
+
"font-sans text-[10px] leading-none text-muted-foreground",
|
|
42
|
+
className
|
|
43
|
+
),
|
|
44
|
+
children
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
function CommandRow({ item, primary, onSelect }) {
|
|
49
|
+
return /* @__PURE__ */ jsxs(
|
|
50
|
+
Command.Item,
|
|
51
|
+
{
|
|
52
|
+
value: [item.label, ...item.keywords ?? []].join(" "),
|
|
53
|
+
onSelect,
|
|
54
|
+
className: cn(
|
|
55
|
+
"group/item flex cursor-pointer items-center gap-3 rounded-xl",
|
|
56
|
+
"transition-colors duration-150 aria-selected:bg-primary/10",
|
|
57
|
+
primary ? "px-2.5 py-2.5" : "px-2.5 py-2"
|
|
58
|
+
),
|
|
59
|
+
children: [
|
|
60
|
+
/* @__PURE__ */ jsx(
|
|
61
|
+
"span",
|
|
62
|
+
{
|
|
63
|
+
className: cn(
|
|
64
|
+
"flex shrink-0 items-center justify-center rounded-lg transition-colors",
|
|
65
|
+
"bg-muted/50 group-aria-selected/item:bg-primary/15",
|
|
66
|
+
primary ? "h-9 w-9" : "h-8 w-8"
|
|
67
|
+
),
|
|
68
|
+
children: /* @__PURE__ */ jsx(
|
|
69
|
+
item.icon,
|
|
70
|
+
{
|
|
71
|
+
className: cn(
|
|
72
|
+
"text-foreground/80 transition-colors group-aria-selected/item:text-primary",
|
|
73
|
+
primary ? "h-[18px] w-[18px]" : "h-4 w-4"
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
),
|
|
79
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
80
|
+
/* @__PURE__ */ jsx(
|
|
81
|
+
"span",
|
|
82
|
+
{
|
|
83
|
+
className: cn(
|
|
84
|
+
"block truncate leading-tight",
|
|
85
|
+
primary ? "text-[15px] font-medium" : "text-sm"
|
|
86
|
+
),
|
|
87
|
+
children: item.label
|
|
88
|
+
}
|
|
89
|
+
),
|
|
90
|
+
item.description && /* @__PURE__ */ jsx("span", { className: "mt-0.5 block truncate text-[11.5px] leading-tight text-muted-foreground", children: item.description })
|
|
91
|
+
] }),
|
|
92
|
+
/* @__PURE__ */ jsx(ArrowRight, { className: "h-3.5 w-3.5 shrink-0 text-muted-foreground/30 transition-colors group-aria-selected/item:text-primary/70" })
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const GROUP_HEADING_CLASSES = cn(
|
|
98
|
+
"[&_[cmdk-group-heading]]:px-2.5",
|
|
99
|
+
"[&_[cmdk-group-heading]]:pt-2",
|
|
100
|
+
"[&_[cmdk-group-heading]]:pb-1",
|
|
101
|
+
"[&_[cmdk-group-heading]]:text-[10.5px]",
|
|
102
|
+
"[&_[cmdk-group-heading]]:font-semibold",
|
|
103
|
+
"[&_[cmdk-group-heading]]:uppercase",
|
|
104
|
+
"[&_[cmdk-group-heading]]:tracking-[0.08em]",
|
|
105
|
+
"[&_[cmdk-group-heading]]:text-muted-foreground/60"
|
|
106
|
+
);
|
|
17
107
|
function NavCommandMenu({ commands, trigger, accountSlot, footerSlot }) {
|
|
18
108
|
const [open, setOpen] = React.useState(false);
|
|
109
|
+
const [query, setQuery] = React.useState("");
|
|
110
|
+
const [recentIds, setRecentIds] = React.useState([]);
|
|
19
111
|
const router = useRouter();
|
|
20
112
|
const inputRef = React.useRef(null);
|
|
21
113
|
React.useEffect(() => {
|
|
22
114
|
if (!open) return;
|
|
115
|
+
setQuery("");
|
|
116
|
+
setRecentIds(readRecents());
|
|
23
117
|
const t = setTimeout(() => inputRef.current?.focus(), 60);
|
|
24
118
|
return () => clearTimeout(t);
|
|
25
119
|
}, [open]);
|
|
@@ -44,12 +138,19 @@ function NavCommandMenu({ commands, trigger, accountSlot, footerSlot }) {
|
|
|
44
138
|
}, []);
|
|
45
139
|
const runCommand = React.useCallback(
|
|
46
140
|
(cmd) => {
|
|
141
|
+
pushRecent(cmd.id);
|
|
47
142
|
setOpen(false);
|
|
48
143
|
if (cmd.href) router.push(cmd.href);
|
|
49
144
|
else cmd.action?.();
|
|
50
145
|
},
|
|
51
146
|
[router]
|
|
52
147
|
);
|
|
148
|
+
const recentItems = React.useMemo(() => {
|
|
149
|
+
if (recentIds.length === 0) return [];
|
|
150
|
+
const all = new Map(commands.flatMap((g) => g.items.map((it) => [it.id, it])));
|
|
151
|
+
return recentIds.map((id) => all.get(id)).filter((it) => Boolean(it));
|
|
152
|
+
}, [recentIds, commands]);
|
|
153
|
+
const showRecents = query === "" && recentItems.length > 0;
|
|
53
154
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
54
155
|
trigger,
|
|
55
156
|
/* @__PURE__ */ jsx(AnimatePresence, { children: open && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -67,86 +168,100 @@ function NavCommandMenu({ commands, trigger, accountSlot, footerSlot }) {
|
|
|
67
168
|
/* @__PURE__ */ jsx(
|
|
68
169
|
motion.div,
|
|
69
170
|
{
|
|
70
|
-
className: "fixed inset-0 z-[101] flex items-
|
|
71
|
-
initial: { opacity: 0, scale:
|
|
72
|
-
animate: { opacity: 1, scale: 1 },
|
|
73
|
-
exit: { opacity: 0,
|
|
74
|
-
transition: { duration: 0.
|
|
171
|
+
className: "fixed inset-0 z-[101] flex items-end justify-center p-3 pb-4 sm:items-center sm:p-4",
|
|
172
|
+
initial: { opacity: 0, y: 24, scale: 1 },
|
|
173
|
+
animate: { opacity: 1, y: 0, scale: 1 },
|
|
174
|
+
exit: { opacity: 0, y: 24 },
|
|
175
|
+
transition: { duration: 0.18, ease: "easeOut" },
|
|
75
176
|
onClick: () => setOpen(false),
|
|
76
177
|
children: /* @__PURE__ */ jsx(
|
|
77
178
|
"div",
|
|
78
179
|
{
|
|
79
|
-
className:
|
|
180
|
+
className: cn(
|
|
181
|
+
"flex w-full max-h-[85dvh] flex-col overflow-hidden rounded-[20px] sm:max-h-[min(680px,88dvh)] sm:max-w-[620px]",
|
|
182
|
+
"border border-border/40 bg-background/90 shadow-2xl backdrop-blur-2xl backdrop-saturate-150"
|
|
183
|
+
),
|
|
80
184
|
onClick: (e) => e.stopPropagation(),
|
|
81
|
-
children: /* @__PURE__ */ jsxs(Command, { shouldFilter: true, label: "Medialane navigation", children: [
|
|
82
|
-
/* @__PURE__ */
|
|
83
|
-
|
|
185
|
+
children: /* @__PURE__ */ jsxs(Command, { shouldFilter: true, label: "Medialane navigation", className: "flex min-h-0 flex-1 flex-col", children: [
|
|
186
|
+
/* @__PURE__ */ jsx("div", { className: "flex justify-center pt-2.5 sm:hidden", "aria-hidden": "true", children: /* @__PURE__ */ jsx("span", { className: "h-1 w-9 rounded-full bg-muted-foreground/30" }) }),
|
|
187
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 border-b border-border/40 px-4 py-3.5", children: [
|
|
188
|
+
/* @__PURE__ */ jsx(
|
|
189
|
+
Search,
|
|
190
|
+
{
|
|
191
|
+
className: cn(
|
|
192
|
+
"h-[18px] w-[18px] shrink-0 transition-colors",
|
|
193
|
+
query ? "text-primary" : "text-muted-foreground"
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
),
|
|
84
197
|
/* @__PURE__ */ jsx(
|
|
85
198
|
Command.Input,
|
|
86
199
|
{
|
|
87
200
|
ref: inputRef,
|
|
88
|
-
|
|
201
|
+
value: query,
|
|
202
|
+
onValueChange: setQuery,
|
|
203
|
+
placeholder: "Search or run a command\u2026",
|
|
89
204
|
className: "flex-1 bg-transparent text-[15px] outline-none placeholder:text-muted-foreground"
|
|
90
205
|
}
|
|
91
206
|
),
|
|
207
|
+
/* @__PURE__ */ jsx(Kbd, { className: "hidden sm:inline-flex", children: "esc" }),
|
|
92
208
|
/* @__PURE__ */ jsx(
|
|
93
209
|
"button",
|
|
94
210
|
{
|
|
95
211
|
onClick: () => setOpen(false),
|
|
96
|
-
className: "p-1
|
|
212
|
+
className: "rounded-md p-1 transition-colors hover:bg-muted/50 sm:hidden",
|
|
97
213
|
"aria-label": "Close",
|
|
98
214
|
children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4 text-muted-foreground" })
|
|
99
215
|
}
|
|
100
216
|
)
|
|
101
217
|
] }),
|
|
102
|
-
/* @__PURE__ */ jsxs(Command.List, { className: "
|
|
218
|
+
/* @__PURE__ */ jsxs(Command.List, { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain p-2", children: [
|
|
103
219
|
/* @__PURE__ */ jsx(Command.Empty, { className: "py-8 text-center text-sm text-muted-foreground", children: "No results found." }),
|
|
220
|
+
showRecents && /* @__PURE__ */ jsx(Command.Group, { heading: "Recent", className: GROUP_HEADING_CLASSES, children: recentItems.map((item) => /* @__PURE__ */ jsx(
|
|
221
|
+
CommandRow,
|
|
222
|
+
{
|
|
223
|
+
item,
|
|
224
|
+
primary: false,
|
|
225
|
+
onSelect: () => runCommand(item)
|
|
226
|
+
},
|
|
227
|
+
`recent-${item.id}`
|
|
228
|
+
)) }),
|
|
104
229
|
commands.map((group, i) => {
|
|
105
230
|
const primary = !group.heading;
|
|
106
231
|
return /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
107
|
-
i > 0 && /* @__PURE__ */ jsx(Command.Separator, { className: "my-1.5 h-px bg-border/40" }),
|
|
108
|
-
/* @__PURE__ */ jsx(
|
|
109
|
-
|
|
232
|
+
(i > 0 || showRecents) && /* @__PURE__ */ jsx(Command.Separator, { className: "my-1.5 h-px bg-border/40" }),
|
|
233
|
+
/* @__PURE__ */ jsx(Command.Group, { heading: group.heading, className: GROUP_HEADING_CLASSES, children: group.items.map((item) => /* @__PURE__ */ jsx(
|
|
234
|
+
CommandRow,
|
|
110
235
|
{
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"[&_[cmdk-group-heading]]:font-semibold",
|
|
118
|
-
"[&_[cmdk-group-heading]]:uppercase",
|
|
119
|
-
"[&_[cmdk-group-heading]]:tracking-wider",
|
|
120
|
-
"[&_[cmdk-group-heading]]:text-muted-foreground/70"
|
|
121
|
-
),
|
|
122
|
-
children: group.items.map((item) => /* @__PURE__ */ jsxs(
|
|
123
|
-
Command.Item,
|
|
124
|
-
{
|
|
125
|
-
value: [item.label, ...item.keywords ?? []].join(" "),
|
|
126
|
-
onSelect: () => runCommand(item),
|
|
127
|
-
className: cn(
|
|
128
|
-
"group/item flex items-center gap-3 rounded-xl cursor-pointer",
|
|
129
|
-
"transition-colors duration-150",
|
|
130
|
-
"aria-selected:bg-primary/10",
|
|
131
|
-
primary ? "px-2.5 py-2.5 text-[15px] font-medium" : "px-3 py-2 text-sm"
|
|
132
|
-
),
|
|
133
|
-
children: [
|
|
134
|
-
primary ? /* @__PURE__ */ jsx("span", { className: "flex h-8 w-8 items-center justify-center rounded-lg bg-muted/50 group-aria-selected/item:bg-primary/15 transition-colors shrink-0", children: /* @__PURE__ */ jsx(item.icon, { className: "h-[18px] w-[18px] text-foreground/80 group-aria-selected/item:text-primary transition-colors" }) }) : /* @__PURE__ */ jsx(item.icon, { className: "h-4 w-4 text-muted-foreground group-aria-selected/item:text-foreground transition-colors shrink-0" }),
|
|
135
|
-
/* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: item.label }),
|
|
136
|
-
/* @__PURE__ */ jsx(ArrowRight, { className: "h-3.5 w-3.5 text-muted-foreground/30 group-aria-selected/item:text-primary/70 transition-colors shrink-0" })
|
|
137
|
-
]
|
|
138
|
-
},
|
|
139
|
-
item.id
|
|
140
|
-
))
|
|
141
|
-
}
|
|
142
|
-
)
|
|
236
|
+
item,
|
|
237
|
+
primary,
|
|
238
|
+
onSelect: () => runCommand(item)
|
|
239
|
+
},
|
|
240
|
+
item.id
|
|
241
|
+
)) })
|
|
143
242
|
] }, group.heading ?? `__primary-${i}`);
|
|
144
243
|
})
|
|
145
244
|
] }),
|
|
146
|
-
accountSlot && /* @__PURE__ */ jsx("div", { className: "border-t border-border/40 bg-
|
|
147
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
148
|
-
|
|
149
|
-
|
|
245
|
+
accountSlot && /* @__PURE__ */ jsx("div", { className: "border-t border-border/40 bg-muted/20 px-3 py-3", children: accountSlot }),
|
|
246
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 border-t border-border/40 bg-muted/20 px-3 py-2", children: [
|
|
247
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
248
|
+
footerSlot,
|
|
249
|
+
/* @__PURE__ */ jsxs("span", { className: "hidden items-center gap-3 text-[10.5px] text-muted-foreground/70 sm:flex", children: [
|
|
250
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
|
|
251
|
+
/* @__PURE__ */ jsx(Kbd, { children: "\u2191" }),
|
|
252
|
+
/* @__PURE__ */ jsx(Kbd, { children: "\u2193" }),
|
|
253
|
+
" Navigate"
|
|
254
|
+
] }),
|
|
255
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
|
|
256
|
+
/* @__PURE__ */ jsx(Kbd, { children: "\u21B5" }),
|
|
257
|
+
" Open"
|
|
258
|
+
] })
|
|
259
|
+
] })
|
|
260
|
+
] }),
|
|
261
|
+
/* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-2 text-[10px] text-muted-foreground/50", children: [
|
|
262
|
+
"medialane",
|
|
263
|
+
/* @__PURE__ */ jsx(Kbd, { className: "hidden sm:inline-flex", children: "\u2318K" })
|
|
264
|
+
] })
|
|
150
265
|
] })
|
|
151
266
|
] })
|
|
152
267
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/nav-command-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { Command } from \"cmdk\";\nimport { useRouter } from \"next/navigation\";\nimport { Search, X, ArrowRight } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { cn } from \"../utils/cn.js\";\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport interface NavCommand {\n id: string;\n label: string;\n icon: React.ComponentType<{ className?: string }>;\n href?: string;\n action?: () => void;\n /** Extra search terms beyond the label */\n keywords?: string[];\n}\n\nexport interface NavCommandGroup {\n /**\n * Optional group heading. Omit it for the primary group so its items\n * read as the top-level menu — they render first, emphasized, with no\n * label, separated from the headed groups below.\n */\n heading?: string;\n items: NavCommand[];\n}\n\nexport interface NavCommandMenuProps {\n commands: NavCommandGroup[];\n /**\n * Optional trigger element rendered inline at the component's mount point.\n * For most apps, omit this and call `useNavCommandMenu().open()` from a\n * separate button — that keeps the trigger in the right place in the layout.\n */\n trigger?: React.ReactNode;\n /**\n * Optional pinned account/connect area rendered below command results.\n * Apps own the auth implementation here (Clerk, ChipiPay, wallet connectors,\n * Privy, Cartridge, etc.) so the shared nav stays framework-agnostic.\n */\n accountSlot?: React.ReactNode;\n /**\n * Optional control rendered in the footer row (e.g. a theme toggle).\n * Apps own theme state (next-themes etc.) so the shared nav stays\n * framework-agnostic — same pattern as `accountSlot`.\n */\n footerSlot?: React.ReactNode;\n}\n\n// ── Singleton hook ─────────────────────────────────────────────────────────────\n\nconst ML_NAV_OPEN = \"ml:nav-open\";\nconst ML_NAV_CLOSE = \"ml:nav-close\";\n\nexport function useNavCommandMenu() {\n return {\n open: () => document.dispatchEvent(new CustomEvent(ML_NAV_OPEN)),\n close: () => document.dispatchEvent(new CustomEvent(ML_NAV_CLOSE)),\n };\n}\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport function NavCommandMenu({ commands, trigger, accountSlot, footerSlot }: NavCommandMenuProps) {\n const [open, setOpen] = React.useState(false);\n const router = useRouter();\n const inputRef = React.useRef<HTMLInputElement>(null);\n\n React.useEffect(() => {\n if (!open) return;\n const t = setTimeout(() => inputRef.current?.focus(), 60);\n return () => clearTimeout(t);\n }, [open]);\n\n React.useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"k\" && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n setOpen((prev) => !prev);\n }\n if (e.key === \"Escape\") setOpen(false);\n };\n const onOpen = () => setOpen(true);\n const onClose = () => setOpen(false);\n\n document.addEventListener(\"keydown\", onKey);\n document.addEventListener(ML_NAV_OPEN, onOpen);\n document.addEventListener(ML_NAV_CLOSE, onClose);\n return () => {\n document.removeEventListener(\"keydown\", onKey);\n document.removeEventListener(ML_NAV_OPEN, onOpen);\n document.removeEventListener(ML_NAV_CLOSE, onClose);\n };\n }, []);\n\n const runCommand = React.useCallback(\n (cmd: NavCommand) => {\n setOpen(false);\n if (cmd.href) router.push(cmd.href);\n else cmd.action?.();\n },\n [router]\n );\n\n return (\n <>\n {trigger}\n\n <AnimatePresence>\n {open && (\n <>\n {/* Backdrop blur */}\n <motion.div\n className=\"nav-canvas-overlay\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.15 }}\n onClick={() => setOpen(false)}\n />\n\n {/* Command panel */}\n <motion.div\n className=\"fixed inset-0 z-[101] flex items-center justify-center p-4\"\n initial={{ opacity: 0, scale: 0.97 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={{ opacity: 0, scale: 0.97 }}\n transition={{ duration: 0.15, ease: \"easeOut\" }}\n onClick={() => setOpen(false)}\n >\n <div\n className=\"w-full max-w-lg bg-background/90 border border-border/40 rounded-2xl overflow-hidden shadow-2xl\"\n onClick={(e) => e.stopPropagation()}\n >\n <Command shouldFilter label=\"Medialane navigation\">\n {/* Search bar */}\n <div className=\"flex items-center gap-3 px-4 py-3.5 border-b border-border/40\">\n <Search className=\"h-[18px] w-[18px] text-muted-foreground shrink-0\" />\n <Command.Input\n ref={inputRef}\n placeholder=\"Type a command or search…\"\n className=\"flex-1 bg-transparent text-[15px] outline-none placeholder:text-muted-foreground\"\n />\n <button\n onClick={() => setOpen(false)}\n className=\"p-1 rounded-md hover:bg-muted/50 transition-colors\"\n aria-label=\"Close\"\n >\n <X className=\"h-4 w-4 text-muted-foreground\" />\n </button>\n </div>\n\n {/* Results */}\n <Command.List className=\"max-h-[60vh] overflow-y-auto p-2\">\n <Command.Empty className=\"py-8 text-center text-sm text-muted-foreground\">\n No results found.\n </Command.Empty>\n\n {commands.map((group, i) => {\n const primary = !group.heading;\n return (\n <React.Fragment key={group.heading ?? `__primary-${i}`}>\n {i > 0 && (\n <Command.Separator className=\"my-1.5 h-px bg-border/40\" />\n )}\n <Command.Group\n heading={group.heading}\n className={cn(\n \"[&_[cmdk-group-heading]]:px-2\",\n \"[&_[cmdk-group-heading]]:pt-1.5\",\n \"[&_[cmdk-group-heading]]:pb-1\",\n \"[&_[cmdk-group-heading]]:text-[11px]\",\n \"[&_[cmdk-group-heading]]:font-semibold\",\n \"[&_[cmdk-group-heading]]:uppercase\",\n \"[&_[cmdk-group-heading]]:tracking-wider\",\n \"[&_[cmdk-group-heading]]:text-muted-foreground/70\"\n )}\n >\n {group.items.map((item) => (\n <Command.Item\n key={item.id}\n value={[item.label, ...(item.keywords ?? [])].join(\" \")}\n onSelect={() => runCommand(item)}\n className={cn(\n \"group/item flex items-center gap-3 rounded-xl cursor-pointer\",\n \"transition-colors duration-150\",\n \"aria-selected:bg-primary/10\",\n primary\n ? \"px-2.5 py-2.5 text-[15px] font-medium\"\n : \"px-3 py-2 text-sm\"\n )}\n >\n {primary ? (\n <span className=\"flex h-8 w-8 items-center justify-center rounded-lg bg-muted/50 group-aria-selected/item:bg-primary/15 transition-colors shrink-0\">\n <item.icon className=\"h-[18px] w-[18px] text-foreground/80 group-aria-selected/item:text-primary transition-colors\" />\n </span>\n ) : (\n <item.icon className=\"h-4 w-4 text-muted-foreground group-aria-selected/item:text-foreground transition-colors shrink-0\" />\n )}\n <span className=\"flex-1 truncate\">{item.label}</span>\n <ArrowRight className=\"h-3.5 w-3.5 text-muted-foreground/30 group-aria-selected/item:text-primary/70 transition-colors shrink-0\" />\n </Command.Item>\n ))}\n </Command.Group>\n </React.Fragment>\n );\n })}\n </Command.List>\n\n {accountSlot && (\n <div className=\"border-t border-border/40 bg-background/40 px-3 py-3\">\n {accountSlot}\n </div>\n )}\n\n {/* Footer */}\n <div className=\"px-3 py-2 border-t border-border/40 flex items-center justify-between gap-3\">\n {footerSlot ?? (\n <span className=\"text-[10px] text-muted-foreground/50 pl-1\">medialane</span>\n )}\n <kbd className=\"text-[10px] text-muted-foreground/50 font-mono shrink-0\">⌘K</kbd>\n </div>\n </Command>\n </div>\n </motion.div>\n </>\n )}\n </AnimatePresence>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/nav-command-menu.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { Command } from \"cmdk\";\nimport { useRouter } from \"next/navigation\";\nimport { Search, X, ArrowRight } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { cn } from \"../utils/cn.js\";\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport interface NavCommand {\n id: string;\n label: string;\n icon: React.ComponentType<{ className?: string }>;\n href?: string;\n action?: () => void;\n /** Extra search terms beyond the label */\n keywords?: string[];\n /** Optional one-line description shown under the label */\n description?: string;\n}\n\nexport interface NavCommandGroup {\n /**\n * Optional group heading. Omit it for the primary group so its items\n * read as the top-level menu — they render first, emphasized, with no\n * label, separated from the headed groups below.\n */\n heading?: string;\n items: NavCommand[];\n}\n\nexport interface NavCommandMenuProps {\n commands: NavCommandGroup[];\n /**\n * Optional trigger element rendered inline at the component's mount point.\n * For most apps, omit this and call `useNavCommandMenu().open()` from a\n * separate button — that keeps the trigger in the right place in the layout.\n */\n trigger?: React.ReactNode;\n /**\n * Optional pinned account/connect area rendered below command results.\n * Apps own the auth implementation here (Clerk, ChipiPay, wallet connectors,\n * Privy, Cartridge, etc.) so the shared nav stays framework-agnostic.\n */\n accountSlot?: React.ReactNode;\n /**\n * Optional control rendered in the footer row (e.g. a theme toggle).\n * Apps own theme state (next-themes etc.) so the shared nav stays\n * framework-agnostic — same pattern as `accountSlot`.\n */\n footerSlot?: React.ReactNode;\n}\n\n// ── Singleton hook ─────────────────────────────────────────────────────────────\n\nconst ML_NAV_OPEN = \"ml:nav-open\";\nconst ML_NAV_CLOSE = \"ml:nav-close\";\n\nexport function useNavCommandMenu() {\n return {\n open: () => document.dispatchEvent(new CustomEvent(ML_NAV_OPEN)),\n close: () => document.dispatchEvent(new CustomEvent(ML_NAV_CLOSE)),\n };\n}\n\n// ── Recents (localStorage) ─────────────────────────────────────────────────────\n\nconst RECENT_KEY = \"ml.nav.recent\";\nconst RECENT_MAX = 3;\n\nfunction readRecents(): string[] {\n try {\n const raw = localStorage.getItem(RECENT_KEY);\n const ids = raw ? (JSON.parse(raw) as unknown) : [];\n return Array.isArray(ids) ? ids.filter((x): x is string => typeof x === \"string\") : [];\n } catch {\n return [];\n }\n}\n\nfunction pushRecent(id: string) {\n try {\n const next = [id, ...readRecents().filter((x) => x !== id)].slice(0, RECENT_MAX);\n localStorage.setItem(RECENT_KEY, JSON.stringify(next));\n } catch {\n /* storage unavailable — recents are a nicety, never an error */\n }\n}\n\n// ── Small primitives ──────────────────────────────────────────────────────────\n\nfunction Kbd({ children, className }: { children: React.ReactNode; className?: string }) {\n return (\n <kbd\n className={cn(\n \"inline-flex min-w-[18px] items-center justify-center rounded-md bg-muted/60 px-1.5 py-0.5\",\n \"font-sans text-[10px] leading-none text-muted-foreground\",\n className\n )}\n >\n {children}\n </kbd>\n );\n}\n\nfunction CommandRow({ item, primary, onSelect }: { item: NavCommand; primary: boolean; onSelect: () => void }) {\n return (\n <Command.Item\n value={[item.label, ...(item.keywords ?? [])].join(\" \")}\n onSelect={onSelect}\n className={cn(\n \"group/item flex cursor-pointer items-center gap-3 rounded-xl\",\n \"transition-colors duration-150 aria-selected:bg-primary/10\",\n primary ? \"px-2.5 py-2.5\" : \"px-2.5 py-2\"\n )}\n >\n <span\n className={cn(\n \"flex shrink-0 items-center justify-center rounded-lg transition-colors\",\n \"bg-muted/50 group-aria-selected/item:bg-primary/15\",\n primary ? \"h-9 w-9\" : \"h-8 w-8\"\n )}\n >\n <item.icon\n className={cn(\n \"text-foreground/80 transition-colors group-aria-selected/item:text-primary\",\n primary ? \"h-[18px] w-[18px]\" : \"h-4 w-4\"\n )}\n />\n </span>\n <span className=\"min-w-0 flex-1\">\n <span\n className={cn(\n \"block truncate leading-tight\",\n primary ? \"text-[15px] font-medium\" : \"text-sm\"\n )}\n >\n {item.label}\n </span>\n {item.description && (\n <span className=\"mt-0.5 block truncate text-[11.5px] leading-tight text-muted-foreground\">\n {item.description}\n </span>\n )}\n </span>\n <ArrowRight className=\"h-3.5 w-3.5 shrink-0 text-muted-foreground/30 transition-colors group-aria-selected/item:text-primary/70\" />\n </Command.Item>\n );\n}\n\nconst GROUP_HEADING_CLASSES = cn(\n \"[&_[cmdk-group-heading]]:px-2.5\",\n \"[&_[cmdk-group-heading]]:pt-2\",\n \"[&_[cmdk-group-heading]]:pb-1\",\n \"[&_[cmdk-group-heading]]:text-[10.5px]\",\n \"[&_[cmdk-group-heading]]:font-semibold\",\n \"[&_[cmdk-group-heading]]:uppercase\",\n \"[&_[cmdk-group-heading]]:tracking-[0.08em]\",\n \"[&_[cmdk-group-heading]]:text-muted-foreground/60\"\n);\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport function NavCommandMenu({ commands, trigger, accountSlot, footerSlot }: NavCommandMenuProps) {\n const [open, setOpen] = React.useState(false);\n const [query, setQuery] = React.useState(\"\");\n const [recentIds, setRecentIds] = React.useState<string[]>([]);\n const router = useRouter();\n const inputRef = React.useRef<HTMLInputElement>(null);\n\n React.useEffect(() => {\n if (!open) return;\n setQuery(\"\");\n setRecentIds(readRecents());\n const t = setTimeout(() => inputRef.current?.focus(), 60);\n return () => clearTimeout(t);\n }, [open]);\n\n React.useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"k\" && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n setOpen((prev) => !prev);\n }\n if (e.key === \"Escape\") setOpen(false);\n };\n const onOpen = () => setOpen(true);\n const onClose = () => setOpen(false);\n\n document.addEventListener(\"keydown\", onKey);\n document.addEventListener(ML_NAV_OPEN, onOpen);\n document.addEventListener(ML_NAV_CLOSE, onClose);\n return () => {\n document.removeEventListener(\"keydown\", onKey);\n document.removeEventListener(ML_NAV_OPEN, onOpen);\n document.removeEventListener(ML_NAV_CLOSE, onClose);\n };\n }, []);\n\n const runCommand = React.useCallback(\n (cmd: NavCommand) => {\n pushRecent(cmd.id);\n setOpen(false);\n if (cmd.href) router.push(cmd.href);\n else cmd.action?.();\n },\n [router]\n );\n\n // Recents only render while the query is empty (search results replace them).\n const recentItems = React.useMemo(() => {\n if (recentIds.length === 0) return [];\n const all = new Map(commands.flatMap((g) => g.items.map((it) => [it.id, it] as const)));\n return recentIds.map((id) => all.get(id)).filter((it): it is NavCommand => Boolean(it));\n }, [recentIds, commands]);\n const showRecents = query === \"\" && recentItems.length > 0;\n\n return (\n <>\n {trigger}\n\n <AnimatePresence>\n {open && (\n <>\n {/* Backdrop blur */}\n <motion.div\n className=\"nav-canvas-overlay\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.15 }}\n onClick={() => setOpen(false)}\n />\n\n {/* Command panel — centered on desktop, bottom sheet on mobile */}\n <motion.div\n className=\"fixed inset-0 z-[101] flex items-end justify-center p-3 pb-4 sm:items-center sm:p-4\"\n initial={{ opacity: 0, y: 24, scale: 1 }}\n animate={{ opacity: 1, y: 0, scale: 1 }}\n exit={{ opacity: 0, y: 24 }}\n transition={{ duration: 0.18, ease: \"easeOut\" }}\n onClick={() => setOpen(false)}\n >\n <div\n className={cn(\n \"flex w-full max-h-[85dvh] flex-col overflow-hidden rounded-[20px] sm:max-h-[min(680px,88dvh)] sm:max-w-[620px]\",\n \"border border-border/40 bg-background/90 shadow-2xl backdrop-blur-2xl backdrop-saturate-150\"\n )}\n onClick={(e) => e.stopPropagation()}\n >\n <Command shouldFilter label=\"Medialane navigation\" className=\"flex min-h-0 flex-1 flex-col\">\n {/* Drag handle (mobile) */}\n <div className=\"flex justify-center pt-2.5 sm:hidden\" aria-hidden=\"true\">\n <span className=\"h-1 w-9 rounded-full bg-muted-foreground/30\" />\n </div>\n\n {/* Search bar */}\n <div className=\"flex items-center gap-3 border-b border-border/40 px-4 py-3.5\">\n <Search\n className={cn(\n \"h-[18px] w-[18px] shrink-0 transition-colors\",\n query ? \"text-primary\" : \"text-muted-foreground\"\n )}\n />\n <Command.Input\n ref={inputRef}\n value={query}\n onValueChange={setQuery}\n placeholder=\"Search or run a command…\"\n className=\"flex-1 bg-transparent text-[15px] outline-none placeholder:text-muted-foreground\"\n />\n <Kbd className=\"hidden sm:inline-flex\">esc</Kbd>\n <button\n onClick={() => setOpen(false)}\n className=\"rounded-md p-1 transition-colors hover:bg-muted/50 sm:hidden\"\n aria-label=\"Close\"\n >\n <X className=\"h-4 w-4 text-muted-foreground\" />\n </button>\n </div>\n\n {/* Results */}\n <Command.List className=\"min-h-0 flex-1 overflow-y-auto overscroll-contain p-2\">\n <Command.Empty className=\"py-8 text-center text-sm text-muted-foreground\">\n No results found.\n </Command.Empty>\n\n {showRecents && (\n <Command.Group heading=\"Recent\" className={GROUP_HEADING_CLASSES}>\n {recentItems.map((item) => (\n <CommandRow\n key={`recent-${item.id}`}\n item={item}\n primary={false}\n onSelect={() => runCommand(item)}\n />\n ))}\n </Command.Group>\n )}\n\n {commands.map((group, i) => {\n const primary = !group.heading;\n return (\n <React.Fragment key={group.heading ?? `__primary-${i}`}>\n {(i > 0 || showRecents) && (\n <Command.Separator className=\"my-1.5 h-px bg-border/40\" />\n )}\n <Command.Group heading={group.heading} className={GROUP_HEADING_CLASSES}>\n {group.items.map((item) => (\n <CommandRow\n key={item.id}\n item={item}\n primary={primary}\n onSelect={() => runCommand(item)}\n />\n ))}\n </Command.Group>\n </React.Fragment>\n );\n })}\n </Command.List>\n\n {accountSlot && (\n <div className=\"border-t border-border/40 bg-muted/20 px-3 py-3\">\n {accountSlot}\n </div>\n )}\n\n {/* Footer */}\n <div className=\"flex items-center justify-between gap-3 border-t border-border/40 bg-muted/20 px-3 py-2\">\n <div className=\"flex items-center gap-3\">\n {footerSlot}\n <span className=\"hidden items-center gap-3 text-[10.5px] text-muted-foreground/70 sm:flex\">\n <span className=\"flex items-center gap-1\"><Kbd>↑</Kbd><Kbd>↓</Kbd> Navigate</span>\n <span className=\"flex items-center gap-1\"><Kbd>↵</Kbd> Open</span>\n </span>\n </div>\n <span className=\"flex shrink-0 items-center gap-2 text-[10px] text-muted-foreground/50\">\n medialane\n <Kbd className=\"hidden sm:inline-flex\">⌘K</Kbd>\n </span>\n </div>\n </Command>\n </div>\n </motion.div>\n </>\n )}\n </AnimatePresence>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
"use client";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
var nav_shell_exports = {};
|
|
31
|
+
__export(nav_shell_exports, {
|
|
32
|
+
NavAccountSheet: () => NavAccountSheet,
|
|
33
|
+
NavBrandButton: () => NavBrandButton,
|
|
34
|
+
NavIconButton: () => NavIconButton,
|
|
35
|
+
useNavAccountSheet: () => useNavAccountSheet
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(nav_shell_exports);
|
|
38
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
39
|
+
var React = __toESM(require("react"), 1);
|
|
40
|
+
var import_framer_motion = require("framer-motion");
|
|
41
|
+
var import_cn = require("../utils/cn.js");
|
|
42
|
+
function NavBrandButton({ onClick, className, iconSrc = "/icon.png", ...rest }) {
|
|
43
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
44
|
+
"button",
|
|
45
|
+
{
|
|
46
|
+
type: "button",
|
|
47
|
+
onClick,
|
|
48
|
+
"aria-label": rest["aria-label"] ?? "Open navigation",
|
|
49
|
+
className: (0, import_cn.cn)(
|
|
50
|
+
"group flex h-11 items-center gap-2 rounded-full border border-border/40",
|
|
51
|
+
"bg-background/70 pl-1.5 pr-3 backdrop-blur-xl backdrop-saturate-150 shadow-sm",
|
|
52
|
+
"transition-colors hover:bg-background/90 active:scale-[0.97]",
|
|
53
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60",
|
|
54
|
+
className
|
|
55
|
+
),
|
|
56
|
+
children: [
|
|
57
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: iconSrc, alt: "Medialane", width: 30, height: 30, className: "h-[30px] w-[30px] shrink-0" }),
|
|
58
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "flex flex-col gap-[5px]", "aria-hidden": "true", children: [
|
|
59
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "h-[1.5px] w-4 rounded-full bg-muted-foreground transition-all group-hover:w-4 group-hover:bg-foreground" }),
|
|
60
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "h-[1.5px] w-2.5 rounded-full bg-muted-foreground transition-all group-hover:w-4 group-hover:bg-foreground" })
|
|
61
|
+
] })
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
function NavIconButton({ onClick, className, indicator, children, ...rest }) {
|
|
67
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
68
|
+
"button",
|
|
69
|
+
{
|
|
70
|
+
type: "button",
|
|
71
|
+
onClick,
|
|
72
|
+
"aria-label": rest["aria-label"],
|
|
73
|
+
className: (0, import_cn.cn)(
|
|
74
|
+
"relative flex h-11 w-11 items-center justify-center rounded-full border border-border/40",
|
|
75
|
+
"bg-background/70 text-muted-foreground backdrop-blur-xl backdrop-saturate-150 shadow-sm",
|
|
76
|
+
"transition-colors hover:bg-background/90 hover:text-foreground active:scale-[0.97]",
|
|
77
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60",
|
|
78
|
+
className
|
|
79
|
+
),
|
|
80
|
+
children: [
|
|
81
|
+
children,
|
|
82
|
+
indicator && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "absolute right-[3px] top-[3px] h-2.5 w-2.5 rounded-full border-2 border-background bg-brand-blue" })
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const ML_ACCOUNT_OPEN = "ml:account-open";
|
|
88
|
+
const ML_ACCOUNT_CLOSE = "ml:account-close";
|
|
89
|
+
const ML_NAV_OPEN = "ml:nav-open";
|
|
90
|
+
const ML_NAV_CLOSE = "ml:nav-close";
|
|
91
|
+
function useNavAccountSheet() {
|
|
92
|
+
return {
|
|
93
|
+
open: () => document.dispatchEvent(new CustomEvent(ML_ACCOUNT_OPEN)),
|
|
94
|
+
close: () => document.dispatchEvent(new CustomEvent(ML_ACCOUNT_CLOSE))
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function NavAccountSheet({ children, title = "Account" }) {
|
|
98
|
+
const [open, setOpen] = React.useState(false);
|
|
99
|
+
React.useEffect(() => {
|
|
100
|
+
const onOpen = () => {
|
|
101
|
+
document.dispatchEvent(new CustomEvent(ML_NAV_CLOSE));
|
|
102
|
+
setOpen(true);
|
|
103
|
+
};
|
|
104
|
+
const onClose = () => setOpen(false);
|
|
105
|
+
const onKey = (e) => {
|
|
106
|
+
if (e.key === "Escape") setOpen(false);
|
|
107
|
+
};
|
|
108
|
+
document.addEventListener(ML_ACCOUNT_OPEN, onOpen);
|
|
109
|
+
document.addEventListener(ML_ACCOUNT_CLOSE, onClose);
|
|
110
|
+
document.addEventListener(ML_NAV_OPEN, onClose);
|
|
111
|
+
document.addEventListener(ML_NAV_CLOSE, onClose);
|
|
112
|
+
document.addEventListener("keydown", onKey);
|
|
113
|
+
return () => {
|
|
114
|
+
document.removeEventListener(ML_ACCOUNT_OPEN, onOpen);
|
|
115
|
+
document.removeEventListener(ML_ACCOUNT_CLOSE, onClose);
|
|
116
|
+
document.removeEventListener(ML_NAV_OPEN, onClose);
|
|
117
|
+
document.removeEventListener(ML_NAV_CLOSE, onClose);
|
|
118
|
+
document.removeEventListener("keydown", onKey);
|
|
119
|
+
};
|
|
120
|
+
}, []);
|
|
121
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_framer_motion.AnimatePresence, { children: open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
122
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
123
|
+
import_framer_motion.motion.div,
|
|
124
|
+
{
|
|
125
|
+
className: "nav-canvas-overlay",
|
|
126
|
+
initial: { opacity: 0 },
|
|
127
|
+
animate: { opacity: 1 },
|
|
128
|
+
exit: { opacity: 0 },
|
|
129
|
+
transition: { duration: 0.15 },
|
|
130
|
+
onClick: () => setOpen(false)
|
|
131
|
+
}
|
|
132
|
+
),
|
|
133
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
134
|
+
import_framer_motion.motion.div,
|
|
135
|
+
{
|
|
136
|
+
className: "fixed inset-x-3 bottom-3 z-[101] sm:inset-auto sm:right-4 sm:top-4 sm:w-[360px] lg:right-8",
|
|
137
|
+
initial: { opacity: 0, y: 24 },
|
|
138
|
+
animate: { opacity: 1, y: 0 },
|
|
139
|
+
exit: { opacity: 0, y: 24 },
|
|
140
|
+
transition: { duration: 0.18, ease: "easeOut" },
|
|
141
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "overflow-hidden rounded-[20px] border border-border/40 bg-background/90 shadow-2xl backdrop-blur-2xl backdrop-saturate-150", children: [
|
|
142
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex justify-center pt-2.5 sm:hidden", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "h-1 w-9 rounded-full bg-muted-foreground/30" }) }),
|
|
143
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between px-4 pb-1 pt-2 sm:pt-3", children: [
|
|
144
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70", children: title }),
|
|
145
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
146
|
+
"button",
|
|
147
|
+
{
|
|
148
|
+
onClick: () => setOpen(false),
|
|
149
|
+
className: "rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted/50",
|
|
150
|
+
"aria-label": "Close",
|
|
151
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M18 6 6 18M6 6l12 12" }) })
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
] }),
|
|
155
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "px-3 pb-4 pt-1 sm:pb-3", children })
|
|
156
|
+
] })
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
] }) });
|
|
160
|
+
}
|
|
161
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
162
|
+
0 && (module.exports = {
|
|
163
|
+
NavAccountSheet,
|
|
164
|
+
NavBrandButton,
|
|
165
|
+
NavIconButton,
|
|
166
|
+
useNavAccountSheet
|
|
167
|
+
});
|
|
168
|
+
//# sourceMappingURL=nav-shell.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/nav-shell.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport { cn } from \"../utils/cn.js\";\n\n// ── Header buttons ────────────────────────────────────────────────────────────\n\nexport interface NavBrandButtonProps {\n onClick?: () => void;\n className?: string;\n \"aria-label\"?: string;\n /** Path to the brand icon image (the app's real icon asset). */\n iconSrc?: string;\n}\n\n/**\n * The main header trigger: brand mark + menu glyph in one quiet glass pill.\n * Mobile-first — a single ≥44px tap target instead of two small adjacent icons.\n */\nexport function NavBrandButton({ onClick, className, iconSrc = \"/icon.png\", ...rest }: NavBrandButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={rest[\"aria-label\"] ?? \"Open navigation\"}\n className={cn(\n \"group flex h-11 items-center gap-2 rounded-full border border-border/40\",\n \"bg-background/70 pl-1.5 pr-3 backdrop-blur-xl backdrop-saturate-150 shadow-sm\",\n \"transition-colors hover:bg-background/90 active:scale-[0.97]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60\",\n className\n )}\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={iconSrc} alt=\"Medialane\" width={30} height={30} className=\"h-[30px] w-[30px] shrink-0\" />\n <span className=\"flex flex-col gap-[5px]\" aria-hidden=\"true\">\n <span className=\"h-[1.5px] w-4 rounded-full bg-muted-foreground transition-all group-hover:w-4 group-hover:bg-foreground\" />\n <span className=\"h-[1.5px] w-2.5 rounded-full bg-muted-foreground transition-all group-hover:w-4 group-hover:bg-foreground\" />\n </span>\n </button>\n );\n}\n\nexport interface NavIconButtonProps {\n onClick?: () => void;\n className?: string;\n \"aria-label\": string;\n /** Show a small brand-colored status dot (e.g. wallet connected). */\n indicator?: boolean;\n children: React.ReactNode;\n}\n\n/**\n * Circular glass header button — the right-side counterpart to NavBrandButton\n * (wallet / account trigger).\n */\nexport function NavIconButton({ onClick, className, indicator, children, ...rest }: NavIconButtonProps) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-label={rest[\"aria-label\"]}\n className={cn(\n \"relative flex h-11 w-11 items-center justify-center rounded-full border border-border/40\",\n \"bg-background/70 text-muted-foreground backdrop-blur-xl backdrop-saturate-150 shadow-sm\",\n \"transition-colors hover:bg-background/90 hover:text-foreground active:scale-[0.97]\",\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/60\",\n className\n )}\n >\n {children}\n {indicator && (\n <span className=\"absolute right-[3px] top-[3px] h-2.5 w-2.5 rounded-full border-2 border-background bg-brand-blue\" />\n )}\n </button>\n );\n}\n\n// ── Account sheet ─────────────────────────────────────────────────────────────\n\nconst ML_ACCOUNT_OPEN = \"ml:account-open\";\nconst ML_ACCOUNT_CLOSE = \"ml:account-close\";\nconst ML_NAV_OPEN = \"ml:nav-open\";\nconst ML_NAV_CLOSE = \"ml:nav-close\";\n\nexport function useNavAccountSheet() {\n return {\n open: () => document.dispatchEvent(new CustomEvent(ML_ACCOUNT_OPEN)),\n close: () => document.dispatchEvent(new CustomEvent(ML_ACCOUNT_CLOSE)),\n };\n}\n\nexport interface NavAccountSheetProps {\n /** The app's account/connect panel (Clerk, wallet connectors, …). */\n children: React.ReactNode;\n /** Optional heading above the panel. */\n title?: string;\n}\n\n/**\n * The wallet-side surface: a compact glass sheet anchored to the top-right\n * header button (bottom sheet on mobile). Content is app-owned — pass the same\n * account panel the command menu uses in its accountSlot.\n *\n * Opens via `useNavAccountSheet().open()`. Mutually exclusive with the command\n * menu: opening one closes the other, and the shared `ml:nav-close` event\n * (fired by `useNavCommandMenu().close()`) closes this sheet too, so account\n * panels behave identically in both hosts.\n */\nexport function NavAccountSheet({ children, title = \"Account\" }: NavAccountSheetProps) {\n const [open, setOpen] = React.useState(false);\n\n React.useEffect(() => {\n const onOpen = () => {\n document.dispatchEvent(new CustomEvent(ML_NAV_CLOSE));\n setOpen(true);\n };\n const onClose = () => setOpen(false);\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(false);\n };\n document.addEventListener(ML_ACCOUNT_OPEN, onOpen);\n document.addEventListener(ML_ACCOUNT_CLOSE, onClose);\n document.addEventListener(ML_NAV_OPEN, onClose);\n document.addEventListener(ML_NAV_CLOSE, onClose);\n document.addEventListener(\"keydown\", onKey);\n return () => {\n document.removeEventListener(ML_ACCOUNT_OPEN, onOpen);\n document.removeEventListener(ML_ACCOUNT_CLOSE, onClose);\n document.removeEventListener(ML_NAV_OPEN, onClose);\n document.removeEventListener(ML_NAV_CLOSE, onClose);\n document.removeEventListener(\"keydown\", onKey);\n };\n }, []);\n\n return (\n <AnimatePresence>\n {open && (\n <>\n <motion.div\n className=\"nav-canvas-overlay\"\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{ opacity: 0 }}\n transition={{ duration: 0.15 }}\n onClick={() => setOpen(false)}\n />\n <motion.div\n className=\"fixed inset-x-3 bottom-3 z-[101] sm:inset-auto sm:right-4 sm:top-4 sm:w-[360px] lg:right-8\"\n initial={{ opacity: 0, y: 24 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: 24 }}\n transition={{ duration: 0.18, ease: \"easeOut\" }}\n >\n <div className=\"overflow-hidden rounded-[20px] border border-border/40 bg-background/90 shadow-2xl backdrop-blur-2xl backdrop-saturate-150\">\n <div className=\"flex justify-center pt-2.5 sm:hidden\" aria-hidden=\"true\">\n <span className=\"h-1 w-9 rounded-full bg-muted-foreground/30\" />\n </div>\n <div className=\"flex items-center justify-between px-4 pb-1 pt-2 sm:pt-3\">\n <span className=\"text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70\">\n {title}\n </span>\n <button\n onClick={() => setOpen(false)}\n className=\"rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted/50\"\n aria-label=\"Close\"\n >\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\">\n <path d=\"M18 6 6 18M6 6l12 12\" />\n </svg>\n </button>\n </div>\n <div className=\"px-3 pb-4 pt-1 sm:pb-3\">{children}</div>\n </div>\n </motion.div>\n </>\n )}\n </AnimatePresence>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
interface NavBrandButtonProps {
|
|
5
|
+
onClick?: () => void;
|
|
6
|
+
className?: string;
|
|
7
|
+
"aria-label"?: string;
|
|
8
|
+
/** Path to the brand icon image (the app's real icon asset). */
|
|
9
|
+
iconSrc?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The main header trigger: brand mark + menu glyph in one quiet glass pill.
|
|
13
|
+
* Mobile-first — a single ≥44px tap target instead of two small adjacent icons.
|
|
14
|
+
*/
|
|
15
|
+
declare function NavBrandButton({ onClick, className, iconSrc, ...rest }: NavBrandButtonProps): react_jsx_runtime.JSX.Element;
|
|
16
|
+
interface NavIconButtonProps {
|
|
17
|
+
onClick?: () => void;
|
|
18
|
+
className?: string;
|
|
19
|
+
"aria-label": string;
|
|
20
|
+
/** Show a small brand-colored status dot (e.g. wallet connected). */
|
|
21
|
+
indicator?: boolean;
|
|
22
|
+
children: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Circular glass header button — the right-side counterpart to NavBrandButton
|
|
26
|
+
* (wallet / account trigger).
|
|
27
|
+
*/
|
|
28
|
+
declare function NavIconButton({ onClick, className, indicator, children, ...rest }: NavIconButtonProps): react_jsx_runtime.JSX.Element;
|
|
29
|
+
declare function useNavAccountSheet(): {
|
|
30
|
+
open: () => boolean;
|
|
31
|
+
close: () => boolean;
|
|
32
|
+
};
|
|
33
|
+
interface NavAccountSheetProps {
|
|
34
|
+
/** The app's account/connect panel (Clerk, wallet connectors, …). */
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
/** Optional heading above the panel. */
|
|
37
|
+
title?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The wallet-side surface: a compact glass sheet anchored to the top-right
|
|
41
|
+
* header button (bottom sheet on mobile). Content is app-owned — pass the same
|
|
42
|
+
* account panel the command menu uses in its accountSlot.
|
|
43
|
+
*
|
|
44
|
+
* Opens via `useNavAccountSheet().open()`. Mutually exclusive with the command
|
|
45
|
+
* menu: opening one closes the other, and the shared `ml:nav-close` event
|
|
46
|
+
* (fired by `useNavCommandMenu().close()`) closes this sheet too, so account
|
|
47
|
+
* panels behave identically in both hosts.
|
|
48
|
+
*/
|
|
49
|
+
declare function NavAccountSheet({ children, title }: NavAccountSheetProps): react_jsx_runtime.JSX.Element;
|
|
50
|
+
|
|
51
|
+
export { NavAccountSheet, type NavAccountSheetProps, NavBrandButton, type NavBrandButtonProps, NavIconButton, type NavIconButtonProps, useNavAccountSheet };
|