@the-portland-company/shell 0.5.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 — 2026-07-01
4
+
5
+ ### Added (native chrome — all backward-compatible)
6
+
7
+ - **Expandable nav groups**: `NavItem` gains optional `children?: NavItem[]`. A parent with children renders as an accordion group header — clicking toggles (never navigates, even with `href`; the href only feeds active detection), a right-aligned chevron rotates 180° when open, the sidebar keeps a single group open at a time, and the group containing the active route auto-expands on route change. Sub-items render as smaller nav rows on a lighter panel through the same injected `LinkComponent` (with `onClick` button support), with the main app's exact treatments: parent solid race-red when itself-or-a-child is active, active sub-item on elevated panel with race-red text, inactive rows keep the orange→red hover gradient sweep. Flat menus are byte-identical to before.
8
+ - **`standardPolitogyNav`** (new export, with `StandardNavOptions` / `StandardNavIconKey`): the canonical Politogy VRM menu — Dashboard, Contacts, Email Broadcast, SMS/MMS Blast, Calls to Action, Forms & Surveys, Support — encoded once in the shell with core-relative routes absolutized against `coreBaseUrl`. Apps map icon slots to their own icon library, and an app that owns an item nests its local pages via `contactsItem: { href, children }` (used verbatim, not absolutized) while everything else links out to VRM core. `supportItem` supplies the Support handler (`onClick`/`href`) or omits the row.
9
+
10
+ ### Tests
11
+
12
+ - Group behavior: render-when-expanded, toggle, auto-expand on active child, single-open accordion (6 tests).
13
+ - `standardPolitogyNav`: order/wording, absolutization, href-less CTA/Forms parents, default vs local Contacts children, Support variants, icon mapping (7 tests).
14
+
15
+ ## 0.6.0 — 2026-07-01
16
+
17
+ ### Added (native chrome — all backward-compatible)
18
+
19
+ Chrome-parity upgrades to the `./native` module (`NavSidebar`/`NavTopBar`/`NavAppShell`) so the dependency-light chrome is a faithful twin of the main VRM app's Chakra chrome. No Chakra components touched.
20
+
21
+ - **`NavFooter`** gains optional `commitDate?: string` and `commitAuthor?: string`. The footer now renders the main app's two-row build block: row 1 is `[env chip] | {commit last-4} | v{version}` (env chip is a color-coded capsule via the extended `envChipClasses`), row 2 (only when date/author present) is `| {Mon D} | {firstName} |` in mono.
22
+ - **`NavOrg`** gains optional `id?: string | null`. When present, the "Organization" label row shows a right-aligned `...{last-8}` copy button that writes the full id to the clipboard (`navigator.clipboard.writeText`) with a brief "Copied" affordance.
23
+ - **`NavItem`** gains optional `onClick?: () => void` (`href` is now optional). onClick items render as a `<button>` styled identically to the link rows — used for a sidebar "Support" action.
24
+ - **`NavTopBarProps`** gains optional `actionsList?: NavTopBarAction[]` (new exported `NavTopBarAction` type). Each renders as a 32×32 circular icon button (20px icon) with an optional red count badge (`badge > 0`), `warning`/`danger` tones coloring the icon red, and `href` support (external links open in a new tab). Rendered before the existing `actions` ReactNode slot (kept for back-compat).
25
+ - **Nav hover gradient sweep**: inactive sidebar nav rows (links and buttons) animate a left→right `linear-gradient(90deg,#ea6a3a,#E63946)` width 0→100% over 300ms with white text on hover; active rows keep solid race-red. No layout shift.
26
+ - **Org pill default color** changed from dark slate `#2d2f36` to race red (`var(--color-race-red)`, `#E63946`), matching the main app's fallback.
27
+ - `envChipClasses` extended: production red, staging orange, local/dev green, unknown gray.
28
+
29
+ ### Tests
30
+
31
+ - Added `__tests__/native.test.tsx` covering `actionsList` badge/href rendering, org-id copy, onClick nav items, footer rows, and `envChipClasses` tones (6 tests; suite now 49).
32
+
3
33
  ## 0.3.5 — 2026-05-14
4
34
 
5
35
  ### Fixed
package/dist/native.cjs CHANGED
@@ -1,21 +1,111 @@
1
1
  'use strict';
