@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.
Files changed (47) hide show
  1. package/dist/cli/cli.js +686 -373
  2. package/dist/declarations/config/apitogo-config-core.d.ts +14 -4
  3. package/dist/declarations/config/landing-manifest.d.ts +76 -0
  4. package/dist/declarations/config/local-manifest.d.ts +91 -1
  5. package/dist/declarations/config/validators/ModulesSchema.d.ts +227 -0
  6. package/dist/declarations/config/validators/ZudokuConfig.d.ts +84 -2
  7. package/dist/declarations/lib/authentication/header-nav-filter.d.ts +4 -0
  8. package/dist/declarations/lib/authentication/providers/dev-portal-auth-pages.d.ts +15 -0
  9. package/dist/declarations/lib/authentication/providers/dev-portal-constants.d.ts +19 -1
  10. package/dist/declarations/lib/authentication/providers/dev-portal-utils.d.ts +5 -1
  11. package/dist/declarations/lib/authentication/providers/dev-portal.d.ts +5 -0
  12. package/dist/declarations/lib/components/HeaderAuthActions.d.ts +3 -0
  13. package/dist/declarations/lib/components/SiteTitle.d.ts +6 -0
  14. package/dist/declarations/lib/components/ThemeSwitch.d.ts +3 -1
  15. package/dist/declarations/lib/components/TopNavigation.d.ts +1 -1
  16. package/dist/declarations/lib/core/ZudokuContext.d.ts +1 -0
  17. package/dist/flat-config.d.ts +61 -1
  18. package/package.json +1 -1
  19. package/src/app/main.css +2 -2
  20. package/src/config/apitogo-config-core.ts +140 -11
  21. package/src/config/apitogo-config-loader.browser.ts +17 -0
  22. package/src/config/apitogo-config-loader.node.ts +28 -8
  23. package/src/config/build-apitogo-config.ts +13 -4
  24. package/src/config/landing-manifest.ts +11 -0
  25. package/src/config/landing-openapi-showcase.ts +115 -0
  26. package/src/config/loader.ts +17 -3
  27. package/src/config/local-manifest.ts +146 -11
  28. package/src/config/resolve-modules.ts +22 -1
  29. package/src/config/validators/ModulesSchema.ts +84 -0
  30. package/src/config/validators/ZudokuConfig.ts +26 -1
  31. package/src/lib/authentication/auth-header-nav.ts +5 -15
  32. package/src/lib/authentication/header-nav-filter.ts +36 -0
  33. package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +499 -0
  34. package/src/lib/authentication/providers/dev-portal-constants.ts +15 -1
  35. package/src/lib/authentication/providers/dev-portal-utils.ts +128 -3
  36. package/src/lib/authentication/providers/dev-portal.tsx +51 -6
  37. package/src/lib/components/Header.tsx +49 -39
  38. package/src/lib/components/HeaderAuthActions.tsx +36 -0
  39. package/src/lib/components/HeaderNavigation.tsx +11 -9
  40. package/src/lib/components/MobileTopNavigation.tsx +43 -24
  41. package/src/lib/components/SiteTitle.tsx +20 -0
  42. package/src/lib/components/ThemeSwitch.tsx +36 -2
  43. package/src/lib/components/TopNavigation.tsx +20 -33
  44. package/src/lib/core/ZudokuContext.ts +1 -0
  45. package/src/vite/config.ts +21 -0
  46. package/src/vite/plugin-auth.ts +4 -1
  47. 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
- const apiBaseUrl = this.ensureConfiguredBackend();
279
+ this.ensureConfiguredBackend();
239
280
  const target =
240
281
  redirectTo ?? this.redirectToAfterSignIn ?? window.location.href;
241
- window.location.assign(buildDevPortalLoginUrl(apiBaseUrl, target));
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
- const apiBaseUrl = this.ensureConfiguredBackend();
291
+ this.ensureConfiguredBackend();
249
292
  const target =
250
293
  redirectTo ?? this.redirectToAfterSignUp ?? window.location.href;
251
- window.location.assign(buildDevPortalLoginUrl(apiBaseUrl, target));
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 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
+ };
@@ -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={cn(
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 = context.options.header?.navigation;
150
- const navPosition = context.options.header?.placements?.navigation ?? "end";
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 = header?.navigation ?? [];
131
- const { isAuthenticated, profile, isAuthEnabled } = authState;
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
- Logout
249
+ Log out
245
250
  </Link>
246
251
  </Button>
247
252
  ) : (
248
- <Button asChild variant="outline">
249
- <Link
250
- to={`/signin?redirect=${encodeURIComponent(location.pathname)}`}
251
- onClick={() => setDrawerOpen(false)}
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
- Login
254
- </Link>
255
- </Button>
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={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
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 <style>{`:root { --top-nav-height: 0px; }`}</style>;
19
+ return null;
22
20
  }
23
21
 
24
22
  return (
25
23
  <Suspense>
26
- <div className="items-center justify-between px-8 h-(--top-nav-height) hidden lg:flex text-sm relative">
27
- <nav className="text-sm">
28
- <ul className="flex flex-row items-center gap-8">
29
- {filteredItems.map((item) =>
30
- item.type === "separator" ? (
31
- <li key={item.label} className="-mx-4 h-7">
32
- <Separator orientation="vertical" />
33
- </li>
34
- ) : item.type !== "section" && item.type !== "filter" ? (
35
- <li key={item.label + item.type}>
36
- <TopNavItem {...item} />
37
- </li>
38
- ) : null,
39
- )}
40
- </ul>
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:py-3.5 font-medium -mb-px transition duration-150 delay-75 relative",
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
- "text-foreground",
67
- // underline with view transition animation
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}