@octane-xplat/ui 0.10.0 → 0.11.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 (219) hide show
  1. package/dist/native/Image.tsrx.js +151 -16
  2. package/dist/native/hash-image.js +285 -0
  3. package/dist/native/image-content-binding.js +158 -0
  4. package/dist/native/image-content.js +206 -0
  5. package/dist/native/image-sizing.js +102 -0
  6. package/dist/native/octane-client-build.json +1 -1
  7. package/dist/native/vendor/ui-svg/index.android.js +1 -1
  8. package/dist/web/Image.web.tsrx.js +112 -19
  9. package/dist/web/hash-image.js +285 -0
  10. package/dist/web/image-content.js +133 -0
  11. package/dist/web/octane-client-build.json +1 -1
  12. package/package.json +3 -2
  13. package/src/Image.macos.tsrx +11 -2
  14. package/src/Image.tsrx +203 -17
  15. package/src/Image.web.tsrx +91 -5
  16. package/src/Tabs.macos.tsrx +2 -0
  17. package/src/Timestamp.macos.tsrx +1 -1
  18. package/src/hash-image.test.ts +72 -0
  19. package/src/hash-image.ts +310 -0
  20. package/src/hosted-unsupported.macos.tsrx +1 -2
  21. package/src/image-content-binding.mobile.test.ts +174 -0
  22. package/src/image-content-binding.ts +231 -0
  23. package/src/image-content.mobile.test.tsrx +84 -0
  24. package/src/image-content.test.ts +219 -0
  25. package/src/image-content.ts +309 -0
  26. package/src/image-decoding.web.test.tsrx +26 -0
  27. package/src/image-placeholder.mobile.test.tsrx +113 -0
  28. package/src/image-placeholder.web.test.tsrx +48 -0
  29. package/src/image-recycling.mobile.test.ts +61 -0
  30. package/src/image-sizing.mobile.test.ts +158 -0
  31. package/src/image-sizing.ts +117 -0
  32. package/src/image.mobile.test.tsrx +81 -0
  33. package/src/index.macos.ts +1 -0
  34. package/src/index.shared.ts +7 -0
  35. package/src/index.shared.web.ts +7 -0
  36. package/src/list-viewability-observer.ios.ts +1 -0
  37. package/src/list-viewability.mobile.test.ts +1 -0
  38. package/src/props.ts +100 -1
  39. package/src/safeAreaInsets.macos.tsrx +6 -1
  40. package/src/styled.macos.tsrx +1 -1
  41. package/src/vendor/ui-svg/src/index.android.ts +5 -1
  42. package/types/generated/.tsrx-typegen-manifest.json +170 -0
  43. package/types/generated/Accordion.macos.d.ts +5 -0
  44. package/types/generated/Alert.macos.d.ts +3 -0
  45. package/types/generated/AlertDialog.macos.d.ts +4 -0
  46. package/types/generated/AppShell.macos.d.ts +7 -0
  47. package/types/generated/AspectRatio.macos.d.ts +5 -0
  48. package/types/generated/Avatar.macos.d.ts +6 -0
  49. package/types/generated/AvatarGroup.macos.d.ts +6 -0
  50. package/types/generated/Badge.macos.d.ts +5 -0
  51. package/types/generated/Banner.macos.d.ts +4 -0
  52. package/types/generated/BaseTypeahead.macos.d.ts +7 -0
  53. package/types/generated/Blockquote.macos.d.ts +6 -0
  54. package/types/generated/BottomSheet.macos.d.ts +3 -0
  55. package/types/generated/Breadcrumb.macos.d.ts +4 -0
  56. package/types/generated/ButtonGroup.macos.d.ts +3 -0
  57. package/types/generated/Calendar.macos.d.ts +5 -0
  58. package/types/generated/Card.macos.d.ts +4 -0
  59. package/types/generated/Carousel.macos.d.ts +6 -0
  60. package/types/generated/Center.macos.d.ts +4 -0
  61. package/types/generated/CheckIndicator.macos.d.ts +4 -0
  62. package/types/generated/CheckboxGroup.macos.d.ts +3 -0
  63. package/types/generated/CheckboxIndicator.macos.d.ts +4 -0
  64. package/types/generated/Chip.macos.d.ts +5 -0
  65. package/types/generated/Citation.macos.d.ts +4 -0
  66. package/types/generated/ClickableCard.macos.d.ts +3 -0
  67. package/types/generated/Code.macos.d.ts +5 -0
  68. package/types/generated/CodeBlock.macos.d.ts +6 -0
  69. package/types/generated/Collapsible.macos.d.ts +5 -0
  70. package/types/generated/CommandPalette.macos.d.ts +3 -0
  71. package/types/generated/ComplexSelector.macos.d.ts +3 -0
  72. package/types/generated/ContextMenu.macos.d.ts +5 -0
  73. package/types/generated/DateInput.macos.d.ts +6 -0
  74. package/types/generated/DateRangeInput.macos.d.ts +5 -0
  75. package/types/generated/DateTimeInput.macos.d.ts +5 -0
  76. package/types/generated/Dialog.macos.d.ts +3 -0
  77. package/types/generated/Drawer.macos.d.ts +7 -0
  78. package/types/generated/DropdownMenu.macos.d.ts +5 -0
  79. package/types/generated/Empty.macos.d.ts +4 -0
  80. package/types/generated/FieldGroup.macos.d.ts +4 -0
  81. package/types/generated/FormField.macos.d.ts +4 -0
  82. package/types/generated/FormLayout.macos.d.ts +6 -0
  83. package/types/generated/HStack.macos.d.ts +4 -0
  84. package/types/generated/Heading.macos.d.ts +3 -0
  85. package/types/generated/Icon.macos.d.ts +3 -0
  86. package/types/generated/IconButton.macos.d.ts +3 -0
  87. package/types/generated/Image.d.ts +0 -5
  88. package/types/generated/Image.macos.d.ts +2 -0
  89. package/types/generated/Image.web.d.ts +12 -1
  90. package/types/generated/InputGroup.macos.d.ts +5 -0
  91. package/types/generated/InputNumber.macos.d.ts +5 -0
  92. package/types/generated/InputRating.macos.d.ts +4 -0
  93. package/types/generated/Item.macos.d.ts +3 -0
  94. package/types/generated/Kbd.macos.d.ts +4 -0
  95. package/types/generated/KeyboardAvoiding.macos.d.ts +3 -0
  96. package/types/generated/Lightbox.macos.d.ts +2 -0
  97. package/types/generated/Link.macos.d.ts +3 -0
  98. package/types/generated/List.macos.d.ts +4 -0
  99. package/types/generated/ListItem.macos.d.ts +5 -0
  100. package/types/generated/MenuParts.macos.d.ts +9 -0
  101. package/types/generated/MenuSurface.macos.d.ts +4 -0
  102. package/types/generated/MetadataList.macos.d.ts +4 -0
  103. package/types/generated/MetadataListItem.macos.d.ts +3 -0
  104. package/types/generated/Meter.macos.d.ts +3 -0
  105. package/types/generated/MobileNav.macos.d.ts +15 -0
  106. package/types/generated/MoreMenu.macos.d.ts +2 -0
  107. package/types/generated/NavIcon.macos.d.ts +4 -0
  108. package/types/generated/NavLink.macos.d.ts +3 -0
  109. package/types/generated/NavMenu.macos.d.ts +11 -0
  110. package/types/generated/NavigationMenu.macos.d.ts +4 -0
  111. package/types/generated/Outline.macos.d.ts +5 -0
  112. package/types/generated/OverflowList.macos.d.ts +6 -0
  113. package/types/generated/Overlay.macos.d.ts +3 -0
  114. package/types/generated/Pagination.macos.d.ts +4 -0
  115. package/types/generated/PinInput.macos.d.ts +4 -0
  116. package/types/generated/Popover.macos.d.ts +3 -0
  117. package/types/generated/PowerSearch.macos.d.ts +7 -0
  118. package/types/generated/PowerSearchFilterEditor.macos.d.ts +12 -0
  119. package/types/generated/PowerSearchToken.macos.d.ts +7 -0
  120. package/types/generated/Pressable.macos.d.ts +2 -0
  121. package/types/generated/ProgressBar.macos.d.ts +6 -0
  122. package/types/generated/ProgressGroup.macos.d.ts +3 -0
  123. package/types/generated/RadioGroup.macos.d.ts +5 -0
  124. package/types/generated/RadioIndicator.macos.d.ts +3 -0
  125. package/types/generated/ResizeHandle.macos.d.ts +7 -0
  126. package/types/generated/RouteBoundary.macos.d.ts +8 -0
  127. package/types/generated/RouteHost.macos.d.ts +13 -0
  128. package/types/generated/Row.macos.d.ts +2 -0
  129. package/types/generated/SafeArea.macos.d.ts +3 -0
  130. package/types/generated/Screen.macos.d.ts +3 -0
  131. package/types/generated/ScrollableArea.macos.d.ts +7 -0
  132. package/types/generated/Section.macos.d.ts +5 -0
  133. package/types/generated/Select.macos.d.ts +3 -0
  134. package/types/generated/SelectableCard.macos.d.ts +3 -0
  135. package/types/generated/Separator.macos.d.ts +5 -0
  136. package/types/generated/SideNav.macos.d.ts +19 -0
  137. package/types/generated/Skeleton.macos.d.ts +5 -0
  138. package/types/generated/StatusDot.macos.d.ts +5 -0
  139. package/types/generated/Stepper.macos.d.ts +4 -0
  140. package/types/generated/TabList.macos.d.ts +18 -0
  141. package/types/generated/Table.macos.d.ts +4 -0
  142. package/types/generated/Tabs.macos.d.ts +3 -0
  143. package/types/generated/Text.macos.d.ts +5 -0
  144. package/types/generated/Thumbnail.macos.d.ts +7 -0
  145. package/types/generated/TimeInput.macos.d.ts +6 -0
  146. package/types/generated/TimeWheelPanel.macos.d.ts +16 -0
  147. package/types/generated/Timeline.macos.d.ts +3 -0
  148. package/types/generated/Timer.macos.d.ts +3 -0
  149. package/types/generated/Timestamp.macos.d.ts +5 -0
  150. package/types/generated/Toast.macos.d.ts +3 -0
  151. package/types/generated/ToggleButton.macos.d.ts +3 -0
  152. package/types/generated/Token.macos.d.ts +5 -0
  153. package/types/generated/Tokenizer.macos.d.ts +6 -0
  154. package/types/generated/Toolbar.macos.d.ts +6 -0
  155. package/types/generated/TopNav.macos.d.ts +25 -0
  156. package/types/generated/Tree.macos.d.ts +4 -0
  157. package/types/generated/Typeahead.macos.d.ts +8 -0
  158. package/types/generated/TypeaheadItem.macos.d.ts +5 -0
  159. package/types/generated/User.macos.d.ts +5 -0
  160. package/types/generated/VStack.macos.d.ts +4 -0
  161. package/types/generated/View.macos.d.ts +3 -0
  162. package/types/generated/VirtualList.macos.d.ts +6 -0
  163. package/types/generated/VisuallyHidden.macos.d.ts +5 -0
  164. package/types/generated/WebView.macos.d.ts +2 -0
  165. package/types/generated/aliases.macos.d.ts +3 -0
  166. package/types/generated/anim.macos.d.ts +3 -0
  167. package/types/generated/animation-host.macos.d.ts +3 -0
  168. package/types/generated/bottom-sheet-panel.macos.d.ts +7 -0
  169. package/types/generated/bottom-sheet-service.macos.d.ts +5 -0
  170. package/types/generated/clipboard-write.macos.d.ts +3 -0
  171. package/types/generated/colorScheme.macos.d.ts +4 -0
  172. package/types/generated/command-palette-keys.macos.d.ts +2 -0
  173. package/types/generated/command-palette-scroll.macos.d.ts +2 -0
  174. package/types/generated/controls.macos.d.ts +8 -0
  175. package/types/generated/date-composition.macos.d.ts +5 -0
  176. package/types/generated/escape-props.macos.d.ts +5 -0
  177. package/types/generated/hash-image.d.ts +13 -0
  178. package/types/generated/hosted-unsupported.macos.d.ts +1 -0
  179. package/types/generated/image-content-binding.d.ts +64 -0
  180. package/types/generated/image-content.d.ts +56 -0
  181. package/types/generated/image-sizing.d.ts +37 -0
  182. package/types/generated/index.macos.d.ts +196 -0
  183. package/types/generated/index.shared.d.ts +1 -1
  184. package/types/generated/index.shared.web.d.ts +1 -1
  185. package/types/generated/layout-shells.macos.d.ts +3 -0
  186. package/types/generated/layout.macos.d.ts +9 -0
  187. package/types/generated/macos-extras.macos.d.ts +4 -0
  188. package/types/generated/measure-bounds.macos.d.ts +3 -0
  189. package/types/generated/nav-href.macos.d.ts +4 -0
  190. package/types/generated/outline-dom.macos.d.ts +2 -0
  191. package/types/generated/pan.macos.d.ts +3 -0
  192. package/types/generated/platform.macos.d.ts +1 -0
  193. package/types/generated/presentation.macos.d.ts +15 -0
  194. package/types/generated/props.d.ts +76 -1
  195. package/types/generated/query-storage.macos.d.ts +8 -0
  196. package/types/generated/resize-basis.macos.d.ts +4 -0
  197. package/types/generated/resize-persist.macos.d.ts +8 -0
  198. package/types/generated/route.macos.d.ts +28 -0
  199. package/types/generated/safeAreaInsets.macos.d.ts +10 -0
  200. package/types/generated/stacks.macos.d.ts +3 -0
  201. package/types/generated/styled.macos.d.ts +9 -0
  202. package/types/generated/surfaces.macos.d.ts +4 -0
  203. package/types/generated/svg-image.macos.d.ts +3 -0
  204. package/types/generated/text-controls.macos.d.ts +4 -0
  205. package/types/generated/theme/theme-scheme.macos.d.ts +8 -0
  206. package/types/generated/toast-service.macos.d.ts +3 -0
  207. package/types/generated/toast-viewport.macos.d.ts +5 -0
  208. package/types/generated/translate.macos.d.ts +1 -0
  209. package/types/generated/unsupported.macos.d.ts +1 -0
  210. package/types/generated/use-imperative-alert-dialog.macos.d.ts +2 -0
  211. package/types/generated/use-imperative-dialog.macos.d.ts +2 -0
  212. package/types/generated/use-lightbox.macos.d.ts +2 -0
  213. package/types/generated/use-scrollable-area.macos.d.ts +6 -0
  214. package/types/generated/useMeasure.macos.d.ts +3 -0
  215. package/types/generated/useResizable.macos.d.ts +8 -0
  216. package/types/generated/useStore.macos.d.ts +2 -0
  217. package/types/generated/windows.macos.d.ts +2 -0
  218. package/src/CameraView.macos.tsrx +0 -6
  219. package/types/index.macos.d.ts +0 -448
