@xforge-dev/forge-kit 0.16.0 → 0.17.1

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/client.d.ts CHANGED
@@ -6,7 +6,7 @@ export { dismissBanner, isBannerDismissed, bannerKey } from './banner-storage.js
6
6
  export { useCurrentUrl } from './use-current-url.js';
7
7
  export { isForgeAuthConfigured } from './config.js';
8
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';
9
+ export { ForgeHeader, ForgeAvatar, ForgePanelSection, ForgePanelItem, ForgeSwitchRow, DiscordIcon, type ForgeHeaderProps, isNavGroup, type ForgeNavItem, type ForgeNavLink, type ForgeNavGroup, type ForgeNavSection, type ForgePanelItemProps, } from './forge-header.js';
10
10
  export * from './theme.js';
11
11
  export { ForgeLogo, type ForgeLogoProps } from './forge-logo.js';
12
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';
package/dist/client.js CHANGED
@@ -7,7 +7,7 @@ export { dismissBanner, isBannerDismissed, bannerKey } from './banner-storage.js
7
7
  export { useCurrentUrl } from './use-current-url.js';
8
8
  export { isForgeAuthConfigured } from './config.js';
9
9
  export { ForgeThemeProvider, useForgeTheme, resolveTheme, storeForgeTheme, } from './theme-provider.js';
10
- export { ForgeHeader, ForgeAvatar, ForgePanelSection, ForgePanelItem, ForgeSwitchRow, DiscordIcon, } from './forge-header.js';
10
+ export { ForgeHeader, ForgeAvatar, ForgePanelSection, ForgePanelItem, ForgeSwitchRow, DiscordIcon, isNavGroup, } from './forge-header.js';
11
11
  export * from './theme.js';
12
12
  export { ForgeLogo } from './forge-logo.js';
13
13
  export { ForgeSupportButton, BUY_ME_A_COFFEE_URL, BUY_ME_A_COFFEE_BUTTON, FORGE_SPONSOR_TIERS, ForgeSupporterBadge, } from './support-button.js';
@@ -2,17 +2,33 @@ import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
2
  import { type LucideIcon } from 'lucide-react';
3
3
  import { type ForgeAppId } from './apps.js';
4
4
  import type { ForgeAccount } from './use-forge-user.js';
5
- export type ForgeNavItem = {
5
+ export type ForgeNavLink = {
6
6
  href: string;
7
7
  label: string;
8
8
  icon?: LucideIcon;
9
9
  active?: boolean;
10
+ description?: string;
10
11
  };
12
+ /** A run of links inside a nav group, under an optional heading (text, or e.g. an avatar and a name). */
13
+ export type ForgeNavSection = {
14
+ key?: string;
15
+ title?: ReactNode;
16
+ items: ForgeNavLink[];
17
+ };
18
+ /** A header menu of related pages; active when one of its links is. */
19
+ export type ForgeNavGroup = {
20
+ label: string;
21
+ icon?: LucideIcon;
22
+ active?: boolean;
23
+ sections: ForgeNavSection[];
24
+ };
25
+ export type ForgeNavItem = ForgeNavLink | ForgeNavGroup;
26
+ export declare const isNavGroup: (item: ForgeNavItem) => item is ForgeNavGroup;
11
27
  export type ForgeHeaderProps = {
12
28
  current?: ForgeAppId;
13
29
  account: ForgeAccount | null;
14
30
  syncError?: boolean;
15
- /** Page links: inline on desktop, in the account panel on mobile. */
31
+ /** Page links and groups of them: inline (groups as menus) on desktop, in the account panel on mobile. */
16
32
  nav?: ForgeNavItem[];
17
33
  /** Header buttons shown left of the avatar. */
18
34
  actions?: ReactNode;
@@ -1,9 +1,10 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import * as Dialog from '@radix-ui/react-dialog';
4
+ import * as Menu from '@radix-ui/react-dropdown-menu';
4
5
  import Link from 'next/link.js';
5
6
  import { useState } from 'react';
6
- import { ExternalLink, LogIn, LogOut, Moon, RefreshCw, Sun, UserRound, X } from 'lucide-react';
7
+ import { Check, ChevronDown, ExternalLink, LogIn, LogOut, Moon, RefreshCw, Sun, UserRound, X } from 'lucide-react';
7
8
  import { accountUrl } from './account-url.js';
8
9
  import { AppSwitcher } from './app-switcher.js';
9
10
  import { FORGE_APPS } from './apps.js';
@@ -11,15 +12,23 @@ import { ForgeSupporterBadge } from './support-button.js';
11
12
  import { FORGE_COLOR_THEMES } from './theme.js';
12
13
  import { useForgeTheme } from './theme-provider.js';
13
14
  import { useCurrentUrl } from './use-current-url.js';
15
+ export const isNavGroup = (item) => 'sections' in item;
16
+ const navPill = (active) => active
17
+ ? 'rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground'
18
+ : 'rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground';
14
19
  /**
15
20
  * The header every xForge app shares: app switcher, page links, app buttons,
16
21
  * and an avatar that opens the account panel with the theme settings.
17
22
  */
18
23
  export function ForgeHeader({ current, account, syncError, nav = [], actions, accountDetails, guestDetails, accountActions, panel, onBeforeSignOut, containerClassName = 'max-w-6xl', currentUrl, }) {
19
24
  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 }) })] })] })] }) }));
