astrogators-shared-ui 0.15.0 → 0.16.1

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.
@@ -2,6 +2,16 @@ import { default as React } from 'react';
2
2
  export interface AllyCodeDropdownProps {
3
3
  onAllyCodeSelected?: (allyCode: string | null) => void;
4
4
  className?: string;
5
+ /** Controlled manage-panel open state, so a parent (NavBar) can enforce
6
+ * "only one popover open at a time" across the whole bar. Falls back to
7
+ * self-managed state when omitted. */
8
+ isOpen?: boolean;
9
+ onOpenChange?: (isOpen: boolean) => void;
10
+ /** 'floating' (default): an absolutely-positioned popover, for the desktop
11
+ * bar. 'inline': a plain block in normal flow — used when this is nested
12
+ * inside MobileNavPanel's own scroll container, where a floating popover
13
+ * could get clipped by an ancestor's `overflow`. */
14
+ variant?: 'floating' | 'inline';
5
15
  }
6
16
  /**
7
17
  * AllyCodeDropdown component
@@ -0,0 +1,13 @@
1
+ import { default as React } from 'react';
2
+ export interface AccountClusterProps {
3
+ /** Render the username/Admin/Logout (or Login/Sign Up) cluster. Default
4
+ * true. Set false for a static page that wants no auth UI. */
5
+ showAuth?: boolean;
6
+ }
7
+ /**
8
+ * The suite's account block: username/Profile + Admin-if-admin + Logout, or
9
+ * Login + Sign Up when logged out. Identical on every app — this is the one
10
+ * place the Admin link lives now, gated directly on `user.role === 'admin'`,
11
+ * so no app opts in or out of it individually.
12
+ */
13
+ export declare const AccountCluster: React.FC<AccountClusterProps>;
@@ -0,0 +1,24 @@
1
+ import { default as React } from 'react';
2
+ import { SuiteAppId, SuiteNavSection } from '../../navigation/suiteNav';
3
+ export interface MobileNavPanelProps {
4
+ isOpen: boolean;
5
+ onClose: () => void;
6
+ currentApp: SuiteAppId;
7
+ activeSectionId?: string;
8
+ onSectionClick?: (section: SuiteNavSection, event: React.MouseEvent<HTMLAnchorElement>) => void;
9
+ rightExtras?: React.ReactNode;
10
+ showAuth?: boolean;
11
+ panelId: string;
12
+ /** The burger button — focus returns here when Escape closes the panel. */
13
+ returnFocusRef: React.RefObject<HTMLElement | null>;
14
+ }
15
+ /**
16
+ * Portalled to `document.body`: TopBar's `backdrop-filter` makes the header
17
+ * a containing block for `position: fixed` descendants, which would clip a
18
+ * fixed panel/backdrop rendered inside it. This escapes that entirely.
19
+ *
20
+ * Unlike the lightweight desktop NavMenu disclosures, this behaves like a
21
+ * modal — focus moves in on open, Escape closes and returns focus to the
22
+ * burger, body scroll is locked while open.
23
+ */
24
+ export declare const MobileNavPanel: React.FC<MobileNavPanelProps>;
@@ -1,48 +1,41 @@
1
1
  import { default as React } from 'react';
