figma-plugin-utilities 0.4.0 → 0.5.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/CHANGELOG.md +36 -1
- package/README.md +134 -26
- package/package.json +26 -1
- package/src/components/CodeExportModal.svelte +98 -0
- package/src/components/ConfirmModal.svelte +66 -0
- package/src/components/DataTable.svelte +349 -0
- package/src/components/FieldGrid.svelte +18 -0
- package/src/components/Footer.svelte +10 -0
- package/src/components/Header.svelte +6 -1
- package/src/components/LadderBadges.svelte +35 -0
- package/src/components/ListItem.svelte +53 -28
- package/src/components/MappingChip.svelte +150 -0
- package/src/components/RampCurve.svelte +383 -0
- package/src/components/Section.svelte +49 -0
- package/src/components/SteppedField.svelte +52 -0
- package/src/components/index.js +9 -0
- package/src/index.js +19 -3
- package/src/lib/colors.ts +99 -0
- package/src/lib/confirm.ts +55 -0
- package/src/lib/errorHandling.js +0 -39
- package/src/lib/figma-frame-builders.ts +2 -2
- package/src/lib/figma-helpers.ts +103 -59
- package/src/lib/figma-variables.ts +151 -0
- package/src/lib/format.ts +19 -0
- package/src/lib/index.js +13 -4
- package/src/lib/scale.ts +147 -0
- package/src/lib/colors.js +0 -80
package/src/lib/scale.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Responsive scale math shared by plugins that shape a scale across
|
|
2
|
+
// breakpoints, such as Vitrine Tools' Type and Spacing tabs: the ramp's quadratic
|
|
3
|
+
// Bézier, blending by viewport width, values between breakpoints, fallback
|
|
4
|
+
// widths and fluid CSS. Pure — no Figma API — so both threads may import it.
|
|
5
|
+
//
|
|
6
|
+
// Units are the caller's: the Type tab works in px along its ladder, the
|
|
7
|
+
// Spacing tab in ladder rungs. Nothing here maps a value onto a ladder.
|
|
8
|
+
|
|
9
|
+
/** The quadratic Bézier through p0 and p2, pulled toward p1, at t in 0…1. */
|
|
10
|
+
export const bezier = (t: number, p0: number, p1: number, p2: number) =>
|
|
11
|
+
(1 - t) ** 2 * p0 + 2 * (1 - t) * t * p1 + t * t * p2;
|
|
12
|
+
|
|
13
|
+
/** How far the bend may move toward either end; at 0 or 1 the curve folds. */
|
|
14
|
+
export const clampPosition = (c: number) => Math.min(0.98, Math.max(0.02, c));
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The curve's parameter t for a point at x, 0…1, when the bend sits at `c`
|
|
18
|
+
* along x: the curve runs through x(t) = 2(1−t)t·c + t², solved for t in the
|
|
19
|
+
* conjugate form, which has no division by zero at c = 0.5 (where t = x).
|
|
20
|
+
*/
|
|
21
|
+
export function levelT(x: number, c = 0.5): number {
|
|
22
|
+
const pos = clampPosition(c);
|
|
23
|
+
return x / (Math.sqrt(pos * pos + (1 - 2 * pos) * x) + pos);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Rounds halves down, so a growing value holds its smaller size a breakpoint
|
|
30
|
+
* longer — 16 16 18 18 20 rather than 16 18 18 20 20.
|
|
31
|
+
*/
|
|
32
|
+
export const roundHalfDown = (x: number) => Math.ceil(x - 0.5);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* How far breakpoint `index` sits from the smallest toward the largest, 0…1:
|
|
36
|
+
* by viewport width when every width is known and they widen, so the blend
|
|
37
|
+
* matches CSS vw, and evenly by position otherwise.
|
|
38
|
+
*/
|
|
39
|
+
export function blendAt(
|
|
40
|
+
index: number,
|
|
41
|
+
count: number,
|
|
42
|
+
widths?: number[],
|
|
43
|
+
): number {
|
|
44
|
+
if (count < 2) return 0;
|
|
45
|
+
const w0 = widths?.[0];
|
|
46
|
+
const w1 = widths?.[count - 1];
|
|
47
|
+
if (
|
|
48
|
+
widths?.length === count &&
|
|
49
|
+
w0 !== undefined &&
|
|
50
|
+
w1 !== undefined &&
|
|
51
|
+
w1 > w0
|
|
52
|
+
) {
|
|
53
|
+
return Math.min(1, Math.max(0, (widths[index] - w0) / (w1 - w0)));
|
|
54
|
+
}
|
|
55
|
+
return index / (count - 1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A value at viewport width `width`, from its value at each breakpoint:
|
|
60
|
+
* straight between the breakpoints either side, as fluid CSS has it, and
|
|
61
|
+
* held beyond the smallest and largest. Stepped, it is the value of the last
|
|
62
|
+
* breakpoint at or below the width, as a media query holds it.
|
|
63
|
+
*/
|
|
64
|
+
export function valueAtWidth(
|
|
65
|
+
widths: number[],
|
|
66
|
+
values: number[],
|
|
67
|
+
width: number,
|
|
68
|
+
stepped = false,
|
|
69
|
+
): number {
|
|
70
|
+
const last = widths.length - 1;
|
|
71
|
+
if (last < 0) return 0;
|
|
72
|
+
if (width <= widths[0]) return values[0];
|
|
73
|
+
if (width >= widths[last]) return values[last];
|
|
74
|
+
let b = 0;
|
|
75
|
+
while (b < last - 1 && widths[b + 1] <= width) b++;
|
|
76
|
+
if (stepped) return values[b];
|
|
77
|
+
const t = (width - widths[b]) / (widths[b + 1] - widths[b]);
|
|
78
|
+
return values[b] + (values[b + 1] - values[b]) * t;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Breakpoint widths by name, for a file that doesn't give them. */
|
|
82
|
+
export const FALLBACK_WIDTHS: Record<string, number> = {
|
|
83
|
+
xs: 320,
|
|
84
|
+
sm: 400,
|
|
85
|
+
md: 768,
|
|
86
|
+
lg: 1024,
|
|
87
|
+
xl: 1440,
|
|
88
|
+
xlg: 1440,
|
|
89
|
+
"2xl": 1920,
|
|
90
|
+
xxl: 1920,
|
|
91
|
+
"3xl": 2560,
|
|
92
|
+
xxxl: 2560,
|
|
93
|
+
max: 2560,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** Whether a name is one the fallback widths know, in any case. */
|
|
97
|
+
export const isBreakpointName = (name: string) =>
|
|
98
|
+
Object.prototype.hasOwnProperty.call(FALLBACK_WIDTHS, name.toLowerCase());
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Each breakpoint's width: the file's (`known`), else the usual one for its
|
|
102
|
+
* name, else 400px past the one before. Out of order, a width is taken as
|
|
103
|
+
* unknown — a breakpoint can't sit narrower than the one before it.
|
|
104
|
+
*/
|
|
105
|
+
export function widthsFor(
|
|
106
|
+
names: string[],
|
|
107
|
+
known: Record<string, number> = {},
|
|
108
|
+
): number[] {
|
|
109
|
+
const out: number[] = [];
|
|
110
|
+
names.forEach((name, i) => {
|
|
111
|
+
const previous = out[i - 1] ?? 0;
|
|
112
|
+
const given = known[name] ?? FALLBACK_WIDTHS[name.toLowerCase()];
|
|
113
|
+
out.push(
|
|
114
|
+
given !== undefined && given > previous
|
|
115
|
+
? given
|
|
116
|
+
: previous
|
|
117
|
+
? previous + 400
|
|
118
|
+
: 400,
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A number as CSS writes it: no trailing zeros, at most `places` decimals. */
|
|
125
|
+
export const trimNumber = (n: number, places = 4) =>
|
|
126
|
+
String(Number(n.toFixed(places)));
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A length fluid between two viewport widths, as CSS: `from` at `w0`, `to`
|
|
130
|
+
* at `w1`, straight between and held beyond — a clamp() over vw, or the
|
|
131
|
+
* length alone when it doesn't change. `length` writes a px value in the
|
|
132
|
+
* caller's unit, such as `(px) => `${px / 16}rem``.
|
|
133
|
+
*/
|
|
134
|
+
export function fluidClamp(
|
|
135
|
+
from: number,
|
|
136
|
+
to: number,
|
|
137
|
+
w0: number,
|
|
138
|
+
w1: number,
|
|
139
|
+
length: (px: number) => string,
|
|
140
|
+
): string {
|
|
141
|
+
if (w1 === w0 || Math.abs(from - to) < 1e-9) return length(from);
|
|
142
|
+
const slope = (to - from) / (w1 - w0);
|
|
143
|
+
const intercept = from - slope * w0;
|
|
144
|
+
const vw = slope * 100;
|
|
145
|
+
const sign = vw < 0 ? "-" : "+";
|
|
146
|
+
return `clamp(${length(Math.min(from, to))}, ${length(intercept)} ${sign} ${trimNumber(Math.abs(vw))}vw, ${length(Math.max(from, to))})`;
|
|
147
|
+
}
|
package/src/lib/colors.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Color utilities for Figma plugins
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Convert RGB color (0-1 range) to HEX string
|
|
7
|
-
* @param {{r: number, g: number, b: number}} color - RGB color with values 0-1
|
|
8
|
-
* @returns {string} HEX color string (e.g., "#FF0000")
|
|
9
|
-
*/
|
|
10
|
-
export function rgbToHex({ r, g, b }) {
|
|
11
|
-
const toHex = (value) => {
|
|
12
|
-
const hex = Math.round(value * 255)
|
|
13
|
-
.toString(16)
|
|
14
|
-
.padStart(2, "0");
|
|
15
|
-
return hex;
|
|
16
|
-
};
|
|
17
|
-
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Convert HEX string to RGB color (0-1 range)
|
|
22
|
-
* @param {string} hex - HEX color string (e.g., "#FF0000" or "FF0000")
|
|
23
|
-
* @returns {{r: number, g: number, b: number} | null} RGB color with values 0-1, or null if invalid
|
|
24
|
-
*/
|
|
25
|
-
export function hexToRgb(hex) {
|
|
26
|
-
const cleanHex = hex.replace(/^#/, "");
|
|
27
|
-
if (!/^[0-9A-Fa-f]{6}$/.test(cleanHex)) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
const r = parseInt(cleanHex.substring(0, 2), 16) / 255;
|
|
31
|
-
const g = parseInt(cleanHex.substring(2, 4), 16) / 255;
|
|
32
|
-
const b = parseInt(cleanHex.substring(4, 6), 16) / 255;
|
|
33
|
-
return { r, g, b };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Get relative luminance of a color (for contrast calculations)
|
|
38
|
-
* @param {{r: number, g: number, b: number}} color - RGB color with values 0-1
|
|
39
|
-
* @returns {number} Relative luminance (0-1)
|
|
40
|
-
*/
|
|
41
|
-
export function getLuminance({ r, g, b }) {
|
|
42
|
-
const adjust = (c) =>
|
|
43
|
-
c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
44
|
-
return 0.2126 * adjust(r) + 0.7152 * adjust(g) + 0.0722 * adjust(b);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Calculate contrast ratio between two colors (WCAG formula)
|
|
49
|
-
* @param {{r: number, g: number, b: number}} color1 - First RGB color (0-1 range)
|
|
50
|
-
* @param {{r: number, g: number, b: number}} color2 - Second RGB color (0-1 range)
|
|
51
|
-
* @returns {number} Contrast ratio (1-21)
|
|
52
|
-
*/
|
|
53
|
-
export function getContrastRatio(color1, color2) {
|
|
54
|
-
const l1 = getLuminance(color1);
|
|
55
|
-
const l2 = getLuminance(color2);
|
|
56
|
-
const lighter = Math.max(l1, l2);
|
|
57
|
-
const darker = Math.min(l1, l2);
|
|
58
|
-
return (lighter + 0.05) / (darker + 0.05);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Check if contrast ratio meets WCAG level
|
|
63
|
-
* @param {number} ratio - Contrast ratio
|
|
64
|
-
* @param {"AA" | "AAA" | "AA-large" | "AAA-large"} level - WCAG level to check
|
|
65
|
-
* @returns {boolean} Whether the ratio meets the level
|
|
66
|
-
*/
|
|
67
|
-
export function meetsContrastLevel(ratio, level) {
|
|
68
|
-
switch (level) {
|
|
69
|
-
case "AAA":
|
|
70
|
-
return ratio >= 7;
|
|
71
|
-
case "AAA-large":
|
|
72
|
-
return ratio >= 4.5;
|
|
73
|
-
case "AA":
|
|
74
|
-
return ratio >= 4.5;
|
|
75
|
-
case "AA-large":
|
|
76
|
-
return ratio >= 3;
|
|
77
|
-
default:
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
}
|