@a4ui/core 0.35.0 → 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.
@@ -7,6 +7,8 @@ export interface RingProgressProps {
7
7
  thickness?: number;
8
8
  /** Content centered over the ring. Defaults to the rounded percent. */
9
9
  label?: JSX.Element;
10
+ /** Accessible name for the `role="progressbar"` element. */
11
+ 'aria-label'?: string;
10
12
  class?: string;
11
13
  }
12
14
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a4ui/core",
3
- "version": "0.35.0",
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.0'
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'
@@ -167,10 +167,8 @@ export function AudioWaveform(props: AudioWaveformProps): JSX.Element {
167
167
  {(peak, index) => {
168
168
  const played = createMemo(() => index() / bars().length <= progress())
169
169
  return (
170
- <button
171
- type="button"
170
+ <div
172
171
  aria-hidden="true"
173
- tabIndex={-1}
174
172
  onClick={(event) => {
175
173
  event.stopPropagation()
176
174
  handleBarClick(index())
@@ -203,7 +203,13 @@ export function EventScheduler(props: EventSchedulerProps): JSX.Element {
203
203
  </div>
204
204
 
205
205
  {/* Body: scrolls vertically; gutter of hour labels + day columns */}
206
- <div class="flex overflow-y-auto" style={{ 'max-height': '32rem' }}>
206
+ <div
207
+ role="group"
208
+ aria-label="Schedule grid"
209
+ tabIndex={0}
210
+ class="flex overflow-y-auto outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary"
211
+ style={{ 'max-height': '32rem' }}
212
+ >
207
213
  <div class="w-14 shrink-0" style={{ height: `${gridHeight()}px` }}>
208
214
  <For each={hours()}>
209
215
  {(h) => (
@@ -155,7 +155,15 @@ export function GanttChart(props: GanttChartProps): JSX.Element {
155
155
  })
156
156
 
157
157
  return (
158
- <div class={cn('overflow-x-auto rounded-lg border border-border bg-card', props.class)}>
158
+ <div
159
+ role="group"
160
+ aria-label="Gantt chart"
161
+ tabIndex={0}
162
+ class={cn(
163
+ 'overflow-x-auto rounded-lg border border-border bg-card outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary',
164
+ props.class,
165
+ )}
166
+ >
159
167
  <Show
160
168
  when={props.tasks.length > 0}
161
169
  fallback={<p class="p-4 text-sm text-muted-foreground">No tasks to display.</p>}
@@ -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
  )}
@@ -79,6 +79,7 @@ export function OnboardingChecklist(props: OnboardingChecklistProps): JSX.Elemen
79
79
  value={percent()}
80
80
  size={56}
81
81
  thickness={6}
82
+ aria-label={`${done()} of ${total()} steps complete`}
82
83
  label={
83
84
  <span class="text-xs">
84
85
  {done()}/{total()}
Binary file
@@ -243,10 +243,10 @@ function RuleRow(props: RuleRowProps): JSX.Element {
243
243
 
244
244
  return (
245
245
  <div class="flex flex-wrap items-center gap-2">
246
- <Select class="w-40" value={props.rule.field} onChange={setField}>
246
+ <Select class="w-40" value={props.rule.field} onChange={setField} aria-label="Field">
247
247
  <For each={props.fields}>{(f) => <option value={f.name}>{f.label}</option>}</For>
248
248
  </Select>
249
- <Select class="w-32" value={props.rule.operator} onChange={setOperator}>
249
+ <Select class="w-32" value={props.rule.operator} onChange={setOperator} aria-label="Operator">
250
250
  <For each={props.operatorsFor(props.rule.field)}>{(op) => <option value={op}>{op}</option>}</For>
251
251
  </Select>
252
252
  <Show
@@ -257,10 +257,11 @@ function RuleRow(props: RuleRowProps): JSX.Element {
257
257
  type={field()?.type === 'number' ? 'number' : 'text'}
258
258
  value={props.rule.value}
259
259
  onInput={setValue}
260
+ aria-label="Value"
260
261
  />
261
262
  }
262
263
  >
263
- <Select class="w-40" value={props.rule.value} onChange={setValue}>
264
+ <Select class="w-40" value={props.rule.value} onChange={setValue} aria-label="Value">
264
265
  <option value="" disabled>
265
266
  Select…
266
267
  </option>
@@ -12,6 +12,8 @@ export interface RingProgressProps {
12
12
  thickness?: number
13
13
  /** Content centered over the ring. Defaults to the rounded percent. */
14
14
  label?: JSX.Element
15
+ /** Accessible name for the `role="progressbar"` element. */
16
+ 'aria-label'?: string
15
17
  class?: string
16
18
  }
17
19
 
@@ -37,6 +39,7 @@ export function RingProgress(props: RingProgressProps): JSX.Element {
37
39
  return (
38
40
  <div
39
41
  role="progressbar"
42
+ aria-label={props['aria-label']}
40
43
  aria-valuenow={clamped()}
41
44
  aria-valuemin={0}
42
45
  aria-valuemax={100}
@@ -235,6 +235,7 @@ export function SpreadsheetGrid(props: SpreadsheetGridProps): JSX.Element {
235
235
  ref={containerRef}
236
236
  tabIndex={0}
237
237
  role="grid"
238
+ aria-label="Spreadsheet"
238
239
  aria-rowcount={props.rows}
239
240
  aria-colcount={props.cols}
240
241
  class={cn(
@@ -244,20 +245,22 @@ export function SpreadsheetGrid(props: SpreadsheetGridProps): JSX.Element {
244
245
  onKeyDown={handleKeyDown}
245
246
  >
246
247
  <div class="grid" style={{ 'grid-template-columns': `48px repeat(${props.cols}, minmax(80px, 1fr))` }}>
247
- <div class="border-b border-r border-border bg-muted" />
248
- <For each={headers()}>
249
- {(h) => (
250
- <div
251
- role="columnheader"
252
- class="border-b border-r border-border bg-muted px-2 py-1 text-center text-xs font-semibold text-muted-foreground"
253
- >
254
- {h}
255
- </div>
256
- )}
257
- </For>
248
+ <div role="row" class="contents">
249
+ <div aria-hidden="true" class="border-b border-r border-border bg-muted" />
250
+ <For each={headers()}>
251
+ {(h) => (
252
+ <div
253
+ role="columnheader"
254
+ class="border-b border-r border-border bg-muted px-2 py-1 text-center text-xs font-semibold text-muted-foreground"
255
+ >
256
+ {h}
257
+ </div>
258
+ )}
259
+ </For>
260
+ </div>
258
261
  <For each={rowIndices()}>
259
262
  {(r) => (
260
- <>
263
+ <div role="row" class="contents">
261
264
  <div
262
265
  role="rowheader"
263
266
  class="border-b border-r border-border bg-muted px-2 py-1 text-center text-xs text-muted-foreground"
@@ -295,7 +298,7 @@ export function SpreadsheetGrid(props: SpreadsheetGridProps): JSX.Element {
295
298
  </div>
296
299
  )}
297
300
  </For>
298
- </>
301
+ </div>
299
302
  )}
300
303
  </For>
301
304
  </div>