@the-portland-company/shell 0.4.0 → 0.6.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/dist/native.d.cts CHANGED
@@ -6,9 +6,15 @@ type NavIcon = ComponentType<{
6
6
  className?: string;
7
7
  }>;
8
8
  type NavItem = {
9
- href: string;
9
+ /** Route to link to. Optional when `onClick` is supplied (renders a button). */
10
+ href?: string;
10
11
  label: string;
11
12
  icon?: NavIcon;
13
+ /**
14
+ * When supplied the row renders as a `<button>` (styled identically to the
15
+ * link rows) instead of a routed link — used for actions like "Support".
16
+ */
17
+ onClick?: () => void;
12
18
  };
13
19
  type NavLinkProps = {
14
20
  href: string;
@@ -19,18 +25,34 @@ type NavLinkProps = {
19
25
  type NavLinkComponent = ComponentType<NavLinkProps>;
20
26
  /** Default active-route test: exact match for "/", prefix match otherwise. */
21
27
  declare function isNavItemActive(pathname: string, href: string): boolean;
22
- /** Token-driven tone classes for the sidebar environment chip. */
28
+ /**
29
+ * Color-coded tone classes for the sidebar environment chip, matching the main
30
+ * VRM app: prod red, staging orange, local/dev green, unknown gray.
31
+ */
23
32
  declare function envChipClasses(label: string): string;
24
33
  type NavBrand = {
25
34
  /** Where the logo links to. Defaults to "/". */
26
35
  homeHref?: string;
27
- /** Short label shown inside the brand badge (e.g. "P"). */
36
+ /**
37
+ * Brand logo image src. When supplied this renders as the wordmark logo
38
+ * (matching the main app's BrandLogo), and `badgeLabel`/`title`/`subtitle`
39
+ * are used only as the no-image fallback.
40
+ */
41
+ logoSrc?: string;
42
+ logoAlt?: string;
43
+ /** Short label shown inside the brand badge fallback (e.g. "P"). */
28
44
  badgeLabel: string;
29
45
  title: string;
30
46
  subtitle?: string;
31
47
  };
32
48
  type NavOrg = {
33
49
  name: string | null;
50
+ /** Full org id. When present, a copy affordance is shown by the label. */
51
+ id?: string | null;
52
+ /** Optional org logo for the pill badge; falls back to initials. */
53
+ logoUrl?: string | null;
54
+ /** Solid pill background (the org's brand color). Defaults to race red. */
55
+ brandColor?: string | null;
34
56
  onClick?: () => void;
35
57
  };
36
58
  type NavMode = {
@@ -40,12 +62,17 @@ type NavMode = {
40
62
  type NavFooterLink = {
41
63
  label: string;
42
64
  href: string;
65
+ external?: boolean;
43
66
  };
44
67
  type NavFooter = {
45
68
  envLabel: string;
46
69
  version: string;
47
70
  commit: string;
48
71
  links?: NavFooterLink[];
72
+ /** ISO date (or any Date-parseable string) of the deployed commit. */
73
+ commitDate?: string;
74
+ /** Full name of the commit author (only the first name is shown). */
75
+ commitAuthor?: string;
49
76
  };
50
77
  type NavSidebarProps = {
51
78
  pathname: string;
@@ -63,20 +90,38 @@ type NavSidebarProps = {
63
90
  declare function NavSidebar({ pathname, brand, nav, org, mode, footer, linkComponent, }: NavSidebarProps): react_jsx_runtime.JSX.Element;
64
91
  type NavUser = {
65
92
  name: string | null;
93
+ email?: string | null;
94
+ /** Avatar image URL; falls back to initials on the brand color. */
95
+ avatarUrl?: string | null;
66
96
  tier?: string | null;
67
97
  role?: string | null;
68
98
  };
99
+ type NavTopBarAction = {
100
+ key: string;
101
+ icon: NavIcon;
102
+ label: string;
103
+ onClick?: () => void;
104
+ href?: string;
105
+ badge?: number | null;
106
+ tone?: 'default' | 'warning' | 'danger';
107
+ };
69
108
  type NavTopBarProps = {
70
109
  title: string;
71
110
  subtitle?: string;
72
111
  user?: NavUser | null;
73
112
  onSignOut?: () => void;
113
+ /**
114
+ * Structured icon-button action cluster (32×32 circular buttons with optional
115
+ * count badges), rendered before the `actions` slot. Matches the main app's
116
+ * topbar action icons.
117
+ */
118
+ actionsList?: NavTopBarAction[];
74
119
  /** App-specific action cluster rendered left of the theme toggle. */
75
120
  actions?: ReactNode;
76
121
  /** Theme toggle slot (injected so this package needs no theme dependency). */
77
122
  themeToggle?: ReactNode;
78
123
  };
79
- declare function NavTopBar({ title, subtitle, user, onSignOut, actions, themeToggle, }: NavTopBarProps): react_jsx_runtime.JSX.Element;
124
+ declare function NavTopBar({ title, subtitle, user, onSignOut, actionsList, actions, themeToggle, }: NavTopBarProps): react_jsx_runtime.JSX.Element;
80
125
  type NavAppShellProps = {
81
126
  sidebar: NavSidebarProps;
82
127
  topBar: NavTopBarProps;
@@ -88,4 +133,4 @@ type NavAppShellProps = {
88
133
  */
89
134
  declare function NavAppShell({ sidebar, topBar, children }: NavAppShellProps): react_jsx_runtime.JSX.Element;
90
135
 
91
- export { NavAppShell, type NavAppShellProps, type NavBrand, type NavFooter, type NavFooterLink, type NavIcon, type NavItem, type NavLinkComponent, type NavLinkProps, type NavMode, type NavOrg, NavSidebar, type NavSidebarProps, NavTopBar, type NavTopBarProps, type NavUser, envChipClasses, isNavItemActive };
136
+ export { NavAppShell, type NavAppShellProps, type NavBrand, type NavFooter, type NavFooterLink, type NavIcon, type NavItem, type NavLinkComponent, type NavLinkProps, type NavMode, type NavOrg, NavSidebar, type NavSidebarProps, NavTopBar, type NavTopBarAction, type NavTopBarProps, type NavUser, envChipClasses, isNavItemActive };
package/dist/native.d.ts CHANGED
@@ -6,9 +6,15 @@ type NavIcon = ComponentType<{
6
6
  className?: string;
7
7
  }>;
8
8
  type NavItem = {
9
- href: string;
9
+ /** Route to link to. Optional when `onClick` is supplied (renders a button). */
10
+ href?: string;
10
11
  label: string;
11
12
  icon?: NavIcon;
13
+ /**
14
+ * When supplied the row renders as a `<button>` (styled identically to the
15
+ * link rows) instead of a routed link — used for actions like "Support".
16
+ */
17
+ onClick?: () => void;
12
18
  };
13
19
  type NavLinkProps = {
14
20
  href: string;
@@ -19,18 +25,34 @@ type NavLinkProps = {
19
25
  type NavLinkComponent = ComponentType<NavLinkProps>;
20
26
  /** Default active-route test: exact match for "/", prefix match otherwise. */
21
27
  declare function isNavItemActive(pathname: string, href: string): boolean;
22
- /** Token-driven tone classes for the sidebar environment chip. */
28
+ /**
29
+ * Color-coded tone classes for the sidebar environment chip, matching the main
30
+ * VRM app: prod red, staging orange, local/dev green, unknown gray.
31
+ */
23
32
  declare function envChipClasses(label: string): string;
24
33
  type NavBrand = {
25
34
  /** Where the logo links to. Defaults to "/". */
26
35
  homeHref?: string;
27
- /** Short label shown inside the brand badge (e.g. "P"). */
36
+ /**
37
+ * Brand logo image src. When supplied this renders as the wordmark logo
38
+ * (matching the main app's BrandLogo), and `badgeLabel`/`title`/`subtitle`
39
+ * are used only as the no-image fallback.
40
+ */
41
+ logoSrc?: string;
42
+ logoAlt?: string;
43
+ /** Short label shown inside the brand badge fallback (e.g. "P"). */
28
44
  badgeLabel: string;
29
45
  title: string;
30
46
  subtitle?: string;
31
47
  };
32
48
  type NavOrg = {
33
49
  name: string | null;
50
+ /** Full org id. When present, a copy affordance is shown by the label. */
51
+ id?: string | null;
52
+ /** Optional org logo for the pill badge; falls back to initials. */
53
+ logoUrl?: string | null;
54
+ /** Solid pill background (the org's brand color). Defaults to race red. */
55
+ brandColor?: string | null;
34
56
  onClick?: () => void;
35
57
  };
36
58
  type NavMode = {
@@ -40,12 +62,17 @@ type NavMode = {
40
62
  type NavFooterLink = {
41
63
  label: string;
42
64
  href: string;
65
+ external?: boolean;
43
66
  };
44
67
  type NavFooter = {
45
68
  envLabel: string;
46
69
  version: string;
47
70
  commit: string;
48
71
  links?: NavFooterLink[];
72
+ /** ISO date (or any Date-parseable string) of the deployed commit. */
73
+ commitDate?: string;
74
+ /** Full name of the commit author (only the first name is shown). */
75
+ commitAuthor?: string;
49
76
  };
50
77
  type NavSidebarProps = {
51
78
  pathname: string;
@@ -63,20 +90,38 @@ type NavSidebarProps = {
63
90
  declare function NavSidebar({ pathname, brand, nav, org, mode, footer, linkComponent, }: NavSidebarProps): react_jsx_runtime.JSX.Element;
64
91
  type NavUser = {
65
92
  name: string | null;
93
+ email?: string | null;
94
+ /** Avatar image URL; falls back to initials on the brand color. */
95
+ avatarUrl?: string | null;
66
96
  tier?: string | null;
67
97
  role?: string | null;
68
98
  };
99
+ type NavTopBarAction = {
100
+ key: string;
101
+ icon: NavIcon;
102
+ label: string;
103
+ onClick?: () => void;
104
+ href?: string;
105
+ badge?: number | null;
106
+ tone?: 'default' | 'warning' | 'danger';
107
+ };
69
108
  type NavTopBarProps = {
70
109
  title: string;
71
110
  subtitle?: string;
72
111
  user?: NavUser | null;
73
112
  onSignOut?: () => void;
113
+ /**
114
+ * Structured icon-button action cluster (32×32 circular buttons with optional
115
+ * count badges), rendered before the `actions` slot. Matches the main app's
116
+ * topbar action icons.
117
+ */
118
+ actionsList?: NavTopBarAction[];
74
119
  /** App-specific action cluster rendered left of the theme toggle. */
75
120
  actions?: ReactNode;
76
121
  /** Theme toggle slot (injected so this package needs no theme dependency). */
77
122
  themeToggle?: ReactNode;
78
123
  };
79
- declare function NavTopBar({ title, subtitle, user, onSignOut, actions, themeToggle, }: NavTopBarProps): react_jsx_runtime.JSX.Element;
124
+ declare function NavTopBar({ title, subtitle, user, onSignOut, actionsList, actions, themeToggle, }: NavTopBarProps): react_jsx_runtime.JSX.Element;
80
125
  type NavAppShellProps = {
81
126
  sidebar: NavSidebarProps;
82
127
  topBar: NavTopBarProps;
@@ -88,4 +133,4 @@ type NavAppShellProps = {
88
133
  */
89
134
  declare function NavAppShell({ sidebar, topBar, children }: NavAppShellProps): react_jsx_runtime.JSX.Element;
90
135
 
91
- export { NavAppShell, type NavAppShellProps, type NavBrand, type NavFooter, type NavFooterLink, type NavIcon, type NavItem, type NavLinkComponent, type NavLinkProps, type NavMode, type NavOrg, NavSidebar, type NavSidebarProps, NavTopBar, type NavTopBarProps, type NavUser, envChipClasses, isNavItemActive };
136
+ export { NavAppShell, type NavAppShellProps, type NavBrand, type NavFooter, type NavFooterLink, type NavIcon, type NavItem, type NavLinkComponent, type NavLinkProps, type NavMode, type NavOrg, NavSidebar, type NavSidebarProps, NavTopBar, type NavTopBarAction, type NavTopBarProps, type NavUser, envChipClasses, isNavItemActive };
package/dist/native.js CHANGED
@@ -1,4 +1,5 @@
1
- import { jsxs, jsx } from 'react/jsx-runtime';
1
+ import { useState } from 'react';
2
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
3
 
3
4
  // src/native/index.tsx
4
5
  var DefaultLink = ({ href, children, ...rest }) => /* @__PURE__ */ jsx("a", { href, ...rest, children });
@@ -8,11 +9,15 @@ function isNavItemActive(pathname, href) {
8
9
  }
9
10
  function envChipClasses(label) {
10
11
  const l = label.toLowerCase();
11
- if (l === "production")
12
- return "bg-[rgba(230,57,70,0.12)] text-[color:var(--color-race-red)]";
13
- if (l === "staging") return "bg-[rgba(242,201,76,0.18)] text-[#a6791f]";
14
- return "bg-[rgba(0,166,81,0.14)] text-[color:var(--color-green-flag)]";
12
+ if (l === "production" || l === "prod")
13
+ return "bg-[#fee2e2] text-[#ef4444]";
14
+ if (l === "staging" || l === "stage")
15
+ return "bg-[#ffedd5] text-[#f97316]";
16
+ if (l === "local" || l === "development" || l === "dev")
17
+ return "bg-[#dcfce7] text-[#22c55e]";
18
+ return "bg-[rgba(107,114,128,0.16)] text-[#6b7280]";
15
19
  }
20
+ var ORG_PILL_DEFAULT = "var(--color-race-red)";
16
21
  function NavSidebar({
17
22
  pathname,
18
23
  brand,
@@ -25,102 +30,202 @@ function NavSidebar({
25
30
  const Link = linkComponent ?? DefaultLink;
26
31
  const activeFn = nav.isActive ?? isNavItemActive;
27
32
  const ModeIcon = mode?.icon;
28
- return /* @__PURE__ */ jsxs("aside", { className: "fixed inset-y-0 left-0 z-30 hidden w-[264px] flex-col border-r border-[color:var(--line)] bg-[color:var(--panel-elevated)] px-4 py-5 lg:flex", children: [
29
- /* @__PURE__ */ jsxs(Link, { href: brand.homeHref ?? "/", className: "flex items-center gap-3 px-1 py-1", children: [
30
- /* @__PURE__ */ jsx("span", { className: "flex h-9 w-9 items-center justify-center rounded-xl bg-[color:var(--color-race-red)] text-base font-black text-white shadow-sm", children: brand.badgeLabel }),
31
- /* @__PURE__ */ jsxs("span", { className: "flex flex-col leading-tight", children: [
32
- /* @__PURE__ */ jsx("span", { className: "text-sm font-bold tracking-[-0.01em] text-[color:var(--foreground)]", children: brand.title }),
33
- brand.subtitle ? /* @__PURE__ */ jsx("span", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-[color:var(--muted)]", children: brand.subtitle }) : null
34
- ] })
35
- ] }),
36
- org ? /* @__PURE__ */ jsxs(
37
- "button",
33
+ const orgBg = org?.brandColor || ORG_PILL_DEFAULT;
34
+ return /* @__PURE__ */ jsxs("aside", { className: "fixed inset-y-0 left-0 z-30 hidden w-[240px] flex-col border-r border-[color:var(--line)] bg-[color:var(--panel-elevated)] lg:flex", children: [
35
+ /* @__PURE__ */ jsx(
36
+ Link,
38
37
  {
39
- type: "button",
40
- onClick: org.onClick,
41
- className: "mt-5 flex w-full items-center justify-between rounded-xl border border-[color:var(--line)] bg-[color:var(--panel)] px-3 py-2.5 text-left transition hover:border-[color:var(--color-race-red)]/40",
42
- children: [
43
- /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2.5 overflow-hidden", children: [
44
- /* @__PURE__ */ jsx("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-[color:var(--accent-muted)] text-xs font-bold text-[color:var(--color-race-red)]", children: (org.name ?? "Org").slice(0, 2).toUpperCase() }),
45
- /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-[color:var(--foreground)]", children: org.name ?? "Account" })
46
- ] }),
47
- /* @__PURE__ */ jsx(ChevronDownUp, {})
48
- ]
38
+ href: brand.homeHref ?? "/",
39
+ className: "flex h-[64px] flex-shrink-0 items-center gap-2.5 px-6",
40
+ children: brand.logoSrc ? /* @__PURE__ */ jsx(
41
+ "img",
42
+ {
43
+ src: brand.logoSrc,
44
+ alt: brand.logoAlt ?? brand.title,
45
+ draggable: false,
46
+ className: "h-10 w-auto object-contain"
47
+ }
48
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
49
+ /* @__PURE__ */ jsx("span", { className: "flex h-9 w-9 items-center justify-center rounded-xl bg-[color:var(--color-race-red)] text-base font-black text-white shadow-sm", children: brand.badgeLabel }),
50
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-col leading-tight", children: [
51
+ /* @__PURE__ */ jsx("span", { className: "text-base font-extrabold tracking-[-0.01em] text-[color:var(--color-race-red)]", children: brand.title }),
52
+ brand.subtitle ? /* @__PURE__ */ jsx("span", { className: "text-[11px] font-semibold uppercase tracking-[0.16em] text-[color:var(--muted)]", children: brand.subtitle }) : null
53
+ ] })
54
+ ] })
49
55
  }
50
- ) : null,
51
- mode ? /* @__PURE__ */ jsxs("div", { className: "mt-2.5 flex items-center gap-2.5 rounded-xl bg-[color:var(--color-race-red)] px-3 py-2.5 text-white shadow-sm", children: [
52
- ModeIcon ? /* @__PURE__ */ jsx(ModeIcon, { size: 16, className: "shrink-0" }) : null,
53
- /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold tracking-[-0.01em]", children: mode.label })
54
- ] }) : null,
55
- /* @__PURE__ */ jsxs("nav", { className: "mt-5 flex flex-1 flex-col gap-1 overflow-y-auto", children: [
56
- nav.groupLabel ? /* @__PURE__ */ jsx("p", { className: "px-3 pb-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-[color:var(--muted)]", children: nav.groupLabel }) : null,
57
- nav.items.map((item) => {
58
- const active = activeFn(pathname, item.href);
59
- const Icon = item.icon;
60
- return /* @__PURE__ */ jsxs(
61
- Link,
56
+ ),
57
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col overflow-y-auto px-4 pb-2", children: [
58
+ org ? /* @__PURE__ */ jsxs("div", { className: "mb-3", children: [
59
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between px-1", children: [
60
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Organization" }),
61
+ org.id ? /* @__PURE__ */ jsx(OrgIdCopyButton, { id: org.id }) : null
62
+ ] }),
63
+ /* @__PURE__ */ jsxs(
64
+ "button",
62
65
  {
63
- href: item.href,
64
- "aria-current": active ? "page" : void 0,
65
- className: [
66
- "group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-semibold transition",
67
- active ? "bg-[color:var(--accent-muted)] text-[color:var(--color-race-red)]" : "text-[color:var(--muted)] hover:bg-[color:var(--panel)] hover:text-[color:var(--foreground)]"
68
- ].join(" "),
66
+ type: "button",
67
+ onClick: org.onClick,
68
+ style: { backgroundColor: orgBg },
69
+ className: "flex w-full items-center justify-between gap-2 rounded-md px-2.5 py-2 text-left text-white shadow-sm transition hover:opacity-95",
69
70
  children: [
70
- Icon ? /* @__PURE__ */ jsx(
71
- Icon,
72
- {
73
- size: 18,
74
- className: active ? "text-[color:var(--color-race-red)]" : "text-[color:var(--muted)] group-hover:text-[color:var(--foreground)]"
75
- }
76
- ) : null,
77
- item.label
71
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-2.5", children: [
72
+ /* @__PURE__ */ jsx("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-full bg-white/20 text-[11px] font-bold text-white", children: org.logoUrl ? /* @__PURE__ */ jsx("img", { src: org.logoUrl, alt: "", className: "h-full w-full object-cover" }) : (org.name ?? "Org").slice(0, 2).toUpperCase() }),
73
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold", children: org.name ?? "No organizations" })
74
+ ] }),
75
+ /* @__PURE__ */ jsx(ChevronDown, {})
78
76
  ]
79
- },
80
- item.href
81
- );
82
- })
77
+ }
78
+ )
79
+ ] }) : null,
80
+ mode ? /* @__PURE__ */ jsx("div", { className: "mb-4", children: /* @__PURE__ */ jsxs(
81
+ "button",
82
+ {
83
+ type: "button",
84
+ className: "flex w-full items-center justify-between gap-2 rounded-md bg-[color:var(--color-race-red)] px-2.5 py-2 text-white shadow-sm",
85
+ children: [
86
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2.5", children: [
87
+ ModeIcon ? /* @__PURE__ */ jsx(ModeIcon, { size: 16, className: "shrink-0" }) : null,
88
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold tracking-[-0.01em]", children: mode.label })
89
+ ] }),
90
+ /* @__PURE__ */ jsx(ChevronDown, {})
91
+ ]
92
+ }
93
+ ) }) : null,
94
+ /* @__PURE__ */ jsxs("nav", { className: "flex flex-col gap-1", children: [
95
+ nav.groupLabel ? /* @__PURE__ */ jsx("p", { className: "px-2 pb-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: nav.groupLabel }) : null,
96
+ nav.items.map((item, idx) => {
97
+ const active = item.href ? activeFn(pathname, item.href) : false;
98
+ return /* @__PURE__ */ jsx(
99
+ NavRow,
100
+ {
101
+ item,
102
+ active,
103
+ Link
104
+ },
105
+ item.href ?? `nav-${idx}-${item.label}`
106
+ );
107
+ })
108
+ ] })
83
109
  ] }),
84
- footer ? /* @__PURE__ */ jsxs("div", { className: "mt-4 border-t border-[color:var(--line)] pt-3", children: [
85
- /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
110
+ footer ? /* @__PURE__ */ jsxs("div", { className: "flex-shrink-0 border-t border-[color:var(--line)] px-4 py-3", children: [
111
+ footer.links && footer.links.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-1.5 text-xs", children: footer.links.map((l, idx) => /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
112
+ idx > 0 ? /* @__PURE__ */ jsx("span", { className: "text-[color:var(--muted)]", children: "|" }) : null,
86
113
  /* @__PURE__ */ jsx(
87
- "span",
114
+ "a",
88
115
  {
89
- className: `rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-[0.12em] ${envChipClasses(
90
- footer.envLabel
91
- )}`,
92
- children: footer.envLabel
116
+ href: l.href,
117
+ className: "font-medium text-[color:var(--color-race-red)] hover:underline",
118
+ children: l.label
93
119
  }
94
- ),
95
- /* @__PURE__ */ jsxs("span", { className: "font-[family-name:var(--font-plex-mono)] text-[10px] text-[color:var(--muted)]", children: [
96
- "v",
97
- footer.version,
98
- " \xB7 ",
99
- footer.commit.slice(0, 7)
100
- ] })
101
- ] }),
102
- footer.links && footer.links.length > 0 ? /* @__PURE__ */ jsx("div", { className: "mt-2 flex gap-3 text-[11px] font-medium text-[color:var(--muted)]", children: footer.links.map((l) => /* @__PURE__ */ jsx("a", { href: l.href, className: "hover:text-[color:var(--foreground)]", children: l.label }, l.label)) }) : null
120
+ )
121
+ ] }, l.label)) }) : null,
122
+ /* @__PURE__ */ jsxs("div", { className: "mt-1.5 flex flex-col items-center gap-0.5 font-[family-name:var(--font-plex-mono)] text-[10px] text-[color:var(--muted)]", children: [
123
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
124
+ /* @__PURE__ */ jsx(
125
+ "span",
126
+ {
127
+ className: [
128
+ "rounded-md px-1.5 py-0.5 text-[10px] font-semibold",
129
+ envChipClasses(footer.envLabel)
130
+ ].join(" "),
131
+ children: footer.envLabel
132
+ }
133
+ ),
134
+ /* @__PURE__ */ jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
135
+ /* @__PURE__ */ jsx("span", { children: footer.commit.slice(-4) }),
136
+ /* @__PURE__ */ jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
137
+ /* @__PURE__ */ jsxs("span", { children: [
138
+ "v",
139
+ footer.version
140
+ ] })
141
+ ] }),
142
+ footer.commitDate || footer.commitAuthor ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
143
+ /* @__PURE__ */ jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
144
+ footer.commitDate ? /* @__PURE__ */ jsx("span", { children: new Date(footer.commitDate).toLocaleDateString(void 0, {
145
+ month: "short",
146
+ day: "numeric"
147
+ }) }) : null,
148
+ /* @__PURE__ */ jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
149
+ footer.commitAuthor ? /* @__PURE__ */ jsx("span", { children: footer.commitAuthor.split(/\s+/)[0] }) : null,
150
+ /* @__PURE__ */ jsx("span", { className: "text-[color:var(--muted)]", children: "|" })
151
+ ] }) : null
152
+ ] })
103
153
  ] }) : null
