@the-portland-company/shell 0.3.0 → 0.3.2

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,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.2 — 2026-05-13
4
+
5
+ ### Added
6
+
7
+ - `<ShellChrome userAvatarUrl />` (and matching `<ShellProvider userAvatarUrl />` prop) — app-supplied avatar URL override for the header `<UserMenu />` avatar. When provided, takes precedence over the avatar URL on the Supabase session's `user_metadata.avatar_url`.
8
+
9
+ ### Fixed
10
+
11
+ - `<UserMenu />` was rendering only `user.raw.user_metadata.avatar_url`, which missed app-specific avatar sources like `profile.avatar_url`, memoji selections persisted to localStorage, and branded default URLs. Apps with these fallback chains were seeing the initials placeholder instead of the user's portrait. Pass the resolved URL via `userAvatarUrl` to restore parity with the legacy chrome.
12
+
13
+ ## 0.3.1 — 2026-05-13
14
+
15
+ Closes the last two visual gaps in the shell-v3 chrome vs production.
16
+
17
+ ### Fixed
18
+
19
+ - **Header icon order**: Removed the `devNotesMenu` slot from
20
+ `<Header>`'s icon row. Previously the shell appended DevNotes (the
21
+ "Task" icon) AFTER `headerIconsSlot`, forcing it to the rightmost
22
+ position. That broke production order (Fuel → Stack → Bell → Task →
23
+ Support → Avatar). Apps must now inline DevNotesMenu inside
24
+ `headerIconsSlot` at the position they want. The `devNotesMenu` prop
25
+ on `<ShellChrome>` is still accepted for backward compat but is
26
+ ignored at render time.
27
+ - **Sidebar footer commit info**: Replaced the plain-text
28
+ `"staging v2.34.1 #51e4e3f"` row with a full port of the legacy
29
+ production `CommitCopyButton` (env chip + last-4 SHA + version +
30
+ optional diagnostics icon, plus a second-line byline `| Mon D |
31
+ firstName |`). Clicking the env chip copies the full SHA. Hovering
32
+ shows the full commit message body when `commitLog` is supplied.
33
+
34
+ ### Added
35
+
36
+ - `<CommitCopyButton>` — new exported component. Ported from
37
+ `react/app/src/layouts/AuthenticatedLayout.tsx::CommitCopyButton`
38
+ (lines 3017–3284). Accepts `envNode` (pre-rendered chip),
39
+ `environmentLabel` (default styled chip — green/orange/red for
40
+ local/staging/production), `commit`, `version`, `commitDate`,
41
+ `commitAuthor`, `commitLog`, and `onDiagnosticsClick`. Used by the
42
+ shell's `<Sidebar>` footer; also exported for apps that want to mount
43
+ it elsewhere.
44
+ - `ShellFooterValue.envNode?: ReactNode` — pre-rendered chip override
45
+ (useful when the app already has its own `EnvironmentBanner`).
46
+ - `ShellFooterValue.onDiagnosticsClick?: () => void` — gates the
47
+ diagnostics icon. When unset, the icon is omitted.
48
+
49
+ ### Doc-only
50
+
51
+ - `ShellProviderProps.support` doc no longer claims the shell renders
52
+ a `<SupportRequestButton />` automatically. It never did — apps wire
53
+ their own support trigger via `headerIconsSlot`.
54
+
3
55
  ## 0.3.0 — 2026-05-13
4
56
 
5
57
  Visual-parity rebuild. Ports the legacy production sidebar + header chrome