2
- /**
3
- * A single top-level section link in the NavBar tab strip.
4
- *
5
- * NavBar is intentionally router-agnostic: shared-ui takes no react-router
6
- * dependency. The consumer decides whether a tab is `active` (e.g. from its own
7
- * `useLocation`) and may supply `render` to inject a router `<Link>` for soft
8
- * client-side navigation. Without `render`, the tab is a plain `<a href>`.
9
- */
10
- export interface NavItem {
11
- label: string;
12
- href: string;
13
- active?: boolean;
14
- render?: (props: {
15
- className: string;
16
- children: React.ReactNode;
17
- }) => React.ReactNode;
18
- }
2
+ import { SuiteAppId, SuiteNavSection } from '../../navigation/suiteNav';
19
3
  export interface NavBarProps {
20
- /** Target for the "The Astrogator's Table" logo always the hub. Default '/'. */
21
- hubUrl?: string;
22
- /** This app's display name, e.g. "Mod Ledger". Omit on the hub (the logo is
23
- * already the suite identity) for a logo-only left section. */
24
- appName?: string;
25
- /** Link target for the app name — the app's own root. Default '/'. */
26
- appHref?: string;
27
- /** Top-level section tabs. Omit (or empty) for apps with no sections. */
28
- navItems?: NavItem[];
29
- /** Render the shared AllyCodeDropdown in the right cluster. Default false. */
30
- showAllyCode?: boolean;
31
- /** Render the baked username/login/logout cluster. Default true. Set false for
32
- * static pages (e.g. docs) that want no auth UI. */
33
- showAuth?: boolean;
34
- /** App-specific controls, rendered immediately left of the ally dropdown. */
4
+ /** Which app is rendering the bardrives which trigger is highlighted
5
+ * and which app's sections `onNavigate` fires for. */
6
+ currentApp: SuiteAppId;
7
+ /** Which of currentApp's own sections is active, e.g. from the app's own
8
+ * router. Apps detect "where am I" differently (some have no router at
9
+ * all), so this is supplied by the consumer, not computed by NavBar. */
10
+ activeSectionId?: string;
11
+ /** Fires only for sections belonging to currentApp cross-app links are
12
+ * always real `<a href>` full page loads, never intercepted. Call
13
+ * `event.preventDefault()` to soft-navigate instead; the menu closes
14
+ * either way, since no page load will unmount it for you. */
15
+ onNavigate?: (section: SuiteNavSection, event: React.MouseEvent<HTMLAnchorElement>) => void;
16
+ /** The one per-app element allowed in the bar: RosterRefresh. Always in
17
+ * the same slot, just left of the ally-code dropdown on desktop, and
18
+ * moves into the mobile panel on small screens. Its position never
19
+ * varies — only its presence/content does. */
35
20
  rightExtras?: React.ReactNode;
21
+ /** Render the account cluster (username/Admin/Logout, or Login/Sign Up).
22
+ * Default true. Set false only for a fully static page. */
23
+ showAuth?: boolean;
24
+ /** Non-layout hooks only (e.g. a data attribute for tests). Nothing
25
+ * passed here may change the bar's width, height, padding, or item
26
+ * positions — that capability doesn't exist on this component. */
36
27
  className?: string;
37
28
  }
38
29
  /**
39
- * NavBar — the opinionated, suite-wide top bar built on top of the dumb `TopBar`
40
- * primitive. It bakes the blessed layout so every app's chrome is identical:
41
- *
42
- * [ logo hub ] · [ appName ] [ section tabs ] ···· [ rightExtras ] [ ally ] [ auth cluster ]
43
- *
44
- * The username/profile/login/register/logout cluster is owned here (via useAuth);
45
- * apps no longer hand-roll it. Auth links point at the hub origin as plain anchors
46
- * because the auth UI lives in the hub and everything is served single-origin.
30
+ * NavBar — the suite-wide top bar. It is built from exactly one source,
31
+ * `SUITE_NAV` (`../../navigation/suiteNav.ts`), and exactly one component.
32
+ * No app passes in its own nav structure, name, or logo target — that
33
+ * capability was removed, not just left unused, because per-app
34
+ * configurability of the bar is what caused it to look different in every
35
+ * app before this component existed. The logo always targets `/`. If a
36
+ * future app needs something the manifest can't express, the manifest's
37
+ * shape grows to cover it; this component does not grow an escape-hatch
38
+ * prop instead. See `../../../CLAUDE.md`'s NavBar note for the full
39
+ * rationale.
47
40
  */
48
41
  export declare const NavBar: React.FC<NavBarProps>;
@@ -0,0 +1,11 @@
1
+ export interface NavBurgerProps {
2
+ isOpen: boolean;
3
+ onClick: () => void;
4
+ controlsId: string;
5
+ }
6
+ /**
7
+ * Mobile-only burger/✕ toggle. Stateless — NavBar owns whether the mobile
8
+ * panel is open; this just renders the current state and reports clicks.
9
+ * Forwards its ref so MobileNavPanel can return focus here on Escape.
10
+ */
11
+ export declare const NavBurger: import('react').ForwardRefExoticComponent<NavBurgerProps & import('react').RefAttributes<HTMLButtonElement>>;
@@ -0,0 +1,17 @@
1
+ import { default as React } from 'react';
2
+ import { SuiteNavApp, SuiteNavSection } from '../../navigation/suiteNav';
3
+ export interface NavMenuProps {
4
+ app: SuiteNavApp;
5
+ isCurrentApp: boolean;
6
+ activeSectionId?: string;
7
+ isOpen: boolean;
8
+ onOpenChange: (isOpen: boolean) => void;
9
+ onSectionClick?: (section: SuiteNavSection, event: React.MouseEvent<HTMLAnchorElement>) => void;
10
+ }
11
+ /**
12
+ * One trigger + dropdown panel per SUITE_NAV app. Disclosure pattern, not
13
+ * `role="menu"` — these are page links, so Tab moves through them like any
14
+ * other link list; arrow keys are an additive convenience, not a
15
+ * replacement for Tab.
16
+ */
17
+ export declare const NavMenu: React.FC<NavMenuProps>;
@@ -3,6 +3,6 @@ export type { ContainerProps } from './Container';
3
3
  export { TopBar } from './TopBar';
