@flow.os/style 0.0.1-dev.1771665310

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/breakpoints.ts ADDED
@@ -0,0 +1,20 @@
1
+ /** Chiavi viewport: ordine da piccolo a grande (merge sovrascrive). */
2
+ export const VIEWPORT_KEYS = ['mob', 'tab', 'des'] as const;
3
+ export type ViewportKey = (typeof VIEWPORT_KEYS)[number];
4
+
5
+ /** Chiavi pseudo-stato (hover, active, focus). */
6
+ export const PSEUDO_KEYS = ['hover', 'active', 'focus', 'focusVisible'] as const;
7
+ export type PseudoKey = (typeof PSEUDO_KEYS)[number];
8
+
9
+ /** Larghezze min (px) per viewport. mob < 640, tab 640–1023, des 1024+. */
10
+ export const VIEWPORT_WIDTHS: Record<ViewportKey, number> = {
11
+ mob: 0,
12
+ tab: 640,
13
+ des: 1024,
14
+ };
15
+
16
+ export function getViewportKeyFromWidth(w: number): ViewportKey {
17
+ if (w >= 1024) return 'des';
18
+ if (w >= 640) return 'tab';
19
+ return 'mob';
20
+ }
@@ -0,0 +1,5 @@
1
+ .vscode/**
2
+ src/**
3
+ tsconfig.json
4
+ .gitignore
5
+ **/*.map
@@ -0,0 +1,7 @@
1
+ # Flow Style — Color Picker
2
+
3
+ - **Pallino** (cerchio) prima di ogni colore `#hex` o `rgb(...)`.
4
+ - **Quadratino** dell’editor sul colore: **click** → picker nativo inline.
5
+ - Alternativa: **Ctrl+Shift+C** con cursore sul colore → input hex/rgb.
6
+
7
+ Installazione: Install from VSIX → `flow-style-colors-0.0.1.vsix`.
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.activate = activate;
4
+ exports.deactivate = deactivate;
5
+ const vscode = require("vscode");
6
+ function parseHexColor(text) {
7
+ const hex = text.replace(/^#/, '');
8
+ if (hex.length === 3) {
9
+ const r = parseInt(hex[0] + hex[0], 16) / 255;
10
+ const g = parseInt(hex[1] + hex[1], 16) / 255;
11
+ const b = parseInt(hex[2] + hex[2], 16) / 255;
12
+ return { r, g, b, a: 1 };
13
+ }
14
+ if (hex.length === 6 || hex.length === 8) {
15
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
16
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
17
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
18
+ const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1;
19
+ return { r, g, b, a };
20
+ }
21
+ return null;
22
+ }
23
+ function parseRgb(text) {
24
+ const m = text.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
25
+ if (!m)
26
+ return null;
27
+ return {
28
+ r: Math.min(255, parseInt(m[1], 10)) / 255,
29
+ g: Math.min(255, parseInt(m[2], 10)) / 255,
30
+ b: Math.min(255, parseInt(m[3], 10)) / 255,
31
+ a: m[4] != null ? parseFloat(m[4]) : 1,
32
+ };
33
+ }
34
+ function findColorRanges(document) {
35
+ const text = document.getText();
36
+ const results = [];
37
+ const hexPattern = /#[0-9a-fA-F]{3,8}(?![0-9a-fA-F])/g;
38
+ let m;
39
+ while ((m = hexPattern.exec(text)) !== null) {
40
+ const full = m[0];
41
+ const color = parseHexColor(full);
42
+ if (color) {
43
+ results.push({
44
+ range: new vscode.Range(document.positionAt(m.index), document.positionAt(m.index + full.length)),
45
+ color: new vscode.Color(color.r, color.g, color.b, color.a),
46
+ });
47
+ }
48
+ }
49
+ const rgbPattern = /rgba?\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+(?:\s*,\s*[\d.]+)?\s*\)/g;
50
+ while ((m = rgbPattern.exec(text)) !== null) {
51
+ const color = parseRgb(m[0]);
52
+ if (color) {
53
+ results.push({
54
+ range: new vscode.Range(document.positionAt(m.index), document.positionAt(m.index + m[0].length)),
55
+ color: new vscode.Color(color.r, color.g, color.b, color.a),
56
+ });
57
+ }
58
+ }
59
+ return results;
60
+ }
61
+ function provideColorPresentations(color, context) {
62
+ const { red, green, blue, alpha } = color;
63
+ const r = Math.round(red * 255);
64
+ const g = Math.round(green * 255);
65
+ const b = Math.round(blue * 255);
66
+ const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
67
+ const out = [
68
+ { label: hex, textEdit: new vscode.TextEdit(context.range, hex) },
69
+ ];
70
+ if (alpha < 1) {
71
+ const hexa = hex + Math.round(alpha * 255).toString(16).padStart(2, '0');
72
+ out.push({ label: hexa, textEdit: new vscode.TextEdit(context.range, hexa) }, { label: `rgba(${r}, ${g}, ${b}, ${alpha.toFixed(2)})`, textEdit: new vscode.TextEdit(context.range, `rgba(${r}, ${g}, ${b}, ${alpha.toFixed(2)})`) });
73
+ }
74
+ else {
75
+ out.push({ label: `rgb(${r}, ${g}, ${b})`, textEdit: new vscode.TextEdit(context.range, `rgb(${r}, ${g}, ${b})`) });
76
+ }
77
+ return out;
78
+ }
79
+ function activate(context) {
80
+ // Assicura che il quadratino colore sia visibile (può essere stato disattivato in precedenza)
81
+ const cfg = vscode.workspace.getConfiguration('editor');
82
+ if (cfg.get('colorDecorators') === false) {
83
+ void cfg.update('colorDecorators', true, vscode.ConfigurationTarget.Workspace);
84
+ }
85
+ // Quadrato nativo: click sul colore → picker inline
86
+ const colorProvider = vscode.languages.registerColorProvider([{ language: 'typescript' }, { language: 'typescriptreact' }, { language: 'javascript' }, { language: 'javascriptreact' }], {
87
+ provideDocumentColors(document) {
88
+ return findColorRanges(document);
89
+ },
90
+ provideColorPresentations(color, ctx) {
91
+ return provideColorPresentations(color, ctx);
92
+ },
93
+ });
94
+ context.subscriptions.push(colorProvider);
95
+ // Comando alternativo: cursore sul colore → Ctrl+Shift+C → input hex/rgb
96
+ const editCmd = vscode.commands.registerCommand('flow-style-colors.editColor', async () => {
97
+ const editor = vscode.window.activeTextEditor;
98
+ if (!editor)
99
+ return;
100
+ const doc = editor.document;
101
+ const pos = editor.selection.active;
102
+ const infos = findColorRanges(doc);
103
+ const info = infos.find((i) => i.range.contains(pos));
104
+ if (!info) {
105
+ void vscode.window.showInformationMessage('Posiziona il cursore su un colore (#hex o rgb(...))');
106
+ return;
107
+ }
108
+ const current = doc.getText(info.range);
109
+ const newValue = await vscode.window.showInputBox({
110
+ title: 'Modifica colore',
111
+ value: current,
112
+ prompt: 'Hex (#fff) o rgb(r,g,b)',
113
+ validateInput(v) {
114
+ const t = v.trim();
115
+ if (/^#[0-9a-fA-F]{3,8}$/.test(t))
116
+ return null;
117
+ if (/^rgba?\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+(?:\s*,\s*[\d.]+)?\s*\)$/.test(t))
118
+ return null;
119
+ return 'Inserisci #hex o rgb(r,g,b)';
120
+ },
121
+ });
122
+ if (newValue == null)
123
+ return;
124
+ const edit = new vscode.WorkspaceEdit();
125
+ edit.replace(doc.uri, info.range, newValue.trim());
126
+ await vscode.workspace.applyEdit(edit);
127
+ });
128
+ context.subscriptions.push(editCmd);
129
+ }
130
+ function deactivate() { }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "flow-style-colors",
3
+ "displayName": "Flow Style — Color Picker",
4
+ "description": "Cerchio + color picker per hex/rgb in TS/JS",
5
+ "version": "0.0.1",
6
+ "publisher": "flow-framework",
7
+ "engines": { "vscode": "^1.85.0" },
8
+ "categories": ["Other"],
9
+ "activationEvents": ["onLanguage:typescript", "onLanguage:typescriptreact", "onLanguage:javascript", "onLanguage:javascriptreact"],
10
+ "main": "./out/extension.js",
11
+ "contributes": {
12
+ "commands": [{ "command": "flow-style-colors.editColor", "title": "Flow Style: Modifica colore" }],
13
+ "keybindings": [{ "command": "flow-style-colors.editColor", "key": "ctrl+shift+c", "when": "editorTextFocus" }]
14
+ },
15
+ "scripts": {
16
+ "vscode:prepublish": "tsc -p .",
17
+ "compile": "tsc -p .",
18
+ "watch": "tsc -w -p ."
19
+ },
20
+ "devDependencies": {
21
+ "@types/vscode": "^1.85.0",
22
+ "typescript": "~5.7.0"
23
+ }
24
+ }
@@ -0,0 +1,146 @@
1
+ import * as vscode from 'vscode';
2
+
3
+ function parseHexColor(text: string): { r: number; g: number; b: number; a: number } | null {
4
+ const hex = text.replace(/^#/, '');
5
+ if (hex.length === 3) {
6
+ const r = parseInt(hex[0] + hex[0], 16) / 255;
7
+ const g = parseInt(hex[1] + hex[1], 16) / 255;
8
+ const b = parseInt(hex[2] + hex[2], 16) / 255;
9
+ return { r, g, b, a: 1 };
10
+ }
11
+ if (hex.length === 6 || hex.length === 8) {
12
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
13
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
14
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
15
+ const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1;
16
+ return { r, g, b, a };
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function parseRgb(text: string): { r: number; g: number; b: number; a: number } | null {
22
+ const m = text.match(/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
23
+ if (!m) return null;
24
+ return {
25
+ r: Math.min(255, parseInt(m[1], 10)) / 255,
26
+ g: Math.min(255, parseInt(m[2], 10)) / 255,
27
+ b: Math.min(255, parseInt(m[3], 10)) / 255,
28
+ a: m[4] != null ? parseFloat(m[4]) : 1,
29
+ };
30
+ }
31
+
32
+ function findColorRanges(document: vscode.TextDocument): vscode.ColorInformation[] {
33
+ const text = document.getText();
34
+ const results: vscode.ColorInformation[] = [];
35
+
36
+ const hexPattern = /#[0-9a-fA-F]{3,8}(?![0-9a-fA-F])/g;
37
+ let m: RegExpExecArray | null;
38
+ while ((m = hexPattern.exec(text)) !== null) {
39
+ const full = m[0];
40
+ const color = parseHexColor(full);
41
+ if (color) {
42
+ results.push({
43
+ range: new vscode.Range(
44
+ document.positionAt(m.index),
45
+ document.positionAt(m.index + full.length)
46
+ ),
47
+ color: new vscode.Color(color.r, color.g, color.b, color.a),
48
+ });
49
+ }
50
+ }
51
+
52
+ const rgbPattern = /rgba?\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+(?:\s*,\s*[\d.]+)?\s*\)/g;
53
+ while ((m = rgbPattern.exec(text)) !== null) {
54
+ const color = parseRgb(m[0]);
55
+ if (color) {
56
+ results.push({
57
+ range: new vscode.Range(
58
+ document.positionAt(m.index),
59
+ document.positionAt(m.index + m[0].length)
60
+ ),
61
+ color: new vscode.Color(color.r, color.g, color.b, color.a),
62
+ });
63
+ }
64
+ }
65
+
66
+ return results;
67
+ }
68
+
69
+ function provideColorPresentations(
70
+ color: vscode.Color,
71
+ context: { document: vscode.TextDocument; range: vscode.Range }
72
+ ): vscode.ColorPresentation[] {
73
+ const { red, green, blue, alpha } = color;
74
+ const r = Math.round(red * 255);
75
+ const g = Math.round(green * 255);
76
+ const b = Math.round(blue * 255);
77
+ const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
78
+ const out: vscode.ColorPresentation[] = [
79
+ { label: hex, textEdit: new vscode.TextEdit(context.range, hex) },
80
+ ];
81
+ if (alpha < 1) {
82
+ const hexa = hex + Math.round(alpha * 255).toString(16).padStart(2, '0');
83
+ out.push(
84
+ { label: hexa, textEdit: new vscode.TextEdit(context.range, hexa) },
85
+ { label: `rgba(${r}, ${g}, ${b}, ${alpha.toFixed(2)})`, textEdit: new vscode.TextEdit(context.range, `rgba(${r}, ${g}, ${b}, ${alpha.toFixed(2)})`) }
86
+ );
87
+ } else {
88
+ out.push({ label: `rgb(${r}, ${g}, ${b})`, textEdit: new vscode.TextEdit(context.range, `rgb(${r}, ${g}, ${b})`) });
89
+ }
90
+ return out;
91
+ }
92
+
93
+ export function activate(context: vscode.ExtensionContext): void {
94
+ // Assicura che il quadratino colore sia visibile (può essere stato disattivato in precedenza)
95
+ const cfg = vscode.workspace.getConfiguration('editor');
96
+ if (cfg.get('colorDecorators') === false) {
97
+ void cfg.update('colorDecorators', true, vscode.ConfigurationTarget.Workspace);
98
+ }
99
+
100
+ // Quadrato nativo: click sul colore → picker inline
101
+ const colorProvider = vscode.languages.registerColorProvider(
102
+ [{ language: 'typescript' }, { language: 'typescriptreact' }, { language: 'javascript' }, { language: 'javascriptreact' }],
103
+ {
104
+ provideDocumentColors(document) {
105
+ return findColorRanges(document);
106
+ },
107
+ provideColorPresentations(color, ctx) {
108
+ return provideColorPresentations(color, ctx);
109
+ },
110
+ }
111
+ );
112
+ context.subscriptions.push(colorProvider);
113
+
114
+ // Comando alternativo: cursore sul colore → Ctrl+Shift+C → input hex/rgb
115
+ const editCmd = vscode.commands.registerCommand('flow-style-colors.editColor', async () => {
116
+ const editor = vscode.window.activeTextEditor;
117
+ if (!editor) return;
118
+ const doc = editor.document;
119
+ const pos = editor.selection.active;
120
+ const infos = findColorRanges(doc);
121
+ const info = infos.find((i) => i.range.contains(pos));
122
+ if (!info) {
123
+ void vscode.window.showInformationMessage('Posiziona il cursore su un colore (#hex o rgb(...))');
124
+ return;
125
+ }
126
+ const current = doc.getText(info.range);
127
+ const newValue = await vscode.window.showInputBox({
128
+ title: 'Modifica colore',
129
+ value: current,
130
+ prompt: 'Hex (#fff) o rgb(r,g,b)',
131
+ validateInput(v) {
132
+ const t = v.trim();
133
+ if (/^#[0-9a-fA-F]{3,8}$/.test(t)) return null;
134
+ if (/^rgba?\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+(?:\s*,\s*[\d.]+)?\s*\)$/.test(t)) return null;
135
+ return 'Inserisci #hex o rgb(r,g,b)';
136
+ },
137
+ });
138
+ if (newValue == null) return;
139
+ const edit = new vscode.WorkspaceEdit();
140
+ edit.replace(doc.uri, info.range, newValue.trim());
141
+ await vscode.workspace.applyEdit(edit);
142
+ });
143
+ context.subscriptions.push(editCmd);
144
+ }
145
+
146
+ export function deactivate(): void {}
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022"],
6
+ "outDir": "out",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "skipLibCheck": true
10
+ },
11
+ "include": ["src/**/*"]
12
+ }
package/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export { SHORTHAND_MAP, isStyleKey, getStyleProp, toStyleValue } from './shorthand.js';
2
+ export type { StyleShorthandKey } from './shorthand.js';
3
+ export {
4
+ VIEWPORT_KEYS,
5
+ PSEUDO_KEYS,
6
+ VIEWPORT_WIDTHS,
7
+ getViewportKeyFromWidth,
8
+ } from './breakpoints.js';
9
+ export type { ViewportKey, PseudoKey } from './breakpoints.js';
10
+ export {
11
+ resolveLayer,
12
+ mergeResolved,
13
+ resolvePseudoStyle,
14
+ styleToCssText,
15
+ } from './resolve.js';
16
+ export type { PlainLayer, ResolvedLayer } from './resolve.js';
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@flow.os/style",
3
+ "version": "0.0.1-dev.1771665310",
4
+ "type": "module",
5
+ "main": "./index.ts",
6
+ "types": "./index.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./index.ts",
10
+ "import": "./index.ts",
11
+ "default": "./index.ts"
12
+ }
13
+ }
14
+ }
package/resolve.ts ADDED
@@ -0,0 +1,84 @@
1
+ import { isStyleKey, getStyleProp, toStyleValue } from './shorthand.js';
2
+ import { VIEWPORT_KEYS, PSEUDO_KEYS } from './breakpoints.js';
3
+
4
+ const DEFAULT_TOKENS = new Set(['primary', 'muted', 'primary-foreground']);
5
+
6
+ /** Layer già "plain" (getter chiamati dal runtime). base + flat style/class. */
7
+ export type PlainLayer = {
8
+ base?: string;
9
+ [key: string]: string | number | boolean | PlainLayer | undefined;
10
+ };
11
+
12
+ export type ResolvedLayer = { class: string; style: Record<string, string> };
13
+
14
+ const RESERVED = new Set([
15
+ ...VIEWPORT_KEYS,
16
+ ...PSEUDO_KEYS,
17
+ ]);
18
+
19
+ function isReserved(key: string): boolean {
20
+ return RESERVED.has(key as 'mob');
21
+ }
22
+
23
+ /** Risolve un layer plain in class + style. Non considera mob/tab/des/hover/active. */
24
+ export function resolveLayer(plain: PlainLayer, tokens?: Set<string>): ResolvedLayer {
25
+ const t = tokens ?? DEFAULT_TOKENS;
26
+ const classes: string[] = [];
27
+ const style: Record<string, string> = {};
28
+
29
+ if (plain.base) classes.push(plain.base.trim());
30
+
31
+ for (const [k, v] of Object.entries(plain)) {
32
+ if (k === 'base' || isReserved(k)) continue;
33
+ if (v === undefined || v === null) continue;
34
+
35
+ if (isStyleKey(k)) {
36
+ const prop = getStyleProp(k);
37
+ if (prop) {
38
+ let val = toStyleValue(v, t);
39
+ if (k === 'scale' && typeof v === 'number') val = `scale(${v})`;
40
+ style[prop] = val;
41
+ }
42
+ } else {
43
+ if (v === true) classes.push(k);
44
+ if (typeof v === 'string' && v) classes.push(v);
45
+ }
46
+ }
47
+
48
+ return {
49
+ class: classes.filter(Boolean).join(' '),
50
+ style,
51
+ };
52
+ }
53
+
54
+ /** Merge due resolved: second sovrascrive style, class è concatenata. */
55
+ export function mergeResolved(a: ResolvedLayer, b: ResolvedLayer): ResolvedLayer {
56
+ return {
57
+ class: [a.class, b.class].filter(Boolean).join(' '),
58
+ style: { ...a.style, ...b.style },
59
+ };
60
+ }
61
+
62
+ /** Solo stile da un layer (per pseudo). Ignora base e classList. */
63
+ export function resolvePseudoStyle(plain: PlainLayer, tokens?: Set<string>): Record<string, string> {
64
+ const t = tokens ?? DEFAULT_TOKENS;
65
+ const style: Record<string, string> = {};
66
+ for (const [k, v] of Object.entries(plain)) {
67
+ if (k === 'base' || isReserved(k)) continue;
68
+ if (!isStyleKey(k)) continue;
69
+ const prop = getStyleProp(k);
70
+ if (prop && v !== undefined && v !== null) {
71
+ let val = toStyleValue(v, t);
72
+ if (k === 'scale' && typeof v === 'number') val = `scale(${v})`;
73
+ style[prop] = val;
74
+ }
75
+ }
76
+ return style;
77
+ }
78
+
79
+ /** Da oggetto stile a stringa CSS (per iniettare regola :hover). */
80
+ export function styleToCssText(style: Record<string, string>): string {
81
+ return Object.entries(style)
82
+ .map(([k, v]) => `${k}: ${v}`)
83
+ .join('; ');
84
+ }
package/shorthand.ts ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Shorthand → CSS property. Solo chiavi "pure" (no suffisso tipo mb-10).
3
+ * Usato per decidere se una chiave è stile (valore number/string/getter) o classList (bool/getter).
4
+ */
5
+ export const SHORTHAND_MAP = {
6
+ text: 'fontSize',
7
+ mb: 'marginBottom',
8
+ mt: 'marginTop',
9
+ ml: 'marginLeft',
10
+ mr: 'marginRight',
11
+ mx: 'marginLeft',
12
+ my: 'marginTop',
13
+ m: 'margin',
14
+ pb: 'paddingBottom',
15
+ pt: 'paddingTop',
16
+ pl: 'paddingLeft',
17
+ pr: 'paddingRight',
18
+ px: 'paddingLeft',
19
+ py: 'paddingTop',
20
+ p: 'padding',
21
+ w: 'width',
22
+ h: 'height',
23
+ minW: 'minWidth',
24
+ minH: 'minHeight',
25
+ maxW: 'maxWidth',
26
+ maxH: 'maxHeight',
27
+ gap: 'gap',
28
+ rounded: 'borderRadius',
29
+ top: 'top',
30
+ left: 'left',
31
+ right: 'right',
32
+ bottom: 'bottom',
33
+ color: 'color',
34
+ bg: 'background',
35
+ scale: 'transform',
36
+ outline: 'outline',
37
+ opacity: 'opacity',
38
+ flex: 'flex',
39
+ display: 'display',
40
+ } as const satisfies Record<string, string>;
41
+
42
+ export type StyleShorthandKey = keyof typeof SHORTHAND_MAP;
43
+
44
+ const SHORTHAND_SET = new Set<string>(Object.keys(SHORTHAND_MAP));
45
+
46
+ export function isStyleKey(key: string): boolean {
47
+ return key.startsWith('--') || SHORTHAND_SET.has(key);
48
+ }
49
+
50
+ export function getStyleProp(key: string): string | undefined {
51
+ if (key.startsWith('--')) return key;
52
+ return key in SHORTHAND_MAP ? SHORTHAND_MAP[key as StyleShorthandKey] : undefined;
53
+ }
54
+
55
+ /** Numero → "Npx", altrimenti stringa. Token "primary" → var(--color-primary) se in tokens. (scale gestito in resolve.) */
56
+ export function toStyleValue(v: unknown, tokens?: Set<string>): string {
57
+ if (v == null) return '';
58
+ if (typeof v === 'number') return `${v}px`;
59
+ const s = String(v);
60
+ if (tokens && tokens.has(s)) return `var(--color-${s})`;
61
+ return s;
62
+ }