@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.
- package/CHANGELOG.md +30 -0
- package/README.md +2 -1
- package/dist/capabilities/index.d.ts +2 -74
- package/dist/capabilities/index.d.ts.map +1 -1
- package/dist/capabilities/index.js +6158 -59
- package/dist/capabilities/index.js.map +118 -1
- package/dist/cli/index.d.ts +2 -74
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/features/grid-view/adapters/inbound/GridView.d.ts +2 -7
- package/dist/features/grid-view/adapters/inbound/GridView.d.ts.map +1 -1
- package/dist/features/grid-view/adapters/inbound/GridView.js +309 -51
- package/dist/features/grid-view/adapters/inbound/GridView.js.map +1 -1
- package/dist/features/grid-view/adapters/inbound/TileGrid.d.ts +1 -1
- package/dist/features/grid-view/adapters/inbound/TileGrid.d.ts.map +1 -1
- package/dist/features/grid-view/adapters/inbound/TileGrid.js +11 -4
- package/dist/features/grid-view/adapters/inbound/TileGrid.js.map +1 -1
- package/dist/features/matrix-grid/adapters/inbound/MatrixGrid.d.ts +5 -0
- package/dist/features/matrix-grid/adapters/inbound/MatrixGrid.d.ts.map +1 -0
- package/dist/features/matrix-grid/adapters/inbound/MatrixGrid.js +59 -0
- package/dist/features/matrix-grid/adapters/inbound/MatrixGrid.js.map +1 -0
- package/dist/features/matrix-grid/application/resolveMatrixGridContentSize.d.ts +7 -0
- package/dist/features/matrix-grid/application/resolveMatrixGridContentSize.d.ts.map +1 -0
- package/dist/features/matrix-grid/application/resolveMatrixGridContentSize.js +24 -0
- package/dist/features/matrix-grid/application/resolveMatrixGridContentSize.js.map +1 -0
- package/dist/features/matrix-grid/application/resolveMatrixGridSelection.d.ts +5 -0
- package/dist/features/matrix-grid/application/resolveMatrixGridSelection.d.ts.map +1 -0
- package/dist/features/matrix-grid/application/resolveMatrixGridSelection.js +8 -0
- package/dist/features/matrix-grid/application/resolveMatrixGridSelection.js.map +1 -0
- package/dist/features/matrix-grid/meta.d.ts +11 -0
- package/dist/features/matrix-grid/meta.d.ts.map +1 -0
- package/dist/features/matrix-grid/meta.js +11 -0
- package/dist/features/matrix-grid/meta.js.map +1 -0
- package/dist/features/matrix-grid/public.d.ts +4 -0
- package/dist/features/matrix-grid/public.d.ts.map +1 -0
- package/dist/features/matrix-grid/public.js +3 -0
- package/dist/features/matrix-grid/public.js.map +1 -0
- package/dist/features/registry/ZORA_COMPONENT_REGISTRY.d.ts.map +1 -1
- package/dist/features/registry/ZORA_COMPONENT_REGISTRY.js +2 -0
- package/dist/features/registry/ZORA_COMPONENT_REGISTRY.js.map +1 -1
- package/dist/features/registry/componentMeta.d.ts.map +1 -1
- package/dist/features/registry/componentMeta.js +2 -0
- package/dist/features/registry/componentMeta.js.map +1 -1
- package/dist/features/registry/createEventCapabilities.d.ts +4 -0
- package/dist/features/registry/createEventCapabilities.d.ts.map +1 -0
- package/dist/features/registry/createEventCapabilities.js +76 -0
- package/dist/features/registry/createEventCapabilities.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/types/grid-view.d.ts +18 -1
- package/dist/types/grid-view.d.ts.map +1 -1
- package/dist/types/grid-view.js.map +1 -1
- package/dist/types/matrix-grid.d.ts +23 -0
- package/dist/types/matrix-grid.d.ts.map +1 -0
- package/dist/types/matrix-grid.js +2 -0
- package/dist/types/matrix-grid.js.map +1 -0
- package/package.json +1822 -4
- package/src/capabilities/index.test.ts +87 -15
- package/src/capabilities/index.ts +5 -2
- package/src/cli/index.test.ts +5 -3
- package/src/cli/index.ts +5 -2
- package/src/features/grid-view/adapters/inbound/GridView.interaction.test.tsx +585 -0
- package/src/features/grid-view/adapters/inbound/GridView.test.tsx +74 -0
- package/src/features/grid-view/adapters/inbound/GridView.tsx +506 -77
- package/src/features/grid-view/adapters/inbound/TileGrid.tsx +26 -3
- package/src/features/matrix-grid/adapters/inbound/MatrixGrid.test.tsx +232 -0
- package/src/features/matrix-grid/adapters/inbound/MatrixGrid.tsx +142 -0
- package/src/features/matrix-grid/application/resolveMatrixGridContentSize.test.ts +32 -0
- package/src/features/matrix-grid/application/resolveMatrixGridContentSize.ts +28 -0
- package/src/features/matrix-grid/application/resolveMatrixGridSelection.test.ts +10 -0
- package/src/features/matrix-grid/application/resolveMatrixGridSelection.ts +15 -0
- package/src/features/matrix-grid/application/resolveMatrixGridVisibleCells.test.ts +35 -0
- package/src/features/matrix-grid/meta.ts +12 -0
- package/src/features/matrix-grid/public.ts +3 -0
- package/src/features/registry/ZORA_COMPONENT_REGISTRY.ts +2 -0
- package/src/features/registry/componentMeta.ts +2 -0
- package/src/features/registry/createEventCapabilities.ts +108 -0
- package/src/features/time-grid/adapters/inbound/TimeGrid.test.tsx +35 -8
- package/src/index.ts +2 -0
- package/src/structureOwnership.test.ts +5 -0
- package/src/types/grid-view.ts +27 -1
- package/src/types/matrix-grid.ts +29 -0
- package/web-dist/chunks/{Accordion-d6azgz09.js → Accordion-2r0gqk3x.js} +1 -1
- package/web-dist/chunks/Accordion-a6551mh4.js +515 -0
- package/web-dist/chunks/{Accordion-0my2z6qe.js → Accordion-bz04casa.js} +132 -3
- package/web-dist/chunks/{Accordion-7qfd4cb4.js → Accordion-jh72azm2.js} +1 -1
- package/web-dist/components/explorer/Explorer.js +2 -2
- package/web-dist/components/file-explorer/FileExplorer.js +2 -2
- package/web-dist/components/grid-line-overlay/GridLineOverlay.js +1 -1
- package/web-dist/components/grid-ruler/GridRuler.js +1 -1
- package/web-dist/components/grid-view/GridView.js +1 -1
- package/web-dist/components/matrix-grid/MatrixGrid.d.ts +3 -0
- package/web-dist/components/matrix-grid/MatrixGrid.js +153 -0
- package/web-dist/components/matrix-grid/index.d.ts +1 -0
- package/web-dist/components/matrix-grid/index.js +2 -0
- package/web-dist/components/media-explorer/MediaExplorer.js +2 -2
- package/web-dist/components/reader/Reader.js +1 -1
- package/web-dist/components/spatial-grid/SpatialGrid.js +1 -1
- package/web-dist/components/tile-grid/TileGrid.js +1 -1
- package/web-dist/components/time-grid/TimeGrid.js +2 -2
- package/web-dist/components/uploader/Uploader.js +2 -2
- package/web-dist/manifest.json +53 -24
- 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,
|
|
29
|
-
[
|
|
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 /
|
|
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;
|
|
@@ -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,
|