@flow.os/style 0.0.1-dev.1771783145 → 0.0.1-dev.1771840262
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/README.md +18 -1
- package/index.ts +29 -15
- package/package.json +6 -1
- package/react.ts +68 -0
- package/resolve.ts +44 -1
- package/server.ts +299 -0
- package/shorthand.ts +34 -0
- package/style-builder/button.ts +41 -0
- package/style-builder/constants.ts +16 -0
- package/style-builder/dom.ts +18 -0
- package/style-builder/index.ts +48 -0
- package/style-builder/panel.ts +69 -0
- package/style-builder/position.ts +25 -0
- package/visual-builder.ts +822 -0
- package/vite-plugin.ts +86 -0
package/README.md
CHANGED
|
@@ -4,6 +4,23 @@ Stili: shorthand, breakpoints, pseudo, resolve, CSS.
|
|
|
4
4
|
|
|
5
5
|
## API (`index.ts`)
|
|
6
6
|
|
|
7
|
-
- `SHORTHAND_MAP`, `isStyleKey`, `getStyleProp`, `toStyleValue`, `StyleShorthandKey`, `VIEWPORT_KEYS`, `PSEUDO_KEYS`, `VIEWPORT_WIDTHS`, `getViewportKeyFromWidth`, `ViewportKey`, `PseudoKey`, `resolveLayer`, `mergeResolved`, `resolvePseudoStyle`, `styleToCssText`, `PlainLayer`, `ResolvedLayer`.
|
|
7
|
+
- `SHORTHAND_MAP`, `CSS_TO_SHORTHAND`, `isStyleKey`, `getStyleProp`, `toStyleValue`, `StyleShorthandKey`, `VIEWPORT_KEYS`, `PSEUDO_KEYS`, `VIEWPORT_WIDTHS`, `getViewportKeyFromWidth`, `ViewportKey`, `PseudoKey`, `resolveLayer`, `mergeResolved`, `resolvePseudoStyle`, `styleToCssText`, `PlainLayer`, `ResolvedLayer`.
|
|
8
|
+
|
|
9
|
+
## Visual Style Builder
|
|
10
|
+
|
|
11
|
+
Builder visivo per sviluppare più velocemente: seleziona un elemento (div, p, span, …) nella pagina e modificalo dal **menu laterale** (colori, padding, margin, dimensioni, flex, gap, rounded, ecc.) in **tempo reale**. Le modifiche vengono applicate subito al DOM; puoi poi **copiare l’oggetto** da incollare nel codice (`s={...}` o styleFlow).
|
|
12
|
+
|
|
13
|
+
- **React**: monta il componente dove serve (es. solo in dev):
|
|
14
|
+
```tsx
|
|
15
|
+
import { VisualStyleBuilder } from '@flow.os/style';
|
|
16
|
+
// in root o layout: <VisualStyleBuilder />
|
|
17
|
+
```
|
|
18
|
+
- **App senza React** (es. Flow client): in dev chiama il mount dopo il load. Richiede `react` e `react-dom` installati (es. devDependencies):
|
|
19
|
+
```ts
|
|
20
|
+
import { initVisualStyleBuilder } from '@flow.os/style';
|
|
21
|
+
if (import.meta.env.DEV) initVisualStyleBuilder();
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Il pannello mostra tag e classi dell’elemento selezionato, gruppi di proprietà (Spacing, Dimensioni, Tipografia e colore, Layout, Bordo e effetto, Posizione) e pulsanti **Copia oggetto** / **Copia s={}** per incollare nel sorgente.
|
|
8
25
|
|
|
9
26
|
Extension Vite/VS Code in `extension/` (progetto separato).
|
package/index.ts
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// For other frameworks for wrapping App
|
|
3
|
+
// <FlowStyle> <App /> </FlowStyle>
|
|
4
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
export { flowStyle as FlowStyle } from './resolve.js';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Visual builder (UI)
|
|
10
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
export { VisualStyleBuilder, initVisualStyleBuilder } from './visual-builder.js';
|
|
12
|
+
export type { VisualStyleBuilderOptions } from './visual-builder.js';
|
|
13
|
+
|
|
14
|
+
// Plugin Vite (node only): usa import('@flow.os/style/vite-plugin').styleFlowServerPlugin()
|
|
15
|
+
|
|
16
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
// Flow Style Builder – sempre esportato da style; pulsante trascinabile + pannello
|
|
18
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
export { initFlowStyleBuilder, flowStyleBuilder } from './style-builder/index.js';
|
|
20
|
+
export type { FlowStyleBuilderOptions, FlowStyleBuilderPosition } from './style-builder/index.js';
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
// For @flow.os/client internal use
|
|
25
|
+
// ───────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
export { resolveLayer, resolvePseudoStyle, styleToCssText } from './resolve.js';
|
|
27
|
+
export { VIEWPORT_KEYS, PSEUDO_KEYS, getViewportKeyFromWidth } from './breakpoints.js';
|
|
28
|
+
export type { PlainLayer } from './resolve.js';
|
|
9
29
|
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';
|
|
30
|
+
export type { StyleShorthandKey } from './shorthand.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flow.os/style",
|
|
3
|
-
"version": "0.0.1-dev.
|
|
3
|
+
"version": "0.0.1-dev.1771840262",
|
|
4
4
|
"license": "PolyForm-Shield-1.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"types": "./index.ts",
|
|
11
11
|
"import": "./index.ts",
|
|
12
12
|
"default": "./index.ts"
|
|
13
|
+
},
|
|
14
|
+
"./vite-plugin": {
|
|
15
|
+
"types": "./vite-plugin.ts",
|
|
16
|
+
"import": "./vite-plugin.ts",
|
|
17
|
+
"default": "./vite-plugin.ts"
|
|
13
18
|
}
|
|
14
19
|
}
|
|
15
20
|
}
|
package/react.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React: use styleFlow or s prop (same as Flow framework).
|
|
3
|
+
* 1. Call initFlowStyle(React) once in entry.
|
|
4
|
+
* 2. Wrap app in <FlowStyleRoot>.
|
|
5
|
+
* 3. Use <div styleFlow={...} /> or <div s={...} />.
|
|
6
|
+
* Peer dependency: react >= 18.
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { useState, useEffect } from 'react';
|
|
10
|
+
import { flowStyle } from './resolve.js';
|
|
11
|
+
import { getViewportKeyFromWidth } from './breakpoints.js';
|
|
12
|
+
import type { ViewportKey } from './breakpoints.js';
|
|
13
|
+
|
|
14
|
+
let currentViewport: ViewportKey =
|
|
15
|
+
typeof window !== 'undefined' ? getViewportKeyFromWidth(window.innerWidth) : 'mob';
|
|
16
|
+
|
|
17
|
+
let patched = false;
|
|
18
|
+
|
|
19
|
+
export type FlowStyleValue = Parameters<typeof flowStyle>[0];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Call once in your entry (e.g. main.tsx). Then use styleFlow or s prop (same as Flow framework).
|
|
23
|
+
*/
|
|
24
|
+
export function initFlowStyle(ReactLib: typeof React): void {
|
|
25
|
+
if (patched) return;
|
|
26
|
+
const orig = ReactLib.createElement;
|
|
27
|
+
(ReactLib as unknown as { createElement: typeof React.createElement }).createElement = function (
|
|
28
|
+
type: unknown,
|
|
29
|
+
config: Record<string, unknown> | null,
|
|
30
|
+
...args: unknown[]
|
|
31
|
+
) {
|
|
32
|
+
const flowValue = config?.styleFlow ?? config?.s;
|
|
33
|
+
if (flowValue != null) {
|
|
34
|
+
const resolved = flowStyle(flowValue as FlowStyleValue, currentViewport);
|
|
35
|
+
const prevStyle = (config!.style as Record<string, string>) ?? {};
|
|
36
|
+
config = { ...config };
|
|
37
|
+
config.className = [config.className, resolved.class].filter(Boolean).join(' ');
|
|
38
|
+
config.style = { ...resolved.style, ...prevStyle };
|
|
39
|
+
delete config.styleFlow;
|
|
40
|
+
delete config.s;
|
|
41
|
+
}
|
|
42
|
+
return (orig as (...a: unknown[]) => React.ReactElement).call(this, type, config, ...args);
|
|
43
|
+
};
|
|
44
|
+
patched = true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Wrap your app so viewport updates on resize; then use styleFlow or s on any element. */
|
|
48
|
+
export function FlowStyleRoot({ children }: { children: React.ReactNode }): React.ReactElement {
|
|
49
|
+
const [viewport, setViewport] = useState<ViewportKey>(() => currentViewport);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
currentViewport = viewport;
|
|
53
|
+
}, [viewport]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (typeof window === 'undefined') return;
|
|
57
|
+
const onResize = () => {
|
|
58
|
+
const next = getViewportKeyFromWidth(window.innerWidth);
|
|
59
|
+
currentViewport = next;
|
|
60
|
+
setViewport(next);
|
|
61
|
+
};
|
|
62
|
+
window.addEventListener('resize', onResize);
|
|
63
|
+
return () => window.removeEventListener('resize', onResize);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
return <>{children}</>;
|
|
67
|
+
}
|
|
68
|
+
|
package/resolve.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isStyleKey, getStyleProp, toStyleValue } from './shorthand.js';
|
|
2
|
-
import { VIEWPORT_KEYS, PSEUDO_KEYS } from './breakpoints.js';
|
|
2
|
+
import { VIEWPORT_KEYS, PSEUDO_KEYS, type ViewportKey } from './breakpoints.js';
|
|
3
3
|
|
|
4
4
|
const DEFAULT_TOKENS = new Set(['primary', 'muted', 'primary-foreground']);
|
|
5
5
|
|
|
@@ -82,3 +82,46 @@ export function styleToCssText(style: Record<string, string>): string {
|
|
|
82
82
|
.map(([k, v]) => `${k}: ${v}`)
|
|
83
83
|
.join('; ');
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
/** Stesso formato di styleFlow (stringa | [base, layer] | layer), ma solo valori plain (no getter). */
|
|
87
|
+
type FlowStyleValue =
|
|
88
|
+
| string
|
|
89
|
+
| [string, PlainLayer]
|
|
90
|
+
| (PlainLayer & { base?: string; mob?: PlainLayer; tab?: PlainLayer; des?: PlainLayer });
|
|
91
|
+
|
|
92
|
+
/** Normalizza value in base + layer per un viewport. Usabile da React/altro: flowStyle(value, viewport) → { class, style }. */
|
|
93
|
+
export function flowStyle(
|
|
94
|
+
value: FlowStyleValue | null | undefined,
|
|
95
|
+
viewport?: ViewportKey,
|
|
96
|
+
tokens?: Set<string>
|
|
97
|
+
): ResolvedLayer {
|
|
98
|
+
if (value == null) return { class: '', style: {} };
|
|
99
|
+
let base = '';
|
|
100
|
+
let layer: PlainLayer = {};
|
|
101
|
+
if (typeof value === 'string') {
|
|
102
|
+
base = value;
|
|
103
|
+
} else if (Array.isArray(value)) {
|
|
104
|
+
base = typeof value[0] === 'string' ? value[0] : '';
|
|
105
|
+
layer = (value[1] && typeof value[1] === 'object' && !Array.isArray(value[1])) ? (value[1] as PlainLayer) : {};
|
|
106
|
+
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
107
|
+
layer = { ...value };
|
|
108
|
+
base = typeof layer.base === 'string' ? layer.base : '';
|
|
109
|
+
}
|
|
110
|
+
const merged: PlainLayer = { base };
|
|
111
|
+
for (const [k, v] of Object.entries(layer)) {
|
|
112
|
+
if (k === 'base' || isReserved(k)) continue;
|
|
113
|
+
(merged as Record<string, unknown>)[k] = v;
|
|
114
|
+
}
|
|
115
|
+
if (viewport) {
|
|
116
|
+
const vp = layer[viewport];
|
|
117
|
+
if (vp && typeof vp === 'object' && !Array.isArray(vp)) {
|
|
118
|
+
const vpPlain = vp as PlainLayer;
|
|
119
|
+
if (vpPlain.base) merged.base = (merged.base ? `${merged.base} ` : '') + (vpPlain.base ?? '');
|
|
120
|
+
for (const [k, v] of Object.entries(vpPlain)) {
|
|
121
|
+
if (k === 'base' || isReserved(k)) continue;
|
|
122
|
+
(merged as Record<string, unknown>)[k] = v;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return resolveLayer(merged, tokens);
|
|
127
|
+
}
|
package/server.ts
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server per il Visual Style Builder: espone POST /__flow-style-patch per modificare
|
|
3
|
+
* i file sorgente (s= / styleFlow=). Avviabile da CLI o dal plugin Vite.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import http from 'node:http';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_PORT = 3757;
|
|
10
|
+
|
|
11
|
+
export type StyleFlowServerOptions = {
|
|
12
|
+
port?: number;
|
|
13
|
+
root?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Trova la posizione della } che chiude la { aperta a openIdx (con nesting). */
|
|
17
|
+
function findMatchingClose(content: string, openIdx: number): number {
|
|
18
|
+
let depth = 0;
|
|
19
|
+
for (let i = openIdx; i < content.length; i++) {
|
|
20
|
+
const c = content[i];
|
|
21
|
+
if (c === '{') depth++;
|
|
22
|
+
else if (c === '}') {
|
|
23
|
+
depth--;
|
|
24
|
+
if (depth === 0) return i;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return -1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Trova l’oggetto s= o styleFlow= che contiene la riga (1-based); ritorna [start, end] degli indici. */
|
|
31
|
+
/** Trova tutti i blocchi s= o styleFlow= nel file; ritorna [{ line }] riga 1-based. */
|
|
32
|
+
function findAllStyleBlocks(content: string): { line: number }[] {
|
|
33
|
+
const blocks: { line: number }[] = [];
|
|
34
|
+
const re = /\s(s|styleFlow)\s*=\s*\{/g;
|
|
35
|
+
let m: RegExpExecArray | null;
|
|
36
|
+
while ((m = re.exec(content)) !== null) {
|
|
37
|
+
const line = content.slice(0, m.index).split(/\r?\n/).length;
|
|
38
|
+
const openIdx = content.indexOf('{', m.index);
|
|
39
|
+
if (findMatchingClose(content, openIdx) >= 0) blocks.push({ line });
|
|
40
|
+
}
|
|
41
|
+
return blocks;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const SHORTHAND_KEYS = [
|
|
45
|
+
'p', 'pt', 'pr', 'pb', 'pl', 'm', 'mt', 'mr', 'mb', 'ml',
|
|
46
|
+
'w', 'h', 'minW', 'minH', 'maxW', 'maxH', 'gap', 'rounded', 'text', 'color', 'bg',
|
|
47
|
+
'display', 'flex', 'opacity', 'top', 'left', 'right', 'bottom',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/** Estrae valori semplici dall inner per le chiavi note. */
|
|
51
|
+
function parseStyleInner(inner: string): Record<string, number | string> {
|
|
52
|
+
const out: Record<string, number | string> = {};
|
|
53
|
+
for (const key of SHORTHAND_KEYS) {
|
|
54
|
+
const re = new RegExp(`(?:^|[,\\n])\\s*(?:${key}|"${key}")\\s*:\\s*((-?\\d+(?:\\.\\d+)?)|"([^"]*)"|'([^']*)')`, 'g');
|
|
55
|
+
const m = re.exec(inner);
|
|
56
|
+
if (!m) continue;
|
|
57
|
+
if (m[2] !== undefined) out[key] = parseFloat(m[2]);
|
|
58
|
+
else if (m[3] !== undefined) out[key] = m[3];
|
|
59
|
+
else if (m[4] !== undefined) out[key] = m[4];
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function findStyleObjectRange(content: string, line: number): [number, number] | null {
|
|
65
|
+
const lines = content.split(/\r?\n/);
|
|
66
|
+
const lineIdx = Math.max(0, line - 1);
|
|
67
|
+
const propNames = ['s=', 'styleFlow='];
|
|
68
|
+
let searchLineIdx = lineIdx;
|
|
69
|
+
while (searchLineIdx >= 0) {
|
|
70
|
+
const row = lines[searchLineIdx] ?? '';
|
|
71
|
+
for (const name of propNames) {
|
|
72
|
+
const idx = row.indexOf(name);
|
|
73
|
+
if (idx !== -1) {
|
|
74
|
+
const openBrace = row.indexOf('{', idx);
|
|
75
|
+
if (openBrace !== -1) {
|
|
76
|
+
const globalOpen = lines.slice(0, searchLineIdx).join('\n').length + openBrace;
|
|
77
|
+
const closeIdx = findMatchingClose(content, globalOpen);
|
|
78
|
+
if (closeIdx !== -1) return [globalOpen, closeIdx];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
searchLineIdx--;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function findValueRange(inner: string, key: string): [number, number] | null {
|
|
88
|
+
const reKey = new RegExp(`(?:^|[,\\n])\\s*(?:${key}|"${key}")\\s*:\\s*`, 'g');
|
|
89
|
+
const m = reKey.exec(inner);
|
|
90
|
+
if (!m) return null;
|
|
91
|
+
const valueStart = m.index + m[0].length;
|
|
92
|
+
let i = valueStart;
|
|
93
|
+
const ch = inner[i];
|
|
94
|
+
if (ch === '"' || ch === "'") {
|
|
95
|
+
const q = ch;
|
|
96
|
+
i++;
|
|
97
|
+
while (i < inner.length) {
|
|
98
|
+
if (inner[i] === '\\') i += 2;
|
|
99
|
+
else if (inner[i] === q) return [valueStart, i + 1];
|
|
100
|
+
else i++;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (ch === '{' || ch === '[') {
|
|
105
|
+
const close = findMatchingClose(inner, i);
|
|
106
|
+
return close >= 0 ? [valueStart, close + 1] : null;
|
|
107
|
+
}
|
|
108
|
+
while (i < inner.length && /[0-9.eE+-]/.test(inner[i] ?? '')) i++;
|
|
109
|
+
while (i < inner.length && /[\s)]/.test(inner[i] ?? '')) i++;
|
|
110
|
+
if (i < inner.length && inner[i] === ')') i++;
|
|
111
|
+
return [valueStart, i];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Merge nel file: aggiorna solo le chiavi in style nell’oggetto esistente (s= o styleFlow=). */
|
|
115
|
+
function getStyleObjectFromInner(fullInner: string): { inner: string; patchStart: number; patchEnd: number } {
|
|
116
|
+
const trimmed = fullInner.trim();
|
|
117
|
+
if (!trimmed.startsWith('[')) {
|
|
118
|
+
return { inner: fullInner, patchStart: 0, patchEnd: fullInner.length };
|
|
119
|
+
}
|
|
120
|
+
const firstBrace = fullInner.indexOf('{');
|
|
121
|
+
if (firstBrace === -1) return { inner: fullInner, patchStart: 0, patchEnd: fullInner.length };
|
|
122
|
+
const closeBrace = findMatchingClose(fullInner, firstBrace);
|
|
123
|
+
if (closeBrace === -1) return { inner: fullInner, patchStart: 0, patchEnd: fullInner.length };
|
|
124
|
+
return {
|
|
125
|
+
inner: fullInner.slice(firstBrace + 1, closeBrace),
|
|
126
|
+
patchStart: firstBrace + 1,
|
|
127
|
+
patchEnd: closeBrace,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function patchStyleInFile(
|
|
132
|
+
content: string,
|
|
133
|
+
line: number,
|
|
134
|
+
style: Record<string, number | string>
|
|
135
|
+
): string {
|
|
136
|
+
const range = findStyleObjectRange(content, line);
|
|
137
|
+
if (!range) return content;
|
|
138
|
+
const [openIdx, closeIdx] = range;
|
|
139
|
+
const fullInner = content.slice(openIdx + 1, closeIdx);
|
|
140
|
+
const { inner: innerSlice, patchStart, patchEnd } = getStyleObjectFromInner(fullInner);
|
|
141
|
+
let inner = innerSlice;
|
|
142
|
+
for (const [key, value] of Object.entries(style)) {
|
|
143
|
+
const serialized = typeof value === 'number' ? String(value) : JSON.stringify(value);
|
|
144
|
+
const valRange = findValueRange(inner, key);
|
|
145
|
+
if (valRange) {
|
|
146
|
+
const [vs, ve] = valRange;
|
|
147
|
+
inner = inner.slice(0, vs) + serialized + inner.slice(ve);
|
|
148
|
+
} else {
|
|
149
|
+
const trim = inner.trimEnd();
|
|
150
|
+
inner = trim + (trim.endsWith(',') ? '' : ',') + `\n ${key}: ${serialized}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return (
|
|
154
|
+
content.slice(0, openIdx + 1 + patchStart) +
|
|
155
|
+
inner +
|
|
156
|
+
content.slice(openIdx + 1 + patchEnd)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function createServer(options: StyleFlowServerOptions = {}) {
|
|
161
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
162
|
+
const root = path.resolve(options.root ?? process.cwd());
|
|
163
|
+
|
|
164
|
+
const server = http.createServer((req, res) => {
|
|
165
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
166
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
167
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
168
|
+
|
|
169
|
+
if (req.method === 'OPTIONS') {
|
|
170
|
+
res.writeHead(204);
|
|
171
|
+
res.end();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const url = req.url ?? '';
|
|
176
|
+
const urlObj = url.startsWith('/') ? new URL(url, 'http://localhost') : null;
|
|
177
|
+
const pathname = urlObj?.pathname ?? url.split('?')[0];
|
|
178
|
+
const fileParam = urlObj?.searchParams.get('file');
|
|
179
|
+
const lineParam = urlObj?.searchParams.get('line');
|
|
180
|
+
|
|
181
|
+
if (req.method === 'GET' && pathname === '/__flow-style-blocks' && fileParam) {
|
|
182
|
+
try {
|
|
183
|
+
const file = fileParam.trim();
|
|
184
|
+
const safePath = path.normalize(file).replace(/^(\.\.(\/|\\|$))+/, '');
|
|
185
|
+
const absPath = path.join(root, safePath);
|
|
186
|
+
if (!absPath.startsWith(root)) {
|
|
187
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
188
|
+
res.end(JSON.stringify({ error: 'Invalid path' }));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const content = fs.readFileSync(absPath, 'utf8');
|
|
192
|
+
const blocks = findAllStyleBlocks(content);
|
|
193
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
194
|
+
res.end(JSON.stringify({ blocks }));
|
|
195
|
+
} catch (err) {
|
|
196
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
197
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : 'Unknown error' }));
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (req.method === 'GET' && pathname === '/__flow-style-read' && fileParam && lineParam) {
|
|
203
|
+
try {
|
|
204
|
+
const file = fileParam.trim();
|
|
205
|
+
const line = parseInt(lineParam, 10);
|
|
206
|
+
const safePath = path.normalize(file).replace(/^(\.\.(\/|\\|$))+/, '');
|
|
207
|
+
const absPath = path.join(root, safePath);
|
|
208
|
+
if (!absPath.startsWith(root)) {
|
|
209
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
210
|
+
res.end(JSON.stringify({ error: 'Invalid path' }));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const content = fs.readFileSync(absPath, 'utf8');
|
|
214
|
+
const range = findStyleObjectRange(content, line);
|
|
215
|
+
if (!range) {
|
|
216
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
217
|
+
res.end(JSON.stringify({ error: 'Block not found' }));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const fullInner = content.slice(range[0] + 1, range[1]);
|
|
221
|
+
const { inner } = getStyleObjectFromInner(fullInner);
|
|
222
|
+
const style = parseStyleInner(inner);
|
|
223
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
224
|
+
res.end(JSON.stringify({ style }));
|
|
225
|
+
} catch (err) {
|
|
226
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
227
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : 'Unknown error' }));
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (req.method !== 'POST' || pathname !== '/__flow-style-patch') {
|
|
233
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
234
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let body = '';
|
|
239
|
+
req.on('data', (chunk) => (body += chunk));
|
|
240
|
+
req.on('end', () => {
|
|
241
|
+
try {
|
|
242
|
+
const { file, line, style } = JSON.parse(body) as {
|
|
243
|
+
file?: string;
|
|
244
|
+
line?: number;
|
|
245
|
+
style?: Record<string, number | string>;
|
|
246
|
+
};
|
|
247
|
+
if (!file || typeof line !== 'number' || !style || typeof style !== 'object') {
|
|
248
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
249
|
+
res.end(JSON.stringify({ error: 'file, line, style required' }));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const safePath = path.normalize(file).replace(/^(\.\.(\/|\\|$))+/, '');
|
|
253
|
+
const absPath = path.join(root, safePath);
|
|
254
|
+
if (!absPath.startsWith(root)) {
|
|
255
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
256
|
+
res.end(JSON.stringify({ error: 'Invalid path' }));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const content = fs.readFileSync(absPath, 'utf8');
|
|
260
|
+
const next = patchStyleInFile(content, line, style);
|
|
261
|
+
fs.writeFileSync(absPath, next, 'utf8');
|
|
262
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
263
|
+
res.end(JSON.stringify({ ok: true }));
|
|
264
|
+
} catch (err) {
|
|
265
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
266
|
+
res.end(
|
|
267
|
+
JSON.stringify({
|
|
268
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return { server, port };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Avvia il server di patch per il Style Builder.
|
|
280
|
+
* Chiamabile da script o dal plugin Vite.
|
|
281
|
+
*/
|
|
282
|
+
export function styleFlowServer(options: StyleFlowServerOptions = {}): void {
|
|
283
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
284
|
+
const root = options.root ?? process.cwd();
|
|
285
|
+
const { server } = createServer({ port, root });
|
|
286
|
+
server.listen(port, '127.0.0.1', () => {
|
|
287
|
+
console.log(`[flow-style] Patch server http://127.0.0.1:${port} (root: ${root})`);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Per uso interno del plugin Vite: ritorna l'istanza e la porta. */
|
|
292
|
+
export function createStyleFlowServer(options: StyleFlowServerOptions = {}): {
|
|
293
|
+
server: http.Server;
|
|
294
|
+
port: number;
|
|
295
|
+
} {
|
|
296
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
297
|
+
const { server } = createServer({ ...options, port });
|
|
298
|
+
return { server, port };
|
|
299
|
+
}
|
package/shorthand.ts
CHANGED
|
@@ -52,6 +52,40 @@ export function getStyleProp(key: string): string | undefined {
|
|
|
52
52
|
return key in SHORTHAND_MAP ? SHORTHAND_MAP[key as StyleShorthandKey] : undefined;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/** CSS property → shorthand (per visual builder: leggere stile da DOM e copiare come styleFlow). */
|
|
56
|
+
export const CSS_TO_SHORTHAND: Record<string, string> = {
|
|
57
|
+
fontSize: 'text',
|
|
58
|
+
marginBottom: 'mb',
|
|
59
|
+
marginTop: 'mt',
|
|
60
|
+
marginLeft: 'ml',
|
|
61
|
+
marginRight: 'mr',
|
|
62
|
+
margin: 'm',
|
|
63
|
+
paddingBottom: 'pb',
|
|
64
|
+
paddingTop: 'pt',
|
|
65
|
+
paddingLeft: 'pl',
|
|
66
|
+
paddingRight: 'pr',
|
|
67
|
+
padding: 'p',
|
|
68
|
+
width: 'w',
|
|
69
|
+
height: 'h',
|
|
70
|
+
minWidth: 'minW',
|
|
71
|
+
minHeight: 'minH',
|
|
72
|
+
maxWidth: 'maxW',
|
|
73
|
+
maxHeight: 'maxH',
|
|
74
|
+
gap: 'gap',
|
|
75
|
+
borderRadius: 'rounded',
|
|
76
|
+
top: 'top',
|
|
77
|
+
left: 'left',
|
|
78
|
+
right: 'right',
|
|
79
|
+
bottom: 'bottom',
|
|
80
|
+
color: 'color',
|
|
81
|
+
background: 'bg',
|
|
82
|
+
transform: 'scale',
|
|
83
|
+
outline: 'outline',
|
|
84
|
+
opacity: 'opacity',
|
|
85
|
+
flex: 'flex',
|
|
86
|
+
display: 'display',
|
|
87
|
+
};
|
|
88
|
+
|
|
55
89
|
/** Numero → "Npx", altrimenti stringa. Token "primary" → var(--color-primary) se in tokens. (scale gestito in resolve.) */
|
|
56
90
|
export function toStyleValue(v: unknown, tokens?: Set<string>): string {
|
|
57
91
|
if (v == null) return '';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Pulsante tondo del Flow Style Builder. */
|
|
2
|
+
|
|
3
|
+
import { createEl } from './dom.js';
|
|
4
|
+
import { BUTTON_ID, BUTTON_SIZE, THEME } from './constants.js';
|
|
5
|
+
import { cornerToStyles } from './position.js';
|
|
6
|
+
import type { FlowStyleBuilderPosition } from './position.js';
|
|
7
|
+
|
|
8
|
+
export function createButton(
|
|
9
|
+
corner: FlowStyleBuilderPosition,
|
|
10
|
+
onClick: () => void
|
|
11
|
+
): HTMLButtonElement {
|
|
12
|
+
const size = `${BUTTON_SIZE}px`;
|
|
13
|
+
const btn = createEl('button', {
|
|
14
|
+
position: 'fixed',
|
|
15
|
+
width: size,
|
|
16
|
+
height: size,
|
|
17
|
+
minWidth: size,
|
|
18
|
+
minHeight: size,
|
|
19
|
+
maxWidth: size,
|
|
20
|
+
maxHeight: size,
|
|
21
|
+
boxSizing: 'border-box',
|
|
22
|
+
borderRadius: '50%',
|
|
23
|
+
border: `2px solid ${THEME.border}`,
|
|
24
|
+
background: THEME.surface,
|
|
25
|
+
color: THEME.accent,
|
|
26
|
+
cursor: 'pointer',
|
|
27
|
+
display: 'flex',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
justifyContent: 'center',
|
|
30
|
+
flexShrink: '0',
|
|
31
|
+
zIndex: '2147483647',
|
|
32
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
|
|
33
|
+
}) as HTMLButtonElement;
|
|
34
|
+
btn.id = BUTTON_ID;
|
|
35
|
+
btn.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2"/></svg>';
|
|
36
|
+
btn.title = 'Flow Style Builder';
|
|
37
|
+
|
|
38
|
+
Object.assign(btn.style, cornerToStyles(corner));
|
|
39
|
+
btn.addEventListener('click', onClick);
|
|
40
|
+
return btn;
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Costanti UI del Flow Style Builder. */
|
|
2
|
+
|
|
3
|
+
export const BUTTON_ID = 'flowStyleBuilder';
|
|
4
|
+
export const PANEL_ID = 'flowStyleBuilderPanel';
|
|
5
|
+
export const MARGIN = 16;
|
|
6
|
+
export const BUTTON_SIZE = 48;
|
|
7
|
+
export const PANEL_WIDTH = 300;
|
|
8
|
+
|
|
9
|
+
export const THEME = {
|
|
10
|
+
bg: '#0f1419',
|
|
11
|
+
surface: '#1a1f26',
|
|
12
|
+
border: '#2d3748',
|
|
13
|
+
accent: '#14b8a6',
|
|
14
|
+
text: '#e2e8f0',
|
|
15
|
+
muted: '#94a3b8',
|
|
16
|
+
} as const;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Helper DOM per il Flow Style Builder. */
|
|
2
|
+
|
|
3
|
+
export function setStyles(el: HTMLElement, s: Partial<CSSStyleDeclaration>): void {
|
|
4
|
+
for (const [k, v] of Object.entries(s)) {
|
|
5
|
+
if (v != null && typeof v === 'string') (el.style as unknown as Record<string, string>)[k] = v;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createEl<K extends keyof HTMLElementTagNameMap>(
|
|
10
|
+
tag: K,
|
|
11
|
+
style?: Partial<CSSStyleDeclaration>,
|
|
12
|
+
attrs?: Record<string, string>
|
|
13
|
+
): HTMLElementTagNameMap[K] {
|
|
14
|
+
const el = document.createElement(tag);
|
|
15
|
+
if (style) setStyles(el as unknown as HTMLElement, style);
|
|
16
|
+
if (attrs) for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
|
|
17
|
+
return el;
|
|
18
|
+
}
|