@bridge-ds/toolbar 0.2.0 → 0.4.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,59 @@
1
+ /** Where a changed surface sits on the page (scroll included), to read the changes top to bottom, then left to right. */
2
+ export interface Place {
3
+ top: number;
4
+ left: number;
5
+ }
6
+ /** The indexes of `places`, in reading order: top to bottom, then left to right on one line. */
7
+ export declare function readingOrder(places: Place[]): number[];
8
+ /**
9
+ * The next position (delta 1) or the previous one (delta -1) among `count` changes, wrapping at both ends. Nothing
10
+ * current yet: Next starts at the first, Previous at the last. No change: null.
11
+ */
12
+ export declare function stepTo(current: number | null, delta: 1 | -1, count: number): number | null;
13
+ /**
14
+ * "13 changed": what the bar shows before stepping. Never "13 changes": it sits next to "This change" (the change being
15
+ * tried) and must not read as a number of changes.
16
+ */
17
+ export declare const changeCount: (n: number) => string;
18
+ /** "2 / 13 changed": the bar while stepping. */
19
+ export declare const positionLabel: (position: number, n: number) => string;
20
+ /** "Changed element 2 of 13: Save bar (Save changes)": what a screen reader hears on each step. */
21
+ export declare const stepAnnouncement: (position: number, n: number, label: string) => string;
22
+ /** No changed surface on this screen: say so, and where the change shows when the toolbar knows it. */
23
+ export declare function elsewhereLine(screens: string[]): string;
24
+ interface KeyLike {
25
+ key: string;
26
+ altKey: boolean;
27
+ ctrlKey: boolean;
28
+ metaKey: boolean;
29
+ target: unknown;
30
+ }
31
+ /** Whether the person is typing: a field, an editable region, a combobox. Shortcuts never fire there. */
32
+ export declare function isTyping(target: unknown): boolean;
33
+ /** Escape turns Isolate off, unless the person is typing (Escape clears or closes the field there). */
34
+ export declare function isEscape(e: KeyLike): boolean;
35
+ export interface Box {
36
+ left: number;
37
+ top: number;
38
+ right: number;
39
+ bottom: number;
40
+ }
41
+ export type TipSide = 'bottom' | 'right' | 'left' | 'top';
42
+ /** Room the before and after tooltip of the current element needs, with its offset from the element. */
43
+ export declare const TIP_ROOM: {
44
+ width: number;
45
+ height: number;
46
+ };
47
+ /**
48
+ * Where the before and after tooltip of the current element goes: below it when there is room above the bar (the
49
+ * label region of an element, its title or heading, usually sits above it), else beside it (right, then left), and
50
+ * above it only when nothing else fits. The bar's top is the floor: the tooltip never sits over the bar.
51
+ */
52
+ export declare function tipSide(box: Box, view: {
53
+ width: number;
54
+ height: number;
55
+ }, bar: Box | null, room?: {
56
+ width: number;
57
+ height: number;
58
+ }): TipSide;
59
+ export {};
package/dist/outline.d.ts CHANGED
@@ -33,6 +33,10 @@ export declare function pageRules(): RuleLike[];
33
33
  export declare function describe(el: Element): string;
34
34
  /** The part of an element inside the viewport, or null when none of it shows (hidden, transparent, off screen). */
35
35
  export declare function shownRect(el: Element): DOMRect | null;
