@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 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, always with ▲ or ▼ (`Delta`).
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
- <AppSidebar collapsible="none" navMain={views} navLabel="Views" className="rounded-lg border" />
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 className="grid gap-6 md:grid-cols-2 md:gap-0 md:divide-x">
27
- <div className="md:pr-6">
28
- <h3 className="text-muted-foreground mb-2 text-xs font-medium">Gains</h3>
29
- <BarList data={gains} max={max} color="var(--positive)" valueFormatter={(v) => money(v)} />
30
- </div>
31
- <div className="md:pl-6">
32
- <h3 className="text-muted-foreground mb-2 text-xs font-medium">Losses</h3>
33
- <BarList data={losses} max={max} color="var(--negative)" valueFormatter={(v) => money(v)} />
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
- <Calendar
8
- mode="single"
9
- selected={date}
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
  }
@@ -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 className="text-muted-foreground">Updated after the market close</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 className="text-muted-foreground">
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
- <Command className="rounded-lg border">
7
- <CommandInput placeholder="Jump to a page" />
8
- <CommandList>
9
- <CommandEmpty>No page matches.</CommandEmpty>
10
- <CommandGroup heading="Pages">
11
- <CommandItem>
12
- <WalletIcon />
13
- Holdings
14
- </CommandItem>
15
- <CommandItem>
16
- <ChartLineIcon />
17
- Realized
18
- </CommandItem>
19
- <CommandItem>
20
- <LandmarkIcon />
21
- Cash flow
22
- </CommandItem>
23
- </CommandGroup>
24
- </CommandList>
25
- </Command>
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="flex h-24 w-full items-center justify-center rounded-lg border border-dashed text-muted-foreground">
7
- Right-click or long-press here
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 className="flex flex-col gap-2">
37
- <DistributionBar
38
- min={-reach}
39
- max={reach}
40
- showLegend={false}
41
- markers={[{ at: 0 }]}
42
- highlighted={focus?.name ?? null}
43
- onHover={setFocus}
44
- segments={positions.map(({ name, value }) => ({
45
- name,
46
- value,
47
- color: value > 0 ? "var(--positive)" : "var(--negative)",
48
- }))}
49
- />
50
- <div className="grid grid-cols-3 text-xs text-muted-foreground tabular-nums">
51
- <span>
52
- losers <Delta value={lost} format="money" />
53
- </span>
54
- <span className="text-center">break-even</span>
55
- <span className="text-right">
56
- winners <Delta value={won} format="money" />
57
- </span>
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="items-center justify-between gap-x-6 gap-y-2 whitespace-normal">
49
- <p aria-live="polite" className="text-sm">
50
- {picked
51
- ? `${picked.date}: ${count(picked.value)} trades`
52
- : "Select a day for its trade count. Use arrow keys to move between days."}
53
- </p>
54
- <CalendarHeatmapLegend labels={{ less: "0 trades", more: `${count(busiest)} trades` }} />
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="items-center justify-between gap-x-6 gap-y-2 whitespace-normal">
45
- <p aria-live="polite" className="text-sm">
46
- {picked
47
- ? `${picked.date}: ${["unknown", "critical", "degraded", "healthy"][picked.value] ?? "unknown"}`
48
- : "Select a day for its health. Use arrow keys to move between days."}
49
- </p>
50
- {/* No day lacks data, so the legend leaves out "No Data". */}
51
- <StatusHeatmapLegend className="gap-3" />
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
- data={data}
44
- hours={[9, 10, 11, 12, 13, 14, 15]}
45
- weekdays={[1, 2, 3, 4, 5]}
46
- className="gap-4 lg:flex-row lg:items-center lg:gap-8"
47
- >
48
- <WeekdayHeatmapBody className="lg:w-md lg:shrink-0">
49
- {({ activity }) => (
50
- <WeekdayHeatmapBlock
51
- activity={activity}
52
- selected={isPicked(activity)}
53
- onCellClick={(cell) => setPicked(isPicked(cell) ? null : cell)}
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
- </WeekdayHeatmapBody>
57
- <div className="flex flex-col items-start gap-3 text-sm">
58
- <WeekdayHeatmapLegend
59
- className="ml-0"
60
- labels={{ less: "0 orders", more: `${count(busiest.value)} orders an hour` }}
61
- />
62
- <p>
63
- Busiest: {DAYS[busiest.weekday]} at {hourLabel(busiest.hour)}, {count(busiest.value)} orders
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 className="text-xs text-muted-foreground">
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 className="flex flex-col gap-8 @xl/range:flex-row @xl/range:items-end">
29
- <DataList className="@xl/range:w-56 @xl/range:shrink-0">
30
- <DataListItem label="52-week low, 7 months ago">
31
- <span className="tabular-nums">{price(low)}</span>
32
- </DataListItem>
33
- <DataListItem label="52-week high, 11 weeks ago">
34
- <span className="tabular-nums">{price(high)}</span>
35
- </DataListItem>
36
- <DataListItem label="Last close">
37
- <span className="tabular-nums">{price(close)}</span>
38
- </DataListItem>
39
- </DataList>
40
- <RangePosition
41
- low={low}
42
- high={high}
43
- value={close}
44
- lowLabel="52-week low"
45
- highLabel="52-week high"
46
- caption="of the way up the 52-week range"
47
- valueFormatter={price}
48
- className="@xl/range:flex-1"
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
- <ScrollArea className="h-48 w-56 rounded-lg border">
10
- <div className="p-4">
11
- {months.map((month) => (
12
- <div key={month}>
13
- <div className="text-sm">{month}</div>
14
- <Separator className="my-2" />
15
- </div>
16
- ))}
17
- </div>
18
- </ScrollArea>
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 className="font-mono">{row.symbol}</TableCell>
79
- <TableCell className="tabular-nums">{money(row.value)}</TableCell>
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 className="grid gap-2">
63
- <p className="text-sm text-muted-foreground">
64
- Bar: today's weight. Tall tick: the target. Shading: within 5 points of it, darker within 2.
65
- </p>
66
- <TargetRows rows={rows} domain={[0, 0.5]} valueFormatter={(value) => f.pct(value)} />
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" className="flex-col items-start gap-0 tabular-nums">
61
- {hovered && (
62
- <>
63
- <span className="font-medium">
64
- {hovered.name} · {hovered.broker}
65
- </span>
66
- <span>{hovered.group}</span>
67
- <span>{figures(hovered)}</span>
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: dimUnselected(tone, isSelected),
45
+ stroke: tone,
46
46
  strokeWidth: 1.5
47
- }, i) : /* @__PURE__ */ jsx(Cell, { fill: dimUnselected(tone, isSelected) }, i);
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/50 focus-visible:ring-inset"
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,opacity] duration-300",
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
- opacity: highlighted === null || highlighted === segment.key ? 1 : .35
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-1", className),
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
- /** A signed figure: ▲ in --positive, ▼ in --negative, so colour is never the only cue. */
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
- /** A signed figure: ▲ in --positive, ▼ in --negative, so colour is never the only cue. */
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" : null;
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
- className: cn("inline-flex items-center gap-1 tabular-nums", direction === "up" && "text-positive", direction === "down" && "text-negative", direction === null && "text-muted-foreground", className),
19
+ "data-direction": direction,
20
+ className: cn("inline-flex items-center gap-1 whitespace-nowrap", className),
13
21
  children: [
14
- direction && /* @__PURE__ */ jsx("span", {
22
+ /* @__PURE__ */ jsx(Icon, {
15
23
  "aria-hidden": "true",
16
- children: direction === "up" ? "▲" : "▼"
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 ? "▲ " : value < 0 ? "▼ " : ""}${pct(Math.abs(value))}`;
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: ["▼ over ", over] }),
196
+ /* @__PURE__ */ jsxs("span", { children: ["down over ", over] }),
197
197
  /* @__PURE__ */ jsxs("span", { children: [pct(step, digits), " a step"] }),
198
- /* @__PURE__ */ jsxs("span", { children: ["▲ over ", over] })
198
+ /* @__PURE__ */ jsxs("span", { children: ["up over ", over] })
199
199
  ]
200
200
  }),
201
201
  /* @__PURE__ */ jsxs("p", {
@@ -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 arrow.
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,
@@ -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 arrow.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siyahi/ui",
3
- "version": "0.1.9-next.21",
3
+ "version": "0.1.9-next.22",
4
4
  "description": "Stock shadcn/ui on Base UI and Tailwind v4, a finance theme, a few compositions and formatters.",
5
5
  "keywords": [
6
6
  "base-ui",
package/report.css CHANGED
@@ -5,7 +5,7 @@
5
5
  */
6
6
  :root {
7
7
  color-scheme: light dark;
8
- --radius: 0.625rem;
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.556 0 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.577 0.245 27.325);
23
+ --destructive: oklch(0.49 0.19 27.325);
24
24
  --border: oklch(0.922 0 0);
25
- --input: oklch(0.922 0 0);
25
+ --input: oklch(0.64 0 0);
26
26
  --ring: oklch(0.62 0.16 275);
27
- --positive: oklch(0.527 0.154 150.069);
27
+ --positive: oklch(0.527 0.145 150.069);
28
28
  --positive-foreground: oklch(0.985 0 0);
29
- --negative: oklch(0.577 0.245 27.325);
29
+ --negative: oklch(0.577 0.235 27.325);
30
30
  --negative-foreground: oklch(0.985 0 0);
31
- --warning: oklch(0.555 0.163 48.998);
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.66 0.11 235);
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.704 0.191 22.216);
65
+ --destructive: oklch(0.8 0.1 22.216);
66
66
  --border: oklch(1 0 0 / 10%);
67
- --input: oklch(1 0 0 / 15%);
67
+ --input: oklch(0.62 0 0);
68
68
  --ring: oklch(0.6 0.14 275);
69
- --positive: oklch(0.74 0.16 151.711);
69
+ --positive: oklch(0.69 0.16 151.711);
70
70
  --positive-foreground: oklch(0.145 0 0);
71
- --negative: oklch(0.704 0.191 22.216);
71
+ --negative: oklch(0.704 0.187 22.216);
72
72
  --negative-foreground: oklch(0.145 0 0);
73
- --warning: oklch(0.828 0.189 84.429);
73
+ --warning: oklch(0.828 0.169 84.429);
74
74
  --warning-foreground: oklch(0.145 0 0);
75
- --link: oklch(0.75 0.13 275);
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
- --radius: 0.625rem;
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
- --muted-foreground: oklch(0.556 0 0);
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
- --destructive: oklch(0.577 0.245 27.325);
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
- --input: oklch(0.922 0 0);
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, blue-600. */
106
- --positive: oklch(0.527 0.154 150.069);
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.245 27.325);
112
+ --negative: oklch(0.577 0.235 27.325);
109
113
  --negative-foreground: oklch(0.985 0 0);
110
- --warning: oklch(0.555 0.163 48.998);
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 against the
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.66 0.11 235);
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
- --destructive: oklch(0.704 0.191 22.216);
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(1 0 0 / 15%);
169
+ --input: oklch(0.62 0 0);
165
170
  --ring: oklch(0.6 0.14 275);
166
- /* Green toned down from Tailwind green-400 to the lightness and calm of red-400 beside it, so gains do not shout; amber-400. */
167
- --positive: oklch(0.74 0.16 151.711);
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.191 22.216);
178
+ --negative: oklch(0.704 0.187 22.216);
170
179
  --negative-foreground: oklch(0.145 0 0);
171
- --warning: oklch(0.828 0.189 84.429);
180
+ --warning: oklch(0.828 0.169 84.429);
172
181
  --warning-foreground: oklch(0.145 0 0);
173
- --link: oklch(0.75 0.13 275);
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. Unlayered, to outrank
217
- * the stock aria-pressed:bg-muted.
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.