astrogators-shared-ui
v0.21.0
Published
Shared UI components and utilities for Astrogator's Table applications
Maintainers
Readme
astrogators-shared-ui
Shared React components, auth, and API client for the Astrogator's Table
frontends (astrogators-hub, mod-ledger-ui, nightwatcher-ui,
navicharts-ui). Published to the public npm registry as
astrogators-shared-ui (unscoped).
The git repo lives under
psytor/astrogators-shared-uion GitHub, but the package itself is published to npmjs.org, not GitHub Packages. No.npmrcscope config is needed to install.
This is a library — there is no app shell here. See PUBLISHING.md for the
release flow, CHANGELOG.md for what shipped in each version, and
CLAUDE.md for architecture notes.
Installation
npm install astrogators-shared-uiPeer dependencies: react and react-dom (18 or 19).
Setup
1. Wrap the app in AuthProvider
AuthProvider initializes the API client with the given apiBaseUrl and
manages auth, feature flags, and ally codes for the whole app. Pass the
prefixed backend URL — workspace backends mount their routes under
/<service-name> (see the workspace CLAUDE.md).
import { AuthProvider } from 'astrogators-shared-ui';
import 'astrogators-shared-ui/styles';
function App() {
return (
<AuthProvider apiBaseUrl={import.meta.env.VITE_API_BASE_URL}>
{/* your app */}
</AuthProvider>
);
}VITE_API_BASE_URL looks like http://localhost:8000/astrogators-table in
dev.
2. (Optional) Reconfigure the API client
AuthProvider already calls initializeApiClient. Call it yourself only if
you need a custom onUnauthorized handler (e.g. router-driven redirects):
import { initializeApiClient } from 'astrogators-shared-ui';
initializeApiClient({
baseURL: import.meta.env.VITE_API_BASE_URL,
onUnauthorized: () => navigate('/login'),
});Components
All components are styled via CSS Modules and the global design tokens in
./styles. Override the design system by redefining CSS variables on :root
(see "Theming").
Layout
NavBar is the suite-wide top bar — every app renders it, unmodified, with
no per-app name/tabs/logo of its own. Its contents come from the exported
SUITE_NAV manifest, not from props:
import { NavBar, Container, Footer } from 'astrogators-shared-ui';
<NavBar
currentApp="mod-ledger"
activeSectionId="evaluations"
onNavigate={(section, event) => {
// fires only for sections belonging to currentApp; cross-app links are
// always full page loads. Call event.preventDefault() to soft-navigate
// with your own router instead.
event.preventDefault();
navigate(section.href.replace('/mod-ledger', ''));
}}
rightExtras={<RosterRefresh lastSynced={lastSynced} onRefresh={sync} />}
/>
<Container maxWidth="lg" padding>{children}</Container>
<Footer />TopBar underneath is the dumb, unstyled-by-apps primitive — consumers
render NavBar, not TopBar, directly.
Forms
import { Button, Input, Select, AllyCodeDropdown } from 'astrogators-shared-ui';
<Button variant="primary" size="md" loading={submitting}>Save</Button>
<Input label="Email" type="email" required error={errors.email} />
<Select label="Profile" options={profiles} placeholder="Choose…" />
// Wired into useAuth — manages the user's ally codes (DB-backed when
// authenticated, localStorage when anonymous):
<AllyCodeDropdown />Button variants: primary | secondary | outline | ghost | danger.
Display
import { Card, Badge, Modal } from 'astrogators-shared-ui';
<Card variant="elevated" chamfered chamferSize="md" padding="lg" hoverable>
…
</Card>
<Badge variant="success">Active</Badge>
<Badge variant="warning" size="sm">Beta</Badge>
<Modal isOpen={open} onClose={close} title="Login" size="md">…</Modal>Feedback
import { Loader } from 'astrogators-shared-ui';
<Loader size="md" variant="spinner" />
<Loader size="lg" variant="dots" />Authentication
useAuth returns the full auth + ally-code surface. The hook must be called
inside AuthProvider.
import { useAuth } from 'astrogators-shared-ui';
const {
// session
user, isAuthenticated, isLoading,
login, register, logout, refreshUser,
forgotPassword, resetPassword, resendVerification,
// backend feature flags (e.g. auth_enabled)
authEnabled, isLoadingFeatures,
// ally codes — DB-backed when logged in, localStorage when anonymous
allyCodes, selectedAllyCode, isLoadingAllyCodes,
fetchAllyCodes, addAllyCode, removeAllyCode,
selectAllyCode, updateAllyCodeLastUsed,
// localStorage → DB migration prompt for users who sign up after
// adding ally codes anonymously
migrationPrompt, dismissMigrationPrompt, migrateLocalStorageCodes,
} = useAuth();Tokens are stored in localStorage. Registration does not auto-login —
it requires email verification.
API client
import { apiClient } from 'astrogators-shared-ui';
const characters = await apiClient.get('/api/v1/game-data/characters');
const result = await apiClient.post('/api/v1/mod-ledger/evaluate/123456789', {
profile_name: 'standard',
});The client:
- injects the access token into
Authorization - proactively refreshes before a request goes out if the access token is
expired or expiring within 30s (decoded client-side), and reactively on a
401from the resource server — both via/api/v1/auth/refresh, retrying the original request once - calls
onUnauthorizedif refresh fails - retries a
502/503/504response (idempotent methods only) or a thrownfetch(any method, since nothing reached the server) a couple of times with a short backoff, to ride out a backend container swap mid-deploy
Endpoints are written without the service prefix — the prefix lives in
the configured baseURL.
Ally code utilities
For UI that needs to format/validate the 9-digit SWGOH player IDs outside the
context of useAuth:
import {
formatAllyCode, // "123456789" → "123-456-789"
unformatAllyCode, // "123-456-789" → "123456789"
getAllyCodesFromStorage,
saveAllyCodeToStorage,
removeAllyCodeFromStorage,
getSelectedAllyCode,
setSelectedAllyCode,
clearAllyCodes,
} from 'astrogators-shared-ui';Prefer useAuth when you can — it keeps DB and localStorage in sync.
TypeScript
All public types are re-exported from the package root, including:
import type {
User, LoginRequest, LoginResponse,
RegisterRequest, ForgotPasswordRequest, ResetPasswordRequest,
AllyCode, AllyCodeCreate, AllyCodeListResponse, StoredAllyCode,
ApiResponse, ApiError, PaginatedResponse,
ParsedMod, ModStat, ModEvaluation, EvaluationRequest, EvaluationResponse,
} from 'astrogators-shared-ui';Consumers should set "moduleResolution": "bundler" (or "node16") in
tsconfig.json so the bundled .d.ts files resolve.
Theming
Design tokens are CSS variables on :root. Override anything you need in
your own stylesheet, loaded after the library styles:
:root {
--color-primary: #1a73e8;
--color-secondary: #34a853;
--font-primary: 'Your Font', sans-serif;
--spacing-md: 1rem;
--chamfer-size: 8px;
}Chamfered boxes
Sci-fi cut-corner effect, available as utility classes or via Card:
<div className="chamfered-box">…</div>
<div className="chamfered-box-sm">…</div>
<div className="chamfered-box-lg">…</div>
<Card chamfered chamferSize="lg">…</Card>Development
npm install
npm run build # tsc && vite build → dist/
npm run type-check # tsc --noEmit
npm test # vitest run — logic tests in tests/ (not visual)There is no dev server worth running (this is a library, not an app).
Iterate by rebuilding and reinstalling in a consumer.
See PUBLISHING.md for the release procedure. Two non-negotiable rules:
- Always
npm run buildbeforenpm publish—dist/is gitignored but is the only thing shipped, so skipping the build re-publishes stale code. npm loginandnpm publishmust be run by the user, not by Claude — publishing needs interactive npm auth.
License
MIT
