@the-portland-company/shell 0.76.14 → 0.76.18
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 +35 -0
- package/dist/app-chrome.cjs +138 -17
- package/dist/app-chrome.cjs.map +1 -1
- package/dist/app-chrome.d.cts +7 -2
- package/dist/app-chrome.d.ts +7 -2
- package/dist/app-chrome.js +10 -5
- package/dist/app-chrome.js.map +1 -1
- package/dist/{chunk-HSC6EYRR.js → chunk-GGM4AD43.js} +6 -1769
- package/dist/chunk-GGM4AD43.js.map +1 -0
- package/dist/chunk-TUK2IEL5.js +2002 -0
- package/dist/chunk-TUK2IEL5.js.map +1 -0
- package/dist/index-Cu5frXRQ.d.cts +990 -0
- package/dist/index-DJ3qBzfQ.d.ts +990 -0
- package/dist/index.cjs +170 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +37 -36
- package/dist/index.js.map +1 -1
- package/dist/native.cjs +131 -15
- package/dist/native.cjs.map +1 -1
- package/dist/native.d.cts +4 -929
- package/dist/native.d.ts +4 -929
- package/dist/native.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-HSC6EYRR.js.map +0 -1
- package/dist/chunk-MY6G3LMN.js +0 -125
- package/dist/chunk-MY6G3LMN.js.map +0 -1
package/dist/chunk-MY6G3LMN.js
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import clsx from 'clsx';
|
|
2
|
-
import { jsx } from 'react/jsx-runtime';
|
|
3
|
-
import { useMemo, useEffect } from 'react';
|
|
4
|
-
|
|
5
|
-
// src/components/FormColumn.tsx
|
|
6
|
-
function FormColumn({ children, className }) {
|
|
7
|
-
return /* @__PURE__ */ jsx("div", { "data-shell-form-column": "", className: clsx("w-full max-w-[960px]", className), children });
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// src/lib/deprecation.ts
|
|
11
|
-
var warned = /* @__PURE__ */ new Set();
|
|
12
|
-
function isDev() {
|
|
13
|
-
try {
|
|
14
|
-
return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
15
|
-
} catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function warnDeprecated(key, message) {
|
|
20
|
-
if (!isDev() || warned.has(key)) return;
|
|
21
|
-
warned.add(key);
|
|
22
|
-
console.warn(`[@the-portland-company/shell] ${message}`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// src/speculation/emitRules.ts
|
|
26
|
-
function buildSpeculationRules(currentApp, registry) {
|
|
27
|
-
const others = registry.filter(
|
|
28
|
-
(entry) => entry.id !== currentApp && entry.visible !== false
|
|
29
|
-
);
|
|
30
|
-
if (others.length === 0) return null;
|
|
31
|
-
const payload = {
|
|
32
|
-
prerender: [
|
|
33
|
-
{
|
|
34
|
-
urls: others.map((entry) => entry.path),
|
|
35
|
-
eagerness: "moderate"
|
|
36
|
-
}
|
|
37
|
-
]
|
|
38
|
-
};
|
|
39
|
-
return JSON.stringify(payload);
|
|
40
|
-
}
|
|
41
|
-
var DESTRUCTIVE_LABEL_PATTERNS = ["sign out", "signout", "log out", "logout", "delete"];
|
|
42
|
-
var DESTRUCTIVE_PATH_PATTERNS = ["/signout", "/sign-out", "/logout", "/log-out"];
|
|
43
|
-
function isDestructive(item) {
|
|
44
|
-
const label = item.label.toLowerCase();
|
|
45
|
-
if (DESTRUCTIVE_LABEL_PATTERNS.some((p) => label.includes(p))) return true;
|
|
46
|
-
const href = (item.href ?? "").toLowerCase();
|
|
47
|
-
return DESTRUCTIVE_PATH_PATTERNS.some((p) => href.includes(p));
|
|
48
|
-
}
|
|
49
|
-
function collectSiblingAppUrls(items, currentOrigin) {
|
|
50
|
-
const urls = /* @__PURE__ */ new Set();
|
|
51
|
-
const walk = (list) => {
|
|
52
|
-
for (const item of list) {
|
|
53
|
-
if (item.kind === "section") continue;
|
|
54
|
-
if (item.href && !isDestructive(item)) {
|
|
55
|
-
try {
|
|
56
|
-
const url = new URL(item.href, currentOrigin);
|
|
57
|
-
if (url.origin !== currentOrigin && /^https?:$/.test(url.protocol)) {
|
|
58
|
-
urls.add(url.href);
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (item.children?.length) walk(item.children);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
walk(items);
|
|
67
|
-
return Array.from(urls);
|
|
68
|
-
}
|
|
69
|
-
function buildNavSpeculationRules(items, currentOrigin = typeof window !== "undefined" ? window.location.origin : "") {
|
|
70
|
-
const urls = collectSiblingAppUrls(items, currentOrigin);
|
|
71
|
-
if (urls.length === 0) return null;
|
|
72
|
-
return {
|
|
73
|
-
prerender: [{ source: "list", urls, eagerness: "moderate" }]
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
var SCRIPT_ID = "politogy-speculation-rules";
|
|
77
|
-
var PREFETCH_ATTR = "data-politogy-speculation-prefetch";
|
|
78
|
-
function SpeculationRulesScript({ items }) {
|
|
79
|
-
const origin = typeof window !== "undefined" ? window.location.origin : "";
|
|
80
|
-
const rules = useMemo(() => buildNavSpeculationRules(items, origin), [items, origin]);
|
|
81
|
-
const json = rules ? JSON.stringify(rules) : null;
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (!json || typeof document === "undefined") return;
|
|
84
|
-
let el = document.getElementById(SCRIPT_ID);
|
|
85
|
-
if (!el) {
|
|
86
|
-
el = document.createElement("script");
|
|
87
|
-
el.id = SCRIPT_ID;
|
|
88
|
-
el.type = "speculationrules";
|
|
89
|
-
document.head.appendChild(el);
|
|
90
|
-
}
|
|
91
|
-
el.textContent = json;
|
|
92
|
-
return () => {
|
|
93
|
-
};
|
|
94
|
-
}, [json]);
|
|
95
|
-
useEffect(() => {
|
|
96
|
-
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
97
|
-
const supportsSpecRules = typeof HTMLScriptElement !== "undefined" && typeof HTMLScriptElement.supports === "function" && HTMLScriptElement.supports(
|
|
98
|
-
"speculationrules"
|
|
99
|
-
);
|
|
100
|
-
if (supportsSpecRules) return;
|
|
101
|
-
const urls = collectSiblingAppUrls(items, origin);
|
|
102
|
-
if (urls.length === 0) return;
|
|
103
|
-
const schedule = typeof window.requestIdleCallback === "function" ? window.requestIdleCallback : (cb) => setTimeout(cb, 2e3);
|
|
104
|
-
const added = [];
|
|
105
|
-
schedule(() => {
|
|
106
|
-
for (const url of urls) {
|
|
107
|
-
if (document.querySelector(`link[${PREFETCH_ATTR}][href="${url}"]`)) continue;
|
|
108
|
-
const link = document.createElement("link");
|
|
109
|
-
link.rel = "prefetch";
|
|
110
|
-
link.href = url;
|
|
111
|
-
link.setAttribute(PREFETCH_ATTR, "");
|
|
112
|
-
document.head.appendChild(link);
|
|
113
|
-
added.push(link);
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
return () => {
|
|
117
|
-
added.forEach((l) => l.remove());
|
|
118
|
-
};
|
|
119
|
-
}, [items, origin]);
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export { FormColumn, SpeculationRulesScript, buildNavSpeculationRules, buildSpeculationRules, collectSiblingAppUrls, warnDeprecated };
|
|
124
|
-
//# sourceMappingURL=chunk-MY6G3LMN.js.map
|
|
125
|
-
//# sourceMappingURL=chunk-MY6G3LMN.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/FormColumn.tsx","../src/lib/deprecation.ts","../src/speculation/emitRules.ts","../src/speculation/SpeculationRulesScript.tsx"],"names":[],"mappings":"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","file":"chunk-MY6G3LMN.js","sourcesContent":["import type { ReactNode } from 'react';\nimport clsx from 'clsx';\n\ntype FormColumnProps = {\n children: ReactNode;\n className?: string;\n};\n\n/**\n * The ONE constrained layout inside the frame's fluid `<main>` (Brand guideline\n * \"Content width is fluid\"): a single left-aligned column, 960px max, for form\n * and settings pages — single-record edit forms, Organization Settings tabs,\n * My Profile, activation and setup flows. Data surfaces (tables, dashboards,\n * maps) never use it; they take the full fluid width.\n *\n * It adds no gutter of its own — `<main>` already owns the 24px / 16px gutter —\n * and it is never centered. There is no width prop on purpose: a per-page\n * max-width is exactly what the guideline forbids.\n */\nexport default function FormColumn({ children, className }: FormColumnProps) {\n return (\n <div data-shell-form-column=\"\" className={clsx('w-full max-w-[960px]', className)}>\n {children}\n </div>\n );\n}\n","// One-time console warnings for retired shell API, in development only.\n\nconst warned = new Set<string>()\n\nfunction isDev(): boolean {\n try {\n return typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production'\n } catch {\n return false\n }\n}\n\n/** Warn once per `key` in dev; silent in production builds. */\nexport function warnDeprecated(key: string, message: string): void {\n if (!isDev() || warned.has(key)) return\n warned.add(key)\n console.warn(`[@the-portland-company/shell] ${message}`)\n}\n\n/** Test hook: forget which warnings have fired. */\nexport function resetDeprecationWarnings(): void {\n warned.clear()\n}\n","import type { AppRegistryEntry } from '../types'\nimport type { NavItem } from '../native/index'\n\n/**\n * Build a Speculation Rules JSON payload that prerenders other apps in the\n * registry on moderate eagerness. Returns null when there are no eligible\n * other apps to prerender (e.g., this is the only app in the registry).\n *\n * @deprecated Legacy `AppRegistryEntry[]`-based registry is unused by any\n * current consumer. Cross-app sidebar nav now ships real `NavItem[]` trees\n * (`standardPolitogyNav`) — use `buildNavSpeculationRules` instead, which this\n * now delegates to.\n *\n * The host app is expected to render this as\n * `<script type=\"speculationrules\">{json}</script>` in <head> at runtime.\n */\nexport function buildSpeculationRules(\n currentApp: string,\n registry: AppRegistryEntry[],\n): string | null {\n const others = registry.filter(\n (entry) => entry.id !== currentApp && entry.visible !== false,\n )\n if (others.length === 0) return null\n const payload = {\n prerender: [\n {\n urls: others.map((entry) => entry.path),\n eagerness: 'moderate' as const,\n },\n ],\n }\n return JSON.stringify(payload)\n}\n\n/**\n * Labels the shell will never prerender or prefetch, even if a consumer's nav\n * tree somehow carries an `href` for them (sign-out is normally a footer\n * `onClick`, never a `NavItem.href` — this is a second guard, not the primary\n * one). Case-insensitive substring match against the row's label.\n */\nconst DESTRUCTIVE_LABEL_PATTERNS = ['sign out', 'signout', 'log out', 'logout', 'delete']\n\n/** URL substrings that mark a link as destructive/non-idempotent regardless of label. */\nconst DESTRUCTIVE_PATH_PATTERNS = ['/signout', '/sign-out', '/logout', '/log-out']\n\nfunction isDestructive(item: { label: string; href?: string }): boolean {\n const label = item.label.toLowerCase()\n if (DESTRUCTIVE_LABEL_PATTERNS.some((p) => label.includes(p))) return true\n const href = (item.href ?? '').toLowerCase()\n return DESTRUCTIVE_PATH_PATTERNS.some((p) => href.includes(p))\n}\n\n/**\n * Walk a `NavItem[]` tree (as produced by `standardPolitogyNav`) and collect\n * every absolute, cross-origin, non-destructive, GET-safe link — the\n * candidate set for prerender/prefetch. Sections and action rows (`onClick`,\n * no `href`) are skipped; children are walked too (sub-menu rows like\n * \"Command Center\" / \"New Poll\" are real cross-app destinations).\n */\nexport function collectSiblingAppUrls(items: NavItem[], currentOrigin: string): string[] {\n const urls = new Set<string>()\n const walk = (list: NavItem[]) => {\n for (const item of list) {\n if (item.kind === 'section') continue\n if (item.href && !isDestructive(item)) {\n try {\n const url = new URL(item.href, currentOrigin)\n if (url.origin !== currentOrigin && /^https?:$/.test(url.protocol)) {\n urls.add(url.href)\n }\n } catch {\n // Malformed href — skip rather than throw during nav render.\n }\n }\n if (item.children?.length) walk(item.children)\n }\n }\n walk(items)\n return Array.from(urls)\n}\n\nexport interface NavSpeculationRules {\n prerender: [{ source: 'list'; urls: string[]; eagerness: 'moderate' }]\n}\n\n/**\n * Build the Speculation Rules payload for a consumer's real sidebar nav tree:\n * every sibling `*.politogyvrm.com` app link, `eagerness: \"moderate\"` (the\n * browser's own heuristic — prerender starts on hover/pointerdown of a\n * matching `<a>`, not on page load). Sign-out and other destructive rows are\n * excluded by `collectSiblingAppUrls`. Returns `null` when there is nothing\n * to speculate on (e.g., an empty or single-app tree).\n *\n * Each target origin must opt in by sending\n * `Supports-Loading-Mode: credentialed-prerender` on its responses, per the\n * Speculation Rules cross-origin prerender contract — without it the browser\n * silently falls back to a normal navigation. (Other apps are adding this\n * header separately.)\n */\nexport function buildNavSpeculationRules(\n items: NavItem[],\n currentOrigin: string = typeof window !== 'undefined' ? window.location.origin : '',\n): NavSpeculationRules | null {\n const urls = collectSiblingAppUrls(items, currentOrigin)\n if (urls.length === 0) return null\n return {\n prerender: [{ source: 'list', urls, eagerness: 'moderate' }],\n }\n}\n","import { useEffect, useMemo } from 'react'\nimport type { NavItem } from '../native/index'\nimport { buildNavSpeculationRules, collectSiblingAppUrls } from './emitRules'\n\nconst SCRIPT_ID = 'politogy-speculation-rules'\nconst PREFETCH_ATTR = 'data-politogy-speculation-prefetch'\n\n/**\n * Renders the cross-app Speculation Rules `<script>` for the current\n * consumer's sidebar nav tree, and mounts in every `NavAppShell` — so it is\n * never an opt-in an app has to remember. Browsers that support Speculation\n * Rules prerender a sibling `*.politogyvrm.com` app the moment the user\n * hovers or pointers-down on its sidebar link (`eagerness: \"moderate\"`);\n * browsers that don't fall back, after the page goes idle, to plain\n * `<link rel=\"prefetch\">` tags for the same URLs, so the next click is still\n * warmed by a DNS/TLS/HTML fetch even without a prerendered document.\n *\n * Each sibling origin must send `Supports-Loading-Mode:\n * credentialed-prerender` on its responses for the prerender to actually\n * happen while the user is authenticated (cross-origin prerender is opt-in\n * on the destination). Without it, browsers degrade this to a normal\n * navigation — the hover/click still works, it's just not pre-warmed.\n *\n * Renders nothing visible; this is a side-effecting `<script>` injector.\n */\nexport function SpeculationRulesScript({ items }: { items: NavItem[] }) {\n const origin = typeof window !== 'undefined' ? window.location.origin : ''\n const rules = useMemo(() => buildNavSpeculationRules(items, origin), [items, origin])\n const json = rules ? JSON.stringify(rules) : null\n\n // Native Speculation Rules — covers the hover/pointerdown prerender case in\n // supporting browsers (Chromium). Declarative: no listeners needed, the\n // browser applies its own \"moderate\" heuristic to matching <a> elements.\n useEffect(() => {\n if (!json || typeof document === 'undefined') return\n let el = document.getElementById(SCRIPT_ID) as HTMLScriptElement | null\n if (!el) {\n el = document.createElement('script')\n el.id = SCRIPT_ID\n el.type = 'speculationrules'\n document.head.appendChild(el)\n }\n el.textContent = json\n return () => {\n // Leave the tag in place across route changes within the same app —\n // only strip it if this effect instance's app unmounts entirely with no\n // sibling to replace it.\n }\n }, [json])\n\n // Idle-prefetch fallback for browsers without Speculation Rules support\n // (or where the destination hasn't sent the opt-in header): warm the\n // connection/document via <link rel=\"prefetch\"> once the main thread is\n // idle, so it never competes with first-paint work.\n useEffect(() => {\n if (typeof document === 'undefined' || typeof window === 'undefined') return\n const supportsSpecRules =\n typeof HTMLScriptElement !== 'undefined' &&\n typeof (HTMLScriptElement as unknown as { supports?: (t: string) => boolean }).supports ===\n 'function' &&\n (HTMLScriptElement as unknown as { supports: (t: string) => boolean }).supports(\n 'speculationrules',\n )\n if (supportsSpecRules) return\n const urls = collectSiblingAppUrls(items, origin)\n if (urls.length === 0) return\n\n const schedule: (cb: () => void) => void =\n typeof (window as unknown as { requestIdleCallback?: (cb: () => void) => void })\n .requestIdleCallback === 'function'\n ? (window as unknown as { requestIdleCallback: (cb: () => void) => void })\n .requestIdleCallback\n : (cb: () => void) => setTimeout(cb, 2000)\n\n const added: HTMLLinkElement[] = []\n schedule(() => {\n for (const url of urls) {\n if (document.querySelector(`link[${PREFETCH_ATTR}][href=\"${url}\"]`)) continue\n const link = document.createElement('link')\n link.rel = 'prefetch'\n link.href = url\n link.setAttribute(PREFETCH_ATTR, '')\n document.head.appendChild(link)\n added.push(link)\n }\n })\n return () => {\n added.forEach((l) => l.remove())\n }\n }, [items, origin])\n\n return null\n}\n"]}
|