@@ -0,0 +1,206 @@
1
+ //#region src/image-content.ts
2
+ /** Shared `contentFit`/`contentPosition` + multi-source math for `Image` —
3
+ * pure functions used by every leaf (docs/notes/expo-image-study.md L7/L8).
4
+ * Semantics mirror CSS `object-fit`/`object-position`, which is also
5
+ * expo-image's contract: the leaves compute the drawn-content rect
6
+ * themselves instead of delegating to platform scaleType/contentMode,
7
+ * so all platforms get the same answer.
8
+ *
9
+ * The default fit is 'cover' everywhere (the expo/RN default). Today's
10
+ * platform defaults disagree — web <img> is 'fill' and NS stretch is
11
+ * 'aspectFit' — so an un-propped Image was silently divergent. */
12
+ var DEFAULT_CONTENT_FIT = "cover";
13
+ function resolveContentFit(fit) {
14
+ return fit ?? "cover";
15
+ }
16
+ /** expo-image's keyword shorthands; the object form positions relative to
17
+ * any corner pair ({top,left}, {top,right}, {bottom,left}, {bottom,right}). */
18
+ var POSITION_KEYWORDS = {
19
+ center: {
20
+ top: "50%",
21
+ left: "50%"
22
+ },
23
+ top: {
24
+ top: 0,
25
+ left: "50%"
26
+ },
27
+ right: {
28
+ top: "50%",
29
+ right: 0
30
+ },
31
+ bottom: {
32
+ bottom: 0,
33
+ left: "50%"
34
+ },
35
+ left: {
36
+ top: "50%",
37
+ left: 0
38
+ },
39
+ "top center": {
40
+ top: 0,
41
+ left: "50%"
42
+ },
43
+ "top right": {
44
+ top: 0,
45
+ right: 0
46
+ },
47
+ "top left": {
48
+ top: 0,
49
+ left: 0
50
+ },
51
+ "right center": {
52
+ top: "50%",
53
+ right: 0
54
+ },
55
+ "right top": {
56
+ top: 0,
57
+ right: 0
58
+ },
59
+ "right bottom": {
60
+ bottom: 0,
61
+ right: 0
62
+ },
63
+ "bottom center": {
64
+ bottom: 0,
65
+ left: "50%"
66
+ },
67
+ "bottom right": {
68
+ bottom: 0,
69
+ right: 0
70
+ },
71
+ "bottom left": {
72
+ bottom: 0,
73
+ left: 0
74
+ },
75
+ "left center": {
76
+ top: "50%",
77
+ left: 0
78
+ },
79
+ "left top": {
80
+ top: 0,
81
+ left: 0
82
+ },
83
+ "left bottom": {
84
+ bottom: 0,
85
+ left: 0
86
+ }
87
+ };
88
+ function resolveContentPosition(position) {
89
+ if (typeof position === "string") return POSITION_KEYWORDS[position] ?? POSITION_KEYWORDS.center;
90
+ return position ?? POSITION_KEYWORDS.center;
91
+ }
92
+ function parseAxisValue(value) {
93
+ if (value == null) return null;
94
+ if (typeof value === "number") return { dips: value };
95
+ const text = String(value).trim();
96
+ if (text === "center") return { percent: 50 };
97
+ if (text.endsWith("%")) {
98
+ const parsed = Number.parseFloat(text);
99
+ return Number.isNaN(parsed) ? null : { percent: parsed };
100
+ }
101
+ const parsed = Number.parseFloat(text);
102
+ return Number.isNaN(parsed) ? null : { dips: parsed };
103
+ }
104
+ /** Offset of the drawn content's start edge inside the view along one axis.
105
+ * expo-image `calcTranslation`: percentages run over the free space
106
+ * (view − content), absolute dips measure from the named edge, and the
107
+ * reverse edge (right/bottom) counts back from the far side. */
108
+ function contentAxisOffset(viewSize, contentSize, start, end) {
109
+ const from = parseAxisValue(start);
110
+ if (from) return "percent" in from ? from.percent / 100 * (viewSize - contentSize) : from.dips;
111
+ const to = parseAxisValue(end);
112
+ if (to) return "percent" in to ? (1 - to.percent / 100) * (viewSize - contentSize) : viewSize - contentSize - to.dips;
113
+ return (viewSize - contentSize) / 2;
114
+ }
115
+ function centeredAxis(start, end) {
116
+ const from = parseAxisValue(start);
117
+ if (from) return "percent" in from && from.percent === 50;
118
+ const to = parseAxisValue(end);
119
+ if (to) return "percent" in to && to.percent === 50;
120
+ return true;
121
+ }
122
+ function isCenteredContentPosition(position) {
123
+ return centeredAxis(position.left, position.right) && centeredAxis(position.top, position.bottom);
124
+ }
125
+ /** The rect the bitmap occupies inside the view box, in the same units the
126
+ * caller measures with (native: dips; web: CSS px via object-fit anyway —
127
+ * only the native leaf calls this). Every length unit is dips except the
128
+ * intrinsic size, which the leaf must supply in dips too. */
129
+ function contentRect(fit, position, viewWidth, viewHeight, imageWidth, imageHeight) {
130
+ let width;
131
+ let height;
132
+ switch (fit) {
133
+ case "fill":
134
+ width = viewWidth;
135
+ height = viewHeight;
136
+ break;
137
+ case "cover": {
138
+ const scale = Math.max(viewWidth / imageWidth, viewHeight / imageHeight);
139
+ width = imageWidth * scale;
140
+ height = imageHeight * scale;
141
+ break;
142
+ }
143
+ case "scale-down": {
144
+ const scale = Math.min(1, Math.min(viewWidth / imageWidth, viewHeight / imageHeight));
145
+ width = imageWidth * scale;
146
+ height = imageHeight * scale;
147
+ break;
148
+ }
149
+ case "none":
150
+ width = imageWidth;
151
+ height = imageHeight;
152
+ break;
153
+ default: {
154
+ const scale = Math.min(viewWidth / imageWidth, viewHeight / imageHeight);
155
+ width = imageWidth * scale;
156
+ height = imageHeight * scale;
157
+ }
158
+ }
159
+ return {
160
+ x: contentAxisOffset(viewWidth, width, position.left, position.right),
161
+ y: contentAxisOffset(viewHeight, height, position.top, position.bottom),
162
+ width,
163
+ height
164
+ };
165
+ }
166
+ /** True when no platform stretch/scaleType can express the request and the
167
+ * leaf must measure + lay out the image itself: 'none' (iOS maps NS
168
+ * stretch='none' to TopLeft, not CSS centered), 'scale-down' (needs the
169
+ * intrinsic-vs-view compare), or any non-center position. 'fill' is
170
+ * exempt — its rect is always the whole box, so position can't matter. */
171
+ function needsContentRect(fit, position) {
172
+ if (fit === "none" || fit === "scale-down") return true;
173
+ return fit !== "fill" && !isCenteredContentPosition(position);
174
+ }
175
+ /** NS `stretch` for the fast path — only reached for fits the platform
176
+ * vocabulary expresses identically (contain/cover/fill centered). */
177
+ function contentFitToStretch(fit) {
178
+ switch (fit) {
179
+ case "contain": return "aspectFit";
180
+ case "fill": return "fill";
181
+ default: return "aspectFill";
182
+ }
183
+ }
184
+ /** L8 — expo `getBestSource`: the source whose declared pixel count
185
+ * (`width·height·scale²`, dips × density) is closest to the view's pixel
186
+ * count. Needs a real measured size; one source or no size picks nothing
187
+ * to decide. Unsized sources score worst and are skipped when any sized
188
+ * candidate exists. */
189
+ function bestImageSource(sources, viewWidth, viewHeight, screenScale) {
190
+ if (sources.length === 0 || viewWidth <= 0 || viewHeight <= 0) return null;
191
+ if (sources.length === 1) return sources[0];
192
+ const target = viewWidth * viewHeight * screenScale * screenScale;
193
+ let best = null;
194
+ let bestFit = Infinity;
195
+ for (const source of sources) {
196
+ const pixelCount = (source.width ?? 0) * (source.height ?? 0) * (source.scale ?? 1) ** 2;
197
+ const fit = Math.abs(1 - pixelCount / target);
198
+ if (fit < bestFit) {
199
+ best = source;
200
+ bestFit = fit;
201
+ }
202
+ }
203
+ return best;
204
+ }
205
+ //#endregion
206
+ export { DEFAULT_CONTENT_FIT, bestImageSource, contentAxisOffset, contentFitToStretch, contentRect, isCenteredContentPosition, needsContentRect, resolveContentFit, resolveContentPosition };
@@ -0,0 +1,102 @@
1
+ //#region src/image-sizing.ts
2
+ /** View-sized decode for the native <image> leaf.
3
+ *
4
+ * NS Android reads decodeWidth/decodeHeight only when `src` is written —
5
+ * `srcProperty.setNative` → `_createImageSourceFromSrc` →
6
+ * `imageView.setUri(value, decodeWidth, decodeHeight, …)` — and the
7
+ * properties default to 0 → `Fetcher.calculateInSampleSize` maps
8
+ * reqWidth ≤ 0 to the source dims → inSampleSize 1 → a full-resolution
9
+ * decode (~48 MB ARGB_8888 per 12 MP photo; docs/notes/expo-image-study.md
10
+ * L1). So the leaf holds `src` until the view's first real layout, writes
11
+ * the decode dims in device pixels, then assigns src. A later resize
12
+ * re-issues at the new size — the same reload expo-image runs from
13
+ * onSizeChanged — under the same gate: 'fill'/'none' stretches keep the
14
+ * existing decode because a resize can't change the ideal sample there
15
+ * ('none' additionally never downsamples — its contract is source
16
+ * pixels). iOS ignores the decode dims entirely; the deferred assignment
17
+ * is behavior-identical there, so one path serves both platforms.
18
+ *
19
+ * The ref callback re-fires on every render (identity change), so
20
+ * `update` must be idempotent: same view + same src + same size is a
21
+ * no-op. Null ref calls on detach are ignored by the leaf — re-attaching
22
+ * the same view must not lose `applied`, or each render would re-issue
23
+ * setUri. The dead-view listener left behind on real unmount is a
24
+ * self-contained cycle the GC collects with the component record.
25
+ *
26
+ * `afterIssue` runs right after every `src` write — including a same-src
27
+ * resize re-issue mid-load, which fires no `isLoadingChange` (isLoading is
28
+ * already true). The Image placeholder hooks this to re-apply its bitmap
29
+ * after the write has nulled `imageSource` (L5, decision #103). */
30
+ function createSizedImageBinding(options = {}) {
31
+ let view = null;
32
+ let listening = false;
33
+ let pendingSrc;
34
+ let pendingStretch;
35
+ let appliedSrc;
36
+ let applied = false;
37
+ let width = 0;
38
+ let height = 0;
39
+ const issue = (next) => {
40
+ const downscale = (pendingStretch ?? view.stretch) !== "none";
41
+ view.decodeWidth = {
42
+ value: downscale ? width : 0,
43
+ unit: "px"
44
+ };
45
+ view.decodeHeight = {
46
+ value: downscale ? height : 0,
47
+ unit: "px"
48
+ };
49
+ view.src = next;
50
+ appliedSrc = next;
51
+ applied = true;
52
+ options.afterIssue?.(view);
53
+ };
54
+ const evaluate = () => {
55
+ if (!view) return;
56
+ const measuredWidth = view.getMeasuredWidth?.() ?? 0;
57
+ const measuredHeight = view.getMeasuredHeight?.() ?? 0;
58
+ if (measuredWidth <= 0 || measuredHeight <= 0) return;
59
+ if (!applied || pendingSrc !== appliedSrc) {
60
+ width = measuredWidth;
61
+ height = measuredHeight;
62
+ issue(pendingSrc);
63
+ return;
64
+ }
65
+ if (measuredWidth === width && measuredHeight === height) return;
66
+ const stretch = pendingStretch ?? view.stretch;
67
+ if (stretch === "fill" || stretch === "none") return;
68
+ width = measuredWidth;
69
+ height = measuredHeight;
70
+ issue(appliedSrc);
71
+ };
72
+ const onLayout = () => evaluate();
73
+ return {
74
+ /** (Re)bind source → view; issues once the view reports a real layout.
75
+ * `stretchForDecode` overrides view.stretch for the decode/reload
76
+ * gates — see `issue`. */
77
+ update(next, source, stretchForDecode) {
78
+ if (next !== view) {
79
+ view?.off?.("layoutChanged", onLayout);
80
+ view = next;
81
+ listening = false;
82
+ applied = false;
83
+ appliedSrc = void 0;
84
+ }
85
+ pendingSrc = source;
86
+ pendingStretch = stretchForDecode;
87
+ if (view && !listening && typeof view.on === "function") {
88
+ view.on("layoutChanged", onLayout);
89
+ listening = true;
90
+ }
91
+ evaluate();
92
+ },
93
+ dispose() {
94
+ view?.off?.("layoutChanged", onLayout);
95
+ view = null;
96
+ listening = false;
97
+ applied = false;
98
+ }
99
+ };
100
+ }
101
+ //#endregion
102
+ export { createSizedImageBinding };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "buildId": "f7051bda-1e6f-4589-a0af-fabe255e93f2",
3
+ "buildId": "34e7eb98-53f0-4d28-9246-10c4851ef1b3",
4
4
  "mode": "production",
