@bridge-ds/core 0.5.0 → 0.6.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,19 @@ 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[];
120
173
  }
121
174
  /** A declaration, prop or part that has no Figma equivalent in this version, with the reason. */
122
175
  export interface SpecUnmapped {
@@ -127,6 +180,26 @@ export interface SpecUnmapped {
127
180
  }
128
181
  export interface ComponentSpec {
129
182
  name: string;
183
+ /** Component folder the set comes from, when it is not the set's name (Radio comes from RadioGroup). */
184
+ component?: string | undefined;
185
+ /** Props pinned for this set (`{ variant: 'circular' }` for Progress/Circular, a set per layer tree). */
186
+ fixed?: Record<string, string> | undefined;
187
+ /** A popup drawn open (`popup`), or the component without its popup (`without-popup`). */
188
+ view?: 'popup' | 'without-popup' | undefined;
189
+ /**
190
+ * Drafted with Claude, not generated: the generator cannot read this component, so its spec was written by hand from
191
+ * the code (packages/react/component-spec-overrides/<Name>.json, `"source": "drafted"`) and validated like a generated
192
+ * one. Shown in the plugin as "Drafted, check it". `stale`: the code it was drafted from changed since.
193
+ */
194
+ drafted?: {
195
+ reason: string;
196
+ stale?: boolean | undefined;
197
+ } | undefined;
198
+ /**
199
+ * The code example the set's content comes from (the docs preview or a component sandbox example): the element, its
200
+ * props and children, so the fidelity check renders the same thing. Not in the hash: it does not change the drawing.
201
+ */
202
+ example?: SpecExample | undefined;
130
203
  /** FNV-1a of everything else in the spec: unchanged hash, nothing to rebuild. */
131
204
  hash: string;
132
205
  /** Files it was read from, repo-relative. */
@@ -137,6 +210,12 @@ export interface ComponentSpec {
137
210
  variants: SpecVariant[];
138
211
  unmapped: SpecUnmapped[];
139
212
  }
213
+ /** An element of a code example: component export name, literal props, children (texts or elements). */
214
+ export interface SpecExample {
215
+ c: string;
216
+ p?: Record<string, unknown> | undefined;
217
+ children?: (string | SpecExample)[] | undefined;
218
+ }
140
219
  export interface ComponentSpecsFile {
141
220
  version: number;
142
221
  /** Human note at the top of the file. */
@@ -147,12 +226,50 @@ export interface ComponentSpecsFile {
147
226
  iconStyle?: SpecIconStyle | undefined;
148
227
  /** Content components slots swap to. */
149
228
  slots?: SpecSlot[] | undefined;
229
+ /**
230
+ * Every component of the design system and whether it is in Figma: generated (its sets are in `components`),
231
+ * unsupported (with the generator's reason) or layout (no visual of its own: a layout helper, use auto layout).
232
+ * Undefined in files written before coverage was recorded.
233
+ */
234
+ coverage?: SpecCoverage[] | undefined;
150
235
  }
236
+ export type CoverageStatus = 'generated' | 'drafted' | 'unsupported' | 'layout';
237
+ export interface SpecCoverage {
238
+ /** Component folder name ("RadioGroup"). */
239
+ name: string;
240
+ status: CoverageStatus;
241
+ /** Plain reason, for unsupported, drafted and layout components ("opens a popup", "uses CSS grid"). */
242
+ reason?: string | undefined;
243
+ /** Sets generated from it, when they are not just its own name ("RadioGroup", "Radio"). */
244
+ sets?: string[] | undefined;
245
+ }
246
+ /**
247
+ * Coverage counts from a specs file: visual components (generated, drafted with Claude, unsupported), in Figma
248
+ * (generated + drafted), and layout helpers.
249
+ */
250
+ export declare function coverageSummary(file: Pick<ComponentSpecsFile, 'coverage' | 'components'>): {
251
+ generated: number;
252
+ drafted: number;
253
+ inFigma: number;
254
+ visual: number;
255
+ layout: number;
256
+ total: number;
257
+ missing: SpecCoverage[];
258
+ };
259
+ /**
260
+ * Checks a component spec against the format (the schema every set follows, generated or drafted): axes and their
261
+ * variants (one per combination, no extra), properties, parts (one root, known parents in order, text and visibility
262
+ * properties that exist), each value's field and type, and, given `tokenTypes` (token path -> $type), that every token
263
+ * path exists and has a type the field takes. Returns the problems in plain language, none when the spec is valid.
264
+ */
265
+ export declare function validateComponentSpec(spec: Omit<ComponentSpec, 'hash'> & {
266
+ hash?: string;
267
+ }, tokenTypes?: Map<string, string>): string[];
151
268
  /** Figma component name of an icon: "Icon/Plus". */
152
269
  export declare const iconComponentName: (name: string) => string;
153
270
  /** FNV-1a, 32 bits, 8 hex digits. */
154
271
  export declare function specHash(text: string): string;
155
- /** Hash of a spec without its own hash field. */
272
+ /** Hash of a spec without its own hash field (and without its code example, which does not change the drawing). */
156
273
  export declare function hashSpec(spec: Omit<ComponentSpec, 'hash'> | ComponentSpec): string;
157
274
  /** Figma variant name for a variant's props: "Variant=primary, Size=md, State=Default" (axis order). */
158
275
  export declare const variantName: (props: Record<string, string>) => 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;
package/dist/index.d.ts CHANGED
@@ -43,3 +43,5 @@ export * from './explorations';
43
43
  export * from './explorations-github';
44
44
  export * from './requests-github';
45
45
  export * from './component-spec';
46
+ export * from './fidelity';
47
+ export * from './fidelity-references';