@bridge-ds/core 0.5.1 → 0.7.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.
@@ -5,30 +5,37 @@ export declare const DEFAULT_COMPONENT_SPECS_FILE = "packages/react/component-sp
5
5
  /**
6
6
  * A Figma-side property a CSS declaration maps to.
7
7
  * Frames: fill (background-color), stroke and stroke weights (border), cornerRadius (border-radius), paddings, itemSpacing
8
- * (gap), sizes. Text: textFill (color), font family, size, weight, line height, letter spacing.
8
+ * (gap), sizes. Text: textFill (color), font family, size, weight, line height, letter spacing, and textDecoration (a
9
+ * keyword, "UNDERLINE" or "STRIKETHROUGH", not a token).
9
10
  */
10
- export type SpecField = 'fill' | 'stroke' | 'strokeWeight' | 'strokeTopWeight' | 'strokeRightWeight' | 'strokeBottomWeight' | 'strokeLeftWeight' | 'cornerRadius' | 'paddingTop' | 'paddingRight' | 'paddingBottom' | 'paddingLeft' | 'itemSpacing' | 'width' | 'height' | 'minWidth' | 'minHeight' | 'textFill' | 'fontFamily' | 'fontSize' | 'fontWeight' | 'lineHeight' | 'letterSpacing';
11
+ export type SpecField = 'fill' | 'stroke' | 'strokeWeight' | 'strokeTopWeight' | 'strokeRightWeight' | 'strokeBottomWeight' | 'strokeLeftWeight' | 'cornerRadius' | 'paddingTop' | 'paddingRight' | 'paddingBottom' | 'paddingLeft' | 'itemSpacing' | 'counterAxisSpacing' | 'width' | 'height' | 'minWidth' | 'minHeight' | 'opacity' | 'textFill' | 'fontFamily' | 'fontSize' | 'fontWeight' | 'lineHeight' | 'letterSpacing' | 'textDecoration';
11
12
  export declare const SPEC_FIELDS: readonly SpecField[];
12
13
  /** Fields whose token must be a color. */
13
14
  export declare const COLOR_FIELDS: ReadonlySet<SpecField>;
14
15
  /**
15
- * A token path ("button.primary.bg"), a literal zero (`padding: 0`), null: no paint (`transparent`), or a calc() of token
16
- * paths ("calc(space.2 + space.0-5)", operators + - * / and px numbers): resolved to a number, set but not bound.
16
+ * A token path ("button.primary.bg"), a literal number (`padding: 0`, a unitless `stroke-width: 3` or `opacity: 0.5`),
17
+ * null: no paint (`transparent`), or a calc() of token paths ("calc(space.2 + space.0-5)", operators + - * / and px
18
+ * numbers): resolved to a number, set but not bound.
17
19
  */
18
- export type SpecValue = string | 0 | null;
20
+ export type SpecValue = string | number | null;
19
21
  export interface SpecLayout {
20
22
  direction: 'HORIZONTAL' | 'VERTICAL';
21
23
  /** justify-content. */
22
24
  primary: 'MIN' | 'CENTER' | 'MAX' | 'SPACE_BETWEEN';
23
25
  /** align-items. STRETCH: children fill the cross axis. */
24
26
  counter: 'MIN' | 'CENTER' | 'MAX' | 'STRETCH';
27
+ /** `flex-direction: row-reverse | column-reverse`: children are drawn in reverse order. */
28
+ reverse?: boolean | undefined;
29
+ /** `flex-wrap: wrap` on a row: items wrap to new lines (Figma auto layout wrap). */
30
+ wrap?: boolean | undefined;
25
31
  }
