astrogators-shared-ui 0.6.0 → 0.6.2

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 CHANGED
@@ -1,172 +1,115 @@
1
- # @psytor/astrogators-shared-ui
1
+ # astrogators-shared-ui
2
2
 
3
- Shared UI components and utilities for Astrogator's Table applications.
3
+ Shared React components, auth, and API client for the Astrogator's Table
4
+ frontends (`astrogators-hub`, `mod-ledger-ui`, `nightwatcher-ui`). Published
5
+ to the public npm registry as `astrogators-shared-ui` (unscoped).
6
+
7
+ > The git repo lives under `psytor/astrogators-shared-ui` on GitHub, but the
8
+ > package itself is published to **npmjs.org**, not GitHub Packages. No
9
+ > `.npmrc` scope config is needed to install.
10
+
11
+ This is a library — there is no app shell here. See `PUBLISHING.md` for the
12
+ release flow, `CHANGELOG.md` for what shipped in each version, and
13
+ `CLAUDE.md` for architecture notes.
4
14
 
5
15
  ## Installation
6
16
 
7
17
  ```bash
8
- npm install @psytor/astrogators-shared-ui
18
+ npm install astrogators-shared-ui
9
19
  ```
10
20
 
11
- ## Setup
12
-
13
- ### 1. Initialize API Client
21
+ Peer dependencies: `react` and `react-dom` (18 or 19).
14
22
 
15
- In your application's entry point (e.g., `main.tsx` or `App.tsx`):
16
-
17
- ```tsx
18
- import { initializeApiClient } from '@psytor/astrogators-shared-ui';
23
+ ## Setup
19
24
 
20
- initializeApiClient({
21
- baseURL: 'http://localhost:8000', // Your backend API URL
22
- onUnauthorized: () => {
23
- // Handle unauthorized (e.g., redirect to login)
24
- window.location.href = '/login';
25
- },
26
- });
27
- ```
25
+ ### 1. Wrap the app in `AuthProvider`
28
26
 
29
- ### 2. Wrap Application with AuthProvider
27
+ `AuthProvider` initializes the API client with the given `apiBaseUrl` and
28
+ manages auth, feature flags, and ally codes for the whole app. Pass the
29
+ **prefixed** backend URL — workspace backends mount their routes under
30
+ `/<service-name>` (see the workspace `CLAUDE.md`).
30
31
 
31
32
  ```tsx
32
- import { AuthProvider } from '@psytor/astrogators-shared-ui';
33
+ import { AuthProvider } from 'astrogators-shared-ui';
34
+ import 'astrogators-shared-ui/styles';
33
35
 
34
36
  function App() {
35
37
  return (
36
- <AuthProvider>
37
- {/* Your app content */}
38
+ <AuthProvider apiBaseUrl={import.meta.env.VITE_API_BASE_URL}>
39
+ {/* your app */}
38
40
  </AuthProvider>
39
41
  );
40
42
  }
41
43
  ```
42
44
 
43
- ### 3. Import Global Styles
44
-
45
- ```tsx
46
- import '@psytor/astrogators-shared-ui/styles';
47
- ```
48
-
49
- ## Components
45
+ `VITE_API_BASE_URL` looks like `http://localhost:8000/astrogators-table` in
46
+ dev.
50
47
 
51
- ### Layout Components
48
+ ### 2. (Optional) Reconfigure the API client
52
49
 
53
- #### TopBar
50
+ `AuthProvider` already calls `initializeApiClient`. Call it yourself only if
51
+ you need a custom `onUnauthorized` handler (e.g. router-driven redirects):
54
52
 
55
53
  ```tsx
56
- import { TopBar } from '@psytor/astrogators-shared-ui';
54
+ import { initializeApiClient } from 'astrogators-shared-ui';
57
55
 
58
- <TopBar
59
- logo={<div>Astrogator's Table</div>}
60
- rightContent={<button>Login</button>}
61
- />
56
+ initializeApiClient({
57
+ baseURL: import.meta.env.VITE_API_BASE_URL,
58
+ onUnauthorized: () => navigate('/login'),
59
+ });
62
60
  ```
63
61
 
64
- #### Container
65
-
66
- ```tsx
67
- import { Container } from '@psytor/astrogators-shared-ui';
62
+ ## Components
68
63
 
69
- <Container maxWidth="lg" padding>
70
- {/* Content */}
71
- </Container>
72
- ```
64
+ All components are styled via CSS Modules and the global design tokens in
65
+ `./styles`. Override the design system by redefining CSS variables on `:root`
66
+ (see "Theming").
73
67
 