2
2
 
3
+ var react = require('react');
3
4
  var jsxRuntime = require('react/jsx-runtime');
4
5
 
5
6
  // src/native/index.tsx
7
+
8
+ // src/native/standardNav.ts
9
+ var CONTACTS_DEFAULT_CHILDREN = [
10
+ { label: "Dashboard", icon: "grid", path: "/relationship/contacts/dashboard" },
11
+ { label: "All Contacts", icon: "list", path: "/relationship/contacts/all" },
12
+ { label: "Tribes", icon: "users", path: "/relationship/contacts/groups" },
13
+ { label: "Import", icon: "upload", path: "/relationship/contacts/import" },
14
+ { label: "Import History", icon: "fileText", path: "/relationship/contacts/import-history" },
15
+ { label: "Merge Rules", icon: "layout", path: "/relationship/contacts/merge-rules" }
16
+ ];
17
+ var EMAIL_CHILDREN = [
18
+ { label: "Dashboard", icon: "grid", path: "/relationship/email/dashboard" },
19
+ { label: "Create & Edit", icon: "edit", path: "/relationship/email/create-and-edit" },
20
+ { label: "Sender Information", icon: "mail", path: "/relationship/email/inbox" },
21
+ { label: "Recipients", icon: "user", path: "/relationship/email/send-to" },
22
+ { label: "Send & Automate", icon: "clock", path: "/relationship/email/send-and-automate" },
23
+ { label: "Statistics", icon: "barChart", path: "/relationship/email/statistics" }
24
+ ];
25
+ var SMS_CHILDREN = [
26
+ { label: "Dashboard", icon: "grid", path: "/relationship/sms-mms-blast/dashboard" },
27
+ { label: "Compose & Send", icon: "send", path: "/relationship/sms-mms-blast/compose-and-send" },
28
+ { label: "Conversations", icon: "messageSquare", path: "/relationship/sms-mms-blast/conversations" }
29
+ ];
30
+ var CTA_CHILDREN = [
31
+ { label: "Dashboard", icon: "grid", path: "/relationship/cta-database/dashboard" },
32
+ { label: "Create & Edit", icon: "edit", path: "/relationship/cta-creator/create-and-edit" },
33
+ { label: "Submissions", icon: "fileText", path: "/relationship/cta-submissions/submissions" }
34
+ ];
35
+ var FORMS_CHILDREN = [
36
+ { label: "Dashboard", icon: "grid", path: "/relationship/forms" },
37
+ { label: "Create & Edit", icon: "edit", path: "/relationship/forms/create-and-edit" },
38
+ { label: "Submissions", icon: "fileText", path: "/relationship/forms/submissions" }
39
+ ];
40
+ function standardPolitogyNav(opts) {
41
+ const base = opts.coreBaseUrl.replace(/\/+$/, "");
42
+ const abs = (path) => `${base}${path.startsWith("/") ? path : `/${path}`}`;
43
+ const icon = (key) => opts.icons?.[key];
44
+ const leaves = (children) => children.map((c) => ({ label: c.label, icon: icon(c.icon), href: abs(c.path) }));
45
+ const contacts = opts.contactsItem ? {
46
+ label: "Contacts",
47
+ icon: icon("users"),
48
+ href: opts.contactsItem.href,
49
+ children: opts.contactsItem.children
50
+ } : {
51
+ label: "Contacts",
52
+ icon: icon("users"),
53
+ href: abs("/relationship/contacts/dashboard"),
54
+ children: leaves(CONTACTS_DEFAULT_CHILDREN)
55
+ };
56
+ const items = [
57
+ { label: "Dashboard", icon: icon("home"), href: abs("/relationship/dashboard") },
58
+ contacts,
59
+ {
60
+ label: "Email Broadcast",
61
+ icon: icon("mail"),
62
+ href: abs("/relationship/email/dashboard"),
63
+ children: leaves(EMAIL_CHILDREN)
64
+ },
65
+ {
66
+ label: "SMS/MMS Blast",
67
+ icon: icon("messageSquare"),
68
+ href: abs("/relationship/sms-mms-blast/dashboard"),
69
+ children: leaves(SMS_CHILDREN)
70
+ },
71
+ // These two carry no top-level href in VRM core (parent row only toggles).
72
+ { label: "Calls to Action", icon: icon("layout"), children: leaves(CTA_CHILDREN) },
73
+ { label: "Forms & Surveys", icon: icon("activity"), children: leaves(FORMS_CHILDREN) }
74
+ ];
75
+ if (opts.supportItem) {
76
+ items.push({
77
+ label: "Support",
78
+ icon: icon("lifeBuoy"),
79
+ href: opts.supportItem.href,
80
+ onClick: opts.supportItem.onClick
81
+ });
82
+ }
83
+ return items;
84
+ }
6
85
  var DefaultLink = ({ href, children, ...rest }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href, ...rest, children });
