create-flow-os 0.0.1-dev.1771785969 → 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 +1 -1
- package/package.json +1 -1
- package/packages/client/client/root.tsx +1 -8
- package/packages/client/client/routes/index.tsx +8 -7
- package/profiles/client/bun.lock +30 -13
- package/profiles/client/client/root.tsx +1 -8
- package/profiles/client/client/routes/about.tsx +7 -0
- package/profiles/client/client/routes/index.tsx +8 -7
- package/profiles/client/flow.config.ts +4 -1
- package/profiles/client/package.json +3 -0
- package/profiles/client/packages/client/build/config.ts +67 -22
- package/profiles/client/packages/client/build/vite.ts +52 -8
- package/profiles/client/packages/client/features/attrs.ts +1 -1
- package/profiles/client/packages/client/index.ts +2 -1
- package/profiles/client/packages/client/package.json +3 -3
- package/profiles/client/packages/client/runtime/jsx-types.d.ts +2 -0
- package/profiles/client/packages/client/runtime/jsx.ts +4 -2
- package/profiles/client/packages/router/index.ts +14 -168
- package/profiles/client/packages/router/matcher.ts +105 -0
- package/profiles/client/packages/router/router.ts +185 -0
- package/profiles/client/packages/router/types.ts +34 -0
- package/profiles/client/packages/router/utils.ts +45 -0
- package/profiles/client/packages/style/index.ts +29 -15
- package/profiles/client/packages/style/package.json +6 -1
- package/profiles/client/packages/style/react.ts +68 -0
- package/profiles/client/packages/style/resolve.ts +44 -1
- package/profiles/client/packages/style/server.ts +299 -0
- package/profiles/client/packages/style/shorthand.ts +34 -0
- package/profiles/client/packages/style/style-builder/button.ts +41 -0
- package/profiles/client/packages/style/style-builder/constants.ts +16 -0
- package/profiles/client/packages/style/style-builder/dom.ts +18 -0
- package/profiles/client/packages/style/style-builder/index.ts +48 -0
- package/profiles/client/packages/style/style-builder/panel.ts +69 -0
- package/profiles/client/packages/style/style-builder/position.ts +25 -0
- package/profiles/client/packages/style/visual-builder.ts +822 -0
- package/profiles/client/packages/style/vite-plugin.ts +86 -0
- package/profiles/full/bun.lock +30 -13
- package/profiles/full/client/root.tsx +1 -8
- package/profiles/full/client/routes/about.tsx +7 -0
- package/profiles/full/client/routes/index.tsx +8 -7
- package/profiles/full/flow.config.ts +2 -4
- package/profiles/full/package.json +3 -0
- package/profiles/full/packages/client/build/config.ts +67 -22
- package/profiles/full/packages/client/build/vite.ts +52 -8
- package/profiles/full/packages/client/features/attrs.ts +1 -1
- package/profiles/full/packages/client/index.ts +2 -1
- package/profiles/full/packages/client/package.json +3 -3
- package/profiles/full/packages/client/runtime/jsx-types.d.ts +2 -0
- package/profiles/full/packages/client/runtime/jsx.ts +4 -2
- package/profiles/full/packages/router/index.ts +14 -168
- package/profiles/full/packages/router/matcher.ts +105 -0
- package/profiles/full/packages/router/router.ts +185 -0
- package/profiles/full/packages/router/types.ts +34 -0
- package/profiles/full/packages/router/utils.ts +45 -0
- package/profiles/full/packages/style/index.ts +29 -15
- package/profiles/full/packages/style/package.json +6 -1
- package/profiles/full/packages/style/react.ts +68 -0
- package/profiles/full/packages/style/resolve.ts +44 -1
- package/profiles/full/packages/style/server.ts +299 -0
- package/profiles/full/packages/style/shorthand.ts +34 -0
- package/profiles/full/packages/style/style-builder/button.ts +41 -0
- package/profiles/full/packages/style/style-builder/constants.ts +16 -0
- package/profiles/full/packages/style/style-builder/dom.ts +18 -0
- package/profiles/full/packages/style/style-builder/index.ts +48 -0
- package/profiles/full/packages/style/style-builder/panel.ts +69 -0
- package/profiles/full/packages/style/style-builder/position.ts +25 -0
- package/profiles/full/packages/style/visual-builder.ts +822 -0
- package/profiles/full/packages/style/vite-plugin.ts +86 -0
- package/profiles/server/bun.lock +30 -13
- package/profiles/server/flow.config.ts +4 -3
- package/profiles/server/package.json +3 -0
- package/profiles/server/packages/router/index.ts +14 -168
- package/profiles/server/packages/router/matcher.ts +105 -0
- package/profiles/server/packages/router/router.ts +185 -0
- package/profiles/server/packages/router/types.ts +34 -0
- package/profiles/server/packages/router/utils.ts +45 -0
- package/profiles/server/packages/style/index.ts +29 -15
- package/profiles/server/packages/style/package.json +6 -1
- package/profiles/server/packages/style/react.ts +68 -0
- package/profiles/server/packages/style/resolve.ts +44 -1
- package/profiles/server/packages/style/server.ts +299 -0
- package/profiles/server/packages/style/shorthand.ts +34 -0
- package/profiles/server/packages/style/style-builder/button.ts +41 -0
- package/profiles/server/packages/style/style-builder/constants.ts +16 -0
- package/profiles/server/packages/style/style-builder/dom.ts +18 -0
- package/profiles/server/packages/style/style-builder/index.ts +48 -0
- package/profiles/server/packages/style/style-builder/panel.ts +69 -0
- package/profiles/server/packages/style/style-builder/position.ts +25 -0
- package/profiles/server/packages/style/visual-builder.ts +822 -0
- package/profiles/server/packages/style/vite-plugin.ts +86 -0
- package/templates/flow.config.client.ts +4 -1
- package/templates/flow.config.full.ts +6 -0
- package/templates/flow.config.server.ts +4 -3
|
@@ -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
|
+
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow Style Builder – pulsante tondo fisso in un angolo,
|
|
3
|
+
* al click apre il pannello laterale (sx o dx in base alla posizione).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BUTTON_ID, PANEL_ID } from './constants.js';
|
|
7
|
+
import { createPanel } from './panel.js';
|
|
8
|
+
import { createButton } from './button.js';
|
|
9
|
+
import { flowStyleBuilder } from './position.js';
|
|
10
|
+
import type { FlowStyleBuilderPosition } from './position.js';
|
|
11
|
+
|
|
12
|
+
export type { FlowStyleBuilderPosition };
|
|
13
|
+
export { flowStyleBuilder };
|
|
14
|
+
|
|
15
|
+
export type FlowStyleBuilderOptions = {
|
|
16
|
+
/** Iframe che mostra l'app (stesso origin). */
|
|
17
|
+
previewIframe?: HTMLIFrameElement;
|
|
18
|
+
/** Posizione del pulsante (obbligatoria da flow.config). */
|
|
19
|
+
position: FlowStyleBuilderPosition;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Inizializza il Flow Style Builder: pulsante fisso in angolo + pannello laterale al click. */
|
|
23
|
+
export function initFlowStyleBuilder(options: FlowStyleBuilderOptions): void {
|
|
24
|
+
if (document.getElementById(BUTTON_ID) || document.getElementById(PANEL_ID)) return;
|
|
25
|
+
|
|
26
|
+
const corner = options.position;
|
|
27
|
+
let panelOpen = false;
|
|
28
|
+
const panelRight = corner === 'top-right' || corner === 'bottom-right';
|
|
29
|
+
|
|
30
|
+
const { element: panel, open: openPanel, close: closePanel } = createPanel(panelRight, () => {
|
|
31
|
+
panelOpen = false;
|
|
32
|
+
closePanel();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
document.body.appendChild(panel);
|
|
36
|
+
|
|
37
|
+
const btn = createButton(corner, () => {
|
|
38
|
+
if (panelOpen) {
|
|
39
|
+
panelOpen = false;
|
|
40
|
+
closePanel();
|
|
41
|
+
} else {
|
|
42
|
+
panelOpen = true;
|
|
43
|
+
openPanel();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
document.body.appendChild(btn);
|
|
48
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Pannello laterale del Flow Style Builder. */
|
|
2
|
+
|
|
3
|
+
import { createEl } from './dom.js';
|
|
4
|
+
import { PANEL_ID, PANEL_WIDTH, THEME } from './constants.js';
|
|
5
|
+
|
|
6
|
+
export function createPanel(initialRight: boolean, onClose: () => void): {
|
|
7
|
+
element: HTMLDivElement;
|
|
8
|
+
open: () => void;
|
|
9
|
+
close: () => void;
|
|
10
|
+
} {
|
|
11
|
+
const panel = createEl('div', {
|
|
12
|
+
position: 'fixed',
|
|
13
|
+
top: '0',
|
|
14
|
+
width: `${PANEL_WIDTH}px`,
|
|
15
|
+
height: '100vh',
|
|
16
|
+
background: THEME.bg,
|
|
17
|
+
color: THEME.text,
|
|
18
|
+
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
19
|
+
fontSize: '12px',
|
|
20
|
+
boxShadow: '0 0 24px rgba(0,0,0,0.4)',
|
|
21
|
+
display: 'none',
|
|
22
|
+
flexDirection: 'column',
|
|
23
|
+
zIndex: '2147483646',
|
|
24
|
+
overflow: 'hidden',
|
|
25
|
+
});
|
|
26
|
+
panel.id = PANEL_ID;
|
|
27
|
+
|
|
28
|
+
function open(): void {
|
|
29
|
+
const s = panel.style as unknown as Record<string, string>;
|
|
30
|
+
s['right'] = initialRight ? '0' : '';
|
|
31
|
+
s['left'] = initialRight ? '' : '0';
|
|
32
|
+
panel.style.display = 'flex';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function close(): void {
|
|
36
|
+
panel.style.display = 'none';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const header = createEl('div', {
|
|
40
|
+
padding: '10px 12px',
|
|
41
|
+
borderBottom: `1px solid ${THEME.border}`,
|
|
42
|
+
display: 'flex',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
justifyContent: 'space-between',
|
|
45
|
+
background: THEME.surface,
|
|
46
|
+
});
|
|
47
|
+
const title = createEl('span', { fontWeight: '600', fontSize: '13px' });
|
|
48
|
+
title.textContent = 'Flow Style Builder';
|
|
49
|
+
const closeBtn = createEl('button', {
|
|
50
|
+
padding: '6px',
|
|
51
|
+
border: `1px solid ${THEME.border}`,
|
|
52
|
+
borderRadius: '6px',
|
|
53
|
+
background: 'transparent',
|
|
54
|
+
color: THEME.muted,
|
|
55
|
+
cursor: 'pointer',
|
|
56
|
+
}) as HTMLButtonElement;
|
|
57
|
+
closeBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"/></svg>';
|
|
58
|
+
closeBtn.title = 'Chiudi';
|
|
59
|
+
closeBtn.addEventListener('click', onClose);
|
|
60
|
+
header.appendChild(title);
|
|
61
|
+
header.appendChild(closeBtn);
|
|
62
|
+
panel.appendChild(header);
|
|
63
|
+
|
|
64
|
+
const body = createEl('div', { flex: '1', overflow: 'auto', padding: '12px', color: THEME.muted });
|
|
65
|
+
body.textContent = 'Contenuto da aggiungere.';
|
|
66
|
+
panel.appendChild(body);
|
|
67
|
+
|
|
68
|
+
return { element: panel, open, close };
|
|
69
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Posizione del pulsante: uno dei quattro angoli. */
|
|
2
|
+
|
|
3
|
+
import { MARGIN } from './constants.js';
|
|
4
|
+
|
|
5
|
+
export type FlowStyleBuilderPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
6
|
+
|
|
7
|
+
/** Passa la posizione (es. flowStyleBuilder: flowStyleBuilder('bottom-right') in flow.config). */
|
|
8
|
+
export function flowStyleBuilder(position: FlowStyleBuilderPosition): FlowStyleBuilderPosition {
|
|
9
|
+
return position;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function cornerToStyles(corner: FlowStyleBuilderPosition): Partial<CSSStyleDeclaration> {
|
|
13
|
+
const m = `${MARGIN}px`;
|
|
14
|
+
switch (corner) {
|
|
15
|
+
case 'top-left':
|
|
16
|
+
return { top: m, left: m, right: '', bottom: '' };
|
|
17
|
+
case 'top-right':
|
|
18
|
+
return { top: m, left: '', right: m, bottom: '' };
|
|
19
|
+
case 'bottom-left':
|
|
20
|
+
return { top: '', left: m, right: '', bottom: m };
|
|
21
|
+
case 'bottom-right':
|
|
22
|
+
default:
|
|
23
|
+
return { top: '', left: '', right: m, bottom: m };
|
|
24
|
+
}
|
|
25
|
+
}
|