package/dist/index.cjs CHANGED
@@ -1153,7 +1153,8 @@ function ShellProvider({
1153
1153
  linkedAccounts,
1154
1154
  support,
1155
1155
  devNotesMenu,
1156
- myProfileHref
1156
+ myProfileHref,
1157
+ userAvatarUrl
1157
1158
  }) {
1158
1159
  const [status, setStatus] = react$1.useState("loading");
1159
1160
  const [session, setSession] = react$1.useState(null);
@@ -1195,9 +1196,10 @@ function ShellProvider({
1195
1196
  const chrome = react$1.useMemo(
1196
1197
  () => ({
1197
1198
  devNotesMenu: devNotesMenu ?? null,
1198
- myProfileHref: myProfileHref ?? "/my-profile"
1199
+ myProfileHref: myProfileHref ?? "/my-profile",
1200
+ userAvatarUrl
1199
1201
  }),
1200
- [devNotesMenu, myProfileHref]
1202
+ [devNotesMenu, myProfileHref, userAvatarUrl]
1201
1203
  );
1202
1204
  return /* @__PURE__ */ jsxRuntime.jsx(ShellAuthContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsx(ShellMetaContext.Provider, { value: meta, children: /* @__PURE__ */ jsxRuntime.jsx(ShellOrgContext.Provider, { value: organization ?? null, children: /* @__PURE__ */ jsxRuntime.jsx(ShellLinkedAccountsContext.Provider, { value: linkedAccounts ?? null, children: /* @__PURE__ */ jsxRuntime.jsx(ShellSupportProviderContext.Provider, { value: support ?? null, children: /* @__PURE__ */ jsxRuntime.jsx(ShellModeContext.Provider, { value: null, children: /* @__PURE__ */ jsxRuntime.jsx(ShellNavItemsContext.Provider, { value: null, children: /* @__PURE__ */ jsxRuntime.jsx(ShellNavigationPreferenceContext.Provider, { value: null, children: /* @__PURE__ */ jsxRuntime.jsx(ShellRoleContext.Provider, { value: null, children: /* @__PURE__ */ jsxRuntime.jsx(ShellFooterContext.Provider, { value: null, children: /* @__PURE__ */ jsxRuntime.jsx(ShellChromeContext.Provider, { value: chrome, children: /* @__PURE__ */ jsxRuntime.jsx(react.ChakraProvider, { theme: politogyTheme, children }) }) }) }) }) }) }) }) }) }) }) });
1203
1205
  }
@@ -1284,7 +1286,8 @@ function UserMenu() {
1284
1286
  myProfileHref,
1285
1287
  organizationSettingsHref,
1286
1288
  showContinueOnboarding,
1287
- onContinueOnboarding
1289
+ onContinueOnboarding,
1290
+ userAvatarUrl
1288
1291
  } = useShellChrome();
1289
1292
  const linked = useShellLinkedAccounts();
1290
1293
  const mode = useShellMode();
@@ -1295,7 +1298,7 @@ function UserMenu() {
1295
1298
  const menuListBorderColor = react.useColorModeValue("gray.200", "gray.700");
1296
1299
  if (status !== "authenticated" || !user) return null;
1297
1300
  const displayName = getUserDisplayName(user.raw);
1298
- const avatarUrl = avatarUrlFromUser(user);
1301
+ const avatarUrl = userAvatarUrl ?? avatarUrlFromUser(user);
1299
1302
  const modeConfig = mode?.getModeConfig() ?? null;
1300
1303
  const currentOrg = org?.currentOrg ?? null;
1301
1304
  const linkedAccounts = linked?.linkedAccounts ?? [];
@@ -1450,8 +1453,7 @@ function Header({ onOpen, id }) {
1450
1453
  breadcrumbs,
1451
1454
  headerActions,
1452
1455
  headerIconsSlot,
1453
- brandLogoSrc,
1454
- devNotesMenu
1456
+ brandLogoSrc
1455
1457
  } = useShellChrome();
1456
1458
  const mobileNavBg = react.useColorModeValue("white", "gray.900");
1457
1459
  const mobileNavBorderColor = react.useColorModeValue("gray.200", "gray.700");
@@ -1527,8 +1529,7 @@ function Header({ onOpen, id }) {
1527
1529
  },
1528
1530
  children: [
1529
1531
  headerActions,
1530
- headerIconsSlot,
1531
- devNotesMenu
1532
+ headerIconsSlot
1532
1533
  ]
1533
1534
  }
1534
1535
  ),
@@ -2184,6 +2185,237 @@ function ModePill() {
2184
2185
  ] }) });
2185
2186
  }
2186
2187
  var ModePill_default = ModePill;
