@ankhorage/zora 25.5.0 → 25.6.2

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 (106) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +2 -1
  3. package/dist/capabilities/index.d.ts +2 -74
  4. package/dist/capabilities/index.d.ts.map +1 -1
  5. package/dist/capabilities/index.js +6158 -59
  6. package/dist/capabilities/index.js.map +118 -1
  7. package/dist/cli/index.d.ts +2 -74
  8. package/dist/cli/index.d.ts.map +1 -1
  9. package/dist/cli/index.js +2 -1
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/features/grid-view/adapters/inbound/GridView.d.ts +2 -7
  12. package/dist/features/grid-view/adapters/inbound/GridView.d.ts.map +1 -1
  13. package/dist/features/grid-view/adapters/inbound/GridView.js +309 -51
  14. package/dist/features/grid-view/adapters/inbound/GridView.js.map +1 -1
  15. package/dist/features/grid-view/adapters/inbound/TileGrid.d.ts +1 -1
  16. package/dist/features/grid-view/adapters/inbound/TileGrid.d.ts.map +1 -1
  17. package/dist/features/grid-view/adapters/inbound/TileGrid.js +11 -4
  18. package/dist/features/grid-view/adapters/inbound/TileGrid.js.map +1 -1
  19. package/dist/features/matrix-grid/adapters/inbound/MatrixGrid.d.ts +5 -0
  20. package/dist/features/matrix-grid/adapters/inbound/MatrixGrid.d.ts.map +1 -0
  21. package/dist/features/matrix-grid/adapters/inbound/MatrixGrid.js +59 -0
  22. package/dist/features/matrix-grid/adapters/inbound/MatrixGrid.js.map +1 -0
  23. package/dist/features/matrix-grid/application/resolveMatrixGridContentSize.d.ts +7 -0
  24. package/dist/features/matrix-grid/application/resolveMatrixGridContentSize.d.ts.map +1 -0
  25. package/dist/features/matrix-grid/application/resolveMatrixGridContentSize.js +24 -0
  26. package/dist/features/matrix-grid/application/resolveMatrixGridContentSize.js.map +1 -0
  27. package/dist/features/matrix-grid/application/resolveMatrixGridSelection.d.ts +5 -0
  28. package/dist/features/matrix-grid/application/resolveMatrixGridSelection.d.ts.map +1 -0
  29. package/dist/features/matrix-grid/application/resolveMatrixGridSelection.js +8 -0
  30. package/dist/features/matrix-grid/application/resolveMatrixGridSelection.js.map +1 -0
  31. package/dist/features/matrix-grid/meta.d.ts +11 -0
  32. package/dist/features/matrix-grid/meta.d.ts.map +1 -0
  33. package/dist/features/matrix-grid/meta.js +11 -0
  34. package/dist/features/matrix-grid/meta.js.map +1 -0
  35. package/dist/features/matrix-grid/public.d.ts +4 -0
  36. package/dist/features/matrix-grid/public.d.ts.map +1 -0
  37. package/dist/features/matrix-grid/public.js +3 -0
  38. package/dist/features/matrix-grid/public.js.map +1 -0
  39. package/dist/features/registry/ZORA_COMPONENT_REGISTRY.d.ts.map +1 -1
  40. package/dist/features/registry/ZORA_COMPONENT_REGISTRY.js +2 -0
  41. package/dist/features/registry/ZORA_COMPONENT_REGISTRY.js.map +1 -1
  42. package/dist/features/registry/componentMeta.d.ts.map +1 -1
  43. package/dist/features/registry/componentMeta.js +2 -0
  44. package/dist/features/registry/componentMeta.js.map +1 -1
  45. package/dist/features/registry/createEventCapabilities.d.ts +4 -0
  46. package/dist/features/registry/createEventCapabilities.d.ts.map +1 -0
  47. package/dist/features/registry/createEventCapabilities.js +76 -0
  48. package/dist/features/registry/createEventCapabilities.js.map +1 -0
  49. package/dist/index.d.ts +2 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/types/grid-view.d.ts +18 -1
  54. package/dist/types/grid-view.d.ts.map +1 -1
  55. package/dist/types/grid-view.js.map +1 -1
  56. package/dist/types/matrix-grid.d.ts +23 -0
  57. package/dist/types/matrix-grid.d.ts.map +1 -0
  58. package/dist/types/matrix-grid.js +2 -0
  59. package/dist/types/matrix-grid.js.map +1 -0
  60. package/package.json +1822 -4
  61. package/src/capabilities/index.test.ts +87 -15
  62. package/src/capabilities/index.ts +5 -2
  63. package/src/cli/index.test.ts +5 -3
  64. package/src/cli/index.ts +5 -2
  65. package/src/features/grid-view/adapters/inbound/GridView.interaction.test.tsx +585 -0
  66. package/src/features/grid-view/adapters/inbound/GridView.test.tsx +74 -0
  67. package/src/features/grid-view/adapters/inbound/GridView.tsx +506 -77
  68. package/src/features/grid-view/adapters/inbound/TileGrid.tsx +26 -3
  69. package/src/features/matrix-grid/adapters/inbound/MatrixGrid.test.tsx +232 -0
  70. package/src/features/matrix-grid/adapters/inbound/MatrixGrid.tsx +142 -0
  71. package/src/features/matrix-grid/application/resolveMatrixGridContentSize.test.ts +32 -0
  72. package/src/features/matrix-grid/application/resolveMatrixGridContentSize.ts +28 -0
  73. package/src/features/matrix-grid/application/resolveMatrixGridSelection.test.ts +10 -0
  74. package/src/features/matrix-grid/application/resolveMatrixGridSelection.ts +15 -0
  75. package/src/features/matrix-grid/application/resolveMatrixGridVisibleCells.test.ts +35 -0
  76. package/src/features/matrix-grid/meta.ts +12 -0
  77. package/src/features/matrix-grid/public.ts +3 -0
  78. package/src/features/registry/ZORA_COMPONENT_REGISTRY.ts +2 -0
  79. package/src/features/registry/componentMeta.ts +2 -0
  80. package/src/features/registry/createEventCapabilities.ts +108 -0
  81. package/src/features/time-grid/adapters/inbound/TimeGrid.test.tsx +35 -8
  82. package/src/index.ts +2 -0
  83. package/src/structureOwnership.test.ts +5 -0
  84. package/src/types/grid-view.ts +27 -1
  85. package/src/types/matrix-grid.ts +29 -0
  86. package/web-dist/chunks/{Accordion-d6azgz09.js → Accordion-2r0gqk3x.js} +1 -1
  87. package/web-dist/chunks/Accordion-a6551mh4.js +515 -0
  88. package/web-dist/chunks/{Accordion-0my2z6qe.js → Accordion-bz04casa.js} +132 -3
  89. package/web-dist/chunks/{Accordion-7qfd4cb4.js → Accordion-jh72azm2.js} +1 -1
  90. package/web-dist/components/explorer/Explorer.js +2 -2
  91. package/web-dist/components/file-explorer/FileExplorer.js +2 -2
  92. package/web-dist/components/grid-line-overlay/GridLineOverlay.js +1 -1
  93. package/web-dist/components/grid-ruler/GridRuler.js +1 -1
  94. package/web-dist/components/grid-view/GridView.js +1 -1
  95. package/web-dist/components/matrix-grid/MatrixGrid.d.ts +3 -0
  96. package/web-dist/components/matrix-grid/MatrixGrid.js +153 -0
  97. package/web-dist/components/matrix-grid/index.d.ts +1 -0
  98. package/web-dist/components/matrix-grid/index.js +2 -0
  99. package/web-dist/components/media-explorer/MediaExplorer.js +2 -2
  100. package/web-dist/components/reader/Reader.js +1 -1
  101. package/web-dist/components/spatial-grid/SpatialGrid.js +1 -1
  102. package/web-dist/components/tile-grid/TileGrid.js +1 -1
  103. package/web-dist/components/time-grid/TimeGrid.js +2 -2
  104. package/web-dist/components/uploader/Uploader.js +2 -2
  105. package/web-dist/manifest.json +53 -24
  106. package/web-dist/chunks/Accordion-gxj5qbpr.js +0 -194
