@lukoweb/apitogo 0.1.54 → 0.1.56

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 (45) hide show
  1. package/dist/cli/cli.js +831 -398
  2. package/dist/declarations/config/default-landing-content.d.ts +2 -0
  3. package/dist/declarations/config/landing-manifest.d.ts +82 -0
  4. package/dist/declarations/config/local-manifest.d.ts +82 -0
  5. package/dist/declarations/config/resolve-modules.d.ts +3 -1
  6. package/dist/declarations/config/validators/ModulesSchema.d.ts +259 -0
  7. package/dist/declarations/config/validators/ZudokuConfig.d.ts +57 -0
  8. package/dist/declarations/lib/authentication/header-nav-filter.d.ts +4 -0
  9. package/dist/declarations/lib/authentication/providers/dev-portal-auth-pages.d.ts +5 -5
  10. package/dist/declarations/lib/authentication/providers/dev-portal-utils.d.ts +1 -0
  11. package/dist/declarations/lib/components/HeaderAuthActions.d.ts +3 -0
  12. package/dist/declarations/lib/components/SiteTitle.d.ts +6 -0
  13. package/dist/declarations/lib/components/ThemeSwitch.d.ts +3 -1
  14. package/dist/declarations/lib/components/TopNavigation.d.ts +1 -1
  15. package/dist/declarations/lib/core/ZudokuContext.d.ts +1 -0
  16. package/dist/flat-config.d.ts +95 -38
  17. package/package.json +1 -1
  18. package/src/app/main.css +2 -2
  19. package/src/config/apitogo-config-core.ts +13 -0
  20. package/src/config/apitogo-config-loader.browser.ts +17 -0
  21. package/src/config/build-apitogo-config.ts +13 -4
  22. package/src/config/default-landing-content.ts +154 -0
  23. package/src/config/landing-manifest.ts +11 -0
  24. package/src/config/landing-openapi-showcase.ts +115 -0
  25. package/src/config/loader.ts +17 -3
  26. package/src/config/local-manifest.ts +114 -10
  27. package/src/config/resolve-modules.ts +121 -32
  28. package/src/config/validators/ModulesSchema.ts +109 -0
  29. package/src/config/validators/ZudokuConfig.ts +1 -0
  30. package/src/lib/authentication/auth-header-nav.ts +5 -15
  31. package/src/lib/authentication/header-nav-filter.ts +36 -0
  32. package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +95 -35
  33. package/src/lib/authentication/providers/dev-portal-utils.ts +26 -7
  34. package/src/lib/authentication/providers/dev-portal.tsx +18 -8
  35. package/src/lib/components/Header.tsx +49 -39
  36. package/src/lib/components/HeaderAuthActions.tsx +36 -0
  37. package/src/lib/components/HeaderNavigation.tsx +11 -9
  38. package/src/lib/components/MobileTopNavigation.tsx +43 -24
  39. package/src/lib/components/SiteTitle.tsx +20 -0
  40. package/src/lib/components/ThemeSwitch.tsx +36 -2
  41. package/src/lib/components/TopNavigation.tsx +20 -33
  42. package/src/lib/core/ZudokuContext.ts +1 -0
  43. package/src/vite/config.ts +21 -0
  44. package/src/vite/plugin-auth.ts +4 -1
  45. package/src/vite/plugin-config.ts +39 -4
