@ankhorage/zora 25.3.0 → 25.5.0

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 (113) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -1
  3. package/dist/features/registry/ZORA_COMPONENT_REGISTRY.d.ts.map +1 -1
  4. package/dist/features/registry/ZORA_COMPONENT_REGISTRY.js +4 -0
  5. package/dist/features/registry/ZORA_COMPONENT_REGISTRY.js.map +1 -1
  6. package/dist/features/registry/componentMeta.d.ts.map +1 -1
  7. package/dist/features/registry/componentMeta.js +4 -0
  8. package/dist/features/registry/componentMeta.js.map +1 -1
  9. package/dist/features/spatial-grid/adapters/inbound/SpatialGrid.d.ts +5 -0
  10. package/dist/features/spatial-grid/adapters/inbound/SpatialGrid.d.ts.map +1 -0
  11. package/dist/features/spatial-grid/adapters/inbound/SpatialGrid.js +38 -0
  12. package/dist/features/spatial-grid/adapters/inbound/SpatialGrid.js.map +1 -0
  13. package/dist/features/spatial-grid/application/sortSpatialGridItems.d.ts +4 -0
  14. package/dist/features/spatial-grid/application/sortSpatialGridItems.d.ts.map +1 -0
  15. package/dist/features/spatial-grid/application/sortSpatialGridItems.js +8 -0
  16. package/dist/features/spatial-grid/application/sortSpatialGridItems.js.map +1 -0
  17. package/dist/features/spatial-grid/meta.d.ts +11 -0
  18. package/dist/features/spatial-grid/meta.d.ts.map +1 -0
  19. package/dist/features/spatial-grid/meta.js +11 -0
  20. package/dist/features/spatial-grid/meta.js.map +1 -0
  21. package/dist/features/spatial-grid/public.d.ts +3 -0
  22. package/dist/features/spatial-grid/public.d.ts.map +1 -0
  23. package/dist/features/spatial-grid/public.js +2 -0
  24. package/dist/features/spatial-grid/public.js.map +1 -0
  25. package/dist/features/time-grid/adapters/inbound/TimeGrid.d.ts +5 -0
  26. package/dist/features/time-grid/adapters/inbound/TimeGrid.d.ts.map +1 -0
  27. package/dist/features/time-grid/adapters/inbound/TimeGrid.js +107 -0
  28. package/dist/features/time-grid/adapters/inbound/TimeGrid.js.map +1 -0
  29. package/dist/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.d.ts +9 -0
  30. package/dist/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.d.ts.map +1 -0
  31. package/dist/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.js +5 -0
  32. package/dist/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.js.map +1 -0
  33. package/dist/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.native.d.ts +9 -0
  34. package/dist/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.native.d.ts.map +1 -0
  35. package/dist/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.native.js +5 -0
  36. package/dist/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.native.js.map +1 -0
  37. package/dist/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.web.d.ts +9 -0
  38. package/dist/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.web.d.ts.map +1 -0
  39. package/dist/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.web.js +10 -0
  40. package/dist/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.web.js.map +1 -0
  41. package/dist/features/time-grid/application/resolveTimeGridNextFocusId.d.ts +4 -0
  42. package/dist/features/time-grid/application/resolveTimeGridNextFocusId.d.ts.map +1 -0
  43. package/dist/features/time-grid/application/resolveTimeGridNextFocusId.js +31 -0
  44. package/dist/features/time-grid/application/resolveTimeGridNextFocusId.js.map +1 -0
  45. package/dist/features/time-grid/application/resolveTimeGridVisibleIntervals.d.ts +5 -0
  46. package/dist/features/time-grid/application/resolveTimeGridVisibleIntervals.d.ts.map +1 -0
  47. package/dist/features/time-grid/application/resolveTimeGridVisibleIntervals.js +6 -0
  48. package/dist/features/time-grid/application/resolveTimeGridVisibleIntervals.js.map +1 -0
  49. package/dist/features/time-grid/meta.d.ts +11 -0
  50. package/dist/features/time-grid/meta.d.ts.map +1 -0
  51. package/dist/features/time-grid/meta.js +11 -0
  52. package/dist/features/time-grid/meta.js.map +1 -0
  53. package/dist/features/time-grid/public.d.ts +5 -0
  54. package/dist/features/time-grid/public.d.ts.map +1 -0
  55. package/dist/features/time-grid/public.js +4 -0
  56. package/dist/features/time-grid/public.js.map +1 -0
  57. package/dist/index.d.ts +4 -0
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +2 -0
  60. package/dist/index.js.map +1 -1
  61. package/dist/types/spatial-grid.d.ts +41 -0
  62. package/dist/types/spatial-grid.d.ts.map +1 -0
  63. package/dist/types/spatial-grid.js +2 -0
  64. package/dist/types/spatial-grid.js.map +1 -0
  65. package/dist/types/time-grid.d.ts +30 -0
  66. package/dist/types/time-grid.d.ts.map +1 -0
  67. package/dist/types/time-grid.js +2 -0
  68. package/dist/types/time-grid.js.map +1 -0
  69. package/package.json +3 -3
  70. package/src/features/registry/ZORA_COMPONENT_REGISTRY.ts +4 -0
  71. package/src/features/registry/componentMeta.ts +4 -0
  72. package/src/features/spatial-grid/adapters/inbound/SpatialGrid.test.tsx +248 -0
  73. package/src/features/spatial-grid/adapters/inbound/SpatialGrid.tsx +112 -0
  74. package/src/features/spatial-grid/application/sortSpatialGridItems.test.ts +42 -0
  75. package/src/features/spatial-grid/application/sortSpatialGridItems.ts +14 -0
  76. package/src/features/spatial-grid/meta.ts +12 -0
  77. package/src/features/spatial-grid/public.ts +6 -0
  78. package/src/features/time-grid/adapters/inbound/TimeGrid.test.tsx +131 -0
  79. package/src/features/time-grid/adapters/inbound/TimeGrid.tsx +197 -0
  80. package/src/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.native.tsx +11 -0
  81. package/src/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.tsx +11 -0
  82. package/src/features/time-grid/adapters/inbound/TimeGridKeyboardProxy.web.tsx +20 -0
  83. package/src/features/time-grid/application/resolveTimeGridNextFocusId.test.ts +46 -0
  84. package/src/features/time-grid/application/resolveTimeGridNextFocusId.ts +41 -0
  85. package/src/features/time-grid/application/resolveTimeGridVisibleIntervals.test.ts +33 -0
  86. package/src/features/time-grid/application/resolveTimeGridVisibleIntervals.ts +13 -0
  87. package/src/features/time-grid/meta.ts +12 -0
  88. package/src/features/time-grid/public.ts +4 -0
  89. package/src/index.ts +8 -0
  90. package/src/types/spatial-grid.ts +44 -0
  91. package/src/types/time-grid.ts +33 -0
  92. package/web-dist/chunks/{Accordion-e6ddpz5v.js → Accordion-0my2z6qe.js} +85 -1
  93. package/web-dist/chunks/{Accordion-e9ewv367.js → Accordion-7qfd4cb4.js} +1 -1
  94. package/web-dist/chunks/{Accordion-689w7frk.js → Accordion-d6azgz09.js} +1 -1
  95. package/web-dist/chunks/{Accordion-q2z00p5r.js → Accordion-gxj5qbpr.js} +1 -1
  96. package/web-dist/components/explorer/Explorer.js +2 -2
  97. package/web-dist/components/file-explorer/FileExplorer.js +2 -2
  98. package/web-dist/components/grid-line-overlay/GridLineOverlay.js +1 -1
  99. package/web-dist/components/grid-ruler/GridRuler.js +1 -1
  100. package/web-dist/components/grid-view/GridView.js +1 -1
  101. package/web-dist/components/media-explorer/MediaExplorer.js +2 -2
  102. package/web-dist/components/reader/Reader.js +1 -1
  103. package/web-dist/components/spatial-grid/SpatialGrid.d.ts +3 -0
  104. package/web-dist/components/spatial-grid/SpatialGrid.js +102 -0
  105. package/web-dist/components/spatial-grid/index.d.ts +1 -0
  106. package/web-dist/components/spatial-grid/index.js +2 -0
  107. package/web-dist/components/tile-grid/TileGrid.js +1 -1
  108. package/web-dist/components/time-grid/TimeGrid.d.ts +3 -0
  109. package/web-dist/components/time-grid/TimeGrid.js +208 -0
  110. package/web-dist/components/time-grid/index.d.ts +1 -0
  111. package/web-dist/components/time-grid/index.js +2 -0
  112. package/web-dist/components/uploader/Uploader.js +2 -2
  113. package/web-dist/manifest.json +52 -20
