@the-portland-company/shell 0.94.0 → 0.96.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/app-chrome.cjs +105 -46
  3. package/dist/app-chrome.cjs.map +1 -1
  4. package/dist/app-chrome.d.cts +2 -1
  5. package/dist/app-chrome.d.ts +2 -1
  6. package/dist/app-chrome.js +6 -50
  7. package/dist/app-chrome.js.map +1 -1
  8. package/dist/app-icons.cjs +9 -0
  9. package/dist/app-icons.cjs.map +1 -1
  10. package/dist/app-icons.d.cts +5 -3
  11. package/dist/app-icons.d.ts +5 -3
  12. package/dist/app-icons.js +9 -1
  13. package/dist/app-icons.js.map +1 -1
  14. package/dist/brand-favicon.cjs +87 -0
  15. package/dist/brand-favicon.cjs.map +1 -0
  16. package/dist/brand-favicon.d.cts +18 -0
  17. package/dist/brand-favicon.d.ts +18 -0
  18. package/dist/brand-favicon.js +4 -0
  19. package/dist/brand-favicon.js.map +1 -0
  20. package/dist/{chunk-JQ53VZO5.js → chunk-5OTXPYZU.js} +257 -5
  21. package/dist/chunk-5OTXPYZU.js.map +1 -0
  22. package/dist/chunk-BMR5WAWH.js +111 -0
  23. package/dist/chunk-BMR5WAWH.js.map +1 -0
  24. package/dist/{chunk-MCMD4WR6.js → chunk-QGNJLKT5.js} +4 -4
  25. package/dist/{chunk-MCMD4WR6.js.map → chunk-QGNJLKT5.js.map} +1 -1
  26. package/dist/{index-ByUbBbta.d.cts → index-3YFy4JHI.d.cts} +47 -1
  27. package/dist/{index-PRu56mmW.d.ts → index-CqHKy3vG.d.ts} +47 -1
  28. package/dist/index.cjs +336 -2
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +3 -2
  31. package/dist/index.d.ts +3 -2
  32. package/dist/index.js +3 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/native.cjs +332 -0
  35. package/dist/native.cjs.map +1 -1
  36. package/dist/native.d.cts +2 -1
  37. package/dist/native.d.ts +2 -1
  38. package/dist/native.js +4 -3
  39. package/package.json +6 -1
  40. package/dist/chunk-JQ53VZO5.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## 0.96.0
2
+
3
+ One shared "⋯" row menu for list rows (Ben 2026-10-26: Email's broadcast menu stuck open, and on the last row Archive and Delete were cut off by the table frame).
4
+
5
+ - **New `RowMenu`** (root and `/native`): the trigger button plus a panel rendered in a portal on `<body>` with fixed positioning, so a table wrapper or card with `overflow: hidden/auto` never clips it. It opens below the trigger, right-aligned, and flips above when it does not fit below. It closes on an outside click, on Escape (focus back on the trigger), after a `role="menuitem"` is chosen, when another row's menu opens, and on page scroll or resize. Arrow keys, Home and End move between items. Clicks on the trigger and in the panel do not bubble to the row (React carries portal events to ancestors), so a clickable row does not open too. Clicks on anything that is not a menu item (an inline form expanding in the panel) keep it open; the close after a choice waits a tick so a submit button's form still submits. Pass `triggerClassName` / `panelClassName` to use an app's own classes instead of the default token look.
6
+ - **New `RowMenuItem`**: a plain button item (`onSelect`, `disabled`, `danger`).
7
+ - Also exported: `rowMenuPosition()` (the placement math) and the `RowMenuPlacement` type.
8
+ - Tests: `__tests__/rowMenu.test.tsx`.
9
+
10
+ ## 0.95.0
11
+
12
+ Every app's browser tab shows the Brand's VRM tile (Brand `assets.favicon`; Reference/UI-Platform conformance: "Favicons resolve from Brand `assets.favicon`, not static files").
13
+
14
+ - **New `BrandFavicon`** (root and `/native`), rendered by `PolitogyAppFrame`, so every app on the frame inherits it with no code change. After mount it reads `--asset-favicon` and `--asset-apple-icon` from the runtime token sheet (`/api/tokens?format=css`, already loaded by the browser in every app) and sets `<link rel="icon">` and `<link rel="apple-touch-icon">`. The link is kept the last icon link in `<head>`, so a stale metadata icon or a leftover static file never wins. Draws nothing until the sheet defines the asset.
15
+ - Why: `politogyAppIcons()` resolves icons on the server, and in production Cloudflare's bot check answers the Worker's request to `specs.politogyvrm.com/api/icons/<app>` with a challenge, so the metadata had no icon and tabs showed the browser default or an app's leftover Next.js `favicon.ico` triangle.
16
+ - **`politogyAppIcons()` fallback:** when the API can't be reached or has no favicon, it now returns `SPECS_FAVICON_URL` (`https://specs.politogyvrm.com/api/favicon`, Specs' public 302 to Brand `assets.favicon`; `?kind=apple` for the iOS icon) instead of no entries. The icon link is in the server HTML, so the browser never asks for `/favicon.ico` and never flashes a blank tab. Exported: `SPECS_FAVICON_URL`.
17
+ - Also exported: `applyBrandFavicon()`, `watchBrandFavicon()`, `BRAND_FAVICON_LINK_ID`, `BRAND_APPLE_ICON_LINK_ID`, for apps outside the frame.
18
+ - **New subpath `@the-portland-company/shell/brand-favicon`** (client entry, React only): import `BrandFavicon` from it in a root layout to cover pages that do not render the frame (public forms, surveys, polls), without pulling in `./native`.
19
+ - Apps should delete any `app/favicon.ico` or `app/icon.svg` they still carry.
20
+ - Tests: `__tests__/brandFavicon.test.tsx`.
21
+
1
22
  ## 0.94.0
