@lukoweb/apitogo 0.1.60 → 0.1.61

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 CHANGED
@@ -419,6 +419,13 @@ function syncAuthentication(normalized) {
419
419
  if (!Array.isArray(auth.providers)) {
420
420
  auth.providers = [];
421
421
  }
422
+ const topLevelDemoMode = normalized.demoMode;
423
+ if (auth.demoMode === void 0) {
424
+ auth.demoMode = topLevelDemoMode !== false;
425
+ }
426
+ }
427
+ if (normalized.demoMode !== void 0) {
428
+ auth.demoMode = normalized.demoMode !== false;
422
429
  }
423
430
  normalized.authentication = auth;
424
431
  }
@@ -1600,7 +1607,7 @@ var init_shadcn_registry = __esm({
1600
1607
  import { createRequire } from "node:module";
1601
1608
  import path4 from "node:path";
1602
1609
  import { normalizeTheme } from "shiki";
1603
- var THEME_VARIABLES, uncamelize, processColorValue, generateCss, generateRegistryCss, processCustomCss, MAIN_REPLACE, DEFAULT_THEME_REPLACE, GOOGLE_FONTS, getGoogleFontUrl, processFont, processFontConfig, processFonts, MODULE_SOURCE_PACKAGES, resolveModuleSourceDirs, virtualModuleId, resolvedVirtualModuleId, viteThemePlugin;
1610
+ var THEME_VARIABLES, uncamelize, processColorValue, generateCss, generateRegistryCss, processCustomCss, MAIN_REPLACE, DEFAULT_THEME_REPLACE, GOOGLE_FONTS, getGoogleFontUrl, processFont, processFontConfig, DEFAULT_FONT_SANS, DEFAULT_FONT_MONO, DEFAULT_FONT_HEADING, processFonts, MODULE_SOURCE_PACKAGES, resolveModuleSourceDirs, virtualModuleId, resolvedVirtualModuleId, viteThemePlugin;
1604
1611
  var init_plugin_theme = __esm({
1605
1612
  "src/vite/plugin-theme.ts"() {
1606
1613
  init_loader();
@@ -1712,12 +1719,16 @@ var init_plugin_theme = __esm({
1712
1719
  fontFamily: fontConfig.fontFamily
1713
1720
  };
1714
1721
  };
1722
+ DEFAULT_FONT_SANS = '"IBM Plex Sans", system-ui, sans-serif';
1723
+ DEFAULT_FONT_MONO = '"JetBrains Mono", monospace';
1724
+ DEFAULT_FONT_HEADING = '"Space Grotesk", system-ui, sans-serif';
1715
1725
  processFonts = async (themeConfig) => {
1716
1726
  const imports = [];
1717
1727
  const families = {
1718
- sans: "Geist, sans-serif",
1728
+ sans: DEFAULT_FONT_SANS,
1719
1729
  serif: void 0,
1720
- mono: '"Geist Mono", monospace'
1730
+ mono: DEFAULT_FONT_MONO,
1731
+ heading: DEFAULT_FONT_HEADING
1721
1732
  };
1722
1733
  const userFonts = {
1723
1734
  sans: processFontConfig(themeConfig?.fonts?.sans),
@@ -1754,18 +1765,16 @@ var init_plugin_theme = __esm({
1754
1765
  } catch {
1755
1766
  }
1756
1767
  }
1757
- families.sans = userFonts.sans.fontFamily || registryFonts.sans || "Geist, sans-serif";
1768
+ families.sans = userFonts.sans.fontFamily || registryFonts.sans || DEFAULT_FONT_SANS;
1758
1769
  families.serif = userFonts.serif.fontFamily || registryFonts.serif;
1759
- families.mono = userFonts.mono.fontFamily || registryFonts.mono || '"Geist Mono", monospace';
1770
+ families.mono = userFonts.mono.fontFamily || registryFonts.mono || DEFAULT_FONT_MONO;
1760
1771
  if (!userFonts.sans.fontFamily && !registryFonts.sans) {
1761
- imports.push(getGoogleFontUrl("Geist"));
1762
- }
1763
- if (!userFonts.serif.fontFamily && !registryFonts.serif) {
1764
- imports.push(getGoogleFontUrl("Playfair Display"));
1772
+ imports.push(getGoogleFontUrl("IBM Plex Sans"));
1765
1773
  }
1766
1774
  if (!userFonts.mono.fontFamily && !registryFonts.mono) {
1767
- imports.push(getGoogleFontUrl("Geist Mono"));
1775
+ imports.push(getGoogleFontUrl("JetBrains Mono"));
1768
1776
  }
1777
+ imports.push(getGoogleFontUrl("Space Grotesk"));
1769
1778
  return { imports, families };
1770
1779
  };
1771
1780
  MODULE_SOURCE_PACKAGES = [
@@ -1882,7 +1891,8 @@ ${generateCss(themeConfig.dark)}
1882
1891
  }
1883
1892
  const rootVars = [
1884
1893
  ` --font-sans: ${fonts.families.sans};`,
1885
- ` --font-mono: ${fonts.families.mono};`
1894
+ ` --font-mono: ${fonts.families.mono};`,
1895
+ ` --font-heading: ${fonts.families.heading};`
1886
1896
  ];
1887
1897
  if (fonts.families.serif) {
1888
1898
  rootVars.push(` --font-serif: ${fonts.families.serif};`);
@@ -4575,6 +4585,7 @@ var init_ZudokuConfig = __esm({
4575
4585
  z9.object({
4576
4586
  type: z9.literal("dev-portal").optional().default("dev-portal"),
4577
4587
  apiBaseUrl: z9.string().optional(),
4588
+ demoMode: z9.boolean().optional(),
4578
4589
  native: z9.object({
4579
4590
  enabled: z9.boolean().optional()
4580
4591
  }).optional(),
@@ -5227,7 +5238,7 @@ import {
5227
5238
  // package.json
5228
5239
  var package_default = {
5229
5240
  name: "@lukoweb/apitogo",
5230
- version: "0.1.60",
5241
+ version: "0.1.61",
5231
5242
  type: "module",
5232
5243
  sideEffects: [
5233
5244
  "**/*.css",
@@ -0,0 +1,2 @@
1
+ import type { ZudokuConfig } from "../config/validators/ZudokuConfig.js";
2
+ export declare const apitogoBrandTheme: NonNullable<ZudokuConfig["theme"]>;
@@ -369,6 +369,7 @@ declare const AuthenticationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
369
369
  }, z.core.$strip>, z.ZodObject<{
370
370
  type: z.ZodDefault<z.ZodOptional<z.ZodLiteral<"dev-portal">>>;
371
371
  apiBaseUrl: z.ZodOptional<z.ZodString>;
372
+ demoMode: z.ZodOptional<z.ZodBoolean>;
372
373
  native: z.ZodOptional<z.ZodObject<{
373
374
  enabled: z.ZodOptional<z.ZodBoolean>;
374
375
  }, z.core.$strip>>;
@@ -6790,6 +6791,7 @@ export declare const ZudokuConfig: z.ZodObject<{
6790
6791
  }, z.core.$strip>, z.ZodObject<{
6791
6792
  type: z.ZodDefault<z.ZodOptional<z.ZodLiteral<"dev-portal">>>;
6792
6793
  apiBaseUrl: z.ZodOptional<z.ZodString>;
6794
+ demoMode: z.ZodOptional<z.ZodBoolean>;
6793
6795
  native: z.ZodOptional<z.ZodObject<{
6794
6796
  enabled: z.ZodOptional<z.ZodBoolean>;
6795
6797
  }, z.core.$strip>>;
@@ -17,5 +17,6 @@ export { defaultLanguages } from "./lib/shiki.js";
17
17
  export { cn } from "./lib/ui/util.js";
18
18
  export { joinUrl } from "./lib/util/joinUrl.js";
19
19
  export { type ProblemJson, throwIfProblemJson, } from "./lib/util/problemJson.js";
20
+ export { apitogoBrandTheme } from "./app/apitogoBrandTheme.js";
20
21
  export { buildApitogoConfig } from "./config/build-apitogo-config.js";
21
22
  export { APITOGO_CONFIG_FILENAME, applyEnvOverrides, extractManifest, extractSiteConfig, normalizeApitogoConfig, resolveProjectDisplayName, } from "./config/apitogo-config-core.js";
@@ -1,5 +1,6 @@
1
- export declare function DevPortalLoginPage({ apiBaseUrl }: {
1
+ export declare function DevPortalLoginPage({ apiBaseUrl, demoMode, }: {
2
2
  apiBaseUrl?: string;
3
+ demoMode?: boolean;
3
4
  }): import("react/jsx-runtime").JSX.Element;
4
5
  export declare function DevPortalRegisterPage({ apiBaseUrl }: {
5
6
  apiBaseUrl?: string;
@@ -0,0 +1,30 @@
1
+ import type { DevPortalAuthenticationConfig } from "../../../config/config.js";
2
+ import type { UserProfile } from "../state.js";
3
+ import type { EndUserMeResponse } from "./dev-portal-constants.js";
4
+ export declare const DEMO_EMAIL = "demo@apitogo.local";
5
+ export declare const DEMO_PASSWORD = "password";
6
+ export declare const DEMO_USER_ID = "00000000-0000-0000-0000-000000000001";
7
+ export declare const DEMO_DISPLAY_NAME = "Demo User";
8
+ export declare const DEMO_SUBSCRIPTION_KEY = "demo-subscription-key";
9
+ export type DemoPlanInput = {
10
+ sku: string;
11
+ displayName?: string;
12
+ };
13
+ export declare function resolveDemoMode(config: Pick<DevPortalAuthenticationConfig, "demoMode">): boolean;
14
+ export declare function isDemoCredential(email: string, password: string): boolean;
15
+ export declare function createDemoProfile(): UserProfile;
16
+ export declare function resolveDemoPlanSku(plans: DemoPlanInput[] | undefined): string;
17
+ export declare function createDemoMeResponse(plans?: DemoPlanInput[]): EndUserMeResponse;
18
+ export type DemoCredentialsResponse = {
19
+ idToken: string;
20
+ accessToken: string | null;
21
+ subscriptionKey: string;
22
+ gatewayBaseUrl: string | null;
23
+ planSku: string;
24
+ expiresAt: string | null;
25
+ };
26
+ export declare function createDemoCredentials(plans?: DemoPlanInput[]): DemoCredentialsResponse;
27
+ export declare function readDemoSession(): boolean;
28
+ export declare function writeDemoSession(): void;
29
+ export declare function clearDemoSession(): void;
30
+ export declare function activateDemoSession(apiBaseUrl?: string): void;
@@ -7,6 +7,8 @@ export type DevPortalProviderData = {
7
7
  type: "dev-portal";
8
8
  apiBaseUrl: string;
9
9
  authMode: DevPortalAuthMode;
10
+ isDemoSession?: boolean;
11
+ demoMode?: boolean;
10
12
  };
11
13
  declare module "../state.js" {
12
14
  interface ProviderDataRegistry {
@@ -15,6 +17,7 @@ declare module "../state.js" {
15
17
  }
16
18
  export declare class DevPortalAuthenticationProvider extends CoreAuthenticationPlugin implements AuthenticationPlugin {
17
19
  private readonly apiBaseUrl;
20
+ private readonly demoMode;
18
21
  private readonly redirectToAfterSignIn;
19
22
  private readonly redirectToAfterSignUp;
20
23
  private readonly redirectToAfterSignOut;
@@ -22,7 +25,7 @@ export declare class DevPortalAuthenticationProvider extends CoreAuthenticationP
22
25
  private refreshGeneration;
23
26
  private refreshInFlight;
24
27
  private authConfigNativeEnabled;
25
- constructor({ apiBaseUrl, redirectToAfterSignIn, redirectToAfterSignUp, redirectToAfterSignOut, }: DevPortalAuthenticationConfig);
28
+ constructor({ apiBaseUrl, demoMode, redirectToAfterSignIn, redirectToAfterSignUp, redirectToAfterSignOut, }: DevPortalAuthenticationConfig);
26
29
  getRoutes(): {
27
30
  path: string;
28
31
  element: import("react/jsx-runtime").JSX.Element;
@@ -31,6 +34,7 @@ export declare class DevPortalAuthenticationProvider extends CoreAuthenticationP
31
34
  onPageLoad: () => Promise<void>;
32
35
  private syncBackendAvailability;
33
36
  private setPreviewState;
37
+ private setDemoAuthenticatedState;
34
38
  private ensureConfiguredBackend;
35
39
  refreshUserProfile(): Promise<boolean>;
36
40
  private refreshUserProfileInternal;
@@ -39,6 +43,7 @@ export declare class DevPortalAuthenticationProvider extends CoreAuthenticationP
39
43
  signOut(_: AuthActionContext): Promise<void>;
40
44
  hasDistinctSignUpFlow(): boolean;
41
45
  signRequest(request: Request): Promise<Request>;
46
+ completeDemoLogin(): void;
42
47
  }
43
48
  declare const devPortalAuth: AuthenticationProviderInitializer<DevPortalAuthenticationConfig>;
44
49
  export default devPortalAuth;
@@ -322,6 +322,7 @@ export interface FlatZudokuConfig {
322
322
  } | {
323
323
  type?: "dev-portal"
324
324
  apiBaseUrl?: string
325
+ demoMode?: boolean
325
326
  native?: {
326
327
  enabled?: boolean
327
328
  }
@@ -47,10 +47,22 @@ VITE_APITOGO_AUTHENTICATION_API_BASE_URL=https://your-dev-portal-api.example.com
47
47
  You can also set `authentication.apiBaseUrl` in `apitogo.config.json` or
48
48
  `APITOGO_CONFIG_authentication__apiBaseUrl` in `.env`.
49
49
 
50
- **Local preview:** When the backend URL is a placeholder or unreachable, the account area shows a
51
- message that sign-in unlocks after publish. Docs and API reference remain public. Plans on
52
- `/pricing` load from `apitogo.config.json` (overridable via `.env` — see [Manifest](./manifest.md),
53
- including `includedActionKeys` per plan). Plan edits hot-reload without restarting the dev server.
50
+ **Local demo mode:** In development (`npm run dev`), `demoMode` is enabled by default when the user
51
+ panel is on. You do not need `VITE_APITOGO_AUTHENTICATION_API_BASE_URL`, OIDC, or a running
52
+ dev-portal backend to explore protected account pages locally. Sign in at `/login` with:
53
+
54
+ ```text
55
+ demo@apitogo.local / password
56
+ ```
57
+
58
+ Disable with `APITOGO_CONFIG_demoMode=false` in `.env` or `"demoMode": false` in
59
+ `apitogo.config.json`. Demo mode is never active in production builds.
60
+
61
+ **Local preview (demo off):** When the backend URL is a placeholder or unreachable and demo mode is
62
+ disabled, the account area shows a message that sign-in unlocks after publish. Docs and API
63
+ reference remain public. Plans on `/pricing` load from `apitogo.config.json` (overridable via `.env`
64
+ — see [Manifest](./manifest.md), including `includedActionKeys` per plan). Plan edits hot-reload
65
+ without restarting the dev server.
54
66
 
55
67
  ### Dev portal configuration files
56
68
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.1.60",
3
+ "version": "0.1.61",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -0,0 +1,46 @@
1
+ import type { ZudokuConfig } from "../config/validators/ZudokuConfig.js";
2
+
3
+ /**
4
+ * APIToGo brand theme for per-site overrides via apitogo.config.
5
+ * Keep in sync with defaultTheme.css (full palette including sidebar/chart tokens).
6
+ */
7
+ export const apitogoBrandTheme: NonNullable<ZudokuConfig["theme"]> = {
8
+ fonts: {
9
+ sans: "IBM Plex Sans",
10
+ mono: "JetBrains Mono",
11
+ },
12
+ light: {
13
+ background: "#F5F6F9",
14
+ foreground: "#0E1016",
15
+ card: "#FFFFFF",
16
+ cardForeground: "#0E1016",
17
+ primary: "#E11D2A",
18
+ primaryForeground: "#ffffff",
19
+ secondary: "#FAFBFC",
20
+ secondaryForeground: "#0E1016",
21
+ muted: "#FAFBFC",
22
+ mutedForeground: "#565E70",
23
+ accent: "rgba(225, 29, 42, 0.08)",
24
+ accentForeground: "#E11D2A",
25
+ border: "#E6E8EF",
26
+ input: "#D6DAE3",
27
+ ring: "#E11D2A",
28
+ },
29
+ dark: {
30
+ background: "#0B0D12",
31
+ foreground: "#EDEFF5",
32
+ card: "#14171F",
33
+ cardForeground: "#EDEFF5",
34
+ primary: "#F23645",
35
+ primaryForeground: "#ffffff",
36
+ secondary: "#191D27",
37
+ secondaryForeground: "#EDEFF5",
38
+ muted: "#191D27",
39
+ mutedForeground: "#A6AEBF",
40
+ accent: "rgba(242, 54, 69, 0.13)",
41
+ accentForeground: "#F23645",
42
+ border: "#252B37",
43
+ input: "#333A48",
44
+ ring: "#F23645",
45
+ },
46
+ };
@@ -1,77 +1,77 @@
1
1
  /**
2
- * Default theme for Zudoku. Based on shadcn/ui zinc base colors.
3
- * See: https://ui.shadcn.com/docs/theming#zinc
2
+ * Default APIToGo brand theme for scaffolded sites.
3
+ * Color tokens sourced from webapp-template/APIToGo.dc.html (:root dark + light override).
4
4
  */
5
5
 
6
6
  :root {
7
7
  --radius: 0.625rem;
8
- --background: oklch(1 0 0);
9
- --foreground: oklch(0.141 0.005 285.823);
10
- --card: oklch(1 0 0);
11
- --card-foreground: oklch(0.141 0.005 285.823);
12
- --popover: oklch(1 0 0);
13
- --popover-foreground: oklch(0.141 0.005 285.823);
14
- --primary: oklch(0.21 0.006 285.885);
15
- --primary-foreground: oklch(0.985 0 0);
16
- --secondary: oklch(0.967 0.001 286.375);
17
- --secondary-foreground: oklch(0.21 0.006 285.885);
18
- --muted: oklch(0.967 0.001 286.375);
19
- --muted-foreground: oklch(0.552 0.016 285.938);
20
- --accent: oklch(0.967 0.001 286.375);
21
- --accent-foreground: oklch(0.21 0.006 285.885);
22
- --destructive: oklch(0.577 0.245 27.325);
8
+ --background: #f5f6f9;
9
+ --foreground: #0e1016;
10
+ --card: #ffffff;
11
+ --card-foreground: #0e1016;
12
+ --popover: #ffffff;
13
+ --popover-foreground: #0e1016;
14
+ --primary: #e11d2a;
15
+ --primary-foreground: #ffffff;
16
+ --secondary: #fafbfc;
17
+ --secondary-foreground: #0e1016;
18
+ --muted: #fafbfc;
19
+ --muted-foreground: #565e70;
20
+ --accent: rgba(225, 29, 42, 0.08);
21
+ --accent-foreground: #e11d2a;
22
+ --destructive: #e11d2a;
23
23
  --warning: oklch(0.84 0.16 84);
24
24
  --warning-foreground: oklch(0.47 0.14 46.2);
25
- --border: oklch(0.92 0.004 286.32);
26
- --input: oklch(0.92 0.004 286.32);
27
- --ring: oklch(0.705 0.015 286.067);
28
- --chart-1: oklch(0.646 0.222 41.116);
29
- --chart-2: oklch(0.6 0.118 184.704);
30
- --chart-3: oklch(0.398 0.07 227.392);
31
- --chart-4: oklch(0.828 0.189 84.429);
32
- --chart-5: oklch(0.769 0.188 70.08);
33
- --sidebar: oklch(0.985 0 0);
34
- --sidebar-foreground: oklch(0.141 0.005 285.823);
35
- --sidebar-primary: oklch(0.21 0.006 285.885);
36
- --sidebar-primary-foreground: oklch(0.985 0 0);
37
- --sidebar-accent: oklch(0.967 0.001 286.375);
38
- --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
39
- --sidebar-border: oklch(0.92 0.004 286.32);
40
- --sidebar-ring: oklch(0.705 0.015 286.067);
25
+ --border: #e6e8ef;
26
+ --input: #d6dae3;
27
+ --ring: #e11d2a;
28
+ --chart-1: #e11d2a;
29
+ --chart-2: #0e9e63;
30
+ --chart-3: #4e5ac0;
31
+ --chart-4: #8a92a3;
32
+ --chart-5: #565e70;
33
+ --sidebar: #ffffff;
34
+ --sidebar-foreground: #0e1016;
35
+ --sidebar-primary: #e11d2a;
36
+ --sidebar-primary-foreground: #ffffff;
37
+ --sidebar-accent: #fafbfc;
38
+ --sidebar-accent-foreground: #0e1016;
39
+ --sidebar-border: #e6e8ef;
40
+ --sidebar-ring: #e11d2a;
41
41
  }
42
42
 
43
43
  .dark {
44
- --background: oklch(0.141 0.005 285.823);
45
- --foreground: oklch(0.985 0 0);
46
- --card: oklch(0.21 0.006 285.885);
47
- --card-foreground: oklch(0.985 0 0);
48
- --popover: oklch(0.21 0.006 285.885);
49
- --popover-foreground: oklch(0.985 0 0);
50
- --primary: oklch(0.92 0.004 286.32);
51
- --primary-foreground: oklch(0.21 0.006 285.885);
52
- --secondary: oklch(0.274 0.006 286.033);
53
- --secondary-foreground: oklch(0.985 0 0);
54
- --muted: oklch(0.274 0.006 286.033);
55
- --muted-foreground: oklch(0.705 0.015 286.067);
56
- --accent: oklch(0.274 0.006 286.033);
57
- --accent-foreground: oklch(0.985 0 0);
58
- --destructive: oklch(0.704 0.191 22.216);
44
+ --background: #0b0d12;
45
+ --foreground: #edeff5;
46
+ --card: #14171f;
47
+ --card-foreground: #edeff5;
48
+ --popover: #1d222d;
49
+ --popover-foreground: #edeff5;
50
+ --primary: #f23645;
51
+ --primary-foreground: #ffffff;
52
+ --secondary: #191d27;
53
+ --secondary-foreground: #edeff5;
54
+ --muted: #191d27;
55
+ --muted-foreground: #a6aebf;
56
+ --accent: rgba(242, 54, 69, 0.13);
57
+ --accent-foreground: #f23645;
58
+ --destructive: #f23645;
59
59
  --warning: oklch(0.41 0.11 46);
60
60
  --warning-foreground: oklch(0.99 0.02 95);
61
- --border: oklch(1 0 0 / 10%);
62
- --input: oklch(1 0 0 / 15%);
63
- --ring: oklch(0.552 0.016 285.938);
64
- --chart-1: oklch(0.488 0.243 264.376);
65
- --chart-2: oklch(0.696 0.17 162.48);
66
- --chart-3: oklch(0.769 0.188 70.08);
67
- --chart-4: oklch(0.627 0.265 303.9);
68
- --chart-5: oklch(0.645 0.246 16.439);
69
- --sidebar: oklch(0.21 0.006 285.885);
70
- --sidebar-foreground: oklch(0.985 0 0);
71
- --sidebar-primary: oklch(0.488 0.243 264.376);
72
- --sidebar-primary-foreground: oklch(0.985 0 0);
73
- --sidebar-accent: oklch(0.274 0.006 286.033);
74
- --sidebar-accent-foreground: oklch(0.985 0 0);
75
- --sidebar-border: oklch(1 0 0 / 10%);
76
- --sidebar-ring: oklch(0.552 0.016 285.938);
61
+ --border: #252b37;
62
+ --input: #333a48;
63
+ --ring: #f23645;
64
+ --chart-1: #f23645;
65
+ --chart-2: #16c784;
66
+ --chart-3: #aeb9e8;
67
+ --chart-4: #6b7385;
68
+ --chart-5: #a6aebf;
69
+ --sidebar: #0e1119;
70
+ --sidebar-foreground: #edeff5;
71
+ --sidebar-primary: #f23645;
72
+ --sidebar-primary-foreground: #ffffff;
73
+ --sidebar-accent: #191d27;
74
+ --sidebar-accent-foreground: #edeff5;
75
+ --sidebar-border: #252b37;
76
+ --sidebar-ring: #f23645;
77
77
  }
package/src/app/main.css CHANGED
@@ -49,6 +49,7 @@
49
49
  --font-sans: var(--font-sans);
50
50
  --font-mono: var(--font-mono);
51
51
  --font-serif: var(--font-serif);
52
+ --font-heading: var(--font-heading);
52
53
  }
53
54
 
54
55
  @utility prose {
@@ -161,6 +162,15 @@
161
162
  "calt" 1;
162
163
  }
163
164
 
165
+ h1,
166
+ h2,
167
+ h3,
168
+ h4,
169
+ h5,
170
+ h6 {
171
+ font-family: var(--font-heading);
172
+ }
173
+
164
174
  ::selection {
165
175
  background: var(--color-primary);
166
176
  color: var(--color-primary-foreground);
@@ -452,6 +452,15 @@ function syncAuthentication(normalized: ApitogoJsonConfig): void {
452
452
  if (!Array.isArray(auth.providers)) {
453
453
  auth.providers = [];
454
454
  }
455
+
456
+ const topLevelDemoMode = normalized.demoMode;
457
+ if (auth.demoMode === undefined) {
458
+ auth.demoMode = topLevelDemoMode !== false;
459
+ }
460
+ }
461
+
462
+ if (normalized.demoMode !== undefined) {
463
+ auth.demoMode = normalized.demoMode !== false;
455
464
  }
456
465
 
457
466
  normalized.authentication = auth;
@@ -494,6 +494,7 @@ const AuthenticationSchema = z.discriminatedUnion("type", [
494
494
  z.object({
495
495
  type: z.literal("dev-portal").optional().default("dev-portal"),
496
496
  apiBaseUrl: z.string().optional(),
497
+ demoMode: z.boolean().optional(),
497
498
  native: z
498
499
  .object({
499
500
  enabled: z.boolean().optional(),
package/src/index.ts CHANGED
@@ -71,6 +71,7 @@ export {
71
71
  type ProblemJson,
72
72
  throwIfProblemJson,
73
73
  } from "./lib/util/problemJson.js";
74
+ export { apitogoBrandTheme } from "./app/apitogoBrandTheme.js";
74
75
  export { buildApitogoConfig } from "./config/build-apitogo-config.js";
75
76
  export {
76
77
  APITOGO_CONFIG_FILENAME,
@@ -74,11 +74,20 @@ export const useAuth = () => {
74
74
 
75
75
  useRefreshUserProfile();
76
76
 
77
+ const providerData = authState.providerData as
78
+ | { isDemoSession?: boolean; demoMode?: boolean }
79
+ | null
80
+ | undefined;
81
+ const isDemoSession = providerData?.isDemoSession === true;
82
+ const isDemoMode = providerData?.demoMode === true;
83
+
77
84
  return {
78
85
  isAuthEnabled,
79
86
  ...authState,
80
87
  isBackendAvailable: authState.isBackendAvailable,
81
88
  authMode: authState.authMode,
89
+ isDemoSession,
90
+ isDemoMode,
82
91
  hasDistinctSignUpFlow: authentication?.hasDistinctSignUpFlow?.() ?? true,
83
92
 
84
93
  login: async (options?: AuthActionOptions) => {
@@ -12,10 +12,17 @@ import { Label } from "@lukoweb/apitogo/ui/Label.js";
12
12
  import { useCallback, useEffect, useState } from "react";
13
13
  import { Link, useSearchParams } from "react-router";
14
14
  import type { DevPortalAuthConfigResponse } from "./dev-portal-constants.js";
15
+ import {
16
+ DEMO_EMAIL,
17
+ DEMO_PASSWORD,
18
+ activateDemoSession,
19
+ isDemoCredential,
20
+ } from "./dev-portal-demo.js";
15
21
  import {
16
22
  buildDevPortalOidcChallengeUrl,
17
23
  fetchDevPortalAuthConfig,
18
24
  isNativeAuthEnabled,
25
+ isPlaceholderDevPortalApiUrl,
19
26
  postDevPortalAuth,
20
27
  resolveDevPortalApiBaseUrl,
21
28
  toAbsoluteReturnUrl,
@@ -100,20 +107,60 @@ function OidcProviderButtons({
100
107
  );
101
108
  }
102
109
 
103
- export function DevPortalLoginPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
110
+ function DemoLoginHint() {
111
+ return (
112
+ <div className="rounded-lg border border-dashed bg-muted/30 p-3 text-sm text-muted-foreground">
113
+ <p className="font-medium text-foreground">Local demo sign-in</p>
114
+ <p className="mt-1">
115
+ Use <code className="rounded bg-muted px-1 py-0.5">{DEMO_EMAIL}</code>{" "}
116
+ with password{" "}
117
+ <code className="rounded bg-muted px-1 py-0.5">{DEMO_PASSWORD}</code> to
118
+ explore the account area without a dev-portal backend.
119
+ </p>
120
+ </div>
121
+ );
122
+ }
123
+
124
+ export function DevPortalLoginPage({
125
+ apiBaseUrl,
126
+ demoMode = false,
127
+ }: {
128
+ apiBaseUrl?: string;
129
+ demoMode?: boolean;
130
+ }) {
104
131
  const baseUrl = useApiBaseUrl(apiBaseUrl);
105
132
  const returnUrl = useReturnUrl();
106
133
  const [config, setConfig] = useState<DevPortalAuthConfigResponse | null>(
107
134
  null,
108
135
  );
109
- const [configLoading, setConfigLoading] = useState(true);
136
+ const [configLoading, setConfigLoading] = useState(
137
+ Boolean(baseUrl) && !isPlaceholderDevPortalApiUrl(baseUrl),
138
+ );
110
139
  const [configError, setConfigError] = useState<string | null>(null);
140
+ const [useDemoFallback, setUseDemoFallback] = useState(
141
+ demoMode && (!baseUrl || isPlaceholderDevPortalApiUrl(baseUrl)),
142
+ );
143
+ const demoAuthAvailable = demoMode && useDemoFallback;
111
144
  const [email, setEmail] = useState("");
112
145
  const [password, setPassword] = useState("");
113
146
  const [error, setError] = useState<string | null>(null);
114
147
  const [pending, setPending] = useState(false);
115
148
 
116
149
  useEffect(() => {
150
+ if (!demoMode) {
151
+ setUseDemoFallback(false);
152
+ } else if (!baseUrl || isPlaceholderDevPortalApiUrl(baseUrl)) {
153
+ setUseDemoFallback(true);
154
+ }
155
+ }, [baseUrl, demoMode]);
156
+
157
+ useEffect(() => {
158
+ if (demoAuthAvailable) {
159
+ setConfig({ native: { enabled: true }, email: null, providers: [] });
160
+ setConfigLoading(false);
161
+ return;
162
+ }
163
+
117
164
  if (!baseUrl) return;
118
165
  setConfigLoading(true);
119
166
  setConfigError(null);
@@ -123,20 +170,41 @@ export function DevPortalLoginPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
123
170
  setConfigLoading(false);
124
171
  })
125
172
  .catch(() => {
173
+ if (demoMode) {
174
+ setUseDemoFallback(true);
175
+ setConfig({ native: { enabled: true }, email: null, providers: [] });
176
+ setConfigLoading(false);
177
+ setConfigError(null);
178
+ return;
179
+ }
180
+
126
181
  setConfig(null);
127
182
  setConfigLoading(false);
128
183
  setConfigError(
129
184
  `Could not reach the dev portal API at ${baseUrl}. If you use a local HTTPS certificate, open ${baseUrl}/api/v1/auth/status in your browser first to trust it.`,
130
185
  );
131
186
  });
132
- }, [baseUrl]);
187
+ }, [baseUrl, demoAuthAvailable, demoMode]);
133
188
 
134
189
  const onSubmit = useCallback(
135
190
  async (event: React.FormEvent) => {
136
191
  event.preventDefault();
137
- if (!baseUrl) return;
138
192
  setPending(true);
139
193
  setError(null);
194
+
195
+ if (demoAuthAvailable) {
196
+ if (!isDemoCredential(email, password)) {
197
+ setError("Invalid demo credentials.");
198
+ setPending(false);
199
+ return;
200
+ }
201
+
202
+ activateDemoSession(baseUrl);
203
+ window.location.assign(toAbsoluteReturnUrl(returnUrl));
204
+ return;
205
+ }
206
+
207
+ if (!baseUrl) return;
140
208
  try {
141
209
  await postDevPortalAuth(baseUrl, "/api/v1/auth/login", {
142
210
  email,
@@ -149,10 +217,10 @@ export function DevPortalLoginPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
149
217
  setPending(false);
150
218
  }
151
219
  },
152
- [baseUrl, email, password, returnUrl],
220
+ [baseUrl, demoAuthAvailable, email, password, returnUrl],
153
221
  );
154
222
 
155
- if (!baseUrl) {
223
+ if (!baseUrl && !demoAuthAvailable) {
156
224
  return (
157
225
  <AuthPageShell
158
226
  title="Sign in"
@@ -167,7 +235,8 @@ export function DevPortalLoginPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
167
235
 
168
236
  return (
169
237
  <AuthPageShell title="Sign in" description="Access your developer account.">
170
- {isNativeAuthEnabled(config) ? (
238
+ {demoAuthAvailable ? <DemoLoginHint /> : null}
239
+ {isNativeAuthEnabled(config) || demoAuthAvailable ? (
171
240
  <form className="space-y-4" onSubmit={onSubmit}>
172
241
  <div className="space-y-2">
173
242
  <Label htmlFor="email">Email</Label>
@@ -211,12 +280,14 @@ export function DevPortalLoginPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
211
280
  </div>
212
281
  </form>
213
282
  ) : null}
214
- {configError ? <AuthError message={configError} /> : null}
283
+ {configError && !demoAuthAvailable ? (
284
+ <AuthError message={configError} />
285
+ ) : null}
215
286
  {configLoading ? (
216
287
  <div className="flex justify-center">
217
288
  <Spinner />
218
289
  </div>
219
- ) : config ? (
290
+ ) : config && !demoAuthAvailable ? (
220
291
  <>
221
292
  <OidcProviderButtons
222
293
  apiBaseUrl={baseUrl}
@@ -0,0 +1,153 @@
1
+ import type { DevPortalAuthenticationConfig } from "../../../config/config.js";
2
+ import type { UserProfile } from "../state.js";
3
+ import { useAuthState } from "../state.js";
4
+ import type { EndUserMeResponse } from "./dev-portal-constants.js";
5
+
6
+ export const DEMO_EMAIL = "demo@apitogo.local";
7
+ export const DEMO_PASSWORD = "password";
8
+ export const DEMO_USER_ID = "00000000-0000-0000-0000-000000000001";
9
+ export const DEMO_DISPLAY_NAME = "Demo User";
10
+ export const DEMO_SUBSCRIPTION_KEY = "demo-subscription-key";
11
+
12
+ const DEMO_SESSION_STORAGE_KEY = "apitogo-demo-session";
13
+
14
+ export type DemoPlanInput = {
15
+ sku: string;
16
+ displayName?: string;
17
+ };
18
+
19
+ export function resolveDemoMode(
20
+ config: Pick<DevPortalAuthenticationConfig, "demoMode">,
21
+ ): boolean {
22
+ if (typeof import.meta !== "undefined" && !import.meta.env.DEV) {
23
+ return false;
24
+ }
25
+
26
+ return config.demoMode !== false;
27
+ }
28
+
29
+ export function isDemoCredential(email: string, password: string): boolean {
30
+ return (
31
+ email.trim().toLowerCase() === DEMO_EMAIL && password === DEMO_PASSWORD
32
+ );
33
+ }
34
+
35
+ export function createDemoProfile(): UserProfile {
36
+ return {
37
+ sub: DEMO_USER_ID,
38
+ email: DEMO_EMAIL,
39
+ emailVerified: true,
40
+ name: DEMO_DISPLAY_NAME,
41
+ pictureUrl: undefined,
42
+ };
43
+ }
44
+
45
+ export function resolveDemoPlanSku(plans: DemoPlanInput[] | undefined): string {
46
+ const first = plans?.[0];
47
+ return first?.sku ?? "free";
48
+ }
49
+
50
+ export function createDemoMeResponse(
51
+ plans?: DemoPlanInput[],
52
+ ): EndUserMeResponse {
53
+ const planSku = resolveDemoPlanSku(plans);
54
+ const periodEnd = new Date();
55
+ periodEnd.setMonth(periodEnd.getMonth() + 1);
56
+
57
+ return {
58
+ userId: DEMO_USER_ID,
59
+ displayName: DEMO_DISPLAY_NAME,
60
+ email: DEMO_EMAIL,
61
+ emailVerified: true,
62
+ subscriptions: [
63
+ {
64
+ planSku,
65
+ status: "Active",
66
+ entitlementState: "active",
67
+ currentPeriodEnd: periodEnd.toISOString(),
68
+ },
69
+ ],
70
+ };
71
+ }
72
+
73
+ export type DemoCredentialsResponse = {
74
+ idToken: string;
75
+ accessToken: string | null;
76
+ subscriptionKey: string;
77
+ gatewayBaseUrl: string | null;
78
+ planSku: string;
79
+ expiresAt: string | null;
80
+ };
81
+
82
+ export function createDemoCredentials(
83
+ plans?: DemoPlanInput[],
84
+ ): DemoCredentialsResponse {
85
+ const planSku = resolveDemoPlanSku(plans);
86
+
87
+ return {
88
+ idToken: "demo-id-token",
89
+ accessToken: null,
90
+ subscriptionKey: DEMO_SUBSCRIPTION_KEY,
91
+ gatewayBaseUrl: null,
92
+ planSku,
93
+ expiresAt: null,
94
+ };
95
+ }
96
+
97
+ export function readDemoSession(): boolean {
98
+ if (typeof window === "undefined") {
99
+ return false;
100
+ }
101
+
102
+ try {
103
+ return localStorage.getItem(DEMO_SESSION_STORAGE_KEY) === "1";
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ export function writeDemoSession(): void {
110
+ if (typeof window === "undefined") {
111
+ return;
112
+ }
113
+
114
+ try {
115
+ localStorage.setItem(DEMO_SESSION_STORAGE_KEY, "1");
116
+ } catch {
117
+ // ignore quota / privacy errors
118
+ }
119
+ }
120
+
121
+ export function clearDemoSession(): void {
122
+ if (typeof window === "undefined") {
123
+ return;
124
+ }
125
+
126
+ try {
127
+ localStorage.removeItem(DEMO_SESSION_STORAGE_KEY);
128
+ } catch {
129
+ // ignore
130
+ }
131
+ }
132
+
133
+ export function activateDemoSession(apiBaseUrl?: string): void {
134
+ writeDemoSession();
135
+
136
+ const profile = createDemoProfile();
137
+
138
+ useAuthState.setState({
139
+ isAuthenticated: true,
140
+ isPending: false,
141
+ sessionChecked: true,
142
+ profile,
143
+ providerData: {
144
+ type: "dev-portal",
145
+ apiBaseUrl: apiBaseUrl ?? "",
146
+ authMode: "preview",
147
+ isDemoSession: true,
148
+ demoMode: true,
149
+ },
150
+ isBackendAvailable: false,
151
+ authMode: "preview",
152
+ });
153
+ }
@@ -23,6 +23,12 @@ import type {
23
23
  DevPortalAuthMode,
24
24
  EndUserMeResponse,
25
25
  } from "./dev-portal-constants.js";
26
+ import {
27
+ clearDemoSession,
28
+ readDemoSession,
29
+ resolveDemoMode,
30
+ activateDemoSession,
31
+ } from "./dev-portal-demo.js";
26
32
  import {
27
33
  buildDevPortalLogoutUrl,
28
34
  isNativeAuthEnabled,
@@ -37,6 +43,8 @@ export type DevPortalProviderData = {
37
43
  type: "dev-portal";
38
44
  apiBaseUrl: string;
39
45
  authMode: DevPortalAuthMode;
46
+ isDemoSession?: boolean;
47
+ demoMode?: boolean;
40
48
  };
41
49
 
42
50
  declare module "../state.js" {
@@ -50,6 +58,7 @@ export class DevPortalAuthenticationProvider
50
58
  implements AuthenticationPlugin
51
59
  {
52
60
  private readonly apiBaseUrl: string | undefined;
61
+ private readonly demoMode: boolean;
53
62
  private readonly redirectToAfterSignIn: string | undefined;
54
63
  private readonly redirectToAfterSignUp: string | undefined;
55
64
  private readonly redirectToAfterSignOut: string;
@@ -61,19 +70,24 @@ export class DevPortalAuthenticationProvider
61
70
 
62
71
  constructor({
63
72
  apiBaseUrl,
73
+ demoMode,
64
74
  redirectToAfterSignIn,
65
75
  redirectToAfterSignUp,
66
76
  redirectToAfterSignOut = "/",
67
77
  }: DevPortalAuthenticationConfig) {
68
78
  super();
69
79
  this.apiBaseUrl = resolveDevPortalApiBaseUrl({ apiBaseUrl });
80
+ this.demoMode = resolveDemoMode({ demoMode });
70
81
  this.redirectToAfterSignIn = redirectToAfterSignIn;
71
82
  this.redirectToAfterSignUp = redirectToAfterSignUp;
72
83
  this.redirectToAfterSignOut = redirectToAfterSignOut;
73
84
  }
74
85
 
75
86
  getRoutes() {
76
- const pageProps = { apiBaseUrl: this.apiBaseUrl };
87
+ const pageProps = {
88
+ apiBaseUrl: this.apiBaseUrl,
89
+ demoMode: this.demoMode,
90
+ };
77
91
  return [
78
92
  ...super.getRoutes(),
79
93
  { path: "/login", element: <DevPortalLoginPage {...pageProps} /> },
@@ -112,6 +126,11 @@ export class DevPortalAuthenticationProvider
112
126
  }
113
127
  }
114
128
  if (!this.backendAvailable) {
129
+ if (this.demoMode && readDemoSession()) {
130
+ this.setDemoAuthenticatedState();
131
+ return;
132
+ }
133
+
115
134
  this.setPreviewState();
116
135
  return;
117
136
  }
@@ -127,6 +146,11 @@ export class DevPortalAuthenticationProvider
127
146
  await waitForAuthStateHydration();
128
147
  await this.syncBackendAvailability();
129
148
  if (!this.backendAvailable) {
149
+ if (this.demoMode && readDemoSession()) {
150
+ this.setDemoAuthenticatedState();
151
+ return;
152
+ }
153
+
130
154
  this.setPreviewState();
131
155
  return;
132
156
  }
@@ -154,15 +178,31 @@ export class DevPortalAuthenticationProvider
154
178
  type: "dev-portal",
155
179
  apiBaseUrl: this.apiBaseUrl,
156
180
  authMode: "preview",
181
+ demoMode: this.demoMode,
157
182
  }
158
- : null,
183
+ : this.demoMode
184
+ ? {
185
+ type: "dev-portal",
186
+ apiBaseUrl: "",
187
+ authMode: "preview",
188
+ demoMode: true,
189
+ }
190
+ : null,
159
191
  isBackendAvailable: false,
160
192
  authMode: "preview",
161
193
  });
162
194
  }
163
195
 
196
+ private setDemoAuthenticatedState(): void {
197
+ activateDemoSession(this.apiBaseUrl);
198
+ }
199
+
164
200
  private ensureConfiguredBackend(): string {
165
201
  if (!this.apiBaseUrl || isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
202
+ if (this.demoMode) {
203
+ return "";
204
+ }
205
+
166
206
  throw new Error("Dev portal authentication is not available in preview.");
167
207
  }
168
208
 
@@ -182,12 +222,22 @@ export class DevPortalAuthenticationProvider
182
222
  }
183
223
 
184
224
  private async refreshUserProfileInternal(): Promise<boolean> {
225
+ if (this.demoMode && readDemoSession() && !this.backendAvailable) {
226
+ this.setDemoAuthenticatedState();
227
+ return true;
228
+ }
229
+
185
230
  if (!this.apiBaseUrl) {
186
231
  this.setPreviewState();
187
232
  return false;
188
233
  }
189
234
 
190
235
  if (isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
236
+ if (this.demoMode && readDemoSession()) {
237
+ this.setDemoAuthenticatedState();
238
+ return true;
239
+ }
240
+
191
241
  this.setPreviewState();
192
242
  return false;
193
243
  }
@@ -195,6 +245,11 @@ export class DevPortalAuthenticationProvider
195
245
  await waitForAuthStateHydration();
196
246
  await this.syncBackendAvailability();
197
247
  if (!this.backendAvailable) {
248
+ if (this.demoMode && readDemoSession()) {
249
+ this.setDemoAuthenticatedState();
250
+ return true;
251
+ }
252
+
198
253
  this.setPreviewState();
199
254
  return false;
200
255
  }
@@ -229,6 +284,7 @@ export class DevPortalAuthenticationProvider
229
284
  type: "dev-portal",
230
285
  apiBaseUrl: this.apiBaseUrl,
231
286
  authMode: "live",
287
+ demoMode: this.demoMode,
232
288
  },
233
289
  isBackendAvailable: true,
234
290
  authMode: "live",
@@ -257,6 +313,7 @@ export class DevPortalAuthenticationProvider
257
313
  type: "dev-portal",
258
314
  apiBaseUrl: this.apiBaseUrl,
259
315
  authMode: "live",
316
+ demoMode: this.demoMode,
260
317
  },
261
318
  isBackendAvailable: true,
262
319
  authMode: "live",
@@ -276,6 +333,15 @@ export class DevPortalAuthenticationProvider
276
333
  _: AuthActionContext,
277
334
  { redirectTo }: AuthActionOptions = {},
278
335
  ): Promise<void> {
336
+ if (!this.backendAvailable && this.demoMode) {
337
+ const target =
338
+ redirectTo ?? this.redirectToAfterSignIn ?? window.location.href;
339
+ const url = new URL("/login", window.location.origin);
340
+ url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
341
+ window.location.assign(url.toString());
342
+ return;
343
+ }
344
+
279
345
  this.ensureConfiguredBackend();
280
346
  const target =
281
347
  redirectTo ?? this.redirectToAfterSignIn ?? window.location.href;
@@ -288,6 +354,15 @@ export class DevPortalAuthenticationProvider
288
354
  _: AuthActionContext,
289
355
  { redirectTo }: AuthActionOptions = {},
290
356
  ): Promise<void> {
357
+ if (!this.backendAvailable && this.demoMode) {
358
+ const target =
359
+ redirectTo ?? this.redirectToAfterSignUp ?? window.location.href;
360
+ const url = new URL("/register", window.location.origin);
361
+ url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
362
+ window.location.assign(url.toString());
363
+ return;
364
+ }
365
+
291
366
  this.ensureConfiguredBackend();
292
367
  const target =
293
368
  redirectTo ?? this.redirectToAfterSignUp ?? window.location.href;
@@ -297,6 +372,17 @@ export class DevPortalAuthenticationProvider
297
372
  }
298
373
 
299
374
  async signOut(_: AuthActionContext): Promise<void> {
375
+ const providerData = useAuthState.getState().providerData as
376
+ | DevPortalProviderData
377
+ | null
378
+ | undefined;
379
+
380
+ if (providerData?.isDemoSession || (this.demoMode && readDemoSession())) {
381
+ clearDemoSession();
382
+ this.setPreviewState();
383
+ return;
384
+ }
385
+
300
386
  const apiBaseUrl = this.ensureConfiguredBackend();
301
387
  window.location.assign(
302
388
  buildDevPortalLogoutUrl(apiBaseUrl, this.redirectToAfterSignOut),
@@ -304,12 +390,21 @@ export class DevPortalAuthenticationProvider
304
390
  }
305
391
 
306
392
  hasDistinctSignUpFlow(): boolean {
393
+ if (this.demoMode && !this.backendAvailable) {
394
+ return true;
395
+ }
396
+
307
397
  return this.authConfigNativeEnabled !== false;
308
398
  }
309
399
 
310
400
  async signRequest(request: Request): Promise<Request> {
311
401
  return request;
312
402
  }
403
+
404
+ /** Called from the login page after successful demo credential check. */
405
+ completeDemoLogin(): void {
406
+ activateDemoSession(this.apiBaseUrl);
407
+ }
313
408
  }
314
409
 
315
410
  const devPortalAuth: AuthenticationProviderInitializer<
@@ -171,13 +171,14 @@ export const RouteGuard = () => {
171
171
  const redirectTo = isBlocked
172
172
  ? blocker.location.pathname + blocker.location.search
173
173
  : location.pathname + location.search;
174
+ const canPromptForSignIn = auth.isBackendAvailable || auth.isDemoMode;
174
175
  const showDialog =
175
- auth.isBackendAvailable && needsAuth && auth.hasDistinctSignUpFlow;
176
+ canPromptForSignIn && needsAuth && auth.hasDistinctSignUpFlow;
176
177
  const unifiedRedirectKeyRef = useRef<string | null>(null);
177
178
 
178
179
  useEffect(() => {
179
180
  if (
180
- !auth.isBackendAvailable ||
181
+ !canPromptForSignIn ||
181
182
  auth.isPending ||
182
183
  !auth.sessionChecked ||
183
184
  !needsAuth ||
@@ -195,7 +196,7 @@ export const RouteGuard = () => {
195
196
  unifiedRedirectKeyRef.current = redirectTo;
196
197
  void auth.login({ redirectTo });
197
198
  }, [
198
- auth.isBackendAvailable,
199
+ canPromptForSignIn,
199
200
  auth.isPending,
200
201
  auth.sessionChecked,
201
202
  auth.hasDistinctSignUpFlow,
@@ -238,7 +239,8 @@ export const RouteGuard = () => {
238
239
  isProtectedRoute &&
239
240
  needsToSignIn &&
240
241
  auth.isAuthEnabled &&
241
- !auth.isBackendAvailable
242
+ !auth.isBackendAvailable &&
243
+ !auth.isDemoSession
242
244
  ) {
243
245
  return <ProductionUnlockPage />;
244
246
  }
@@ -125,12 +125,17 @@ const processFontConfig = (fontConfig?: FontConfig) => {
125
125
  };
126
126
  };
127
127
 
128
+ const DEFAULT_FONT_SANS = '"IBM Plex Sans", system-ui, sans-serif';
129
+ const DEFAULT_FONT_MONO = '"JetBrains Mono", monospace';
130
+ const DEFAULT_FONT_HEADING = '"Space Grotesk", system-ui, sans-serif';
131
+
128
132
  const processFonts = async (themeConfig: ZudokuConfig["theme"]) => {
129
133
  const imports: string[] = [];
130
134
  const families = {
131
- sans: "Geist, sans-serif",
135
+ sans: DEFAULT_FONT_SANS,
132
136
  serif: undefined as string | undefined,
133
- mono: '"Geist Mono", monospace',
137
+ mono: DEFAULT_FONT_MONO,
138
+ heading: DEFAULT_FONT_HEADING,
134
139
  };
135
140
 
136
141
  // Process user fonts
@@ -181,23 +186,19 @@ const processFonts = async (themeConfig: ZudokuConfig["theme"]) => {
181
186
 
182
187
  // Determine final font families with priority: user > registry > defaults
183
188
  families.sans =
184
- userFonts.sans.fontFamily || registryFonts.sans || "Geist, sans-serif";
189
+ userFonts.sans.fontFamily || registryFonts.sans || DEFAULT_FONT_SANS;
185
190
  families.serif = userFonts.serif.fontFamily || registryFonts.serif;
186
191
  families.mono =
187
- userFonts.mono.fontFamily ||
188
- registryFonts.mono ||
189
- '"Geist Mono", monospace';
192
+ userFonts.mono.fontFamily || registryFonts.mono || DEFAULT_FONT_MONO;
190
193
 
191
194
  // Add default font imports if no user or registry fonts
192
195
  if (!userFonts.sans.fontFamily && !registryFonts.sans) {
193
- imports.push(getGoogleFontUrl("Geist"));
194
- }
195
- if (!userFonts.serif.fontFamily && !registryFonts.serif) {
196
- imports.push(getGoogleFontUrl("Playfair Display"));
196
+ imports.push(getGoogleFontUrl("IBM Plex Sans"));
197
197
  }
198
198
  if (!userFonts.mono.fontFamily && !registryFonts.mono) {
199
- imports.push(getGoogleFontUrl("Geist Mono"));
199
+ imports.push(getGoogleFontUrl("JetBrains Mono"));
200
200
  }
201
+ imports.push(getGoogleFontUrl("Space Grotesk"));
201
202
 
202
203
  return { imports, families };
203
204
  };
@@ -347,6 +348,7 @@ export const viteThemePlugin = (): Plugin => {
347
348
  const rootVars = [
348
349
  ` --font-sans: ${fonts.families.sans};`,
349
350
  ` --font-mono: ${fonts.families.mono};`,
351
+ ` --font-heading: ${fonts.families.heading};`,
350
352
  ];
351
353
 
352
354
  if (fonts.families.serif) {