@@ -0,0 +1,197 @@
1
+ import { getLaneIntervalPlacement, type GridViewport } from '@ankhorage/grid-view';
2
+ import React from 'react';
3
+ import { Pressable, View } from 'react-native';
4
+
5
+ import type { TimeGridProps } from '../../../../types/time-grid';
6
+ import { GridView } from '../../../grid-view/public';
7
+ import { resolveTimeGridNextFocusId } from '../../application/resolveTimeGridNextFocusId';
8
+ import { resolveTimeGridVisibleIntervals } from '../../application/resolveTimeGridVisibleIntervals';
9
+ import { TimeGridKeyboardProxy } from './TimeGridKeyboardProxy';
10
+
11
+ /*** Presents virtualized variable-height interval lanes through ZORA's shared GridView renderer. */
12
+ export function TimeGrid({
13
+ contentWidth,
14
+ focusedIntervalId,
15
+ height,
16
+ interactionPolicy,
17
+ intervals,
18
+ lanes,
19
+ onFocusedIntervalIdChange,
20
+ onIntervalPress,
21
+ onViewportChange,
22
+ onVisibleIntervalIdsChange,
23
+ overscanPixels,
24
+ renderInterval,
25
+ renderLaneLabel,
26
+ revealPaddingPixels,
27
+ selectedIntervalIds = [],
28
+ testID,
29
+ width,
30
+ zoom,
31
+ }: TimeGridProps) {
32
+ const scale = Math.max(0.01, zoom ?? 1);
33
+ const [scrollOffsets, setScrollOffsets] = React.useState(() => ({
34
+ offsetX: 0,
35
+ offsetY: 0,
36
+ }));
37
+ const viewport = React.useMemo<GridViewport>(
38
+ () => ({
39
+ ...scrollOffsets,
40
+ height,
41
+ pixelsPerUnitX: scale,
42
+ pixelsPerUnitY: scale,
43
+ width,
44
+ }),
45
+ [height, scale, scrollOffsets, width],
46
+ );
47
+ const intervalById = React.useMemo(
48
+ () => new Map(intervals.map((interval) => [interval.id, interval])),
49
+ [intervals],
50
+ );
51
+ const selectedIds = React.useMemo(() => new Set(selectedIntervalIds), [selectedIntervalIds]);
52
+ const contentHeight = React.useMemo(
53
+ () => lanes.reduce((total, lane) => total + lane.height, 0),
54
+ [lanes],
55
+ );
56
+ const visibleIntervals = React.useMemo(
57
+ () => resolveTimeGridVisibleIntervals(lanes, intervals, viewport, overscanPixels),
58
+ [intervals, lanes, overscanPixels, viewport],
59
+ );
60
+ const focusedPlacement = React.useMemo(() => {
61
+ const focusedInterval = intervals.find((interval) => interval.id === focusedIntervalId);
62
+ return focusedInterval ? getLaneIntervalPlacement(lanes, focusedInterval) : undefined;
63
+ }, [focusedIntervalId, intervals, lanes]);
64
+ const gridItems = React.useMemo(
65
+ () =>
66
+ focusedPlacement && !visibleIntervals.some((interval) => interval.id === focusedPlacement.id)
67
+ ? [...visibleIntervals, focusedPlacement]
68
+ : visibleIntervals,
69
+ [focusedPlacement, visibleIntervals],
70
+ );
71
+ const labelIntervals = React.useMemo(
72
+ () =>
73
+ lanes.map((lane) => ({
74
+ extent: contentWidth,
75
+ id: `lane-label-${lane.id}`,
76
+ laneId: lane.id,
77
+ start: 0,
78
+ })),
79
+ [contentWidth, lanes],
80
+ );
81
+ const visibleLabels = React.useMemo(
82
+ () => resolveTimeGridVisibleIntervals(lanes, labelIntervals, viewport, overscanPixels),
83
+ [labelIntervals, lanes, overscanPixels, viewport],
84
+ );
85
+
86
+ const handleViewportChange = React.useCallback(
87
+ (nextViewport: GridViewport) => {
88
+ setScrollOffsets((current) =>
89
+ current.offsetX === nextViewport.offsetX && current.offsetY === nextViewport.offsetY
90
+ ? current
91
+ : { offsetX: nextViewport.offsetX, offsetY: nextViewport.offsetY },
92
+ );
93
+ onViewportChange?.(nextViewport);
94
+ },
95
+ [onViewportChange],
96
+ );
97
+
98
+ const resolveNextFocusId = React.useCallback(
99
+ (intervalId: string, direction: 'down' | 'left' | 'right' | 'up') => {
100
+ const allIntervals = resolveTimeGridVisibleIntervals(lanes, intervals, {
101
+ height: contentHeight,
102
+ offsetX: 0,
103
+ offsetY: 0,
104
+ pixelsPerUnitX: 1,
105
+ pixelsPerUnitY: 1,
106
+ width: contentWidth,
107
+ });
108
+ return resolveTimeGridNextFocusId(allIntervals, intervalId, direction);
109
+ },
110
+ [contentHeight, contentWidth, intervals, lanes],
111
+ );
112
+
113
+ return (
114
+ <View style={{ flexDirection: 'row', height, width }} testID={testID}>
115
+ {renderLaneLabel ? (
116
+ <View
117
+ accessibilityElementsHidden
118
+ pointerEvents="none"
119
+ style={{ height, overflow: 'hidden' }}
120
+ >
121
+ {visibleLabels.map((placement) => {
122
+ const lane = lanes[placement.laneIndex];
123
+ if (!lane) return null;
124
+ return (
125
+ <View
126
+ key={lane.id}
127
+ style={{
128
+ height: placement.height * scale,
129
+ position: 'absolute',
130
+ top: (placement.y - viewport.offsetY) * scale,
131
+ }}
132
+ testID={testID === undefined ? undefined : `${testID}-lane-label-${lane.id}`}
133
+ >
134
+ {renderLaneLabel(lane)}
135
+ </View>
136
+ );
137
+ })}
138
+ </View>
139
+ ) : null}
140
+ <GridView
141
+ contentHeight={contentHeight}
142
+ contentWidth={contentWidth}
143
+ focusedItemId={focusedIntervalId}
144
+ height={height}
145
+ interactionPolicy={interactionPolicy}
146
+ items={gridItems}
147
+ onViewportChange={handleViewportChange}
148
+ onVisibleItemIdsChange={onVisibleIntervalIdsChange}
149
+ overscanPixels={overscanPixels}
150
+ renderItem={(placement) => {
151
+ const interval = intervalById.get(placement.id);
152
+ if (!interval) return null;
153
+ const selected = selectedIds.has(interval.id);
154
+ return (
155
+ <TimeGridKeyboardProxy
156
+ onKeyDown={(key) => {
157
+ const direction = resolveKeyboardDirection(key);
158
+ const nextId =
159
+ direction === undefined ? undefined : resolveNextFocusId(interval.id, direction);
160
+ if (!nextId) return false;
161
+ onFocusedIntervalIdChange?.(nextId);
162
+ return true;
163
+ }}
164
+ >
165
+ <Pressable
166
+ accessibilityLabel={`Interval ${interval.id}`}
167
+ accessibilityRole="button"
168
+ disabled={interactionPolicy === 'passive'}
169
+ onPress={() => {
170
+ onFocusedIntervalIdChange?.(interval.id);
171
+ onIntervalPress?.(interval);
172
+ }}
173
+ style={{ flex: 1 }}
174
+ testID={testID === undefined ? undefined : `${testID}-interval-${interval.id}`}
175
+ >
176
+ {renderInterval(interval, selected)}
177
+ </Pressable>
178
+ </TimeGridKeyboardProxy>
179
+ );
180
+ }}
181
+ revealPaddingPixels={revealPaddingPixels}
182
+ testID={testID === undefined ? undefined : `${testID}-viewport`}
183
+ width={width}
184
+ zoom={zoom}
185
+ />
186
+ </View>
187
+ );
188
+ }
189
+
190
+ /*** Maps desktop arrow keys to the platform-neutral interval navigation directions. */
191
+ function resolveKeyboardDirection(key: string): 'down' | 'left' | 'right' | 'up' | undefined {
192
+ if (key === 'ArrowDown') return 'down';
193
+ if (key === 'ArrowLeft') return 'left';
194
+ if (key === 'ArrowRight') return 'right';
195
+ if (key === 'ArrowUp') return 'up';
196
+ return undefined;
197
+ }
@@ -0,0 +1,11 @@
1
+ import type React from 'react';
2
+
3
+ /*** Preserves native activation and touch interaction where DOM arrow-key events do not exist. */
4
+ export function TimeGridKeyboardProxy({ children }: TimeGridKeyboardProxyProps) {
5
+ return <>{children}</>;
6
+ }
7
+
8
+ interface TimeGridKeyboardProxyProps {
9
+ readonly children: React.ReactNode;
10
+ readonly onKeyDown: (key: string) => boolean;
11
+ }
@@ -0,0 +1,11 @@
1
+ import type React from 'react';
2
+
3
+ /*** Provides the native fallback type surface; React Native Web resolves the DOM adapter at runtime. */
4
+ export function TimeGridKeyboardProxy({ children }: TimeGridKeyboardProxyProps) {
5
+ return <>{children}</>;
6
+ }
7
+
8
+ interface TimeGridKeyboardProxyProps {
9
+ readonly children: React.ReactNode;
10
+ readonly onKeyDown: (key: string) => boolean;
11
+ }
@@ -0,0 +1,20 @@
1
+ import type React from 'react';
2
+
3
+ /*** Bridges DOM arrow keys into portable interval focus navigation on React Native Web. */
4
+ export function TimeGridKeyboardProxy({ children, onKeyDown }: TimeGridKeyboardProxyProps) {
5
+ return (
6
+ <div
7
+ onKeyDown={(event) => {
8
+ if (onKeyDown(event.key)) event.preventDefault();
9
+ }}
10
+ style={{ display: 'flex', flex: 1 }}
11
+ >
12
+ {children}
13
+ </div>
14
+ );
15
+ }
16
+
17
+ interface TimeGridKeyboardProxyProps {
18
+ readonly children: React.ReactNode;
19
+ readonly onKeyDown: (key: string) => boolean;
20
+ }
@@ -0,0 +1,46 @@
1
+ import { expect, test } from 'bun:test';
2
+
3
+ import { resolveTimeGridNextFocusId } from './resolveTimeGridNextFocusId';
4
+
5
+ const placements = [
6
+ {
7
+ extent: 4,
8
+ height: 20,
9
+ id: 'drums-a',
10
+ laneId: 'drums',
11
+ laneIndex: 0,
12
+ start: 2,
13
+ width: 4,
14
+ x: 2,
15
+ y: 0,
16
+ },
17
+ {
18
+ extent: 4,
19
+ height: 20,
20
+ id: 'drums-b',
21
+ laneId: 'drums',
22
+ laneIndex: 0,
23
+ start: 12,
24
+ width: 4,
25
+ x: 12,
26
+ y: 0,
27
+ },
28
+ {
29
+ extent: 4,
30
+ height: 20,
31
+ id: 'bass-a',
32
+ laneId: 'bass',
33
+ laneIndex: 1,
34
+ start: 3,
35
+ width: 4,
36
+ x: 3,
37
+ y: 20,
38
+ },
39
+ ] as const;
40
+
41
+ test('moves keyboard focus by interval geometry without calendar or music semantics', () => {
42
+ expect(resolveTimeGridNextFocusId(placements, 'drums-a', 'right')).toBe('drums-b');
43
+ expect(resolveTimeGridNextFocusId(placements, 'drums-b', 'left')).toBe('drums-a');
44
+ expect(resolveTimeGridNextFocusId(placements, 'drums-a', 'down')).toBe('bass-a');
45
+ expect(resolveTimeGridNextFocusId(placements, 'bass-a', 'up')).toBe('drums-a');
46
+ });
@@ -0,0 +1,41 @@
1
+ import type { GridLaneIntervalPlacement } from '@ankhorage/grid-view';
2
+
3
+ /*** Resolves a neighboring interval for keyboard movement without attaching domain meaning to lanes. */
4
+ export function resolveTimeGridNextFocusId(
5
+ placements: readonly GridLaneIntervalPlacement[],
6
+ focusedId: string,
7
+ direction: 'down' | 'left' | 'right' | 'up',
8
+ ): string | undefined {
9
+ const focused = placements.find((placement) => placement.id === focusedId);
10
+ if (!focused) return undefined;
11
+ const candidates = placements.filter((placement) => {
12
+ if (direction === 'left') return placement.laneId === focused.laneId && placement.x < focused.x;
13
+ if (direction === 'right')
14
+ return placement.laneId === focused.laneId && placement.x > focused.x;
15
+ if (direction === 'up') return placement.laneIndex < focused.laneIndex;
16
+ return placement.laneIndex > focused.laneIndex;
17
+ });
18
+ return candidates
19
+ .sort(
20
+ (first, second) =>
21
+ resolveFocusDistance(first, focused, direction) -
22
+ resolveFocusDistance(second, focused, direction),
23
+ )
24
+ .at(0)?.id;
25
+ }
26
+
27
+ /*** Measures nearby candidates by axis first and timeline/lane offset second. */
28
+ function resolveFocusDistance(
29
+ candidate: GridLaneIntervalPlacement,
30
+ focused: GridLaneIntervalPlacement,
31
+ direction: 'down' | 'left' | 'right' | 'up',
32
+ ): number {
33
+ const horizontal = direction === 'left' || direction === 'right';
34
+ const primary = horizontal
35
+ ? Math.abs(candidate.x - focused.x)
36
+ : Math.abs(candidate.laneIndex - focused.laneIndex);
37
+ const secondary = horizontal
38
+ ? Math.abs(candidate.laneIndex - focused.laneIndex)
39
+ : Math.abs(candidate.x - focused.x);
40
+ return primary * 1_000_000 + secondary;
41
+ }
@@ -0,0 +1,33 @@
1
+ import { expect, test } from 'bun:test';
2
+
3
+ import { resolveTimeGridVisibleIntervals } from './resolveTimeGridVisibleIntervals';
4
+
5
+ test('culls sparse intervals across both time and lane axes', () => {
6
+ expect(
7
+ resolveTimeGridVisibleIntervals(
8
+ [
9
+ { height: 20, id: 'drums' },
10
+ { height: 40, id: 'bass' },
11
+ { height: 30, id: 'strings' },
12
+ ],
13
+ [
14
+ { extent: 4, id: 'intro', laneId: 'drums', start: 2 },
15
+ { extent: 4, id: 'verse', laneId: 'bass', start: 20 },
16
+ { extent: 4, id: 'outro', laneId: 'strings', start: 2 },
17
+ ],
18
+ { height: 45, offsetX: 0, offsetY: 0, pixelsPerUnitX: 1, pixelsPerUnitY: 1, width: 12 },
19
+ ),
20
+ ).toEqual([
21
+ {
22
+ extent: 4,
23
+ height: 20,
24
+ id: 'intro',
25
+ laneId: 'drums',
26
+ laneIndex: 0,
27
+ start: 2,
28
+ width: 4,
29
+ x: 2,
30
+ y: 0,
31
+ },
32
+ ]);
33
+ });
@@ -0,0 +1,13 @@
1
+ import { getVisibleLaneIntervals, type GridViewport } from '@ankhorage/grid-view';
2
+
3
+ import type { TimeGridInterval, TimeGridLane } from '../../../types/time-grid';
4
+
5
+ /*** Resolves the ordered interval placements visible in both world-space axes. */
6
+ export function resolveTimeGridVisibleIntervals(
7
+ lanes: readonly TimeGridLane[],
8
+ intervals: readonly TimeGridInterval[],
9
+ viewport: GridViewport,
10
+ overscanPixels?: number,
11
+ ) {
12
+ return getVisibleLaneIntervals(lanes, intervals, viewport, overscanPixels);
13
+ }
@@ -0,0 +1,12 @@
1
+ import type { ZoraComponentMeta } from '../../types/authoring';
2
+
3
+ /** TimeGrid remains code-only because interval and lane presentation require executable renderers. */
4
+ export const timeGridMeta = {
5
+ name: 'TimeGrid',
6
+ category: 'foundation',
7
+ description: 'Virtualized variable-height world-space interval lanes for code-defined timelines.',
8
+ directManifestNode: false,
9
+ note: 'Requires executable interval and optional lane-label renderers; available through code APIs, not a serializable manifest node.',
10
+ allowedChildren: [],
11
+ props: {},
12
+ } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,4 @@
1
+ export type { TimeGridInterval, TimeGridLane, TimeGridProps } from '../../types/time-grid';
2
+ export { TimeGrid } from './adapters/inbound/TimeGrid';
3
+ export { resolveTimeGridNextFocusId } from './application/resolveTimeGridNextFocusId';
4
+ export { resolveTimeGridVisibleIntervals } from './application/resolveTimeGridVisibleIntervals';
package/src/index.ts CHANGED
@@ -416,6 +416,12 @@ export {
416
416
  SkeletonText,
417
417
  type SkeletonTextProps,
418
418
  } from './features/skeleton/public';
