@xforge-dev/forge-kit 0.19.1 → 0.21.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.
- package/dist/forge-header.d.ts +11 -2
- package/dist/forge-header.js +14 -6
- package/dist/tokens.css +58 -13
- package/package.json +1 -1
package/dist/forge-header.d.ts
CHANGED
|
@@ -6,14 +6,21 @@ export type ForgeNavLink = {
|
|
|
6
6
|
href: string;
|
|
7
7
|
label: string;
|
|
8
8
|
icon?: LucideIcon;
|
|
9
|
+
/** Full-colour art (e.g. a game icon) in place of `icon`: drawn in a 20px box and left untinted. */
|
|
10
|
+
media?: ReactNode;
|
|
9
11
|
active?: boolean;
|
|
10
12
|
description?: string;
|
|
11
13
|
};
|
|
12
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* A run of links inside a nav group, under an optional heading (text, or e.g. an avatar and a name). `rail` indents
|
|
16
|
+
* the links behind a line, kept within the heading's width, to show they all belong to it (e.g. one character's
|
|
17
|
+
* pages); the active link lights its stretch of the line.
|
|
18
|
+
*/
|
|
13
19
|
export type ForgeNavSection = {
|
|
14
20
|
key?: string;
|
|
15
21
|
title?: ReactNode;
|
|
16
22
|
items: ForgeNavLink[];
|
|
23
|
+
rail?: boolean;
|
|
17
24
|
};
|
|
18
25
|
/** A header menu of related pages; active when one of its links is. */
|
|
19
26
|
export type ForgeNavGroup = {
|
|
@@ -68,6 +75,8 @@ export declare function ForgePanelSection({ title, description, children, }: {
|
|
|
68
75
|
}): import("react").JSX.Element;
|
|
69
76
|
export type ForgePanelItemProps = {
|
|
70
77
|
icon?: LucideIcon;
|
|
78
|
+
/** Full-colour art in place of `icon` (see ForgeNavLink). */
|
|
79
|
+
media?: ReactNode;
|
|
71
80
|
label: ReactNode;
|
|
72
81
|
description?: ReactNode;
|
|
73
82
|
/** In-app path (client navigation) or, with `external`, a full URL. */
|
|
@@ -81,7 +90,7 @@ export type ForgePanelItemProps = {
|
|
|
81
90
|
keepOpen?: boolean;
|
|
82
91
|
} & Omit<ComponentPropsWithRef<'button'>, 'children' | 'onClick' | 'disabled'>;
|
|
83
92
|
/** A row in the account panel: a link or a button that closes the panel. */
|
|
84
|
-
export declare function ForgePanelItem({ icon
|
|
93
|
+
export declare function ForgePanelItem({ icon, media, label, description, href, external, onClick, disabled, active, tone, keepOpen, className: extraClassName, ...buttonProps }: ForgePanelItemProps): import("react").JSX.Element;
|
|
85
94
|
/** A labelled on/off switch row for panel sections. */
|
|
86
95
|
export declare function ForgeSwitchRow({ icon: Icon, label, description, checked, onCheckedChange, }: {
|
|
87
96
|
icon?: LucideIcon;
|
package/dist/forge-header.js
CHANGED
|
@@ -12,10 +12,18 @@ import { ForgeSupporterBadge } from './support-button.js';
|
|
|
12
12
|
import { FORGE_COLOR_THEMES } from './theme.js';
|
|
13
13
|
import { useForgeTheme } from './theme-provider.js';
|
|
14
14
|
import { useCurrentUrl } from './use-current-url.js';
|
|
15
|
+
/** The active link's stretch of a section's rail, on a `relative` row inset 4px from a 2px line. */
|
|
16
|
+
const railMark = 'before:absolute before:inset-y-1 before:-left-1.5 before:w-0.5 before:rounded-full before:bg-primary';
|
|
17
|
+
/** A link's leading art or icon; `icon` keeps the 16px tinted look, `media` gets a 20px untinted box. */
|
|
18
|
+
function LinkLead({ icon: Icon, media, tint }) {
|
|
19
|
+
if (media)
|
|
20
|
+
return _jsx("span", { "aria-hidden": true, className: "flex size-5 shrink-0 items-center justify-center", children: media });
|
|
21
|
+
return Icon ? _jsx(Icon, { "aria-hidden": true, className: `mt-0.5 h-4 w-4 shrink-0 ${tint ? 'text-muted-foreground' : ''}` }) : null;
|
|
22
|
+
}
|
|
15
23
|
export const isNavGroup = (item) => 'sections' in item;
|
|
16
24
|
const navPill = (active) => active
|
|
17
|
-
? 'rounded-
|
|
18
|
-
: 'rounded-
|
|
25
|
+
? 'rounded-lg bg-primary-tint px-3 py-1.5 text-sm font-semibold text-primary-text'
|
|
26
|
+
: 'rounded-lg px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground';
|
|
19
27
|
/**
|
|
20
28
|
* The header every xForge app shares: app switcher, page links, app buttons,
|
|
21
29
|
* and an avatar that opens the account panel with the theme settings.
|
|
@@ -28,7 +36,7 @@ const menuItem = 'flex cursor-pointer select-none items-start gap-3 rounded-sm p
|
|
|
28
36
|
/** A nav group on desktop: a pill that opens its links, sections split by a seam. */
|
|
29
37
|
function NavMenu({ group }) {
|
|
30
38
|
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
|
|
39
|
+
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 })), _jsx("div", { className: section.rail ? 'ml-3.5 mr-2 border-l-2 pl-1' : undefined, children: section.items.map(({ href, label, icon, media, active, description }) => (_jsx(Menu.Item, { asChild: true, children: _jsxs(Link, { href: href, "aria-current": active ? 'page' : undefined, className: `${menuItem} ${active ? 'bg-accent font-medium' : ''} ${section.rail ? `relative ${active ? railMark : ''}` : ''}`, children: [_jsx(LinkLead, { icon: icon, media: media, tint: true }), _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))) }) })] }));
|
|
32
40
|
}
|
|
33
41
|
function initials(name) {
|
|
34
42
|
return name.slice(0, 2).toUpperCase();
|
|
@@ -65,7 +73,7 @@ function PanelNavGroup({ group }) {
|
|
|
65
73
|
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
74
|
// A text title sits over the links' labels; a rich one (an avatar and name) brings its own leading
|
|
67
75
|
// 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)))] }));
|
|
76
|
+
_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 })), _jsx("div", { className: section.rail ? 'ml-4 mr-2 space-y-1 border-l-2 pl-1' : 'space-y-1', children: section.items.map(({ href, label, icon, media, active }) => (_jsx(ForgePanelItem, { icon: icon, media: media, label: label, href: href, active: active, className: section.rail ? `relative ${active ? railMark : ''}` : '' }, href))) })] }, section.key ?? i)))] }));
|
|
69
77
|
}
|
|
70
78
|
/** Discord's mark in `currentColor`; give it Discord's blurple (`text-[#5865F2]`) next to a Discord name. */
|
|
71
79
|
export function DiscordIcon({ className }) {
|
|
@@ -80,9 +88,9 @@ export function ForgePanelSection({ title, description, children, }) {
|
|
|
80
88
|
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 })] }));
|
|
81
89
|
}
|
|
82
90
|
/** A row in the account panel: a link or a button that closes the panel. */
|
|
83
|
-
export function ForgePanelItem({ icon
|
|
91
|
+
export function ForgePanelItem({ icon, media, label, description, href, external, onClick, disabled, active, tone = 'default', keepOpen, className: extraClassName = '', ...buttonProps }) {
|
|
84
92
|
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}`;
|
|
85
|
-
const body = (_jsxs(_Fragment, { children: [
|
|
93
|
+
const body = (_jsxs(_Fragment, { children: [_jsx(LinkLead, { icon: icon, media: media }), _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" }))] }));
|
|
86
94
|
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 }));
|
|
87
95
|
return keepOpen ? element : _jsx(Dialog.Close, { asChild: true, children: element });
|
|
88
96
|
}
|
package/dist/tokens.css
CHANGED
|
@@ -46,6 +46,16 @@
|
|
|
46
46
|
--color-border: var(--border);
|
|
47
47
|
--color-input: var(--input);
|
|
48
48
|
--color-ring: var(--ring);
|
|
49
|
+
--color-primary-text: var(--primary-text);
|
|
50
|
+
--color-primary-muted: var(--primary-muted);
|
|
51
|
+
--color-primary-tint: var(--primary-tint);
|
|
52
|
+
--color-primary-border: var(--primary-border);
|
|
53
|
+
--color-primary-surface: var(--primary-surface);
|
|
54
|
+
--color-primary-surface-border: var(--primary-surface-border);
|
|
55
|
+
--color-inset: var(--inset);
|
|
56
|
+
--color-divider: var(--divider);
|
|
57
|
+
--color-chip: var(--chip);
|
|
58
|
+
--color-track: var(--track);
|
|
49
59
|
|
|
50
60
|
--radius-lg: var(--radius);
|
|
51
61
|
--radius-md: calc(var(--radius) - 2px);
|
|
@@ -75,6 +85,14 @@
|
|
|
75
85
|
--border: hsl(0 0% 89.8%);
|
|
76
86
|
--input: hsl(0 0% 89.8%);
|
|
77
87
|
--ring: hsl(38 100% 65%);
|
|
88
|
+
/* The accent as small text (11–14px): at least 4.5:1 on --card. Each theme tunes it per mode below;
|
|
89
|
+
big bold figures can use it too. Gold on white needs it far darker than the swatch. */
|
|
90
|
+
--primary-text: hsl(38 100% 29%);
|
|
91
|
+
/* Neutral roles inside panels: rows set into a card, the rules between rows, chips, empty meter tracks. */
|
|
92
|
+
--inset: hsl(0 0% 96.1%);
|
|
93
|
+
--divider: hsl(0 0% 92%);
|
|
94
|
+
--chip: hsl(0 0% 93%);
|
|
95
|
+
--track: hsl(0 0% 89.8%);
|
|
78
96
|
--radius: 0.5rem;
|
|
79
97
|
/* Explicit Tailwind v4 utility vars (ensures runtime overrides work) */
|
|
80
98
|
--color-primary: hsl(38 100% 65%);
|
|
@@ -103,24 +121,51 @@
|
|
|
103
121
|
--border: hsl(0 0% 20%);
|
|
104
122
|
--input: hsl(0 0% 20%);
|
|
105
123
|
--ring: hsl(38 100% 65%);
|
|
124
|
+
--primary-text: hsl(38 100% 65%);
|
|
125
|
+
--inset: hsl(0 0% 10.5%);
|
|
126
|
+
--divider: hsl(0 0% 17%);
|
|
127
|
+
--chip: hsl(0 0% 19%);
|
|
128
|
+
--track: hsl(0 0% 18%);
|
|
106
129
|
--color-primary: hsl(38 100% 65%);
|
|
107
130
|
--color-primary-foreground: hsl(38 100% 10%);
|
|
108
131
|
--color-ring: hsl(38 100% 65%);
|
|
109
132
|
}
|
|
110
133
|
|
|
111
|
-
/*
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
/* Everything else accent-coloured derives from --primary / --primary-text, so a theme is one swatch. */
|
|
135
|
+
:root {
|
|
136
|
+
--primary-muted: color-mix(in oklab, var(--primary-text) 65%, var(--muted-foreground));
|
|
137
|
+
--primary-tint: color-mix(in oklab, var(--primary) 12%, transparent);
|
|
138
|
+
--primary-border: color-mix(in oklab, var(--primary) 35%, transparent);
|
|
139
|
+
--primary-surface: color-mix(in oklab, var(--primary) 7%, var(--card));
|
|
140
|
+
--primary-surface-border: color-mix(in oklab, var(--primary) 25%, var(--border));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ── Solid colour themes (one per FORGE_COLOR_THEMES entry; see theme.ts) ──
|
|
144
|
+
Each sets the swatch, the ink on a solid fill of it (--primary-foreground: dark ink, or white where that reads
|
|
145
|
+
better), and --primary-text per mode, stepped until it reaches 4.5:1 on --card. Gradient themes use one solid
|
|
146
|
+
representative colour here: never put a gradient on text or a thin bar. */
|
|
147
|
+
html[data-color="blue"] { --primary: hsl(217 91% 60%); --color-primary: hsl(217 91% 60%); --ring: hsl(217 91% 60%); --color-ring: hsl(217 91% 60%); --primary-foreground: hsl(0 0% 4%); --color-primary-foreground: hsl(0 0% 4%); --primary-text: hsl(217 91% 49%); }
|
|
148
|
+
html[data-color="blue"].dark { --primary-text: hsl(217 91% 65%); }
|
|
149
|
+
html[data-color="green"] { --primary: hsl(142 71% 45%); --color-primary: hsl(142 71% 45%); --ring: hsl(142 71% 45%); --color-ring: hsl(142 71% 45%); --primary-text: hsl(142 71% 27%); }
|
|
150
|
+
html[data-color="green"].dark { --primary-text: hsl(142 71% 47%); }
|
|
151
|
+
html[data-color="rose"] { --primary: hsl(346 72% 58%); --color-primary: hsl(346 72% 58%); --ring: hsl(346 72% 58%); --color-ring: hsl(346 72% 58%); --primary-foreground: hsl(0 0% 4%); --color-primary-foreground: hsl(0 0% 4%); --primary-text: hsl(346 72% 49%); }
|
|
152
|
+
html[data-color="rose"].dark { --primary-text: hsl(346 72% 66%); }
|
|
153
|
+
html[data-color="violet"] { --primary: hsl(262 83% 58%); --color-primary: hsl(262 83% 58%); --ring: hsl(262 83% 58%); --color-ring: hsl(262 83% 58%); --primary-foreground: hsl(0 0% 98%); --color-primary-foreground: hsl(0 0% 98%); --primary-text: hsl(262 83% 55%); }
|
|
154
|
+
html[data-color="violet"].dark { --primary-text: hsl(262 83% 72%); }
|
|
155
|
+
html[data-color="sunset"] { --primary: hsl(27 100% 74%); --color-primary: hsl(27 100% 74%); --ring: hsl(27 100% 74%); --color-ring: hsl(27 100% 74%); --primary-text: hsl(27 100% 34%); }
|
|
156
|
+
html[data-color="sunset"].dark { --primary-text: hsl(27 100% 74%); }
|
|
157
|
+
html[data-color="ocean"] { --primary: hsl(209 100% 50%); --color-primary: hsl(209 100% 50%); --ring: hsl(209 100% 50%); --color-ring: hsl(209 100% 50%); --primary-foreground: hsl(0 0% 4%); --color-primary-foreground: hsl(0 0% 4%); --primary-text: hsl(209 100% 40%); }
|
|
158
|
+
html[data-color="ocean"].dark { --primary-text: hsl(209 100% 56%); }
|
|
159
|
+
html[data-color="lush"] { --primary: hsl(130 52% 52%); --color-primary: hsl(130 52% 52%); --ring: hsl(130 52% 52%); --color-ring: hsl(130 52% 52%); --primary-text: hsl(130 52% 31%); }
|
|
160
|
+
html[data-color="lush"].dark { --primary-text: hsl(130 52% 54%); }
|
|
161
|
+
html[data-color="passion"] { --primary: hsl(7 99% 62%); --color-primary: hsl(7 99% 62%); --ring: hsl(7 99% 62%); --color-ring: hsl(7 99% 62%); --primary-text: hsl(7 99% 42%); }
|
|
162
|
+
html[data-color="passion"].dark { --primary-text: hsl(7 99% 64%); }
|
|
163
|
+
html[data-color="royal"] { --primary: hsl(276 86% 57%); --color-primary: hsl(276 86% 57%); --ring: hsl(276 86% 57%); --color-ring: hsl(276 86% 57%); --primary-foreground: hsl(0 0% 98%); --color-primary-foreground: hsl(0 0% 98%); --primary-text: hsl(276 86% 54%); }
|
|
164
|
+
html[data-color="royal"].dark { --primary-text: hsl(276 86% 70%); }
|
|
165
|
+
html[data-color="steel"] { --primary: hsl(0 0% 60%); --color-primary: hsl(0 0% 60%); --ring: hsl(0 0% 60%); --color-ring: hsl(0 0% 60%); --primary-text: hsl(0 0% 42%); }
|
|
166
|
+
html[data-color="steel"].dark { --primary-text: hsl(0 0% 62%); }
|
|
167
|
+
html[data-color="mono"]:not(.dark) { --primary: hsl(0 0% 9%); --color-primary: hsl(0 0% 9%); --primary-foreground: hsl(0 0% 98%); --color-primary-foreground: hsl(0 0% 98%); --ring: hsl(0 0% 9%); --color-ring: hsl(0 0% 9%); --primary-text: hsl(0 0% 9%); }
|
|
168
|
+
html[data-color="mono"].dark { --primary: hsl(0 0% 98%); --color-primary: hsl(0 0% 98%); --primary-foreground: hsl(0 0% 9%); --color-primary-foreground: hsl(0 0% 9%); --ring: hsl(0 0% 98%); --color-ring: hsl(0 0% 98%); --primary-text: hsl(0 0% 98%); }
|
|
124
169
|
|
|
125
170
|
/* ── Gradient themes: replace the accent fill with a gradient ──
|
|
126
171
|
Unlayered on purpose: in Tailwind 4, cascade layers beat specificity, so
|