@the-portland-company/shell 0.67.0 → 0.68.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/chrome.css CHANGED
@@ -150,11 +150,12 @@
150
150
  --tab-radius: 12px;
151
151
 
152
152
  /* Sidebar (nav chrome) */
153
- --sidebar-width: 240px;
153
+ --sidebar-width: 236px;
154
154
  --sidebar-bg: #FFFFFF;
155
155
  --sidebar-border: #E2E8F0;
156
- --sidebar-brand-logo-height: 40px;
157
- --sidebar-org-logo-max-height: 70px;
156
+ --sidebar-brand-logo-height: 24px;
157
+ --sidebar-org-logo-max-height: 34px;
158
+ --sidebar-org-band-height: 48px;
158
159
  --sidebar-item-radius: 8px;
159
160
  --sidebar-item-padding-x: 16px;
160
161
  --sidebar-item-padding-y: 10px;
@@ -169,16 +170,20 @@
169
170
  --sidebar-scrollbar-thumb: #CBD5E0;
170
171
 
171
172
  /* Topbar */
172
- --topbar-height: 80px;
173
+ --topbar-height: 60px;
173
174
  --topbar-bg: #FFFFFF;
174
175
  --topbar-border: #E2E8F0;
175
- --topbar-padding-x: 16px;
176
+ --topbar-padding-x: 20px;
176
177
  --topbar-title-size: 18px;
177
178
  --topbar-title-weight: 700;
178
179
  --topbar-title-color: #2F3136;
179
180
  --topbar-breadcrumb-color: #718096;
180
- --topbar-icon-button-size: 32px;
181
- --topbar-icon-size: 20px;
181
+ --topbar-icon-button-size: 34px;
182
+ --topbar-icon-button-radius: 5px;
183
+ --topbar-icon-gap: 6px;
184
+ --topbar-icon-size: 17px;
185
+ /* Sunken field surface (universal search) — design `--surface-sunken`. */
186
+ --surface-sunken: #F0F4F7;
182
187
 
183
188
  /* Shell chrome: brand color ramps (= Specs race-red) */
184
189
  --app-color-brand-50: #FDECEE;
@@ -390,6 +395,30 @@
390
395
  --sidebar-text-muted: #B5B7BB;
391
396
  --sidebar-meta-text: #9A9CA0;
392
397
  --sidebar-hover-bg: #2A2B30;
398
+ /* Selected mode button: the Mode accent, graphite for Relationship. */
399
+ --sidebar-mode-selected-bg: var(--mode-accent, #2F3136);
400
+ }
401
+ /* Specs/Contacts design rail (ink-900 rail): hairlines and type washes are
402
+ white at fixed alphas; cards sit on ink-800. These are NOT mode-scoped —
403
+ the rail is dark in every app and every theme (the first cut put them under
404
+ `[data-politogy-mode]`, which an unscoped app such as Contacts never
405
+ stamps, so its light theme drew the org card in the light panel colour with
406
+ white text and the active row in solid brand red). None of these names is
407
+ defined by the runtime Brand sheet, so `:root` here is authoritative; the
408
+ `.dark` block below does not touch them either. */
409
+ :root {
410
+ --sidebar-line: rgba(255, 255, 255, 0.16);
411
+ --border-inverse: rgba(255, 255, 255, 0.16);
412
+ --sidebar-eyebrow: rgba(255, 255, 255, 0.38);
413
+ --sidebar-caption: rgba(255, 255, 255, 0.5);
414
+ --sidebar-row-text: rgba(255, 255, 255, 0.66);
415
+ --sidebar-row-hover-bg: rgba(255, 255, 255, 0.05);
416
+ --sidebar-active-bg: rgba(255, 255, 255, 0.09);
417
+ --sidebar-active-text: #FFFFFF;
418
+ --sidebar-card-bg: #232428;
419
+ --sidebar-avatar-bg: #3B3E44;
420
+ --mode-track-bg: #232428;
421
+ --sidebar-mode-selected-bg: #2F3136;
393
422
  }
394
423
 
