astrogators-shared-ui 0.16.3 → 0.17.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 psytor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # astrogators-shared-ui
2
2
 
3
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).
4
+ frontends (`astrogators-hub`, `mod-ledger-ui`, `nightwatcher-ui`,
5
+ `navicharts-ui`). Published to the public npm registry as
6
+ `astrogators-shared-ui` (unscoped).
6
7
 
7
8
  > The git repo lives under `psytor/astrogators-shared-ui` on GitHub, but the
8
9
  > package itself is published to **npmjs.org**, not GitHub Packages. No
@@ -177,9 +178,14 @@ const result = await apiClient.post('/api/v1/mod-ledger/evaluate/123456789', {
177
178
 
178
179
  The client:
179
180
  - injects the access token into `Authorization`
180
- - on `401`, transparently refreshes via `/api/v1/auth/refresh-token` and
181
- retries the original request once
181
+ - proactively refreshes before a request goes out if the access token is
182
+ expired or expiring within 30s (decoded client-side), and reactively on a
183
+ `401` from the resource server — both via `/api/v1/auth/refresh`, retrying
184
+ the original request once
182
185
  - calls `onUnauthorized` if refresh fails
186
+ - retries a `502`/`503`/`504` response (idempotent methods only) or a thrown
187
+ `fetch` (any method, since nothing reached the server) a couple of times
188
+ with a short backoff, to ride out a backend container swap mid-deploy
183
189
 
184
190
  Endpoints are written **without** the service prefix — the prefix lives in
185
191
  the configured `baseURL`.
@@ -254,6 +260,7 @@ Sci-fi cut-corner effect, available as utility classes or via `Card`:
254
260
  npm install
255
261
  npm run build # tsc && vite build → dist/
256
262
  npm run type-check # tsc --noEmit
263
+ npm test # vitest run — logic tests in tests/ (not visual)
257
264
  ```
258
265
 
259
266
  There is no `dev` server worth running (this is a library, not an app).
package/dist/index.js CHANGED
@@ -140,9 +140,9 @@ var _ = [
140
140
  x(e), w(t);
141
141
  }, D = () => {
142
142
  S(), T();
143
- }, O = () => !!b(), ee = "", k, A = null, te = (e) => {
144
- ee = e.baseURL, k = e.onUnauthorized;
145
- }, j = () => A || (A = (async () => {
143
+ }, O = () => !!b(), ee = "", te, k = null, A = (e) => {
144
+ ee = e.baseURL, te = e.onUnauthorized;
145
+ }, j = () => k || (k = (async () => {
146
146
  let e = C();
147
147
  if (!e) throw Error("No refresh token available");
148
148
  let t = await fetch(`${ee}/api/v1/auth/refresh`, {
@@ -150,22 +150,22 @@ var _ = [
150
150
  headers: { "Content-Type": "application/json" },
151
151
  body: JSON.stringify({ refresh_token: e })
152
152
  });
153
- if (!t.ok) throw D(), k?.(), Error("Token refresh failed");
153
+ if (!t.ok) throw D(), te?.(), Error("Token refresh failed");
154
154
  let n = await t.json();
155
155
  return E(n.access_token, n.refresh_token), n.access_token;
156
156
  })().finally(() => {
157
- A = null;
158
- }), A), ne = 30, M = [700, 1500], N = new Set([
157
+ k = null;
158
+ }), k), ne = 30, M = [700, 1500], N = /* @__PURE__ */ new Set([
159
159
  502,
160
160
  503,
161
161
  504
162
- ]), re = (e) => {
162
+ ]), P = (e) => {
163
163
  let t = (e ?? "GET").toUpperCase();
164
164
  return t === "GET" || t === "HEAD" || t === "OPTIONS";
165
- }, P = (e) => e == null || typeof e == "string", ie = (e) => new Promise((t) => setTimeout(t, e)), ae = async (e, t) => {
166
- let n = re(t.method) && P(t.body), r = P(t.body), i;
165
+ }, F = (e) => e == null || typeof e == "string", re = (e) => new Promise((t) => setTimeout(t, e)), I = async (e, t) => {
166
+ let n = P(t.method) && F(t.body), r = F(t.body), i;
167
167
  for (let a = 0; a <= M.length; a++) {
168
- a > 0 && await ie(M[a - 1]);
168
+ a > 0 && await re(M[a - 1]);
169
169
  let o = a < M.length;
170
170
  try {
171
171
  let r = await fetch(e, t);
@@ -177,39 +177,39 @@ var _ = [
177
177
  }
178
178
  }
179
179
  throw i ?? /* @__PURE__ */ Error("fetchWithRetry: retries exhausted");
180
- }, oe = (e) => {
180
+ }, ie = (e) => {
181
181
  try {
182
182
  let t = e.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"), { exp: n } = JSON.parse(atob(t));
183
183
  return typeof n == "number" && Date.now() / 1e3 >= n - ne;
184
184
  } catch {
185
185
  return !1;
186
186
  }
187
- }, se = async (e, t = {}) => {
187
+ }, ae = async (e, t = {}) => {
188
188
  let n = b();
189
- if (n && oe(n)) try {
189
+ if (n && ie(n)) try {
190
190
  n = await j();
191
191
  } catch {
192
192
  n = b();
193
193
  }
194
194
  let r = { ...t.headers };
195
195
  n && (r.Authorization = `Bearer ${n}`);
196
- let i = await ae(e, {
196
+ let i = await I(e, {
197
197
  ...t,
198
198
  headers: r
199
199
  });
200
200
  if (i.status !== 401 || !n) return i;
201
201
  try {
202
- return r.Authorization = `Bearer ${await j()}`, await ae(e, {
202
+ return r.Authorization = `Bearer ${await j()}`, await I(e, {
203
203
  ...t,
204
204
  headers: r
205
205
  });
206
206
  } catch {
207
207
  return i;
208
208
  }
209
- }, ce = class {
209
+ }, oe = class {
210
210
  baseURL;
211
211
  constructor(e) {
212
- this.baseURL = e.baseURL, te({
212
+ this.baseURL = e.baseURL, A({
213
213
  baseURL: e.baseURL,
214
214
  onUnauthorized: e.onUnauthorized
215
215
  });
@@ -218,7 +218,7 @@ var _ = [
218
218
  let n = {
219
219
  "Content-Type": "application/json",
220
220
  ...t.headers
221
- }, r = await se(`${this.baseURL}${e}`, {
221
+ }, r = await ae(`${this.baseURL}${e}`, {
222
222
  ...t,
223
223
  headers: n
224
224
  });
@@ -268,53 +268,57 @@ var _ = [
268
268
  method: "DELETE"
269
269
  });
270
270
  }
271
- }, F, le = (e) => {
272
- F = new ce(e);
273
- }, I = "astrogators_ally_codes", ue = "astrogators_selected_ally_code", L = () => {
271
+ }, L, se = (e) => {
272
+ L = new oe(e);
273
+ }, ce = async (e) => {
274
+ let t = await L.patch("/api/v1/users/me", e);
275
+ return t.tokens && E(t.tokens.access_token, t.tokens.refresh_token), t;
276
+ }, le = async () => (await L.post("/api/v1/auth/logout-all")).message, R = "astrogators_ally_codes", ue = "astrogators_selected_ally_code", z = () => {
274
277
  try {
275
- let e = localStorage.getItem(I);
278
+ let e = localStorage.getItem(R);
276
279
  return e ? JSON.parse(e) : [];
277
280
  } catch (e) {
278
281
  return console.error("Failed to read ally codes from localStorage:", e), [];
279
282
  }
280
283
  }, de = (e) => {
281
284
  try {
282
- let t = L();
285
+ let t = z();
283
286
  if (t.some((t) => t.ally_code === e.ally_code)) return;
284
- t.push(e), localStorage.setItem(I, JSON.stringify(t));
287
+ t.push(e), localStorage.setItem(R, JSON.stringify(t));
285
288
  } catch (e) {
286
289
  console.error("Failed to save ally code to localStorage:", e);
287
290
  }
288
291
  }, fe = (e) => {
289
292
  try {
290
- let t = L().filter((t) => t.ally_code !== e);
291
- localStorage.setItem(I, JSON.stringify(t));
293
+ let t = z().filter((t) => t.ally_code !== e);
294
+ localStorage.setItem(R, JSON.stringify(t));
292
295
  } catch (e) {
293
296
  console.error("Failed to remove ally code from localStorage:", e);
294
297
  }
295
298
  }, pe = (e) => {
296
299
  try {
297
- let t = L().map((t) => t.ally_code === e ? {
300
+ let t = z().map((t) => t.ally_code === e ? {
298
301
  ...t,
299
302
  last_used_at: (/* @__PURE__ */ new Date()).toISOString()
300
303
  } : t);
301
- localStorage.setItem(I, JSON.stringify(t));
304
+ localStorage.setItem(R, JSON.stringify(t));
302
305
  } catch (e) {
303
306
  console.error("Failed to update ally code last used:", e);
304
307
  }
305
- }, me = () => localStorage.getItem(ue), R = (e) => {
308
+ }, me = () => localStorage.getItem(ue), B = (e) => {
306
309
  e ? localStorage.setItem(ue, e) : localStorage.removeItem(ue);
307
310
  }, he = () => {
308
- localStorage.removeItem(I), localStorage.removeItem(ue);
311
+ localStorage.removeItem(R), localStorage.removeItem(ue);
309
312
  }, ge = t(void 0), _e = ({ children: e, apiBaseUrl: t }) => {
310
313
  let [n, i] = s(null), [o, c] = s(!0), [u, d] = s(!0), [f, p] = s(!0), [m, h] = s([]), [g, _] = s(null), [v, y] = s(!1), [b, x] = s({
311
314
  show: !1,
312
315
  localStorageCodes: []
313
316
  });
314
- le({ baseURL: t }), a(() => {
317
+ se({ baseURL: t }), a(() => {
315
318
  (async () => {
316
319
  try {
317
- d((await F.get("/api/v1/config/features")).auth_enabled);
320
+ let e = await L.get("/api/v1/config/features");
321
+ d(e.auth_enabled);
318
322
  } catch (e) {
319
323
  console.error("Failed to fetch features:", e), d(!0);
320
324
  } finally {
@@ -328,7 +332,8 @@ var _ = [
328
332
  return;
329
333
  }
330
334
  try {
331
- i(await F.get("/api/v1/users/me"));
335
+ let e = await L.get("/api/v1/users/me");
336
+ i(e);
332
337
  } catch (e) {
333
338
  console.error("Failed to fetch user:", e), i(null), D();
334
339
  } finally {
@@ -340,122 +345,133 @@ var _ = [
340
345
  }, [S]);
341
346
  let C = async (e) => {
342
347
  try {
343
- let t = await F.post("/api/v1/auth/login", e);
344
- E(t.access_token, t.refresh_token), i(await F.get("/api/v1/users/me"));
348
+ let t = await L.post("/api/v1/auth/login", e);
349
+ E(t.access_token, t.refresh_token);
350
+ let n = await L.get("/api/v1/users/me");
351
+ i(n);
345
352
  } catch (e) {
346
353
  throw console.error("Login failed:", e), e;
347
354
  }
348
355
  }, w = async (e) => {
349
356
  try {
350
- await F.post("/api/v1/auth/register", e);
357
+ await L.post("/api/v1/auth/register", e);
351
358
  } catch (e) {
352
359
  throw console.error("Registration failed:", e), e;
353
360
  }
354
361
  }, T = () => {
355
- D(), i(null), h([]), _(null), R(null), x({
362
+ D(), i(null), h([]), _(null), B(null), x({
356
363
  show: !1,
357
364
  localStorageCodes: []
358
365
  });
359
366
  }, ee = async () => {
360
367
  await S();
368
+ }, te = async () => {
369
+ try {
370
+ await le();
371
+ } finally {
372
+ T();
373
+ }
361
374
  }, k = async (e) => {
375
+ let t = await ce(e);
376
+ return i(t), t;
377
+ }, A = async (e) => {
362
378
  try {
363
- return (await F.post("/api/v1/auth/forgot-password", e)).message;
379
+ return (await L.post("/api/v1/auth/forgot-password", e)).message;
364
380
  } catch (e) {
365
381
  throw console.error("Forgot password failed:", e), e;
366
382
  }
367
- }, A = async (e) => {
383
+ }, j = async (e) => {
368
384
  try {
369
- return (await F.post("/api/v1/auth/reset-password", e)).message;
385
+ return (await L.post("/api/v1/auth/reset-password", e)).message;
370
386
  } catch (e) {
371
387
  throw console.error("Reset password failed:", e), e;
372
388
  }
373
- }, te = async (e) => {
389
+ }, ne = async (e) => {
374
390
  try {
375
- return (await F.post("/api/v1/auth/resend-verification", e)).message;
391
+ return (await L.post("/api/v1/auth/resend-verification", e)).message;
376
392
  } catch (e) {
377
393
  throw console.error("Resend verification failed:", e), e;
378
394
  }
379
- }, j = r(async () => {
395
+ }, M = r(async () => {
380
396
  y(!0);
381
397
  try {
382
398
  let e = [];
383
399
  if (n) {
384
- let t = await F.get("/api/v1/users/me/ally-codes");
400
+ let t = await L.get("/api/v1/users/me/ally-codes");
385
401
  h(t.ally_codes), e = t.ally_codes.map((e) => e.ally_code);
386
- let n = L();
402
+ let n = z();
387
403
  n.length > 0 && x({
388
404
  show: !0,
389
405
  localStorageCodes: n.map((e) => e.ally_code)
390
406
  });
391
407
  } else {
392
- let t = L();
408
+ let t = z();
393
409
  h(t), e = t.map((e) => e.ally_code);
394
410
  }
395
411
  let t = me(), r = t && e.includes(t) ? t : null;
396
- _(r), r !== t && R(r);
412
+ _(r), r !== t && B(r);
397
413
  } catch (e) {
398
414
  console.error("Failed to fetch ally codes:", e), h([]);
399
415
  } finally {
400
416
  y(!1);
401
417
  }
402
- }, [n]), ne = r(async (e) => {
403
- n && typeof e == "number" ? (await F.put(`/api/v1/users/me/ally-codes/${e}/use`, {}), await j()) : !n && typeof e == "string" && (pe(e), h(L()));
404
- }, [n, j]), M = r((e) => {
405
- if (_(e), R(e), e && n) {
418
+ }, [n]), N = r(async (e) => {
419
+ n && typeof e == "number" ? (await L.put(`/api/v1/users/me/ally-codes/${e}/use`, {}), await M()) : !n && typeof e == "string" && (pe(e), h(z()));
420
+ }, [n, M]), P = r((e) => {
421
+ if (_(e), B(e), e && n) {
406
422
  let t = m.find((t) => "ally_code" in t && t.ally_code === e);
407
- t && "id" in t && ne(t.id);
423
+ t && "id" in t && N(t.id);
408
424
  } else e && !n && pe(e);
409
425
  }, [
410
426
  n,
411
427
  m,
412
- ne
413
- ]), N = r(async (e) => {
428
+ N
429
+ ]), F = r(async (e) => {
414
430
  if (!/^\d{9}$/.test(e)) throw Error("Ally code must be exactly 9 digits");
415
431
  if (n) {
416
- let t = await F.post("/api/v1/users/me/ally-codes", { ally_code: e });
417
- h((e) => [t, ...e]), m.length === 0 && M(e);
432
+ let t = await L.post("/api/v1/users/me/ally-codes", { ally_code: e });
433
+ h((e) => [t, ...e]), m.length === 0 && P(e);
418
434
  } else {
419
- let t = await F.get(`/api/v1/player-data/player/${e}`);
435
+ let t = await L.get(`/api/v1/player-data/player/${e}`);
420
436
  if (!t) throw Error("Invalid ally code - player not found");
421
437
  de({
422
438
  ally_code: e,
423
439
  player_name: t.data?.name ?? null,
424
440
  last_used_at: (/* @__PURE__ */ new Date()).toISOString()
425
- }), h(L()), m.length === 0 && M(e);
441
+ }), h(z()), m.length === 0 && P(e);
426
442
  }
427
443
  }, [
428
444
  n,
429
445
  m.length,
430
- M
446
+ P
431
447
  ]), re = r(async (e) => {
432
- n && typeof e == "number" ? (await F.delete(`/api/v1/users/me/ally-codes/${e}`), h((t) => t.filter((t) => "id" in t && t.id !== e))) : !n && typeof e == "string" && (fe(e), h(L()), g === e && M(null));
448
+ n && typeof e == "number" ? (await L.delete(`/api/v1/users/me/ally-codes/${e}`), h((t) => t.filter((t) => "id" in t && t.id !== e))) : !n && typeof e == "string" && (fe(e), h(z()), g === e && P(null));
433
449
  }, [
434
450
  n,
435
451
  g,
436
- M
437
- ]), P = r(() => {
452
+ P
453
+ ]), I = r(() => {
438
454
  x({
439
455
  show: !1,
440
456
  localStorageCodes: []
441
457
  });
442
458
  }, []), ie = r(async () => {
443
459
  if (!n) return;
444
- let e = L();
460
+ let e = z();
445
461
  for (let t of e) try {
446
- await N(t.ally_code);
462
+ await F(t.ally_code);
447
463
  } catch (e) {
448
464
  console.error(`Failed to migrate ally code ${t.ally_code}:`, e);
449
465
  }
450
- he(), P();
466
+ he(), I();
451
467
  }, [
452
468
  n,
453
- N,
454
- P
469
+ F,
470
+ I
455
471
  ]);
456
472
  a(() => {
457
- j();
458
- }, [j]);
473
+ M();
474
+ }, [M]);
459
475
  let ae = {
460
476
  user: n,
461
477
  isAuthenticated: !!n,
@@ -463,29 +479,31 @@ var _ = [
463
479
  login: C,
464
480
  register: w,
465
481
  logout: T,
482
+ logoutAll: te,
466
483
  refreshUser: ee,
467
- forgotPassword: k,
468
- resetPassword: A,
469
- resendVerification: te,
484
+ forgotPassword: A,
485
+ resetPassword: j,
486
+ resendVerification: ne,
487
+ updateAccount: k,
470
488
  authEnabled: u,
471
489
  isLoadingFeatures: f,
472
490
  allyCodes: m,
473
491
  selectedAllyCode: g,
474
492
  isLoadingAllyCodes: v,
475
- fetchAllyCodes: j,
476
- addAllyCode: N,
493
+ fetchAllyCodes: M,
494
+ addAllyCode: F,
477
495
  removeAllyCode: re,
478
- selectAllyCode: M,
479
- updateAllyCodeLastUsed: ne,
496
+ selectAllyCode: P,
497
+ updateAllyCodeLastUsed: N,
480
498
  migrationPrompt: b,
481
- dismissMigrationPrompt: P,
499
+ dismissMigrationPrompt: I,
482
500
  migrateLocalStorageCodes: ie
483
501
  };
484
502
  return /* @__PURE__ */ l(ge.Provider, {
485
503
  value: ae,
486
504
  children: e
487
505
  });
488
- }, z = () => {
506
+ }, V = () => {
489
507
  let e = i(ge);
490
508
  if (e === void 0) throw Error("useAuth must be used within an AuthProvider");
491
509
  return e;
@@ -511,7 +529,7 @@ function ve({ isOpen: e, onClose: t, triggerRef: n, panelRef: r }) {
511
529
  r
512
530
  ]);
513
531
  }
514
- var B = {
532
+ var H = {
515
533
  navMenu: "_navMenu_vspnm_1",
516
534
  trigger: "_trigger_vspnm_8",
517
535
  tabActive: "_tabActive_vspnm_29",
@@ -525,7 +543,7 @@ var B = {
525
543
  groupItems: "_groupItems_vspnm_97",
526
544
  groupItem: "_groupItem_vspnm_97"
527
545
  }, ye = ({ app: e, isCurrentApp: t, activeSectionId: n, isOpen: r, onOpenChange: i, onSectionClick: a }) => {
528
- let { isAuthenticated: s, user: c } = z(), d = o(null), f = o(null), p = `navmenu-panel-${e.id}`;
546
+ let { isAuthenticated: s, user: c } = V(), d = o(null), f = o(null), p = `navmenu-panel-${e.id}`;
529
547
  ve({
530
548
  isOpen: r,
531
549
  onClose: () => i(!1),
@@ -542,24 +560,24 @@ var B = {
542
560
  let i = t && n === e.id;
543
561
  return /* @__PURE__ */ l("a", {
544
562
  href: e.href,
545
- className: `${B.sectionLink} ${r ?? ""} ${i ? B.sectionActive : ""}`,
563
+ className: `${H.sectionLink} ${r ?? ""} ${i ? H.sectionActive : ""}`,
546
564
  "aria-current": i ? "page" : void 0,
547
565
  onClick: h(e),
548
566
  children: e.label
549
567
  }, e.id);
550
568
  };
551
569
  return /* @__PURE__ */ u("div", {
552
- className: B.navMenu,
570
+ className: H.navMenu,
553
571
  children: [/* @__PURE__ */ u("button", {
554
572
  type: "button",
555
573
  ref: d,
556
- className: `${B.trigger} ${t ? B.tabActive : ""}`,
574
+ className: `${H.trigger} ${t ? H.tabActive : ""}`,
557
575
  "aria-expanded": r,
558
576
  "aria-controls": p,
559
577
  "aria-haspopup": "true",
560
578
  onClick: () => i(!r),
561
579
  children: [e.label, /* @__PURE__ */ l("svg", {
562
- className: B.chevron,
580
+ className: H.chevron,
563
581
  width: "10",
564
582
  height: "6",
565
583
  viewBox: "0 0 10 6",
@@ -576,9 +594,9 @@ var B = {
576
594
  }), r && /* @__PURE__ */ l("div", {
577
595
  id: p,
578
596
  ref: f,
579
- className: B.panel,
597
+ className: H.panel,
580
598
  children: e.status === "coming-soon" ? /* @__PURE__ */ l("div", {
581
- className: B.comingSoon,
599
+ className: H.comingSoon,
582
600
  children: "Coming soon"
583
601
  }) : /* @__PURE__ */ l("nav", {
584
602
  "aria-label": `${e.label} sections`,
@@ -587,10 +605,10 @@ var B = {
587
605
  if (!g(e)) return m(e) ? v(e) : null;
588
606
  let t = e.items.filter(m);
589
607
  return /* @__PURE__ */ u("div", {
590
- className: B.group,
591
- children: [v(e, B.groupLabel), /* @__PURE__ */ l("div", {
592
- className: B.groupItems,
593
- children: t.map((e) => v(e, B.groupItem))
608
+ className: H.group,
609
+ children: [v(e, H.groupLabel), /* @__PURE__ */ l("div", {
610
+ className: H.groupItems,
611
+ children: t.map((e) => v(e, H.groupItem))
594
612
  })]
595
613
  }, e.id);
596
614
  })
@@ -630,7 +648,7 @@ var B = {
630
648
  })
631
649
  }));
632
650
  xe.displayName = "NavBurger";
633
- var V = {
651
+ var U = {
634
652
  selectWrapper: "_selectWrapper_ir4bq_1",
635
653
  fullWidth: "_fullWidth_ir4bq_7",
636
654
  label: "_label_ir4bq_11",
@@ -644,22 +662,22 @@ var V = {
644
662
  }, Se = e.forwardRef(({ label: e, error: t, helperText: n, options: r, placeholder: i, fullWidth: a = !1, className: o = "", id: s, ...c }, d) => {
645
663
  let f = s || `select-${Math.random().toString(36).substr(2, 9)}`, p = !!t;
646
664
  return /* @__PURE__ */ u("div", {
647
- className: `${V.selectWrapper} ${a ? V.fullWidth : ""} ${o}`,
665
+ className: `${U.selectWrapper} ${a ? U.fullWidth : ""} ${o}`,
648
666
  children: [
649
667
  e && /* @__PURE__ */ u("label", {
650
668
  htmlFor: f,
651
- className: V.label,
669
+ className: U.label,
652
670
  children: [e, c.required && /* @__PURE__ */ l("span", {
653
- className: V.required,
671
+ className: U.required,
654
672
  children: "*"
655
673
  })]
656
674
  }),
657
675
  /* @__PURE__ */ u("div", {
658
- className: V.selectContainer,
676
+ className: U.selectContainer,
659
677
  children: [/* @__PURE__ */ u("select", {
660
678
  ref: d,
661
679
  id: f,
662
- className: `${V.select} ${p ? V.error : ""}`,
680
+ className: `${U.select} ${p ? U.error : ""}`,
663
681
  ...c,
664
682
  children: [
665
683
  i && /* @__PURE__ */ l("option", {
@@ -675,23 +693,23 @@ var V = {
675
693
  !r && c.children
676
694
  ]
677
695
  }), /* @__PURE__ */ l("span", {
678
- className: V.arrow,
696
+ className: U.arrow,
679
697
  children: "▼"
680
698
  })]
681
699
  }),
682
700
  t && /* @__PURE__ */ l("span", {
683
- className: V.errorText,
701
+ className: U.errorText,
684
702
  children: t
685
703
  }),
686
704
  n && !t && /* @__PURE__ */ l("span", {
687
- className: V.helperText,
705
+ className: U.helperText,
688
706
  children: n
689
707
  })
690
708
  ]
691
709
  });
692
710
  });
693
711
  Se.displayName = "Select";
694
- var H = {
712
+ var W = {
695
713
  button: "_button_159ms_1",
696
714
  sm: "_sm_159ms_22",
697
715
  md: "_md_159ms_28",
@@ -707,18 +725,18 @@ var H = {
707
725
  spinner: "_spinner_159ms_116",
708
726
  spin: "_spin_159ms_116",
709
727
  loadingText: "_loadingText_159ms_131"
710
- }, U = ({ children: e, variant: t = "primary", size: n = "md", fullWidth: r = !1, loading: i = !1, disabled: a = !1, className: o = "", ...s }) => /* @__PURE__ */ l("button", {
711
- className: `${H.button} ${H[t]} ${H[n]} ${r ? H.fullWidth : ""} ${i ? H.loading : ""} ${o}`,
728
+ }, G = ({ children: e, variant: t = "primary", size: n = "md", fullWidth: r = !1, loading: i = !1, disabled: a = !1, className: o = "", ...s }) => /* @__PURE__ */ l("button", {
729
+ className: `${W.button} ${W[t]} ${W[n]} ${r ? W.fullWidth : ""} ${i ? W.loading : ""} ${o}`,
712
730
  disabled: a || i,
713
731
  ...s,
714
732
  children: i ? /* @__PURE__ */ u("span", {
715
- className: H.loadingSpinner,
716
- children: [/* @__PURE__ */ l("span", { className: H.spinner }), /* @__PURE__ */ l("span", {
717
- className: H.loadingText,
733
+ className: W.loadingSpinner,
734
+ children: [/* @__PURE__ */ l("span", { className: W.spinner }), /* @__PURE__ */ l("span", {
735
+ className: W.loadingText,
718
736
  children: e
719
737
  })]
720
738
  }) : e
721
- }), W = {
739
+ }), K = {
722
740
  inputWrapper: "_inputWrapper_15t4g_1",
723
741
  fullWidth: "_fullWidth_15t4g_7",
724
742
  label: "_label_15t4g_11",
@@ -730,28 +748,28 @@ var H = {
730
748
  }, Ce = e.forwardRef(({ label: e, error: t, helperText: n, fullWidth: r = !1, className: i = "", id: a, ...o }, s) => {
731
749
  let c = a || `input-${Math.random().toString(36).substr(2, 9)}`, d = !!t;
732
750
  return /* @__PURE__ */ u("div", {
733
- className: `${W.inputWrapper} ${r ? W.fullWidth : ""} ${i}`,
751
+ className: `${K.inputWrapper} ${r ? K.fullWidth : ""} ${i}`,
734
752
  children: [
735
753
  e && /* @__PURE__ */ u("label", {
736
754
  htmlFor: c,
737
- className: W.label,
755
+ className: K.label,
738
756
  children: [e, o.required && /* @__PURE__ */ l("span", {
739
- className: W.required,
757
+ className: K.required,
740
758
  children: "*"
741
759
  })]
742
760
  }),
743
761
  /* @__PURE__ */ l("input", {
744
762
  ref: s,
745
763
  id: c,
746
- className: `${W.input} ${d ? W.error : ""}`,
764
+ className: `${K.input} ${d ? K.error : ""}`,
747
765
  ...o
748
766
  }),
749
767
  t && /* @__PURE__ */ l("span", {
750
- className: W.errorText,
768
+ className: K.errorText,
751
769
  children: t
752
770
  }),
753
771
  n && !t && /* @__PURE__ */ l("span", {
754
- className: W.helperText,
772
+ className: K.helperText,
755
773
  children: n
756
774
  })
757
775
  ]
@@ -760,7 +778,7 @@ var H = {
760
778
  Ce.displayName = "Input";
761
779
  //#endregion
762
780
  //#region src/utils/formatAllyCode.ts
763
- var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e.slice(6, 9)}`, we = (e) => e.replace(/-/g, ""), K = {
781
+ var q = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e.slice(6, 9)}`, we = (e) => e.replace(/-/g, ""), J = {
764
782
  allyCodeDropdown: "_allyCodeDropdown_1pawi_1",
765
783
  select: "_select_1pawi_8",
766
784
  manageButton: "_manageButton_1pawi_12",
@@ -774,7 +792,7 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
774
792
  doneButton: "_doneButton_1pawi_88",
775
793
  inline: "_inline_1pawi_107"
776
794
  }, Te = ({ onAllyCodeSelected: e, className: t = "", isOpen: n, onOpenChange: r, variant: i = "floating" }) => {
777
- let { allyCodes: a, selectedAllyCode: c, selectAllyCode: d, removeAllyCode: f, addAllyCode: p, isLoadingAllyCodes: m } = z(), [h, g] = s(!1), _ = n ?? h, v = (e) => {
795
+ let { allyCodes: a, selectedAllyCode: c, selectAllyCode: d, removeAllyCode: f, addAllyCode: p, isLoadingAllyCodes: m } = V(), [h, g] = s(!1), _ = n ?? h, v = (e) => {
778
796
  g(e), r?.(e);
779
797
  }, [y, b] = s(""), [x, S] = s(""), [C, w] = s(""), [T, E] = s(!1), D = o(null), O = o(null);
780
798
  ve({
@@ -785,14 +803,17 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
785
803
  });
786
804
  let ee = a.map((e) => ({
787
805
  value: e.ally_code,
788
- label: e.player_name ? `${e.player_name} (${G(e.ally_code)})` : G(e.ally_code)
789
- })), k = (t) => {
806
+ label: e.player_name ? `${e.player_name} (${q(e.ally_code)})` : q(e.ally_code)
807
+ })), te = (t) => {
790
808
  let n = t.target.value || null;
791
809
  d(n), e?.(n);
792
- }, A = async (e) => {
810
+ }, k = async (e) => {
793
811
  let t = a.find((t) => t.ally_code === e);
794
- t && await f("id" in t ? t.id : t.ally_code);
795
- }, te = async () => {
812
+ if (t) {
813
+ let e = "id" in t ? t.id : t.ally_code;
814
+ await f(e);
815
+ }
816
+ }, A = async () => {
796
817
  if (S(""), w(""), !/^\d{9}$/.test(y)) {
797
818
  S("Ally code must be exactly 9 digits");
798
819
  return;
@@ -807,80 +828,80 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
807
828
  }
808
829
  }, j = a.length > 0;
809
830
  return /* @__PURE__ */ u("div", {
810
- className: `${K.allyCodeDropdown} ${i === "inline" ? K.inline : ""} ${t}`,
831
+ className: `${J.allyCodeDropdown} ${i === "inline" ? J.inline : ""} ${t}`,
811
832
  children: [
812
833
  j && /* @__PURE__ */ l(Se, {
813
834
  options: ee,
814
835
  value: c || "",
815
- onChange: k,
836
+ onChange: te,
816
837
  placeholder: "Select ally code",
817
838
  disabled: m,
818
- className: K.select
839
+ className: J.select
819
840
  }),
820
841
  _ && /* @__PURE__ */ u("div", {
821
- className: K.managePanel,
842
+ className: J.managePanel,
822
843
  ref: O,
823
844
  id: "ally-code-manage-panel",
824
845
  children: [
825
846
  /* @__PURE__ */ l("h4", { children: "Manage Ally Codes" }),
826
847
  /* @__PURE__ */ u("div", {
827
- className: K.addSection,
848
+ className: J.addSection,
828
849
  children: [
829
850
  /* @__PURE__ */ u("div", {
830
- className: K.addInputGroup,
851
+ className: J.addInputGroup,
831
852
  children: [/* @__PURE__ */ l(Ce, {
832
853
  type: "text",
833
854
  placeholder: "9-digit ally code",
834
855
  value: y,
835
856
  onChange: (e) => b(e.target.value.replace(/\D/g, "").slice(0, 9)),
836
857
  disabled: T
837
- }), /* @__PURE__ */ l(U, {
858
+ }), /* @__PURE__ */ l(G, {
838
859
  variant: "primary",
839
860
  size: "sm",
840
- onClick: te,
861
+ onClick: A,
841
862
  loading: T,
842
863
  disabled: y.length !== 9,
843
864
  children: "Add"
844
865
  })]
845
866
  }),
846
867
  x && /* @__PURE__ */ l("div", {
847
- className: K.error,
868
+ className: J.error,
848
869
  children: x
849
870
  }),
850
871
  C && /* @__PURE__ */ l("div", {
851
- className: K.success,
872
+ className: J.success,
852
873
  children: C
853
874
  })
854
875
  ]
855
876
  }),
856
877
  /* @__PURE__ */ l("div", {
857
- className: K.codesList,
878
+ className: J.codesList,
858
879
  children: a.map((e) => /* @__PURE__ */ u("div", {
859
- className: K.codeItem,
860
- children: [/* @__PURE__ */ l("span", { children: e.player_name ? `${e.player_name} (${G(e.ally_code)})` : G(e.ally_code) }), /* @__PURE__ */ l(U, {
880
+ className: J.codeItem,
881
+ children: [/* @__PURE__ */ l("span", { children: e.player_name ? `${e.player_name} (${q(e.ally_code)})` : q(e.ally_code) }), /* @__PURE__ */ l(G, {
861
882
  variant: "ghost",
862
883
  size: "sm",
863
- onClick: () => A(e.ally_code),
884
+ onClick: () => k(e.ally_code),
864
885
  children: "Remove"
865
886
  })]
866
887
  }, e.ally_code))
867
888
  }),
868
- /* @__PURE__ */ l(U, {
889
+ /* @__PURE__ */ l(G, {
869
890
  variant: "secondary",
870
891
  size: "sm",
871
892
  onClick: () => v(!1),
872
- className: K.doneButton,
893
+ className: J.doneButton,
873
894
  children: "Done"
874
895
  })
875
896
  ]
876
897
  }),
877
898
  /* @__PURE__ */ l("span", {
878
899
  ref: D,
879
- children: /* @__PURE__ */ l(U, {
900
+ children: /* @__PURE__ */ l(G, {
880
901
  variant: "ghost",
881
902
  size: "sm",
882
903
  onClick: () => v(!_),
883
- className: K.manageButton,
904
+ className: J.manageButton,
884
905
  "aria-expanded": _,
885
906
  "aria-controls": "ally-code-manage-panel",
886
907
  "aria-haspopup": "true",
@@ -889,26 +910,26 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
889
910
  })
890
911
  ]
891
912
  });
892
- }, q = {
913
+ }, Ee = {
893
914
  authCluster: "_authCluster_17o10_1",
894
915
  userLink: "_userLink_17o10_7",
895
916
  adminLink: "_adminLink_17o10_16"
896
- }, Ee = ({ showAuth: e = !0 }) => {
897
- let { user: t, isAuthenticated: n, logout: r, authEnabled: i } = z();
917
+ }, De = ({ showAuth: e = !0 }) => {
918
+ let { user: t, isAuthenticated: n, logout: r, authEnabled: i } = V();
898
919
  return e ? n ? /* @__PURE__ */ u("div", {
899
- className: q.authCluster,
920
+ className: Ee.authCluster,
900
921
  children: [
901
922
  t?.role === "admin" && /* @__PURE__ */ l("a", {
902
923
  href: "/admin/users",
903
- className: q.adminLink,
924
+ className: Ee.adminLink,
904
925
  children: "Admin"
905
926
  }),
906
927
  /* @__PURE__ */ l("a", {
907
928
  href: "/profile",
908
- className: q.userLink,
929
+ className: Ee.userLink,
909
930
  children: t?.username
910
931
  }),
911
- /* @__PURE__ */ l(U, {
932
+ /* @__PURE__ */ l(G, {
912
933
  variant: "ghost",
913
934
  size: "sm",
914
935
  onClick: () => {
@@ -918,24 +939,24 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
918
939
  })
919
940
  ]
920
941
  }) : i ? /* @__PURE__ */ u("div", {
921
- className: q.authCluster,
942
+ className: Ee.authCluster,
922
943
  children: [/* @__PURE__ */ l("a", {
923
944
  href: "/login",
924
- children: /* @__PURE__ */ l(U, {
945
+ children: /* @__PURE__ */ l(G, {
925
946
  variant: "ghost",
926
947
  size: "sm",
927
948
  children: "Login"
928
949
  })
929
950
  }), /* @__PURE__ */ l("a", {
930
951
  href: "/register",
931
- children: /* @__PURE__ */ l(U, {
952
+ children: /* @__PURE__ */ l(G, {
932
953
  variant: "primary",
933
954
  size: "sm",
934
955
  children: "Sign Up"
935
956
  })
936
957
  })]
937
958
  }) : null : null;
938
- }, J = {
959
+ }, Y = {
939
960
  backdrop: "_backdrop_3ax21_1",
940
961
  panel: "_panel_3ax21_11",
941
962
  accordion: "_accordion_3ax21_24",
@@ -955,14 +976,23 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
955
976
  allySection: "_allySection_3ax21_112",
956
977
  extrasSection: "_extrasSection_3ax21_113",
957
978
  accountSection: "_accountSection_3ax21_114"
958
- }, De = ({ isOpen: e, onClose: t, currentApp: n, activeSectionId: r, onSectionClick: i, rightExtras: c, showAuth: f, panelId: p, returnFocusRef: m }) => {
959
- let { isAuthenticated: h, user: v } = z(), [y, b] = s(n === "hub" ? null : n), x = o(null);
979
+ }, Oe = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])", ke = ({ isOpen: e, onClose: t, currentApp: n, activeSectionId: r, onSectionClick: i, rightExtras: c, showAuth: f, panelId: p, returnFocusRef: m }) => {
980
+ let { isAuthenticated: h, user: v } = V(), [y, b] = s(n === "hub" ? null : n), x = o(null);
960
981
  if (a(() => (e ? document.body.style.overflow = "hidden" : document.body.style.overflow = "unset", () => {
961
982
  document.body.style.overflow = "unset";
962
983
  }), [e]), a(() => {
963
984
  if (!e) return;
964
985
  let r = (e) => {
965
- e.key === "Escape" && (t(), m.current?.focus());
986
+ if (e.key === "Escape") {
987
+ t(), m.current?.focus();
988
+ return;
989
+ }
990
+ if (e.key === "Tab") {
991
+ let t = x.current?.querySelectorAll(Oe);
992
+ if (!t || t.length === 0) return;
993
+ let n = t[0], r = t[t.length - 1], i = document.activeElement, a = i === x.current || !x.current?.contains(i);
994
+ e.shiftKey && (i === n || a) ? (e.preventDefault(), r.focus()) : !e.shiftKey && (i === r || a) && (e.preventDefault(), n.focus());
995
+ }
966
996
  };
967
997
  return document.addEventListener("keydown", r), b(n === "hub" ? null : n), x.current?.focus(), () => document.removeEventListener("keydown", r);
968
998
  }, [e]), !e) return null;
@@ -970,7 +1000,7 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
970
1000
  e === n && i?.(r, a), t();
971
1001
  };
972
1002
  return d(/* @__PURE__ */ l("div", {
973
- className: J.backdrop,
1003
+ className: Y.backdrop,
974
1004
  onClick: t,
975
1005
  children: /* @__PURE__ */ u("div", {
976
1006
  ref: x,
@@ -979,32 +1009,32 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
979
1009
  "aria-modal": "true",
980
1010
  "aria-label": "Navigation menu",
981
1011
  tabIndex: -1,
982
- className: J.panel,
1012
+ className: Y.panel,
983
1013
  onClick: (e) => e.stopPropagation(),
984
1014
  children: [
985
1015
  /* @__PURE__ */ l("div", {
986
- className: J.accordion,
1016
+ className: Y.accordion,
987
1017
  children: _.map((e) => {
988
1018
  let t = y === e.id, i = n === e.id, a = (e) => !(e.requiresAuth && !h || e.roles && (!v || !e.roles.includes(v.role))), o = `mobile-accordion-${e.id}`, s = (t, n) => {
989
1019
  let a = i && r === t.id;
990
1020
  return /* @__PURE__ */ l("a", {
991
1021
  href: t.href,
992
- className: `${J.sectionLink} ${n ?? ""} ${a ? J.sectionActive : ""}`,
1022
+ className: `${Y.sectionLink} ${n ?? ""} ${a ? Y.sectionActive : ""}`,
993
1023
  "aria-current": a ? "page" : void 0,
994
1024
  onClick: S(e.id, t),
995
1025
  children: t.label
996
1026
  }, t.id);
997
1027
  };
998
1028
  return /* @__PURE__ */ u("div", {
999
- className: J.accordionItem,
1029
+ className: Y.accordionItem,
1000
1030
  children: [/* @__PURE__ */ u("button", {
1001
1031
  type: "button",
1002
- className: `${J.accordionHeader} ${i ? J.currentApp : ""}`,
1032
+ className: `${Y.accordionHeader} ${i ? Y.currentApp : ""}`,
1003
1033
  "aria-expanded": t,
1004
1034
  "aria-controls": o,
1005
1035
  onClick: () => b(t ? null : e.id),
1006
1036
  children: [e.label, /* @__PURE__ */ l("svg", {
1007
- className: `${J.chevron} ${t ? J.chevronOpen : ""}`,
1037
+ className: `${Y.chevron} ${t ? Y.chevronOpen : ""}`,
1008
1038
  width: "10",
1009
1039
  height: "6",
1010
1040
  viewBox: "0 0 10 6",
@@ -1020,18 +1050,18 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
1020
1050
  })]
1021
1051
  }), t && /* @__PURE__ */ l("div", {
1022
1052
  id: o,
1023
- className: J.accordionPanel,
1053
+ className: Y.accordionPanel,
1024
1054
  children: e.status === "coming-soon" ? /* @__PURE__ */ l("div", {
1025
- className: J.comingSoon,
1055
+ className: Y.comingSoon,
1026
1056
  children: "Coming soon"
1027
1057
  }) : e.sections.map((e) => {
1028
1058
  if (!g(e)) return a(e) ? s(e) : null;
1029
1059
  let t = e.items.filter(a);
1030
1060
  return /* @__PURE__ */ u("div", {
1031
- className: J.group,
1032
- children: [s(e, J.groupLabel), /* @__PURE__ */ l("div", {
1033
- className: J.groupItems,
1034
- children: t.map((e) => s(e, J.groupItem))
1061
+ className: Y.group,
1062
+ children: [s(e, Y.groupLabel), /* @__PURE__ */ l("div", {
1063
+ className: Y.groupItems,
1064
+ children: t.map((e) => s(e, Y.groupItem))
1035
1065
  })]
1036
1066
  }, e.id);
1037
1067
  })
@@ -1040,26 +1070,26 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
1040
1070
  })
1041
1071
  }),
1042
1072
  /* @__PURE__ */ l("div", {
1043
- className: J.allySection,
1073
+ className: Y.allySection,
1044
1074
  children: /* @__PURE__ */ l(Te, { variant: "inline" })
1045
1075
  }),
1046
1076
  c && /* @__PURE__ */ l("div", {
1047
- className: J.extrasSection,
1077
+ className: Y.extrasSection,
1048
1078
  children: c
1049
1079
  }),
1050
1080
  /* @__PURE__ */ l("div", {
1051
- className: J.accountSection,
1052
- children: /* @__PURE__ */ l(Ee, { showAuth: f })
1081
+ className: Y.accountSection,
1082
+ children: /* @__PURE__ */ l(De, { showAuth: f })
1053
1083
  })
1054
1084
  ]
1055
1085
  })
1056
1086
  }), document.body);
1057
- }, Y = {
1087
+ }, Ae = {
1058
1088
  logoLink: "_logoLink_12f5f_3",
1059
1089
  appMenus: "_appMenus_12f5f_10",
1060
1090
  desktopRight: "_desktopRight_12f5f_19",
1061
1091
  extrasGroup: "_extrasGroup_12f5f_25"
1062
- }, Oe = ({ currentApp: e, activeSectionId: t, onNavigate: n, rightExtras: r, showAuth: i = !0, className: d }) => {
1092
+ }, je = ({ currentApp: e, activeSectionId: t, onNavigate: n, rightExtras: r, showAuth: i = !0, className: d }) => {
1063
1093
  let [f, p] = s(null), [m, g] = s(!1), v = o(null), y = "suite-mobile-nav-panel";
1064
1094
  return a(() => {
1065
1095
  let e = window.matchMedia("(max-width: 768px)"), t = () => {
@@ -1070,11 +1100,11 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
1070
1100
  className: d,
1071
1101
  logo: /* @__PURE__ */ l("a", {
1072
1102
  href: "/",
1073
- className: Y.logoLink,
1103
+ className: Ae.logoLink,
1074
1104
  children: "The Astrogator's Table"
1075
1105
  }),
1076
1106
  leftContent: /* @__PURE__ */ l("nav", {
1077
- className: Y.appMenus,
1107
+ className: Ae.appMenus,
1078
1108
  children: _.map((r) => /* @__PURE__ */ l(ye, {
1079
1109
  app: r,
1080
1110
  isCurrentApp: e === r.id,
@@ -1085,17 +1115,17 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
1085
1115
  }, r.id))
1086
1116
  }),
1087
1117
  rightContent: /* @__PURE__ */ u(c, { children: [/* @__PURE__ */ u("div", {
1088
- className: Y.desktopRight,
1118
+ className: Ae.desktopRight,
1089
1119
  children: [
1090
1120
  r && /* @__PURE__ */ l("div", {
1091
- className: Y.extrasGroup,
1121
+ className: Ae.extrasGroup,
1092
1122
  children: r
1093
1123
  }),
1094
1124
  /* @__PURE__ */ l(Te, {
1095
1125
  isOpen: f === "ally",
1096
1126
  onOpenChange: (e) => p(e ? "ally" : null)
1097
1127
  }),
1098
- /* @__PURE__ */ l(Ee, { showAuth: i })
1128
+ /* @__PURE__ */ l(De, { showAuth: i })
1099
1129
  ]
1100
1130
  }), /* @__PURE__ */ l(xe, {
1101
1131
  ref: v,
@@ -1105,7 +1135,7 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
1105
1135
  },
1106
1136
  controlsId: y
1107
1137
  })] })
1108
- }), /* @__PURE__ */ l(De, {
1138
+ }), /* @__PURE__ */ l(ke, {
1109
1139
  isOpen: m,
1110
1140
  onClose: () => g(!1),
1111
1141
  currentApp: e,
@@ -1122,7 +1152,7 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
1122
1152
  defaultContent: "_defaultContent_zjuj8_13",
1123
1153
  copyright: "_copyright_zjuj8_21",
1124
1154
  disclaimer: "_disclaimer_zjuj8_26"
1125
- }, ke = ({ children: e, className: t = "" }) => /* @__PURE__ */ l("footer", {
1155
+ }, Me = ({ children: e, className: t = "" }) => /* @__PURE__ */ l("footer", {
1126
1156
  className: `${X.footer} ${t}`,
1127
1157
  children: /* @__PURE__ */ l("div", {
1128
1158
  className: X.content,
@@ -1144,11 +1174,11 @@ var G = (e) => !e || e.length !== 9 ? e : `${e.slice(0, 3)}-${e.slice(3, 6)}-${e
1144
1174
  });
1145
1175
  //#endregion
1146
1176
  //#region src/utils/formatTime.ts
1147
- function Ae(e) {
1177
+ function Ne(e) {
1148
1178
  let t = /([zZ]|[+-]\d{2}:?\d{2})$/.test(e);
1149
1179
  return new Date(t ? e : `${e}Z`);
1150
1180
  }
1151
- function je(e, t) {
1181
+ function Pe(e, t) {
1152
1182
  let n = Math.max(0, Math.floor((t - e.getTime()) / 1e3));
1153
1183
  if (n < 60) return `${n}s ago`;
1154
1184
  let r = Math.floor(n / 60);
@@ -1156,38 +1186,38 @@ function je(e, t) {
1156
1186
  let i = Math.floor(r / 60);
1157
1187
  return i < 24 ? `${i}h ago` : `${Math.floor(i / 24)}d ago`;
1158
1188
  }
1159
- function Me(e) {
1189
+ function Fe(e) {
1160
1190
  return `${Math.floor(e / 60)}:${(e % 60).toString().padStart(2, "0")}`;
1161
1191
  }
1162
- var Ne = {
1192
+ var Ie = {
1163
1193
  rosterRefresh: "_rosterRefresh_1rbul_3",
1164
1194
  updatedLabel: "_updatedLabel_1rbul_11",
1165
1195
  refreshButton: "_refreshButton_1rbul_19",
1166
1196
  spin: "_spin_1rbul_31"
1167
- }, Pe = ({ onRefresh: e, isRefreshing: t, cachedAt: n = null, refreshAvailableAt: r = null, labelPrefix: i = "Updated", className: o = "" }) => {
1197
+ }, Le = ({ onRefresh: e, isRefreshing: t, cachedAt: n = null, refreshAvailableAt: r = null, labelPrefix: i = "Updated", className: o = "" }) => {
1168
1198
  let [c, d] = s(() => Date.now()), f = !!n || r != null && r > c;
1169
1199
  a(() => {
1170
1200
  if (!f) return;
1171
1201
  let e = setInterval(() => d(Date.now()), 1e3);
1172
1202
  return () => clearInterval(e);
1173
1203
  }, [f]);
1174
- let p = r ? Math.max(0, Math.ceil((r - c) / 1e3)) : 0, m = n ? je(Ae(n), c) : null, h = t || p > 0, g = p > 0 ? `Fresh data available in ${Me(p)}` : "Refresh roster";
1204
+ let p = r ? Math.max(0, Math.ceil((r - c) / 1e3)) : 0, m = n ? Pe(Ne(n), c) : null, h = t || p > 0, g = p > 0 ? `Fresh data available in ${Fe(p)}` : "Refresh roster";
1175
1205
  return /* @__PURE__ */ u("div", {
1176
- className: `${Ne.rosterRefresh} ${o}`,
1206
+ className: `${Ie.rosterRefresh} ${o}`,
1177
1207
  children: [m && /* @__PURE__ */ l("span", {
1178
- className: Ne.updatedLabel,
1208
+ className: Ie.updatedLabel,
1179
1209
  title: g,
1180
- children: p > 0 ? `${i} ${m} · fresh in ${Me(p)}` : `${i} ${m}`
1181
- }), /* @__PURE__ */ l(U, {
1210
+ children: p > 0 ? `${i} ${m} · fresh in ${Fe(p)}` : `${i} ${m}`
1211
+ }), /* @__PURE__ */ l(G, {
1182
1212
  variant: "outline",
1183
1213
  size: "sm",
1184
1214
  onClick: e,
1185
1215
  disabled: h,
1186
1216
  title: g,
1187
1217
  "aria-label": "Refresh roster",
1188
- className: Ne.refreshButton,
1218
+ className: Ie.refreshButton,
1189
1219
  children: /* @__PURE__ */ l("span", {
1190
- className: t ? Ne.spin : void 0,
1220
+ className: t ? Ie.spin : void 0,
1191
1221
  children: "⟳"
1192
1222
  })
1193
1223
  })]
@@ -1203,7 +1233,7 @@ var Ne = {
1203
1233
  "padding-md": "_padding-md_1vw8g_43",
1204
1234
  "padding-lg": "_padding-lg_1vw8g_47",
1205
1235
  hoverable: "_hoverable_1vw8g_52"
1206
- }, Fe = ({ children: e, variant: t = "default", chamfered: n = !1, chamferSize: r = "md", padding: i = "md", hoverable: a = !1, showDiagonalBorders: o = !0, edgeColor: s, className: d = "", onClick: f, style: p }) => {
1236
+ }, Re = ({ children: e, variant: t = "default", chamfered: n = !1, chamferSize: r = "md", padding: i = "md", hoverable: a = !1, showDiagonalBorders: o = !0, edgeColor: s, className: d = "", onClick: f, style: p }) => {
1207
1237
  let m = n ? r === "asymmetric" ? "chamfered-box-asymmetric" : r === "sm" ? "chamfered-box-sm" : r === "lg" ? "chamfered-box-lg" : "chamfered-box" : "", h = s ?? (n ? "var(--color-border)" : void 0), g = h ? { color: h } : void 0, _ = h ? {
1208
1238
  ...p,
1209
1239
  "--card-edge-color": h
@@ -1233,7 +1263,7 @@ var Ne = {
1233
1263
  })
1234
1264
  ] }), e]
1235
1265
  });
1236
- }, Ie = {
1266
+ }, ze = {
1237
1267
  badge: "_badge_rk7t5_1",
1238
1268
  sm: "_sm_rk7t5_11",
1239
1269
  md: "_md_rk7t5_16",
@@ -1245,8 +1275,8 @@ var Ne = {
1245
1275
  warning: "_warning_rk7t5_47",
1246
1276
  error: "_error_rk7t5_52",
1247
1277
  info: "_info_rk7t5_57"
1248
- }, Le = ({ children: e, variant: t = "default", size: n = "md", className: r = "" }) => /* @__PURE__ */ l("span", {
1249
- className: `${Ie.badge} ${Ie[t]} ${Ie[n]} ${r}`,
1278
+ }, Be = ({ children: e, variant: t = "default", size: n = "md", className: r = "" }) => /* @__PURE__ */ l("span", {
1279
+ className: `${ze.badge} ${ze[t]} ${ze[n]} ${r}`,
1250
1280
  children: e
1251
1281
  }), Q = {
1252
1282
  overlay: "_overlay_1b3yt_1",
@@ -1261,7 +1291,7 @@ var Ne = {
1261
1291
  title: "_title_1b3yt_53",
1262
1292
  closeButton: "_closeButton_1b3yt_60",
1263
1293
  content: "_content_1b3yt_78"
1264
- }, Re = ({ isOpen: e, onClose: t, title: n, children: r, size: i = "md", closeOnOverlayClick: o = !0, className: s = "" }) => (a(() => (e ? document.body.style.overflow = "hidden" : document.body.style.overflow = "unset", () => {
1294
+ }, Ve = ({ isOpen: e, onClose: t, title: n, children: r, size: i = "md", closeOnOverlayClick: o = !0, className: s = "" }) => (a(() => (e ? document.body.style.overflow = "hidden" : document.body.style.overflow = "unset", () => {
1265
1295
  document.body.style.overflow = "unset";
1266
1296
  }), [e]), a(() => {
1267
1297
  let n = (n) => {
@@ -1303,7 +1333,7 @@ var Ne = {
1303
1333
  dot: "_dot_1uhnh_34",
1304
1334
  bounce: "_bounce_1uhnh_1",
1305
1335
  visuallyHidden: "_visuallyHidden_1uhnh_91"
1306
- }, ze = ({ size: e = "md", variant: t = "spinner", className: n = "" }) => t === "dots" ? /* @__PURE__ */ u("div", {
1336
+ }, He = ({ size: e = "md", variant: t = "spinner", className: n = "" }) => t === "dots" ? /* @__PURE__ */ u("div", {
1307
1337
  className: `${$.dots} ${$[e]} ${n}`,
1308
1338
  children: [
1309
1339
  /* @__PURE__ */ l("span", { className: $.dot }),
@@ -1317,10 +1347,10 @@ var Ne = {
1317
1347
  className: $.visuallyHidden,
1318
1348
  children: "Loading..."
1319
1349
  })
1320
- }), Be = async (e) => {
1350
+ }), Ue = async (e) => {
1321
1351
  if (e.length === 0) return {};
1322
- let t = [...new Set(e)], n = await F.get(`/api/v1/users/lookup?ids=${t.join(",")}`);
1352
+ let t = [...new Set(e)], n = await L.get(`/api/v1/users/lookup?ids=${t.join(",")}`);
1323
1353
  return Object.fromEntries(n.users.map((e) => [e.id, e.username]));
1324
1354
  };
1325
1355
  //#endregion
1326
- export { Te as AllyCodeDropdown, ce as ApiClient, _e as AuthProvider, Le as Badge, U as Button, Fe as Card, p as Container, ke as Footer, Ce as Input, ze as Loader, Re as Modal, Oe as NavBar, Pe as RosterRefresh, _ as SUITE_NAV, Se as Select, h as TopBar, F as apiClient, se as authedFetch, he as clearAllyCodes, D as clearTokens, te as configureAuthRefresh, Be as fetchUsernames, G as formatAllyCode, Me as formatCountdown, je as formatRelativeTime, b as getAccessToken, L as getAllyCodesFromStorage, C as getRefreshToken, me as getSelectedAllyCode, le as initializeApiClient, O as isAuthenticated, g as isSuiteNavGroup, Ae as parseBackendTimestamp, j as refreshAccessToken, S as removeAccessToken, fe as removeAllyCodeFromStorage, T as removeRefreshToken, de as saveAllyCodeToStorage, x as setAccessToken, w as setRefreshToken, R as setSelectedAllyCode, E as setTokens, we as unformatAllyCode, pe as updateAllyCodeLastUsed, z as useAuth };
1356
+ export { Te as AllyCodeDropdown, oe as ApiClient, _e as AuthProvider, Be as Badge, G as Button, Re as Card, p as Container, Me as Footer, Ce as Input, He as Loader, Ve as Modal, je as NavBar, Le as RosterRefresh, _ as SUITE_NAV, Se as Select, h as TopBar, L as apiClient, ae as authedFetch, he as clearAllyCodes, D as clearTokens, A as configureAuthRefresh, Ue as fetchUsernames, q as formatAllyCode, Fe as formatCountdown, Pe as formatRelativeTime, b as getAccessToken, z as getAllyCodesFromStorage, C as getRefreshToken, me as getSelectedAllyCode, se as initializeApiClient, O as isAuthenticated, g as isSuiteNavGroup, Ne as parseBackendTimestamp, j as refreshAccessToken, S as removeAccessToken, fe as removeAllyCodeFromStorage, T as removeRefreshToken, de as saveAllyCodeToStorage, x as setAccessToken, w as setRefreshToken, B as setSelectedAllyCode, E as setTokens, we as unformatAllyCode, pe as updateAllyCodeLastUsed, V as useAuth };
@@ -1,5 +1,5 @@
1
1
  import { default as React } from 'react';
2
- import { User, LoginRequest, RegisterRequest, ForgotPasswordRequest, ResetPasswordRequest, ResendVerificationRequest, AllyCode, AllyCodeMigrationPrompt } from '../types';
2
+ import { User, LoginRequest, RegisterRequest, ForgotPasswordRequest, ResetPasswordRequest, ResendVerificationRequest, AllyCode, AllyCodeMigrationPrompt, UpdateAccountRequest, UpdateAccountResponse } from '../types';
3
3
  import { StoredAllyCode } from '../services/allyCodeStorage';
4
4
  export interface AuthContextValue {
5
5
  user: User | null;
@@ -8,10 +8,25 @@ export interface AuthContextValue {
8
8
  login: (credentials: LoginRequest) => Promise<void>;
9
9
  register: (data: RegisterRequest) => Promise<void>;
10
10
  logout: () => void;
11
+ /**
12
+ * Revoke every token for the account (server-side), then perform the same
13
+ * local sign-out as `logout()`. The local half always runs, even if the
14
+ * server call fails after the request lands, so the UI never stays "signed
15
+ * in" against a session the server will 401 on its next real call anyway.
16
+ */
17
+ logoutAll: () => Promise<void>;
11
18
  refreshUser: () => Promise<void>;
12
19
  forgotPassword: (data: ForgotPasswordRequest) => Promise<string>;
13
20
  resetPassword: (data: ResetPasswordRequest) => Promise<string>;
14
21
  resendVerification: (data: ResendVerificationRequest) => Promise<string>;
22
+ /**
23
+ * Change email and/or password (current_password required by the backend
24
+ * for either). Adopts the fresh tokens the change returns and updates
25
+ * `user` in place — including `is_verified` flipping back to false when the
26
+ * email changed. Throws on a wrong/missing current_password (400) or a
27
+ * locked account (403), same as every other auth action here.
28
+ */
29
+ updateAccount: (data: UpdateAccountRequest) => Promise<UpdateAccountResponse>;
15
30
  authEnabled: boolean;
16
31
  isLoadingFeatures: boolean;
17
32
  allyCodes: AllyCode[] | StoredAllyCode[];
@@ -20,4 +20,4 @@ export { fetchUsernames } from './services/users';
20
20
  export type { StoredAllyCode } from './services/allyCodeStorage';
21
21
  export { formatAllyCode, unformatAllyCode } from './utils/formatAllyCode';
22
22
  export { parseBackendTimestamp, formatRelativeTime, formatCountdown } from './utils/formatTime';
23
- export type { User, LoginRequest, LoginResponse, RegisterRequest, RegisterResponse, RefreshTokenRequest, RefreshTokenResponse, ForgotPasswordRequest, ForgotPasswordResponse, ResetPasswordRequest, ResetPasswordResponse, VerifyEmailRequest, VerifyEmailResponse, ResendVerificationRequest, ResendVerificationResponse, AllyCode, AllyCodeCreate, AllyCodeListResponse, AllyCodeMigrationPrompt, ApiResponse, ApiError, PaginatedResponse, ModStat, ParsedMod, ModListResponse, EvaluationScores, ModEvaluation, EvaluationRequest, EvaluationResponse, } from './types';
23
+ export type { User, LoginRequest, LoginResponse, RegisterRequest, RegisterResponse, RefreshTokenRequest, RefreshTokenResponse, ForgotPasswordRequest, ForgotPasswordResponse, ResetPasswordRequest, ResetPasswordResponse, VerifyEmailRequest, VerifyEmailResponse, ResendVerificationRequest, ResendVerificationResponse, AllyCode, AllyCodeCreate, AllyCodeListResponse, AllyCodeMigrationPrompt, TokenResponse, UpdateAccountRequest, UpdateAccountResponse, LogoutAllResponse, ApiResponse, ApiError, PaginatedResponse, ModStat, ParsedMod, ModListResponse, EvaluationScores, ModEvaluation, EvaluationRequest, EvaluationResponse, } from './types';
@@ -0,0 +1,21 @@
1
+ import { UpdateAccountRequest, UpdateAccountResponse } from '../types';
2
+ /**
3
+ * PATCH /users/me: change email and/or password. The backend requires
4
+ * `current_password` whenever either is set (a stolen access token alone must
5
+ * not be enough), and applies both together or not at all.
6
+ *
7
+ * A successful change revokes every outstanding token for the account,
8
+ * including the caller's own — so a fresh pair comes back in `tokens` and is
9
+ * adopted here immediately, before this function returns, so the caller's own
10
+ * session survives the change. `tokens` is `null` for a no-op body (nothing
11
+ * to change): nothing to adopt, nothing revoked.
12
+ */
13
+ export declare const updateAccount: (data: UpdateAccountRequest) => Promise<UpdateAccountResponse>;
14
+ /**
15
+ * POST /auth/logout-all: revoke every token for the account, including the
16
+ * caller's own. This only reaches the server — it does NOT clear this
17
+ * browser's stored tokens; the caller (AuthContext's `logoutAll`) does the
18
+ * local half so "revoked server-side" and "signed out here" always happen
19
+ * together.
20
+ */
21
+ export declare const logoutAllSessions: () => Promise<string>;
@@ -1,3 +1,3 @@
1
- export type { User, LoginRequest, LoginResponse, RegisterRequest, RegisterResponse, RefreshTokenRequest, RefreshTokenResponse, ForgotPasswordRequest, ForgotPasswordResponse, ResetPasswordRequest, ResetPasswordResponse, VerifyEmailRequest, VerifyEmailResponse, ResendVerificationRequest, ResendVerificationResponse, AllyCode, AllyCodeCreate, AllyCodeListResponse, AllyCodeMigrationPrompt, } from './user';
1
+ export type { User, LoginRequest, LoginResponse, RegisterRequest, RegisterResponse, RefreshTokenRequest, RefreshTokenResponse, ForgotPasswordRequest, ForgotPasswordResponse, ResetPasswordRequest, ResetPasswordResponse, VerifyEmailRequest, VerifyEmailResponse, ResendVerificationRequest, ResendVerificationResponse, AllyCode, AllyCodeCreate, AllyCodeListResponse, AllyCodeMigrationPrompt, TokenResponse, UpdateAccountRequest, UpdateAccountResponse, LogoutAllResponse, } from './user';
2
2
  export type { ApiResponse, ApiError, PaginatedResponse } from './api';
3
3
  export type { ModStat, ParsedMod, ModListResponse, EvaluationScores, ModEvaluation, EvaluationRequest, EvaluationResponse, } from './mod';
@@ -86,3 +86,27 @@ export interface AllyCodeMigrationPrompt {
86
86
  show: boolean;
87
87
  localStorageCodes: string[];
88
88
  }
89
+ export interface TokenResponse {
90
+ access_token: string;
91
+ refresh_token: string;
92
+ token_type: string;
93
+ expires_in: number;
94
+ }
95
+ export interface UpdateAccountRequest {
96
+ email?: string;
97
+ /** Required by the backend whenever `email` or `new_password` is set. */
98
+ current_password?: string;
99
+ new_password?: string;
100
+ }
101
+ /**
102
+ * PATCH /users/me response. `tokens` is set only when the email or password
103
+ * actually changed — that revokes every outstanding token for the account
104
+ * (including the caller's own), so fresh ones come back here. `null` means
105
+ * nothing changed (a no-op body, or the same email re-submitted).
106
+ */
107
+ export interface UpdateAccountResponse extends User {
108
+ tokens: TokenResponse | null;
109
+ }
110
+ export interface LogoutAllResponse {
111
+ message: string;
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astrogators-shared-ui",
3
- "version": "0.16.3",
3
+ "version": "0.17.0",
4
4
  "description": "Shared UI components and utilities for Astrogator's Table applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,7 +23,9 @@
23
23
  "dev": "vite",
24
24
  "build": "tsc && vite build",
25
25
  "preview": "vite preview",
26
- "type-check": "tsc --noEmit"
26
+ "type-check": "tsc --noEmit",
27
+ "test": "vitest run",
28
+ "prepublishOnly": "npm run type-check && npm test"
27
29
  },
28
30
  "repository": {
29
31
  "type": "git",
@@ -48,8 +50,8 @@
48
50
  "react": "^19.2.5",
49
51
  "react-dom": "^19.2.5",
50
52
  "typescript": "^6.0.3",
51
- "vite": "^8.0.9",
52
- "vite-plugin-dts": "^4.5.4"
53
- },
54
- "dependencies": {}
53
+ "vite": "^8.3.0",
54
+ "vite-plugin-dts": "^4.5.4",
55
+ "vitest": "^5.0.1"
56
+ }
55
57
  }