2188
+ function ActivityIcon() {
2189
+ return /* @__PURE__ */ jsxRuntime.jsx(
2190
+ "svg",
2191
+ {
2192
+ width: "12",
2193
+ height: "12",
2194
+ viewBox: "0 0 24 24",
2195
+ fill: "none",
2196
+ stroke: "currentColor",
2197
+ strokeWidth: "2",
2198
+ strokeLinecap: "round",
2199
+ strokeLinejoin: "round",
2200
+ "aria-hidden": "true",
2201
+ children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "22 12 18 12 15 21 9 3 6 12 2 12" })
2202
+ }
2203
+ );
2204
+ }
2205
+ function defaultEnvChip(label) {
2206
+ if (!label) return null;
2207
+ const lower = label.toLowerCase();
2208
+ if (lower.includes("prod")) {
2209
+ return { color: "#ef4444", bg: "#fee2e2", border: "#fecaca", name: label };
2210
+ }
2211
+ if (lower.includes("stag")) {
2212
+ return { color: "#f97316", bg: "#ffedd5", border: "#fed7aa", name: label };
2213
+ }
2214
+ if (lower.includes("local") || lower.includes("dev")) {
2215
+ return { color: "#22c55e", bg: "#dcfce7", border: "#bbf7d0", name: label };
2216
+ }
2217
+ return { color: "#6b7280", bg: "#f3f4f6", border: "#e5e7eb", name: label };
2218
+ }
2219
+ var PLACEHOLDER_SHA_VALUES = /* @__PURE__ */ new Set(["unknown", "n/a", "null", "undefined", ""]);
2220
+ function isMeaningfulCommit(value) {
2221
+ if (typeof value !== "string") return false;
2222
+ const trimmed = value.trim();
2223
+ if (!trimmed) return false;
2224
+ return !PLACEHOLDER_SHA_VALUES.has(trimmed.toLowerCase());
2225
+ }
2226
+ function isMeaningfulAuthor(value) {
2227
+ if (typeof value !== "string") return false;
2228
+ const trimmed = value.trim();
2229
+ if (!trimmed) return false;
2230
+ return !PLACEHOLDER_SHA_VALUES.has(trimmed.toLowerCase());
2231
+ }
2232
+ function getFirstName(value) {
2233
+ if (!value) return "";
2234
+ const stripped = value.replace(/<[^>]+>/g, "").trim();
2235
+ if (!stripped) return "";
2236
+ const [firstToken] = stripped.split(/\s+/);
2237
+ return firstToken || stripped;
2238
+ }
2239
+ function CommitCopyButton({
2240
+ envNode,
2241
+ environmentLabel,
2242
+ commit,
2243
+ version = "",
2244
+ commitDate = "",
2245
+ commitAuthor = "",
2246
+ commitLog = "",
2247
+ onDiagnosticsClick
2248
+ }) {
2249
+ const safeCommit = isMeaningfulCommit(commit) ? commit.trim() : "";
2250
+ const { hasCopied, onCopy } = react.useClipboard(safeCommit);
2251
+ const shortCommit = safeCommit ? safeCommit.slice(-4) : "";
2252
+ const copiedAnimation = react$2.keyframes`
2253
+ 0% { opacity: 0; transform: translateY(6px); }
2254
+ 20% { opacity: 1; transform: translateY(0); }
2255
+ 100% { opacity: 0; transform: translateY(-8px); }
2256
+ `;
2257
+ const formattedDate = react$1.useMemo(() => {
2258
+ if (!commitDate) return "";
2259
+ const parsed = new Date(commitDate);
2260
+ if (Number.isNaN(parsed.getTime())) return commitDate;
2261
+ return parsed.toLocaleDateString(void 0, { month: "short", day: "numeric" });
2262
+ }, [commitDate]);
2263
+ const metaLine = react$1.useMemo(() => {
2264
+ const firstName = isMeaningfulAuthor(commitAuthor) ? getFirstName(commitAuthor) : "";
2265
+ return [formattedDate, firstName].filter(Boolean).join(" | ");
2266
+ }, [formattedDate, commitAuthor]);
2267
+ const normalizedCommitLog = react$1.useMemo(() => {
2268
+ if (commitLog?.trim()) {
2269
+ return commitLog.replace(/\r\n/g, "\n").replace(/\\n/g, "\n");
2270
+ }
2271
+ return "";
2272
+ }, [commitLog]);
2273
+ const tooltipContent = normalizedCommitLog.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
2274
+ react.Box,
2275
+ {
2276
+ maxW: "600px",
2277
+ width: "min(600px, 90vw)",
2278
+ maxH: "80vh",
2279
+ whiteSpace: "pre-wrap",
2280
+ overflowY: "auto",
2281
+ overflowX: "auto",
2282
+ fontSize: "xs",
2283
+ fontFamily: "mono",
2284
+ wordBreak: "break-word",
2285
+ children: normalizedCommitLog
2286
+ }
2287
+ ) : void 0;
2288
+ const envChip = react$1.useMemo(() => {
2289
+ if (envNode) return envNode;
2290
+ const chip = defaultEnvChip(environmentLabel);
2291
+ if (!chip) return null;
2292
+ return /* @__PURE__ */ jsxRuntime.jsx(
2293
+ react.Box,
2294
+ {
2295
+ as: "span",
2296
+ display: "inline-flex",
2297
+ alignItems: "center",
2298
+ px: "1.5",
2299
+ py: "0.5",
2300
+ borderWidth: "1px",
2301
+ borderColor: chip.border,
2302
+ bg: chip.bg,
2303
+ color: chip.color,
2304
+ borderRadius: "md",
2305
+ fontSize: "2xs",
2306
+ fontWeight: "600",
2307
+ lineHeight: "1",
2308
+ textTransform: "capitalize",
2309
+ "aria-label": `Environment: ${chip.name}`,
2310
+ children: chip.name
2311
+ }
2312
+ );
2313
+ }, [envNode, environmentLabel]);
2314
+ const showEnv = Boolean(envChip);
2315
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2316
+ react.VStack,
2317
+ {
2318
+ justify: "center",
2319
+ align: "center",
2320
+ borderRadius: "md",
2321
+ py: 0.5,
2322
+ px: 1,
2323
+ position: "relative",
2324
+ spacing: 0,
2325
+ textAlign: "center",
2326
+ w: "full",
2327
+ children: [
2328
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Flex, { justify: "center", align: "center", gap: 2, whiteSpace: "nowrap", children: [
2329
+ showEnv ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2330
+ /* @__PURE__ */ jsxRuntime.jsx(react.Box, { flexShrink: 0, children: /* @__PURE__ */ jsxRuntime.jsx(
2331
+ react.Tooltip,
2332
+ {
2333
+ label: tooltipContent,
2334
+ isDisabled: !tooltipContent,
2335
+ hasArrow: true,
2336
+ placement: "top-start",
2337
+ openDelay: 150,
2338
+ closeOnClick: false,
2339
+ shouldWrapChildren: true,
2340
+ modifiers: [
2341
+ { name: "preventOverflow", options: { padding: 16 } },
2342
+ { name: "offset", options: { offset: [0, 12] } }
2343
+ ],
2344
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2345
+ react.Button,
2346
+ {
2347
+ variant: "unstyled",
2348
+ minW: "auto",
2349
+ h: "auto",
2350
+ p: 0,
2351
+ lineHeight: "1",
2352
+ color: "inherit",
2353
+ onClick: onCopy,
2354
+ "aria-label": "Copy commit ID",
2355
+ isDisabled: !safeCommit,
2356
+ children: envChip
2357
+ }
2358
+ )
2359
+ }
2360
+ ) }),
2361
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "2xs", color: "gray.300", flexShrink: 0, children: "|" })
2362
+ ] }) : null,
2363
+ shortCommit ? /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "2xs", color: hasCopied ? "green.500" : "gray.400", flexShrink: 0, children: shortCommit }) : null,
2364
+ version ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2365
+ shortCommit ? /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "2xs", color: "gray.300", flexShrink: 0, children: "|" }) : null,
2366
+ /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { fontSize: "2xs", color: "gray.400", flexShrink: 0, children: [
2367
+ "v",
2368
+ version
2369
+ ] })
2370
+ ] }) : null,
2371
+ onDiagnosticsClick ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2372
+ /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontSize: "2xs", color: "gray.300", flexShrink: 0, children: "|" }),
2373
+ /* @__PURE__ */ jsxRuntime.jsx(react.Tooltip, { label: "Diagnostics", hasArrow: true, placement: "top", children: /* @__PURE__ */ jsxRuntime.jsx(
2374
+ react.Button,
2375
+ {
2376
+ variant: "unstyled",
2377
+ minW: "auto",
2378
+ h: "auto",
2379
+ p: 0,
2380
+ lineHeight: "1",
2381
+ color: "gray.400",
2382
+ _hover: { color: "gray.600" },
2383
+ onClick: (e) => {
2384
+ e.stopPropagation();
2385
+ onDiagnosticsClick();
2386
+ },
2387
+ "aria-label": "Diagnostics",
2388
+ children: /* @__PURE__ */ jsxRuntime.jsx(react.Icon, { as: ActivityIcon, boxSize: 3 })
2389
+ }
2390
+ ) })
2391
+ ] }) : null
2392
+ ] }),
2393
+ metaLine ? /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { fontSize: "2xs", color: "gray.500", textAlign: "center", whiteSpace: "nowrap", children: [
2394
+ "| ",
2395
+ metaLine,
2396
+ " |"
2397
+ ] }) : null,
2398
+ hasCopied && /* @__PURE__ */ jsxRuntime.jsx(
2399
+ react.Box,
2400
+ {
2401
+ position: "absolute",
2402
+ bottom: "100%",
2403
+ mb: "6px",
2404
+ px: "2",
2405
+ py: "1",
2406
+ fontSize: "xs",
2407
+ bg: "gray.700",
2408
+ color: "white",
2409
+ borderRadius: "md",
2410
+ animation: `${copiedAnimation} 0.9s ease-out`,
2411
+ pointerEvents: "none",
2412
+ children: "Copied"
2413
+ }
2414
+ )
2415
+ ]
2416
+ }
2417
+ );
2418
+ }
2187
2419
  function Sidebar({ onClose, id }) {
2188
2420
  const { brandLogoSrc } = useShellChrome();
2189
2421
  const navItems = useShellNavItems()?.items ?? [];
@@ -2316,17 +2548,19 @@ function Sidebar({ onClose, id }) {
2316
2548
  }
2317
2549
  )