395
424
  [data-politogy-mode="campaign"] {
@@ -410,16 +439,16 @@
410
439
  --------------------------------------------------------------------------- */
411
440
  .topbar-widget-slot button {
412
441
  position: relative !important;
413
- height: var(--topbar-icon-button-size, 32px) !important;
414
- min-height: var(--topbar-icon-button-size, 32px) !important;
415
- width: var(--topbar-icon-button-size, 32px) !important;
416
- min-width: var(--topbar-icon-button-size, 32px) !important;
442
+ height: var(--topbar-icon-button-size, 34px) !important;
443
+ min-height: var(--topbar-icon-button-size, 34px) !important;
444
+ width: var(--topbar-icon-button-size, 34px) !important;
445
+ min-width: var(--topbar-icon-button-size, 34px) !important;
417
446
  padding: 0 !important;
418
447
  display: inline-flex !important;
419
448
  align-items: center !important;
420
449
  justify-content: center !important;
421
450
  border: 1px solid var(--line) !important;
422
- border-radius: 9999px !important;
451
+ border-radius: var(--topbar-icon-button-radius, 5px) !important;
423
452
  background: transparent !important;
424
453
  color: var(--topbar-icon-color, var(--muted)) !important;
425
454
  }
@@ -447,8 +476,8 @@
447
476
  otherwise win. Keep these in lockstep with TopBarCountBadge. */
448
477
  .topbar-widget-slot button > span > span {
449
478
  position: absolute !important;
450
- top: -4px !important;
451
- right: -4px !important;
479
+ top: -5px !important;
480
+ right: -5px !important;
452
481
  left: auto !important;
453
482
  bottom: auto !important;
454
483
  display: inline-flex !important;
@@ -463,7 +492,58 @@
463
492
  background: var(--chrome-danger, #e63946) !important;
464
493
  color: #fff !important;
465
494
  font-size: 10px !important;
466
- font-weight: 400 !important;
467
- line-height: 1 !important;
495
+ font-weight: 700 !important;
496
+ line-height: 1rem !important;
468
497
  box-shadow: none !important;
469
498
  }
499
+
500
+ /* ---------------------------------------------------------------------------
501
+ Dark theme — `.dark` / `[data-theme="dark"]` on <html> (applied by
502
+ `theme-preference` from the politogy_theme cookie). Neutral-black surfaces
503
+ per Reference/Global-Rules → Dark Mode (neutral black, never tinted); the
504
+ rail is already ink in every Mode so only the page/top-bar/popover tokens
505
+ move. Specs/Contacts Visual Regression gates light AND dark.
506
+ --------------------------------------------------------------------------- */
507
+ .dark,
508
+ [data-theme="dark"] {
509
+ color-scheme: dark;
510
+ --surface-1: #1B1C20;
511
+ --surface-2: #232428;
512
+ --surface-sunken: #141518;
513
+ --sidebar-bg: #1B1C20;
514
+ --sidebar-border: rgba(255, 255, 255, 0.16);
515
+ --sidebar-text: #F5F5F5;
516
+ --sidebar-text-muted: #B5B7BB;
517
+ --sidebar-meta-text: #9A9CA0;
518
+ --topbar-bg: #1B1C20;
519
+ --topbar-border: rgba(255, 255, 255, 0.16);
520
+ --topbar-title-color: #F5F5F5;
521
+ --topbar-breadcrumb-color: #B5B7BB;
522
+ --panel: var(--topbar-bg);
523
+ --panel-elevated: var(--surface-2);
524
+ --nav-surface: var(--sidebar-bg);
525
+ --line: var(--topbar-border);
526
+ --chrome-page-bg: var(--surface-sunken);
527
+ --color-bg: var(--surface-sunken);
528
+ --color-surface: var(--surface-1);
529
+ --color-border: var(--topbar-border);
530
+ --color-text: var(--topbar-title-color);
531
+ --nav-submenu-bg: var(--surface-2);
532
+ --beta-banner-bg: #3A2F14;
533
+ --beta-banner-text: #FDE8C8;
534
+ }
535
+
536
+ /* The runtime Brand sheet (specs /api/tokens?format=css, appended AFTER this
537
+ file by every consumer) ends its own `.dark {}` with shadcn-style HSL
538
+ triples — `--foreground: 210 40% 98%`, `--muted: 217.2 32.6% 17.5%`,
539
+ `--background: 222.2 84% 4.9%`. The native chrome reads those names as full
540
+ colors (`text-[color:var(--foreground)]`), so in dark they were invalid at
541
+ computed-value time and every label fell to the inherited black. `html.dark`
542
+ outranks a bare `.dark` whatever the source order, and the aliases point at
543
+ the Brand sheet's own dark values so Brand stays the source of truth. */
544
+ html.dark,
545
+ html[data-theme="dark"] {
546
+ --background: var(--surface-1);
547
+ --foreground: var(--topbar-title-color);
548
+ --muted: var(--topbar-breadcrumb-color);
549
+ }
@@ -56,6 +56,15 @@ function clearPendingThemeChoice() {
56
56
  writeSharedCookie(THEME_PENDING_COOKIE, "", 0);
57
57
  }
58
58
  function resolveTheme(pref) {
59
+ if (pref === "dark") return "dark";
60
+ if (pref === "system") {
61
+ try {
62
+ if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
63
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
64
+ }
65
+ } catch {
66
+ }
67
+ }
59
68
  return "light";
60
69
  }
61
70
  function readStoredThemePreference() {
@@ -73,10 +82,11 @@ function readResolvedTheme() {
73
82
  }
74
83
  function applyThemePreference(pref) {
75
84
  if (typeof document === "undefined") return;
76
- const resolved = resolveTheme();
85
+ const resolved = resolveTheme(pref);
77
86
  const d = document.documentElement;
78
- d.classList.remove("dark");
87
+ d.classList.toggle("dark", resolved === "dark");
79
88
  try {
89
+ d.setAttribute("data-theme", resolved);
80
90
  d.style.colorScheme = resolved;
81
91
  } catch {
82
92
  }
@@ -90,13 +100,12 @@ var ls=null;try{ls=localStorage.getItem(KEY);}catch(e){}
90
100
  var pref=cookie||ls||"system";
91
101
  if(VALID.indexOf(pref)===-1)pref="system";
92
102
  if(cookie&&cookie!==ls){try{localStorage.setItem(KEY,cookie);}catch(e){}}
93
- // Dark Mode retired platform-wide 2026-09-21 \u2014 always light, and strip any
94
- // stale dark class a pre-retirement cookie/localStorage value left behind.
103
+ var dark=pref==="dark"||(pref==="system"&&!!(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches));
95
104
  var d=document.documentElement;
96
- d.classList.remove("dark");
97
- try{d.style.colorScheme="light";}catch(e){}
105
+ if(dark)d.classList.add("dark");else d.classList.remove("dark");
106
+ try{d.setAttribute("data-theme",dark?"dark":"light");d.style.colorScheme=dark?"dark":"light";}catch(e){}
98
107
  }catch(e){}})();`;
99
108
 
100
109
  export { THEME_COOKIE, THEME_PENDING_COOKIE, THEME_PREPAINT_SCRIPT, THEME_STORAGE_KEY, applyThemePreference, clearPendingThemeChoice, isThemePreference, markThemeChoicePending, readPendingThemeChoice, readResolvedTheme, readStoredThemePreference, readThemeCookie, resolveTheme, writeThemeCookie };
101
- //# sourceMappingURL=chunk-JHIFJZ73.js.map
102
- //# sourceMappingURL=chunk-JHIFJZ73.js.map
110
+ //# sourceMappingURL=chunk-NHBNM5P3.js.map
111
+ //# sourceMappingURL=chunk-NHBNM5P3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/theme-preference.ts"],"names":[],"mappings":"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","file":"chunk-NHBNM5P3.js","sourcesContent":["// Universal theme preference — the ONE cross-app contract.\n//\n// One theme choice (Light / Dark / System) follows the authenticated user\n// across every Politogy app + subdomain. Three layers cooperate:\n// 1. VRM Core `profiles.theme_preference` — source of truth, persisted via\n// the chrome API (`PATCH me/preferences { themePreference }`) and read\n// back from `GET me` (`themePreference`).\n// 2. `.politogyvrm.com` cookie `politogy_theme` — zero-flash pre-paint\n// vehicle that crosses subdomains (auth session cookies are host-only).\n// 3. localStorage `politogy_theme` — offline fallback store.\n//\n// Ported verbatim from contacts/apps/web/src/lib/theme.ts (the origin of the\n// contract) so every app runs the identical behavior from npm. This module is\n// server-safe (no 'use client'): layouts import THEME_PREPAINT_SCRIPT for the\n// pre-paint inline script. The client-side hook lives in ./native\n// (useThemePreference).\n\nexport type ThemePreference = 'light' | 'dark' | 'system'\n\n// Single converged key for BOTH the localStorage store and the\n// cross-subdomain cookie, so a choice in one app is the same key in the next.\nexport const THEME_STORAGE_KEY = 'politogy_theme'\nexport const THEME_COOKIE = 'politogy_theme'\n\n// A local choice that has NOT yet reached VRM Core `profiles.theme_preference`.\n//\n// Politogy ID (auth.politogyvrm.com) has no chrome-API session to PATCH, so a\n// theme picked on the login screen only ever lands in the cookie. Without this\n// marker the next app booted the stale profile value through the reconcile step\n// below and overwrote the user's fresh choice — the \"I set System and it keeps\n// going back to Light\" bug (HITL 2026-07-23). The flag rides the same\n// cross-subdomain cookie so the first app that CAN talk to the chrome API\n// pushes the choice up instead of pulling the stale one down.\nexport const THEME_PENDING_COOKIE = 'politogy_theme_pending'\n\nconst VALID: readonly ThemePreference[] = ['light', 'dark', 'system']\n\nexport function isThemePreference(v: unknown): v is ThemePreference {\n return typeof v === 'string' && (VALID as readonly string[]).includes(v)\n}\n\n/** Read the shared cookie (browser only; null on the server or when unset). */\nexport function readThemeCookie(): ThemePreference | null {\n if (typeof document === 'undefined') return null\n try {\n const m = document.cookie.match(\n new RegExp(`(?:^|;\\\\s*)${THEME_COOKIE}=([^;]+)`),\n )\n const v = m ? decodeURIComponent(m[1]) : null\n return isThemePreference(v) ? v : null\n } catch {\n return null\n }\n}\n\n// Set the cross-subdomain cookie. On a *.politogyvrm.com host we scope it to\n// the parent domain so every app subdomain reads it; on any other host\n// (localhost, *.pages.dev preview) we fall back to a host-only cookie so\n// local/preview theming still works without a mismatched Domain attribute\n// (which browsers silently drop). 1-year, SameSite=Lax, Secure on https.\nexport function writeThemeCookie(pref: ThemePreference): void {\n if (typeof document === 'undefined' || typeof window === 'undefined') return\n const host = window.location.hostname\n const onParentDomain = /(^|\\.)politogyvrm\\.com$/i.test(host)\n const domainAttr = onParentDomain ? '; Domain=.politogyvrm.com' : ''\n const secure = window.location.protocol === 'https:' ? '; Secure' : ''\n document.cookie = `${THEME_COOKIE}=${encodeURIComponent(\n pref,\n )}; Path=/; Max-Age=31536000; SameSite=Lax${domainAttr}${secure}`\n}\n\n// Write an arbitrary cross-subdomain cookie with the same scoping rules as the\n// theme cookie itself (parent domain on *.politogyvrm.com, host-only elsewhere).\nfunction writeSharedCookie(name: string, value: string, maxAgeSeconds: number): void {\n if (typeof document === 'undefined' || typeof window === 'undefined') return\n const host = window.location.hostname\n const domainAttr = /(^|\\.)politogyvrm\\.com$/i.test(host) ? '; Domain=.politogyvrm.com' : ''\n const secure = window.location.protocol === 'https:' ? '; Secure' : ''\n document.cookie = `${name}=${encodeURIComponent(\n value,\n )}; Path=/; Max-Age=${maxAgeSeconds}; SameSite=Lax${domainAttr}${secure}`\n}\n\n/**\n * Mark the current cookie value as a local choice that still has to reach the\n * user's profile. Call this from any surface that changes the theme WITHOUT a\n * chrome-API session (Politogy ID's login screen is the only one today).\n */\nexport function markThemeChoicePending(pref: ThemePreference): void {\n writeSharedCookie(THEME_PENDING_COOKIE, pref, 31536000)\n}\n\n/** The pending (not-yet-persisted) local choice, if any. */\nexport function readPendingThemeChoice(): ThemePreference | null {\n if (typeof document === 'undefined') return null\n try {\n const m = document.cookie.match(\n new RegExp(`(?:^|;\\\\s*)${THEME_PENDING_COOKIE}=([^;]+)`),\n )\n const v = m ? decodeURIComponent(m[1]) : null\n return isThemePreference(v) ? v : null\n } catch {\n return null\n }\n}\n\n/** Clear the pending marker once the choice has been written to the profile. */\nexport function clearPendingThemeChoice(): void {\n writeSharedCookie(THEME_PENDING_COOKIE, '', 0)\n}\n\n/** Resolve a preference to the concrete light/dark it renders as right now. */\nexport function resolveTheme(pref: ThemePreference): 'light' | 'dark' {\n // Dark is honoured again from 0.68.0 (Specs/Contacts Visual Regression\n // gate runs light AND dark; the chrome ships a `.dark` token block). An\n // explicit 'dark' resolves dark; 'system' follows the OS; anything else —\n // and any environment without matchMedia — is light.\n if (pref === 'dark') return 'dark'\n if (pref === 'system') {\n try {\n if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'\n }\n } catch {\n /* fall through */\n }\n }\n return 'light'\n}\n\n/**\n * Read the stored preference the way the pre-paint script does — cookie →\n * localStorage → `'system'`. Kept in lockstep with THEME_PREPAINT_SCRIPT so a\n * runtime reader can never disagree with what was applied before paint.\n */\nexport function readStoredThemePreference(): ThemePreference {\n const cookie = readThemeCookie()\n if (cookie) return cookie\n try {\n const stored = window.localStorage.getItem(THEME_STORAGE_KEY)\n if (isThemePreference(stored)) return stored\n } catch {\n /* localStorage may be unavailable (private mode) */\n }\n return 'system'\n}\n\n/**\n * The concrete light/dark the document is rendering as right now.\n *\n * This is the value anything theme-derived must follow — notably Chakra's\n * `colorMode`. Chakra used to be pinned to light while this resolved to dark,\n * which rendered dark chrome tokens over light Chakra content (the \"dark\n * labels on a light page\" desync).\n */\nexport function readResolvedTheme(): 'light' | 'dark' {\n return resolveTheme(readStoredThemePreference())\n}\n\n/** Apply a preference to the document: toggles `.dark` + `data-theme` on\n * `<html>` (both selectors the chrome's dark token block matches) and sets\n * `color-scheme` so native controls follow. */\nexport function applyThemePreference(pref: ThemePreference): void {\n if (typeof document === 'undefined') return\n const resolved = resolveTheme(pref)\n const d = document.documentElement\n d.classList.toggle('dark', resolved === 'dark')\n try {\n d.setAttribute('data-theme', resolved)\n d.style.colorScheme = resolved\n } catch {\n /* non-fatal */\n }\n}\n\n// Inline pre-paint script — render it in <head> (or as the first child of\n// <body>) so the theme applies before first paint. Resolves the preference\n// from cookie → localStorage → OS, applies the `.dark` class + `color-scheme`,\n// and seeds localStorage from the cookie so the app hydrates to the synced\n// value (the cross-subdomain flash fix). Self-contained, dependency-free, and\n// wrapped so any failure is silent (never blocks paint).\nexport const THEME_PREPAINT_SCRIPT = `(function(){try{\nvar KEY=${JSON.stringify(THEME_STORAGE_KEY)};\nvar VALID=[\"light\",\"dark\",\"system\"];\nfunction fromCookie(){try{var m=document.cookie.match(/(?:^|;\\\\s*)${THEME_COOKIE}=([^;]+)/);return m?decodeURIComponent(m[1]):null;}catch(e){return null;}}\nvar cookie=fromCookie();\nvar ls=null;try{ls=localStorage.getItem(KEY);}catch(e){}\nvar pref=cookie||ls||\"system\";\nif(VALID.indexOf(pref)===-1)pref=\"system\";\nif(cookie&&cookie!==ls){try{localStorage.setItem(KEY,cookie);}catch(e){}}\nvar dark=pref===\"dark\"||(pref===\"system\"&&!!(window.matchMedia&&window.matchMedia(\"(prefers-color-scheme: dark)\").matches));\nvar d=document.documentElement;\nif(dark)d.classList.add(\"dark\");else d.classList.remove(\"dark\");\ntry{d.setAttribute(\"data-theme\",dark?\"dark\":\"light\");d.style.colorScheme=dark?\"dark\":\"light\";}catch(e){}\n}catch(e){}})();`\n"]}