5
5
  "capabilities": {
6
6
  "independentHydration": false
@@ -30,7 +30,7 @@ function getSVG(src) {
30
30
  var SVGExternalFileResolver = class extends com.caverock.androidsvg.SVGExternalFileResolver {
31
31
  resolveFont(fontFamily, fontWeight, fontStyle) {
32
32
  if (fontFamily) fontFamily = fontFamily.replace(/\\\//, "/");
33
- return new Font(fontFamily, void 0, fontStyle.toLowerCase(), fontWeight + "").getAndroidTypeface();
33
+ return Font.default.withFontFamily(fontFamily).withFontStyle(fontStyle.toLowerCase()).withFontWeight(fontWeight + "").getAndroidTypeface();
34
34
  }
35
35
  resolveImage(filename) {
36
36
  let bitmap = null;
@@ -1,28 +1,76 @@
1
1
  import { applyEscapeProps } from "./escape-props.web.js";
2
- import { layoutChildProps } from "./layout-child.web.js";
3
- import { __s, bag8, clone, snapshotSpread, template } from "octane";
2
+ import { layoutChildProps, normStyle } from "./layout-child.web.js";
3
+ import { imageSrcSet, objectPositionCSSValue, resolveContentFit, resolveContentPosition } from "./image-content.js";
4
+ import { hashToPngBase64 } from "./hash-image.js";
5
+ import { __s, bag11, clone, hookSlots, snapshotSpread, template, useLayoutEffect, useRef } from "octane";
4
6
  import { bindSignalHostPropSources, enableSignalBindings } from "octane/internal/client";
5
7
  //#region src/Image.web.tsrx
6
8
  enableSignalBindings(1, true);
7
9
  var _t$0 = /* @__PURE__ */ template("<img/>");
10
+ var _hs$ = /* @__PURE__ */ hookSlots(2);
8
11
  var _rm$0 = [
9
12
  "s",
10
- "h",
11
- "g"
13
+ "k",
14
+ "j"
12
15
  ];
16
+ function placeholderUrl(placeholder) {
17
+ const png = hashToPngBase64(placeholder);
18
+ return png ? `data:image/png;base64,${png}` : placeholder;
19
+ }
13
20
  var Image = /* @__PURE__ */ __s(function Image(props, __s, __extra) {
21
+ const imgRef = useRef(null, _hs$ + 0);
22
+ const sources = Array.isArray(props.src) ? props.src : null;
23
+ const selected = sources ? imageSrcSet(sources) : {
24
+ src: props.src,
25
+ srcSet: void 0
26
+ };
27
+ useLayoutEffect(() => {
28
+ const img = imgRef.current;
29
+ if (!img || !props.placeholder || img.complete) return;
30
+ const placeholder = placeholderUrl(props.placeholder);
31
+ const { src: final, srcSet: finalSrcSet } = selected;
32
+ const warm = document.createElement("img");
33
+ warm.src = final;
34
+ if (finalSrcSet) warm.srcset = finalSrcSet;
35
+ const swap = () => {
36
+ img.src = final;
37
+ if (finalSrcSet) img.srcset = finalSrcSet;
38
+ };
39
+ img.addEventListener("load", swap, { once: true });
40
+ img.addEventListener("error", swap, { once: true });
41
+ if (finalSrcSet) img.srcset = "";
42
+ img.src = placeholder;
43
+ return () => {
44
+ img.removeEventListener("load", swap);
45
+ img.removeEventListener("error", swap);
46
+ };
47
+ }, [props.src, props.placeholder], _hs$ + 1);
48
+ const style = {
49
+ objectFit: "cover",
50
+ objectPosition: "50% 50%",
51
+ ...normStyle(props.style),
52
+ ...props.contentFit !== void 0 ? { objectFit: resolveContentFit(props.contentFit) } : null,
53
+ ...props.contentPosition !== void 0 ? { objectPosition: objectPositionCSSValue(resolveContentPosition(props.contentPosition)) } : null
54
+ };
55
+ if (__s.block.pending && !__s.block.crossRenderUpdate) return;
14
56
  let _b = __s.slots[0];
15
57
  if (_b === void 0) {
16
- let _m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7;
58
+ let _m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7, _m8, _m9, _m10;
17
59
  const _root = clone(_t$0);
18
- _m0 = (e) => applyEscapeProps(e, props);
60
+ _m0 = (e) => {
61
+ imgRef.current = e;
62
+ applyEscapeProps(e, props);
63
+ };
19
64
  _m1 = props.id;
20
65
  _m2 = props.className;
21
66
  _m3 = snapshotSpread(layoutChildProps(props));
22
- _m4 = props.src;
23
- _m5 = props.alt;
24
- _m6 = _root;
25
- _m7 = bindSignalHostPropSources(__s, void 0, _root, [
67
+ _m4 = selected.src;
68
+ _m5 = selected.srcSet;
69
+ _m6 = props.decoding ?? "async";
70
+ _m7 = props.alt;
71
+ _m8 = style;
72
+ _m9 = _root;
73
+ _m10 = bindSignalHostPropSources(__s, void 0, _root, [
26
74
  [
27
75
  false,
28
76
  "ref",
@@ -46,15 +94,33 @@ var Image = /* @__PURE__ */ __s(function Image(props, __s, __extra) {
46
94
  ],
47
95
  [
48
96
  false,
49
- "alt",
97
+ "srcSet",
50
98
  _m5
99
+ ],
100
+ [
101
+ false,
102
+ "decoding",
103
+ _m6
104
+ ],
105
+ [
106
+ false,
107
+ "alt",
108
+ _m7
109
+ ],
110
+ [
111
+ false,
112
+ "style",
113
+ _m8
51
114
  ]
52
- ], "b:ed5f7791", false);
53
- _b = bag8(__s, _root, _m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7);
115
+ ], "b:2396612c", false);
116
+ _b = bag11(__s, _root, _m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7, _m8, _m9, _m10);
54
117
  __s.refFields = _rm$0;
55
118
  } else {
56
119
  {
57
- const _v = (e) => applyEscapeProps(e, props);
120
+ const _v = (e) => {
121
+ imgRef.current = e;
122
+ applyEscapeProps(e, props);
123
+ };
58
124
  if (_b.a !== _v) _b.a = _v;
59
125
  }
60
126
  {
@@ -67,14 +133,26 @@ var Image = /* @__PURE__ */ __s(function Image(props, __s, __extra) {
67
133
  }
68
134
  _b.d = snapshotSpread(layoutChildProps(props));
69
135
  {
70
- const _v = props.src;
136
+ const _v = selected.src;
71
137
  if (_b.e !== _v) _b.e = _v;
72
138
  }
73
139
  {
74
- const _v = props.alt;
140
+ const _v = selected.srcSet;
75
141
  if (_b.f !== _v) _b.f = _v;
76
142
  }
77
- _b.h = bindSignalHostPropSources(__s, _b.h, _b.g, [
143
+ {
144
+ const _v = props.decoding ?? "async";
145
+ if (_b.g !== _v) _b.g = _v;
146
+ }
147
+ {
148
+ const _v = props.alt;
149
+ if (_b.h !== _v) _b.h = _v;
150
+ }
151
+ {
152
+ const _v = style;
153
+ if (_b.i !== _v) _b.i = _v;
154
+ }
155
+ _b.k = bindSignalHostPropSources(__s, _b.k, _b.j, [
78
156
  [
79
157
  false,
80
158
  "ref",
@@ -98,10 +176,25 @@ var Image = /* @__PURE__ */ __s(function Image(props, __s, __extra) {
98
176
  ],
99
177
  [
100
178
  false,
101
- "alt",
179
+ "srcSet",
102
180
  _b.f
181
+ ],
182
+ [
183
+ false,
184
+ "decoding",
185
+ _b.g
186
+ ],
187
+ [
188
+ false,
189
+ "alt",
190
+ _b.h
191
+ ],
192
+ [
193
+ false,
194
+ "style",
195
+ _b.i
103
196
  ]
104
- ], "b:ed5f7791", false);
197
+ ], "b:2396612c", false);
105
198
  }
106
199
  });
107
200
  //#endregion