25
+ 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((item) => isNavGroup(item) ? (_jsx(NavMenu, { group: item }, item.label)) : (_jsx(Link, { href: item.href, "aria-current": item.active ? 'page' : undefined, className: navPill(item.active), children: item.label }, item.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 }) })] })] })] }) }));
26
+ }
27
+ const menuItem = 'flex cursor-pointer select-none items-start gap-3 rounded-sm px-2 py-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground';
28
+ /** A nav group on desktop: a pill that opens its links, sections split by a seam. */
29
+ function NavMenu({ group }) {
30
+ const sections = group.sections.filter((s) => s.items.length > 0);
31
+ return (_jsxs(Menu.Root, { modal: false, children: [_jsxs(Menu.Trigger, { className: `group flex items-center gap-1 ${navPill(group.active)} focus-visible:outline-2 focus-visible:outline-ring`, children: [group.label, _jsx(ChevronDown, { "aria-hidden": true, className: "h-3.5 w-3.5 opacity-70 transition-transform duration-200 group-data-[state=open]:rotate-180" })] }), _jsx(Menu.Portal, { children: _jsx(Menu.Content, { align: "start", sideOffset: 6, className: "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] w-64 overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md", children: sections.map((section, i) => (_jsxs(Menu.Group, { children: [i > 0 && _jsx(Menu.Separator, { className: "-mx-1 my-1 h-px bg-border" }), section.title && (_jsx(Menu.Label, { className: "px-2 pb-1 pt-1.5 text-xs font-medium text-muted-foreground", children: section.title })), section.items.map(({ href, label, icon: Icon, active, description }) => (_jsx(Menu.Item, { asChild: true, children: _jsxs(Link, { href: href, "aria-current": active ? 'page' : undefined, className: `${menuItem} ${active ? 'bg-accent font-medium' : ''}`, children: [Icon && _jsx(Icon, { "aria-hidden": true, className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block", children: label }), description && _jsx("span", { className: "block text-xs text-muted-foreground", children: description })] }), active && _jsx(Check, { "aria-hidden": true, className: "mt-0.5 h-4 w-4 shrink-0" })] }) }, href)))] }, section.key ?? i))) }) })] }));
23
32
  }
24
33
  function initials(name) {
25
34
  return name.slice(0, 2).toUpperCase();
@@ -48,7 +57,15 @@ function AccountPanel({ current, account, syncError, nav = [], accountDetails, g
48
57
  window.location.assign(signOutHref);
49
58
  }
50
59
  }
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] })] }));
60
+ 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((item) => isNavGroup(item) ? (_jsx(PanelNavGroup, { group: item }, item.label)) : (_jsx(ForgePanelItem, { icon: item.icon, label: item.label, href: item.href, active: item.active }, item.href))) }) })), _jsx(AppearanceSection, {}), panel] })] }));
61
+ }
62
+ /** A nav group in the mobile panel: its name as a small heading, then its links, a section title before each run. */
63
+ function PanelNavGroup({ group }) {
64
+ const Icon = group.icon;
65
+ return (_jsxs("div", { role: "group", "aria-label": group.label, className: "pt-2", children: [_jsxs("p", { className: "flex items-center gap-2 px-2 pb-1 text-xs font-medium text-muted-foreground", children: [Icon && _jsx(Icon, { "aria-hidden": true, className: "h-3.5 w-3.5" }), group.label] }), group.sections.map((section, i) => (_jsxs("div", { className: "space-y-1", children: [section.title && section.items.length > 0 && (
66
+ // A text title sits over the links' labels; a rich one (an avatar and name) brings its own leading
67
+ // media, so it lines up with their icons instead.
68
+ _jsx("div", { className: `px-2 pb-0.5 pt-1.5 text-xs text-muted-foreground ${typeof section.title === 'string' ? 'pl-9' : ''}`, children: section.title })), section.items.map(({ href, label, icon, active }) => (_jsx(ForgePanelItem, { icon: icon, label: label, href: href, active: active }, href)))] }, section.key ?? i)))] }));
52
69
  }
53
70
  /** Discord's mark in `currentColor`; give it Discord's blurple (`text-[#5865F2]`) next to a Discord name. */
54
71
  export function DiscordIcon({ className }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xforge-dev/forge-kit",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "Shared auth, app registry and UI for xForge apps",
5
5
  "license": "MIT",
6
6
  "repository": {