104
154
  ] });
105
155
  }
106
- function ChevronDownUp() {
107
- return /* @__PURE__ */ jsxs(
156
+ function ChevronDown() {
157
+ return /* @__PURE__ */ jsx(
108
158
  "svg",
109
159
  {
110
- width: 15,
111
- height: 15,
160
+ width: 16,
161
+ height: 16,
112
162
  viewBox: "0 0 24 24",
113
163
  fill: "none",
114
164
  stroke: "currentColor",
115
165
  strokeWidth: 2,
116
166
  strokeLinecap: "round",
117
167
  strokeLinejoin: "round",
118
- className: "shrink-0 text-[color:var(--muted)]",
168
+ className: "shrink-0 opacity-80",
119
169
  "aria-hidden": "true",
120
- children: [
121
- /* @__PURE__ */ jsx("path", { d: "m7 15 5 5 5-5" }),
122
- /* @__PURE__ */ jsx("path", { d: "m7 9 5-5 5 5" })
123
- ]
170
+ children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
171
+ }
172
+ );
173
+ }
174
+ var NAV_HOVER_GRADIENT = "linear-gradient(90deg,#ea6a3a 0%,#E63946 100%)";
175
+ function NavRow({
176
+ item,
177
+ active,
178
+ Link
179
+ }) {
180
+ const Icon = item.icon;
181
+ const wrapperClasses = [
182
+ "group relative block overflow-hidden rounded-md transition",
183
+ active ? "bg-[color:var(--color-race-red)] text-white shadow-sm" : "text-[color:var(--muted)] hover:text-white"
184
+ ].join(" ");
185
+ const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
186
+ !active ? /* @__PURE__ */ jsx(
187
+ "span",
188
+ {
189
+ "aria-hidden": "true",
190
+ className: "pointer-events-none absolute inset-y-0 left-0 w-0 transition-[width] duration-300 group-hover:w-full",
191
+ style: { background: NAV_HOVER_GRADIENT }
192
+ }
193
+ ) : null,
194
+ /* @__PURE__ */ jsxs("span", { className: "relative z-10 flex items-center gap-3 px-3 py-2 text-sm font-semibold", children: [
195
+ Icon ? /* @__PURE__ */ jsx(Icon, { size: 18, className: active ? "text-white" : "text-current" }) : null,
196
+ item.label
197
+ ] })
198
+ ] });
199
+ if (item.onClick && !item.href) {
200
+ return /* @__PURE__ */ jsx("button", { type: "button", onClick: item.onClick, className: `w-full text-left ${wrapperClasses}`, children: inner });
201
+ }
202
+ return /* @__PURE__ */ jsx(
203
+ Link,
204
+ {
205
+ href: item.href ?? "#",
206
+ "aria-current": active ? "page" : void 0,
207
+ className: wrapperClasses,
208
+ children: inner
209
+ }
210
+ );
211
+ }
212
+ function OrgIdCopyButton({ id }) {
213
+ const [copied, setCopied] = useState(false);
214
+ return /* @__PURE__ */ jsx(
215
+ "button",
216
+ {
217
+ type: "button",
218
+ onClick: () => {
219
+ try {
220
+ void navigator.clipboard?.writeText(id);
221
+ } catch {
222
+ }
223
+ setCopied(true);
224
+ setTimeout(() => setCopied(false), 1500);
225
+ },
226
+ title: "Copy organization id",
227
+ className: "rounded px-1 text-[11px] text-[color:var(--muted)] hover:bg-[color:var(--panel)]",
228
+ children: copied ? "Copied" : `...${id.slice(-8)}`
124
229
  }
125
230
  );
126
231
  }
