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.
@@ -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
- }