7
86
  function isNavItemActive(pathname, href) {
8
87
  if (href === "/") return pathname === "/";
9
88
  return pathname === href || pathname.startsWith(`${href}/`);
10
89
  }
90
+ function groupKeyOf(item, index) {
91
+ return item.href ?? `nav-group-${index}-${item.label}`;
92
+ }
93
+ function hasActiveChild(item, pathname, activeFn) {
94
+ return (item.children ?? []).some(
95
+ (child) => child.href ? activeFn(pathname, child.href) : false
96
+ );
97
+ }
11
98
  function envChipClasses(label) {
12
99
  const l = label.toLowerCase();
13
- if (l === "production")
14
- return "bg-[rgba(230,57,70,0.12)] text-[color:var(--color-race-red)]";
15
- if (l === "staging") return "bg-[rgba(242,201,76,0.18)] text-[#a6791f]";
16
- return "bg-[rgba(0,166,81,0.14)] text-[color:var(--color-green-flag)]";
100
+ if (l === "production" || l === "prod")
101
+ return "bg-[#fee2e2] text-[#ef4444]";
102
+ if (l === "staging" || l === "stage")
103
+ return "bg-[#ffedd5] text-[#f97316]";
104
+ if (l === "local" || l === "development" || l === "dev")
105
+ return "bg-[#dcfce7] text-[#22c55e]";
106
+ return "bg-[rgba(107,114,128,0.16)] text-[#6b7280]";
17
107
  }