2318
2550
  ] }, `${l.label}-${idx}`)) }),
2319
- (footer.version || footer.commit || footer.environmentLabel) && /* @__PURE__ */ jsxRuntime.jsxs(react.HStack, { spacing: 2, fontSize: "xs", color: "gray.500", wrap: "wrap", children: [
2320
- footer.environmentLabel ? /* @__PURE__ */ jsxRuntime.jsx(react.Text, { fontWeight: "semibold", children: footer.environmentLabel }) : null,
2321
- footer.version ? /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { children: [
2322
- "v",
2323
- footer.version
2324
- ] }) : null,
2325
- footer.commit ? /* @__PURE__ */ jsxRuntime.jsxs(react.Text, { fontFamily: "mono", children: [
2326
- "#",
2327
- footer.commit.slice(-7)
2328
- ] }) : null
2329
- ] })
2551
+ (footer.version || footer.commit || footer.commitFull || footer.environmentLabel || footer.envNode) && /* @__PURE__ */ jsxRuntime.jsx(
2552
+ CommitCopyButton,
2553
+ {
2554
+ envNode: footer.envNode,
2555
+ environmentLabel: footer.environmentLabel,
2556
+ commit: footer.commitFull ?? footer.commit,
2557
+ version: footer.version,
2558
+ commitDate: footer.commitDate,
2559
+ commitAuthor: footer.commitAuthor,
2560
+ commitLog: footer.commitLog,
2561
+ onDiagnosticsClick: footer.onDiagnosticsClick
2562
+ }
2563
+ )
2330
2564
  ] })
