@a4ui/core 0.35.1 → 0.35.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a4ui/core",
3
- "version": "0.35.1",
3
+ "version": "0.35.2",
4
4
  "description": "A4ui — Spatial Glass design system & component library for SolidJS (Kobalte behavior + Tailwind glass tokens + motion).",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  // import '@a4ui/core/styles.css'
9
9
  // import { Button, Card, Modal } from '@a4ui/core'
10
10
 
11
- export const A4UI_VERSION = '0.35.1'
11
+ export const A4UI_VERSION = '0.35.2'
12
12
 
13
13
  // Helpers (src/lib) — generic, framework-level utilities.
14
14
  export { cn } from './lib/cn'
@@ -51,10 +51,21 @@ interface WorldPixel {
51
51
  y: number
52
52
  }
53
53
 
54
+ /**
55
+ * A tile's IDENTITY, with no position in it.
56
+ *
57
+ * `id` and `src` are both pure functions of z/x/y, so a given tile's
58
+ * descriptor never changes while it is on screen — only where it sits does.
59
+ * Keeping position out of this type is what lets the memo below hand `<For>`
60
+ * the same object across a pan, so the `<img>` survives instead of being torn
61
+ * down and rebuilt.
62
+ */
54
63
  interface TileDescriptor {
55
64
  id: string
56
- left: number
57
- top: number
65
+ /** Tile column, unwrapped — multiplied by TILE_SIZE to place the tile. */
66
+ x: number
67
+ /** Tile row. */
68
+ y: number
58
69
  src: string
59
70
  }
60
71
 
@@ -174,9 +185,33 @@ export function InteractiveMap(props: InteractiveMapProps): JSX.Element {
174
185
  return { x: c.x - w / 2, y: c.y - h / 2 }
175
186
  })
176
187
 
188
+ /**
189
+ * Live tiles, keyed by id so identity survives a pan.
190
+ *
191
+ * `<For>` in Solid keys by REFERENCE. This memo used to build fresh
192
+ * descriptor objects on every recompute, so every pointer move made every
193
+ * tile a "new" item: the whole grid was removed from the DOM and rebuilt,
194
+ * even though the same tiles were still on screen. Measured on a 20-tile
195
+ * map, one ~500 ms drag produced 340 element insertions and 344 removals.
196
+ *
197
+ * The fade-in below turned that churn into a visible strobe. A rebuilt
198
+ * `<img>` starts at `opacity-0` and only becomes visible when its `load`
199
+ * event re-adds `a4ui-tile-loaded`, so during a pan every tile blinked out
200
+ * and back roughly seventeen times a second — the file header has always
201
+ * claimed tiles are "repositioned (not reloaded) as the user pans", and now
202
+ * they are.
203
+ *
204
+ * Reusing entries also means the cache holds only what is on screen: ids
205
+ * that fall out of range are dropped on the same pass that adds new ones.
206
+ */
207
+ const tileCache = new Map<string, TileDescriptor>()
208
+
177
209
  const tiles = createMemo<TileDescriptor[]>(() => {
178
210
  const { w, h } = size()
179
- if (w <= 0 || h <= 0) return []
211
+ if (w <= 0 || h <= 0) {
212
+ tileCache.clear()
213
+ return []
214
+ }
180
215
  const z = zoom()
181
216
  const numTiles = 2 ** z
182
217
  const tl = topLeftPx()
@@ -188,17 +223,29 @@ export function InteractiveMap(props: InteractiveMapProps): JSX.Element {
188
223
  const maxY = Math.min(numTiles - 1, Math.floor((tl.y + h) / TILE_SIZE) + 1)
189
224
 
190
225
  const list: TileDescriptor[] = []
226
+ const live = new Set<string>()
227
+
191
228
  for (let x = minX; x <= maxX; x++) {
192
229
  const wrappedX = ((x % numTiles) + numTiles) % numTiles
193
230
  for (let y = minY; y <= maxY; y++) {
194
- list.push({
195
- id: `${z}/${x}/${y}`,
196
- left: x * TILE_SIZE - tl.x,
197
- top: y * TILE_SIZE - tl.y,
198
- src: buildTileUrl(url, z, wrappedX, y),
199
- })
231
+ const id = `${z}/${x}/${y}`
232
+ live.add(id)
233
+ let tile = tileCache.get(id)
234
+ // The url prop can change at runtime, so a cached tile is only
235
+ // reusable while its src still matches.
236
+ const src = buildTileUrl(url, z, wrappedX, y)
237
+ if (!tile || tile.src !== src) {
238
+ tile = { id, x, y, src }
239
+ tileCache.set(id, tile)
240
+ }
241
+ list.push(tile)
200
242
  }
201
243
  }
244
+
245
+ for (const id of tileCache.keys()) {
246
+ if (!live.has(id)) tileCache.delete(id)
247
+ }
248
+
202
249
  return list
203
250
  })
204
251
 
@@ -309,7 +356,13 @@ export function InteractiveMap(props: InteractiveMapProps): JSX.Element {
309
356
  !motionReduced() &&
310
357
  'opacity-0 transition-opacity duration-150 [&.a4ui-tile-loaded]:opacity-100',
311
358
  )}
312
- style={{ left: `${tile.left}px`, top: `${tile.top}px` }}
359
+ // Read from the reactive origin rather than from the tile, so
360
+ // panning updates two style properties on an element that stays
361
+ // put instead of replacing it.
362
+ style={{
363
+ left: `${tile.x * TILE_SIZE - topLeftPx().x}px`,
364
+ top: `${tile.y * TILE_SIZE - topLeftPx().y}px`,
365
+ }}
313
366
  onLoad={(event) => event.currentTarget.classList.add('a4ui-tile-loaded')}
314
367
  />
315
368
  )}