@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/dist/native.js CHANGED
@@ -1,19 +1,109 @@
1
+ import { useMemo, useState, useEffect } from 'react';
1
2
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
3
 
3
4
  // src/native/index.tsx
5
+
6
+ // src/native/standardNav.ts
7
+ var CONTACTS_DEFAULT_CHILDREN = [
8
+ { label: "Dashboard", icon: "grid", path: "/relationship/contacts/dashboard" },
9
+ { label: "All Contacts", icon: "list", path: "/relationship/contacts/all" },
10
+ { label: "Tribes", icon: "users", path: "/relationship/contacts/groups" },
11
+ { label: "Import", icon: "upload", path: "/relationship/contacts/import" },
12
+ { label: "Import History", icon: "fileText", path: "/relationship/contacts/import-history" },
13
+ { label: "Merge Rules", icon: "layout", path: "/relationship/contacts/merge-rules" }
14
+ ];
15
+ var EMAIL_CHILDREN = [
16
+ { label: "Dashboard", icon: "grid", path: "/relationship/email/dashboard" },
17
+ { label: "Create & Edit", icon: "edit", path: "/relationship/email/create-and-edit" },
18
+ { label: "Sender Information", icon: "mail", path: "/relationship/email/inbox" },
19
+ { label: "Recipients", icon: "user", path: "/relationship/email/send-to" },
20
+ { label: "Send & Automate", icon: "clock", path: "/relationship/email/send-and-automate" },
21
+ { label: "Statistics", icon: "barChart", path: "/relationship/email/statistics" }
22
+ ];
23
+ var SMS_CHILDREN = [
24
+ { label: "Dashboard", icon: "grid", path: "/relationship/sms-mms-blast/dashboard" },
25
+ { label: "Compose & Send", icon: "send", path: "/relationship/sms-mms-blast/compose-and-send" },
26
+ { label: "Conversations", icon: "messageSquare", path: "/relationship/sms-mms-blast/conversations" }
27
+ ];
28
+ var CTA_CHILDREN = [
29
+ { label: "Dashboard", icon: "grid", path: "/relationship/cta-database/dashboard" },
30
+ { label: "Create & Edit", icon: "edit", path: "/relationship/cta-creator/create-and-edit" },
31
+ { label: "Submissions", icon: "fileText", path: "/relationship/cta-submissions/submissions" }
32
+ ];
33
+ var FORMS_CHILDREN = [
34
+ { label: "Dashboard", icon: "grid", path: "/relationship/forms" },
35
+ { label: "Create & Edit", icon: "edit", path: "/relationship/forms/create-and-edit" },
36
+ { label: "Submissions", icon: "fileText", path: "/relationship/forms/submissions" }
37
+ ];
38
+ function standardPolitogyNav(opts) {
39
+ const base = opts.coreBaseUrl.replace(/\/+$/, "");
40
+ const abs = (path) => `${base}${path.startsWith("/") ? path : `/${path}`}`;
41
+ const icon = (key) => opts.icons?.[key];
42
+ const leaves = (children) => children.map((c) => ({ label: c.label, icon: icon(c.icon), href: abs(c.path) }));
43
+ const contacts = opts.contactsItem ? {
44
+ label: "Contacts",
45
+ icon: icon("users"),
46
+ href: opts.contactsItem.href,
47
+ children: opts.contactsItem.children
48
+ } : {
49
+ label: "Contacts",
50
+ icon: icon("users"),
51
+ href: abs("/relationship/contacts/dashboard"),
52
+ children: leaves(CONTACTS_DEFAULT_CHILDREN)
53
+ };
54
+ const items = [
55
+ { label: "Dashboard", icon: icon("home"), href: abs("/relationship/dashboard") },
56
+ contacts,
57
+ {
58
+ label: "Email Broadcast",
59
+ icon: icon("mail"),
60
+ href: abs("/relationship/email/dashboard"),
61
+ children: leaves(EMAIL_CHILDREN)
62
+ },
63
+ {
64
+ label: "SMS/MMS Blast",
65
+ icon: icon("messageSquare"),
66
+ href: abs("/relationship/sms-mms-blast/dashboard"),
67
+ children: leaves(SMS_CHILDREN)
68
+ },
69
+ // These two carry no top-level href in VRM core (parent row only toggles).
70
+ { label: "Calls to Action", icon: icon("layout"), children: leaves(CTA_CHILDREN) },
71
+ { label: "Forms & Surveys", icon: icon("activity"), children: leaves(FORMS_CHILDREN) }
72
+ ];
73
+ if (opts.supportItem) {
74
+ items.push({
75
+ label: "Support",
76
+ icon: icon("lifeBuoy"),
77
+ href: opts.supportItem.href,
78
+ onClick: opts.supportItem.onClick
79
+ });
80
+ }
81
+ return items;
82
+ }
4
83
  var DefaultLink = ({ href, children, ...rest }) => /* @__PURE__ */ jsx("a", { href, ...rest, children });