36
+ /** Whether an element shows somewhere on the page (in view or after a scroll): what the changes count and step through. */
37
+ export declare function rendered(el: Element): boolean;
38
+ /** The page's own canvas (html, body): stepping to it never scrolls. */
39
+ export declare const isPage: (el: Element) => boolean;
36
40
  /**
37
41
  * Compares the candidates under both token sets. `sheet` is the toolbar's stylesheet that switches sides (loaded):
38
42
  * the live tokens over a change's preview (`sheetIs` 'before', one change), or the combination over the live product
@@ -1,6 +1,7 @@
1
1
  const PARAM = "bridge";
2
2
  const RETURN_PARAM = "bridge-return";
3
3
  const SCROLL_PARAM = "bridge-scroll";
4
+ const MARKS_PARAM = "bridge-marks";
4
5
  const SESSION_KEY = "bridge-admin:change";
5
6
  const RETURN_KEY = "bridge-admin:return";
6
7
  const DOCK_KEY = "bridge-admin:toolbar-dock";
@@ -123,9 +124,11 @@ function sameScreenOn(target, here, extra) {
123
124
  const params = new URLSearchParams(here.search);
124
125
  params.delete(RETURN_PARAM);
125
126
  params.delete(SCROLL_PARAM);
127
+ params.delete(MARKS_PARAM);
126
128
  params.set(PARAM, String(extra.change));
127
129
  if (extra.returnTo) params.set(RETURN_PARAM, extra.returnTo);
128
130
  if (extra.scroll && extra.scroll > 0) params.set(SCROLL_PARAM, String(Math.round(extra.scroll)));
131
+ if (typeof extra.marks === "number" && extra.marks >= 0) params.set(MARKS_PARAM, String(extra.marks));
129
132
  url.pathname = "/";
130
133
  url.search = params.toString();
131
134
  url.hash = here.hash;
@@ -291,6 +294,7 @@ export {
291
294
  dockAt as C,
292
295
  stepDock as D,
293
296
  DOCK_KEY as E,
297
+ MARKS_PARAM as M,
294
298
  OPEN_KEY as O,
295
299
  PARAM as P,
296
300
  SCROLL_PARAM as S,
@@ -311,15 +315,15 @@ export {
311
315
  noVisibleDifference as n,
312
316
  studioChangeUrl as o,
313
317
  contrastRatio as p,
314
- TOGETHER_HINT as q,
318
+ toHex as q,
315
319
  readChanges as r,
316
320
  stepOfToken as s,
317
321
  tokenChanges as t,
318
- isSubtle as u,
319
- toHex as v,
322
+ fromText as u,
323
+ TOGETHER_HINT as v,
320
324
  whereAmI as w,
321
- fromText as x,
325
+ isSubtle as x,
322
326
  isFlipKey as y,
323
327
  placeToolbar as z
324
328
  };
325
- //# sourceMappingURL=session-CzlIsFDY.js.map
329
+ //# sourceMappingURL=session-Dvma9jtT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-Dvma9jtT.js","sources":["../src/session.ts"],"sourcesContent":["// The Bridge toolbar's pure logic: when it shows, which change it follows, what Before and After mean for that\n// change, and the screens it lists. A product opens it only when Bridge Studio links here with `?bridge=<change>`;\n// production renders nothing, except the Before side of a code change, which arrives with a handoff from a preview.\n\nexport interface StorageLike {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\n/**\n * What the toolbar knows about the product and its design system: from bridge.config.json (the product's entry in\n * products[] and previews), read at build time (the Admin demo) or from the design system repository at run time.\n */\nexport interface BridgeBuild {\n /** The repository whose pull requests `?bridge=<n>` names: the product's own repository, else the design system's. */\n owner: string;\n repo: string;\n /** Production Studio (previews.production.docs): the link back and the shipped tokens.css. */\n studio: string | null;\n /** The product in production (products[].production, else previews.production.<key>): Before of a code change. */\n production: string | null;\n /** The product's preview template (products[].preview): only these hosts may hand a code change over to production. */\n preview: string | null;\n /** VERCEL_ENV of the build ('production', 'preview', 'development'), null when unknown. */\n deployEnv: string | null;\n /** The library branch (repo.defaultBranch) and the token files' root (tokens.root): changes tried together. */\n defaultBranch?: string;\n tokensRoot?: string;\n /** The product's key (products[].key) and the design system repository (\"owner/name\"), for the way back to Studio. */\n product?: string;\n ds?: string;\n /** Bridge's origin for GitHub reads (its read proxy); default production Bridge, null to read GitHub directly. */\n proxy?: string | null;\n}\n\nexport const PARAM = 'bridge';\n/** Where After lives for a code change, carried to production on Before. */\nexport const RETURN_PARAM = 'bridge-return';\n/** Scroll position carried across a Before/After navigation. */\nexport const SCROLL_PARAM = 'bridge-scroll';\n/** How many areas a code change marks on the screen, carried from This change to Live product (markers.ts). */\nexport const MARKS_PARAM = 'bridge-marks';\nexport const SESSION_KEY = 'bridge-admin:change';\nexport const RETURN_KEY = 'bridge-admin:return';\nexport const DOCK_KEY = 'bridge-admin:toolbar-dock';\nexport const OPEN_KEY = 'bridge-admin:toolbar-open';\n/** Changes tried together: the picks of the tokens they set differently (core encodePicks), from Studio. */\nexport const USE_PARAM = 'bridge-use';\nexport const USE_KEY = 'bridge-admin:use';\n/** At most this many changes are tried together. */\nexport const MAX_TOGETHER = 4;\n\nconst safe = <T>(fn: () => T, fallback: T): T => {\n try {\n return fn();\n } catch {\n return fallback;\n }\n};\n\n/**\n * The change the toolbar follows: `?bridge=<n>` wins and is kept for the session (the admin's hash router keeps the\n * query, a reload or a new path does not need it again); `?bridge=off` forgets it. Blocked storage only loses the\n * memory, never the toolbar of the current page.\n */\nexport function readChange(search: string, storage: StorageLike | null): number | null {\n return readChanges(search, storage)[0] ?? null;\n}\n\n/**\n * The change numbers in \"101\" or \"101,118,120\" (an encoded comma too): positive, at most 7 digits, no repeats, at\n * most MAX_TOGETHER. Anything else in the list makes it unreadable (empty), never a guess.\n */\nexport function parseChanges(raw: string | null | undefined): number[] {\n const parts = (raw ?? '').trim().split(/,|%2C/i).map((p) => p.trim());\n if (!parts.every((p) => /^\\d{1,7}$/.test(p) && Number(p) > 0)) return [];\n return [...new Set(parts.map(Number))].slice(0, MAX_TOGETHER);\n}\n\n/**\n * The changes the toolbar follows: one from \"Try it in the product\", 2 to 4 from \"Try them together\". Same memory as\n * one change: `?bridge=` wins and is kept for the session, `?bridge=off` forgets it, blocked storage only loses the\n * memory. The picks (`bridge-use`) travel with the list and are kept, or forgotten, with it.\n */\nexport function readChanges(search: string, storage: StorageLike | null): number[] {\n const params = new URLSearchParams(search);\n if (params.has(PARAM)) {\n const list = parseChanges(params.get(PARAM));\n if (!list.length) {\n if (storage) safe(() => (storage.removeItem(SESSION_KEY), storage.removeItem(USE_KEY)), undefined);\n return [];\n }\n if (storage) {\n safe(() => storage.setItem(SESSION_KEY, list.join(',')), undefined);\n const use = params.get(USE_PARAM);\n safe(() => (use && list.length > 1 ? storage.setItem(USE_KEY, use) : storage.removeItem(USE_KEY)), undefined);\n }\n return list;\n }\n return storage ? parseChanges(safe(() => storage.getItem(SESSION_KEY), null)) : [];\n}\n\n/** The picks that came with the list: from the address, else the session. */\nexport function readUse(search: string, storage: StorageLike | null): string | null {\n const params = new URLSearchParams(search);\n if (params.has(PARAM)) return params.get(USE_PARAM);\n return storage ? safe(() => storage.getItem(USE_KEY), null) : null;\n}\n\n/** A host that matches the product's preview template (`bridge-admin-git-{branch}-team.vercel.app`), https only. */\nexport function isPreviewUrl(raw: string | null | undefined, template: string | null): boolean {\n if (!raw || !template) return false;\n let url: URL;\n let tpl: URL;\n try {\n url = new URL(raw);\n tpl = new URL(template.replace(/\\{[a-z]+\\}/gi, 'x'));\n } catch {\n return false;\n }\n if (url.protocol !== 'https:' || url.username || url.password || url.port) return false;\n const tplHost = /^https?:\\/\\/([^/?#]+)/i.exec(template)?.[1]?.toLowerCase();\n if (!tplHost || tpl.protocol !== 'https:') return false;\n const pattern = new RegExp(`^${tplHost.split(/\\{[a-z]+\\}/i).map((p) => p.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')).join('[a-z0-9-]+')}$`);\n return pattern.test(url.hostname.toLowerCase());\n}\n\n/** The preview to return to from production, from `?bridge-return=` or the session. Only a preview host counts. */\nexport function readReturn(search: string, storage: StorageLike | null, template: string | null): string | null {\n const raw = new URLSearchParams(search).get(RETURN_PARAM);\n if (raw !== null) {\n if (isPreviewUrl(raw, template)) {\n const origin = new URL(raw).origin;\n if (storage) safe(() => storage.setItem(RETURN_KEY, origin), undefined);\n return origin;\n }\n return null;\n }\n const stored = storage ? safe(() => storage.getItem(RETURN_KEY), null) : null;\n return isPreviewUrl(stored, template) ? new URL(stored!).origin : null;\n}\n\nexport type Where = 'production' | 'preview' | 'local';\n\n/** Which deployment this page is: the product in production by host or by build, a preview, or a local build. */\nexport function whereAmI(host: string, build: Pick<BridgeBuild, 'production' | 'deployEnv'>): Where {\n const prodHost = build.production ? safe(() => new URL(build.production!).host, null) : null;\n if (build.deployEnv === 'production' || (prodHost && host.toLowerCase() === prodHost.toLowerCase())) return 'production';\n if (build.deployEnv === 'preview') return 'preview';\n return 'local';\n}\n\n/**\n * Whether the toolbar renders. Never without a change from Studio. In production only for the Before side of a code\n * change, that is with a preview to return to: production on its own renders nothing.\n */\nexport function shouldRender(input: { change: number | null; where: Where; returnTo: string | null; together?: boolean }): boolean {\n if (input.change === null) return false;\n // Changes tried together run on production: no preview holds all of them.\n if (input.together) return true;\n if (input.where === 'production') return input.returnTo !== null;\n return true;\n}\n\n/** \"These 3 changes\": the After side of changes tried together, counting the ones switched on. */\nexport const togetherLabel = (n: number) => (n === 1 ? 'This change' : `These ${n} changes`);\n\n/** The one hint line of changes tried together: where they run and why. */\nexport const TOGETHER_HINT = \"The live product, with these changes' tokens applied in your browser only. No preview has all of them together.\";\n\n/** Show what changed found nothing on this screen for changes tried together. */\nexport const TOGETHER_EMPTY_LINE = \"These changes don't touch this screen. Try another screen.\";\n\n/** Switches one change on or off, keeping the order Studio listed them in. */\nexport function toggleIncluded(all: number[], included: number[], n: number, on: boolean): number[] {\n const next = new Set(included);\n if (on) next.add(n);\n else next.delete(n);\n return all.filter((x) => next.has(x));\n}\n\n/** Studio's Changes list, where changes are tried together (and the way back). */\nexport function studioChangesUrl(build: Pick<BridgeBuild, 'studio' | 'owner' | 'repo'>): string {\n return build.studio ? `${build.studio.replace(/\\/$/, '')}/studio/changes/` : `https://github.com/${build.owner}/${build.repo}/pulls`;\n}\n\nexport type ChangeMode = 'tokens' | 'code';\n\n/** Token files (DTCG and the contrast gate) and changesets only: Before swaps tokens live. Anything else is code. */\nexport function changeMode(files: string[]): ChangeMode {\n const tokenOnly = files.length > 0 && files.every((f) => f.endsWith('.tokens.json') || /contrast\\.checks\\.json$/.test(f) || f.startsWith('.changeset/'));\n return tokenOnly ? 'tokens' : 'code';\n}\n\n/** The live tokens (main's build, served by production Studio at `/tokens.css`), or null when not configured. */\nexport function liveTokensUrl(build: Pick<BridgeBuild, 'studio'>): string | null {\n return build.studio ? safe(() => new URL('tokens.css', build.studio!.replace(/\\/?$/, '/')).href, null) : null;\n}\n\n/**\n * Studio's page for the change. A change in a product's own repository (an exploration there) opens as\n * `?exploration=<n>&product=<key>`; a change of the design system repository as `?pr=<n>`.\n */\nexport function studioChangeUrl(build: Pick<BridgeBuild, 'studio' | 'owner' | 'repo' | 'product' | 'ds'>, change: number): string {\n if (!build.studio) return `https://github.com/${build.owner}/${build.repo}/pull/${change}`;\n const studio = build.studio.replace(/\\/$/, '');\n const own = build.product && build.ds && build.ds.toLowerCase() !== `${build.owner}/${build.repo}`.toLowerCase();\n return own ? `${studio}/studio/changes/?exploration=${change}&product=${encodeURIComponent(build.product!)}` : `${studio}/studio/changes/?pr=${change}`;\n}\n\n/** Shape of a visual impact manifest that the toolbar reads (written by @bridge-ds/visual). */\nexport interface ManifestShot {\n kind: 'story' | 'route';\n label: string;\n route?: string;\n tenant?: string;\n theme: string;\n status: string;\n percent: number;\n}\n\nexport interface Screen {\n label: string;\n route: string;\n tenant: string;\n theme: string;\n percent: number;\n}\n\n/** The product screens the change alters, most changed first, one entry per screen, shop and theme. */\nexport function changedScreens(shots: ManifestShot[] | null | undefined): Screen[] {\n if (!Array.isArray(shots)) return [];\n const seen = new Set<string>();\n return shots\n .filter((s) => s.kind === 'route' && s.route && s.tenant && (s.status === 'changed' || s.status === 'added'))\n .sort((a, b) => b.percent - a.percent)\n .filter((s) => {\n const key = `${s.route}|${s.tenant}|${s.theme}`;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n })\n .map((s) => ({ label: s.label, route: s.route!, tenant: s.tenant!, theme: s.theme, percent: s.percent }));\n}\n\n/**\n * The same page on another deployment (code changes: production for Before, the preview for After), with the\n * change, the way back and the scroll position in the query. The hash route (ra-core) is kept.\n */\nexport function sameScreenOn(target: string, here: { search: string; hash: string }, extra: { change: number; returnTo?: string | null; scroll?: number; marks?: number | null }): string {\n const url = new URL(target);\n const params = new URLSearchParams(here.search);\n params.delete(RETURN_PARAM);\n params.delete(SCROLL_PARAM);\n params.delete(MARKS_PARAM);\n params.set(PARAM, String(extra.change));\n if (extra.returnTo) params.set(RETURN_PARAM, extra.returnTo);\n if (extra.scroll && extra.scroll > 0) params.set(SCROLL_PARAM, String(Math.round(extra.scroll)));\n // The areas a code change marks on this screen: Live product says how many wait on This change.\n if (typeof extra.marks === 'number' && extra.marks >= 0) params.set(MARKS_PARAM, String(extra.marks));\n url.pathname = '/';\n url.search = params.toString();\n url.hash = here.hash;\n return url.href;\n}\n\n/** The scroll position a Before/After navigation carried, once: the caller removes it from the address. */\nexport function readScroll(search: string): number | null {\n const raw = new URLSearchParams(search).get(SCROLL_PARAM);\n return raw && /^\\d{1,7}$/.test(raw) ? Number(raw) : null;\n}\n\nexport type Dock = 'start' | 'center' | 'end';\nconst DOCKS: Dock[] = ['start', 'center', 'end'];\n\n/** Bottom right by default: the corner a back office leaves free most often. */\nexport function readDock(storage: StorageLike | null): Dock {\n const v = storage ? safe(() => storage.getItem(DOCK_KEY), null) : null;\n return DOCKS.includes(v as Dock) ? (v as Dock) : 'end';\n}\n\n/** One step left (-1) or right (+1), stopping at the edges. */\nexport function stepDock(dock: Dock, step: -1 | 1): Dock {\n return DOCKS[Math.min(DOCKS.length - 1, Math.max(0, DOCKS.indexOf(dock) + step))]!;\n}\n\n/** The dock nearest to where a drag ended: thirds of the viewport width. */\nexport function dockAt(x: number, width: number): Dock {\n if (width <= 0) return 'center';\n const r = x / width;\n return r < 1 / 3 ? 'start' : r > 2 / 3 ? 'end' : 'center';\n}\n\n/** Title of a change as people read it: conventional prefixes (\"tokens: \") dropped, first letter up. */\nexport function readableTitle(title: string): string {\n const t = title.replace(/^\\s*[a-z]+(\\([^)]*\\))?!?:\\s*/i, '').trim() || title.trim();\n return t.charAt(0).toUpperCase() + t.slice(1);\n}\n\n/** The B shortcut flips Before and After, unless the person is typing or holds a modifier. */\nexport function isFlipKey(e: { key: string; altKey: boolean; ctrlKey: boolean; metaKey: boolean; target: unknown }): boolean {\n if (e.altKey || e.ctrlKey || e.metaKey || e.key.toLowerCase() !== 'b') return false;\n const t = e.target as { tagName?: string; isContentEditable?: boolean; closest?: (s: string) => unknown } | null;\n if (!t) return true;\n if (t.isContentEditable) return false;\n const tag = (t.tagName ?? '').toLowerCase();\n if (tag === 'input' || tag === 'textarea' || tag === 'select') return false;\n return !(t.closest && t.closest('[role=\"combobox\"], [role=\"textbox\"], [role=\"searchbox\"]'));\n}\n\nexport interface Rect {\n left: number;\n top: number;\n right: number;\n bottom: number;\n}\n\nconst overlaps = (a: Rect, b: Rect, margin: number) => a.left < b.right + margin && a.right > b.left - margin && a.top < b.bottom + margin && a.bottom > b.top - margin;\n\n/**\n * Where the toolbar goes so it never covers a primary or submit button of the page: the chosen dock if it is free,\n * else the opposite corner, else the centre, else the chosen dock lifted just above the buttons it would cover. The\n * docks sit inside the area [left, right] (the content column, so it never sits over the sidebar).\n */\nexport function placeToolbar(input: { left: number; right: number; vh: number; w: number; h: number; gutter: number; preferred: Dock; obstacles: Rect[]; margin?: number }): { dock: Dock; lift: number } {\n const { left: areaLeft, right: areaRight, vh, w, h, gutter, preferred, obstacles } = input;\n const margin = input.margin ?? 8;\n const rectAt = (dock: Dock, lift = 0): Rect => {\n const left = dock === 'start' ? areaLeft : dock === 'end' ? areaRight - w : (areaLeft + areaRight - w) / 2;\n const bottom = vh - gutter - lift;\n return { left, right: left + w, top: bottom - h, bottom };\n };\n const opposite: Dock = preferred === 'start' ? 'end' : 'start';\n const order: Dock[] = [preferred, opposite, ...DOCKS.filter((d) => d !== preferred && d !== opposite)];\n for (const dock of order) {\n const r = rectAt(dock);\n if (!obstacles.some((o) => overlaps(r, o, margin))) return { dock, lift: 0 };\n }\n // Every dock covers something: stay put and rise above the highest button in the way.\n const base = rectAt(preferred);\n const inWay = obstacles.filter((o) => o.left < base.right + margin && o.right > base.left - margin && o.bottom > base.top - margin);\n const top = Math.min(...inWay.map((o) => o.top));\n const lift = Math.max(0, Math.ceil(base.bottom - (top - margin)));\n return { dock: preferred, lift: Math.min(lift, Math.max(0, vh - h - 2 * gutter)) };\n}\n\n/** What one token file can change on screen: a theme and a brand, or every one of them (null). */\nfunction fileScope(file: string): { theme: string | null; brand: string | null } | 'none' {\n if (file.startsWith('.changeset/')) return 'none';\n let m = /(?:^|\\/)semantic\\/([a-z0-9-]+)\\.tokens\\.json$/i.exec(file);\n if (m && m[1] !== 'base') return { theme: m[1]!, brand: null };\n m = /(?:^|\\/)brands\\/([a-z0-9-]+)\\.([a-z0-9-]+)\\.tokens\\.json$/i.exec(file);\n if (m) return { theme: m[2]!, brand: m[1]! };\n m = /(?:^|\\/)brands\\/([a-z0-9-]+)\\.tokens\\.json$/i.exec(file);\n if (m) return { theme: null, brand: m[1]! };\n // Primitives, base, density, components and anything else: could change every theme and brand.\n return { theme: null, brand: null };\n}\n\nexport type NoDifference = { kind: 'theme'; here: string; touched: string } | { kind: 'brand'; here: string; touched: string };\n\n/**\n * Whether a token change shows nothing in this theme and brand, from the token files it edits (DTCG layers named\n * semantic/<theme>, brands/<brand>.<theme>, brands/<brand>). Null when it may show, or when that cannot be told:\n * never claims \"no difference\" on a guess.\n */\nexport function noVisibleDifference(files: string[], here: { theme: string; brand: string }): NoDifference | null {\n const scopes = files.map(fileScope).filter((s): s is { theme: string | null; brand: string | null } => s !== 'none');\n if (!scopes.length) return null;\n const hits = (theme: string, brand: string) => scopes.some((s) => (s.theme === null || s.theme === theme) && (s.brand === null || s.brand === brand));\n if (hits(here.theme, here.brand)) return null;\n const theme = scopes.find((s) => s.theme !== null && s.theme !== here.theme && (s.brand === null || s.brand === here.brand))?.theme;\n if (theme) return { kind: 'theme', here: here.theme, touched: theme };\n const brand = scopes.find((s) => s.brand !== null && s.brand !== here.brand)?.brand;\n return brand ? { kind: 'brand', here: here.brand, touched: brand } : null;\n}\n\n/** \"darker default background.\" The change title as a short phrase after \"This change:\". */\nexport function changeSummary(title: string): string {\n const t = readableTitle(title).replace(/[.\\s]+$/, '');\n if (!t) return '';\n const lower = /^[A-Z][a-z]/.test(t) ? t.charAt(0).toLowerCase() + t.slice(1) : t;\n return `${lower}.`;\n}\n\nexport interface TokenChange {\n /** Token path, \"color.bg.default\". */\n path: string;\n /** CSS variable the token builds to, \"--color-bg-default\". */\n cssVar: string;\n /** The role it plays, what designers call it: \"Page canvas\" (its description), \"Brand primary\" (a palette role). */\n name: string;\n /** The exact step, second: \"Primary 700\" for a palette step, null for a semantic token. */\n step: string | null;\n type: string | null;\n /** The theme and brand its file applies to (null: every one). */\n theme: string | null;\n brand: string | null;\n}\n\ntype TokenNode = { $value?: unknown; $type?: string; $description?: string; [k: string]: unknown };\ntype Leaf = { value: string; type: string | null; description: string | null };\n\nfunction leaves(node: unknown, path: string[] = [], inheritedType: string | null = null, out = new Map<string, Leaf>()): Map<string, Leaf> {\n if (!node || typeof node !== 'object') return out;\n const n = node as TokenNode;\n const type = typeof n.$type === 'string' ? n.$type : inheritedType;\n if ('$value' in n) {\n out.set(path.join('.'), { value: JSON.stringify(n.$value), type, description: typeof n.$description === 'string' ? n.$description : null });\n return out;\n }\n for (const [k, v] of Object.entries(n)) if (!k.startsWith('$')) leaves(v, [...path, k], type, out);\n return out;\n}\n\n/** Palette roles a brand re-points (brands/<brand>.tokens.json), in designers' words. */\nconst PALETTE_ROLES: Record<string, string> = { primary: 'Brand primary', link: 'Link', neutral: 'Neutral', accent: 'Accent', success: 'Success', warning: 'Warning', danger: 'Danger', info: 'Info', categorical: 'Chart colours' };\n\n/** The role a token plays: its description's first phrase, a palette role (\"Brand primary\"), else its path in words. */\nexport function roleOfToken(path: string, description: string | null): string {\n const first = description?.split(/[.:;(]/)[0]?.trim();\n if (first && first.length <= 40) return first;\n const m = /^palette\\.([a-z-]+)\\./.exec(path);\n if (m) return PALETTE_ROLES[m[1]!] ?? designerName(`palette.${m[1]}`, null);\n return designerName(path, null);\n}\n\n/** The palette step, shown second: \"palette.primary.700\" -> \"Primary 700\"; null for other tokens. */\nexport function stepOfToken(path: string): string | null {\n return path.startsWith('palette.') ? designerName(path, null) : null;\n}\n\n/** Designer name of a token: the first phrase of its description, else its path in words (\"Bg default\"). */\nexport function designerName(path: string, description: string | null): string {\n const first = description?.split(/[.:;(]/)[0]?.trim();\n if (first && first.length <= 40) return first;\n const words = path.replace(/^(color|palette)\\./, '').split('.').join(' ');\n return words.charAt(0).toUpperCase() + words.slice(1);\n}\n\n/**\n * The tokens one file changes between two versions of it (parsed DTCG JSON, null when the file is added or removed):\n * every leaf whose $value differs, with the theme and brand its layer applies to.\n */\nexport function tokenChanges(file: string, before: unknown, after: unknown): TokenChange[] {\n const scope = fileScope(file);\n if (scope === 'none') return [];\n const a = leaves(before);\n const b = leaves(after);\n const out: TokenChange[] = [];\n for (const path of new Set([...a.keys(), ...b.keys()])) {\n const x = a.get(path);\n const y = b.get(path);\n if (x?.value === y?.value) continue;\n out.push({\n path,\n cssVar: `--${path.split('.').join('-')}`,\n name: roleOfToken(path, y?.description ?? x?.description ?? null),\n step: stepOfToken(path),\n type: y?.type ?? x?.type ?? null,\n theme: scope.theme,\n brand: scope.brand,\n });\n }\n return out;\n}\n\n/** WCAG contrast ratio between two rgb() colours, or null when either is not rgb (a non-colour value). */\nexport function contrastRatio(a: string, b: string): number | null {\n const lum = (c: string) => {\n const m = /^rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/.exec(c.trim());\n if (!m) return null;\n const [r, g, bl] = [m[1], m[2], m[3]].map((n) => {\n const v = Number(n) / 255;\n return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;\n }) as [number, number, number];\n return 0.2126 * r + 0.7152 * g + 0.0722 * bl;\n };\n const x = lum(a);\n const y = lum(b);\n if (x === null || y === null) return null;\n return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05);\n}\n\n/** Two colours under 1.1:1 read as the same at a glance: the swatch says \"Subtle change\" so it does not look broken. */\nexport const isSubtle = (a: string, b: string): boolean => {\n const r = contrastRatio(a, b);\n return r !== null && r < 1.1;\n};\n\n/** rgb(245, 247, 249) -> #f5f7f9; anything else as is. */\nexport function toHex(color: string): string {\n const m = /^rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*([\\d.]+))?\\)$/.exec(color.trim());\n if (!m) return color;\n const hex = [m[1], m[2], m[3]].map((n) => Number(n).toString(16).padStart(2, '0')).join('');\n return m[4] !== undefined && Number(m[4]) < 1 ? `#${hex}${Math.round(Number(m[4]) * 255).toString(16).padStart(2, '0')}` : `#${hex}`;\n}\n\n/** \"from Darker default background\", \"from #101 and #118\": which change a token's value comes from (changes tried together). */\nexport function fromText(from: number[], titleOf: (n: number) => string | null): string {\n const names = from.map((n) => titleOf(n) ?? `#${n}`);\n return `from ${names.length <= 2 ? names.join(' and ') : `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`}`;\n}\n"],"names":["lift","theme","brand"],"mappings":"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"}
package/dist/session.d.ts CHANGED
@@ -33,6 +33,8 @@ export declare const PARAM = "bridge";
33
33
  export declare const RETURN_PARAM = "bridge-return";
34
34
  /** Scroll position carried across a Before/After navigation. */
35
35
  export declare const SCROLL_PARAM = "bridge-scroll";
36
+ /** How many areas a code change marks on the screen, carried from This change to Live product (markers.ts). */
37
+ export declare const MARKS_PARAM = "bridge-marks";
36
38
  export declare const SESSION_KEY = "bridge-admin:change";
37
39
  export declare const RETURN_KEY = "bridge-admin:return";
38
40
  export declare const DOCK_KEY = "bridge-admin:toolbar-dock";
@@ -128,6 +130,7 @@ export declare function sameScreenOn(target: string, here: {
128
130
  change: number;
129
131
  returnTo?: string | null;
130
132
  scroll?: number;
133
+ marks?: number | null;
131
134
  }): string;
132
135
  /** The scroll position a Before/After navigation carried, once: the caller removes it from the address. */
133
136
  export declare function readScroll(search: string): number | null;
package/dist/together.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { combineChanges, buildTokenSet, previewOverrides, cssVarOf, resolve, parseColor, cssOf } from "@bridge-ds/core";
2
2
  import { changeEdits, decodePicks } from "@bridge-ds/core";
3
- import { s as stepOfToken, a as roleOfToken } from "./session-CzlIsFDY.js";
3
+ import { s as stepOfToken, a as roleOfToken } from "./session-Dvma9jtT.js";
4
4
  function togetherBase(latest, options, modes) {
5
5
  return { latest, options, modes, main: buildTokenSet(latest, options) };
6
6
  }
@@ -0,0 +1,2 @@
1
+ import type { ChangedElement } from './outline';
2
+ export declare function useMarkedChanges(on: boolean, screen: string): ChangedElement[] | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge-ds/toolbar",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "The Bridge toolbar for a product built with the Bridge design system: opened from Bridge Studio with ?bridge=<change>, it flips the real product between before and after and shows what changed.",
6
6
  "license": "MIT",
@@ -49,7 +49,7 @@
49
49
  "build:package": "node ../../scripts/build-packages.mjs toolbar"
50
50
  },
51
51
  "dependencies": {
52
- "@bridge-ds/core": "^0.23.0",
52
+ "@bridge-ds/core": "^0.24.1",
53
53
  "lucide-react": "1.58.0"
54
54
  },
55
55
  "peerDependencies": {