@xforge-dev/forge-kit 0.24.3 → 0.25.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/README.md +63 -0
- package/dist/forge-header.d.ts +1 -1
- package/dist/forge-header.js +21 -6
- package/dist/guest-banner.js +1 -1
- package/dist/ign-input.js +1 -1
- package/dist/jobs-snapshot.js +7 -3
- package/dist/support-button.d.ts +5 -4
- package/dist/support-button.js +5 -4
- package/dist/tokens.css +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,6 +80,8 @@ import { forgeThemeScript } from '@xforge-dev/forge-kit';
|
|
|
80
80
|
|
|
81
81
|
The avatar opens a side panel with the account links, the page links on mobile, Appearance
|
|
82
82
|
(dark mode, color theme, "Same theme in all xForge apps") and then the app's own sections.
|
|
83
|
+
Below 640px a "Menu" button left of the avatar opens the same panel with the page links first.
|
|
84
|
+
`panelNav={false}` (for an app with its own phone navigation) drops both the panel links and the button.
|
|
83
85
|
Build those from `ForgePanelSection`, `ForgePanelItem` and `ForgeSwitchRow`.
|
|
84
86
|
Under the username the panel shows the account's supporter badge and Discord name (from
|
|
85
87
|
`ForgeAccount.supporterTier` / `discordUsername`, which `useForgeUser` reads from `profiles`), then
|
|
@@ -140,6 +142,67 @@ owned event blocks and the two equipped ones add a member's points of their rank
|
|
|
140
142
|
presets (`SharedPoints`, stored under `points` in the shared row so an older board write leaves it alone) and the 5th
|
|
141
143
|
Champion slot (`championSlotsOpen`).
|
|
142
144
|
|
|
145
|
+
## Keeping apps on the latest kit
|
|
146
|
+
|
|
147
|
+
After a publish, xforge's release workflow (`notify-apps`) sends a `forge-kit-published` repository dispatch, with
|
|
148
|
+
the version, to bossforge, legionforge, familiarforge and cardforge. That needs an `APPS_DISPATCH_TOKEN` secret
|
|
149
|
+
(a fine-grained PAT or GitHub App token with Contents: read and write on those four repos); without it the job logs a
|
|
150
|
+
notice and does nothing. Each app repo then needs this `.github/workflows/kit-bump.yml`, which also runs on a
|
|
151
|
+
schedule so it catches up when no dispatch arrives:
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
name: Kit bump
|
|
155
|
+
# Installs the newest @xforge-dev/forge-kit, checks the app and pushes the bump to main. xforge's release workflow
|
|
156
|
+
# dispatches forge-kit-published right after a publish; the schedule catches up when that dispatch is missing.
|
|
157
|
+
on:
|
|
158
|
+
repository_dispatch: { types: [forge-kit-published] }
|
|
159
|
+
schedule: [{ cron: '23 */6 * * *' }]
|
|
160
|
+
workflow_dispatch:
|
|
161
|
+
permissions:
|
|
162
|
+
contents: write
|
|
163
|
+
concurrency: { group: kit-bump, cancel-in-progress: false }
|
|
164
|
+
jobs:
|
|
165
|
+
bump:
|
|
166
|
+
# Home lab runner: no Actions minutes. Its image has no npm, hence setup-node.
|
|
167
|
+
runs-on: [self-hosted, homelab]
|
|
168
|
+
steps:
|
|
169
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
|
170
|
+
with: { ref: main }
|
|
171
|
+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
172
|
+
with: { node-version: 24, cache: npm }
|
|
173
|
+
- id: bump
|
|
174
|
+
env:
|
|
175
|
+
WANT: ${{ github.event.client_payload.version }}
|
|
176
|
+
run: |
|
|
177
|
+
# A dispatch can beat the registry by a few seconds: wait for the announced version to resolve.
|
|
178
|
+
if [ -n "$WANT" ]; then
|
|
179
|
+
for i in $(seq 30); do npm view "@xforge-dev/forge-kit@$WANT" version >/dev/null 2>&1 && break; sleep 10; done
|
|
180
|
+
fi
|
|
181
|
+
current=$(node -p "require('./package-lock.json').packages['node_modules/@xforge-dev/forge-kit'].version")
|
|
182
|
+
latest=$(npm view @xforge-dev/forge-kit version)
|
|
183
|
+
if [ "$current" = "$latest" ]; then
|
|
184
|
+
echo "forge-kit is already $latest"; echo "bump=false" >> "$GITHUB_OUTPUT"; exit 0
|
|
185
|
+
fi
|
|
186
|
+
npm install "@xforge-dev/forge-kit@^$latest"
|
|
187
|
+
echo "bump=true" >> "$GITHUB_OUTPUT"
|
|
188
|
+
echo "from=$current" >> "$GITHUB_OUTPUT"
|
|
189
|
+
echo "to=$latest" >> "$GITHUB_OUTPUT"
|
|
190
|
+
# A GITHUB_TOKEN push starts no other workflow (CI included), so check here before pushing.
|
|
191
|
+
- if: steps.bump.outputs.bump == 'true'
|
|
192
|
+
run: npm run typecheck && npm test
|
|
193
|
+
- if: steps.bump.outputs.bump == 'true'
|
|
194
|
+
env:
|
|
195
|
+
FROM: ${{ steps.bump.outputs.from }}
|
|
196
|
+
TO: ${{ steps.bump.outputs.to }}
|
|
197
|
+
run: |
|
|
198
|
+
git config user.name "github-actions[bot]"
|
|
199
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
200
|
+
git commit -am "chore(deps): bump @xforge-dev/forge-kit from $FROM to $TO"
|
|
201
|
+
# Another push may have landed meanwhile; fast-forward only, never force.
|
|
202
|
+
git pull --rebase origin main
|
|
203
|
+
git push origin HEAD:main
|
|
204
|
+
```
|
|
205
|
+
|
|
143
206
|
## Exports
|
|
144
207
|
|
|
145
208
|
`isForgeAuthConfigured` is exported from the root and `/client` entry points.
|
package/dist/forge-header.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ export declare function ForgeAvatar({ account, className }: {
|
|
|
66
66
|
account: ForgeAccount | null;
|
|
67
67
|
className?: string;
|
|
68
68
|
}): import("react").JSX.Element;
|
|
69
|
-
/** Discord's mark in `currentColor`; give it
|
|
69
|
+
/** Discord's mark in `currentColor`; give it the `text-discord-text` token next to a Discord name. */
|
|
70
70
|
export declare function DiscordIcon({ className }: {
|
|
71
71
|
className?: string;
|
|
72
72
|
}): import("react").JSX.Element;
|
package/dist/forge-header.js
CHANGED
|
@@ -3,8 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import * as Dialog from '@radix-ui/react-dialog';
|
|
4
4
|
import * as Menu from '@radix-ui/react-dropdown-menu';
|
|
5
5
|
import Link from 'next/link.js';
|
|
6
|
-
import { useState } from 'react';
|
|
7
|
-
import { ChevronDown, ExternalLink, LogIn, LogOut, Moon, RefreshCw, Sun, UserRound, X } from 'lucide-react';
|
|
6
|
+
import { useRef, useState } from 'react';
|
|
7
|
+
import { ChevronDown, ExternalLink, LogIn, LogOut, Menu as MenuIcon, Moon, RefreshCw, Sun, UserRound, X } from 'lucide-react';
|
|
8
8
|
import { accountUrl } from './account-url.js';
|
|
9
9
|
import { AppSwitcher } from './app-switcher.js';
|
|
10
10
|
import { FORGE_APPS } from './apps.js';
|
|
@@ -39,7 +39,20 @@ const navPill = (active) => active
|
|
|
39
39
|
*/
|
|
40
40
|
export function ForgeHeader({ current, account, syncError, nav = [], panelNav = true, actions, accountDetails, guestDetails, accountActions, panel, onBeforeSignOut, containerClassName = 'max-w-6xl', currentUrl, }) {
|
|
41
41
|
const [open, setOpen] = useState(false);
|
|
42
|
-
|
|
42
|
+
// Phones get a menu button for the page links (the avatar alone gave no hint of them). It opens the same panel
|
|
43
|
+
// with Navigate first, and takes focus back when the panel closes.
|
|
44
|
+
const showMenu = panelNav && nav.length > 0;
|
|
45
|
+
const [navFirst, setNavFirst] = useState(false);
|
|
46
|
+
const menuButton = useRef(null);
|
|
47
|
+
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 }), showMenu && (_jsx("button", { ref: menuButton, type: "button", "aria-label": "Menu", "aria-haspopup": "dialog", "aria-expanded": open && navFirst, onClick: () => {
|
|
48
|
+
setNavFirst(true);
|
|
49
|
+
setOpen(true);
|
|
50
|
+
}, className: "inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring sm:hidden", children: _jsx(MenuIcon, { "aria-hidden": true, className: "h-5 w-5" }) })), _jsxs(Dialog.Root, { open: open, onOpenChange: setOpen, children: [_jsxs(Dialog.Trigger, { onClick: () => setNavFirst(false), "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, { onCloseAutoFocus: (e) => {
|
|
51
|
+
if (!navFirst || !menuButton.current)
|
|
52
|
+
return;
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
menuButton.current.focus();
|
|
55
|
+
}, 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: panelNav ? nav : [], navFirst: navFirst, accountDetails: accountDetails, guestDetails: guestDetails, accountActions: accountActions, panel: panel, onBeforeSignOut: onBeforeSignOut, currentUrl: currentUrl }) })] })] })] }) }));
|
|
43
56
|
}
|
|
44
57
|
const menuItem = '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';
|
|
45
58
|
/** The page on screen: the hover fill plus a 2px accent bar down its left edge. */
|
|
@@ -61,12 +74,14 @@ export function ForgeAvatar({ account, className = '' }) {
|
|
|
61
74
|
}
|
|
62
75
|
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" }) }));
|
|
63
76
|
}
|
|
64
|
-
function AccountPanel({ current, account, syncError, nav = [], accountDetails, guestDetails, accountActions, panel, onBeforeSignOut, currentUrl, }) {
|
|
77
|
+
function AccountPanel({ current, account, syncError, nav = [], navFirst = false, accountDetails, guestDetails, accountActions, panel, onBeforeSignOut, currentUrl, }) {
|
|
65
78
|
const appUrl = FORGE_APPS.find((a) => a.id === current)?.url ?? accountUrl('/');
|
|
66
79
|
const liveUrl = useCurrentUrl(appUrl);
|
|
67
80
|
const next = currentUrl ?? liveUrl;
|
|
68
81
|
const signOutHref = accountUrl('/logout', { next });
|
|
69
82
|
const [signingOut, setSigningOut] = useState(false);
|
|
83
|
+
// Phone-only: desktop shows these links in the header.
|
|
84
|
+
const navigate = 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))) }) }));
|
|
70
85
|
async function signOut() {
|
|
71
86
|
setSigningOut(true);
|
|
72
87
|
try {
|
|
@@ -76,7 +91,7 @@ function AccountPanel({ current, account, syncError, nav = [], accountDetails, g
|
|
|
76
91
|
window.location.assign(signOutHref);
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
|
-
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-
|
|
94
|
+
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-discord-text", 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: [navFirst && navigate, _jsx(ForgePanelSection, { title: "Account", children: account ? (_jsxs(_Fragment, { children: [_jsx(ForgePanelItem, { icon: UserRound, label: "xForge account", description: "Email, password and Discord sign-in", 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] })) }), !navFirst && navigate, _jsx(AppearanceSection, {}), panel] })] }));
|
|
80
95
|
}
|
|
81
96
|
/** A nav group in the mobile panel: its name as a small heading, then its links, a section title before each run. */
|
|
82
97
|
function PanelNavGroup({ group }) {
|
|
@@ -86,7 +101,7 @@ function PanelNavGroup({ group }) {
|
|
|
86
101
|
// media, so it lines up with their icons instead.
|
|
87
102
|
_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)))] }));
|
|
88
103
|
}
|
|
89
|
-
/** Discord's mark in `currentColor`; give it
|
|
104
|
+
/** Discord's mark in `currentColor`; give it the `text-discord-text` token next to a Discord name. */
|
|
90
105
|
export function DiscordIcon({ className }) {
|
|
91
106
|
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" }) }));
|
|
92
107
|
}
|
package/dist/guest-banner.js
CHANGED
|
@@ -17,7 +17,7 @@ export function GuestBanner({ appId, appName, signedIn, currentUrl }) {
|
|
|
17
17
|
}, [appId]);
|
|
18
18
|
if (signedIn === undefined || signedIn || !visible)
|
|
19
19
|
return null;
|
|
20
|
-
return (_jsxs("div", { className: "flex items-center gap-3 border-b bg-muted px-4 py-2 text-sm text-muted-foreground", children: [_jsxs("p", { className: "flex-1", children: [_jsx("span", { className: "sm:hidden", children: "Guest mode: your data lives only in this browser." }), _jsxs("span", { className: "hidden sm:inline", children: ["You're using ", appName, " in guest mode \u2014 your data lives only in this browser. Create a free xForge account to keep it safe and use it on any device."] })] }), _jsx("a", { href: accountUrl('/register', { next }), className: "shrink-0 whitespace-nowrap rounded-md bg-primary px-3 py-1 font-medium text-primary-foreground hover:bg-primary/90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", children: "Create account" }), _jsx("a", { href: accountUrl('/login', { next }), className: "shrink-0 whitespace-nowrap font-medium text-primary hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", children: "Sign in" }), _jsx("button", { type: "button", "aria-label": "Dismiss", onClick: () => {
|
|
20
|
+
return (_jsxs("div", { className: "flex items-center gap-3 border-b bg-muted px-4 py-2 text-sm text-muted-foreground", children: [_jsxs("p", { className: "flex-1", children: [_jsx("span", { className: "sm:hidden", children: "Guest mode: your data lives only in this browser." }), _jsxs("span", { className: "hidden sm:inline", children: ["You're using ", appName, " in guest mode \u2014 your data lives only in this browser. Create a free xForge account to keep it safe and use it on any device."] })] }), _jsx("a", { href: accountUrl('/register', { next }), className: "shrink-0 whitespace-nowrap rounded-md bg-primary px-3 py-1 font-medium text-primary-foreground hover:bg-primary/90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", children: "Create account" }), _jsx("a", { href: accountUrl('/login', { next }), className: "shrink-0 whitespace-nowrap font-medium text-primary-text hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", children: "Sign in" }), _jsx("button", { type: "button", "aria-label": "Dismiss", onClick: () => {
|
|
21
21
|
dismissBanner(appId);
|
|
22
22
|
setVisible(false);
|
|
23
23
|
}, className: "rounded-md p-1 hover:bg-accent hover:text-accent-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", children: _jsx(X, { className: "h-4 w-4" }) })] }));
|
package/dist/ign-input.js
CHANGED
|
@@ -45,5 +45,5 @@ export function ForgeIgnInput({ value, onValueChange, history = [], onPick, onFo
|
|
|
45
45
|
// ß and ÿ have no single-letter capital in names, so they stay as they are.
|
|
46
46
|
const shown = upper && letter !== 'ß' && letter !== 'ÿ' ? letter.toUpperCase() : letter;
|
|
47
47
|
return (_jsx("button", { type: "button", onClick: () => insert(shown), className: "h-8 rounded-md border text-base hover:bg-accent hover:text-accent-foreground focus-visible:outline-2 focus-visible:outline-ring", children: shown }, letter));
|
|
48
|
-
}) })] }) })] })] }), recent.length > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Recent:" }), recent.map((name) => (_jsxs("span", { className: "inline-flex items-center rounded-full border text-xs", children: [_jsx("button", { type: "button", onClick: () => onPick?.(name), className: `py-1 pl-2.5 hover:text-primary focus-visible:outline-2 focus-visible:outline-ring ${onForget ? 'rounded-l-full pr-1.5' : 'rounded-full pr-2.5'}`, title: `Use ${name}`, children: name }), onForget && (_jsx("button", { type: "button", onClick: () => onForget(name), "aria-label": `Remove ${name} from recent names`, className: "rounded-r-full py-1 pl-0.5 pr-2 text-muted-foreground hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring", children: _jsx(X, { className: "h-3 w-3" }) }))] }, name)))] }))] }));
|
|
48
|
+
}) })] }) })] })] }), recent.length > 0 && (_jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: "Recent:" }), recent.map((name) => (_jsxs("span", { className: "inline-flex items-center rounded-full border text-xs", children: [_jsx("button", { type: "button", onClick: () => onPick?.(name), className: `py-1 pl-2.5 hover:text-primary-text focus-visible:outline-2 focus-visible:outline-ring ${onForget ? 'rounded-l-full pr-1.5' : 'rounded-full pr-2.5'}`, title: `Use ${name}`, children: name }), onForget && (_jsx("button", { type: "button", onClick: () => onForget(name), "aria-label": `Remove ${name} from recent names`, className: "rounded-r-full py-1 pl-0.5 pr-2 text-muted-foreground hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring", children: _jsx(X, { className: "h-3 w-3" }) }))] }, name)))] }))] }));
|
|
49
49
|
}
|
package/dist/jobs-snapshot.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const JOBS_SNAPSHOT = {
|
|
2
2
|
"version": 1,
|
|
3
|
-
"updated": "2026-11-
|
|
3
|
+
"updated": "2026-11-11T03:00:00Z",
|
|
4
4
|
"jobs": [
|
|
5
5
|
{
|
|
6
6
|
"name": "Hero",
|
|
@@ -2600,7 +2600,9 @@ export const JOBS_SNAPSHOT = {
|
|
|
2600
2600
|
},
|
|
2601
2601
|
{
|
|
2602
2602
|
"name": "Sia Astelle",
|
|
2603
|
-
"aliases": [
|
|
2603
|
+
"aliases": [
|
|
2604
|
+
"Sia Astra"
|
|
2605
|
+
],
|
|
2604
2606
|
"branches": [
|
|
2605
2607
|
"magician"
|
|
2606
2608
|
],
|
|
@@ -2650,7 +2652,9 @@ export const JOBS_SNAPSHOT = {
|
|
|
2650
2652
|
},
|
|
2651
2653
|
{
|
|
2652
2654
|
"name": "Ren",
|
|
2653
|
-
"aliases": [
|
|
2655
|
+
"aliases": [
|
|
2656
|
+
"Len"
|
|
2657
|
+
],
|
|
2654
2658
|
"branches": [
|
|
2655
2659
|
"warrior"
|
|
2656
2660
|
],
|
package/dist/support-button.d.ts
CHANGED
|
@@ -2,25 +2,26 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
export declare const BUY_ME_A_COFFEE_URL = "https://www.buymeacoffee.com/xtherebellion";
|
|
3
3
|
/** The apps' CSP `img-src` must allow https://cdn.buymeacoffee.com for this to load. */
|
|
4
4
|
export declare const BUY_ME_A_COFFEE_BUTTON = "https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png";
|
|
5
|
-
/** Supporter badge tiers, shared by every xForge app. `id` matches `profiles.supporter_tier`.
|
|
5
|
+
/** Supporter badge tiers, shared by every xForge app. `id` matches `profiles.supporter_tier`. Colours are the
|
|
6
|
+
* per-mode `--supporter-*` tokens in tokens.css (fixed, never the accent). */
|
|
6
7
|
export declare const FORGE_SPONSOR_TIERS: readonly [{
|
|
7
8
|
readonly id: "gold";
|
|
8
9
|
readonly Icon: import("lucide-react").LucideIcon;
|
|
9
10
|
readonly label: "Gold Sponsor";
|
|
10
11
|
readonly threshold: "$10+";
|
|
11
|
-
readonly className: "border-
|
|
12
|
+
readonly className: "border-supporter-gold/50 bg-supporter-gold/10 text-supporter-gold";
|
|
12
13
|
}, {
|
|
13
14
|
readonly id: "platinum";
|
|
14
15
|
readonly Icon: import("lucide-react").LucideIcon;
|
|
15
16
|
readonly label: "Platinum Sponsor";
|
|
16
17
|
readonly threshold: "$100+";
|
|
17
|
-
readonly className: "border-
|
|
18
|
+
readonly className: "border-supporter-platinum/60 bg-supporter-platinum/10 text-supporter-platinum";
|
|
18
19
|
}, {
|
|
19
20
|
readonly id: "diamond";
|
|
20
21
|
readonly Icon: import("lucide-react").LucideIcon;
|
|
21
22
|
readonly label: "Diamond Sponsor";
|
|
22
23
|
readonly threshold: "$250+";
|
|
23
|
-
readonly className: "border-
|
|
24
|
+
readonly className: "border-supporter-diamond/50 bg-supporter-diamond/10 text-supporter-diamond";
|
|
24
25
|
}];
|
|
25
26
|
export type ForgeSupporterTier = (typeof FORGE_SPONSOR_TIERS)[number]['id'];
|
|
26
27
|
/** A supporter tier as a small pill, e.g. under a username. */
|
package/dist/support-button.js
CHANGED
|
@@ -5,28 +5,29 @@ import { Award, Gem, Heart, Star, X } from 'lucide-react';
|
|
|
5
5
|
export const BUY_ME_A_COFFEE_URL = 'https://www.buymeacoffee.com/xtherebellion';
|
|
6
6
|
/** The apps' CSP `img-src` must allow https://cdn.buymeacoffee.com for this to load. */
|
|
7
7
|
export const BUY_ME_A_COFFEE_BUTTON = 'https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png';
|
|
8
|
-
/** Supporter badge tiers, shared by every xForge app. `id` matches `profiles.supporter_tier`.
|
|
8
|
+
/** Supporter badge tiers, shared by every xForge app. `id` matches `profiles.supporter_tier`. Colours are the
|
|
9
|
+
* per-mode `--supporter-*` tokens in tokens.css (fixed, never the accent). */
|
|
9
10
|
export const FORGE_SPONSOR_TIERS = [
|
|
10
11
|
{
|
|
11
12
|
id: 'gold',
|
|
12
13
|
Icon: Star,
|
|
13
14
|
label: 'Gold Sponsor',
|
|
14
15
|
threshold: '$10+',
|
|
15
|
-
className: 'border-
|
|
16
|
+
className: 'border-supporter-gold/50 bg-supporter-gold/10 text-supporter-gold',
|
|
16
17
|
},
|
|
17
18
|
{
|
|
18
19
|
id: 'platinum',
|
|
19
20
|
Icon: Award,
|
|
20
21
|
label: 'Platinum Sponsor',
|
|
21
22
|
threshold: '$100+',
|
|
22
|
-
className: 'border-
|
|
23
|
+
className: 'border-supporter-platinum/60 bg-supporter-platinum/10 text-supporter-platinum',
|
|
23
24
|
},
|
|
24
25
|
{
|
|
25
26
|
id: 'diamond',
|
|
26
27
|
Icon: Gem,
|
|
27
28
|
label: 'Diamond Sponsor',
|
|
28
29
|
threshold: '$250+',
|
|
29
|
-
className: 'border-
|
|
30
|
+
className: 'border-supporter-diamond/50 bg-supporter-diamond/10 text-supporter-diamond',
|
|
30
31
|
},
|
|
31
32
|
];
|
|
32
33
|
/** A supporter tier as a small pill, e.g. under a username. */
|
package/dist/tokens.css
CHANGED
|
@@ -56,6 +56,10 @@
|
|
|
56
56
|
--color-divider: var(--divider);
|
|
57
57
|
--color-chip: var(--chip);
|
|
58
58
|
--color-track: var(--track);
|
|
59
|
+
--color-supporter-gold: var(--supporter-gold);
|
|
60
|
+
--color-supporter-platinum: var(--supporter-platinum);
|
|
61
|
+
--color-supporter-diamond: var(--supporter-diamond);
|
|
62
|
+
--color-discord-text: var(--discord-text);
|
|
59
63
|
|
|
60
64
|
--radius-lg: var(--radius);
|
|
61
65
|
--radius-md: calc(var(--radius) - 2px);
|
|
@@ -93,6 +97,12 @@
|
|
|
93
97
|
--divider: hsl(0 0% 92%);
|
|
94
98
|
--chip: hsl(0 0% 93%);
|
|
95
99
|
--track: hsl(0 0% 89.8%);
|
|
100
|
+
/* Fixed colours that keep their meaning whatever the accent: supporter tiers (ForgeSupporterBadge) and the
|
|
101
|
+
Discord name. Text at 4.5:1 on --background and --card in each mode, also over the tier's own 10% fill. */
|
|
102
|
+
--supporter-gold: hsl(40 96% 27%);
|
|
103
|
+
--supporter-platinum: hsl(215 19% 35%);
|
|
104
|
+
--supporter-diamond: hsl(192 82% 29%);
|
|
105
|
+
--discord-text: hsl(235 75% 57%);
|
|
96
106
|
--radius: 0.5rem;
|
|
97
107
|
/* Explicit Tailwind v4 utility vars (ensures runtime overrides work) */
|
|
98
108
|
--color-primary: hsl(38 100% 65%);
|
|
@@ -126,6 +136,10 @@
|
|
|
126
136
|
--divider: hsl(0 0% 17%);
|
|
127
137
|
--chip: hsl(0 0% 19%);
|
|
128
138
|
--track: hsl(0 0% 18%);
|
|
139
|
+
--supporter-gold: hsl(45 93% 47%);
|
|
140
|
+
--supporter-platinum: hsl(213 27% 84%);
|
|
141
|
+
--supporter-diamond: hsl(188 86% 53%);
|
|
142
|
+
--discord-text: hsl(235 86% 75%);
|
|
129
143
|
--color-primary: hsl(38 100% 65%);
|
|
130
144
|
--color-primary-foreground: hsl(38 100% 10%);
|
|
131
145
|
--color-ring: hsl(38 100% 65%);
|