@a4ui/core 0.35.1 → 0.36.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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1561 -1530
- package/dist/ui/DataGrid.d.ts +12 -0
- package/dist/ui/LocationPicker.d.ts +9 -0
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/ui/DataGrid.tsx +15 -3
- package/src/ui/InteractiveMap.tsx +63 -10
- package/src/ui/LocationPicker.tsx +15 -2
- package/src/ui/Table.tsx +9 -1
package/dist/ui/DataGrid.d.ts
CHANGED
|
@@ -18,6 +18,18 @@ export interface DataGridProps {
|
|
|
18
18
|
pageSize?: number;
|
|
19
19
|
/** Show the global text filter above the table. Defaults to true. */
|
|
20
20
|
filterable?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Copy overrides, for apps that do not run in English. Every user-visible
|
|
23
|
+
* string this component renders on its own is here; without them a Spanish
|
|
24
|
+
* or French page shows "Filter…" and "12 rows" in the middle of its own
|
|
25
|
+
* table with no way to change it.
|
|
26
|
+
* @default 'Filter…'
|
|
27
|
+
*/
|
|
28
|
+
filterPlaceholder?: string;
|
|
29
|
+
/** Accessible name for the filter box. @default 'Filter rows' */
|
|
30
|
+
filterLabel?: string;
|
|
31
|
+
/** Builds the row-count summary. @default (n) => `${n} rows` */
|
|
32
|
+
rowsSummary?: (count: number) => string;
|
|
21
33
|
class?: string;
|
|
22
34
|
}
|
|
23
35
|
/**
|
|
@@ -9,6 +9,15 @@ export interface LocationPickerProps {
|
|
|
9
9
|
value?: LocationValue;
|
|
10
10
|
/** Fires with the full value whenever the label, the pin, or geolocation changes it. */
|
|
11
11
|
onChange?: (v: LocationValue) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Placeholder for the free-text label field. Overridable because this was
|
|
14
|
+
* hardcoded English inside an otherwise fully translatable component — a
|
|
15
|
+
* Spanish form showed "Location label" in the middle of its own wizard.
|
|
16
|
+
* @default 'Location label'
|
|
17
|
+
*/
|
|
18
|
+
labelPlaceholder?: string;
|
|
19
|
+
/** Accessible name for the "use my location" button. @default 'Use my location' */
|
|
20
|
+
geolocateLabel?: string;
|
|
12
21
|
class?: string;
|
|
13
22
|
}
|
|
14
23
|
/**
|
package/package.json
CHANGED
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.
|
|
11
|
+
export const A4UI_VERSION = '0.36.0'
|
|
12
12
|
|
|
13
13
|
// Helpers (src/lib) — generic, framework-level utilities.
|
|
14
14
|
export { cn } from './lib/cn'
|
package/src/ui/DataGrid.tsx
CHANGED
|
@@ -31,6 +31,18 @@ export interface DataGridProps {
|
|
|
31
31
|
pageSize?: number
|
|
32
32
|
/** Show the global text filter above the table. Defaults to true. */
|
|
33
33
|
filterable?: boolean
|
|
34
|
+
/**
|
|
35
|
+
* Copy overrides, for apps that do not run in English. Every user-visible
|
|
36
|
+
* string this component renders on its own is here; without them a Spanish
|
|
37
|
+
* or French page shows "Filter…" and "12 rows" in the middle of its own
|
|
38
|
+
* table with no way to change it.
|
|
39
|
+
* @default 'Filter…'
|
|
40
|
+
*/
|
|
41
|
+
filterPlaceholder?: string
|
|
42
|
+
/** Accessible name for the filter box. @default 'Filter rows' */
|
|
43
|
+
filterLabel?: string
|
|
44
|
+
/** Builds the row-count summary. @default (n) => `${n} rows` */
|
|
45
|
+
rowsSummary?: (count: number) => string
|
|
34
46
|
class?: string
|
|
35
47
|
}
|
|
36
48
|
|
|
@@ -126,8 +138,8 @@ export function DataGrid(props: DataGridProps): JSX.Element {
|
|
|
126
138
|
setQuery(v)
|
|
127
139
|
setPage(1)
|
|
128
140
|
}}
|
|
129
|
-
placeholder=
|
|
130
|
-
aria-label=
|
|
141
|
+
placeholder={props.filterPlaceholder ?? 'Filter…'}
|
|
142
|
+
aria-label={props.filterLabel ?? 'Filter rows'}
|
|
131
143
|
/>
|
|
132
144
|
</div>
|
|
133
145
|
)}
|
|
@@ -170,7 +182,7 @@ export function DataGrid(props: DataGridProps): JSX.Element {
|
|
|
170
182
|
page={Math.min(page(), totalPages())}
|
|
171
183
|
totalPages={totalPages()}
|
|
172
184
|
onChange={setPage}
|
|
173
|
-
summary={`${sorted().length} rows`}
|
|
185
|
+
summary={props.rowsSummary?.(sorted().length) ?? `${sorted().length} rows`}
|
|
174
186
|
/>
|
|
175
187
|
</div>
|
|
176
188
|
)
|
|
@@ -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
|
-
|
|
57
|
-
|
|
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)
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
)}
|
|
@@ -20,6 +20,15 @@ export interface LocationPickerProps {
|
|
|
20
20
|
value?: LocationValue
|
|
21
21
|
/** Fires with the full value whenever the label, the pin, or geolocation changes it. */
|
|
22
22
|
onChange?: (v: LocationValue) => void
|
|
23
|
+
/**
|
|
24
|
+
* Placeholder for the free-text label field. Overridable because this was
|
|
25
|
+
* hardcoded English inside an otherwise fully translatable component — a
|
|
26
|
+
* Spanish form showed "Location label" in the middle of its own wizard.
|
|
27
|
+
* @default 'Location label'
|
|
28
|
+
*/
|
|
29
|
+
labelPlaceholder?: string
|
|
30
|
+
/** Accessible name for the "use my location" button. @default 'Use my location' */
|
|
31
|
+
geolocateLabel?: string
|
|
23
32
|
class?: string
|
|
24
33
|
}
|
|
25
34
|
|
|
@@ -77,10 +86,14 @@ export function LocationPicker(props: LocationPickerProps): JSX.Element {
|
|
|
77
86
|
type="text"
|
|
78
87
|
value={current().label ?? ''}
|
|
79
88
|
onInput={(event) => setLabel(event.currentTarget.value)}
|
|
80
|
-
placeholder=
|
|
89
|
+
placeholder={props.labelPlaceholder ?? 'Location label'}
|
|
81
90
|
class="h-9 flex-1 rounded-md border border-border bg-input px-3 text-sm text-foreground placeholder:text-muted-foreground focus:ring-2 focus:ring-ring focus:outline-none"
|
|
82
91
|
/>
|
|
83
|
-
<Button
|
|
92
|
+
<Button
|
|
93
|
+
variant="outline"
|
|
94
|
+
aria-label={props.geolocateLabel ?? 'Use my location'}
|
|
95
|
+
onClick={useMyLocation}
|
|
96
|
+
>
|
|
84
97
|
<Crosshair class="h-4 w-4" />
|
|
85
98
|
</Button>
|
|
86
99
|
</div>
|
package/src/ui/Table.tsx
CHANGED
|
@@ -34,7 +34,15 @@ interface TableProps extends ParentProps {
|
|
|
34
34
|
export function Table(props: TableProps): JSX.Element {
|
|
35
35
|
const [local, rest] = splitProps(props, ['class', 'children'])
|
|
36
36
|
return (
|
|
37
|
-
|
|
37
|
+
// `relative` is load-bearing, not decoration: `overflow` only clips a
|
|
38
|
+
// descendant whose containing block is inside the scroller. Without a
|
|
39
|
+
// positioned wrapper, an absolutely-positioned descendant (a
|
|
40
|
+
// screen-reader-only input inside a cell control, for instance) resolves
|
|
41
|
+
// against an ancestor further up, escapes the clip at the table's full
|
|
42
|
+
// scroll width, and drags the whole DOCUMENT into horizontal scrolling —
|
|
43
|
+
// measured on a phone: a 356 px table inside a 390 px viewport pushed the
|
|
44
|
+
// page to 413 px.
|
|
45
|
+
<div class="relative w-full overflow-x-auto">
|
|
38
46
|
<table class={cn('w-full border-collapse text-sm', local.class)} {...rest}>
|
|
39
47
|
{local.children}
|
|
40
48
|
</table>
|