@mdxui/do 4.0.1 → 4.0.8
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/app/index.d.ts +32 -186
- package/dist/app/index.js +19 -11
- package/dist/auth/index.d.ts +48 -0
- package/dist/auth/index.js +4 -0
- package/dist/{breadcrumbs-C9Qn3S7d.d.ts → breadcrumbs-DltlCiHt.d.ts} +3 -3
- package/dist/chunk-2FWXT4HH.js +576 -0
- package/dist/chunk-2FWXT4HH.js.map +1 -0
- package/dist/chunk-5PC6U46L.js +604 -0
- package/dist/chunk-5PC6U46L.js.map +1 -0
- package/dist/chunk-63VCRTDQ.js +2505 -0
- package/dist/chunk-63VCRTDQ.js.map +1 -0
- package/dist/chunk-7J3RSIG4.js +178 -0
- package/dist/chunk-7J3RSIG4.js.map +1 -0
- package/dist/chunk-7QCTRNEJ.js +173 -0
- package/dist/chunk-7QCTRNEJ.js.map +1 -0
- package/dist/chunk-7TFHUKS7.js +66 -0
- package/dist/chunk-7TFHUKS7.js.map +1 -0
- package/dist/{chunk-LJIWB7KE.js → chunk-BU5HMHGQ.js} +3 -3
- package/dist/chunk-BU5HMHGQ.js.map +1 -0
- package/dist/{chunk-GGO5GW72.js → chunk-BZURBNFD.js} +305 -51
- package/dist/chunk-BZURBNFD.js.map +1 -0
- package/dist/{chunk-XH3LVW7J.js → chunk-FM2RTAHV.js} +5 -65
- package/dist/chunk-FM2RTAHV.js.map +1 -0
- package/dist/{chunk-OHWWWONS.js → chunk-HC4PBXV4.js} +131 -58
- package/dist/chunk-HC4PBXV4.js.map +1 -0
- package/dist/{chunk-Y52IEYVM.js → chunk-JPZ6RZJE.js} +78 -45
- package/dist/chunk-JPZ6RZJE.js.map +1 -0
- package/dist/{chunk-KAZJ43F6.js → chunk-KLN5OTQH.js} +168 -361
- package/dist/chunk-KLN5OTQH.js.map +1 -0
- package/dist/chunk-LCYBQR35.js +79 -0
- package/dist/chunk-LCYBQR35.js.map +1 -0
- package/dist/chunk-PJYGRD7N.js +216 -0
- package/dist/chunk-PJYGRD7N.js.map +1 -0
- package/dist/chunk-QEXY4FZV.js +292 -0
- package/dist/chunk-QEXY4FZV.js.map +1 -0
- package/dist/chunk-SX4IIE2R.js +53 -0
- package/dist/chunk-SX4IIE2R.js.map +1 -0
- package/dist/chunk-UCWMSKCW.js +901 -0
- package/dist/chunk-UCWMSKCW.js.map +1 -0
- package/dist/{chunk-YMQRC6IC.js → chunk-WC6SFBAF.js} +59 -6
- package/dist/chunk-WC6SFBAF.js.map +1 -0
- package/dist/chunk-WIKU77ZY.js +18 -0
- package/dist/chunk-WIKU77ZY.js.map +1 -0
- package/dist/chunk-X3AWNFBF.js +47 -0
- package/dist/chunk-X3AWNFBF.js.map +1 -0
- package/dist/chunk-ZZTQGMLX.js +23 -0
- package/dist/chunk-ZZTQGMLX.js.map +1 -0
- package/dist/{lib → client}/index.d.ts +97 -345
- package/dist/client/index.js +3 -0
- package/dist/common-DW_JM2dW.d.ts +454 -0
- package/dist/components/index.d.ts +11 -2
- package/dist/components/index.js +8 -3
- package/dist/{config-CxvpD8Y6.d.ts → config-DB14_LhF.d.ts} +1 -1
- package/dist/{do-D27i5bU0.d.ts → do-D37hbmL9.d.ts} +6 -14
- package/dist/dotdo-client-2DkwXHM2.d.ts +344 -0
- package/dist/errors-BOY11CJs.d.ts +373 -0
- package/dist/features/data-browser/index.d.ts +51 -0
- package/dist/features/data-browser/index.js +12 -0
- package/dist/features/data-browser/index.js.map +1 -0
- package/dist/features/data-grid/index.d.ts +22 -0
- package/dist/features/data-grid/index.js +12 -0
- package/dist/features/data-grid/index.js.map +1 -0
- package/dist/features/document-editor/index.d.ts +26 -0
- package/dist/features/document-editor/index.js +12 -0
- package/dist/features/document-editor/index.js.map +1 -0
- package/dist/features/function-editor/index.d.ts +215 -0
- package/dist/features/function-editor/index.js +7 -0
- package/dist/features/function-editor/index.js.map +1 -0
- package/dist/hooks/index.d.ts +64 -7
- package/dist/hooks/index.js +8 -7
- package/dist/hooks/things/index.d.ts +5 -297
- package/dist/hooks/things/index.js +6 -6
- package/dist/index-C0m9UI6W.d.ts +444 -0
- package/dist/index.d.ts +41 -31
- package/dist/index.js +21 -13
- package/dist/providers/index.d.ts +16 -28
- package/dist/providers/index.js +3 -3
- package/dist/schemas/index.d.ts +4551 -109
- package/dist/schemas/index.js +2 -260
- package/dist/schemas/index.js.map +1 -1
- package/dist/shell/index.d.ts +92 -0
- package/dist/shell/index.js +6 -0
- package/dist/shell/index.js.map +1 -0
- package/dist/{thing-BF25aUtJ.d.ts → thing-Dc3AE2XI.d.ts} +22 -22
- package/dist/thing-adapters-aMjF0h9u.d.ts +1214 -0
- package/dist/types/index.d.ts +954 -7521
- package/dist/types/index.js +2 -2
- package/dist/ui-filters-BvrjMP_U.d.ts +108 -0
- package/dist/{errors-DratdVIz.d.ts → utils/index.d.ts} +38 -77
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist-app/assets/index-DWX3479M.js +7 -0
- package/dist-app/assets/index-DWX3479M.js.map +1 -0
- package/dist-app/assets/main-BptePr_C.js +318 -0
- package/dist-app/assets/main-BptePr_C.js.map +1 -0
- package/dist-app/assets/main-CS9jgKzj.css +1 -0
- package/dist-app/index.html +38 -0
- package/package.json +46 -33
- package/dist/agents-2_r9e9i7.d.ts +0 -1043
- package/dist/capnweb-client-Bq78FtEA.d.ts +0 -229
- package/dist/chunk-3XKYQRXY.js +0 -192
- package/dist/chunk-3XKYQRXY.js.map +0 -1
- package/dist/chunk-4KXVN3EQ.js +0 -56
- package/dist/chunk-4KXVN3EQ.js.map +0 -1
- package/dist/chunk-GGO5GW72.js.map +0 -1
- package/dist/chunk-GYIMQZE7.js +0 -1994
- package/dist/chunk-GYIMQZE7.js.map +0 -1
- package/dist/chunk-IQ23B4ME.js +0 -944
- package/dist/chunk-IQ23B4ME.js.map +0 -1
- package/dist/chunk-JJLAES6W.js +0 -76
- package/dist/chunk-JJLAES6W.js.map +0 -1
- package/dist/chunk-KAZJ43F6.js.map +0 -1
- package/dist/chunk-LJIWB7KE.js.map +0 -1
- package/dist/chunk-OHWWWONS.js.map +0 -1
- package/dist/chunk-VRLUXCLD.js +0 -31
- package/dist/chunk-VRLUXCLD.js.map +0 -1
- package/dist/chunk-XH3LVW7J.js.map +0 -1
- package/dist/chunk-Y52IEYVM.js.map +0 -1
- package/dist/chunk-YMQRC6IC.js.map +0 -1
- package/dist/lib/index.js +0 -6
- package/dist/query-keys-CZNFikIi.d.ts +0 -153
- package/dist/views/index.d.ts +0 -131
- package/dist/views/index.js +0 -11
- /package/dist/{lib → auth}/index.js.map +0 -0
- /package/dist/{views → client}/index.js.map +0 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { createContext, useMemo, useContext } from 'react';
|
|
2
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
import { useAuth } from '@mdxui/auth';
|
|
4
|
+
import { useSidebar, DropdownMenu, DropdownMenuTrigger, SidebarMenuButton, Avatar, AvatarImage, AvatarFallback, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuItem } from '@mdxui/primitives';
|
|
5
|
+
import { ChevronsUpDown, User, Settings, LogOut } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
// src/features/shell/config-context.tsx
|
|
8
|
+
var DOConfigContext = createContext(null);
|
|
9
|
+
var defaultIdentity = {
|
|
10
|
+
clientId: "",
|
|
11
|
+
devMode: false,
|
|
12
|
+
required: true,
|
|
13
|
+
onUnauthenticated: "landing"
|
|
14
|
+
};
|
|
15
|
+
var defaultConfig = {
|
|
16
|
+
basePath: "/",
|
|
17
|
+
defaultNamespace: "default",
|
|
18
|
+
branding: { name: "DO Admin" },
|
|
19
|
+
theme: { mode: "system" },
|
|
20
|
+
routes: {},
|
|
21
|
+
customRoutes: []
|
|
22
|
+
};
|
|
23
|
+
function DOConfigProvider({ config, children }) {
|
|
24
|
+
const value = useMemo(() => {
|
|
25
|
+
const mergedConfig = {
|
|
26
|
+
...defaultConfig,
|
|
27
|
+
...config,
|
|
28
|
+
branding: { ...defaultConfig.branding, ...config.branding },
|
|
29
|
+
theme: { ...defaultConfig.theme, ...config.theme },
|
|
30
|
+
identity: { ...defaultIdentity, ...config.identity }
|
|
31
|
+
};
|
|
32
|
+
return { config: mergedConfig };
|
|
33
|
+
}, [config]);
|
|
34
|
+
return /* @__PURE__ */ jsx(DOConfigContext.Provider, { value, children });
|
|
35
|
+
}
|
|
36
|
+
function useDOConfig() {
|
|
37
|
+
const context = useContext(DOConfigContext);
|
|
38
|
+
if (!context) {
|
|
39
|
+
throw new Error("useDOConfig must be used within a DOConfigProvider");
|
|
40
|
+
}
|
|
41
|
+
return context;
|
|
42
|
+
}
|
|
43
|
+
function useDORoutes() {
|
|
44
|
+
const { config } = useDOConfig();
|
|
45
|
+
return config.routes ?? {};
|
|
46
|
+
}
|
|
47
|
+
function useDOCustomRoutes() {
|
|
48
|
+
const { config } = useDOConfig();
|
|
49
|
+
return config.customRoutes ?? [];
|
|
50
|
+
}
|
|
51
|
+
function DefaultLandingPage() {
|
|
52
|
+
const { signIn } = useAuth();
|
|
53
|
+
return /* @__PURE__ */ jsx("div", { className: "min-h-screen bg-background flex flex-col items-center justify-center p-4", children: /* @__PURE__ */ jsxs("div", { className: "text-center max-w-md", children: [
|
|
54
|
+
/* @__PURE__ */ jsx("h1", { className: "text-3xl font-bold tracking-tight mb-4", children: "DO Admin" }),
|
|
55
|
+
/* @__PURE__ */ jsx("p", { className: "text-muted-foreground mb-8", children: "Sign in to access the admin dashboard and manage your Durable Objects." }),
|
|
56
|
+
/* @__PURE__ */ jsx(
|
|
57
|
+
"button",
|
|
58
|
+
{
|
|
59
|
+
type: "button",
|
|
60
|
+
onClick: () => signIn(),
|
|
61
|
+
className: "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-6",
|
|
62
|
+
children: "Sign In"
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
] }) });
|
|
66
|
+
}
|
|
67
|
+
function AuthGate({ children }) {
|
|
68
|
+
const { config } = useDOConfig();
|
|
69
|
+
const { user, isLoading } = useAuth();
|
|
70
|
+
const { identity } = config;
|
|
71
|
+
const required = identity.required ?? true;
|
|
72
|
+
const onUnauthenticated = identity.onUnauthenticated ?? "landing";
|
|
73
|
+
if (!required) {
|
|
74
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
75
|
+
}
|
|
76
|
+
if (isLoading) {
|
|
77
|
+
return /* @__PURE__ */ jsx("div", { className: "min-h-screen bg-background flex items-center justify-center", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-4", children: [
|
|
78
|
+
/* @__PURE__ */ jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" }),
|
|
79
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Loading..." })
|
|
80
|
+
] }) });
|
|
81
|
+
}
|
|
82
|
+
if (user) {
|
|
83
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
84
|
+
}
|
|
85
|
+
switch (onUnauthenticated) {
|
|
86
|
+
case "redirect":
|
|
87
|
+
if (identity.unauthenticatedRedirectUrl && typeof window !== "undefined") {
|
|
88
|
+
window.location.href = identity.unauthenticatedRedirectUrl;
|
|
89
|
+
return /* @__PURE__ */ jsx("div", { className: "min-h-screen bg-background flex items-center justify-center", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Redirecting..." }) });
|
|
90
|
+
}
|
|
91
|
+
return identity.landingComponent ? identity.landingComponent : /* @__PURE__ */ jsx(DefaultLandingPage, {});
|
|
92
|
+
case "allow":
|
|
93
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
94
|
+
case "landing":
|
|
95
|
+
default:
|
|
96
|
+
return identity.landingComponent ? identity.landingComponent : /* @__PURE__ */ jsx(DefaultLandingPage, {});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function getInitials(name) {
|
|
100
|
+
if (!name) return "?";
|
|
101
|
+
const parts = name.trim().split(/\s+/);
|
|
102
|
+
if (parts.length === 1) return parts[0].charAt(0).toUpperCase();
|
|
103
|
+
return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
|
|
104
|
+
}
|
|
105
|
+
function UserMenu({ compact = false }) {
|
|
106
|
+
const { user, signOut, isLoading } = useAuth();
|
|
107
|
+
const { isMobile } = useSidebar();
|
|
108
|
+
if (isLoading) {
|
|
109
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 p-2", children: [
|
|
110
|
+
/* @__PURE__ */ jsx("div", { className: "h-8 w-8 animate-pulse rounded-full bg-muted" }),
|
|
111
|
+
!compact && /* @__PURE__ */ jsx("div", { className: "h-4 w-24 animate-pulse rounded bg-muted" })
|
|
112
|
+
] });
|
|
113
|
+
}
|
|
114
|
+
if (!user) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const displayName = user.firstName ? `${user.firstName}${user.lastName ? ` ${user.lastName}` : ""}` : user.email;
|
|
118
|
+
const initials = getInitials(user.firstName ? `${user.firstName} ${user.lastName ?? ""}` : user.email);
|
|
119
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
120
|
+
/* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
121
|
+
SidebarMenuButton,
|
|
122
|
+
{
|
|
123
|
+
size: "lg",
|
|
124
|
+
className: "data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground",
|
|
125
|
+
children: [
|
|
126
|
+
/* @__PURE__ */ jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [
|
|
127
|
+
/* @__PURE__ */ jsx(AvatarImage, { src: user.profilePictureUrl ?? void 0, alt: displayName }),
|
|
128
|
+
/* @__PURE__ */ jsx(AvatarFallback, { className: "rounded-lg", children: initials })
|
|
129
|
+
] }),
|
|
130
|
+
!compact && /* @__PURE__ */ jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [
|
|
131
|
+
/* @__PURE__ */ jsx("span", { className: "truncate font-semibold", children: displayName }),
|
|
132
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: user.email })
|
|
133
|
+
] }),
|
|
134
|
+
/* @__PURE__ */ jsx(ChevronsUpDown, { className: "ml-auto size-4" })
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
) }),
|
|
138
|
+
/* @__PURE__ */ jsxs(
|
|
139
|
+
DropdownMenuContent,
|
|
140
|
+
{
|
|
141
|
+
className: "w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded",
|
|
142
|
+
side: isMobile ? "bottom" : "right",
|
|
143
|
+
align: "end",
|
|
144
|
+
sideOffset: 4,
|
|
145
|
+
children: [
|
|
146
|
+
/* @__PURE__ */ jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-1 py-1.5 text-left text-sm", children: [
|
|
147
|
+
/* @__PURE__ */ jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [
|
|
148
|
+
/* @__PURE__ */ jsx(AvatarImage, { src: user.profilePictureUrl ?? void 0, alt: displayName }),
|
|
149
|
+
/* @__PURE__ */ jsx(AvatarFallback, { className: "rounded-lg", children: initials })
|
|
150
|
+
] }),
|
|
151
|
+
/* @__PURE__ */ jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [
|
|
152
|
+
/* @__PURE__ */ jsx("span", { className: "truncate font-semibold", children: displayName }),
|
|
153
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: user.email })
|
|
154
|
+
] })
|
|
155
|
+
] }) }),
|
|
156
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
157
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, { children: [
|
|
158
|
+
/* @__PURE__ */ jsx(User, { className: "mr-2 size-4" }),
|
|
159
|
+
"Account"
|
|
160
|
+
] }),
|
|
161
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, { children: [
|
|
162
|
+
/* @__PURE__ */ jsx(Settings, { className: "mr-2 size-4" }),
|
|
163
|
+
"Settings"
|
|
164
|
+
] }),
|
|
165
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
166
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => signOut(), children: [
|
|
167
|
+
/* @__PURE__ */ jsx(LogOut, { className: "mr-2 size-4" }),
|
|
168
|
+
"Sign Out"
|
|
169
|
+
] })
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
] });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export { AuthGate, DOConfigProvider, UserMenu, useDOConfig, useDOCustomRoutes, useDORoutes };
|
|
177
|
+
//# sourceMappingURL=chunk-7J3RSIG4.js.map
|
|
178
|
+
//# sourceMappingURL=chunk-7J3RSIG4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/features/shell/config-context.tsx","../src/features/auth/auth-gate.tsx","../src/features/auth/user-menu.tsx"],"names":["jsx","useAuth","jsxs","UserIcon"],"mappings":"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","file":"chunk-7J3RSIG4.js","sourcesContent":["'use client'\n\n/**\n * DO Config Context\n *\n * Provides static configuration (branding, routes, identity) to shell components.\n * Dynamic state (endpoint, namespace) is managed via TanStack Query in ../state/\n */\n\nimport { createContext, type ReactNode, useContext, useMemo } from 'react'\nimport type { DOShellConfig } from '../../types/config'\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Static config context value (no state, just config)\n */\nexport interface DOConfigContextValue {\n /** Merged shell configuration */\n config: DOShellConfig\n}\n\n// ============================================================================\n// Context\n// ============================================================================\n\nconst DOConfigContext = createContext<DOConfigContextValue | null>(null)\n\n// ============================================================================\n// Defaults\n// ============================================================================\n\nconst defaultIdentity = {\n clientId: '',\n devMode: false,\n required: true,\n onUnauthenticated: 'landing' as const,\n}\n\nconst defaultConfig = {\n basePath: '/',\n defaultNamespace: 'default',\n branding: { name: 'DO Admin' },\n theme: { mode: 'system' as const },\n routes: {},\n customRoutes: [],\n}\n\n// ============================================================================\n// Provider\n// ============================================================================\n\ninterface DOConfigProviderProps {\n config: DOShellConfig\n children: ReactNode\n}\n\n/**\n * DOConfigProvider - Provides static shell configuration\n *\n * This is a minimal context for static config only.\n * Dynamic state (endpoint, namespace) is managed via TanStack Query.\n */\nexport function DOConfigProvider({ config, children }: DOConfigProviderProps) {\n const value = useMemo<DOConfigContextValue>(() => {\n const mergedConfig: DOShellConfig = {\n ...defaultConfig,\n ...config,\n branding: { ...defaultConfig.branding, ...config.branding },\n theme: { ...defaultConfig.theme, ...config.theme },\n identity: { ...defaultIdentity, ...config.identity },\n }\n\n return { config: mergedConfig }\n }, [config])\n\n return <DOConfigContext.Provider value={value}>{children}</DOConfigContext.Provider>\n}\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\n/**\n * Hook to access static shell configuration\n */\nexport function useDOConfig(): DOConfigContextValue {\n const context = useContext(DOConfigContext)\n if (!context) {\n throw new Error('useDOConfig must be used within a DOConfigProvider')\n }\n return context\n}\n\n/**\n * Hook to access enabled routes configuration\n */\nexport function useDORoutes() {\n const { config } = useDOConfig()\n return config.routes ?? {}\n}\n\n/**\n * Hook to access custom routes\n */\nexport function useDOCustomRoutes() {\n const { config } = useDOConfig()\n return config.customRoutes ?? []\n}\n","'use client'\n\nimport { useAuth } from '@mdxui/auth'\nimport type { ReactNode } from 'react'\nimport { useDOConfig } from '../shell/config-context'\n\nexport interface AuthGateProps {\n children: ReactNode\n}\n\n/**\n * Default landing page component shown when user is not authenticated\n */\nfunction DefaultLandingPage() {\n const { signIn } = useAuth()\n\n return (\n <div className=\"min-h-screen bg-background flex flex-col items-center justify-center p-4\">\n <div className=\"text-center max-w-md\">\n <h1 className=\"text-3xl font-bold tracking-tight mb-4\">DO Admin</h1>\n <p className=\"text-muted-foreground mb-8\">\n Sign in to access the admin dashboard and manage your Durable Objects.\n </p>\n <button\n type=\"button\"\n onClick={() => signIn()}\n className=\"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-6\"\n >\n Sign In\n </button>\n </div>\n </div>\n )\n}\n\n/**\n * AuthGate handles authentication state and shows appropriate UI:\n * - Loading state while checking auth\n * - Landing page or redirect when not authenticated\n * - Children (dashboard) when authenticated\n */\nexport function AuthGate({ children }: AuthGateProps) {\n const { config } = useDOConfig()\n const { user, isLoading } = useAuth()\n\n const { identity } = config\n const required = identity.required ?? true\n const onUnauthenticated = identity.onUnauthenticated ?? 'landing'\n\n // If auth is not required, always show children\n if (!required) {\n return <>{children}</>\n }\n\n // Show loading state while checking authentication\n if (isLoading) {\n return (\n <div className=\"min-h-screen bg-background flex items-center justify-center\">\n <div className=\"flex flex-col items-center gap-4\">\n <div className=\"h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent\" />\n <p className=\"text-sm text-muted-foreground\">Loading...</p>\n </div>\n </div>\n )\n }\n\n // User is authenticated, show dashboard\n if (user) {\n return <>{children}</>\n }\n\n // User is not authenticated - handle based on config\n switch (onUnauthenticated) {\n case 'redirect':\n // Redirect to external URL\n if (identity.unauthenticatedRedirectUrl && typeof window !== 'undefined') {\n window.location.href = identity.unauthenticatedRedirectUrl\n return (\n <div className=\"min-h-screen bg-background flex items-center justify-center\">\n <p className=\"text-sm text-muted-foreground\">Redirecting...</p>\n </div>\n )\n }\n // Fall through to landing if no redirect URL\n return identity.landingComponent ? identity.landingComponent : <DefaultLandingPage />\n\n case 'allow':\n // Allow access even without auth (rare use case)\n return <>{children}</>\n\n case 'landing':\n default:\n // Show landing page (custom or default)\n return identity.landingComponent ? identity.landingComponent : <DefaultLandingPage />\n }\n}\n","'use client'\n\nimport { useAuth } from '@mdxui/auth'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n SidebarMenuButton,\n Avatar,\n AvatarFallback,\n AvatarImage,\n useSidebar\n} from '@mdxui/primitives'\nimport { ChevronsUpDown, LogOut, Settings, User as UserIcon } from 'lucide-react'\n\ninterface UserMenuProps {\n /** Whether to show in compact mode (icon only) */\n compact?: boolean\n}\n\n/**\n * Get initials from a name (first letter of first and last name)\n */\nfunction getInitials(name: string | undefined): string {\n if (!name) return '?'\n const parts = name.trim().split(/\\s+/)\n if (parts.length === 1) return parts[0].charAt(0).toUpperCase()\n return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase()\n}\n\n/**\n * UserMenu - User dropdown with profile info and sign out\n *\n * Displays the current user's avatar and name, with options for\n * settings and signing out.\n */\nexport function UserMenu({ compact = false }: UserMenuProps) {\n const { user, signOut, isLoading } = useAuth()\n const { isMobile } = useSidebar()\n\n if (isLoading) {\n return (\n <div className=\"flex items-center gap-2 p-2\">\n <div className=\"h-8 w-8 animate-pulse rounded-full bg-muted\" />\n {!compact && <div className=\"h-4 w-24 animate-pulse rounded bg-muted\" />}\n </div>\n )\n }\n\n if (!user) {\n return null\n }\n\n const displayName = user.firstName\n ? `${user.firstName}${user.lastName ? ` ${user.lastName}` : ''}`\n : user.email\n\n const initials = getInitials(user.firstName ? `${user.firstName} ${user.lastName ?? ''}` : user.email)\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <SidebarMenuButton\n size=\"lg\"\n className=\"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground\"\n >\n <Avatar className=\"h-8 w-8 rounded-lg\">\n <AvatarImage src={user.profilePictureUrl ?? undefined} alt={displayName} />\n <AvatarFallback className=\"rounded-lg\">{initials}</AvatarFallback>\n </Avatar>\n {!compact && (\n <div className=\"grid flex-1 text-left text-sm leading-tight\">\n <span className=\"truncate font-semibold\">{displayName}</span>\n <span className=\"truncate text-xs text-muted-foreground\">{user.email}</span>\n </div>\n )}\n <ChevronsUpDown className=\"ml-auto size-4\" />\n </SidebarMenuButton>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n className=\"w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded\"\n side={isMobile ? 'bottom' : 'right'}\n align=\"end\"\n sideOffset={4}\n >\n <DropdownMenuLabel className=\"p-0 font-normal\">\n <div className=\"flex items-center gap-2 px-1 py-1.5 text-left text-sm\">\n <Avatar className=\"h-8 w-8 rounded-lg\">\n <AvatarImage src={user.profilePictureUrl ?? undefined} alt={displayName} />\n <AvatarFallback className=\"rounded-lg\">{initials}</AvatarFallback>\n </Avatar>\n <div className=\"grid flex-1 text-left text-sm leading-tight\">\n <span className=\"truncate font-semibold\">{displayName}</span>\n <span className=\"truncate text-xs text-muted-foreground\">{user.email}</span>\n </div>\n </div>\n </DropdownMenuLabel>\n <DropdownMenuSeparator />\n <DropdownMenuItem>\n <UserIcon className=\"mr-2 size-4\" />\n Account\n </DropdownMenuItem>\n <DropdownMenuItem>\n <Settings className=\"mr-2 size-4\" />\n Settings\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={() => signOut()}>\n <LogOut className=\"mr-2 size-4\" />\n Sign Out\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"]}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Input, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandSeparator } from '@mdxui/primitives';
|
|
2
|
+
import { Search, Globe, Server, History } from 'lucide-react';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/components/endpoint-selector.tsx
|
|
7
|
+
function useIsMac() {
|
|
8
|
+
const [isMac, setIsMac] = React.useState(false);
|
|
9
|
+
React.useEffect(() => {
|
|
10
|
+
setIsMac(navigator.platform.toUpperCase().indexOf("MAC") >= 0);
|
|
11
|
+
}, []);
|
|
12
|
+
return isMac;
|
|
13
|
+
}
|
|
14
|
+
function looksLikeUrl(str) {
|
|
15
|
+
const trimmed = str.trim();
|
|
16
|
+
if (!trimmed) return false;
|
|
17
|
+
return trimmed.includes("://") || trimmed.includes(".") || trimmed.startsWith("localhost");
|
|
18
|
+
}
|
|
19
|
+
function normalizeUrl(url) {
|
|
20
|
+
const trimmed = url.trim();
|
|
21
|
+
if (!trimmed) return trimmed;
|
|
22
|
+
if (trimmed.includes("://")) return trimmed;
|
|
23
|
+
if (trimmed.startsWith("localhost")) return `http://${trimmed}`;
|
|
24
|
+
return `https://${trimmed}`;
|
|
25
|
+
}
|
|
26
|
+
function formatEndpoint(url) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = new URL(url);
|
|
29
|
+
return parsed.hostname;
|
|
30
|
+
} catch {
|
|
31
|
+
return url;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function EndpointSelector({
|
|
35
|
+
endpoint,
|
|
36
|
+
onEndpointChange,
|
|
37
|
+
recentEndpoints = [],
|
|
38
|
+
placeholder = "Connect to DO...",
|
|
39
|
+
className
|
|
40
|
+
}) {
|
|
41
|
+
const [open, setOpen] = React.useState(false);
|
|
42
|
+
const [search, setSearch] = React.useState("");
|
|
43
|
+
const isMac = useIsMac();
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
if (!open) {
|
|
46
|
+
setSearch("");
|
|
47
|
+
}
|
|
48
|
+
}, [open]);
|
|
49
|
+
React.useEffect(() => {
|
|
50
|
+
function handleKeyDown(e) {
|
|
51
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
setOpen((prev) => !prev);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
57
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
58
|
+
}, []);
|
|
59
|
+
const handleSelect = React.useCallback(
|
|
60
|
+
(url) => {
|
|
61
|
+
setOpen(false);
|
|
62
|
+
setSearch("");
|
|
63
|
+
setTimeout(() => onEndpointChange(url), 0);
|
|
64
|
+
},
|
|
65
|
+
[onEndpointChange]
|
|
66
|
+
);
|
|
67
|
+
const allEndpoints = React.useMemo(() => {
|
|
68
|
+
const endpoints = [endpoint, ...recentEndpoints];
|
|
69
|
+
return [...new Set(endpoints)];
|
|
70
|
+
}, [endpoint, recentEndpoints]);
|
|
71
|
+
const isNewUrl = React.useMemo(() => {
|
|
72
|
+
if (!looksLikeUrl(search)) return false;
|
|
73
|
+
const normalized = normalizeUrl(search);
|
|
74
|
+
return !allEndpoints.some(
|
|
75
|
+
(ep) => ep === normalized || ep === search || formatEndpoint(ep).toLowerCase().includes(search.toLowerCase())
|
|
76
|
+
);
|
|
77
|
+
}, [search, allEndpoints]);
|
|
78
|
+
const filteredRecent = recentEndpoints.filter((url) => url !== endpoint);
|
|
79
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
80
|
+
/* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
|
|
81
|
+
/* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" }),
|
|
82
|
+
/* @__PURE__ */ jsx(
|
|
83
|
+
Input,
|
|
84
|
+
{
|
|
85
|
+
type: "text",
|
|
86
|
+
placeholder,
|
|
87
|
+
value: formatEndpoint(endpoint),
|
|
88
|
+
readOnly: true,
|
|
89
|
+
onClick: () => setOpen(true),
|
|
90
|
+
className: "w-full cursor-pointer pl-9 pr-12 shadow-none"
|
|
91
|
+
}
|
|
92
|
+
),
|
|
93
|
+
/* @__PURE__ */ jsx("kbd", { className: "pointer-events-none absolute right-3 top-1/2 hidden -translate-y-1/2 select-none rounded-sm border border-border bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground sm:inline-block", children: isMac ? "\u2318K" : "Ctrl+K" })
|
|
94
|
+
] }) }),
|
|
95
|
+
/* @__PURE__ */ jsxs(
|
|
96
|
+
CommandDialog,
|
|
97
|
+
{
|
|
98
|
+
open,
|
|
99
|
+
onOpenChange: setOpen,
|
|
100
|
+
title: "Connect to DO",
|
|
101
|
+
description: "Select a DO server endpoint to connect to",
|
|
102
|
+
children: [
|
|
103
|
+
/* @__PURE__ */ jsx(
|
|
104
|
+
CommandInput,
|
|
105
|
+
{
|
|
106
|
+
placeholder: "Search or enter endpoint URL...",
|
|
107
|
+
value: search,
|
|
108
|
+
onValueChange: setSearch
|
|
109
|
+
}
|
|
110
|
+
),
|
|
111
|
+
/* @__PURE__ */ jsxs(CommandList, { children: [
|
|
112
|
+
/* @__PURE__ */ jsx(CommandEmpty, { children: looksLikeUrl(search) ? /* @__PURE__ */ jsxs(
|
|
113
|
+
"button",
|
|
114
|
+
{
|
|
115
|
+
type: "button",
|
|
116
|
+
onClick: () => handleSelect(normalizeUrl(search)),
|
|
117
|
+
className: "flex w-full items-center gap-2 rounded-sm px-2 py-3 text-sm hover:bg-accent",
|
|
118
|
+
children: [
|
|
119
|
+
/* @__PURE__ */ jsx(Globe, { className: "size-4 text-primary" }),
|
|
120
|
+
/* @__PURE__ */ jsx("span", { children: "Connect to " }),
|
|
121
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: normalizeUrl(search) })
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
) : /* @__PURE__ */ jsx("div", { className: "py-6 text-center text-sm text-muted-foreground", children: "Enter a URL to connect to a new endpoint" }) }),
|
|
125
|
+
isNewUrl && /* @__PURE__ */ jsx(CommandGroup, { children: /* @__PURE__ */ jsxs(
|
|
126
|
+
CommandItem,
|
|
127
|
+
{
|
|
128
|
+
value: `connect-new-${search}`,
|
|
129
|
+
onSelect: () => handleSelect(normalizeUrl(search)),
|
|
130
|
+
children: [
|
|
131
|
+
/* @__PURE__ */ jsx(Globe, { className: "text-primary" }),
|
|
132
|
+
/* @__PURE__ */ jsx("span", { children: "Connect to " }),
|
|
133
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: normalizeUrl(search) })
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
) }),
|
|
137
|
+
/* @__PURE__ */ jsx(CommandGroup, { heading: "Current", children: /* @__PURE__ */ jsxs(
|
|
138
|
+
CommandItem,
|
|
139
|
+
{
|
|
140
|
+
value: endpoint,
|
|
141
|
+
onSelect: () => handleSelect(endpoint),
|
|
142
|
+
children: [
|
|
143
|
+
/* @__PURE__ */ jsx(Server, { className: "text-primary" }),
|
|
144
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1 font-medium", children: formatEndpoint(endpoint) }),
|
|
145
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: "Connected" })
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
) }),
|
|
149
|
+
filteredRecent.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
150
|
+
/* @__PURE__ */ jsx(CommandSeparator, {}),
|
|
151
|
+
/* @__PURE__ */ jsx(CommandGroup, { heading: "Recent", children: filteredRecent.map((url) => /* @__PURE__ */ jsxs(
|
|
152
|
+
CommandItem,
|
|
153
|
+
{
|
|
154
|
+
value: url,
|
|
155
|
+
onSelect: () => handleSelect(url),
|
|
156
|
+
children: [
|
|
157
|
+
/* @__PURE__ */ jsx(History, { className: "text-muted-foreground" }),
|
|
158
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1", children: formatEndpoint(url) })
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
url
|
|
162
|
+
)) })
|
|
163
|
+
] })
|
|
164
|
+
] })
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
] });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export { EndpointSelector };
|
|
172
|
+
//# sourceMappingURL=chunk-7QCTRNEJ.js.map
|
|
173
|
+
//# sourceMappingURL=chunk-7QCTRNEJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/endpoint-selector.tsx"],"names":[],"mappings":"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","file":"chunk-7QCTRNEJ.js","sourcesContent":["\"use client\"\n\n/**\n * EndpointSelector Component\n *\n * A command palette-style input for selecting/changing the DO server endpoint.\n * Supports keyboard shortcuts (⌘K on Mac, Ctrl+K on Windows).\n */\n\nimport {\n CommandDialog,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n Input,\n} from \"@mdxui/primitives\"\nimport { Globe, History, Search, Server } from \"lucide-react\"\nimport * as React from \"react\"\n\nexport interface EndpointSelectorProps {\n /** Current endpoint URL */\n endpoint: string\n /** Callback when endpoint changes */\n onEndpointChange: (endpoint: string) => void\n /** Recent endpoints for quick selection */\n recentEndpoints?: string[]\n /** Placeholder text for the trigger input */\n placeholder?: string\n /** Additional class name for the trigger */\n className?: string\n}\n\n/**\n * Detects if the current platform is Mac\n */\nfunction useIsMac() {\n const [isMac, setIsMac] = React.useState(false)\n\n React.useEffect(() => {\n setIsMac(navigator.platform.toUpperCase().indexOf(\"MAC\") >= 0)\n }, [])\n\n return isMac\n}\n\n/**\n * Check if a string looks like a URL\n */\nfunction looksLikeUrl(str: string): boolean {\n const trimmed = str.trim()\n if (!trimmed) return false\n // Contains protocol or looks like a domain\n return (\n trimmed.includes(\"://\") ||\n trimmed.includes(\".\") ||\n trimmed.startsWith(\"localhost\")\n )\n}\n\n/**\n * Normalize URL - add https:// if no protocol specified\n */\nfunction normalizeUrl(url: string): string {\n const trimmed = url.trim()\n if (!trimmed) return trimmed\n if (trimmed.includes(\"://\")) return trimmed\n if (trimmed.startsWith(\"localhost\")) return `http://${trimmed}`\n return `https://${trimmed}`\n}\n\n/**\n * Format endpoint URL for display (show just the hostname)\n */\nfunction formatEndpoint(url: string): string {\n try {\n const parsed = new URL(url)\n return parsed.hostname\n } catch {\n return url\n }\n}\n\n/**\n * EndpointSelector - Search-like input that opens a command palette for endpoint selection\n */\nexport function EndpointSelector({\n endpoint,\n onEndpointChange,\n recentEndpoints = [],\n placeholder = \"Connect to DO...\",\n className,\n}: EndpointSelectorProps) {\n const [open, setOpen] = React.useState(false)\n const [search, setSearch] = React.useState(\"\")\n const isMac = useIsMac()\n\n // Reset search when dialog closes\n React.useEffect(() => {\n if (!open) {\n setSearch(\"\")\n }\n }, [open])\n\n // Global keyboard shortcut handler\n React.useEffect(() => {\n function handleKeyDown(e: KeyboardEvent) {\n // ⌘K on Mac, Ctrl+K on Windows/Linux\n if ((e.metaKey || e.ctrlKey) && e.key === \"k\") {\n e.preventDefault()\n setOpen((prev) => !prev)\n }\n }\n\n document.addEventListener(\"keydown\", handleKeyDown)\n return () => document.removeEventListener(\"keydown\", handleKeyDown)\n }, [])\n\n // Handle selecting an endpoint\n const handleSelect = React.useCallback(\n (url: string) => {\n // Close dialog and clear search first for responsive UI\n setOpen(false)\n setSearch(\"\")\n // Defer endpoint change to allow dialog to close first\n setTimeout(() => onEndpointChange(url), 0)\n },\n [onEndpointChange]\n )\n\n // All known endpoints (current + recent, deduplicated)\n const allEndpoints = React.useMemo(() => {\n const endpoints = [endpoint, ...recentEndpoints]\n return [...new Set(endpoints)]\n }, [endpoint, recentEndpoints])\n\n // Check if search input looks like a new URL not in our list\n const isNewUrl = React.useMemo(() => {\n if (!looksLikeUrl(search)) return false\n const normalized = normalizeUrl(search)\n // Check if it matches any existing endpoint\n return !allEndpoints.some(\n (ep) =>\n ep === normalized ||\n ep === search ||\n formatEndpoint(ep).toLowerCase().includes(search.toLowerCase())\n )\n }, [search, allEndpoints])\n\n // Filter recent endpoints (exclude current)\n const filteredRecent = recentEndpoints.filter((url) => url !== endpoint)\n\n return (\n <>\n {/* Trigger - search-like input */}\n <div className={className}>\n <div className=\"relative w-full\">\n <Search className=\"absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground\" />\n <Input\n type=\"text\"\n placeholder={placeholder}\n value={formatEndpoint(endpoint)}\n readOnly\n onClick={() => setOpen(true)}\n className=\"w-full cursor-pointer pl-9 pr-12 shadow-none\"\n />\n <kbd className=\"pointer-events-none absolute right-3 top-1/2 hidden -translate-y-1/2 select-none rounded-sm border border-border bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground sm:inline-block\">\n {isMac ? \"⌘K\" : \"Ctrl+K\"}\n </kbd>\n </div>\n </div>\n\n {/* Command Dialog */}\n <CommandDialog\n open={open}\n onOpenChange={setOpen}\n title=\"Connect to DO\"\n description=\"Select a DO server endpoint to connect to\"\n >\n <CommandInput\n placeholder=\"Search or enter endpoint URL...\"\n value={search}\n onValueChange={setSearch}\n />\n <CommandList>\n <CommandEmpty>\n {looksLikeUrl(search) ? (\n <button\n type=\"button\"\n onClick={() => handleSelect(normalizeUrl(search))}\n className=\"flex w-full items-center gap-2 rounded-sm px-2 py-3 text-sm hover:bg-accent\"\n >\n <Globe className=\"size-4 text-primary\" />\n <span>Connect to </span>\n <span className=\"font-medium\">{normalizeUrl(search)}</span>\n </button>\n ) : (\n <div className=\"py-6 text-center text-sm text-muted-foreground\">\n Enter a URL to connect to a new endpoint\n </div>\n )}\n </CommandEmpty>\n\n {/* Option to connect to new URL (shown when typing a URL-like string) */}\n {isNewUrl && (\n <CommandGroup>\n <CommandItem\n value={`connect-new-${search}`}\n onSelect={() => handleSelect(normalizeUrl(search))}\n >\n <Globe className=\"text-primary\" />\n <span>Connect to </span>\n <span className=\"font-medium\">{normalizeUrl(search)}</span>\n </CommandItem>\n </CommandGroup>\n )}\n\n {/* Current endpoint */}\n <CommandGroup heading=\"Current\">\n <CommandItem\n value={endpoint}\n onSelect={() => handleSelect(endpoint)}\n >\n <Server className=\"text-primary\" />\n <span className=\"flex-1 font-medium\">\n {formatEndpoint(endpoint)}\n </span>\n <span className=\"text-xs text-muted-foreground\">Connected</span>\n </CommandItem>\n </CommandGroup>\n\n {/* Recent endpoints */}\n {filteredRecent.length > 0 && (\n <>\n <CommandSeparator />\n <CommandGroup heading=\"Recent\">\n {filteredRecent.map((url) => (\n <CommandItem\n key={url}\n value={url}\n onSelect={() => handleSelect(url)}\n >\n <History className=\"text-muted-foreground\" />\n <span className=\"flex-1\">{formatEndpoint(url)}</span>\n </CommandItem>\n ))}\n </CommandGroup>\n </>\n )}\n </CommandList>\n </CommandDialog>\n </>\n )\n}\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useWebLocation } from '@mdxui/navigation/web';
|
|
2
|
+
|
|
3
|
+
// src/hooks/use-breadcrumbs.ts
|
|
4
|
+
var ROUTE_LABELS = {
|
|
5
|
+
// Main
|
|
6
|
+
"/": "Overview",
|
|
7
|
+
// Development
|
|
8
|
+
"/agents": "Agents",
|
|
9
|
+
"/functions": "Functions",
|
|
10
|
+
"/workflows": "Workflows",
|
|
11
|
+
"/databases": "Databases",
|
|
12
|
+
// Data Model
|
|
13
|
+
"/things": "Things",
|
|
14
|
+
"/nouns": "Nouns",
|
|
15
|
+
"/verbs": "Verbs",
|
|
16
|
+
"/actions": "Actions",
|
|
17
|
+
"/relationships": "Relationships",
|
|
18
|
+
"/events": "Events",
|
|
19
|
+
// SaaS
|
|
20
|
+
"/customers": "Customers",
|
|
21
|
+
"/subscriptions": "Subscriptions",
|
|
22
|
+
"/integrations": "Integrations",
|
|
23
|
+
// Admin
|
|
24
|
+
"/orgs": "Organizations",
|
|
25
|
+
"/roles": "Roles",
|
|
26
|
+
"/users": "Users",
|
|
27
|
+
"/api-keys": "API Keys",
|
|
28
|
+
"/settings": "Settings"
|
|
29
|
+
};
|
|
30
|
+
function segmentToLabel(segment) {
|
|
31
|
+
const knownLabel = ROUTE_LABELS[`/${segment}`];
|
|
32
|
+
if (knownLabel) return knownLabel;
|
|
33
|
+
return segment.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
34
|
+
}
|
|
35
|
+
function useBreadcrumbs(options) {
|
|
36
|
+
const { rootLabel = "Admin", includeRoot = true } = options ?? {};
|
|
37
|
+
const location = useWebLocation();
|
|
38
|
+
const pathname = location.pathname;
|
|
39
|
+
const breadcrumbs = [];
|
|
40
|
+
if (pathname === "/" || pathname === "") {
|
|
41
|
+
if (includeRoot) {
|
|
42
|
+
breadcrumbs.push({ label: rootLabel });
|
|
43
|
+
}
|
|
44
|
+
return breadcrumbs;
|
|
45
|
+
}
|
|
46
|
+
if (includeRoot) {
|
|
47
|
+
breadcrumbs.push({ label: rootLabel, href: "/" });
|
|
48
|
+
}
|
|
49
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
50
|
+
let currentPath = "";
|
|
51
|
+
segments.forEach((segment, index) => {
|
|
52
|
+
currentPath += `/${segment}`;
|
|
53
|
+
const isLast = index === segments.length - 1;
|
|
54
|
+
const label = ROUTE_LABELS[currentPath] || segmentToLabel(segment);
|
|
55
|
+
breadcrumbs.push({
|
|
56
|
+
label,
|
|
57
|
+
// Last item doesn't get a href (it's the current page)
|
|
58
|
+
href: isLast ? void 0 : currentPath
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
return breadcrumbs;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { useBreadcrumbs };
|
|
65
|
+
//# sourceMappingURL=chunk-7TFHUKS7.js.map
|
|
66
|
+
//# sourceMappingURL=chunk-7TFHUKS7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/use-breadcrumbs.ts"],"names":[],"mappings":";;;AAeA,IAAM,YAAA,GAAuC;AAAA;AAAA,EAE3C,GAAA,EAAK,UAAA;AAAA;AAAA,EAGL,SAAA,EAAW,QAAA;AAAA,EACX,YAAA,EAAc,WAAA;AAAA,EACd,YAAA,EAAc,WAAA;AAAA,EACd,YAAA,EAAc,WAAA;AAAA;AAAA,EAGd,SAAA,EAAW,QAAA;AAAA,EACX,QAAA,EAAU,OAAA;AAAA,EACV,QAAA,EAAU,OAAA;AAAA,EACV,UAAA,EAAY,SAAA;AAAA,EACZ,gBAAA,EAAkB,eAAA;AAAA,EAClB,SAAA,EAAW,QAAA;AAAA;AAAA,EAGX,YAAA,EAAc,WAAA;AAAA,EACd,gBAAA,EAAkB,eAAA;AAAA,EAClB,eAAA,EAAiB,cAAA;AAAA;AAAA,EAGjB,OAAA,EAAS,eAAA;AAAA,EACT,QAAA,EAAU,OAAA;AAAA,EACV,QAAA,EAAU,OAAA;AAAA,EACV,WAAA,EAAa,UAAA;AAAA,EACb,WAAA,EAAa;AACf,CAAA;AAMA,SAAS,eAAe,OAAA,EAAyB;AAE/C,EAAA,MAAM,UAAA,GAAa,YAAA,CAAa,CAAA,CAAA,EAAI,OAAO,CAAA,CAAE,CAAA;AAC7C,EAAA,IAAI,YAAY,OAAO,UAAA;AAGvB,EAAA,OAAO,QACJ,KAAA,CAAM,GAAG,EACT,GAAA,CAAI,CAAC,SAAS,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,CAAE,WAAA,KAAgB,IAAA,CAAK,KAAA,CAAM,CAAC,CAAC,CAAA,CAC1D,KAAK,GAAG,CAAA;AACb;AAyBO,SAAS,eAAe,OAAA,EAAyD;AACtF,EAAA,MAAM,EAAE,SAAA,GAAY,OAAA,EAAS,cAAc,IAAA,EAAK,GAAI,WAAW,EAAC;AAChE,EAAA,MAAM,WAAW,cAAA,EAAe;AAChC,EAAA,MAAM,WAAW,QAAA,CAAS,QAAA;AAG1B,EAAA,MAAM,cAAsC,EAAC;AAG7C,EAAA,IAAI,QAAA,KAAa,GAAA,IAAO,QAAA,KAAa,EAAA,EAAI;AACvC,IAAA,IAAI,WAAA,EAAa;AACf,MAAA,WAAA,CAAY,IAAA,CAAK,EAAE,KAAA,EAAO,SAAA,EAAW,CAAA;AAAA,IACvC;AACA,IAAA,OAAO,WAAA;AAAA,EACT;AAGA,EAAA,IAAI,WAAA,EAAa;AACf,IAAA,WAAA,CAAY,KAAK,EAAE,KAAA,EAAO,SAAA,EAAW,IAAA,EAAM,KAAK,CAAA;AAAA,EAClD;AAGA,EAAA,MAAM,WAAW,QAAA,CAAS,KAAA,CAAM,GAAG,CAAA,CAAE,OAAO,OAAO,CAAA;AAGnD,EAAA,IAAI,WAAA,GAAc,EAAA;AAClB,EAAA,QAAA,CAAS,OAAA,CAAQ,CAAC,OAAA,EAAS,KAAA,KAAU;AACnC,IAAA,WAAA,IAAe,IAAI,OAAO,CAAA,CAAA;AAC1B,IAAA,MAAM,MAAA,GAAS,KAAA,KAAU,QAAA,CAAS,MAAA,GAAS,CAAA;AAG3C,IAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,WAAW,CAAA,IAAK,eAAe,OAAO,CAAA;AAEjE,IAAA,WAAA,CAAY,IAAA,CAAK;AAAA,MACf,KAAA;AAAA;AAAA,MAEA,IAAA,EAAM,SAAS,MAAA,GAAY;AAAA,KAC5B,CAAA;AAAA,EACH,CAAC,CAAA;AAED,EAAA,OAAO,WAAA;AACT","file":"chunk-7TFHUKS7.js","sourcesContent":["'use client'\n\n/**\n * useBreadcrumbs Hook\n *\n * Auto-generates breadcrumbs from the current TanStack Router route.\n */\n\nimport { useWebLocation } from '@mdxui/navigation/web'\nimport type { BreadcrumbItemConfig } from '../components/breadcrumbs'\n\n/**\n * Route label mapping for human-readable breadcrumb labels\n * Maps route paths/segments to display labels\n */\nconst ROUTE_LABELS: Record<string, string> = {\n // Main\n '/': 'Overview',\n\n // Development\n '/agents': 'Agents',\n '/functions': 'Functions',\n '/workflows': 'Workflows',\n '/databases': 'Databases',\n\n // Data Model\n '/things': 'Things',\n '/nouns': 'Nouns',\n '/verbs': 'Verbs',\n '/actions': 'Actions',\n '/relationships': 'Relationships',\n '/events': 'Events',\n\n // SaaS\n '/customers': 'Customers',\n '/subscriptions': 'Subscriptions',\n '/integrations': 'Integrations',\n\n // Admin\n '/orgs': 'Organizations',\n '/roles': 'Roles',\n '/users': 'Users',\n '/api-keys': 'API Keys',\n '/settings': 'Settings',\n}\n\n/**\n * Converts a path segment to a human-readable label\n * e.g., \"api-keys\" → \"API Keys\", \"users\" → \"Users\"\n */\nfunction segmentToLabel(segment: string): string {\n // Check if it's a known route\n const knownLabel = ROUTE_LABELS[`/${segment}`]\n if (knownLabel) return knownLabel\n\n // Convert kebab-case to Title Case\n return segment\n .split('-')\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n}\n\nexport interface UseBreadcrumbsOptions {\n /** Label for the root breadcrumb (default: \"Admin\") */\n rootLabel?: string\n /** Whether to include root breadcrumb (default: true) */\n includeRoot?: boolean\n}\n\n/**\n * Hook to auto-generate breadcrumbs from the current route\n *\n * @param options.rootLabel - Label for the root breadcrumb (default: \"Admin\")\n * @param options.includeRoot - Whether to include root breadcrumb (default: true)\n * @returns Array of breadcrumb items\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const breadcrumbs = useBreadcrumbs()\n * // On /users: [{ label: 'Admin', href: '/' }, { label: 'Users' }]\n * return <Breadcrumbs items={breadcrumbs} LinkComponent={RouterLink} />\n * }\n * ```\n */\nexport function useBreadcrumbs(options?: UseBreadcrumbsOptions): BreadcrumbItemConfig[] {\n const { rootLabel = 'Admin', includeRoot = true } = options ?? {}\n const location = useWebLocation()\n const pathname = location.pathname\n\n // Build breadcrumb items\n const breadcrumbs: BreadcrumbItemConfig[] = []\n\n // On home page, just show root label (no href since we're on it)\n if (pathname === '/' || pathname === '') {\n if (includeRoot) {\n breadcrumbs.push({ label: rootLabel })\n }\n return breadcrumbs\n }\n\n // Add root with link to home\n if (includeRoot) {\n breadcrumbs.push({ label: rootLabel, href: '/' })\n }\n\n // Split pathname into segments\n const segments = pathname.split('/').filter(Boolean)\n\n // Build path progressively\n let currentPath = ''\n segments.forEach((segment, index) => {\n currentPath += `/${segment}`\n const isLast = index === segments.length - 1\n\n // Get label from known routes or convert segment\n const label = ROUTE_LABELS[currentPath] || segmentToLabel(segment)\n\n breadcrumbs.push({\n label,\n // Last item doesn't get a href (it's the current page)\n href: isLast ? undefined : currentPath,\n })\n })\n\n return breadcrumbs\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cn } from './chunk-
|
|
1
|
+
import { cn } from './chunk-JPZ6RZJE.js';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
4
|
|
|
@@ -91,5 +91,5 @@ function useErrorBoundary() {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
export { DOErrorBoundary, useErrorBoundary };
|
|
94
|
-
//# sourceMappingURL=chunk-
|
|
95
|
-
//# sourceMappingURL=chunk-
|
|
94
|
+
//# sourceMappingURL=chunk-BU5HMHGQ.js.map
|
|
95
|
+
//# sourceMappingURL=chunk-BU5HMHGQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/error-boundary.tsx"],"names":[],"mappings":"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","file":"chunk-BU5HMHGQ.js","sourcesContent":["'use client'\n\n/**\n * Error Boundary Component for @mdxui/do\n *\n * Catches JavaScript errors anywhere in the child component tree,\n * logs errors, and displays a fallback UI.\n *\n * @example\n * ```tsx\n * <DOErrorBoundary>\n * <ThingsList />\n * </DOErrorBoundary>\n *\n * // With custom fallback\n * <DOErrorBoundary fallback={<CustomErrorUI />}>\n * <Dashboard />\n * </DOErrorBoundary>\n *\n * // With fallback function\n * <DOErrorBoundary\n * fallback={(error, reset) => (\n * <div>\n * <p>Error: {error.message}</p>\n * <button onClick={reset}>Retry</button>\n * </div>\n * )}\n * >\n * <DataView />\n * </DOErrorBoundary>\n * ```\n */\n\nimport * as React from 'react'\nimport { cn } from '../utils'\n\nexport interface DOErrorBoundaryProps {\n /** Child components to render */\n children: React.ReactNode\n /** Custom fallback UI - can be ReactNode or render function */\n fallback?: React.ReactNode | ((error: Error, reset: () => void) => React.ReactNode)\n /** Callback when error is caught */\n onError?: (error: Error, errorInfo: React.ErrorInfo) => void\n /** Key that triggers reset when changed (useful for route changes) */\n resetKey?: string | number\n /** Additional className for the error container */\n className?: string\n}\n\ninterface DOErrorBoundaryState {\n hasError: boolean\n error: Error | null\n}\n\n/**\n * DOErrorBoundary - Catches and handles errors in child components\n *\n * Features:\n * - Catches render errors in child tree\n * - Displays customizable error UI\n * - Provides retry functionality\n * - Supports error callbacks for logging\n * - Resets on resetKey change (e.g., route changes)\n */\nexport class DOErrorBoundary extends React.Component<\n DOErrorBoundaryProps,\n DOErrorBoundaryState\n> {\n constructor(props: DOErrorBoundaryProps) {\n super(props)\n this.state = { hasError: false, error: null }\n }\n\n static getDerivedStateFromError(error: Error): DOErrorBoundaryState {\n return { hasError: true, error }\n }\n\n componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {\n // Log error to console\n console.error('DOErrorBoundary caught error:', error, errorInfo)\n\n // Call optional error callback\n if (this.props.onError) {\n this.props.onError(error, errorInfo)\n }\n }\n\n componentDidUpdate(prevProps: DOErrorBoundaryProps): void {\n // Reset error state when resetKey changes\n if (\n this.state.hasError &&\n prevProps.resetKey !== this.props.resetKey\n ) {\n this.reset()\n }\n }\n\n reset = (): void => {\n this.setState({ hasError: false, error: null })\n }\n\n render(): React.ReactNode {\n if (this.state.hasError) {\n const { fallback, className } = this.props\n const error = this.state.error ?? new Error('Unknown error')\n\n // Render function fallback\n if (typeof fallback === 'function') {\n return fallback(error, this.reset)\n }\n\n // ReactNode fallback\n if (fallback) {\n return fallback\n }\n\n // Default error UI\n return (\n <div\n className={cn(\n 'p-6 rounded-lg bg-destructive/10 border border-destructive/20',\n className\n )}\n role=\"alert\"\n >\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-shrink-0 mt-0.5\">\n <svg\n className=\"h-5 w-5 text-destructive\"\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n fillRule=\"evenodd\"\n d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z\"\n clipRule=\"evenodd\"\n />\n </svg>\n </div>\n <div className=\"flex-1\">\n <h2 className=\"text-sm font-semibold text-destructive\">\n Something went wrong\n </h2>\n {error.message && (\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {error.message}\n </p>\n )}\n <button\n type=\"button\"\n onClick={this.reset}\n className=\"mt-4 inline-flex items-center rounded-md bg-destructive px-3 py-2 text-sm font-semibold text-destructive-foreground shadow-sm hover:bg-destructive/90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-destructive\"\n >\n Try again\n </button>\n </div>\n </div>\n </div>\n )\n }\n\n return this.props.children\n }\n}\n\n/**\n * Hook to manually trigger error boundary\n * Useful for catching errors in async operations\n */\nexport function useErrorBoundary() {\n const [error, setError] = React.useState<Error | null>(null)\n\n if (error) {\n throw error\n }\n\n return {\n showBoundary: setError,\n }\n}\n\nexport default DOErrorBoundary\n"]}
|