18
- var ORG_PILL_DEFAULT = "#2d2f36";
108
+ var ORG_PILL_DEFAULT = "var(--color-race-red)";
19
109
  function NavSidebar({
20
110
  pathname,
21
111
  brand,
@@ -29,6 +119,20 @@ function NavSidebar({
29
119
  const activeFn = nav.isActive ?? isNavItemActive;
30
120
  const ModeIcon = mode?.icon;
31
121
  const orgBg = org?.brandColor || ORG_PILL_DEFAULT;
122
+ const activeGroupKey = react.useMemo(() => {
123
+ for (let i = 0; i < nav.items.length; i++) {
124
+ const item = nav.items[i];
125
+ if (item.children && item.children.length > 0 && hasActiveChild(item, pathname, activeFn)) {
126
+ return groupKeyOf(item, i);
127
+ }
128
+ }
129
+ return null;
130
+ }, [nav.items, pathname, activeFn]);
131
+ const [openGroup, setOpenGroup] = react.useState(activeGroupKey);
132
+ react.useEffect(() => {
133
+ setOpenGroup(activeGroupKey);
134
+ }, [activeGroupKey]);
135
+ const toggleGroup = (key) => setOpenGroup((prev) => prev === key ? null : key);
32
136
  return /* @__PURE__ */ jsxRuntime.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: [
33
137
  /* @__PURE__ */ jsxRuntime.jsx(
34
138
  Link,
@@ -54,7 +158,10 @@ function NavSidebar({
54
158
  ),
55
159
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-1 flex-col overflow-y-auto px-4 pb-2", children: [
56
160
  org ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-3", children: [
57
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "mb-2 px-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Organization" }),
161
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-2 flex items-center justify-between px-1", children: [
162
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Organization" }),
163
+ org.id ? /* @__PURE__ */ jsxRuntime.jsx(OrgIdCopyButton, { id: org.id }) : null
164
+ ] }),
58
165
  /* @__PURE__ */ jsxRuntime.jsxs(
59
166
  "button",
60
167
  {
@@ -88,30 +195,34 @@ function NavSidebar({
88
195
  ) }) : null,
89
196
  /* @__PURE__ */ jsxRuntime.jsxs("nav", { className: "flex flex-col gap-1", children: [
90
197
  nav.groupLabel ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-2 pb-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: nav.groupLabel }) : null,
91
- nav.items.map((item) => {
92
- const active = activeFn(pathname, item.href);
93
- const Icon = item.icon;
94
- return /* @__PURE__ */ jsxRuntime.jsxs(
95
- Link,
198
+ nav.items.map((item, idx) => {
199
+ if (item.children && item.children.length > 0) {
200
+ const key = groupKeyOf(item, idx);
201
+ const selfActive = item.href ? activeFn(pathname, item.href) : false;
202
+ const parentActive = selfActive || hasActiveChild(item, pathname, activeFn);
203
+ return /* @__PURE__ */ jsxRuntime.jsx(
204
+ NavGroup,
205
+ {
206
+ item,
207
+ active: parentActive,
208
+ open: openGroup === key,
209
+ onToggle: () => toggleGroup(key),
210
+ pathname,
211
+ activeFn,
212
+ Link
213
+ },
214
+ key
215
+ );
216
+ }
217
+ const active = item.href ? activeFn(pathname, item.href) : false;
218
+ return /* @__PURE__ */ jsxRuntime.jsx(
219
+ NavRow,
96
220
  {
97
- href: item.href,
98
- "aria-current": active ? "page" : void 0,
99
- className: [
100
- "group flex items-center gap-3 rounded-md px-3 py-2 text-sm font-semibold transition",
101
- active ? "bg-[color:var(--color-race-red)] text-white shadow-sm" : "text-[color:var(--muted)] hover:bg-[color:var(--panel)] hover:text-[color:var(--foreground)]"
102
- ].join(" "),
103
- children: [
104
- Icon ? /* @__PURE__ */ jsxRuntime.jsx(
105
- Icon,
106
- {
107
- size: 18,
108
- className: active ? "text-white" : "text-current"
109
- }
110
- ) : null,
111
- item.label
112
- ]
221
+ item,
222
+ active,
223
+ Link
113
224
  },
114
- item.href
225
+ item.href ?? `nav-${idx}-${item.label}`
115
226
  );
116
227
  })
117
228
  ] })
@@ -128,17 +239,41 @@ function NavSidebar({
128
239
  }
129
240
  )
130
241
  ] }, l.label)) }) : null,
131
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "mt-1.5 font-[family-name:var(--font-plex-mono)] text-[10px] text-[color:var(--muted)]", children: [
132
- footer.envLabel,
133
- " \xB7 v",
134
- footer.version,
135
- " \xB7 ",
136
- footer.commit.slice(0, 7)
242
+ /* @__PURE__ */ jsxRuntime.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: [
243
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
244
+ /* @__PURE__ */ jsxRuntime.jsx(
245
+ "span",
246
+ {
247
+ className: [
248
+ "rounded-md px-1.5 py-0.5 text-[10px] font-semibold",
249
+ envChipClasses(footer.envLabel)
250
+ ].join(" "),
251
+ children: footer.envLabel
252
+ }
253
+ ),
254
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
255
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: footer.commit.slice(-4) }),
256
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
257
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
258
+ "v",
259
+ footer.version
260
+ ] })
261
+ ] }),
262
+ footer.commitDate || footer.commitAuthor ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
263
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
264
+ footer.commitDate ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: new Date(footer.commitDate).toLocaleDateString(void 0, {
265
+ month: "short",
266
+ day: "numeric"
267
+ }) }) : null,
268
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
269
+ footer.commitAuthor ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: footer.commitAuthor.split(/\s+/)[0] }) : null,
270
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[color:var(--muted)]", children: "|" })
271
+ ] }) : null
137
272
  ] })
138
273
  ] }) : null
139
274
  ] });
140
275
  }