2331
2565
  }
2332
2566
  )
@@ -2680,7 +2914,8 @@ function ShellChrome({
2680
2914
  onContinueOnboarding,
2681
2915
  pageTitle,
2682
2916
  breadcrumbs,
2683
- headerActions
2917
+ headerActions,
2918
+ userAvatarUrl
2684
2919
  }) {
2685
2920
  const chrome = react$1.useMemo(
2686
2921
  () => ({
@@ -2693,7 +2928,8 @@ function ShellChrome({
2693
2928
  onContinueOnboarding,
2694
2929
  pageTitle,
2695
2930
  breadcrumbs,
2696
- headerActions
2931
+ headerActions,
2932
+ userAvatarUrl
2697
2933
  }),
2698
2934
  [
2699
2935
  devNotesMenu,
@@ -2705,7 +2941,8 @@ function ShellChrome({
2705
2941
  onContinueOnboarding,
2706
2942
  pageTitle,
2707
2943
  breadcrumbs,
2708
- headerActions
2944
+ headerActions,
2945
+ userAvatarUrl
2709
2946
  ]
2710
2947
  );
2711
2948
  const navItemsValue = react$1.useMemo(
@@ -2750,6 +2987,7 @@ exports.BrandIcon = BrandIcon;
2750
2987
  exports.BrandLogo = BrandLogo;
2751
2988
  exports.BreadcrumbBar = BreadcrumbBar;
2752
2989
  exports.Card = Card;
2990
+ exports.CommitCopyButton = CommitCopyButton;
2753
2991
  exports.Footer = Footer;
2754
2992
  exports.GlobalUiIdProvider = GlobalUiIdProvider;
2755
2993
  exports.Header = Header;