@xforge-dev/forge-kit 0.0.0-stage → 0.15.0

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 (49) hide show
  1. package/README.md +131 -2
  2. package/dist/account-url.d.ts +5 -0
  3. package/dist/account-url.js +7 -0
  4. package/dist/app-switcher.d.ts +12 -0
  5. package/dist/app-switcher.js +19 -0
  6. package/dist/apps.d.ts +12 -0
  7. package/dist/apps.js +41 -0
  8. package/dist/banner-storage.d.ts +4 -0
  9. package/dist/banner-storage.js +25 -0
  10. package/dist/brand.d.ts +14 -0
  11. package/dist/brand.js +18 -0
  12. package/dist/browser-client.d.ts +2 -0
  13. package/dist/browser-client.js +11 -0
  14. package/dist/client.d.ts +13 -0
  15. package/dist/client.js +14 -0
  16. package/dist/config.d.ts +8 -0
  17. package/dist/config.js +17 -0
  18. package/dist/cookies.d.ts +11 -0
  19. package/dist/cookies.js +9 -0
  20. package/dist/forge-header.d.ts +76 -0
  21. package/dist/forge-header.js +75 -0
  22. package/dist/forge-logo.d.ts +12 -0
  23. package/dist/forge-logo.js +11 -0
  24. package/dist/guest-banner.d.ts +10 -0
  25. package/dist/guest-banner.js +24 -0
  26. package/dist/ign-input.d.ts +30 -0
  27. package/dist/ign-input.js +49 -0
  28. package/dist/index.d.ts +9 -0
  29. package/dist/index.js +9 -0
  30. package/dist/legacy.d.ts +50 -0
  31. package/dist/legacy.js +100 -0
  32. package/dist/next-url.d.ts +14 -0
  33. package/dist/next-url.js +40 -0
  34. package/dist/security-headers.d.ts +25 -0
  35. package/dist/security-headers.js +34 -0
  36. package/dist/server.d.ts +26 -0
  37. package/dist/server.js +78 -0
  38. package/dist/support-button.d.ts +35 -0
  39. package/dist/support-button.js +43 -0
  40. package/dist/theme-provider.d.ts +31 -0
  41. package/dist/theme-provider.js +127 -0
  42. package/dist/theme.d.ts +75 -0
  43. package/dist/theme.js +70 -0
  44. package/dist/tokens.css +196 -0
  45. package/dist/use-current-url.d.ts +2 -0
  46. package/dist/use-current-url.js +10 -0
  47. package/dist/use-forge-user.d.ts +22 -0
  48. package/dist/use-forge-user.js +75 -0
  49. package/package.json +67 -4