74
- #### Footer
68
+ ### Layout
75
69
 
76
70
  ```tsx
77
- import { Footer } from '@psytor/astrogators-shared-ui';
71
+ import { TopBar, Container, Footer } from 'astrogators-shared-ui';
78
72
 
73
+ <TopBar logo={<Logo />} rightContent={<UserMenu />} />
74
+ <Container maxWidth="lg" padding>{children}</Container>
79
75
  <Footer />
80
76
  ```
81
77
 
82
- ### Form Components
83
-
84
- #### Button
78
+ ### Forms
85
79
 
86
80
  ```tsx
87
- import { Button } from '@psytor/astrogators-shared-ui';
81
+ import { Button, Input, Select, AllyCodeDropdown } from 'astrogators-shared-ui';
88
82
 
89
- <Button variant="primary" size="md" onClick={handleClick}>
90
- Click Me
91
- </Button>
83
+ <Button variant="primary" size="md" loading={submitting}>Save</Button>
84
+ <Input label="Email" type="email" required error={errors.email} />
85
+ <Select label="Profile" options={profiles} placeholder="Choose…" />
92
86
 
93
- <Button variant="outline" loading>
94
- Loading...
95
- </Button>
87
+ // Wired into useAuth — manages the user's ally codes (DB-backed when
88
+ // authenticated, localStorage when anonymous):
89
+ <AllyCodeDropdown />
96
90
  ```
97
91
 
98
- #### Input
92
+ `Button` variants: `primary | secondary | outline | ghost | danger`.
99
93
 
100
- ```tsx
101
- import { Input } from '@psytor/astrogators-shared-ui';
102
-
103
- <Input
104
- label="Email"
105
- type="email"
106
- placeholder="Enter your email"
107
- required
108
- error={errors.email}
109
- />
110
- ```
111
-
112
- #### Select
94
+ ### Display
113
95
 
114
96
  ```tsx
115
- import { Select } from '@psytor/astrogators-shared-ui';
116
-
117
- <Select
118
- label="Choose Profile"
119
- options={[
120
- { value: 'standard', label: 'Standard' },
121
- { value: 'speed', label: 'Speed Focus' },
122
- ]}
123
- placeholder="Select a profile"
124
- />
125
- ```
126
-
127
- ### Display Components
128
-
129
- #### Card
130
-
131
- ```tsx
132
- import { Card } from '@psytor/astrogators-shared-ui';
97
+ import { Card, Badge, Modal } from 'astrogators-shared-ui';
133
98
 
134
99
  <Card variant="elevated" chamfered chamferSize="md" padding="lg" hoverable>
135
- <h3>The Mod Ledger</h3>
136
- <p>Analyze your mods</p>
100
+
137
101
  </Card>
138
- ```
139
-
140
- #### Badge
141
-
142
- ```tsx
143
- import { Badge } from '@psytor/astrogators-shared-ui';
144
102
 
145
103
  <Badge variant="success">Active</Badge>
146
104
  <Badge variant="warning" size="sm">Beta</Badge>
147
- ```
148
105
 
149
- #### Modal
150
-
151
- ```tsx
152
- import { Modal } from '@psytor/astrogators-shared-ui';
153
-
154
- <Modal
155
- isOpen={isOpen}
156
- onClose={() => setIsOpen(false)}
157
- title="Login"
158
- size="md"
159
- >
160
- {/* Modal content */}
161
- </Modal>
106
+ <Modal isOpen={open} onClose={close} title="Login" size="md">…</Modal>
162
107
  ```
163
108
 
164
- ### Feedback Components
165
-
166
- #### Loader
109
+ ### Feedback
167
110
 
168
111
  ```tsx
169
- import { Loader } from '@psytor/astrogators-shared-ui';
112
+ import { Loader } from 'astrogators-shared-ui';
170
113
 
171
114
  <Loader size="md" variant="spinner" />
172
115
  <Loader size="lg" variant="dots" />
@@ -174,82 +117,96 @@ import { Loader } from '@psytor/astrogators-shared-ui';
174
117
 
175
118
  ## Authentication
176
119
 
177
- ### useAuth Hook
120
+ `useAuth` returns the full auth + ally-code surface. The hook must be called
121
+ inside `AuthProvider`.
178
122
 
179
123
  ```tsx
180
- import { useAuth } from '@psytor/astrogators-shared-ui';
124
+ import { useAuth } from 'astrogators-shared-ui';
181
125
 