5
84
  function isNavItemActive(pathname, href) {
6
85
  if (href === "/") return pathname === "/";
7
86
  return pathname === href || pathname.startsWith(`${href}/`);
8
87
  }
88
+ function groupKeyOf(item, index) {
89
+ return item.href ?? `nav-group-${index}-${item.label}`;
90
+ }
91
+ function hasActiveChild(item, pathname, activeFn) {
92
+ return (item.children ?? []).some(
93
+ (child) => child.href ? activeFn(pathname, child.href) : false
94
+ );
95
+ }
9
96
  function envChipClasses(label) {
10
97
  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)]";
98
+ if (l === "production" || l === "prod")
99
+ return "bg-[#fee2e2] text-[#ef4444]";
100
+ if (l === "staging" || l === "stage")
101
+ return "bg-[#ffedd5] text-[#f97316]";
102
+ if (l === "local" || l === "development" || l === "dev")
103
+ return "bg-[#dcfce7] text-[#22c55e]";
104
+ return "bg-[rgba(107,114,128,0.16)] text-[#6b7280]";
15
105
  }
16
- var ORG_PILL_DEFAULT = "#2d2f36";
106
+ var ORG_PILL_DEFAULT = "var(--color-race-red)";
17
107
  function NavSidebar({
18
108
  pathname,
19
109
  brand,
@@ -27,6 +117,20 @@ function NavSidebar({
27
117
  const activeFn = nav.isActive ?? isNavItemActive;
28
118
  const ModeIcon = mode?.icon;
29
119
  const orgBg = org?.brandColor || ORG_PILL_DEFAULT;
120
+ const activeGroupKey = useMemo(() => {
121
+ for (let i = 0; i < nav.items.length; i++) {
122
+ const item = nav.items[i];
123
+ if (item.children && item.children.length > 0 && hasActiveChild(item, pathname, activeFn)) {
124
+ return groupKeyOf(item, i);
125
+ }
126
+ }
127
+ return null;
128
+ }, [nav.items, pathname, activeFn]);
129
+ const [openGroup, setOpenGroup] = useState(activeGroupKey);
130
+ useEffect(() => {
131
+ setOpenGroup(activeGroupKey);
132
+ }, [activeGroupKey]);
133
+ const toggleGroup = (key) => setOpenGroup((prev) => prev === key ? null : key);
30
134
  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: [
31
135
  /* @__PURE__ */ jsx(
32
136
  Link,
@@ -52,7 +156,10 @@ function NavSidebar({
52
156
  ),
53
157
  /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col overflow-y-auto px-4 pb-2", children: [
54
158
  org ? /* @__PURE__ */ jsxs("div", { className: "mb-3", children: [
55
- /* @__PURE__ */ jsx("p", { className: "mb-2 px-1 text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Organization" }),
159
+ /* @__PURE__ */ jsxs("div", { className: "mb-2 flex items-center justify-between px-1", children: [
160
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] font-semibold uppercase tracking-[0.08em] text-[color:var(--muted)]", children: "Organization" }),
161
+ org.id ? /* @__PURE__ */ jsx(OrgIdCopyButton, { id: org.id }) : null
162
+ ] }),
56
163
  /* @__PURE__ */ jsxs(
57
164
  "button",
58
165
  {
@@ -86,30 +193,34 @@ function NavSidebar({
86
193
  ) }) : null,
87
194
  /* @__PURE__ */ jsxs("nav", { className: "flex flex-col gap-1", children: [
88
195
  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,
89
- nav.items.map((item) => {
90
- const active = activeFn(pathname, item.href);
91
- const Icon = item.icon;
92
- return /* @__PURE__ */ jsxs(
93
- Link,
196
+ nav.items.map((item, idx) => {
197
+ if (item.children && item.children.length > 0) {
198
+ const key = groupKeyOf(item, idx);
199
+ const selfActive = item.href ? activeFn(pathname, item.href) : false;
200
+ const parentActive = selfActive || hasActiveChild(item, pathname, activeFn);
201
+ return /* @__PURE__ */ jsx(
202
+ NavGroup,
203
+ {
204
+ item,
205
+ active: parentActive,
206
+ open: openGroup === key,
207
+ onToggle: () => toggleGroup(key),
208
+ pathname,
209
+ activeFn,
210
+ Link
211
+ },
212
+ key
213
+ );
214
+ }
215
+ const active = item.href ? activeFn(pathname, item.href) : false;
216
+ return /* @__PURE__ */ jsx(
217
+ NavRow,
94
218
  {
95
- href: item.href,
96
- "aria-current": active ? "page" : void 0,
97
- className: [
98
- "group flex items-center gap-3 rounded-md px-3 py-2 text-sm font-semibold transition",
99
- 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)]"
100
- ].join(" "),
101
- children: [
102
- Icon ? /* @__PURE__ */ jsx(
103
- Icon,
104
- {
105
- size: 18,
106
- className: active ? "text-white" : "text-current"
107
- }
108
- ) : null,
109
- item.label
110
- ]
219
+ item,
220
+ active,
221
+ Link
111
222
  },
112
- item.href
223
+ item.href ?? `nav-${idx}-${item.label}`
113
224
  );
114
225
  })
115
226
  ] })
@@ -126,17 +237,41 @@ function NavSidebar({
126
237
  }
127
238
  )
128
239
  ] }, l.label)) }) : null,
129
- /* @__PURE__ */ jsxs("p", { className: "mt-1.5 font-[family-name:var(--font-plex-mono)] text-[10px] text-[color:var(--muted)]", children: [
130
- footer.envLabel,
131
- " \xB7 v",
132
- footer.version,
133
- " \xB7 ",
134
- footer.commit.slice(0, 7)
240
+ /* @__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: [
241
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
242
+ /* @__PURE__ */ jsx(
243
+ "span",
244
+ {
245
+ className: [
246
+ "rounded-md px-1.5 py-0.5 text-[10px] font-semibold",
247
+ envChipClasses(footer.envLabel)
248
+ ].join(" "),
249
+ children: footer.envLabel
250
+ }
251
+ ),
252
+ /* @__PURE__ */ jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
253
+ /* @__PURE__ */ jsx("span", { children: footer.commit.slice(-4) }),
254
+ /* @__PURE__ */ jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
255
+ /* @__PURE__ */ jsxs("span", { children: [
256
+ "v",
257
+ footer.version
258
+ ] })
259
+ ] }),
260
+ footer.commitDate || footer.commitAuthor ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
261
+ /* @__PURE__ */ jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
262
+ footer.commitDate ? /* @__PURE__ */ jsx("span", { children: new Date(footer.commitDate).toLocaleDateString(void 0, {
263
+ month: "short",
264
+ day: "numeric"
265
+ }) }) : null,
266
+ /* @__PURE__ */ jsx("span", { className: "text-[color:var(--muted)]", children: "|" }),
267
+ footer.commitAuthor ? /* @__PURE__ */ jsx("span", { children: footer.commitAuthor.split(/\s+/)[0] }) : null,
268
+ /* @__PURE__ */ jsx("span", { className: "text-[color:var(--muted)]", children: "|" })
269
+ ] }) : null
135
270
  ] })