package/README.md CHANGED
@@ -1,3 +1,132 @@
1
- # Temporary Holding Version
1
+ # @xforge-dev/forge-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Shared auth, app registry and UI for xForge apps (`*.xforge.dev`).
4
+
5
+ ```ts
6
+ import { accountUrl, resolveNext, FORGE_APPS } from '@xforge-dev/forge-kit';
7
+ import { createForgeServerClient, updateForgeSession } from '@xforge-dev/forge-kit/server';
8
+ import { AppSwitcher, GuestBanner, useForgeUser } from '@xforge-dev/forge-kit/client';
9
+ ```
10
+
11
+ Env: `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` (`sb_publishable_…`),
12
+ `NEXT_PUBLIC_FORGE_COOKIE_DOMAIN` (`.xforge.dev` in production, empty locally),
13
+ `NEXT_PUBLIC_FORGE_ACCOUNT_URL` (defaults to `https://accounts.xforge.dev`).
14
+
15
+ Without both Supabase vars, `isForgeAuthConfigured()` returns `false`: `useForgeUser` reports a
16
+ signed-out user without loading, `updateForgeSession` returns `user: null` and `supabase: null`,
17
+ and the client factories throw a clear error. Apps can therefore run guest-only.
18
+
19
+ ## Tailwind 4
20
+
21
+ Add an `@source` line to your `globals.css`. The path is relative to that file:
22
+
23
+ - `app/globals.css`: `@source "../node_modules/@xforge-dev/forge-kit/dist";`
24
+ - `src/app/globals.css`: `@source "../../node_modules/@xforge-dev/forge-kit/dist";`
25
+
26
+ ## Design tokens and font
27
+
28
+ Every app takes its palette, the 12 colour themes, radii, dark variant and base styles from the
29
+ kit. Import the tokens right after Tailwind, and keep only app-specific tokens in `globals.css`:
30
+
31
+ ```css
32
+ @import "tailwindcss";
33
+ @import "@xforge-dev/forge-kit/tokens.css";
34
+ @source "../node_modules/@xforge-dev/forge-kit/dist";
35
+ ```
36
+
37
+ Colours are full `hsl()` values: use `var(--primary)` directly and `color-mix()` for alpha, never
38
+ `hsl(var(--primary))`. The body font is Rubik, loaded by each app (next/font can't run from a
39
+ package) into `--font-forge-sans`:
40
+
41
+ ```tsx
42
+ import { Rubik } from 'next/font/google';
43
+ const forgeSans = Rubik({ subsets: ['latin'], variable: '--font-forge-sans', display: 'swap' });
44
+ <html className={forgeSans.variable}>
45
+ ```
46
+
47
+ Shared component sizes, so the apps feel the same: buttons and inputs 36px (`h-9`), inputs use
48
+ `text-base md:text-sm` (16px on phones, so iOS doesn't zoom), cards and dialogs `rounded-xl`.
49
+
50
+ ## Showing the banner and switcher
51
+
52
+ ```tsx
53
+ const { user, account, loading } = useForgeUser();
54
+
55
+ <GuestBanner appId="legion" appName="LegionForge" signedIn={loading ? undefined : !!user} />
56
+ <AppSwitcher account={account} … />
57
+ ```
58
+
59
+ `signedIn={undefined}` means auth state is unknown, so the banner renders nothing until it resolves.
60
+
61
+ ## Header, account panel and theme
62
+
63
+ Every app uses the same header so switching apps feels seamless:
64
+
65
+ ```tsx
66
+ // layout.tsx
67
+ import { forgeThemeScript } from '@xforge-dev/forge-kit';
68
+ <head><script dangerouslySetInnerHTML={{ __html: forgeThemeScript }} /></head>
69
+ <ForgeThemeProvider onChange={…}>{children}</ForgeThemeProvider>
70
+
71
+ // header
72
+ <ForgeHeader
73
+ current="boss"
74
+ account={account}
75
+ nav={[{ href: '/parties', label: 'Parties', icon: Users, active: true }]}
76
+ actions={<ForgeSupportButton />}
77
+ panel={<ForgePanelSection title="Screensaver">…</ForgePanelSection>}
78
+ />
79
+ ```
80
+
81
+ The avatar opens a side panel with the account links, the page links on mobile, Appearance
82
+ (dark mode, color theme, "Same theme in all xForge apps") and then the app's own sections.
83
+ Build those from `ForgePanelSection`, `ForgePanelItem` and `ForgeSwitchRow`.
84
+ Under the username the panel shows the account's supporter badge and Discord name (from
85
+ `ForgeAccount.supporterTier` / `discordUsername`, which `useForgeUser` reads from `profiles`), then
86
+ the app's `accountDetails`.
87
+
88
+ Themes live in localStorage (`theme-mode`, `theme-color`) and, while syncing (the default,
89
+ opt out per app with `forge-theme-sync=false`), in the `forge-theme` cookie on
90
+ `NEXT_PUBLIC_FORGE_COOKIE_DOMAIN`. Apps must define CSS for every `FORGE_COLOR_THEMES` name under
91
+ `html[data-color="…"]`. Call `storeForgeTheme()` when something other than the provider changes
92
+ the theme (an account sync or an import) so the shared copy follows.
93
+
94
+ ## Support button
95
+
96
+ `ForgeSupportButton` is the header heart that opens the shared "show your support" dialog with
97
+ the Buy Me a Coffee button and the supporter badge tiers (`FORGE_SPONSOR_TIERS`). Children render
98
+ after the tiers for anything app-specific. The button image loads from `https://cdn.buymeacoffee.com`, so add that to the app's CSP
99
+ `img-src`.
100
+
101
+ ## Security headers
102
+
103
+ `forgeSecurityHeaders(extra, { dev })` returns the family's headers and CSP; pass app-specific
104
+ sources in `extra`. The policy allows no scripts from other origins: every Forge subdomain shares
105
+ the sign-in cookie, so a third-party script on one can read every user's session. For analytics,
106
+ apps add Vercel Web Analytics (`<Analytics />` from `@vercel/analytics/next` in the root layout);
107
+ it loads from the app's own origin, so the policy needs no extra sources.
108
+
109
+ ## Exports
110
+
111
+ `isForgeAuthConfigured` is exported from the root and `/client` entry points.
112
+
113
+ ## Proxy notes
114
+
115
+ Do not write cookies through the `supabase` client returned by `updateForgeSession` after it
116
+ returns. Writes made later are not reflected in the returned `response`.
117
+
118
+ ## Moving off xtherebellion.com
119
+
120
+ The apps moved from `*.xtherebellion.com` to `*.xforge.dev`. Each app's proxy calls
121
+ `forgeLegacyResponse(request)` first. On an old host, a browser navigation gets a small
122
+ page that carries the origin's localStorage and readable cookies (never the Supabase
123
+ session) to `/__forge/handoff` on the new host, in the URL fragment; everything else
124
+ (API calls, crawlers) gets a 308. The receiving page, answered by the same call before
125
+ any auth redirect, imports only what the new origin doesn't have, and only when the
126
+ referrer is that origin's old host, then goes on to the original path.
127
+
128
+ ```ts
129
+ // proxy.ts
130
+ const legacy = forgeLegacyResponse(request);
131
+ if (legacy) return legacy;
132
+ ```
@@ -0,0 +1,5 @@
1
+ export declare const ACCOUNT_ORIGIN: string;
2
+ export type AccountPath = '/' | '/login' | '/register' | '/logout' | '/reset-password';
3
+ export declare function accountUrl(path: AccountPath, opts?: {
4
+ next?: string;
5
+ }, origin?: string): string;
@@ -0,0 +1,7 @@
1
+ export const ACCOUNT_ORIGIN = process.env.NEXT_PUBLIC_FORGE_ACCOUNT_URL || 'https://accounts.xforge.dev';
2
+ export function accountUrl(path, opts = {}, origin = ACCOUNT_ORIGIN) {
3
+ const url = new URL(path, origin);
4
+ if (opts.next)
5
+ url.searchParams.set('next', opts.next);
6
+ return url.toString();
7
+ }
@@ -0,0 +1,12 @@
1
+ import { type ForgeAppId } from './apps.js';
2
+ import type { ForgeAccount } from './use-forge-user.js';
3
+ export type AppSwitcherProps = {
4
+ current?: ForgeAppId;
5
+ account: ForgeAccount | null;
6
+ syncError?: boolean;
7
+ /** Show the account and sign-in items; ForgeHeader turns this off because its avatar panel has them. */
8
+ showAccount?: boolean;
9
+ /** Override for tests; defaults to the live page URL. */
10
+ currentUrl?: string;
11
+ };
12
+ export declare function AppSwitcher({ current, account, syncError, showAccount, currentUrl }: AppSwitcherProps): import("react").JSX.Element;
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import * as Menu from '@radix-ui/react-dropdown-menu';
4
+ import { Check, ChevronDown, LogIn, LogOut, UserRound } from 'lucide-react';
5
+ import { accountUrl } from './account-url.js';
6
+ import { FORGE_APPS } from './apps.js';
7
+ import { ForgeLogo } from './forge-logo.js';
8
+ import { useCurrentUrl } from './use-current-url.js';
9
+ const item = 'flex cursor-pointer select-none items-center gap-3 rounded-sm px-2 py-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50';
10
+ export function AppSwitcher({ current, account, syncError, showAccount = true, currentUrl }) {
11
+ const currentApp = FORGE_APPS.find((a) => a.id === current);
12
+ const liveUrl = useCurrentUrl(currentApp?.url ?? accountUrl('/'));
13
+ const next = currentUrl ?? liveUrl;
14
+ return (_jsxs(Menu.Root, { children: [_jsxs(Menu.Trigger, { className: "flex items-center gap-2 rounded-md px-2 py-1 font-semibold hover:bg-accent focus-visible:outline-2 focus-visible:outline-ring", children: [_jsx(ForgeLogo, { brand: currentApp?.id ?? 'xforge', variant: "icon", className: "h-8", alt: "" }), _jsx("span", { children: currentApp?.name ?? 'xForge' }), _jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" }), syncError && (_jsx("span", { role: "img", "aria-label": "Not synced", title: "Not synced", className: "h-2 w-2 rounded-full bg-destructive" }))] }), _jsx(Menu.Portal, { children: _jsxs(Menu.Content, { align: "start", sideOffset: 6, className: "z-50 w-72 rounded-md border bg-popover p-1 text-popover-foreground shadow-md", children: [_jsxs(Menu.Label, { className: "flex items-center gap-2 px-2 py-1.5 text-xs font-medium text-muted-foreground", children: [_jsx(ForgeLogo, { brand: "xforge", variant: "icon", className: "h-4", alt: "" }), "xForge apps"] }), FORGE_APPS.map((app) => {
15
+ const isCurrent = app.id === current;
16
+ const disabled = app.status === 'soon';
17
+ return (_jsx(Menu.Item, { asChild: true, disabled: disabled, children: _jsxs("a", { href: disabled ? undefined : app.url, "aria-current": isCurrent ? 'page' : undefined, className: item, children: [_jsx(ForgeLogo, { brand: app.id, variant: "icon", className: "h-9 shrink-0", alt: "" }), _jsxs("span", { className: "flex-1", children: [_jsx("span", { className: "block font-medium", children: app.name }), _jsx("span", { className: "block text-xs text-muted-foreground", children: disabled ? 'Coming soon' : app.description })] }), isCurrent && _jsx(Check, { className: "h-4 w-4" })] }) }, app.id));
18
+ }), showAccount && _jsx(Menu.Separator, { className: "-mx-1 my-1 h-px bg-border" }), !showAccount ? null : account ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center gap-2 px-2 py-1.5 text-sm", children: [account.avatarUrl ? (_jsx("img", { src: account.avatarUrl, alt: "", className: "h-6 w-6 rounded-full" })) : (_jsx(UserRound, { className: "h-5 w-5" })), _jsx("span", { className: "font-medium", children: account.username })] }), _jsx(Menu.Item, { asChild: true, children: _jsxs("a", { href: accountUrl('/'), className: item, children: [_jsx(UserRound, { className: "h-4 w-4" }), " xForge account"] }) }), _jsx(Menu.Item, { asChild: true, children: _jsxs("a", { href: accountUrl('/logout', { next }), className: item, children: [_jsx(LogOut, { className: "h-4 w-4" }), " Sign out"] }) })] })) : (_jsx(Menu.Item, { asChild: true, children: _jsxs("a", { href: accountUrl('/login', { next }), className: item, children: [_jsx(LogIn, { className: "h-4 w-4" }), " Sign in / Create account"] }) }))] }) })] }));
19
+ }
package/dist/apps.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ export type ForgeAppId = 'boss' | 'legion' | 'cards' | 'fams';
2
+ export type ForgeApp = {
3
+ id: ForgeAppId;
4
+ name: string;
5
+ url: string;
6
+ description: string;
7
+ status: 'live' | 'soon';
8
+ /** Where "Continue without an account" lands, for apps that gate guest mode. */
9
+ guestPath?: string;
10
+ };
11
+ export declare const FORGE_APPS: readonly ForgeApp[];
12
+ export declare function appForUrl(url: string): ForgeApp | undefined;
package/dist/apps.js ADDED
@@ -0,0 +1,41 @@
1
+ export const FORGE_APPS = [
2
+ {
3
+ id: 'boss',
4
+ name: 'BossForge',
5
+ url: 'https://boss.xforge.dev',
6
+ description: 'Organize boss parties across time zones',
7
+ status: 'live',
8
+ guestPath: '/auth/guest',
9
+ },
10
+ {
11
+ id: 'legion',
12
+ name: 'LegionForge',
13
+ url: 'https://legion.xforge.dev',
14
+ description: 'Track your Legion and character progress',
15
+ status: 'live',
16
+ },
17
+ {
18
+ id: 'cards',
19
+ name: 'CardForge',
20
+ url: 'https://cards.xforge.dev',
21
+ description: 'Design trading cards for your characters',
22
+ status: 'live',
23
+ },
24
+ {
25
+ id: 'fams',
26
+ name: 'FamiliarForge',
27
+ url: 'https://fams.xforge.dev',
28
+ description: 'Track your familiar cards and badges',
29
+ status: 'live',
30
+ },
31
+ ];
32
+ export function appForUrl(url) {
33
+ let origin;
34
+ try {
35
+ origin = new URL(url).origin;
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ return FORGE_APPS.find((app) => app.url === origin);
41
+ }
@@ -0,0 +1,4 @@
1
+ import type { ForgeAppId } from './apps.js';
2
+ export declare const bannerKey: (appId: ForgeAppId) => string;
3
+ export declare function isBannerDismissed(appId: ForgeAppId, storage?: Storage | null): boolean;
4
+ export declare function dismissBanner(appId: ForgeAppId, storage?: Storage | null): void;
@@ -0,0 +1,25 @@
1
+ export const bannerKey = (appId) => `forge-guest-banner-dismissed:${appId}`;
2
+ function defaultStorage() {
3
+ try {
4
+ return typeof window === 'undefined' ? null : window.localStorage;
5
+ }
6
+ catch {
7
+ return null;
8
+ }
9
+ }
10
+ export function isBannerDismissed(appId, storage = defaultStorage()) {
11
+ try {
12
+ return storage?.getItem(bannerKey(appId)) === '1';
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ export function dismissBanner(appId, storage = defaultStorage()) {
19
+ try {
20
+ storage?.setItem(bannerKey(appId), '1');
21
+ }
22
+ catch {
23
+ // Storage unavailable: the banner just comes back next visit.
24
+ }
25
+ }
@@ -0,0 +1,14 @@
1
+ import type { ForgeAppId } from './apps.js';
2
+ export type ForgeBrandId = ForgeAppId | 'xforge';
3
+ export type ForgeBrand = {
4
+ name: string;
5
+ /** Square mark. */
6
+ icon: string;
7
+ /** Mark plus wordmark; `light` reads best on light backgrounds, `dark` on dark ones. */
8
+ logo: {
9
+ light: string;
10
+ dark: string;
11
+ };
12
+ };
13
+ export declare const FORGE_CDN = "https://cdn.xforge.dev";
14
+ export declare const FORGE_BRANDS: Record<ForgeBrandId, ForgeBrand>;
package/dist/brand.js ADDED
@@ -0,0 +1,18 @@
1
+ export const FORGE_CDN = 'https://cdn.xforge.dev';
2
+ /** Bump after replacing files on the CDN so browsers and Cloudflare fetch the new art. */
3
+ const BRAND_VERSION = '3';
4
+ function brand(name, slug) {
5
+ const file = (path) => `${FORGE_CDN}/apps/${slug}/${path}?v=${BRAND_VERSION}`;
6
+ return {
7
+ name,
8
+ icon: file(`icons/${slug}-icon.png`),
9
+ logo: { light: file(`logos/${slug}-light.png`), dark: file(`logos/${slug}-dark.png`) },
10
+ };
11
+ }
12
+ export const FORGE_BRANDS = {
13
+ xforge: brand('xForge', 'xforge'),
14
+ boss: brand('BossForge', 'bossforge'),
15
+ legion: brand('LegionForge', 'legionforge'),
16
+ cards: brand('CardForge', 'cardforge'),
17
+ fams: brand('FamiliarForge', 'familiarforge'),
18
+ };
@@ -0,0 +1,2 @@
1
+ import type { SupabaseClient } from '@supabase/supabase-js';
2
+ export declare function createForgeBrowserClient(): SupabaseClient;
@@ -0,0 +1,11 @@
1
+ import { createBrowserClient } from '@supabase/ssr';
2
+ import { requireForgeAuthEnv } from './config.js';
3
+ import { forgeCookieOptions } from './cookies.js';
4
+ let client;
5
+ export function createForgeBrowserClient() {
6
+ if (!client) {
7
+ const { url, key } = requireForgeAuthEnv();
8
+ client = createBrowserClient(url, key, { cookieOptions: forgeCookieOptions() });
9
+ }
10
+ return client;
11
+ }
@@ -0,0 +1,13 @@
1
+ export { AppSwitcher, type AppSwitcherProps } from './app-switcher.js';
2
+ export { GuestBanner, type GuestBannerProps } from './guest-banner.js';
3
+ export { useForgeUser, toForgeAccount, type ForgeAccount } from './use-forge-user.js';
4
+ export { createForgeBrowserClient } from './browser-client.js';
5
+ export { dismissBanner, isBannerDismissed, bannerKey } from './banner-storage.js';
6
+ export { useCurrentUrl } from './use-current-url.js';
7
+ export { isForgeAuthConfigured } from './config.js';
8
+ export { ForgeThemeProvider, useForgeTheme, resolveTheme, storeForgeTheme, type ForgeThemeContext, type ForgeThemeProviderProps, } from './theme-provider.js';
9
+ export { ForgeHeader, ForgeAvatar, ForgePanelSection, ForgePanelItem, ForgeSwitchRow, DiscordIcon, type ForgeHeaderProps, type ForgeNavItem, type ForgePanelItemProps, } from './forge-header.js';
10
+ export * from './theme.js';
11
+ export { ForgeLogo, type ForgeLogoProps } from './forge-logo.js';
12
+ export { ForgeSupportButton, BUY_ME_A_COFFEE_URL, BUY_ME_A_COFFEE_BUTTON, FORGE_SPONSOR_TIERS, ForgeSupporterBadge, type ForgeSupporterTier, type ForgeSupportButtonProps, } from './support-button.js';
13
+ export { ForgeIgnInput, FORGE_ACCENTED_LETTERS, FORGE_NO_AUTOFILL, type ForgeIgnInputProps, } from './ign-input.js';
package/dist/client.js ADDED
@@ -0,0 +1,14 @@
1
+ 'use client';
2
+ export { AppSwitcher } from './app-switcher.js';
3
+ export { GuestBanner } from './guest-banner.js';
4
+ export { useForgeUser, toForgeAccount } from './use-forge-user.js';
5
+ export { createForgeBrowserClient } from './browser-client.js';
6
+ export { dismissBanner, isBannerDismissed, bannerKey } from './banner-storage.js';
7
+ export { useCurrentUrl } from './use-current-url.js';
8
+ export { isForgeAuthConfigured } from './config.js';
9
+ export { ForgeThemeProvider, useForgeTheme, resolveTheme, storeForgeTheme, } from './theme-provider.js';
10
+ export { ForgeHeader, ForgeAvatar, ForgePanelSection, ForgePanelItem, ForgeSwitchRow, DiscordIcon, } from './forge-header.js';
11
+ export * from './theme.js';
12
+ export { ForgeLogo } from './forge-logo.js';
13
+ export { ForgeSupportButton, BUY_ME_A_COFFEE_URL, BUY_ME_A_COFFEE_BUTTON, FORGE_SPONSOR_TIERS, ForgeSupporterBadge, } from './support-button.js';
14
+ export { ForgeIgnInput, FORGE_ACCENTED_LETTERS, FORGE_NO_AUTOFILL, } from './ign-input.js';
@@ -0,0 +1,8 @@
1
+ export declare const AUTH_NOT_CONFIGURED_MESSAGE = "xForge auth is not configured: set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY";
2
+ /** True iff the Supabase URL and publishable key are set. */
3
+ export declare function isForgeAuthConfigured(): boolean;
4
+ /** Returns the Supabase credentials or throws a clear error. */
5
+ export declare function requireForgeAuthEnv(): {
6
+ url: string;
7
+ key: string;
8
+ };
package/dist/config.js ADDED
@@ -0,0 +1,17 @@
1
+ export const AUTH_NOT_CONFIGURED_MESSAGE = 'xForge auth is not configured: set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY';
2
+ /** The Supabase publishable key (`sb_publishable_…`), referenced literally so Next inlines it. */
3
+ function publishableKey() {
4
+ return process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY;
5
+ }
6
+ /** True iff the Supabase URL and publishable key are set. */
7
+ export function isForgeAuthConfigured() {
8
+ return !!process.env.NEXT_PUBLIC_SUPABASE_URL && !!publishableKey();
9
+ }
10
+ /** Returns the Supabase credentials or throws a clear error. */
11
+ export function requireForgeAuthEnv() {
12
+ const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
13
+ const key = publishableKey();
14
+ if (!url || !key)
15
+ throw new Error(AUTH_NOT_CONFIGURED_MESSAGE);
16
+ return { url, key };
17
+ }
@@ -0,0 +1,11 @@
1
+ export type ForgeCookieOptions = {
2
+ domain?: string;
3
+ path: '/';
4
+ sameSite: 'lax';
5
+ secure: boolean;
6
+ };
7
+ /**
8
+ * Every Forge app must use identical options or the browser ends up holding
9
+ * two auth cookies with the same name on different scopes.
10
+ */
11
+ export declare function forgeCookieOptions(domain?: string | undefined): ForgeCookieOptions;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Every Forge app must use identical options or the browser ends up holding
3
+ * two auth cookies with the same name on different scopes.
4
+ */
5
+ export function forgeCookieOptions(domain = process.env.NEXT_PUBLIC_FORGE_COOKIE_DOMAIN) {
6
+ return domain
7
+ ? { domain, path: '/', sameSite: 'lax', secure: true }
8
+ : { path: '/', sameSite: 'lax', secure: false };
9
+ }
@@ -0,0 +1,76 @@
1
+ import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
+ import { type LucideIcon } from 'lucide-react';
3
+ import { type ForgeAppId } from './apps.js';
4
+ import type { ForgeAccount } from './use-forge-user.js';
5
+ export type ForgeNavItem = {
6
+ href: string;
7
+ label: string;
8
+ icon?: LucideIcon;
9
+ active?: boolean;
10
+ };
11
+ export type ForgeHeaderProps = {
12
+ current?: ForgeAppId;
13
+ account: ForgeAccount | null;
14
+ syncError?: boolean;
15
+ /** Page links: inline on desktop, in the account panel on mobile. */
16
+ nav?: ForgeNavItem[];
17
+ /** Header buttons shown left of the avatar. */
18
+ actions?: ReactNode;
19
+ /** Shown in the panel under the username, supporter badge and Discord name (e.g. time zone). */
20
+ accountDetails?: ReactNode;
21
+ /** Replaces the panel's name and details for guests. */
22
+ guestDetails?: ReactNode;
23
+ /** Extra items in the panel's account section, after the built-in ones. */
24
+ accountActions?: ReactNode;
25
+ /** App sections in the panel, after Appearance. Build them from `ForgePanelSection`. */
26
+ panel?: ReactNode;
27
+ /** Runs before navigating to sign out, e.g. to flush unsaved data. */
28
+ onBeforeSignOut?: () => Promise<void> | void;
29
+ /** Width of the header's inner row; defaults to `max-w-6xl`. */
30
+ containerClassName?: string;
31
+ /** Override for tests; defaults to the live page URL. */
32
+ currentUrl?: string;
33
+ };
34
+ /**
35
+ * The header every xForge app shares: app switcher, page links, app buttons,
36
+ * and an avatar that opens the account panel with the theme settings.
37
+ */
38
+ export declare function ForgeHeader({ current, account, syncError, nav, actions, accountDetails, guestDetails, accountActions, panel, onBeforeSignOut, containerClassName, currentUrl, }: ForgeHeaderProps): import("react").JSX.Element;
39
+ export declare function ForgeAvatar({ account, className }: {
40
+ account: ForgeAccount | null;
41
+ className?: string;
42
+ }): import("react").JSX.Element;
43
+ /** Discord's mark in `currentColor`; give it Discord's blurple (`text-[#5865F2]`) next to a Discord name. */
44
+ export declare function DiscordIcon({ className }: {
45
+ className?: string;
46
+ }): import("react").JSX.Element;
47
+ /** A titled group in the account panel. */
48
+ export declare function ForgePanelSection({ title, description, children, }: {
49
+ title: string;
50
+ description?: ReactNode;
51
+ children: ReactNode;
52
+ }): import("react").JSX.Element;
53
+ export type ForgePanelItemProps = {
54
+ icon?: LucideIcon;
55
+ label: ReactNode;
56
+ description?: ReactNode;
57
+ /** In-app path (client navigation) or, with `external`, a full URL. */
58
+ href?: string;
59
+ external?: boolean;
60
+ onClick?: () => void;
61
+ disabled?: boolean;
62
+ active?: boolean;
63
+ tone?: 'default' | 'destructive';
64
+ /** Keep the panel open after clicking (default closes it), e.g. to open a dialog from it. */
65
+ keepOpen?: boolean;
66
+ } & Omit<ComponentPropsWithRef<'button'>, 'children' | 'onClick' | 'disabled'>;
67
+ /** A row in the account panel: a link or a button that closes the panel. */
68
+ export declare function ForgePanelItem({ icon: Icon, label, description, href, external, onClick, disabled, active, tone, keepOpen, className: extraClassName, ...buttonProps }: ForgePanelItemProps): import("react").JSX.Element;
69
+ /** A labelled on/off switch row for panel sections. */
70
+ export declare function ForgeSwitchRow({ icon: Icon, label, description, checked, onCheckedChange, }: {
71
+ icon?: LucideIcon;
72
+ label: ReactNode;
73
+ description?: ReactNode;
74
+ checked: boolean;
75
+ onCheckedChange: (checked: boolean) => void;
76
+ }): import("react").JSX.Element;
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import * as Dialog from '@radix-ui/react-dialog';
4
+ import Link from 'next/link.js';
5
+ import { useState } from 'react';
6
+ import { ExternalLink, LogIn, LogOut, Moon, RefreshCw, Sun, UserRound, X } from 'lucide-react';
7
+ import { accountUrl } from './account-url.js';
8
+ import { AppSwitcher } from './app-switcher.js';
9
+ import { FORGE_APPS } from './apps.js';
10
+ import { ForgeSupporterBadge } from './support-button.js';
11
+ import { FORGE_COLOR_THEMES } from './theme.js';
12
+ import { useForgeTheme } from './theme-provider.js';
13
+ import { useCurrentUrl } from './use-current-url.js';
14
+ /**
15
+ * The header every xForge app shares: app switcher, page links, app buttons,
16
+ * and an avatar that opens the account panel with the theme settings.
17
+ */
18
+ export function ForgeHeader({ current, account, syncError, nav = [], actions, accountDetails, guestDetails, accountActions, panel, onBeforeSignOut, containerClassName = 'max-w-6xl', currentUrl, }) {
19
+ const [open, setOpen] = useState(false);
20
+ return (_jsx("header", { className: "sticky top-0 z-40 w-full border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/80", children: _jsxs("div", { className: `mx-auto flex h-14 w-full items-center gap-2 px-4 sm:gap-4 ${containerClassName}`, children: [_jsx(AppSwitcher, { current: current, account: account, showAccount: false, currentUrl: currentUrl }), _jsx("nav", { className: "hidden flex-1 items-center gap-1 sm:flex", children: nav.map(({ href, label, active }) => (_jsx(Link, { href: href, "aria-current": active ? 'page' : undefined, className: active
21
+ ? 'rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground'
22
+ : 'rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground', children: label }, href))) }), _jsx("div", { className: "flex-1 sm:hidden" }), actions && _jsx("div", { className: "flex items-center gap-2", children: actions }), _jsxs(Dialog.Root, { open: open, onOpenChange: setOpen, children: [_jsxs(Dialog.Trigger, { "aria-label": "Account and settings", className: "relative shrink-0 rounded-full focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", children: [_jsx(ForgeAvatar, { account: account, className: "h-8 w-8" }), syncError && (_jsx("span", { role: "img", "aria-label": "Not synced", title: "Not synced", className: "absolute -right-0.5 -top-0.5 h-2.5 w-2.5 rounded-full border-2 border-background bg-destructive" }))] }), _jsxs(Dialog.Portal, { children: [_jsx(Dialog.Overlay, { className: "fixed inset-0 z-50 bg-black/60" }), _jsx(Dialog.Content, { className: "fixed inset-y-0 right-0 z-50 flex w-[88vw] max-w-sm flex-col border-l bg-background text-foreground shadow-lg outline-hidden", children: _jsx(AccountPanel, { current: current, account: account, syncError: syncError, nav: nav, accountDetails: accountDetails, guestDetails: guestDetails, accountActions: accountActions, panel: panel, onBeforeSignOut: onBeforeSignOut, currentUrl: currentUrl }) })] })] })] }) }));
23
+ }
24
+ function initials(name) {
25
+ return name.slice(0, 2).toUpperCase();
26
+ }
27
+ export function ForgeAvatar({ account, className = '' }) {
28
+ if (account?.avatarUrl) {
29
+ return _jsx("img", { src: account.avatarUrl, alt: "", className: `${className} rounded-full object-cover` });
30
+ }
31
+ if (account) {
32
+ return (_jsx("span", { className: `${className} flex items-center justify-center rounded-full bg-primary text-xs font-semibold text-primary-foreground`, children: initials(account.username) }));
33
+ }
34
+ return (_jsx("span", { className: `${className} flex items-center justify-center rounded-full bg-muted text-muted-foreground`, children: _jsx(UserRound, { className: "h-1/2 w-1/2" }) }));
35
+ }
36
+ function AccountPanel({ current, account, syncError, nav = [], accountDetails, guestDetails, accountActions, panel, onBeforeSignOut, currentUrl, }) {
37
+ const appUrl = FORGE_APPS.find((a) => a.id === current)?.url ?? accountUrl('/');
38
+ const liveUrl = useCurrentUrl(appUrl);
39
+ const next = currentUrl ?? liveUrl;
40
+ const signOutHref = accountUrl('/logout', { next });
41
+ const [signingOut, setSigningOut] = useState(false);
42
+ async function signOut() {
43
+ setSigningOut(true);
44
+ try {
45
+ await onBeforeSignOut?.();
46
+ }
47
+ finally {
48
+ window.location.assign(signOutHref);
49
+ }
50
+ }
51
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-start gap-3 border-b p-4", children: [_jsx(ForgeAvatar, { account: account, className: "h-12 w-12 shrink-0 text-sm" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(Dialog.Title, { className: "truncate font-semibold", children: account ? account.username : 'Guest' }), _jsx(Dialog.Description, { asChild: true, children: _jsx("div", { className: "text-sm text-muted-foreground", children: account ? (_jsxs(_Fragment, { children: [account.supporterTier && (_jsx("span", { className: "mt-1 block", children: _jsx(ForgeSupporterBadge, { tier: account.supporterTier }) })), account.discordUsername && (_jsxs("span", { className: "mt-1 flex items-center gap-1 text-xs text-[#5865F2]", children: [_jsx(DiscordIcon, { className: "h-3 w-3 shrink-0" }), account.discordUsername] })), accountDetails] })) : (guestDetails ?? 'Your data lives only in this browser.') }) }), account && syncError && (_jsxs("p", { className: "mt-1 flex items-center gap-1 text-xs text-destructive", children: [_jsx(RefreshCw, { className: "h-3 w-3" }), " Not synced to your account"] }))] }), _jsx(Dialog.Close, { "aria-label": "Close", className: "rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring", children: _jsx(X, { className: "h-4 w-4" }) })] }), _jsxs("div", { className: "flex-1 space-y-6 overflow-y-auto p-4", children: [_jsx(ForgePanelSection, { title: "Account", children: account ? (_jsxs(_Fragment, { children: [_jsx(ForgePanelItem, { icon: UserRound, label: "xForge account", description: "Profile, password and linked apps", href: accountUrl('/'), external: true }), accountActions, onBeforeSignOut ? (_jsx(ForgePanelItem, { icon: LogOut, label: signingOut ? 'Signing out…' : 'Sign out', onClick: signOut, disabled: signingOut, tone: "destructive" })) : (_jsx(ForgePanelItem, { icon: LogOut, label: "Sign out", href: signOutHref, external: true, tone: "destructive" }))] })) : (_jsxs(_Fragment, { children: [_jsx(ForgePanelItem, { icon: LogIn, label: "Sign in / Create account", description: "Keep your data safe and use it on any device", href: accountUrl('/login', { next }), external: true }), accountActions] })) }), nav.length > 0 && (_jsx("div", { className: "sm:hidden", children: _jsx(ForgePanelSection, { title: "Navigate", children: nav.map(({ href, label, icon, active }) => (_jsx(ForgePanelItem, { icon: icon, label: label, href: href, active: active }, href))) }) })), _jsx(AppearanceSection, {}), panel] })] }));
52
+ }
53
+ /** Discord's mark in `currentColor`; give it Discord's blurple (`text-[#5865F2]`) next to a Discord name. */
54
+ export function DiscordIcon({ className }) {
55
+ return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 640 640", className: className, fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M524.5 133.8C524.3 133.5 524.1 133.2 523.7 133.1C485.6 115.6 445.3 103.1 404 96C403.6 95.9 403.2 96 402.9 96.1C402.6 96.2 402.3 96.5 402.1 96.9C396.6 106.8 391.6 117.1 387.2 127.5C342.6 120.7 297.3 120.7 252.8 127.5C248.3 117 243.3 106.8 237.7 96.9C237.5 96.6 237.2 96.3 236.9 96.1C236.6 95.9 236.2 95.9 235.8 95.9C194.5 103 154.2 115.5 116.1 133C115.8 133.1 115.5 133.4 115.3 133.7C39.1 247.5 18.2 358.6 28.4 468.2C28.4 468.5 28.5 468.7 28.6 469C28.7 469.3 28.9 469.4 29.1 469.6C73.5 502.5 123.1 527.6 175.9 543.8C176.3 543.9 176.7 543.9 177 543.8C177.3 543.7 177.7 543.4 177.9 543.1C189.2 527.7 199.3 511.3 207.9 494.3C208 494.1 208.1 493.8 208.1 493.5C208.1 493.2 208.1 493 208 492.7C207.9 492.4 207.8 492.2 207.6 492.1C207.4 492 207.2 491.8 206.9 491.7C191.1 485.6 175.7 478.3 161 469.8C160.7 469.6 160.5 469.4 160.3 469.2C160.1 469 160 468.6 160 468.3C160 468 160 467.7 160.2 467.4C160.4 467.1 160.5 466.9 160.8 466.7C163.9 464.4 167 462 169.9 459.6C170.2 459.4 170.5 459.2 170.8 459.2C171.1 459.2 171.5 459.2 171.8 459.3C268 503.2 372.2 503.2 467.3 459.3C467.6 459.2 468 459.1 468.3 459.1C468.6 459.1 469 459.3 469.2 459.5C472.1 461.9 475.2 464.4 478.3 466.7C478.5 466.9 478.7 467.1 478.9 467.4C479.1 467.7 479.1 468 479.1 468.3C479.1 468.6 479 468.9 478.8 469.2C478.6 469.5 478.4 469.7 478.2 469.8C463.5 478.4 448.2 485.7 432.3 491.6C432.1 491.7 431.8 491.8 431.6 492C431.4 492.2 431.3 492.4 431.2 492.7C431.1 493 431.1 493.2 431.1 493.5C431.1 493.8 431.2 494 431.3 494.3C440.1 511.3 450.1 527.6 461.3 543.1C461.5 543.4 461.9 543.7 462.2 543.8C462.5 543.9 463 543.9 463.3 543.8C516.2 527.6 565.9 502.5 610.4 469.6C610.6 469.4 610.8 469.2 610.9 469C611 468.8 611.1 468.5 611.1 468.2C623.4 341.4 590.6 231.3 524.2 133.7zM222.5 401.5C193.5 401.5 169.7 374.9 169.7 342.3C169.7 309.7 193.1 283.1 222.5 283.1C252.2 283.1 275.8 309.9 275.3 342.3C275.3 375 251.9 401.5 222.5 401.5zM417.9 401.5C388.9 401.5 365.1 374.9 365.1 342.3C365.1 309.7 388.5 283.1 417.9 283.1C447.6 283.1 471.2 309.9 470.7 342.3C470.7 375 447.5 401.5 417.9 401.5z" }) }));
56
+ }
57
+ function AppearanceSection() {
58
+ const { theme, setTheme, colorTheme, setColorTheme, syncTheme, setSyncTheme } = useForgeTheme();
59
+ return (_jsx(ForgePanelSection, { title: "Appearance", children: _jsxs("div", { className: "space-y-4 rounded-lg border p-3", children: [_jsx(ForgeSwitchRow, { icon: theme === 'dark' ? Moon : Sun, label: "Dark mode", checked: theme === 'dark', onCheckedChange: (dark) => setTheme(dark ? 'dark' : 'light') }), _jsx("div", { className: "grid grid-cols-6 gap-2", role: "radiogroup", "aria-label": "Color theme", children: FORGE_COLOR_THEMES.map((t) => (_jsx("button", { type: "button", role: "radio", "aria-checked": colorTheme === t.name, "aria-label": t.label, title: t.label, onClick: () => setColorTheme(t.name), className: `h-8 w-8 justify-self-center rounded-full ring-2 ring-offset-2 ring-offset-background transition-transform focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring ${colorTheme === t.name ? 'scale-110 ring-foreground' : 'ring-transparent hover:ring-foreground/40'}`, style: { background: t.color } }, t.name))) }), _jsx(ForgeSwitchRow, { label: "Same theme in all xForge apps", description: "Changing it here changes it everywhere", checked: syncTheme, onCheckedChange: setSyncTheme })] }) }));
60
+ }
61
+ /** A titled group in the account panel. */
62
+ export function ForgePanelSection({ title, description, children, }) {
63
+ return (_jsxs("section", { className: "space-y-2", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: title }), description && _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: description })] }), _jsx("div", { className: "space-y-1", children: children })] }));
64
+ }
65
+ /** A row in the account panel: a link or a button that closes the panel. */
66
+ export function ForgePanelItem({ icon: Icon, label, description, href, external, onClick, disabled, active, tone = 'default', keepOpen, className: extraClassName = '', ...buttonProps }) {
67
+ const className = `flex w-full items-start gap-3 rounded-md px-2 py-2 text-left text-sm transition-colors hover:bg-accent focus-visible:outline-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 ${tone === 'destructive' ? 'text-destructive' : ''} ${active ? 'bg-accent font-medium' : ''} ${extraClassName}`;
68
+ const body = (_jsxs(_Fragment, { children: [Icon && _jsx(Icon, { className: "mt-0.5 h-4 w-4 shrink-0" }), _jsxs("span", { className: "flex-1", children: [_jsx("span", { className: "block font-medium", children: label }), description && _jsx("span", { className: "block text-xs text-muted-foreground", children: description })] }), external && href?.startsWith('http') && tone === 'default' && (_jsx(ExternalLink, { className: "mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" }))] }));
69
+ const element = href ? (external ? (_jsx("a", { href: href, className: className, "aria-current": active ? 'page' : undefined, children: body })) : (_jsx(Link, { href: href, className: className, "aria-current": active ? 'page' : undefined, children: body }))) : (_jsx("button", { type: "button", ...buttonProps, onClick: onClick, disabled: disabled, className: className, children: body }));
70
+ return keepOpen ? element : _jsx(Dialog.Close, { asChild: true, children: element });
71
+ }
72
+ /** A labelled on/off switch row for panel sections. */
73
+ export function ForgeSwitchRow({ icon: Icon, label, description, checked, onCheckedChange, }) {
74
+ return (_jsxs("label", { className: "flex cursor-pointer items-center gap-3 text-sm", children: [Icon && _jsx(Icon, { className: "h-4 w-4 shrink-0" }), _jsxs("span", { className: "flex-1", children: [_jsx("span", { className: "block font-medium", children: label }), description && _jsx("span", { className: "block text-xs text-muted-foreground", children: description })] }), _jsx("button", { type: "button", role: "switch", "aria-checked": checked, onClick: () => onCheckedChange(!checked), className: `relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring ${checked ? 'bg-primary' : 'bg-input'}`, children: _jsx("span", { className: `inline-block h-4 w-4 rounded-full bg-background shadow transition-transform ${checked ? 'translate-x-4' : 'translate-x-0.5'}` }) })] }));
75
+ }
@@ -0,0 +1,12 @@
1
+ import { type ForgeBrandId } from './brand.js';
2
+ export type ForgeLogoProps = {
3
+ brand: ForgeBrandId;
4
+ /** `logo` is the mark plus wordmark; `icon` is the square mark. */
5
+ variant?: 'logo' | 'icon';
6
+ /** Size it with height classes, e.g. `h-8`; width follows. */
7
+ className?: string;
8
+ /** Defaults to the brand name; pass `""` when a visible label already names it. */
9
+ alt?: string;
10
+ };
11
+ /** An xForge app's logo or icon, switching to the right logo for light and dark mode. */
12
+ export declare function ForgeLogo({ brand, variant, className, alt }: ForgeLogoProps): import("react").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { FORGE_BRANDS } from './brand.js';
3
+ /** An xForge app's logo or icon, switching to the right logo for light and dark mode. */
4
+ export function ForgeLogo({ brand, variant = 'logo', className = 'h-8', alt }) {
5
+ const b = FORGE_BRANDS[brand];
6
+ const label = alt ?? b.name;
7
+ if (variant === 'icon') {
8
+ return _jsx("img", { src: b.icon, alt: label, className: `${className} aspect-square w-auto`, draggable: false });
9
+ }
10
+ return (_jsxs(_Fragment, { children: [_jsx("img", { src: b.logo.light, alt: label, className: `${className} w-auto dark:hidden`, draggable: false }), _jsx("img", { src: b.logo.dark, alt: label, className: `${className} hidden w-auto dark:block`, draggable: false })] }));
11
+ }
@@ -0,0 +1,10 @@
1
+ import { type ForgeAppId } from './apps.js';
2
+ export type GuestBannerProps = {
3
+ appId: ForgeAppId;
4
+ appName: string;
5
+ /** `undefined` while auth state is still loading; the banner renders nothing until known. */
6
+ signedIn: boolean | undefined;
7
+ /** Override for tests; defaults to the live page URL. */
8
+ currentUrl?: string;
9
+ };
10
+ export declare function GuestBanner({ appId, appName, signedIn, currentUrl }: GuestBannerProps): import("react").JSX.Element | null;