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,185 @@
|
|
|
1
|
+
import type { RouteModule, RouterOptions, RunOptions } from './types.js';
|
|
2
|
+
import { buildRouteEntries } from './matcher.js';
|
|
3
|
+
import { match } from './matcher.js';
|
|
4
|
+
|
|
5
|
+
let currentRouter: RouterAPI | null = null;
|
|
6
|
+
let currentParams: Record<string, string> = {};
|
|
7
|
+
|
|
8
|
+
export const APP_ID = 'flow-app';
|
|
9
|
+
|
|
10
|
+
/** Root container where the router mounts the current page. Use once in root as <App />. */
|
|
11
|
+
export function App(): HTMLDivElement {
|
|
12
|
+
const el = document.createElement('div');
|
|
13
|
+
el.id = APP_ID;
|
|
14
|
+
el.setAttribute('role', 'main');
|
|
15
|
+
return el;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function setContent(container: HTMLElement, content: Node | string): void {
|
|
19
|
+
container.innerHTML = '';
|
|
20
|
+
if (typeof content === 'string') container.textContent = content;
|
|
21
|
+
else container.appendChild(content);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function defaultNotFound(path: string): Node {
|
|
25
|
+
const wrap = document.createElement('div');
|
|
26
|
+
wrap.setAttribute('role', 'alert');
|
|
27
|
+
const h = document.createElement('h1');
|
|
28
|
+
h.textContent = '404';
|
|
29
|
+
const p = document.createElement('p');
|
|
30
|
+
p.textContent = `Page not found: ${path || '/'}`;
|
|
31
|
+
wrap.append(h, p);
|
|
32
|
+
return wrap;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RouterAPI {
|
|
36
|
+
/** Navigate to a path. Use replace=true to avoid adding a history entry. */
|
|
37
|
+
go(path: string, replace?: boolean): void;
|
|
38
|
+
/** Start listening to popstate and link clicks. Call once after mounting App. */
|
|
39
|
+
start(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const NOT_FOUND_PATTERN = '404';
|
|
43
|
+
|
|
44
|
+
export function createRouter(
|
|
45
|
+
modules: Record<string, () => Promise<RouteModule>>,
|
|
46
|
+
container: HTMLElement,
|
|
47
|
+
options: RouterOptions = {}
|
|
48
|
+
): RouterAPI {
|
|
49
|
+
const allEntries = buildRouteEntries(modules);
|
|
50
|
+
const routes = allEntries.filter((e) => e.pattern !== NOT_FOUND_PATTERN);
|
|
51
|
+
const notFoundLoader = allEntries.find((e) => e.pattern === NOT_FOUND_PATTERN)?.loader ?? null;
|
|
52
|
+
|
|
53
|
+
const notFound: Node | ((path: string) => Node | Promise<Node>) =
|
|
54
|
+
options.notFound ??
|
|
55
|
+
(notFoundLoader
|
|
56
|
+
? (path) =>
|
|
57
|
+
notFoundLoader().then((mod) => {
|
|
58
|
+
const def = mod.default;
|
|
59
|
+
return typeof def === 'function' ? (def as (p: string) => Node)(path) : (def as unknown as Node);
|
|
60
|
+
})
|
|
61
|
+
: defaultNotFound);
|
|
62
|
+
const fullPageNotFound = Boolean(
|
|
63
|
+
options.appRoot && options.getLayout && (options.notFound != null || notFoundLoader != null)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
function getContainer(): HTMLElement | null {
|
|
67
|
+
return document.getElementById(APP_ID);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function render(pathname: string): Promise<boolean> {
|
|
71
|
+
const path = pathname === '/' ? '' : pathname.slice(1);
|
|
72
|
+
|
|
73
|
+
for (const { pattern, loader } of routes) {
|
|
74
|
+
const routeParams = match(pattern, path);
|
|
75
|
+
if (routeParams === null) continue;
|
|
76
|
+
|
|
77
|
+
currentParams = routeParams;
|
|
78
|
+
let target = getContainer();
|
|
79
|
+
if (fullPageNotFound && !target) {
|
|
80
|
+
options.appRoot!.innerHTML = '';
|
|
81
|
+
options.appRoot!.appendChild(options.getLayout!());
|
|
82
|
+
target = getContainer();
|
|
83
|
+
}
|
|
84
|
+
if (!target) continue;
|
|
85
|
+
|
|
86
|
+
const fallbackNode =
|
|
87
|
+
options.fallback != null
|
|
88
|
+
? typeof options.fallback === 'function'
|
|
89
|
+
? options.fallback()
|
|
90
|
+
: options.fallback
|
|
91
|
+
: null;
|
|
92
|
+
if (fallbackNode) setContent(target, fallbackNode);
|
|
93
|
+
|
|
94
|
+
const mod = await loader();
|
|
95
|
+
const def = mod.default;
|
|
96
|
+
if (typeof def === 'function') {
|
|
97
|
+
const out = def.length
|
|
98
|
+
? (def as (p: Record<string, string>) => Node)(routeParams)
|
|
99
|
+
: (def as () => Node | string)();
|
|
100
|
+
if (out instanceof Node) setContent(target, out);
|
|
101
|
+
else if (typeof out === 'string') target.innerHTML = out;
|
|
102
|
+
else target.innerHTML = '';
|
|
103
|
+
} else {
|
|
104
|
+
setContent(target, typeof def === 'string' ? def : (def as unknown as Node));
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
currentParams = {};
|
|
110
|
+
const nfRaw = typeof notFound === 'function' ? notFound(pathname) : notFound;
|
|
111
|
+
const nfNode = nfRaw && typeof (nfRaw as Promise<unknown>).then === 'function' ? await (nfRaw as Promise<Node>) : (nfRaw as Node);
|
|
112
|
+
if (fullPageNotFound) {
|
|
113
|
+
options.appRoot!.innerHTML = '';
|
|
114
|
+
options.appRoot!.appendChild(nfNode);
|
|
115
|
+
} else {
|
|
116
|
+
setContent(container, nfNode);
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function go(path: string, replace = false): void {
|
|
122
|
+
const p = path.startsWith('/') ? path : `/${path}`;
|
|
123
|
+
render(p).then((ok) => {
|
|
124
|
+
if (ok) (replace ? history.replaceState : history.pushState).call(history, null, '', p);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function start(): void {
|
|
129
|
+
render(location.pathname || '/');
|
|
130
|
+
window.addEventListener('popstate', () => render(location.pathname || '/'));
|
|
131
|
+
document.addEventListener('click', (e) => {
|
|
132
|
+
const a = (e.target as Element).closest('a');
|
|
133
|
+
if (a?.getAttribute('href')?.startsWith('/') && !a.href.startsWith('//')) {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
go(a.getAttribute('href')!);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const api: RouterAPI = { go, start };
|
|
141
|
+
currentRouter = api;
|
|
142
|
+
return api;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Navigate to a path. Import and use anywhere; works after run() (or createRouter().start()) has been called.
|
|
147
|
+
* No-op if the router is not started yet.
|
|
148
|
+
*/
|
|
149
|
+
export function go(path: string, replace = false): void {
|
|
150
|
+
currentRouter?.go(path, replace);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Current route params. params() → all, params('id') → one. */
|
|
154
|
+
export function params(): Record<string, string>;
|
|
155
|
+
export function params(name: string): string | undefined;
|
|
156
|
+
export function params(name?: string): Record<string, string> | string | undefined {
|
|
157
|
+
if (name === undefined) return { ...currentParams };
|
|
158
|
+
return currentParams[name] ?? undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Mounts App and starts the router. Use in your virtual entry.
|
|
163
|
+
* Returns the router API so you can call go() (and start()) from outside.
|
|
164
|
+
*/
|
|
165
|
+
export function run(
|
|
166
|
+
AppFactory: () => Node,
|
|
167
|
+
modules: Record<string, () => Promise<RouteModule>>,
|
|
168
|
+
options?: RunOptions
|
|
169
|
+
): RouterAPI {
|
|
170
|
+
const app = document.getElementById('app')!;
|
|
171
|
+
app.appendChild(AppFactory());
|
|
172
|
+
const appContainer = document.getElementById(APP_ID);
|
|
173
|
+
if (!appContainer) {
|
|
174
|
+
throw new Error(`#${APP_ID} not found: use <App /> from @flow.os/router in your root layout.`);
|
|
175
|
+
}
|
|
176
|
+
const routerOptions: RouterOptions = {
|
|
177
|
+
fallback: options?.fallback,
|
|
178
|
+
notFound: options?.notFound,
|
|
179
|
+
appRoot: app,
|
|
180
|
+
getLayout: () => AppFactory(),
|
|
181
|
+
};
|
|
182
|
+
const api = createRouter(modules, appContainer as HTMLElement, routerOptions);
|
|
183
|
+
api.start();
|
|
184
|
+
return api;
|
|
185
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Route module: default can be a component (with optional params), a factory, or raw string/Node. */
|
|
2
|
+
export type RouteModule = {
|
|
3
|
+
default:
|
|
4
|
+
| (() => Node)
|
|
5
|
+
| ((params: Record<string, string>) => Node)
|
|
6
|
+
| (() => string)
|
|
7
|
+
| string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Options for run(): fallback while loading, custom 404. */
|
|
11
|
+
export type RunOptions = {
|
|
12
|
+
/** Node (or factory) shown while the lazy route is loading. */
|
|
13
|
+
fallback?: Node | (() => Node);
|
|
14
|
+
/** 404 page: if provided, shown full-page (replaces entire #app, no shared layout). */
|
|
15
|
+
notFound?: Node | ((path: string) => Node);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Internal router options (fallback, notFound, full-page 404 wiring). */
|
|
19
|
+
export type RouterOptions = {
|
|
20
|
+
fallback?: Node | (() => Node);
|
|
21
|
+
notFound?: Node | ((path: string) => Node | Promise<Node>);
|
|
22
|
+
appRoot?: HTMLElement;
|
|
23
|
+
getLayout?: () => Node;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Internal: normalized route entry for matching. */
|
|
27
|
+
export type RouteEntry = {
|
|
28
|
+
pattern: string;
|
|
29
|
+
loader: () => Promise<RouteModule>;
|
|
30
|
+
/** Number of static segments (for sort order). */
|
|
31
|
+
staticSegments: number;
|
|
32
|
+
/** True if pattern ends with catch-all. */
|
|
33
|
+
catchAll: boolean;
|
|
34
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Origin: protocol + host (e.g. https://localhost:5173 or https://example.com). */
|
|
2
|
+
export function base(): string {
|
|
3
|
+
return typeof location !== 'undefined' ? location.origin : '';
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Current pathname (e.g. /posts/123). */
|
|
7
|
+
export function path(p?: string): string {
|
|
8
|
+
const raw = p ?? (typeof location !== 'undefined' ? location.pathname || '/' : '/');
|
|
9
|
+
return raw.startsWith('/') ? raw : `/${raw}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Full path + search + hash. No args = current. */
|
|
13
|
+
export function href(p?: string): string {
|
|
14
|
+
const basePath = path(p);
|
|
15
|
+
if (typeof location === 'undefined') return basePath;
|
|
16
|
+
return basePath + (location.search ?? '') + (location.hash ?? '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Query (after ?). query() → all as object, query('key') → one value. */
|
|
20
|
+
export function query(): Record<string, string>;
|
|
21
|
+
export function query(name: string): string | null;
|
|
22
|
+
export function query(name?: string): Record<string, string> | string | null {
|
|
23
|
+
const s = typeof location !== 'undefined' ? location.search : '';
|
|
24
|
+
const p = new URLSearchParams(s);
|
|
25
|
+
if (name !== undefined) return p.get(name);
|
|
26
|
+
const out: Record<string, string> = {};
|
|
27
|
+
p.forEach((v, k) => (out[k] = v));
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** New path with query updated. go(setQuery({ page: '2' })) to navigate. */
|
|
32
|
+
export function setQuery(
|
|
33
|
+
obj: Record<string, string | number | boolean | undefined | null>,
|
|
34
|
+
p?: string
|
|
35
|
+
): string {
|
|
36
|
+
const basePath = path(p);
|
|
37
|
+
const current = query();
|
|
38
|
+
const next = { ...current };
|
|
39
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
40
|
+
if (value === undefined || value === null) delete next[key];
|
|
41
|
+
else next[key] = String(value);
|
|
42
|
+
}
|
|
43
|
+
const search = new URLSearchParams(next).toString();
|
|
44
|
+
return search ? `${basePath}?${search}` : basePath;
|
|
45
|
+
}
|
|
@@ -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';
|
|
@@ -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
|
+
}
|