basalt-ui 1.31.0 → 1.32.1
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/CHANGELOG.md +34 -0
- package/MIGRATING.md +212 -20
- package/agent/rules/basalt-mantine.md +4 -4
- package/configs/oxlint-plugin.js +240 -59
- package/configs/oxlint-plugin.test.ts +329 -1
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -1
- package/dist/charts/kinds/BandStrip.js +18 -66
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.js +5 -26
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -1
- package/dist/charts/kinds/MirroredBars.js +10 -68
- package/dist/charts/kinds/band-chrome.d.ts +31 -0
- package/dist/charts/kinds/band-chrome.d.ts.map +1 -0
- package/dist/charts/kinds/band-chrome.js +71 -0
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +49 -76
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +5 -23
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +23 -40
- package/dist/charts/primitives/ChartLegend.d.ts.map +1 -1
- package/dist/charts/primitives/ChartLegend.js +5 -133
- package/dist/charts/primitives/ChartTooltip.d.ts +5 -0
- package/dist/charts/primitives/ChartTooltip.d.ts.map +1 -1
- package/dist/charts/primitives/ChartTooltip.js +16 -1
- package/dist/charts/primitives/HoverOverlay.d.ts +24 -9
- package/dist/charts/primitives/HoverOverlay.d.ts.map +1 -1
- package/dist/charts/primitives/HoverOverlay.js +17 -1
- package/dist/charts/primitives/chart-card-context.d.ts +17 -13
- package/dist/charts/primitives/chart-card-context.d.ts.map +1 -1
- package/dist/charts/primitives/chart-card-context.js +10 -5
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
- package/dist/charts/primitives/chart-frame-layout.js +25 -31
- package/dist/charts/primitives/legend-disclosure.d.ts +27 -0
- package/dist/charts/primitives/legend-disclosure.d.ts.map +1 -0
- package/dist/charts/primitives/legend-disclosure.js +153 -0
- package/dist/controls/controls.module.css +5 -0
- package/dist/controls/sync-button.module.css +1 -0
- package/dist/dashboard/stat-group.module.css +19 -64
- package/dist/dashboard/widget-grid.module.css +11 -38
- package/dist/data/data-table-state.d.ts +83 -0
- package/dist/data/data-table-state.d.ts.map +1 -0
- package/dist/data/data-table-state.js +139 -0
- package/dist/data/data-table.d.ts +40 -2
- package/dist/data/data-table.d.ts.map +1 -1
- package/dist/data/data-table.js +179 -277
- package/dist/forms/form-layout.d.ts +6 -1
- package/dist/forms/form-layout.d.ts.map +1 -1
- package/dist/forms/form-layout.module.css +6 -13
- package/dist/guard/index.d.ts +2 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +10 -14
- package/dist/notifications/center.d.ts.map +1 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +12 -10
- package/dist/shell/aside-docking.d.ts +31 -0
- package/dist/shell/aside-docking.d.ts.map +1 -0
- package/dist/shell/aside-docking.js +19 -0
- package/dist/shell/index.d.ts +0 -11
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +13 -14
- package/dist/shell/page-bar.d.ts +24 -0
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/use-breakpoint.d.ts +1 -1
- package/dist/theme/floating.module.css +1 -0
- package/dist/theme-lab/derive-controls.d.ts.map +1 -1
- package/dist/tokens/index.d.ts +2 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +4 -0
- package/llms.txt +2 -2
- package/package.json +2 -1
- package/src/charts/kinds/BandStrip.tsx +18 -64
- package/src/charts/kinds/DualPanel.tsx +4 -24
- package/src/charts/kinds/MirroredBars.tsx +12 -66
- package/src/charts/kinds/band-chrome.tsx +91 -0
- package/src/charts/primitives/CartesianChart.tsx +79 -122
- package/src/charts/primitives/ChartCard.tsx +5 -27
- package/src/charts/primitives/ChartFrame.tsx +37 -76
- package/src/charts/primitives/ChartLegend.tsx +7 -154
- package/src/charts/primitives/ChartTooltip.tsx +16 -0
- package/src/charts/primitives/HoverOverlay.tsx +52 -21
- package/src/charts/primitives/chart-card-context.ts +25 -16
- package/src/charts/primitives/chart-frame-layout.ts +44 -56
- package/src/charts/primitives/legend-disclosure.tsx +177 -0
- package/src/controls/controls.module.css +5 -0
- package/src/controls/sync-button.module.css +1 -0
- package/src/dashboard/stat-group.module.css +19 -64
- package/src/dashboard/widget-grid.module.css +11 -38
- package/src/data/data-table-state.ts +218 -0
- package/src/data/data-table.tsx +134 -218
- package/src/forms/form-layout.module.css +6 -13
- package/src/forms/form-layout.tsx +6 -1
- package/src/guard/index.ts +38 -34
- package/src/notifications/center.tsx +3 -0
- package/src/provider/index.tsx +18 -17
- package/src/shell/aside-docking.ts +54 -0
- package/src/shell/index.tsx +15 -37
- package/src/shell/page-bar.tsx +24 -0
- package/src/shell/use-breakpoint.ts +2 -2
- package/src/theme/floating.module.css +1 -0
- package/src/theme-lab/derive-controls.tsx +2 -0
- package/src/tokens/index.ts +5 -0
|
@@ -286,38 +286,46 @@ const legendEntryWidth = (item: LegendEntry): number =>
|
|
|
286
286
|
)
|
|
287
287
|
|
|
288
288
|
/**
|
|
289
|
-
* The greedy
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
* entry always fits" guarantee a bare row-fit needs
|
|
293
|
-
*
|
|
294
|
-
* would always
|
|
289
|
+
* The ONE greedy wrap every legend fit runs — the same wrap the flex container performs, measured
|
|
290
|
+
* rather than assumed. Each item costs `widthOf(item)` (plus `gap` after the row's first) and
|
|
291
|
+
* `extraAfter(i)` more once placed (a group divider is its own flex item). `allowZero` (default
|
|
292
|
+
* false) drops the "the first entry always fits" guarantee a bare row-fit needs: a reserved pass
|
|
293
|
+
* (the `All N` chip already took its room) must be able to answer 0, or the SAME entry the caller
|
|
294
|
+
* is about to draw the chip beside would always count as fitting even when the two provably don't.
|
|
295
295
|
*/
|
|
296
|
-
function
|
|
297
|
-
items: readonly
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
296
|
+
function fitGreedyRows<T>(
|
|
297
|
+
items: readonly T[],
|
|
298
|
+
fit: {
|
|
299
|
+
widthOf: (item: T) => number
|
|
300
|
+
gap: number
|
|
301
|
+
width: number
|
|
302
|
+
rows: number
|
|
303
|
+
allowZero?: boolean
|
|
304
|
+
extraAfter?: (index: number) => number
|
|
305
|
+
},
|
|
301
306
|
): number {
|
|
302
307
|
let row = 1
|
|
303
308
|
let x = 0
|
|
304
309
|
let fitted = 0
|
|
305
|
-
for (const item of items) {
|
|
306
|
-
const w =
|
|
307
|
-
const next = x === 0 ? w : x +
|
|
308
|
-
if (x === 0 && allowZero && next > width) return 0
|
|
309
|
-
if (next > width && x > 0) {
|
|
310
|
+
for (const [i, item] of items.entries()) {
|
|
311
|
+
const w = fit.widthOf(item)
|
|
312
|
+
const next = x === 0 ? w : x + fit.gap + w
|
|
313
|
+
if (x === 0 && fit.allowZero === true && next > fit.width) return 0
|
|
314
|
+
if (next > fit.width && x > 0) {
|
|
310
315
|
row += 1
|
|
311
|
-
if (row > rows) return fitted
|
|
316
|
+
if (row > fit.rows) return fitted
|
|
312
317
|
x = w
|
|
313
318
|
} else {
|
|
314
319
|
x = next
|
|
315
320
|
}
|
|
316
321
|
fitted += 1
|
|
322
|
+
x += fit.extraAfter?.(i) ?? 0
|
|
317
323
|
}
|
|
318
324
|
return fitted
|
|
319
325
|
}
|
|
320
326
|
|
|
327
|
+
const chipFit = { widthOf: legendEntryWidth, gap: VX.legendGap }
|
|
328
|
+
|
|
321
329
|
/** How many of `items` fit in `rows` wrapped rows of `width` — the same greedy wrap the flex
|
|
322
330
|
* container performs, measured rather than assumed (`docs/CHARTS-SPEC.md` §1). */
|
|
323
331
|
export function entriesWithinRows(
|
|
@@ -325,7 +333,7 @@ export function entriesWithinRows(
|
|
|
325
333
|
width: number,
|
|
326
334
|
rows: number,
|
|
327
335
|
): number {
|
|
328
|
-
return
|
|
336
|
+
return fitGreedyRows(items, { ...chipFit, width, rows })
|
|
329
337
|
}
|
|
330
338
|
|
|
331
339
|
/**
|
|
@@ -342,44 +350,15 @@ export function entriesWithinChipRows(
|
|
|
342
350
|
rows: number,
|
|
343
351
|
): number {
|
|
344
352
|
const total = items.length
|
|
345
|
-
const fittedNoReserve =
|
|
353
|
+
const fittedNoReserve = entriesWithinRows(items, width, rows)
|
|
346
354
|
if (fittedNoReserve >= total) return fittedNoReserve
|
|
347
355
|
const reserve = measureText(`All ${total}`, VX.legendFontSize) + VX.legendGap
|
|
348
|
-
return
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
* own flag: without it the first dot always counts as fitted regardless of `width`, which is wrong
|
|
355
|
-
* for the reserved pass once reserving has left no usable room at all. */
|
|
356
|
-
function fitDotRows(
|
|
357
|
-
items: readonly LegendEntry[],
|
|
358
|
-
width: number,
|
|
359
|
-
rows: number,
|
|
360
|
-
dividerAfter: ReadonlySet<number>,
|
|
361
|
-
allowZero = false,
|
|
362
|
-
): number {
|
|
363
|
-
let row = 1
|
|
364
|
-
let x = 0
|
|
365
|
-
let fitted = 0
|
|
366
|
-
for (let i = 0; i < items.length; i += 1) {
|
|
367
|
-
// `DOT_PITCH` already IS one dot's full footprint (its own size plus the gap to a neighbour,
|
|
368
|
-
// `LEGEND_DOT_SIZE + DOTS_HIT_GAP`) — adding `LEGEND_DOT_SIZE` again here double-counted every
|
|
369
|
-
// dot after the row's first, wrapping a legend to `All N` well before it actually overflowed.
|
|
370
|
-
const next = x === 0 ? LEGEND_DOT_SIZE : x + DOT_PITCH
|
|
371
|
-
if (x === 0 && allowZero && next > width) return 0
|
|
372
|
-
if (next > width && x > 0) {
|
|
373
|
-
row += 1
|
|
374
|
-
if (row > rows) return fitted
|
|
375
|
-
x = LEGEND_DOT_SIZE
|
|
376
|
-
} else {
|
|
377
|
-
x = next
|
|
378
|
-
}
|
|
379
|
-
fitted += 1
|
|
380
|
-
if (dividerAfter.has(i)) x += DOT_PITCH
|
|
381
|
-
}
|
|
382
|
-
return fitted
|
|
356
|
+
return fitGreedyRows(items, {
|
|
357
|
+
...chipFit,
|
|
358
|
+
width: Math.max(width - reserve, 0),
|
|
359
|
+
rows,
|
|
360
|
+
allowZero: true,
|
|
361
|
+
})
|
|
383
362
|
}
|
|
384
363
|
|
|
385
364
|
/**
|
|
@@ -398,11 +377,20 @@ export function entriesWithinDotRows(
|
|
|
398
377
|
rows: number,
|
|
399
378
|
dividerAfter: ReadonlySet<number> = new Set(),
|
|
400
379
|
): number {
|
|
380
|
+
// A dot is `LEGEND_DOT_SIZE` wide and `DOT_PITCH` already IS its full footprint (size plus the
|
|
381
|
+
// gap to a neighbour) — so the gap is the pitch LESS the dot; adding the dot again double-counted
|
|
382
|
+
// every dot after the row's first and wrapped to `All N` well before it overflowed.
|
|
383
|
+
const dotFit = {
|
|
384
|
+
widthOf: () => LEGEND_DOT_SIZE,
|
|
385
|
+
gap: DOT_PITCH - LEGEND_DOT_SIZE,
|
|
386
|
+
rows,
|
|
387
|
+
extraAfter: (i: number) => (dividerAfter.has(i) ? DOT_PITCH : 0),
|
|
388
|
+
}
|
|
401
389
|
const total = items.length
|
|
402
|
-
const fittedNoReserve =
|
|
390
|
+
const fittedNoReserve = fitGreedyRows(items, { ...dotFit, width })
|
|
403
391
|
if (fittedNoReserve >= total) return fittedNoReserve
|
|
404
392
|
const reserve = measureText(`All ${total}`, VX.legendFontSize) + DOT_PITCH
|
|
405
|
-
return
|
|
393
|
+
return fitGreedyRows(items, { ...dotFit, width: Math.max(width - reserve, 0), allowZero: true })
|
|
406
394
|
}
|
|
407
395
|
|
|
408
396
|
/**
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react'
|
|
2
|
+
import { useCallback, useEffect, useId, useRef, useState } from 'react'
|
|
3
|
+
import { createPortal } from 'react-dom'
|
|
4
|
+
import { alpha, VX } from '../../tokens'
|
|
5
|
+
import { useDismissOnOutside } from '../cursor/touch-pin'
|
|
6
|
+
import { LEGEND_ROW_GAP } from './chart-frame-layout'
|
|
7
|
+
|
|
8
|
+
const DISCLOSURE_PANEL: CSSProperties = {
|
|
9
|
+
position: 'fixed',
|
|
10
|
+
zIndex: VX.zIndexFloating,
|
|
11
|
+
display: 'flex',
|
|
12
|
+
flexDirection: 'column',
|
|
13
|
+
gap: LEGEND_ROW_GAP,
|
|
14
|
+
// theme-allow raw-scroll-container — a fixed floating panel of one legend, not app chrome
|
|
15
|
+
overflowY: 'auto',
|
|
16
|
+
// `<dialog open>` brings UA chrome (centring margin, border, fit-content box) this resets.
|
|
17
|
+
margin: 0,
|
|
18
|
+
border: 'none',
|
|
19
|
+
width: 'auto',
|
|
20
|
+
height: 'auto',
|
|
21
|
+
padding: 10,
|
|
22
|
+
boxSizing: 'border-box',
|
|
23
|
+
backgroundColor: VX.surface.overlay,
|
|
24
|
+
boxShadow: VX.shadowOverlay,
|
|
25
|
+
color: VX.muted,
|
|
26
|
+
borderRadius: VX.radiusCard,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A popover never collapses below one usable row, however little room the chip leaves. */
|
|
30
|
+
const DISCLOSURE_MIN_HEIGHT = 96
|
|
31
|
+
|
|
32
|
+
/** A popover hangs off its chip (flipping above when there is no room below); a sheet is docked. */
|
|
33
|
+
function disclosurePlacement(chip: DOMRect, coarse: boolean): CSSProperties {
|
|
34
|
+
if (coarse) {
|
|
35
|
+
return {
|
|
36
|
+
left: 0,
|
|
37
|
+
right: 0,
|
|
38
|
+
bottom: 0,
|
|
39
|
+
maxHeight: '60dvh',
|
|
40
|
+
borderBottomLeftRadius: 0,
|
|
41
|
+
borderBottomRightRadius: 0,
|
|
42
|
+
padding: '12px 16px calc(12px + env(safe-area-inset-bottom, 0px))',
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const below = window.innerHeight - chip.bottom
|
|
46
|
+
const left = Math.max(8, Math.min(chip.left, window.innerWidth - 248))
|
|
47
|
+
const base = { left, right: 'auto', minWidth: 200, maxWidth: 'calc(100vw - 16px)' }
|
|
48
|
+
return below >= 200 || below >= chip.top
|
|
49
|
+
? { ...base, top: chip.bottom + 4, maxHeight: Math.max(below - 12, DISCLOSURE_MIN_HEIGHT) }
|
|
50
|
+
: {
|
|
51
|
+
...base,
|
|
52
|
+
bottom: window.innerHeight - chip.top + 4,
|
|
53
|
+
maxHeight: Math.max(chip.top - 12, DISCLOSURE_MIN_HEIGHT),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `ChartLegend`'s `All N` disclosure state: which button opened it (the chip, or a dots-mode entry)
|
|
59
|
+
* and where the fixed panel sits. Focus moves into the panel on open and back to that trigger on
|
|
60
|
+
* EVERY close path; a scroll outside the panel or a resize closes it, since it is fixed to where the
|
|
61
|
+
* trigger WAS.
|
|
62
|
+
*/
|
|
63
|
+
export function useLegendDisclosure(coarse: boolean) {
|
|
64
|
+
const [placement, setPlacement] = useState<CSSProperties | null>(null)
|
|
65
|
+
const triggerRef = useRef<HTMLButtonElement | null>(null)
|
|
66
|
+
const panelRef = useRef<HTMLDialogElement>(null)
|
|
67
|
+
const panelId = useId()
|
|
68
|
+
const wasOpen = useRef(false)
|
|
69
|
+
const open = placement !== null
|
|
70
|
+
const close = useCallback(() => setPlacement(null), [])
|
|
71
|
+
const openFrom = (trigger: HTMLButtonElement) => {
|
|
72
|
+
triggerRef.current = trigger
|
|
73
|
+
setPlacement(disclosurePlacement(trigger.getBoundingClientRect(), coarse))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (open) panelRef.current?.focus()
|
|
78
|
+
else if (wasOpen.current) triggerRef.current?.focus()
|
|
79
|
+
wasOpen.current = open
|
|
80
|
+
}, [open])
|
|
81
|
+
|
|
82
|
+
// Shared with the chart cursors' own outside-dismiss (`useChartCursor`/`useDiscreteCursor`, see
|
|
83
|
+
// `touch-pin.ts`); the panel's own scroll is not an outside one.
|
|
84
|
+
const isInsidePanel = useCallback(
|
|
85
|
+
(target: Node): boolean =>
|
|
86
|
+
panelRef.current?.contains(target) === true || triggerRef.current?.contains(target) === true,
|
|
87
|
+
[],
|
|
88
|
+
)
|
|
89
|
+
useDismissOnOutside({
|
|
90
|
+
enabled: open,
|
|
91
|
+
isInside: isInsidePanel,
|
|
92
|
+
onDismiss: close,
|
|
93
|
+
dismissOnResize: true,
|
|
94
|
+
scrollExemptInside: true,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// The coarse sheet claims `aria-modal` (below), which is a claim that Tab cannot leave it — a
|
|
98
|
+
// native `<dialog open>` (no `showModal()`) does not enforce that on its own (B14). Wrap Tab at
|
|
99
|
+
// the panel's own first/last focusable rather than the fine popover, which is dismissed by an
|
|
100
|
+
// outside click/Escape just as easily and never claimed to be modal in the first place.
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!open || !coarse) return
|
|
103
|
+
const panel = panelRef.current
|
|
104
|
+
if (panel === null) return
|
|
105
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
106
|
+
if (e.key !== 'Tab') return
|
|
107
|
+
const focusable = panel.querySelectorAll<HTMLElement>(
|
|
108
|
+
'button, [tabindex]:not([tabindex="-1"])',
|
|
109
|
+
)
|
|
110
|
+
const first = focusable[0]
|
|
111
|
+
const last = focusable[focusable.length - 1]
|
|
112
|
+
if (first === undefined || last === undefined) return
|
|
113
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
114
|
+
e.preventDefault()
|
|
115
|
+
last.focus()
|
|
116
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
117
|
+
e.preventDefault()
|
|
118
|
+
first.focus()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
panel.addEventListener('keydown', onKeyDown)
|
|
122
|
+
return () => panel.removeEventListener('keydown', onKeyDown)
|
|
123
|
+
}, [open, coarse])
|
|
124
|
+
|
|
125
|
+
return { open, panelId, openFrom, close, panel: { panelRef, panelId, coarse, placement, close } }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The portalled panel itself — a popover under the chip on a fine pointer, a docked sheet (over a
|
|
129
|
+
* scrim) on a coarse one. Fixed-position, so opening it never grows the legend band. */
|
|
130
|
+
export function LegendDisclosure({
|
|
131
|
+
panelRef,
|
|
132
|
+
panelId,
|
|
133
|
+
coarse,
|
|
134
|
+
placement,
|
|
135
|
+
close,
|
|
136
|
+
fontSize,
|
|
137
|
+
children,
|
|
138
|
+
}: ReturnType<typeof useLegendDisclosure>['panel'] & {
|
|
139
|
+
fontSize: number
|
|
140
|
+
children: ReactNode
|
|
141
|
+
}) {
|
|
142
|
+
if (placement === null) return null
|
|
143
|
+
return createPortal(
|
|
144
|
+
<>
|
|
145
|
+
{coarse && (
|
|
146
|
+
<div
|
|
147
|
+
aria-hidden
|
|
148
|
+
onPointerDown={(e) => {
|
|
149
|
+
e.stopPropagation()
|
|
150
|
+
close()
|
|
151
|
+
}}
|
|
152
|
+
style={{
|
|
153
|
+
position: 'fixed',
|
|
154
|
+
inset: 0,
|
|
155
|
+
// One below the panel it sits behind — still `VX.zIndexFloating`-derived, never a
|
|
156
|
+
// second unrelated literal (B14).
|
|
157
|
+
zIndex: VX.zIndexFloating - 1,
|
|
158
|
+
backgroundColor: alpha(VX.neutral, 0.35),
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
)}
|
|
162
|
+
<dialog
|
|
163
|
+
ref={panelRef}
|
|
164
|
+
id={panelId}
|
|
165
|
+
open
|
|
166
|
+
tabIndex={-1}
|
|
167
|
+
aria-label="Legend"
|
|
168
|
+
{...(coarse && { 'aria-modal': true })}
|
|
169
|
+
data-basalt-legend-disclosure={coarse ? 'sheet' : 'popover'}
|
|
170
|
+
style={{ ...DISCLOSURE_PANEL, fontSize, ...placement }}
|
|
171
|
+
>
|
|
172
|
+
{children}
|
|
173
|
+
</dialog>
|
|
174
|
+
</>,
|
|
175
|
+
document.body,
|
|
176
|
+
)
|
|
177
|
+
}
|
|
@@ -49,6 +49,11 @@
|
|
|
49
49
|
flex-shrink: 0;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/* theme-allow-file raw-media-query — the ControlRow's own C9 swap (pills inline, the rest in a
|
|
53
|
+
sheet) keys on the SIZE CLASS by design: the sheet is compact-class chrome, the same axis the
|
|
54
|
+
shell's bottom bar reads. Every width query here is `SIZE_CLASSES.medium`, pinned by
|
|
55
|
+
`breakpoint-literals.test.ts`. */
|
|
56
|
+
|
|
52
57
|
/* Below `sm` only the first `inline` slots are pills; the rest live in the sheet. */
|
|
53
58
|
@media (max-width: 52.49375em) {
|
|
54
59
|
.pillSlot:not([data-inline]) {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
* width itself, pinned to the `ctl` height so the box is square. This module is unlayered, so it
|
|
46
46
|
* outranks `@layer mantine` regardless of specificity.
|
|
47
47
|
*/
|
|
48
|
+
/* theme-allow raw-media-query — the control's own C9 icon-only swap at the compact size class */
|
|
48
49
|
@media (max-width: 52.49375em) {
|
|
49
50
|
.label,
|
|
50
51
|
.age {
|
|
@@ -15,24 +15,24 @@
|
|
|
15
15
|
* one: a query with no container resolves to false, so a shell-only context would have silently
|
|
16
16
|
* pinned every non-shell consumer to the base count.
|
|
17
17
|
*
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
18
|
+
* The literals are the container-class law (`CONTAINER_CLASSES`, `tokens/size-classes.ts`): the
|
|
19
|
+
* `sm` count opens at `wide` (800px), not at the viewport-era 768. Two are StatGroup's own and say
|
|
20
|
+
* so: 260px (one column, below) and 1200px, where the full `cols` count opens. The class table
|
|
21
|
+
* stops at 800, and 800 is too early for a full KPI row — four cells of ~190px clamp a value a real
|
|
22
|
+
* dashboard prints (`tests/layout/page-aside.layout.test.ts` measures ~195px against the 844px row
|
|
23
|
+
* a claimed aside leaves). Conditions are px, never `em`: `em` inside a `@container` condition
|
|
24
|
+
* resolves against the CONTAINER's font-size, so a consumer panel with smaller text would silently
|
|
25
|
+
* move the boundary. A condition cannot read a custom property, so the numbers are written; the
|
|
26
|
+
* COUNTS are custom properties the component sets per instance.
|
|
23
27
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* condition resolves against the CONTAINER's font-size rather than the root's, so a consumer panel
|
|
28
|
-
* with smaller text would silently move the breakpoint. px is the only spelling that cannot drift.
|
|
29
|
-
* (`47.99375em` → `767.9px`, `74.99375em` → `1199.9px`.) A condition still cannot read a custom property either way, so the numbers are written;
|
|
30
|
-
* the COUNTS are custom properties the component sets per instance.
|
|
28
|
+
* No viewport fallback: basalt's own CSS already requires `color-mix()` (Safari 16.2, Chrome 111,
|
|
29
|
+
* Firefox 113), and every one of those parses `@container` — a `@supports not (container-type)`
|
|
30
|
+
* block is unreachable in any browser that renders basalt at all.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
.container {
|
|
34
34
|
container-type: inline-size;
|
|
35
|
-
container-name: basalt-
|
|
35
|
+
container-name: basalt-grid;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
.root {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
/* 260px is StatGroup's own literal (docs/DESIGN-CORE.md § Layout, elevation, shapes), not a container class: one column below it,
|
|
47
47
|
* so every cell leads a row and the rail draws no leading hairline at all. */
|
|
48
|
-
@container basalt-
|
|
48
|
+
@container basalt-grid (max-width: 259.9px) {
|
|
49
49
|
.root {
|
|
50
50
|
grid-template-columns: minmax(0, 1fr);
|
|
51
51
|
}
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
@container basalt-
|
|
59
|
+
@container basalt-grid (min-width: 800px) {
|
|
60
60
|
.root {
|
|
61
61
|
grid-template-columns: repeat(var(--basalt-stat-group-cols-sm), minmax(0, 1fr));
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
@container basalt-
|
|
65
|
+
@container basalt-grid (min-width: 1200px) {
|
|
66
66
|
.root {
|
|
67
67
|
grid-template-columns: repeat(var(--basalt-stat-group-cols-lg), minmax(0, 1fr));
|
|
68
68
|
}
|
|
@@ -83,14 +83,14 @@
|
|
|
83
83
|
padding-inline-start: var(--vx-space-stack-md);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
@container basalt-
|
|
86
|
+
@container basalt-grid (max-width: 799.9px) {
|
|
87
87
|
.root[data-divided] > *:nth-child(2n + 1) {
|
|
88
88
|
border-inline-start: 0;
|
|
89
89
|
padding-inline-start: 0;
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
@container basalt-
|
|
93
|
+
@container basalt-grid (min-width: 800px) and (max-width: 1199.9px) {
|
|
94
94
|
.root[data-divided][data-sm-cols='2'] > *:nth-child(2n + 1),
|
|
95
95
|
.root[data-divided][data-sm-cols='3'] > *:nth-child(3n + 1) {
|
|
96
96
|
border-inline-start: 0;
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
@container basalt-
|
|
101
|
+
@container basalt-grid (min-width: 1200px) {
|
|
102
102
|
.root[data-divided][data-cols='2'] > *:nth-child(2n + 1),
|
|
103
103
|
.root[data-divided][data-cols='3'] > *:nth-child(3n + 1),
|
|
104
104
|
.root[data-divided][data-cols='4'] > *:nth-child(4n + 1),
|
|
@@ -107,48 +107,3 @@
|
|
|
107
107
|
padding-inline-start: 0;
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
/*
|
|
112
|
-
* The pre-container-query fallback — Safari < 16, Chrome < 106, Firefox < 110. Verbatim the law
|
|
113
|
-
* this file shipped before, keyed on the viewport, and reachable ONLY where `@container` is not.
|
|
114
|
-
* A browser that cannot parse `container-type` also cannot parse the `@container` blocks above, so
|
|
115
|
-
* the two sets are never live at once.
|
|
116
|
-
*/
|
|
117
|
-
@supports not (container-type: inline-size) {
|
|
118
|
-
@media (min-width: 48em) {
|
|
119
|
-
.root {
|
|
120
|
-
grid-template-columns: repeat(var(--basalt-stat-group-cols-sm), minmax(0, 1fr));
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
@media (min-width: 75em) {
|
|
125
|
-
.root {
|
|
126
|
-
grid-template-columns: repeat(var(--basalt-stat-group-cols-lg), minmax(0, 1fr));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
@media (max-width: 47.99375em) {
|
|
131
|
-
.root[data-divided] > *:nth-child(2n + 1) {
|
|
132
|
-
border-inline-start: 0;
|
|
133
|
-
padding-inline-start: 0;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
@media (min-width: 48em) and (max-width: 74.99375em) {
|
|
138
|
-
.root[data-divided][data-sm-cols='2'] > *:nth-child(2n + 1),
|
|
139
|
-
.root[data-divided][data-sm-cols='3'] > *:nth-child(3n + 1) {
|
|
140
|
-
border-inline-start: 0;
|
|
141
|
-
padding-inline-start: 0;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
@media (min-width: 75em) {
|
|
146
|
-
.root[data-divided][data-cols='2'] > *:nth-child(2n + 1),
|
|
147
|
-
.root[data-divided][data-cols='3'] > *:nth-child(3n + 1),
|
|
148
|
-
.root[data-divided][data-cols='4'] > *:nth-child(4n + 1),
|
|
149
|
-
.root[data-divided][data-cols='5'] > *:nth-child(5n + 1) {
|
|
150
|
-
border-inline-start: 0;
|
|
151
|
-
padding-inline-start: 0;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
@@ -15,18 +15,19 @@
|
|
|
15
15
|
* container. A span resolved against the viewport while the tracks follow the container is exactly
|
|
16
16
|
* the phantom-track defect `WidgetGrid.Item`'s context clamp exists to prevent.
|
|
17
17
|
*
|
|
18
|
-
* The literals
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
18
|
+
* The literals are the container-class law, as in `stat-group.module.css`: the `sm` count opens
|
|
19
|
+
* at `wide` (800px); 1200px, where the full `cols` count opens, is the grid's own boundary above
|
|
20
|
+
* the class table's top step. px, never `em` (an `em` condition resolves against the container's
|
|
21
|
+
* font-size). A condition cannot read a custom property, so the numbers are written; the COUNTS
|
|
22
|
+
* are custom properties, set once per instance by the component — three hand-written `repeat()`
|
|
23
|
+
* blocks would have to enumerate every `cols` value, and the whole point of this primitive is that
|
|
24
|
+
* a consumer states one desktop number and basalt owns the rest. No viewport fallback, for the
|
|
25
|
+
* reason `stat-group.module.css` states: nothing that renders basalt lacks `@container`.
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
.container {
|
|
28
29
|
container-type: inline-size;
|
|
29
|
-
container-name: basalt-
|
|
30
|
+
container-name: basalt-grid;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
.root {
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
min-width: 0;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
@container basalt-
|
|
51
|
+
@container basalt-grid (min-width: 800px) {
|
|
51
52
|
.root {
|
|
52
53
|
grid-template-columns: repeat(var(--basalt-widget-grid-cols-sm), minmax(0, 1fr));
|
|
53
54
|
}
|
|
@@ -57,7 +58,7 @@
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
@container basalt-
|
|
61
|
+
@container basalt-grid (min-width: 1200px) {
|
|
61
62
|
.root {
|
|
62
63
|
grid-template-columns: repeat(var(--basalt-widget-grid-cols-lg), minmax(0, 1fr));
|
|
63
64
|
}
|
|
@@ -66,31 +67,3 @@
|
|
|
66
67
|
grid-column: span var(--basalt-widget-grid-span-lg);
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
/*
|
|
71
|
-
* The pre-container-query fallback — Safari < 16, Chrome < 106, Firefox < 110. Verbatim the law
|
|
72
|
-
* this file shipped before, keyed on the viewport, and reachable ONLY where `@container` is not: a
|
|
73
|
-
* browser that cannot parse `container-type` cannot parse the blocks above either, so the two sets
|
|
74
|
-
* are never live at once.
|
|
75
|
-
*/
|
|
76
|
-
@supports not (container-type: inline-size) {
|
|
77
|
-
@media (min-width: 48em) {
|
|
78
|
-
.root {
|
|
79
|
-
grid-template-columns: repeat(var(--basalt-widget-grid-cols-sm), minmax(0, 1fr));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.item {
|
|
83
|
-
grid-column: span var(--basalt-widget-grid-span-sm);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
@media (min-width: 75em) {
|
|
88
|
-
.root {
|
|
89
|
-
grid-template-columns: repeat(var(--basalt-widget-grid-cols-lg), minmax(0, 1fr));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.item {
|
|
93
|
-
grid-column: span var(--basalt-widget-grid-span-lg);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|