@flow.os/client 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/config.ts +73 -0
- package/dom.ts +5 -0
- package/features/attrs.ts +32 -0
- package/features/class-flow.ts +116 -0
- package/features/index.ts +8 -0
- package/features/pseudo-injector.ts +40 -0
- package/features/style-flow.ts +106 -0
- package/features/style.ts +27 -0
- package/features/utils.ts +4 -0
- package/features/viewport.ts +20 -0
- package/index.ts +4 -0
- package/jsx-dev-runtime.ts +1 -0
- package/jsx-runtime.ts +1 -0
- package/jsx-types.d.ts +64 -0
- package/jsx.ts +99 -0
- package/package.json +42 -0
- package/vite.ts +51 -0
package/config.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { Plugin } from 'vite';
|
|
4
|
+
|
|
5
|
+
/** Solo in monorepo (esiste packages/): Rollup in Docker non segue i symlink, quindi alias a path assoluti. Generico: legge packages/ e mappa tutto. Fuori monorepo (npm) ritorna {}. */
|
|
6
|
+
function resolveAlias(): Record<string, string> {
|
|
7
|
+
const root = path.resolve(process.cwd());
|
|
8
|
+
const packagesDir = path.join(root, 'packages');
|
|
9
|
+
if (!fs.existsSync(packagesDir)) return {};
|
|
10
|
+
const alias: Record<string, string> = {};
|
|
11
|
+
const clientJsx = path.join(root, 'packages', 'client', 'jsx-runtime.ts');
|
|
12
|
+
if (fs.existsSync(clientJsx)) {
|
|
13
|
+
alias['@flow.os/client/jsx-runtime'] = clientJsx;
|
|
14
|
+
alias['@flow.os/client/jsx-dev-runtime'] = path.join(root, 'packages', 'client', 'jsx-dev-runtime.ts');
|
|
15
|
+
}
|
|
16
|
+
for (const name of fs.readdirSync(packagesDir)) {
|
|
17
|
+
const dir = path.join(packagesDir, name);
|
|
18
|
+
if (!fs.statSync(dir).isDirectory()) continue;
|
|
19
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
20
|
+
if (!fs.existsSync(pkgPath)) continue;
|
|
21
|
+
const abs = path.resolve(dir);
|
|
22
|
+
try {
|
|
23
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { name?: string };
|
|
24
|
+
if (pkg.name?.startsWith('@flow.os/') || pkg.name === '@flow.os') alias[pkg.name] = abs;
|
|
25
|
+
} catch {}
|
|
26
|
+
if (name !== 'flow.os') alias[`@flow.os/${name}`] = abs;
|
|
27
|
+
}
|
|
28
|
+
return alias;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type FlowConfigOptions = {
|
|
32
|
+
/** Porta del dev server. Default: 3000 */
|
|
33
|
+
port?: number;
|
|
34
|
+
/** Esporre su rete. Default: true */
|
|
35
|
+
host?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Backend in dev: passa flowServer() per usare il server Flow (API da server/routes).
|
|
38
|
+
* Per Go, Rust, Nitro o altro: non passare server e avvia il backend separatamente;
|
|
39
|
+
* opzionalmente aggiungi in plugins un proxy verso la porta del tuo backend.
|
|
40
|
+
*/
|
|
41
|
+
server?: Plugin;
|
|
42
|
+
/** Plugin Vite aggiuntivi. Default: [] */
|
|
43
|
+
plugins?: Plugin[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const DEFAULTS = {
|
|
47
|
+
port: 3000,
|
|
48
|
+
host: true,
|
|
49
|
+
server: undefined as Plugin | undefined,
|
|
50
|
+
plugins: [] as Plugin[],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Config per flow.config.ts. Client + Vite; server opzionale con server: flowServer(). */
|
|
54
|
+
export default function config(
|
|
55
|
+
options: FlowConfigOptions = {}
|
|
56
|
+
): () => Promise<ReturnType<typeof import('vite').defineConfig>> {
|
|
57
|
+
const { port = DEFAULTS.port, host = DEFAULTS.host, server = DEFAULTS.server, plugins = DEFAULTS.plugins } = options;
|
|
58
|
+
return async () => {
|
|
59
|
+
const { flow } = await import('./vite.js');
|
|
60
|
+
const allPlugins = server ? [server, flow(), ...plugins] : [flow(), ...plugins];
|
|
61
|
+
const alias = resolveAlias();
|
|
62
|
+
return {
|
|
63
|
+
resolve: Object.keys(alias).length ? { alias } : {},
|
|
64
|
+
server: { port, host },
|
|
65
|
+
build: {
|
|
66
|
+
rollupOptions: {
|
|
67
|
+
external: ['@flow.os/server', /^@flow.os\/server\/.+/],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
plugins: allPlugins,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
}
|
package/dom.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { effect } from '@flow.os/core';
|
|
2
|
+
import { isGetter } from './utils.js';
|
|
3
|
+
|
|
4
|
+
/** Props gestite altrove (non passare qui). */
|
|
5
|
+
const SKIP = new Set(['children', 'class', 'className', 'classList', 'classFlow', 'style', 'styleFlow']);
|
|
6
|
+
|
|
7
|
+
export function applyAttrs(el: HTMLElement, rest: Record<string, unknown>): void {
|
|
8
|
+
for (const [k, v] of Object.entries(rest)) {
|
|
9
|
+
if (v == null || SKIP.has(k)) continue;
|
|
10
|
+
if (k.startsWith('on') && typeof v === 'function') {
|
|
11
|
+
el.addEventListener(k.slice(2).toLowerCase(), v as EventListener);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (isGetter(v)) {
|
|
15
|
+
effect(() => {
|
|
16
|
+
const x = (v as () => unknown)();
|
|
17
|
+
if (typeof x === 'boolean') {
|
|
18
|
+
if (x) el.setAttribute(k, '');
|
|
19
|
+
else el.removeAttribute(k);
|
|
20
|
+
} else {
|
|
21
|
+
el.setAttribute(k, String(x));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (typeof v === 'boolean') {
|
|
27
|
+
if (v) el.setAttribute(k, '');
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (typeof v === 'string' || typeof v === 'number') el.setAttribute(k, String(v));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { effect } from '@flow.os/core';
|
|
2
|
+
import { isGetter } from './utils.js';
|
|
3
|
+
|
|
4
|
+
export function toStyleValue(v: unknown): string {
|
|
5
|
+
return typeof v === 'number' ? `${v}px` : String(v ?? '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function setStyleProp(el: HTMLElement, key: string, value: string): void {
|
|
9
|
+
if (key.startsWith('--')) el.style.setProperty(key, value);
|
|
10
|
+
else (el.style as unknown as Record<string, string>)[key] = value;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const CLASS_FLOW_MAP: Record<string, string> = {
|
|
14
|
+
text: 'fontSize',
|
|
15
|
+
mb: 'marginBottom',
|
|
16
|
+
mt: 'marginTop',
|
|
17
|
+
ml: 'marginLeft',
|
|
18
|
+
mr: 'marginRight',
|
|
19
|
+
mx: 'marginLeft',
|
|
20
|
+
my: 'marginTop',
|
|
21
|
+
m: 'margin',
|
|
22
|
+
pb: 'paddingBottom',
|
|
23
|
+
pt: 'paddingTop',
|
|
24
|
+
pl: 'paddingLeft',
|
|
25
|
+
pr: 'paddingRight',
|
|
26
|
+
px: 'paddingLeft',
|
|
27
|
+
py: 'paddingTop',
|
|
28
|
+
p: 'padding',
|
|
29
|
+
w: 'width',
|
|
30
|
+
h: 'height',
|
|
31
|
+
minW: 'minWidth',
|
|
32
|
+
minH: 'minHeight',
|
|
33
|
+
maxW: 'maxWidth',
|
|
34
|
+
maxH: 'maxHeight',
|
|
35
|
+
gap: 'gap',
|
|
36
|
+
rounded: 'borderRadius',
|
|
37
|
+
top: 'top',
|
|
38
|
+
left: 'left',
|
|
39
|
+
right: 'right',
|
|
40
|
+
bottom: 'bottom',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function resolveClassFlowKey(k: string): string {
|
|
44
|
+
if (k.startsWith('--')) return k;
|
|
45
|
+
return CLASS_FLOW_MAP[k] ?? k;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isClassFlowReactive(val: unknown): boolean {
|
|
49
|
+
return isGetter(val) || (Array.isArray(val) && val.length === 2 && isGetter(val[0]));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveClassFlowValue(val: unknown): string {
|
|
53
|
+
if (isGetter(val)) return toStyleValue((val as () => unknown)());
|
|
54
|
+
if (Array.isArray(val) && val.length === 2 && isGetter(val[0]))
|
|
55
|
+
return String((val[0] as () => unknown)()) + String(val[1]);
|
|
56
|
+
return toStyleValue(val);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function applyClassFlowObj(el: HTMLElement, obj: Record<string, unknown>): void {
|
|
60
|
+
for (const [k, val] of Object.entries(obj))
|
|
61
|
+
setStyleProp(el, resolveClassFlowKey(k), resolveClassFlowValue(val));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function applyClassFlow(el: HTMLElement, classFlowVal: unknown): void {
|
|
65
|
+
if (classFlowVal == null) return;
|
|
66
|
+
if (isGetter(classFlowVal)) {
|
|
67
|
+
effect(() => applyClassFlowObj(el, (classFlowVal as () => Record<string, unknown>)()));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (typeof classFlowVal !== 'object' || Array.isArray(classFlowVal) || classFlowVal instanceof Node) return;
|
|
71
|
+
const obj = classFlowVal as Record<string, unknown>;
|
|
72
|
+
const hasReactive = Object.values(obj).some(isClassFlowReactive);
|
|
73
|
+
if (hasReactive) {
|
|
74
|
+
effect(() => {
|
|
75
|
+
for (const [k, val] of Object.entries(obj))
|
|
76
|
+
setStyleProp(el, resolveClassFlowKey(k), resolveClassFlowValue(val));
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
applyClassFlowObj(el, obj);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function applyClassAndClassList(
|
|
84
|
+
el: HTMLElement,
|
|
85
|
+
classVal: unknown,
|
|
86
|
+
classNameVal: unknown,
|
|
87
|
+
classListVal: unknown
|
|
88
|
+
): void {
|
|
89
|
+
const base = classVal ?? classNameVal;
|
|
90
|
+
if (base == null && (classListVal == null || typeof classListVal !== 'object')) return;
|
|
91
|
+
effect(() => {
|
|
92
|
+
let baseStr = '';
|
|
93
|
+
if (base != null) {
|
|
94
|
+
if (Array.isArray(base)) {
|
|
95
|
+
baseStr = base
|
|
96
|
+
.map((item) => (isGetter(item) ? String((item as () => unknown)()) : String(item)))
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
.join(' ');
|
|
99
|
+
} else if (isGetter(base)) {
|
|
100
|
+
baseStr = String((base as () => unknown)());
|
|
101
|
+
} else {
|
|
102
|
+
baseStr = String(base);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const list = classListVal as Record<string, boolean | (() => boolean)> | undefined;
|
|
106
|
+
const truthy: string[] = [];
|
|
107
|
+
if (list && typeof list === 'object' && !(list instanceof Node)) {
|
|
108
|
+
for (const [cls, val] of Object.entries(list)) {
|
|
109
|
+
const v = isGetter(val) ? (val as () => boolean)() : val;
|
|
110
|
+
if (v) truthy.push(cls);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const full = [baseStr, ...truthy].filter(Boolean).join(' ');
|
|
114
|
+
el.setAttribute('class', full);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature applicate a div, p, button, ecc.
|
|
3
|
+
* Un file per feature; qui si raggruppano e si espongono a jsx.
|
|
4
|
+
*/
|
|
5
|
+
export { applyClassFlow, applyClassAndClassList } from './class-flow.js';
|
|
6
|
+
export { applyStyle } from './style.js';
|
|
7
|
+
export { applyStyleFlow } from './style-flow.js';
|
|
8
|
+
export { applyAttrs } from './attrs.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { styleToCssText } from '@flow.os/style';
|
|
2
|
+
import type { PseudoKey } from '@flow.os/style';
|
|
3
|
+
|
|
4
|
+
const PSEUDO_SELECTOR: Record<PseudoKey, string> = {
|
|
5
|
+
hover: ':hover',
|
|
6
|
+
active: ':active',
|
|
7
|
+
focus: ':focus',
|
|
8
|
+
focusVisible: ':focus-visible',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let styleEl: HTMLStyleElement | null = null;
|
|
12
|
+
const cache = new Map<string, string>();
|
|
13
|
+
let id = 0;
|
|
14
|
+
|
|
15
|
+
function ensureStyleEl(): HTMLStyleElement {
|
|
16
|
+
if (styleEl) return styleEl;
|
|
17
|
+
styleEl = document.createElement('style');
|
|
18
|
+
styleEl.setAttribute('data-flow-style', '');
|
|
19
|
+
document.head.appendChild(styleEl);
|
|
20
|
+
return styleEl;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Restituisce className da aggiungere all'elemento. La regola .sf-{pseudo}-{id}:{pseudo} { ... } è iniettata una sola volta. */
|
|
24
|
+
export function getPseudoClass(
|
|
25
|
+
pseudo: PseudoKey,
|
|
26
|
+
style: Record<string, string>
|
|
27
|
+
): string {
|
|
28
|
+
const cssText = styleToCssText(style);
|
|
29
|
+
if (!cssText) return '';
|
|
30
|
+
const key = `${pseudo}:${cssText}`;
|
|
31
|
+
let className = cache.get(key);
|
|
32
|
+
if (className) return className;
|
|
33
|
+
className = `sf-${pseudo}-${++id}`;
|
|
34
|
+
cache.set(key, className);
|
|
35
|
+
const selector = `.${className}${PSEUDO_SELECTOR[pseudo]}`;
|
|
36
|
+
const rule = `${selector} { ${cssText} }`;
|
|
37
|
+
const el = ensureStyleEl();
|
|
38
|
+
el.appendChild(document.createTextNode(rule));
|
|
39
|
+
return className;
|
|
40
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { effect } from '@flow.os/core';
|
|
2
|
+
import { isGetter } from './utils.js';
|
|
3
|
+
import {
|
|
4
|
+
VIEWPORT_KEYS,
|
|
5
|
+
PSEUDO_KEYS,
|
|
6
|
+
resolveLayer,
|
|
7
|
+
resolvePseudoStyle,
|
|
8
|
+
type PlainLayer,
|
|
9
|
+
type ViewportKey,
|
|
10
|
+
type PseudoKey,
|
|
11
|
+
} from '@flow.os/style';
|
|
12
|
+
import { getViewportBreakpoint } from './viewport.js';
|
|
13
|
+
import { getPseudoClass } from './pseudo-injector.js';
|
|
14
|
+
|
|
15
|
+
const RESERVED = new Set<string>([...VIEWPORT_KEYS, ...PSEUDO_KEYS]);
|
|
16
|
+
|
|
17
|
+
/** Chiama getter e restituisce oggetto plain (una livello, escluse chiavi reserved). */
|
|
18
|
+
function gatherPlain(obj: Record<string, unknown> | null | undefined): PlainLayer {
|
|
19
|
+
const out: PlainLayer = {};
|
|
20
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return out;
|
|
21
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
22
|
+
if (RESERVED.has(k)) continue;
|
|
23
|
+
if (v === undefined || v === null) continue;
|
|
24
|
+
if (isGetter(v)) {
|
|
25
|
+
const x = (v as () => unknown)();
|
|
26
|
+
if (typeof x === 'boolean') out[k] = x;
|
|
27
|
+
else if (typeof x === 'number' || typeof x === 'string') out[k] = x;
|
|
28
|
+
else if (Array.isArray(x) && x.length === 2 && isGetter(x[0]))
|
|
29
|
+
out[k] = String((x[0] as () => unknown)()) + String(x[1]);
|
|
30
|
+
else out[k] = String(x);
|
|
31
|
+
} else if (typeof v === 'object' && v !== null && !(v instanceof Node)) {
|
|
32
|
+
if (k === 'base') out['base'] = (v as Record<string, string>)['base'] ?? String(v);
|
|
33
|
+
// nested layer (mob/tab/des/hover/...) già esclusi da RESERVED
|
|
34
|
+
} else {
|
|
35
|
+
out[k] = v as string | number | boolean;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Normalizza value in base string + flat object. */
|
|
42
|
+
function normalize(
|
|
43
|
+
value: unknown
|
|
44
|
+
): { base: string; flat: Record<string, unknown> } {
|
|
45
|
+
if (value == null) return { base: '', flat: {} };
|
|
46
|
+
if (typeof value === 'string') return { base: value, flat: {} };
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
const base = typeof value[0] === 'string' ? value[0] : '';
|
|
49
|
+
const flat = (value[1] && typeof value[1] === 'object' && !Array.isArray(value[1]))
|
|
50
|
+
? (value[1] as Record<string, unknown>)
|
|
51
|
+
: {};
|
|
52
|
+
return { base, flat };
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === 'object' && !Array.isArray(value) && !(value instanceof Node)) {
|
|
55
|
+
const flat = value as Record<string, unknown>;
|
|
56
|
+
const base = typeof flat['base'] === 'string' ? flat['base'] : '';
|
|
57
|
+
return { base, flat };
|
|
58
|
+
}
|
|
59
|
+
return { base: '', flat: {} };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function applyStyleFlow(el: HTMLElement, value: unknown): void {
|
|
63
|
+
if (value == null) return;
|
|
64
|
+
const { base, flat } = normalize(value);
|
|
65
|
+
let prevStyleKeys: string[] = [];
|
|
66
|
+
|
|
67
|
+
effect(() => {
|
|
68
|
+
const viewport: ViewportKey = getViewportBreakpoint();
|
|
69
|
+
const plainBase = gatherPlain(flat);
|
|
70
|
+
if (base) plainBase.base = (plainBase.base ? `${plainBase.base} ` : '') + base;
|
|
71
|
+
const viewportLayer = flat[viewport];
|
|
72
|
+
const plainViewport = gatherPlain(
|
|
73
|
+
viewportLayer && typeof viewportLayer === 'object' && !Array.isArray(viewportLayer)
|
|
74
|
+
? (viewportLayer as Record<string, unknown>)
|
|
75
|
+
: {}
|
|
76
|
+
);
|
|
77
|
+
const merged: PlainLayer = { ...plainBase };
|
|
78
|
+
if (plainViewport.base) merged.base = (merged.base ? `${merged.base} ` : '') + plainViewport.base;
|
|
79
|
+
for (const [k, v] of Object.entries(plainViewport)) {
|
|
80
|
+
if (k === 'base') continue;
|
|
81
|
+
(merged as Record<string, unknown>)[k] = v;
|
|
82
|
+
}
|
|
83
|
+
const resolved = resolveLayer(merged);
|
|
84
|
+
|
|
85
|
+
const pseudoClasses: string[] = [];
|
|
86
|
+
for (const pseudo of PSEUDO_KEYS) {
|
|
87
|
+
const rawPseudo = flat[pseudo];
|
|
88
|
+
const plainPseudo = gatherPlain(
|
|
89
|
+
rawPseudo && typeof rawPseudo === 'object' && !Array.isArray(rawPseudo)
|
|
90
|
+
? (rawPseudo as Record<string, unknown>)
|
|
91
|
+
: {}
|
|
92
|
+
);
|
|
93
|
+
const stylePseudo = resolvePseudoStyle(plainPseudo);
|
|
94
|
+
const cls = getPseudoClass(pseudo as PseudoKey, stylePseudo);
|
|
95
|
+
if (cls) pseudoClasses.push(cls);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
el.className = [resolved.class, ...pseudoClasses].filter(Boolean).join(' ');
|
|
99
|
+
for (const k of prevStyleKeys) el.style.removeProperty(k);
|
|
100
|
+
prevStyleKeys = Object.keys(resolved.style);
|
|
101
|
+
for (const [k, v] of Object.entries(resolved.style)) {
|
|
102
|
+
if (k.startsWith('--')) el.style.setProperty(k, v);
|
|
103
|
+
else (el.style as unknown as Record<string, string>)[k] = v;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { effect } from '@flow.os/core';
|
|
2
|
+
import { isGetter } from './utils.js';
|
|
3
|
+
import { setStyleProp, toStyleValue } from './class-flow.js';
|
|
4
|
+
|
|
5
|
+
export function applyStyle(el: HTMLElement, styleVal: unknown): void {
|
|
6
|
+
if (styleVal == null) return;
|
|
7
|
+
if (isGetter(styleVal)) {
|
|
8
|
+
effect(() => {
|
|
9
|
+
const style = (styleVal as () => Record<string, string> | string)();
|
|
10
|
+
if (typeof style === 'string') {
|
|
11
|
+
el.setAttribute('style', style);
|
|
12
|
+
} else if (style && typeof style === 'object') {
|
|
13
|
+
for (const [sk, sv] of Object.entries(style)) setStyleProp(el, sk, toStyleValue(sv));
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (typeof styleVal !== 'object' || styleVal === null || styleVal instanceof Node) return;
|
|
19
|
+
const style = styleVal as Record<string, string | number | (() => string | number)>;
|
|
20
|
+
for (const [sk, sv] of Object.entries(style)) {
|
|
21
|
+
if (isGetter(sv)) {
|
|
22
|
+
effect(() => setStyleProp(el, sk, toStyleValue((sv as () => unknown)())));
|
|
23
|
+
} else {
|
|
24
|
+
setStyleProp(el, sk, toStyleValue(sv));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { state } from '@flow.os/core';
|
|
2
|
+
import type { ViewportKey } from '@flow.os/style';
|
|
3
|
+
import { getViewportKeyFromWidth } from '@flow.os/style';
|
|
4
|
+
|
|
5
|
+
const [getViewport, setViewport] = state<ViewportKey>('mob');
|
|
6
|
+
let subscribed = false;
|
|
7
|
+
|
|
8
|
+
function subscribe(): void {
|
|
9
|
+
if (subscribed) return;
|
|
10
|
+
subscribed = true;
|
|
11
|
+
const update = () => setViewport(getViewportKeyFromWidth(window.innerWidth));
|
|
12
|
+
update();
|
|
13
|
+
window.addEventListener('resize', update);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Breakpoint viewport reattivo (mob/tab/des). Una sola subscription condivisa. */
|
|
17
|
+
export function getViewportBreakpoint(): ViewportKey {
|
|
18
|
+
subscribe();
|
|
19
|
+
return getViewport();
|
|
20
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './jsx.js';
|
package/jsx-runtime.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './jsx.js';
|
package/jsx-types.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { StyleShorthandKey } from '@flow.os/style';
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
namespace JSX {
|
|
5
|
+
type Element = Node;
|
|
6
|
+
|
|
7
|
+
/** classFlow (legacy) */
|
|
8
|
+
type ClassFlowValue = string | number | (() => string | number) | [() => number, string];
|
|
9
|
+
|
|
10
|
+
/** styleFlow: valori per proprietà stile (numero→px, getter, [getter, unità]). */
|
|
11
|
+
type StyleFlowStyleValue =
|
|
12
|
+
| number
|
|
13
|
+
| string
|
|
14
|
+
| (() => number | string)
|
|
15
|
+
| [() => number, string];
|
|
16
|
+
/** styleFlow: valori per classList (bool/getter). */
|
|
17
|
+
type StyleFlowClassValue = boolean | (() => boolean);
|
|
18
|
+
|
|
19
|
+
/** Layer per hover/active/focus: solo shorthand stile. */
|
|
20
|
+
interface StyleFlowPseudoLayer
|
|
21
|
+
extends Partial<Record<StyleShorthandKey, StyleFlowStyleValue>> {
|
|
22
|
+
[key: string]: StyleFlowStyleValue | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Layer styleFlow: base + responsive + pseudo + shorthand + classList. */
|
|
26
|
+
interface StyleFlowLayer
|
|
27
|
+
extends Partial<Record<StyleShorthandKey, StyleFlowStyleValue>> {
|
|
28
|
+
base?: string;
|
|
29
|
+
mob?: StyleFlowLayer;
|
|
30
|
+
tab?: StyleFlowLayer;
|
|
31
|
+
des?: StyleFlowLayer;
|
|
32
|
+
hover?: StyleFlowPseudoLayer;
|
|
33
|
+
active?: StyleFlowPseudoLayer;
|
|
34
|
+
focus?: StyleFlowPseudoLayer;
|
|
35
|
+
focusVisible?: StyleFlowPseudoLayer;
|
|
36
|
+
[key: string]:
|
|
37
|
+
| StyleFlowStyleValue
|
|
38
|
+
| StyleFlowClassValue
|
|
39
|
+
| StyleFlowLayer
|
|
40
|
+
| StyleFlowPseudoLayer
|
|
41
|
+
| string
|
|
42
|
+
| undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** styleFlow: stringa (solo classi) | [base, layer] | layer. */
|
|
46
|
+
type StyleFlow = string | [string, StyleFlowLayer] | StyleFlowLayer;
|
|
47
|
+
|
|
48
|
+
interface FlowHTMLAttributes {
|
|
49
|
+
class?: string | (() => string) | (string | (() => string))[];
|
|
50
|
+
classList?: Record<string, boolean | (() => boolean)>;
|
|
51
|
+
classFlow?: (() => Record<string, string | number>) | Record<string, ClassFlowValue>;
|
|
52
|
+
styleFlow?: StyleFlow;
|
|
53
|
+
style?: Record<string, string | number | (() => string | number)>;
|
|
54
|
+
children?: unknown;
|
|
55
|
+
[key: string]: unknown;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface IntrinsicElements {
|
|
59
|
+
[tag: string]: FlowHTMLAttributes;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {};
|
package/jsx.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/// <reference path="./jsx-types.d.ts" />
|
|
2
|
+
import { effect } from '@flow.os/core';
|
|
3
|
+
import { isGetter } from './features/utils.js';
|
|
4
|
+
import {
|
|
5
|
+
applyClassFlow,
|
|
6
|
+
applyClassAndClassList,
|
|
7
|
+
applyStyle,
|
|
8
|
+
applyStyleFlow,
|
|
9
|
+
applyAttrs,
|
|
10
|
+
} from './features/index.js';
|
|
11
|
+
|
|
12
|
+
export const Fragment = Symbol.for('flow.fragment');
|
|
13
|
+
|
|
14
|
+
type Props = Record<string, unknown> & { children?: unknown };
|
|
15
|
+
type JsxType = string | ((props: Props) => Node | null) | typeof Fragment;
|
|
16
|
+
|
|
17
|
+
function normalizeChild(c: unknown): Node | string | null {
|
|
18
|
+
if (c == null) return null;
|
|
19
|
+
if (typeof c === 'string' || typeof c === 'number') return String(c);
|
|
20
|
+
if (c instanceof Node) return c;
|
|
21
|
+
if (isGetter(c)) {
|
|
22
|
+
const text = document.createTextNode('');
|
|
23
|
+
effect(() => {
|
|
24
|
+
text.textContent = String(c());
|
|
25
|
+
});
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(c)) {
|
|
29
|
+
const frag = document.createDocumentFragment();
|
|
30
|
+
for (const x of c) {
|
|
31
|
+
const n = normalizeChild(x);
|
|
32
|
+
if (n !== null) frag.appendChild(typeof n === 'string' ? document.createTextNode(n) : n);
|
|
33
|
+
}
|
|
34
|
+
return frag;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function appendChildren(el: Node, children: unknown): void {
|
|
40
|
+
const c = normalizeChild(children);
|
|
41
|
+
if (c === null) return;
|
|
42
|
+
if (typeof c === 'string') {
|
|
43
|
+
el.appendChild(document.createTextNode(c));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
el.appendChild(c);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function applyProps(el: HTMLElement, props: Props): void {
|
|
50
|
+
const {
|
|
51
|
+
children,
|
|
52
|
+
class: classProp,
|
|
53
|
+
className: classNameProp,
|
|
54
|
+
classList,
|
|
55
|
+
classFlow,
|
|
56
|
+
style: styleProp,
|
|
57
|
+
styleFlow,
|
|
58
|
+
...rest
|
|
59
|
+
} = props;
|
|
60
|
+
if (styleFlow != null) {
|
|
61
|
+
applyStyleFlow(el, styleFlow);
|
|
62
|
+
} else {
|
|
63
|
+
applyClassFlow(el, classFlow);
|
|
64
|
+
applyClassAndClassList(el, classProp, classNameProp, classList);
|
|
65
|
+
applyStyle(el, styleProp);
|
|
66
|
+
}
|
|
67
|
+
applyAttrs(el, rest);
|
|
68
|
+
if (children !== undefined) appendChildren(el, children);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function jsxs(type: JsxType, props: Props, _key?: string | number): Node {
|
|
72
|
+
return jsx(type, props, _key);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function jsx(type: JsxType, props: Props, _key?: string | number): Node {
|
|
76
|
+
if (type === Fragment) {
|
|
77
|
+
const frag = document.createDocumentFragment();
|
|
78
|
+
if (props.children !== undefined) appendChildren(frag, props.children);
|
|
79
|
+
return frag;
|
|
80
|
+
}
|
|
81
|
+
if (typeof type === 'function') {
|
|
82
|
+
const out = type(props);
|
|
83
|
+
return out ?? document.createDocumentFragment();
|
|
84
|
+
}
|
|
85
|
+
const el = document.createElement(type as string);
|
|
86
|
+
applyProps(el, props);
|
|
87
|
+
return el;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function jsxDEV(
|
|
91
|
+
type: JsxType,
|
|
92
|
+
props: Props,
|
|
93
|
+
key: string | number | undefined,
|
|
94
|
+
_isStatic: boolean,
|
|
95
|
+
_source: unknown,
|
|
96
|
+
_self: unknown
|
|
97
|
+
): Node {
|
|
98
|
+
return jsx(type, props, key as string | undefined);
|
|
99
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flow.os/client",
|
|
3
|
+
"version": "0.0.1-dev.1771665310",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.ts",
|
|
6
|
+
"types": "./index.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@flow.os/core": "^0.0.1",
|
|
9
|
+
"@flow.os/router": "^0.0.1",
|
|
10
|
+
"@flow.os/style": "^0.0.1"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"vite": ">=5.0.0"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./index.ts",
|
|
18
|
+
"import": "./index.ts",
|
|
19
|
+
"default": "./index.ts"
|
|
20
|
+
},
|
|
21
|
+
"./config": {
|
|
22
|
+
"types": "./config.ts",
|
|
23
|
+
"import": "./config.ts",
|
|
24
|
+
"default": "./config.ts"
|
|
25
|
+
},
|
|
26
|
+
"./vite": {
|
|
27
|
+
"types": "./vite.ts",
|
|
28
|
+
"import": "./vite.ts",
|
|
29
|
+
"default": "./vite.ts"
|
|
30
|
+
},
|
|
31
|
+
"./jsx-runtime": {
|
|
32
|
+
"types": "./jsx.ts",
|
|
33
|
+
"import": "./jsx.ts",
|
|
34
|
+
"default": "./jsx.ts"
|
|
35
|
+
},
|
|
36
|
+
"./jsx-dev-runtime": {
|
|
37
|
+
"types": "./jsx.ts",
|
|
38
|
+
"import": "./jsx.ts",
|
|
39
|
+
"default": "./jsx.ts"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
package/vite.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { writeFileSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import type { Plugin } from 'vite';
|
|
5
|
+
|
|
6
|
+
const FLOW_ENTRY_ID = '\0flow-entry';
|
|
7
|
+
const ENTRY_CODE = `import * as root from '/client/root.tsx';
|
|
8
|
+
import { run } from '@flow.os/router';
|
|
9
|
+
run(root.default, import.meta.glob('/client/routes/**/*.tsx'), root.fallback != null ? { fallback: root.fallback } : undefined);
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
/** Reset stili browser: niente margin/padding di default, layout full viewport. */
|
|
13
|
+
const FLOW_BASE_STYLE = `<style>html,body{margin:0;padding:0;min-height:100vh}#app{min-height:100vh;box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}</style>`;
|
|
14
|
+
|
|
15
|
+
const HTML = `<!DOCTYPE html>
|
|
16
|
+
<html lang="it">
|
|
17
|
+
<head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>Flow</title>${FLOW_BASE_STYLE}</head>
|
|
18
|
+
<body><div id="app"></div><script type="module" src="/entry.tsx"></script></body>
|
|
19
|
+
</html>`;
|
|
20
|
+
|
|
21
|
+
export function flow(): Plugin {
|
|
22
|
+
const cwd = process.cwd();
|
|
23
|
+
return {
|
|
24
|
+
name: 'flow',
|
|
25
|
+
config() {
|
|
26
|
+
return {
|
|
27
|
+
root: resolve(cwd),
|
|
28
|
+
base: '/',
|
|
29
|
+
esbuild: { jsx: 'automatic', jsxImportSource: '@flow.os/client' },
|
|
30
|
+
resolve: { alias: { '~': resolve(cwd, 'client') } },
|
|
31
|
+
build: { target: 'esnext', minify: 'esbuild', sourcemap: true },
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
configResolved(_config) {
|
|
35
|
+
writeFileSync(resolve(cwd, 'index.html'), HTML);
|
|
36
|
+
},
|
|
37
|
+
resolveId(id) {
|
|
38
|
+
return id === '/entry.tsx' ? FLOW_ENTRY_ID : null;
|
|
39
|
+
},
|
|
40
|
+
load(id) {
|
|
41
|
+
return id === FLOW_ENTRY_ID ? ENTRY_CODE : null;
|
|
42
|
+
},
|
|
43
|
+
buildEnd(err) {
|
|
44
|
+
if (err) {
|
|
45
|
+
const msg = err.message ?? String(err);
|
|
46
|
+
const stack = err.stack ?? (err as Error).cause?.toString?.();
|
|
47
|
+
console.error('[flow] build error:', msg, stack ?? '');
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|