141
- function ChevronDown() {
276
+ function ChevronDown({ className } = {}) {
142
277
  return /* @__PURE__ */ jsxRuntime.jsx(
143
278
  "svg",
144
279
  {
@@ -150,17 +285,145 @@ function ChevronDown() {
150
285
  strokeWidth: 2,
151
286
  strokeLinecap: "round",
152
287
  strokeLinejoin: "round",
153
- className: "shrink-0 opacity-80",
288
+ className: className ?? "shrink-0 opacity-80",
154
289
  "aria-hidden": "true",
155
290
  children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m6 9 6 6 6-6" })
156
291
  }
157
292
  );
158
293
  }
294
+ var NAV_HOVER_GRADIENT = "linear-gradient(90deg,#ea6a3a 0%,#E63946 100%)";
295
+ function NavRow({
296
+ item,
297
+ active,
298
+ Link,
299
+ variant = "top"
300
+ }) {
301
+ const Icon = item.icon;
302
+ const isSub = variant === "sub";
303
+ const activeClasses = isSub ? "bg-[color:var(--panel-elevated)] text-[color:var(--color-race-red)] shadow-sm" : "bg-[color:var(--color-race-red)] text-white shadow-sm";
304
+ const wrapperClasses = [
305
+ "group relative block overflow-hidden rounded-md transition",
306
+ active ? activeClasses : "text-[color:var(--muted)] hover:text-white"
307
+ ].join(" ");
308
+ const rowPadding = isSub ? "px-3 py-1.5 text-[13px]" : "px-3 py-2 text-sm";
309
+ const iconSize = isSub ? 16 : 18;
310
+ const iconClass = active && !isSub ? "text-white" : "text-current";
311
+ const inner = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
312
+ !active ? /* @__PURE__ */ jsxRuntime.jsx(
313
+ "span",
314
+ {
315
+ "aria-hidden": "true",
316
+ className: "pointer-events-none absolute inset-y-0 left-0 w-0 transition-[width] duration-300 group-hover:w-full",
317
+ style: { background: NAV_HOVER_GRADIENT }
318
+ }
319
+ ) : null,
320
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: `relative z-10 flex items-center gap-3 font-semibold ${rowPadding}`, children: [
321
+ Icon ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: iconSize, className: iconClass }) : null,
322
+ item.label
323
+ ] })
324
+ ] });
325
+ if (item.onClick && !item.href) {
326
+ return /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: item.onClick, className: `w-full text-left ${wrapperClasses}`, children: inner });
327
+ }
328
+ return /* @__PURE__ */ jsxRuntime.jsx(
329
+ Link,
330
+ {
331
+ href: item.href ?? "#",
332
+ "aria-current": active ? "page" : void 0,
333
+ className: wrapperClasses,
334
+ children: inner
335
+ }
336
+ );
337
+ }
338
+ function NavGroup({
339
+ item,
340
+ active,
341
+ open,
342
+ onToggle,
343
+ pathname,
344
+ activeFn,
345
+ Link
346
+ }) {
347
+ const Icon = item.icon;
348
+ const headerClasses = [
349
+ "group relative block w-full overflow-hidden text-left transition",
350
+ open ? "rounded-t-md" : "rounded-md",
351
+ active ? "bg-[color:var(--color-race-red)] text-white shadow-sm" : open ? "bg-[color:var(--panel)] text-[color:var(--foreground)]" : "text-[color:var(--muted)] hover:text-white"
352
+ ].join(" ");
353
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
354
+ /* @__PURE__ */ jsxRuntime.jsxs(
355
+ "button",
356
+ {
357
+ type: "button",
358
+ onClick: onToggle,
359
+ "aria-expanded": open,
360
+ className: headerClasses,
361
+ children: [
362
+ !active ? /* @__PURE__ */ jsxRuntime.jsx(
363
+ "span",
364
+ {
365
+ "aria-hidden": "true",
366
+ className: "pointer-events-none absolute inset-y-0 left-0 w-0 transition-[width] duration-300 group-hover:w-full",
367
+ style: { background: NAV_HOVER_GRADIENT }
368
+ }
369
+ ) : null,
370
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relative z-10 flex items-center gap-3 px-3 py-2 text-sm font-semibold", children: [
371
+ Icon ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: 18, className: active ? "text-white" : "text-current" }) : null,
372
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1", children: item.label }),
373
+ /* @__PURE__ */ jsxRuntime.jsx(
374
+ ChevronDown,
375
+ {
376
+ className: [
377
+ "shrink-0 opacity-80 transition-transform duration-200",
378
+ open ? "rotate-180" : ""
379
+ ].join(" ")
380
+ }
381
+ )
382
+ ] })
383
+ ]
384
+ }
385
+ ),
386
+ open ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-hidden rounded-b-md bg-[color:var(--panel)] p-1", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-0.5", children: item.children.map((child, i) => {
387
+ const childActive = child.href ? activeFn(pathname, child.href) : false;
388
+ return /* @__PURE__ */ jsxRuntime.jsx(
389
+ NavRow,
390
+ {
391
+ item: child,
392
+ active: childActive,
393
+ Link,
394
+ variant: "sub"
395
+ },
396
+ child.href ?? `sub-${i}-${child.label}`
397
+ );
398
+ }) }) }) : null
399
+ ] });
400
+ }
401
+ function OrgIdCopyButton({ id }) {
402
+ const [copied, setCopied] = react.useState(false);
403
+ return /* @__PURE__ */ jsxRuntime.jsx(
404
+ "button",
405
+ {
406
+ type: "button",
407
+ onClick: () => {
408
+ try {
409
+ void navigator.clipboard?.writeText(id);
410
+ } catch {
411
+ }
412
+ setCopied(true);
413
+ setTimeout(() => setCopied(false), 1500);
414
+ },
415
+ title: "Copy organization id",
416
+ className: "rounded px-1 text-[11px] text-[color:var(--muted)] hover:bg-[color:var(--panel)]",
417
+ children: copied ? "Copied" : `...${id.slice(-8)}`
418
+ }
419
+ );
420
+ }
159
421
  function NavTopBar({
160
422
  title,
161
423
  subtitle,
162
424
  user,
163
425
  onSignOut,
426
+ actionsList,
164
427
  actions,
165
428
  themeToggle
166
429
  }) {
@@ -179,6 +442,7 @@ function NavTopBar({
179
442
  subtitle ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm text-[color:var(--muted)]", children: subtitle }) : null
180
443
  ] }),