4
4
  export type { TopBarProps } from './TopBar';
5
5
  export { NavBar } from './NavBar';
6
- export type { NavBarProps, NavItem } from './NavBar';
6
+ export type { NavBarProps } from './NavBar';
7
7
  export { Footer } from './Footer';
8
8
  export type { FooterProps } from './Footer';
@@ -0,0 +1,18 @@
1
+ import { RefObject } from 'react';
2
+ export interface UseDismissableMenuOptions {
3
+ isOpen: boolean;
4
+ onClose: () => void;
5
+ /** The trigger button — clicks on it don't count as "outside", and Escape
6
+ * returns focus here. */
7
+ triggerRef: RefObject<HTMLElement | null>;
8
+ /** The open panel — clicks inside it don't count as "outside" either. */
9
+ panelRef: RefObject<HTMLElement | null>;
10
+ }
11
+ /**
12
+ * Shared outside-click + Escape-to-close-and-refocus behavior for the
13
+ * suite's small header popovers (per-app NavMenu, AllyCodeDropdown in its
14
+ * floating variant). Not used by MobileNavPanel: portalled to
15
+ * `document.body`, it has its own backdrop element to click and a full-panel
16
+ * focus-trap concern this hook doesn't try to cover.
17
+ */
18
+ export declare function useDismissableMenu({ isOpen, onClose, triggerRef, panelRef, }: UseDismissableMenuOptions): void;
@@ -1,5 +1,7 @@
1
1
  export { Container, TopBar, NavBar, Footer } from './components/layout';
2
- export type { ContainerProps, TopBarProps, NavBarProps, NavItem, FooterProps } from './components/layout';
2
+ export type { ContainerProps, TopBarProps, NavBarProps, FooterProps } from './components/layout';
3
+ export { SUITE_NAV } from './navigation/suiteNav';
4
+ export type { SuiteAppId, SuiteNavApp, SuiteNavSection } from './navigation/suiteNav';
3
5
  export { Button, Input, Select, AllyCodeDropdown, RosterRefresh } from './components/forms';
4
6
  export type { ButtonProps, InputProps, SelectProps, SelectOption, AllyCodeDropdownProps, RosterRefreshProps } from './components/forms';
5
7
  export { Card, Badge, Modal } from './components/display';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The single source of truth for the suite-wide NavBar's contents.
3
+ *
4
+ * No app passes its own name, sections, or logo target into `NavBar` — it all
5
+ * comes from here. If a future app needs something this shape can't express,
6
+ * grow the shape (a new field on `SuiteNavApp`/`SuiteNavSection`); an app
7
+ * never gets its own bypass prop instead. See `NavBar`'s doc comment and
8
+ * `../../CLAUDE.md`'s NavBar note for the full rationale.
9
+ */
10
+ export type SuiteAppId = 'hub' | 'mod-ledger' | 'navicharts' | 'nightwatcher';
11
+ export interface SuiteNavSection {
12
+ id: string;
13
+ label: string;
14
+ /** Absolute, same-origin path, e.g. '/mod-ledger/evaluations'. */
15
+ href: string;
16
+ /** Hidden when logged out. */
17
+ requiresAuth?: boolean;
18
+ /** Hidden unless the current user's role is in this list. */
19
+ roles?: string[];
20
+ }
21
+ export interface SuiteNavApp {
22
+ id: Exclude<SuiteAppId, 'hub'>;
23
+ label: string;
24
+ href: string;
25
+ status: 'available' | 'coming-soon';
26
+ sections: SuiteNavSection[];
27
+ }
28
+ export declare const SUITE_NAV: SuiteNavApp[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astrogators-shared-ui",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Shared UI components and utilities for Astrogator's Table applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",