@siyahi/ui 0.1.9-next.20 → 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.
Files changed (101) hide show
  1. package/DESIGN.md +42 -0
  2. package/README.md +3 -3
  3. package/agents/examples/alert.tsx +4 -6
  4. package/agents/examples/app-sidebar.tsx +3 -1
  5. package/agents/examples/bar-list-sides.tsx +13 -3
  6. package/agents/examples/button-states.tsx +23 -0
  7. package/agents/examples/calendar.tsx +3 -7
  8. package/agents/examples/callout.tsx +33 -0
  9. package/agents/examples/card.tsx +3 -1
  10. package/agents/examples/change-bar.tsx +104 -0
  11. package/agents/examples/chart-area.tsx +5 -4
  12. package/agents/examples/chart-bar-flows.tsx +3 -2
  13. package/agents/examples/chart-bar-sectors.tsx +2 -0
  14. package/agents/examples/collapsible.tsx +2 -2
  15. package/agents/examples/command.tsx +22 -20
  16. package/agents/examples/context-menu.tsx +4 -2
  17. package/agents/examples/data-table-compact.tsx +95 -0
  18. package/agents/examples/distribution-bar-tug.tsx +24 -22
  19. package/agents/examples/heatmap-calendar.tsx +19 -7
  20. package/agents/examples/heatmap-status.tsx +19 -11
  21. package/agents/examples/heatmap-weekday.tsx +34 -15
  22. package/agents/examples/input-states.tsx +33 -0
  23. package/agents/examples/metric-strip.tsx +9 -3
  24. package/agents/examples/overview-dashboard.tsx +18 -0
  25. package/agents/examples/page-hero.tsx +38 -0
  26. package/agents/examples/price-chart.tsx +2 -2
  27. package/agents/examples/range-position-table.tsx +101 -0
  28. package/agents/examples/range-position.tsx +55 -0
  29. package/agents/examples/scroll-area.tsx +12 -10
  30. package/agents/examples/section-cards-select.tsx +8 -5
  31. package/agents/examples/surfaces.tsx +7 -4
  32. package/agents/examples/target-rows.tsx +72 -0
  33. package/agents/examples/treemap.tsx +13 -10
  34. package/agents/manifest.json +153 -3
  35. package/dist/components/registry/charts/area-chart-card.js +25 -7
  36. package/dist/components/registry/charts/bar-chart-card.d.ts +0 -1
  37. package/dist/components/registry/charts/bar-chart-card.js +15 -4
  38. package/dist/components/registry/charts/line-chart-card.js +2 -2
  39. package/dist/components/registry/dashboard-01/section-cards.js +1 -1
  40. package/dist/components/registry/dashboardcn/bar-list.js +11 -5
  41. package/dist/components/registry/dashboardcn/distribution-bar.js +5 -3
  42. package/dist/components/registry/dashboardcn/kpi-row-card.js +2 -2
  43. package/dist/components/registry/dashboardcn/sparkline.d.ts +5 -1
  44. package/dist/components/registry/dashboardcn/sparkline.js +40 -18
  45. package/dist/components/registry/heatmap/calendar-heatmap.d.ts +4 -2
  46. package/dist/components/registry/heatmap/calendar-heatmap.js +67 -33
  47. package/dist/components/registry/heatmap/status-heatmap.d.ts +5 -3
  48. package/dist/components/registry/heatmap/status-heatmap.js +31 -16
  49. package/dist/components/registry/heatmap/weekday-heatmap.d.ts +4 -2
  50. package/dist/components/registry/heatmap/weekday-heatmap.js +28 -12
  51. package/dist/components/registry/niko-table/core/data-table-context.d.ts +16 -1
  52. package/dist/components/registry/niko-table/core/data-table-context.js +5 -20
  53. package/dist/components/registry/niko-table/core/data-table-root.js +39 -4
  54. package/dist/components/registry/niko-table/core/data-table-structure.js +2 -0
  55. package/dist/components/registry/niko-table/lib/styles.js +1 -1
  56. package/dist/components/registry/niko-table/types/index.d.ts +9 -0
  57. package/dist/components/registry/sidebar-07/nav-main.d.ts +1 -0
  58. package/dist/components/registry/sidebar-07/nav-main.js +9 -2
  59. package/dist/components/siyahi/callout.d.ts +24 -0
  60. package/dist/components/siyahi/callout.js +40 -0
  61. package/dist/components/siyahi/change-bar.d.ts +21 -0
  62. package/dist/components/siyahi/change-bar.js +35 -0
  63. package/dist/components/siyahi/data-table-compact-columns-toggle.d.ts +4 -0
  64. package/dist/components/siyahi/data-table-compact-columns-toggle.js +19 -0
  65. package/dist/components/siyahi/data-table-footnote.d.ts +12 -0
  66. package/dist/components/siyahi/data-table-footnote.js +35 -0
  67. package/dist/components/siyahi/default-sidebar-open.d.ts +8 -0
  68. package/dist/components/siyahi/default-sidebar-open.js +13 -0
  69. package/dist/components/siyahi/delta.d.ts +5 -1
  70. package/dist/components/siyahi/delta.js +14 -9
  71. package/dist/components/siyahi/page-hero.d.ts +25 -0
  72. package/dist/components/siyahi/page-hero.js +41 -0
  73. package/dist/components/siyahi/page-toc.d.ts +20 -0
  74. package/dist/components/siyahi/page-toc.js +76 -0
  75. package/dist/components/siyahi/quote.d.ts +12 -0
  76. package/dist/components/siyahi/quote.js +23 -0
  77. package/dist/components/siyahi/range-position.d.ts +24 -0
  78. package/dist/components/siyahi/range-position.js +105 -0
  79. package/dist/components/siyahi/siyahi-provider.d.ts +1 -1
  80. package/dist/components/siyahi/siyahi-provider.js +1 -1
  81. package/dist/components/siyahi/target-rows.d.ts +34 -0
  82. package/dist/components/siyahi/target-rows.js +97 -0
  83. package/dist/components/siyahi/treemap.d.ts +4 -1
  84. package/dist/components/siyahi/treemap.js +36 -28
  85. package/dist/components/siyahi/warning-alert.d.ts +10 -0
  86. package/dist/components/siyahi/warning-alert.js +20 -0
  87. package/dist/index.d.ts +15 -4
  88. package/dist/index.js +12 -1
  89. package/dist/lib/chart-frame.d.ts +12 -2
  90. package/dist/lib/chart-frame.js +51 -7
  91. package/dist/lib/chart-series.d.ts +5 -2
  92. package/dist/lib/compact-columns.d.ts +19 -0
  93. package/dist/lib/compact-columns.js +55 -0
  94. package/dist/lib/format.d.ts +8 -3
  95. package/dist/lib/format.js +25 -6
  96. package/dist/lib/treemap.js +6 -1
  97. package/dist/lib/use-element-width.d.ts +5 -0
  98. package/dist/lib/use-element-width.js +15 -0
  99. package/package.json +2 -1
  100. package/report.css +37 -36
  101. package/styles/siyahi.css +243 -55