26
32
  export interface SpecPart {
27
33
  /** Stable id inside the component ("root", "label", "icon-start"). Figma layers keep it in plugin data. */
28
34
  id: string;
29
35
  /** Layer name in Figma. */
30
36
  name: string;
31
- kind: 'frame' | 'text' | 'icon';
37
+ /** vector: an inline SVG drawn from the variant's `vectors` markup (a spinner arc, a progress ring). */
38
+ kind: 'frame' | 'text' | 'icon' | 'vector';
32
39
  /** Parent part id; undefined for the root (the variant itself). */
33
40
  parent?: string | undefined;
34
41
  /** CSS module class the part is styled by ("Button.module.css .label"), for people reading the spec. */
@@ -42,11 +49,44 @@ export interface SpecPart {
42
49
  clip?: boolean | undefined;
43
50
  /** Root only: block-level in code (display block, flex or grid, or width 100%), so it fills its container. Figma draws it at a fixed width. */
44
51
  fill?: boolean | undefined;
52
+ /** Text parts with no TEXT property (content written in code, like a group's option labels): the characters drawn. */
53
+ characters?: string | undefined;
54
+ /**
55
+ * Positioned over its parent instead of flowing in its auto layout (an avatar photo over its initials): `position:
56
+ * absolute` with `inset: 0`, or `width` and `height` 100%. Drawn as an absolute child that fills its parent.
57
+ */
58
+ cover?: boolean | undefined;
59
+ /**
60
+ * `position: absolute` with token offsets (a dialog's close button): out of the auto layout, pinned to the parent's
61
+ * edges by these offsets (top, right, bottom, left).
62
+ */
63
+ pin?: {
64
+ top?: SpecValue;
65
+ right?: SpecValue;
66
+ bottom?: SpecValue;
67
+ left?: SpecValue;
68
+ } | undefined;
69
+ /** `width: 100%` or `height: 100%` in a parent of that size: Fill container on that axis. */
70
+ stretch?: {
71
+ width?: boolean;
72
+ height?: boolean;
73
+ } | undefined;
74
+ /** A value indicator (a progress fill): this share of its parent's width, from the component's value. */
75
+ fraction?: number | undefined;
76
+ /**
77
+ * A thumb placed at the component's value (a slider's): out of the auto layout, its center at this share of its
78
+ * parent's width, centered on the parent's height.
79
+ */
80
+ at?: number | undefined;
81
+ /** `flex: 1` in code: takes the free space along its parent's auto layout direction (Figma "Fill container"). */
82
+ grow?: boolean | undefined;
45
83
  /**
46
84
  * A swappable part: drawn as an instance of a generated component ("Icon/Plus", "Slot/Media") and swapped through an
47
85
  * INSTANCE_SWAP property. Icon parts swap between every icon of `icons`; content slots between `options`.
48
86
  */
49
87
  swap?: SpecSwap | undefined;
88
+ /** Icon parts whose icon is written in code (a checkbox's check mark): an instance of this icon, with no property. */
89
+ icon?: string | undefined;
50
90
  }
51
91
  export interface SpecSwap {
52
92
  /** INSTANCE_SWAP property name ("Icon", "Icon start", "Body"). */
@@ -117,6 +157,72 @@ export interface SpecVariant {
117
157
  values: Record<string, Partial<Record<SpecField, SpecValue>>>;
118
158
  /** Swappable part id -> the component this variant shows by default ("Icon/Plus", "Slot/Media"). */
119
159
  slots?: Record<string, string> | undefined;
160
+ /** Parts this variant does not show (`display: none`, `opacity: 0` or `scale(0)` in this state): hidden layers. */
161
+ hidden?: string[] | undefined;
162
+ /** Parts whose auto layout differs from the part's own in this variant (a switch thumb at the end when on). */
163
+ layout?: Record<string, SpecLayout> | undefined;
164
+ /** Vector parts: the SVG this variant draws and the stroke token of each shape, in order. */
165
+ vectors?: Record<string, SpecVector> | undefined;
166
+ /** Absolute parts placed differently in this variant (offsets from the parent's edges, like SpecPart.pin). */
167
+ pins?: Record<string, NonNullable<SpecPart['pin']>> | undefined;
168
+ }
169
+ /** An inline SVG part: markup with literal geometry (strokes painted #000000) and the token bound to each shape's stroke. */
170
+ export interface SpecVector {
171
+ svg: string;
172
+ strokes: SpecValue[];
173
+ }
174
+ /**
175
+ * A prototype trigger, Figma's names. ON_HOVER "While hovering" and ON_PRESS "While pressing" go back when the pointer
176
+ * leaves or lets go; ON_CLICK "On click" stays. A set with a Checked axis uses MOUSE_ENTER / MOUSE_LEAVE for hover, so a
177
+ * click that changes Checked is not undone when the pointer leaves.
178
+ */
179
+ export type SpecTrigger = 'ON_HOVER' | 'ON_PRESS' | 'ON_CLICK' | 'MOUSE_ENTER' | 'MOUSE_LEAVE';
180
+ /** Designer names of the triggers, as Figma's prototype panel shows them. */
181
+ export declare const TRIGGER_LABELS: Record<SpecTrigger, string>;
182
+ /** An easing Figma can play: linear, or a cubic bezier with the exact points of the code's curve. */
183
+ export interface SpecEasing {
184
+ type: 'LINEAR' | 'CUSTOM_CUBIC_BEZIER';
185
+ /** CUSTOM_CUBIC_BEZIER: x1, y1, x2, y2. */
186
+ bezier?: [number, number, number, number] | undefined;
187
+ }
188
+ /** The code's CSS transition for one change, resolved from the motion tokens. Played with Smart animate. */
189
+ export interface SpecTransition {
190
+ /** Milliseconds. */
191
+ duration: number;
192
+ /** Token behind the duration ("motion.duration.instant"), when it is one. */
193
+ durationToken?: string | undefined;
194
+ easing: SpecEasing;
195
+ /** Token behind the easing ("motion.easing.standard"), when it is one. */
196
+ easingToken?: string | undefined;
197
+ /** CSS properties the code animates in this change ("background-color"). */
198
+ properties: string[];
199
+ /** Why Figma plays it slightly differently from the code (one speed for parts that move at several), when it does. */
200
+ approximated?: string | undefined;
201
+ }
202
+ /**
203
+ * A prototype interaction between variants of the set, read from the CSS: on `trigger`, every variant whose `axis` is
204
+ * `from` changes to the variant with `to` instead (other axes kept). `variants` narrows it to those variant keys
205
+ * (variantKeyOf) when it does not apply to all of them. `transition` null: the code changes it instantly.
206
+ */
207
+ export interface SpecInteraction {
208
+ trigger: SpecTrigger;
209
+ axis: string;
210
+ from: string;
211
+ to: string;
212
+ variants?: string[] | undefined;
213
+ transition: SpecTransition | null;
214
+ }
215
+ /** Motion the code has that a Figma prototype cannot play (a looping spinner, keyboard focus, a popup opening). */
216
+ export interface SpecMotionNote {
217
+ /** What moves, in plain words ("Looping animation spin, 700 ms"). */
218
+ what: string;
219
+ /** Why Figma does not play it. */
220
+ reason: string;
221
+ /** A keyframe animation: the token behind its duration ("motion.duration.spin"), when it is one. */
222
+ durationToken?: string | undefined;
223
+ /** Opening and closing: the code's transition when it opens (enter) and when it closes (exit), when it has one. */
224
+ enter?: SpecTransition | undefined;
225
+ exit?: SpecTransition | undefined;
120
226
  }
121
227
  /** A declaration, prop or part that has no Figma equivalent in this version, with the reason. */
122
228
  export interface SpecUnmapped {
@@ -127,6 +233,26 @@ export interface SpecUnmapped {
127
233
  }
128
234
  export interface ComponentSpec {
129
235
  name: string;
236
+ /** Component folder the set comes from, when it is not the set's name (Radio comes from RadioGroup). */
237
+ component?: string | undefined;
238
+ /** Props pinned for this set (`{ variant: 'circular' }` for Progress/Circular, a set per layer tree). */
239
+ fixed?: Record<string, string> | undefined;
240
+ /** A popup drawn open (`popup`), or the component without its popup (`without-popup`). */
241
+ view?: 'popup' | 'without-popup' | undefined;
242
+ /**
243
+ * Drafted with Claude, not generated: the generator cannot read this component, so its spec was written by hand from
244
+ * the code (packages/react/component-spec-overrides/<Name>.json, `"source": "drafted"`) and validated like a generated
245
+ * one. Shown in the plugin as "Drafted, check it". `stale`: the code it was drafted from changed since.
246
+ */
247
+ drafted?: {
248
+ reason: string;
249
+ stale?: boolean | undefined;
250
+ } | undefined;
251
+ /**
252
+ * The code example the set's content comes from (the docs preview or a component sandbox example): the element, its
253
+ * props and children, so the fidelity check renders the same thing. Not in the hash: it does not change the drawing.
254
+ */
255
+ example?: SpecExample | undefined;
130
256
  /** FNV-1a of everything else in the spec: unchanged hash, nothing to rebuild. */
131
257
  hash: string;
132
258
  /** Files it was read from, repo-relative. */
@@ -136,6 +262,16 @@ export interface ComponentSpec {
136
262
  parts: SpecPart[];
137
263
  variants: SpecVariant[];
138
264
  unmapped: SpecUnmapped[];
265
+ /** Prototype interactions between the variants (hover, press, click to check), with the code's motion. */
266
+ interactions?: SpecInteraction[] | undefined;
267
+ /** Motion of the code a Figma prototype cannot play. */
268
+ motion?: SpecMotionNote[] | undefined;
269
+ }
270
+ /** An element of a code example: component export name, literal props, children (texts or elements). */
271
+ export interface SpecExample {
272
+ c: string;
273
+ p?: Record<string, unknown> | undefined;
274
+ children?: (string | SpecExample)[] | undefined;
139
275
  }
140
276
  export interface ComponentSpecsFile {
141
277
  version: number;
@@ -147,18 +283,113 @@ export interface ComponentSpecsFile {
147
283
  iconStyle?: SpecIconStyle | undefined;
148
284
  /** Content components slots swap to. */
149
285
  slots?: SpecSlot[] | undefined;
286
+ /**
287
+ * Every component of the design system and whether it is in Figma: generated (its sets are in `components`),
288
+ * unsupported (with the generator's reason) or layout (no visual of its own: a layout helper, use auto layout).
289
+ * Undefined in files written before coverage was recorded.
290
+ */
291
+ coverage?: SpecCoverage[] | undefined;
292
+ }
293
+ export type CoverageStatus = 'generated' | 'drafted' | 'unsupported' | 'layout';
294
+ export interface SpecCoverage {
295
+ /** Component folder name ("RadioGroup"). */
296
+ name: string;
297
+ status: CoverageStatus;
298
+ /** Plain reason, for unsupported, drafted and layout components ("opens a popup", "uses CSS grid"). */
299
+ reason?: string | undefined;
300
+ /** Sets generated from it, when they are not just its own name ("RadioGroup", "Radio"). */
301
+ sets?: string[] | undefined;
150
302
  }
303
+ /**
304
+ * Coverage counts from a specs file: visual components (generated, drafted with Claude, unsupported), in Figma
305
+ * (generated + drafted), and layout helpers.
306
+ */
307
+ export declare function coverageSummary(file: Pick<ComponentSpecsFile, 'coverage' | 'components'>): {
308
+ generated: number;
309
+ drafted: number;
310
+ inFigma: number;
311
+ visual: number;
312
+ layout: number;
313
+ total: number;
314
+ missing: SpecCoverage[];
315
+ };
316
+ /**
317
+ * Checks a component spec against the format (the schema every set follows, generated or drafted): axes and their
318
+ * variants (one per combination, no extra), properties, parts (one root, known parents in order, text and visibility
319
+ * properties that exist), each value's field and type, and, given `tokenTypes` (token path -> $type), that every token
320
+ * path exists and has a type the field takes. Returns the problems in plain language, none when the spec is valid.
321
+ */
322
+ export declare function validateComponentSpec(spec: Omit<ComponentSpec, 'hash'> & {
323
+ hash?: string;
324
+ }, tokenTypes?: Map<string, string>): string[];
151
325
  /** Figma component name of an icon: "Icon/Plus". */
152
326
  export declare const iconComponentName: (name: string) => string;
153
327
  /** FNV-1a, 32 bits, 8 hex digits. */
154
328
  export declare function specHash(text: string): string;
155
- /** Hash of a spec without its own hash field. */
329
+ /** Hash of a spec without its own hash field (and without its code example, which does not change the drawing). */
156
330
  export declare function hashSpec(spec: Omit<ComponentSpec, 'hash'> | ComponentSpec): string;
157
331
  /** Figma variant name for a variant's props: "Variant=primary, Size=md, State=Default" (axis order). */
158
332
  export declare const variantName: (props: Record<string, string>) => string;
159
333
  /** Variant key independent of axis order (same as the plugin's signature keys): sorted "Size=md, Variant=primary". */
160
334
  export declare const variantKeyOf: (props: Record<string, string>) => string;
335
+ /** What a set's description says about reduced motion: tokens set every duration to 0 under prefers-reduced-motion. */
336
+ export declare const REDUCED_MOTION_NOTE = "In code, reduced motion turns this off.";
337
+ /** One prototype reaction: on `trigger`, the variant `fromKey` changes to `toKey`, with the code's transition. */
338
+ export interface MotionReaction {
339
+ trigger: SpecTrigger;
340
+ /** Figma's name for the trigger ("While hovering"). */
341
+ label: string;
342
+ from: Record<string, string>;
343
+ to: Record<string, string>;
344
+ /** variantKeyOf(from) and variantKeyOf(to). */
345
+ fromKey: string;
346
+ toKey: string;
347
+ transition: SpecTransition | null;
348
+ }
349
+ /** A component set's motion and interactions, read from its spec: for Figma prototypes, docs and reviews. */
350
+ export interface ComponentMotion {
351
+ name: string;
352
+ /** Every reaction between variants, one per variant it starts from. */
353
+ reactions: MotionReaction[];
354
+ /** The interactions as the spec lists them (one per trigger and change), with the number of variants each covers. */
355
+ triggers: (SpecInteraction & {
356
+ label: string;
357
+ count: number;
358
+ })[];
359
+ /** Motion tokens the transitions use ("motion.duration.instant", "motion.easing.standard"), sorted. */
360
+ tokens: string[];
361
+ /** CSS properties that animate ("background-color"), sorted. */
362
+ properties: string[];
363
+ /** Where Figma plays the motion differently from the code. */
364
+ approximations: string[];
365
+ /** Motion of the code a Figma prototype cannot play. */
366
+ cannotPlay: SpecMotionNote[];
367
+ /** Opening and closing (a popup, a panel): the code's enter and exit transitions, when it has them. */
368
+ opening: {
369
+ enter: SpecTransition | null;
370
+ exit: SpecTransition | null;
371
+ } | undefined;
372
+ /** REDUCED_MOTION_NOTE when the component animates in code, else undefined. */
373
+ reducedMotion: string | undefined;
374
+ }
375
+ /**
376
+ * The motion of a component set from its spec: every reaction between its variants (trigger, from and to variants, the
377
+ * transition with duration and easing resolved from the motion tokens), the tokens and properties involved, what Figma
378
+ * approximates or cannot play, and the reduced motion note. Pure: works in the plugin, the browser and Node.
379
+ */
380
+ export declare function componentMotion(spec: Pick<ComponentSpec, 'name' | 'variants' | 'interactions' | 'motion'>): ComponentMotion;
381
+ /**
382
+ * Sets whose interactions use a motion token, directly or through an alias: `chainOf(path)` gives the tokens a path
383
+ * resolves through (resolve(...).chain), so a change to a primitive ("duration.75") finds the sets using
384
+ * "motion.duration.instant". Without it, only direct uses count. Sorted set names.
385
+ */
386
+ export declare function componentsUsingMotionToken(specs: Pick<ComponentSpecsFile, 'components'>, tokenPath: string, chainOf?: (path: string) => string[]): string[];
161
387
  /** Parses component-specs.json; throws with a readable message when it is not a specs file of a known version. */
162
388
  export declare function parseComponentSpecs(text: string): ComponentSpecsFile;
163
389
  /** CSS custom property of a token path, with the repo convention (`cssVar` in bridge.config.json): "--" + path joined by "-". */
164
390
  export declare const tokenCssVar: (path: string, prefix?: string, separator?: string) => string;
391
+ /** How a component stands in Figma, in Studio's words (the "In Figma" column): a short word and, for the ones not in Figma, the reason as a tooltip. Undefined: no data. */
392
+ export declare function figmaCell(entry: SpecCoverage | undefined): {
393
+ label: string;
394
+ title?: string;
395
+ } | undefined;
package/dist/config.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { IntegrationsConfig } from './requests';
2
2
  import { type PreviewApps, type PreviewsConfig } from './previews';
3
3
  import { type Rights, type VisualImpactConfig } from './rights';
4
+ import { type ProductConfig } from './products';
4
5
  /** File name Bridge looks for at the repository root. */
5
6
  export declare const CONFIG_FILE = "bridge.config.json";
6
7
  export interface RepoRef {
@@ -151,6 +152,8 @@ export interface BridgeConfig {
151
152
  integrations?: IntegrationsConfig | undefined;
152
153
  /** Visual before and after on pull requests: automatic (default) or on demand (label `visual-check`). */
153
154
  visualImpact?: VisualImpactConfig | undefined;
155
+ /** Apps that consume the design system, the default one being the library (see products.ts). Default DEFAULT_PRODUCTS. */
156
+ products: ProductConfig[];
154
157
  }
155
158
  type Partialish<T> = {
156
159
  [K in keyof T]?: T[K] | undefined;
@@ -210,6 +213,7 @@ export interface BridgeConfigInput {
210
213
  };
211
214
  integrations?: IntegrationsConfig;
212
215
  visualImpact?: Partial<VisualImpactConfig>;
216
+ products?: ProductConfig[];
213
217
  }
214
218
  export declare const DEFAULT_TOKEN_PATHS: TokenPaths;
215
219
  export declare const DEFAULT_GOVERNANCE: GovernanceConfig;
@@ -0,0 +1,82 @@
1
+ import { type ComponentDocsJson } from './docs-draft';
2
+ import type { GitHubClient } from './github-client';
3
+ import { type RiskResult } from './risk';
4
+ /** One component's docs.json, before (null: a new file) and after, at its repo path. */
5
+ export interface DocsFileEdit {
6
+ path: string;
7
+ component: string;
8
+ slug: string;
9
+ before: string | null;
10
+ after: string;
11
+ }
12
+ /** Who wrote the docs, for the pull request body and its source label. */
13
+ export interface DocsOrigin {
14
+ /** "in the Bridge docs editor". */
15
+ where: string;
16
+ footer: string;
17
+ label: string;
18
+ }
19
+ export declare const DOCS_EDITOR_ORIGIN: DocsOrigin;
20
+ export declare const DOCS_MCP_ORIGIN: DocsOrigin;
21
+ export interface DocsIssue {
22
+ field: string;
23
+ message: string;
24
+ }
25
+ /** What blocks proposing a docs.json: a summary, no empty list items, no em dash (Bridge voice). */
26
+ export declare function validateDocs(d: ComponentDocsJson): DocsIssue[];
27
+ /** Drops empty list items and trims text, the way the editor saves. */
28
+ export declare function cleanDocs(d: ComponentDocsJson): ComponentDocsJson;
29
+ /** docs.json in the editor's canonical form: cleaned, in the Button.docs.json key order. Used to compare. */
30
+ export declare const docsText: (d: ComponentDocsJson) => string;
31
+ /** docs.json text the editor writes: cleaned, as a minimal diff of the file on main (or the branch) when there is one. */
32
+ export declare const docsFileText: (d: ComponentDocsJson, original: string | null) => string;
33
+ export interface DocsProposal {
34
+ branch: string;
35
+ title: string;
36
+ body: string;
37
+ files: {
38
+ path: string;
39
+ content: string;
40
+ }[];
41
+ risk: RiskResult;
42
+ bump: 'patch';
43
+ labels: string[];
44
+ }
45
+ /** One pull request for the docs edits of a draft: branch, title, body, files with a patch changeset, the tier. */
46
+ export declare function docsProposal(edits: DocsFileEdit[], opts: {
47
+ title?: string | undefined;
48
+ notes?: string | undefined;
49
+ date: Date;
50
+ packageName?: string | undefined;
51
+ changesetDir?: string | undefined;
52
+ origin?: DocsOrigin | undefined;
53
+ }): DocsProposal;
54
+ /**
55
+ * Opens one pull request with the docs edits from the default branch, the Jira key first in the title and the branch
56
+ * name (Jira's GitHub integration links them). Returns its number, link and branch. Labels are best effort.
57
+ */
58
+ export declare function proposeDocsOnGitHub(gh: GitHubClient, edits: DocsFileEdit[], opts: {
59
+ base: string;
60
+ title?: string | undefined;
61
+ notes?: string | undefined;
62
+ jira?: string | null | undefined;
63
+ date?: Date | undefined;
64
+ origin?: DocsOrigin | undefined;
65
+ extraLabels?: string[] | undefined;
66
+ commitNote?: string | undefined;
67
+ }): Promise<{
68
+ number: number;
69
+ url: string;
70
+ branch: string;
71
+ title: string;
72
+ }>;
73
+ /**
74
+ * Rewrites one file on an open branch in one commit (`next` gets the current text). Returns the commit sha, or ''
75
+ * when nothing changed. The Library editor saves an edited docs.json and accepts a Claude draft this way.
76
+ */
77
+ export declare function commitFileOnGitHub(gh: GitHubClient, input: {
78
+ path: string;
79
+ branch: string;
80
+ message: string;
81
+ next: (text: string) => string;
82
+ }): Promise<string>;
@@ -39,6 +39,8 @@ export interface Exploration {
39
39
  source?: ExplorationSource | undefined;
40
40
  /** The AI agent that started it ("claude"), when one did: the GitHub account alone cannot tell. */
41
41
  agent?: string | undefined;
42
+ /** A Jira ticket key ("DS-42"), first in the pull request title so Jira's GitHub integration links it. */
43
+ jira?: string | undefined;
42
44
  }
43
45
  /** "Denser data tables!" -> "denser-data-tables". Up to 40 characters, cut at a word boundary. Empty when nothing is left. */
44
46
  export declare function explorationSlug(name: string): string;
@@ -0,0 +1,78 @@
1
+ import { type ComponentSpec, type ComponentSpecsFile, type SpecExample, type SpecVariant } from './component-spec';
2
+ export declare const FIGMA_REFERENCES_BRANCH = "bridge-visual";
3
+ export declare const FIGMA_REFERENCES_DIR = "figma-references";
4
+ export declare const FIGMA_REFERENCES_MANIFEST = "figma-references/manifest.json";
5
+ /** Story id of packages/react/src/stories/FigmaReference.stories.tsx. */
6
+ export declare const FIGMA_REFERENCE_STORY = "internal-figma-reference--variant";
7
+ /** Width token of a block-level root in Figma (the plugin draws fill roots at this width: FILL_WIDTH_TOKEN). */
8
+ export declare const FIGMA_FILL_WIDTH_TOKEN = "size.80";
9
+ export type ReferencePseudo = 'hover' | 'active' | 'focus-visible';
10
+ /** What the reference story renders for one variant. JSON, passed to the story as a string arg. */
11
+ export interface ReferenceRender {
12
+ /** Export name of the component ("Button", "Card"). */
13
+ component: string;
14
+ /** Props from the variant axes and the default property values ("variant": "primary", "disabled": true). */
15
+ props: Record<string, unknown>;
16
+ /** Text children (the Label property) of a simple component. */
17
+ text?: string | undefined;
18
+ /** Compound parts, in order: export name and text ("CardHeader", "Header"). */
19
+ parts?: {
20
+ component: string;
21
+ text: string;
22
+ }[] | undefined;
23
+ /** Icon props -> Lucide icon name ("icon": "Pencil"). */
24
+ icons?: Record<string, string> | undefined;
25
+ /** Hover, pressed or focus, forced with the pseudo-states addon classes. */
26
+ pseudo?: ReferencePseudo | undefined;
27
+ /** Width token path of the frame the component sits in, for roots that fill their container. */
28
+ width?: string | undefined;
29
+ /**
30
+ * Children from the code example the set was drawn with (a table's rows, a form's fields): texts and design system
31
+ * elements with their props, icons by name. Replaces `text` and `parts`.
32
+ */
33
+ children?: (string | SpecExample)[] | undefined;
34
+ }
35
+ export type ReferencePlan = {
36
+ ok: true;
37
+ render: ReferenceRender;
38
+ } | {
39
+ ok: false;
40
+ reason: string;
41
+ };
42
+ /** How to render one generated variant from code, or why it cannot be compared. */
43
+ export declare function planReference(spec: ComponentSpec, variant: SpecVariant, file?: Pick<ComponentSpecsFile, 'slots'>): ReferencePlan;
44
+ /** File name of a variant's image, stable and file safe: "button/size-md__state-default__variant-primary.png". */
45
+ export declare function referenceImagePath(component: string, key: string): string;
46
+ export type FigmaReferenceEntry = {
47
+ file: string;
48
+ width: number;
49
+ height: number;
50
+ } | {
51
+ skipped: string;
52
+ };
53
+ export interface FigmaReferenceManifest {
54
+ version: 1;
55
+ tool: '@bridge-ds/visual';
56
+ createdAt: string;
57
+ /** Commit of the code the images were made from. */
58
+ sha: string;
59
+ /** Device pixels per CSS pixel, the scale the plugin exports Figma variants at. */
60
+ scale: number;
61
+ mode: {
62
+ brand: string;
63
+ theme: string;
64
+ density: string;
65
+ };
66
+ storyId: string;
67
+ components: Record<string, {
68
+ specHash: string;
69
+ variants: Record<string, FigmaReferenceEntry>;
70
+ }>;
71
+ }
72
+ /** Every variant of every spec with its render plan (the capture's to-do list). */
73
+ export declare function planReferences(file: ComponentSpecsFile): {
74
+ component: string;
75
+ specHash: string;
76
+ key: string;
77
+ plan: ReferencePlan;
78
+ }[];
@@ -0,0 +1,138 @@
1
+ /** An RGBA image, 4 bytes per pixel, row by row (ImageData, pngjs and canvas all use this layout). */
2
+ export interface RgbaImage {
3
+ width: number;
4
+ height: number;
5
+ data: Uint8Array | Uint8ClampedArray;
6
+ }
7
+ export interface Size {
8
+ width: number;
9
+ height: number;
10
+ }
11
+ export interface CompareOptions {
12
+ /** Export scale of both images (device pixels per CSS pixel). Sizes are reported in CSS pixels. Default 2. */
13
+ scale?: number;
14
+ /**
15
+ * Color distance under which two pixels are the same, 0 to 1 (YIQ, as pixelmatch). Default 0.1: absorbs gamma
16
+ * and antialiasing ramps, catches a token swap (a gray one step away is about 0.12).
17
+ */
18
+ threshold?: number;
19
+ /**
20
+ * Neighborhood, in device pixels, searched for a matching pixel before one counts as different. Figma and Chrome
21
+ * rasterize text and curves differently (glyph hinting, subpixel positions, corner antialiasing): a pixel that has
22
+ * its twin within this distance is the same shape drawn half a pixel away. Default: one CSS pixel (= scale).
23
+ */
24
+ radius?: number;
25
+ /** Largest share of different pixels, in percent, that still matches. Default 3. */
26
+ maxPercent?: number;
27
+ /** Largest size difference per side, in CSS pixels, that still matches. Default 1. */
28
+ sizeTolerance?: number;
29
+ /** Also return the diff overlay. Default true. */
30
+ diffImage?: boolean;
31
+ }
32
+ export interface ImageCompareResult {
33
+ matches: boolean;
34
+ /** Different pixels in percent of the larger canvas, two decimals. */
35
+ percent: number;
36
+ differentPixels: number;
37
+ totalPixels: number;
38
+ /** CSS pixel sizes (image size divided by the scale, rounded). */
39
+ size: {
40
+ figma: Size;
41
+ code: Size;
42
+ };
43
+ sizeMatches: boolean;
44
+ /** The code image faded to gray with different pixels in red, on the larger canvas. */
45
+ diff?: RgbaImage | undefined;
46
+ }
47
+ export declare const FIDELITY_DEFAULTS: {
48
+ readonly scale: 2;
49
+ readonly threshold: 0.1;
50
+ readonly maxPercent: 3;
51
+ readonly sizeTolerance: 1;
52
+ };
53
+ /** YIQ squared distance between two opaque colors (pixelmatch's perceptual metric). */
54
+ export declare function colorDelta(a: [number, number, number], b: [number, number, number]): number;
55
+ /**
56
+ * Compares a Figma export with the code reference of the same variant. A pixel differs when its color is not found
57
+ * near the same place in the other image, in either direction: a shape drawn half a pixel away, or text rasterized
58
+ * a little differently, matches; a missing border, another color, other padding or a different size does not.
59
+ * Pixels outside the smaller image count as transparent: there, any visible color differs (a missing footer is a
60
+ * difference even when its gray is light).
61
+ */
62
+ export declare function compareImages(figma: RgbaImage, code: RgbaImage, options?: CompareOptions): ImageCompareResult;
63
+ export type FidelityVariantStatus = 'matches' | 'differs' | 'skipped';
64
+ export interface FidelityVariantResult {
65
+ /** Variant key, as the plugin stores it ("Size=md, State=Default, Variant=primary"). */
66
+ key: string;
67
+ status: FidelityVariantStatus;
68
+ percent?: number | undefined;
69
+ size?: {
70
+ figma: Size;
71
+ code: Size;
72
+ } | undefined;
73
+ /** Skipped variants: why there is nothing to compare, in designer words. */
74
+ reason?: string | undefined;
75
+ }
76
+ export type FidelityComponentStatus = 'matches' | 'differs' | 'not-checked';
77
+ export interface FidelityComponentResult {
78
+ component: string;
79
+ status: FidelityComponentStatus;
80
+ checked: number;
81
+ differing: number;
82
+ skipped: number;
83
+ /** The variant that differs most (or matches least well). */
84
+ worst?: {
85
+ key: string;
86
+ percent: number;
87
+ size: {
88
+ figma: Size;
89
+ code: Size;
90
+ };
91
+ } | undefined;
92
+ /** Not checked at all: why. */
93
+ reason?: string | undefined;
94
+ variants: FidelityVariantResult[];
95
+ }
96
+ export interface FidelityReport {
97
+ version: 1;
98
+ checkedAt: string;
99
+ /** Figma file the sets were exported from. */
100
+ file?: string | undefined;
101
+ /** Commit the code images were made from. */
102
+ reference: {
103
+ sha: string;
104
+ createdAt: string;
105
+ };
106
+ options: {
107
+ scale: number;
108
+ threshold: number;
109
+ radius: number;
110
+ maxPercent: number;
111
+ sizeTolerance: number;
112
+ };
113
+ components: FidelityComponentResult[];
114
+ summary: {
115
+ components: number;
116
+ matches: number;
117
+ differs: number;
118
+ notChecked: number;
119
+ variantsChecked: number;
120
+ variantsSkipped: number;
121
+ };
122
+ }
123
+ /** Rolls variant results up into one component result. */
124
+ export declare function summarizeComponent(component: string, variants: FidelityVariantResult[], reason?: string): FidelityComponentResult;
125
+ export declare function buildFidelityReport(input: {
126
+ components: FidelityComponentResult[];
127
+ reference: {
128
+ sha: string;
129
+ createdAt: string;
130
+ };
131
+ options?: CompareOptions;
132
+ file?: string;
133
+ now?: Date;
134
+ }): FidelityReport;
135
+ /** One variant's verdict: "Matches the code" or "Differs from the code: 12% of pixels, size 120x40 vs 118x40". */
136
+ export declare function fidelityVerdict(v: Pick<FidelityVariantResult, 'status' | 'percent' | 'size' | 'reason'>): string;
137
+ /** One component's line, from its worst variant. */
138
+ export declare function fidelityComponentLine(c: FidelityComponentResult): string;