@@ -1,3 +1,4 @@
1
+ import type { GridViewport } from '@ankhorage/grid-view';
1
2
  import React from 'react';
2
3
  import { View as NativeView } from 'react-native';
3
4
 
@@ -13,20 +14,29 @@ export function TileGrid({
13
14
  tileSize = 120,
14
15
  gap = 12,
15
16
  zoom = 1,
17
+ viewport,
18
+ defaultViewport,
19
+ viewportConstraints,
20
+ zoomLimits,
16
21
  overscanPixels,
17
22
  focusedItemId,
18
23
  revealPaddingPixels,
19
24
  onColumnsChange,
25
+ onViewportChange,
20
26
  onVisibleItemIdsChange,
21
27
  renderItem,
22
28
  interactionPolicy,
23
29
  testID,
24
30
  }: TileGridProps) {
25
31
  const [measuredWidth, setMeasuredWidth] = React.useState(0);
32
+ const [uncontrolledPixelsPerUnitX, setUncontrolledPixelsPerUnitX] = React.useState(
33
+ () => defaultViewport?.pixelsPerUnitX ?? zoom,
34
+ );
26
35
  const resolvedWidth = width ?? measuredWidth;
36
+ const pixelsPerUnitX = viewport?.pixelsPerUnitX ?? uncontrolledPixelsPerUnitX;
27
37
  const layout = React.useMemo(
28
- () => layoutTileGrid(items, resolvedWidth, tileSize, gap, zoom),
29
- [items, resolvedWidth, tileSize, gap, zoom],
38
+ () => layoutTileGrid(items, resolvedWidth, tileSize, gap, pixelsPerUnitX),
39
+ [gap, items, pixelsPerUnitX, resolvedWidth, tileSize],
30
40
  );
31
41
  const itemLookup = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
32
42
 
@@ -34,6 +44,14 @@ export function TileGrid({
34
44
  if (resolvedWidth > 0) onColumnsChange?.(layout.columns);
35
45
  }, [layout.columns, onColumnsChange, resolvedWidth]);
36
46
 
47
+ const handleViewportChange = React.useCallback(
48
+ (nextViewport: GridViewport) => {
49
+ if (viewport === undefined) setUncontrolledPixelsPerUnitX(nextViewport.pixelsPerUnitX);
50
+ onViewportChange?.(nextViewport);
51
+ },
52
+ [onViewportChange, viewport],
53
+ );
54
+
37
55
  return (
38
56
  <NativeView
39
57
  onLayout={(event) => setMeasuredWidth(event.nativeEvent.layout.width)}
@@ -43,12 +61,14 @@ export function TileGrid({
43
61
  {resolvedWidth > 0 ? (
44
62
  <GridView
45
63
  contentHeight={layout.height}
46
- contentWidth={Math.max(resolvedWidth / zoom, layout.width)}
64
+ contentWidth={Math.max(resolvedWidth / pixelsPerUnitX, layout.width)}
65
+ defaultViewport={defaultViewport}
47
66
  height={height}
48
67
  focusedItemId={focusedItemId}
49
68
  interactionPolicy={interactionPolicy}
50
69
  items={layout.items}
51
70
  onVisibleItemIdsChange={onVisibleItemIdsChange}
71
+ onViewportChange={handleViewportChange}
52
72
  overscanPixels={overscanPixels}
53
73
  renderItem={(item) => {
54
74
  const source = itemLookup.get(item.id);
@@ -56,6 +76,9 @@ export function TileGrid({
56
76
  }}
57
77
  revealPaddingPixels={revealPaddingPixels}
58
78
  width={resolvedWidth}
79
+ viewport={viewport}
80
+ viewportConstraints={viewportConstraints}
81
+ zoomLimits={zoomLimits}
59
82
  zoom={zoom}
60
83
  />
61
84
  ) : null}
@@ -0,0 +1,232 @@
1
+ import { join } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+
4
+ import { expect, test } from 'bun:test';
5
+ import { Window } from 'happy-dom';
6
+ import React, { act } from 'react';
7
+ import { createRoot } from 'react-dom/client';
8
+
9
+ const browserKeys = [
10
+ 'window',
11
+ 'document',
12
+ 'Node',
13
+ 'navigator',
14
+ 'ShadowRoot',
15
+ 'IS_REACT_ACT_ENVIRONMENT',
16
+ ] as const;
17
+ const webDistRoot = join(import.meta.dir, '../../../../../web-dist');
18
+ const load = (path: string) => import(pathToFileURL(join(webDistRoot, path)).href);
19
+
20
+ /*** Mount MatrixGrid through React Native Web and restore the ambient DOM after each assertion. */
21
+ async function withMatrixDom(assertion: (host: HTMLElement, browser: Window) => Promise<void>) {
22
+ const browser = new Window();
23
+ const previous = browserKeys.map(
24
+ (key) => [key, Object.getOwnPropertyDescriptor(globalThis, key)] as const,
25
+ );
26
+ Object.assign(globalThis, {
27
+ IS_REACT_ACT_ENVIRONMENT: true,
28
+ window: browser,
29
+ document: browser.document,
30
+ Node: browser.Node,
31
+ navigator: browser.navigator,
32
+ ShadowRoot: browser.ShadowRoot,
33
+ });
34
+ const host = document.createElement('div');
35
+ document.body.appendChild(host);
36
+ try {
37
+ await assertion(host, browser);
38
+ } finally {
39
+ browser.close();
40
+ for (const [key, descriptor] of previous) {
41
+ if (descriptor) Object.defineProperty(globalThis, key, descriptor);
42
+ else Reflect.deleteProperty(globalThis, key);
43
+ }
44
+ }
45
+ }
46
+
47
+ const layout = {
48
+ columns: Array.from({ length: 100 }, (_, index) => ({
49
+ id: `column-${index}`,
50
+ size: 10 + (index % 3),
51
+ })),
52
+ rows: Array.from({ length: 100 }, (_, index) => ({ id: `row-${index}`, size: 8 + (index % 4) })),
53
+ };
54
+
55
+ const cells = layout.rows.flatMap((row) =>
56
+ layout.columns.map((column) => ({
57
+ id: `${row.id}-${column.id}`,
58
+ rowId: row.id,
59
+ columnId: column.id,
60
+ })),
61
+ );
62
+
63
+ type MatrixGridComponent = React.ComponentType<{
64
+ readonly cells: typeof cells;
65
+ readonly height: number;
66
+ readonly layout: typeof layout;
67
+ readonly onSelectionChange?: (ids: readonly string[]) => void;
68
+ readonly onViewportChange?: (viewport: unknown) => void;
69
+ readonly renderCell: (cell: { readonly id: string }) => React.ReactNode;
70
+ readonly selectedCellIds?: readonly string[];
71
+ readonly selectionMode?: 'multi';
72
+ readonly testID: string;
73
+ readonly viewport?: {
74
+ readonly height: number;
75
+ readonly offsetX: number;
76
+ readonly offsetY: number;
77
+ readonly pixelsPerUnitX: number;
78
+ readonly pixelsPerUnitY: number;
79
+ readonly width: number;
80
+ };
81
+ readonly width: number;
82
+ readonly zoomX?: number;
83
+ readonly zoomY?: number;
84
+ }>;
85
+
86
+ type ZoraProviderComponent = React.ComponentType<{ readonly children: React.ReactNode }>;
87
+
88
+ test('mounts only visible sparse cells with independent axis zoom and preserves native-web selection', async () => {
89
+ await withMatrixDom(async (host, browser) => {
90
+ const selected: (readonly string[])[] = [];
91
+ const root = createRoot(host);
92
+ const { MatrixGrid } = (await load('components/matrix-grid/MatrixGrid.js')) as {
93
+ MatrixGrid: MatrixGridComponent;
94
+ };
95
+ const { ZoraProvider } = (await load('runtime/ZoraProvider.js')) as {
96
+ ZoraProvider: ZoraProviderComponent;
97
+ };
98
+ try {
99
+ act(() =>
100
+ root.render(
101
+ <ZoraProvider>
102
+ <MatrixGrid
103
+ cells={cells}
104
+ height={96}
105
+ layout={layout}
106
+ selectedCellIds={[]}
107
+ selectionMode="multi"
108
+ testID="matrix"
109
+ viewport={{
110
+ height: 96,
111
+ offsetX: 0,
112
+ offsetY: 0,
113
+ pixelsPerUnitX: 2,
114
+ pixelsPerUnitY: 3,
115
+ width: 120,
116
+ }}
117
+ width={120}
118
+ onSelectionChange={(ids) => selected.push(ids)}
119
+ renderCell={(cell) => <span>{cell.id}</span>}
120
+ />
121
+ </ZoraProvider>,
122
+ ),
123
+ );
124
+ const mountedCells = host.querySelectorAll('[data-testid^="matrix-cell-"]');
125
+ expect(cells).toHaveLength(10_000);
126
+ expect(mountedCells.length).toBeGreaterThan(1);
127
+ expect(mountedCells.length).toBeLessThan(100);
128
+ const target = host.querySelector('[data-testid="matrix-cell-row-1-column-1"]');
129
+ expect(target).not.toBeNull();
130
+ expect(target?.parentElement?.getAttribute('style')).toContain('left: 20px');
131
+ expect(target?.parentElement?.getAttribute('style')).toContain('top: 24px');
132
+ void act(() => target?.dispatchEvent(new browser.MouseEvent('click', { bubbles: true })));
133
+ expect(selected).toEqual([['row-1-column-1']]);
134
+ } finally {
135
+ act(() => root.unmount());
136
+ }
137
+ });
138
+ });
139
+
140
+ test('does not propose controlled viewports on mount or fresh parent callback identities', async () => {
141
+ await withMatrixDom(async (host) => {
142
+ const proposals: unknown[] = [];
143
+ const root = createRoot(host);
144
+ const { MatrixGrid } = (await load('components/matrix-grid/MatrixGrid.js')) as {
145
+ MatrixGrid: MatrixGridComponent;
146
+ };
147
+ const { ZoraProvider } = (await load('runtime/ZoraProvider.js')) as {
148
+ ZoraProvider: ZoraProviderComponent;
149
+ };
150
+ const render = () => (
151
+ <ZoraProvider>
152
+ <MatrixGrid
153
+ cells={cells}
154
+ height={96}
155
+ layout={layout}
156
+ testID="controlled-matrix"
157
+ viewport={{
158
+ height: 96,
159
+ offsetX: 0,
160
+ offsetY: 0,
161
+ pixelsPerUnitX: 2,
162
+ pixelsPerUnitY: 3,
163
+ width: 120,
164
+ }}
165
+ width={120}
166
+ onViewportChange={(proposal) => proposals.push(proposal)}
167
+ renderCell={(cell) => <span>{cell.id}</span>}
168
+ />
169
+ </ZoraProvider>
170
+ );
171
+ try {
172
+ act(() => root.render(render()));
173
+ act(() => root.render(render()));
174
+ expect(proposals).toEqual([]);
175
+ } finally {
176
+ act(() => root.unmount());
177
+ }
178
+ });
179
+ });
180
+
181
+ test('proposes an uncontrolled horizontal pan in world units while retaining independent axis zoom', async () => {
182
+ await withMatrixDom(async (host, browser) => {
183
+ const proposals: unknown[] = [];
184
+ const root = createRoot(host);
185
+ const { MatrixGrid } = (await load('components/matrix-grid/MatrixGrid.js')) as {
186
+ MatrixGrid: MatrixGridComponent;
187
+ };
188
+ const { ZoraProvider } = (await load('runtime/ZoraProvider.js')) as {
189
+ ZoraProvider: ZoraProviderComponent;
190
+ };
191
+ try {
192
+ act(() =>
193
+ root.render(
194
+ <ZoraProvider>
195
+ <MatrixGrid
196
+ cells={cells}
197
+ height={96}
198
+ layout={layout}
199
+ testID="uncontrolled-matrix"
200
+ width={120}
201
+ zoomX={2}
202
+ zoomY={3}
203
+ onViewportChange={(proposal) => proposals.push(proposal)}
204
+ renderCell={(cell) => <span>{cell.id}</span>}
205
+ />
206
+ </ZoraProvider>,
207
+ ),
208
+ );
209
+ const horizontalScroll = host.querySelector(
210
+ '[data-testid="uncontrolled-matrix-horizontal-scroll"]',
211
+ );
212
+ if (!horizontalScroll) throw new Error('Missing MatrixGrid horizontal scroll viewport.');
213
+ horizontalScroll.scrollLeft = 40;
214
+ await act(async () => {
215
+ horizontalScroll.dispatchEvent(new browser.Event('scroll', { bubbles: true }));
216
+ await new Promise<void>((resolve) => browser.setTimeout(resolve, 200));
217
+ });
218
+ expect(proposals).toEqual([
219
+ {
220
+ height: 96,
221
+ offsetX: 20,
222
+ offsetY: 0,
223
+ pixelsPerUnitX: 2,
224
+ pixelsPerUnitY: 3,
225
+ width: 120,
226
+ },
227
+ ]);
228
+ } finally {
229
+ act(() => root.unmount());
230
+ }
231
+ });
232
+ });
@@ -0,0 +1,142 @@
1
+ import {
2
+ getVisibleMatrixCells,
3
+ type GridMatrixCellPlacement,
4
+ type GridViewport,
5
+ } from '@ankhorage/grid-view';
6
+ import { Pressable } from '@ankhorage/surface';
7
+ import React from 'react';
8
+ import { StyleSheet, View } from 'react-native';
9
+
10
+ import type { MatrixGridProps } from '../../../../types/matrix-grid';
11
+ import { GridView } from '../../../grid-view/public';
12
+ import { resolveSelectionEventIntent } from '../../../selection/public';
13
+ import { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope';
14
+ import { useZoraTheme } from '../../../theme/composition/useZoraTheme';
15
+ import { resolveMatrixGridContentSize } from '../../application/resolveMatrixGridContentSize';
16
+ import { resolveMatrixGridSelection } from '../../application/resolveMatrixGridSelection';
17
+
18
+ /*** Compose the canonical GridView around released sparse matrix placement and culling geometry. */
19
+ function MatrixGridInner({
20
+ accessibilityLabel = 'Matrix grid',
21
+ cells,
22
+ height,
23
+ interactionPolicy,
24
+ layout,
25
+ onSelectionChange,
26
+ onViewportChange,
27
+ overscanPixels = 160,
28
+ renderCell,
29
+ selectedCellIds = [],
30
+ selectionMode = 'single',
31
+ testID,
32
+ viewport: controlledViewport,
33
+ width,
34
+ zoomX = 1,
35
+ zoomY = 1,
36
+ }: MatrixGridProps) {
37
+ const { theme } = useZoraTheme();
38
+ const contentSize = React.useMemo(() => resolveMatrixGridContentSize(layout), [layout]);
39
+ const initialViewport = React.useMemo<GridViewport>(
40
+ () => ({ height, offsetX: 0, offsetY: 0, pixelsPerUnitX: zoomX, pixelsPerUnitY: zoomY, width }),
41
+ [height, width, zoomX, zoomY],
42
+ );
43
+ const [uncontrolledViewport, setUncontrolledViewport] = React.useState(initialViewport);
44
+ const viewport = controlledViewport ?? uncontrolledViewport;
45
+ const visibleCells = React.useMemo(
46
+ () => getVisibleMatrixCells(layout, cells, viewport, overscanPixels),
47
+ [cells, layout, overscanPixels, viewport],
48
+ );
49
+
50
+ const visibleCellById = React.useMemo(
51
+ () => new Map(visibleCells.map((cell) => [cell.id, cell])),
52
+ [visibleCells],
53
+ );
54
+
55
+ const handleViewportChange = React.useCallback(
56
+ (proposal: GridViewport) => {
57
+ if (controlledViewport === undefined) setUncontrolledViewport(proposal);
58
+ onViewportChange?.(proposal);
59
+ },
60
+ [controlledViewport, onViewportChange],
61
+ );
62
+
63
+ const activateCell = React.useCallback(
64
+ (cell: GridMatrixCellPlacement, event: unknown) => {
65
+ if (interactionPolicy === 'passive') return;
66
+ const intent = resolveSelectionEventIntent(event, 'pointer');
67
+ onSelectionChange?.(
68
+ resolveMatrixGridSelection(selectedCellIds, cell.id, intent, selectionMode),
69
+ );
70
+ },
71
+ [interactionPolicy, onSelectionChange, selectedCellIds, selectionMode],
72
+ );
73
+ return (
74
+ <View accessibilityLabel={accessibilityLabel}>
75
+ <GridView
76
+ contentHeight={contentSize.height}
77
+ contentWidth={contentSize.width}
78
+ height={height}
79
+ interactionPolicy={interactionPolicy}
80
+ items={visibleCells}
81
+ overscanPixels={0}
82
+ renderItem={(item) => {
83
+ const cell = visibleCellById.get(item.id);
84
+ if (!cell) return null;
85
+ return renderMatrixCell(
86
+ cell,
87
+ selectedCellIds,
88
+ theme.semantics.neutral.divider,
89
+ theme.colors.primary,
90
+ renderCell,
91
+ activateCell,
92
+ testID,
93
+ );
94
+ }}
95
+ testID={testID}
96
+ viewport={viewport}
97
+ width={width}
98
+ onViewportChange={handleViewportChange}
99
+ />
100
+ </View>
101
+ );
102
+ }
103
+
104
+ /*** Render one positioned sparse cell with native/web accessible selection semantics. */
105
+ function renderMatrixCell(
106
+ cell: GridMatrixCellPlacement,
107
+ selectedCellIds: readonly string[],
108
+ dividerColor: string,
109
+ selectedColor: string,
110
+ renderCell: MatrixGridProps['renderCell'],
111
+ activateCell: (cell: GridMatrixCellPlacement, event: unknown) => void,
112
+ testID: string | undefined,
113
+ ) {
114
+ const selected = selectedCellIds.includes(cell.id);
115
+ return (
116
+ <Pressable
117
+ key={cell.id}
118
+ accessibilityLabel={`Cell ${cell.id}`}
119
+ accessibilityRole="button"
120
+ accessibilityState={{ selected }}
121
+ style={[
122
+ styles.cell,
123
+ {
124
+ borderColor: selected ? selectedColor : dividerColor,
125
+ height: '100%',
126
+ width: '100%',
127
+ },
128
+ ]}
129
+ testID={testID === undefined ? undefined : `${testID}-cell-${cell.id}`}
130
+ onPress={(event) => activateCell(cell, event)}
131
+ >
132
+ {renderCell(cell)}
133
+ </Pressable>
134
+ );
135
+ }
136
+
137
+ const styles = StyleSheet.create({
138
+ cell: { borderWidth: 1, height: '100%', overflow: 'hidden', width: '100%' },
139
+ });
140
+
141
+ /*** A code-first generic sparse matrix grid; formulas and domain vocabulary remain external. */
142
+ export const MatrixGrid = withZoraThemeScope(MatrixGridInner);
@@ -0,0 +1,32 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveMatrixGridContentSize } from './resolveMatrixGridContentSize';
4
+
5
+ describe('resolveMatrixGridContentSize', () => {
6
+ test('sums variable matrix axes without materializing logical cells', () => {
7
+ expect(
8
+ resolveMatrixGridContentSize({
9
+ columns: [
10
+ { id: 'beat', size: 40 },
11
+ { id: 'bar', size: 60 },
12
+ ],
13
+ rows: [
14
+ { id: 'bass', size: 24 },
15
+ { id: 'lead', size: 36 },
16
+ ],
17
+ }),
18
+ ).toEqual({ height: 60, width: 100 });
19
+ });
20
+
21
+ test('rejects non-finite cumulative axis geometry through the published grid-view contract', () => {
22
+ expect(() =>
23
+ resolveMatrixGridContentSize({
24
+ columns: [
25
+ { id: 'left', size: Number.MAX_VALUE },
26
+ { id: 'right', size: Number.MAX_VALUE },
27
+ ],
28
+ rows: [{ id: 'row', size: 1 }],
29
+ }),
30
+ ).toThrow('Layout axis cumulative world coordinates must be finite.');
31
+ });
32
+ });
@@ -0,0 +1,28 @@
1
+ import {
2
+ getMatrixCellPlacement,
3
+ getVisibleMatrixCells,
4
+ type GridMatrixLayout,
5
+ } from '@ankhorage/grid-view';
6
+
7
+ /*** Resolve total sparse-matrix world extents without expanding its logical cells. */
8
+ export function resolveMatrixGridContentSize(layout: GridMatrixLayout) {
9
+ const finalRow = layout.rows.at(-1);
10
+ const finalColumn = layout.columns.at(-1);
11
+ if (!finalRow || !finalColumn) {
12
+ getVisibleMatrixCells(layout, [], {
13
+ height: 1,
14
+ offsetX: 0,
15
+ offsetY: 0,
16
+ pixelsPerUnitX: 1,
17
+ pixelsPerUnitY: 1,
18
+ width: 1,
19
+ });
20
+ return { height: 0, width: 0 };
21
+ }
22
+ const finalCell = getMatrixCellPlacement(layout, {
23
+ id: 'matrix-grid-content-boundary',
24
+ rowId: finalRow.id,
25
+ columnId: finalColumn.id,
26
+ });
27
+ return { height: finalCell.y + finalCell.height, width: finalCell.x + finalCell.width };
28
+ }
@@ -0,0 +1,10 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { resolveMatrixGridSelection } from './resolveMatrixGridSelection';
4
+
5
+ describe('resolveMatrixGridSelection', () => {
6
+ test('preserves the canonical single and multi selection intent rules', () => {
7
+ expect(resolveMatrixGridSelection(['a'], 'b', 'toggle', 'single')).toEqual(['b']);
8
+ expect(resolveMatrixGridSelection(['a'], 'b', 'toggle', 'multi')).toEqual(['a', 'b']);
9
+ });
10
+ });
@@ -0,0 +1,15 @@
1
+ import { applySelectionIntent, type SelectionIntent } from '@ankhorage/utility/selection';
2
+
3
+ import type { SelectionMode } from '../../../types/selection';
4
+
5
+ /*** Apply ZORA's canonical selection intent while retaining the configured selection mode. */
6
+ export function resolveMatrixGridSelection(
7
+ selectedIds: readonly string[],
8
+ cellId: string,
9
+ intent: SelectionIntent,
10
+ mode: SelectionMode,
11
+ ): readonly string[] {
12
+ return mode === 'single'
13
+ ? applySelectionIntent(selectedIds, cellId, 'replace')
14
+ : applySelectionIntent(selectedIds, cellId, intent);
15
+ }
@@ -0,0 +1,35 @@
1
+ import { getMatrixCellPlacement, getVisibleMatrixCells } from '@ankhorage/grid-view';
2
+ import { describe, expect, test } from 'bun:test';
3
+
4
+ describe('MatrixGrid sparse geometry', () => {
5
+ test('culls a 10k sparse cell catalogue in a million-cell logical matrix', () => {
6
+ const layout = {
7
+ columns: Array.from({ length: 1000 }, (_, index) => ({ id: `column-${index}`, size: 20 })),
8
+ rows: Array.from({ length: 1000 }, (_, index) => ({
9
+ id: `row-${index}`,
10
+ size: index % 2 === 0 ? 16 : 24,
11
+ })),
12
+ };
13
+ const cells = Array.from({ length: 10001 }, (_, index) => ({
14
+ columnId: `column-${(index * 37) % 1000}`,
15
+ id: `cell-${index}`,
16
+ rowId: `row-${(index * 91) % 1000}`,
17
+ }));
18
+
19
+ const firstCell = cells.at(0);
20
+ if (!firstCell) throw new Error('The sparse test catalogue must contain a first cell.');
21
+ const first = getMatrixCellPlacement(layout, firstCell);
22
+ const visible = getVisibleMatrixCells(layout, cells, {
23
+ height: 120,
24
+ offsetX: first.x,
25
+ offsetY: first.y,
26
+ pixelsPerUnitX: 1,
27
+ pixelsPerUnitY: 1,
28
+ width: 120,
29
+ });
30
+
31
+ expect(first).toMatchObject({ id: 'cell-0', width: 20 });
32
+ expect(visible.length).toBeLessThan(cells.length);
33
+ expect(visible.every((cell) => cell.width === 20 && cell.height > 0)).toBe(true);
34
+ });
35
+ });
@@ -0,0 +1,12 @@
1
+ import type { ZoraComponentMeta } from '../../types/authoring';
2
+
3
+ /** Code-only rendering and non-serializable cell callbacks stay outside manifest authoring. */
4
+ export const matrixGridMeta = {
5
+ name: 'MatrixGrid',
6
+ category: 'foundation',
7
+ description: 'Virtualized sparse matrix renderer for executable code APIs.',
8
+ directManifestNode: false,
9
+ note: 'Requires a renderCell callback and is intentionally code-only.',
10
+ allowedChildren: [],
11
+ props: {},
12
+ } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,3 @@
1
+ export type { MatrixGridProps } from '../../types/matrix-grid';
2
+ export { MatrixGrid } from './adapters/inbound/MatrixGrid';
3
+ export { resolveMatrixGridContentSize } from './application/resolveMatrixGridContentSize';
@@ -55,6 +55,7 @@ import { Image } from '../image/public';
55
55
  import { KeyboardAvoidingView } from '../keyboard-avoiding-view/public';
56
56
  import { AppShell, Divider, Grid, Screen, ScrollView, View } from '../layout/public';
57
57
  import { FlatList, List, ListItem, ListSection, SectionList } from '../list/public';
58
+ import { MatrixGrid } from '../matrix-grid/public';
58
59
  import { MissingElement } from '../missing-element/public';
59
60
  import { Pagination } from '../pagination/public';
60
61
  import { PaletteItem } from '../palette-item/public';
@@ -202,6 +203,7 @@ const _ZORA_COMPONENT_REGISTRY = {
202
203
  GridView,
203
204
  TileGrid,
204
205
  TimeGrid,
206
+ MatrixGrid,
205
207
  MediaExplorer,
206
208
  Hero,
207
209
  List,
@@ -68,6 +68,7 @@ import { viewMeta } from '../layout/viewMeta';
68
68
  import { flatListMeta } from '../list/flatListMeta';
69
69
  import { listItemMeta, listMeta, listSectionMeta } from '../list/meta';
70
70
  import { sectionListMeta } from '../list/sectionListMeta';
71
+ import { matrixGridMeta } from '../matrix-grid/meta';
71
72
  import { missingElementMeta } from '../missing-element/missingElementMeta';
72
73
  import { paginationMeta } from '../pagination/paginationMeta';
73
74
  import { paletteItemMeta } from '../palette-item/paletteItemMeta';
@@ -206,6 +207,7 @@ export const ZORA_COMPONENT_META: ZoraComponentMetaRegistry = finalizeFeatureMet
206
207
  GridView: gridViewMeta,
207
208
  TileGrid: tileGridMeta,
208
209
  TimeGrid: timeGridMeta,
210
+ MatrixGrid: matrixGridMeta,
209
211
  Hero: heroMeta,
210
212
  List: listMeta,
211
213
  ListItem: listItemMeta,