@ankhorage/zora 25.0.1 → 25.1.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 (63) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +2 -2
  3. package/dist/features/explorer/adapters/inbound/Explorer.d.ts.map +1 -1
  4. package/dist/features/explorer/adapters/inbound/Explorer.js +30 -13
  5. package/dist/features/explorer/adapters/inbound/Explorer.js.map +1 -1
  6. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.d.ts +9 -0
  7. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.d.ts.map +1 -0
  8. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.js +5 -0
  9. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.js.map +1 -0
  10. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.d.ts +9 -0
  11. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.d.ts.map +1 -0
  12. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.js +5 -0
  13. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.js.map +1 -0
  14. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.d.ts +9 -0
  15. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.d.ts.map +1 -0
  16. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.js +10 -0
  17. package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.js.map +1 -0
  18. package/dist/features/explorer/adapters/inbound/useExplorerKeyboardFocus.d.ts +14 -0
  19. package/dist/features/explorer/adapters/inbound/useExplorerKeyboardFocus.d.ts.map +1 -0
  20. package/dist/features/explorer/adapters/inbound/useExplorerKeyboardFocus.js +54 -0
  21. package/dist/features/explorer/adapters/inbound/useExplorerKeyboardFocus.js.map +1 -0
  22. package/dist/features/explorer/application/getExplorerNextFocusId.d.ts +6 -0
  23. package/dist/features/explorer/application/getExplorerNextFocusId.d.ts.map +1 -0
  24. package/dist/features/explorer/application/getExplorerNextFocusId.js +53 -0
  25. package/dist/features/explorer/application/getExplorerNextFocusId.js.map +1 -0
  26. package/dist/features/explorer/public.d.ts +1 -0
  27. package/dist/features/explorer/public.d.ts.map +1 -1
  28. package/dist/features/explorer/public.js +1 -0
  29. package/dist/features/explorer/public.js.map +1 -1
  30. package/dist/features/grid-view/adapters/inbound/GridView.d.ts +1 -1
  31. package/dist/features/grid-view/adapters/inbound/GridView.d.ts.map +1 -1
  32. package/dist/features/grid-view/adapters/inbound/GridView.js +36 -4
  33. package/dist/features/grid-view/adapters/inbound/GridView.js.map +1 -1
  34. package/dist/features/grid-view/adapters/inbound/TileGrid.d.ts +1 -1
  35. package/dist/features/grid-view/adapters/inbound/TileGrid.d.ts.map +1 -1
  36. package/dist/features/grid-view/adapters/inbound/TileGrid.js +7 -3
  37. package/dist/features/grid-view/adapters/inbound/TileGrid.js.map +1 -1
  38. package/dist/types/grid-view.d.ts +10 -0
  39. package/dist/types/grid-view.d.ts.map +1 -1
  40. package/dist/types/grid-view.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/features/explorer/adapters/inbound/Explorer.tsx +79 -43
  43. package/src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.tsx +11 -0
  44. package/src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.tsx +11 -0
  45. package/src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.tsx +20 -0
  46. package/src/features/explorer/adapters/inbound/useExplorerKeyboardFocus.test.tsx +232 -0
  47. package/src/features/explorer/adapters/inbound/useExplorerKeyboardFocus.ts +77 -0
  48. package/src/features/explorer/application/getExplorerNextFocusId.test.ts +37 -0
  49. package/src/features/explorer/application/getExplorerNextFocusId.ts +61 -0
  50. package/src/features/explorer/public.ts +1 -0
  51. package/src/features/grid-view/adapters/inbound/GridView.tsx +49 -1
  52. package/src/features/grid-view/adapters/inbound/TileGrid.tsx +11 -0
  53. package/src/types/grid-view.ts +10 -0
  54. package/web-dist/chunks/{Accordion-b20zfsfz.js → Accordion-54k0ncs3.js} +83 -0
  55. package/web-dist/chunks/Accordion-fmn31fgf.js +321 -0
  56. package/web-dist/components/explorer/Explorer.js +2 -2
  57. package/web-dist/components/file-explorer/FileExplorer.js +2 -2
  58. package/web-dist/components/grid-view/GridView.js +1 -1
  59. package/web-dist/components/media-explorer/MediaExplorer.js +2 -2
  60. package/web-dist/components/tile-grid/TileGrid.js +1 -1
  61. package/web-dist/components/uploader/Uploader.js +2 -2
  62. package/web-dist/manifest.json +10 -10
  63. package/web-dist/chunks/Accordion-0md8wbtx.js +0 -197
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 25.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 52f40c6: Add virtualized Explorer keyboard focus navigation with published GridView reveal geometry and a runnable native/web workspace example.
8
+
3
9
  ## 25.0.1
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # @ankhorage/zora
5
5
 
