tinky 1.7.0 → 1.8.0
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/lib/components/Text.js +12 -3
- package/lib/core/dom.d.ts +3 -0
- package/lib/core/reconciler.js +3 -0
- package/lib/core/size-observer.d.ts +60 -0
- package/lib/core/size-observer.js +133 -0
- package/lib/core/tinky.js +3 -0
- package/lib/hooks/use-size-observer.d.ts +44 -0
- package/lib/hooks/use-size-observer.js +70 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/package.json +1 -1
package/lib/components/Text.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useContext } from "react";
|
|
2
|
+
import { useContext, useCallback } from "react";
|
|
3
3
|
import { applyTextStyles, } from "../utils/apply-text-styles.js";
|
|
4
4
|
import { AccessibilityContext } from "../contexts/AccessibilityContext.js";
|
|
5
5
|
import { backgroundContext } from "../contexts/BackgroundContext.js";
|
|
@@ -41,7 +41,7 @@ export function Text({ color, backgroundColor, dimColor = false, bold = false, i
|
|
|
41
41
|
if (childrenOrAriaLabel === undefined || childrenOrAriaLabel === null) {
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
|
-
const transform = (children) => {
|
|
44
|
+
const transform = useCallback((children) => {
|
|
45
45
|
return applyTextStyles(children, {
|
|
46
46
|
color,
|
|
47
47
|
backgroundColor: effectiveBackgroundColor,
|
|
@@ -52,7 +52,16 @@ export function Text({ color, backgroundColor, dimColor = false, bold = false, i
|
|
|
52
52
|
strikethrough,
|
|
53
53
|
inverse,
|
|
54
54
|
});
|
|
55
|
-
}
|
|
55
|
+
}, [
|
|
56
|
+
color,
|
|
57
|
+
effectiveBackgroundColor,
|
|
58
|
+
dimColor,
|
|
59
|
+
bold,
|
|
60
|
+
italic,
|
|
61
|
+
underline,
|
|
62
|
+
strikethrough,
|
|
63
|
+
inverse,
|
|
64
|
+
]);
|
|
56
65
|
if (isScreenReaderEnabled && ariaHidden) {
|
|
57
66
|
return null;
|
|
58
67
|
}
|
package/lib/core/dom.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Styles } from "./styles.js";
|
|
2
2
|
import { type OutputTransformer } from "./render-node-to-output.js";
|
|
3
3
|
import { TaffyNode } from "./taffy-node.js";
|
|
4
|
+
import { type SizeObserver } from "./size-observer.js";
|
|
4
5
|
/**
|
|
5
6
|
* Interface representing a node in the Tinky tree.
|
|
6
7
|
*/
|
|
@@ -74,6 +75,8 @@ export type DOMElement = {
|
|
|
74
75
|
onRender?: () => void;
|
|
75
76
|
/** Callback to trigger an immediate render. */
|
|
76
77
|
onImmediateRender?: () => void;
|
|
78
|
+
/** Set of resize observers attached to this element. */
|
|
79
|
+
resizeObservers?: Set<SizeObserver>;
|
|
77
80
|
} & TinkyNode;
|
|
78
81
|
/**
|
|
79
82
|
* Interface representing a text node in Tinky.
|
package/lib/core/reconciler.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createContext } from "react";
|
|
|
5
5
|
import { createTextNode, appendChildNode, insertBeforeNode, removeChildNode, setStyle, setTextNodeValue, createNode, setAttribute, } from "./dom.js";
|
|
6
6
|
import { applyStyles } from "./styles.js";
|
|
7
7
|
import { process } from "../utils/node-adapter.js";
|
|
8
|
+
import { removeSizeObserversInSubtree } from "./size-observer.js";
|
|
8
9
|
// We need to conditionally perform devtools connection to avoid
|
|
9
10
|
// accidentally breaking other third-party code.
|
|
10
11
|
if (process?.env?.["DEV"] === "true") {
|
|
@@ -204,6 +205,7 @@ export const reconciler = createReconciler({
|
|
|
204
205
|
insertInContainerBefore: insertBeforeNode,
|
|
205
206
|
removeChildFromContainer(node, removeNode) {
|
|
206
207
|
removeChildNode(node, removeNode);
|
|
208
|
+
removeSizeObserversInSubtree(removeNode);
|
|
207
209
|
cleanupTaffyNode(removeNode.taffyNode);
|
|
208
210
|
},
|
|
209
211
|
commitUpdate(node, _type, oldProps, newProps) {
|
|
@@ -241,6 +243,7 @@ export const reconciler = createReconciler({
|
|
|
241
243
|
},
|
|
242
244
|
removeChild(node, removeNode) {
|
|
243
245
|
removeChildNode(node, removeNode);
|
|
246
|
+
removeSizeObserversInSubtree(removeNode);
|
|
244
247
|
cleanupTaffyNode(removeNode.taffyNode);
|
|
245
248
|
},
|
|
246
249
|
setCurrentUpdatePriority(newPriority) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type DOMElement, type DOMNode } from "./dom.js";
|
|
2
|
+
import { type Dimension } from "../utils/dimension.js";
|
|
3
|
+
/**
|
|
4
|
+
* Interface for elements returned by the SizeObserver callback.
|
|
5
|
+
*/
|
|
6
|
+
export interface SizeObserverEntry {
|
|
7
|
+
target: DOMElement;
|
|
8
|
+
dimension: Dimension;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Callback type for resize observers.
|
|
12
|
+
*/
|
|
13
|
+
export type SizeObserverCallback = (entries: SizeObserverEntry[], observer: SizeObserver) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Global registry mapping DOMElements to their registered SizeObservers.
|
|
16
|
+
*/
|
|
17
|
+
export declare const resizeObserverRegistry: Map<DOMElement, Set<SizeObserver>>;
|
|
18
|
+
/**
|
|
19
|
+
* Implements a SizeObserver API similar to the HTML specification.
|
|
20
|
+
* It allows observing changes to the computed dimensions of Tinky DOM elements.
|
|
21
|
+
*/
|
|
22
|
+
export declare class SizeObserver {
|
|
23
|
+
private callback;
|
|
24
|
+
/**
|
|
25
|
+
* Creates a new SizeObserver.
|
|
26
|
+
*
|
|
27
|
+
* @param callback - The function to call when observed elements resize.
|
|
28
|
+
*/
|
|
29
|
+
constructor(callback: SizeObserverCallback);
|
|
30
|
+
/**
|
|
31
|
+
* Starts observing the specified element.
|
|
32
|
+
*
|
|
33
|
+
* @param target - The element to observe.
|
|
34
|
+
*/
|
|
35
|
+
observe(target: DOMElement): void;
|
|
36
|
+
/**
|
|
37
|
+
* Stops observing the specified element.
|
|
38
|
+
*
|
|
39
|
+
* @param target - The element to stop observing.
|
|
40
|
+
*/
|
|
41
|
+
unobserve(target: DOMElement): void;
|
|
42
|
+
/**
|
|
43
|
+
* Stops observing all elements.
|
|
44
|
+
*/
|
|
45
|
+
disconnect(): void;
|
|
46
|
+
/** @internal */
|
|
47
|
+
_invokeCallback(entries: SizeObserverEntry[]): void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Removes all resize observers from a node and its descendants.
|
|
51
|
+
* Called during mutation cleanup to prevent stale observer entries.
|
|
52
|
+
*
|
|
53
|
+
* @param node - Root of the removed subtree.
|
|
54
|
+
*/
|
|
55
|
+
export declare const removeSizeObserversInSubtree: (node: DOMNode) => void;
|
|
56
|
+
/**
|
|
57
|
+
* Notifies all registered resize observers of their current dimensions.
|
|
58
|
+
* Called by the Tinky instance after each layout computation pass.
|
|
59
|
+
*/
|
|
60
|
+
export declare const notifySizeObservers: () => void;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global registry mapping DOMElements to their registered SizeObservers.
|
|
3
|
+
*/
|
|
4
|
+
export const resizeObserverRegistry = new Map();
|
|
5
|
+
/**
|
|
6
|
+
* Implements a SizeObserver API similar to the HTML specification.
|
|
7
|
+
* It allows observing changes to the computed dimensions of Tinky DOM elements.
|
|
8
|
+
*/
|
|
9
|
+
export class SizeObserver {
|
|
10
|
+
callback;
|
|
11
|
+
/**
|
|
12
|
+
* Creates a new SizeObserver.
|
|
13
|
+
*
|
|
14
|
+
* @param callback - The function to call when observed elements resize.
|
|
15
|
+
*/
|
|
16
|
+
constructor(callback) {
|
|
17
|
+
this.callback = callback;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Starts observing the specified element.
|
|
21
|
+
*
|
|
22
|
+
* @param target - The element to observe.
|
|
23
|
+
*/
|
|
24
|
+
observe(target) {
|
|
25
|
+
if (!target.resizeObservers) {
|
|
26
|
+
target.resizeObservers = new Set();
|
|
27
|
+
}
|
|
28
|
+
target.resizeObservers.add(this);
|
|
29
|
+
let observers = resizeObserverRegistry.get(target);
|
|
30
|
+
if (!observers) {
|
|
31
|
+
observers = new Set();
|
|
32
|
+
resizeObserverRegistry.set(target, observers);
|
|
33
|
+
}
|
|
34
|
+
observers.add(this);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Stops observing the specified element.
|
|
38
|
+
*
|
|
39
|
+
* @param target - The element to stop observing.
|
|
40
|
+
*/
|
|
41
|
+
unobserve(target) {
|
|
42
|
+
target.resizeObservers?.delete(this);
|
|
43
|
+
if (target.resizeObservers?.size === 0) {
|
|
44
|
+
target.resizeObservers = undefined;
|
|
45
|
+
}
|
|
46
|
+
const observers = resizeObserverRegistry.get(target);
|
|
47
|
+
if (observers) {
|
|
48
|
+
observers.delete(this);
|
|
49
|
+
if (observers.size === 0) {
|
|
50
|
+
resizeObserverRegistry.delete(target);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Stops observing all elements.
|
|
56
|
+
*/
|
|
57
|
+
disconnect() {
|
|
58
|
+
for (const [target, observers] of resizeObserverRegistry.entries()) {
|
|
59
|
+
if (observers.has(this)) {
|
|
60
|
+
this.unobserve(target);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** @internal */
|
|
65
|
+
_invokeCallback(entries) {
|
|
66
|
+
this.callback(entries, this);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Removes all resize observers from a node and its descendants.
|
|
71
|
+
* Called during mutation cleanup to prevent stale observer entries.
|
|
72
|
+
*
|
|
73
|
+
* @param node - Root of the removed subtree.
|
|
74
|
+
*/
|
|
75
|
+
export const removeSizeObserversInSubtree = (node) => {
|
|
76
|
+
if (node.nodeName === "#text") {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (node.resizeObservers) {
|
|
80
|
+
// Collect all observers to avoid disconnecting while iterating
|
|
81
|
+
const observers = Array.from(node.resizeObservers);
|
|
82
|
+
for (const observer of observers) {
|
|
83
|
+
observer.unobserve(node);
|
|
84
|
+
}
|
|
85
|
+
node.resizeObservers = undefined;
|
|
86
|
+
}
|
|
87
|
+
// Not strictly necessary here since unobserve handles it,
|
|
88
|
+
// but good for completeness in case it fell through
|
|
89
|
+
resizeObserverRegistry.delete(node);
|
|
90
|
+
for (const childNode of node.childNodes) {
|
|
91
|
+
removeSizeObserversInSubtree(childNode);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Notifies all registered resize observers of their current dimensions.
|
|
96
|
+
* Called by the Tinky instance after each layout computation pass.
|
|
97
|
+
*/
|
|
98
|
+
export const notifySizeObservers = () => {
|
|
99
|
+
const entriesByObserver = new Map();
|
|
100
|
+
for (const [node, observers] of resizeObserverRegistry.entries()) {
|
|
101
|
+
if (!node.taffyNode) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
let layout;
|
|
105
|
+
try {
|
|
106
|
+
layout = node.taffyNode.tree.getLayout(node.taffyNode.id);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Node was removed from the tree before passive effect cleanup ran.
|
|
110
|
+
const nodeObservers = Array.from(observers);
|
|
111
|
+
for (const observer of nodeObservers) {
|
|
112
|
+
observer.unobserve(node);
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const dimension = {
|
|
117
|
+
width: layout?.width ?? 0,
|
|
118
|
+
height: layout?.height ?? 0,
|
|
119
|
+
};
|
|
120
|
+
const entry = { target: node, dimension };
|
|
121
|
+
for (const observer of observers) {
|
|
122
|
+
let entries = entriesByObserver.get(observer);
|
|
123
|
+
if (!entries) {
|
|
124
|
+
entries = [];
|
|
125
|
+
entriesByObserver.set(observer, entries);
|
|
126
|
+
}
|
|
127
|
+
entries.push(entry);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const [observer, entries] of entriesByObserver.entries()) {
|
|
131
|
+
observer._invokeCallback(entries);
|
|
132
|
+
}
|
|
133
|
+
};
|
package/lib/core/tinky.js
CHANGED
|
@@ -19,6 +19,7 @@ import { App } from "../components/App.js";
|
|
|
19
19
|
import { AccessibilityContext } from "../contexts/AccessibilityContext.js";
|
|
20
20
|
import { CellBuffer, StyleRegistry } from "./cell-buffer.js";
|
|
21
21
|
import { normalizeIncrementalRendering, } from "./incremental-rendering.js";
|
|
22
|
+
import { notifySizeObservers } from "./size-observer.js";
|
|
22
23
|
const noop = () => {
|
|
23
24
|
// no-op
|
|
24
25
|
};
|
|
@@ -222,6 +223,8 @@ export class Tinky {
|
|
|
222
223
|
}
|
|
223
224
|
return context.measureFunc(availableSpace.width);
|
|
224
225
|
});
|
|
226
|
+
// Notify all registered resize observers of their current dimensions.
|
|
227
|
+
notifySizeObservers();
|
|
225
228
|
};
|
|
226
229
|
/**
|
|
227
230
|
* Performs the render operation.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type DOMElement } from "../core/dom.js";
|
|
2
|
+
/**
|
|
3
|
+
* Options for the `useSizeObserver` hook.
|
|
4
|
+
*/
|
|
5
|
+
export interface SizeObserverOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Enable or disable the size observer.
|
|
8
|
+
*
|
|
9
|
+
* @defaultValue true
|
|
10
|
+
*/
|
|
11
|
+
isActive?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Hook that observes the computed layout dimensions of a `<Box>` element
|
|
15
|
+
* and triggers a re-render whenever its size changes.
|
|
16
|
+
*
|
|
17
|
+
* The observer is registered in a global resize observer registry that is
|
|
18
|
+
* invoked by the Tinky core after each layout computation pass. This means
|
|
19
|
+
* size changes are detected regardless of which component caused the
|
|
20
|
+
* re-render — including sibling updates and terminal resize events.
|
|
21
|
+
*
|
|
22
|
+
* Multiple observers can be attached to the same element.
|
|
23
|
+
*
|
|
24
|
+
* This is similar to HTML's `SizeObserver` API.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* import { Box, Text, useSizeObserver } from 'tinky';
|
|
29
|
+
*
|
|
30
|
+
* function MyComponent() {
|
|
31
|
+
* const [ref, width, height] = useSizeObserver();
|
|
32
|
+
*
|
|
33
|
+
* return (
|
|
34
|
+
* <Box ref={ref} width="50%" height={10}>
|
|
35
|
+
* <Text>Size: {width}x{height}</Text>
|
|
36
|
+
* </Box>
|
|
37
|
+
* );
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @param options - Configuration options.
|
|
42
|
+
* @returns A tuple containing `[refCallback, width, height]`. Attach `refCallback` to a `<Box>` element.
|
|
43
|
+
*/
|
|
44
|
+
export declare const useSizeObserver: (options?: SizeObserverOptions) => [(node: DOMElement | null) => void, number, number];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from "react";
|
|
2
|
+
import { measureElement } from "../utils/measure-element.js";
|
|
3
|
+
import { SizeObserver } from "../core/size-observer.js";
|
|
4
|
+
import { reconciler } from "../core/reconciler.js";
|
|
5
|
+
/**
|
|
6
|
+
* Hook that observes the computed layout dimensions of a `<Box>` element
|
|
7
|
+
* and triggers a re-render whenever its size changes.
|
|
8
|
+
*
|
|
9
|
+
* The observer is registered in a global resize observer registry that is
|
|
10
|
+
* invoked by the Tinky core after each layout computation pass. This means
|
|
11
|
+
* size changes are detected regardless of which component caused the
|
|
12
|
+
* re-render — including sibling updates and terminal resize events.
|
|
13
|
+
*
|
|
14
|
+
* Multiple observers can be attached to the same element.
|
|
15
|
+
*
|
|
16
|
+
* This is similar to HTML's `SizeObserver` API.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* import { Box, Text, useSizeObserver } from 'tinky';
|
|
21
|
+
*
|
|
22
|
+
* function MyComponent() {
|
|
23
|
+
* const [ref, width, height] = useSizeObserver();
|
|
24
|
+
*
|
|
25
|
+
* return (
|
|
26
|
+
* <Box ref={ref} width="50%" height={10}>
|
|
27
|
+
* <Text>Size: {width}x{height}</Text>
|
|
28
|
+
* </Box>
|
|
29
|
+
* );
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @param options - Configuration options.
|
|
34
|
+
* @returns A tuple containing `[refCallback, width, height]`. Attach `refCallback` to a `<Box>` element.
|
|
35
|
+
*/
|
|
36
|
+
export const useSizeObserver = (options = {}) => {
|
|
37
|
+
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
38
|
+
const observerRef = useRef(null);
|
|
39
|
+
const prevSize = useRef({ width: 0, height: 0 });
|
|
40
|
+
const { isActive = true } = options;
|
|
41
|
+
const callbackRef = useCallback((node) => {
|
|
42
|
+
// Cleanup previous observer
|
|
43
|
+
if (observerRef.current) {
|
|
44
|
+
observerRef.current.disconnect();
|
|
45
|
+
observerRef.current = null;
|
|
46
|
+
}
|
|
47
|
+
// If active and node is attached, start observing
|
|
48
|
+
if (isActive && node) {
|
|
49
|
+
const callback = (entries) => {
|
|
50
|
+
if (entries.length === 0)
|
|
51
|
+
return;
|
|
52
|
+
const { dimension } = entries[0];
|
|
53
|
+
if (dimension.width !== prevSize.current.width ||
|
|
54
|
+
dimension.height !== prevSize.current.height) {
|
|
55
|
+
prevSize.current = { ...dimension };
|
|
56
|
+
reconciler.batchedUpdates(() => {
|
|
57
|
+
setSize({ ...dimension });
|
|
58
|
+
}, null);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const observer = new SizeObserver(callback);
|
|
62
|
+
// Read initial dimensions
|
|
63
|
+
const initial = measureElement(node);
|
|
64
|
+
callback([{ target: node, dimension: initial }]);
|
|
65
|
+
observer.observe(node);
|
|
66
|
+
observerRef.current = observer;
|
|
67
|
+
}
|
|
68
|
+
}, [isActive]);
|
|
69
|
+
return [callbackRef, size.width, size.height];
|
|
70
|
+
};
|
package/lib/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export { useFocus, type FocusOptions, type FocusState, } from "./hooks/use-focus
|
|
|
21
21
|
export { useFocusManager, type FocusManager, } from "./hooks/use-focus-manager.js";
|
|
22
22
|
export { useIsScreenReaderEnabled } from "./hooks/use-is-screen-reader-enabled.js";
|
|
23
23
|
export { measureElement } from "./utils/measure-element.js";
|
|
24
|
+
export { useSizeObserver, type SizeObserverOptions, } from "./hooks/use-size-observer.js";
|
|
24
25
|
export { type Dimension } from "./utils/dimension.js";
|
|
25
26
|
export { type ForegroundColorName } from "./utils/colorize.js";
|
|
26
27
|
export { applyTextStyles, type TextStyles } from "./utils/apply-text-styles.js";
|
package/lib/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export { useFocus, } from "./hooks/use-focus.js";
|
|
|
19
19
|
export { useFocusManager, } from "./hooks/use-focus-manager.js";
|
|
20
20
|
export { useIsScreenReaderEnabled } from "./hooks/use-is-screen-reader-enabled.js";
|
|
21
21
|
export { measureElement } from "./utils/measure-element.js";
|
|
22
|
+
export { useSizeObserver, } from "./hooks/use-size-observer.js";
|
|
22
23
|
export { applyTextStyles } from "./utils/apply-text-styles.js";
|
|
23
24
|
export { boxStyles } from "./core/box-styles.js";
|
|
24
25
|
export { EventEmitter } from "./utils/event-emitter.js";
|