ink-framework 0.1.0-alpha.12 → 0.1.0-alpha.13
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/DESIGN.md +168 -0
- package/package.json +8 -4
- package/src/compiler-runtime.ts +6 -0
- package/src/controller-react.ts +33 -0
- package/src/file-router.tsx +1 -1
- package/src/fixed-list-row.ts +65 -0
- package/src/image.ts +16 -2
- package/src/index.ts +29 -2
- package/src/jsx-runtime.ts +21 -0
- package/src/list.ts +71 -34
- package/src/native-list.ts +12 -18
- package/src/native.ts +8 -0
- package/src/prefresh.d.ts +3 -0
- package/src/react-compat.ts +24 -0
- package/src/react.ts +26 -6
- package/src/refresh.ts +29 -0
- package/src/renderer.ts +183 -140
- package/src/reorder.ts +12 -15
- package/src/visibility.ts +264 -0
package/src/native-list.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { createElement,
|
|
2
|
-
import {
|
|
3
|
-
import { useAction } from "./action";
|
|
1
|
+
import { createElement, useLayoutEffect, useMemo, useRef, useState, type ComponentProps, type ReactNode } from "./react";
|
|
2
|
+
import { ReactList, useListPaginationEvents, type ListPagination, type ListProps } from "./list";
|
|
4
3
|
import type { Row } from "./image";
|
|
5
4
|
import { pressHandler } from "./press";
|
|
6
5
|
import { rememberListPatch, type NativeListItem } from "./list-patch";
|
|
7
6
|
|
|
7
|
+
export { fixedListRow } from "./fixed-list-row";
|
|
8
|
+
|
|
8
9
|
type Captured = { value: unknown; prototype?: object | null; fields?: [PropertyKey, boolean, Captured][] };
|
|
9
10
|
type Scope = { identity: string; values: readonly Captured[] };
|
|
10
11
|
type Capture = { identity: string; values: () => readonly unknown[]; usesIndex?: boolean };
|
|
@@ -73,7 +74,7 @@ export function listText(parts: unknown[]): string {
|
|
|
73
74
|
export function listRowFields(props: ComponentProps<typeof Row>) {
|
|
74
75
|
return { image: props.image, title: props.title, titleMaxLines: props.titleMaxLines,
|
|
75
76
|
titleIcon: props.titleIcon, subtitle: props.subtitle, subtitleIcon: props.subtitleIcon,
|
|
76
|
-
onLongPress: props.onLongPress,
|
|
77
|
+
selected: props.selected, onLongPress: props.onLongPress,
|
|
77
78
|
onPress: pressHandler(props) ?? (props.onLongPress ? () => {} : undefined) };
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -286,11 +287,9 @@ export function nativeListPlan<T>(render: ListProps<T>["renderItem"]): Plan<T> |
|
|
|
286
287
|
return plans.get(render) as Plan<T> | undefined;
|
|
287
288
|
}
|
|
288
289
|
|
|
289
|
-
export function CompiledList<T>({ plan, ...props }: ListProps<T> & { plan: Plan<T
|
|
290
|
-
const { items, keyExtractor, gap = 47, followEnd, initialPosition, measurementKey
|
|
290
|
+
export function CompiledList<T>({ plan, pagination, ...props }: ListProps<T> & { plan: Plan<T>; pagination: ListPagination }) {
|
|
291
|
+
const { items, keyExtractor, gap = 47, followEnd, initialPosition, measurementKey } = props;
|
|
291
292
|
if (!Number.isFinite(gap) || gap < 0) throw new Error("List gap must be finite and non-negative");
|
|
292
|
-
const load = useAction(() => onLoadMore?.());
|
|
293
|
-
const older = useAction(() => onLoadOlder?.());
|
|
294
293
|
const committed = useRef<Records<T> | null>(null);
|
|
295
294
|
const committedRequest = useRef<Request<T> | null>(null);
|
|
296
295
|
const committedPlan = useRef<Plan<T> | null>(null);
|
|
@@ -388,14 +387,13 @@ export function CompiledList<T>({ plan, ...props }: ListProps<T> & { plan: Plan<
|
|
|
388
387
|
if (timer !== undefined) clearTimeout(timer);
|
|
389
388
|
};
|
|
390
389
|
}, [request]);
|
|
390
|
+
const paginationProps = useListPaginationEvents(pagination, preparing || !records,
|
|
391
|
+
records?.data.length ?? 0, records?.data[0]?.key, records?.data.at(-1)?.key);
|
|
391
392
|
if (outcome?.status === "error") throw outcome.error;
|
|
392
|
-
if (!records) return createElement(ReactList<T>, props);
|
|
393
|
+
if (!records) return createElement(ReactList<T>, { ...props, pagination });
|
|
393
394
|
const nativeProps = {
|
|
394
395
|
items: records.data, keyField: "key", template: records.template, gap, followEnd, initialEnd: initialPosition === "end",
|
|
395
|
-
|
|
396
|
-
hasOlder: !preparing && hasOlder && older.status !== "pending" && older.status !== "error",
|
|
397
|
-
onEndReached: !preparing && onLoadMore ? load.run : undefined,
|
|
398
|
-
onStartReached: !preparing && onLoadOlder ? older.run : undefined,
|
|
396
|
+
...paginationProps,
|
|
399
397
|
onRowEvent(key: string, target: number, name: string, ...args: unknown[]) {
|
|
400
398
|
const latest = events.current;
|
|
401
399
|
const path = latest?.targets.get(target)?.[name];
|
|
@@ -432,11 +430,7 @@ export function CompiledList<T>({ plan, ...props }: ListProps<T> & { plan: Plan<
|
|
|
432
430
|
if (typeof callback === "function") callback(...args);
|
|
433
431
|
},
|
|
434
432
|
};
|
|
435
|
-
return createElement(
|
|
436
|
-
older.status === "error" && createElement(ListLoadError, { message: older.error.message, onRetry: older.run }),
|
|
437
|
-
createElement("NativeList", nativeProps),
|
|
438
|
-
load.status === "error" && createElement(ListLoadError, { message: load.error.message, onRetry: load.run }),
|
|
439
|
-
);
|
|
433
|
+
return createElement("NativeList", nativeProps);
|
|
440
434
|
}
|
|
441
435
|
|
|
442
436
|
/** @internal Fixed native descriptions for Ink's own list components. */
|
package/src/native.ts
CHANGED
|
@@ -34,6 +34,14 @@ Object.defineProperty(globalThis, "__inkReceive", {
|
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
Object.defineProperty(globalThis, "__inkResolveNativeResult", {
|
|
38
|
+
value: (id: number, value?: string, kind?: string, message?: string, retryable?: boolean) => {
|
|
39
|
+
listeners.get("result")?.(typeof value === "string"
|
|
40
|
+
? { id, type: "result", value }
|
|
41
|
+
: { id, kind, message, retryable, type: "result" });
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
37
45
|
onNativeMessage("result", message => {
|
|
38
46
|
if (typeof message.id !== "number") throw new Error("Invalid native result ID");
|
|
39
47
|
const request = pending.get(message.id);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "preact/compat";
|
|
2
|
+
import { cloneElement, isValidElement } from "preact";
|
|
3
|
+
import { Activity, useEffect, useLayoutEffect, useSyncExternalStore, useImperativeHandle } from "./visibility";
|
|
4
|
+
import { useRef, useCallback, useLayoutEffect as commitLayoutEffect } from "preact/hooks";
|
|
5
|
+
const compatibleReact = /* @__PURE__ */ Object.assign({}, React, { Activity, cloneElement, isValidElement, useEffect, useLayoutEffect, useSyncExternalStore, useImperativeHandle, useEffectEvent });
|
|
6
|
+
export default compatibleReact;
|
|
7
|
+
export {
|
|
8
|
+
Children, Component, Fragment, PureComponent, StrictMode, Suspense, SuspenseList,
|
|
9
|
+
createContext, createElement, createFactory, createPortal, createRef,
|
|
10
|
+
findDOMNode, flushSync, forwardRef, hydrate, lazy, memo, render,
|
|
11
|
+
startTransition, unmountComponentAtNode, unstable_batchedUpdates,
|
|
12
|
+
useCallback, useContext, useDebugValue, useDeferredValue,
|
|
13
|
+
useId, useInsertionEffect, useMemo, useReducer,
|
|
14
|
+
useRef, useState, useTransition, version,
|
|
15
|
+
} from "preact/compat";
|
|
16
|
+
export { cloneElement, isValidElement } from "preact";
|
|
17
|
+
export { Activity, useEffect, useLayoutEffect, useSyncExternalStore, useImperativeHandle } from "./visibility";
|
|
18
|
+
|
|
19
|
+
/** Effect events retain their identity and read the latest committed callback. */
|
|
20
|
+
export function useEffectEvent<T extends (...args: never[]) => unknown>(callback: T): T {
|
|
21
|
+
const current = useRef(callback);
|
|
22
|
+
commitLayoutEffect(() => { current.current = callback; });
|
|
23
|
+
return useCallback(((...args: Parameters<T>) => current.current(...args)) as T, []);
|
|
24
|
+
}
|
package/src/react.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { createElement as element, createContext as context, isValidElement as valid, Fragment as fragment, Component, type ComponentChildren } from "preact";
|
|
2
|
+
import { useCallback, useContext as contextValue, useMemo, useRef, useState } from "preact/hooks";
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
|
|
5
|
+
// Keep the public React declarations while executing Preact core in Ink.
|
|
6
|
+
export const createElement = element as unknown as typeof React.createElement;
|
|
7
|
+
export const createContext = context as unknown as typeof React.createContext;
|
|
8
|
+
export const useContext = contextValue as unknown as typeof React.useContext;
|
|
9
|
+
export const isValidElement = valid as unknown as typeof React.isValidElement;
|
|
10
|
+
export const Fragment = fragment as unknown as typeof React.Fragment;
|
|
11
|
+
export { Children } from "preact/compat";
|
|
12
|
+
export { useCallback, useMemo, useRef, useState };
|
|
13
|
+
export { Activity, useEffect, useLayoutEffect, useSyncExternalStore } from "./visibility";
|
|
14
|
+
export type { ComponentProps, ComponentType, ReactElement, ReactNode } from "react";
|
|
15
|
+
|
|
16
|
+
// A single core component avoids compat memo's extra function-component layer.
|
|
17
|
+
export function memo<P extends object>(render: (props: P) => React.ReactNode) {
|
|
18
|
+
return class extends Component<P> {
|
|
19
|
+
shouldComponentUpdate(next: P) {
|
|
20
|
+
const previous = this.props;
|
|
21
|
+
const keys = Object.keys(next) as (keyof P)[];
|
|
22
|
+
return keys.length !== Object.keys(previous).length || keys.some(key => !Object.is(previous[key], next[key]));
|
|
23
|
+
}
|
|
24
|
+
render() { return render(this.props) as ComponentChildren; }
|
|
25
|
+
} as unknown as (props: P) => React.ReactNode;
|
|
26
|
+
}
|
package/src/refresh.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import "@prefresh/core";
|
|
2
|
+
import { flush } from "@prefresh/utils";
|
|
3
|
+
|
|
4
|
+
declare const self: { __PREFRESH__: {
|
|
5
|
+
register(type: unknown, id: string): void;
|
|
6
|
+
sign(type: unknown, key: string, forceReset: boolean, getCustomHooks: (() => unknown[]) | undefined, status: string): string;
|
|
7
|
+
} };
|
|
8
|
+
|
|
9
|
+
export const register = (type: unknown, id: string) => self.__PREFRESH__.register(type, id);
|
|
10
|
+
export const performReactRefresh = flush;
|
|
11
|
+
|
|
12
|
+
// Oxc emits React Refresh's signature protocol; translate it to Prefresh.
|
|
13
|
+
export function createSignatureFunctionForTransform() {
|
|
14
|
+
let savedType: unknown;
|
|
15
|
+
let savedKey = "";
|
|
16
|
+
let savedReset = false;
|
|
17
|
+
let savedHooks: (() => unknown[]) | undefined;
|
|
18
|
+
let status = "begin";
|
|
19
|
+
return (type?: unknown, key?: string, forceReset = false, getCustomHooks?: () => unknown[]) => {
|
|
20
|
+
if (type) {
|
|
21
|
+
savedType = type;
|
|
22
|
+
savedKey = key ?? "";
|
|
23
|
+
savedReset = forceReset;
|
|
24
|
+
savedHooks = getCustomHooks;
|
|
25
|
+
}
|
|
26
|
+
status = self.__PREFRESH__.sign(savedType, savedKey, savedReset, savedHooks, status);
|
|
27
|
+
return type;
|
|
28
|
+
};
|
|
29
|
+
}
|
package/src/renderer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
declare const process: { env: { NODE_ENV: string } };
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
2
|
+
import { options, render as renderPreact, type Component, type ComponentChildren, type VNode } from "preact";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { Activity, activityChanged, isComponentVisible } from "./visibility";
|
|
5
5
|
import { onNativeMessage } from "./native";
|
|
6
6
|
import { pressHandler } from "./press";
|
|
7
7
|
import type { Destination } from "./navigation";
|
|
@@ -11,8 +11,74 @@ declare const __inkCommit: (operations: Operation[]) => void;
|
|
|
11
11
|
declare const __inkPost: (message: string) => void;
|
|
12
12
|
|
|
13
13
|
type Props = Record<string, unknown>;
|
|
14
|
-
|
|
15
|
-
type
|
|
14
|
+
const hostProperties = Symbol("ink.hostProps");
|
|
15
|
+
type HostProps = Props & { [hostProperties]?: Props };
|
|
16
|
+
// Preact only needs tree links and mutation methods; no browser DOM is involved.
|
|
17
|
+
class HostNode {
|
|
18
|
+
id: number;
|
|
19
|
+
props: Props = {};
|
|
20
|
+
parentNode: HostNode | null = null;
|
|
21
|
+
firstChild: HostNode | null = null;
|
|
22
|
+
lastChild: HostNode | null = null;
|
|
23
|
+
previousSibling: HostNode | null = null;
|
|
24
|
+
nextSibling: HostNode | null = null;
|
|
25
|
+
hidden = false;
|
|
26
|
+
mounted = false;
|
|
27
|
+
readonly nodeType: number;
|
|
28
|
+
readonly namespaceURI = "http://www.w3.org/1999/xhtml";
|
|
29
|
+
constructor(readonly type: string, text?: string) {
|
|
30
|
+
this.id = type === "#root" ? 0 : allocateHostId();
|
|
31
|
+
this.nodeType = type === "#text" ? 3 : 1;
|
|
32
|
+
if (text !== undefined) this.props = { text: String(text) };
|
|
33
|
+
}
|
|
34
|
+
get localName() { return this.type; }
|
|
35
|
+
get childNodes(): HostNode[] {
|
|
36
|
+
const nodes: HostNode[] = [];
|
|
37
|
+
for (let child = this.firstChild; child; child = child.nextSibling) nodes.push(child);
|
|
38
|
+
return nodes;
|
|
39
|
+
}
|
|
40
|
+
get data() { return this.props.text; }
|
|
41
|
+
set data(text: unknown) {
|
|
42
|
+
this.props = { text: String(text) };
|
|
43
|
+
if (this.mounted) (textChanges ?? createTextBatch()).push(this.id, String(text));
|
|
44
|
+
}
|
|
45
|
+
// Native props and callbacks are read together from the completed host vnode.
|
|
46
|
+
setAttribute() {}
|
|
47
|
+
removeAttribute() {}
|
|
48
|
+
addEventListener() {}
|
|
49
|
+
removeEventListener() {}
|
|
50
|
+
appendChild(child: HostNode) { this.insertBefore(child, null); }
|
|
51
|
+
insertBefore(child: HostNode, before: HostNode | null) {
|
|
52
|
+
if (child === before) return;
|
|
53
|
+
if (child.parentNode) child.parentNode.unlink(child);
|
|
54
|
+
child.parentNode = this;
|
|
55
|
+
child.previousSibling = before ? before.previousSibling : this.lastChild;
|
|
56
|
+
child.nextSibling = before;
|
|
57
|
+
if (child.previousSibling) child.previousSibling.nextSibling = child;
|
|
58
|
+
else this.firstChild = child;
|
|
59
|
+
if (before) before.previousSibling = child;
|
|
60
|
+
else this.lastChild = child;
|
|
61
|
+
if (this.id === 0 || this.mounted) {
|
|
62
|
+
publish(child);
|
|
63
|
+
queue({ op: "insert", id: child.id, parent: this.id, before: before?.id ?? null });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
unlink(child: HostNode) {
|
|
67
|
+
if (child.previousSibling) child.previousSibling.nextSibling = child.nextSibling;
|
|
68
|
+
else this.firstChild = child.nextSibling;
|
|
69
|
+
if (child.nextSibling) child.nextSibling.previousSibling = child.previousSibling;
|
|
70
|
+
else this.lastChild = child.previousSibling;
|
|
71
|
+
child.parentNode = child.previousSibling = child.nextSibling = null;
|
|
72
|
+
}
|
|
73
|
+
removeChild(child: HostNode) {
|
|
74
|
+
this.unlink(child);
|
|
75
|
+
if (child.mounted) {
|
|
76
|
+
queue({ op: "remove", id: child.id, parent: this.id });
|
|
77
|
+
forget(child);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
type Instance = HostNode;
|
|
16
82
|
type Operation =
|
|
17
83
|
| { op: "create"; id: number; type: string; props: Props }
|
|
18
84
|
| { op: "update"; id: number; props: Props }
|
|
@@ -26,8 +92,6 @@ let nextId = 1;
|
|
|
26
92
|
let operations: Operation[] = [];
|
|
27
93
|
let textChanges: (number | string)[] | undefined;
|
|
28
94
|
let commitScheduled = false;
|
|
29
|
-
let priority = DefaultEventPriority;
|
|
30
|
-
const hostContext = {};
|
|
31
95
|
|
|
32
96
|
function allocateHostId() {
|
|
33
97
|
if (!Number.isSafeInteger(nextId)) throw new RangeError("Host identifiers exhausted");
|
|
@@ -47,7 +111,7 @@ function createTextBatch() {
|
|
|
47
111
|
|
|
48
112
|
function textContent(type: string, props: Props): string | null {
|
|
49
113
|
if (type !== "Text") return null;
|
|
50
|
-
const value = props.children;
|
|
114
|
+
const value = props.text ?? props.children;
|
|
51
115
|
return typeof value === "string" ? value : typeof value === "number" || typeof value === "bigint" ? String(value) : null;
|
|
52
116
|
}
|
|
53
117
|
|
|
@@ -75,36 +139,21 @@ function nativeProps(props: Props, type: string): Props {
|
|
|
75
139
|
}
|
|
76
140
|
|
|
77
141
|
function publish(instance: Instance) {
|
|
78
|
-
if (
|
|
142
|
+
if (instance.mounted) return;
|
|
143
|
+
instance.mounted = true;
|
|
79
144
|
mounted.set(instance.id, instance);
|
|
80
145
|
queue({ op: "create", id: instance.id, type: instance.type, props: nativeProps(instance.props, instance.type) });
|
|
81
146
|
if (instance.hidden) queue({ op: "hidden", id: instance.id, value: true });
|
|
82
|
-
for (
|
|
147
|
+
for (let child = instance.firstChild; child; child = child.nextSibling) {
|
|
83
148
|
publish(child);
|
|
84
149
|
queue({ op: "insert", id: child.id, parent: instance.id, before: null });
|
|
85
150
|
}
|
|
86
151
|
}
|
|
87
152
|
|
|
88
|
-
function insert(parent: Container, child: Instance, before?: Instance) {
|
|
89
|
-
// Only a moved child needs searching for; appends stay constant-time.
|
|
90
|
-
if (child.parent === parent) parent.children.splice(parent.children.indexOf(child), 1);
|
|
91
|
-
child.parent = parent;
|
|
92
|
-
if (before) parent.children.splice(parent.children.indexOf(before), 0, child);
|
|
93
|
-
else parent.children.push(child);
|
|
94
|
-
publish(child);
|
|
95
|
-
queue({ op: "insert", id: child.id, parent: parent.id, before: before?.id ?? null });
|
|
96
|
-
}
|
|
97
|
-
|
|
98
153
|
function forget(instance: Instance) {
|
|
154
|
+
instance.mounted = false;
|
|
99
155
|
mounted.delete(instance.id);
|
|
100
|
-
instance.
|
|
101
|
-
for (const child of instance.children) forget(child);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function remove(parent: Container, child: Instance) {
|
|
105
|
-
parent.children.splice(parent.children.indexOf(child), 1);
|
|
106
|
-
queue({ op: "remove", id: child.id, parent: parent.id });
|
|
107
|
-
forget(child);
|
|
156
|
+
for (let child = instance.firstChild; child; child = child.nextSibling) forget(child);
|
|
108
157
|
}
|
|
109
158
|
|
|
110
159
|
function sameListValue(a: unknown, b: unknown): boolean {
|
|
@@ -147,9 +196,11 @@ function compiledListUpdate(previous: Props, props: Props): Props | null {
|
|
|
147
196
|
|
|
148
197
|
function update(instance: Instance, type: string, _oldProps: Props, props: Props) {
|
|
149
198
|
const previous = instance.props;
|
|
199
|
+
if (previous === props) return;
|
|
150
200
|
const isText = type === "Text";
|
|
151
201
|
if (isText) props = hostProps(type, props);
|
|
152
202
|
instance.props = props;
|
|
203
|
+
if (!instance.mounted) return;
|
|
153
204
|
if (type === "NativeList" && props.keyField === "key" && previous.template === props.template) {
|
|
154
205
|
const next = compiledListUpdate(previous, props);
|
|
155
206
|
if (next) queue({ op: "update", id: instance.id, props: next }, true);
|
|
@@ -163,10 +214,11 @@ function update(instance: Instance, type: string, _oldProps: Props, props: Props
|
|
|
163
214
|
queue({ op: "update", id: instance.id, props: nativeProps(props, type) });
|
|
164
215
|
return;
|
|
165
216
|
}
|
|
166
|
-
const children = props.children;
|
|
167
|
-
if (children !== previous.children) {
|
|
217
|
+
const children = props.text ?? props.children;
|
|
218
|
+
if (children !== (previous.text ?? previous.children)) {
|
|
168
219
|
if (typeof children === "string") return (textChanges ?? createTextBatch()).push(instance.id, children);
|
|
169
220
|
if (typeof children === "number" || typeof children === "bigint") return (textChanges ?? createTextBatch()).push(instance.id, String(children));
|
|
221
|
+
if (textContent(type, previous) !== null) (textChanges ?? createTextBatch()).push(instance.id, "");
|
|
170
222
|
}
|
|
171
223
|
return;
|
|
172
224
|
}
|
|
@@ -204,8 +256,9 @@ function update(instance: Instance, type: string, _oldProps: Props, props: Props
|
|
|
204
256
|
}
|
|
205
257
|
|
|
206
258
|
function hide(instance: Instance, value: boolean) {
|
|
259
|
+
if (instance.hidden === value) return;
|
|
207
260
|
instance.hidden = value;
|
|
208
|
-
if (
|
|
261
|
+
if (instance.mounted) queue({ op: "hidden", id: instance.id, value });
|
|
209
262
|
}
|
|
210
263
|
|
|
211
264
|
function errorMessage(error: unknown, info: { componentStack?: string | null }) {
|
|
@@ -217,118 +270,101 @@ function report(error: unknown, info: { componentStack?: string | null }) {
|
|
|
217
270
|
__inkPost(JSON.stringify({ type: "error", message: errorMessage(error, info) }));
|
|
218
271
|
}
|
|
219
272
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
273
|
+
// These fields are the stable, mangled option-hook ABI in the pinned Preact 10.
|
|
274
|
+
type HostVNode = VNode<Props> & { __e?: HostNode; __?: HostVNode; __k?: (HostVNode | null)[]; __c?: Component };
|
|
275
|
+
const hooks = options as typeof options & {
|
|
276
|
+
__c?: (vnode: HostVNode, queue: unknown[]) => void;
|
|
277
|
+
__inkNative?: boolean;
|
|
278
|
+
__inkHost?: (node: HostNode, type: string, props: Props, vnode: HostVNode) => void;
|
|
279
|
+
};
|
|
280
|
+
hooks.__inkHost = (node, type, props, vnode) => {
|
|
281
|
+
update(node, type, node.props, props);
|
|
282
|
+
if (!node.parentNode && isHidden(vnode)) node.hidden = true;
|
|
283
|
+
};
|
|
284
|
+
const previousDiff = options.diffed;
|
|
285
|
+
options.diffed = vnode => {
|
|
286
|
+
if (typeof vnode.type !== "string" && vnode.type != null) previousDiff?.(vnode);
|
|
287
|
+
const host = vnode as HostVNode;
|
|
288
|
+
if (!hooks.__inkNative) {
|
|
289
|
+
if (typeof host.type === "string" && host.__e) update(host.__e, host.type, host.__e.props, (host.props as HostProps)[hostProperties]!);
|
|
290
|
+
if (host.__e && (typeof host.type === "string" || host.type == null)) hide(host.__e, isHidden(host));
|
|
291
|
+
}
|
|
292
|
+
if (hooks.__inkNative && host.type == null && host.__e) hide(host.__e, isHidden(host));
|
|
293
|
+
if (host.type === Activity && host.__c && activityChanged(host.__c)) visitVisibility(host);
|
|
294
|
+
};
|
|
223
295
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
shouldSetTextContent: (type, props) => {
|
|
237
|
-
if (type !== "Text") return false;
|
|
238
|
-
const kind = typeof props.children;
|
|
239
|
-
return kind === "string" || kind === "number" || kind === "bigint";
|
|
240
|
-
},
|
|
241
|
-
getRootHostContext: () => hostContext,
|
|
242
|
-
getChildHostContext: () => hostContext,
|
|
243
|
-
getPublicInstance: instance => instance,
|
|
244
|
-
prepareForCommit: () => null,
|
|
245
|
-
resetAfterCommit: () => {
|
|
246
|
-
if (!operations.length || commitScheduled) return;
|
|
247
|
-
commitScheduled = true;
|
|
248
|
-
queueMicrotask(() => {
|
|
249
|
-
// Revealing an Activity reconnects store subscriptions in passive effects.
|
|
250
|
-
// Include their synchronous corrections before presenting the page.
|
|
251
|
-
while (reconciler.flushPassiveEffects()) {}
|
|
252
|
-
reconciler.flushSyncWork();
|
|
253
|
-
const committed = operations;
|
|
254
|
-
operations = [];
|
|
255
|
-
textChanges = undefined;
|
|
256
|
-
commitScheduled = false;
|
|
257
|
-
if (committed.length) __inkCommit(committed);
|
|
258
|
-
});
|
|
259
|
-
},
|
|
260
|
-
preparePortalMount: () => {},
|
|
261
|
-
scheduleTimeout: setTimeout,
|
|
262
|
-
cancelTimeout: clearTimeout,
|
|
263
|
-
noTimeout: -1,
|
|
264
|
-
scheduleMicrotask: queueMicrotask,
|
|
265
|
-
getInstanceFromNode: () => null,
|
|
266
|
-
beforeActiveInstanceBlur: () => {},
|
|
267
|
-
afterActiveInstanceBlur: () => {},
|
|
268
|
-
prepareScopeUpdate: () => {},
|
|
269
|
-
getInstanceFromScope: () => null,
|
|
270
|
-
detachDeletedInstance: () => {},
|
|
271
|
-
appendChild: insert,
|
|
272
|
-
appendChildToContainer: insert,
|
|
273
|
-
insertBefore: insert,
|
|
274
|
-
insertInContainerBefore: insert,
|
|
275
|
-
removeChild: remove,
|
|
276
|
-
removeChildFromContainer: remove,
|
|
277
|
-
resetTextContent: instance => { (textChanges ?? createTextBatch()).push(instance.id, ""); },
|
|
278
|
-
commitTextUpdate: (instance, _oldText, text) => {
|
|
279
|
-
instance.props = { text };
|
|
280
|
-
(textChanges ?? createTextBatch()).push(instance.id, text);
|
|
281
|
-
},
|
|
282
|
-
commitUpdate: update,
|
|
283
|
-
hideInstance: instance => hide(instance, true),
|
|
284
|
-
hideTextInstance: instance => hide(instance, true),
|
|
285
|
-
unhideInstance: instance => hide(instance, false),
|
|
286
|
-
unhideTextInstance: instance => hide(instance, false),
|
|
287
|
-
clearContainer: container => {
|
|
288
|
-
for (const child of [...container.children]) remove(container, child);
|
|
289
|
-
},
|
|
290
|
-
NotPendingTransition: null,
|
|
291
|
-
// React's public Context type omits the fields consumed by its reconciler.
|
|
292
|
-
HostTransitionContext: createContext(null) as unknown as Reconciler.ReactContext<null>,
|
|
293
|
-
setCurrentUpdatePriority: value => { priority = value; },
|
|
294
|
-
getCurrentUpdatePriority: () => priority,
|
|
295
|
-
resolveUpdatePriority: () => priority,
|
|
296
|
-
resetFormInstance: () => {},
|
|
297
|
-
requestPostPaintCallback: callback => { setTimeout(() => callback(performance.now()), 0); },
|
|
298
|
-
shouldAttemptEagerTransition: () => false,
|
|
299
|
-
trackSchedulerEvent: () => {},
|
|
300
|
-
resolveEventType: () => null,
|
|
301
|
-
resolveEventTimeStamp: () => performance.now(),
|
|
302
|
-
maySuspendCommit: () => false,
|
|
303
|
-
preloadInstance: () => true,
|
|
304
|
-
startSuspendingCommit: () => {},
|
|
305
|
-
suspendInstance: () => {},
|
|
306
|
-
waitForCommitToBeReady: () => null,
|
|
307
|
-
});
|
|
296
|
+
function isHidden(vnode: HostVNode) {
|
|
297
|
+
let component: Component | undefined;
|
|
298
|
+
for (let parent = vnode.__; parent; parent = parent.__) {
|
|
299
|
+
// A native ancestor already carries visibility for this entire branch.
|
|
300
|
+
if (typeof parent.type === "string") return false;
|
|
301
|
+
if (parent.__c) {
|
|
302
|
+
component ??= parent.__c;
|
|
303
|
+
if (parent.type === Activity) return !isComponentVisible(parent.__c);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return component ? !isComponentVisible(component) : false;
|
|
307
|
+
}
|
|
308
308
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
priority = DiscreteEventPriority;
|
|
314
|
-
try {
|
|
315
|
-
handler(...args);
|
|
316
|
-
} finally {
|
|
317
|
-
priority = previous;
|
|
309
|
+
function visitVisibility(vnode: HostVNode) {
|
|
310
|
+
if (typeof vnode.type === "string" || vnode.type == null) {
|
|
311
|
+
if (vnode.__e) hide(vnode.__e, isHidden(vnode));
|
|
312
|
+
return;
|
|
318
313
|
}
|
|
314
|
+
for (const child of vnode.__k ?? []) if (child) visitVisibility(child);
|
|
319
315
|
}
|
|
320
316
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
317
|
+
const previousVNode = options.vnode;
|
|
318
|
+
options.vnode = vnode => {
|
|
319
|
+
if (typeof vnode.type !== "string") {
|
|
320
|
+
previousVNode?.(vnode);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const props = vnode.props as HostProps;
|
|
324
|
+
// Preact reconciles children; Ink compares native props once, after the diff.
|
|
325
|
+
// Keep the original props in this envelope so cloned compiler-cached vnodes
|
|
326
|
+
// retain them without copying or normalising every native property.
|
|
327
|
+
if (!hooks.__inkNative && !props[hostProperties]) {
|
|
328
|
+
(vnode as VNode<HostProps>).props = {
|
|
329
|
+
children: (textContent(vnode.type, props) === null ? props.children : undefined) as ComponentChildren,
|
|
330
|
+
[hostProperties]: props,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// QuickJS has no browser paint cycle. Run effects before presenting queued ops.
|
|
336
|
+
options.requestAnimationFrame = callback => { queueMicrotask(callback); };
|
|
337
|
+
options.debounceRendering = queueMicrotask;
|
|
338
|
+
const previousCommit = hooks.__c;
|
|
339
|
+
hooks.__c = (vnode, components) => {
|
|
340
|
+
previousCommit?.(vnode, components);
|
|
341
|
+
if (commitScheduled) return;
|
|
342
|
+
commitScheduled = true;
|
|
343
|
+
queueMicrotask(() => queueMicrotask(() => {
|
|
344
|
+
const committed = operations;
|
|
345
|
+
operations = [];
|
|
346
|
+
textChanges = undefined;
|
|
347
|
+
commitScheduled = false;
|
|
348
|
+
if (committed.length) __inkCommit(committed);
|
|
349
|
+
}));
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// Preact creates nodes through document, but all layout and painting stay native.
|
|
353
|
+
Object.assign(globalThis, { document: {
|
|
354
|
+
createElementNS: (_namespace: string, type: string) => new HostNode(type),
|
|
355
|
+
createElement: (type: string) => new HostNode(type),
|
|
356
|
+
createTextNode: (text: string) => new HostNode("#text", text),
|
|
357
|
+
} });
|
|
358
|
+
|
|
359
|
+
export function dispatchEvent(id: number, name: string, args: unknown[]) {
|
|
360
|
+
const handler = mounted.get(id)?.props[name];
|
|
361
|
+
if (typeof handler === "function") handler(...args);
|
|
324
362
|
}
|
|
325
363
|
|
|
364
|
+
let developmentRoot: HostNode | undefined;
|
|
326
365
|
export function render(element: ReactNode) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
reconciler.updateContainer(element, root, null, null);
|
|
330
|
-
return () => reconciler.updateContainer(null, root, null, null);
|
|
331
|
-
}
|
|
366
|
+
const container = process.env.NODE_ENV === "development" && developmentRoot || new HostNode("#root");
|
|
367
|
+
if (process.env.NODE_ENV === "development") developmentRoot = container;
|
|
332
368
|
onNativeMessage("event", message => {
|
|
333
369
|
if (typeof message.id !== "number" || !Number.isSafeInteger(message.id)
|
|
334
370
|
|| typeof message.name !== "string" || !Array.isArray(message.args)) {
|
|
@@ -336,9 +372,16 @@ export function render(element: ReactNode) {
|
|
|
336
372
|
}
|
|
337
373
|
dispatchEvent(message.id, message.name, message.args);
|
|
338
374
|
});
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
375
|
+
const updateRoot = () => {
|
|
376
|
+
try {
|
|
377
|
+
renderPreact(element as ComponentChildren, container as unknown as Parameters<typeof renderPreact>[1]);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
report(error, {});
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
// Prefresh replaces mounted component types at the end of the bundle.
|
|
383
|
+
// Apply the new root afterwards so matching components keep their state.
|
|
384
|
+
if (process.env.NODE_ENV === "development") queueMicrotask(updateRoot);
|
|
385
|
+
else updateRoot();
|
|
386
|
+
return () => renderPreact(null, container as unknown as Parameters<typeof renderPreact>[1]);
|
|
344
387
|
}
|
package/src/reorder.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { createElement } from "./react";
|
|
2
|
-
import { Button, Stack, Text } from "./index";
|
|
3
2
|
import { keyboardArrowDown, keyboardArrowUp } from "./icons";
|
|
4
3
|
import { List } from "./list";
|
|
5
|
-
import {
|
|
4
|
+
import { fixedListRow } from "./native-list";
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
type ReorderFields = {
|
|
7
|
+
label: string; maxLines?: number; first: boolean; last: boolean;
|
|
8
|
+
down: () => void; up: () => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const reorderRow = /* @__PURE__ */ fixedListRow<ReorderFields>((node, field) => node("Stack", { axis: "horizontal", align: "center", justify: "space-between", gap: 12 }, [
|
|
12
|
+
node("Text", { text: field(values => values.label), maxLines: field(values => values.maxLines) }),
|
|
9
13
|
node("Stack", { axis: "horizontal", align: "center", gap: 4 }, [
|
|
10
|
-
node("Button", { icon: keyboardArrowDown, disabled: field(
|
|
11
|
-
node("Button", { icon: keyboardArrowUp, disabled: field(
|
|
14
|
+
node("Button", { icon: keyboardArrowDown, disabled: field(values => values.last), onPress: field(values => values.down) }),
|
|
15
|
+
node("Button", { icon: keyboardArrowUp, disabled: field(values => values.first), onPress: field(values => values.up) }),
|
|
12
16
|
]),
|
|
13
17
|
]));
|
|
14
18
|
|
|
@@ -35,15 +39,8 @@ export function ReorderList<T>({ items, keyExtractor, getLabel, onChange, multil
|
|
|
35
39
|
keyExtractor,
|
|
36
40
|
gap,
|
|
37
41
|
measurementKey: multiline ? "multiline" : "single-line",
|
|
38
|
-
renderItem:
|
|
39
|
-
axis: "horizontal", align: "center", justify: "space-between", gap: 12,
|
|
40
|
-
},
|
|
41
|
-
createElement(Text, { maxLines: multiline ? undefined : 1 }, getLabel(item)),
|
|
42
|
-
createElement(Stack, { axis: "horizontal", align: "center", gap: 4 },
|
|
43
|
-
createElement(Button, { icon: keyboardArrowDown, disabled: index === items.length - 1, onPress: () => move(index, 1) }),
|
|
44
|
-
createElement(Button, { icon: keyboardArrowUp, disabled: index === 0, onPress: () => move(index, -1) }),
|
|
45
|
-
)), (item, index) => [{ label: getLabel(item), maxLines: multiline ? undefined : 1,
|
|
42
|
+
renderItem: reorderRow((item: T, index: number) => ({ label: getLabel(item), maxLines: multiline ? undefined : 1,
|
|
46
43
|
first: index === 0, last: index === items.length - 1,
|
|
47
|
-
down: () => move(index, 1), up: () => move(index, -1) }
|
|
44
|
+
down: () => move(index, 1), up: () => move(index, -1) })),
|
|
48
45
|
});
|
|
49
46
|
}
|