182
- function MyComponent() {
183
- const { user, isAuthenticated, login, logout, isLoading } = useAuth();
126
+ const {
127
+ // session
128
+ user, isAuthenticated, isLoading,
129
+ login, register, logout, refreshUser,
130
+ forgotPassword, resetPassword, resendVerification,
184
131
 
185
- const handleLogin = async () => {
186
- try {
187
- await login({
188
- email: 'user@example.com',
189
- password: 'password',
190
- });
191
- } catch (error) {
192
- console.error('Login failed:', error);
193
- }
194
- };
132
+ // backend feature flags (e.g. auth_enabled)
133
+ authEnabled, isLoadingFeatures,
195
134
 
196
- if (isLoading) return <Loader />;
135
+ // ally codes DB-backed when logged in, localStorage when anonymous
136
+ allyCodes, selectedAllyCode, isLoadingAllyCodes,
137
+ fetchAllyCodes, addAllyCode, removeAllyCode,
138
+ selectAllyCode, updateAllyCodeLastUsed,
197
139
 
198
- return (
199
- <div>
200
- {isAuthenticated ? (
201
- <div>
202
- <p>Welcome, {user?.username}!</p>
203
- <button onClick={logout}>Logout</button>
204
- </div>
205
- ) : (
206
- <button onClick={handleLogin}>Login</button>
207
- )}
208
- </div>
209
- );
210
- }
140
+ // localStorage → DB migration prompt for users who sign up after
141
+ // adding ally codes anonymously
142
+ migrationPrompt, dismissMigrationPrompt, migrateLocalStorageCodes,
143
+ } = useAuth();
211
144
  ```
212
145
 
213
- ## API Client
146
+ Tokens are stored in `localStorage`. Registration does **not** auto-login —
147
+ it requires email verification.
214
148
 
215
- ### Using the API Client
149
+ ## API client
216
150
 
217
151
  ```tsx
218
- import { apiClient } from '@psytor/astrogators-shared-ui';
152
+ import { apiClient } from 'astrogators-shared-ui';
219
153
 
220
- // GET request
221
- const data = await apiClient.get('/api/v1/game-data/characters');
222
-
223
- // POST request
154
+ const characters = await apiClient.get('/api/v1/game-data/characters');
224
155
  const result = await apiClient.post('/api/v1/mod-ledger/evaluate/123456789', {
225
156
  profile_name: 'standard',
226
157
  });
227
158
  ```
228
159
 
229
- The API client automatically:
230
- - Injects JWT access token in Authorization header
231
- - Refreshes expired tokens
232
- - Retries failed requests after token refresh
233
- - Calls `onUnauthorized` callback on auth failure
160
+ The client:
161
+ - injects the access token into `Authorization`
162
+ - on `401`, transparently refreshes via `/api/v1/auth/refresh-token` and
163
+ retries the original request once
164
+ - calls `onUnauthorized` if refresh fails
165
+
166
+ Endpoints are written **without** the service prefix — the prefix lives in
167
+ the configured `baseURL`.
234
168
 
235
- ## TypeScript Types
169
+ ## Ally code utilities
236
170
 
237
- All TypeScript types are exported:
171
+ For UI that needs to format/validate the 9-digit SWGOH player IDs outside the
172
+ context of `useAuth`:
173
+
174
+ ```tsx
175
+ import {
176
+ formatAllyCode, // "123456789" → "123-456-789"
177
+ unformatAllyCode, // "123-456-789" → "123456789"
178
+ getAllyCodesFromStorage,
179
+ saveAllyCodeToStorage,
180
+ removeAllyCodeFromStorage,
181
+ getSelectedAllyCode,
182
+ setSelectedAllyCode,
183
+ clearAllyCodes,
184
+ } from 'astrogators-shared-ui';
185
+ ```
186
+
187
+ Prefer `useAuth` when you can — it keeps DB and localStorage in sync.
188
+
189
+ ## TypeScript
190
+
191
+ All public types are re-exported from the package root, including:
238
192
 
239
193
  ```tsx