419
+ export type {
420
+ SpatialGridItem,
421
+ SpatialGridItemState,
422
+ SpatialGridProps,
423
+ } from './features/spatial-grid/public';
424
+ export { SpatialGrid } from './features/spatial-grid/public';
419
425
  export type { SurfaceProps, SurfaceVariant } from './features/surface/public';
420
426
  export { Surface } from './features/surface/public';
421
427
  export type {
@@ -478,6 +484,8 @@ export {
478
484
  } from './features/theme/colorModel';
479
485
  export * from './features/theme/public';
480
486
  export * from './features/theme/runtime';
487
+ export type { TimeGridInterval, TimeGridLane, TimeGridProps } from './features/time-grid/public';
488
+ export { resolveTimeGridVisibleIntervals, TimeGrid } from './features/time-grid/public';
481
489
  export {
482
490
  TimePicker,
483
491
  type TimePickerProps,
@@ -0,0 +1,44 @@
1
+ import type { GridRectItem, GridViewport } from '@ankhorage/grid-view';
2
+ import type React from 'react';
3
+
4
+ import type { ZoraBaseProps } from './base';
5
+
6
+ /** A free-positioned item retains the grid-view world rectangle and adds presentation policy. */
7
+ export interface SpatialGridItem extends GridRectItem {
8
+ /** Higher values are painted and hit-tested above lower values; equal values preserve input order. */
9
+ readonly zIndex?: number;
10
+ /** Accessible name for the item; defaults to a stable ID-derived board-item name. */
11
+ readonly accessibilityLabel?: string;
12
+ /** Prevents selection and activation for this item while keeping it visible. */
13
+ readonly disabled?: boolean;
14
+ }
15
+
16
+ /** State supplied to the code-only item renderer. */
17
+ export interface SpatialGridItemState {
18
+ readonly selected: boolean;
19
+ readonly focused: boolean;
20
+ readonly disabled: boolean;
21
+ }
22
+
23
+ /** Props for the virtualized, free-placement presentation over canonical GridView geometry. */
24
+ export interface SpatialGridProps extends ZoraBaseProps {
25
+ readonly items: readonly SpatialGridItem[];
26
+ readonly contentWidth: number;
27
+ readonly contentHeight: number;
28
+ readonly width: number;
29
+ readonly height: number;
30
+ readonly zoom?: number;
31
+ readonly overscanPixels?: number;
32
+ readonly selectedItemId?: string;
33
+ readonly focusedItemId?: string;
34
+ readonly revealPaddingPixels?: number;
35
+ readonly disabled?: boolean;
36
+ /** Prevents selection and activation while retaining focus and viewport navigation. */
37
+ readonly readOnly?: boolean;
38
+ readonly onSelectionChange?: (id: string) => void;
39
+ readonly onActivation?: (id: string) => void;
40
+ readonly onFocusChange?: (id: string) => void;
41
+ readonly onViewportChange?: (viewport: GridViewport) => void;
42
+ readonly onVisibleItemIdsChange?: (ids: readonly string[]) => void;
43
+ readonly renderItem: (item: SpatialGridItem, state: SpatialGridItemState) => React.ReactNode;
44
+ }
@@ -0,0 +1,33 @@
1
+ import type { GridLane, GridLaneInterval, GridViewport } from '@ankhorage/grid-view';
2
+ import type React from 'react';
3
+
4
+ import type { ZoraBaseProps } from './base';
5
+
6
+ /** An ordered, variable-height track rendered by the generic TimeGrid. */
7
+ export type TimeGridLane = GridLane;
8
+
9
+ /** A stable world-space interval rendered by the generic TimeGrid. */
10
+ export type TimeGridInterval = GridLaneInterval;
11
+
12
+ /**
13
+ * Code API for virtualized interval lanes. Domain adapters map dates, PPQ ticks, and other units
14
+ * to numeric world coordinates before reaching this boundary.
15
+ */
16
+ export interface TimeGridProps extends ZoraBaseProps {
17
+ readonly lanes: readonly TimeGridLane[];
18
+ readonly intervals: readonly TimeGridInterval[];
19
+ readonly contentWidth: number;
20
+ readonly width: number;
21
+ readonly height: number;
22
+ readonly zoom?: number;
23
+ readonly overscanPixels?: number;
24
+ readonly focusedIntervalId?: string;
25
+ readonly onFocusedIntervalIdChange?: (id: string) => void;
26
+ readonly revealPaddingPixels?: number;
27
+ readonly selectedIntervalIds?: readonly string[];
28
+ readonly onIntervalPress?: (interval: TimeGridInterval) => void;
29
+ readonly onViewportChange?: (viewport: GridViewport) => void;
30
+ readonly onVisibleIntervalIdsChange?: (ids: readonly string[]) => void;
31
+ readonly renderInterval: (interval: TimeGridInterval, selected: boolean) => React.ReactNode;
32
+ readonly renderLaneLabel?: (lane: TimeGridLane) => React.ReactNode;
33
+ }
@@ -47,6 +47,68 @@ function getVisibleGridItems(items, viewport, overscanPixels = 0) {
47
47
  return items.filter((item) => item.x <= bounds.x + bounds.width && item.x + item.width >= bounds.x && item.y <= bounds.y + bounds.height && item.y + item.height >= bounds.y);
48
48
  }
49
49
 
50
+ // node_modules/@ankhorage/grid-view/dist/features/layout/createGridAxisMetrics.js
51
+ function createGridAxisMetrics(entries) {
52
+ const indexes = new Map;
53
+ const offsets = [];
54
+ const sizes = [];
55
+ let offset = 0;
56
+ entries.forEach((entry, index) => {
57
+ if (!entry.id || indexes.has(entry.id)) {
58
+ throw new RangeError("Layout axis entries require unique nonempty IDs.");
59
+ }
60
+ if (!Number.isFinite(entry.size) || entry.size <= 0) {
61
+ throw new RangeError("Layout axis entry sizes must be finite positive numbers.");
62
+ }
63
+ const end = offset + entry.size;
64
+ if (!Number.isFinite(end)) {
65
+ throw new RangeError("Layout axis cumulative world coordinates must be finite.");
66
+ }
67
+ indexes.set(entry.id, index);
68
+ offsets.push(offset);
69
+ sizes.push(entry.size);
70
+ offset = end;
71
+ });
72
+ return { indexes, offsets, sizes };
73
+ }
74
+
75
+ // node_modules/@ankhorage/grid-view/dist/features/layout/utils/readGridAxisMetric.js
76
+ function readGridAxisMetric(values, index) {
77
+ const value = values.at(index);
78
+ if (value === undefined) {
79
+ throw new RangeError("Layout axis metric indexes must reference an existing entry.");
80
+ }
81
+ return value;
82
+ }
83
+
84
+ // node_modules/@ankhorage/grid-view/dist/features/layout/utils/resolveLaneIntervalPlacement.js
85
+ function resolveLaneIntervalPlacement(interval, lanes) {
86
+ if (!interval.id || !Number.isFinite(interval.start) || !Number.isFinite(interval.extent) || interval.extent <= 0) {
87
+ throw new RangeError("Lane intervals require an ID, finite start, and positive extent.");
88
+ }
89
+ if (!Number.isFinite(interval.start + interval.extent)) {
90
+ throw new RangeError("Lane interval end coordinates must be finite.");
91
+ }
92
+ const laneIndex = lanes.indexes.get(interval.laneId);
93
+ if (laneIndex === undefined) {
94
+ throw new RangeError("Lane intervals must reference an existing lane.");
95
+ }
96
+ return {
97
+ ...interval,
98
+ laneIndex,
99
+ x: interval.start,
100
+ y: readGridAxisMetric(lanes.offsets, laneIndex),
101
+ width: interval.extent,
102
+ height: readGridAxisMetric(lanes.sizes, laneIndex)
103
+ };
104
+ }
105
+
106
+ // node_modules/@ankhorage/grid-view/dist/features/layout/getLaneIntervalPlacement.js
107
+ function getLaneIntervalPlacement(lanes, interval) {
108
+ const laneMetrics = createGridAxisMetrics(lanes.map(({ id, height }) => ({ id, size: height })));
109
+ return resolveLaneIntervalPlacement(interval, laneMetrics);
110
+ }
111
+
50
112
  // node_modules/@ankhorage/grid-view/dist/features/viewport/constrainViewport.js
51
113
  function constrainViewport(viewport, constraints) {
52
114
  assertValidViewport(viewport);
@@ -206,4 +268,26 @@ function adaptiveStep(minimum) {
206
268
  const multiplier = [1, 2, 5, 10].find((candidate) => candidate * magnitude >= minimum) ?? 10;
207
269
  return multiplier * magnitude;
208
270
  }
209
- export { getAxisCategoryTicks, getAxisTicks, getVisibleGridItems, revealWorldRect, worldToViewport };
271
+ // node_modules/@ankhorage/grid-view/dist/features/layout/getVisibleLaneIntervals.js
272
+ function getVisibleLaneIntervals(lanes, intervals, viewport, overscanPixels = 0) {
273
+ const laneMetrics = createGridAxisMetrics(lanes.map(({ id, height }) => ({ id, size: height })));
274
+ const bounds = getVisibleWorldBounds(viewport, overscanPixels);
275
+ validateUniqueIds(intervals);
276
+ return intervals.flatMap((interval) => {
277
+ const placement = resolveLaneIntervalPlacement(interval, laneMetrics);
278
+ return intersects(placement.x, placement.width, bounds.x, bounds.width) && intersects(placement.y, placement.height, bounds.y, bounds.height) ? [placement] : [];
279
+ });
280
+ }
281
+ function validateUniqueIds(intervals) {
282
+ const ids = new Set;
283
+ intervals.forEach(({ id }) => {
284
+ if (ids.has(id)) {
285
+ throw new RangeError("Lane intervals require unique IDs.");
286
+ }
287
+ ids.add(id);
288
+ });
289
+ }
290
+ function intersects(firstStart, firstExtent, secondStart, secondExtent) {
291
+ return firstStart <= secondStart + secondExtent && firstStart + firstExtent >= secondStart;
292
+ }
293
+ export { getAxisCategoryTicks, getAxisTicks, getVisibleGridItems, getLaneIntervalPlacement, getVisibleLaneIntervals, revealWorldRect, worldToViewport };
@@ -6,7 +6,7 @@ import {
6
6
  getAxisCategoryTicks,
7
7
  getAxisTicks,
8
8
  worldToViewport
9
- } from "./Accordion-e6ddpz5v.js";
9
+ } from "./Accordion-0my2z6qe.js";
10
10
  import {
11
11
  useZoraTheme2
12
12
  } from "./Accordion-srjya6dz.js";
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import {
3
3
  TileGrid2
4
- } from "./Accordion-q2z00p5r.js";
4
+ } from "./Accordion-gxj5qbpr.js";
5
5
  import {
6
6
  withZoraThemeScope
7
7
  } from "./Accordion-42jfy1a1.js";
@@ -3,7 +3,7 @@ import {
3
3
  getVisibleGridItems,
4
4
  revealWorldRect,
5
5
  worldToViewport
6
- } from "./Accordion-e6ddpz5v.js";
6
+ } from "./Accordion-0my2z6qe.js";
7
7
  import {
8
8
  require_cjs
9
9
  } from "./Accordion-xgpzh4wq.js";
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import"../../chunks/Accordion-q2z00p5r.js";
2
+ import"../../chunks/Accordion-gxj5qbpr.js";
3
3
  import"../../chunks/Accordion-bfzeab9x.js";