@@ -129,16 +234,18 @@ function NavTopBar({
129
234
  subtitle,
130
235
  user,
131
236
  onSignOut,
237
+ actionsList,
132
238
  actions,
133
239
  themeToggle
134
240
  }) {
135
241
  const initials = (user?.name ?? "Politogy").slice(0, 2).toUpperCase();
136
- return /* @__PURE__ */ jsxs("header", { className: "sticky top-0 z-20 flex items-center justify-between gap-4 border-b border-[color:var(--line)] bg-[color:var(--panel)] px-6 py-3 backdrop-blur-xl", children: [
242
+ const secondary = user?.email || [user?.tier, user?.role].filter(Boolean).join(" \xB7 ") || "member";
243
+ return /* @__PURE__ */ jsxs("header", { className: "sticky top-0 z-20 flex h-20 items-center justify-between gap-4 border-b border-[color:var(--line)] bg-[color:var(--panel)] px-4 backdrop-blur-xl", children: [
137
244
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
138
245
  /* @__PURE__ */ jsx(
139
246
  "h1",
140
247
  {
141
- className: "truncate text-xl font-semibold tracking-[-0.02em] text-[color:var(--foreground)]",
248
+ className: "truncate text-lg font-bold tracking-[-0.01em] text-[color:var(--foreground)]",
142
249
  "data-display": "true",
143
250
  children: title
144
251
  }
@@ -146,24 +253,30 @@ function NavTopBar({
146
253
  subtitle ? /* @__PURE__ */ jsx("p", { className: "truncate text-sm text-[color:var(--muted)]", children: subtitle }) : null
147
254
  ] }),
148
255
  /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
256
+ actionsList && actionsList.length > 0 ? actionsList.map((action) => /* @__PURE__ */ jsx(NavTopBarActionButton, { action }, action.key)) : null,
149
257
  actions,
150
258
  themeToggle,
151
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 rounded-full border border-[color:var(--line)] bg-[color:var(--panel-elevated)] py-1 pl-1 pr-2.5", children: [
152
- /* @__PURE__ */ jsx("span", { className: "flex h-8 w-8 items-center justify-center rounded-full bg-[color:var(--color-race-red)] text-xs font-bold text-white", children: initials }),
153
- /* @__PURE__ */ jsxs("span", { className: "hidden flex-col leading-tight md:flex", children: [
154
- /* @__PURE__ */ jsx("span", { className: "text-xs font-semibold text-[color:var(--foreground)]", children: user?.name ?? "Signed in" }),
155
- /* @__PURE__ */ jsx("span", { className: "text-[10px] font-medium uppercase tracking-[0.08em] text-[color:var(--muted)]", children: [user?.tier, user?.role].filter(Boolean).join(" \xB7 ") || "member" })
259
+ /* @__PURE__ */ jsxs("details", { className: "group relative", children: [
260
+ /* @__PURE__ */ jsxs("summary", { className: "flex cursor-pointer list-none items-center gap-2.5 rounded-full py-1 pl-1 pr-2 transition hover:bg-[color:var(--panel-elevated)] [&::-webkit-details-marker]:hidden", children: [
261
+ /* @__PURE__ */ jsx("span", { className: "flex h-9 w-9 items-center justify-center overflow-hidden rounded-full bg-[color:var(--color-race-red)] text-xs font-bold text-white", children: user?.avatarUrl ? /* @__PURE__ */ jsx("img", { src: user.avatarUrl, alt: "", className: "h-full w-full object-cover" }) : initials }),
262
+ /* @__PURE__ */ jsxs("span", { className: "hidden flex-col leading-tight md:flex", children: [
263
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-[color:var(--foreground)]", children: user?.name ?? "Signed in" }),
264
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-[color:var(--muted)]", children: secondary })
265
+ ] }),
266
+ /* @__PURE__ */ jsx(ChevronDown, {})
156
267
  ] }),
157
- onSignOut ? /* @__PURE__ */ jsx(
268
+ onSignOut ? /* @__PURE__ */ jsx("div", { className: "absolute right-0 z-30 mt-2 w-44 overflow-hidden rounded-xl border border-[color:var(--line)] bg-[color:var(--panel-elevated)] py-1 shadow-[var(--shadow-soft)]", children: /* @__PURE__ */ jsxs(
158
269
  "button",
159
270
  {
160
271
  type: "button",
161
272
  onClick: onSignOut,
162
- "aria-label": "Sign out",
163
- className: "ml-1 flex h-7 w-7 items-center justify-center rounded-full text-[color:var(--muted)] hover:text-[color:var(--color-race-red)]",
164
- children: /* @__PURE__ */ jsx(SignOutIcon, {})
273
+ className: "flex w-full items-center gap-2.5 px-4 py-2 text-left text-sm font-medium text-[color:var(--foreground)] hover:bg-[color:var(--panel)] hover:text-[color:var(--color-race-red)]",
274
+ children: [
275
+ /* @__PURE__ */ jsx(SignOutIcon, {}),
276
+ "Sign out"
277
+ ]
165
278
  }
166
- ) : null
279
+ ) }) : null
167
280
  ] })
168
281
  ] })
169
282
  ] });
@@ -189,10 +302,48 @@ function SignOutIcon() {
189
302
  }
190
303
  );
191
304
  }
305
+ function NavTopBarActionButton({ action }) {
306
+ const Icon = action.icon;
307
+ const tone = action.tone ?? "default";
308
+ const toneClass = tone === "danger" || tone === "warning" ? "text-[color:var(--color-race-red)] hover:text-[color:var(--color-race-red)]" : "text-[color:var(--muted)] hover:text-[color:var(--foreground)]";
309
+ const className = [
310
+ "relative flex h-8 w-8 items-center justify-center rounded-full border border-[color:var(--line)] transition",
311
+ toneClass
312
+ ].join(" ");
313
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
314
+ /* @__PURE__ */ jsx(Icon, { size: 20, className: "text-current" }),
315
+ typeof action.badge === "number" && action.badge > 0 ? /* @__PURE__ */ jsx("span", { className: "absolute -top-1 -right-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-[color:var(--color-race-red)] px-1 text-[10px] text-white", children: action.badge }) : null
316
+ ] });
317
+ const isExternal = action.href ? /^https?:\/\//.test(action.href) : false;
318
+ if (action.href) {
319
+ return /* @__PURE__ */ jsx(
320
+ "a",
321
+ {
322
+ href: action.href,
323
+ "aria-label": action.label,
324
+ title: action.label,
325
+ className,
326
+ ...isExternal ? { target: "_blank", rel: "noreferrer" } : {},
327
+ children: content
328
+ }
329
+ );
330
+ }
331
+ return /* @__PURE__ */ jsx(
332
+ "button",
333
+ {
334
+ type: "button",
335
+ onClick: action.onClick,
336
+ "aria-label": action.label,
337
+ title: action.label,
338
+ className,
339
+ children: content
340
+ }
341
+ );
342
+ }
192
343
  function NavAppShell({ sidebar, topBar, children }) {
193
344
  return /* @__PURE__ */ jsxs("div", { className: "min-h-screen bg-[color:var(--background)] text-[color:var(--foreground)]", children: [
194
345
  /* @__PURE__ */ jsx(NavSidebar, { ...sidebar }),
195
- /* @__PURE__ */ jsxs("div", { className: "lg:pl-[264px]", children: [
346
+ /* @__PURE__ */ jsxs("div", { className: "lg:pl-[240px]", children: [
196
347
  /* @__PURE__ */ jsx(NavTopBar, { ...topBar }),
197
348
  /* @__PURE__ */ jsx("main", { className: "px-6 py-7", children: /* @__PURE__ */ jsx("div", { className: "mx-auto max-w-7xl", children }) })
198
349
  ] })