@ankhorage/zora 25.2.1 → 25.4.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 (252) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -1
  3. package/dist/features/grid-rulers/adapters/inbound/GridLineOverlay.d.ts +5 -0
  4. package/dist/features/grid-rulers/adapters/inbound/GridLineOverlay.d.ts.map +1 -0
  5. package/dist/features/grid-rulers/adapters/inbound/GridLineOverlay.js +69 -0
  6. package/dist/features/grid-rulers/adapters/inbound/GridLineOverlay.js.map +1 -0
  7. package/dist/features/grid-rulers/adapters/inbound/GridRuler.d.ts +5 -0
  8. package/dist/features/grid-rulers/adapters/inbound/GridRuler.d.ts.map +1 -0
  9. package/dist/features/grid-rulers/adapters/inbound/GridRuler.js +59 -0
  10. package/dist/features/grid-rulers/adapters/inbound/GridRuler.js.map +1 -0
  11. package/dist/features/grid-rulers/application/projectGridRulerPosition.d.ts +4 -0
  12. package/dist/features/grid-rulers/application/projectGridRulerPosition.d.ts.map +1 -0
  13. package/dist/features/grid-rulers/application/projectGridRulerPosition.js +10 -0
  14. package/dist/features/grid-rulers/application/projectGridRulerPosition.js.map +1 -0
  15. package/dist/features/grid-rulers/application/resolveGridRulerMarks.d.ts +5 -0
  16. package/dist/features/grid-rulers/application/resolveGridRulerMarks.d.ts.map +1 -0
  17. package/dist/features/grid-rulers/application/resolveGridRulerMarks.js +22 -0
  18. package/dist/features/grid-rulers/application/resolveGridRulerMarks.js.map +1 -0
  19. package/dist/features/grid-rulers/meta.d.ts +21 -0
  20. package/dist/features/grid-rulers/meta.d.ts.map +1 -0
  21. package/dist/features/grid-rulers/meta.js +22 -0
  22. package/dist/features/grid-rulers/meta.js.map +1 -0
  23. package/dist/features/grid-rulers/public.d.ts +5 -0
  24. package/dist/features/grid-rulers/public.d.ts.map +1 -0
  25. package/dist/features/grid-rulers/public.js +4 -0
  26. package/dist/features/grid-rulers/public.js.map +1 -0
  27. package/dist/features/registry/ZORA_COMPONENT_REGISTRY.d.ts.map +1 -1
  28. package/dist/features/registry/ZORA_COMPONENT_REGISTRY.js +5 -0
  29. package/dist/features/registry/ZORA_COMPONENT_REGISTRY.js.map +1 -1
  30. package/dist/features/registry/componentMeta.d.ts.map +1 -1
  31. package/dist/features/registry/componentMeta.js +5 -0
  32. package/dist/features/registry/componentMeta.js.map +1 -1
  33. package/dist/features/spatial-grid/adapters/inbound/SpatialGrid.d.ts +5 -0
  34. package/dist/features/spatial-grid/adapters/inbound/SpatialGrid.d.ts.map +1 -0
  35. package/dist/features/spatial-grid/adapters/inbound/SpatialGrid.js +38 -0
  36. package/dist/features/spatial-grid/adapters/inbound/SpatialGrid.js.map +1 -0
  37. package/dist/features/spatial-grid/application/sortSpatialGridItems.d.ts +4 -0
  38. package/dist/features/spatial-grid/application/sortSpatialGridItems.d.ts.map +1 -0
  39. package/dist/features/spatial-grid/application/sortSpatialGridItems.js +8 -0
  40. package/dist/features/spatial-grid/application/sortSpatialGridItems.js.map +1 -0
  41. package/dist/features/spatial-grid/meta.d.ts +11 -0
  42. package/dist/features/spatial-grid/meta.d.ts.map +1 -0
  43. package/dist/features/spatial-grid/meta.js +11 -0
  44. package/dist/features/spatial-grid/meta.js.map +1 -0
  45. package/dist/features/spatial-grid/public.d.ts +3 -0
  46. package/dist/features/spatial-grid/public.d.ts.map +1 -0
  47. package/dist/features/spatial-grid/public.js +2 -0
  48. package/dist/features/spatial-grid/public.js.map +1 -0
  49. package/dist/index.d.ts +4 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +2 -0
  52. package/dist/index.js.map +1 -1
  53. package/dist/types/grid-rulers.d.ts +48 -0
  54. package/dist/types/grid-rulers.d.ts.map +1 -0
  55. package/dist/types/grid-rulers.js +2 -0
  56. package/dist/types/grid-rulers.js.map +1 -0
  57. package/dist/types/spatial-grid.d.ts +41 -0
  58. package/dist/types/spatial-grid.d.ts.map +1 -0
  59. package/dist/types/spatial-grid.js +2 -0
  60. package/dist/types/spatial-grid.js.map +1 -0
  61. package/package.json +3 -3
  62. package/src/features/grid-rulers/adapters/inbound/GridLineOverlay.tsx +123 -0
  63. package/src/features/grid-rulers/adapters/inbound/GridRuler.tsx +96 -0
  64. package/src/features/grid-rulers/adapters/inbound/GridRulers.test.tsx +162 -0
  65. package/src/features/grid-rulers/application/projectGridRulerPosition.ts +19 -0
  66. package/src/features/grid-rulers/application/resolveGridRulerMarks.test.ts +90 -0
  67. package/src/features/grid-rulers/application/resolveGridRulerMarks.ts +39 -0
  68. package/src/features/grid-rulers/meta.ts +26 -0
  69. package/src/features/grid-rulers/public.ts +10 -0
  70. package/src/features/registry/ZORA_COMPONENT_REGISTRY.ts +5 -0
  71. package/src/features/registry/componentMeta.ts +5 -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/index.ts +14 -0
  79. package/src/types/grid-rulers.ts +59 -0
  80. package/src/types/spatial-grid.ts +44 -0
  81. package/web-dist/chunks/{Accordion-mh67wb34.js → Accordion-090y18cv.js} +2 -2
  82. package/web-dist/chunks/{Accordion-7yazfxr6.js → Accordion-3d4wkzg7.js} +4 -4
  83. package/web-dist/chunks/{Accordion-ndf3jqhb.js → Accordion-40tenzcg.js} +3 -3
  84. package/web-dist/chunks/{Accordion-cbfa2b8j.js → Accordion-4s68t3h7.js} +5 -5
  85. package/web-dist/chunks/{Accordion-7asr7vb0.js → Accordion-4zqd7twp.js} +4 -4
  86. package/web-dist/chunks/{Accordion-sdcfa14w.js → Accordion-5g05kfmx.js} +3 -3
  87. package/web-dist/chunks/Accordion-60wn9xn9.js +201 -0
  88. package/web-dist/chunks/{Accordion-wgejzesa.js → Accordion-61ntvh14.js} +3 -3
  89. package/web-dist/chunks/{Accordion-q09fjenx.js → Accordion-6rrqpb2h.js} +1 -1
  90. package/web-dist/chunks/{Accordion-mej71qnw.js → Accordion-a4cma5b6.js} +2 -2
  91. package/web-dist/chunks/{Accordion-9dqkq9am.js → Accordion-ejx5gcf9.js} +8 -8
  92. package/web-dist/chunks/{Accordion-pe66x9cv.js → Accordion-ewf1zen9.js} +4 -4
  93. package/web-dist/chunks/{Accordion-xvzrx6bh.js → Accordion-fjxcn6g9.js} +7 -7
  94. package/web-dist/chunks/{Accordion-teg40mpt.js → Accordion-fmzxat3v.js} +3 -3
  95. package/web-dist/chunks/{Accordion-016wvwpg.js → Accordion-h3y8qev2.js} +1 -1
  96. package/web-dist/chunks/{Accordion-t0hbez90.js → Accordion-h7zsx9eg.js} +2 -2
  97. package/web-dist/chunks/{Accordion-ch4q5w0z.js → Accordion-jnc315h7.js} +1 -1
  98. package/web-dist/chunks/{Accordion-fkc50r9z.js → Accordion-kjtcbe09.js} +2 -2
  99. package/web-dist/chunks/{Accordion-tmrjy46p.js → Accordion-nssr5gep.js} +1 -1
  100. package/web-dist/chunks/{Accordion-54k0ncs3.js → Accordion-nxsda1my.js} +5 -73
  101. package/web-dist/chunks/{Accordion-ntndskbj.js → Accordion-pvj5npzc.js} +2 -2
  102. package/web-dist/chunks/{Accordion-ggwxv6k3.js → Accordion-r1cj2w9e.js} +2 -2
  103. package/web-dist/chunks/Accordion-r2w86rwv.js +209 -0
  104. package/web-dist/chunks/{Accordion-kkr7xfh2.js → Accordion-s3d6g59s.js} +4 -4
  105. package/web-dist/chunks/{Accordion-5ga4549x.js → Accordion-sp4k08kw.js} +3 -3
  106. package/web-dist/chunks/{Accordion-1p9r2ack.js → Accordion-sy1cc5my.js} +2 -2
  107. package/web-dist/chunks/{Accordion-hmhdwvqp.js → Accordion-tqpsmdrk.js} +3 -3
  108. package/web-dist/chunks/{Accordion-fzkrrc4z.js → Accordion-v0grdh6q.js} +1 -1
  109. package/web-dist/chunks/{Accordion-ajawc6nq.js → Accordion-zt9yghdz.js} +3 -3
  110. package/web-dist/components/accordion/Accordion.js +4 -4
  111. package/web-dist/components/accordion-item/AccordionItem.js +4 -4
  112. package/web-dist/components/app-bar/AppBar.js +6 -6
  113. package/web-dist/components/app-header/AppHeader.js +6 -6
  114. package/web-dist/components/app-shell/AppShell.js +1 -1
  115. package/web-dist/components/auth-screen/AuthScreen.js +9 -9
  116. package/web-dist/components/avatar/Avatar.js +4 -4
  117. package/web-dist/components/avatar-group/AvatarGroup.js +4 -4
  118. package/web-dist/components/badge/Badge.js +1 -1
  119. package/web-dist/components/barcode-scanner-view/BarcodeScannerView.js +9 -9
  120. package/web-dist/components/bottom-sheet/BottomSheet.js +2 -2
  121. package/web-dist/components/bottom-sheet-flat-list/BottomSheetFlatList.js +2 -2
  122. package/web-dist/components/bottom-sheet-scroll-view/BottomSheetScrollView.js +2 -2
  123. package/web-dist/components/bottom-sheet-section-list/BottomSheetSectionList.js +2 -2
  124. package/web-dist/components/bottom-sheet-view/BottomSheetView.js +2 -2
  125. package/web-dist/components/bottom-sheet-virtualized-list/BottomSheetVirtualizedList.js +2 -2
  126. package/web-dist/components/breadcrumbs/Breadcrumbs.js +4 -4
  127. package/web-dist/components/button/Button.js +2 -2
  128. package/web-dist/components/button-group/ButtonGroup.js +2 -2
  129. package/web-dist/components/camera-permission-view/CameraPermissionView.js +9 -9
  130. package/web-dist/components/card/Card.js +8 -8
  131. package/web-dist/components/card-back/CardBack.js +1 -1
  132. package/web-dist/components/card-hand/CardHand.js +1 -1
  133. package/web-dist/components/chat-list-item/ChatListItem.js +6 -6
  134. package/web-dist/components/checkbox/Checkbox.js +3 -3
  135. package/web-dist/components/checkbox-group/CheckboxGroup.js +3 -3
  136. package/web-dist/components/chess-board/ChessBoard.js +1 -1
  137. package/web-dist/components/chip/Chip.js +4 -4
  138. package/web-dist/components/chip-group/ChipGroup.js +4 -4
  139. package/web-dist/components/collection-editor/CollectionEditor.js +8 -8
  140. package/web-dist/components/content-rail/ContentRail.js +3 -3
  141. package/web-dist/components/data-table/DataTable.js +13 -13
  142. package/web-dist/components/date-picker/DatePicker.js +4 -4
  143. package/web-dist/components/dialog/Dialog.js +3 -3
  144. package/web-dist/components/divider/Divider.js +1 -1
  145. package/web-dist/components/empty-state/EmptyState.js +9 -9
  146. package/web-dist/components/explorer/Explorer.js +6 -6
  147. package/web-dist/components/field/Field.js +3 -3
  148. package/web-dist/components/file-explorer/FileExplorer.js +6 -6
  149. package/web-dist/components/flat-list/FlatList.js +4 -4
  150. package/web-dist/components/forgot-password-form/ForgotPasswordForm.js +9 -9
  151. package/web-dist/components/form/Form.js +5 -5
  152. package/web-dist/components/form-actions/FormActions.js +5 -5
  153. package/web-dist/components/form-error/FormError.js +5 -5
  154. package/web-dist/components/game/Game.js +2 -2
  155. package/web-dist/components/game-entity/GameEntity.js +2 -2
  156. package/web-dist/components/game-field/GameField.js +2 -2
  157. package/web-dist/components/game-input-zone/GameInputZone.js +2 -2
  158. package/web-dist/components/game-measurement-probe/GameMeasurementProbe.js +2 -2
  159. package/web-dist/components/game-overlay/GameOverlay.js +2 -2
  160. package/web-dist/components/gradient/Gradient.js +2 -2
  161. package/web-dist/components/gradient-renderer-provider/GradientRendererProvider.js +2 -2
  162. package/web-dist/components/grid/Grid.js +1 -1
  163. package/web-dist/components/grid-line-overlay/GridLineOverlay.d.ts +4 -0
  164. package/web-dist/components/grid-line-overlay/GridLineOverlay.js +13 -0
  165. package/web-dist/components/grid-line-overlay/index.d.ts +1 -0
  166. package/web-dist/components/grid-line-overlay/index.js +2 -0
  167. package/web-dist/components/grid-ruler/GridRuler.d.ts +4 -0
  168. package/web-dist/components/grid-ruler/GridRuler.js +13 -0
  169. package/web-dist/components/grid-ruler/index.d.ts +1 -0
  170. package/web-dist/components/grid-ruler/index.js +2 -0
  171. package/web-dist/components/grid-view/GridView.js +1 -1
  172. package/web-dist/components/heading/Heading.js +1 -1
  173. package/web-dist/components/hero/Hero.js +8 -8
  174. package/web-dist/components/icon/Icon.js +1 -1
  175. package/web-dist/components/icon-button/IconButton.js +2 -2
  176. package/web-dist/components/image/Image.js +3 -3
  177. package/web-dist/components/keyboard-avoiding-view/KeyboardAvoidingView.js +1 -1
  178. package/web-dist/components/list/List.js +4 -4
  179. package/web-dist/components/list-item/ListItem.js +4 -4
  180. package/web-dist/components/list-section/ListSection.js +4 -4
  181. package/web-dist/components/media-card/MediaCard.js +8 -8
  182. package/web-dist/components/media-explorer/MediaExplorer.js +6 -6
  183. package/web-dist/components/message-bubble/MessageBubble.js +6 -6
  184. package/web-dist/components/metric-card/MetricCard.js +8 -8
  185. package/web-dist/components/missing-element/MissingElement.js +3 -3
  186. package/web-dist/components/o-auth-provider-button/OAuthProviderButton.js +9 -9
  187. package/web-dist/components/o-auth-provider-list/OAuthProviderList.js +9 -9
  188. package/web-dist/components/opening-book/OpeningBook.js +1 -1
  189. package/web-dist/components/otp-form/OtpForm.js +9 -9
  190. package/web-dist/components/pagination/Pagination.js +3 -3
  191. package/web-dist/components/palette-item/PaletteItem.js +8 -8
  192. package/web-dist/components/playing-card/PlayingCard.js +1 -1
  193. package/web-dist/components/poker-training-table/PokerTrainingTable.js +1 -1
  194. package/web-dist/components/popover-menu/PopoverMenu.js +3 -3
  195. package/web-dist/components/post-card/PostCard.js +8 -8
  196. package/web-dist/components/product-card/ProductCard.js +8 -8
  197. package/web-dist/components/progress/Progress.js +3 -3
  198. package/web-dist/components/progress-ring/ProgressRing.js +3 -3
  199. package/web-dist/components/radio/Radio.js +4 -4
  200. package/web-dist/components/radio-group/RadioGroup.js +4 -4
  201. package/web-dist/components/rating/Rating.js +2 -2
  202. package/web-dist/components/reader/Reader.js +9 -9
  203. package/web-dist/components/scan-overlay/ScanOverlay.js +9 -9
  204. package/web-dist/components/screen/Screen.js +1 -1
  205. package/web-dist/components/screen-section/ScreenSection.js +3 -3
  206. package/web-dist/components/scroll-view/ScrollView.js +1 -1
  207. package/web-dist/components/search-input/SearchInput.js +3 -3
  208. package/web-dist/components/section-header/SectionHeader.js +3 -3
  209. package/web-dist/components/section-list/SectionList.js +4 -4
  210. package/web-dist/components/select/Select.js +3 -3
  211. package/web-dist/components/selectable-item/SelectableItem.js +1 -1
  212. package/web-dist/components/selection-provider/SelectionProvider.js +1 -1
  213. package/web-dist/components/sign-in-form/SignInForm.js +9 -9
  214. package/web-dist/components/sign-up-form/SignUpForm.js +9 -9
  215. package/web-dist/components/skeleton/Skeleton.js +11 -11
  216. package/web-dist/components/skeleton-card/SkeletonCard.js +11 -11
  217. package/web-dist/components/skeleton-list/SkeletonList.js +11 -11
  218. package/web-dist/components/skeleton-text/SkeletonText.js +11 -11
  219. package/web-dist/components/spatial-grid/SpatialGrid.d.ts +3 -0
  220. package/web-dist/components/spatial-grid/SpatialGrid.js +102 -0
  221. package/web-dist/components/spatial-grid/index.d.ts +1 -0
  222. package/web-dist/components/spatial-grid/index.js +2 -0
  223. package/web-dist/components/surface/Surface.js +1 -1
  224. package/web-dist/components/tab/Tab.js +1 -1
  225. package/web-dist/components/tab-list/TabList.js +1 -1
  226. package/web-dist/components/tab-panel/TabPanel.js +1 -1
  227. package/web-dist/components/tabletop-table/TabletopTable.js +1 -1
  228. package/web-dist/components/tabs/Tabs.js +1 -1
  229. package/web-dist/components/text/Text.js +1 -1
  230. package/web-dist/components/text-input/TextInput.js +3 -3
  231. package/web-dist/components/tile-grid/TileGrid.js +1 -1
  232. package/web-dist/components/time-picker/TimePicker.js +4 -4
  233. package/web-dist/components/timeline/Timeline.js +3 -3
  234. package/web-dist/components/toast/Toast.js +1 -1
  235. package/web-dist/components/toast-provider/ToastProvider.js +1 -1
  236. package/web-dist/components/toolbar/Toolbar.js +8 -8
  237. package/web-dist/components/tree-item/TreeItem.js +7 -7
  238. package/web-dist/components/uploader/Uploader.js +11 -11
  239. package/web-dist/components/view/View.js +1 -1
  240. package/web-dist/manifest.json +555 -493
  241. package/web-dist/runtime/ZoraProvider.js +3 -3
  242. /package/web-dist/chunks/{Accordion-qkh5yy94.js → Accordion-7kq500r1.js} +0 -0
  243. /package/web-dist/chunks/{Accordion-f0jsd9f9.js → Accordion-7tjz25jh.js} +0 -0
  244. /package/web-dist/chunks/{Accordion-3m3572sa.js → Accordion-88t8fgxv.js} +0 -0
  245. /package/web-dist/chunks/{Accordion-afmybp67.js → Accordion-9w8wg9mf.js} +0 -0
  246. /package/web-dist/chunks/{Accordion-bb8y34wh.js → Accordion-b96evb46.js} +0 -0
  247. /package/web-dist/chunks/{Accordion-nmpmdrba.js → Accordion-dbf93gxh.js} +0 -0
  248. /package/web-dist/chunks/{Accordion-x12fca21.js → Accordion-fhnazn3n.js} +0 -0
  249. /package/web-dist/chunks/{Accordion-n9c5tsqw.js → Accordion-qn398m1x.js} +0 -0
  250. /package/web-dist/chunks/{Accordion-2rgexkze.js → Accordion-rsvphbe4.js} +0 -0
  251. /package/web-dist/chunks/{Accordion-c69374jn.js → Accordion-sw7ep29d.js} +0 -0
  252. /package/web-dist/chunks/{Accordion-8jxc7tx9.js → Accordion-w6qedeba.js} +0 -0