6
- ![license: MIT](././paradox/badges/license.svg) ![npm: v25.0.1](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![paradox: warnings](././paradox/badges/docs.svg)
6
+ ![license: MIT](././paradox/badges/license.svg) ![npm: v25.1.0](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![paradox: warnings](././paradox/badges/docs.svg)
7
7
 
8
8
  Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.
9
9
 
@@ -52,7 +52,7 @@ export default function BasicApp() {
52
52
  }
53
53
  ```
54
54
 
55
- This package contains 5 additional examples. See the generated documentation for the complete set.
55
+ This package contains 6 additional examples. See the generated documentation for the complete set.
56
56
 
57
57
  ## Generated documentation
58
58
 
@@ -1 +1 @@
1
- {"version":3,"file":"Explorer.d.ts","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/Explorer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAS9E,2FAA2F;AAC3F,eAAO,MAAM,QAAQ,qDAAoC,CAAC"}
1
+ {"version":3,"file":"Explorer.d.ts","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/Explorer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAW9E,2FAA2F;AAC3F,eAAO,MAAM,QAAQ,qDAAoC,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Pressable as NativePressable, View as NativeView } from 'react-native';
2
+ import { Platform, Pressable as NativePressable, View as NativeView } from 'react-native';
3
3
  import { TileGrid } from '../../../grid-view/public';
4
4
  import { Icon } from '../../../icon/public';
5
5
  import { Image } from '../../../image/public';
@@ -7,6 +7,8 @@ import { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThem
7
7
  import { useZoraTheme } from '../../../theme/composition/useZoraTheme';
8
8
  import { Text } from '../../../typography/public';
9
9
  import { resolveExplorerSelection } from '../../application/resolveExplorerSelection';
10
+ import { ExplorerKeyboardProxy } from './ExplorerKeyboardProxy';
11
+ import { useExplorerKeyboardFocus } from './useExplorerKeyboardFocus';
10
12
  /*** Single canonical Explorer implementation for media and file catalogue presentation. */
11
13
  export const Explorer = withZoraThemeScope(ExplorerInner);
12
14
  /*** Presents provider-neutral asset tiles with controlled or uncontrolled selection. */
@@ -14,9 +16,11 @@ function ExplorerInner({ items, selectedIds, defaultSelectedIds = [], selectionM
14
16
  const { theme } = useZoraTheme();
15
17
  const [internalSelectedIds, setInternalSelectedIds] = React.useState(defaultSelectedIds);
16
18
  const anchorId = React.useRef(null);
19
+ const [columns, setColumns] = React.useState(1);
20
+ const [visibleIds, setVisibleIds] = React.useState([]);
17
21
  const effectiveSelectedIds = selectedIds ?? internalSelectedIds;
18
22
  const itemLookup = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
19
- const ids = items.filter((item) => !item.disabled).map((item) => item.id);
23
+ const ids = React.useMemo(() => items.filter((item) => !item.disabled).map((item) => item.id), [items]);
20
24
  const selected = new Set(effectiveSelectedIds);
21
25
  const passive = interactionPolicy === 'passive' || disabled || readOnly;
22
26
  const select = (item, intent) => {
@@ -29,13 +33,22 @@ function ExplorerInner({ items, selectedIds, defaultSelectedIds = [], selectionM
29
33
  setInternalSelectedIds(next);
30
34
  onSelectionChange?.({ selectedIds: next });
31
35
  };
36
+ const keyboard = useExplorerKeyboardFocus(items, columns, passive, visibleIds, (targetId, originId, shiftKey) => {
37
+ if (!shiftKey)
38
+ return;
39
+ anchorId.current ??= originId;
40
+ const nextItem = itemLookup.get(targetId);
41
+ if (nextItem)
42
+ select(nextItem, 'range');
43
+ });
32
44
  const renderTile = (tile) => {
33
45
  const item = itemLookup.get(tile.id);
34
46
  if (!item)
35
47
  return null;
36
48
  const isSelected = selected.has(item.id);
37
49
  const canInteract = !passive && !item.disabled;
38
- return (<NativePressable accessibilityLabel={item.name} accessibilityRole="button" accessibilityState={{ selected: isSelected, disabled: !canInteract }} disabled={!canInteract} onLongPress={() => select(item, 'toggle')} onPress={(event) => {
50
+ return (<ExplorerKeyboardProxy onKeyDown={(key, shiftKey) => keyboard.onKeyDown(item.id, key, shiftKey)}>
51
+ <NativePressable accessibilityLabel={item.name} accessibilityRole="button" accessibilityState={{ selected: isSelected, disabled: !canInteract }} disabled={!canInteract} focusable={canInteract && (Platform.OS !== 'web' || item.id === keyboard.tabStopId)} onFocus={() => keyboard.onFocus(item.id)} onLongPress={() => select(item, 'toggle')} onPress={(event) => {
39
52
  const intent = resolveExplorerPressIntent(event.nativeEvent);
40
53
  select(item, intent);
41
54
  if (selectionMode === 'single' && intent === 'replace') {
@@ -50,14 +63,15 @@ function ExplorerInner({ items, selectedIds, defaultSelectedIds = [], selectionM
50
63
  backgroundColor: theme.semantics.neutral.surface,
51
64
  padding: 8,
52
65
  gap: 4,
53
- }} testID={`explorer-item-${item.id}`}>
54
- <NativeView style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
55
- {item.thumbnailUri ? (<Image accessibilityLabel={item.name} aspectRatio={1} source={item.thumbnailUri} width="100%"/>) : (<Icon name={item.kind === 'folder' ? 'folder-outline' : 'document-outline'} size={32}/>)}
56
- </NativeView>
57
- <Text numberOfLines={1} variant="caption">
58
- {item.name}
59
- </Text>
60
- </NativePressable>);
66
+ }} ref={(node) => keyboard.registerTile(item.id, node)} testID={`explorer-item-${item.id}`}>
67
+ <NativeView style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
68
+ {item.thumbnailUri ? (<Image accessibilityLabel={item.name} aspectRatio={1} source={item.thumbnailUri} width="100%"/>) : (<Icon name={item.kind === 'folder' ? 'folder-outline' : 'document-outline'} size={32}/>)}
69
+ </NativeView>
70
+ <Text numberOfLines={1} variant="caption">
71
+ {item.name}
72
+ </Text>
73
+ </NativePressable>
74
+ </ExplorerKeyboardProxy>);
61
75
  };
62
76
  if (errorText)
63
77
  return <Text testID={testID}>{errorText}</Text>;
@@ -65,9 +79,12 @@ function ExplorerInner({ items, selectedIds, defaultSelectedIds = [], selectionM
65
79
  return <Text testID={testID}>Loading…</Text>;
66
80
  if (items.length === 0)
67
81
  return <Text testID={testID}>{emptyText}</Text>;
68
- return (<TileGrid height={height} interactionPolicy={interactionPolicy} items={items} renderItem={renderTile} testID={testID} tileSize={tileSize} width={width} zoom={zoom}/>);
82
+ return (<TileGrid focusedItemId={keyboard.focusedId ?? undefined} height={height} interactionPolicy={interactionPolicy} onColumnsChange={setColumns} onVisibleItemIdsChange={setVisibleIds} items={items} renderItem={renderTile} testID={testID} tileSize={tileSize} width={width} zoom={zoom}/>);
69
83
  }
70
- /*** Normalize platform press modifiers for multi-selection without passing event objects into contracts. */
84
+ /***
85
+ * Normalize platform press modifiers into selection intents
86
+ * without passing event objects into the public contracts.
87
+ */
71
88
  function resolveExplorerPressIntent(event) {
72
89
  if (typeof event !== 'object' || event === null)
73
90
  return 'replace';
@@ -1 +1 @@
1
- {"version":3,"file":"Explorer.js","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/Explorer.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\nimport { Pressable as NativePressable, View as NativeView } from 'react-native';\n\nimport type { ExplorerItem, ExplorerProps } from '../../../../types/explorer';\nimport { TileGrid } from '../../../grid-view/public';\nimport { Icon } from '../../../icon/public';\nimport { Image } from '../../../image/public';\nimport { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope';\nimport { useZoraTheme } from '../../../theme/composition/useZoraTheme';\nimport { Text } from '../../../typography/public';\nimport { resolveExplorerSelection } from '../../application/resolveExplorerSelection';\n\n/*** Single canonical Explorer implementation for media and file catalogue presentation. */\nexport const Explorer = withZoraThemeScope(ExplorerInner);\n\n/*** Presents provider-neutral asset tiles with controlled or uncontrolled selection. */\nfunction ExplorerInner({\n items,\n selectedIds,\n defaultSelectedIds = [],\n selectionMode = 'single',\n onSelectionChange,\n onActivate,\n width,\n height,\n tileSize = 120,\n zoom = 1,\n loading = false,\n errorText,\n emptyText = 'No items',\n disabled = false,\n readOnly = false,\n interactionPolicy,\n testID,\n}: ExplorerProps) {\n const { theme } = useZoraTheme();\n const [internalSelectedIds, setInternalSelectedIds] =\n React.useState<readonly string[]>(defaultSelectedIds);\n const anchorId = React.useRef<string | null>(null);\n const effectiveSelectedIds = selectedIds ?? internalSelectedIds;\n const itemLookup = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);\n const ids = items.filter((item) => !item.disabled).map((item) => item.id);\n const selected = new Set(effectiveSelectedIds);\n const passive = interactionPolicy === 'passive' || disabled || readOnly;\n\n const select = (item: ExplorerItem, intent: 'replace' | 'toggle' | 'range') => {\n if (passive || item.disabled) return;\n const next = resolveExplorerSelection(\n ids,\n effectiveSelectedIds,\n item.id,\n anchorId.current,\n intent,\n selectionMode,\n );\n if (intent !== 'range') anchorId.current = item.id;\n if (selectedIds === undefined) setInternalSelectedIds(next);\n onSelectionChange?.({ selectedIds: next });\n };\n\n const renderTile = (tile: { readonly id: string }) => {\n const item = itemLookup.get(tile.id);\n if (!item) return null;\n const isSelected = selected.has(item.id);\n const canInteract = !passive && !item.disabled;\n return (\n <NativePressable\n accessibilityLabel={item.name}\n accessibilityRole=\"button\"\n accessibilityState={{ selected: isSelected, disabled: !canInteract }}\n disabled={!canInteract}\n onLongPress={() => select(item, 'toggle')}\n onPress={(event) => {\n const intent = resolveExplorerPressIntent(event.nativeEvent);\n select(item, intent);\n if (selectionMode === 'single' && intent === 'replace') {\n onActivate?.({ id: item.id });\n }\n }}\n style={{\n flex: 1,\n overflow: 'hidden',\n borderRadius: 8,\n borderWidth: isSelected ? 2 : 1,\n borderColor: isSelected ? theme.colors.primary : theme.semantics.neutral.divider,\n backgroundColor: theme.semantics.neutral.surface,\n padding: 8,\n gap: 4,\n }}\n testID={`explorer-item-${item.id}`}\n >\n <NativeView style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>\n {item.thumbnailUri ? (\n <Image\n accessibilityLabel={item.name}\n aspectRatio={1}\n source={item.thumbnailUri}\n width=\"100%\"\n />\n ) : (\n <Icon name={item.kind === 'folder' ? 'folder-outline' : 'document-outline'} size={32} />\n )}\n </NativeView>\n <Text numberOfLines={1} variant=\"caption\">\n {item.name}\n </Text>\n </NativePressable>\n );\n };\n\n if (errorText) return <Text testID={testID}>{errorText}</Text>;\n if (loading) return <Text testID={testID}>Loading…</Text>;\n if (items.length === 0) return <Text testID={testID}>{emptyText}</Text>;\n\n return (\n <TileGrid\n height={height}\n interactionPolicy={interactionPolicy}\n items={items}\n renderItem={renderTile}\n testID={testID}\n tileSize={tileSize}\n width={width}\n zoom={zoom}\n />\n );\n}\n\n/*** Normalize platform press modifiers for multi-selection without passing event objects into contracts. */\nfunction resolveExplorerPressIntent(event: unknown): 'replace' | 'toggle' | 'range' {\n if (typeof event !== 'object' || event === null) return 'replace';\n if ('shiftKey' in event && event.shiftKey === true) return 'range';\n if ('ctrlKey' in event && event.ctrlKey === true) return 'toggle';\n if ('metaKey' in event && event.metaKey === true) return 'toggle';\n return 'replace';\n}\n"]}
1
+ {"version":3,"file":"Explorer.js","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/Explorer.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\nimport { Platform, Pressable as NativePressable, View as NativeView } from 'react-native';\n\nimport type { ExplorerItem, ExplorerProps } from '../../../../types/explorer';\nimport { TileGrid } from '../../../grid-view/public';\nimport { Icon } from '../../../icon/public';\nimport { Image } from '../../../image/public';\nimport { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope';\nimport { useZoraTheme } from '../../../theme/composition/useZoraTheme';\nimport { Text } from '../../../typography/public';\nimport { resolveExplorerSelection } from '../../application/resolveExplorerSelection';\nimport { ExplorerKeyboardProxy } from './ExplorerKeyboardProxy';\nimport { useExplorerKeyboardFocus } from './useExplorerKeyboardFocus';\n\n/*** Single canonical Explorer implementation for media and file catalogue presentation. */\nexport const Explorer = withZoraThemeScope(ExplorerInner);\n\n/*** Presents provider-neutral asset tiles with controlled or uncontrolled selection. */\nfunction ExplorerInner({\n items,\n selectedIds,\n defaultSelectedIds = [],\n selectionMode = 'single',\n onSelectionChange,\n onActivate,\n width,\n height,\n tileSize = 120,\n zoom = 1,\n loading = false,\n errorText,\n emptyText = 'No items',\n disabled = false,\n readOnly = false,\n interactionPolicy,\n testID,\n}: ExplorerProps) {\n const { theme } = useZoraTheme();\n const [internalSelectedIds, setInternalSelectedIds] =\n React.useState<readonly string[]>(defaultSelectedIds);\n const anchorId = React.useRef<string | null>(null);\n const [columns, setColumns] = React.useState(1);\n const [visibleIds, setVisibleIds] = React.useState<readonly string[]>([]);\n const effectiveSelectedIds = selectedIds ?? internalSelectedIds;\n const itemLookup = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);\n const ids = React.useMemo(\n () => items.filter((item) => !item.disabled).map((item) => item.id),\n [items],\n );\n const selected = new Set(effectiveSelectedIds);\n const passive = interactionPolicy === 'passive' || disabled || readOnly;\n\n const select = (item: ExplorerItem, intent: 'replace' | 'toggle' | 'range') => {\n if (passive || item.disabled) return;\n const next = resolveExplorerSelection(\n ids,\n effectiveSelectedIds,\n item.id,\n anchorId.current,\n intent,\n selectionMode,\n );\n if (intent !== 'range') anchorId.current = item.id;\n if (selectedIds === undefined) setInternalSelectedIds(next);\n onSelectionChange?.({ selectedIds: next });\n };\n\n const keyboard = useExplorerKeyboardFocus(\n items,\n columns,\n passive,\n visibleIds,\n (targetId, originId, shiftKey) => {\n if (!shiftKey) return;\n anchorId.current ??= originId;\n const nextItem = itemLookup.get(targetId);\n if (nextItem) select(nextItem, 'range');\n },\n );\n\n const renderTile = (tile: { readonly id: string }) => {\n const item = itemLookup.get(tile.id);\n if (!item) return null;\n const isSelected = selected.has(item.id);\n const canInteract = !passive && !item.disabled;\n return (\n <ExplorerKeyboardProxy\n onKeyDown={(key, shiftKey) => keyboard.onKeyDown(item.id, key, shiftKey)}\n >\n <NativePressable\n accessibilityLabel={item.name}\n accessibilityRole=\"button\"\n accessibilityState={{ selected: isSelected, disabled: !canInteract }}\n disabled={!canInteract}\n focusable={canInteract && (Platform.OS !== 'web' || item.id === keyboard.tabStopId)}\n onFocus={() => keyboard.onFocus(item.id)}\n onLongPress={() => select(item, 'toggle')}\n onPress={(event) => {\n const intent = resolveExplorerPressIntent(event.nativeEvent);\n select(item, intent);\n if (selectionMode === 'single' && intent === 'replace') {\n onActivate?.({ id: item.id });\n }\n }}\n style={{\n flex: 1,\n overflow: 'hidden',\n borderRadius: 8,\n borderWidth: isSelected ? 2 : 1,\n borderColor: isSelected ? theme.colors.primary : theme.semantics.neutral.divider,\n backgroundColor: theme.semantics.neutral.surface,\n padding: 8,\n gap: 4,\n }}\n ref={(node) => keyboard.registerTile(item.id, node)}\n testID={`explorer-item-${item.id}`}\n >\n <NativeView style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>\n {item.thumbnailUri ? (\n <Image\n accessibilityLabel={item.name}\n aspectRatio={1}\n source={item.thumbnailUri}\n width=\"100%\"\n />\n ) : (\n <Icon\n name={item.kind === 'folder' ? 'folder-outline' : 'document-outline'}\n size={32}\n />\n )}\n </NativeView>\n <Text numberOfLines={1} variant=\"caption\">\n {item.name}\n </Text>\n </NativePressable>\n </ExplorerKeyboardProxy>\n );\n };\n\n if (errorText) return <Text testID={testID}>{errorText}</Text>;\n if (loading) return <Text testID={testID}>Loading…</Text>;\n if (items.length === 0) return <Text testID={testID}>{emptyText}</Text>;\n\n return (\n <TileGrid\n focusedItemId={keyboard.focusedId ?? undefined}\n height={height}\n interactionPolicy={interactionPolicy}\n onColumnsChange={setColumns}\n onVisibleItemIdsChange={setVisibleIds}\n items={items}\n renderItem={renderTile}\n testID={testID}\n tileSize={tileSize}\n width={width}\n zoom={zoom}\n />\n );\n}\n\n/***\n * Normalize platform press modifiers into selection intents\n * without passing event objects into the public contracts.\n */\nfunction resolveExplorerPressIntent(event: unknown): 'replace' | 'toggle' | 'range' {\n if (typeof event !== 'object' || event === null) return 'replace';\n if ('shiftKey' in event && event.shiftKey === true) return 'range';\n if ('ctrlKey' in event && event.ctrlKey === true) return 'toggle';\n if ('metaKey' in event && event.metaKey === true) return 'toggle';\n return 'replace';\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import type React from 'react';
2
+ /*** Provides the native fallback type surface; RNW resolves the web adapter at runtime. */
3
+ export declare function ExplorerKeyboardProxy({ children }: ExplorerKeyboardProxyProps): React.JSX.Element;
4
+ interface ExplorerKeyboardProxyProps {
5
+ readonly children: React.ReactNode;
6
+ readonly onKeyDown: (key: string, shiftKey: boolean) => boolean;
7
+ }
8
+ export {};
9
+ //# sourceMappingURL=ExplorerKeyboardProxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExplorerKeyboardProxy.d.ts","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,2FAA2F;AAC3F,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,EAAE,0BAA0B,qBAE7E;AAED,UAAU,0BAA0B;IAClC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC;CACjE"}
@@ -0,0 +1,5 @@
1
+ /*** Provides the native fallback type surface; RNW resolves the web adapter at runtime. */
2
+ export function ExplorerKeyboardProxy({ children }) {
3
+ return <>{children}</>;
4
+ }
5
+ //# sourceMappingURL=ExplorerKeyboardProxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExplorerKeyboardProxy.js","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.tsx"],"names":[],"mappings":"AAEA,2FAA2F;AAC3F,MAAM,UAAU,qBAAqB,CAAC,EAAE,QAAQ,EAA8B;IAC5E,OAAO,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC;AACzB,CAAC","sourcesContent":["import type React from 'react';\n\n/*** Provides the native fallback type surface; RNW resolves the web adapter at runtime. */\nexport function ExplorerKeyboardProxy({ children }: ExplorerKeyboardProxyProps) {\n return <>{children}</>;\n}\n\ninterface ExplorerKeyboardProxyProps {\n readonly children: React.ReactNode;\n readonly onKeyDown: (key: string, shiftKey: boolean) => boolean;\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import type React from 'react';
2
+ /*** Keeps Explorer navigation portable when a native host has no DOM keyboard event surface. */
3
+ export declare function ExplorerKeyboardProxy({ children }: ExplorerKeyboardProxyProps): React.JSX.Element;
4
+ interface ExplorerKeyboardProxyProps {
5
+ readonly children: React.ReactNode;
6
+ readonly onKeyDown: (key: string, shiftKey: boolean) => boolean;
7
+ }
8
+ export {};
9
+ //# sourceMappingURL=ExplorerKeyboardProxy.native.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExplorerKeyboardProxy.native.d.ts","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,gGAAgG;AAChG,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,EAAE,0BAA0B,qBAE7E;AAED,UAAU,0BAA0B;IAClC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC;CACjE"}
@@ -0,0 +1,5 @@
1
+ /*** Keeps Explorer navigation portable when a native host has no DOM keyboard event surface. */
2
+ export function ExplorerKeyboardProxy({ children }) {
3
+ return <>{children}</>;
4
+ }
5
+ //# sourceMappingURL=ExplorerKeyboardProxy.native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExplorerKeyboardProxy.native.js","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.tsx"],"names":[],"mappings":"AAEA,gGAAgG;AAChG,MAAM,UAAU,qBAAqB,CAAC,EAAE,QAAQ,EAA8B;IAC5E,OAAO,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC;AACzB,CAAC","sourcesContent":["import type React from 'react';\n\n/*** Keeps Explorer navigation portable when a native host has no DOM keyboard event surface. */\nexport function ExplorerKeyboardProxy({ children }: ExplorerKeyboardProxyProps) {\n return <>{children}</>;\n}\n\ninterface ExplorerKeyboardProxyProps {\n readonly children: React.ReactNode;\n readonly onKeyDown: (key: string, shiftKey: boolean) => boolean;\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import type React from 'react';
2
+ /*** Bridges RNW's rendered DOM focus events into Explorer's platform-neutral navigation policy. */
3
+ export declare function ExplorerKeyboardProxy({ children, onKeyDown }: ExplorerKeyboardProxyProps): React.JSX.Element;
4
+ interface ExplorerKeyboardProxyProps {
5
+ readonly children: React.ReactNode;
6
+ readonly onKeyDown: (key: string, shiftKey: boolean) => boolean;
7
+ }
8
+ export {};
9
+ //# sourceMappingURL=ExplorerKeyboardProxy.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExplorerKeyboardProxy.web.d.ts","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,mGAAmG;AACnG,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,0BAA0B,qBAWxF;AAED,UAAU,0BAA0B;IAClC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC;CACjE"}
@@ -0,0 +1,10 @@
1
+ /*** Bridges RNW's rendered DOM focus events into Explorer's platform-neutral navigation policy. */
2
+ export function ExplorerKeyboardProxy({ children, onKeyDown }) {
3
+ return (<div onKeyDown={(event) => {
4
+ if (onKeyDown(event.key, event.shiftKey))
5
+ event.preventDefault();
6
+ }} style={{ display: 'flex', flex: 1 }}>
7
+ {children}
8
+ </div>);
9
+ }
10
+ //# sourceMappingURL=ExplorerKeyboardProxy.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExplorerKeyboardProxy.web.js","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.tsx"],"names":[],"mappings":"AAEA,mGAAmG;AACnG,MAAM,UAAU,qBAAqB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAA8B;IACvF,OAAO,CACL,CAAC,GAAG,CACF,SAAS,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE;YACnB,IAAI,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,QAAQ,CAAC;gBAAE,KAAK,CAAC,cAAc,EAAE,CAAC;QACnE,CAAC,CAAC,CACF,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAEpC;MAAA,CAAC,QAAQ,CACX;IAAA,EAAE,GAAG,CAAC,CACP,CAAC;AACJ,CAAC","sourcesContent":["import type React from 'react';\n\n/*** Bridges RNW's rendered DOM focus events into Explorer's platform-neutral navigation policy. */\nexport function ExplorerKeyboardProxy({ children, onKeyDown }: ExplorerKeyboardProxyProps) {\n return (\n <div\n onKeyDown={(event) => {\n if (onKeyDown(event.key, event.shiftKey)) event.preventDefault();\n }}\n style={{ display: 'flex', flex: 1 }}\n >\n {children}\n </div>\n );\n}\n\ninterface ExplorerKeyboardProxyProps {\n readonly children: React.ReactNode;\n readonly onKeyDown: (key: string, shiftKey: boolean) => boolean;\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import type { ExplorerItem } from '../../../../types/explorer';
2
+ /*** Tracks the one keyboard tab stop and restores focus only after a virtualized tile mounts. */
3
+ export declare function useExplorerKeyboardFocus(items: readonly ExplorerItem[], columns: number, disabled: boolean, visibleIds: readonly string[], onNavigate: (targetId: string, originId: string, shiftKey: boolean) => void): {
4
+ focusedId: string | null;
5
+ tabStopId: string | undefined;
6
+ onFocus: (id: string) => void;
7
+ onKeyDown: (id: string, key: string, shiftKey: boolean) => boolean;
8
+ registerTile: (id: string, node: FocusableTile | null) => void;
9
+ };
10
+ interface FocusableTile {
11
+ focus: () => void;
12
+ }
13
+ export {};
14
+ //# sourceMappingURL=useExplorerKeyboardFocus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useExplorerKeyboardFocus.d.ts","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/useExplorerKeyboardFocus.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG/D,iGAAiG;AACjG,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,OAAO,EACjB,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI;;;kBAkBpC,MAAM;oBAmBtC,MAAM,OAAO,MAAM,YAAY,OAAO,KAAG,OAAO;uBAbX,MAAM,QAAQ,aAAa,GAAG,IAAI;EAqC/E;AAED,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB"}
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import { getExplorerNextFocusId } from '../../application/getExplorerNextFocusId';
3
+ /*** Tracks the one keyboard tab stop and restores focus only after a virtualized tile mounts. */
4
+ export function useExplorerKeyboardFocus(items, columns, disabled, visibleIds, onNavigate) {
5
+ const [focusedId, setFocusedId] = React.useState(null);
6
+ const focusedRef = React.useRef(null);
7
+ const pendingFocusRef = React.useRef(null);
8
+ const tileRefs = React.useRef(new Map());
9
+ const ids = React.useMemo(() => items.map(({ id }) => id), [items]);
10
+ const idSet = React.useMemo(() => new Set(ids), [ids]);
11
+ const disabledIds = React.useMemo(() => new Set(items.filter((item) => item.disabled).map(({ id }) => id)), [items]);
12
+ const visibleEligibleIds = visibleIds.filter((id) => idSet.has(id) && !disabledIds.has(id));
13
+ const tabStopId = focusedId !== null && visibleEligibleIds.includes(focusedId)
14
+ ? focusedId
15
+ : (visibleEligibleIds[0] ?? ids.find((id) => !disabledIds.has(id)));
16
+ const onFocus = React.useCallback((id) => {
17
+ pendingFocusRef.current = null;
18
+ focusedRef.current = id;
19
+ setFocusedId(id);
20
+ }, []);
21
+ const registerTile = React.useCallback((id, node) => {
22
+ if (node === null) {
23
+ tileRefs.current.delete(id);
24
+ return;
25
+ }
26
+ tileRefs.current.set(id, node);
27
+ if (pendingFocusRef.current === id) {
28
+ pendingFocusRef.current = null;
29
+ node.focus();
30
+ }
31
+ }, []);
32
+ const onKeyDown = React.useCallback((id, key, shiftKey) => {
33
+ if (disabled)
34
+ return false;
35
+ if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(key))
36
+ return false;
37
+ const originId = focusedRef.current ?? id;
38
+ const nextId = getExplorerNextFocusId(ids, originId, key, columns, disabledIds);
39
+ if (nextId === null || nextId === originId)
40
+ return true;
41
+ focusedRef.current = nextId;
42
+ pendingFocusRef.current = nextId;
43
+ setFocusedId(nextId);
44
+ onNavigate(nextId, originId, shiftKey);
45
+ const mounted = tileRefs.current.get(nextId);
46
+ if (mounted) {
47
+ pendingFocusRef.current = null;
48
+ mounted.focus();
49
+ }
50
+ return true;
51
+ }, [columns, disabled, disabledIds, ids, onNavigate]);
52
+ return { focusedId, tabStopId, onFocus, onKeyDown, registerTile };
53
+ }
54
+ //# sourceMappingURL=useExplorerKeyboardFocus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useExplorerKeyboardFocus.js","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/useExplorerKeyboardFocus.ts"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\n\nimport type { ExplorerItem } from '../../../../types/explorer';\nimport { getExplorerNextFocusId } from '../../application/getExplorerNextFocusId';\n\n/*** Tracks the one keyboard tab stop and restores focus only after a virtualized tile mounts. */\nexport function useExplorerKeyboardFocus(\n items: readonly ExplorerItem[],\n columns: number,\n disabled: boolean,\n visibleIds: readonly string[],\n onNavigate: (targetId: string, originId: string, shiftKey: boolean) => void,\n) {\n const [focusedId, setFocusedId] = React.useState<string | null>(null);\n const focusedRef = React.useRef<string | null>(null);\n const pendingFocusRef = React.useRef<string | null>(null);\n const tileRefs = React.useRef(new Map<string, FocusableTile>());\n const ids = React.useMemo(() => items.map(({ id }) => id), [items]);\n const idSet = React.useMemo(() => new Set(ids), [ids]);\n const disabledIds = React.useMemo(\n () => new Set(items.filter((item) => item.disabled).map(({ id }) => id)),\n [items],\n );\n const visibleEligibleIds = visibleIds.filter((id) => idSet.has(id) && !disabledIds.has(id));\n const tabStopId =\n focusedId !== null && visibleEligibleIds.includes(focusedId)\n ? focusedId\n : (visibleEligibleIds[0] ?? ids.find((id) => !disabledIds.has(id)));\n\n const onFocus = React.useCallback((id: string) => {\n pendingFocusRef.current = null;\n focusedRef.current = id;\n setFocusedId(id);\n }, []);\n\n const registerTile = React.useCallback((id: string, node: FocusableTile | null) => {\n if (node === null) {\n tileRefs.current.delete(id);\n return;\n }\n tileRefs.current.set(id, node);\n if (pendingFocusRef.current === id) {\n pendingFocusRef.current = null;\n node.focus();\n }\n }, []);\n\n const onKeyDown = React.useCallback(\n (id: string, key: string, shiftKey: boolean): boolean => {\n if (disabled) return false;\n if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(key))\n return false;\n const originId = focusedRef.current ?? id;\n const nextId = getExplorerNextFocusId(ids, originId, key, columns, disabledIds);\n if (nextId === null || nextId === originId) return true;\n\n focusedRef.current = nextId;\n pendingFocusRef.current = nextId;\n setFocusedId(nextId);\n onNavigate(nextId, originId, shiftKey);\n\n const mounted = tileRefs.current.get(nextId);\n if (mounted) {\n pendingFocusRef.current = null;\n mounted.focus();\n }\n return true;\n },\n [columns, disabled, disabledIds, ids, onNavigate],\n );\n\n return { focusedId, tabStopId, onFocus, onKeyDown, registerTile };\n}\n\ninterface FocusableTile {\n focus: () => void;\n}\n"]}
@@ -0,0 +1,6 @@
1
+ /***
2
+ * Resolve focus along actual tile positions while skipping unavailable items
3
+ * without collapsing the visual row geometry.
4
+ */
5
+ export declare function getExplorerNextFocusId(ids: readonly string[], currentId: string | null, key: string, columns: number, disabledIds?: ReadonlySet<string>): string | null;
6
+ //# sourceMappingURL=getExplorerNextFocusId.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getExplorerNextFocusId.d.ts","sourceRoot":"","sources":["../../../../src/features/explorer/application/getExplorerNextFocusId.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,SAAS,MAAM,EAAE,EACtB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,GAC3C,MAAM,GAAG,IAAI,CA6Bf"}
@@ -0,0 +1,53 @@
1
+ /***
2
+ * Resolve focus along actual tile positions while skipping unavailable items
3
+ * without collapsing the visual row geometry.
4
+ */
5
+ export function getExplorerNextFocusId(ids, currentId, key, columns, disabledIds = new Set()) {
6
+ const delta = resolveFocusDelta(key, columns);
7
+ if (delta === null || ids.length === 0)
8
+ return null;
9
+ const firstIndex = ids.findIndex((id) => !disabledIds.has(id));
10
+ if (firstIndex < 0)
11
+ return null;
12
+ const currentIndex = Math.max(firstIndex, ids.indexOf(currentId ?? ''));
13
+ const candidateIndex = delta === Number.NEGATIVE_INFINITY
14
+ ? 0
15
+ : delta === Number.POSITIVE_INFINITY
16
+ ? ids.length - 1
17
+ : Math.max(0, Math.min(ids.length - 1, currentIndex + delta));
18
+ const direction = delta === Number.NEGATIVE_INFINITY
19
+ ? 1
20
+ : delta === Number.POSITIVE_INFINITY
21
+ ? -1
22
+ : Math.sign(delta);
23
+ for (let index = candidateIndex; index >= 0 && index < ids.length; index += direction) {
24
+ const candidate = ids.at(index);
25
+ if (candidate !== undefined && !disabledIds.has(candidate))
26
+ return candidate;
27
+ }
28
+ const current = ids.at(currentIndex);
29
+ return current !== undefined && !disabledIds.has(current)
30
+ ? current
31
+ : (ids.at(firstIndex) ?? null);
32
+ }
33
+ /*** Translate navigation keys into collection-relative movements. */
34
+ function resolveFocusDelta(key, columns) {
35
+ const rowSize = Math.max(1, Math.floor(columns));
36
+ switch (key) {
37
+ case 'ArrowLeft':
38
+ return -1;
39
+ case 'ArrowRight':
40
+ return 1;
41
+ case 'ArrowUp':
42
+ return -rowSize;
43
+ case 'ArrowDown':
44
+ return rowSize;
45
+ case 'Home':
46
+ return Number.NEGATIVE_INFINITY;
47
+ case 'End':
48
+ return Number.POSITIVE_INFINITY;
49
+ default:
50
+ return null;
51
+ }
52
+ }
53
+ //# sourceMappingURL=getExplorerNextFocusId.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getExplorerNextFocusId.js","sourceRoot":"","sources":["../../../../src/features/explorer/application/getExplorerNextFocusId.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CACpC,GAAsB,EACtB,SAAwB,EACxB,GAAW,EACX,OAAe,EACf,cAAmC,IAAI,GAAG,EAAE;IAE5C,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC9C,IAAI,KAAK,KAAK,IAAI,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEpD,MAAM,UAAU,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/D,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAEhC,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC;IACxE,MAAM,cAAc,GAClB,KAAK,KAAK,MAAM,CAAC,iBAAiB;QAChC,CAAC,CAAC,CAAC;QACH,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,iBAAiB;YAClC,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC;YAChB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,YAAY,GAAG,KAAK,CAAC,CAAC,CAAC;IACpE,MAAM,SAAS,GACb,KAAK,KAAK,MAAM,CAAC,iBAAiB;QAChC,CAAC,CAAC,CAAC;QACH,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,iBAAiB;YAClC,CAAC,CAAC,CAAC,CAAC;YACJ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEzB,KAAK,IAAI,KAAK,GAAG,cAAc,EAAE,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,IAAI,SAAS,EAAE,CAAC;QACtF,MAAM,SAAS,GAAG,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,SAAS,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;IAC/E,CAAC;IACD,MAAM,OAAO,GAAG,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC;IACrC,OAAO,OAAO,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC;QACvD,CAAC,CAAC,OAAO;QACT,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,qEAAqE;AACrE,SAAS,iBAAiB,CAAC,GAAW,EAAE,OAAe;IACrD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;IACjD,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,WAAW;YACd,OAAO,CAAC,CAAC,CAAC;QACZ,KAAK,YAAY;YACf,OAAO,CAAC,CAAC;QACX,KAAK,SAAS;YACZ,OAAO,CAAC,OAAO,CAAC;QAClB,KAAK,WAAW;YACd,OAAO,OAAO,CAAC;QACjB,KAAK,MAAM;YACT,OAAO,MAAM,CAAC,iBAAiB,CAAC;QAClC,KAAK,KAAK;YACR,OAAO,MAAM,CAAC,iBAAiB,CAAC;QAClC;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC","sourcesContent":["/***\n * Resolve focus along actual tile positions while skipping unavailable items\n * without collapsing the visual row geometry.\n */\nexport function getExplorerNextFocusId(\n ids: readonly string[],\n currentId: string | null,\n key: string,\n columns: number,\n disabledIds: ReadonlySet<string> = new Set(),\n): string | null {\n const delta = resolveFocusDelta(key, columns);\n if (delta === null || ids.length === 0) return null;\n\n const firstIndex = ids.findIndex((id) => !disabledIds.has(id));\n if (firstIndex < 0) return null;\n\n const currentIndex = Math.max(firstIndex, ids.indexOf(currentId ?? ''));\n const candidateIndex =\n delta === Number.NEGATIVE_INFINITY\n ? 0\n : delta === Number.POSITIVE_INFINITY\n ? ids.length - 1\n : Math.max(0, Math.min(ids.length - 1, currentIndex + delta));\n const direction =\n delta === Number.NEGATIVE_INFINITY\n ? 1\n : delta === Number.POSITIVE_INFINITY\n ? -1\n : Math.sign(delta);\n\n for (let index = candidateIndex; index >= 0 && index < ids.length; index += direction) {\n const candidate = ids.at(index);\n if (candidate !== undefined && !disabledIds.has(candidate)) return candidate;\n }\n const current = ids.at(currentIndex);\n return current !== undefined && !disabledIds.has(current)\n ? current\n : (ids.at(firstIndex) ?? null);\n}\n\n/*** Translate navigation keys into collection-relative movements. */\nfunction resolveFocusDelta(key: string, columns: number): number | null {\n const rowSize = Math.max(1, Math.floor(columns));\n switch (key) {\n case 'ArrowLeft':\n return -1;\n case 'ArrowRight':\n return 1;\n case 'ArrowUp':\n return -rowSize;\n case 'ArrowDown':\n return rowSize;\n case 'Home':\n return Number.NEGATIVE_INFINITY;\n case 'End':\n return Number.POSITIVE_INFINITY;\n default:\n return null;\n }\n}\n"]}
@@ -2,5 +2,6 @@ export type { ExplorerActivateEvent, ExplorerItem, ExplorerItemKind, ExplorerPro
2
2
  export { Explorer } from './adapters/inbound/Explorer';
3
3
  export { FileExplorer } from './adapters/inbound/FileExplorer';
4
4
  export { MediaExplorer } from './adapters/inbound/MediaExplorer';
5
+ export { getExplorerNextFocusId } from './application/getExplorerNextFocusId';
5
6
  export { resolveExplorerSelection } from './application/resolveExplorerSelection';
6
7
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/features/explorer/public.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,qBAAqB,EACrB,YAAY,EACZ,gBAAgB,EAChB,aAAa,EACb,4BAA4B,EAC5B,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAC"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/features/explorer/public.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,qBAAqB,EACrB,YAAY,EACZ,gBAAgB,EAChB,aAAa,EACb,4BAA4B,EAC5B,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAC9E,OAAO,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAC"}
@@ -1,5 +1,6 @@
1
1
  export { Explorer } from './adapters/inbound/Explorer';
2
2
  export { FileExplorer } from './adapters/inbound/FileExplorer';
3
3
  export { MediaExplorer } from './adapters/inbound/MediaExplorer';
4
+ export { getExplorerNextFocusId } from './application/getExplorerNextFocusId';
4
5
  export { resolveExplorerSelection } from './application/resolveExplorerSelection';
5
6
  //# sourceMappingURL=public.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.js","sourceRoot":"","sources":["../../../src/features/explorer/public.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAC","sourcesContent":["export type {\n ExplorerActivateEvent,\n ExplorerItem,\n ExplorerItemKind,\n ExplorerProps,\n ExplorerSelectionChangeEvent,\n ExplorerSelectionMode,\n} from '../../types/explorer';\nexport { Explorer } from './adapters/inbound/Explorer';\nexport { FileExplorer } from './adapters/inbound/FileExplorer';\nexport { MediaExplorer } from './adapters/inbound/MediaExplorer';\nexport { resolveExplorerSelection } from './application/resolveExplorerSelection';\n"]}
1
+ {"version":3,"file":"public.js","sourceRoot":"","sources":["../../../src/features/explorer/public.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAC9E,OAAO,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAC","sourcesContent":["export type {\n ExplorerActivateEvent,\n ExplorerItem,\n ExplorerItemKind,\n ExplorerProps,\n ExplorerSelectionChangeEvent,\n ExplorerSelectionMode,\n} from '../../types/explorer';\nexport { Explorer } from './adapters/inbound/Explorer';\nexport { FileExplorer } from './adapters/inbound/FileExplorer';\nexport { MediaExplorer } from './adapters/inbound/MediaExplorer';\nexport { getExplorerNextFocusId } from './application/getExplorerNextFocusId';\nexport { resolveExplorerSelection } from './application/resolveExplorerSelection';\n"]}
@@ -6,5 +6,5 @@ import type { GridViewProps } from '../../../../types/grid-view';
6
6
  * This first renderer uses nested native scroll regions. Only visible items are mounted;
7
7
  * logical cells and invisible elements are never rendered.
8
8
  */
9
- export declare function GridView({ items, contentWidth, contentHeight, width, height, zoom, overscanPixels, interactionPolicy, onViewportChange, renderItem, testID, }: GridViewProps): React.JSX.Element;
9
+ export declare function GridView({ items, contentWidth, contentHeight, width, height, zoom, overscanPixels, focusedItemId, revealPaddingPixels, interactionPolicy, onViewportChange, onVisibleItemIdsChange, renderItem, testID, }: GridViewProps): React.JSX.Element;
10
10
  //# sourceMappingURL=GridView.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"GridView.d.ts","sourceRoot":"","sources":["../../../../../src/features/grid-view/adapters/inbound/GridView.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,YAAY,EACZ,aAAa,EACb,KAAK,EACL,MAAM,EACN,IAAQ,EACR,cAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,MAAM,GACP,EAAE,aAAa,qBAsEf"}
1
+ {"version":3,"file":"GridView.d.ts","sourceRoot":"","sources":["../../../../../src/features/grid-view/adapters/inbound/GridView.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,YAAY,EACZ,aAAa,EACb,KAAK,EACL,MAAM,EACN,IAAQ,EACR,cAAoB,EACpB,aAAa,EACb,mBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,UAAU,EACV,MAAM,GACP,EAAE,aAAa,qBA8Gf"}
@@ -1,4 +1,4 @@
1
- import { getVisibleGridItems, worldToViewport } from '@ankhorage/grid-view';
1
+ import { getVisibleGridItems, revealWorldRect, worldToViewport, } from '@ankhorage/grid-view';
2
2
  import React from 'react';
3
3
  import { ScrollView as NativeScrollView, View as NativeView } from 'react-native';
4
4
  /***
@@ -7,9 +7,11 @@ import { ScrollView as NativeScrollView, View as NativeView } from 'react-native
7
7
  * This first renderer uses nested native scroll regions. Only visible items are mounted;
8
8
  * logical cells and invisible elements are never rendered.
9
9
  */
10
- export function GridView({ items, contentWidth, contentHeight, width, height, zoom = 1, overscanPixels = 160, interactionPolicy, onViewportChange, renderItem, testID, }) {
10
+ export function GridView({ items, contentWidth, contentHeight, width, height, zoom = 1, overscanPixels = 160, focusedItemId, revealPaddingPixels = 8, interactionPolicy, onViewportChange, onVisibleItemIdsChange, renderItem, testID, }) {
11
11
  const [scrollX, setScrollX] = React.useState(0);
12
12
  const [scrollY, setScrollY] = React.useState(0);
13
+ const horizontalScrollRef = React.useRef(null);
14
+ const verticalScrollRef = React.useRef(null);
13
15
  const scale = Math.max(0.01, zoom);
14
16
  const viewport = React.useMemo(() => ({
15
17
  width,
@@ -20,11 +22,41 @@ export function GridView({ items, contentWidth, contentHeight, width, height, zo
20
22
  pixelsPerUnitY: scale,
21
23
  }), [height, scale, scrollX, scrollY, width]);
22
24
  const visibleItems = React.useMemo(() => getVisibleGridItems(items, viewport, overscanPixels), [items, overscanPixels, viewport]);
25
+ const viewportVisibleIds = React.useMemo(() => getVisibleGridItems(visibleItems, viewport).map((item) => item.id), [viewport, visibleItems]);
26
+ const previousVisibleIdsRef = React.useRef([]);
27
+ React.useEffect(() => {
28
+ if (!onVisibleItemIdsChange)
29
+ return;
30
+ const previous = previousVisibleIdsRef.current;
31
+ if (previous.length === viewportVisibleIds.length &&
32
+ previous.every((id, index) => id === viewportVisibleIds.at(index))) {
33
+ return;
34
+ }
35
+ previousVisibleIdsRef.current = viewportVisibleIds;
36
+ onVisibleItemIdsChange(viewportVisibleIds);
37
+ }, [onVisibleItemIdsChange, viewportVisibleIds]);
23
38
  React.useEffect(() => {
24
39
  onViewportChange?.(viewport);
25
40
  }, [onViewportChange, viewport]);
26
- return (<NativeScrollView horizontal scrollEnabled={interactionPolicy !== 'passive'} scrollEventThrottle={32} showsHorizontalScrollIndicator style={{ width, height }} testID={testID} onScroll={(event) => setScrollX(event.nativeEvent.contentOffset.x)}>
27
- <NativeScrollView scrollEnabled={interactionPolicy !== 'passive'} scrollEventThrottle={32} showsVerticalScrollIndicator style={{ width: contentWidth * scale, height }} onScroll={(event) => setScrollY(event.nativeEvent.contentOffset.y)}>
41
+ const viewportRef = React.useRef(viewport);
42
+ React.useEffect(() => {
43
+ viewportRef.current = viewport;
44
+ }, [viewport]);
45
+ React.useEffect(() => {
46
+ const currentViewport = viewportRef.current;
47
+ const focusedItem = items.find((item) => item.id === focusedItemId);
48
+ if (!focusedItem)
49
+ return;
50
+ const revealed = revealWorldRect(currentViewport, focusedItem, revealPaddingPixels);
51
+ if (revealed.offsetX !== currentViewport.offsetX) {
52
+ horizontalScrollRef.current?.scrollTo({ x: revealed.offsetX * scale, animated: true });
53
+ }
54
+ if (revealed.offsetY !== currentViewport.offsetY) {
55
+ verticalScrollRef.current?.scrollTo({ y: revealed.offsetY * scale, animated: true });
56
+ }
57
+ }, [focusedItemId, height, items, revealPaddingPixels, scale, width]);
58
+ return (<NativeScrollView ref={horizontalScrollRef} horizontal scrollEnabled={interactionPolicy !== 'passive'} scrollEventThrottle={32} showsHorizontalScrollIndicator style={{ width, height }} testID={testID} onScroll={(event) => setScrollX(event.nativeEvent.contentOffset.x)}>
59
+ <NativeScrollView ref={verticalScrollRef} scrollEnabled={interactionPolicy !== 'passive'} scrollEventThrottle={32} showsVerticalScrollIndicator style={{ width: contentWidth * scale, height }} onScroll={(event) => setScrollY(event.nativeEvent.contentOffset.y)}>
28
60
  <NativeView style={{ width: contentWidth * scale, height: contentHeight * scale }}>
29
61
  {visibleItems.map((item) => {
30
62
  const position = worldToViewport({ x: item.x, y: item.y }, {
@@ -1 +1 @@
1
- {"version":3,"file":"GridView.js","sourceRoot":"","sources":["../../../../../src/features/grid-view/adapters/inbound/GridView.tsx"],"names":[],"mappings":"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","sourcesContent":["import { getVisibleGridItems, type GridViewport, worldToViewport } from '@ankhorage/grid-view';\nimport React from 'react';\nimport { ScrollView as NativeScrollView, View as NativeView } from 'react-native';\n\nimport type { GridViewProps } from '../../../../types/grid-view';\n\n/***\n * Renders world-positioned items on native and web using the canonical viewport/culling engine.\n *\n * This first renderer uses nested native scroll regions. Only visible items are mounted;\n * logical cells and invisible elements are never rendered.\n */\nexport function GridView({\n items,\n contentWidth,\n contentHeight,\n width,\n height,\n zoom = 1,\n overscanPixels = 160,\n interactionPolicy,\n onViewportChange,\n renderItem,\n testID,\n}: GridViewProps) {\n const [scrollX, setScrollX] = React.useState(0);\n const [scrollY, setScrollY] = React.useState(0);\n const scale = Math.max(0.01, zoom);\n const viewport = React.useMemo<GridViewport>(\n () => ({\n width,\n height,\n offsetX: scrollX / scale,\n offsetY: scrollY / scale,\n pixelsPerUnitX: scale,\n pixelsPerUnitY: scale,\n }),\n [height, scale, scrollX, scrollY, width],\n );\n const visibleItems = React.useMemo(\n () => getVisibleGridItems(items, viewport, overscanPixels),\n [items, overscanPixels, viewport],\n );\n\n React.useEffect(() => {\n onViewportChange?.(viewport);\n }, [onViewportChange, viewport]);\n\n return (\n <NativeScrollView\n horizontal\n scrollEnabled={interactionPolicy !== 'passive'}\n scrollEventThrottle={32}\n showsHorizontalScrollIndicator\n style={{ width, height }}\n testID={testID}\n onScroll={(event) => setScrollX(event.nativeEvent.contentOffset.x)}\n >\n <NativeScrollView\n scrollEnabled={interactionPolicy !== 'passive'}\n scrollEventThrottle={32}\n showsVerticalScrollIndicator\n style={{ width: contentWidth * scale, height }}\n onScroll={(event) => setScrollY(event.nativeEvent.contentOffset.y)}\n >\n <NativeView style={{ width: contentWidth * scale, height: contentHeight * scale }}>\n {visibleItems.map((item) => {\n const position = worldToViewport(\n { x: item.x, y: item.y },\n {\n ...viewport,\n offsetX: 0,\n offsetY: 0,\n },\n );\n return (\n <NativeView\n key={item.id}\n style={{\n position: 'absolute',\n left: position.x,\n top: position.y,\n width: item.width * scale,\n height: item.height * scale,\n }}\n >\n {renderItem(item)}\n </NativeView>\n );\n })}\n </NativeView>\n </NativeScrollView>\n </NativeScrollView>\n );\n}\n"]}
1
+ {"version":3,"file":"GridView.js","sourceRoot":"","sources":["../../../../../src/features/grid-view/adapters/inbound/GridView.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n getVisibleGridItems,\n type GridViewport,\n revealWorldRect,\n worldToViewport,\n} from '@ankhorage/grid-view';\nimport React from 'react';\nimport { ScrollView as NativeScrollView, View as NativeView } from 'react-native';\n\nimport type { GridViewProps } from '../../../../types/grid-view';\n\n/***\n * Renders world-positioned items on native and web using the canonical viewport/culling engine.\n *\n * This first renderer uses nested native scroll regions. Only visible items are mounted;\n * logical cells and invisible elements are never rendered.\n */\nexport function GridView({\n items,\n contentWidth,\n contentHeight,\n width,\n height,\n zoom = 1,\n overscanPixels = 160,\n focusedItemId,\n revealPaddingPixels = 8,\n interactionPolicy,\n onViewportChange,\n onVisibleItemIdsChange,\n renderItem,\n testID,\n}: GridViewProps) {\n const [scrollX, setScrollX] = React.useState(0);\n const [scrollY, setScrollY] = React.useState(0);\n const horizontalScrollRef = React.useRef<NativeScrollView>(null);\n const verticalScrollRef = React.useRef<NativeScrollView>(null);\n const scale = Math.max(0.01, zoom);\n const viewport = React.useMemo<GridViewport>(\n () => ({\n width,\n height,\n offsetX: scrollX / scale,\n offsetY: scrollY / scale,\n pixelsPerUnitX: scale,\n pixelsPerUnitY: scale,\n }),\n [height, scale, scrollX, scrollY, width],\n );\n const visibleItems = React.useMemo(\n () => getVisibleGridItems(items, viewport, overscanPixels),\n [items, overscanPixels, viewport],\n );\n const viewportVisibleIds = React.useMemo(\n () => getVisibleGridItems(visibleItems, viewport).map((item) => item.id),\n [viewport, visibleItems],\n );\n const previousVisibleIdsRef = React.useRef<readonly string[]>([]);\n\n React.useEffect(() => {\n if (!onVisibleItemIdsChange) return;\n const previous = previousVisibleIdsRef.current;\n if (\n previous.length === viewportVisibleIds.length &&\n previous.every((id, index) => id === viewportVisibleIds.at(index))\n ) {\n return;\n }\n previousVisibleIdsRef.current = viewportVisibleIds;\n onVisibleItemIdsChange(viewportVisibleIds);\n }, [onVisibleItemIdsChange, viewportVisibleIds]);\n\n React.useEffect(() => {\n onViewportChange?.(viewport);\n }, [onViewportChange, viewport]);\n\n const viewportRef = React.useRef(viewport);\n React.useEffect(() => {\n viewportRef.current = viewport;\n }, [viewport]);\n\n React.useEffect(() => {\n const currentViewport = viewportRef.current;\n const focusedItem = items.find((item) => item.id === focusedItemId);\n if (!focusedItem) return;\n const revealed = revealWorldRect(currentViewport, focusedItem, revealPaddingPixels);\n if (revealed.offsetX !== currentViewport.offsetX) {\n horizontalScrollRef.current?.scrollTo({ x: revealed.offsetX * scale, animated: true });\n }\n if (revealed.offsetY !== currentViewport.offsetY) {\n verticalScrollRef.current?.scrollTo({ y: revealed.offsetY * scale, animated: true });\n }\n }, [focusedItemId, height, items, revealPaddingPixels, scale, width]);\n\n return (\n <NativeScrollView\n ref={horizontalScrollRef}\n horizontal\n scrollEnabled={interactionPolicy !== 'passive'}\n scrollEventThrottle={32}\n showsHorizontalScrollIndicator\n style={{ width, height }}\n testID={testID}\n onScroll={(event) => setScrollX(event.nativeEvent.contentOffset.x)}\n >\n <NativeScrollView\n ref={verticalScrollRef}\n scrollEnabled={interactionPolicy !== 'passive'}\n scrollEventThrottle={32}\n showsVerticalScrollIndicator\n style={{ width: contentWidth * scale, height }}\n onScroll={(event) => setScrollY(event.nativeEvent.contentOffset.y)}\n >\n <NativeView style={{ width: contentWidth * scale, height: contentHeight * scale }}>\n {visibleItems.map((item) => {\n const position = worldToViewport(\n { x: item.x, y: item.y },\n {\n ...viewport,\n offsetX: 0,\n offsetY: 0,\n },\n );\n return (\n <NativeView\n key={item.id}\n style={{\n position: 'absolute',\n left: position.x,\n top: position.y,\n width: item.width * scale,\n height: item.height * scale,\n }}\n >\n {renderItem(item)}\n </NativeView>\n );\n })}\n </NativeView>\n </NativeScrollView>\n </NativeScrollView>\n );\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
2
  import type { TileGridProps } from '../../../../types/grid-view';
3
3
  /*** Composes a responsive, virtualized tile presentation over the generic GridView renderer. */
4
- export declare function TileGrid({ items, width, height, tileSize, gap, zoom, overscanPixels, renderItem, interactionPolicy, testID, }: TileGridProps): React.JSX.Element;
4
+ export declare function TileGrid({ items, width, height, tileSize, gap, zoom, overscanPixels, focusedItemId, revealPaddingPixels, onColumnsChange, onVisibleItemIdsChange, renderItem, interactionPolicy, testID, }: TileGridProps): React.JSX.Element;
5
5
  //# sourceMappingURL=TileGrid.d.ts.map