@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
package/DESIGN.md ADDED
@@ -0,0 +1,42 @@
1
+ # Siyahi design guidelines
2
+
3
+ These rules bind every page built on `@siyahi/ui`: The Book, Dash and any other
4
+ app. Agents must follow them when designing or changing UI; a reviewer treats a
5
+ breach as a defect. The catalogue (https://siyahi.jatinbansal.com) shows each
6
+ component; this file says how to use them.
7
+
8
+ ## Colour
9
+
10
+ Colour is placed, not spread. A page is neutral greys plus colour that carries
11
+ a meaning:
12
+
13
+ - **Ink indigo** is the accent: links, focus rings, the selected sidebar item,
14
+ the first chart series and an `accent` `Callout`. Buttons, badges and toggles
15
+ stay monochrome.
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.
19
+ **Amber** means caution. None of them decorates.
20
+ - **Emphasis tints the whole surface.** A caution is `WarningAlert`; a point set
21
+ apart is `Callout` (`neutral`, `accent`, `warning`, `positive`,
22
+ `negative`); a quotation is `Quote`. Each takes a faint wash of its colour
23
+ inside a 1px border of the same colour all round. Large marks follow the same
24
+ rule: gain and loss bars are a tint inside a full-colour edge, not solid
25
+ slabs.
26
+ - **Never a stripe down one side.** Do not mark a notice, card, row or quote
27
+ with a left (or any single-side) coloured border; tint the box or add an
28
+ icon. 1px neutral dividers between columns are separators, not accents, and
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.
40
+ - Categories (a broker, a sector, an asset class) stay neutral: outline badges,
41
+ never a hue each. Ordered buckets (largest 5, next 5, rest) use steps of one
42
+ colour, not separate hues.
package/README.md CHANGED
@@ -100,10 +100,10 @@ and support one keyboard entry with arrow navigation. `WeekdayHeatmap` accepts
100
100
 
101
101
  `SiyahiProvider` sets how data blocks sit on the page:
102
102
 
103
- - `surface="ruled"` (default, even outside a provider): unboxed blocks;
104
- `PanelGrid` draws hairlines between them.
103
+ - `surface="card"` (default, even outside a provider): each block is a card
104
+ lifted off a faintly tinted canvas.
105
+ - `surface="ruled"`: unboxed blocks; `PanelGrid` draws hairlines between them.
105
106
  - `surface="open"`: no boxes or rules; card padding is removed.
106
- - `surface="card"`: shadcn's filled Nova cards with a ring.
107
107
 
108
108
  Nest a provider to change one section. Stack `PageSection` blocks with
109
109
  `flex flex-col gap-6`; use `PanelGrid` for blocks side by side. Categories use
@@ -1,5 +1,5 @@
1
- import { Alert, AlertDescription, AlertTitle } from "@siyahi/ui";
2
- import { CircleAlertIcon, InfoIcon, TriangleAlertIcon } from "lucide-react";
1
+ import { Alert, AlertDescription, AlertTitle, WarningAlert } from "@siyahi/ui";
2
+ import { CircleAlertIcon, InfoIcon } from "lucide-react";
3
3
 
4
4
  export default function AlertExample() {
5
5
  return (
@@ -9,12 +9,10 @@ export default function AlertExample() {
9
9
  <AlertTitle>Prices are delayed</AlertTitle>
10
10
  <AlertDescription>Quotes refresh every fifteen minutes during market hours.</AlertDescription>
11
11
  </Alert>
12
- <Alert>
13
- {/* Alert sets its icons to the text colour with a stronger selector; the ! lets the icon's colour win. */}
14
- <TriangleAlertIcon className="text-warning!" />
12
+ <WarningAlert>
15
13
  <AlertTitle>Two trades are unmatched</AlertTitle>
16
14
  <AlertDescription>The contract notes list trades the ledger does not have yet.</AlertDescription>
17
- </Alert>
15
+ </WarningAlert>
18
16
  <Alert variant="destructive">
19
17
  <CircleAlertIcon />
20
18
  <AlertTitle>Import failed</AlertTitle>
@@ -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
  }
@@ -21,9 +21,19 @@ export default function BarListSidesExample() {
21
21
  <CardTitle>What drives the P&L</CardTitle>
22
22
  <CardDescription>The biggest ₹ gains and losses, on one scale</CardDescription>
23
23
  </CardHeader>
24
- <CardContent className="grid gap-6 md:grid-cols-2">
25
- <BarList data={gains} max={max} color="var(--positive)" valueFormatter={(v) => money(v)} />
26
- <BarList data={losses} max={max} color="var(--negative)" valueFormatter={(v) => money(v)} />
24
+ {/* Each list is a column of its own, headed and ruled off, with its figures at its own right edge, so a
25
+ gain's figure never reads as the label of the loss beside it. */}
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>
36
+ </div>
27
37
  </CardContent>
28
38
  </Card>
29
39
  );
@@ -0,0 +1,23 @@
1
+ import { Button, Toggle } from "@siyahi/ui";
2
+ import { DownloadIcon, Loader2Icon, PinIcon } from "lucide-react";
3
+
4
+ export default function ButtonStatesExample() {
5
+ return (
6
+ <div className="flex flex-wrap items-center gap-2">
7
+ <Button disabled>Save</Button>
8
+ <Button variant="outline" disabled>
9
+ <DownloadIcon />
10
+ Export
11
+ </Button>
12
+ {/* Busy: the label stays and the icon spins, so the button keeps its width while the work runs. */}
13
+ <Button disabled aria-busy>
14
+ <Loader2Icon className="motion-safe:animate-spin" />
15
+ Saving
16
+ </Button>
17
+ <Toggle variant="outline" defaultPressed aria-label="Pin to top">
18
+ <PinIcon />
19
+ Pinned
20
+ </Toggle>
21
+ </div>
22
+ );
23
+ }
@@ -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
  }
@@ -0,0 +1,33 @@
1
+ import { Callout, Quote, WarningAlert, AlertDescription, AlertTitle } from "@siyahi/ui";
2
+ import { LightbulbIcon, InfoIcon, TriangleAlertIcon, TrendingUpIcon, TrendingDownIcon } from "lucide-react";
3
+
4
+ export default function CalloutExample() {
5
+ return (
6
+ <div className="grid max-w-2xl gap-3">
7
+ <Callout tone="accent" icon={<LightbulbIcon />} title="The takeaway">
8
+ Two of eleven gold rules made a profit in the final test; <strong>none</strong> held up in both metals.
9
+ </Callout>
10
+ <Callout icon={<InfoIcon />} title="How the test was run">
11
+ One mini lot per trade, after charges and a harsher assumed execution cost.
12
+ </Callout>
13
+ <Callout tone="warning" icon={<TriangleAlertIcon />} title="Do not use the combined result">
14
+ Its test used later information, so its profit claims are withdrawn.
15
+ </Callout>
16
+ <div className="grid gap-3 sm:grid-cols-2">
17
+ <Callout tone="positive" icon={<TrendingUpIcon />} title="Best rule">
18
+ A failed range break reverses: <strong>▲₹10,616</strong>.
19
+ </Callout>
20
+ <Callout tone="negative" icon={<TrendingDownIcon />} title="Worst rule">
21
+ Break the first-hour range: <strong>▼₹70,679</strong>.
22
+ </Callout>
23
+ </div>
24
+ <Quote source="eMudhra's incident notice, 21 Oct 2026">
25
+ The affected intermediate keys were generated with insufficient entropy and have been revoked.
26
+ </Quote>
27
+ <WarningAlert>
28
+ <AlertTitle>Ledger history is incomplete</AlertTitle>
29
+ <AlertDescription>Dhan's cash ledger starts in April 2024; earlier flows are missing.</AlertDescription>
30
+ </WarningAlert>
31
+ </div>
32
+ );
33
+ }
@@ -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
  }
@@ -0,0 +1,104 @@
1
+ import {
2
+ Card,
3
+ CardContent,
4
+ CardDescription,
5
+ CardHeader,
6
+ CardTitle,
7
+ ChangeBar,
8
+ DataTable,
9
+ DataTableBody,
10
+ DataTableCompactColumnsToggle,
11
+ DataTableFootnote,
12
+ DataTableHeader,
13
+ DataTableRoot,
14
+ DataTableSortHeader,
15
+ DataTableToolbarSection,
16
+ useFormat,
17
+ type DataTableColumnDef,
18
+ } from "@siyahi/ui";
19
+
20
+ interface Holding {
21
+ symbol: string;
22
+ /** Value at the close of 31 January and of 28 February, in rupees; made-up figures. */
23
+ january: number;
24
+ february: number;
25
+ change: number;
26
+ }
27
+
28
+ const holding = (symbol: string, january: number, february: number): Holding => ({
29
+ symbol,
30
+ january,
31
+ february,
32
+ change: february / january - 1,
33
+ });
34
+
35
+ const holdings: Holding[] = [
36
+ holding("ICICIBANK", 231900, 258000),
37
+ holding("HDFCBANK", 248300, 262500),
38
+ holding("TCS", 161300, 166000),
39
+ holding("NIFTYBEES", 250400, 255600),
40
+ holding("ITC", 283100, 276000),
41
+ holding("INFY", 241800, 224400),
42
+ ];
43
+
44
+ // Every bar is a share of the column's largest move, and the column holds gains and losses, so the bars diverge.
45
+ const largestChange = Math.max(...holdings.map((h) => Math.abs(h.change)));
46
+ const diverging = holdings.some((h) => h.change < 0) && holdings.some((h) => h.change > 0);
47
+
48
+ function Money({ amount, muted = false }: { amount: number; muted?: boolean }) {
49
+ const text = useFormat().money(amount);
50
+ return muted ? <span className="text-muted-foreground">{text}</span> : text;
51
+ }
52
+
53
+ const header = () => <DataTableSortHeader />;
54
+
55
+ const columns: DataTableColumnDef<Holding>[] = [
56
+ { accessorKey: "symbol", header, size: 130, meta: { label: "Holding", compact: true } },
57
+ {
58
+ accessorKey: "january",
59
+ header,
60
+ size: 130,
61
+ meta: { label: "31 Jan", variant: "number" },
62
+ cell: ({ row }) => <Money amount={row.original.january} muted />,
63
+ },
64
+ {
65
+ accessorKey: "february",
66
+ header,
67
+ size: 130,
68
+ meta: { label: "28 Feb", variant: "number", compact: true },
69
+ cell: ({ row }) => <Money amount={row.original.february} />,
70
+ },
71
+ {
72
+ accessorKey: "change",
73
+ header,
74
+ size: 150,
75
+ meta: { label: "Change", variant: "number", compact: true },
76
+ cell: ({ row }) => <ChangeBar value={row.original.change} max={largestChange} diverging={diverging} />,
77
+ },
78
+ ];
79
+
80
+ export default function ChangeBarExample() {
81
+ return (
82
+ <Card>
83
+ <CardHeader>
84
+ <CardTitle>ICICIBANK rose 11.3% in February, INFY fell 7.2%</CardTitle>
85
+ <CardDescription>Each holding's value at the end of February against the end of January</CardDescription>
86
+ </CardHeader>
87
+ <CardContent>
88
+ <DataTableRoot data={holdings} columns={columns}>
89
+ <DataTableToolbarSection>
90
+ <DataTableCompactColumnsToggle />
91
+ </DataTableToolbarSection>
92
+ <DataTable aria-label="February against January">
93
+ <DataTableHeader />
94
+ <DataTableBody />
95
+ <DataTableFootnote>
96
+ Each percent is the change on January's value, and the bar is its size against the largest move. Gains run
97
+ right of the centre line, losses left.
98
+ </DataTableFootnote>
99
+ </DataTable>
100
+ </DataTableRoot>
101
+ </CardContent>
102
+ </Card>
103
+ );
104
+ }
@@ -9,18 +9,19 @@ const data = [
9
9
  { month: "Oct", value: 4512300, invested: 4025000 },
10
10
  ];
11
11
 
12
- // Drawn in this order, so value sits over the money put in.
12
+ // The title states the finding, not the measure: what the reader would say after looking.
13
+ // The first series fills; the money put in draws as a line over it.
13
14
  const series: ChartSeries<(typeof data)[number]> = [
14
- { key: "invested", label: "Invested", color: "var(--chart-2)" },
15
15
  { key: "value", label: "Value", color: "var(--chart-1)" },
16
+ { key: "invested", label: "Invested", color: "var(--chart-2)" },
16
17
  ];
17
18
 
18
19
  export default function ChartAreaExample() {
19
20
  const { money, moneyCompact } = useFormat();
20
21
  return (
21
22
  <AreaChartCard
22
- title="Portfolio value"
23
- description="Month-end value against money put in"
23
+ title="₹4.9L ahead of the money put in"
24
+ description="Portfolio value against money invested, at each month end"
24
25
  action={
25
26
  <ToggleGroup defaultValue={["6M"]} variant="outline" size="sm" aria-label="Range">
26
27
  <ToggleGroupItem value="6M">6M</ToggleGroupItem>
@@ -8,9 +8,10 @@ const data = [
8
8
  { month: "Oct", in: 70000, out: -66000 },
9
9
  ];
10
10
 
11
+ // Money moving is not a gain or a loss, so in and out take chart colours, not green and red.
11
12
  const series: ChartSeries<(typeof data)[number]> = [
12
- { key: "in", label: "In", color: "var(--positive)" },
13
- { key: "out", label: "Out", color: "var(--negative)" },
13
+ { key: "in", label: "In", color: "var(--chart-1)" },
14
+ { key: "out", label: "Out", color: "var(--chart-2)" },
14
15
  ];
15
16
 
16
17
  export default function ChartBarFlowsExample() {
@@ -60,6 +60,8 @@ export default function ChartBarSectorsExample() {
60
60
  xTickDetail={(row) => `${pct(row.you)} vs ${pct(row.index)} · ${gap(row)}`}
61
61
  series={series}
62
62
  valueFormatter={(v) => pct(v)}
63
+ // A tick format puts the value axis along the bottom of the sideways bars.
64
+ yTickFormatter={(v) => pct(v, 0)}
63
65
  // Sideways, y is still the value: the axis runs 0 to 40% and the cap is a value level.
64
66
  yDomain={[0, 0.4]}
65
67
  referenceLines={[{ y: 0.3, label: "cap 30%", labelPosition: "insideBottomRight" }]}
@@ -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>
@@ -0,0 +1,95 @@
1
+ import {
2
+ Card,
3
+ CardContent,
4
+ CardDescription,
5
+ CardHeader,
6
+ CardTitle,
7
+ DataTable,
8
+ DataTableBody,
9
+ DataTableCompactColumnsToggle,
10
+ DataTableHeader,
11
+ DataTableRoot,
12
+ DataTableRowCount,
13
+ DataTableSortHeader,
14
+ DataTableToolbarSection,
15
+ Delta,
16
+ useDataTable,
17
+ useFormat,
18
+ type DataTableColumnDef,
19
+ } from "@siyahi/ui";
20
+
21
+ interface Holding {
22
+ symbol: string;
23
+ sector: string;
24
+ quantity: number;
25
+ value: number;
26
+ change: number;
27
+ }
28
+
29
+ const holdings: Holding[] = [
30
+ { symbol: "INFY", sector: "IT", quantity: 120, value: 224400, change: 0.018 },
31
+ { symbol: "HDFCBANK", sector: "Banks", quantity: 150, value: 262500, change: -0.006 },
32
+ { symbol: "TCS", sector: "IT", quantity: 40, value: 166000, change: 0.004 },
33
+ { symbol: "ITC", sector: "Fast Moving Consumer Goods", quantity: 600, value: 276000, change: -0.012 },
34
+ ];
35
+
36
+ const header = () => <DataTableSortHeader />;
37
+
38
+ // While the Day column is hidden to fit, the day's move rides under the value it belongs to.
39
+ function ValueCell({ holding }: { holding: Holding }) {
40
+ const format = useFormat();
41
+ const { compactColumns } = useDataTable<Holding>();
42
+ if (!compactColumns?.hidden.has("change")) return format.money(holding.value);
43
+ return (
44
+ <div className="flex flex-col items-end">
45
+ {format.money(holding.value)}
46
+ <Delta value={holding.change} />
47
+ </div>
48
+ );
49
+ }
50
+
51
+ const columns: DataTableColumnDef<Holding>[] = [
52
+ { accessorKey: "symbol", header, size: 140, meta: { label: "Symbol", compact: true } },
53
+ { accessorKey: "sector", header, size: 120, meta: { label: "Sector" } },
54
+ { accessorKey: "quantity", header, size: 100, meta: { label: "Qty", variant: "number" } },
55
+ {
56
+ accessorKey: "value",
57
+ header,
58
+ size: 140,
59
+ meta: { label: "Value", variant: "number", compact: true },
60
+ cell: ({ row }) => <ValueCell holding={row.original} />,
61
+ },
62
+ {
63
+ accessorKey: "change",
64
+ header,
65
+ size: 110,
66
+ meta: { label: "Day", variant: "number" },
67
+ cell: ({ row }) => <Delta value={row.original.change} />,
68
+ },
69
+ ];
70
+
71
+ export default function DataTableCompactExample() {
72
+ return (
73
+ <Card>
74
+ <CardHeader>
75
+ <CardTitle>Holdings on a phone</CardTitle>
76
+ <CardDescription>
77
+ Symbol and value are the key columns. When the columns do not fit, the others hide from the right, and the
78
+ day's move rides under the value once its column is gone; All columns brings back the rest
79
+ </CardDescription>
80
+ </CardHeader>
81
+ <CardContent>
82
+ <DataTableRoot data={holdings} columns={columns}>
83
+ <DataTableToolbarSection>
84
+ <DataTableCompactColumnsToggle />
85
+ <DataTableRowCount unit="holdings" />
86
+ </DataTableToolbarSection>
87
+ <DataTable aria-label="Holdings on a phone">
88
+ <DataTableHeader />
89
+ <DataTableBody />
90
+ </DataTable>
91
+ </DataTableRoot>
92
+ </CardContent>
93
+ </Card>
94
+ );
95
+ }
@@ -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">
@@ -3,6 +3,8 @@ import {
3
3
  CalendarHeatmap,
4
4
  CalendarHeatmapBlock,
5
5
  CalendarHeatmapBody,
6
+ CalendarHeatmapFooter,
7
+ CalendarHeatmapLegend,
6
8
  useFormat,
7
9
  Card,
8
10
  CardContent,
@@ -18,32 +20,42 @@ const data = Array.from({ length: 365 }, (_, i) => {
18
20
  const value = weekday === 0 || weekday === 6 ? 0 : (i * 7 + weekday * 3) % 6;
19
21
  return { date: day.toISOString().slice(0, 10), value };
20
22
  });
23
+ const busiest = Math.max(...data.map((day) => day.value));
21
24
 
22
25
  export default function HeatmapCalendarExample() {
23
- const [selected, setSelected] = useState<string | null>(null);
26
+ // The page owns the pick: a second tap on the same day clears it.
27
+ const [picked, setPicked] = useState<(typeof data)[number] | null>(null);
24
28
  const { count } = useFormat();
25
29
  return (
26
30
  <Card>
27
31
  <CardHeader>
28
32
  <CardTitle>Trades per day</CardTitle>
29
- <CardDescription>Counts use a single-hue fade of --chart-1</CardDescription>
33
+ <CardDescription>The last 12 months; counts use a single-hue fade of --chart-1</CardDescription>
30
34
  </CardHeader>
31
35
  <CardContent>
32
- <CalendarHeatmap data={data}>
36
+ <CalendarHeatmap data={data} labels={{ heatmapLabel: "Trades per day over the last 12 months" }}>
33
37
  <CalendarHeatmapBody>
34
38
  {({ activity, dayIndex, weekIndex }) => (
35
39
  <CalendarHeatmapBlock
36
40
  activity={activity}
37
41
  dayIndex={dayIndex}
38
42
  weekIndex={weekIndex}
39
- onCellClick={(cell) => setSelected(`${cell.date}: ${count(cell.value)} trades`)}
43
+ selected={activity.date === picked?.date}
44
+ onCellClick={(cell) => setPicked(cell.date === picked?.date ? null : cell)}
40
45
  />
41
46
  )}
42
47
  </CalendarHeatmapBody>
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>
57
+ </CalendarHeatmapFooter>
43
58
  </CalendarHeatmap>
44
- <p aria-live="polite" className="mt-3 text-sm">
45
- {selected ?? "Select a day for its trade count. Use arrow keys to move between days."}
46
- </p>
47
59
  </CardContent>
48
60
  </Card>
49
61
  );