@@ -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}
@@ -71,6 +71,7 @@ type Site = Partial<{
71
71
  dir?: "ltr" | "rtl";
72
72
  showPoweredBy?: boolean;
73
73
  title?: string;
74
+ showTitleInHeader?: boolean;
74
75
  logo?: {
75
76
  src: {
76
77
  light: string;
@@ -21,6 +21,11 @@ import { ensurePagefindDevStub } from "./pagefind-dev-stub.js";
21
21
  import vitePlugin from "./plugin.js";
22
22
  import { getZuploSystemConfigurations } from "./zuplo.js";
23
23
 
24
+ const browserConfigLoaderPath = path.join(
25
+ getZudokuRootDir(),
26
+ "src/config/apitogo-config-loader.browser.ts",
27
+ );
28
+
24
29
  /** Resolved absolute public directory (Vite default: `<root>/public`). */
25
30
  const resolveMergedPublicDir = (
26
31
  rootDir: string,
@@ -114,6 +119,22 @@ export async function getViteConfig(
114
119
  "@mdx-js/react": import.meta.resolve("@mdx-js/react"),
115
120
  },
116
121
  },
122
+ environments: {
123
+ client: {
124
+ resolve: {
125
+ alias: {
126
+ [path.join(
127
+ getZudokuRootDir(),
128
+ "src/config/apitogo-config-loader.node.ts",
129
+ )]: browserConfigLoaderPath,
130
+ [path.join(
131
+ getZudokuRootDir(),
132
+ "src/config/apitogo-config-loader.node.js",
133
+ )]: browserConfigLoaderPath,
134
+ },
135
+ },
136
+ },
137
+ },
117
138
  define: {
118
139
  "process.env.ZUDOKU_VERSION": JSON.stringify(packageJson.version),
119
140
  "process.env.IS_ZUPLO": ZuploEnv.isZuplo,
@@ -16,7 +16,10 @@ const viteAuthPlugin = (): Plugin => {
16
16
  if (id === resolvedVirtualModuleId) {
17
17
  const config = getCurrentConfig();
18
18
 
19
- if (!config.authentication || config.__meta.mode === "standalone") {
19
+ if (
20
+ !config.authentication?.type ||
21
+ config.__meta.mode === "standalone"
22
+ ) {
20
23
  return `export const configuredAuthProvider = undefined;`;
21
24
  }
22
25
  // TODO: Validate that the authConfig.type is a valid authentication provider
@@ -1,18 +1,50 @@
1
+ import path from "node:path";
1
2
  import { normalizePath, type Plugin, type ResolvedConfig } from "vite";
3
+ import { getZudokuRootDir } from "../cli/common/package-json.js";
2
4
  import { getCurrentConfig, loadZudokuConfig } from "../config/loader.js";
5
+ import type { ConfigWithMeta } from "../config/loader.js";
3
6
 
4
7
  const virtualModuleId = "virtual:zudoku-config";
5
8
  const resolvedVirtualModuleId = `\0${virtualModuleId}`;
9
+ const browserConfigLoaderPath = path.join(
10
+ getZudokuRootDir(),
11
+ "src/config/apitogo-config-loader.browser.ts",
12
+ );
13
+
14
+ function serializeClientConfig(config: ConfigWithMeta): string {
15
+ const {
16
+ plugins: _plugins,
17
+ __meta: _meta,
18
+ __resolvedModules,
19
+ ...rest
20
+ } = config;
21
+
22
+ return JSON.stringify({ ...rest, __resolvedModules }, (_key, value) =>
23
+ typeof value === "function" ? undefined : value,
24
+ );
25
+ }
6
26
 
7
27
  const viteConfigPlugin = (): Plugin => {
8
28
  let viteConfig: ResolvedConfig;
9
29
 
10
30
  return {
11
31
  name: "zudoku-config-plugin",
12
- resolveId(id) {
13
- if (id !== virtualModuleId) return;
32
+ resolveId(source, _importer, options) {
33
+ if (source === virtualModuleId) {
34
+ return resolvedVirtualModuleId;
35
+ }
36
+
37
+ if (!source.includes("apitogo-config-loader.node")) {
38
+ return;
39
+ }
40
+
41
+ const isServerEnvironment =
42
+ options?.ssr === true ||
43
+ this.environment?.config?.consumer === "server";
14
44
 
15
- return resolvedVirtualModuleId;
45
+ if (!isServerEnvironment) {
46
+ return browserConfigLoaderPath;
47
+ }
16
48
  },
17
49
  configResolved(resolvedConfig) {
18
50
  viteConfig = resolvedConfig;
@@ -43,8 +75,11 @@ const viteConfigPlugin = (): Plugin => {
43
75
  import rawConfig from "${normalizePath(configPath)}";
44
76
  import { runPluginTransformConfig } from "@lukoweb/apitogo/plugins";
45
77
 
78
+ const embeddedConfig = ${serializeClientConfig(loadedConfig)};
79
+
46
80
  const config = await runPluginTransformConfig({
47
- ...rawConfig,
81
+ ...embeddedConfig,
82
+ plugins: rawConfig?.plugins ?? embeddedConfig.plugins ?? [],
48
83
  __meta: ${JSON.stringify(clientMeta)},
49
84
  });
50
85
  export default config;