tinky 0.1.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.
Files changed (99) hide show
  1. package/LICENSE +47 -0
  2. package/README.ja-JP.md +350 -0
  3. package/README.md +350 -0
  4. package/README.zh-CN.md +350 -0
  5. package/lib/colorize.d.ts +16 -0
  6. package/lib/colorize.js +55 -0
  7. package/lib/components/AccessibilityContext.d.ts +9 -0
  8. package/lib/components/AccessibilityContext.js +10 -0
  9. package/lib/components/App.d.ts +169 -0
  10. package/lib/components/App.js +314 -0
  11. package/lib/components/AppContext.d.ts +14 -0
  12. package/lib/components/AppContext.js +11 -0
  13. package/lib/components/BackgroundContext.d.ts +10 -0
  14. package/lib/components/BackgroundContext.js +5 -0
  15. package/lib/components/Box.d.ts +153 -0
  16. package/lib/components/Box.js +57 -0
  17. package/lib/components/ErrorOverview.d.ts +9 -0
  18. package/lib/components/ErrorOverview.js +49 -0
  19. package/lib/components/FocusContext.d.ts +51 -0
  20. package/lib/components/FocusContext.js +35 -0
  21. package/lib/components/Newline.d.ts +29 -0
  22. package/lib/components/Newline.js +21 -0
  23. package/lib/components/Spacer.d.ts +19 -0
  24. package/lib/components/Spacer.js +23 -0
  25. package/lib/components/Static.d.ts +48 -0
  26. package/lib/components/Static.js +46 -0
  27. package/lib/components/StderrContext.d.ts +21 -0
  28. package/lib/components/StderrContext.js +12 -0
  29. package/lib/components/StdinContext.d.ts +36 -0
  30. package/lib/components/StdinContext.js +16 -0
  31. package/lib/components/StdoutContext.d.ts +22 -0
  32. package/lib/components/StdoutContext.js +13 -0
  33. package/lib/components/Text.d.ts +84 -0
  34. package/lib/components/Text.js +74 -0
  35. package/lib/components/Transform.d.ts +41 -0
  36. package/lib/components/Transform.js +32 -0
  37. package/lib/devtools-window-polyfill.d.ts +1 -0
  38. package/lib/devtools-window-polyfill.js +70 -0
  39. package/lib/devtools.d.ts +1 -0
  40. package/lib/devtools.js +8 -0
  41. package/lib/dom.d.ts +130 -0
  42. package/lib/dom.js +209 -0
  43. package/lib/get-max-width.d.ts +9 -0
  44. package/lib/get-max-width.js +14 -0
  45. package/lib/hooks/use-app.d.ts +26 -0
  46. package/lib/hooks/use-app.js +28 -0
  47. package/lib/hooks/use-focus-manager.d.ts +40 -0
  48. package/lib/hooks/use-focus-manager.js +18 -0
  49. package/lib/hooks/use-focus.d.ts +67 -0
  50. package/lib/hooks/use-focus.js +65 -0
  51. package/lib/hooks/use-input.d.ts +118 -0
  52. package/lib/hooks/use-input.js +101 -0
  53. package/lib/hooks/use-is-screen-reader-enabled.d.ts +7 -0
  54. package/lib/hooks/use-is-screen-reader-enabled.js +12 -0
  55. package/lib/hooks/use-stderr.d.ts +6 -0
  56. package/lib/hooks/use-stderr.js +8 -0
  57. package/lib/hooks/use-stdin.d.ts +6 -0
  58. package/lib/hooks/use-stdin.js +8 -0
  59. package/lib/hooks/use-stdout.d.ts +7 -0
  60. package/lib/hooks/use-stdout.js +9 -0
  61. package/lib/index.d.ts +22 -0
  62. package/lib/index.js +20 -0
  63. package/lib/instances.d.ts +10 -0
  64. package/lib/instances.js +9 -0
  65. package/lib/log-update.d.ts +32 -0
  66. package/lib/log-update.js +147 -0
  67. package/lib/measure-element.d.ts +19 -0
  68. package/lib/measure-element.js +13 -0
  69. package/lib/measure-text.d.ts +13 -0
  70. package/lib/measure-text.js +26 -0
  71. package/lib/output.d.ts +73 -0
  72. package/lib/output.js +190 -0
  73. package/lib/parse-keypress.d.ts +20 -0
  74. package/lib/parse-keypress.js +228 -0
  75. package/lib/reconciler.d.ts +12 -0
  76. package/lib/reconciler.js +282 -0
  77. package/lib/render-background.d.ts +11 -0
  78. package/lib/render-background.js +32 -0
  79. package/lib/render-border.d.ts +13 -0
  80. package/lib/render-border.js +82 -0
  81. package/lib/render-node-to-output.d.ts +43 -0
  82. package/lib/render-node-to-output.js +179 -0
  83. package/lib/render.d.ts +129 -0
  84. package/lib/render.js +84 -0
  85. package/lib/renderer.d.ts +27 -0
  86. package/lib/renderer.js +62 -0
  87. package/lib/signal-exit.d.ts +11 -0
  88. package/lib/signal-exit.js +24 -0
  89. package/lib/squash-text-nodes.d.ts +10 -0
  90. package/lib/squash-text-nodes.js +36 -0
  91. package/lib/styles.d.ts +402 -0
  92. package/lib/styles.js +633 -0
  93. package/lib/taffy-node.d.ts +39 -0
  94. package/lib/taffy-node.js +60 -0
  95. package/lib/tinky.d.ts +153 -0
  96. package/lib/tinky.js +396 -0
  97. package/lib/wrap-text.d.ts +11 -0
  98. package/lib/wrap-text.js +38 -0
  99. package/package.json +87 -0