4
4
  import"../../chunks/Accordion-6wrr910w.js";
5
5
  import"../../chunks/Accordion-8f0nekwe.js";
@@ -14,7 +14,7 @@ import {
14
14
  Explorer2,
15
15
  FileExplorer2,
16
16
  MediaExplorer2
17
- } from "../../chunks/Accordion-689w7frk.js";
17
+ } from "../../chunks/Accordion-d6azgz09.js";
18
18
  import"../../chunks/Accordion-bff0p3x4.js";
19
19
  import"../../chunks/Accordion-srjya6dz.js";
20
20
  import"../../chunks/Accordion-184r2r82.js";
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import"../../chunks/Accordion-q2z00p5r.js";
2
+ import"../../chunks/Accordion-gxj5qbpr.js";
3
3
  import"../../chunks/Accordion-bfzeab9x.js";
4
4
  import"../../chunks/Accordion-6wrr910w.js";
5
5
  import"../../chunks/Accordion-8f0nekwe.js";
@@ -14,7 +14,7 @@ import {
14
14
  Explorer2,
15
15
  FileExplorer2,
16
16
  MediaExplorer2
17
- } from "../../chunks/Accordion-689w7frk.js";
17
+ } from "../../chunks/Accordion-d6azgz09.js";
18
18
  import"../../chunks/Accordion-bff0p3x4.js";
19
19
  import"../../chunks/Accordion-srjya6dz.js";
20
20
  import"../../chunks/Accordion-184r2r82.js";
@@ -4,7 +4,7 @@ import"../../chunks/Accordion-42jfy1a1.js";
4
4
  import {
5
5
  GridLineOverlay2,
6
6
  GridRuler2
7
- } from "../../chunks/Accordion-e9ewv367.js";
7
+ } from "../../chunks/Accordion-7qfd4cb4.js";
8
8
  import"../../chunks/Accordion-srjya6dz.js";
9
9
  import"../../chunks/Accordion-xgpzh4wq.js";
10
10
  export {