@blario/react-native 0.1.4 → 0.1.6
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 +18 -3
- package/dist/BlarProvider.d.ts +2 -0
- package/dist/actions/scrolling.d.ts +3 -1
- package/dist/actions/select.d.ts +16 -0
- package/dist/actions/text-input.d.ts +1 -0
- package/dist/actions/types.d.ts +3 -0
- package/dist/agent/compose.d.ts +1 -0
- package/dist/agent-mode/keyboard-guard.d.ts +13 -0
- package/dist/backend/app-info.d.ts +1 -0
- package/dist/fiber/text.d.ts +1 -0
- package/dist/snapshot/capture.d.ts +1 -0
- package/dist/snapshot/view.d.ts +3 -0
- package/dist/snapshot/visibility.d.ts +1 -1
- package/dist/snapshot/wrappers.d.ts +0 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/BlarProvider.tsx +13 -0
- package/src/actions/click.ts +2 -1
- package/src/actions/executor.ts +8 -2
- package/src/actions/landing.ts +2 -2
- package/src/actions/parse.ts +2 -0
- package/src/actions/scrolling.ts +3 -20
- package/src/actions/select.ts +176 -0
- package/src/actions/text-input.ts +30 -1
- package/src/actions/types.ts +1 -0
- package/src/agent/compose.ts +2 -0
- package/src/agent/controller.ts +3 -1
- package/src/agent-mode/keyboard-guard.ts +33 -0
- package/src/backend/app-info.ts +3 -0
- package/src/chat/ChatSheet.tsx +29 -9
- package/src/chat/ChatView.tsx +1 -1
- package/src/chat/widget-bundle.generated.ts +2 -2
- package/src/fiber/text.ts +2 -0
- package/src/snapshot/capture.ts +9 -6
- package/src/snapshot/collect.ts +1 -2
- package/src/snapshot/render.ts +4 -2
- package/src/snapshot/view.ts +34 -0
- package/src/snapshot/visibility.ts +11 -10
- package/src/snapshot/wrappers.ts +0 -13
- package/src/version.ts +1 -1
package/src/fiber/text.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { Fiber } from './types.ts';
|
|
|
3
3
|
|
|
4
4
|
const PRIVATE_USE_GLYPHS = /[-\u{F0000}-\u{10FFFF}]/gu;
|
|
5
5
|
|
|
6
|
+
export const TEXT_HOST_TYPE = 'RCTText';
|
|
7
|
+
|
|
6
8
|
export function normalizeText(value: string): string {
|
|
7
9
|
return value.replace(PRIVATE_USE_GLYPHS, '').replace(/\s+/g, ' ').trim();
|
|
8
10
|
}
|
package/src/snapshot/capture.ts
CHANGED
|
@@ -12,8 +12,8 @@ import { describeScrollDirections } from './scroll.ts';
|
|
|
12
12
|
import { groupIntoLines, limitCharacters, type PositionedText } from './text-layout.ts';
|
|
13
13
|
import { orderForReading } from './ordering.ts';
|
|
14
14
|
import { buildView } from './view.ts';
|
|
15
|
-
import { classifyVisibility, keepInTextTree, measureScrollGeometries, type ScrollGeometries } from './visibility.ts';
|
|
16
|
-
import { dropWrapperDuplicates,
|
|
15
|
+
import { classifyVisibility, keepInTextTree, markOffscreenText, measureScrollGeometries, type ScrollGeometries } from './visibility.ts';
|
|
16
|
+
import { dropWrapperDuplicates, type MeasuredCandidate } from './wrappers.ts';
|
|
17
17
|
|
|
18
18
|
export interface Snapshot {
|
|
19
19
|
screen: string;
|
|
@@ -27,6 +27,7 @@ export interface SnapshotDependencies {
|
|
|
27
27
|
getAnchorFiber: () => Fiber | null;
|
|
28
28
|
registry: ElementRegistry;
|
|
29
29
|
getViewport: () => Size;
|
|
30
|
+
getWindow: () => Size;
|
|
30
31
|
getExtraRoutes: () => string[];
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -77,7 +78,7 @@ export class SnapshotCapturer {
|
|
|
77
78
|
return null;
|
|
78
79
|
}
|
|
79
80
|
const visibility = classifyVisibility(rect, viewport, block.scrollContainers, geometries);
|
|
80
|
-
return keepInTextTree(visibility, rect, viewport, options.viewportOnly) ? { rect, text: block.text, heading: block.heading } : null;
|
|
81
|
+
return keepInTextTree(visibility, rect, viewport, options.viewportOnly) ? { rect, text: markOffscreenText(visibility, block.text), heading: block.heading } : null;
|
|
81
82
|
}),
|
|
82
83
|
);
|
|
83
84
|
const lines = limitCharacters(groupIntoLines(measured.filter((entry): entry is PositionedText => entry !== null)), options.maxChars);
|
|
@@ -91,8 +92,9 @@ export class SnapshotCapturer {
|
|
|
91
92
|
return root === null ? [] : collect(root).focusedScreens;
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
// The whole window, not the part above the keyboard: what the keyboard covers is still pressable.
|
|
94
96
|
private async measureVisible(kept: Candidate[], geometries: ScrollGeometries): Promise<MeasuredCandidate[]> {
|
|
95
|
-
const viewport = this.dependencies.
|
|
97
|
+
const viewport = this.dependencies.getWindow();
|
|
96
98
|
const measured = await Promise.all(
|
|
97
99
|
kept.map(async (candidate): Promise<MeasuredCandidate | null> => {
|
|
98
100
|
const rect = await measureHost(candidate.host);
|
|
@@ -104,10 +106,10 @@ export class SnapshotCapturer {
|
|
|
104
106
|
return null;
|
|
105
107
|
}
|
|
106
108
|
const offscreen = visibility.kind === 'offscreen' ? visibility : null;
|
|
107
|
-
return { candidate, rect, offscreen: offscreen?.direction ?? null
|
|
109
|
+
return { candidate, rect, offscreen: offscreen?.direction ?? null };
|
|
108
110
|
}),
|
|
109
111
|
);
|
|
110
|
-
return
|
|
112
|
+
return measured.filter((entry): entry is MeasuredCandidate => entry !== null);
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
private assemble(
|
|
@@ -124,6 +126,7 @@ export class SnapshotCapturer {
|
|
|
124
126
|
rect,
|
|
125
127
|
scroll: candidate.kind === 'scroll' ? describeScrollDirections(geometries.get(candidate.fiber) ?? null) : null,
|
|
126
128
|
offscreen,
|
|
129
|
+
wheelOption: candidate.wheelOption,
|
|
127
130
|
}),
|
|
128
131
|
);
|
|
129
132
|
if (mode === 'register') {
|
package/src/snapshot/collect.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { childrenOf, componentName, firstHostDescendant, isHostComponent, propsOf } from '../fiber/reader.ts';
|
|
2
|
-
import { collectText } from '../fiber/text.ts';
|
|
2
|
+
import { collectText, TEXT_HOST_TYPE } from '../fiber/text.ts';
|
|
3
3
|
import type { Fiber } from '../fiber/types.ts';
|
|
4
4
|
import { classify } from './classify.ts';
|
|
5
5
|
import type { ElementKind } from './kinds.ts';
|
|
@@ -19,7 +19,6 @@ export interface Candidate {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const MODAL_COMPONENT_NAME = 'Modal';
|
|
22
|
-
const TEXT_HOST_TYPE = 'RCTText';
|
|
23
22
|
const WHEEL_OPTION_LIMIT = 24;
|
|
24
23
|
const TEXT_BLOCK_LIMIT = 800;
|
|
25
24
|
const HEADING_ROLES: readonly string[] = ['header', 'heading'];
|
package/src/snapshot/render.ts
CHANGED
|
@@ -17,7 +17,7 @@ function flag(name: string, enabled: boolean): Attribute {
|
|
|
17
17
|
function renderAttributes(attributes: Attribute[]): string {
|
|
18
18
|
return attributes
|
|
19
19
|
.filter((attribute): attribute is readonly [string, string] => attribute[1] !== null)
|
|
20
|
-
.map(([name, value]) => (value === '' ? name : `${name}=${clip(value, ATTRIBUTE_LIMIT)}`))
|
|
20
|
+
.map(([name, value]) => (value === '' ? name : `${name}=${name === 'list' ? value : clip(value, ATTRIBUTE_LIMIT)}`))
|
|
21
21
|
.filter((rendered) => rendered.length > 0)
|
|
22
22
|
.join(' ');
|
|
23
23
|
}
|
|
@@ -51,6 +51,7 @@ function renderButton(view: ElementView, twins: number): string {
|
|
|
51
51
|
const tag = openTag('button', [
|
|
52
52
|
['role', view.role],
|
|
53
53
|
['testID', view.testID],
|
|
54
|
+
['list', view.list],
|
|
54
55
|
['icon', view.icon],
|
|
55
56
|
['offscreen', view.offscreen],
|
|
56
57
|
...commonStateAttributes(view),
|
|
@@ -94,6 +95,7 @@ function renderSwitch(view: ElementView, twins: number): string {
|
|
|
94
95
|
function renderScroll(view: ElementView): string {
|
|
95
96
|
const tag = openTag('scroll', [
|
|
96
97
|
['testID', view.testID],
|
|
98
|
+
['list', view.list],
|
|
97
99
|
['scroll', view.scroll],
|
|
98
100
|
]);
|
|
99
101
|
return `${tag} />`;
|
|
@@ -113,7 +115,7 @@ function renderBody(view: ElementView, twins: number): string {
|
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
export function twinSignature(view: ElementView): string {
|
|
116
|
-
return [view.kind, view.label, view.icon ?? '', view.role ?? ''].join('|');
|
|
118
|
+
return [view.kind, view.label, view.icon ?? '', view.role ?? '', view.list ?? ''].join('|');
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
export function countTwins(views: ElementView[]): Map<string, number> {
|
package/src/snapshot/view.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { collectText, precedingText } from '../fiber/text.ts';
|
|
|
2
2
|
import { propsOf } from '../fiber/reader.ts';
|
|
3
3
|
import type { Fiber } from '../fiber/types.ts';
|
|
4
4
|
import type { Rect } from '../host/rect.ts';
|
|
5
|
+
import { scrollInstanceOf } from './classify.ts';
|
|
5
6
|
import { iconNameIn } from './icon.ts';
|
|
6
7
|
import type { Offscreen } from './visibility.ts';
|
|
7
8
|
import type { ElementKind } from './kinds.ts';
|
|
@@ -33,9 +34,24 @@ export interface ElementView {
|
|
|
33
34
|
selected: boolean | null;
|
|
34
35
|
scroll: string | null;
|
|
35
36
|
offscreen: Offscreen | null;
|
|
37
|
+
list: string | null;
|
|
36
38
|
rect: Rect;
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
const LIST_NAME_DEPTH = 8;
|
|
42
|
+
|
|
43
|
+
// What a list is called: hosts name the view around it, as AgendaPro does with Hours and Minutes.
|
|
44
|
+
export function listNameOf(list: Fiber, ancestors: Fiber[]): string | null {
|
|
45
|
+
for (const fiber of [list, ...ancestors.slice(-LIST_NAME_DEPTH).reverse()]) {
|
|
46
|
+
const props = propsOf(fiber);
|
|
47
|
+
const name = explicitLabelOf(props) ?? testIdOf(props);
|
|
48
|
+
if (name !== null) {
|
|
49
|
+
return name;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
39
55
|
function labelFor(kind: ElementKind, fiber: Fiber, ancestors: Fiber[]): string {
|
|
40
56
|
const explicit = explicitLabelOf(propsOf(fiber));
|
|
41
57
|
if (explicit !== null) {
|
|
@@ -57,6 +73,23 @@ export interface ViewSource {
|
|
|
57
73
|
rect: Rect;
|
|
58
74
|
scroll: string | null;
|
|
59
75
|
offscreen?: Offscreen | null;
|
|
76
|
+
wheelOption?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function listOfView({ kind, fiber, ancestors, wheelOption }: ViewSource): string | null {
|
|
80
|
+
if (kind === 'scroll') {
|
|
81
|
+
return listNameOf(fiber, ancestors);
|
|
82
|
+
}
|
|
83
|
+
if (wheelOption !== true) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
87
|
+
const ancestor = ancestors[index];
|
|
88
|
+
if (ancestor !== undefined && scrollInstanceOf(ancestor) !== null) {
|
|
89
|
+
return listNameOf(ancestor, ancestors.slice(0, index));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
60
93
|
}
|
|
61
94
|
|
|
62
95
|
export function buildView(source: ViewSource): ElementView {
|
|
@@ -82,6 +115,7 @@ export function buildView(source: ViewSource): ElementView {
|
|
|
82
115
|
selected: flags.selected,
|
|
83
116
|
scroll,
|
|
84
117
|
offscreen,
|
|
118
|
+
list: listOfView(source),
|
|
85
119
|
rect,
|
|
86
120
|
};
|
|
87
121
|
}
|
|
@@ -16,7 +16,7 @@ export type Offscreen = 'above' | 'below' | 'left' | 'right';
|
|
|
16
16
|
|
|
17
17
|
export type Visibility =
|
|
18
18
|
| { kind: 'visible' }
|
|
19
|
-
| { kind: 'offscreen'; direction: Offscreen
|
|
19
|
+
| { kind: 'offscreen'; direction: Offscreen }
|
|
20
20
|
| { kind: 'hidden' };
|
|
21
21
|
|
|
22
22
|
const VISIBLE: Visibility = { kind: 'visible' };
|
|
@@ -25,13 +25,9 @@ const HIDDEN: Visibility = { kind: 'hidden' };
|
|
|
25
25
|
function offscreenAgainst(rect: Rect, geometry: ScrollGeometry): Visibility {
|
|
26
26
|
const { viewport, horizontal } = geometry;
|
|
27
27
|
if (horizontal) {
|
|
28
|
-
return rect.x >= viewport.x + viewport.width
|
|
29
|
-
? { kind: 'offscreen', direction: 'right', distance: rect.x - (viewport.x + viewport.width) }
|
|
30
|
-
: { kind: 'offscreen', direction: 'left', distance: viewport.x - (rect.x + rect.width) };
|
|
28
|
+
return { kind: 'offscreen', direction: rect.x >= viewport.x + viewport.width ? 'right' : 'left' };
|
|
31
29
|
}
|
|
32
|
-
return rect.y >= viewport.y + viewport.height
|
|
33
|
-
? { kind: 'offscreen', direction: 'below', distance: rect.y - (viewport.y + viewport.height) }
|
|
34
|
-
: { kind: 'offscreen', direction: 'above', distance: viewport.y - (rect.y + rect.height) };
|
|
30
|
+
return { kind: 'offscreen', direction: rect.y >= viewport.y + viewport.height ? 'below' : 'above' };
|
|
35
31
|
}
|
|
36
32
|
|
|
37
33
|
export function classifyVisibility(rect: Rect, window: Size, containers: Fiber[], geometries: ScrollGeometries): Visibility {
|
|
@@ -52,10 +48,15 @@ function isBeyondWindowEdge(rect: Rect, window: Size): boolean {
|
|
|
52
48
|
return rect.x >= window.width || rect.x + rect.width <= 0;
|
|
53
49
|
}
|
|
54
50
|
|
|
51
|
+
const OFFSCREEN_MARK = '[off-screen]';
|
|
52
|
+
|
|
53
|
+
// Text off to the side can be another page of a pager or another day of a calendar, so it can't read as what's on screen.
|
|
54
|
+
export function markOffscreenText(visibility: Visibility, text: string): string {
|
|
55
|
+
const sideways = visibility.kind === 'offscreen' && (visibility.direction === 'left' || visibility.direction === 'right');
|
|
56
|
+
return sideways ? `${OFFSCREEN_MARK} ${text}` : text;
|
|
57
|
+
}
|
|
58
|
+
|
|
55
59
|
export function keepInTextTree(visibility: Visibility, rect: Rect, window: Size, viewportOnly: boolean): boolean {
|
|
56
|
-
if (visibility.kind === 'offscreen' && (visibility.direction === 'left' || visibility.direction === 'right')) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
60
|
if (visibility.kind === 'hidden' && isBeyondWindowEdge(rect, window)) {
|
|
60
61
|
return false;
|
|
61
62
|
}
|
package/src/snapshot/wrappers.ts
CHANGED
|
@@ -7,7 +7,6 @@ export interface MeasuredCandidate {
|
|
|
7
7
|
candidate: Candidate;
|
|
8
8
|
rect: Rect;
|
|
9
9
|
offscreen: Offscreen | null;
|
|
10
|
-
distance: number;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
const RECT_TOLERANCE = 1;
|
|
@@ -56,15 +55,3 @@ export function dropWrapperDuplicates(measured: MeasuredCandidate[]): MeasuredCa
|
|
|
56
55
|
}
|
|
57
56
|
return measured.filter((entry) => !wrappers.has(entry));
|
|
58
57
|
}
|
|
59
|
-
|
|
60
|
-
const OFFSCREEN_LIMIT = 30;
|
|
61
|
-
|
|
62
|
-
export function limitOffscreen(measured: MeasuredCandidate[]): MeasuredCandidate[] {
|
|
63
|
-
// Every option of a wheel stays listed: one dropped here could never be selected.
|
|
64
|
-
const hidden = measured.filter((entry) => entry.offscreen !== null && entry.candidate.kind !== 'scroll' && entry.candidate.wheelOption !== true);
|
|
65
|
-
if (hidden.length <= OFFSCREEN_LIMIT) {
|
|
66
|
-
return measured;
|
|
67
|
-
}
|
|
68
|
-
const dropped = new Set(hidden.sort((left, right) => left.distance - right.distance).slice(OFFSCREEN_LIMIT));
|
|
69
|
-
return measured.filter((entry) => !dropped.has(entry));
|
|
70
|
-
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '0.1.
|
|
1
|
+
export const SDK_VERSION = '0.1.6';
|