@lukoweb/apitogo 0.1.54 → 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.
Files changed (41) hide show
  1. package/dist/cli/cli.js +604 -376
  2. package/dist/declarations/config/landing-manifest.d.ts +76 -0
  3. package/dist/declarations/config/local-manifest.d.ts +76 -0
  4. package/dist/declarations/config/validators/ModulesSchema.d.ts +227 -0
  5. package/dist/declarations/config/validators/ZudokuConfig.d.ts +50 -0
  6. package/dist/declarations/lib/authentication/header-nav-filter.d.ts +4 -0
  7. package/dist/declarations/lib/authentication/providers/dev-portal-auth-pages.d.ts +5 -5
  8. package/dist/declarations/lib/components/HeaderAuthActions.d.ts +3 -0
  9. package/dist/declarations/lib/components/SiteTitle.d.ts +6 -0
  10. package/dist/declarations/lib/components/ThemeSwitch.d.ts +3 -1
  11. package/dist/declarations/lib/components/TopNavigation.d.ts +1 -1
  12. package/dist/declarations/lib/core/ZudokuContext.d.ts +1 -0
  13. package/dist/flat-config.d.ts +44 -0
  14. package/package.json +1 -1
  15. package/src/app/main.css +2 -2
  16. package/src/config/apitogo-config-core.ts +13 -0
  17. package/src/config/apitogo-config-loader.browser.ts +17 -0
  18. package/src/config/build-apitogo-config.ts +13 -4
  19. package/src/config/landing-manifest.ts +11 -0
  20. package/src/config/landing-openapi-showcase.ts +115 -0
  21. package/src/config/loader.ts +17 -3
  22. package/src/config/local-manifest.ts +114 -10
  23. package/src/config/resolve-modules.ts +22 -1
  24. package/src/config/validators/ModulesSchema.ts +84 -0
  25. package/src/config/validators/ZudokuConfig.ts +1 -0
  26. package/src/lib/authentication/auth-header-nav.ts +5 -15
  27. package/src/lib/authentication/header-nav-filter.ts +36 -0
  28. package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +95 -35
  29. package/src/lib/authentication/providers/dev-portal-utils.ts +17 -7
  30. package/src/lib/authentication/providers/dev-portal.tsx +18 -8
  31. package/src/lib/components/Header.tsx +49 -39
  32. package/src/lib/components/HeaderAuthActions.tsx +36 -0
  33. package/src/lib/components/HeaderNavigation.tsx +11 -9
  34. package/src/lib/components/MobileTopNavigation.tsx +43 -24
  35. package/src/lib/components/SiteTitle.tsx +20 -0
  36. package/src/lib/components/ThemeSwitch.tsx +36 -2
  37. package/src/lib/components/TopNavigation.tsx +20 -33
  38. package/src/lib/core/ZudokuContext.ts +1 -0
  39. package/src/vite/config.ts +21 -0
  40. package/src/vite/plugin-auth.ts +4 -1
  41. package/src/vite/plugin-config.ts +39 -4
