@flow.os/core 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/dom.ts +43 -0
- package/for.ts +42 -0
- package/index.ts +5 -0
- package/lifecycle.ts +6 -0
- package/package.json +15 -0
- package/show-switch.ts +56 -0
- package/state.ts +56 -0
package/dom.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper DOM per Show/For: normalizza children (getter, string, Node, array) e setContent.
|
|
3
|
+
*/
|
|
4
|
+
import { effect } from './state.js';
|
|
5
|
+
|
|
6
|
+
function isGetter(fn: unknown): fn is () => unknown {
|
|
7
|
+
return typeof fn === 'function' && fn.length === 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function normalizeChild(c: unknown): Node | string | null {
|
|
11
|
+
if (c == null) return null;
|
|
12
|
+
if (typeof c === 'string' || typeof c === 'number') return String(c);
|
|
13
|
+
if (c instanceof Node) return c;
|
|
14
|
+
if (isGetter(c)) {
|
|
15
|
+
const text = document.createTextNode('');
|
|
16
|
+
effect(() => {
|
|
17
|
+
text.textContent = String(c());
|
|
18
|
+
});
|
|
19
|
+
return text;
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(c)) {
|
|
22
|
+
const frag = document.createDocumentFragment();
|
|
23
|
+
for (const x of c) {
|
|
24
|
+
const n = normalizeChild(x);
|
|
25
|
+
if (n !== null) frag.appendChild(typeof n === 'string' ? document.createTextNode(n) : n);
|
|
26
|
+
}
|
|
27
|
+
return frag;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function setContent(el: HTMLElement, c: unknown): void {
|
|
33
|
+
const n = normalizeChild(c);
|
|
34
|
+
if (n === null) {
|
|
35
|
+
el.replaceChildren();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (typeof n === 'string') {
|
|
39
|
+
el.replaceChildren(document.createTextNode(n));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
el.replaceChildren(n);
|
|
43
|
+
}
|
package/for.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* For: lista reattiva. each={() => T[]}, children (item, index) => content.
|
|
3
|
+
* Opzionale key (item, index) => string|number per reconciliation keyed.
|
|
4
|
+
*/
|
|
5
|
+
import { effect } from './state.js';
|
|
6
|
+
import { normalizeChild } from './dom.js';
|
|
7
|
+
|
|
8
|
+
function isGetter(fn: unknown): fn is () => unknown {
|
|
9
|
+
return typeof fn === 'function' && fn.length === 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function appendNormalized(parent: Node, c: unknown): void {
|
|
13
|
+
const n = normalizeChild(c);
|
|
14
|
+
if (n === null) return;
|
|
15
|
+
parent.appendChild(typeof n === 'string' ? document.createTextNode(n) : n);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** For: rende una lista reattiva. each=getter array, children=(item, index)=>content, fallback=vuoto. */
|
|
19
|
+
export function For<T>(props: {
|
|
20
|
+
each: (() => T[]) | T[];
|
|
21
|
+
fallback?: unknown;
|
|
22
|
+
children: (item: T, index: number) => unknown;
|
|
23
|
+
}): HTMLElement {
|
|
24
|
+
const container = document.createElement('span');
|
|
25
|
+
container.style.display = 'contents';
|
|
26
|
+
const getList = isGetter(props.each) ? props.each : () => props.each as T[];
|
|
27
|
+
effect(() => {
|
|
28
|
+
const list = getList();
|
|
29
|
+
const len = Array.isArray(list) ? list.length : 0;
|
|
30
|
+
if (len === 0) {
|
|
31
|
+
appendNormalized(container, props.fallback);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
container.replaceChildren();
|
|
35
|
+
for (let i = 0; i < len; i++) {
|
|
36
|
+
const item = list[i];
|
|
37
|
+
if (item === undefined) continue;
|
|
38
|
+
appendNormalized(container, props.children(item, i));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return container;
|
|
42
|
+
}
|
package/index.ts
ADDED
package/lifecycle.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flow.os/core",
|
|
3
|
+
"version": "0.0.1-dev.1771665310",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.ts",
|
|
6
|
+
"types": "./index.ts",
|
|
7
|
+
"dependencies": {},
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.ts",
|
|
11
|
+
"import": "./index.ts",
|
|
12
|
+
"default": "./index.ts"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
package/show-switch.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Show: rendering condizionale (when/fallback).
|
|
3
|
+
* Switch: primo branch con when() truthy, altrimenti fallback.
|
|
4
|
+
*/
|
|
5
|
+
import { effect } from './state.js';
|
|
6
|
+
import { setContent } from './dom.js';
|
|
7
|
+
|
|
8
|
+
function isGetter(fn: unknown): fn is () => unknown {
|
|
9
|
+
return typeof fn === 'function' && fn.length === 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Show: quando when() è truthy mostra children, altrimenti fallback. */
|
|
13
|
+
export function Show(props: {
|
|
14
|
+
when: (() => unknown) | unknown;
|
|
15
|
+
fallback?: unknown;
|
|
16
|
+
children?: unknown;
|
|
17
|
+
}): HTMLElement {
|
|
18
|
+
const wrapper = document.createElement('span');
|
|
19
|
+
wrapper.style.display = 'contents';
|
|
20
|
+
const getWhen = isGetter(props.when) ? props.when : () => props.when;
|
|
21
|
+
effect(() => {
|
|
22
|
+
setContent(wrapper, getWhen() ? props.children : props.fallback);
|
|
23
|
+
});
|
|
24
|
+
return wrapper;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type MatchBranch = { when: () => boolean; children: unknown };
|
|
28
|
+
|
|
29
|
+
/** Switch: primo branch con when() truthy, altrimenti fallback. branches = [{ when, children }, ...]. */
|
|
30
|
+
export function Switch(props: {
|
|
31
|
+
fallback?: unknown;
|
|
32
|
+
branches: MatchBranch[];
|
|
33
|
+
}): HTMLElement {
|
|
34
|
+
const wrapper = document.createElement('span');
|
|
35
|
+
wrapper.style.display = 'contents';
|
|
36
|
+
effect(() => {
|
|
37
|
+
const list = props.branches;
|
|
38
|
+
for (let i = 0; i < list.length; i++) {
|
|
39
|
+
const b = list[i];
|
|
40
|
+
if (b === undefined) continue;
|
|
41
|
+
const cond = isGetter(b.when) ? b.when() : b.when;
|
|
42
|
+
if (cond) {
|
|
43
|
+
setContent(wrapper, b.children);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
setContent(wrapper, props.fallback);
|
|
48
|
+
});
|
|
49
|
+
return wrapper;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Helper per costruire i branch di Switch: Match({ when: () => cond, children: <Node/> }) => branch. */
|
|
53
|
+
export function Match(props: { when: (() => boolean) | boolean; children?: unknown }): MatchBranch {
|
|
54
|
+
const whenFn = isGetter(props.when) ? props.when : () => Boolean(props.when);
|
|
55
|
+
return { when: whenFn, children: props.children };
|
|
56
|
+
}
|
package/state.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
type Subscriber = () => void;
|
|
2
|
+
type SubscriberSet = Set<Subscriber>;
|
|
3
|
+
|
|
4
|
+
interface EffectRecord {
|
|
5
|
+
run: () => void;
|
|
6
|
+
deps: Set<SubscriberSet>;
|
|
7
|
+
cleanups: Array<() => void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let currentEffect: EffectRecord | null = null;
|
|
11
|
+
|
|
12
|
+
export function state<T>(initialValue: T): [() => T, (value: T | ((prev: T) => T)) => void] {
|
|
13
|
+
let value = initialValue;
|
|
14
|
+
const subscribers: SubscriberSet = new Set();
|
|
15
|
+
|
|
16
|
+
function get(): T {
|
|
17
|
+
if (currentEffect !== null) {
|
|
18
|
+
subscribers.add(currentEffect.run);
|
|
19
|
+
currentEffect.deps.add(subscribers);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function set(next: T | ((prev: T) => T)): void {
|
|
25
|
+
const nextValue = typeof next === 'function' ? (next as (prev: T) => T)(value) : next;
|
|
26
|
+
if (Object.is(value, nextValue)) return;
|
|
27
|
+
value = nextValue;
|
|
28
|
+
const copy = Array.from(subscribers);
|
|
29
|
+
copy.forEach((fn) => fn());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return [get, set];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function effect(fn: Subscriber): void {
|
|
36
|
+
const deps = new Set<SubscriberSet>();
|
|
37
|
+
const cleanups: Array<() => void> = [];
|
|
38
|
+
const run: Subscriber = () => {
|
|
39
|
+
for (const fn of cleanups) fn();
|
|
40
|
+
cleanups.length = 0;
|
|
41
|
+
deps.forEach((subs) => subs.delete(run));
|
|
42
|
+
deps.clear();
|
|
43
|
+
const prev = currentEffect;
|
|
44
|
+
currentEffect = { run, deps, cleanups };
|
|
45
|
+
try {
|
|
46
|
+
fn();
|
|
47
|
+
} finally {
|
|
48
|
+
currentEffect = prev;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
run();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function onCleanup(fn: () => void): void {
|
|
55
|
+
if (currentEffect !== null) currentEffect.cleanups.push(fn);
|
|
56
|
+
}
|