@@ -8,17 +8,20 @@ import {
8
8
  StatusHeatmap,
9
9
  StatusHeatmapBlock,
10
10
  StatusHeatmapBody,
11
+ StatusHeatmapFooter,
12
+ StatusHeatmapLegend,
11
13
  } from "@siyahi/ui";
12
14
 
13
15
  // One cell per day for the last 60 days, as a status the heatmap colours:
14
- // 3 healthy (--positive), 2 degraded (--warning), 1 critical (--negative).
16
+ // 3 healthy (quiet grey), 2 degraded (--warning), 1 critical (--destructive).
15
17
  const data = Array.from({ length: 60 }, (_, i) => {
16
18
  const day = new Date(Date.UTC(2026, 7, 22 + i));
17
19
  return { date: day.toISOString().slice(0, 10), value: i % 17 === 5 ? 1 : i % 11 === 3 ? 2 : 3 };
18
20
  });
19
21
 
20
22
  export default function HeatmapStatusExample() {
21
- const [selected, setSelected] = useState<string | null>(null);
23
+ // The page owns the pick: a second tap on the same day clears it.
24
+ const [picked, setPicked] = useState<(typeof data)[number] | null>(null);
22
25
 
23
26
  return (
24
27
  <Card>
@@ -27,24 +30,29 @@ export default function HeatmapStatusExample() {
27
30
  <CardDescription>Daily health: healthy, degraded or critical</CardDescription>
28
31
  </CardHeader>
29
32
  <CardContent>
30
- <StatusHeatmap data={data}>
33
+ <StatusHeatmap data={data} statusValues={[3, 2, 1]}>
31
34
  <StatusHeatmapBody>
32
35
  {({ activity, dayIndex }) => (
33
36
  <StatusHeatmapBlock
34
37
  activity={activity}
35
38
  dayIndex={dayIndex}
36
- onCellClick={(cell) =>
37
- setSelected(
38
- `${cell.date}: ${["unknown", "critical", "degraded", "healthy"][cell.value] ?? "unknown"}`,
39
- )
40
- }
39
+ selected={activity.date === picked?.date}
40
+ onCellClick={(cell) => setPicked(cell.date === picked?.date ? null : cell)}
41
41
  />
42
42
  )}
43
43
  </StatusHeatmapBody>
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>
54
+ </StatusHeatmapFooter>
44
55
  </StatusHeatmap>
45
- <p aria-live="polite" className="mt-3 text-sm">
46
- {selected ?? "Select a day for its health. Use arrow keys to move between days."}
47
- </p>
48
56
  </CardContent>
49
57
  </Card>
50
58
  );
@@ -9,6 +9,7 @@ import {
9
9
  WeekdayHeatmap,
10
10
  WeekdayHeatmapBlock,
11
11
  WeekdayHeatmapBody,
12
+ WeekdayHeatmapLegend,
12
13
  } from "@siyahi/ui";
13
14
 
14
15
  // Orders placed by weekday and hour of the trading session.
@@ -19,9 +20,15 @@ const data = [1, 2, 3, 4, 5].flatMap((weekday) =>
19
20
  value: ((weekday * 5 + hour * 3) % 9) + (hour === 9 ? 6 : 0),
20
21
  })),