240
194
  import type {
241
- User,
242
- LoginRequest,
243
- LoginResponse,
244
- ParsedMod,
245
- ModEvaluation,
246
- ApiError,
247
- } from '@psytor/astrogators-shared-ui';
195
+ User, LoginRequest, LoginResponse,
196
+ RegisterRequest, ForgotPasswordRequest, ResetPasswordRequest,
197
+ AllyCode, AllyCodeCreate, AllyCodeListResponse, StoredAllyCode,
198
+ ApiResponse, ApiError, PaginatedResponse,
199
+ ParsedMod, ModStat, ModEvaluation, EvaluationRequest, EvaluationResponse,
200
+ } from 'astrogators-shared-ui';
248
201
  ```
249
202
 
250
- ## CSS Variables
203
+ Consumers should set `"moduleResolution": "bundler"` (or `"node16"`) in
204
+ `tsconfig.json` so the bundled `.d.ts` files resolve.
251
205
 
252
- Customize the design system by overriding CSS variables:
206
+ ## Theming
207
+
208
+ Design tokens are CSS variables on `:root`. Override anything you need in
209
+ your own stylesheet, loaded after the library styles:
253
210
 
254
211
  ```css
255
212
  :root {
@@ -261,57 +218,35 @@ Customize the design system by overriding CSS variables:
261
218
  }
262
219
  ```
263
220
 
264
- ## Chamfered Boxes
221
+ ### Chamfered boxes
265
222
 
266
- Use the chamfered box effect on any element:
223
+ Sci-fi cut-corner effect, available as utility classes or via `Card`:
267
224
 
268
225
  ```tsx
269
- <div className="chamfered-box">
270
- {/* Content with cut corners */}
271
- </div>
272
-
273
- <div className="chamfered-box-lg">
274
- {/* Large chamfered corners */}
275
- </div>
226
+ <div className="chamfered-box">…</div>
227
+ <div className="chamfered-box-sm">…</div>
228
+ <div className="chamfered-box-lg">…</div>
276
229
 
277
- <div className="chamfered-box-sm">
278
- {/* Small chamfered corners */}
279
- </div>
280
- ```
281
-
282
- Or use the Card component:
283
-
284
- ```tsx
285
- <Card chamfered chamferSize="lg">
286
- {/* Content */}
287
- </Card>
230
+ <Card chamfered chamferSize="lg">…</Card>
288
231
  ```
289
232
 
290
233
  ## Development
291
234
 
292
235
  ```bash
293
- # Install dependencies
294
236
  npm install
