@bridge-ds/toolbar 0.0.0-stage → 0.2.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.
@@ -0,0 +1,145 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { lazy, useState, Suspense, useEffect, useMemo } from "react";
3
+ import { r as readChanges } from "./session-CzlIsFDY.js";
4
+ const Toolbar = lazy(() => import("./Toolbar-CzR1TM9x.js"));
5
+ function asked() {
6
+ let store = null;
7
+ try {
8
+ store = window.sessionStorage;
9
+ } catch {
10
+ store = null;
11
+ }
12
+ return readChanges(window.location.search, store).length > 0;
13
+ }
14
+ function BridgeToolbar(props) {
15
+ const [on] = useState(() => typeof window !== "undefined" && asked());
16
+ if (!on) return null;
17
+ return /* @__PURE__ */ jsx(Suspense, { fallback: null, children: /* @__PURE__ */ jsx(Toolbar, { ...props }) });
18
+ }
19
+ const read = () => {
20
+ const el = document.documentElement;
21
+ return {
22
+ theme: el.getAttribute("data-theme") === "dark" ? "dark" : "light",
23
+ brand: el.getAttribute("data-brand") || "bridge",
24
+ density: el.getAttribute("data-density") || "comfortable"
25
+ };
26
+ };
27
+ function useDocumentScope() {
28
+ const [here, setHere] = useState(read);
29
+ useEffect(() => {
30
+ const mo = new MutationObserver(() => setHere((p) => {
31
+ const n = read();
32
+ return n.theme === p.theme && n.brand === p.brand && n.density === p.density ? p : n;
33
+ }));
34
+ mo.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "data-brand", "data-density"] });
35
+ return () => mo.disconnect();
36
+ }, []);
37
+ return useMemo(
38
+ () => ({
39
+ ...here,
40
+ set: (next) => {
41
+ if (next.theme) document.documentElement.setAttribute("data-theme", next.theme);
42
+ }
43
+ }),
44
+ [here]
45
+ );
46
+ }
47
+ const DEFAULT_PROXY = "https://bridge.floriangouloubi.com";
48
+ const API = "https://api.github.com";
49
+ const RAW = "https://raw.githubusercontent.com";
50
+ class ReadError extends Error {
51
+ constructor(message, status) {
52
+ super(message);
53
+ this.status = status;
54
+ }
55
+ status;
56
+ }
57
+ const LIMIT = "GitHub limits reads without an account to 60 an hour. Try again later.";
58
+ const errorOf = (status) => new ReadError(status === 403 || status === 429 ? LIMIT : `GitHub answered ${status}.`, status);
59
+ const down = /* @__PURE__ */ new Set();
60
+ const proxyRefused = (r) => r.status === 400 || r.status === 401 || r.status === 403 || r.status === 409 || r.status === 429 || r.status >= 500;
61
+ const segments = (path) => path.split("/").map(encodeURIComponent).join("/");
62
+ function githubReads({ team, proxy, fetch: f = (...a) => fetch(...a) }) {
63
+ const base = proxy ? proxy.replace(/\/+$/, "") : null;
64
+ async function read2(gh, accept, direct) {
65
+ if (base && !down.has(base)) {
66
+ try {
67
+ const r = await f(`${base}/api/contribute?repo=${encodeURIComponent(team)}&gh=${encodeURIComponent(gh)}`, { headers: { Accept: accept }, credentials: "omit" });
68
+ if (!proxyRefused(r)) return r;
69
+ } catch {
70
+ }
71
+ down.add(base);
72
+ }
73
+ return f(direct, direct.startsWith(API) ? { headers: { Accept: accept } } : {});
74
+ }
75
+ return {
76
+ async api(path) {
77
+ const r = await read2(path, "application/vnd.github+json", `${API}${path}`);
78
+ if (!r.ok) throw errorOf(r.status);
79
+ return await r.json();
80
+ },
81
+ async file(owner, repo, ref, path) {
82
+ const gh = `/repos/${owner}/${repo}/contents/${segments(path)}${ref ? `?ref=${encodeURIComponent(ref)}` : ""}`;
83
+ const r = await read2(gh, "application/vnd.github.raw", `${RAW}/${owner}/${repo}/${ref ?? "HEAD"}/${segments(path)}`);
84
+ if (!r.ok) throw errorOf(r.status);
85
+ return r.text();
86
+ }
87
+ };
88
+ }
89
+ const REPO = /^[\w.-]+\/[\w.-]+$/;
90
+ function buildFromConfig(raw, ds, product, overrides = {}) {
91
+ if (!raw || typeof raw !== "object" || !REPO.test(ds)) return null;
92
+ const config = raw;
93
+ const entry = (config.products ?? []).find((p) => p?.key === product);
94
+ if (!entry) return null;
95
+ const own = entry.repo && REPO.test(entry.repo) ? entry.repo : null;
96
+ const [owner, repo] = (own ?? ds).split("/");
97
+ const pick = (v, fallback) => v !== void 0 ? v : fallback ?? null;
98
+ return {
99
+ owner,
100
+ repo,
101
+ studio: pick(overrides.studio, config.previews?.production?.docs),
102
+ production: pick(overrides.production, entry.production ?? config.previews?.production?.[product]),
103
+ preview: pick(overrides.preview, entry.preview ?? config.previews?.apps?.[product]),
104
+ deployEnv: overrides.deployEnv ?? null,
105
+ ...overrides.proxy !== void 0 ? { proxy: overrides.proxy } : {},
106
+ defaultBranch: config.repo?.defaultBranch ?? "main",
107
+ tokensRoot: config.tokens?.root ?? "packages/tokens/src",
108
+ product,
109
+ ds
110
+ };
111
+ }
112
+ const cacheKey = (ds) => `bridge-toolbar:config:v1:${ds}`;
113
+ async function loadBuild(ds, product, overrides = {}, gh) {
114
+ let config = null;
115
+ try {
116
+ const cached = window.sessionStorage.getItem(cacheKey(ds));
117
+ if (cached) config = JSON.parse(cached);
118
+ } catch {
119
+ }
120
+ if (!config) {
121
+ if (!REPO.test(ds)) throw new Error(`"${ds}" is not a GitHub repository (owner/name).`);
122
+ const [owner, name] = ds.split("/");
123
+ const reads = githubReads({ team: ds, proxy: overrides.proxy === void 0 ? DEFAULT_PROXY : overrides.proxy });
124
+ const text = await reads.file(owner, name, null, "bridge.config.json").catch((e) => {
125
+ throw new Error(`Could not read bridge.config.json from ${ds} (${e.message})`);
126
+ });
127
+ config = JSON.parse(text);
128
+ try {
129
+ window.sessionStorage.setItem(cacheKey(ds), JSON.stringify(config));
130
+ } catch {
131
+ }
132
+ }
133
+ const build = buildFromConfig(config, ds, product, overrides);
134
+ if (!build) throw new Error(`No product "${product}" in ${ds}'s bridge.config.json.`);
135
+ return build;
136
+ }
137
+ export {
138
+ BridgeToolbar as B,
139
+ DEFAULT_PROXY as D,
140
+ buildFromConfig as b,
141
+ githubReads as g,
142
+ loadBuild as l,
143
+ useDocumentScope as u
144
+ };
145
+ //# sourceMappingURL=index-3f0I3tAL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-3f0I3tAL.js","sources":["../src/BridgeToolbar.tsx","../src/scope.ts","../src/reads.ts","../src/config.ts"],"sourcesContent":["// The one line a product mounts at its root: <BridgeToolbar ds=\"owner/design-system\" product=\"checkout\" />. It renders\n// nothing and loads nothing unless Bridge Studio opened the page with `?bridge=<change>` (kept for the session), then\n// loads the toolbar itself as one lazy chunk, with its styles.\nimport { lazy, Suspense, useState } from 'react';\nimport { readChanges, type BridgeBuild } from './session';\nimport type { ToolbarScope, ToolbarShop } from './scope';\nimport type { TogetherBase } from './together';\n\nexport interface BridgeToolbarProps {\n /** The design system repository, \"owner/name\". The toolbar reads its bridge.config.json when Studio opens the page. */\n ds?: string;\n /** This product's key in the design system's bridge.config.json products[] (\"checkout\"). */\n product?: string;\n /** Overrides of what the config says: production Studio, this product in production, its preview template. */\n studio?: string | null;\n production?: string | null;\n preview?: string | null;\n /**\n * Bridge's origin, whose read proxy serves the toolbar's GitHub reads (no limit of 60 an hour per visitor). Default\n * production Bridge (https://bridge.floriangouloubi.com); null reads GitHub directly. GitHub directly anyway when\n * Bridge refuses or cannot be reached.\n */\n proxy?: string | null;\n /** The deploy environment (\"production\", \"preview\"), when the host tells it at build time (VERCEL_ENV). */\n deployEnv?: string | null;\n /** Every fact at once, read at build time: no config read at run time (the Admin demo passes it). */\n build?: BridgeBuild;\n /** A hook that says where the page is and moves it (theme, shop). Default: the attributes ThemeProvider sets on <html>. */\n scope?: () => ToolbarScope;\n /** The shops of a multi-brand product, for the screens list and \"Switch to <shop>\". */\n shops?: ToolbarShop[];\n /** The latest library's token files as a combination base: lets Studio's \"Try them together\" run here. */\n together?: () => Promise<TogetherBase>;\n}\n\nconst Toolbar = lazy(() => import('./Toolbar'));\n\nfunction asked(): boolean {\n let store: Storage | null = null;\n try {\n store = window.sessionStorage;\n } catch {\n store = null;\n }\n return readChanges(window.location.search, store).length > 0;\n}\n\n/** Mounted once at the app root. Without a change from Studio it renders nothing and loads nothing. */\nexport function BridgeToolbar(props: BridgeToolbarProps) {\n const [on] = useState(() => typeof window !== 'undefined' && asked());\n if (!on) return null;\n return (\n <Suspense fallback={null}>\n <Toolbar {...props} />\n </Suspense>\n );\n}\n","// Where the page is (theme, brand, density, shop) and how the toolbar moves it there. A product passes its own hook\n// when it keeps these in state (the Admin demo's preferences); by default the toolbar reads the attributes the\n// design system's ThemeProvider (asRoot) puts on <html> and sets data-theme there.\nimport { useEffect, useMemo, useState } from 'react';\n\nexport interface ToolbarScope {\n theme: 'light' | 'dark';\n brand: string;\n density: string;\n /** The shop (tenant) the page shows, when the product has several. */\n shop?: string;\n /** Moves the page to a theme, a shop or both, in one step. */\n set: (next: { theme?: 'light' | 'dark'; shop?: string }) => void;\n}\n\n/** A shop of a multi-brand product: its id (in `?tenant=` links), its name and its brand. */\nexport interface ToolbarShop {\n id: string;\n name: string;\n brand: string;\n}\n\nconst read = () => {\n const el = document.documentElement;\n return {\n theme: (el.getAttribute('data-theme') === 'dark' ? 'dark' : 'light') as 'light' | 'dark',\n brand: el.getAttribute('data-brand') || 'bridge',\n density: el.getAttribute('data-density') || 'comfortable',\n };\n};\n\n/** The default scope: <html data-theme data-brand data-density>, kept in sync. */\nexport function useDocumentScope(): ToolbarScope {\n const [here, setHere] = useState(read);\n useEffect(() => {\n const mo = new MutationObserver(() => setHere((p) => {\n const n = read();\n return n.theme === p.theme && n.brand === p.brand && n.density === p.density ? p : n;\n }));\n mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'data-brand', 'data-density'] });\n return () => mo.disconnect();\n }, []);\n return useMemo(\n () => ({\n ...here,\n set: (next) => {\n if (next.theme) document.documentElement.setAttribute('data-theme', next.theme);\n },\n }),\n [here],\n );\n}\n","// How the toolbar reads GitHub: through Bridge's read proxy (GET <proxy>/api/contribute?repo=<design system>&gh=<path>,\n// allow-listed, cached, with a read-only token on the server), so a visitor never meets GitHub's limit of 60 anonymous\n// reads an hour per address. When Bridge refuses or cannot be reached (an older Bridge, a design system it does not\n// serve, an origin its config does not name), the toolbar reads GitHub directly, as before, for the rest of the page.\n// The requests stay \"simple\" (GET, Accept only, no credentials): no CORS preflight, no cookie sent.\n\n/** Production Bridge: the read proxy the toolbar uses unless the product says otherwise. */\nexport const DEFAULT_PROXY = 'https://bridge.floriangouloubi.com';\n\nconst API = 'https://api.github.com';\nconst RAW = 'https://raw.githubusercontent.com';\n\nexport interface ReadsOptions {\n /** The design system repository (\"owner/name\"): Bridge serves its reads and those of the products it lists. */\n team: string;\n /** Bridge's origin, or null to read GitHub directly. */\n proxy: string | null;\n fetch?: typeof fetch;\n}\n\nexport interface Reads {\n /** A GitHub REST read under /repos/<owner>/<name>/..., as JSON. */\n api<T>(path: string): Promise<T>;\n /** A file of a repository at a ref (a sha or a branch; none: the default branch), as text. */\n file(owner: string, repo: string, ref: string | null, path: string): Promise<string>;\n}\n\nexport class ReadError extends Error {\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message);\n }\n}\n\nconst LIMIT = 'GitHub limits reads without an account to 60 an hour. Try again later.';\nconst errorOf = (status: number) => new ReadError(status === 403 || status === 429 ? LIMIT : `GitHub answered ${status}.`, status);\n\n/** Proxies that refused or did not answer on this page: every later read goes to GitHub directly. */\nconst down = new Set<string>();\n\n/** Tests start fresh. */\nexport function resetReads() {\n down.clear();\n}\n\n/**\n * Bridge's answer is GitHub's when it read GitHub (any status but its own refusals). Its own: 400 (a path it does not\n * take), 401 (no reader), 403 (not allowed, origin), 409 (app missing), 429 (busy), 5xx; then GitHub directly.\n */\nconst proxyRefused = (r: Response) => r.status === 400 || r.status === 401 || r.status === 403 || r.status === 409 || r.status === 429 || r.status >= 500;\n\nconst segments = (path: string) => path.split('/').map(encodeURIComponent).join('/');\n\nexport function githubReads({ team, proxy, fetch: f = (...a) => fetch(...a) }: ReadsOptions): Reads {\n const base = proxy ? proxy.replace(/\\/+$/, '') : null;\n\n /** Bridge first, then GitHub. `direct` is GitHub's own URL for the same read. */\n async function read(gh: string, accept: string, direct: string): Promise<Response> {\n if (base && !down.has(base)) {\n try {\n const r = await f(`${base}/api/contribute?repo=${encodeURIComponent(team)}&gh=${encodeURIComponent(gh)}`, { headers: { Accept: accept }, credentials: 'omit' });\n if (!proxyRefused(r)) return r;\n } catch {\n // Unreachable, or the browser refused the answer (CORS): GitHub directly.\n }\n down.add(base);\n }\n return f(direct, direct.startsWith(API) ? { headers: { Accept: accept } } : {});\n }\n\n return {\n async api<T>(path: string): Promise<T> {\n const r = await read(path, 'application/vnd.github+json', `${API}${path}`);\n if (!r.ok) throw errorOf(r.status);\n return (await r.json()) as T;\n },\n async file(owner, repo, ref, path) {\n const gh = `/repos/${owner}/${repo}/contents/${segments(path)}${ref ? `?ref=${encodeURIComponent(ref)}` : ''}`;\n const r = await read(gh, 'application/vnd.github.raw', `${RAW}/${owner}/${repo}/${ref ?? 'HEAD'}/${segments(path)}`);\n if (!r.ok) throw errorOf(r.status);\n return r.text();\n },\n };\n}\n","// Where a product's toolbar gets its facts when it is mounted with only the design system repository and its product\n// key: the design system's bridge.config.json, read once per session through Bridge's read proxy (GitHub directly when\n// Bridge refuses; public repositories, no account). Pure part (buildFromConfig) is tested; loadBuild adds the read and\n// the session cache.\nimport { DEFAULT_PROXY, githubReads, type Reads } from './reads';\nimport type { BridgeBuild } from './session';\n\ninterface ConfigLike {\n repo?: { owner?: string; name?: string; defaultBranch?: string };\n tokens?: { root?: string };\n previews?: { apps?: Record<string, string | undefined>; production?: Record<string, string | undefined> };\n products?: { key?: string; repo?: string; preview?: string; production?: string }[];\n}\n\n/** Values the product sets itself win over the config: the Studio, production and preview URLs. */\nexport interface BuildOverrides {\n studio?: string | null;\n production?: string | null;\n preview?: string | null;\n deployEnv?: string | null;\n /** Bridge's origin for reads (default production Bridge), null to read GitHub directly. */\n proxy?: string | null;\n}\n\nconst REPO = /^[\\w.-]+\\/[\\w.-]+$/;\n\n/**\n * The toolbar's facts for one product of a design system config: its repository (its own, else the design system's),\n * production Studio, its production URL and preview template. Null when the product is not in the config.\n */\nexport function buildFromConfig(raw: unknown, ds: string, product: string, overrides: BuildOverrides = {}): BridgeBuild | null {\n if (!raw || typeof raw !== 'object' || !REPO.test(ds)) return null;\n const config = raw as ConfigLike;\n const entry = (config.products ?? []).find((p) => p?.key === product);\n if (!entry) return null;\n const own = entry.repo && REPO.test(entry.repo) ? entry.repo : null;\n const [owner, repo] = (own ?? ds).split('/') as [string, string];\n const pick = (v: string | null | undefined, fallback: string | null | undefined) => (v !== undefined ? v : (fallback ?? null));\n return {\n owner,\n repo,\n studio: pick(overrides.studio, config.previews?.production?.docs),\n production: pick(overrides.production, entry.production ?? config.previews?.production?.[product]),\n preview: pick(overrides.preview, entry.preview ?? config.previews?.apps?.[product]),\n deployEnv: overrides.deployEnv ?? null,\n ...(overrides.proxy !== undefined ? { proxy: overrides.proxy } : {}),\n defaultBranch: config.repo?.defaultBranch ?? 'main',\n tokensRoot: config.tokens?.root ?? 'packages/tokens/src',\n product,\n ds,\n };\n}\n\nconst cacheKey = (ds: string) => `bridge-toolbar:config:v1:${ds}`;\n\n/** Reads the design system's bridge.config.json (its default branch), kept for the session. */\nexport async function loadBuild(ds: string, product: string, overrides: BuildOverrides = {}, gh?: Reads): Promise<BridgeBuild> {\n let config: unknown = null;\n try {\n const cached = window.sessionStorage.getItem(cacheKey(ds));\n if (cached) config = JSON.parse(cached);\n } catch {\n // Storage blocked or a broken entry: read it again.\n }\n if (!config) {\n if (!REPO.test(ds)) throw new Error(`\"${ds}\" is not a GitHub repository (owner/name).`);\n const [owner, name] = ds.split('/') as [string, string];\n const reads = gh ?? githubReads({ team: ds, proxy: overrides.proxy === undefined ? DEFAULT_PROXY : overrides.proxy });\n const text = await reads.file(owner, name, null, 'bridge.config.json').catch((e: Error) => {\n throw new Error(`Could not read bridge.config.json from ${ds} (${e.message})`);\n });\n config = JSON.parse(text);\n try {\n window.sessionStorage.setItem(cacheKey(ds), JSON.stringify(config));\n } catch {\n // Storage blocked: the next page reads it again.\n }\n }\n const build = buildFromConfig(config, ds, product, overrides);\n if (!build) throw new Error(`No product \"${product}\" in ${ds}'s bridge.config.json.`);\n return build;\n}\n"],"names":["read"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export { BridgeToolbar, type BridgeToolbarProps } from './BridgeToolbar';
2
+ export { useDocumentScope, type ToolbarScope, type ToolbarShop } from './scope';
3
+ export { buildFromConfig, type BuildOverrides } from './config';
4
+ export { DEFAULT_PROXY } from './reads';
5
+ export type { BridgeBuild } from './session';
6
+ export type { TogetherBase } from './together';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import { B, D, b, u } from "./index-3f0I3tAL.js";
2
+ export {
3
+ B as BridgeToolbar,
4
+ D as DEFAULT_PROXY,
5
+ b as buildFromConfig,
6
+ u as useDocumentScope
7
+ };
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,41 @@
1
+ import { type RuleLike } from './diff';
2
+ export interface PropChange {
3
+ prop: string;
4
+ /** Computed value with the live tokens. */
5
+ before: string;
6
+ /** Computed value with the change's tokens. */
7
+ after: string;
8
+ }
9
+ export interface ChangedElement {
10
+ el: Element;
11
+ pseudo: string | null;
12
+ /** CSS variables of the changed tokens behind the difference. */
13
+ roots: string[];
14
+ props: PropChange[];
15
+ /** What a person calls it: "Page", "Save bar (Save changes)". */
16
+ label: string;
17
+ /** "root|property" pairs: an ancestor with all of them paints the same change. */
18
+ keys: string[];
19
+ /** Sticky or fixed: paints over other content, so it is a surface even inside a changed ancestor. */
20
+ floating: boolean;
21
+ }
22
+ export interface DiffResult {
23
+ /** Every element whose computed style differs. */
24
+ elements: ChangedElement[];
25
+ /** The outermost of them: what is outlined and counted (when visible). */
26
+ surfaces: ChangedElement[];
27
+ /** Time the comparison took, in ms (rules are read once per page load and cached). */
28
+ ms: number;
29
+ }
30
+ /** The page's own style rules (same origin), without the live tokens stylesheet the toolbar adds. Cached. */
31
+ export declare function pageRules(): RuleLike[];
32
+ /** "Page", "Content column", "Save bar (Save changes)": landmark, ARIA role or component first, its text after. */
33
+ export declare function describe(el: Element): string;
34
+ /** The part of an element inside the viewport, or null when none of it shows (hidden, transparent, off screen). */
35
+ export declare function shownRect(el: Element): DOMRect | null;
36
+ /**
37
+ * Compares the candidates under both token sets. `sheet` is the toolbar's stylesheet that switches sides (loaded):
38
+ * the live tokens over a change's preview (`sheetIs` 'before', one change), or the combination over the live product
39
+ * (`sheetIs` 'after', changes tried together). `ignore` holds the toolbar's own nodes.
40
+ */
41
+ export declare function changedElements(changedVars: string[], sheet: HTMLLinkElement | HTMLStyleElement, ignore: (el: Element) => boolean, sheetIs?: 'before' | 'after'): DiffResult;
package/dist/pin.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /** Marks an element (and what is inside it) as pinned: the toolbar root, the outline layer, their popups. */
2
+ export declare const PIN_ATTR = "data-bridge-pin";
3
+ /** Same, as a class, for popups the DS portals out (Tooltip and Popover accept a className, not attributes). */
4
+ export declare const PIN_CLASS = "bridge-pin";
5
+ /** Writes the pinned tokens for the current theme. Three-fold selectors out-rank the tokens' attribute rules. */
6
+ export declare function usePinnedBridgeTokens(url: string | null, theme: string): void;
@@ -0,0 +1,22 @@
1
+ /** Production Bridge: the read proxy the toolbar uses unless the product says otherwise. */
2
+ export declare const DEFAULT_PROXY = "https://bridge.floriangouloubi.com";
3
+ export interface ReadsOptions {
4
+ /** The design system repository ("owner/name"): Bridge serves its reads and those of the products it lists. */
5
+ team: string;
6
+ /** Bridge's origin, or null to read GitHub directly. */
7
+ proxy: string | null;
8
+ fetch?: typeof fetch;
9
+ }
10
+ export interface Reads {
11
+ /** A GitHub REST read under /repos/<owner>/<name>/..., as JSON. */
12
+ api<T>(path: string): Promise<T>;
13
+ /** A file of a repository at a ref (a sha or a branch; none: the default branch), as text. */
14
+ file(owner: string, repo: string, ref: string | null, path: string): Promise<string>;
15
+ }
16
+ export declare class ReadError extends Error {
17
+ readonly status: number;
18
+ constructor(message: string, status: number);
19
+ }
20
+ /** Tests start fresh. */
21
+ export declare function resetReads(): void;
22
+ export declare function githubReads({ team, proxy, fetch: f }: ReadsOptions): Reads;
@@ -0,0 +1,20 @@
1
+ export interface ToolbarScope {
2
+ theme: 'light' | 'dark';
3
+ brand: string;
4
+ density: string;
5
+ /** The shop (tenant) the page shows, when the product has several. */
6
+ shop?: string;
7
+ /** Moves the page to a theme, a shop or both, in one step. */
8
+ set: (next: {
9
+ theme?: 'light' | 'dark';
10
+ shop?: string;
11
+ }) => void;
12
+ }
13
+ /** A shop of a multi-brand product: its id (in `?tenant=` links), its name and its brand. */
14
+ export interface ToolbarShop {
15
+ id: string;
16
+ name: string;
17
+ brand: string;
18
+ }
19
+ /** The default scope: <html data-theme data-brand data-density>, kept in sync. */
20
+ export declare function useDocumentScope(): ToolbarScope;
@@ -0,0 +1,325 @@
1
+ const PARAM = "bridge";
2
+ const RETURN_PARAM = "bridge-return";
3
+ const SCROLL_PARAM = "bridge-scroll";
4
+ const SESSION_KEY = "bridge-admin:change";
5
+ const RETURN_KEY = "bridge-admin:return";
6
+ const DOCK_KEY = "bridge-admin:toolbar-dock";
7
+ const OPEN_KEY = "bridge-admin:toolbar-open";
8
+ const USE_PARAM = "bridge-use";
9
+ const USE_KEY = "bridge-admin:use";
10
+ const MAX_TOGETHER = 4;
11
+ const safe = (fn, fallback) => {
12
+ try {
13
+ return fn();
14
+ } catch {
15
+ return fallback;
16
+ }
17
+ };
18
+ function parseChanges(raw) {
19
+ const parts = (raw ?? "").trim().split(/,|%2C/i).map((p) => p.trim());
20
+ if (!parts.every((p) => /^\d{1,7}$/.test(p) && Number(p) > 0)) return [];
21
+ return [...new Set(parts.map(Number))].slice(0, MAX_TOGETHER);
22
+ }
23
+ function readChanges(search, storage) {
24
+ const params = new URLSearchParams(search);
25
+ if (params.has(PARAM)) {
26
+ const list = parseChanges(params.get(PARAM));
27
+ if (!list.length) {
28
+ if (storage) safe(() => (storage.removeItem(SESSION_KEY), storage.removeItem(USE_KEY)), void 0);
29
+ return [];
30
+ }
31
+ if (storage) {
32
+ safe(() => storage.setItem(SESSION_KEY, list.join(",")), void 0);
33
+ const use = params.get(USE_PARAM);
34
+ safe(() => use && list.length > 1 ? storage.setItem(USE_KEY, use) : storage.removeItem(USE_KEY), void 0);
35
+ }
36
+ return list;
37
+ }
38
+ return storage ? parseChanges(safe(() => storage.getItem(SESSION_KEY), null)) : [];
39
+ }
40
+ function readUse(search, storage) {
41
+ const params = new URLSearchParams(search);
42
+ if (params.has(PARAM)) return params.get(USE_PARAM);
43
+ return storage ? safe(() => storage.getItem(USE_KEY), null) : null;
44
+ }
45
+ function isPreviewUrl(raw, template) {
46
+ if (!raw || !template) return false;
47
+ let url;
48
+ let tpl;
49
+ try {
50
+ url = new URL(raw);
51
+ tpl = new URL(template.replace(/\{[a-z]+\}/gi, "x"));
52
+ } catch {
53
+ return false;
54
+ }
55
+ if (url.protocol !== "https:" || url.username || url.password || url.port) return false;
56
+ const tplHost = /^https?:\/\/([^/?#]+)/i.exec(template)?.[1]?.toLowerCase();
57
+ if (!tplHost || tpl.protocol !== "https:") return false;
58
+ const pattern = new RegExp(`^${tplHost.split(/\{[a-z]+\}/i).map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[a-z0-9-]+")}$`);
59
+ return pattern.test(url.hostname.toLowerCase());
60
+ }
61
+ function readReturn(search, storage, template) {
62
+ const raw = new URLSearchParams(search).get(RETURN_PARAM);
63
+ if (raw !== null) {
64
+ if (isPreviewUrl(raw, template)) {
65
+ const origin = new URL(raw).origin;
66
+ if (storage) safe(() => storage.setItem(RETURN_KEY, origin), void 0);
67
+ return origin;
68
+ }
69
+ return null;
70
+ }
71
+ const stored = storage ? safe(() => storage.getItem(RETURN_KEY), null) : null;
72
+ return isPreviewUrl(stored, template) ? new URL(stored).origin : null;
73
+ }
74
+ function whereAmI(host, build) {
75
+ const prodHost = build.production ? safe(() => new URL(build.production).host, null) : null;
76
+ if (build.deployEnv === "production" || prodHost && host.toLowerCase() === prodHost.toLowerCase()) return "production";
77
+ if (build.deployEnv === "preview") return "preview";
78
+ return "local";
79
+ }
80
+ function shouldRender(input) {
81
+ if (input.change === null) return false;
82
+ if (input.together) return true;
83
+ if (input.where === "production") return input.returnTo !== null;
84
+ return true;
85
+ }
86
+ const togetherLabel = (n) => n === 1 ? "This change" : `These ${n} changes`;
87
+ const TOGETHER_HINT = "The live product, with these changes' tokens applied in your browser only. No preview has all of them together.";
88
+ const TOGETHER_EMPTY_LINE = "These changes don't touch this screen. Try another screen.";
89
+ function toggleIncluded(all, included, n, on) {
90
+ const next = new Set(included);
91
+ if (on) next.add(n);
92
+ else next.delete(n);
93
+ return all.filter((x) => next.has(x));
94
+ }
95
+ function studioChangesUrl(build) {
96
+ return build.studio ? `${build.studio.replace(/\/$/, "")}/studio/changes/` : `https://github.com/${build.owner}/${build.repo}/pulls`;
97
+ }
98
+ function changeMode(files) {
99
+ const tokenOnly = files.length > 0 && files.every((f) => f.endsWith(".tokens.json") || /contrast\.checks\.json$/.test(f) || f.startsWith(".changeset/"));
100
+ return tokenOnly ? "tokens" : "code";
101
+ }
102
+ function liveTokensUrl(build) {
103
+ return build.studio ? safe(() => new URL("tokens.css", build.studio.replace(/\/?$/, "/")).href, null) : null;
104
+ }
105
+ function studioChangeUrl(build, change) {
106
+ if (!build.studio) return `https://github.com/${build.owner}/${build.repo}/pull/${change}`;
107
+ const studio = build.studio.replace(/\/$/, "");
108
+ const own = build.product && build.ds && build.ds.toLowerCase() !== `${build.owner}/${build.repo}`.toLowerCase();
109
+ return own ? `${studio}/studio/changes/?exploration=${change}&product=${encodeURIComponent(build.product)}` : `${studio}/studio/changes/?pr=${change}`;
110
+ }
111
+ function changedScreens(shots) {
112
+ if (!Array.isArray(shots)) return [];
113
+ const seen = /* @__PURE__ */ new Set();
114
+ return shots.filter((s) => s.kind === "route" && s.route && s.tenant && (s.status === "changed" || s.status === "added")).sort((a, b) => b.percent - a.percent).filter((s) => {
115
+ const key = `${s.route}|${s.tenant}|${s.theme}`;
116
+ if (seen.has(key)) return false;
117
+ seen.add(key);
118
+ return true;
119
+ }).map((s) => ({ label: s.label, route: s.route, tenant: s.tenant, theme: s.theme, percent: s.percent }));
120
+ }
121
+ function sameScreenOn(target, here, extra) {
122
+ const url = new URL(target);
123
+ const params = new URLSearchParams(here.search);
124
+ params.delete(RETURN_PARAM);
125
+ params.delete(SCROLL_PARAM);
126
+ params.set(PARAM, String(extra.change));
127
+ if (extra.returnTo) params.set(RETURN_PARAM, extra.returnTo);
128
+ if (extra.scroll && extra.scroll > 0) params.set(SCROLL_PARAM, String(Math.round(extra.scroll)));
129
+ url.pathname = "/";
130
+ url.search = params.toString();
131
+ url.hash = here.hash;
132
+ return url.href;
133
+ }
134
+ function readScroll(search) {
135
+ const raw = new URLSearchParams(search).get(SCROLL_PARAM);
136
+ return raw && /^\d{1,7}$/.test(raw) ? Number(raw) : null;
137
+ }
138
+ const DOCKS = ["start", "center", "end"];
139
+ function readDock(storage) {
140
+ const v = storage ? safe(() => storage.getItem(DOCK_KEY), null) : null;
141
+ return DOCKS.includes(v) ? v : "end";
142
+ }
143
+ function stepDock(dock, step) {
144
+ return DOCKS[Math.min(DOCKS.length - 1, Math.max(0, DOCKS.indexOf(dock) + step))];
145
+ }
146
+ function dockAt(x, width) {
147
+ if (width <= 0) return "center";
148
+ const r = x / width;
149
+ return r < 1 / 3 ? "start" : r > 2 / 3 ? "end" : "center";
150
+ }
151
+ function readableTitle(title) {
152
+ const t = title.replace(/^\s*[a-z]+(\([^)]*\))?!?:\s*/i, "").trim() || title.trim();
153
+ return t.charAt(0).toUpperCase() + t.slice(1);
154
+ }
155
+ function isFlipKey(e) {
156
+ if (e.altKey || e.ctrlKey || e.metaKey || e.key.toLowerCase() !== "b") return false;
157
+ const t = e.target;
158
+ if (!t) return true;
159
+ if (t.isContentEditable) return false;
160
+ const tag = (t.tagName ?? "").toLowerCase();
161
+ if (tag === "input" || tag === "textarea" || tag === "select") return false;
162
+ return !(t.closest && t.closest('[role="combobox"], [role="textbox"], [role="searchbox"]'));
163
+ }
164
+ const overlaps = (a, b, margin) => a.left < b.right + margin && a.right > b.left - margin && a.top < b.bottom + margin && a.bottom > b.top - margin;
165
+ function placeToolbar(input) {
166
+ const { left: areaLeft, right: areaRight, vh, w, h, gutter, preferred, obstacles } = input;
167
+ const margin = input.margin ?? 8;
168
+ const rectAt = (dock, lift2 = 0) => {
169
+ const left = dock === "start" ? areaLeft : dock === "end" ? areaRight - w : (areaLeft + areaRight - w) / 2;
170
+ const bottom = vh - gutter - lift2;
171
+ return { left, right: left + w, top: bottom - h, bottom };
172
+ };
173
+ const opposite = preferred === "start" ? "end" : "start";
174
+ const order = [preferred, opposite, ...DOCKS.filter((d) => d !== preferred && d !== opposite)];
175
+ for (const dock of order) {
176
+ const r = rectAt(dock);
177
+ if (!obstacles.some((o) => overlaps(r, o, margin))) return { dock, lift: 0 };
178
+ }
179
+ const base = rectAt(preferred);
180
+ const inWay = obstacles.filter((o) => o.left < base.right + margin && o.right > base.left - margin && o.bottom > base.top - margin);
181
+ const top = Math.min(...inWay.map((o) => o.top));
182
+ const lift = Math.max(0, Math.ceil(base.bottom - (top - margin)));
183
+ return { dock: preferred, lift: Math.min(lift, Math.max(0, vh - h - 2 * gutter)) };
184
+ }
185
+ function fileScope(file) {
186
+ if (file.startsWith(".changeset/")) return "none";
187
+ let m = /(?:^|\/)semantic\/([a-z0-9-]+)\.tokens\.json$/i.exec(file);
188
+ if (m && m[1] !== "base") return { theme: m[1], brand: null };
189
+ m = /(?:^|\/)brands\/([a-z0-9-]+)\.([a-z0-9-]+)\.tokens\.json$/i.exec(file);
190
+ if (m) return { theme: m[2], brand: m[1] };
191
+ m = /(?:^|\/)brands\/([a-z0-9-]+)\.tokens\.json$/i.exec(file);
192
+ if (m) return { theme: null, brand: m[1] };
193
+ return { theme: null, brand: null };
194
+ }
195
+ function noVisibleDifference(files, here) {
196
+ const scopes = files.map(fileScope).filter((s) => s !== "none");
197
+ if (!scopes.length) return null;
198
+ const hits = (theme2, brand2) => scopes.some((s) => (s.theme === null || s.theme === theme2) && (s.brand === null || s.brand === brand2));
199
+ if (hits(here.theme, here.brand)) return null;
200
+ const theme = scopes.find((s) => s.theme !== null && s.theme !== here.theme && (s.brand === null || s.brand === here.brand))?.theme;
201
+ if (theme) return { kind: "theme", here: here.theme, touched: theme };
202
+ const brand = scopes.find((s) => s.brand !== null && s.brand !== here.brand)?.brand;
203
+ return brand ? { kind: "brand", here: here.brand, touched: brand } : null;
204
+ }
205
+ function changeSummary(title) {
206
+ const t = readableTitle(title).replace(/[.\s]+$/, "");
207
+ if (!t) return "";
208
+ const lower = /^[A-Z][a-z]/.test(t) ? t.charAt(0).toLowerCase() + t.slice(1) : t;
209
+ return `${lower}.`;
210
+ }
211
+ function leaves(node, path = [], inheritedType = null, out = /* @__PURE__ */ new Map()) {
212
+ if (!node || typeof node !== "object") return out;
213
+ const n = node;
214
+ const type = typeof n.$type === "string" ? n.$type : inheritedType;
215
+ if ("$value" in n) {
216
+ out.set(path.join("."), { value: JSON.stringify(n.$value), type, description: typeof n.$description === "string" ? n.$description : null });
217
+ return out;
218
+ }
219
+ for (const [k, v] of Object.entries(n)) if (!k.startsWith("$")) leaves(v, [...path, k], type, out);
220
+ return out;
221
+ }
222
+ const PALETTE_ROLES = { primary: "Brand primary", link: "Link", neutral: "Neutral", accent: "Accent", success: "Success", warning: "Warning", danger: "Danger", info: "Info", categorical: "Chart colours" };
223
+ function roleOfToken(path, description) {
224
+ const first = description?.split(/[.:;(]/)[0]?.trim();
225
+ if (first && first.length <= 40) return first;
226
+ const m = /^palette\.([a-z-]+)\./.exec(path);
227
+ if (m) return PALETTE_ROLES[m[1]] ?? designerName(`palette.${m[1]}`);
228
+ return designerName(path);
229
+ }
230
+ function stepOfToken(path) {
231
+ return path.startsWith("palette.") ? designerName(path) : null;
232
+ }
233
+ function designerName(path, description) {
234
+ const words = path.replace(/^(color|palette)\./, "").split(".").join(" ");
235
+ return words.charAt(0).toUpperCase() + words.slice(1);
236
+ }
237
+ function tokenChanges(file, before, after) {
238
+ const scope = fileScope(file);
239
+ if (scope === "none") return [];
240
+ const a = leaves(before);
241
+ const b = leaves(after);
242
+ const out = [];
243
+ for (const path of /* @__PURE__ */ new Set([...a.keys(), ...b.keys()])) {
244
+ const x = a.get(path);
245
+ const y = b.get(path);
246
+ if (x?.value === y?.value) continue;
247
+ out.push({
248
+ path,
249
+ cssVar: `--${path.split(".").join("-")}`,
250
+ name: roleOfToken(path, y?.description ?? x?.description ?? null),
251
+ step: stepOfToken(path),
252
+ type: y?.type ?? x?.type ?? null,
253
+ theme: scope.theme,
254
+ brand: scope.brand
255
+ });
256
+ }
257
+ return out;
258
+ }
259
+ function contrastRatio(a, b) {
260
+ const lum = (c) => {
261
+ const m = /^rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(c.trim());
262
+ if (!m) return null;
263
+ const [r, g, bl] = [m[1], m[2], m[3]].map((n) => {
264
+ const v = Number(n) / 255;
265
+ return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
266
+ });
267
+ return 0.2126 * r + 0.7152 * g + 0.0722 * bl;
268
+ };
269
+ const x = lum(a);
270
+ const y = lum(b);
271
+ if (x === null || y === null) return null;
272
+ return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);
273
+ }
274
+ const isSubtle = (a, b) => {
275
+ const r = contrastRatio(a, b);
276
+ return r !== null && r < 1.1;
277
+ };
278
+ function toHex(color) {
279
+ const m = /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)$/.exec(color.trim());
280
+ if (!m) return color;
281
+ const hex = [m[1], m[2], m[3]].map((n) => Number(n).toString(16).padStart(2, "0")).join("");
282
+ return m[4] !== void 0 && Number(m[4]) < 1 ? `#${hex}${Math.round(Number(m[4]) * 255).toString(16).padStart(2, "0")}` : `#${hex}`;
283
+ }
284
+ function fromText(from, titleOf) {
285
+ const names = from.map((n) => titleOf(n) ?? `#${n}`);
286
+ return `from ${names.length <= 2 ? names.join(" and ") : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`}`;
287
+ }
288
+ export {
289
+ togetherLabel as A,
290
+ changeSummary as B,
291
+ dockAt as C,
292
+ stepDock as D,
293
+ DOCK_KEY as E,
294
+ OPEN_KEY as O,
295
+ PARAM as P,
296
+ SCROLL_PARAM as S,
297
+ TOGETHER_EMPTY_LINE as T,
298
+ roleOfToken as a,
299
+ changeMode as b,
300
+ changedScreens as c,
301
+ readableTitle as d,
302
+ toggleIncluded as e,
303
+ readReturn as f,
304
+ readUse as g,
305
+ shouldRender as h,
306
+ readDock as i,
307
+ readScroll as j,
308
+ sameScreenOn as k,
309
+ liveTokensUrl as l,
310
+ studioChangesUrl as m,
311
+ noVisibleDifference as n,
312
+ studioChangeUrl as o,
313
+ contrastRatio as p,
314
+ TOGETHER_HINT as q,
315
+ readChanges as r,
316
+ stepOfToken as s,
317
+ tokenChanges as t,
318
+ isSubtle as u,
319
+ toHex as v,
320
+ whereAmI as w,
321
+ fromText as x,
322
+ isFlipKey as y,
323
+ placeToolbar as z
324
+ };
325
+ //# sourceMappingURL=session-CzlIsFDY.js.map