21
22
  );
23
+ const busiest = data.reduce((top, cell) => (cell.value > top.value ? cell : top));
24
+ const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
25
+ const hourLabel = (hour: number) => `${String(hour).padStart(2, "0")}:00`;
22
26
 
23
27
  export default function HeatmapWeekdayExample() {
24
- const [selected, setSelected] = useState<string | null>(null);
28
+ // The page owns the pick: a second tap on the same cell clears it.
29
+ const [picked, setPicked] = useState<(typeof data)[number] | null>(null);
30
+ const isPicked = (cell: { weekday: number; hour: number }) =>
31
+ cell.weekday === picked?.weekday && cell.hour === picked.hour;
25
32
  const { count } = useFormat();
26
33
  return (
27
34
  <Card>
@@ -29,24 +36,36 @@ export default function HeatmapWeekdayExample() {
29
36
  <CardTitle>When orders are placed</CardTitle>
30
37
  <CardDescription>Weekday by hour in India Standard Time, market hours only</CardDescription>
31
38
  </CardHeader>
39
+ {/* Seven hours by five days is a small grid: capped, it keeps square cells, and the legend and the answer
40
+ sit beside it instead of a wide empty card. */}
32
41
  <CardContent>
33
42
  <WeekdayHeatmap data={data} hours={[9, 10, 11, 12, 13, 14, 15]} weekdays={[1, 2, 3, 4, 5]}>
34
- <WeekdayHeatmapBody>
35
- {({ activity }) => (
36
- <WeekdayHeatmapBlock
37
- activity={activity}
38
- onCellClick={(cell) =>
39
- setSelected(
40
- `${["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][cell.weekday]}, ${String(cell.hour).padStart(2, "0")}:00 IST: ${count(cell.value)} orders`,
41
- )
42
- }
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` }}
43
57
  />
44
- )}
45
- </WeekdayHeatmapBody>
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>
67
+ </div>
46
68
  </WeekdayHeatmap>
47
- <p aria-live="polite" className="mt-3 text-sm">
48
- {selected ?? "Select a cell for its order count. Use arrow keys to move between cells."}
49
- </p>
50
69
  </CardContent>
51
70
  </Card>
52
71
  );
@@ -0,0 +1,33 @@
1
+ import { Input } from "@siyahi/ui";
2
+
3
+ // A field is its label, the input and one line under it: a hint, or the error that replaces it.
4
+ export default function InputStatesExample() {
5
+ return (
6
+ <div className="grid max-w-sm gap-5">
7
+ <div className="grid gap-1.5">
8
+ <label htmlFor="budget" className="text-sm font-medium">
9
+ Monthly budget
10
+ </label>
11
+ <Input id="budget" inputMode="numeric" defaultValue="25,000" aria-describedby="budget-hint" />
12
+ <p id="budget-hint" className="text-sm text-muted-foreground">
13
+ In rupees, before brokerage
14
+ </p>
15
+ </div>
16
+ <div className="grid gap-1.5">
17
+ <label htmlFor="folio" className="text-sm font-medium">
18
+ Folio number
19
+ </label>
20
+ <Input id="folio" defaultValue="12/34" aria-invalid aria-describedby="folio-error" />
21
+ <p id="folio-error" className="text-sm text-destructive">
22
+ A folio is digits only, such as 1234567
23
+ </p>
24
+ </div>
25
+ <div className="grid gap-1.5">
26
+ <label htmlFor="broker" className="text-sm font-medium text-muted-foreground">
27
+ Broker
28
+ </label>
29
+ <Input id="broker" defaultValue="Kite" disabled />
30
+ </div>
31
+ </div>
32
+ );
33
+ }
@@ -19,13 +19,19 @@ export default function MetricStripExample() {
19
19
  trend: [3600000, 3685000, 3770000, 3855000, 3940000, 4025000],
20
20
  },
21
21
  { label: "XIRR", value: pct(0.142), change: -0.004, trend: [0.151, 0.149, 0.152, 0.144, 0.146, 0.142] },
22
- { label: "Realised", value: moneyCompact(-61200), change: -61200, changeFormat: "money" },
22
+ {
23
+ label: "Realised",
24
+ value: moneyCompact(-61200),
25
+ change: -61200,
26
+ changeFormat: "money",
27
+ trend: [14200, -3800, 9100, 2600, -18400, -61200],
28
+ },
23
29
  { label: "Dividends", value: moneyCompact(38400), change: 0.18, trend: [4100, 0, 12800, 3300, 9900, 8300] },
24
- { label: "Holdings", value: count(42), note: "3 bought in Oct" },
30
+ { label: "Holdings", value: count(42), note: "3 bought in Oct", trend: [38, 39, 39, 40, 39, 42] },
25
31
  ];
26
32
  return (
27
33
  <MetricStrip
28
- title="October"
34
+ title="Portfolio up 3.1% in October"
29
35
  description="Month-end figures with the six-month run behind each"
30
36
  action={
31
37
  <ToggleGroup defaultValue={["1M"]} variant="outline" size="sm" aria-label="Period">
@@ -0,0 +1,18 @@
1
+ import ChartArea from "./chart-area";
2
+ import DataTableExample from "./data-table";
3
+ import MetricStrip from "./metric-strip";
4
+
5
+ // What a page built with Siyahi looks like: a metric strip, a chart card and a data table, each the catalogue's own example.
6
+ export default function OverviewDashboard() {
7
+ return (
8
+ <div className="flex flex-col gap-4">
9
+ <MetricStrip />
10
+ {/* Side by side only where the table's five columns fit beside the chart; at 1440 they would be cut off.
11
+ Each card keeps its own height: stretched to the taller one, the chart card ends in blank space. */}
12
+ <div className="grid items-start gap-4 2xl:grid-cols-2">
13
+ <ChartArea />
14
+ <DataTableExample />
15
+ </div>
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,38 @@
1
+ import { Button, PageHero } from "@siyahi/ui";
2
+ import { ArrowRightIcon } from "lucide-react";
3
+
4
+ // The catalogue's own ink drop, painted with currentColor so the hero tints it.
5
+ function InkDrop() {
6
+ return (
7
+ <svg viewBox="0 0 16 16">
8
+ <path
9
+ fill="currentColor"
10
+ fillRule="evenodd"
11
+ d="M8 .5C8 .5 2.5 6.3 2.5 10a5.5 5.5 0 0 0 11 0C13.5 6.3 8 .5 8 .5ZM4.75 10A3.25 3.25 0 0 0 8 13.25v-1.5A1.75 1.75 0 0 1 6.25 10Z"
12
+ />
13
+ </svg>
14
+ );
15
+ }
16
+
17
+ export default function PageHeroExample() {
18
+ return (
19
+ <PageHero
20
+ eyebrow="@siyahi/ui"
21
+ title="Siyahi"
22
+ lead="Ink for numbers. A design system for pages people read figures from: portfolios, ledgers, research."
23
+ detail="Stock shadcn/ui on Base UI and Tailwind v4, with a finance theme, charts and tables that fit a phone, Indian-aware formatters and a lint. Siyahi is Hindi for ink."
24
+ actions={
25
+ <>
26
+ <Button nativeButton={false} render={<a href="#/data" />}>
27
+ Browse the data components
28
+ <ArrowRightIcon />
29
+ </Button>
30
+ <Button variant="outline" nativeButton={false} render={<a href="#/overview/install" />}>
31
+ Install
32
+ </Button>
33
+ </>
34
+ }
35
+ mark={<InkDrop />}
36
+ />
37
+ );
38
+ }
@@ -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"
@@ -0,0 +1,101 @@
1
+ import {
2
+ Card,
3
+ CardContent,
4
+ CardDescription,
5
+ CardHeader,
6
+ CardTitle,
7
+ DataTable,
8
+ DataTableBody,
9
+ DataTableCompactColumnsToggle,
10
+ DataTableHeader,
11
+ DataTableRoot,
12
+ DataTableSortHeader,
13
+ DataTableToolbarSection,
14
+ RangePosition,
15
+ useFormat,
16
+ type DataTableColumnDef,
17
+ } from "@siyahi/ui";
18
+
19
+ interface Holding {
20
+ symbol: string;
21
+ /** Last close and the 52-week low and high closes, in rupees; made-up figures. */
22
+ close: number;
23
+ low: number;
24
+ high: number;
25
+ /** How far up its range the close sits, 0 at the low and 1 at the high, so the column sorts. */
26
+ position: number;
27
+ }
28
+
29
+ const holding = (symbol: string, close: number, low: number, high: number): Holding => ({
30
+ symbol,
31
+ close,
32
+ low,
33
+ high,
34
+ position: (close - low) / (high - low),
35
+ });
36
+
37
+ const holdings: Holding[] = [
38
+ holding("NIFTYBEES", 284, 241.2, 291.8),
39
+ holding("INFY", 1870, 1307, 2006.45),
40
+ holding("ICICIBANK", 1290, 1051.05, 1362.35),
41
+ holding("TCS", 4150, 3591.5, 4592.25),
42
+ holding("ITC", 460, 399.35, 528.5),
43
+ holding("HDFCBANK", 1750, 1613.6, 2027.4),
44
+ ];
45
+
46
+ function Price({ amount }: { amount: number }) {
47
+ return useFormat().money(amount, 2);
48
+ }
49
+
50
+ const header = () => <DataTableSortHeader />;
51
+
52
+ const columns: DataTableColumnDef<Holding>[] = [
53
+ { accessorKey: "symbol", header, size: 130, meta: { label: "Symbol", compact: true } },
54
+ {
55
+ accessorKey: "close",
56
+ header,
57
+ size: 120,
58
+ meta: { label: "Close", variant: "number" },
59
+ cell: ({ row }) => <Price amount={row.original.close} />,
60
+ },
61
+ {
62
+ accessorKey: "position",
63
+ header,
64
+ size: 200,
65
+ meta: { label: "52-week range", compact: true },
66
+ cell: ({ row }) => (
67
+ <RangePosition
68
+ variant="compact"
69
+ low={row.original.low}
70
+ high={row.original.high}
71
+ value={row.original.close}
72
+ lowLabel="52-week low"
73
+ highLabel="52-week high"
74
+ />
75
+ ),
76
+ },
77
+ ];
78
+
79
+ export default function RangePositionTableExample() {
80
+ return (
81
+ <Card>
82
+ <CardHeader>
83
+ <CardTitle>NIFTYBEES trades near its 52-week high, HDFCBANK near its low</CardTitle>
84
+ <CardDescription>
85
+ Each close on a track from the year's low close to its high; sort to rank them
86
+ </CardDescription>
87
+ </CardHeader>
88
+ <CardContent>
89
+ <DataTableRoot data={holdings} columns={columns}>
90
+ <DataTableToolbarSection>
91
+ <DataTableCompactColumnsToggle />
92
+ </DataTableToolbarSection>
93
+ <DataTable aria-label="Holdings in their 52-week range">
94
+ <DataTableHeader />
95
+ <DataTableBody />
96
+ </DataTable>
97
+ </DataTableRoot>
98
+ </CardContent>
99
+ </Card>
100
+ );
101
+ }
@@ -0,0 +1,55 @@
1
+ import {
2
+ Card,
3
+ CardContent,
4
+ CardDescription,
5
+ CardHeader,
6
+ CardTitle,
7
+ DataList,
8
+ DataListItem,
9
+ RangePosition,
10
+ useFormat,
11
+ } from "@siyahi/ui";
12
+
13
+ // HDFCBANK over the last 52 weeks; made-up figures.
14
+ const low = 1613.6;
15
+ const high = 2027.4;
16
+ const close = 1750.0;
17
+
18
+ export default function RangePositionExample() {
19
+ const f = useFormat();
20
+ const price = (value: number) => f.money(value, 2);
21
+ return (
22
+ // Side by side once the card, not the screen, has room: in a narrow panel on a wide screen they stack.
23
+ <Card className="@container/range">
24
+ <CardHeader>
25
+ <CardTitle>HDFCBANK sits {f.pct((close - low) / (high - low), 0)} of the way up its 52-week range</CardTitle>
26
+ <CardDescription>The last close against the year's lowest and highest close</CardDescription>
27
+ </CardHeader>
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>
52
+ </CardContent>
53
+ </Card>
54
+ );
55
+ }
@@ -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
  }
@@ -1,4 +1,4 @@
1
- import { Delta, SectionCards, useFormat, type SectionCard } from "@siyahi/ui";
1
+ import { Delta, SectionCards, SiyahiProvider, useFormat, type SectionCard } from "@siyahi/ui";
2
2
  import { useState } from "react";
3
3
 
4
4
  const classes = [
@@ -19,9 +19,12 @@ export default function SectionCardsSelectExample() {
19
19
  disabled: c.positions === 0,
20
20
  }));
21
21
  return (
22
- <div className="flex flex-col gap-3">
23
- <SectionCards items={items} selected={scope} onSelect={setScope} />
24
- <p className="text-sm text-muted-foreground">Showing {scope}</p>
25
- </div>
22
+ // On a ruled surface the tiles part with hairlines, as on a trading screen, instead of sitting as cards.
23
+ <SiyahiProvider surface="ruled">
24
+ <div className="flex flex-col gap-3">
25
+ <SectionCards items={items} selected={scope} onSelect={setScope} />
26
+ <p className="text-sm text-muted-foreground">Showing {scope}</p>
27
+ </div>
28
+ </SiyahiProvider>
26
29
  );
27
30
  }
@@ -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>
@@ -89,7 +91,7 @@ function Dashboard() {
89
91
  }
90
92
 
91
93
  export default function SurfacesExample() {
92
- const [surface, setSurface] = useState<Surface>("ruled");
94
+ const [surface, setSurface] = useState<Surface>("card");
93
95
  return (
94
96
  <div className="flex flex-col gap-4">
95
97
  <ToggleGroup
@@ -102,9 +104,10 @@ export default function SurfacesExample() {
102
104
  size="sm"
103
105
  aria-label="Surface"
104
106
  >
107
+ {/* The default first. */}
108
+ <ToggleGroupItem value="card">Card</ToggleGroupItem>
105
109
  <ToggleGroupItem value="ruled">Ruled</ToggleGroupItem>
106
110
  <ToggleGroupItem value="open">Open</ToggleGroupItem>
107
- <ToggleGroupItem value="card">Card</ToggleGroupItem>
108
111
  </ToggleGroup>
109
112
  {/* A nested provider sets the surface for its part of the page. */}
110
113
  <SiyahiProvider surface={surface}>
@@ -0,0 +1,72 @@
1
+ import {
2
+ Card,
3
+ CardContent,
4
+ CardDescription,
5
+ CardHeader,
6
+ CardTitle,
7
+ TargetRows,
8
+ useFormat,
9
+ type TargetRow,
10
+ type TargetStatus,
11
+ } from "@siyahi/ui";
12
+
13
+ interface Sleeve {
14
+ name: string;
15
+ holdings: string;
16
+ /** Today's share of the portfolio and the plan's share, as fractions; made-up figures. */
17
+ weight: number;
18
+ target: number;
19
+ }
20
+
21
+ const sleeves: Sleeve[] = [
22
+ { name: "Large-cap equity", holdings: "HDFCBANK, ICICIBANK, INFY, TCS", weight: 0.426, target: 0.4 },
23
+ { name: "Mid and small cap", holdings: "Two mid-cap funds", weight: 0.189, target: 0.2 },
24
+ { name: "Debt", holdings: "Corporate bond fund, PPF", weight: 0.194, target: 0.25 },
25
+ { name: "Gold", holdings: "GOLDBEES, Sovereign Gold Bonds", weight: 0.093, target: 0.1 },
26
+ { name: "International", holdings: "Nasdaq 100 fund of funds", weight: 0.098, target: 0.05 },
27
+ ];
28
+
29
+ // Within two points of the plan is on target; past five is far enough to rebalance.
30
+ const NEAR = 0.02;
31
+ const FAR = 0.05;
32
+
33
+ const pts = (value: number) => `${(value * 100).toFixed(1)} pts`;
34
+
35
+ function statusOf({ weight, target }: Sleeve): TargetStatus {
36
+ const gap = weight - target;
37
+ const size = `${pts(Math.abs(gap))} ${gap > 0 ? "over" : "under"}`;
38
+ if (Math.abs(gap) <= NEAR) return { kind: "met", label: "On target" };
39
+ if (Math.abs(gap) <= FAR) return { kind: "missed", label: size };
40
+ return { kind: "warning", label: `${size}, rebalance` };
41
+ }
42
+
43
+ export default function TargetRowsExample() {
44
+ const f = useFormat();
45
+ const rows: TargetRow[] = sleeves.map((sleeve) => ({
46
+ label: sleeve.name,
47
+ detail: sleeve.holdings,
48
+ value: sleeve.weight,
49
+ target: sleeve.target,
50
+ bands: [
51
+ [sleeve.target - FAR, sleeve.target + FAR],
52
+ [sleeve.target - NEAR, sleeve.target + NEAR],
53
+ ],
54
+ status: statusOf(sleeve),
55
+ }));
56
+ return (
57
+ <Card>
58
+ <CardHeader>
59
+ <CardTitle>Debt is 5.6 points under its target</CardTitle>
60
+ <CardDescription>Each sleeve's share of the portfolio against the plan, on one 0–50% scale</CardDescription>
61
+ </CardHeader>
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>
69
+ </CardContent>
70
+ </Card>
71
+ );
72
+ }
@@ -57,15 +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>{figures(hovered)}</span>
67
- </>
68
- )}
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>
69
72
  </TooltipContent>
70
73
  </Tooltip>
71
74
  <TreemapLegend range={0.6} className="mt-3 ml-auto" />
@@ -75,7 +78,7 @@ export default function TreemapExample() {
75
78
  <span className="font-medium">
76
79
  {selected.name} · {selected.broker}
77
80
  </span>{" "}
78
- · {figures(selected)}
81
+ · {selected.group} · {figures(selected)}
79
82
  </>
80
83
  ) : (
81
84
  <span className="text-muted-foreground">Tap a tile for its figures</span>