295
-
296
- # Build library
297
- npm run build
298
-
299
- # Type check
300
- npm run type-check
237
+ npm run build # tsc && vite build → dist/
238
+ npm run type-check # tsc --noEmit
301
239
  ```
302
240
 
303
- ## Publishing
241
+ There is no `dev` server worth running (this is a library, not an app).
242
+ Iterate by rebuilding and reinstalling in a consumer.
304
243
 
305
- ```bash
306
- # Bump version
307
- npm version patch # or minor, or major
308
-
309
- # Build
310
- npm run build
311
-
312
- # Publish to GitHub Packages
313
- npm publish
314
- ```
244
+ See `PUBLISHING.md` for the release procedure. Two non-negotiable rules:
245
+ - **Always `npm run build` before `npm publish`** — `dist/` is gitignored
246
+ but is the only thing shipped, so skipping the build re-publishes stale
247
+ code.
248
+ - **`npm login` and `npm publish` must be run by the user**, not by Claude
249
+ — publishing needs interactive npm auth.
315
250
 
316
251
  ## License
317
252
 
package/dist/index.js CHANGED
@@ -211,14 +211,14 @@ var x = "astrogators_access_token", S = "astrogators_refresh_token", C = () => l
211
211
  async refreshAccessToken() {
212
212
  let e = E();
213
213
  if (!e) throw Error("No refresh token available");
214
- let t = await fetch(`${this.baseURL}/api/v1/auth/refresh-token`, {
214
+ let t = await fetch(`${this.baseURL}/api/v1/auth/refresh`, {
215
215
  method: "POST",
216
216
  headers: { "Content-Type": "application/json" },
217
217
  body: JSON.stringify({ refresh_token: e })
218
218
  });
219
219
  if (!t.ok) throw A(), this.onUnauthorized?.(), Error("Token refresh failed");
220
220
  let n = await t.json();
221
- return k(n.access_token, e), n.access_token;
221
+ return k(n.access_token, n.refresh_token), n.access_token;
222
222
  }
223
223
  async request(e, t = {}) {
224
224
  let n = C(), r = {
@@ -417,29 +417,41 @@ var x = "astrogators_access_token", S = "astrogators_refresh_token", C = () => l
417
417
  y(!1);
418
418
  }
419
419
  }, [r]), I = n(async (e) => {
420
+ r && typeof e == "number" ? (await N.put(`/api/v1/users/me/ally-codes/${e}/use`, {}), await F()) : !r && typeof e == "string" && (B(e), h(L()));
421
+ }, [r, F]), G = n((e) => {
422
+ if (_(e), H(e), e && r) {
423
+ let t = m.find((t) => "ally_code" in t && t.ally_code === e);
424
+ t && "id" in t && I(t.id);
425
+ } else e && !r && B(e);
426
+ }, [
427
+ r,
428
+ m,
429
+ I
430
+ ]), K = n(async (e) => {
420
431
  if (!/^\d{9}$/.test(e)) throw Error("Ally code must be exactly 9 digits");
421
432
  if (r) {
422
433
  let t = await N.post("/api/v1/users/me/ally-codes", { ally_code: e });
423
- h((e) => [t, ...e]), m.length === 0 && K(e);
434
+ h((e) => [t, ...e]), m.length === 0 && G(e);
424
435
  } else {
425
436
  let t = await N.get(`/api/v1/player-data/player/${e}`);
426
437
  if (!t) throw Error("Invalid ally code - player not found");
427
438
  R({
428
439
  ally_code: e,
429
- player_name: t?.data?.name || null,
440
+ player_name: t.data?.name ?? null,
430
441
  last_used_at: (/* @__PURE__ */ new Date()).toISOString()
431
- }), h(L()), m.length === 0 && K(e);
442
+ }), h(L()), m.length === 0 && G(e);
432
443
  }
433
- }, [r, m.length]), G = n(async (e) => {
434
- r && typeof e == "number" ? (await N.delete(`/api/v1/users/me/ally-codes/${e}`), h((t) => t.filter((t) => "id" in t && t.id !== e))) : !r && typeof e == "string" && (z(e), h(L()), g === e && K(null));
435
- }, [r, g]), K = n((e) => {
436
- if (_(e), H(e), e && r) {
437
- let t = m.find((t) => "ally_code" in t && t.ally_code === e);
438
- t && "id" in t && q(t.id);
439
- } else e && !r && B(e);
440
- }, [r, m]), q = n(async (e) => {
441
- r && typeof e == "number" ? (await N.put(`/api/v1/users/me/ally-codes/${e}/use`, {}), await F()) : !r && typeof e == "string" && (B(e), h(L()));
442
- }, [r, F]), J = n(() => {
444
+ }, [
445
+ r,
446
+ m.length,
447
+ G
448
+ ]), q = n(async (e) => {
449
+ r && typeof e == "number" ? (await N.delete(`/api/v1/users/me/ally-codes/${e}`), h((t) => t.filter((t) => "id" in t && t.id !== e))) : !r && typeof e == "string" && (z(e), h(L()), g === e && G(null));
450
+ }, [
451
+ r,
452
+ g,
453
+ G
454
+ ]), J = n(() => {
443
455
  x({
444
456
  show: !1,
445
457
  localStorageCodes: []
@@ -448,14 +460,14 @@ var x = "astrogators_access_token", S = "astrogators_refresh_token", C = () => l
448
460
  if (!r) return;
449
461
  let e = L();
450
462
  for (let t of e) try {
451
- await I(t.ally_code);
463
+ await K(t.ally_code);
452
464
  } catch (e) {
453
465
  console.error(`Failed to migrate ally code ${t.ally_code}:`, e);
454
466
  }
455
467
  U(), J();
456
468
  }, [
457
469
  r,
458
- I,
470
+ K,
459
471
  J
460
472
  ]);
461
473
  i(() => {
@@ -478,10 +490,10 @@ var x = "astrogators_access_token", S = "astrogators_refresh_token", C = () => l
478
490
  selectedAllyCode: g,
479
491
  isLoadingAllyCodes: v,
480
492
  fetchAllyCodes: F,
481
- addAllyCode: I,
482
- removeAllyCode: G,
483
- selectAllyCode: K,
484
- updateAllyCodeLastUsed: q,
493
+ addAllyCode: K,
494
+ removeAllyCode: q,
495
+ selectAllyCode: G,
496
+ updateAllyCodeLastUsed: I,
485
497
  migrationPrompt: b,
486
498
  dismissMigrationPrompt: J,
487
499
  migrateLocalStorageCodes: Y
@@ -37,7 +37,9 @@ export interface RefreshTokenRequest {
37
37
  }
38
38
  export interface RefreshTokenResponse {
39
39
  access_token: string;
40
+ refresh_token: string;
40
41
  token_type: string;
42
+ expires_in: number;
41
43
  }
42
44
  export interface ForgotPasswordRequest {
43
45
  email: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astrogators-shared-ui",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Shared UI components and utilities for Astrogator's Table applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",