@@ -0,0 +1,123 @@
1
+ import React from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+
4
+ import type { GridLineOverlayProps } from '../../../../types/grid-rulers';
5
+ import { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope';
6
+ import { useZoraTheme } from '../../../theme/composition/useZoraTheme';
7
+ import { projectGridRulerPosition } from '../../application/projectGridRulerPosition';
8
+ import { resolveGridRulerMarks } from '../../application/resolveGridRulerMarks';
9
+
10
+ /*** Renders visible world-grid lines and explicit guides without owning viewport interaction. */
11
+ function GridLineOverlayInner({
12
+ accessibilityLabel = 'Grid guides',
13
+ direction = 'ltr',
14
+ guides = [],
15
+ testID,
16
+ viewport,
17
+ xTickSource,
18
+ yTickSource,
19
+ }: GridLineOverlayProps) {
20
+ const { theme } = useZoraTheme();
21
+ const xMarks = xTickSource
22
+ ? resolveGridRulerMarks(viewport, 'x', xTickSource, undefined, direction)
23
+ : [];
24
+ const yMarks = yTickSource ? resolveGridRulerMarks(viewport, 'y', yTickSource) : [];
25
+ const visibleGuides = resolveVisibleGridGuides(guides, viewport, direction);
26
+
27
+ return (
28
+ <View
29
+ accessible={false}
30
+ importantForAccessibility="no"
31
+ pointerEvents="none"
32
+ style={[styles.overlay, { height: viewport.height, width: viewport.width }]}
33
+ testID={testID}
34
+ >
35
+ {xMarks.map((mark, index) => (
36
+ <View
37
+ key={`x-${mark.position}-${index}`}
38
+ importantForAccessibility="no-hide-descendants"
39
+ style={[
40
+ styles.vertical,
41
+ {
42
+ backgroundColor: theme.semantics.neutral.divider,
43
+ left: mark.position,
44
+ opacity: mark.level === 'major' ? 0.5 : 0.25,
45
+ },
46
+ ]}
47
+ testID={testID === undefined ? undefined : `${testID}-x-line-${index}`}
48
+ />
49
+ ))}
50
+ {yMarks.map((mark, index) => (
51
+ <View
52
+ key={`y-${mark.position}-${index}`}
53
+ importantForAccessibility="no-hide-descendants"
54
+ style={[
55
+ styles.horizontal,
56
+ {
57
+ backgroundColor: theme.semantics.neutral.divider,
58
+ opacity: mark.level === 'major' ? 0.5 : 0.25,
59
+ top: mark.position,
60
+ },
61
+ ]}
62
+ testID={testID === undefined ? undefined : `${testID}-y-line-${index}`}
63
+ />
64
+ ))}
65
+ {visibleGuides.map(({ guide, position }) => (
66
+ <View
67
+ key={guide.id}
68
+ accessibilityLabel={createGuideAccessibilityLabel(accessibilityLabel, guide)}
69
+ accessibilityRole="text"
70
+ accessible
71
+ style={[
72
+ guide.axis === 'x' ? styles.vertical : styles.horizontal,
73
+ guide.axis === 'x'
74
+ ? {
75
+ backgroundColor: theme.colors.primary,
76
+ left: position,
77
+ }
78
+ : {
79
+ backgroundColor: theme.colors.primary,
80
+ top: position,
81
+ },
82
+ ]}
83
+ testID={testID === undefined ? undefined : `${testID}-guide-${guide.id}`}
84
+ />
85
+ ))}
86
+ </View>
87
+ );
88
+ }
89
+
90
+ const styles = StyleSheet.create({
91
+ horizontal: { height: 1, left: 0, position: 'absolute', right: 0 },
92
+ overlay: { overflow: 'hidden', position: 'absolute' },
93
+ vertical: { bottom: 0, position: 'absolute', top: 0, width: 1 },
94
+ });
95
+
96
+ /*** Selects finite guides whose shared world-to-visual projection falls inside the viewport. */
97
+ function resolveVisibleGridGuides(
98
+ guides: NonNullable<GridLineOverlayProps['guides']>,
99
+ viewport: GridLineOverlayProps['viewport'],
100
+ direction: NonNullable<GridLineOverlayProps['direction']>,
101
+ ) {
102
+ return guides.flatMap((guide) => {
103
+ if (!Number.isFinite(guide.position)) return [];
104
+
105
+ const position = projectGridRulerPosition(viewport, guide.axis, guide.position, direction);
106
+ const size = guide.axis === 'x' ? viewport.width : viewport.height;
107
+
108
+ return Number.isFinite(position) && position >= 0 && position <= size
109
+ ? [{ guide, position }]
110
+ : [];
111
+ });
112
+ }
113
+
114
+ /*** Gives each visible guide an independent screen-reader identity with its stable world position. */
115
+ function createGuideAccessibilityLabel(
116
+ overlayLabel: string,
117
+ guide: NonNullable<GridLineOverlayProps['guides']>[number],
118
+ ): string {
119
+ return `${overlayLabel}: ${guide.label ?? `Guide ${guide.id}`} at ${guide.position}`;
120
+ }
121
+
122
+ /*** A reusable passive world-space grid and guide overlay. */
123
+ export const GridLineOverlay = withZoraThemeScope(GridLineOverlayInner);
@@ -0,0 +1,96 @@
1
+ import React from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+
4
+ import type { GridRulerProps } from '../../../../types/grid-rulers';
5
+ import { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope';
6
+ import { useZoraTheme } from '../../../theme/composition/useZoraTheme';
7
+ import { resolveGridRulerMarks } from '../../application/resolveGridRulerMarks';
8
+
9
+ /*** Renders a passive axis ruler from published grid-view tick geometry. */
10
+ function GridRulerInner({
11
+ accessibilityLabel,
12
+ axis,
13
+ direction = 'ltr',
14
+ formatLabel,
15
+ offset = 0,
16
+ position = 'start',
17
+ testID,
18
+ thickness = 24,
19
+ tickSource,
20
+ viewport,
21
+ }: GridRulerProps) {
22
+ const { theme } = useZoraTheme();
23
+ const marks = resolveGridRulerMarks(viewport, axis, tickSource, formatLabel, direction);
24
+ const horizontal = axis === 'x';
25
+ const size = horizontal ? viewport.width : viewport.height;
26
+ const label = accessibilityLabel ?? `${horizontal ? 'Horizontal' : 'Vertical'} ruler`;
27
+
28
+ return (
29
+ <View
30
+ accessibilityLabel={label}
31
+ accessible
32
+ pointerEvents="none"
33
+ style={[
34
+ styles.ruler,
35
+ horizontal ? { height: thickness, width: size } : { height: size, width: thickness },
36
+ position === 'start'
37
+ ? horizontal
38
+ ? { top: offset }
39
+ : { left: offset }
40
+ : horizontal
41
+ ? { bottom: offset }
42
+ : { right: offset },
43
+ ]}
44
+ testID={testID}
45
+ >
46
+ {marks.map((mark, index) => {
47
+ const coordinate = mark.position;
48
+ const tickStyle = horizontal
49
+ ? {
50
+ height: mark.level === 'major' ? thickness : thickness / 2,
51
+ left: coordinate,
52
+ top: 0,
53
+ width: 1,
54
+ }
55
+ : {
56
+ height: 1,
57
+ left: 0,
58
+ top: coordinate,
59
+ width: mark.level === 'major' ? thickness : thickness / 2,
60
+ };
61
+ return (
62
+ <React.Fragment key={`${mark.position}-${index}`}>
63
+ <View
64
+ importantForAccessibility="no-hide-descendants"
65
+ style={[styles.tick, { backgroundColor: theme.semantics.neutral.divider }, tickStyle]}
66
+ testID={testID === undefined ? undefined : `${testID}-tick-${index}`}
67
+ />
68
+ {mark.label !== undefined ? (
69
+ <Text
70
+ importantForAccessibility="no-hide-descendants"
71
+ numberOfLines={1}
72
+ style={[
73
+ styles.label,
74
+ { color: theme.semantics.neutral.textMuted },
75
+ horizontal ? { left: coordinate + 3, top: 2 } : { left: 3, top: coordinate + 2 },
76
+ ]}
77
+ testID={testID === undefined ? undefined : `${testID}-label-${index}`}
78
+ >
79
+ {mark.label}
80
+ </Text>
81
+ ) : null}
82
+ </React.Fragment>
83
+ );
84
+ })}
85
+ </View>
86
+ );
87
+ }
88
+
89
+ const styles = StyleSheet.create({
90
+ label: { fontSize: 10, position: 'absolute' },
91
+ ruler: { overflow: 'hidden', position: 'absolute' },
92
+ tick: { position: 'absolute' },
93
+ });
94
+
95
+ /*** A reusable passive horizontal or vertical world-space ruler. */
96
+ export const GridRuler = withZoraThemeScope(GridRulerInner);
@@ -0,0 +1,162 @@
1
+ import { join } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+
4
+ import type { GridViewport } from '@ankhorage/grid-view';
5
+ import { expect, test } from 'bun:test';
6
+ import { Window } from 'happy-dom';
7
+ import React from 'react';
8
+ import { renderToStaticMarkup } from 'react-dom/server';
9
+
10
+ import type { GridLineOverlayProps, GridRulerProps } from '../../../../types/grid-rulers';
11
+ import type { ZoraProviderProps } from '../../../theme/adapters/inbound/ZoraProvider';
12
+
13
+ const webDistRoot = join(import.meta.dir, '../../../../../web-dist');
14
+
15
+ /*** Loads the independently bundled React Native Web implementation under test. */
16
+ function load(path: string) {
17
+ return import(pathToFileURL(join(webDistRoot, path)).href);
18
+ }
19
+
20
+ const viewport: GridViewport = {
21
+ height: 100,
22
+ offsetX: 40,
23
+ offsetY: 20,
24
+ pixelsPerUnitX: 2,
25
+ pixelsPerUnitY: 2,
26
+ width: 300,
27
+ };
28
+
29
+ const xTickSource = { kind: 'ticks' as const, specification: { mode: 'fixed' as const, step: 20 } };
30
+
31
+ /*** Renders the public native components through React Native Web for behavioral DOM assertions. */
32
+ function renderGridRulers(
33
+ ZoraProvider: React.ComponentType<ZoraProviderProps>,
34
+ GridRuler: React.ComponentType<GridRulerProps>,
35
+ GridLineOverlay: React.ComponentType<GridLineOverlayProps>,
36
+ direction: 'ltr' | 'rtl',
37
+ guides = [{ axis: 'x' as const, id: 'playhead', label: 'Playhead', position: 60 }],
38
+ ) {
39
+ return renderToStaticMarkup(
40
+ <ZoraProvider mode="light">
41
+ <GridRuler
42
+ axis="x"
43
+ direction={direction}
44
+ testID="ruler"
45
+ tickSource={xTickSource}
46
+ viewport={viewport}
47
+ />
48
+ <GridLineOverlay
49
+ direction={direction}
50
+ guides={guides}
51
+ testID="overlay"
52
+ viewport={viewport}
53
+ xTickSource={xTickSource}
54
+ />
55
+ </ZoraProvider>,
56
+ );
57
+ }
58
+
59
+ /*** Reads React Native Web output without relying on implementation-only geometry helpers. */
60
+ function inspectMarkup(markup: string): Window {
61
+ const browserWindow = new Window();
62
+ browserWindow.document.body.innerHTML = markup;
63
+ return browserWindow;
64
+ }
65
+
66
+ test('renders ruler ticks, grid lines, and guides at the same non-zero-offset world projection in LTR and RTL', async () => {
67
+ const { ZoraProvider } = (await load('runtime/ZoraProvider.js')) as {
68
+ ZoraProvider: React.ComponentType<ZoraProviderProps>;
69
+ };
70
+ const { GridRuler } = (await load('components/grid-ruler/GridRuler.js')) as {
71
+ GridRuler: React.ComponentType<GridRulerProps>;
72
+ };
73
+ const { GridLineOverlay } = (await load('components/grid-line-overlay/GridLineOverlay.js')) as {
74
+ GridLineOverlay: React.ComponentType<GridLineOverlayProps>;
75
+ };
76
+ const ltr = inspectMarkup(renderGridRulers(ZoraProvider, GridRuler, GridLineOverlay, 'ltr'));
77
+ const rtl = inspectMarkup(renderGridRulers(ZoraProvider, GridRuler, GridLineOverlay, 'rtl'));
78
+
79
+ try {
80
+ for (const [browserWindow, expectedPosition] of [
81
+ [ltr, '40px'],
82
+ [rtl, '260px'],
83
+ ] as const) {
84
+ expect(
85
+ browserWindow.document.querySelector('[data-testid="ruler-tick-1"]')?.getAttribute('style'),
86
+ ).toContain(`left:${expectedPosition}`);
87
+ expect(
88
+ browserWindow.document
89
+ .querySelector('[data-testid="overlay-x-line-1"]')
90
+ ?.getAttribute('style'),
91
+ ).toContain(`left:${expectedPosition}`);
92
+ expect(
93
+ browserWindow.document
94
+ .querySelector('[data-testid="overlay-guide-playhead"]')
95
+ ?.getAttribute('style'),
96
+ ).toContain(`left:${expectedPosition}`);
97
+ }
98
+ } finally {
99
+ ltr.close();
100
+ rtl.close();
101
+ }
102
+ });
103
+
104
+ test('renders only finite visible guides and exposes each guide without making grid decoration navigable', async () => {
105
+ const { ZoraProvider } = (await load('runtime/ZoraProvider.js')) as {
106
+ ZoraProvider: React.ComponentType<ZoraProviderProps>;
107
+ };
108
+ const { GridRuler } = (await load('components/grid-ruler/GridRuler.js')) as {
109
+ GridRuler: React.ComponentType<GridRulerProps>;
110
+ };
111
+ const { GridLineOverlay } = (await load('components/grid-line-overlay/GridLineOverlay.js')) as {
112
+ GridLineOverlay: React.ComponentType<GridLineOverlayProps>;
113
+ };
114
+ const browserWindow = inspectMarkup(
115
+ renderGridRulers(ZoraProvider, GridRuler, GridLineOverlay, 'ltr', [
116
+ { axis: 'x', id: 'before', position: 39 },
117
+ { axis: 'x', id: 'start', label: 'Start', position: 40 },
118
+ { axis: 'x', id: 'end', label: 'End', position: 190 },
119
+ { axis: 'x', id: 'after', position: 191 },
120
+ { axis: 'y', id: 'invalid', position: Number.POSITIVE_INFINITY },
121
+ ]),
122
+ );
123
+
124
+ try {
125
+ expect(browserWindow.document.querySelectorAll('[data-testid^="overlay-guide-"]')).toHaveLength(
126
+ 2,
127
+ );
128
+ expect(
129
+ browserWindow.document.querySelector('[data-testid="overlay-guide-start"]'),
130
+ ).not.toBeNull();
131
+ expect(
132
+ browserWindow.document.querySelector('[data-testid="overlay-guide-end"]'),
133
+ ).not.toBeNull();
134
+ expect(browserWindow.document.querySelector('[data-testid="overlay-guide-before"]')).toBeNull();
135
+ expect(browserWindow.document.querySelector('[data-testid="overlay-guide-after"]')).toBeNull();
136
+ expect(
137
+ browserWindow.document.querySelector('[data-testid="overlay"]')?.getAttribute('aria-label'),
138
+ ).toBeNull();
139
+ expect(
140
+ browserWindow.document
141
+ .querySelector('[data-testid="overlay-guide-start"]')
142
+ ?.getAttribute('aria-label'),
143
+ ).toBe('Grid guides: Start at 40');
144
+ expect(
145
+ browserWindow.document
146
+ .querySelector('[data-testid="overlay-guide-end"]')
147
+ ?.getAttribute('aria-label'),
148
+ ).toBe('Grid guides: End at 190');
149
+ expect(
150
+ browserWindow.document
151
+ .querySelector('[data-testid="overlay-x-line-1"]')
152
+ ?.getAttribute('aria-label'),
153
+ ).toBeNull();
154
+ expect(
155
+ browserWindow.document
156
+ .querySelector('[data-testid="overlay-x-line-1"]')
157
+ ?.getAttribute('role'),
158
+ ).toBeNull();
159
+ } finally {
160
+ browserWindow.close();
161
+ }
162
+ });
@@ -0,0 +1,19 @@
1
+ import { type GridAxisName, type GridViewport, worldToViewport } from '@ankhorage/grid-view';
2
+
3
+ /*** Projects a stable world-axis coordinate into the matching visual ruler or overlay position. */
4
+ export function projectGridRulerPosition(
5
+ viewport: GridViewport,
6
+ axis: GridAxisName,
7
+ position: number,
8
+ direction: 'ltr' | 'rtl' = 'ltr',
9
+ ): number {
10
+ const point = worldToViewport(
11
+ axis === 'x' ? { x: position, y: viewport.offsetY } : { x: viewport.offsetX, y: position },
12
+ viewport,
13
+ );
14
+ const projectedPosition = axis === 'x' ? point.x : point.y;
15
+
16
+ return axis === 'x' && direction === 'rtl'
17
+ ? viewport.width - projectedPosition
18
+ : projectedPosition;
19
+ }
@@ -0,0 +1,90 @@
1
+ import { type GridViewport, snapWorldCoordinate } from '@ankhorage/grid-view';
2
+ import { describe, expect, test } from 'bun:test';
3
+
4
+ import { resolveGridRulerMarks } from './resolveGridRulerMarks';
5
+
6
+ const viewport: GridViewport = {
7
+ width: 300,
8
+ height: 160,
9
+ offsetX: 40,
10
+ offsetY: 20,
11
+ pixelsPerUnitX: 2,
12
+ pixelsPerUnitY: 4,
13
+ };
14
+
15
+ describe('resolveGridRulerMarks', () => {
16
+ test('aligns independent axis scales and scroll offsets through the published viewport transform', () => {
17
+ expect(
18
+ resolveGridRulerMarks(viewport, 'x', {
19
+ kind: 'ticks',
20
+ specification: { mode: 'fixed', step: 20 },
21
+ }).map((mark) => mark.position),
22
+ ).toEqual([0, 40, 80, 120, 160, 200, 240, 280]);
23
+ expect(
24
+ resolveGridRulerMarks(viewport, 'y', {
25
+ kind: 'ticks',
26
+ specification: { mode: 'fixed', step: 10 },
27
+ }).map((mark) => mark.position),
28
+ ).toEqual([0, 40, 80, 120, 160]);
29
+ });
30
+
31
+ test('projects only visible variable-width category starts', () => {
32
+ expect(
33
+ resolveGridRulerMarks(viewport, 'x', {
34
+ kind: 'categories',
35
+ categories: [
36
+ { id: 'before', size: 10, start: 20 },
37
+ { id: 'visible-a', size: 37, start: 40 },
38
+ { id: 'visible-b', size: 91, start: 77 },
39
+ ],
40
+ }),
41
+ ).toEqual([
42
+ { categoryId: 'visible-a', level: 'major', position: 0 },
43
+ { categoryId: 'visible-b', level: 'major', position: 74 },
44
+ ]);
45
+ });
46
+
47
+ test('keeps a fixed interaction snap unchanged when display density changes', () => {
48
+ const snap = { mode: 'fixed' as const, step: 25 };
49
+ const sparse = resolveGridRulerMarks(viewport, 'x', {
50
+ kind: 'ticks',
51
+ specification: { mode: 'fixed', step: 50 },
52
+ });
53
+ const dense = resolveGridRulerMarks(viewport, 'x', {
54
+ kind: 'ticks',
55
+ specification: { mode: 'fixed', step: 5 },
56
+ });
57
+
58
+ expect(dense.length).toBeGreaterThan(sparse.length);
59
+ expect(snapWorldCoordinate(62, snap)).toBe(50);
60
+ });
61
+
62
+ test('accepts externally supplied labels without introducing domain adapters', () => {
63
+ expect(
64
+ resolveGridRulerMarks(
65
+ viewport,
66
+ 'x',
67
+ { kind: 'ticks', specification: { mode: 'fixed', step: 20 } },
68
+ (tick) => `bar ${tick.position}`,
69
+ )[0],
70
+ ).toEqual({ label: 'bar 40', level: 'major', position: 0 });
71
+ });
72
+
73
+ test('projects horizontal marks consistently in RTL and evaluates each formatter once', () => {
74
+ let formatterCalls = 0;
75
+
76
+ const marks = resolveGridRulerMarks(
77
+ viewport,
78
+ 'x',
79
+ { kind: 'ticks', specification: { mode: 'fixed', step: 20 } },
80
+ (tick) => {
81
+ formatterCalls += 1;
82
+ return `tick ${tick.position}`;
83
+ },
84
+ 'rtl',
85
+ );
86
+
87
+ expect(marks.map((mark) => mark.position)).toEqual([300, 260, 220, 180, 140, 100, 60, 20]);
88
+ expect(formatterCalls).toBe(marks.length);
89
+ });
90
+ });
@@ -0,0 +1,39 @@
1
+ import {
2
+ getAxisCategoryTicks,
3
+ getAxisTicks,
4
+ type GridAxisName,
5
+ type GridAxisTick,
6
+ type GridViewport,
7
+ } from '@ankhorage/grid-view';
8
+
9
+ import type { GridRulerMark, GridRulerTickSource } from '../../../types/grid-rulers';
10
+ import { projectGridRulerPosition } from './projectGridRulerPosition';
11
+
12
+ /*** Resolves bounded engine ticks into viewport-pixel ruler marks without changing interaction state. */
13
+ export function resolveGridRulerMarks(
14
+ viewport: GridViewport,
15
+ axis: GridAxisName,
16
+ tickSource: GridRulerTickSource,
17
+ formatLabel?: (tick: GridAxisTick) => string | undefined,
18
+ direction: 'ltr' | 'rtl' = 'ltr',
19
+ ): readonly GridRulerMark[] {
20
+ const ticks =
21
+ tickSource.kind === 'categories'
22
+ ? getAxisCategoryTicks(viewport, axis, tickSource.categories)
23
+ : getAxisTicks(viewport, axis, tickSource.specification, tickSource.provider);
24
+
25
+ return ticks.map((tick) => {
26
+ const formattedLabel = formatLabel?.(tick);
27
+
28
+ return {
29
+ position: projectGridRulerPosition(viewport, axis, tick.position, direction),
30
+ level: tick.level,
31
+ ...(tick.label !== undefined || formattedLabel !== undefined
32
+ ? { label: formattedLabel ?? tick.label }
33
+ : {}),
34
+ ...('categoryId' in tick && typeof tick.categoryId === 'string'
35
+ ? { categoryId: tick.categoryId }
36
+ : {}),
37
+ };
38
+ });
39
+ }
@@ -0,0 +1,26 @@
1
+ import type { ZoraComponentMeta } from '../../types/authoring';
2
+
3
+ const GRID_RULERS_NOTE =
4
+ 'Requires a live viewport and optional executable tick/label providers; available through code APIs, not a serializable manifest node.';
5
+
6
+ /** Passive viewport decorations intentionally remain outside direct manifest authoring. */
7
+ export const gridLineOverlayMeta = {
8
+ name: 'GridLineOverlay',
9
+ category: 'foundation',
10
+ description: 'Projects bounded world-grid lines and explicit guides from a supplied viewport.',
11
+ directManifestNode: false,
12
+ note: GRID_RULERS_NOTE,
13
+ allowedChildren: [],
14
+ props: {},
15
+ } as const satisfies ZoraComponentMeta;
16
+
17
+ /** Rulers accept code-dependent tick and label providers and therefore remain code-only. */
18
+ export const gridRulerMeta = {
19
+ name: 'GridRuler',
20
+ category: 'foundation',
21
+ description: 'Projects visible world-axis ticks into a passive horizontal or vertical ruler.',
22
+ directManifestNode: false,
23
+ note: GRID_RULERS_NOTE,
24
+ allowedChildren: [],
25
+ props: {},
26
+ } as const satisfies ZoraComponentMeta;
@@ -0,0 +1,10 @@
1
+ export type {
2
+ GridGuide,
3
+ GridLineOverlayProps,
4
+ GridRulerMark,
5
+ GridRulerProps,
6
+ GridRulerTickSource,
7
+ } from '../../types/grid-rulers';
8
+ export { GridLineOverlay } from './adapters/inbound/GridLineOverlay';
9
+ export { GridRuler } from './adapters/inbound/GridRuler';
10
+ export { resolveGridRulerMarks } from './application/resolveGridRulerMarks';
@@ -47,6 +47,7 @@ import {
47
47
  GameOverlay,
48
48
  } from '../game/public';
49
49
  import { Gradient } from '../gradient/public';
50
+ import { GridLineOverlay, GridRuler } from '../grid-rulers/public';
50
51
  import { GridView, TileGrid } from '../grid-view/public';
51
52
  import { Hero } from '../hero/public';
52
53
  import { Icon } from '../icon/public';
@@ -65,6 +66,7 @@ import { BarcodeScannerView, CameraPermissionView, ScanOverlay } from '../scanne
65
66
  import { ScreenSection, SectionHeader } from '../section/public';
66
67
  import { SelectableItem } from '../selection/public';
67
68
  import { Skeleton, SkeletonCard, SkeletonList, SkeletonText } from '../skeleton/public';
69
+ import { SpatialGrid } from '../spatial-grid/public';
68
70
  import { Surface } from '../surface/public';
69
71
  import {
70
72
  CardBack,
@@ -194,6 +196,8 @@ const _ZORA_COMPONENT_REGISTRY = {
194
196
  EmptyState,
195
197
  Explorer,
196
198
  FileExplorer,
199
+ GridLineOverlay,
200
+ GridRuler,
197
201
  GridView,
198
202
  TileGrid,
199
203
  MediaExplorer,
@@ -212,6 +216,7 @@ const _ZORA_COMPONENT_REGISTRY = {
212
216
  ScanOverlay,
213
217
  SectionHeader,
214
218
  SelectableItem,
219
+ SpatialGrid,
215
220
  PaletteItem,
216
221
  CardBack,
217
222
  CardHand,
@@ -53,6 +53,7 @@ import { gameMeasurementProbeMeta } from '../game/meta/gameMeasurementProbeMeta'
53
53
  import { gameMeta } from '../game/meta/gameMeta';
54
54
  import { gameOverlayMeta } from '../game/meta/gameOverlayMeta';
55
55
  import { gradientMeta } from '../gradient/gradientMeta';
56
+ import { gridLineOverlayMeta, gridRulerMeta } from '../grid-rulers/meta';
56
57
  import { gridViewMeta, tileGridMeta } from '../grid-view/meta';
57
58
  import { heroMeta } from '../hero/heroMeta';
58
59
  import { iconMeta } from '../icon/iconMeta';
@@ -84,6 +85,7 @@ import {
84
85
  skeletonMeta,
85
86
  skeletonTextMeta,
86
87
  } from '../skeleton/skeletonMeta';
88
+ import { spatialGridMeta } from '../spatial-grid/meta';
87
89
  import { surfaceMeta } from '../surface/surfaceMeta';
88
90
  import {
89
91
  cardBackMeta,
@@ -198,6 +200,8 @@ export const ZORA_COMPONENT_META: ZoraComponentMetaRegistry = finalizeFeatureMet
198
200
  Explorer: explorerMeta,
199
201
  FileExplorer: fileExplorerMeta,
200
202
  MediaExplorer: mediaExplorerMeta,
203
+ GridLineOverlay: gridLineOverlayMeta,
204
+ GridRuler: gridRulerMeta,
201
205
  GridView: gridViewMeta,
202
206
  TileGrid: tileGridMeta,
203
207
  Hero: heroMeta,
@@ -215,6 +219,7 @@ export const ZORA_COMPONENT_META: ZoraComponentMetaRegistry = finalizeFeatureMet
215
219
  ScanOverlay: scanOverlayMeta,
216
220
  SectionHeader: sectionHeaderMeta,
217
221
  SelectableItem: selectableItemMeta,
222
+ SpatialGrid: spatialGridMeta,
218
223
  SelectionProvider: selectionProviderMeta,
219
224
  PaletteItem: paletteItemMeta,
220
225
  Timeline: timelineMeta,