@@ -27,22 +27,12 @@ export function applyAuthenticationHeaderNav<T extends ZudokuConfig>(
27
27
  const authNavEnabled = config.__meta?.authenticationEnabled === true;
28
28
  const navigation = config.header?.navigation ?? [];
29
29
 
30
- if (!authNavEnabled) {
31
- const filtered = withoutLoginNavigation(navigation);
32
- if (filtered.length === navigation.length) {
33
- return config;
34
- }
35
-
36
- return {
37
- ...config,
38
- header: {
39
- ...config.header,
40
- navigation: filtered,
41
- },
42
- };
30
+ if (authNavEnabled) {
31
+ return config;
43
32
  }
44
33
 
45
- if (navigation.some((item) => "to" in item && item.to === LOGIN_NAV_PATH)) {
34
+ const filtered = withoutLoginNavigation(navigation);
35
+ if (filtered.length === navigation.length) {
46
36
  return config;
47
37
  }
48
38
 
@@ -50,7 +40,7 @@ export function applyAuthenticationHeaderNav<T extends ZudokuConfig>(
50
40
  ...config,
51
41
  header: {
52
42
  ...config.header,
53
- navigation: [...navigation, LOGIN_HEADER_NAV_ITEM],
43
+ navigation: filtered,
54
44
  },
55
45
  };
56
46
  }
@@ -0,0 +1,36 @@
1
+ import type { HeaderNavigation } from "../../config/validators/HeaderNavigationSchema.js";
2
+ import { LOGIN_NAV_PATH } from "./auth-header-nav.js";
3
+
4
+ const AUTH_HEADER_PATHS = new Set([
5
+ LOGIN_NAV_PATH,
6
+ "/signin",
7
+ "/login",
8
+ "/signup",
9
+ "/register",
10
+ ]);
11
+
12
+ const PRICING_HEADER_PATHS = new Set(["/pricing"]);
13
+
14
+ export function withoutAuthHeaderNavigation(
15
+ navigation: HeaderNavigation,
16
+ ): HeaderNavigation {
17
+ return navigation.filter(
18
+ (item) => !("to" in item) || !AUTH_HEADER_PATHS.has(item.to),
19
+ );
20
+ }
21
+
22
+ export function withoutPricingHeaderNavigation(
23
+ navigation: HeaderNavigation,
24
+ ): HeaderNavigation {
25
+ return navigation.filter(
26
+ (item) => !("to" in item) || !PRICING_HEADER_PATHS.has(item.to),
27
+ );
28
+ }
29
+
30
+ export function withoutMarketingCtaHeaderNavigation(
31
+ navigation: HeaderNavigation,
32
+ ): HeaderNavigation {
33
+ return withoutPricingHeaderNavigation(
34
+ withoutAuthHeaderNavigation(navigation),
35
+ );
36
+ }
@@ -33,7 +33,9 @@ function AuthPageShell({ title, description, children }: AuthPageShellProps) {
33
33
  <Card className="max-w-md w-full">
34
34
  <CardHeader>
35
35
  <CardTitle className="text-lg">{title}</CardTitle>
36
- {description ? <CardDescription>{description}</CardDescription> : null}
36
+ {description ? (
37
+ <CardDescription>{description}</CardDescription>
38
+ ) : null}
37
39
  </CardHeader>
38
40
  <CardContent className="space-y-4">{children}</CardContent>
39
41
  </Card>
@@ -71,7 +73,9 @@ function OidcProviderButtons({
71
73
  return (
72
74
  <div className="space-y-2">
73
75
  {isNativeAuthEnabled(config) ? (
74
- <p className="text-sm text-muted-foreground text-center">or continue with</p>
76
+ <p className="text-sm text-muted-foreground text-center">
77
+ or continue with
78
+ </p>
75
79
  ) : null}
76
80
  {config.providers.map((provider) => (
77
81
  <Button
@@ -81,7 +85,11 @@ function OidcProviderButtons({
81
85
  className="w-full"
82
86
  onClick={() => {
83
87
  window.location.assign(
84
- buildDevPortalOidcChallengeUrl(apiBaseUrl, provider.id, returnUrl),
88
+ buildDevPortalOidcChallengeUrl(
89
+ apiBaseUrl,
90
+ provider.id,
91
+ returnUrl,
92
+ ),
85
93
  );
86
94
  }}
87
95
  >
@@ -92,14 +100,12 @@ function OidcProviderButtons({
92
100
  );
93
101
  }
94
102
 
95
- export function DevPortalLoginPage({
96
- apiBaseUrl,
97
- }: {
98
- apiBaseUrl?: string;
99
- }) {
103
+ export function DevPortalLoginPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
100
104
  const baseUrl = useApiBaseUrl(apiBaseUrl);
101
105
  const returnUrl = useReturnUrl();
102
- const [config, setConfig] = useState<DevPortalAuthConfigResponse | null>(null);
106
+ const [config, setConfig] = useState<DevPortalAuthConfigResponse | null>(
107
+ null,
108
+ );
103
109
  const [configLoading, setConfigLoading] = useState(true);
104
110
  const [configError, setConfigError] = useState<string | null>(null);
105
111
  const [email, setEmail] = useState("");
@@ -132,7 +138,10 @@ export function DevPortalLoginPage({
132
138
  setPending(true);
133
139
  setError(null);
134
140
  try {
135
- await postDevPortalAuth(baseUrl, "/api/v1/auth/login", { email, password });
141
+ await postDevPortalAuth(baseUrl, "/api/v1/auth/login", {
142
+ email,
143
+ password,
144
+ });
136
145
  window.location.assign(toAbsoluteReturnUrl(returnUrl));
137
146
  } catch (err) {
138
147
  setError(err instanceof Error ? err.message : "Login failed.");
@@ -145,8 +154,13 @@ export function DevPortalLoginPage({
145
154
 
146
155
  if (!baseUrl) {
147
156
  return (
148
- <AuthPageShell title="Sign in" description="Dev portal API is not configured.">
149
- <p className="text-sm text-muted-foreground">Set authentication.apiBaseUrl to continue.</p>
157
+ <AuthPageShell
158
+ title="Sign in"
159
+ description="Dev portal API is not configured."
160
+ >
161
+ <p className="text-sm text-muted-foreground">
162
+ Set authentication.apiBaseUrl to continue.
163
+ </p>
150
164
  </AuthPageShell>
151
165
  );
152
166
  }
@@ -182,10 +196,16 @@ export function DevPortalLoginPage({
182
196
  {pending ? <Spinner /> : "Sign in"}
183
197
  </Button>
184
198
  <div className="flex justify-between text-sm">
185
- <Link className="text-primary hover:underline" to="/forgot-password">
199
+ <Link
200
+ className="text-primary hover:underline"
201
+ to="/forgot-password"
202
+ >
186
203
  Forgot password?
187
204
  </Link>
188
- <Link className="text-primary hover:underline" to={`/register?redirect=${encodeURIComponent(returnUrl)}`}>
205
+ <Link
206
+ className="text-primary hover:underline"
207
+ to={`/register?redirect=${encodeURIComponent(returnUrl)}`}
208
+ >
189
209
  Create account
190
210
  </Link>
191
211
  </div>
@@ -198,7 +218,11 @@ export function DevPortalLoginPage({
198
218
  </div>
199
219
  ) : config ? (
200
220
  <>
201
- <OidcProviderButtons apiBaseUrl={baseUrl} config={config} returnUrl={returnUrl} />
221
+ <OidcProviderButtons
222
+ apiBaseUrl={baseUrl}
223
+ config={config}
224
+ returnUrl={returnUrl}
225
+ />
202
226
  {!isNativeAuthEnabled(config) && !config.providers.length ? (
203
227
  <p className="text-sm text-muted-foreground text-center">
204
228
  No sign-in methods are configured.
@@ -210,11 +234,7 @@ export function DevPortalLoginPage({
210
234
  );
211
235
  }
212
236
 
213
- export function DevPortalRegisterPage({
214
- apiBaseUrl,
215
- }: {
216
- apiBaseUrl?: string;
217
- }) {
237
+ export function DevPortalRegisterPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
218
238
  const baseUrl = useApiBaseUrl(apiBaseUrl);
219
239
  const returnUrl = useReturnUrl();
220
240
  const [email, setEmail] = useState("");
@@ -249,14 +269,22 @@ export function DevPortalRegisterPage({
249
269
 
250
270
  if (!baseUrl) {
251
271
  return (
252
- <AuthPageShell title="Create account" description="Dev portal API is not configured.">
253
- <p className="text-sm text-muted-foreground">Set authentication.apiBaseUrl to continue.</p>
272
+ <AuthPageShell
273
+ title="Create account"
274
+ description="Dev portal API is not configured."
275
+ >
276
+ <p className="text-sm text-muted-foreground">
277
+ Set authentication.apiBaseUrl to continue.
278
+ </p>
254
279
  </AuthPageShell>
255
280
  );
256
281
  }
257
282
 
258
283
  return (
259
- <AuthPageShell title="Create account" description="Register for API access.">
284
+ <AuthPageShell
285
+ title="Create account"
286
+ description="Register for API access."
287
+ >
260
288
  <form className="space-y-4" onSubmit={onSubmit}>
261
289
  <div className="space-y-2">
262
290
  <Label htmlFor="displayName">Name</Label>
@@ -289,12 +317,17 @@ export function DevPortalRegisterPage({
289
317
  />
290
318
  </div>
291
319
  <AuthError message={error} />
292
- {message ? <p className="text-sm text-muted-foreground">{message}</p> : null}
320
+ {message ? (
321
+ <p className="text-sm text-muted-foreground">{message}</p>
322
+ ) : null}
293
323
  <Button type="submit" className="w-full" disabled={pending}>
294
324
  {pending ? <Spinner /> : "Create account"}
295
325
  </Button>
296
326
  <p className="text-sm text-center">
297
- <Link className="text-primary hover:underline" to={`/login?redirect=${encodeURIComponent(returnUrl)}`}>
327
+ <Link
328
+ className="text-primary hover:underline"
329
+ to={`/login?redirect=${encodeURIComponent(returnUrl)}`}
330
+ >
298
331
  Back to sign in
299
332
  </Link>
300
333
  </p>
@@ -303,7 +336,11 @@ export function DevPortalRegisterPage({
303
336
  );
304
337
  }
305
338
 
306
- export function DevPortalVerifyEmailPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
339
+ export function DevPortalVerifyEmailPage({
340
+ apiBaseUrl,
341
+ }: {
342
+ apiBaseUrl?: string;
343
+ }) {
307
344
  const baseUrl = useApiBaseUrl(apiBaseUrl);
308
345
  const [search] = useSearchParams();
309
346
  const [message, setMessage] = useState<string | null>(null);
@@ -312,9 +349,13 @@ export function DevPortalVerifyEmailPage({ apiBaseUrl }: { apiBaseUrl?: string }
312
349
  useEffect(() => {
313
350
  const token = search.get("token");
314
351
  if (!baseUrl || !token) return;
315
- void postDevPortalAuth<{ message: string }>(baseUrl, "/api/v1/auth/verify-email", {
316
- token,
317
- })
352
+ void postDevPortalAuth<{ message: string }>(
353
+ baseUrl,
354
+ "/api/v1/auth/verify-email",
355
+ {
356
+ token,
357
+ },
358
+ )
318
359
  .then((response) => setMessage(response.message))
319
360
  .catch((err) =>
320
361
  setError(err instanceof Error ? err.message : "Verification failed."),
@@ -324,7 +365,11 @@ export function DevPortalVerifyEmailPage({ apiBaseUrl }: { apiBaseUrl?: string }
324
365
  return (
325
366
  <AuthPageShell title="Verify email">
326
367
  <AuthError message={error} />
327
- {message ? <p className="text-sm text-muted-foreground">{message}</p> : <Spinner />}
368
+ {message ? (
369
+ <p className="text-sm text-muted-foreground">{message}</p>
370
+ ) : (
371
+ <Spinner />
372
+ )}
328
373
  <Link className="text-sm text-primary hover:underline" to="/login">
329
374
  Sign in
330
375
  </Link>
@@ -332,7 +377,11 @@ export function DevPortalVerifyEmailPage({ apiBaseUrl }: { apiBaseUrl?: string }
332
377
  );
333
378
  }
334
379
 
335
- export function DevPortalForgotPasswordPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
380
+ export function DevPortalForgotPasswordPage({
381
+ apiBaseUrl,
382
+ }: {
383
+ apiBaseUrl?: string;
384
+ }) {
336
385
  const baseUrl = useApiBaseUrl(apiBaseUrl);
337
386
  const [email, setEmail] = useState("");
338
387
  const [message, setMessage] = useState<string | null>(null);
@@ -362,7 +411,10 @@ export function DevPortalForgotPasswordPage({ apiBaseUrl }: { apiBaseUrl?: strin
362
411
  );
363
412
 
364
413
  return (
365
- <AuthPageShell title="Reset password" description="We'll email you a reset link.">
414
+ <AuthPageShell
415
+ title="Reset password"
416
+ description="We'll email you a reset link."
417
+ >
366
418
  <form className="space-y-4" onSubmit={onSubmit}>
367
419
  <div className="space-y-2">
368
420
  <Label htmlFor="email">Email</Label>
@@ -375,7 +427,9 @@ export function DevPortalForgotPasswordPage({ apiBaseUrl }: { apiBaseUrl?: strin
375
427
  />
376
428
  </div>
377
429
  <AuthError message={error} />
378
- {message ? <p className="text-sm text-muted-foreground">{message}</p> : null}
430
+ {message ? (
431
+ <p className="text-sm text-muted-foreground">{message}</p>
432
+ ) : null}
379
433
  <Button type="submit" className="w-full" disabled={pending}>
380
434
  {pending ? <Spinner /> : "Send reset link"}
381
435
  </Button>
@@ -384,7 +438,11 @@ export function DevPortalForgotPasswordPage({ apiBaseUrl }: { apiBaseUrl?: strin
384
438
  );
385
439
  }
386
440
 
387
- export function DevPortalResetPasswordPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
441
+ export function DevPortalResetPasswordPage({
442
+ apiBaseUrl,
443
+ }: {
444
+ apiBaseUrl?: string;
445
+ }) {
388
446
  const baseUrl = useApiBaseUrl(apiBaseUrl);
389
447
  const [search] = useSearchParams();
390
448
  const [password, setPassword] = useState("");
@@ -429,7 +487,9 @@ export function DevPortalResetPasswordPage({ apiBaseUrl }: { apiBaseUrl?: string
429
487
  />
430
488
  </div>
431
489
  <AuthError message={error} />
432
- {message ? <p className="text-sm text-muted-foreground">{message}</p> : null}
490
+ {message ? (
491
+ <p className="text-sm text-muted-foreground">{message}</p>
492
+ ) : null}
433
493
  <Button type="submit" className="w-full" disabled={pending}>
434
494
  {pending ? <Spinner /> : "Update password"}
435
495
  </Button>
@@ -58,7 +58,10 @@ export function buildDevPortalOidcChallengeUrl(
58
58
  providerId: string,
59
59
  returnUrl: string,
60
60
  ): string {
61
- const url = new URL(`/api/v1/auth/oidc/${encodeURIComponent(providerId)}/challenge`, apiBaseUrl);
61
+ const url = new URL(
62
+ `/api/v1/auth/oidc/${encodeURIComponent(providerId)}/challenge`,
63
+ apiBaseUrl,
64
+ );
62
65
  url.searchParams.set("returnUrl", toAbsoluteReturnUrl(returnUrl));
63
66
  return url.toString();
64
67
  }
@@ -122,15 +125,19 @@ export function normalizeDevPortalAuthConfig(
122
125
  }
123
126
 
124
127
  export function isNativeAuthEnabled(
125
- config: import("./dev-portal-constants.js").DevPortalAuthConfigResponse | null | undefined,
128
+ config:
129
+ | import("./dev-portal-constants.js").DevPortalAuthConfigResponse
130
+ | null
131
+ | undefined,
126
132
  ): boolean {
127
133
  if (!config) {
128
134
  return false;
129
135
  }
130
136
 
131
- const raw = config as import("./dev-portal-constants.js").DevPortalAuthConfigResponse & {
132
- nativeEnabled?: boolean;
133
- };
137
+ const raw =
138
+ config as import("./dev-portal-constants.js").DevPortalAuthConfigResponse & {
139
+ nativeEnabled?: boolean;
140
+ };
134
141
 
135
142
  return raw.native?.enabled === true || raw.nativeEnabled === true;
136
143
  }
@@ -175,7 +182,10 @@ export async function postDevPortalAuth<T>(
175
182
  if (!response.ok) {
176
183
  let detail = "Request failed.";
177
184
  try {
178
- const problem = (await response.json()) as { detail?: string; title?: string };
185
+ const problem = (await response.json()) as {
186
+ detail?: string;
187
+ title?: string;
188
+ };
179
189
  detail = problem.detail ?? problem.title ?? detail;
180
190
  } catch {
181
191
  // ignore parse errors
@@ -252,7 +262,7 @@ export function mapEndUserMeToProfile(
252
262
  raw.emailVerified,
253
263
  raw.EmailVerified,
254
264
  raw.email_verified,
255
- ) ?? (email != null ? true : false);
265
+ ) ?? email != null;
256
266
 
257
267
  return {
258
268
  sub: me.userId,
@@ -12,10 +12,6 @@ import {
12
12
  useAuthState,
13
13
  waitForAuthStateHydration,
14
14
  } from "../state.js";
15
- import type {
16
- DevPortalAuthMode,
17
- EndUserMeResponse,
18
- } from "./dev-portal-constants.js";
19
15
  import {
20
16
  DevPortalForgotPasswordPage,
21
17
  DevPortalLoginPage,
@@ -23,6 +19,10 @@ import {
23
19
  DevPortalResetPasswordPage,
24
20
  DevPortalVerifyEmailPage,
25
21
  } from "./dev-portal-auth-pages.js";
22
+ import type {
23
+ DevPortalAuthMode,
24
+ EndUserMeResponse,
25
+ } from "./dev-portal-constants.js";
26
26
  import {
27
27
  buildDevPortalLogoutUrl,
28
28
  isNativeAuthEnabled,
@@ -78,9 +78,18 @@ export class DevPortalAuthenticationProvider
78
78
  ...super.getRoutes(),
79
79
  { path: "/login", element: <DevPortalLoginPage {...pageProps} /> },
80
80
  { path: "/register", element: <DevPortalRegisterPage {...pageProps} /> },
81
- { path: "/verify-email", element: <DevPortalVerifyEmailPage {...pageProps} /> },
82
- { path: "/forgot-password", element: <DevPortalForgotPasswordPage {...pageProps} /> },
83
- { path: "/reset-password", element: <DevPortalResetPasswordPage {...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
+ },
84
93
  ];
85
94
  }
86
95
 
@@ -94,7 +103,8 @@ export class DevPortalAuthenticationProvider
94
103
  await this.syncBackendAvailability();
95
104
  if (this.backendAvailable && this.apiBaseUrl) {
96
105
  try {
97
- const { fetchDevPortalAuthConfig } = await import("./dev-portal-utils.js");
106
+ const { fetchDevPortalAuthConfig } =
107
+ await import("./dev-portal-utils.js");
98
108
  const config = await fetchDevPortalAuthConfig(this.apiBaseUrl);
99
109
  this.authConfigNativeEnabled = isNativeAuthEnabled(config);
100
110
  } catch {
@@ -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 size="lg" variant="ghost">
82
- {profile?.name ?? "My Account"}
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
- Logout
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 ?? "center";
135
- const navPosition = header?.placements?.navigation ?? "end";
136
- const authPlacement = header?.placements?.auth ?? "navigation";
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 = "inset-shadow-[0_-1px_0_0_var(--border)]";
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/80 backdrop-blur w-full"
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="max-w-screen-2xl mx-auto flex lg:grid lg:grid-cols-[1fr_auto_1fr] gap-2 items-center justify-between h-(--top-header-height) px-4 lg:px-8 border-transparent">
162
- <div className="flex items-center gap-4 min-w-0 justify-self-start">
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-semibold text-2xl">{site?.title}</span>
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="hidden lg:block min-w-0">
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 gap-2">
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
+ };