136
271
  ] }) : null
137
272
  ] });
138
273
  }
139
- function ChevronDown() {
274
+ function ChevronDown({ className } = {}) {
140
275
  return /* @__PURE__ */ jsx(
141
276
  "svg",
142
277
  {
@@ -148,17 +283,145 @@ function ChevronDown() {
148
283
  strokeWidth: 2,
149
284
  strokeLinecap: "round",
150
285
  strokeLinejoin: "round",
151
- className: "shrink-0 opacity-80",
286
+ className: className ?? "shrink-0 opacity-80",
152
287
  "aria-hidden": "true",
153
288
  children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
154
289
  }
155
290
  );
156
291
  }
292
+ var NAV_HOVER_GRADIENT = "linear-gradient(90deg,#ea6a3a 0%,#E63946 100%)";
293
+ function NavRow({
294
+ item,
295
+ active,
296
+ Link,
297
+ variant = "top"
298
+ }) {
299
+ const Icon = item.icon;
300
+ const isSub = variant === "sub";
301
+ 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";
302
+ const wrapperClasses = [
303
+ "group relative block overflow-hidden rounded-md transition",
304
+ active ? activeClasses : "text-[color:var(--muted)] hover:text-white"
305
+ ].join(" ");
306
+ const rowPadding = isSub ? "px-3 py-1.5 text-[13px]" : "px-3 py-2 text-sm";
307
+ const iconSize = isSub ? 16 : 18;
308
+ const iconClass = active && !isSub ? "text-white" : "text-current";
309
+ const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
310
+ !active ? /* @__PURE__ */ jsx(
311
+ "span",
312
+ {
313
+ "aria-hidden": "true",
314
+ className: "pointer-events-none absolute inset-y-0 left-0 w-0 transition-[width] duration-300 group-hover:w-full",
315
+ style: { background: NAV_HOVER_GRADIENT }
316
+ }
317
+ ) : null,
318
+ /* @__PURE__ */ jsxs("span", { className: `relative z-10 flex items-center gap-3 font-semibold ${rowPadding}`, children: [
319
+ Icon ? /* @__PURE__ */ jsx(Icon, { size: iconSize, className: iconClass }) : null,
320
+ item.label
321
+ ] })
322
+ ] });
323
+ if (item.onClick && !item.href) {
324
+ return /* @__PURE__ */ jsx("button", { type: "button", onClick: item.onClick, className: `w-full text-left ${wrapperClasses}`, children: inner });
325
+ }
326
+ return /* @__PURE__ */ jsx(
327
+ Link,
328
+ {
329
+ href: item.href ?? "#",
330
+ "aria-current": active ? "page" : void 0,
331
+ className: wrapperClasses,
332
+ children: inner
333
+ }
334
+ );
335
+ }
336
+ function NavGroup({
337
+ item,
338
+ active,
339
+ open,
340
+ onToggle,
341
+ pathname,
342
+ activeFn,
343
+ Link
344
+ }) {
345
+ const Icon = item.icon;
346
+ const headerClasses = [
347
+ "group relative block w-full overflow-hidden text-left transition",
348
+ open ? "rounded-t-md" : "rounded-md",
349
+ 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"
350
+ ].join(" ");
351
+ return /* @__PURE__ */ jsxs("div", { children: [
352
+ /* @__PURE__ */ jsxs(
353
+ "button",
354
+ {
355
+ type: "button",
356
+ onClick: onToggle,
357
+ "aria-expanded": open,
358
+ className: headerClasses,
359
+ children: [
360
+ !active ? /* @__PURE__ */ jsx(
361
+ "span",
362
+ {
363
+ "aria-hidden": "true",
364
+ className: "pointer-events-none absolute inset-y-0 left-0 w-0 transition-[width] duration-300 group-hover:w-full",
365
+ style: { background: NAV_HOVER_GRADIENT }
366
+ }
367
+ ) : null,
368
+ /* @__PURE__ */ jsxs("span", { className: "relative z-10 flex items-center gap-3 px-3 py-2 text-sm font-semibold", children: [
369
+ Icon ? /* @__PURE__ */ jsx(Icon, { size: 18, className: active ? "text-white" : "text-current" }) : null,
370
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children: item.label }),
371
+ /* @__PURE__ */ jsx(
372
+ ChevronDown,
373
+ {
374
+ className: [
375
+ "shrink-0 opacity-80 transition-transform duration-200",
376
+ open ? "rotate-180" : ""
377
+ ].join(" ")
378
+ }
379
+ )
380
+ ] })
381
+ ]
382
+ }
383
+ ),
384
+ open ? /* @__PURE__ */ jsx("div", { className: "overflow-hidden rounded-b-md bg-[color:var(--panel)] p-1", children: /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-0.5", children: item.children.map((child, i) => {
385
+ const childActive = child.href ? activeFn(pathname, child.href) : false;
386
+ return /* @__PURE__ */ jsx(
387
+ NavRow,
388
+ {
389
+ item: child,
390
+ active: childActive,
391
+ Link,
392
+ variant: "sub"
393
+ },
394
+ child.href ?? `sub-${i}-${child.label}`
395
+ );
396
+ }) }) }) : null
397
+ ] });
398
+ }
399
+ function OrgIdCopyButton({ id }) {
400
+ const [copied, setCopied] = useState(false);
401
+ return /* @__PURE__ */ jsx(
402
+ "button",
403
+ {
404
+ type: "button",
405
+ onClick: () => {
406
+ try {
407
+ void navigator.clipboard?.writeText(id);
408
+ } catch {
409
+ }
410
+ setCopied(true);
411
+ setTimeout(() => setCopied(false), 1500);
412
+ },
413
+ title: "Copy organization id",
414
+ className: "rounded px-1 text-[11px] text-[color:var(--muted)] hover:bg-[color:var(--panel)]",
415
+ children: copied ? "Copied" : `...${id.slice(-8)}`
416
+ }
417
+ );
418
+ }
157
419
  function NavTopBar({
158
420
  title,
159
421
  subtitle,
160
422
  user,
161
423
  onSignOut,
424
+ actionsList,
162
425
  actions,
163
426
  themeToggle
164
427
  }) {
@@ -177,6 +440,7 @@ function NavTopBar({
177
440
  subtitle ? /* @__PURE__ */ jsx("p", { className: "truncate text-sm text-[color:var(--muted)]", children: subtitle }) : null
178
441
  ] }),