181
444
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
445
+ actionsList && actionsList.length > 0 ? actionsList.map((action) => /* @__PURE__ */ jsxRuntime.jsx(NavTopBarActionButton, { action }, action.key)) : null,
182
446
  actions,
183
447
  themeToggle,
184
448
  /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "group relative", children: [
@@ -227,6 +491,44 @@ function SignOutIcon() {
227
491
  }
228
492
  );
229
493
  }
494
+ function NavTopBarActionButton({ action }) {
495
+ const Icon = action.icon;
496
+ const tone = action.tone ?? "default";
497
+ 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)]";
498
+ const className = [
499
+ "relative flex h-8 w-8 items-center justify-center rounded-full border border-[color:var(--line)] transition",
500
+ toneClass
501
+ ].join(" ");
502
+ const content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
503
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { size: 20, className: "text-current" }),
504
+ typeof action.badge === "number" && action.badge > 0 ? /* @__PURE__ */ jsxRuntime.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
505
+ ] });
506
+ const isExternal = action.href ? /^https?:\/\//.test(action.href) : false;
507
+ if (action.href) {
508
+ return /* @__PURE__ */ jsxRuntime.jsx(
509
+ "a",
510
+ {
511
+ href: action.href,
512
+ "aria-label": action.label,
513
+ title: action.label,
514
+ className,
515
+ ...isExternal ? { target: "_blank", rel: "noreferrer" } : {},
516
+ children: content
517
+ }
518
+ );
519
+ }
520
+ return /* @__PURE__ */ jsxRuntime.jsx(
521
+ "button",
522
+ {
523
+ type: "button",
524
+ onClick: action.onClick,
525
+ "aria-label": action.label,
526
+ title: action.label,
527
+ className,
528
+ children: content
529
+ }
530
+ );
531
+ }
230
532
  function NavAppShell({ sidebar, topBar, children }) {
231
533
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-h-screen bg-[color:var(--background)] text-[color:var(--foreground)]", children: [
232
534
  /* @__PURE__ */ jsxRuntime.jsx(NavSidebar, { ...sidebar }),
@@ -242,5 +544,6 @@ exports.NavSidebar = NavSidebar;
242
544
  exports.NavTopBar = NavTopBar;
243
545
  exports.envChipClasses = envChipClasses;
244
546
  exports.isNavItemActive = isNavItemActive;
547
+ exports.standardPolitogyNav = standardPolitogyNav;
245
548
  //# sourceMappingURL=native.cjs.map
246
549
  //# sourceMappingURL=native.cjs.map