@bridge-ds/core 0.6.0 → 0.8.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.
- package/bridge.config.schema.json +69 -0
- package/dist/branch-actions.d.ts +70 -0
- package/dist/combine-branches.d.ts +41 -0
- package/dist/{component-spec-GFVEE6Fc.js → component-spec-SKuywZqr.js} +366 -184
- package/dist/component-spec-SKuywZqr.js.map +1 -0
- package/dist/component-spec.d.ts +114 -0
- package/dist/config.d.ts +4 -0
- package/dist/docs-proposal.d.ts +154 -0
- package/dist/explorations-github.d.ts +2 -0
- package/dist/github-client.d.ts +89 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1280 -298
- package/dist/index.js.map +1 -1
- package/dist/jira.d.ts +15 -0
- package/dist/library-update.d.ts +145 -0
- package/dist/node/component-specs.d.ts +2 -0
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.js +301 -5
- package/dist/node/index.js.map +1 -1
- package/dist/node/spec-motion.d.ts +66 -0
- package/dist/products.d.ts +65 -0
- package/dist/review-gate.d.ts +251 -0
- package/dist/share-branch.d.ts +85 -0
- package/dist/try-in-product.d.ts +16 -0
- package/package.json +1 -1
- package/dist/component-spec-GFVEE6Fc.js.map +0 -1
package/dist/jira.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Jira key given when a branch is created, normalised ("ds-42 " is "DS-42"), or null when empty. Throws a
|
|
3
|
+
* sentence that says how to fix it when the text is not a key.
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseJiraKey(input: string | null | undefined): string | null;
|
|
6
|
+
/** Whether a text is a Jira key, without throwing (for live field validation). */
|
|
7
|
+
export declare const isJiraKey: (input: string) => boolean;
|
|
8
|
+
/** The Jira key a title starts with ("DS-42 tokens: darker primary" gives "DS-42"), else null. */
|
|
9
|
+
export declare function jiraKeyOf(title: string): string | null;
|
|
10
|
+
/** The title without its leading Jira key. */
|
|
11
|
+
export declare const withoutJiraKey: (title: string) => string;
|
|
12
|
+
/** The pull request title with the key first: "DS-42 tokens: darker primary". Idempotent; no key, no change. */
|
|
13
|
+
export declare function titleWithJiraKey(title: string, key: string | null): string;
|
|
14
|
+
/** The branch name with the key first in its last part: "tokens/DS-42-darker-primary-20261019-1200". */
|
|
15
|
+
export declare function branchWithJiraKey(branch: string, key: string | null): string;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { GitHubClient, TreeEntry } from './github-client';
|
|
2
|
+
import { type LayoutOptions, type TokenSet } from './token-set';
|
|
3
|
+
import { type ContrastPair } from './wcag';
|
|
4
|
+
import { type MergeChoices, type TokenMergeConflict, type TokenMergeInput, type TokenMergePlan } from './token-merge';
|
|
5
|
+
/** A file of a compare (GET /repos/{o}/{r}/compare/{base}...{head}). */
|
|
6
|
+
export interface CompareFile {
|
|
7
|
+
filename: string;
|
|
8
|
+
status: string;
|
|
9
|
+
previous_filename?: string | undefined;
|
|
10
|
+
sha?: string | null | undefined;
|
|
11
|
+
}
|
|
12
|
+
/** Files both the branch and main changed since they parted, split into token files and the rest (code). */
|
|
13
|
+
export declare function overlap(changeFiles: CompareFile[], libraryFiles: CompareFile[], isTokenFile: (path: string) => boolean): {
|
|
14
|
+
tokenFiles: string[];
|
|
15
|
+
codeFiles: string[];
|
|
16
|
+
};
|
|
17
|
+
/** GitHub lists at most 300 files per compare: past that, Bridge cannot know what both sides touched. */
|
|
18
|
+
export declare const COMPARE_FILE_LIMIT = 300;
|
|
19
|
+
export type ConflictRoute = {
|
|
20
|
+
kind: 'tokens';
|
|
21
|
+
files: string[];
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'code';
|
|
24
|
+
alsoTokens: boolean;
|
|
25
|
+
files: string[];
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Who resolves the conflict. Only token files on both sides: the designer, in Bridge. Any code file on both sides, a
|
|
29
|
+
* file list GitHub cut short, or no shared file at all (GitHub saw a conflict Bridge cannot explain): a developer.
|
|
30
|
+
*/
|
|
31
|
+
export declare function conflictRoute(o: {
|
|
32
|
+
tokenFiles: string[];
|
|
33
|
+
codeFiles: string[];
|
|
34
|
+
}, opts?: {
|
|
35
|
+
truncated?: boolean;
|
|
36
|
+
}): ConflictRoute;
|
|
37
|
+
/** A rule from GET /repos/{o}/{r}/rules/branches/{branch}. */
|
|
38
|
+
export interface BranchRuleLike {
|
|
39
|
+
type: string;
|
|
40
|
+
parameters?: {
|
|
41
|
+
dismiss_stale_reviews_on_push?: boolean;
|
|
42
|
+
} | null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Whether GitHub dismisses approvals when new commits arrive, from the rules that apply to the library branch. True
|
|
46
|
+
* or false when a pull request rule says so; null when no rule says (classic branch protection is not readable
|
|
47
|
+
* without admin rights).
|
|
48
|
+
*/
|
|
49
|
+
export declare function dismissStaleFrom(rules: BranchRuleLike[] | null | undefined): boolean | null;
|
|
50
|
+
/** The commit message of the resolution, so History and the branch say what the person chose. */
|
|
51
|
+
export declare function resolutionMessage(input: {
|
|
52
|
+
library: string;
|
|
53
|
+
conflicts: TokenMergeConflict[];
|
|
54
|
+
choices: MergeChoices;
|
|
55
|
+
who?: string | null | undefined;
|
|
56
|
+
}): string;
|
|
57
|
+
export type LibraryState = 'current' | 'behind' | 'conflict' | 'unknown';
|
|
58
|
+
export interface Freshness {
|
|
59
|
+
state: LibraryState;
|
|
60
|
+
/** Library updates (commits on the library branch) since the change started; null when not read. */
|
|
61
|
+
behindBy: number | null;
|
|
62
|
+
/** The commit the change and the library share. */
|
|
63
|
+
mergeBase: string | null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* How the change stands against the latest library. A conflict (mergeable false) wins: GitHub cannot combine them.
|
|
67
|
+
* Otherwise it is behind when the library has commits the change has not. A shipped or closed change has nothing to
|
|
68
|
+
* bring in. A compare that could not be read says unknown, never "up to date".
|
|
69
|
+
*/
|
|
70
|
+
export declare function freshness<C extends {
|
|
71
|
+
behind_by?: number | undefined;
|
|
72
|
+
merge_base_commit?: {
|
|
73
|
+
sha: string;
|
|
74
|
+
} | null | undefined;
|
|
75
|
+
}>(input: {
|
|
76
|
+
compare: C | null | undefined;
|
|
77
|
+
mergeable: boolean | null | undefined;
|
|
78
|
+
open: boolean;
|
|
79
|
+
}): Freshness;
|
|
80
|
+
/** The compare of main and a branch head, in the shape the conflict reader needs (client.compareDetailed). */
|
|
81
|
+
export interface LibraryCompare {
|
|
82
|
+
mergeBase: string | null;
|
|
83
|
+
baseSha: string | null;
|
|
84
|
+
baseTree: string | null;
|
|
85
|
+
files: CompareFile[];
|
|
86
|
+
}
|
|
87
|
+
export interface LibraryConflicts {
|
|
88
|
+
route: ConflictRoute;
|
|
89
|
+
/** Present for a pure token-file conflict. */
|
|
90
|
+
tokens?: {
|
|
91
|
+
root: string;
|
|
92
|
+
plan: TokenMergePlan;
|
|
93
|
+
input: TokenMergeInput;
|
|
94
|
+
sets: {
|
|
95
|
+
base: TokenSet;
|
|
96
|
+
mine: TokenSet;
|
|
97
|
+
theirs: TokenSet;
|
|
98
|
+
};
|
|
99
|
+
/** Every token file of each side, relative to the tokens root. */
|
|
100
|
+
mineFiles: Record<string, string>;
|
|
101
|
+
theirsFiles: Record<string, string>;
|
|
102
|
+
pairs: ContrastPair[];
|
|
103
|
+
options: LayoutOptions;
|
|
104
|
+
};
|
|
105
|
+
/** What the commit needs: main's tip and its tree, the branch head tree and its own files. */
|
|
106
|
+
write?: {
|
|
107
|
+
mainSha: string;
|
|
108
|
+
mainTree: string;
|
|
109
|
+
headSha: string;
|
|
110
|
+
headTree: Map<string, TreeEntry>;
|
|
111
|
+
changeFiles: CompareFile[];
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* What conflicts and who resolves it. Reads the files main changed since the branch started (one compare), then, for
|
|
116
|
+
* a pure token-file conflict, the token files at the shared start, in the branch and in main. `tokensRoot` is the
|
|
117
|
+
* best guess before the config is read (Studio: its site config); `contrastFallback` the contrast checks to use when
|
|
118
|
+
* main has none.
|
|
119
|
+
*/
|
|
120
|
+
export declare function readLibraryConflicts(gh: GitHubClient, compare: LibraryCompare, headSha: string, opts?: {
|
|
121
|
+
tokensRoot?: string | undefined;
|
|
122
|
+
contrastFallback?: string | undefined;
|
|
123
|
+
}): Promise<LibraryConflicts>;
|
|
124
|
+
/**
|
|
125
|
+
* Every token file once resolved, relative to the tokens root: main's files, the branch's own token files where only
|
|
126
|
+
* the branch touched them, and the merged files where both did.
|
|
127
|
+
*/
|
|
128
|
+
export declare function resolvedTokenFiles(data: LibraryConflicts, choices: MergeChoices): {
|
|
129
|
+
all: Record<string, string>;
|
|
130
|
+
merged: Record<string, string | null>;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Commits the resolution to the branch: a merge commit of main and the branch, whose tree is main plus the branch's
|
|
134
|
+
* own files plus the merged token files. Fast-forward only: when the branch moved meanwhile, GitHub refuses and
|
|
135
|
+
* nothing is lost.
|
|
136
|
+
*/
|
|
137
|
+
export declare function commitLibraryResolution(gh: GitHubClient, pull: {
|
|
138
|
+
head: {
|
|
139
|
+
ref: string;
|
|
140
|
+
sha: string;
|
|
141
|
+
};
|
|
142
|
+
base: {
|
|
143
|
+
ref: string;
|
|
144
|
+
};
|
|
145
|
+
}, data: LibraryConflicts, choices: MergeChoices, who: string | null | undefined): Promise<string>;
|
|
@@ -3,6 +3,8 @@ import { buildTokenSet } from '../token-set';
|
|
|
3
3
|
export interface TokenInfo {
|
|
4
4
|
path: string;
|
|
5
5
|
type: string;
|
|
6
|
+
/** Durations and easings: the value in the default modes (`{ value: 150, unit: 'ms' }`, `[0.2, 0, 0, 1]`), for motion. */
|
|
7
|
+
value?: unknown;
|
|
6
8
|
}
|
|
7
9
|
export interface SpecGeneratorOptions {
|
|
8
10
|
/** Repo-relative components directory ("packages/react/src/components"). */
|
package/dist/node/index.d.ts
CHANGED
package/dist/node/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join, resolve, relative, dirname, sep, posix } from "node:path";
|
|
5
|
-
import { a as assertSafeBranch, c as collectHistory, p as packageDirCandidates, b as parseChangelog, m as mergeReleases, r as releasesFromTags, d as buildReport, e as prNumberFromSubject, l as loadConfig, s as specHash, v as validateComponentSpec, h as hashSpec, i as iconComponentName, f as variantKeyOf, C as CONFIG_FILE, g as layoutOptions, j as buildTokenSet, t as tokenCssVar,
|
|
5
|
+
import { a as assertSafeBranch, c as collectHistory, p as packageDirCandidates, b as parseChangelog, m as mergeReleases, r as releasesFromTags, d as buildReport, e as prNumberFromSubject, l as loadConfig, s as specHash, v as validateComponentSpec, h as hashSpec, i as iconComponentName, f as variantKeyOf, C as CONFIG_FILE, g as layoutOptions, j as buildTokenSet, k as defaultContext, t as tokenCssVar, n as resolve$1, o as COMPONENT_SPECS_VERSION, q as COLOR_FIELDS } from "../component-spec-SKuywZqr.js";
|
|
6
6
|
import { s as selectTokenFiles, g as globToRegExp, L as LayeredTokenSet } from "../layered-BnbmToGs.js";
|
|
7
7
|
import { statSync, readFileSync, existsSync, writeFileSync, readdirSync } from "node:fs";
|
|
8
8
|
import { v as isAuditable, Q as planCssFixes, O as parseAllowlist, c as DesignSystemIndex, k as buildAuditReport, n as compareWithBaseline, a as AUDIT_IGNORE_DIRS } from "../report-iPSnsjYi.js";
|
|
@@ -784,6 +784,236 @@ function computeValues(own, parent) {
|
|
|
784
784
|
}
|
|
785
785
|
return out;
|
|
786
786
|
}
|
|
787
|
+
const KEYWORDS = {
|
|
788
|
+
ease: [0.25, 0.1, 0.25, 1],
|
|
789
|
+
"ease-in": [0.42, 0, 1, 1],
|
|
790
|
+
"ease-out": [0, 0, 0.58, 1],
|
|
791
|
+
"ease-in-out": [0.42, 0, 0.58, 1]
|
|
792
|
+
};
|
|
793
|
+
const SILENT = /^(cursor|pointer-events|user-select|-webkit-.*|transition.*|animation.*|will-change|outline.*|content|touch-action)$/;
|
|
794
|
+
function substituteVars(raw, computed, tokens, depth = 0) {
|
|
795
|
+
if (depth > 12) return raw;
|
|
796
|
+
return raw.replace(/var\(\s*(--[\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/g, (m, name, fallback) => {
|
|
797
|
+
if (tokens.has(name)) return m;
|
|
798
|
+
const v = computed.get(name);
|
|
799
|
+
if (v !== void 0 && v !== "initial") return substituteVars(v, computed, tokens, depth + 1);
|
|
800
|
+
return fallback !== void 0 ? substituteVars(fallback.trim(), computed, tokens, depth + 1) : m;
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
const isTime = (t, tokens) => /^-?[\d.]+m?s$/i.test(t) || tokens.get(varName(t) ?? "")?.type === "duration";
|
|
804
|
+
const varName = (t) => /^var\(\s*(--[\w-]+)/.exec(t)?.[1];
|
|
805
|
+
function parseTransitions(computed, tokens) {
|
|
806
|
+
const sub = (k) => {
|
|
807
|
+
const v = computed.get(k);
|
|
808
|
+
return v === void 0 ? void 0 : substituteVars(v, computed, tokens);
|
|
809
|
+
};
|
|
810
|
+
const out = [];
|
|
811
|
+
const short = sub("transition");
|
|
812
|
+
if (short && short !== "none") {
|
|
813
|
+
for (const item of splitTop(short, ",")) {
|
|
814
|
+
const words = splitTop(item.trim(), /\s/).filter(Boolean);
|
|
815
|
+
const e = { property: "all", duration: "0s", easing: "ease", delay: "0s" };
|
|
816
|
+
let times = 0;
|
|
817
|
+
for (const w of words) {
|
|
818
|
+
if (isTime(w, tokens)) {
|
|
819
|
+
if (times++ === 0) e.duration = w;
|
|
820
|
+
else e.delay = w;
|
|
821
|
+
} else if (KEYWORDS[w] || w === "linear" || /^(cubic-bezier|steps|linear)\(/.test(w) || /^step-(start|end)$/.test(w) || tokens.get(varName(w) ?? "")?.type === "cubicBezier") e.easing = w;
|
|
822
|
+
else e.property = w;
|
|
823
|
+
}
|
|
824
|
+
out.push(e);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const props = sub("transition-property");
|
|
828
|
+
if (props === void 0) return out;
|
|
829
|
+
const list = (k, def) => {
|
|
830
|
+
const v = sub(k);
|
|
831
|
+
return v === void 0 ? [def] : splitTop(v, ",").map((x) => x.trim());
|
|
832
|
+
};
|
|
833
|
+
const durations = list("transition-duration", "0s");
|
|
834
|
+
const easings = list("transition-timing-function", "ease");
|
|
835
|
+
const delays = list("transition-delay", "0s");
|
|
836
|
+
return splitTop(props, ",").map((p) => p.trim()).filter((p) => p && p !== "none").map((property, i) => ({ property, duration: durations[i % durations.length], easing: easings[i % easings.length], delay: delays[i % delays.length] }));
|
|
837
|
+
}
|
|
838
|
+
function resolveDuration(raw, tokens) {
|
|
839
|
+
const v = raw.trim();
|
|
840
|
+
const lit = /^(-?[\d.]+)(m?s)$/i.exec(v);
|
|
841
|
+
if (lit) return { ms: Number(lit[1]) * (lit[2].toLowerCase() === "s" ? 1e3 : 1) };
|
|
842
|
+
const t = tokens.get(varName(v) ?? "");
|
|
843
|
+
if (!t || t.type !== "duration") return void 0;
|
|
844
|
+
const value = t.value;
|
|
845
|
+
if (typeof value === "string") {
|
|
846
|
+
const r = resolveDuration(value, /* @__PURE__ */ new Map());
|
|
847
|
+
return r ? { ms: r.ms, token: t.path } : void 0;
|
|
848
|
+
}
|
|
849
|
+
if (value && typeof value.value === "number") return { ms: value.value * (value.unit === "s" ? 1e3 : 1), token: t.path };
|
|
850
|
+
return void 0;
|
|
851
|
+
}
|
|
852
|
+
function resolveEasing(raw, tokens) {
|
|
853
|
+
const v = raw.trim().toLowerCase();
|
|
854
|
+
const curve = (b) => b[0] === 0 && b[1] === 0 && b[2] === 1 && b[3] === 1 ? { type: "LINEAR" } : { type: "CUSTOM_CUBIC_BEZIER", bezier: [b[0], b[1], b[2], b[3]] };
|
|
855
|
+
if (v === "linear") return { easing: { type: "LINEAR" } };
|
|
856
|
+
if (KEYWORDS[v]) return { easing: curve(KEYWORDS[v]) };
|
|
857
|
+
const cb = /^cubic-bezier\(([^)]*)\)$/.exec(v);
|
|
858
|
+
if (cb) {
|
|
859
|
+
const n = cb[1].split(",").map((x) => Number(x.trim()));
|
|
860
|
+
return n.length === 4 && n.every((x) => Number.isFinite(x)) ? { easing: curve(n) } : void 0;
|
|
861
|
+
}
|
|
862
|
+
if (/^(steps\(|step-(start|end)$)/.test(v)) return { easing: { type: "LINEAR" }, approximated: `${raw.trim()} moves in steps, Figma has no stepped easing: linear` };
|
|
863
|
+
if (/^linear\(/.test(v)) return { easing: { type: "LINEAR" }, approximated: "a linear() curve has no Figma easing: linear" };
|
|
864
|
+
const t = tokens.get(varName(raw.trim()) ?? "");
|
|
865
|
+
if (!t || t.type !== "cubicBezier") return void 0;
|
|
866
|
+
const value = t.value;
|
|
867
|
+
if (Array.isArray(value) && value.length === 4 && value.every((x) => typeof x === "number")) return { easing: curve(value), token: t.path };
|
|
868
|
+
if (typeof value === "string") {
|
|
869
|
+
const r = resolveEasing(value, /* @__PURE__ */ new Map());
|
|
870
|
+
return r ? { ...r, token: t.path } : void 0;
|
|
871
|
+
}
|
|
872
|
+
return void 0;
|
|
873
|
+
}
|
|
874
|
+
const shorthandOf = (prop) => prop.replace(/-(top|bottom)-(left|right)(?=-|$)/, "").replace(/-(top|right|bottom|left)(?=-|$)/, "").replace(/-(block|inline)(-(start|end))?(?=-|$)/, "");
|
|
875
|
+
function entryFor(prop, entries) {
|
|
876
|
+
const general = shorthandOf(prop);
|
|
877
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
878
|
+
const p = entries[i].property;
|
|
879
|
+
if (p === "all" || p === prop || p === general || prop.startsWith(`${p}-`) || general.startsWith(`${p}-`)) return entries[i];
|
|
880
|
+
}
|
|
881
|
+
return void 0;
|
|
882
|
+
}
|
|
883
|
+
function changedProperties(from, to, tokens) {
|
|
884
|
+
const a = from ?? /* @__PURE__ */ new Map();
|
|
885
|
+
const b = to ?? /* @__PURE__ */ new Map();
|
|
886
|
+
const out = [];
|
|
887
|
+
for (const k of /* @__PURE__ */ new Set([...a.keys(), ...b.keys()])) {
|
|
888
|
+
if (k.startsWith("--") || SILENT.test(k)) continue;
|
|
889
|
+
const x = a.get(k);
|
|
890
|
+
const y = b.get(k);
|
|
891
|
+
if ((x === void 0 ? void 0 : substituteVars(x, a, tokens)) !== (y === void 0 ? void 0 : substituteVars(y, b, tokens))) out.push(k);
|
|
892
|
+
}
|
|
893
|
+
return out.sort();
|
|
894
|
+
}
|
|
895
|
+
const fmtMs = (ms) => `${Math.round(ms)} ms`;
|
|
896
|
+
function transitionBetween(from, to, tokens) {
|
|
897
|
+
const animated = [];
|
|
898
|
+
for (const id of /* @__PURE__ */ new Set([...from.keys(), ...to.keys()])) {
|
|
899
|
+
const changed = changedProperties(from.get(id), to.get(id), tokens);
|
|
900
|
+
if (!changed.length) continue;
|
|
901
|
+
const entries = parseTransitions(to.get(id) ?? /* @__PURE__ */ new Map(), tokens);
|
|
902
|
+
for (const prop of changed) {
|
|
903
|
+
const e = entryFor(prop, entries);
|
|
904
|
+
if (!e) continue;
|
|
905
|
+
const d = resolveDuration(e.duration, tokens);
|
|
906
|
+
const ease = resolveEasing(e.easing, tokens);
|
|
907
|
+
if (!d || d.ms <= 0 || !ease) continue;
|
|
908
|
+
const delay = resolveDuration(e.delay, tokens)?.ms ?? 0;
|
|
909
|
+
animated.push({ prop, ms: d.ms, ...d.token ? { durationToken: d.token } : {}, easing: ease.easing, ...ease.token ? { easingToken: ease.token } : {}, ...ease.approximated ? { approximated: ease.approximated } : {}, delay });
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (!animated.length) return null;
|
|
913
|
+
const main = animated.reduce((m, x) => x.ms > m.ms ? x : m, animated[0]);
|
|
914
|
+
const notes = [];
|
|
915
|
+
const speeds = new Set(animated.map((x) => `${x.ms}|${JSON.stringify(x.easing)}`));
|
|
916
|
+
if (speeds.size > 1) {
|
|
917
|
+
const others = [...new Set(animated.filter((x) => x.ms !== main.ms).map((x) => fmtMs(x.ms)))];
|
|
918
|
+
notes.push(others.length ? `parts move at ${[fmtMs(main.ms), ...others].join(" and ")} in code, Figma plays one speed: ${fmtMs(main.ms)}` : "parts use different easings in code, Figma plays one");
|
|
919
|
+
}
|
|
920
|
+
if (animated.some((x) => x.delay > 0)) notes.push("a transition delay in code is not played");
|
|
921
|
+
for (const x of animated) if (x.approximated && !notes.includes(x.approximated)) notes.push(x.approximated);
|
|
922
|
+
return {
|
|
923
|
+
duration: main.ms,
|
|
924
|
+
...main.durationToken ? { durationToken: main.durationToken } : {},
|
|
925
|
+
easing: main.easing,
|
|
926
|
+
...main.easingToken ? { easingToken: main.easingToken } : {},
|
|
927
|
+
properties: [...new Set(animated.map((x) => shorthandOf(x.prop)))].sort(),
|
|
928
|
+
...notes.length ? { approximated: notes.join("; ") } : {}
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
function targetClasses(rule) {
|
|
932
|
+
const last = rule.selector.parts[rule.selector.parts.length - 1];
|
|
933
|
+
return (last?.compound ?? []).filter((s) => s.t === "class").map((s) => `${rule.module}|${s.name}`);
|
|
934
|
+
}
|
|
935
|
+
const OPEN_ATTR = /\[data-(starting-style|ending-style|open|closed|popup-open|panel-open|expanded)\]/;
|
|
936
|
+
function motionNotes(rules, classes, tokens, view, elements = [...classes].map((c) => [c])) {
|
|
937
|
+
const out = [];
|
|
938
|
+
const add = (n) => {
|
|
939
|
+
if (!out.some((x) => x.what === n.what)) out.push(n);
|
|
940
|
+
};
|
|
941
|
+
const styled = (r) => targetClasses(r).some((c) => classes.has(c));
|
|
942
|
+
const opened = /* @__PURE__ */ new Set();
|
|
943
|
+
for (const r of rules) {
|
|
944
|
+
if (!styled(r)) continue;
|
|
945
|
+
const values = new Map(r.decls.map((d) => [d.prop, d.value]));
|
|
946
|
+
const anim = values.get("animation") ?? values.get("animation-name");
|
|
947
|
+
if (anim && !/^none\b/.test(anim.trim())) {
|
|
948
|
+
const words = splitTop(anim.trim(), /\s/).filter(Boolean);
|
|
949
|
+
const name = values.get("animation-name") ?? words.find((w) => !isTime(w, tokens) && !resolveEasing(w, tokens) && !/^(infinite|\d+|alternate|reverse|normal|forwards|backwards|both|running|paused)$/.test(w));
|
|
950
|
+
const dur = words.map((w) => resolveDuration(w, tokens)).find(Boolean) ?? (values.get("animation-duration") ? resolveDuration(values.get("animation-duration"), tokens) : void 0);
|
|
951
|
+
const loops = /\binfinite\b/.test(anim) || values.get("animation-iteration-count") === "infinite";
|
|
952
|
+
add({
|
|
953
|
+
what: `${loops ? "Looping animation" : "Keyframe animation"} ${name ?? ""}${dur ? `, ${fmtMs(dur.ms)}` : ""}`.replace(/\s+,/, ",").trim(),
|
|
954
|
+
reason: loops ? "a Figma component cannot loop an animation on its own" : "keyframes have no Figma equivalent between variants",
|
|
955
|
+
...dur?.token ? { durationToken: dur.token } : {}
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
if (/:focus(-visible|-within)?\b/.test(r.text)) add({ what: "Keyboard focus", reason: "the focus ring shows when a keyboard moves to the component; Figma prototypes have no focus state" });
|
|
959
|
+
if (OPEN_ATTR.test(r.text)) {
|
|
960
|
+
for (const c of targetClasses(r)) if (classes.has(c)) opened.add(c);
|
|
961
|
+
add({
|
|
962
|
+
what: view === "popup" ? "Opening and closing the popup" : "Opening and closing",
|
|
963
|
+
reason: view === "popup" ? "the popup is its own set; a Figma component cannot open another one from its trigger" : "the open state is not drawn as a variant"
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
const note = out.find((n) => n.what.startsWith("Opening and closing"));
|
|
968
|
+
if (note) {
|
|
969
|
+
const { enter, exit } = openingTransitions(rules, elements.filter((el) => el.some((c) => opened.has(c))), tokens);
|
|
970
|
+
if (enter) note.enter = enter;
|
|
971
|
+
if (exit) note.exit = exit;
|
|
972
|
+
}
|
|
973
|
+
return out;
|
|
974
|
+
}
|
|
975
|
+
const ENDING_ATTR = /\[data-(ending-style|closed)\]/;
|
|
976
|
+
const STATE_PSEUDO = /:(hover|active|focus|focus-visible|focus-within|disabled|checked)\b/;
|
|
977
|
+
function openingTransitions(rules, elements, tokens) {
|
|
978
|
+
const best = {};
|
|
979
|
+
for (const el of elements) {
|
|
980
|
+
const mine = rules.filter((r) => targetClasses(r).some((c) => el.includes(c))).sort((a, b) => a.order - b.order);
|
|
981
|
+
const base = mine.filter((r) => !/\[data-/.test(r.text) && !STATE_PSEUDO.test(r.text));
|
|
982
|
+
const states = mine.filter((r) => OPEN_ATTR.test(r.text));
|
|
983
|
+
const opens = states.filter((r) => !/\[data-(starting-style|ending-style|closed)\]/.test(r.text));
|
|
984
|
+
const endings = states.filter((r) => ENDING_ATTR.test(r.text));
|
|
985
|
+
const changed = new Set(states.flatMap((r) => r.decls.map((d) => d.prop)).filter((p) => !p.startsWith("--") && !SILENT.test(p)));
|
|
986
|
+
if (!changed.size) continue;
|
|
987
|
+
const settle = (list) => {
|
|
988
|
+
const computed = /* @__PURE__ */ new Map();
|
|
989
|
+
for (const r of [...list].sort((a, b) => a.order - b.order)) for (const d of r.decls) computed.set(d.prop, d.value);
|
|
990
|
+
return computed;
|
|
991
|
+
};
|
|
992
|
+
const find = (computed) => {
|
|
993
|
+
const entries = parseTransitions(computed, tokens);
|
|
994
|
+
let main;
|
|
995
|
+
const props = /* @__PURE__ */ new Set();
|
|
996
|
+
for (const prop of changed) {
|
|
997
|
+
const e = entryFor(prop, entries);
|
|
998
|
+
if (!e) continue;
|
|
999
|
+
const d = resolveDuration(e.duration, tokens);
|
|
1000
|
+
const ease = resolveEasing(e.easing, tokens);
|
|
1001
|
+
if (!d || d.ms <= 0 || !ease) continue;
|
|
1002
|
+
props.add(shorthandOf(prop));
|
|
1003
|
+
if (!main || d.ms > main.ms) main = { ms: d.ms, ...d.token ? { durationToken: d.token } : {}, easing: ease.easing, ...ease.token ? { easingToken: ease.token } : {}, props };
|
|
1004
|
+
}
|
|
1005
|
+
return main;
|
|
1006
|
+
};
|
|
1007
|
+
for (const [dir, found] of [["enter", find(settle([...base, ...opens]))], ["exit", find(settle([...base, ...endings]))]]) {
|
|
1008
|
+
if (!found) continue;
|
|
1009
|
+
const cur = best[dir];
|
|
1010
|
+
if (!cur || found.ms > cur.ms) best[dir] = { ...found, props: /* @__PURE__ */ new Set([...cur?.props ?? [], ...found.props]) };
|
|
1011
|
+
else for (const p of found.props) cur.props.add(p);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const out = (f) => f ? { duration: f.ms, ...f.durationToken ? { durationToken: f.durationToken } : {}, easing: f.easing, ...f.easingToken ? { easingToken: f.easingToken } : {}, properties: [...f.props].sort() } : null;
|
|
1015
|
+
return { enter: out(best.enter), exit: out(best.exit) };
|
|
1016
|
+
}
|
|
787
1017
|
const STATES = [
|
|
788
1018
|
{ name: "Default", state: DEFAULT_STATE },
|
|
789
1019
|
{ name: "Hover", state: { hover: true, active: false, disabled: false } },
|
|
@@ -2393,6 +2623,8 @@ function svgVector(node, id, els, rules, state, computed, tokens) {
|
|
|
2393
2623
|
if (opacity !== void 0 && opacity !== 1) out.opacity = String(opacity);
|
|
2394
2624
|
const cap = own.get("stroke-linecap");
|
|
2395
2625
|
if (cap) out["stroke-linecap"] = cap;
|
|
2626
|
+
const join2 = own.get("stroke-linejoin");
|
|
2627
|
+
if (join2) out["stroke-linejoin"] = join2;
|
|
2396
2628
|
const pathLength = num(attrs.pathLength, c);
|
|
2397
2629
|
const r = num(attrs.r, c);
|
|
2398
2630
|
const scale = pathLength && r !== void 0 ? 2 * Math.PI * r / pathLength : 1;
|
|
@@ -2802,6 +3034,15 @@ function generateComponentSpec(name, opts, view, setName = name, fixed = {}) {
|
|
|
2802
3034
|
delete e.props[axis.name];
|
|
2803
3035
|
}
|
|
2804
3036
|
}
|
|
3037
|
+
const interactions = deriveInteractions(axes, evaluated, opts.tokens, /radio/i.test(setName));
|
|
3038
|
+
const drawnClasses = /* @__PURE__ */ new Set();
|
|
3039
|
+
const drawnElements = /* @__PURE__ */ new Map();
|
|
3040
|
+
for (const { ev } of evaluated)
|
|
3041
|
+
for (const n of ev.built.parts.values()) {
|
|
3042
|
+
for (const c of n.classes) drawnClasses.add(c);
|
|
3043
|
+
if (n.classes.length) drawnElements.set([...n.classes].sort().join(" "), [...n.classes]);
|
|
3044
|
+
}
|
|
3045
|
+
const motion = motionNotes(rules, drawnClasses, opts.tokens, view, [...drawnElements.values()]);
|
|
2805
3046
|
const variants = [];
|
|
2806
3047
|
const unmapped = [];
|
|
2807
3048
|
const neverShown = new Set(order.filter((id) => evaluated.every((e) => e.ev.hidden.has(id))));
|
|
@@ -2907,8 +3148,50 @@ function generateComponentSpec(name, opts, view, setName = name, fixed = {}) {
|
|
|
2907
3148
|
...view ? { view } : {},
|
|
2908
3149
|
...example ? { example } : {}
|
|
2909
3150
|
};
|
|
2910
|
-
const
|
|
2911
|
-
|
|
3151
|
+
const motionFields = { ...interactions.length ? { interactions } : {}, ...motion.length ? { motion } : {} };
|
|
3152
|
+
const spec = { name: setName, ...component, source, axes, properties, parts, variants, unmapped: dedup, ...motionFields };
|
|
3153
|
+
return { name: setName, ...component, hash: hashSpec(spec), source, axes, properties, parts, variants, unmapped: dedup, ...motionFields };
|
|
3154
|
+
}
|
|
3155
|
+
function deriveInteractions(axes, evaluated, tokens, radio) {
|
|
3156
|
+
const byKey = new Map(evaluated.map((e) => [variantKeyOf(e.props), e]));
|
|
3157
|
+
const state = axes.find((a) => a.name === "State");
|
|
3158
|
+
const checked = axes.find((a) => a.name === "Checked");
|
|
3159
|
+
const rules = [];
|
|
3160
|
+
if (state) {
|
|
3161
|
+
const has = (v) => state.values.includes(v);
|
|
3162
|
+
if (has("Hover")) {
|
|
3163
|
+
if (checked) rules.push({ trigger: "MOUSE_ENTER", axis: "State", from: "Default", to: "Hover" }, { trigger: "MOUSE_LEAVE", axis: "State", from: "Hover", to: "Default" });
|
|
3164
|
+
else rules.push({ trigger: "ON_HOVER", axis: "State", from: "Default", to: "Hover" });
|
|
3165
|
+
}
|
|
3166
|
+
if (has("Pressed")) rules.push({ trigger: "ON_PRESS", axis: "State", from: has("Hover") ? "Hover" : "Default", to: "Pressed" });
|
|
3167
|
+
}
|
|
3168
|
+
if (checked) {
|
|
3169
|
+
const pairs = [["false", "true"]];
|
|
3170
|
+
if (!radio) pairs.push(["true", "false"]);
|
|
3171
|
+
if (checked.values.includes("mixed")) pairs.push(["mixed", "true"]);
|
|
3172
|
+
const only = (p) => p.State === void 0 || p.State === "Default" || p.State === "Hover";
|
|
3173
|
+
for (const [from, to] of pairs) rules.push({ trigger: "ON_CLICK", axis: "Checked", from, to, only });
|
|
3174
|
+
}
|
|
3175
|
+
const out = [];
|
|
3176
|
+
for (const r of rules) {
|
|
3177
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3178
|
+
let candidates = 0;
|
|
3179
|
+
for (const e of evaluated) {
|
|
3180
|
+
if (e.props[r.axis] !== r.from) continue;
|
|
3181
|
+
candidates++;
|
|
3182
|
+
if (r.only && !r.only(e.props)) continue;
|
|
3183
|
+
const target = byKey.get(variantKeyOf({ ...e.props, [r.axis]: r.to }));
|
|
3184
|
+
if (!target || drawn(target.ev) === drawn(e.ev)) continue;
|
|
3185
|
+
const transition = transitionBetween(e.ev.computed, target.ev.computed, tokens);
|
|
3186
|
+
const k = JSON.stringify(transition);
|
|
3187
|
+
const g = groups.get(k) ?? { transition, keys: [] };
|
|
3188
|
+
g.keys.push(variantKeyOf(e.props));
|
|
3189
|
+
groups.set(k, g);
|
|
3190
|
+
}
|
|
3191
|
+
const total = [...groups.values()].reduce((n, g) => n + g.keys.length, 0);
|
|
3192
|
+
for (const g of groups.values()) out.push({ trigger: r.trigger, axis: r.axis, from: r.from, to: r.to, ...groups.size > 1 || total < candidates ? { variants: g.keys } : {}, transition: g.transition });
|
|
3193
|
+
}
|
|
3194
|
+
return out;
|
|
2912
3195
|
}
|
|
2913
3196
|
function specDrawsSomething(spec) {
|
|
2914
3197
|
const kinds = new Map(spec.parts.map((p) => [p.id, p]));
|
|
@@ -3286,9 +3569,12 @@ function draftedSpec(folder, opts) {
|
|
|
3286
3569
|
function tokenCssIndex(files, opts = {}) {
|
|
3287
3570
|
const set = buildTokenSet(files, opts.layout ?? {});
|
|
3288
3571
|
const out = /* @__PURE__ */ new Map();
|
|
3572
|
+
const ctx = defaultContext(set);
|
|
3289
3573
|
for (const t of set.tokens.values()) {
|
|
3290
3574
|
const v = tokenCssVar(t.path, opts.prefix, opts.separator);
|
|
3291
|
-
if (
|
|
3575
|
+
if (out.has(v)) continue;
|
|
3576
|
+
const value = t.type === "duration" || t.type === "cubicBezier" ? resolve$1(set.byPath, t.path, ctx).value : void 0;
|
|
3577
|
+
out.set(v, { path: t.path, type: t.type, ...value !== void 0 ? { value } : {} });
|
|
3292
3578
|
}
|
|
3293
3579
|
return out;
|
|
3294
3580
|
}
|
|
@@ -3333,6 +3619,7 @@ export {
|
|
|
3333
3619
|
MEDIA_SLOT,
|
|
3334
3620
|
UnsupportedComponentError,
|
|
3335
3621
|
branchExists,
|
|
3622
|
+
changedProperties,
|
|
3336
3623
|
collectAuditFiles,
|
|
3337
3624
|
commitToNewLocalBranch,
|
|
3338
3625
|
computeValues,
|
|
@@ -3340,6 +3627,7 @@ export {
|
|
|
3340
3627
|
declaredFor,
|
|
3341
3628
|
draftedCodeHash,
|
|
3342
3629
|
draftedSpec,
|
|
3630
|
+
entryFor,
|
|
3343
3631
|
expandDecl,
|
|
3344
3632
|
findDesignSystemRoot,
|
|
3345
3633
|
fixCss,
|
|
@@ -3360,17 +3648,25 @@ export {
|
|
|
3360
3648
|
localTags,
|
|
3361
3649
|
matchComplex,
|
|
3362
3650
|
mediaApplies,
|
|
3651
|
+
motionNotes,
|
|
3652
|
+
openingTransitions,
|
|
3363
3653
|
originRepo,
|
|
3364
3654
|
overridesDirOf,
|
|
3365
3655
|
parseCssModule,
|
|
3366
3656
|
parseSelector,
|
|
3657
|
+
parseTransitions,
|
|
3367
3658
|
repoSpecOptions,
|
|
3659
|
+
resolveDuration,
|
|
3660
|
+
resolveEasing,
|
|
3368
3661
|
runAudit,
|
|
3369
3662
|
sentenceCase,
|
|
3370
3663
|
serializeComponentSpecs,
|
|
3664
|
+
shorthandOf,
|
|
3371
3665
|
showFile,
|
|
3372
3666
|
specDrawsSomething,
|
|
3373
3667
|
splitTop,
|
|
3374
|
-
|
|
3668
|
+
substituteVars,
|
|
3669
|
+
tokenCssIndex,
|
|
3670
|
+
transitionBetween
|
|
3375
3671
|
};
|
|
3376
3672
|
//# sourceMappingURL=index.js.map
|