179
442
  /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
443
+ actionsList && actionsList.length > 0 ? actionsList.map((action) => /* @__PURE__ */ jsx(NavTopBarActionButton, { action }, action.key)) : null,
180
444
  actions,
181
445
  themeToggle,
182
446
  /* @__PURE__ */ jsxs("details", { className: "group relative", children: [
@@ -225,6 +489,44 @@ function SignOutIcon() {
225
489
  }
226
490
  );
227
491
  }
492
+ function NavTopBarActionButton({ action }) {
493
+ const Icon = action.icon;
494
+ const tone = action.tone ?? "default";
495
+ 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)]";
496
+ const className = [
497
+ "relative flex h-8 w-8 items-center justify-center rounded-full border border-[color:var(--line)] transition",
498
+ toneClass
499
+ ].join(" ");
500
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
501
+ /* @__PURE__ */ jsx(Icon, { size: 20, className: "text-current" }),
502
+ 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
503
+ ] });
504
+ const isExternal = action.href ? /^https?:\/\//.test(action.href) : false;
505
+ if (action.href) {
506
+ return /* @__PURE__ */ jsx(
507
+ "a",
508
+ {
509
+ href: action.href,
510
+ "aria-label": action.label,
511
+ title: action.label,
512
+ className,
513
+ ...isExternal ? { target: "_blank", rel: "noreferrer" } : {},
514
+ children: content
515
+ }
516
+ );
517
+ }
518
+ return /* @__PURE__ */ jsx(
519
+ "button",
520
+ {
521
+ type: "button",
522
+ onClick: action.onClick,
523
+ "aria-label": action.label,
524
+ title: action.label,
525
+ className,
526
+ children: content
527
+ }
528
+ );
529
+ }
228
530
  function NavAppShell({ sidebar, topBar, children }) {
229
531
  return /* @__PURE__ */ jsxs("div", { className: "min-h-screen bg-[color:var(--background)] text-[color:var(--foreground)]", children: [
230
532
  /* @__PURE__ */ jsx(NavSidebar, { ...sidebar }),
@@ -235,6 +537,6 @@ function NavAppShell({ sidebar, topBar, children }) {
235
537
  ] });
236
538
  }
237
539
 
238
- export { NavAppShell, NavSidebar, NavTopBar, envChipClasses, isNavItemActive };
540
+ export { NavAppShell, NavSidebar, NavTopBar, envChipClasses, isNavItemActive, standardPolitogyNav };
239
541
  //# sourceMappingURL=native.js.map
240
542
  //# sourceMappingURL=native.js.map