@siyahi/ui 0.1.9-next.21 → 0.1.9-next.22
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/DESIGN.md +13 -1
- package/agents/examples/app-sidebar.tsx +3 -1
- package/agents/examples/bar-list-sides.tsx +10 -8
- package/agents/examples/calendar.tsx +3 -7
- package/agents/examples/card.tsx +3 -1
- package/agents/examples/collapsible.tsx +2 -2
- package/agents/examples/command.tsx +22 -20
- package/agents/examples/context-menu.tsx +4 -2
- package/agents/examples/distribution-bar-tug.tsx +24 -22
- package/agents/examples/heatmap-calendar.tsx +9 -7
- package/agents/examples/heatmap-status.tsx +10 -8
- package/agents/examples/heatmap-weekday.tsx +24 -27
- package/agents/examples/price-chart.tsx +2 -2
- package/agents/examples/range-position.tsx +24 -22
- package/agents/examples/scroll-area.tsx +12 -10
- package/agents/examples/surfaces.tsx +4 -2
- package/agents/examples/target-rows.tsx +7 -5
- package/agents/examples/treemap.tsx +12 -10
- package/dist/components/registry/charts/bar-chart-card.js +6 -2
- package/dist/components/registry/dashboard-01/section-cards.js +1 -1
- package/dist/components/registry/dashboardcn/distribution-bar.js +5 -3
- package/dist/components/registry/heatmap/status-heatmap.js +1 -1
- package/dist/components/siyahi/delta.d.ts +5 -1
- package/dist/components/siyahi/delta.js +14 -9
- package/dist/components/siyahi/treemap.js +3 -3
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +1 -1
- package/package.json +1 -1
- package/report.css +14 -14
- package/styles/siyahi.css +92 -20
package/DESIGN.md
CHANGED
|
@@ -13,7 +13,9 @@ a meaning:
|
|
|
13
13
|
- **Ink indigo** is the accent: links, focus rings, the selected sidebar item,
|
|
14
14
|
the first chart series and an `accent` `Callout`. Buttons, badges and toggles
|
|
15
15
|
stay monochrome.
|
|
16
|
-
- **Green and red** mean a gain and a loss
|
|
16
|
+
- **Green and red** mean a gain and a loss. A signed figure is `Delta`: the figure in the text colour with its
|
|
17
|
+
`+` or `−`, and only a small trend icon in green or red. In plain text (labels, tooltips) write the sign; never
|
|
18
|
+
colour a run of figures green and red.
|
|
17
19
|
**Amber** means caution. None of them decorates.
|
|
18
20
|
- **Emphasis tints the whole surface.** A caution is `WarningAlert`; a point set
|
|
19
21
|
apart is `Callout` (`neutral`, `accent`, `warning`, `positive`,
|
|
@@ -25,6 +27,16 @@ a meaning:
|
|
|
25
27
|
with a left (or any single-side) coloured border; tint the box or add an
|
|
26
28
|
icon. 1px neutral dividers between columns are separators, not accents, and
|
|
27
29
|
are fine.
|
|
30
|
+
- **Edges that matter reach 3:1; the rest stay faint.** A field's edge (input,
|
|
31
|
+
textarea, select, input group, the command search) is `--input`, at 3:1 on the
|
|
32
|
+
canvas and the card; dividers, card rings, gridlines and the outlines of
|
|
33
|
+
buttons, toggles and code blocks are the faint `--border`. Focus is an opaque
|
|
34
|
+
`--ring`, or the opaque destructive red on a destructive control. A selected
|
|
35
|
+
toggle or pill tab fills with the foreground. A chart bar that a selection
|
|
36
|
+
dims keeps its full-colour outline and fades only inside.
|
|
37
|
+
- **Destructive is not loss.** `--destructive` (delete, an invalid field) is a
|
|
38
|
+
deeper red than `--negative` (a loss figure), so its label reads on its own
|
|
39
|
+
tint; never use one for the other.
|
|
28
40
|
- Categories (a broker, a sector, an asset class) stay neutral: outline badges,
|
|
29
41
|
never a hue each. Ordered buckets (largest 5, next 5, rest) use steps of one
|
|
30
42
|
colour, not separate hues.
|
|
@@ -32,7 +32,9 @@ export default function AppSidebarExample() {
|
|
|
32
32
|
return (
|
|
33
33
|
// In an app, AppSidebar sits fixed at the edge; collapsible="none" draws it in place here.
|
|
34
34
|
<SidebarProvider className="min-h-0">
|
|
35
|
-
<
|
|
35
|
+
<div className="flex overflow-hidden rounded-lg border">
|
|
36
|
+
<AppSidebar collapsible="none" navMain={views} navLabel="Views" />
|
|
37
|
+
</div>
|
|
36
38
|
</SidebarProvider>
|
|
37
39
|
);
|
|
38
40
|
}
|
|
@@ -23,14 +23,16 @@ export default function BarListSidesExample() {
|
|
|
23
23
|
</CardHeader>
|
|
24
24
|
{/* Each list is a column of its own, headed and ruled off, with its figures at its own right edge, so a
|
|
25
25
|
gain's figure never reads as the label of the loss beside it. */}
|
|
26
|
-
<CardContent
|
|
27
|
-
<div className="md:
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
|
|
26
|
+
<CardContent>
|
|
27
|
+
<div className="grid gap-6 md:grid-cols-2 md:gap-0 md:divide-x">
|
|
28
|
+
<div className="md:pr-6">
|
|
29
|
+
<h3 className="text-muted-foreground mb-2 text-xs font-medium">Gains</h3>
|
|
30
|
+
<BarList data={gains} max={max} color="var(--positive)" valueFormatter={(v) => money(v)} />
|
|
31
|
+
</div>
|
|
32
|
+
<div className="md:pl-6">
|
|
33
|
+
<h3 className="text-muted-foreground mb-2 text-xs font-medium">Losses</h3>
|
|
34
|
+
<BarList data={losses} max={max} color="var(--negative)" valueFormatter={(v) => money(v)} />
|
|
35
|
+
</div>
|
|
34
36
|
</div>
|
|
35
37
|
</CardContent>
|
|
36
38
|
</Card>
|
|
@@ -4,12 +4,8 @@ import { useState } from "react";
|
|
|
4
4
|
export default function CalendarExample() {
|
|
5
5
|
const [date, setDate] = useState<Date | undefined>(new Date(2026, 9, 20));
|
|
6
6
|
return (
|
|
7
|
-
<
|
|
8
|
-
mode="single"
|
|
9
|
-
|
|
10
|
-
onSelect={setDate}
|
|
11
|
-
defaultMonth={new Date(2026, 9, 1)}
|
|
12
|
-
className="w-fit rounded-lg border"
|
|
13
|
-
/>
|
|
7
|
+
<div className="w-fit rounded-lg border">
|
|
8
|
+
<Calendar mode="single" selected={date} onSelect={setDate} defaultMonth={new Date(2026, 9, 1)} />
|
|
9
|
+
</div>
|
|
14
10
|
);
|
|
15
11
|
}
|
package/agents/examples/card.tsx
CHANGED
|
@@ -28,7 +28,9 @@ export default function CardExample() {
|
|
|
28
28
|
<CardContent>
|
|
29
29
|
<p className="text-3xl font-semibold">{money(4512300)}</p>
|
|
30
30
|
</CardContent>
|
|
31
|
-
<CardFooter
|
|
31
|
+
<CardFooter>
|
|
32
|
+
<p className="text-muted-foreground">Updated after the market close</p>
|
|
33
|
+
</CardFooter>
|
|
32
34
|
</Card>
|
|
33
35
|
);
|
|
34
36
|
}
|
|
@@ -8,8 +8,8 @@ export default function CollapsibleExample() {
|
|
|
8
8
|
<ChevronsUpDownIcon />
|
|
9
9
|
How lots are matched
|
|
10
10
|
</CollapsibleTrigger>
|
|
11
|
-
<CollapsibleContent
|
|
12
|
-
Sells are matched to the oldest buy first, as the tax rules require
|
|
11
|
+
<CollapsibleContent>
|
|
12
|
+
<p className="text-muted-foreground">Sells are matched to the oldest buy first, as the tax rules require.</p>
|
|
13
13
|
</CollapsibleContent>
|
|
14
14
|
</Collapsible>
|
|
15
15
|
);
|
|
@@ -3,25 +3,27 @@ import { ChartLineIcon, LandmarkIcon, WalletIcon } from "lucide-react";
|
|
|
3
3
|
|
|
4
4
|
export default function CommandExample() {
|
|
5
5
|
return (
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
6
|
+
<div className="overflow-hidden rounded-lg border">
|
|
7
|
+
<Command>
|
|
8
|
+
<CommandInput placeholder="Jump to a page" />
|
|
9
|
+
<CommandList>
|
|
10
|
+
<CommandEmpty>No page matches.</CommandEmpty>
|
|
11
|
+
<CommandGroup heading="Pages">
|
|
12
|
+
<CommandItem>
|
|
13
|
+
<WalletIcon />
|
|
14
|
+
Holdings
|
|
15
|
+
</CommandItem>
|
|
16
|
+
<CommandItem>
|
|
17
|
+
<ChartLineIcon />
|
|
18
|
+
Realized
|
|
19
|
+
</CommandItem>
|
|
20
|
+
<CommandItem>
|
|
21
|
+
<LandmarkIcon />
|
|
22
|
+
Cash flow
|
|
23
|
+
</CommandItem>
|
|
24
|
+
</CommandGroup>
|
|
25
|
+
</CommandList>
|
|
26
|
+
</Command>
|
|
27
|
+
</div>
|
|
26
28
|
);
|
|
27
29
|
}
|
|
@@ -3,8 +3,10 @@ import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger }
|
|
|
3
3
|
export default function ContextMenuExample() {
|
|
4
4
|
return (
|
|
5
5
|
<ContextMenu>
|
|
6
|
-
<ContextMenuTrigger className="
|
|
7
|
-
|
|
6
|
+
<ContextMenuTrigger className="w-full">
|
|
7
|
+
<div className="flex h-24 items-center justify-center rounded-lg border border-dashed text-muted-foreground">
|
|
8
|
+
Right-click or long-press here
|
|
9
|
+
</div>
|
|
8
10
|
</ContextMenuTrigger>
|
|
9
11
|
<ContextMenuContent>
|
|
10
12
|
<ContextMenuItem>Copy symbol</ContextMenuItem>
|
|
@@ -33,28 +33,30 @@ export default function DistributionBarTugExample() {
|
|
|
33
33
|
<CardTitle>The tug-of-war</CardTitle>
|
|
34
34
|
<CardDescription>Each closed position, losses left of break-even and gains right</CardDescription>
|
|
35
35
|
</CardHeader>
|
|
36
|
-
<CardContent
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
name,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
36
|
+
<CardContent>
|
|
37
|
+
<div className="flex flex-col gap-2">
|
|
38
|
+
<DistributionBar
|
|
39
|
+
min={-reach}
|
|
40
|
+
max={reach}
|
|
41
|
+
showLegend={false}
|
|
42
|
+
markers={[{ at: 0 }]}
|
|
43
|
+
highlighted={focus?.name ?? null}
|
|
44
|
+
onHover={setFocus}
|
|
45
|
+
segments={positions.map(({ name, value }) => ({
|
|
46
|
+
name,
|
|
47
|
+
value,
|
|
48
|
+
color: value > 0 ? "var(--positive)" : "var(--negative)",
|
|
49
|
+
}))}
|
|
50
|
+
/>
|
|
51
|
+
<div className="grid grid-cols-3 text-xs text-muted-foreground tabular-nums">
|
|
52
|
+
<span>
|
|
53
|
+
losers <Delta value={lost} format="money" />
|
|
54
|
+
</span>
|
|
55
|
+
<span className="text-center">break-even</span>
|
|
56
|
+
<span className="text-right">
|
|
57
|
+
winners <Delta value={won} format="money" />
|
|
58
|
+
</span>
|
|
59
|
+
</div>
|
|
58
60
|
</div>
|
|
59
61
|
</CardContent>
|
|
60
62
|
<CardFooter className="min-h-12" aria-live="polite">
|
|
@@ -45,13 +45,15 @@ export default function HeatmapCalendarExample() {
|
|
|
45
45
|
/>
|
|
46
46
|
)}
|
|
47
47
|
</CalendarHeatmapBody>
|
|
48
|
-
<CalendarHeatmapFooter className="
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
<CalendarHeatmapFooter className="whitespace-normal">
|
|
49
|
+
<div className="flex w-full flex-wrap items-center justify-between gap-x-6 gap-y-2">
|
|
50
|
+
<p aria-live="polite" className="text-sm">
|
|
51
|
+
{picked
|
|
52
|
+
? `${picked.date}: ${count(picked.value)} trades`
|
|
53
|
+
: "Select a day for its trade count. Use arrow keys to move between days."}
|
|
54
|
+
</p>
|
|
55
|
+
<CalendarHeatmapLegend labels={{ less: "0 trades", more: `${count(busiest)} trades` }} />
|
|
56
|
+
</div>
|
|
55
57
|
</CalendarHeatmapFooter>
|
|
56
58
|
</CalendarHeatmap>
|
|
57
59
|
</CardContent>
|
|
@@ -41,14 +41,16 @@ export default function HeatmapStatusExample() {
|
|
|
41
41
|
/>
|
|
42
42
|
)}
|
|
43
43
|
</StatusHeatmapBody>
|
|
44
|
-
<StatusHeatmapFooter className="
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
44
|
+
<StatusHeatmapFooter className="whitespace-normal">
|
|
45
|
+
<div className="flex w-full flex-wrap items-center justify-between gap-x-6 gap-y-2">
|
|
46
|
+
<p aria-live="polite" className="text-sm">
|
|
47
|
+
{picked
|
|
48
|
+
? `${picked.date}: ${["unknown", "critical", "degraded", "healthy"][picked.value] ?? "unknown"}`
|
|
49
|
+
: "Select a day for its health. Use arrow keys to move between days."}
|
|
50
|
+
</p>
|
|
51
|
+
{/* No day lacks data, so the legend leaves out "No Data". */}
|
|
52
|
+
<StatusHeatmapLegend />
|
|
53
|
+
</div>
|
|
52
54
|
</StatusHeatmapFooter>
|
|
53
55
|
</StatusHeatmap>
|
|
54
56
|
</CardContent>
|
|
@@ -39,34 +39,31 @@ export default function HeatmapWeekdayExample() {
|
|
|
39
39
|
{/* Seven hours by five days is a small grid: capped, it keeps square cells, and the legend and the answer
|
|
40
40
|
sit beside it instead of a wide empty card. */}
|
|
41
41
|
<CardContent>
|
|
42
|
-
<WeekdayHeatmap
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
42
|
+
<WeekdayHeatmap data={data} hours={[9, 10, 11, 12, 13, 14, 15]} weekdays={[1, 2, 3, 4, 5]}>
|
|
43
|
+
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:gap-8">
|
|
44
|
+
<WeekdayHeatmapBody className="lg:w-md lg:shrink-0">
|
|
45
|
+
{({ activity }) => (
|
|
46
|
+
<WeekdayHeatmapBlock
|
|
47
|
+
activity={activity}
|
|
48
|
+
selected={isPicked(activity)}
|
|
49
|
+
onCellClick={(cell) => setPicked(isPicked(cell) ? null : cell)}
|
|
50
|
+
/>
|
|
51
|
+
)}
|
|
52
|
+
</WeekdayHeatmapBody>
|
|
53
|
+
<div className="flex flex-col items-start gap-3 text-sm">
|
|
54
|
+
<WeekdayHeatmapLegend
|
|
55
|
+
className="ml-0"
|
|
56
|
+
labels={{ less: "0 orders", more: `${count(busiest.value)} orders an hour` }}
|
|
54
57
|
/>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
</p>
|
|
65
|
-
<p aria-live="polite" className="text-muted-foreground">
|
|
66
|
-
{picked
|
|
67
|
-
? `${DAYS[picked.weekday]}, ${hourLabel(picked.hour)} IST: ${count(picked.value)} orders`
|
|
68
|
-
: "Select a cell for its order count. Use arrow keys to move between cells."}
|
|
69
|
-
</p>
|
|
58
|
+
<p>
|
|
59
|
+
Busiest: {DAYS[busiest.weekday]} at {hourLabel(busiest.hour)}, {count(busiest.value)} orders
|
|
60
|
+
</p>
|
|
61
|
+
<p aria-live="polite" className="text-muted-foreground">
|
|
62
|
+
{picked
|
|
63
|
+
? `${DAYS[picked.weekday]}, ${hourLabel(picked.hour)} IST: ${count(picked.value)} orders`
|
|
64
|
+
: "Select a cell for its order count. Use arrow keys to move between cells."}
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
70
67
|
</div>
|
|
71
68
|
</WeekdayHeatmap>
|
|
72
69
|
</CardContent>
|
|
@@ -16,8 +16,8 @@ export default function PriceChartExample() {
|
|
|
16
16
|
<CardContent>
|
|
17
17
|
<PriceChart data={data} height={256} />
|
|
18
18
|
</CardContent>
|
|
19
|
-
<CardFooter
|
|
20
|
-
<span>
|
|
19
|
+
<CardFooter>
|
|
20
|
+
<span className="text-xs text-muted-foreground">
|
|
21
21
|
Chart by{" "}
|
|
22
22
|
<a
|
|
23
23
|
className="text-link underline-offset-4 hover:underline"
|
|
@@ -25,28 +25,30 @@ export default function RangePositionExample() {
|
|
|
25
25
|
<CardTitle>HDFCBANK sits {f.pct((close - low) / (high - low), 0)} of the way up its 52-week range</CardTitle>
|
|
26
26
|
<CardDescription>The last close against the year's lowest and highest close</CardDescription>
|
|
27
27
|
</CardHeader>
|
|
28
|
-
<CardContent
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
28
|
+
<CardContent>
|
|
29
|
+
<div className="flex flex-col gap-8 @xl/range:flex-row @xl/range:items-end">
|
|
30
|
+
<DataList className="@xl/range:w-56 @xl/range:shrink-0">
|
|
31
|
+
<DataListItem label="52-week low, 7 months ago">
|
|
32
|
+
<span className="tabular-nums">{price(low)}</span>
|
|
33
|
+
</DataListItem>
|
|
34
|
+
<DataListItem label="52-week high, 11 weeks ago">
|
|
35
|
+
<span className="tabular-nums">{price(high)}</span>
|
|
36
|
+
</DataListItem>
|
|
37
|
+
<DataListItem label="Last close">
|
|
38
|
+
<span className="tabular-nums">{price(close)}</span>
|
|
39
|
+
</DataListItem>
|
|
40
|
+
</DataList>
|
|
41
|
+
<RangePosition
|
|
42
|
+
low={low}
|
|
43
|
+
high={high}
|
|
44
|
+
value={close}
|
|
45
|
+
lowLabel="52-week low"
|
|
46
|
+
highLabel="52-week high"
|
|
47
|
+
caption="of the way up the 52-week range"
|
|
48
|
+
valueFormatter={price}
|
|
49
|
+
className="@xl/range:flex-1"
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
50
52
|
</CardContent>
|
|
51
53
|
</Card>
|
|
52
54
|
);
|
|
@@ -6,15 +6,17 @@ const months = Array.from({ length: 24 }, (_, i) =>
|
|
|
6
6
|
|
|
7
7
|
export default function ScrollAreaExample() {
|
|
8
8
|
return (
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<div
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
<div className="h-48 w-56 overflow-hidden rounded-lg border">
|
|
10
|
+
<ScrollArea className="h-full">
|
|
11
|
+
<div className="p-4">
|
|
12
|
+
{months.map((month) => (
|
|
13
|
+
<div key={month}>
|
|
14
|
+
<div className="text-sm">{month}</div>
|
|
15
|
+
<Separator className="my-2" />
|
|
16
|
+
</div>
|
|
17
|
+
))}
|
|
18
|
+
</div>
|
|
19
|
+
</ScrollArea>
|
|
20
|
+
</div>
|
|
19
21
|
);
|
|
20
22
|
}
|
|
@@ -75,8 +75,10 @@ function Dashboard() {
|
|
|
75
75
|
<TableBody>
|
|
76
76
|
{holdings.map((row) => (
|
|
77
77
|
<TableRow key={row.symbol}>
|
|
78
|
-
<TableCell
|
|
79
|
-
|
|
78
|
+
<TableCell>
|
|
79
|
+
<span className="font-mono">{row.symbol}</span>
|
|
80
|
+
</TableCell>
|
|
81
|
+
<TableCell>{money(row.value)}</TableCell>
|
|
80
82
|
</TableRow>
|
|
81
83
|
))}
|
|
82
84
|
</TableBody>
|
|
@@ -59,11 +59,13 @@ export default function TargetRowsExample() {
|
|
|
59
59
|
<CardTitle>Debt is 5.6 points under its target</CardTitle>
|
|
60
60
|
<CardDescription>Each sleeve's share of the portfolio against the plan, on one 0–50% scale</CardDescription>
|
|
61
61
|
</CardHeader>
|
|
62
|
-
<CardContent
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
<CardContent>
|
|
63
|
+
<div className="grid gap-2">
|
|
64
|
+
<p className="text-sm text-muted-foreground">
|
|
65
|
+
Bar: today's weight. Tall tick: the target. Shading: within 5 points of it, darker within 2.
|
|
66
|
+
</p>
|
|
67
|
+
<TargetRows rows={rows} domain={[0, 0.5]} valueFormatter={(value) => f.pct(value)} />
|
|
68
|
+
</div>
|
|
67
69
|
</CardContent>
|
|
68
70
|
</Card>
|
|
69
71
|
);
|
|
@@ -57,16 +57,18 @@ export default function TreemapExample() {
|
|
|
57
57
|
onSelect={setSelected}
|
|
58
58
|
/>
|
|
59
59
|
</TooltipTrigger>
|
|
60
|
-
<TooltipContent side="bottom"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
<TooltipContent side="bottom">
|
|
61
|
+
<div className="flex flex-col items-start">
|
|
62
|
+
{hovered && (
|
|
63
|
+
<>
|
|
64
|
+
<span className="font-medium">
|
|
65
|
+
{hovered.name} · {hovered.broker}
|
|
66
|
+
</span>
|
|
67
|
+
<span>{hovered.group}</span>
|
|
68
|
+
<span>{figures(hovered)}</span>
|
|
69
|
+
</>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
70
72
|
</TooltipContent>
|
|
71
73
|
</Tooltip>
|
|
72
74
|
<TreemapLegend range={0.6} className="mt-3 ml-auto" />
|
|
@@ -42,9 +42,13 @@ function BarChartCard({ stacked = false, selected = null, onSelect, ...props })
|
|
|
42
42
|
const tone = negativeColor && Number(row[key]) < 0 ? negativeColor : color;
|
|
43
43
|
return negativeColor ? /* @__PURE__ */ jsx(Cell, {
|
|
44
44
|
fill: signedFill(tone, isSelected),
|
|
45
|
-
stroke:
|
|
45
|
+
stroke: tone,
|
|
46
46
|
strokeWidth: 1.5
|
|
47
|
-
}, i) : /* @__PURE__ */ jsx(Cell, {
|
|
47
|
+
}, i) : /* @__PURE__ */ jsx(Cell, {
|
|
48
|
+
fill: dimUnselected(tone, isSelected),
|
|
49
|
+
stroke: tone,
|
|
50
|
+
strokeWidth: 1.5
|
|
51
|
+
}, i);
|
|
48
52
|
})
|
|
49
53
|
}, key);
|
|
50
54
|
})
|
|
@@ -31,7 +31,7 @@ function SectionCards({ items, selected, onSelect }) {
|
|
|
31
31
|
"aria-pressed": item.label === selected,
|
|
32
32
|
disabled: item.disabled,
|
|
33
33
|
onClick: () => onSelect(item.label),
|
|
34
|
-
className: "absolute inset-0 z-10 rounded-[inherit] outline-none focus-visible:ring-3 focus-visible:ring-ring
|
|
34
|
+
className: "absolute inset-0 z-10 rounded-[inherit] outline-none focus-visible:ring-3 focus-visible:ring-ring focus-visible:ring-inset"
|
|
35
35
|
}),
|
|
36
36
|
/* @__PURE__ */ jsxs(CardHeader, { children: [
|
|
37
37
|
/* @__PURE__ */ jsx(CardDescription, { children: item.label }),
|
|
@@ -66,12 +66,14 @@ function DistributionBar({ segments, valueFormatter, showLegend = true, showValu
|
|
|
66
66
|
onMouseLeave: onHover ? () => onHover(null) : void 0,
|
|
67
67
|
children: resolved.map((segment) => segment.width > 0 ? /* @__PURE__ */ jsx("div", {
|
|
68
68
|
"data-slot": "distribution-segment",
|
|
69
|
-
className: "border-card absolute inset-y-0 border-x transition-[width,left,
|
|
69
|
+
className: "border-card absolute inset-y-0 border-x transition-[width,left,background-color] duration-300",
|
|
70
70
|
style: {
|
|
71
71
|
left: `${segment.left}%`,
|
|
72
72
|
width: `${segment.width}%`,
|
|
73
|
-
backgroundColor: segment.color
|
|
74
|
-
|
|
73
|
+
...highlighted === null || highlighted === segment.key ? { backgroundColor: segment.color } : {
|
|
74
|
+
backgroundColor: `color-mix(in oklab, ${segment.color} 35%, var(--card))`,
|
|
75
|
+
boxShadow: `inset 0 0 0 1px ${segment.color}`
|
|
76
|
+
}
|
|
75
77
|
},
|
|
76
78
|
onMouseEnter: onHover ? () => onHover(segment) : void 0
|
|
77
79
|
}, segment.key) : null)
|
|
@@ -322,7 +322,7 @@ const StatusHeatmapLegend = ({ labels: labelsProp, className, children, ...props
|
|
|
322
322
|
return /* @__PURE__ */ jsx("fieldset", {
|
|
323
323
|
"data-slot": "status-heatmap-legend",
|
|
324
324
|
"aria-label": labels.legendLabel ?? "Status legend",
|
|
325
|
-
className: cn("ml-auto flex items-center gap-
|
|
325
|
+
className: cn("ml-auto flex items-center gap-3", className),
|
|
326
326
|
...props,
|
|
327
327
|
children: statuses.map((status) => children ? /* @__PURE__ */ jsx(Fragment$1, { children: children({
|
|
328
328
|
value: status.value,
|
|
@@ -5,6 +5,10 @@ export interface DeltaProps {
|
|
|
5
5
|
format?: "pct" | "money";
|
|
6
6
|
className?: string;
|
|
7
7
|
}
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* A signed change: the figure in the surrounding text colour with its sign, and a trend icon in --positive or
|
|
10
|
+
* --negative. The sign and the icon's shape carry the direction, so colour is never the only cue, and a column of
|
|
11
|
+
* changes reads as figures rather than a wall of red and green.
|
|
12
|
+
*/
|
|
9
13
|
export declare function Delta({ value, format, className }: DeltaProps): import("react").JSX.Element;
|
|
10
14
|
//#endregion
|
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
import { useFormat } from "../../lib/format.js";
|
|
2
2
|
import { cn } from "cn";
|
|
3
|
+
import { MinusIcon, TrendingDownIcon, TrendingUpIcon } from "lucide-react";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
//#region src/components/siyahi/delta.tsx
|
|
5
|
-
|
|
6
|
+
const MINUS = "−";
|
|
7
|
+
/**
|
|
8
|
+
* A signed change: the figure in the surrounding text colour with its sign, and a trend icon in --positive or
|
|
9
|
+
* --negative. The sign and the icon's shape carry the direction, so colour is never the only cue, and a column of
|
|
10
|
+
* changes reads as figures rather than a wall of red and green.
|
|
11
|
+
*/
|
|
6
12
|
function Delta({ value, format = "pct", className }) {
|
|
7
13
|
const f = useFormat();
|
|
8
14
|
const size = format === "money" ? f.money(Math.abs(value)) : f.pct(Math.abs(value));
|
|
9
|
-
const direction = value > 0 ? "up" : value < 0 ? "down" :
|
|
15
|
+
const direction = value > 0 ? "up" : value < 0 ? "down" : "none";
|
|
16
|
+
const Icon = direction === "up" ? TrendingUpIcon : direction === "down" ? TrendingDownIcon : MinusIcon;
|
|
10
17
|
return /* @__PURE__ */ jsxs("span", {
|
|
11
18
|
"data-slot": "delta",
|
|
12
|
-
|
|
19
|
+
"data-direction": direction,
|
|
20
|
+
className: cn("inline-flex items-center gap-1 whitespace-nowrap", className),
|
|
13
21
|
children: [
|
|
14
|
-
|
|
22
|
+
/* @__PURE__ */ jsx(Icon, {
|
|
15
23
|
"aria-hidden": "true",
|
|
16
|
-
|
|
17
|
-
}),
|
|
18
|
-
direction && /* @__PURE__ */ jsx("span", {
|
|
19
|
-
className: "sr-only",
|
|
20
|
-
children: direction
|
|
24
|
+
className: cn("size-3.5 shrink-0", direction === "up" && "text-positive", direction === "down" && "text-negative", direction === "none" && "text-muted-foreground")
|
|
21
25
|
}),
|
|
26
|
+
direction === "up" ? "+" : direction === "down" ? MINUS : "",
|
|
22
27
|
size
|
|
23
28
|
]
|
|
24
29
|
});
|
|
@@ -27,7 +27,7 @@ function Treemap({ items, height, range, onHover, selected, onSelect }) {
|
|
|
27
27
|
height
|
|
28
28
|
]);
|
|
29
29
|
const focusKey = tiles.some(({ item }) => (item.key ?? item.name) === focusedKey) ? focusedKey : tiles[0]?.item.key ?? tiles[0]?.item.name;
|
|
30
|
-
const change = (value) => value === null ? "—" : `${value > 0 ? "
|
|
30
|
+
const change = (value) => value === null ? "—" : `${value > 0 ? "+" : value < 0 ? "−" : ""}${pct(Math.abs(value))}`;
|
|
31
31
|
return /* @__PURE__ */ jsx("div", {
|
|
32
32
|
ref,
|
|
33
33
|
"data-slot": "treemap",
|
|
@@ -193,9 +193,9 @@ function TreemapLegend({ range, className }) {
|
|
|
193
193
|
/* @__PURE__ */ jsxs("div", {
|
|
194
194
|
className: "flex justify-between gap-2 tabular-nums",
|
|
195
195
|
children: [
|
|
196
|
-
/* @__PURE__ */ jsxs("span", { children: ["
|
|
196
|
+
/* @__PURE__ */ jsxs("span", { children: ["down over ", over] }),
|
|
197
197
|
/* @__PURE__ */ jsxs("span", { children: [pct(step, digits), " a step"] }),
|
|
198
|
-
/* @__PURE__ */ jsxs("span", { children: ["
|
|
198
|
+
/* @__PURE__ */ jsxs("span", { children: ["up over ", over] })
|
|
199
199
|
]
|
|
200
200
|
}),
|
|
201
201
|
/* @__PURE__ */ jsxs("p", {
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare function money(value: number, locale?: SiyahiLocale, fractionDigi
|
|
|
6
6
|
* A short amount: ₹2.4L and ₹40Cr in en-IN, $2.4M in en-US. Indian readers
|
|
7
7
|
* count in thousands up to a lakh, so in an Indian-region locale amounts under
|
|
8
8
|
* one lakh print in full (₹99,999), never as ₹99.9K. Signed figures belong in
|
|
9
|
-
* `Delta`, which adds the
|
|
9
|
+
* `Delta`, which adds the sign and a trend icon.
|
|
10
10
|
*
|
|
11
11
|
* Give `scale`, the largest amount on the same axis, and every tick takes that
|
|
12
12
|
* amount's unit, to two decimals so close ticks stay apart: ₹1.5L, ₹0.75L,
|
package/dist/lib/format.js
CHANGED
|
@@ -34,7 +34,7 @@ const NUMBER = /* @__PURE__ */ new Set([
|
|
|
34
34
|
* A short amount: ₹2.4L and ₹40Cr in en-IN, $2.4M in en-US. Indian readers
|
|
35
35
|
* count in thousands up to a lakh, so in an Indian-region locale amounts under
|
|
36
36
|
* one lakh print in full (₹99,999), never as ₹99.9K. Signed figures belong in
|
|
37
|
-
* `Delta`, which adds the
|
|
37
|
+
* `Delta`, which adds the sign and a trend icon.
|
|
38
38
|
*
|
|
39
39
|
* Give `scale`, the largest amount on the same axis, and every tick takes that
|
|
40
40
|
* amount's unit, to two decimals so close ticks stay apart: ₹1.5L, ₹0.75L,
|
package/package.json
CHANGED
package/report.css
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
:root {
|
|
7
7
|
color-scheme: light dark;
|
|
8
|
-
--radius: 0.
|
|
8
|
+
--radius: 0.45rem;
|
|
9
9
|
--background: oklch(0.985 0 0);
|
|
10
10
|
--foreground: oklch(0.145 0 0);
|
|
11
11
|
--card: oklch(1 0 0);
|
|
@@ -17,24 +17,24 @@
|
|
|
17
17
|
--secondary: oklch(0.97 0 0);
|
|
18
18
|
--secondary-foreground: oklch(0.205 0 0);
|
|
19
19
|
--muted: oklch(0.97 0 0);
|
|
20
|
-
--muted-foreground: oklch(0.
|
|
20
|
+
--muted-foreground: oklch(0.51 0 0);
|
|
21
21
|
--accent: oklch(0.97 0 0);
|
|
22
22
|
--accent-foreground: oklch(0.205 0 0);
|
|
23
|
-
--destructive: oklch(0.
|
|
23
|
+
--destructive: oklch(0.49 0.19 27.325);
|
|
24
24
|
--border: oklch(0.922 0 0);
|
|
25
|
-
--input: oklch(0.
|
|
25
|
+
--input: oklch(0.64 0 0);
|
|
26
26
|
--ring: oklch(0.62 0.16 275);
|
|
27
|
-
--positive: oklch(0.527 0.
|
|
27
|
+
--positive: oklch(0.527 0.145 150.069);
|
|
28
28
|
--positive-foreground: oklch(0.985 0 0);
|
|
29
|
-
--negative: oklch(0.577 0.
|
|
29
|
+
--negative: oklch(0.577 0.235 27.325);
|
|
30
30
|
--negative-foreground: oklch(0.985 0 0);
|
|
31
|
-
--warning: oklch(0.555 0.
|
|
31
|
+
--warning: oklch(0.555 0.149 48.998);
|
|
32
32
|
--warning-foreground: oklch(0.985 0 0);
|
|
33
33
|
--link: oklch(0.51 0.2 275);
|
|
34
34
|
--chart-1: oklch(0.4 0.17 275);
|
|
35
35
|
--chart-2: oklch(0.66 0.13 70);
|
|
36
36
|
--chart-3: oklch(0.38 0.13 335);
|
|
37
|
-
--chart-4: oklch(0.
|
|
37
|
+
--chart-4: oklch(0.65 0.11 235);
|
|
38
38
|
--chart-5: oklch(0.36 0.06 205);
|
|
39
39
|
--sidebar: oklch(0.985 0 0);
|
|
40
40
|
--sidebar-foreground: oklch(0.145 0 0);
|
|
@@ -62,17 +62,17 @@
|
|
|
62
62
|
--muted-foreground: oklch(0.74 0 0);
|
|
63
63
|
--accent: oklch(0.269 0 0);
|
|
64
64
|
--accent-foreground: oklch(0.922 0 0);
|
|
65
|
-
--destructive: oklch(0.
|
|
65
|
+
--destructive: oklch(0.8 0.1 22.216);
|
|
66
66
|
--border: oklch(1 0 0 / 10%);
|
|
67
|
-
--input: oklch(
|
|
67
|
+
--input: oklch(0.62 0 0);
|
|
68
68
|
--ring: oklch(0.6 0.14 275);
|
|
69
|
-
--positive: oklch(0.
|
|
69
|
+
--positive: oklch(0.69 0.16 151.711);
|
|
70
70
|
--positive-foreground: oklch(0.145 0 0);
|
|
71
|
-
--negative: oklch(0.704 0.
|
|
71
|
+
--negative: oklch(0.704 0.187 22.216);
|
|
72
72
|
--negative-foreground: oklch(0.145 0 0);
|
|
73
|
-
--warning: oklch(0.828 0.
|
|
73
|
+
--warning: oklch(0.828 0.169 84.429);
|
|
74
74
|
--warning-foreground: oklch(0.145 0 0);
|
|
75
|
-
--link: oklch(0.75 0.
|
|
75
|
+
--link: oklch(0.75 0.128 275);
|
|
76
76
|
--chart-1: oklch(0.58 0.2 275);
|
|
77
77
|
--chart-2: oklch(0.88 0.08 75);
|
|
78
78
|
--chart-3: oklch(0.58 0.16 335);
|
package/styles/siyahi.css
CHANGED
|
@@ -83,7 +83,8 @@
|
|
|
83
83
|
|
|
84
84
|
:root {
|
|
85
85
|
color-scheme: light dark;
|
|
86
|
-
|
|
86
|
+
/* shadcn's "small": crisper than its 0.625rem default, still not square. Cards 10px, buttons 7px. */
|
|
87
|
+
--radius: 0.45rem;
|
|
87
88
|
--background: oklch(0.985 0 0);
|
|
88
89
|
--foreground: oklch(0.145 0 0);
|
|
89
90
|
--card: oklch(1 0 0);
|
|
@@ -95,24 +96,27 @@
|
|
|
95
96
|
--secondary: oklch(0.97 0 0);
|
|
96
97
|
--secondary-foreground: oklch(0.205 0 0);
|
|
97
98
|
--muted: oklch(0.97 0 0);
|
|
98
|
-
|
|
99
|
+
/* Below shadcn's 0.556, so muted text keeps 4.5:1 on the muted fill and on every Callout's tint. */
|
|
100
|
+
--muted-foreground: oklch(0.51 0 0);
|
|
99
101
|
--accent: oklch(0.97 0 0);
|
|
100
102
|
--accent-foreground: oklch(0.205 0 0);
|
|
101
|
-
|
|
103
|
+
/* Deeper than the loss red, so a destructive button's label reads on its own red tint, hovered too. */
|
|
104
|
+
--destructive: oklch(0.49 0.19 27.325);
|
|
102
105
|
--border: oklch(0.922 0 0);
|
|
103
|
-
|
|
106
|
+
/* The edge of a field (input, textarea, select, input group) at 3:1: --border stays faint for dividers. */
|
|
107
|
+
--input: oklch(0.64 0 0);
|
|
104
108
|
--ring: oklch(0.62 0.16 275);
|
|
105
|
-
/* Tailwind green-700, red-600, amber-700,
|
|
106
|
-
--positive: oklch(0.527 0.
|
|
109
|
+
/* Tailwind green-700, red-600, amber-700, with their chroma lowered to the sRGB edge. */
|
|
110
|
+
--positive: oklch(0.527 0.145 150.069);
|
|
107
111
|
--positive-foreground: oklch(0.985 0 0);
|
|
108
|
-
--negative: oklch(0.577 0.
|
|
112
|
+
--negative: oklch(0.577 0.235 27.325);
|
|
109
113
|
--negative-foreground: oklch(0.985 0 0);
|
|
110
|
-
--warning: oklch(0.555 0.
|
|
114
|
+
--warning: oklch(0.555 0.149 48.998);
|
|
111
115
|
--warning-foreground: oklch(0.985 0 0);
|
|
112
116
|
--link: oklch(0.51 0.2 275);
|
|
113
117
|
/*
|
|
114
118
|
* Indigo, amber, plum, sky, teal: no green or red, so no series reads as a
|
|
115
|
-
* gain or a loss. Each slot reaches 3:1 against the card and
|
|
119
|
+
* gain or a loss. Each slot reaches 3:1 against the canvas, the card and the
|
|
116
120
|
* slots next to it, which only lightness can give, so indigo, plum and teal
|
|
117
121
|
* are the deep members and amber and sky the pale ones, in both schemes: a
|
|
118
122
|
* pale indigo in dark read as lavender and lost the accent. Slots two apart
|
|
@@ -123,7 +127,7 @@
|
|
|
123
127
|
--chart-1: oklch(0.4 0.17 275);
|
|
124
128
|
--chart-2: oklch(0.66 0.13 70);
|
|
125
129
|
--chart-3: oklch(0.38 0.13 335);
|
|
126
|
-
--chart-4: oklch(0.
|
|
130
|
+
--chart-4: oklch(0.65 0.11 235);
|
|
127
131
|
--chart-5: oklch(0.36 0.06 205);
|
|
128
132
|
--sidebar: oklch(0.985 0 0);
|
|
129
133
|
--sidebar-foreground: oklch(0.145 0 0);
|
|
@@ -159,18 +163,23 @@
|
|
|
159
163
|
--muted-foreground: oklch(0.74 0 0);
|
|
160
164
|
--accent: oklch(0.269 0 0);
|
|
161
165
|
--accent-foreground: oklch(0.922 0 0);
|
|
162
|
-
|
|
166
|
+
/* Paler and calmer than the loss red, so a destructive label reads on its red tint over the card, hovered too. */
|
|
167
|
+
--destructive: oklch(0.8 0.1 22.216);
|
|
163
168
|
--border: oklch(1 0 0 / 10%);
|
|
164
|
-
--input: oklch(
|
|
169
|
+
--input: oklch(0.62 0 0);
|
|
165
170
|
--ring: oklch(0.6 0.14 275);
|
|
166
|
-
/*
|
|
167
|
-
|
|
171
|
+
/*
|
|
172
|
+
* Green toned down from Tailwind green-400, below red-400's lightness so a
|
|
173
|
+
* treemap's deepest green tile keeps its light label at 4.5:1; amber-400.
|
|
174
|
+
* Red, amber and the link sit at the sRGB edge.
|
|
175
|
+
*/
|
|
176
|
+
--positive: oklch(0.69 0.16 151.711);
|
|
168
177
|
--positive-foreground: oklch(0.145 0 0);
|
|
169
|
-
--negative: oklch(0.704 0.
|
|
178
|
+
--negative: oklch(0.704 0.187 22.216);
|
|
170
179
|
--negative-foreground: oklch(0.145 0 0);
|
|
171
|
-
--warning: oklch(0.828 0.
|
|
180
|
+
--warning: oklch(0.828 0.169 84.429);
|
|
172
181
|
--warning-foreground: oklch(0.145 0 0);
|
|
173
|
-
--link: oklch(0.75 0.
|
|
182
|
+
--link: oklch(0.75 0.128 275);
|
|
174
183
|
--chart-1: oklch(0.58 0.2 275);
|
|
175
184
|
--chart-2: oklch(0.88 0.08 75);
|
|
176
185
|
--chart-3: oklch(0.58 0.16 335);
|
|
@@ -213,15 +222,78 @@
|
|
|
213
222
|
/*
|
|
214
223
|
* Every selected toggle, a view switch or a filter chip alike, fills with the
|
|
215
224
|
* foreground, black on white and white on black, so the reader sees at once
|
|
216
|
-
* what is on; stock shadcn marks it with a faint grey.
|
|
217
|
-
* the stock
|
|
225
|
+
* what is on; stock shadcn marks it with a faint grey. A default (pill) tab
|
|
226
|
+
* selects the same way: stock fills it with the canvas colour, 1.05:1 against
|
|
227
|
+
* its grey list. Unlayered, to outrank the stock aria-pressed:bg-muted.
|
|
218
228
|
*/
|
|
219
|
-
:is([data-slot="toggle"], [data-slot="toggle-group-item"]):is([aria-pressed="true"], [data-pressed])
|
|
229
|
+
:is([data-slot="toggle"], [data-slot="toggle-group-item"]):is([aria-pressed="true"], [data-pressed]),
|
|
230
|
+
[data-slot="tabs-list"][data-variant="default"] [data-slot="tabs-trigger"][data-active] {
|
|
220
231
|
background-color: var(--foreground);
|
|
221
232
|
color: var(--background);
|
|
222
233
|
border-color: var(--foreground);
|
|
223
234
|
}
|
|
224
235
|
|
|
236
|
+
/*
|
|
237
|
+
* --input is the edge of a field, where the reader types or picks. A button,
|
|
238
|
+
* a toggle and a code block are framed by their label or their content, so
|
|
239
|
+
* their outline stays the faint divider colour; scoping the variable keeps
|
|
240
|
+
* the fill each derives from it faint too.
|
|
241
|
+
*/
|
|
242
|
+
:is([data-slot="button"], [data-slot="toggle"], [data-slot="toggle-group-item"], [data-slot="code-block"]) {
|
|
243
|
+
--input: var(--border);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/*
|
|
247
|
+
* The command palette's search field is a field: the --input edge on a muted
|
|
248
|
+
* fill, and the muted placeholder every other field uses (stock leaves it at
|
|
249
|
+
* Tailwind's half-strength text, 3.7:1).
|
|
250
|
+
*/
|
|
251
|
+
[data-slot="command-input-wrapper"] > [data-slot="input-group"] {
|
|
252
|
+
border-color: var(--input);
|
|
253
|
+
background-color: color-mix(in oklab, var(--muted) 50%, transparent);
|
|
254
|
+
}
|
|
255
|
+
[data-slot="command-input"]::placeholder {
|
|
256
|
+
color: var(--muted-foreground);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/*
|
|
260
|
+
* Focus is opaque wherever the ring is the only sign of it: stock draws the
|
|
261
|
+
* ring at half strength (under 2:1 on the canvas) and relies on an opaque
|
|
262
|
+
* border these controls do not have. The slider's thumb is a div around a
|
|
263
|
+
* hidden range input, so it shows the input's focus.
|
|
264
|
+
*/
|
|
265
|
+
:is([data-slot="toggle"], [data-slot="toggle-group-item"], [data-slot="scroll-area-viewport"]):focus-visible,
|
|
266
|
+
[data-slot="button"][data-day]:focus-visible {
|
|
267
|
+
--tw-ring-color: var(--ring);
|
|
268
|
+
}
|
|
269
|
+
[data-slot="slider-thumb"]:has(input:focus-visible) {
|
|
270
|
+
box-shadow: 0 0 0 3px var(--ring);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/*
|
|
274
|
+
* A destructive control focuses, and an invalid field is edged, in the full
|
|
275
|
+
* destructive colour; stock fades both to 40-50%, under 3:1.
|
|
276
|
+
*/
|
|
277
|
+
[data-slot="button"][class~="text-destructive"]:focus-visible,
|
|
278
|
+
[data-slot="code-block"][data-variant="destructive"]:has(> [data-slot="code-block-content"]:focus-visible),
|
|
279
|
+
:is(
|
|
280
|
+
[data-slot="input"],
|
|
281
|
+
[data-slot="textarea"],
|
|
282
|
+
[data-slot="select-trigger"],
|
|
283
|
+
[data-slot="button"]
|
|
284
|
+
)[aria-invalid="true"],
|
|
285
|
+
[data-slot="input-group"]:has([data-slot][aria-invalid="true"]) {
|
|
286
|
+
border-color: var(--destructive);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/*
|
|
290
|
+
* A slider's unfilled track is outlined in the field edge, so its extent reads
|
|
291
|
+
* at 3:1; the muted track alone is 1.05:1 on the canvas.
|
|
292
|
+
*/
|
|
293
|
+
[data-slot="slider-track"] {
|
|
294
|
+
box-shadow: inset 0 0 0 1px var(--input);
|
|
295
|
+
}
|
|
296
|
+
|
|
225
297
|
/*
|
|
226
298
|
* A number column (meta.variant "number") right-aligns its head and cells, so
|
|
227
299
|
* figures line up by their units down the column, as a ledger reads.
|