2
23
 
3
24
  One shared look for the three "no AI Credits" states (VRM-Admin/Pricing-and-Metering §8.6 and §8.10.3; Ben's go on the AI metering plan, 2026-10-24).
@@ -2176,6 +2176,110 @@ var NAV_DISPLAY_OPTIONS = [
2176
2176
  { value: "tabs", label: "Tabs Only" },
2177
2177
  { value: "both", label: "Both" }
2178
2178
  ];
2179
+ function parseCssUrl(value) {
2180
+ if (!value) return null;
2181
+ const m = /^url\(\s*(['"]?)(.*?)\1\s*\)$/i.exec(value.trim());
2182
+ const url = m?.[2]?.trim();
2183
+ return url && /^https:\/\//i.test(url) ? url : null;
2184
+ }
2185
+ function readBrandAsset(kind) {
2186
+ if (typeof document === "undefined") return null;
2187
+ try {
2188
+ return parseCssUrl(
2189
+ window.getComputedStyle(document.documentElement).getPropertyValue(`--asset-${kind}`)
2190
+ );
2191
+ } catch {
2192
+ return null;
2193
+ }
2194
+ }
2195
+ var EMPTY = { logoHorizontal: null, logoSquare: null };
2196
+ var RETRY_MS = [0, 250, 1e3, 3e3];
2197
+ function useBrandAssets() {
2198
+ const [assets, setAssets] = react.useState(EMPTY);
2199
+ react.useEffect(() => {
2200
+ let done = false;
2201
+ const read = () => {
2202
+ if (done) return;
2203
+ const next = {
2204
+ logoHorizontal: readBrandAsset("logo-horizontal"),
2205
+ logoSquare: readBrandAsset("logo-square")
2206
+ };
2207
+ setAssets(
2208
+ (prev) => prev.logoHorizontal === next.logoHorizontal && prev.logoSquare === next.logoSquare ? prev : next
2209
+ );
2210
+ };
2211
+ const links = Array.from(
2212
+ document.querySelectorAll('link[rel="stylesheet"][href*="api/tokens"]')
2213
+ );
2214
+ links.forEach((l) => l.addEventListener("load", read));
2215
+ const timers = RETRY_MS.map((ms) => window.setTimeout(read, ms));
2216
+ read();
2217
+ return () => {
2218
+ done = true;
2219
+ links.forEach((l) => l.removeEventListener("load", read));
2220
+ timers.forEach((t) => window.clearTimeout(t));
2221
+ };
2222
+ }, []);
2223
+ return assets;
2224
+ }
2225
+
2226
+ // src/lib/brandFavicon.ts
2227
+ var BRAND_FAVICON_LINK_ID = "politogy-brand-favicon";
2228
+ var BRAND_APPLE_ICON_LINK_ID = "politogy-brand-apple-icon";
2229
+ function upsertLink(id, rel, href) {
2230
+ let link = document.getElementById(id);
2231
+ if (!link) {
2232
+ link = document.createElement("link");
2233
+ link.id = id;
2234
+ link.rel = rel;
2235
+ }
2236
+ if (link.getAttribute("href") !== href) link.href = href;
2237
+ const icons = document.head.querySelectorAll(`link[rel~="${rel.split(" ")[0]}"]`);
2238
+ if (icons[icons.length - 1] !== link) document.head.appendChild(link);
2239
+ }
2240
+ function applyBrandFavicon() {
2241
+ if (typeof document === "undefined") return false;
2242
+ const favicon = readBrandAsset("favicon");
2243
+ if (!favicon) return false;
2244
+ upsertLink(BRAND_FAVICON_LINK_ID, "icon", favicon);
2245
+ const apple = readBrandAsset("apple-icon");
2246
+ if (apple) upsertLink(BRAND_APPLE_ICON_LINK_ID, "apple-touch-icon", apple);
2247
+ return true;
2248
+ }
2249
+ var RETRY_MS2 = [0, 250, 1e3, 3e3];
2250
+ function watchBrandFavicon() {
2251
+ if (typeof document === "undefined") return () => {
2252
+ };
2253
+ let done = false;
2254
+ const apply = () => {
2255
+ if (!done) applyBrandFavicon();
2256
+ };
2257
+ const sheets = /* @__PURE__ */ new Set();
2258
+ const listen = () => {
2259
+ document.querySelectorAll('link[rel="stylesheet"][href*="api/tokens"]').forEach((l) => {
2260
+ if (sheets.has(l)) return;
2261
+ sheets.add(l);
2262
+ l.addEventListener("load", apply);
2263
+ });
2264
+ };
2265
+ listen();
2266
+ const timers = RETRY_MS2.map((ms) => window.setTimeout(apply, ms));
2267
+ const observer = typeof MutationObserver === "undefined" ? null : new MutationObserver(() => {
2268
+ listen();
2269
+ apply();
2270
+ });
2271
+ observer?.observe(document.head, { childList: true });
2272
+ return () => {
2273
+ done = true;
2274
+ sheets.forEach((l) => l.removeEventListener("load", apply));
2275
+ timers.forEach((t) => window.clearTimeout(t));
2276
+ observer?.disconnect();
2277
+ };
2278
+ }
2279
+ function BrandFavicon() {
2280
+ react.useEffect(() => watchBrandFavicon(), []);
2281
+ return null;
2282
+ }
2179
2283
  var standardNavIcons = {
2180
2284
  home: lu.LuHouse,
2181
2285
  users: lu.LuUsers,
@@ -4610,52 +4714,6 @@ function writeUrlState(current, patch) {
4610
4714
  }
4611
4715
  return buildSearch(state);
4612
4716
  }
4613
- function parseCssUrl(value) {
4614
- if (!value) return null;
4615
- const m = /^url\(\s*(['"]?)(.*?)\1\s*\)$/i.exec(value.trim());
4616
- const url = m?.[2]?.trim();
4617
- return url && /^https:\/\//i.test(url) ? url : null;
4618
- }
4619
- function readBrandAsset(kind) {
4620
- if (typeof document === "undefined") return null;
4621
- try {
4622
- return parseCssUrl(
4623
- window.getComputedStyle(document.documentElement).getPropertyValue(`--asset-${kind}`)
4624
- );
4625
- } catch {
4626
- return null;
4627
- }
4628
- }
4629
- var EMPTY = { logoHorizontal: null, logoSquare: null };
4630
- var RETRY_MS = [0, 250, 1e3, 3e3];
4631
- function useBrandAssets() {
4632
- const [assets, setAssets] = react.useState(EMPTY);
4633
- react.useEffect(() => {
4634
- let done = false;
4635
- const read = () => {
4636
- if (done) return;
4637
- const next = {
4638
- logoHorizontal: readBrandAsset("logo-horizontal"),
4639
- logoSquare: readBrandAsset("logo-square")
4640
- };
4641
- setAssets(
4642
- (prev) => prev.logoHorizontal === next.logoHorizontal && prev.logoSquare === next.logoSquare ? prev : next
4643
- );
4644
- };
4645
- const links = Array.from(
4646
- document.querySelectorAll('link[rel="stylesheet"][href*="api/tokens"]')
4647
- );
4648
- links.forEach((l) => l.addEventListener("load", read));
4649
- const timers = RETRY_MS.map((ms) => window.setTimeout(read, ms));
4650
- read();
4651
- return () => {
4652
- done = true;
4653
- links.forEach((l) => l.removeEventListener("load", read));
4654
- timers.forEach((t) => window.clearTimeout(t));
4655
- };
4656
- }, []);
4657
- return assets;
4658
- }
4659
4717
  var CORE_APP = "vrm";
4660
4718
  var DEFAULT_SIGN_OUT_URL = "/auth/signout";
4661
4719
  var POLITOGY_APPS = {
@@ -4970,6 +5028,7 @@ function FrameBody({
4970
5028
  );
4971
5029
  const breadcrumbs = core ? core.page?.breadcrumbs : grants?.includes("breadcrumbs") ? granted("breadcrumbs") : derivedBreadcrumbs;
4972
5030
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5031
+ /* @__PURE__ */ jsxRuntime.jsx(BrandFavicon, {}),
4973
5032
  /* @__PURE__ */ jsxRuntime.jsx(
4974
5033
  LoadingScreen,
4975
5034
  {