@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.
Files changed (40) hide show
  1. package/README.md +18 -3
  2. package/dist/BlarProvider.d.ts +2 -0
  3. package/dist/actions/scrolling.d.ts +3 -1
  4. package/dist/actions/select.d.ts +16 -0
  5. package/dist/actions/text-input.d.ts +1 -0
  6. package/dist/actions/types.d.ts +3 -0
  7. package/dist/agent/compose.d.ts +1 -0
  8. package/dist/agent-mode/keyboard-guard.d.ts +13 -0
  9. package/dist/backend/app-info.d.ts +1 -0
  10. package/dist/fiber/text.d.ts +1 -0
  11. package/dist/snapshot/capture.d.ts +1 -0
  12. package/dist/snapshot/view.d.ts +3 -0
  13. package/dist/snapshot/visibility.d.ts +1 -1
  14. package/dist/snapshot/wrappers.d.ts +0 -2
  15. package/dist/version.d.ts +1 -1
  16. package/package.json +1 -1
  17. package/src/BlarProvider.tsx +13 -0
  18. package/src/actions/click.ts +2 -1
  19. package/src/actions/executor.ts +8 -2
  20. package/src/actions/landing.ts +2 -2
  21. package/src/actions/parse.ts +2 -0
  22. package/src/actions/scrolling.ts +3 -20
  23. package/src/actions/select.ts +176 -0
  24. package/src/actions/text-input.ts +30 -1
  25. package/src/actions/types.ts +1 -0
  26. package/src/agent/compose.ts +2 -0
  27. package/src/agent/controller.ts +3 -1
  28. package/src/agent-mode/keyboard-guard.ts +33 -0
  29. package/src/backend/app-info.ts +3 -0
  30. package/src/chat/ChatSheet.tsx +29 -9
  31. package/src/chat/ChatView.tsx +1 -1
  32. package/src/chat/widget-bundle.generated.ts +2 -2
  33. package/src/fiber/text.ts +2 -0
  34. package/src/snapshot/capture.ts +9 -6
  35. package/src/snapshot/collect.ts +1 -2
  36. package/src/snapshot/render.ts +4 -2
  37. package/src/snapshot/view.ts +34 -0
  38. package/src/snapshot/visibility.ts +11 -10
  39. package/src/snapshot/wrappers.ts +0 -13
  40. 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
  }
@@ -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, limitOffscreen, type MeasuredCandidate } from './wrappers.ts';
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.getViewport();
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, distance: offscreen?.distance ?? 0 };
109
+ return { candidate, rect, offscreen: offscreen?.direction ?? null };
108
110
  }),
109
111
  );
110
- return limitOffscreen(measured.filter((entry): entry is MeasuredCandidate => entry !== null));
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') {
@@ -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'];
@@ -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> {
@@ -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; distance: number }
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
  }
@@ -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.4';
1
+ export const SDK_VERSION = '0.1.6';