@lukoweb/apitogo 0.1.53 → 0.1.55
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/cli/cli.js +686 -373
- package/dist/declarations/config/apitogo-config-core.d.ts +14 -4
- package/dist/declarations/config/landing-manifest.d.ts +76 -0
- package/dist/declarations/config/local-manifest.d.ts +91 -1
- package/dist/declarations/config/validators/ModulesSchema.d.ts +227 -0
- package/dist/declarations/config/validators/ZudokuConfig.d.ts +84 -2
- package/dist/declarations/lib/authentication/header-nav-filter.d.ts +4 -0
- package/dist/declarations/lib/authentication/providers/dev-portal-auth-pages.d.ts +15 -0
- package/dist/declarations/lib/authentication/providers/dev-portal-constants.d.ts +19 -1
- package/dist/declarations/lib/authentication/providers/dev-portal-utils.d.ts +5 -1
- package/dist/declarations/lib/authentication/providers/dev-portal.d.ts +5 -0
- package/dist/declarations/lib/components/HeaderAuthActions.d.ts +3 -0
- package/dist/declarations/lib/components/SiteTitle.d.ts +6 -0
- package/dist/declarations/lib/components/ThemeSwitch.d.ts +3 -1
- package/dist/declarations/lib/components/TopNavigation.d.ts +1 -1
- package/dist/declarations/lib/core/ZudokuContext.d.ts +1 -0
- package/dist/flat-config.d.ts +61 -1
- package/package.json +1 -1
- package/src/app/main.css +2 -2
- package/src/config/apitogo-config-core.ts +140 -11
- package/src/config/apitogo-config-loader.browser.ts +17 -0
- package/src/config/apitogo-config-loader.node.ts +28 -8
- package/src/config/build-apitogo-config.ts +13 -4
- package/src/config/landing-manifest.ts +11 -0
- package/src/config/landing-openapi-showcase.ts +115 -0
- package/src/config/loader.ts +17 -3
- package/src/config/local-manifest.ts +146 -11
- package/src/config/resolve-modules.ts +22 -1
- package/src/config/validators/ModulesSchema.ts +84 -0
- package/src/config/validators/ZudokuConfig.ts +26 -1
- package/src/lib/authentication/auth-header-nav.ts +5 -15
- package/src/lib/authentication/header-nav-filter.ts +36 -0
- package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +499 -0
- package/src/lib/authentication/providers/dev-portal-constants.ts +15 -1
- package/src/lib/authentication/providers/dev-portal-utils.ts +128 -3
- package/src/lib/authentication/providers/dev-portal.tsx +51 -6
- package/src/lib/components/Header.tsx +49 -39
- package/src/lib/components/HeaderAuthActions.tsx +36 -0
- package/src/lib/components/HeaderNavigation.tsx +11 -9
- package/src/lib/components/MobileTopNavigation.tsx +43 -24
- package/src/lib/components/SiteTitle.tsx +20 -0
- package/src/lib/components/ThemeSwitch.tsx +36 -2
- package/src/lib/components/TopNavigation.tsx +20 -33
- package/src/lib/core/ZudokuContext.ts +1 -0
- package/src/vite/config.ts +21 -0
- package/src/vite/plugin-auth.ts +4 -1
- package/src/vite/plugin-config.ts +39 -4
|
@@ -12,17 +12,25 @@ import {
|
|
|
12
12
|
useAuthState,
|
|
13
13
|
waitForAuthStateHydration,
|
|
14
14
|
} from "../state.js";
|
|
15
|
+
import {
|
|
16
|
+
DevPortalForgotPasswordPage,
|
|
17
|
+
DevPortalLoginPage,
|
|
18
|
+
DevPortalRegisterPage,
|
|
19
|
+
DevPortalResetPasswordPage,
|
|
20
|
+
DevPortalVerifyEmailPage,
|
|
21
|
+
} from "./dev-portal-auth-pages.js";
|
|
15
22
|
import type {
|
|
16
23
|
DevPortalAuthMode,
|
|
17
24
|
EndUserMeResponse,
|
|
18
25
|
} from "./dev-portal-constants.js";
|
|
19
26
|
import {
|
|
20
|
-
buildDevPortalLoginUrl,
|
|
21
27
|
buildDevPortalLogoutUrl,
|
|
28
|
+
isNativeAuthEnabled,
|
|
22
29
|
isPlaceholderDevPortalApiUrl,
|
|
23
30
|
mapEndUserMeToProfile,
|
|
24
31
|
probeDevPortalBackend,
|
|
25
32
|
resolveDevPortalApiBaseUrl,
|
|
33
|
+
toAbsoluteReturnUrl,
|
|
26
34
|
} from "./dev-portal-utils.js";
|
|
27
35
|
|
|
28
36
|
export type DevPortalProviderData = {
|
|
@@ -49,6 +57,8 @@ export class DevPortalAuthenticationProvider
|
|
|
49
57
|
private refreshGeneration = 0;
|
|
50
58
|
private refreshInFlight: Promise<boolean> | null = null;
|
|
51
59
|
|
|
60
|
+
private authConfigNativeEnabled: boolean | null = null;
|
|
61
|
+
|
|
52
62
|
constructor({
|
|
53
63
|
apiBaseUrl,
|
|
54
64
|
redirectToAfterSignIn,
|
|
@@ -62,6 +72,27 @@ export class DevPortalAuthenticationProvider
|
|
|
62
72
|
this.redirectToAfterSignOut = redirectToAfterSignOut;
|
|
63
73
|
}
|
|
64
74
|
|
|
75
|
+
getRoutes() {
|
|
76
|
+
const pageProps = { apiBaseUrl: this.apiBaseUrl };
|
|
77
|
+
return [
|
|
78
|
+
...super.getRoutes(),
|
|
79
|
+
{ path: "/login", element: <DevPortalLoginPage {...pageProps} /> },
|
|
80
|
+
{ path: "/register", element: <DevPortalRegisterPage {...pageProps} /> },
|
|
81
|
+
{
|
|
82
|
+
path: "/verify-email",
|
|
83
|
+
element: <DevPortalVerifyEmailPage {...pageProps} />,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
path: "/forgot-password",
|
|
87
|
+
element: <DevPortalForgotPasswordPage {...pageProps} />,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
path: "/reset-password",
|
|
91
|
+
element: <DevPortalResetPasswordPage {...pageProps} />,
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
65
96
|
async initialize(_context: ZudokuContext): Promise<void> {
|
|
66
97
|
// Node SSR cannot reach local HTTPS backends with self-signed certs.
|
|
67
98
|
if (typeof window === "undefined") {
|
|
@@ -70,6 +101,16 @@ export class DevPortalAuthenticationProvider
|
|
|
70
101
|
|
|
71
102
|
await waitForAuthStateHydration();
|
|
72
103
|
await this.syncBackendAvailability();
|
|
104
|
+
if (this.backendAvailable && this.apiBaseUrl) {
|
|
105
|
+
try {
|
|
106
|
+
const { fetchDevPortalAuthConfig } =
|
|
107
|
+
await import("./dev-portal-utils.js");
|
|
108
|
+
const config = await fetchDevPortalAuthConfig(this.apiBaseUrl);
|
|
109
|
+
this.authConfigNativeEnabled = isNativeAuthEnabled(config);
|
|
110
|
+
} catch {
|
|
111
|
+
this.authConfigNativeEnabled = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
73
114
|
if (!this.backendAvailable) {
|
|
74
115
|
this.setPreviewState();
|
|
75
116
|
return;
|
|
@@ -235,20 +276,24 @@ export class DevPortalAuthenticationProvider
|
|
|
235
276
|
_: AuthActionContext,
|
|
236
277
|
{ redirectTo }: AuthActionOptions = {},
|
|
237
278
|
): Promise<void> {
|
|
238
|
-
|
|
279
|
+
this.ensureConfiguredBackend();
|
|
239
280
|
const target =
|
|
240
281
|
redirectTo ?? this.redirectToAfterSignIn ?? window.location.href;
|
|
241
|
-
window.location.
|
|
282
|
+
const url = new URL("/login", window.location.origin);
|
|
283
|
+
url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
|
|
284
|
+
window.location.assign(url.toString());
|
|
242
285
|
}
|
|
243
286
|
|
|
244
287
|
async signUp(
|
|
245
288
|
_: AuthActionContext,
|
|
246
289
|
{ redirectTo }: AuthActionOptions = {},
|
|
247
290
|
): Promise<void> {
|
|
248
|
-
|
|
291
|
+
this.ensureConfiguredBackend();
|
|
249
292
|
const target =
|
|
250
293
|
redirectTo ?? this.redirectToAfterSignUp ?? window.location.href;
|
|
251
|
-
window.location.
|
|
294
|
+
const url = new URL("/register", window.location.origin);
|
|
295
|
+
url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
|
|
296
|
+
window.location.assign(url.toString());
|
|
252
297
|
}
|
|
253
298
|
|
|
254
299
|
async signOut(_: AuthActionContext): Promise<void> {
|
|
@@ -259,7 +304,7 @@ export class DevPortalAuthenticationProvider
|
|
|
259
304
|
}
|
|
260
305
|
|
|
261
306
|
hasDistinctSignUpFlow(): boolean {
|
|
262
|
-
return false;
|
|
307
|
+
return this.authConfigNativeEnabled !== false;
|
|
263
308
|
}
|
|
264
309
|
|
|
265
310
|
async signRequest(request: Request): Promise<Request> {
|
|
@@ -20,9 +20,11 @@ import { cn } from "../util/cn.js";
|
|
|
20
20
|
import { joinUrl } from "../util/joinUrl.js";
|
|
21
21
|
import { Banner } from "./Banner.js";
|
|
22
22
|
import { useZudoku } from "./context/ZudokuContext.js";
|
|
23
|
+
import { HeaderAuthActions } from "./HeaderAuthActions.js";
|
|
23
24
|
import { MobileTopNavigation } from "./MobileTopNavigation.js";
|
|
24
25
|
import { PageProgress } from "./PageProgress.js";
|
|
25
26
|
import { Search } from "./Search.js";
|
|
27
|
+
import { SiteTitle } from "./SiteTitle.js";
|
|
26
28
|
import { Slot } from "./Slot.js";
|
|
27
29
|
import { ThemeSwitch } from "./ThemeSwitch.js";
|
|
28
30
|
import { TopNavigation } from "./TopNavigation.js";
|
|
@@ -78,8 +80,17 @@ const ProfileMenu = () => {
|
|
|
78
80
|
return (
|
|
79
81
|
<DropdownMenu modal={false}>
|
|
80
82
|
<DropdownMenuTrigger asChild>
|
|
81
|
-
<Button
|
|
82
|
-
|
|
83
|
+
<Button
|
|
84
|
+
size="lg"
|
|
85
|
+
variant="outline"
|
|
86
|
+
className="gap-2 rounded-[10px] border-border bg-muted/40 px-2.5"
|
|
87
|
+
>
|
|
88
|
+
<span className="flex size-7 items-center justify-center rounded-[7px] bg-primary text-xs font-bold text-primary-foreground">
|
|
89
|
+
{(profile?.name ?? "A").slice(0, 1).toUpperCase()}
|
|
90
|
+
</span>
|
|
91
|
+
<span className="max-w-32 truncate font-semibold">
|
|
92
|
+
{profile?.name ?? "My Account"}
|
|
93
|
+
</span>
|
|
83
94
|
</Button>
|
|
84
95
|
</DropdownMenuTrigger>
|
|
85
96
|
<DropdownMenuContent className="w-56">
|
|
@@ -116,24 +127,25 @@ const ProfileMenu = () => {
|
|
|
116
127
|
))}
|
|
117
128
|
<DropdownMenuSeparator />
|
|
118
129
|
<Link to="/signout">
|
|
119
|
-
<DropdownMenuItem className="flex gap-2">
|
|
130
|
+
<DropdownMenuItem className="flex gap-2 text-primary">
|
|
120
131
|
<LogOutIcon size={16} strokeWidth={1} absoluteStrokeWidth />
|
|
121
|
-
|
|
132
|
+
Log out
|
|
122
133
|
</DropdownMenuItem>
|
|
123
134
|
</Link>
|
|
124
135
|
</DropdownMenuContent>
|
|
125
136
|
</DropdownMenu>
|
|
126
137
|
);
|
|
127
138
|
};
|
|
139
|
+
|
|
128
140
|
export const Header = memo(function HeaderInner() {
|
|
129
141
|
const context = useZudoku();
|
|
130
142
|
const {
|
|
131
143
|
options: { site, header, basePath },
|
|
132
144
|
} = context;
|
|
133
145
|
|
|
134
|
-
const searchPosition = header?.placements?.search ?? "
|
|
135
|
-
const navPosition = header?.placements?.navigation ?? "
|
|
136
|
-
const authPlacement = header?.placements?.auth ?? "
|
|
146
|
+
const searchPosition = header?.placements?.search ?? "end";
|
|
147
|
+
const navPosition = header?.placements?.navigation ?? "start";
|
|
148
|
+
const authPlacement = header?.placements?.auth ?? "end";
|
|
137
149
|
const authPosition =
|
|
138
150
|
authPlacement === "navigation" ? navPosition : authPlacement;
|
|
139
151
|
|
|
@@ -148,18 +160,18 @@ export const Header = memo(function HeaderInner() {
|
|
|
148
160
|
: joinUrl(basePath, site.logo.src.dark)
|
|
149
161
|
: undefined;
|
|
150
162
|
|
|
151
|
-
const borderBottom = "
|
|
163
|
+
const borderBottom = "border-b border-border";
|
|
152
164
|
|
|
153
165
|
return (
|
|
154
166
|
<header
|
|
155
|
-
className="sticky lg:top-0 z-10 bg-background/
|
|
167
|
+
className="sticky lg:top-0 z-10 w-full bg-background/82 backdrop-blur-[14px]"
|
|
156
168
|
data-pagefind-ignore="all"
|
|
157
169
|
>
|
|
158
170
|
<Banner />
|
|
159
171
|
<div className={cn(borderBottom, "relative")}>
|
|
160
172
|
<PageProgress />
|
|
161
|
-
<div className="
|
|
162
|
-
<div className="flex
|
|
173
|
+
<div className="mx-auto flex h-(--top-header-height) max-w-[1200px] items-center justify-between gap-6 px-6">
|
|
174
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
163
175
|
<Link
|
|
164
176
|
to={site?.logo?.href ?? "/"}
|
|
165
177
|
reloadDocument={site?.logo?.reloadDocument ?? true}
|
|
@@ -182,69 +194,67 @@ export const Header = memo(function HeaderInner() {
|
|
|
182
194
|
className="max-h-(--top-header-height) hidden dark:block"
|
|
183
195
|
loading="lazy"
|
|
184
196
|
/>
|
|
197
|
+
{site.title && site.showTitleInHeader !== false ? (
|
|
198
|
+
<SiteTitle title={site.title} />
|
|
199
|
+
) : null}
|
|
185
200
|
</>
|
|
186
201
|
) : (
|
|
187
|
-
<span className="font-
|
|
202
|
+
<span className="text-xl font-bold tracking-tight">
|
|
203
|
+
{site?.title}
|
|
204
|
+
</span>
|
|
188
205
|
)}
|
|
189
206
|
</div>
|
|
190
207
|
</Link>
|
|
191
208
|
<Slot.Target name="head-navigation-start" />
|
|
209
|
+
<div className="hidden lg:flex min-w-0 items-center ml-2">
|
|
210
|
+
<Slot.Target name="top-navigation-before" />
|
|
211
|
+
<TopNavigation />
|
|
212
|
+
<Slot.Target name="top-navigation-after" />
|
|
213
|
+
</div>
|
|
192
214
|
{searchPosition === "start" && (
|
|
193
215
|
<Search className="hidden lg:flex" />
|
|
194
216
|
)}
|
|
195
|
-
{authPosition === "start" && (
|
|
196
|
-
<div className="hidden lg:flex">
|
|
197
|
-
<ProfileMenu />
|
|
198
|
-
</div>
|
|
199
|
-
)}
|
|
200
217
|
{navPosition === "start" && (
|
|
201
|
-
<div className="hidden lg:block min-w-0">
|
|
218
|
+
<div className="hidden lg:block min-w-0 ml-2">
|
|
202
219
|
<SuspendedHeaderNavigation />
|
|
203
220
|
</div>
|
|
204
221
|
)}
|
|
205
222
|
</div>
|
|
206
223
|
|
|
207
|
-
<div className="flex items-center justify-center">
|
|
224
|
+
<div className="hidden lg:flex items-center justify-center">
|
|
208
225
|
<Search
|
|
209
|
-
className={cn(
|
|
210
|
-
searchPosition === "center" ? "flex" : "flex lg:hidden",
|
|
211
|
-
)}
|
|
226
|
+
className={cn(searchPosition === "center" ? "flex" : "hidden")}
|
|
212
227
|
/>
|
|
213
228
|
{navPosition === "center" && (
|
|
214
|
-
<div className="
|
|
229
|
+
<div className="min-w-0">
|
|
215
230
|
<SuspendedHeaderNavigation />
|
|
216
231
|
</div>
|
|
217
232
|
)}
|
|
218
|
-
{authPosition === "center" && (
|
|
219
|
-
<div className="hidden lg:flex">
|
|
220
|
-
<ProfileMenu />
|
|
221
|
-
</div>
|
|
222
|
-
)}
|
|
223
233
|
</div>
|
|
224
234
|
|
|
225
|
-
<div className="flex items-center gap-2 justify-self-end">
|
|
235
|
+
<div className="flex items-center gap-2.5 justify-self-end">
|
|
226
236
|
<MobileTopNavigation />
|
|
227
|
-
<div className="hidden lg:flex items-center text-sm
|
|
237
|
+
<div className="hidden lg:flex items-center gap-2.5 text-sm">
|
|
228
238
|
{navPosition === "end" && (
|
|
229
239
|
<div className="min-w-0">
|
|
230
240
|
<SuspendedHeaderNavigation />
|
|
231
241
|
</div>
|
|
232
242
|
)}
|
|
233
|
-
{authPosition === "end" && <ProfileMenu />}
|
|
234
243
|
{searchPosition === "end" && <Search />}
|
|
235
244
|
<Slot.Target name="head-navigation-end" />
|
|
236
|
-
<ThemeSwitch />
|
|
245
|
+
<ThemeSwitch variant="compact" />
|
|
246
|
+
{authPosition === "end" ? (
|
|
247
|
+
<>
|
|
248
|
+
<HeaderAuthActions />
|
|
249
|
+
<ProfileMenu />
|
|
250
|
+
</>
|
|
251
|
+
) : (
|
|
252
|
+
<ProfileMenu />
|
|
253
|
+
)}
|
|
237
254
|
</div>
|
|
238
255
|
</div>
|
|
239
256
|
</div>
|
|
240
257
|
</div>
|
|
241
|
-
<div className={cn("hidden lg:block", borderBottom)}>
|
|
242
|
-
<div className="max-w-screen-2xl mx-auto border-transparent relative">
|
|
243
|
-
<Slot.Target name="top-navigation-before" />
|
|
244
|
-
<TopNavigation />
|
|
245
|
-
<Slot.Target name="top-navigation-after" />
|
|
246
|
-
</div>
|
|
247
|
-
</div>
|
|
248
258
|
</header>
|
|
249
259
|
);
|
|
250
260
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
2
|
+
import { ArrowRightIcon } from "lucide-react";
|
|
3
|
+
import { useLocation } from "react-router";
|
|
4
|
+
import { useAuth } from "../authentication/hook.js";
|
|
5
|
+
import { cn } from "../util/cn.js";
|
|
6
|
+
|
|
7
|
+
export const HeaderAuthActions = ({ className }: { className?: string }) => {
|
|
8
|
+
const auth = useAuth();
|
|
9
|
+
const location = useLocation();
|
|
10
|
+
const { isAuthEnabled, isAuthenticated } = auth;
|
|
11
|
+
|
|
12
|
+
if (!isAuthEnabled || isAuthenticated) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className={cn("hidden lg:flex items-center gap-2.5", className)}>
|
|
18
|
+
<Button
|
|
19
|
+
variant="ghost"
|
|
20
|
+
size="lg"
|
|
21
|
+
className="font-semibold text-foreground hover:text-primary px-3"
|
|
22
|
+
onClick={() => void auth.login({ redirectTo: location.pathname })}
|
|
23
|
+
>
|
|
24
|
+
Sign in
|
|
25
|
+
</Button>
|
|
26
|
+
<Button
|
|
27
|
+
size="lg"
|
|
28
|
+
className="gap-1.5 rounded-[9px] px-4 font-semibold shadow-[0_4px_14px_-4px] shadow-primary/30"
|
|
29
|
+
onClick={() => void auth.signup({ redirectTo: location.pathname })}
|
|
30
|
+
>
|
|
31
|
+
Start free
|
|
32
|
+
<ArrowRightIcon size={15} strokeWidth={2} />
|
|
33
|
+
</Button>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type HeaderNavItem,
|
|
7
7
|
type HeaderNavLinkItem,
|
|
8
8
|
} from "../../config/validators/HeaderNavigationSchema.js";
|
|
9
|
+
import { withoutMarketingCtaHeaderNavigation } from "../authentication/header-nav-filter.js";
|
|
9
10
|
import { useAuth } from "../authentication/hook.js";
|
|
10
11
|
import type { ZudokuContext } from "../core/ZudokuContext.js";
|
|
11
12
|
import {
|
|
@@ -15,12 +16,14 @@ import {
|
|
|
15
16
|
NavigationMenuLink,
|
|
16
17
|
NavigationMenuList,
|
|
17
18
|
NavigationMenuTrigger,
|
|
18
|
-
navigationMenuTriggerStyle,
|
|
19
19
|
} from "../ui/NavigationMenu.js";
|
|
20
20
|
import { cn } from "../util/cn.js";
|
|
21
21
|
import { useZudoku } from "./context/ZudokuContext.js";
|
|
22
22
|
import { shouldShowItem } from "./navigation/utils.js";
|
|
23
23
|
|
|
24
|
+
const marketingNavLinkClassName =
|
|
25
|
+
"inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground";
|
|
26
|
+
|
|
24
27
|
const NavLinkItem = ({ item }: { item: HeaderNavLinkItem }) => {
|
|
25
28
|
const Icon = item.icon as LucideIcon | undefined;
|
|
26
29
|
return (
|
|
@@ -118,10 +121,7 @@ const HeaderNavItemComponent = ({
|
|
|
118
121
|
to={item.to}
|
|
119
122
|
target={item.target}
|
|
120
123
|
rel={item.target === "_blank" ? "noopener noreferrer" : undefined}
|
|
121
|
-
className={
|
|
122
|
-
navigationMenuTriggerStyle(),
|
|
123
|
-
"flex items-center gap-2",
|
|
124
|
-
)}
|
|
124
|
+
className={marketingNavLinkClassName}
|
|
125
125
|
>
|
|
126
126
|
{Icon && <Icon size={16} />}
|
|
127
127
|
{item.label}
|
|
@@ -146,8 +146,10 @@ const HeaderNavItemComponent = ({
|
|
|
146
146
|
export const HeaderNavigation = () => {
|
|
147
147
|
const context = useZudoku();
|
|
148
148
|
const auth = useAuth();
|
|
149
|
-
const items =
|
|
150
|
-
|
|
149
|
+
const items = withoutMarketingCtaHeaderNavigation(
|
|
150
|
+
context.options.header?.navigation ?? [],
|
|
151
|
+
);
|
|
152
|
+
const navPosition = context.options.header?.placements?.navigation ?? "start";
|
|
151
153
|
if (!items || items.length === 0) return null;
|
|
152
154
|
|
|
153
155
|
const viewportAlign =
|
|
@@ -158,8 +160,8 @@ export const HeaderNavigation = () => {
|
|
|
158
160
|
: undefined;
|
|
159
161
|
|
|
160
162
|
return (
|
|
161
|
-
<NavigationMenu className={viewportAlign}>
|
|
162
|
-
<NavigationMenuList>
|
|
163
|
+
<NavigationMenu className={cn("max-w-none", viewportAlign)}>
|
|
164
|
+
<NavigationMenuList className="gap-1">
|
|
163
165
|
{items.map((item) => (
|
|
164
166
|
<HeaderNavItemComponent
|
|
165
167
|
key={"to" in item ? item.to : item.label}
|
|
@@ -3,6 +3,7 @@ import { Separator } from "@lukoweb/apitogo/ui/Separator.js";
|
|
|
3
3
|
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
|
4
4
|
import { deepEqual } from "fast-equals";
|
|
5
5
|
import {
|
|
6
|
+
ArrowRightIcon,
|
|
6
7
|
ChevronDownIcon,
|
|
7
8
|
LogOutIcon,
|
|
8
9
|
MenuIcon,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
type HeaderNavItem,
|
|
16
17
|
type HeaderNavLinkItem,
|
|
17
18
|
} from "../../config/validators/HeaderNavigationSchema.js";
|
|
19
|
+
import { withoutMarketingCtaHeaderNavigation } from "../authentication/header-nav-filter.js";
|
|
18
20
|
import { useAuth } from "../authentication/hook.js";
|
|
19
21
|
import {
|
|
20
22
|
Collapsible,
|
|
@@ -127,8 +129,10 @@ export const MobileTopNavigation = () => {
|
|
|
127
129
|
options: { header, navigation = [], site },
|
|
128
130
|
getProfileMenuItems,
|
|
129
131
|
} = context;
|
|
130
|
-
const headerNavigation =
|
|
131
|
-
|
|
132
|
+
const headerNavigation = withoutMarketingCtaHeaderNavigation(
|
|
133
|
+
header?.navigation ?? [],
|
|
134
|
+
);
|
|
135
|
+
const { isAuthenticated, profile, isAuthEnabled, login, signup } = authState;
|
|
132
136
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
133
137
|
|
|
134
138
|
const accountItems = getProfileMenuItems();
|
|
@@ -158,18 +162,6 @@ export const MobileTopNavigation = () => {
|
|
|
158
162
|
<DrawerTitle>Navigation</DrawerTitle>
|
|
159
163
|
</VisuallyHidden>
|
|
160
164
|
<ul className="flex flex-col gap-1 px-4">
|
|
161
|
-
{headerNavigation.map((item) => (
|
|
162
|
-
<MobileHeaderNavItem
|
|
163
|
-
key={item.label}
|
|
164
|
-
item={item}
|
|
165
|
-
onNavigate={() => setDrawerOpen(false)}
|
|
166
|
-
/>
|
|
167
|
-
))}
|
|
168
|
-
{headerNavigation.length > 0 && <Separator className="my-2" />}
|
|
169
|
-
<li className="empty:hidden">
|
|
170
|
-
<Slot.Target name="top-navigation-side" />
|
|
171
|
-
</li>
|
|
172
|
-
|
|
173
165
|
{filteredItems.map((item) => {
|
|
174
166
|
if (item.type === "separator") {
|
|
175
167
|
return <Separator className="my-2" key={item.label} />;
|
|
@@ -192,6 +184,19 @@ export const MobileTopNavigation = () => {
|
|
|
192
184
|
</li>
|
|
193
185
|
);
|
|
194
186
|
})}
|
|
187
|
+
{filteredItems.length > 0 && headerNavigation.length > 0 && (
|
|
188
|
+
<Separator className="my-2" />
|
|
189
|
+
)}
|
|
190
|
+
<li className="empty:hidden">
|
|
191
|
+
<Slot.Target name="top-navigation-side" />
|
|
192
|
+
</li>
|
|
193
|
+
{headerNavigation.map((item) => (
|
|
194
|
+
<MobileHeaderNavItem
|
|
195
|
+
key={item.label}
|
|
196
|
+
item={item}
|
|
197
|
+
onNavigate={() => setDrawerOpen(false)}
|
|
198
|
+
/>
|
|
199
|
+
))}
|
|
195
200
|
{isAuthEnabled && isAuthenticated && (
|
|
196
201
|
<>
|
|
197
202
|
<Separator className="my-2" />
|
|
@@ -227,7 +232,7 @@ export const MobileTopNavigation = () => {
|
|
|
227
232
|
</ul>
|
|
228
233
|
</div>
|
|
229
234
|
<div className="border-t shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)] px-4 pt-3 flex flex-col gap-2">
|
|
230
|
-
<div className="flex items-center justify-between">
|
|
235
|
+
<div className="flex items-center justify-between gap-2">
|
|
231
236
|
{isAuthEnabled &&
|
|
232
237
|
(isAuthenticated ? (
|
|
233
238
|
<Button asChild variant="outline">
|
|
@@ -241,20 +246,34 @@ export const MobileTopNavigation = () => {
|
|
|
241
246
|
strokeWidth={1}
|
|
242
247
|
absoluteStrokeWidth
|
|
243
248
|
/>
|
|
244
|
-
|
|
249
|
+
Log out
|
|
245
250
|
</Link>
|
|
246
251
|
</Button>
|
|
247
252
|
) : (
|
|
248
|
-
<
|
|
249
|
-
<
|
|
250
|
-
|
|
251
|
-
|
|
253
|
+
<div className="flex flex-1 items-center gap-2">
|
|
254
|
+
<Button
|
|
255
|
+
variant="ghost"
|
|
256
|
+
className="font-semibold"
|
|
257
|
+
onClick={() => {
|
|
258
|
+
setDrawerOpen(false);
|
|
259
|
+
void login({ redirectTo: location.pathname });
|
|
260
|
+
}}
|
|
252
261
|
>
|
|
253
|
-
|
|
254
|
-
</
|
|
255
|
-
|
|
262
|
+
Sign in
|
|
263
|
+
</Button>
|
|
264
|
+
<Button
|
|
265
|
+
className="gap-1.5"
|
|
266
|
+
onClick={() => {
|
|
267
|
+
setDrawerOpen(false);
|
|
268
|
+
void signup({ redirectTo: location.pathname });
|
|
269
|
+
}}
|
|
270
|
+
>
|
|
271
|
+
Start free
|
|
272
|
+
<ArrowRightIcon size={15} strokeWidth={2} />
|
|
273
|
+
</Button>
|
|
274
|
+
</div>
|
|
256
275
|
))}
|
|
257
|
-
<ThemeSwitch />
|
|
276
|
+
<ThemeSwitch variant="compact" />
|
|
258
277
|
</div>
|
|
259
278
|
{site?.showPoweredBy !== false && (
|
|
260
279
|
<PoweredByZudoku className="grow-0 justify-center gap-1" />
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cn } from "../util/cn.js";
|
|
2
|
+
|
|
3
|
+
type SiteTitleProps = {
|
|
4
|
+
title: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const SiteTitle = ({ title, className }: SiteTitleProps) => {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={cn(
|
|
12
|
+
"shrink-0 font-bold text-[19px] tracking-[-0.02em] text-foreground",
|
|
13
|
+
className,
|
|
14
|
+
)}
|
|
15
|
+
style={{ fontFamily: '"Space Grotesk", var(--font-sans, sans-serif)' }}
|
|
16
|
+
>
|
|
17
|
+
{title}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -4,17 +4,51 @@ import { focusRing } from "../ui/util.js";
|
|
|
4
4
|
import { cn } from "../util/cn.js";
|
|
5
5
|
import { useIsClient } from "./ClientOnly.js";
|
|
6
6
|
|
|
7
|
-
export const ThemeSwitch = (
|
|
7
|
+
export const ThemeSwitch = ({
|
|
8
|
+
variant = "default",
|
|
9
|
+
}: {
|
|
10
|
+
variant?: "default" | "compact";
|
|
11
|
+
}) => {
|
|
8
12
|
const { resolvedTheme, setTheme } = useTheme();
|
|
9
13
|
const isClient = useIsClient();
|
|
10
14
|
|
|
11
15
|
const theme = isClient ? resolvedTheme : undefined;
|
|
12
16
|
|
|
17
|
+
const handleThemeToggle = () => {
|
|
18
|
+
setTheme(resolvedTheme === "dark" ? "light" : "dark");
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (variant === "compact") {
|
|
22
|
+
return (
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
className={cn(
|
|
26
|
+
"grid size-[38px] place-items-center rounded-[9px] border border-border text-muted-foreground transition-colors hover:border-border hover:text-foreground",
|
|
27
|
+
focusRing,
|
|
28
|
+
)}
|
|
29
|
+
onClick={handleThemeToggle}
|
|
30
|
+
aria-label={
|
|
31
|
+
!isClient
|
|
32
|
+
? "Toggle theme"
|
|
33
|
+
: theme === "dark"
|
|
34
|
+
? "Switch to light mode"
|
|
35
|
+
: "Switch to dark mode"
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
{!isClient || theme === "dark" ? (
|
|
39
|
+
<SunIcon size={17} strokeWidth={1.8} />
|
|
40
|
+
) : (
|
|
41
|
+
<MoonIcon size={17} strokeWidth={1.8} />
|
|
42
|
+
)}
|
|
43
|
+
</button>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
13
47
|
return (
|
|
14
48
|
<button
|
|
15
49
|
type="button"
|
|
16
50
|
className={cn("flex rounded-full border p-0.5 gap-0.5 group", focusRing)}
|
|
17
|
-
onClick={
|
|
51
|
+
onClick={handleThemeToggle}
|
|
18
52
|
aria-label={
|
|
19
53
|
!isClient
|
|
20
54
|
? "Toggle theme"
|
|
@@ -7,8 +7,6 @@ import type { NavigationItem } from "../../config/validators/NavigationSchema.js
|
|
|
7
7
|
import { useAuth } from "../authentication/hook.js";
|
|
8
8
|
import { useCurrentNavigation, useZudoku } from "./context/ZudokuContext.js";
|
|
9
9
|
import { getFirstMatchingPath, shouldShowItem } from "./navigation/utils.js";
|
|
10
|
-
import { Slot } from "./Slot.js";
|
|
11
|
-
|
|
12
10
|
export const TopNavigation = () => {
|
|
13
11
|
const context = useZudoku();
|
|
14
12
|
const {
|
|
@@ -18,30 +16,26 @@ export const TopNavigation = () => {
|
|
|
18
16
|
const filteredItems = navigation.filter(shouldShowItem({ auth, context }));
|
|
19
17
|
|
|
20
18
|
if (filteredItems.length === 0 || import.meta.env.MODE === "standalone") {
|
|
21
|
-
return
|
|
19
|
+
return null;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
22
|
return (
|
|
25
23
|
<Suspense>
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
item.
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
</nav>
|
|
42
|
-
<Slot.Target name="top-navigation-side" />
|
|
43
|
-
</div>
|
|
44
|
-
{/* <PageProgress /> */}
|
|
24
|
+
<nav className="min-w-0 text-sm">
|
|
25
|
+
<ul className="flex flex-row items-center gap-1">
|
|
26
|
+
{filteredItems.map((item) =>
|
|
27
|
+
item.type === "separator" ? (
|
|
28
|
+
<li key={item.label} className="mx-1 h-5">
|
|
29
|
+
<Separator orientation="vertical" />
|
|
30
|
+
</li>
|
|
31
|
+
) : item.type !== "section" && item.type !== "filter" ? (
|
|
32
|
+
<li key={item.label + item.type}>
|
|
33
|
+
<TopNavItem {...item} />
|
|
34
|
+
</li>
|
|
35
|
+
) : null,
|
|
36
|
+
)}
|
|
37
|
+
</ul>
|
|
38
|
+
</nav>
|
|
45
39
|
</Suspense>
|
|
46
40
|
);
|
|
47
41
|
};
|
|
@@ -60,18 +54,11 @@ export const TopNavLink = ({
|
|
|
60
54
|
className={({ isActive: isActiveNavLink, isPending }) => {
|
|
61
55
|
const isActiveReal = isActiveNavLink || isActive;
|
|
62
56
|
return cx(
|
|
63
|
-
"flex items-center gap-2 lg
|
|
57
|
+
"inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors",
|
|
64
58
|
isActiveReal || isPending
|
|
65
|
-
?
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"after:content-[''] after:absolute after:bottom-0 after:left-0 after:right-0",
|
|
69
|
-
"after:h-0.5 after:bg-primary",
|
|
70
|
-
isActiveReal &&
|
|
71
|
-
"after:[view-transition-name:top-nav-underline]",
|
|
72
|
-
isPending && "after:bg-primary/25",
|
|
73
|
-
]
|
|
74
|
-
: "text-foreground/75 hover:text-foreground",
|
|
59
|
+
? "bg-muted text-foreground"
|
|
60
|
+
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
61
|
+
isPending && "opacity-75",
|
|
75
62
|
);
|
|
76
63
|
}}
|
|
77
64
|
{...props}
|