@@ -0,0 +1,13 @@
1
+ import process from "node:process";
2
+ import { createContext } from "react";
3
+ /**
4
+ * `StdoutContext` is a React context that exposes the stdout stream where
5
+ * Tinky renders your app.
6
+ */
7
+ export const StdoutContext = createContext({
8
+ stdout: process.stdout,
9
+ write() {
10
+ // no-op
11
+ },
12
+ });
13
+ StdoutContext.displayName = "InternalStdoutContext";
@@ -0,0 +1,84 @@
1
+ import { type ReactNode } from "react";
2
+ import { type ForegroundColorName } from "chalk";
3
+ import { type LiteralUnion } from "type-fest";
4
+ import { type Styles } from "../styles.js";
5
+ /**
6
+ * Props for the Text component.
7
+ */
8
+ export interface TextProps {
9
+ /**
10
+ * A label for the element for screen readers.
11
+ */
12
+ readonly "aria-label"?: string;
13
+ /**
14
+ * Hide the element from screen readers.
15
+ */
16
+ readonly "aria-hidden"?: boolean;
17
+ /**
18
+ * Change text color. Tinky uses Chalk under the hood, so all its functionality
19
+ * is supported.
20
+ */
21
+ readonly color?: LiteralUnion<ForegroundColorName, string>;
22
+ /**
23
+ * Same as `color`, but for the background.
24
+ */
25
+ readonly backgroundColor?: LiteralUnion<ForegroundColorName, string>;
26
+ /**
27
+ * Dim the color (make it less bright).
28
+ */
29
+ readonly dimColor?: boolean;
30
+ /**
31
+ * Make the text bold.
32
+ */
33
+ readonly bold?: boolean;
34
+ /**
35
+ * Make the text italic.
36
+ */
37
+ readonly italic?: boolean;
38
+ /**
39
+ * Make the text underlined.
40
+ */
41
+ readonly underline?: boolean;
42
+ /**
43
+ * Make the text crossed out with a line.
44
+ */
45
+ readonly strikethrough?: boolean;
46
+ /**
47
+ * Inverse background and foreground colors.
48
+ */
49
+ readonly inverse?: boolean;
50
+ /**
51
+ * This property tells Tinky to wrap or truncate text if its width is larger
52
+ * than the container. If `wrap` is passed (the default), Tinky will wrap text
53
+ * and split it into multiple lines. If `truncate-*` is passed, Tinky will
54
+ * truncate text instead, resulting in one line with the rest cut off.
55
+ */
56
+ readonly wrap?: Styles["textWrap"];
57
+ /**
58
+ * Children of the component.
59
+ */
60
+ readonly children?: ReactNode;
61
+ }
62
+ /**
63
+ * This component can display text and change its style to make it bold,
64
+ * underlined, italic, or strikethrough.
65
+ *
66
+ * @example
67
+ * ```tsx
68
+ * import {render, Text} from 'tinky';
69
+ *
70
+ * render(<Text color="green">I am green</Text>);
71
+ * ```
72
+ *
73
+ * @example
74
+ * ```tsx
75
+ * import {render, Text} from 'tinky';
76
+ *
77
+ * render(
78
+ * <Text bold backgroundColor="blue">
79
+ * I am bold on blue background
80
+ * </Text>
81
+ * );
82
+ * ```
83
+ */
84
+ export declare function Text({ color, backgroundColor, dimColor, bold, italic, underline, strikethrough, inverse, wrap, children, "aria-label": ariaLabel, "aria-hidden": ariaHidden, }: TextProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useContext } from "react";
3
+ import chalk from "chalk";
4
+ import { colorize } from "../colorize.js";
5
+ import { AccessibilityContext } from "./AccessibilityContext.js";
6
+ import { backgroundContext } from "./BackgroundContext.js";
7
+ /**
8
+ * This component can display text and change its style to make it bold,
9
+ * underlined, italic, or strikethrough.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * import {render, Text} from 'tinky';
14
+ *
15
+ * render(<Text color="green">I am green</Text>);
16
+ * ```
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * import {render, Text} from 'tinky';
21
+ *
22
+ * render(
23
+ * <Text bold backgroundColor="blue">
24
+ * I am bold on blue background
25
+ * </Text>
26
+ * );
27
+ * ```
28
+ */
29
+ export function Text({ color, backgroundColor, dimColor = false, bold = false, italic = false, underline = false, strikethrough = false, inverse = false, wrap = "wrap", children, "aria-label": ariaLabel, "aria-hidden": ariaHidden = false, }) {
30
+ const { isScreenReaderEnabled } = useContext(AccessibilityContext);
31
+ const inheritedBackgroundColor = useContext(backgroundContext);
32
+ const childrenOrAriaLabel = isScreenReaderEnabled && ariaLabel ? ariaLabel : children;
33
+ if (childrenOrAriaLabel === undefined || childrenOrAriaLabel === null) {
34
+ return null;
35
+ }
36
+ const transform = (children) => {
37
+ if (dimColor) {
38
+ children = chalk.dim(children);
39
+ }
40
+ if (color) {
41
+ children = colorize(children, color, "foreground");
42
+ }
43
+ // Use explicit backgroundColor if provided, otherwise use inherited from parent Box
44
+ const effectiveBackgroundColor = backgroundColor ?? inheritedBackgroundColor;
45
+ if (effectiveBackgroundColor) {
46
+ children = colorize(children, effectiveBackgroundColor, "background");
47
+ }
48
+ if (bold) {
49
+ children = chalk.bold(children);
50
+ }
51
+ if (italic) {
52
+ children = chalk.italic(children);
53
+ }
54
+ if (underline) {
55
+ children = chalk.underline(children);
56
+ }
57
+ if (strikethrough) {
58
+ children = chalk.strikethrough(children);
59
+ }
60
+ if (inverse) {
61
+ children = chalk.inverse(children);
62
+ }
63
+ return children;
64
+ };
65
+ if (isScreenReaderEnabled && ariaHidden) {
66
+ return null;
67
+ }
68
+ return (_jsx("tinky-text", { style: {
69
+ flexGrow: 0,
70
+ flexShrink: 1,
71
+ flexDirection: "row",
72
+ textWrap: wrap,
73
+ }, internal_transform: transform, children: isScreenReaderEnabled && ariaLabel ? ariaLabel : children }));
74
+ }
@@ -0,0 +1,41 @@
1
+ import { type ReactNode } from "react";
2
+ /**
3
+ * Props for the Transform component.
4
+ */
5
+ export interface TransformProps {
6
+ /**
7
+ * Screen-reader-specific text to output. If set, all children will be
8
+ * ignored.
9
+ */
10
+ readonly accessibilityLabel?: string;
11
+ /**
12
+ * Function that transforms children output. It accepts children and must
13
+ * return transformed children as well.
14
+ */
15
+ readonly transform: (children: string, index: number) => string;
16
+ /**
17
+ * Children to transform.
18
+ */
19
+ readonly children?: ReactNode;
20
+ }
21
+ /**
22
+ * Transform a string representation of React components before they're written
23
+ * to output. For example, you might want to apply a gradient to text, add a
24
+ * clickable link, or create some text effects. These use cases can't accept
25
+ * React nodes as input; they expect a string. That's what the <Transform>
26
+ * component does: it gives you an output string of its child components and
27
+ * lets you transform it in any way.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * import {render, Transform, Text} from 'tinky';
32
+ *
33
+ * render(
34
+ * <Transform transform={output => output.toUpperCase()}>
35
+ * <Text>Hello World</Text>
36
+ * </Transform>
37
+ * );
38
+ * // Output: HELLO WORLD
39
+ * ```
40
+ */
41
+ export declare function Transform({ children, transform, accessibilityLabel, }: TransformProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useContext } from "react";
3
+ import { AccessibilityContext } from "./AccessibilityContext.js";
4
+ /**
5
+ * Transform a string representation of React components before they're written
6
+ * to output. For example, you might want to apply a gradient to text, add a
7
+ * clickable link, or create some text effects. These use cases can't accept
8
+ * React nodes as input; they expect a string. That's what the <Transform>
9
+ * component does: it gives you an output string of its child components and
10
+ * lets you transform it in any way.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * import {render, Transform, Text} from 'tinky';
15
+ *
16
+ * render(
17
+ * <Transform transform={output => output.toUpperCase()}>
18
+ * <Text>Hello World</Text>
19
+ * </Transform>
20
+ * );
21
+ * // Output: HELLO WORLD
22
+ * ```
23
+ */
24
+ export function Transform({ children, transform, accessibilityLabel, }) {
25
+ const { isScreenReaderEnabled } = useContext(AccessibilityContext);
26
+ if (children === undefined || children === null) {
27
+ return null;
28
+ }
29
+ return (_jsx("tinky-text", { style: { flexGrow: 0, flexShrink: 1, flexDirection: "row" }, internal_transform: transform, children: isScreenReaderEnabled && accessibilityLabel
30
+ ? accessibilityLabel
31
+ : children }));
32
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ import ws from "ws";
2
+ const customGlobal = global;
3
+ // Polyfill WebSocket for React DevTools backend communication.
4
+ // Node.js does not have a native WebSocket implementation, so we use 'ws'.
5
+ customGlobal.WebSocket ||= ws;
6
+ // Polyfill 'window' and 'self' to point to the global object.
7
+ // React DevTools expects a browser-like environment where these exist.
8
+ customGlobal.window ||= global;
9
+ customGlobal.self ||= global;
10
+ // Configure filters to hide internal Tinky components from the React DevTools
11
+ // component tree.
12
+ // This keeps the DevTools view clean, showing only the user's components.
13
+ customGlobal.window.__REACT_DEVTOOLS_COMPONENT_FILTERS__ = [
14
+ {
15
+ // ComponentFilterElementType
16
+ type: 1,
17
+ // ElementTypeHostComponent: Hide host components (e.g. text nodes rendered
18
+ // by Tinky)
19
+ value: 7,
20
+ isEnabled: true,
21
+ },
22
+ {
23
+ // ComponentFilterDisplayName
24
+ type: 2,
25
+ // Hide the internal wrapper component 'InternalApp'
26
+ value: "InternalApp",
27
+ isEnabled: true,
28
+ isValid: true,
29
+ },
30
+ {
31
+ // ComponentFilterDisplayName
32
+ type: 2,
33
+ // Hide the internal AppContext provider
34
+ value: "InternalAppContext",
35
+ isEnabled: true,
36
+ isValid: true,
37
+ },
38
+ {
39
+ // ComponentFilterDisplayName
40
+ type: 2,
41
+ // Hide the internal StdoutContext provider
42
+ value: "InternalStdoutContext",
43
+ isEnabled: true,
44
+ isValid: true,
45
+ },
46
+ {
47
+ // ComponentFilterDisplayName
48
+ type: 2,
49
+ // Hide the internal StderrContext provider
50
+ value: "InternalStderrContext",
51
+ isEnabled: true,
52
+ isValid: true,
53
+ },
54
+ {
55
+ // ComponentFilterDisplayName
56
+ type: 2,
57
+ // Hide the internal StdinContext provider
58
+ value: "InternalStdinContext",
59
+ isEnabled: true,
60
+ isValid: true,
61
+ },
62
+ {
63
+ // ComponentFilterDisplayName
64
+ type: 2,
65
+ // Hide the internal FocusContext provider
66
+ value: "InternalFocusContext",
67
+ isEnabled: true,
68
+ isValid: true,
69
+ },
70
+ ];
@@ -0,0 +1 @@
1
+ import "./devtools-window-polyfill.js";
@@ -0,0 +1,8 @@
1
+ import "./devtools-window-polyfill.js";
2
+ import devtools from "react-devtools-core";
3
+ /**
4
+ * Initializes React DevTools for Tinky functionality.
5
+ * This sets up the WebSocket connection required for the standalone DevTools.
6
+ */
7
+ devtools.initialize();
8
+ devtools.connectToDevTools();
package/lib/dom.d.ts ADDED
@@ -0,0 +1,130 @@
1
+ import { type Styles } from "./styles.js";
2
+ import { type OutputTransformer } from "./render-node-to-output.js";
3
+ import { TaffyNode } from "./taffy-node.js";
4
+ /**
5
+ * Interface representing a node in the Tinky tree.
6
+ */
7
+ interface TinkyNode {
8
+ parentNode: DOMElement | undefined;
9
+ taffyNode?: TaffyNode;
10
+ internal_static?: boolean;
11
+ style: Styles;
12
+ }
13
+ /**
14
+ * Type representing a text node name.
15
+ */
16
+ export type TextName = "#text";
17
+ /**
18
+ * Type representing element names in the Tinky DOM.
19
+ */
20
+ export type ElementNames = "tinky-root" | "tinky-box" | "tinky-text" | "tinky-virtual-text";
21
+ /**
22
+ * Union type of all possible node names.
23
+ */
24
+ export type NodeNames = ElementNames | TextName;
25
+ /**
26
+ * Interface representing a DOM element in Tinky.
27
+ */
28
+ export type DOMElement = {
29
+ nodeName: ElementNames;
30
+ attributes: Record<string, DOMNodeAttribute>;
31
+ childNodes: DOMNode[];
32
+ internal_transform?: OutputTransformer;
33
+ internal_accessibility?: {
34
+ role?: "button" | "checkbox" | "combobox" | "list" | "listbox" | "listitem" | "menu" | "menuitem" | "option" | "progressbar" | "radio" | "radiogroup" | "tab" | "tablist" | "table" | "textbox" | "timer" | "toolbar";
35
+ state?: {
36
+ busy?: boolean;
37
+ checked?: boolean;
38
+ disabled?: boolean;
39
+ expanded?: boolean;
40
+ multiline?: boolean;
41
+ multiselectable?: boolean;
42
+ readonly?: boolean;
43
+ required?: boolean;
44
+ selected?: boolean;
45
+ };
46
+ };
47
+ isStaticDirty?: boolean;
48
+ staticNode?: DOMElement;
49
+ onComputeLayout?: () => void;
50
+ onRender?: () => void;
51
+ onImmediateRender?: () => void;
52
+ } & TinkyNode;
53
+ /**
54
+ * Interface representing a text node in Tinky.
55
+ */
56
+ export type TextNode = {
57
+ nodeName: TextName;
58
+ nodeValue: string;
59
+ } & TinkyNode;
60
+ /**
61
+ * Union type representing either a DOM element or a text node.
62
+ */
63
+ export type DOMNode<T = {
64
+ nodeName: NodeNames;
65
+ }> = T extends {
66
+ nodeName: infer U;
67
+ } ? U extends "#text" ? TextNode : DOMElement : never;
68
+ /**
69
+ * Type representing possible types for DOM node attributes.
70
+ */
71
+ export type DOMNodeAttribute = boolean | string | number;
72
+ /**
73
+ * Creates a new DOM element.
74
+ *
75
+ * @param nodeName - The name of the node to create.
76
+ * @returns The created DOM element.
77
+ */
78
+ export declare const createNode: (nodeName: ElementNames) => DOMElement;
79
+ /**
80
+ * Appends a child node to a parent node.
81
+ *
82
+ * @param node - The parent node.
83
+ * @param childNode - The child node to append.
84
+ */
85
+ export declare const appendChildNode: (node: DOMElement, childNode: DOMElement) => void;
86
+ /**
87
+ * Inserts a new child node before a reference node.
88
+ *
89
+ * @param node - The parent node.
90
+ * @param newChildNode - The new child node to insert.
91
+ * @param beforeChildNode - The reference node before which to insert the new node.
92
+ */
93
+ export declare const insertBeforeNode: (node: DOMElement, newChildNode: DOMNode, beforeChildNode: DOMNode) => void;
94
+ /**
95
+ * Removes a child node from a parent node.
96
+ *
97
+ * @param node - The parent node.
98
+ * @param removeNode - The child node to remove.
99
+ */
100
+ export declare const removeChildNode: (node: DOMElement, removeNode: DOMNode) => void;
101
+ /**
102
+ * Sets an attribute on a DOM element.
103
+ *
104
+ * @param node - The DOM element.
105
+ * @param key - The attribute key.
106
+ * @param value - The attribute value.
107
+ */
108
+ export declare const setAttribute: (node: DOMElement, key: string, value: DOMNodeAttribute) => void;
109
+ /**
110
+ * Sets the style of a DOM node.
111
+ *
112
+ * @param node - The DOM node.
113
+ * @param style - The style object.
114
+ */
115
+ export declare const setStyle: (node: DOMNode, style: Styles) => void;
116
+ /**
117
+ * Creates a text node.
118
+ *
119
+ * @param text - The text content.
120
+ * @returns The created text node.
121
+ */
122
+ export declare const createTextNode: (text: string) => TextNode;
123
+ /**
124
+ * Sets the value of a text node.
125
+ *
126
+ * @param node - The text node.
127
+ * @param text - The new text value.
128
+ */
129
+ export declare const setTextNodeValue: (node: TextNode, text: string) => void;
130
+ export {};
package/lib/dom.js ADDED
@@ -0,0 +1,209 @@
1
+ import stringWidth from "string-width";
2
+ import { measureText } from "./measure-text.js";
3
+ import { wrapText } from "./wrap-text.js";
4
+ import { squashTextNodes } from "./squash-text-nodes.js";
5
+ import { TaffyNode } from "./taffy-node.js";
6
+ /**
7
+ * Creates a new DOM element.
8
+ *
9
+ * @param nodeName - The name of the node to create.
10
+ * @returns The created DOM element.
11
+ */
12
+ export const createNode = (nodeName) => {
13
+ const node = {
14
+ nodeName,
15
+ style: {},
16
+ attributes: {},
17
+ childNodes: [],
18
+ parentNode: undefined,
19
+ taffyNode: nodeName === "tinky-virtual-text" ? undefined : new TaffyNode(nodeName),
20
+ internal_accessibility: {},
21
+ };
22
+ if (nodeName === "tinky-text" && node.taffyNode) {
23
+ node.taffyNode.measureFunc = measureTextNode.bind(null, node);
24
+ }
25
+ return node;
26
+ };
27
+ /**
28
+ * Appends a child node to a parent node.
29
+ *
30
+ * @param node - The parent node.
31
+ * @param childNode - The child node to append.
32
+ */
33
+ export const appendChildNode = (node, childNode) => {
34
+ if (childNode.parentNode) {
35
+ removeChildNode(childNode.parentNode, childNode);
36
+ }
37
+ childNode.parentNode = node;
38
+ node.childNodes.push(childNode);
39
+ if (childNode.taffyNode) {
40
+ node.taffyNode?.tree.addChild(node.taffyNode.id, childNode.taffyNode.id);
41
+ }
42
+ if (node.nodeName === "tinky-text" ||
43
+ node.nodeName === "tinky-virtual-text") {
44
+ markNodeAsDirty(node);
45
+ }
46
+ };
47
+ /**
48
+ * Inserts a new child node before a reference node.
49
+ *
50
+ * @param node - The parent node.
51
+ * @param newChildNode - The new child node to insert.
52
+ * @param beforeChildNode - The reference node before which to insert the new node.
53
+ */
54
+ export const insertBeforeNode = (node, newChildNode, beforeChildNode) => {
55
+ if (newChildNode.parentNode) {
56
+ removeChildNode(newChildNode.parentNode, newChildNode);
57
+ }
58
+ newChildNode.parentNode = node;
59
+ const index = node.childNodes.indexOf(beforeChildNode);
60
+ if (index >= 0) {
61
+ node.childNodes.splice(index, 0, newChildNode);
62
+ if (newChildNode.taffyNode) {
63
+ node.taffyNode?.tree.insertChildAtIndex(node.taffyNode.id, index, newChildNode.taffyNode.id);
64
+ }
65
+ return;
66
+ }
67
+ node.childNodes.push(newChildNode);
68
+ if (newChildNode.taffyNode) {
69
+ node.taffyNode?.tree.addChild(node.taffyNode.id, newChildNode.taffyNode.id);
70
+ }
71
+ if (node.nodeName === "tinky-text" ||
72
+ node.nodeName === "tinky-virtual-text") {
73
+ markNodeAsDirty(node);
74
+ }
75
+ };
76
+ /**
77
+ * Removes a child node from a parent node.
78
+ *
79
+ * @param node - The parent node.
80
+ * @param removeNode - The child node to remove.
81
+ */
82
+ export const removeChildNode = (node, removeNode) => {
83
+ if (removeNode.taffyNode) {
84
+ removeNode.parentNode?.taffyNode?.tree.removeChild(removeNode.parentNode.taffyNode.id, removeNode.taffyNode.id);
85
+ }
86
+ removeNode.parentNode = undefined;
87
+ const index = node.childNodes.indexOf(removeNode);
88
+ if (index >= 0) {
89
+ node.childNodes.splice(index, 1);
90
+ }
91
+ if (node.nodeName === "tinky-text" ||
92
+ node.nodeName === "tinky-virtual-text") {
93
+ markNodeAsDirty(node);
94
+ }
95
+ };
96
+ /**
97
+ * Sets an attribute on a DOM element.
98
+ *
99
+ * @param node - The DOM element.
100
+ * @param key - The attribute key.
101
+ * @param value - The attribute value.
102
+ */
103
+ export const setAttribute = (node, key, value) => {
104
+ if (key === "internal_accessibility") {
105
+ node.internal_accessibility = value;
106
+ return;
107
+ }
108
+ node.attributes[key] = value;
109
+ };
110
+ /**
111
+ * Sets the style of a DOM node.
112
+ *
113
+ * @param node - The DOM node.
114
+ * @param style - The style object.
115
+ */
116
+ export const setStyle = (node, style) => {
117
+ node.style = style;
118
+ };
119
+ /**
120
+ * Creates a text node.
121
+ *
122
+ * @param text - The text content.
123
+ * @returns The created text node.
124
+ */
125
+ export const createTextNode = (text) => {
126
+ const node = {
127
+ nodeName: "#text",
128
+ nodeValue: text,
129
+ taffyNode: undefined,
130
+ parentNode: undefined,
131
+ style: {},
132
+ };
133
+ setTextNodeValue(node, text);
134
+ return node;
135
+ };
136
+ /**
137
+ * Measures the dimensions of a text node.
138
+ *
139
+ * @param node - The text node to measure.
140
+ * @param width - The available width constraint.
141
+ * @returns The measured width and height.
142
+ */
143
+ const measureTextNode = function (node, width) {
144
+ const text = node.nodeName === "#text" ? node.nodeValue : squashTextNodes(node);
145
+ // For minContent mode, compute the minimum possible width for the text.
146
+ // This is the width of the widest character (e.g., emojis are typically 2
147
+ // columns, ASCII chars are 1).
148
+ if (width === "min-content") {
149
+ const chars = [...text];
150
+ const maxCharWidth = Math.max(...chars.map((c) => stringWidth(c)), 1);
151
+ const textWrap = node.style?.textWrap ?? "wrap";
152
+ const wrappedText = wrapText(text, maxCharWidth, textWrap);
153
+ return measureText(wrappedText);
154
+ }
155
+ const dimensions = measureText(text);
156
+ // For maxContent mode, return the natural text dimensions without wrapping
157
+ if (width === "max-content") {
158
+ return dimensions;
159
+ }
160
+ // For definite mode with width constraint:
161
+ // Text fits into container, no need to wrap
162
+ if (dimensions.width <= width) {
163
+ return dimensions;
164
+ }
165
+ // This is happening when <Box> is shrinking child nodes and layout engine
166
+ // asks if we can fit this text node in a <1px space, so we just tell it "no"
167
+ if (dimensions.width >= 1 && width > 0 && width < 1) {
168
+ return dimensions;
169
+ }
170
+ const textWrap = node.style?.textWrap ?? "wrap";
171
+ const wrappedText = wrapText(text, width, textWrap);
172
+ return measureText(wrappedText);
173
+ };
174
+ /**
175
+ * Finds the closest Taffy node for a given DOM node.
176
+ * Traverses up the DOM tree until a node with a Taffy node is found.
177
+ *
178
+ * @param node - The DOM node to start searching from.
179
+ * @returns The closest Taffy node or undefined if not found.
180
+ */
181
+ const findClosestTaffyNode = (node) => {
182
+ if (!node?.parentNode) {
183
+ return undefined;
184
+ }
185
+ return node.taffyNode ?? findClosestTaffyNode(node.parentNode);
186
+ };
187
+ /**
188
+ * Marks a node as dirty, triggering a re-measure of its layout.
189
+ *
190
+ * @param node - The DOM node to mark as dirty.
191
+ */
192
+ const markNodeAsDirty = (node) => {
193
+ // Mark closest Taffy node as dirty to measure text dimensions again
194
+ const taffyNode = findClosestTaffyNode(node);
195
+ taffyNode?.tree.markDirty(taffyNode.id);
196
+ };
197
+ /**
198
+ * Sets the value of a text node.
199
+ *
200
+ * @param node - The text node.
201
+ * @param text - The new text value.
202
+ */
203
+ export const setTextNodeValue = (node, text) => {
204
+ if (typeof text !== "string") {
205
+ text = String(text);
206
+ }
207
+ node.nodeValue = text;
208
+ markNodeAsDirty(node);
209
+ };
@@ -0,0 +1,9 @@
1
+ import { type Layout } from "taffy-layout";
2
+ /**
3
+ * Calculates the maximum available width for content within a Taffy layout.
4
+ * It subtracts padding and borders from the total width of the element.
5
+ *
6
+ * @param layout - The Taffy layout object with width, padding, and border info.
7
+ * @returns The maximum width available for content.
8
+ */
9
+ export declare const getMaxWidth: (layout: Layout) => number;